tag because the
- # Sphinx `:download:` role causes the link to appear in a
tag
- # and in HTML5,
tags cannot be nested!
- cfg_right_just_para_text = """.. raw:: html
-
-
"""
- cfg_end_right_just_para_text = """.. raw:: html
-
-
"""
- # Blank lines are required due to the directives.
- cfg_pdf_link_ref_block_str = \
- cfg_right_just_para_text + os.linesep \
- + os.linesep \
- + pdf_link_ref_str + os.linesep + \
- os.linesep \
- + cfg_end_right_just_para_text + os.linesep \
- + os.linesep
-
- # ---------------------------------------------------------------------
- # Change to script directory for consistent run-time environment.
- # ---------------------------------------------------------------------
- os.chdir(base_dir)
- announce(__file__, f'Intermediate dir: [{intermediate_dir}]')
- announce(__file__, f'Output dir : [{output_dir}]')
- announce(__file__, f'Running from : [{base_dir}]')
-
- # ---------------------------------------------------------------------
- # Clean? If so, clean (like `make clean`), but do not exit.
- # ---------------------------------------------------------------------
- some_cleaning_to_be_done = cfg_target_clean_intermediate in args.targets \
- or cfg_target_clean_html in args.targets \
- or cfg_target_clean_latex in args.targets \
- or cfg_target_clean_all in args.targets \
- or ( \
- os.path.isdir(intermediate_dir) \
- and cfg_target_intermediate in args.targets \
- )
-
- if some_cleaning_to_be_done:
- announce(__file__, "Cleaning...", box=True)
-
- if cfg_target_clean_intermediate in args.targets:
- remove_dir(intermediate_dir)
-
- if cfg_target_clean_html in args.targets:
- remove_dir(html_output_dir)
-
- if cfg_target_clean_latex in args.targets:
- remove_dir(latex_output_dir)
-
- if cfg_target_clean_all in args.targets:
- remove_dir(output_dir)
-
- if os.path.isdir(intermediate_dir) and cfg_target_intermediate in args.targets:
- remove_dir(intermediate_dir)
-
- # ---------------------------------------------------------------------
- # Populate LVGL_URLPATH and LVGL_GITCOMMIT environment variables:
- # - LVGL_URLPATH <= 'master' or '8.4' '9.2' etc.
- # - LVGL_GITCOMMIT <= same (see 03-Oct-2024 note below).
- #
- # These supply input later in the doc-generation process as follows:
- #
- # LVGL_URLPATH is used by:
- # - `conf.py` to build `html_baseurl` for Sphinx for
- # - generated index
- # - generated search window
- # - establishing canonical page for search engines
- # - `link_roles.py` to generate translation links
- # - `doxygen_xml.py` to generate links to API pages
- #
- # LVGL_GITCOMMIT is used by:
- # - `conf.py` => html_context['github_version'] for
- # Sphinx Read-the-Docs theme to add to [Edit on GitHub] links
- # - `conf.py` => repo_commit_hash for generated EXAMPLES pages for:
- # - [View on GitHub] buttons (view C code examples)
- # - [View on GitHub] buttons (view Python code examples)
- # ---------------------------------------------------------------------
- # 03-Oct-2024: Gabor requested LVGL_GITCOMMIT be changed to a branch
- # name since that will always be current, and it will fix a large
- # number of broken links on the docs website, since commits that
- # generated docs can sometimes go away. This gets used in:
- # - [Edit on GitHub] links in doc pages (via Sphinx theme), and
- # - [View on GitHub] links in example pages (via `example_list.py`
- # and `lv_example.py`).
- # Original code:
- # status, br = subprocess.getstatusoutput("git branch --show-current")
- # _, gitcommit = subprocess.getstatusoutput("git rev-parse HEAD")
- # br = re.sub(r'\* ', '', br)
- # 're' was previously used to remove leading '* ' from current branch
- # string when we were parsing output from bare `git branch` output.
- # This is no longer needed with `--show-current` option now used.
- # ---------------------------------------------------------------------
- status, branch = subprocess.getstatusoutput("git branch --show-current")
-
- # If above failed (i.e. `branch` not valid), default to 'master'.
- if status != 0:
- branch = cfg_default_branch
- elif branch == cfg_default_branch:
- # Expected in most cases. Nothing to change.
- pass
- else:
- # `branch` is valid. Capture release version if in a 'release/' branch.
- if branch.startswith('release/'):
- branch = branch[8:]
- else:
- # Default to 'master'.
- branch = cfg_default_branch
-
- os.environ['LVGL_URLPATH'] = branch
- os.environ['LVGL_GITCOMMIT'] = branch
-
- # ---------------------------------------------------------------------
- # Prep `intermediate_dir` to become the `sphinx-build` source dir.
- # ---------------------------------------------------------------------
- # dirsync `exclude_list` = list of regex patterns to exclude.
- intermediate_re = r'^' + cfg_default_intermediate_dir + r'.*'
- output_re = r'^' + cfg_default_output_dir + r'.*'
- exclude_list = [r'lv_conf\.h', r'^__pycache__.*', intermediate_re, output_re]
-
- if intermediate_dir_contents_exists(intermediate_dir):
- # We are just doing an update of the intermediate_dir contents.
- announce(__file__, "Updating intermediate directory...", box=True)
-
- exclude_list.append(r'examples.*')
- options = {
- 'verbose': True, # Report files copied.
- 'create': True, # Create directories if they don't exist.
- 'twoway': False, # False means data flow only src => tgt.
- 'purge': False, # False means DO NOT remove orphan files/dirs in tgt dir (preserving examples/ dir).
- 'exclude': exclude_list
- }
- # action == 'sync' means copy files even when they do not already exist in tgt dir.
- # action == 'update' means DO NOT copy files when they do not already exist in tgt dir.
- dirsync.sync(cfg_doc_src_dir, intermediate_dir, 'sync', **options)
- dirsync.sync(examples_dir, os.path.join(intermediate_dir, cfg_examples_dir), 'sync', **options)
- elif cfg_target_intermediate in args.targets or cfg_target_html in args.targets or cfg_target_latex in args.targets:
- # We are having to create the intermediate_dir contents by copying.
- announce(__file__, "Building intermediate directory...", box=True)
-
- t1 = datetime.now()
- copy_method = 1
-
- # Both of these methods work.
- if copy_method == 0:
- # --------- Method 0:
- ignore_func = shutil.ignore_patterns('tmp*', 'output*')
- announce(__file__, 'Copying docs...')
- shutil.copytree(cfg_doc_src_dir, intermediate_dir, ignore=ignore_func, dirs_exist_ok=True)
- announce(__file__, 'Copying examples...')
- shutil.copytree(examples_dir, os.path.join(intermediate_dir, cfg_examples_dir), dirs_exist_ok=True)
- else:
- # --------- Method 1:
- options = {
- 'create': True, # Create directories if they don't exist.
- 'exclude': exclude_list
- }
- # action == 'sync' means copy files even when they do not already exist in tgt dir.
- # action == 'update' means DO NOT copy files when they do not already exist in tgt dir.
- announce(__file__, 'Copying docs...')
- dirsync.sync(cfg_doc_src_dir, intermediate_dir, 'sync', **options)
- announce(__file__, 'Copying examples...')
- dirsync.sync(examples_dir, os.path.join(intermediate_dir, cfg_examples_dir), 'sync', **options)
-
- # -----------------------------------------------------------------
- # Build
/lv_conf.h from lv_conf_template.h.
- # -----------------------------------------------------------------
- config_builder.run(lv_conf_file)
- # Build a temporary version of this file in ../src/ so Doxygen can see it.
- # Reason: Doxygen's input is the master `lvgl/src/` directory,
- # not the intermediate directory. This file gets deleted later.
- config_builder.run(lv_temp_conf_file_for_doxygen)
-
- # -----------------------------------------------------------------
- # Copy `lv_version.h` into intermediate directory.
- # -----------------------------------------------------------------
- shutil.copyfile(version_src_file, version_dst_file)
-
- # -----------------------------------------------------------------
- # Generate examples pages. Include sub-pages pages that get included
- # in individual documents where applicable.
- # -----------------------------------------------------------------
- announce(__file__, "Generating examples...")
- example_list.make_warnings_into_errors()
- example_list.exec(intermediate_dir)
-
- # -----------------------------------------------------------------
- # Add translation links.
- # This is being skipped in favor of a manually-placed
- # translation link at the top of `./docs/index.rst`.
- # -----------------------------------------------------------------
- # Original code:
- # if True:
- # announce(__file__, "Skipping adding translation links.")
- # else:
- # announce(__file__, "Adding translation links...")
- # add_translation.exec(intermediate_dir)
-
- if args.skip_api:
- announce(__file__, "Skipping API generation as requested.")
- else:
- # -------------------------------------------------------------
- # Generate API pages and links thereto.
- # -------------------------------------------------------------
- announce(__file__, "API page and link processing...")
- api_doc_builder.EMIT_WARNINGS = False
-
- # api_doc_builder.run() => doxy_xml_parser.DoxygenXml() now:
- # - preps and runs Doxygen generating XML,
- # - loads generated XML.
- # Then api_doc_builder.run():
- # - creates .RST files for API pages, and
- # - adds API hyperlinks to .RST files in the directories in passed array.
- api_doc_builder.build_api_docs(lvgl_src_dir,
- intermediate_dir,
- doxyfile_src_file,
- 'auxiliary-modules',
- 'common-widget-features',
- 'contributing',
- 'debugging',
- 'getting_started',
- 'guides',
- 'integration',
- 'introduction',
- 'libs',
- 'main-modules',
- 'widgets',
- 'xml',
- )
-
- # Now that Doxygen has run, this file is no longer needed.
- # Clean up now rather than later to keep logic clean.
- os.remove(lv_temp_conf_file_for_doxygen)
-
- # Note time when this stage completed.
- t2 = datetime.now()
- announce(__file__, 'Example/API run time: ' + str(t2 - t1))
-
- # ---------------------------------------------------------------------
- # Build PDF
- # ---------------------------------------------------------------------
- if cfg_target_latex not in args.targets:
- announce(__file__, "Skipping Latex build.")
- else:
- t1 = datetime.now()
- announce(__file__, "Building Latex output...", box=True)
-
- # If PDF link is present in top index.rst, remove it so PDF
- # does not have a link to itself.
- with open(top_index_file, 'rb') as f:
- index_data = f.read().decode('utf-8')
-
- if pdf_link_ref_str in index_data:
- index_data = index_data.replace(pdf_link_ref_str, '')
-
- with open(top_index_file, 'wb') as f:
- f.write(index_data.encode('utf-8'))
-
- src = intermediate_dir
- dst = output_dir
- cpu = os.cpu_count()
-
- # The -D option correctly replaces (overrides) configuration attribute
- # values in the `conf.py` module. Since `conf.py` now correctly
- # computes its own `version` value, we don't have to override it here
- # with a -D options. If it should need to be used in the future,
- # the value after the '=' MUST NOT have quotation marks around it
- # or it won't work. Correct usage: f'-D version={ver}' .
- cmd_line = f'sphinx-build -M latex "{src}" "{dst}" -j {cpu} --fail-on-warning --keep-going'
- cmd(cmd_line)
-
- # Generate PDF.
- announce(__file__, "Building PDF...", box=True)
- cmd_line = 'latexmk -pdf "LVGL.tex"'
- cmd(cmd_line, latex_output_dir, False)
-
- # Move resulting PDF to its output directory.
- if not os.path.exists(pdf_output_dir):
- os.makedirs(pdf_output_dir)
-
- shutil.move(pdf_src_file, pdf_dst_file)
- t2 = datetime.now()
- announce(__file__, 'PDF : ' + pdf_dst_file)
- announce(__file__, 'Latex gen time: ' + str(t2 - t1))
-
- # ---------------------------------------------------------------------
- # Build HTML
- # ---------------------------------------------------------------------
- if cfg_target_html not in args.targets:
- announce(__file__, "Skipping HTML build.")
- else:
- t1 = datetime.now()
- announce(__file__, "Building HTML output...", box=True)
-
- # If PDF is present in build directory, copy it to
- # intermediate directory for use by HTML build.
- # (Sphinx copies it to its HTML output, so it ends
- # up on the webserver where it can be downloaded).
- if os.path.isfile(pdf_dst_file):
- # Create _static/download/ directory if needed.
- if not os.path.exists(pdf_intermediate_dst_dir):
- os.makedirs(pdf_intermediate_dst_dir)
-
- shutil.copyfile(pdf_dst_file, pdf_intermediate_dst_file)
-
- # If PDF is present, ensure there is a link to it in the top
- # index.rst so HTML build will have it.
- # Support both Windows and Linux platforms with `os.linesep`.
- if os.path.isfile(pdf_intermediate_dst_file):
- with open(top_index_file, 'rb') as f:
- index_data = f.read().decode('utf-8')
-
- if pdf_link_ref_str not in index_data:
- index_data = cfg_pdf_link_ref_block_str + index_data
-
- with open(top_index_file, 'wb') as f:
- f.write(index_data.encode('utf-8'))
-
- # Note: While it can be done (e.g. if one needs to set a stop point
- # in Sphinx code for development purposes), it is NOT a good idea to
- # run Sphinx from script as
- # from sphinx.cmd.build import main as sphinx_build
- # sphinx_args = [...]
- # sphinx_build(sphinx_args)
- # because it takes ~10X longer to run than `sphinx_build` executable,
- # literally > 3 hours.
-
- ver = lvgl_version(version_src_file)
- src = intermediate_dir
- dst = output_dir
- cpu = os.cpu_count()
-
- debugging_breathe = 0
- if debugging_breathe:
- from sphinx.cmd.build import main as sphinx_build
- # Don't allow parallel processing while debugging (the '-j' arg is removed).
- sphinx_args = ['-M', 'html', f'{src}', f'{dst}']
-
- if len(env_opt) > 0:
- sphinx_args.append(f'{env_opt}')
-
- sphinx_build(sphinx_args)
- else:
- # The -D option correctly replaces (overrides) configuration attribute
- # values in the `conf.py` module. Since `conf.py` now correctly
- # computes its own `version` value, we don't have to override it here
- # with a -D options. If it should need to be used in the future,
- # the value after the '=' MUST NOT have quotation marks around it
- # or it won't work. Correct usage: f'-D version={ver}' .
- cmd_line = f'sphinx-build -M html "{src}" "{dst}" -j {cpu} {env_opt} --fail-on-warning --keep-going'
- cmd(cmd_line)
-
- t2 = datetime.now()
- announce(__file__, 'HTML gen time : ' + str(t2 - t1))
-
- # ---------------------------------------------------------------------
- # Indicate results.
- # ---------------------------------------------------------------------
- t_end = datetime.now()
- announce(__file__, 'Total run time: ' + str(t_end - t0))
- announce(__file__, 'Done.')
-
-
-if __name__ == '__main__':
- """Make module importable as well as run-able."""
- run()
diff --git a/docs/config_builder.py b/docs/config_builder.py
deleted file mode 100644
index 645a9c9b82..0000000000
--- a/docs/config_builder.py
+++ /dev/null
@@ -1,76 +0,0 @@
-"""
-Create lv_conf.h in same directory as this file
-from ../lv_conf_template.h that has:
-
-1. all its #define LV_USE... 0-or-1 options set to 1
- (except for LV_USER_PROFILER),
-2. all its #define LV_FONT... 0-or-1 options set to 1,
-3. its #if 0 directive set to #if 1.
-"""
-import os
-import sys
-import re
-
-base_path = os.path.dirname(__file__)
-dest_config = os.path.join(base_path, 'lv_conf.h')
-src_config = os.path.abspath(os.path.join(
- base_path,
- '..',
- 'lv_conf_template.h'
-))
-disabled_option_re = re.compile(r'^\s*#define\s+(LV_(?:USE|FONT)_\w+)\s+(\b0\b)')
-
-leave_disabled_list = [
- 'LV_USE_PROFILER',
- 'LV_USE_DRAW_ARM2D_SYNC',
- 'LV_USE_NATIVE_HELIUM_ASM',
-]
-
-
-def run(output_cfg_path=None):
- global dest_config
- enable_content_macro_processed = False
- os.chdir(base_path)
-
- if output_cfg_path is not None:
- dest_config = output_cfg_path
-
- with open(src_config, 'r') as f:
- data = f.read()
-
- lines = data.split('\n')
-
- for i, line in enumerate(lines):
- if not enable_content_macro_processed:
- if line.startswith('#if 0'):
- line = line.replace('#if 0', '#if 1')
- lines[i] = line
- enable_content_macro_processed = True
- else:
- match = disabled_option_re.search(line)
- if match:
- # Except for these...
- if match[1] in leave_disabled_list:
- continue
- else:
- # ...replace '0' with '1' without altering any other part of line.
- # Set `j` to index where '0' was found.
- j = match.regs[2][0]
- # Surgically insert '1' in place of '0'. Strings are immutable.
- line = line[:j] + '1' + line[j + 1:]
- lines[i] = line
-
- data = '\n'.join(lines)
-
- with open(dest_config, 'w') as f:
- f.write(data)
-
-
-def cleanup():
- if os.path.exists(dest_config):
- os.remove(dest_config)
-
-
-if __name__ == '__main__':
- """Make module importable as well as run-able."""
- run()
diff --git a/docs/doxygen_config.py b/docs/doxygen_config.py
deleted file mode 100644
index 1763083f14..0000000000
--- a/docs/doxygen_config.py
+++ /dev/null
@@ -1,429 +0,0 @@
-"""doxygen_config.py
-Python Interface to Doxygen Config Files (Doxyfiles)
-
-Author : "Victor Wheeler"
-Copyright: "Copyright (C) 2025 WGA Crystal Research, Inc."
-License : "MIT"
-Version : "1.0"
-
-This work was inspired by the `doxygen-python-interface` project at
-https://github.com/TraceSoftwareInternational/doxygen-python-interface.
-On 27-Feb-2025 I was engaged in a production project wherein I wanted
-to find a Python module that I could re-use to reliably work with
-Doxygen configuration files (Doxyfiles). The best one I found was
-`doxygen-python-interface`. Unfortunately, the ``configParser`` from
-that project could not be used because it both had important bugs and
-design flaws in it (conflicts with legal Doxygen config syntax), and
-it appears to have been abandoned after 26-Apr-2018, preventing these
-things from being remedied.
-
-So a brand-new module has been created herewith based on sound O-O design
-principles and a design that actually works in alignment with Doxygen
-configuration syntax.
-
-Usage:
-
- import doxygen_config
- ...
- # 1. Load configuration from Doxyfile.
- cfg = doxygen_config.DoxygenConfig()
- cfg.load(doxyfile_src_file)
-
- # 2. Get a list of Doxygen option names.
- opt_list = cfg.options()
- ok_to_proceed = cfg.is_valid_option('PREDEFINED') \
- and cfg.is_valid_option('INPUT')
-
- # 3. Update it.
- if ok_to_proceed:
- temp = cfg.value('PREDEFINED')
- temp = temp.replace('<>', config_file)
- cfg.set('PREDEFINED', temp)
-
- temp = cfg.value('INPUT')
- temp = temp.replace('<>', f'"{pjt_src_dir}"')
- cfg.set('INPUT', temp)
-
- # 4. Save it.
- # The original comments and order of config options are preserved.
- # The ``bare`` argument discards comments from the output.
- cfg.save(cfg_dict, doxyfile_dst_file, bare=True)
-
-Design Differences from `doxygen-python-interface`:
-
- - The DoxygenConfig class represents the actual Doxygen configuration,
- in alignment with O-O theory --- it is not just a place to store a
- set of functions that never needed to be a class.
-
- - If the user does a default ``save()`` (not requesting a "bare"
- version of the Doxygen configuration), the saved Doxyfile
- should be a binary match to the original Doxyfile loaded.
-
- Exceptions:
-
- 1. Any trailing whitespace in original Doxyfile after the ``=``
- on empty options is not preserved.
-
- 2. Multi-line lists that had unaligned backslashes after them like this:
-
- EXCLUDE_PATTERNS = */libs/barcode/code* \
- */libs/freetype/ft* \
- */libs/gif/gif* \
- */libs/lodepng/lode* \
- */libs/qrcode/qr* \
- */libs/thorvg/* \
- */libs/tiny_ttf/stb* \
- */libs/tjpgd/tjp* \
- */others/vg_lite_tvg/vg*
-
- will be saved like this:
-
- EXCLUDE_PATTERNS = */libs/barcode/code* \
- */libs/freetype/ft* \
- */libs/gif/gif* \
- */libs/lodepng/lode* \
- */libs/qrcode/qr* \
- */libs/thorvg/* \
- */libs/tiny_ttf/stb* \
- */libs/tjpgd/tjp* \
- */others/vg_lite_tvg/vg*
-
- ``doxygen-python-interface`` did not save the comments so an
- "edit in place" of a Doxyfile could be catastrophic if the
- comments were needed in the source Doxyfile as they often are
- in production scenarios.
-
- - The ``save()`` method has an optional ``bare`` argument (default False)
- that can be used to save a "bare" version of the Doxyfile options,
- discarding the comments from the currently-loaded Doxyfile.
-
- - Input values are preserved exactly as they were found. The
- `doxygen-python-interface`'s ``configParser`` class removed
- quotation marks from incoming values and added quotation marks
- to values containing spaces before storing them again. While
- this "sounds nice", it was incompatible with Doxygen for every
- type of item that could have a "list" as a value, such as the
- PREDEFINED and ABBREVIATE_BRIEF options.
-
- Examples:
-
- PREDEFINED = USE_LIST USE_TABLE USE_CHART
-
- PREDEFINED = DOXYGEN CONFIG_PATH="/path with spaces/to/config.h"
-
- PREDEFINED = DOXYGEN \
- CONFIG_PATH="/path with spaces/to/config.h"
-
- These are all valid values for the PREDEFINED option and
- MUST NOT have quotes around any of them! Can you imagine the havoc
- that would result if a Python module meant to handle Doxygen Doxyfiles
- altered Doxygen configuration items like this?
-
- PREDEFINED = "USE_LIST USE_TABLE USE_CHART"
-
- Thus, it is up to the user to know when values he is changing
- have space(s) AND ALSO need quotes and take appropriate measures
- by adding quotes when needed and not otherwise.
-
- - The storage of the list of Doxygen options is encapsulated
- in the instance of the DoxygenConfig class instead of being
- returned as a dictionary from the ``load...()`` function.
- Its values are readable and writeable via methods. The
- end user is not able to add options that were not part
- of the original input Doxyfile, nor remove options that were
- part of the original input Doxyfile. This gives some level of
- control on retaining valid Doxygen options.
-
- It is an error to attempt to set a value with an option name
- that does not exist in the configuration. A NameError exception
- is raised if it is attempted. Attempting to read the value of
- an option name that does not exist returns the value ``None``.
-
- While Doxygen options change from time to time, it is up to the
- end user to use ``doxygen -u Doxyfile`` to keep his input
- Doxyfile(s) up to date.
-
-Storage:
-
- The actual configuration values are represented in an internal
- dictionary not intended to be accessed directly by the typical end
- user. The keys are the Doxygen option names and the values are:
-
- - str : single values with possibly embedded spaces
- - list: multi-line values with possibly embedded spaces
-
- Quotation marks are neither removed nor added, so it is up to the
- user to set values compatible with Doxygen configuration syntax.
- This also makes it okay for multi-line values to have more than one
- value per line: if it is okay by Doxygen, then it is okay by
- the DoxygenConfig class.
-
- If the user sets an option value passing a list, those values
- will be represented as a multi-line value in the saved Doxyfile.
-
-The Philosophy of Removing Quotation Marks Is Not Workable for Doxygen:
-
- When one asks, "Is it appropriate to remove the quotation marks?"
- What if a value looked like this (2 quoted items in one line),
- removing quotation marks would be an error:
-
- "abc def" "ghi jkl"
-
- The ABBREVIATE_BRIEF list could indeed appear like this.
-
- If it were argued that all multi-value items should be formatted as
- multi-line lists, then quotation marks theory works, as the
- ABBREVIATE_BRIEF option does not require quotation marks around
- every value.
-
- However, since Doxygen does not require this, there is still a
- strong argument for not tampering with quotation marks at all
- when importing values. The strongest reasons are:
-
- - Doxygen can and does accept values like this where the value of
- an option can be a list. Doxygen sees this as 2 separate values:
-
- "abc def" "ghi jkl"
-
- - If the end user is going to set values with spaces in them,
- it could be made the user's responsibility to know when
- there are spaces and thus include quotes when needed.
-
- In the end, the "do not tamper with quotation marks" argument wins
- for sake of reliability. So the policy is: quotation marks are
- neither removed nor added. It is up to the user to know when they
- are needed and add them himself.
-"""
-import logging
-import os
-import re
-
-
-__author__ = "Victor Wheeler"
-__copyright__ = "Copyright (C) 2025 WGA Crystal Research, Inc."
-__license__ = "MIT"
-__version__ = "1.0"
-
-
-class ParseException(Exception):
- """Exception thrown upon unexpected parsing errors."""
- pass
-
-
-class DoxygenConfig:
- """Doxygen Configurations (from/to Doxyfiles)"""
-
- def __init__(self):
- """Prepare instantiated DoxygenConfig for use."""
- # Regexes used during Doxyfile parsing
- self._re_single_line_option = re.compile(r'^\s*(\w+)\s*=\s*([^\\]*)\s*$')
- self._re_top_of_multiline_option = re.compile(r'^\s*(\w+)\s*=\s*(|.*\S)\s*\\$')
- # Doxygen cfg items by option name
- self._cfg_items_dict = {}
- # Comments by name of option below it.
- # Comments at end of file have key 'self._end_key'.
- self._cfg_comments_dict = {}
- # Key used for comments found after last option in Doxyfile
- self._end_key = 'END'
- # Configuration to match Doxygen -g output (template Doxyfile)
- self._char_count_before_equals = 23
-
- def load(self, doxyfile: str):
- """Load options and comments from `doxyfile`
-
- :param doxyfile: Path to doxyfile
-
- :raise FileNotFoundError: When doxyfile not found
- :raise ParseException: When there is a parsing error
- """
-
- if not os.path.exists(doxyfile):
- logging.error(f'Doxyfile not found {doxyfile}.')
- raise FileNotFoundError(doxyfile)
-
- self._cfg_items_dict.clear()
- self._cfg_comments_dict.clear()
-
- # Default encoding: UTF-8.
- with open(doxyfile, 'r') as file:
- in_multiline_opt = False
- multiline_opt_name_bep = None # "bep" = "being processed"
- accumulated_other_lines = []
-
- for line in file.readlines():
- line = line.strip()
-
- if in_multiline_opt:
- # There are 2 ways this list can end:
- # 1. the normal way when last item has no trailing `\`, or
- # 2. the last item has a trailing `\` and there is a blank-
- # or comment-line after it, which should NOT be added
- # to the list, but instead signal end-of-list.
- if not line.endswith('\\'):
- in_multiline_opt = False
-
- val = line.rstrip('\\').strip()
-
- if self._bool_comment_or_blank_line(val):
- accumulated_other_lines.append(line)
- in_multiline_opt = False
- else:
- self._cfg_items_dict[multiline_opt_name_bep].append(val)
-
- elif self._bool_comment_or_blank_line(line):
- accumulated_other_lines.append(line)
-
- elif self._bool_top_of_multiline_option(line):
- multiline_opt_name_bep, val = self._parse_multiline_option(line)
- self._cfg_items_dict[multiline_opt_name_bep] = [val]
- self._cfg_comments_dict[multiline_opt_name_bep] = accumulated_other_lines
- accumulated_other_lines = []
- in_multiline_opt = True
-
- elif self._bool_single_line_option(line):
- option_name, val = self._parse_single_line_option(line)
- self._cfg_items_dict[option_name] = val
- self._cfg_comments_dict[option_name] = accumulated_other_lines
- accumulated_other_lines = []
-
- # Any comments or blank lines found after last Doxygen option
- # are represented in _cfg_comments_dict with key `self._end_key`.
- if accumulated_other_lines:
- self._cfg_comments_dict[self._end_key] = accumulated_other_lines
- accumulated_other_lines.clear()
-
- def save(self, doxyfile: str, bare=False):
- """Save configuration to `doxyfile`.
-
- :param doxyfile: Output path where Doxygen configuration will be
- written. Overwrites file if it exists.
- :param bare: Do not preserve comments from loaded file.
- """
-
- lines = []
-
- for option_name, val in self._cfg_items_dict.items():
- if not bare:
- lines.extend(self._cfg_comments_dict[option_name])
-
- if type(val) is list:
- # We will be aligning the backslashes after the
- # items in the list, so we need to know the longest.
- # First value in list:
- multi_line_indent = ' ' * (self._char_count_before_equals + 2)
- longest_len = len(max(val, key=len))
- val_w_len = val[0].ljust(longest_len)
- lines.append(f'{option_name:<23}= {val_w_len} \\')
-
- # Next n-2 values in list:
- if len(val) > 2:
- for temp in val[1:-1]:
- val_w_len = temp.ljust(longest_len)
- lines.append(f'{multi_line_indent}{val_w_len} \\')
-
- # Last value in list:
- lines.append(f'{multi_line_indent}{val[-1]}')
- elif type(val) is str:
- val_w_len = option_name.ljust(self._char_count_before_equals)
- if len(val) == 0:
- lines.append(f'{val_w_len}=')
- else:
- lines.append(f'{val_w_len}= {val}')
-
- if self._end_key in self._cfg_comments_dict:
- if not bare:
- lines.extend(self._cfg_comments_dict[self._end_key])
-
- # Ensure there is exactly 1 newline at end of file.
- lines.append('')
-
- with open(doxyfile, 'w') as file:
- file.write('\n'.join(lines))
-
- logging.debug(f'Saved configuration to [{doxyfile}].')
-
- def option_names(self):
- """List of contained Doxygen option names"""
- return self._cfg_items_dict.keys()
-
- def is_valid_option(self, option_name: str) -> bool:
- """Is `option_name` a valid option name?"""
- return option_name in self._cfg_items_dict
-
- def set(self, option_name: str, val: str or list):
- """Set value of specified option
-
- :param option_name: Name of Doxygen option whose value to fetch
- :param val: Value to set
- - str = single-line value;
- - list = multi-line value.
-
- :raises NameError: When ``name`` is not found.
- """
- if option_name in self._cfg_items_dict:
- self._cfg_items_dict[option_name] = val
- if type(val) is list:
- logging.debug(f'Item [{option_name}] set to list.')
- else:
- logging.debug(f'Item [{option_name}] set to [{val}].')
- else:
- logging.error(f'Doxyfile option {option_name} not found.')
- raise NameError(f'Doxygen option {option_name} not found.')
-
- def value(self, option_name: str) -> str or list:
- """Value of specified option
-
- :param option_name: Name of Doxygen option whose value to fetch
-
- :returns string: single-line value
- :returns list: multi-line value
- :returns None: When ``option_name`` is not found.
- """
- if option_name in self._cfg_items_dict:
- result = self._cfg_items_dict[option_name]
- logging.debug(f'Item [{option_name}] fetched.')
- else:
- result = None
- logging.debug(f'Item [{option_name}] not found.')
-
- return result
-
- def _parse_multiline_option(self, line) -> (str, str):
- """Extract option name and first line of value of multi-line option.
-
- :param line: line to parse
- :return: name and first line of multi-line option
- :raise ParseException: When process fail to extract data
- """
-
- matches = self._re_top_of_multiline_option.search(line)
- if matches is None or len(matches.groups()) != 2:
- logging.error(f'Error extracting first value in multi-line option from [{line}].')
- raise ParseException(f'Error extracting first value in multi-line option from [{line}].')
-
- return matches.group(1), matches.group(2)
-
- def _parse_single_line_option(self, line) -> (str, str):
- """Extract option name and value of single line option.
-
- :param line: line to parse
- :return: option name and value
- :raise ParseException: When process fail to extract data
- """
-
- matches = self._re_single_line_option.search(line)
-
- if matches is None or len(matches.groups()) != 2:
- logging.error(f'Error extracting option name and value from [{line}].')
- raise ParseException(f'Error extracting option name and value from [{line}].')
-
- return matches.group(1), matches.group(2)
-
- def _bool_single_line_option(self, line: str) -> bool:
- return self._re_single_line_option.match(line) is not None
-
- def _bool_comment_or_blank_line(self, line: str) -> bool: # NoQA
- return line.startswith("#") or (len(line) == 0)
-
- def _bool_top_of_multiline_option(self, line) -> bool:
- return self._re_top_of_multiline_option.match(line) is not None
diff --git a/docs/doxygen_xml.py b/docs/doxygen_xml.py
deleted file mode 100644
index e84891a31a..0000000000
--- a/docs/doxygen_xml.py
+++ /dev/null
@@ -1,1756 +0,0 @@
-"""doxygen_xml.py
-
-Python Interface to Doxygen XML Output
-
-DoxygenXml is the primary class defined herein.
-It uses all the other classes, and is used by:
-
-- ./docs/build.py via ./docs/api_doc_builder.py for the LVGL doc build to:
- - edit Doxyfile to replace 2 string tokens
- - run Doxygen
- - load Doxygen XML output and use it to:
- - generate API `.rst` pages,
- - populate a set of `doxygen_xml.<...>` dictionaries, and
- - use those dictionaries to determine in what LVGL documents
- hyperlinks to API pages might be useful and appends them if they
- are not already present.
-
-- ./scripts/gen_json/gen_json.py => pycparser_monkeypatch.py to
- - edit Doxyfile to replace 2 string tokens
- - silently run Doxygen
- - load Doxygen XML output and use it to:
- - populate a set of `doxygen_xml.<...>` dictionaries, and
- - monkey-patch pycparser AST (Abstract Syntax Tree) output to
- extract API documentation into its JSON output.
-
-"Loading Doxygen XML output" consists of
-
-- opening and parsing the XML `index.xml` file generated by Doxygen which
- is a summary of Doxygen-generated documentation linked to additional `.xml`
- filenames that have more details. Note that `index.xml` has a complete list
- of documented symbol-names from the C code. `./docs/api_doc_builder.py`
- searches those names using search term(s) to determine in what LVGL
- documents hyperlinks to API pages might be useful, and appends them
- to those documents if they are not already present.
-
- `index.xml` structure is described in the `doxygen/doxygen` repository
- on GitHub under `./templates/xml/index.xsd`, and the structure of the
- other files is described in `./templates/xml/compound.xsd`.
-
-
- Unlimited-length list of nodes.
- ...
- ...
- ...
-
- ...
-
-
-In `index.xml`, every element:
-
- - always has attributes "refid" and "kind",
- - always has exactly 1 element, and
- - is followed by an unbounded number of elements.
-
-Each element's 'kind' attribute will be one of the following:
-
- class struct union interface
- protocol category exception file
- namespace group page example
- dir type concept module
-
-which is a group that applies to many languages. Since Doxygen
-is parsing a C-language project, we will only find these in the
-`index.xml` file:
-
- - struct
- - union
- - file
- - page (unused)
- - dir (unused)
-
-In `'index.xml`, every element:
-
- - always has attributes "refid" and "kind",
- - always has exactly 1 element.
-
-The contents of the elements differ based on the "kind" of
- element they are contained by.
-
-kind="struct"
-
- - sub-elements only have `kind="variable"` attributes.
-
-kind="union"
-
- - sub-elements only have `kind="variable"` attributes.
-
-kind="file"
-
- - sub-elements can have `kind` attributes:
- - define
- - enum
- - enumvalue has refid that begins with containing enum's refid
- - variable
- - function
- - typedef
-
- -
- Has child element with macro name. refid contains something
- that looks like this:
-
- "lv__obj_8h_1a8ec0fe9743c5c015162d64df591cd719"
- /____________________________________________/
- /_________/ |
- | +-- ``id`` attribute of applicable item
- |
- +-- stem of `.xml` filename containing details
-
-lv__obj_8h.xml has this format:
-
-
- lv_obj.h
- Section dealing with #include's (list, dependency graph, etc.)
-
-
-
-
-
-
-
-
- ...List of elements.
- ...kind can be:
- - define
- - enum
- - var
- - func
-
- ...
- ...
- ...
- ...List of all defines in file.
-
-
- ...
-
-
-
- LV_STATE_DEFAULT
- = 0x0000
-
-
-
-
-
- ...List of all the enumeration's enumvalue's.
-
- ...List of all enums in the file.
-
-
-
- ...List of all variables in file.
-
-
-
- ...List of all functions in file.
-
- @file @brief description
-
- @file detailed description
-
- Only included if XML_PROGRAMLISTING = YES.
- ...
- ...
- ...
-
-
-
-
-kind="page"
-
- - Has no child members, but does have a child element
- (e.g. "deprecated" and "todo").
-
-kind="dir"
-
- - do not have sub-elements
- - contents of element is a full path to a directory
- - If sorted alphabetically, they form a sequence that can be
- used to create something:
- E:
- E:/Dev
- E:/Dev/lvgl
- E:/Dev/lvgl/lvgl
- E:/Dev/lvgl/lvgl/src
- etc.
- - refid="..." contains the stem of another `.xml` file that
- contains more information on the directory, including any
- documentation on it found by Doxygen. That XML file's structure is:
-
- .xml:
- ------------
-
- full_path_of_dir_wo_trailing_slash
- subdirectory_path
- subdirectory_path
- subdirectory_path
- ... (list elements --- one for each subdirectory in directory)
- ...
- filename
- filename
- filename
- ... (list elements --- one for each file in directory)
- ...
-
- Brief description of directory (Doxygen documentation)
-
-
- Detailed description of directory (Doxygen documentation)
-
-
-
-
-
-list
-
- - define
- - dir (unused)
- - enum
- - enumvalue
- - example (unused)
- - file
- - function
- - page (unused)
- - struct
- - typedef
- - union
- - variable
-
-Most elements have child elements with their own contents
-depending on the 'kind' of element they are in.
-
-This file defines classes, whose class names are the upper-case names above
-except for the unused ones. These classes are used to build a data structure
-that matches that of the `index.xml` file.
-
-The list of classes is:
-
- +-----------+----------------------------------------------------+
- | Class | Adds New Instance to this Dictionary in __init__() |
- +===========+====================================================+
- | DEFINE | defines |
- +-----------+----------------------------------------------------+
- | ENUM | enums (have child ENUMVALUE objects) |
- +-----------+----------------------------------------------------+
- | VARIABLE | variables |
- +-----------+----------------------------------------------------+
- | NAMESPACE | namespaces |
- +-----------+----------------------------------------------------+
- | STRUCT | structures (have child STRUCT_FIELD objects) |
- +-----------+----------------------------------------------------+
- | UNION | unions (have child STRUCT_FIELD objects) |
- +-----------+----------------------------------------------------+
- | TYPEDEF | typedefs |
- +-----------+----------------------------------------------------+
- | FUNCTION | functions (have child FUNC_ARG objects) |
- +-----------+----------------------------------------------------+
- | GROUP | groups |
- +-----------+----------------------------------------------------+
- | FILE | files |
- +-----------+----------------------------------------------------+
- | CLASS | classes |
- +-----------+----------------------------------------------------+
-
-Additional classes:
-
- - FUNC_ARG(object): (becomes child members of FUNCTION objects)
- - STRUCT_FIELD(object): (becomes child members of STRUCT objects)
- - ENUMVALUE(object): (becomes child members of ENUM objects)
- - XMLSearch(object): (used only by ./scripts/gen_json/gen_json.py)
-
-Each of the above Dictionary variables has entries with
-
- - keys = documented symbol name from C code that Doxygen found
- - values = instances of classes named above
-
-Samples:
-
- 'defines': {'ZERO_MEM_SENTINEL': ,
- 'LV_GLOBAL_DEFAULT': ,
- 'LV_ASSERT_OBJ': ,
- 'LV_TRACE_OBJ_CREATE': ,...}
-
- 'enums': {'lv_key_t': ,
- 'lv_group_refocus_policy_t': ,
- 'lv_obj_flag_t': ,
- 'lv_obj_class_editable_t': ,...}
-
- 'variables': {'lv_global': ,
- 'lv_obj_class': ,
- 'lv_font_montserrat_8': ,
- 'lv_font_montserrat_10': ,...}
-
- 'namespaces': {},
-
- 'structures': {'_lv_anim_t::_lv_anim_path_para_t': ,
- '_lv_anim_t': ,
- '_lv_animimg_t': ,
- '_lv_arc_t': ,...}
-
- 'unions': {},
-
- 'typedefs': {'lv_global_t': ,
- 'lv_group_focus_cb_t': ,
- 'lv_group_edge_cb_t': ,...}
-
- 'functions': {'lv_group_create': ,
- 'lv_group_delete': ,
- 'lv_group_set_default': ,...}
-
-Additional dictionaries:
- 'files': {'lv_global.h': ,
- 'lv_group.h': ,
- 'lv_group_private.h': ,...}
-
-"""
-import os
-import sys
-import subprocess
-from xml.etree import ElementTree
-import doxygen_config
-from announce import *
-
-EMIT_WARNINGS = True
-DOXYGEN_OUTPUT = True
-
-MISSING_FUNC = 'MissingFunctionDoc'
-MISSING_FUNC_ARG = 'MissingFunctionArgDoc'
-MISSING_FUNC_RETURN = 'MissingFunctionReturnDoc'
-MISSING_FUNC_ARG_MISMATCH = 'FunctionArgMissing'
-MISSING_STRUCT = 'MissingStructureDoc'
-MISSING_STRUCT_FIELD = 'MissingStructureFieldDoc'
-MISSING_UNION = 'MissingUnionDoc'
-MISSING_UNION_FIELD = 'MissingUnionFieldDoc'
-MISSING_ENUM = 'MissingEnumDoc'
-MISSING_ENUM_ITEM = 'MissingEnumItemDoc'
-MISSING_TYPEDEF = 'MissingTypedefDoc'
-MISSING_VARIABLE = 'MissingVariableDoc'
-MISSING_MACRO = 'MissingMacroDoc'
-
-# Dictionaries built from Doxygen XML output via `xml.etree.ElementTree`
-defines = {} # dictionary of doxygen_xml.DEFINE objects
-enums = {} # dictionary of doxygen_xml.ENUM objects
-variables = {} # dictionary of doxygen_xml.VARIABLE objects
-namespaces = {} # dictionary of doxygen_xml.NAMESPACE objects
-structures = {} # dictionary of doxygen_xml.STRUCT objects
-typedefs = {} # dictionary of doxygen_xml.TYPEDEF objects
-functions = {} # dictionary of doxygen_xml.FUNCTION objects
-groups = {} # dictionary of doxygen_xml.GROUP objects
-files = {} # dictionary of doxygen_xml.FILE objects
-classes = {} # dictionary of doxygen_xml.CLASS objects
-unions = {} # appears to be unused at this time (unions => structures dict).
-
-# Module-Global Variables
-xml_path = ''
-
-
-def run_ext_cmd(cmd_str: str, start_dir: str = None, quiet: bool = False, exit_on_error: bool = True):
- """
- Run external command `cmd_str` (possibly silently) in directory
- `start_dir` (if provided), and optionally abort execution on error.
-
- :param cmd_str: String to pass to OS.
- :param start_dir: Directory to start in, or `None` to run in "cwd".
- :param quiet: Should STDOUT and STDERR be suppressed?
- :param exit_on_error: Should this function abort execution on
- a non-zero exit status?
- :return: n/a
- """
- saved_dir = None
-
- if start_dir is not None:
- saved_dir = os.getcwd()
- os.chdir(start_dir)
-
- if quiet:
- # This method of running Doxygen is used because we do not
- # want anything going to STDOUT. Running it via `os.system()`
- # would send its output to STDOUT.
- p = subprocess.Popen(
- cmd_str,
- stdout=subprocess.PIPE,
- stderr=subprocess.PIPE,
- shell=True
- )
-
- out, err = p.communicate()
-
- if p.returncode:
- if out:
- # Note the `.decode("utf-8")` is required because
- # `sys.stdout.write()` requires a string, and `out` is
- # a byte array; generates an exception if passed alone.
- sys.stdout.write(out.decode("utf-8"))
- sys.stdout.flush()
- if err:
- sys.stderr.write(err.decode("utf-8"))
- sys.stdout.flush()
-
- if exit_on_error:
- sys.exit(p.returncode)
- else:
- announce_start(__file__, f'Running [{cmd_str}] in [{os.getcwd()}]...')
- return_code = os.system(cmd_str)
- announce_finish()
-
- if return_code != 0 and exit_on_error:
- print(f'Exiting due to error [{return_code}] running [{cmd_str}].')
- sys.exit(return_code)
-
- if saved_dir is not None:
- os.chdir(saved_dir)
-
-
-def warn(warning_type, *args):
- if EMIT_WARNINGS:
- args = ' '.join(str(arg) for arg in args)
-
- if warning_type is None:
- output = f'\033[31;1m {args}\033[0m\n'
- else:
- output = f'\033[31;1m{warning_type}: {args}\033[0m\n'
-
- sys.stdout.write(output)
- sys.stdout.flush()
-
-
-def build_docstring(element):
- docstring = None
- if element.tag == 'parameterlist':
- return None
-
- if element.text:
- docstring = element.text.strip()
-
- for item in element:
- ds = build_docstring(item)
- if ds:
- if docstring:
- docstring += ' ' + ds
- else:
- docstring = ds.strip()
-
- if element.tag == 'para':
- if docstring:
- docstring = '\n\n' + docstring
-
- if element.tag == 'ref':
- docstring = f':ref:`{docstring}`'
-
- if element.tail:
- if docstring:
- docstring += ' ' + element.tail.strip()
- else:
- docstring = element.tail.strip()
-
- return docstring
-
-
-def read_as_xml(d):
- try:
- return ElementTree.fromstring(d)
- except: # NOQA
- return None
-
-
-def load_xml_etree(fle):
- fle = os.path.join(xml_path, fle + '.xml')
-
- with open(fle, 'rb') as f:
- d = f.read().decode('utf-8')
-
- # This code is to correct a bug in Doxygen. That bug incorrectly parses
- # a typedef, and it causes an error to occur building the docs. The Error
- # doesn't stop the documentation from being generated, I just don't want
- # to see the ugly red output.
- #
- # if 'typedef void() lv_lru_free_t(void *v)' in d:
- # d = d.replace(
- # 'void()\n '
- # 'typedef void() lv_lru_free_t(void *v)',
- # 'void\n '
- # 'typedef void(lv_lru_free_t)(void *v)'
- # )
- # with open(fle, 'wb') as f:
- # f.write(d.encode('utf-8'))
-
- return ElementTree.fromstring(d)
-
-
-def get_type(node):
- def gt(n):
- for c in n:
- if c.tag == 'ref':
- t = c.text.strip()
- break
- else:
- t = node.text.strip()
-
- return t.replace('*', '').replace('(', '').replace(')', '').strip()
-
- for child in node:
- if child.tag == 'type':
- return gt(child)
-
-
-def build_define(element):
- define = None
-
- if element.text:
- define = element.text.strip()
-
- for item in element:
- ds = build_define(item)
- if ds:
- if define:
- define += ' ' + ds
- else:
- define = ds.strip()
-
- if element.tail:
- if define:
- define += ' ' + element.tail.strip()
- else:
- define = element.tail.strip()
-
- return define
-
-
-class STRUCT_FIELD(object):
-
- def __init__(self, name, _type, description, file_name, line_no):
- self.name = name
- self.type = _type
- self.description = description
- self.file_name = file_name
- self.line_no = line_no
-
-
-class STRUCT(object):
- """ elements in Doxygen `index.xml`"""
- _missing = MISSING_STRUCT
- _missing_field = MISSING_STRUCT_FIELD
-
- template = '''\
-.. doxygenstruct:: {name}
- :project: lvgl
- :members:
- :protected-members:
- :private-members:
- :undoc-members:
-'''
-
- def __init__(self, parent, refid, name, **_):
- global structures
- global unions
-
- if type(self) is UNION:
- # UNION inherits from STRUCT.
- if name in unions:
- self.__dict__.update(unions[name].__dict__)
- else:
- unions[name] = self
- self.parent = parent
- self.refid = refid
- self.name = name
- self.types = set()
- self._deps = None
- self.header_file = ''
- self.description = None
- self.fields = []
- self.file_name = None
- self.line_no = None
- else:
- # STRUCT type
- if name in structures:
- self.__dict__.update(structures[name].__dict__)
- else:
- structures[name] = self
- self.parent = parent
- self.refid = refid
- self.name = name
- self.types = set()
- self._deps = None
- self.header_file = ''
- self.description = None
- self.fields = []
- self.file_name = None
- self.line_no = None
-
- # Prior to 9-Mar-2025, the code below was never executing since this
- # __init__() was never called with a `parent` value other than `None`.
- # Reason: `kind="struct"` only occurs in `index.xml` as a top-level
- # entry, and not as a child element of `kind="file"` as do
- # elements with kind = define, var, enum and func.
- # Original code:
- # if parent and refid:
- if refid:
- root = load_xml_etree(refid)
-
- for compounddef in root:
- if compounddef.attrib['id'] != self.refid:
- continue
-
- for child in compounddef:
- if child.tag == 'includes':
- self.header_file = os.path.splitext(child.text)[0]
- continue
-
- elif child.tag == 'location':
- self.file_name = child.attrib['file']
- self.line_no = child.attrib['line']
-
- elif child.tag == 'detaileddescription':
- self.description = build_docstring(child)
-
- elif child.tag == 'sectiondef':
- for memberdef in child:
- t = get_type(memberdef)
- description = None
- name = ''
- file_name = None
- line_no = None
-
- # For each struct member...
- for element in memberdef:
- if element.tag == 'location':
- file_name = element.attrib['file']
- line_no = element.attrib['line']
-
- elif element.tag == 'name':
- name = element.text
-
- elif element.tag == 'detaileddescription':
- description = build_docstring(element)
-
- field = STRUCT_FIELD(name, t, description, file_name, line_no)
- self.fields.append(field)
-
- if t is None:
- continue
-
- self.types.add(t)
-
- if EMIT_WARNINGS:
- if not self.description:
- warn(self._missing, self.name)
- warn(None, 'FILE:', self.file_name)
- warn(None, 'LINE:', self.line_no)
- warn(None)
-
- for field in self.fields:
- if not field.description:
- warn(self._missing_field, self.name)
- warn(None, 'FIELD:', field.name)
- warn(None, 'FILE:', field.file_name)
- warn(None, 'LINE:', field.line_no)
- warn(None)
-
- def get_field(self, name):
- for field in self.fields:
- if field.name == name:
- return field
-
- @property
- def deps(self):
- if self._deps is None:
- self._deps = dict(
- typedefs=set(),
- functions=set(),
- enums=set(),
- structures=set(),
- unions=set(),
- namespaces=set(),
- variables=set(),
- )
- for type_ in self.types:
- if type_ in typedefs:
- self._deps['typedefs'].add(typedefs[type_])
- elif type_ in structures:
- self._deps['structures'].add(structures[type_])
- elif type_ in unions:
- self._deps['unions'].add(unions[type_])
- elif type_ in enums:
- self._deps['enums'].add(enums[type_])
- elif type_ in functions:
- self._deps['functions'].add(functions[type_])
- elif type_ in variables:
- self._deps['variables'].add(variables[type_])
- elif type_ in namespaces:
- self._deps['namespaces'].add(namespaces[type_])
- return self._deps
-
- def __str__(self):
- return self.template.format(name=self.name)
-
-
-class UNION(STRUCT):
- """ elements in Doxygen `index.xml`"""
- _missing = MISSING_UNION
- _missing_field = MISSING_UNION_FIELD
-
- template = '''\
-.. doxygenunion:: {name}
- :project: lvgl
-'''
-
-
-class VARIABLE(object):
- """
- elements in Doxygen `index.xml`"""
- template = '''\
-.. doxygenvariable:: {name}
- :project: lvgl
-'''
-
- def __init__(self, parent, refid, name, **_):
- global variables
-
- if name in variables:
- self.__dict__.update(variables[name].__dict__)
- else:
- variables[name] = self
- self.parent = parent
- self.refid = refid
- self.name = name
- self.description = None
- self.type = ''
- self.file_name = None
- self.line_no = None
-
- if parent is not None:
- root = load_xml_etree(parent.refid)
-
- for compounddef in root:
- if compounddef.attrib['id'] != parent.refid:
- continue
-
- for child in compounddef:
- if (
- child.tag == 'sectiondef' and
- child.attrib['kind'] == 'var'
- ):
- for memberdef in child:
- if memberdef.attrib['id'] == refid:
- break
- else:
- continue
-
- self.type = get_type(memberdef)
-
- for element in memberdef:
- if element.tag == 'location':
- self.file_name = element.attrib['file']
- self.line_no = element.attrib['line']
- elif element.tag == 'detaileddescription':
- self.description = build_docstring(element)
-
- if not self.description:
- warn(MISSING_VARIABLE, self.name)
- warn(None, 'FILE:', self.file_name)
- warn(None, 'LINE:', self.line_no)
- warn(None)
-
- def __str__(self):
- return self.template.format(name=self.name)
-
-
-class NAMESPACE(object):
- """ elements in Doxygen `index.xml`"""
- template = '''\
-.. doxygennamespace:: {name}
- :project: lvgl
- :members:
- :protected-members:
- :private-members:
- :undoc-members:
-'''
-
- def __init__(self, parent, refid, name, **_):
- global namespaces
-
- if name in namespaces:
- self.__dict__.update(namespaces[name].__dict__)
- else:
- namespaces[name] = self
- self.parent = parent
- self.refid = refid
- self.name = name
- self.description = None
- self.line_no = None
- self.file_name = None
- self.enums = []
- self.funcs = []
- self.vars = []
- self.typedefs = []
- self.structs = []
- self.unions = []
- self.classes = []
-
- # root = load_xml(refid)
- #
- # for compounddef in root:
- # if compounddef.attrib['id'] != refid:
- # continue
- #
- # for sectiondef in compounddef:
- # if sectiondef.tag != 'sectiondef':
- # continue
- #
- # enum
- # typedef
- # func
- # struct
- # union
- #
- #
- # cls = globals()[sectiondef.attrib['kind'].upper()]
- # if cls == ENUM:
- # if sectiondef[0].text:
- # sectiondef.attrib['name'] = sectiondef[0].text.strip()
- # enums_.append(cls(self, **sectiondef.attrib))
- # else:
- # sectiondef.attrib['name'] = None
- # enums_.append(cls(self, **sectiondef.attrib))
- #
- # elif cls == ENUMVALUE:
- # if enums_[-1].is_member(sectiondef):
- # enums_[-1].add_member(sectiondef)
- #
- # else:
- # sectiondef.attrib['name'] = sectiondef[0].text.strip()
- # cls(self, **sectiondef.attrib)
-
- def __str__(self):
- return self.template.format(name=self.name)
-
-
-class GROUP(object):
- """ elements in Doxygen `index.xml`"""
- template = '''\
-.. doxygengroup:: {name}
- :project: lvgl
-'''
-
- def __init__(self, parent, refid, name, **_):
- global groups
-
- if name in groups:
- self.__dict__.update(functions[name].__dict__)
- else:
- functions[name] = self
- self.parent = parent
- self.refid = refid
- self.name = name
- self.description = None
-
- def __str__(self):
- return self.template.format(name=self.name)
-
-
-class FUNC_ARG(object):
- """ elements in Doxygen `index.xml`"""
-
- def __init__(self, name, _type):
- self.name = name
- self.type = _type
- self.description = None
-
-
-class FUNCTION(object):
- """
- elements in Doxygen `index.xml`"""
- template = '''\
-.. doxygenfunction:: {name}
- :project: lvgl
-'''
-
- def __init__(self, parent, refid, name, **_):
- global functions
-
- if name in functions:
- self.__dict__.update(functions[name].__dict__)
- else:
- functions[name] = self
- self.parent = parent
- self.refid = refid
- self.name = name
- self.types = set()
- self.restype = None
- self.args = []
- self._deps = None
- self.description = None
- self.res_description = None
- self.file_name = None
- self.line_no = None
- self.void_return = False
-
- if parent is not None:
- root = load_xml_etree(parent.refid)
-
- for compounddef in root:
- if compounddef.attrib['id'] != parent.refid:
- continue
-
- for child in compounddef:
- if child.tag != 'sectiondef':
- continue
-
- if child.attrib['kind'] != 'func':
- continue
-
- for memberdef in child:
- if 'id' not in memberdef.attrib:
- continue
-
- if memberdef.attrib['id'] == refid:
- break
- else:
- continue
-
- break
- else:
- continue
-
- break
- else:
- return
-
- self.restype = get_type(memberdef)
-
- for child in memberdef:
- if child.tag == 'type':
- if child.text and child.text.strip() == 'void':
- self.void_return = True
-
- if child.tag == 'param':
- t = get_type(child)
- if t is not None:
- self.types.add(t)
-
- for element in child:
- if element.tag == 'declname':
- arg = FUNC_ARG(element.text, t)
- self.args.append(arg)
-
- for child in memberdef:
- if child.tag == 'location':
- self.file_name = child.attrib['file']
- self.line_no = child.attrib['line']
-
- elif child.tag == 'detaileddescription':
- self.description = build_docstring(child)
- for element in child:
- if element.tag != 'para':
- continue
-
- for desc_element in element:
- if desc_element.tag == 'simplesect' and desc_element.attrib['kind'] == 'return':
- self.res_description = build_docstring(desc_element)
-
- if desc_element.tag != 'parameterlist':
- continue
-
- for parameter_item in desc_element:
- parameternamelist = parameter_item[0]
- if parameternamelist.tag != 'parameternamelist':
- continue
-
- parameter_name = parameternamelist[0].text
-
- try:
- parameterdescription = parameter_item[1]
- if parameterdescription.tag == 'parameterdescription':
- parameter_description = build_docstring(parameterdescription)
- else:
- parameter_description = None
- except IndexError:
- parameter_description = None
-
- if parameter_name is not None:
- for arg in self.args:
- if arg.name != parameter_name:
- continue
-
- arg.description = parameter_description
- break
- else:
- warn(MISSING_FUNC_ARG_MISMATCH, self.name)
- warn(None, 'ARG:', parameter_name)
- warn(None, 'FILE:', self.file_name)
- warn(None, 'LINE:', self.line_no)
- warn(None)
-
- if not self.description:
- warn(MISSING_FUNC, self.name)
- warn(None, 'FILE:', self.file_name)
- warn(None, 'LINE:', self.line_no)
- warn(None)
- else:
- for arg in self.args:
- if not arg.description:
- warn(MISSING_FUNC_ARG, self.name)
- warn(None, 'ARG:', arg.name)
- warn(None, 'FILE:', self.file_name)
- warn(None, 'LINE:', self.line_no)
- warn(None)
-
- if not self.res_description and not self.void_return:
- warn(MISSING_FUNC_RETURN, self.name)
- warn(None, 'FILE:', self.file_name)
- warn(None, 'LINE:', self.line_no)
- warn(None)
-
- if self.restype in self.types:
- self.restype = None
-
- @property
- def deps(self):
- if self._deps is None:
- self._deps = dict(
- typedefs=set(),
- functions=set(),
- enums=set(),
- structures=set(),
- unions=set(),
- namespaces=set(),
- variables=set(),
- )
- if self.restype is not None:
- self.types.add(self.restype)
-
- for type_ in self.types:
- if type_ in typedefs:
- self._deps['typedefs'].add(typedefs[type_])
- elif type_ in structures:
- self._deps['structures'].add(structures[type_])
- elif type_ in unions:
- self._deps['unions'].add(unions[type_])
- elif type_ in enums:
- self._deps['enums'].add(enums[type_])
- elif type_ in functions:
- self._deps['functions'].add(functions[type_])
- elif type_ in variables:
- self._deps['variables'].add(variables[type_])
- elif type_ in namespaces:
- self._deps['namespaces'].add(namespaces[type_])
- return self._deps
-
- def __str__(self):
- return self.template.format(name=self.name)
-
-
-class FILE(object):
- """ elements in Doxygen `index.xml`"""
-
- def __init__(self, _, refid, name, node, **__):
- global files
-
- if name.endswith('lv_types.h'):
- return
-
- if name in files:
- self.__dict__.update(files[name].__dict__)
- return
-
- files[name] = self
-
- self.refid = refid
- self.name = name
- self.header_file = os.path.splitext(name)[0]
- self.types_contained = set()
-
- enums_ = []
-
- for member in node:
- if member.tag != 'member':
- continue
-
- cls_name = member.attrib['kind'].upper()
- self.types_contained.add(cls_name)
- cls = globals()[cls_name]
- if cls == ENUM:
- if member[0].text:
- member.attrib['name'] = member[0].text.strip()
- enums_.append(cls(self, **member.attrib))
- else:
- member.attrib['name'] = None
- enums_.append(cls(self, **member.attrib))
-
- elif cls == ENUMVALUE:
- if enums_[-1].is_member(member):
- enums_[-1].add_member(member)
-
- else:
- member.attrib['name'] = member[0].text.strip()
- cls(self, **member.attrib)
-
-
-class ENUMVALUE(object):
- """
-
- elements in Doxygen `index.xml`"""
- template = '''\
-.. doxygenenumvalue:: {name}
- :project: lvgl
-'''
-
- def __init__(self, parent, refid, name, **_):
- self.parent = parent
- self.refid = refid
- self.name = name
- self.description = None
- self.file_name = None
- self.line_no = None
-
- if parent is not None:
- if parent.file_name is not None:
- self.file_name = parent.file_name
- elif hasattr(parent, 'parent') and parent.parent is not None:
- if parent.parent.name is not None:
- self.file_name = parent.parent.name
-
- def __str__(self):
- return self.template.format(name=self.name)
-
-
-class ENUM(object):
- """
- elements in Doxygen `index.xml`"""
- template = '''\
-.. doxygenenum:: {name}
- :project: lvgl
-'''
-
- def __init__(self, parent, refid, name, **_):
- global enums
-
- if name in enums:
- # This happens when `name` is `None`, for example.
- # This is true for unnamed enumerations.
- self.__dict__.update(enums[name].__dict__)
- else:
-
- enums[name] = self
-
- self.parent = parent
- self.refid = refid
- self.name = name
- self.members = []
- self.description = None
- self.file_name = None
- self.line_no = None
-
- if parent is not None:
- root = load_xml_etree(parent.refid)
-
- for compounddef in root:
- if compounddef.attrib['id'] != parent.refid:
- continue
-
- for child in compounddef:
- if child.tag != 'sectiondef':
- continue
-
- if child.attrib['kind'] != 'enum':
- continue
-
- for memberdef in child:
- if 'id' not in memberdef.attrib:
- continue
-
- if memberdef.attrib['id'] == refid:
- break
- else:
- continue
-
- break
- else:
- continue
-
- break
- else:
- return
- # raise RuntimeError(f'not able to locate enum {name} ({refid})')
-
- for element in memberdef:
- if element.tag == 'location':
- self.file_name = element.attrib['file']
- self.line_no = element.attrib['line']
-
- if element.tag == 'detaileddescription':
- self.description = build_docstring(element)
- elif element.tag == 'enumvalue':
- item_name = None
- item_description = None
- item_file_name = None
- item_line_no = None
-
- for s_element in element:
- if s_element.tag == 'name':
- item_name = s_element.text
- elif s_element.tag == 'detaileddescription':
- item_description = build_docstring(s_element)
-
- elif s_element.tag == 'location':
- item_file_name = child.attrib['file']
- item_line_no = child.attrib['line']
-
- if item_name is not None:
- for ev in self.members:
- if ev.name != item_name:
- continue
- break
- else:
- ev = ENUMVALUE(
- self,
- element.attrib['id'],
- item_name
- )
-
- self.members.append(ev)
-
- ev.description = item_description
-
- if not self.description:
- warn(MISSING_ENUM, self.name)
- warn(None, 'FILE:', self.file_name)
- warn(None, 'LINE:', self.line_no)
- warn(None)
-
- for member in self.members:
- if not member.description:
- warn(MISSING_ENUM_ITEM, self.name)
- warn(None, 'MEMBER:', member.name)
- warn(None, 'FILE:', self.file_name)
- warn(None, 'LINE:', self.line_no)
- warn(None)
-
- def is_member(self, member):
- return (
- member.attrib['kind'] == 'enumvalue' and
- member.attrib['refid'].startswith(self.refid)
- )
-
- def add_member(self, member):
- name = member[0].text.strip()
- for ev in self.members:
- if ev.name == name:
- return
-
- self.members.append(
- ENUMVALUE(
- self,
- member.attrib['refid'],
- name
- )
- )
-
- def __str__(self):
- template = [self.template.format(name=self.name)]
- template.extend(list(str(member) for member in self.members))
-
- return '\n'.join(template)
-
-
-class DEFINE(object):
- """
- elements in Doxygen `index.xml`"""
- template = '''\
-.. doxygendefine:: {name}
- :project: lvgl
-'''
-
- def __init__(self, parent, refid, name, **_):
- global defines
-
- if name in defines:
- self.__dict__.update(defines[name].__dict__)
- else:
- defines[name] = self
-
- self.parent = parent
- self.refid = refid
- self.name = name
- self.description = None
- self.file_name = None
- self.line_no = None
- self.params = None
- self.initializer = None
-
- if parent is not None:
- root = load_xml_etree(parent.refid)
- memberdef = []
-
- for compounddef in root:
- if compounddef.attrib['id'] != parent.refid:
- continue
-
- for child in compounddef:
- if child.tag != 'sectiondef':
- continue
-
- if child.attrib['kind'] != 'define':
- continue
-
- for memberdef in child:
- if memberdef.attrib['id'] == refid:
- break
- else:
- continue
-
- break
- else:
- continue
-
- break
- else:
- return
-
- for element in memberdef:
- if element.tag == 'location':
- self.file_name = element.attrib['file']
- self.line_no = element.attrib['line']
-
- elif element.tag == 'detaileddescription':
- self.description = build_docstring(element)
-
- elif element.tag == 'param':
- for child in element:
- if child.tag == 'defname':
- if self.params is None:
- self.params = []
-
- if child.text:
- self.params.append(child.text)
-
- elif element.tag == 'initializer':
- initializer = build_define(element)
- if initializer is None:
- self.initializer = ''
- else:
- self.initializer = initializer
-
- if not self.description:
- warn(MISSING_MACRO, self.name)
- warn(None, 'FILE:', self.file_name)
- warn(None, 'LINE:', self.line_no)
- warn(None)
-
- def __str__(self):
- return self.template.format(name=self.name)
-
-
-class TYPEDEF(object):
- template = '''\
-.. doxygentypedef:: {name}
- :project: lvgl
-'''
-
- def __init__(self, parent, refid, name, **_):
- global typedefs
-
- if name in typedefs:
- self.__dict__.update(typedefs[name].__dict__)
- else:
- typedefs[name] = self
-
- self.parent = parent
- self.refid = refid
- self.name = name
- self.type = None
- self._deps = None
- self.description = None
- self.file_name = None
- self.line_no = None
-
- if parent is not None:
- root = load_xml_etree(parent.refid)
-
- for compounddef in root:
- if compounddef.attrib['id'] != parent.refid:
- continue
-
- for child in compounddef:
- if child.tag != 'sectiondef':
- continue
- if child.attrib['kind'] != 'typedef':
- continue
-
- for memberdef in child:
- if 'id' not in memberdef.attrib:
- continue
-
- if memberdef.attrib['id'] == refid:
- break
- else:
- continue
-
- break
- else:
- continue
-
- break
- else:
- return
-
- for element in memberdef:
- if element.tag == 'location':
- self.file_name = element.attrib['file']
- self.line_no = element.attrib['line']
-
- if element.tag == 'detaileddescription':
- self.description = build_docstring(element)
-
- if not self.description:
- warn(MISSING_TYPEDEF, self.name)
- warn(None, 'FILE:', self.file_name)
- warn(None, 'LINE:', self.line_no)
- warn(None)
-
- self.type = get_type(memberdef)
-
- @property
- def deps(self):
- if self._deps is None:
- self._deps = dict(
- typedefs=set(),
- functions=set(),
- enums=set(),
- structures=set(),
- unions=set(),
- namespaces=set(),
- variables=set(),
- )
- if self.type is not None:
- type_ = self.type
-
- if type_ in typedefs:
- self._deps['typedefs'].add(typedefs[type_])
- elif type_ in structures:
- self._deps['structures'].add(structures[type_])
- elif type_ in unions:
- self._deps['unions'].add(unions[type_])
- elif type_ in enums:
- self._deps['enums'].add(enums[type_])
- elif type_ in functions:
- self._deps['functions'].add(functions[type_])
- elif type_ in variables:
- self._deps['variables'].add(variables[type_])
- elif type_ in namespaces:
- self._deps['namespaces'].add(namespaces[type_])
-
- return self._deps
-
- def __str__(self):
- return self.template.format(name=self.name)
-
-
-class CLASS(object):
- """
- elements in Doxygen `index.xml`"""
-
- def __init__(self, _, refid, name, node, **__):
- global classes
-
- if name in classes:
- self.__dict__.update(classes[name].__dict__)
- return
-
- classes[name] = self
-
- self.refid = refid
- self.name = name
-
- enums_ = []
-
- for member in node:
- if member.tag != 'member':
- continue
-
- cls = globals()[member.attrib['kind'].upper()]
- if cls == ENUM:
- member.attrib['name'] = member[0].text.strip()
- enums_.append(cls(self, **member.attrib))
- elif cls == ENUMVALUE:
- if enums_[-1].is_member(member):
- enums_[-1].add_member(member)
-
- else:
- member.attrib['name'] = member[0].text.strip()
- cls(self, **member.attrib)
-
-
-class DoxygenXml(object):
- """Opens, parses and loads a Doxygen-generated `index.xml` file
- and makes it available as a set of dictionary attributes of this
- module, documented at the top of this file and named below in
- `global` statements.
- """
-
- def __init__(self,
- lvgl_src_dir: str,
- intermediate_dir: str,
- doxyfile_src_file: str,
- silent_mode=False,
- doxy_tagfile=''):
- """
- - Prepare and run Doxygen, generating XML output.
- - Load that XML output, and use it to populate
- :param lvgl_src_dir:
- :param intermediate_dir:
- :param doxyfile_src_file:
- :param silent_mode:
- """
- # Dictionaries to Be Populated:
- global defines
- global enums
- global variables
- global namespaces
- global structures
- global typedefs
- global functions
- global groups
- global files
- global classes
- global unions
-
- global xml_path
-
- announce_set_silent_mode(silent_mode)
- base_dir = os.path.abspath(os.path.dirname(__file__))
- doxyfile_filename = str(os.path.split(doxyfile_src_file)[1])
- doxyfile_dst_file = str(os.path.join(intermediate_dir, doxyfile_filename))
- lv_conf_file = os.path.join(intermediate_dir, 'lv_conf.h')
- xml_path = os.path.join(intermediate_dir, 'xml')
-
- # In case DoxygenXml() is ever instantiated twice in 1 session,
- # clear these dictionaries before they are (re-)populated below.
- defines.clear()
- enums.clear()
- variables.clear()
- namespaces.clear()
- structures.clear()
- typedefs.clear()
- functions.clear()
- groups.clear()
- files.clear()
- classes.clear()
- unions.clear()
-
- # -----------------------------------------------------------------
- # Prep and run Doxygen
- # -----------------------------------------------------------------
- # Generate Doxyfile into `intermediate_dir` replacing certain
- # config options for this run.
- # 1. Load from Doxyfile
- cfg = doxygen_config.DoxygenConfig()
- cfg.load(doxyfile_src_file)
- # 2. Update cfg.
- cfg.set('OUTPUT_DIRECTORY', '.')
- cfg.set('XML_OUTPUT', "xml")
- cfg.set('HTML_OUTPUT', 'doxygen_html')
- cfg.set('INPUT', lvgl_src_dir)
- cfg.set('QUIET', 'YES')
- cfg.set('GENERATE_HTML', 'NO')
- cfg.set('GENERATE_DOCSET', 'NO')
- cfg.set('GENERATE_HTMLHELP', 'NO')
- cfg.set('GENERATE_CHI', 'NO')
- cfg.set('GENERATE_QHP', 'NO')
- cfg.set('GENERATE_ECLIPSEHELP', 'NO')
- cfg.set('GENERATE_LATEX', 'NO')
- cfg.set('GENERATE_RTF', 'NO')
- cfg.set('GENERATE_MAN', 'NO')
- cfg.set('GENERATE_XML', 'YES')
- cfg.set('GENERATE_DOCBOOK', 'NO')
- cfg.set('GENERATE_PERLMOD', 'NO')
-
- # The predefined definitions are:
- # - DOXYGEN (defines it as 1 and allows conditional directives [e.g. #if]
- # to do special things when DOXYGEN is processing it, as opposed to
- # a C compiler.
- # - LV_CONF_PATH predefines the path where `lv_conf_internal.h` will
- # include the `lv_conf.h` file.
- # - All the others here are used when macros prefix a line of code like this:
- #
- # LV_ATTRIBUTE_EXTERN_DATA extern const lv_obj_class_t lv_obj_class;
- #
- # which occurs in `lv_obj.h`. When these are added to the PREDEFINED list as
- # "MACRO_NAME=" with no value, Doxygen expands the macro to an empty string
- # allowing Doxygen to correctly parse the line. The additional macros that
- # are treated that way are found in the middle of `lv_conf_template.h` in a
- # section called "COMPILER SETTINGS" with no definitions, made for prefixing
- # various code. (This works around Doxygen's failure to correctly deal with
- # macros that are defined with no values, as these are in lv_conf.h.)
- # This list is current as of 28-Apr-2025.
- predefined_symbols = [
- 'DOXYGEN',
- f'LV_CONF_PATH="{lv_conf_file}"',
- 'LV_ATTRIBUTE_TICK_INC=',
- 'LV_ATTRIBUTE_TIMER_HANDLER=',
- 'LV_ATTRIBUTE_FLUSH_READY=',
- 'LV_ATTRIBUTE_MEM_ALIGN=',
- 'LV_ATTRIBUTE_LARGE_CONST=',
- 'LV_ATTRIBUTE_LARGE_RAM_ARRAY=',
- 'LV_ATTRIBUTE_FAST_MEM=',
- 'LV_ATTRIBUTE_EXTERN_DATA=',
- 'LV_FORMAT_ATTRIBUTE(fmt,va)=',
- 'FASTGLTF_EXPORT=',
- ]
-
- cfg.set('PREDEFINED', predefined_symbols)
-
- # Exclude OSAL `.h` files except for `lv_os_none.h`. Reason: they define
- # lv_mutex_t, lv_thread_t and lv_thread_sync_t in multiple places. Doxygen
- # must only see one.
- osal_dir = 'osal'
-
- osal_exclude_list = [
- 'lv_cmsis_rtos2.h',
- 'lv_freertos.h',
- 'lv_mqx.h',
- 'lv_pthread.h',
- 'lv_rtthread.h',
- 'lv_sdl2.h',
- 'lv_windows.h',
- ]
-
- exclude_paths = []
-
- for osal_h in osal_exclude_list:
- full_path = os.path.join(lvgl_src_dir, osal_dir, osal_h)
- exclude_paths.append(full_path)
-
- # Exclude as a workaround for Breathe parsing problems.
- full_path = os.path.join(lvgl_src_dir, 'core', 'lv_obj_property.h')
- exclude_paths.append(full_path)
-
- # Exclude GLTF templates that Breathe appears to not know how to parse.
- full_path = os.path.join(lvgl_src_dir, 'libs', 'gltf', 'fastgltf', 'lv_fastgltf.hpp')
- exclude_paths.append(full_path)
-
- # As of 07-Jan-2026, LVGL doc-build now replaces `lv_conf_internal.h`
- # (which has no Doxygen documentation) with a temporary `lv_conf.h`
- # generated ONLY for the purpose of doc-builds, which file goes away
- # after Doxygen is done. This causes conflicts of symbols between
- # these 2 files because the former is a "generated copy" of the latter.
- # So this exclusion removes the file with no documentation in it.
- full_path = os.path.join(lvgl_src_dir, 'lv_conf_internal.h')
- exclude_paths.append(full_path)
-
- cfg.set('EXCLUDE', exclude_paths)
-
- # Include TAGFILES if requested.
- if doxy_tagfile:
- cfg.set('GENERATE_TAGFILE', doxy_tagfile)
-
- # 3. Store it for use by Doxygen in intermediate directory.
- cfg.save(doxyfile_dst_file)
-
- # Run Doxygen in intermediate directory.
- run_ext_cmd('doxygen Doxyfile', intermediate_dir, quiet=silent_mode)
-
- # -----------------------------------------------------------------
- # Load root of Doxygen output (index.xml) as an `xml.etree.ElementTree`.
- # -----------------------------------------------------------------
- index_xml_etree = load_xml_etree('index')
-
- # Populate these dictionaries.
- # Keys : C-code-element names (str) found by Doxygen.
- # Values: The XML-node created by `xml.etree::ElementTree` in `load_xml()` above.
- #
- # defines, enums, variables,
- # namespaces, structures, typedefs,
- # functions, unions, groups,
- # files, classes.
- announce_start(__file__, "Building source-code symbol dictionaries...")
- module_namespace = globals()
-
- for compound in index_xml_etree:
- # Here we will encounter these "kind" in the index.xml
- # elements: dir, file, page, struct, union.
- compound.attrib['name'] = compound[0].text.strip()
-
- # Filter out dir, page, example.
- if compound.attrib['kind'] not in ('example', 'page', 'dir'):
- class_name = compound.attrib['kind'].upper()
- class_obj = module_namespace[class_name]
- _ = class_obj(None, node=compound, **compound.attrib)
-
- announce_finish()
-
- # Additional data: the above instantiates the class, but doesn't
- # store the resulting object anywhere, since each class' __init__()
- # function adds the new object to the appropriate dictionary,
- # and uses the arguments it gets to "build out" additional
- # structure and/or populate additional dictionaries based on
- # content. Each class __init__() function specifies args it
- # in its parameter list. They match needed key values in the
- # `**compound.attrib` dictionary. The remaining (unused)
- # keys in that dictionary are accepted in the `**_` parameter
- # at the end of the parameter list.
- #
- # FILE class populates `files` dict plus:
- # - `defines` dictionary
- # - `enums` dictionary with children:
- # - ENUMVALUEs
- # - `variables` dictionary
- # - `typedefs` dictionary
- # - `functions` dictionary with children:
- # - FUNC_ARGs
- # STRUCT class populates the `structures` dictionary with children:
- # - STRUCT_FIELDs
- #
- # and possibly `namespaces`, `groups` and `classes` if
- # they are present in `index.xml`.
-
-
- def get_macros(self):
- global defines
- return list(defines.values())
-
- def get_enum_item(self, e_name):
- global enums
- for enum, obj in enums.items():
- for enum_item in obj.members:
- if enum_item.name == e_name:
- return enum_item
-
- def get_enum(self, e_name):
- global enums
- return enums.get(e_name, None)
-
- def get_function(self, f_name):
- global functions
- return functions.get(f_name, None)
-
- def get_variable(self, v_name):
- global variables
- return variables.get(v_name, None)
-
- def get_union(self, u_name):
- global unions
- return unions.get(u_name, None)
-
- def get_structure(self, s_name):
- global structures
- return structures.get(s_name, None)
-
- def get_typedef(self, t_name):
- global typedefs
- return typedefs.get(t_name, None)
-
- def get_macro(self, m_name):
- global defines
- return defines.get(m_name, None)
diff --git a/docs/example_list.py b/docs/example_list.py
deleted file mode 100755
index 6a4fd8742f..0000000000
--- a/docs/example_list.py
+++ /dev/null
@@ -1,561 +0,0 @@
-#!/usr/bin/env python3
-""" example_list.py -- Build `examples.rst`
-
-`examples.rst` is built by gathering all examples recursively under
-`lvgl/examples/`. The recognized directives in that file are then
-processed by Sphinx using `lvgl/docs/src/_ext/lv_example.py`.
-
-Subsection names within `examples.rst` are made up of the capitalized
-directory names ('_' and '-' characters are converted to spaces) or
-are spelled out in `index.rst` files if the subsection names should be
-spelled differently. See "index.rst Format" section below for details.
-
-An "example" is defined as:
-
- - being anywhere under the `lvgl/examples/` directory,
- - the presence of an `index.rst` in a directory with examples, and
- - example C code in file names typically starting with "lv_example_".
-
-That C code and a live, interactive running example, is pulled into each
-example in the EXAMPLES HTML page, as well as every doc where such
-examples are included, by a directive like this:
-
-.. code::
-
- .. include:: /examples/layouts/flex/index.rst
-
-Note that in the intermediate dir (from which LVGL user docs are generated),
-``./examples/`` is a top-level subdirectory, so no relative "../../.." etc.
-is required in the path. Using a path starting at "root" tells Sphinx to
-start at the top-level directory where the docs are being generated,
-typically ``lvgl/docs/intermediate/``.
-
-See `build.py` for more information about the intermediate directory.
-
-If a subdirectory is included in the `avoid_dirs` list herein, it will
-not generate any output in the resulting `examples.rst` file.
-Example: `lvgl/examples/assets/`.
-
-
-Usage
-*****
-
-.. code::
-
- import example_list
- example_list.make_warnings_into_errors() # Optional
- example_list.DEBUG_MODE = True # Optional
- example_list.exec(intermediate_dir)
-
-or run it directly:
-
-.. code:: bash
-
- python3 example_list.py "/home/lvgl/docs/intermediate"
-
-In both cases, the `intermediate_dir` passed is expected to have
-an `./examples/` directory in it (copy of `lvgl/examples/`) as a
-top-level subdirectory.
-
-
-Examples Directory Requirements
-*******************************
-
-.. code:: text
-
- lvgl/examples/
- index.rst (.. dir_order: pseudo-directive since sub-dirs
- are not presented in alphabetical order)
- anim/
- index.rst (see below for expected contents)
- lv_example_anim_1.c
- lv_example_anim_2.c
- lv_example_anim_3.c
- lv_example_anim_4.c
- lv_example_anim_timeline_1.c
- ...
- layouts/
- flex/
- index.rst (see below for expected contents)
- lv_example_flex_1.c
- lv_example_flex_2.c
- lv_example_flex_3.c
- etc.
- grid/
- etc.
- libs/
- index.rst (section-heading name: "3rd-Party Libraries"
- [since it is different than parent directory name])
- barcode/
- index.rst (see below for expected contents)
- lv_example_barcode_1.c
- lv_example_barcode_1.h
- bmp/
- etc.
- etc.
- etc.
-
-.. note:: The above shows only 2 levels of directories but deeper
- levels are supported (up to 5 below document title).
-
-
-index.rst Format
-****************
-
-Examples
---------
-Examples are included in `index.rst` files when they are sibling files with
-the example C code.
-
-.. code::
-
- Example 1 Title <-- required for each example
- --------------- <-- required for each example
- <-- blank lines are ignored
- .. lv_example:: anim/lv_example_anim_1 <-- path relative to the `lvgl/examples/` dir
- :language: c
-
-Repeat the above pattern for each example in the current directory. That number
-may be zero (0) for directories like `libs/` in which all examples are in directories
-below that level. See directory structure above.
-
-For paths outside the current directory, simply provide the path to the code example
-relative to the `lvgl/examples/` directory. Example from
-`lvgl/examples/widgets/scale/index.rst`:
-
-.. code::
-
- ...
-
- A round scale style simulating a compass
- ----------------------------------------
-
- .. lv_example:: widgets/scale/lv_example_scale_12
- :language: c
-
- Axis ticks and labels with scrolling on a chart
- -----------------------------------------------
-
- .. lv_example:: widgets/chart/lv_example_chart_2 <-- path is outside scale/ dir
- :language: c
-
-.. note::
-
- Starting the example code filename with `lv_example_` is not a requirement of the
- this script, but does make it clear that it is an example, so this pattern should
- be preserved for new and changed examples.
-
-
-Custom Section Headings
------------------------
-If a section heading needs to be spelled differently than the capitalized name of the
-parent directory, then an `index.rst` file in that directory may contain the desired
-section-heading name in an ``.. example_heading`` pseudo-directive. Example from
-`lvgl/examples/libs/index.rst`:
-
-.. code::
-
- .. example_heading: 3rd-Party Libraries
-
-
-Directory Reordering
---------------------
-There are cases where it is not appropriate to present the contents of a
-set of subdirectories in alphabetical order. When this is the case, a
-pseudo-directive in the `index.rst` file in the parent directory can be specified
-to govern the sequence its subdirectories are processed. The example below
-is from `lvgl/examples/widgets/index.rst`. It is provided in order to
-cause the "Base Widget" (obj) directory to be processed first (and thus
-included in the output first).
-
-.. code::
-
- .. dir_order:
-
- obj
- animimg
- arc
- arclabel
- bar
- button
- buttonmatrix
- etc.
-
-.. note::
-
- A warning is issued if either:
-
- - a subdirectory is named that does not exist, or
- - a subdirectory exists that is not in the list and not in the `avoid_dirs` list.
-
-
-Making Warnings into Errors
-***************************
-
-If `make_warnings_into_errors()` was called, if there were any warnings
-issued, after the output is completely generated, this script will exit
-with a non-zero exit code. This can be done by client modules like this:
-
-.. code::
-
- import example_list
- example_list.make_warnings_into_errors()
- example_list.exec(intermediate_dir)
-
-To turn it off:
-
-.. code::
-
- example_list.make_warnings_into_errors(False)
-
-
-Debug Output
-************
-
-Enable debug output like this:
-
-.. code::
-
- example_list.DEBUG_MODE = True
-
-Suppressing DEBUG_MODE is the default.
-
-
-"""
-import os
-import sys
-from io import TextIOWrapper
-from announce import *
-
-# -------------------------------------------------------------------------
-# This is the order that LVGL documentation uses for the section heading
-# levels. header_defs[0] is the highest and header_defs[5] is the lowest.
-# If this order is not kept in the reST files Sphinx will complain, and
-# have difficulty formatting the TOC correctly.
-# -------------------------------------------------------------------------
-TITLE = '='
-CHAPTER = '*'
-SECTION = '-'
-SUBSECTION = '~'
-SUBSUBSECTION = '^'
-SUBSUBSUBSECTION = "'"
-
-header_defs = [
- TITLE,
- CHAPTER,
- SECTION,
- SUBSECTION,
- SUBSUBSECTION,
- SUBSUBSUBSECTION,
-]
-
-LV_EXAMPLE_DIRECTIVE = '.. lv_example::'
-EXAMPLE_HEADING_DIRECTIVE = '.. example_heading:'
-DIR_ORDER_DIRECTIVE = '.. dir_order:'
-DIR_SEP = os.sep
-INDEX_FILENAME = 'index.rst'
-MAKE_WARNINGS_INTO_ERRORS = False
-DEBUG_MODE = False
-THIS_FILE = os.path.basename(__file__)
-_warning_issued = False
-_top_level_heading_count = 0
-
-avoid_dirs = [
- os.path.join('examples', 'arduino'),
- os.path.join('examples', 'assets'),
-]
-
-
-def make_warnings_into_errors(val: bool = True):
- global MAKE_WARNINGS_INTO_ERRORS
- MAKE_WARNINGS_INTO_ERRORS = val
-
-
-def _default_section_heading(level: int, path: str, is_file: bool) -> str:
- if is_file:
- dir_path = os.path.dirname(path)
- else:
- dir_path = path
-
- parent_dir = os.path.basename(dir_path)
-
- # Compose default section heading based on capitalized words in `parent_dir`.
- word_list = parent_dir.replace('_', ' ').replace('-', ' ').split(' ')
- result = ''
-
- for word in word_list:
- result += ' ' + word.capitalize()
-
- # Remove leading space.
- if result:
- result = result[1:]
-
- return result
-
-
-def _warn(msg: str):
- global _warning_issued
- if MAKE_WARNINGS_INTO_ERRORS:
- warning_type = 'Error'
- else:
- warning_type = 'Warning'
-
- if is_silent_mode():
- print(f'{THIS_FILE}: ' + '\x1b[31m' + f'>>> {warning_type}: ' + msg + '\x1b[0m')
- else:
- announce_colored(THIS_FILE, 'red', f'>>> {warning_type}: ' + msg)
-
- _warning_issued = True
-
-
-def _in_avoid_dirs_list(dir_bep: str) -> bool:
- result = False
-
- for avoid_dir in avoid_dirs:
- if avoid_dir in dir_bep:
- result = True
- break
-
- return result
-
-
-def _validate_sub_dirs(sub_dirs: list[str], index_rst_path: str) -> bool:
- """ Validate sub-dirs that come from an `index.rst` `.. dir_order:` directive.
-
- 1. Check that that each one is an existing directory.
- 2. Check that there are none missing except those in `avoid_dirs`.
-
- Issue warning if either fails and set `_warning_issued`.
- If MAKE_WARNINGS_INTO_ERRORS, this will make script exit with an error
- code at the end.
-
- :param sub_dirs: List of sub-dirs to validate
- :param index_rst_path: Path to index.rst file
- :return: Whether `sub_dirs` is valid per 1 and 2 above
- """
- result = True
-
- # Check that each sub-dir is an existing directory.
- for sub_dir in sub_dirs:
- if not os.path.isdir(sub_dir):
- result = False
- _warn(f'Dir-order directive in {index_rst_path} contains dir [{sub_dir}] that does not exist.')
- # We won't break here so that all such dirs can be listed.
-
- # Check that there are none missing except those in `avoid_dirs`.
- dir_path = os.path.dirname(index_rst_path)
- actual_dirs = []
-
- for dir_item in os.listdir(dir_path):
- path_bep = os.path.join(dir_path, dir_item)
- if os.path.isdir(path_bep):
- actual_dirs.append(path_bep)
-
- # Are there any missing that are not in `avoid_dirs` list?
- # If so, issue warning about each directory missing.
- for sub_dir in actual_dirs:
- if sub_dir not in sub_dirs:
- if not _in_avoid_dirs_list(sub_dir):
- _warn(f'Dir-order directive in {index_rst_path} is missing dir [{sub_dir}].')
- result = False
-
- return result
-
-
-def _emit_heading(level: int, hdg: str, f: TextIOWrapper):
- """
- Emit reST heading using `header_defs`.
-
- :param level: Directory depth below the top directory in tree. [0-5]
- :param hdg: Heading text
- :param f: Output file (examples.rst)
- :return:
- """
- assert 0 <= level < len(header_defs), "level out of range"
- global _top_level_heading_count
- underline = header_defs[level] * len(hdg)
- announce(__file__, f'Section heading [{hdg}] at level [{level}].')
-
- if level == 0:
- _top_level_heading_count += 1
- if _top_level_heading_count > 1:
- f.write('\n\n\n') # 3 extra lines above (doc) root titles, except 1st
- f.write(underline)
- f.write('\n')
- elif level == 1:
- f.write('\n\n') # 2 extra lines above chapters
- elif level == 2:
- f.write('\n') # 1 extra line above sections
-
- f.write(hdg)
- f.write('\n')
- f.write(underline)
- f.write('\n\n')
-
-
-def _generate_output_from_dir(level: int,
- root_len: int,
- file_or_dir: str,
- is_file: bool,
- orig_dir_list: list[str],
- f: TextIOWrapper) -> list[str]:
- """ Output to `f` based on contents of `file_or_dir`.
-
- :param level: Directory depth below the top directory in tree. [0-4]
- :param root_len: Length of root path -- is deleted from paths so that
- paths are relative to the `lvgl/examples/` dir.
- :param file_or_dir: Path to "thing" being processed.
- :param is_file: True if `file_or_dir` is an `index.rst` file.
- :param orig_dir_list: List of sub-dirs below dir being processed.
- Will be returned from this function if a
- dir-order directive is NOT found in index file.
- :param f: Output file (examples.rst)
- :return: `orig_dir_list` or `dir_order_override` if a
- dir-order directive is found in index file.
- """
- dir_order_override = []
- result_dir_list = orig_dir_list
-
- # It is an error to proceed with `level` out of range. Clamping it to be in
- # range is also an error because it would cause the output to be corrupted with
- # an invalid section-heading underscore, which would generate an error later.
- if 0 <= level and level + 1 < len(header_defs):
- section_heading = _default_section_heading(level, file_or_dir, is_file)
- example_tuples = []
- relative_dir = ''
-
- if is_file:
- announce(__file__, f'Processing file [{file_or_dir}]...')
- # We are processing an index.rst file.
- with open(file_or_dir, 'r', encoding='utf-8') as fidx:
- # It is important that this is NOT fidx.readlines() because
- # it leaves blank lines containing '\n' instead of ''.
- lines = fidx.read().split('\n')
-
- example_title = ''
- prev_line = ''
- dir_path = os.path.dirname(file_or_dir)
- in_dir_order_directive = False
-
- # Accumulate data from `index.rst`. This needs to be done
- # first in case it overrides the default section heading.
- for line in lines:
- if in_dir_order_directive:
- leading_non_blank = (len(line) > 0) and not (line[0] == ' ' or line[0] == '\t')
-
- if leading_non_blank:
- # Leading non-blank character ends dir-order directive.
- in_dir_order_directive = False
- else:
- # Still in dir-order directive.
- stripped_line = line.strip()
- if stripped_line:
- dir_order_override.append(os.path.join(dir_path, stripped_line))
- continue
-
- stripped_line = line.strip()
-
- if not stripped_line:
- continue # Skip blank line.
- elif stripped_line.startswith(EXAMPLE_HEADING_DIRECTIVE):
- new_heading = stripped_line.replace(EXAMPLE_HEADING_DIRECTIVE, '').strip()
- announce(__file__, f'Default section heading [{section_heading}] replaced with [{new_heading}]...')
- section_heading = new_heading
- elif stripped_line.startswith('---'):
- example_title = prev_line
- announce(__file__, f'Found example [{example_title}]...')
- elif stripped_line.startswith(LV_EXAMPLE_DIRECTIVE):
- rel_path_from_examples_dir = stripped_line.replace(LV_EXAMPLE_DIRECTIVE, '').strip()
- announce(__file__, f' [{rel_path_from_examples_dir}]')
- example_tuples.append( (example_title, rel_path_from_examples_dir) )
- elif not in_dir_order_directive and stripped_line.startswith(DIR_ORDER_DIRECTIVE):
- in_dir_order_directive = True
- announce(__file__, f'Found DIR-ORDER directive.')
-
- prev_line = stripped_line
-
- # Output section heading. This occurs even when we have descended into an
- # empty directory with example sub-dirs below it.
- _emit_heading(level, section_heading, f)
-
- # Output examples, if any. Will be empty when we are in an empty directory
- # with example sub-dirs below it.
- for example_tuple in example_tuples:
- example_title = example_tuple[0]
- rel_path_from_examples_dir = example_tuple[1]
- example_hdg_underline = header_defs[level + 1] * len(example_title)
- announce(__file__, f'Writing example [{rel_path_from_examples_dir}]...')
- _emit_heading(level + 1, example_title, f)
- f.write(LV_EXAMPLE_DIRECTIVE + ' ' + rel_path_from_examples_dir)
- f.write('\n\n')
-
- if dir_order_override:
- _validate_sub_dirs(dir_order_override, file_or_dir)
- result_dir_list = dir_order_override
-
- return result_dir_list
-
-
-def process_dir_recursively(level: int, root_len: int, dir_bep: str, f: TextIOWrapper):
- """ Process dir `dir_bep` recursively, avoiding sub-dirs in `avoid_dirs`.
-
- :param level: Directory depth below the top directory in tree.
- :param root_len: Length of root path (gets removed from full path)
- :param dir_bep: Directory *being processed*
- :param f: Output file (examples.rst)
- """
- announce(__file__, f'Processing dir [{dir_bep}]...')
- sub_dirs = []
- idx_files = []
-
- if not os.path.isdir(dir_bep):
- _warn(f'process_dir_recursively: `dir_bep` [{dir_bep}] does not exist.')
- return
-
- # For each "thing" found in `dir_bep`, build lists: sub_dirs and idx_files.
- for dir_item in os.listdir(dir_bep):
- path_bep = os.path.join(dir_bep, dir_item)
- if os.path.isdir(path_bep):
- sub_dirs.append(path_bep)
- elif path_bep.endswith(INDEX_FILENAME):
- idx_files.append(path_bep)
-
- if idx_files:
- sub_dirs = _generate_output_from_dir(level, root_len, idx_files[0], True, sub_dirs, f)
- # `sub_dirs` can be replaced if `index.rst` contains a dir-order directive.
- else:
- _generate_output_from_dir(level, root_len, dir_bep, False, sub_dirs, f)
-
- # Now recursively process sub_dirs.
- for subdir in sub_dirs:
- if not _in_avoid_dirs_list(subdir):
- process_dir_recursively(level + 1, root_len, subdir, f)
-
-
-def exec(intermediate_dir):
- announce_set_silent_mode(not DEBUG_MODE)
- output_path = os.path.join(intermediate_dir, 'examples.rst')
- input_paths = [
- os.path.join('..', 'examples'),
- # os.path.join('..', 'demos')
- ]
-
- with open(output_path, 'w', encoding='utf-8') as f:
- f.write('.. _examples:\n')
- f.write('\n')
-
- # Recursively walk the directories in `input_paths` array for
- # ``index.rst`` files.
- for root_path in input_paths:
- root_len = len(root_path) + 1
- process_dir_recursively(0, root_len, root_path, f)
-
- if MAKE_WARNINGS_INTO_ERRORS and _warning_issued:
- exit(1)
-
-
-if __name__ == '__main__':
- """Make module run-able as well as importable."""
- base_dir = os.path.abspath(os.path.dirname(__file__))
- os.chdir(base_dir)
- exec(sys.argv[1])
diff --git a/docs/flyers/LVGL-Chinese-Flyer.pdf b/docs/flyers/LVGL-Chinese-Flyer.pdf
deleted file mode 100644
index 45862b3879..0000000000
--- a/docs/flyers/LVGL-Chinese-Flyer.pdf
+++ /dev/null
@@ -1,23082 +0,0 @@
-%PDF-1.6
%
-1 0 obj
<>/OCGs[9 0 R]>>/Pages 3 0 R/Type/Catalog>>
endobj
2 0 obj
<>stream
-
-
-
-
- application/pdf
-
-
- LVGL
-
-
- 2022-12-22T10:59:49+08:00
- 2022-12-22T10:59:49+08:00
- 2022-12-22T10:59:49+08:00
- Adobe Illustrator 24.2 (Macintosh)
-
-
-
- 256
- 132
- JPEG
- /9j/4AAQSkZJRgABAgEASABIAAD/7QAsUGhvdG9zaG9wIDMuMAA4QklNA+0AAAAAABAASAAAAAEA
AQBIAAAAAQAB/+4ADkFkb2JlAGTAAAAAAf/bAIQABgQEBAUEBgUFBgkGBQYJCwgGBggLDAoKCwoK
DBAMDAwMDAwQDA4PEA8ODBMTFBQTExwbGxscHx8fHx8fHx8fHwEHBwcNDA0YEBAYGhURFRofHx8f
Hx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8f/8AAEQgAhAEAAwER
AAIRAQMRAf/EAaIAAAAHAQEBAQEAAAAAAAAAAAQFAwIGAQAHCAkKCwEAAgIDAQEBAQEAAAAAAAAA
AQACAwQFBgcICQoLEAACAQMDAgQCBgcDBAIGAnMBAgMRBAAFIRIxQVEGE2EicYEUMpGhBxWxQiPB
UtHhMxZi8CRygvElQzRTkqKyY3PCNUQnk6OzNhdUZHTD0uIIJoMJChgZhJRFRqS0VtNVKBry4/PE
1OT0ZXWFlaW1xdXl9WZ2hpamtsbW5vY3R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo+Ck5SVlpeYmZ
qbnJ2en5KjpKWmp6ipqqusra6voRAAICAQIDBQUEBQYECAMDbQEAAhEDBCESMUEFURNhIgZxgZEy
obHwFMHR4SNCFVJicvEzJDRDghaSUyWiY7LCB3PSNeJEgxdUkwgJChgZJjZFGidkdFU38qOzwygp
0+PzhJSktMTU5PRldYWVpbXF1eX1RlZmdoaWprbG1ub2R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo
+DlJWWl5iZmpucnZ6fkqOkpaanqKmqq6ytrq+v/aAAwDAQACEQMRAD8A9U4q7FXYq7FXYq7FXYqx
Pyf+avkDzleT2XlzVlvru2j9aaAxTwsI+QXmBPHHyAYgHjWlRXrirIZdZ0iE24lvreM3aq9qHlRf
VVpI4lMdT8QMlxGgp+06jqwxVF4q7FXYq7FXYq7FXYq7FXYq7FXYqwrVfzY0DTtev9JktL2WLSmt
4tT1OOOP6rDPdlBbw1eRJJHf1F2jRuNd6CpDaaY2P+cn/wApzGJFurxkMIuKi0l+wbgWw2IBJLmu
3b32wWtLdO/5yf8AyxvUinH6QgsZDMr301tSCMwo0lHKuzcpAh4Kqk+NBvja0mGof85EflfZXdlA
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
-
-
-
- uuid:4e89b697-303c-294a-b8f3-70c5d55ad096
- xmp.did:9470a9f6-758f-44c9-8395-b1c9a3b5ee31
- uuid:5D20892493BFDB11914A8590D31508C8
- proof:pdf
-
- xmp.iid:23c50436-694a-4232-b206-91e82dccbdb0
- xmp.did:23c50436-694a-4232-b206-91e82dccbdb0
- uuid:5D20892493BFDB11914A8590D31508C8
- proof:pdf
-
-
-
-
- saved
- xmp.iid:0fa511da-558c-6c4e-8e2d-71cb7f1f59ac
- 2022-12-15T14:38:35+08:00
- Adobe Illustrator 26.0 (Windows)
- /
-
-
- saved
- xmp.iid:9470a9f6-758f-44c9-8395-b1c9a3b5ee31
- 2022-12-22T10:59:35+08:00
- Adobe Illustrator 24.2 (Macintosh)
- /
-
-
-
-
-
-
- EmbedByReference
-
- /Users/hongliu/Desktop/baise.png
-
-
-
- EmbedByReference
-
- /Users/hongliu/Desktop/摄图网_401728284_全球即时通讯(企业商用).png
-
-
-
- EmbedByReference
-
- logo_lvgl.png
-
-
-
-
-
-
-
- /Users/hongliu/Desktop/baise.png
-
-
- /Users/hongliu/Desktop/摄图网_401728284_全球即时通讯(企业商用).png
-
-
- logo_lvgl.png
-
-
-
- Print
- Adobe Illustrator
- False
- True
- 1
-
- 188.130000
- 248.835000
- Millimeters
-
-
-
-
- SourceHanSansCN-Regular
- 思源黑体 CN
- Regular
- Open Type
- Version 1.000;PS 1;hotconv 1.0.78;makeotf.lib2.5.61930
- False
- 思源黑体SourceHanSansCN-Regular.otf
-
-
- SourceHanSansCN-Medium
- 思源黑体 CN
- Medium
- Open Type
- Version 1.000;PS 1;hotconv 1.0.78;makeotf.lib2.5.61930
- False
- SourceHanSansCN-Medium.otf
-
-
- SourceHanSansCN-Bold
- 思源黑体 CN
- Bold
- Open Type
- Version 1.004;PS 1.004;hotconv 1.0.82;makeotf.lib2.5.63406
- False
- SourceHanSansCN-Bold_0.otf
-
-
-
-
-
- Cyan
- Magenta
- Yellow
- Black
-
-
-
-
-
- 默认色板组
- 0
-
-
-
- 白色
- CMYK
- PROCESS
- 0.000000
- 0.000000
- 0.000000
- 0.000000
-
-
- 黑色
- CMYK
- PROCESS
- 0.000000
- 0.000000
- 0.000000
- 100.000000
-
-
- CMYK 红
- CMYK
- PROCESS
- 0.000000
- 100.000000
- 100.000000
- 0.000000
-
-
- CMYK 黄
- CMYK
- PROCESS
- 0.000000
- 0.000000
- 100.000000
- 0.000000
-
-
- CMYK 绿
- CMYK
- PROCESS
- 100.000000
- 0.000000
- 100.000000
- 0.000000
-
-
- CMYK 青
- CMYK
- PROCESS
- 100.000000
- 0.000000
- 0.000000
- 0.000000
-
-
- CMYK 蓝
- CMYK
- PROCESS
- 100.000000
- 100.000000
- 0.000000
- 0.000000
-
-
- CMYK 洋红
- CMYK
- PROCESS
- 0.000000
- 100.000000
- 0.000000
- 0.000000
-
-
- C=15 M=100 Y=90 K=10
- CMYK
- PROCESS
- 15.000000
- 100.000000
- 90.000000
- 10.000000
-
-
- C=0 M=90 Y=85 K=0
- CMYK
- PROCESS
- 0.000000
- 90.000000
- 85.000000
- 0.000000
-
-
- C=0 M=80 Y=95 K=0
- CMYK
- PROCESS
- 0.000000
- 80.000000
- 95.000000
- 0.000000
-
-
- C=0 M=50 Y=100 K=0
- CMYK
- PROCESS
- 0.000000
- 50.000000
- 100.000000
- 0.000000
-
-
- C=0 M=35 Y=85 K=0
- CMYK
- PROCESS
- 0.000000
- 35.000000
- 85.000000
- 0.000000
-
-
- C=5 M=0 Y=90 K=0
- CMYK
- PROCESS
- 5.000000
- 0.000000
- 90.000000
- 0.000000
-
-
- C=20 M=0 Y=100 K=0
- CMYK
- PROCESS
- 20.000000
- 0.000000
- 100.000000
- 0.000000
-
-
- C=50 M=0 Y=100 K=0
- CMYK
- PROCESS
- 50.000000
- 0.000000
- 100.000000
- 0.000000
-
-
- C=75 M=0 Y=100 K=0
- CMYK
- PROCESS
- 75.000000
- 0.000000
- 100.000000
- 0.000000
-
-
- C=85 M=10 Y=100 K=10
- CMYK
- PROCESS
- 85.000000
- 10.000000
- 100.000000
- 10.000000
-
-
- C=90 M=30 Y=95 K=30
- CMYK
- PROCESS
- 90.000000
- 30.000000
- 95.000000
- 30.000000
-
-
- C=75 M=0 Y=75 K=0
- CMYK
- PROCESS
- 75.000000
- 0.000000
- 75.000000
- 0.000000
-
-
- C=80 M=10 Y=45 K=0
- CMYK
- PROCESS
- 80.000000
- 10.000000
- 45.000000
- 0.000000
-
-
- C=70 M=15 Y=0 K=0
- CMYK
- PROCESS
- 70.000000
- 15.000000
- 0.000000
- 0.000000
-
-
- C=85 M=50 Y=0 K=0
- CMYK
- PROCESS
- 85.000000
- 50.000000
- 0.000000
- 0.000000
-
-
- C=100 M=95 Y=5 K=0
- CMYK
- PROCESS
- 100.000000
- 95.000000
- 5.000000
- 0.000000
-
-
- C=100 M=100 Y=25 K=25
- CMYK
- PROCESS
- 100.000000
- 100.000000
- 25.000000
- 25.000000
-
-
- C=75 M=100 Y=0 K=0
- CMYK
- PROCESS
- 75.000000
- 100.000000
- 0.000000
- 0.000000
-
-
- C=50 M=100 Y=0 K=0
- CMYK
- PROCESS
- 50.000000
- 100.000000
- 0.000000
- 0.000000
-
-
- C=35 M=100 Y=35 K=10
- CMYK
- PROCESS
- 35.000000
- 100.000000
- 35.000000
- 10.000000
-
-
- C=10 M=100 Y=50 K=0
- CMYK
- PROCESS
- 10.000000
- 100.000000
- 50.000000
- 0.000000
-
-
- C=0 M=95 Y=20 K=0
- CMYK
- PROCESS
- 0.000000
- 95.000000
- 20.000000
- 0.000000
-
-
- C=25 M=25 Y=40 K=0
- CMYK
- PROCESS
- 25.000000
- 25.000000
- 40.000000
- 0.000000
-
-
- C=40 M=45 Y=50 K=5
- CMYK
- PROCESS
- 40.000000
- 45.000000
- 50.000000
- 5.000000
-
-
- C=50 M=50 Y=60 K=25
- CMYK
- PROCESS
- 50.000000
- 50.000000
- 60.000000
- 25.000000
-
-
- C=55 M=60 Y=65 K=40
- CMYK
- PROCESS
- 55.000000
- 60.000000
- 65.000000
- 40.000000
-
-
- C=25 M=40 Y=65 K=0
- CMYK
- PROCESS
- 25.000000
- 40.000000
- 65.000000
- 0.000000
-
-
- C=30 M=50 Y=75 K=10
- CMYK
- PROCESS
- 30.000000
- 50.000000
- 75.000000
- 10.000000
-
-
- C=35 M=60 Y=80 K=25
- CMYK
- PROCESS
- 35.000000
- 60.000000
- 80.000000
- 25.000000
-
-
- C=40 M=65 Y=90 K=35
- CMYK
- PROCESS
- 40.000000
- 65.000000
- 90.000000
- 35.000000
-
-
- C=40 M=70 Y=100 K=50
- CMYK
- PROCESS
- 40.000000
- 70.000000
- 100.000000
- 50.000000
-
-
- C=50 M=70 Y=80 K=70
- CMYK
- PROCESS
- 50.000000
- 70.000000
- 80.000000
- 70.000000
-
-
-
-
-
- 灰色
- 1
-
-
-
- C=0 M=0 Y=0 K=100
- CMYK
- PROCESS
- 0.000000
- 0.000000
- 0.000000
- 100.000000
-
-
- C=0 M=0 Y=0 K=90
- CMYK
- PROCESS
- 0.000000
- 0.000000
- 0.000000
- 89.999400
-
-
- C=0 M=0 Y=0 K=80
- CMYK
- PROCESS
- 0.000000
- 0.000000
- 0.000000
- 79.998800
-
-
- C=0 M=0 Y=0 K=70
- CMYK
- PROCESS
- 0.000000
- 0.000000
- 0.000000
- 69.999700
-
-
- C=0 M=0 Y=0 K=60
- CMYK
- PROCESS
- 0.000000
- 0.000000
- 0.000000
- 59.999100
-
-
- C=0 M=0 Y=0 K=50
- CMYK
- PROCESS
- 0.000000
- 0.000000
- 0.000000
- 50.000000
-
-
- C=0 M=0 Y=0 K=40
- CMYK
- PROCESS
- 0.000000
- 0.000000
- 0.000000
- 39.999400
-
-
- C=0 M=0 Y=0 K=30
- CMYK
- PROCESS
- 0.000000
- 0.000000
- 0.000000
- 29.998800
-
-
- C=0 M=0 Y=0 K=20
- CMYK
- PROCESS
- 0.000000
- 0.000000
- 0.000000
- 19.999700
-
-
- C=0 M=0 Y=0 K=10
- CMYK
- PROCESS
- 0.000000
- 0.000000
- 0.000000
- 9.999100
-
-
- C=0 M=0 Y=0 K=5
- CMYK
- PROCESS
- 0.000000
- 0.000000
- 0.000000
- 4.998800
-
-
-
-
-
- 明亮
- 1
-
-
-
- C=0 M=100 Y=100 K=0
- CMYK
- PROCESS
- 0.000000
- 100.000000
- 100.000000
- 0.000000
-
-
- C=0 M=75 Y=100 K=0
- CMYK
- PROCESS
- 0.000000
- 75.000000
- 100.000000
- 0.000000
-
-
- C=0 M=10 Y=95 K=0
- CMYK
- PROCESS
- 0.000000
- 10.000000
- 95.000000
- 0.000000
-
-
- C=85 M=10 Y=100 K=0
- CMYK
- PROCESS
- 85.000000
- 10.000000
- 100.000000
- 0.000000
-
-
- C=100 M=90 Y=0 K=0
- CMYK
- PROCESS
- 100.000000
- 90.000000
- 0.000000
- 0.000000
-
-
- C=60 M=90 Y=0 K=0
- CMYK
- PROCESS
- 60.000000
- 90.000000
- 0.003100
- 0.003100
-
-
-
-
-
-
- Adobe PDF library 15.00
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
endstream
endobj
3 0 obj
<>
endobj
5 0 obj
<>/Resources<>/ExtGState<>/Font<>/ProcSet[/PDF/Text/ImageC/ImageI]/Properties<>/XObject<>>>/Thumb 33 0 R/TrimBox[8.50394 8.50394 541.786 713.863]/Type/Page>>
endobj
11 0 obj
<>/Resources<>/Font<>/ProcSet[/PDF/Text]/Properties<>/XObject<>>>/Thumb 44 0 R/TrimBox[8.50394 8.50394 541.786 713.863]/Type/Page>>
endobj
34 0 obj
<>stream
-HWm8"`viRb'HЍtN݃c)ݝ9q;NrꩧM:ai8~rzw߾?݇Oo~lj}!pƌGDq~`V#ᮏ7_+ߋQ-F7Ng~81b/GJj)U~ta8[&?I)㔢iʎٲUJ=?34fp %3)E&,vTgZtA(7Cs#+~{KgB@ٮV (=K oe810a[.X`|r(c'2-R>L3k|^Ôr9q~Lsri־?NgŮ,vx ω}n=\
M6W?C'YKdN<