[PATCH v2 5/7] doc: make pdfdocs: Update kfigure sphinx extension to support inkscape as well

Adriano Carvalho adrianocarvalho.pt at gmail.com
Mon Oct 6 00:10:12 CEST 2025


As it is right now, kfigure relies on ImageMagick's convert. It,
however, fails to convert some SVG files (see below). With this change,
inkscape support is added. If present, inkscape is preferred over
ImageMagick's convert.

With this change kfigure.py is an exact copy of the Linux kernel's at
v6.17. I reviewed all the changes and, it seems to me that, only one
change may cause trouble: logging uses a slightly different approach
(logger v. kernellog).

inkscape is also added as a dependency. Dockerfile updated as well.

Without this change, this is what I get with
docker.io/trini/u-boot-gitlab-ci-runner:jammy-20250714-25Jul2025:

[...]
convert: unrecognized color `context-stroke' @ warning/color.c/GetColorCompliance/1057.
convert: non-conforming drawing primitive definition `stroke' @ error/draw.c/RenderMVGContent/4414.
WARNING: Error #1 when calling: /usr/bin/convert /home/uboot/u-boot/doc/develop/pics/spl_before_reloc.svg /home/uboot/u-boot/doc/output/latex/spl_before_reloc.pdf
convert: unrecognized color `context-stroke' @ warning/color.c/GetColorCompliance/1057.
convert: non-conforming drawing primitive definition `stroke' @ error/draw.c/RenderMVGContent/4414.
WARNING: Error #1 when calling: /usr/bin/convert /home/uboot/u-boot/doc/develop/pics/spl_after_reloc.svg /home/uboot/u-boot/doc/output/latex/spl_after_reloc.pdf
convert: non-conforming drawing primitive definition `Liberation' @ error/draw.c/RenderMVGContent/4414.
WARNING: Error #1 when calling: /usr/bin/convert /home/uboot/u-boot/doc/board/ti/img/ospi_sysfw2.svg /home/uboot/u-boot/doc/output/latex/ospi_sysfw2.pdf
convert: non-conforming drawing primitive definition `Liberation' @ error/draw.c/RenderMVGContent/4414.
WARNING: Error #1 when calling: /usr/bin/convert /home/uboot/u-boot/doc/board/ti/img/ospi_sysfw-am64.svg /home/uboot/u-boot/doc/output/latex/ospi_sysfw-am64.pdf
convert: non-conforming drawing primitive definition `Liberation' @ error/draw.c/RenderMVGContent/4414.
WARNING: Error #1 when calling: /usr/bin/convert /home/uboot/u-boot/doc/board/ti/img/emmc_am65x_evm_boot0.svg /home/uboot/u-boot/doc/output/latex/emmc_am65x_evm_boot0.pdf
convert: non-conforming drawing primitive definition `Liberation' @ error/draw.c/RenderMVGContent/4414.
WARNING: Error #1 when calling: /usr/bin/convert /home/uboot/u-boot/doc/board/ti/img/ospi_sysfw.svg /home/uboot/u-boot/doc/output/latex/ospi_sysfw.pdf
convert: non-conforming drawing primitive definition `Liberation' @ error/draw.c/RenderMVGContent/4414.
WARNING: Error #1 when calling: /usr/bin/convert /home/uboot/u-boot/doc/board/ti/img/emmc_j7200_evm_boot01.svg /home/uboot/u-boot/doc/output/latex/emmc_j7200_evm_boot01.pdf
convert: non-conforming drawing primitive definition `Liberation' @ error/draw.c/RenderMVGContent/4414.
WARNING: Error #1 when calling: /usr/bin/convert /home/uboot/u-boot/doc/board/ti/img/emmc_j7200_evm_udafs.svg /home/uboot/u-boot/doc/output/latex/emmc_j7200_evm_udafs.pdf
convert: non-conforming drawing primitive definition `Liberation' @ error/draw.c/RenderMVGContent/4414.
WARNING: Error #1 when calling: /usr/bin/convert /home/uboot/u-boot/doc/board/ti/img/ospi_sysfw3.svg /home/uboot/u-boot/doc/output/latex/ospi_sysfw3.pdf
[...]
build finished with problems, 9 warnings (with warnings treated as errors).
make[1]: *** [doc/Makefile:84: latexdocs] Error 1
make: *** [Makefile:2687: pdfdocs] Error 2

To get the above the following line in /etc/ImageMagick-6/policy.xml
needs to be deleted or commented out:

<policy domain="coder" rights="none" pattern="PDF" />

Otherwise, we'll get:

convert: attempt to perform an operation not allowed by the security policy `PDF' @ error/constitute.c/IsCoderAuthorized/426.

inkscape has the added advantage of this not being necessary.

NOTE: This won't completely fix the pdfdocs target. See next commit(s).

Signed-off-by: Adriano Carvalho <adrianocarvalho.pt at gmail.com>
---
 doc/build/documentation.rst |   2 +
 doc/sphinx/kfigure.py       | 212 ++++++++++++++++++++++++++----------
 tools/docker/Dockerfile     |   1 +
 3 files changed, 158 insertions(+), 57 deletions(-)

diff --git a/doc/build/documentation.rst b/doc/build/documentation.rst
index 483bdc42227..1342c71addd 100644
--- a/doc/build/documentation.rst
+++ b/doc/build/documentation.rst
@@ -18,6 +18,8 @@ the following dependencies are needed to build the documentation:
 
 * texlive-xetex (if building the `PDF documentation`_ through the *pdfdocs* target)
 
+* inkscape (if building the `PDF documentation`_ through the *pdfdocs* target)
+
 When submitting patches for documentation always build with KDOC_WERROR=1 to
 treat warnings as errors.
 
diff --git a/doc/sphinx/kfigure.py b/doc/sphinx/kfigure.py
index dea7f91ef5a..ad495c0da27 100644
--- a/doc/sphinx/kfigure.py
+++ b/doc/sphinx/kfigure.py
@@ -1,6 +1,7 @@
 # -*- coding: utf-8; mode: python -*-
+# SPDX-License-Identifier: GPL-2.0
 # pylint: disable=C0103, R0903, R0912, R0915
-u"""
+"""
     scalable figure and image handling
     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
@@ -31,10 +32,13 @@ u"""
 
     * ``dot(1)``: Graphviz (https://www.graphviz.org). If Graphviz is not
       available, the DOT language is inserted as literal-block.
+      For conversion to PDF, ``rsvg-convert(1)`` of librsvg
+      (https://gitlab.gnome.org/GNOME/librsvg) is used when available.
 
     * SVG to PDF: To generate PDF, you need at least one of this tools:
 
       - ``convert(1)``: ImageMagick (https://www.imagemagick.org)
+      - ``inkscape(1)``: Inkscape (https://inkscape.org/)
 
     List of customizations:
 
@@ -49,36 +53,21 @@ import os
 from os import path
 import subprocess
 from hashlib import sha1
-import sys
-
+import re
 from docutils import nodes
 from docutils.statemachine import ViewList
 from docutils.parsers.rst import directives
 from docutils.parsers.rst.directives import images
 import sphinx
-
 from sphinx.util.nodes import clean_astext
-from six import iteritems
-
-import kernellog
-
-PY3 = sys.version_info[0] == 3
+from sphinx.util import logging
 
-if PY3:
-    _unicode = str
-else:
-    _unicode = unicode
-
-# Get Sphinx version
-major, minor, patch = sphinx.version_info[:3]
-if major == 1 and minor > 3:
-    # patches.Figure only landed in Sphinx 1.4
-    from sphinx.directives.patches import Figure  # pylint: disable=C0413
-else:
-    Figure = images.Figure
+Figure = images.Figure
 
 __version__  = '1.0.0'
 
+logger = logging.getLogger('kfigure')
+
 # simple helper
 # -------------
 
@@ -121,10 +110,20 @@ def pass_handle(self, node):           # pylint: disable=W0613
 
 # Graphviz's dot(1) support
 dot_cmd = None
+# dot(1) -Tpdf should be used
+dot_Tpdf = False
 
 # ImageMagick' convert(1) support
 convert_cmd = None
 
+# librsvg's rsvg-convert(1) support
+rsvg_convert_cmd = None
+
+# Inkscape's inkscape(1) support
+inkscape_cmd = None
+# Inkscape prior to 1.0 uses different command options
+inkscape_ver_one = False
+
 
 def setup(app):
     # check toolchain first
@@ -167,28 +166,69 @@ def setup(app):
 
 
 def setupTools(app):
-    u"""
+    """
     Check available build tools and log some *verbose* messages.
 
     This function is called once, when the builder is initiated.
     """
-    global dot_cmd, convert_cmd   # pylint: disable=W0603
-    kernellog.verbose(app, "kfigure: check installed tools ...")
+    global dot_cmd, dot_Tpdf, convert_cmd, rsvg_convert_cmd   # pylint: disable=W0603
+    global inkscape_cmd, inkscape_ver_one  # pylint: disable=W0603
+    logger.verbose("kfigure: check installed tools ...")
 
     dot_cmd = which('dot')
     convert_cmd = which('convert')
+    rsvg_convert_cmd = which('rsvg-convert')
+    inkscape_cmd = which('inkscape')
 
     if dot_cmd:
-        kernellog.verbose(app, "use dot(1) from: " + dot_cmd)
+        logger.verbose("use dot(1) from: " + dot_cmd)
+
+        try:
+            dot_Thelp_list = subprocess.check_output([dot_cmd, '-Thelp'],
+                                    stderr=subprocess.STDOUT)
+        except subprocess.CalledProcessError as err:
+            dot_Thelp_list = err.output
+            pass
+
+        dot_Tpdf_ptn = b'pdf'
+        dot_Tpdf = re.search(dot_Tpdf_ptn, dot_Thelp_list)
     else:
-        kernellog.warn(app, "dot(1) not found, for better output quality install "
-                       "graphviz from https://www.graphviz.org")
-    if convert_cmd:
-        kernellog.verbose(app, "use convert(1) from: " + convert_cmd)
+        logger.warning(
+            "dot(1) not found, for better output quality install graphviz from https://www.graphviz.org"
+        )
+    if inkscape_cmd:
+        logger.verbose("use inkscape(1) from: " + inkscape_cmd)
+        inkscape_ver = subprocess.check_output([inkscape_cmd, '--version'],
+                                               stderr=subprocess.DEVNULL)
+        ver_one_ptn = b'Inkscape 1'
+        inkscape_ver_one = re.search(ver_one_ptn, inkscape_ver)
+        convert_cmd = None
+        rsvg_convert_cmd = None
+        dot_Tpdf = False
+
     else:
-        kernellog.warn(app,
-            "convert(1) not found, for SVG to PDF conversion install "
-            "ImageMagick (https://www.imagemagick.org)")
+        if convert_cmd:
+            logger.verbose("use convert(1) from: " + convert_cmd)
+        else:
+            logger.verbose(
+                "Neither inkscape(1) nor convert(1) found.\n"
+                "For SVG to PDF conversion, install either Inkscape (https://inkscape.org/) (preferred) or\n"
+                "ImageMagick (https://www.imagemagick.org)"
+            )
+
+        if rsvg_convert_cmd:
+            logger.verbose("use rsvg-convert(1) from: " + rsvg_convert_cmd)
+            logger.verbose("use 'dot -Tsvg' and rsvg-convert(1) for DOT -> PDF conversion")
+            dot_Tpdf = False
+        else:
+            logger.verbose(
+                "rsvg-convert(1) not found.\n"
+                "  SVG rendering of convert(1) is done by ImageMagick-native renderer."
+            )
+            if dot_Tpdf:
+                logger.verbose("use 'dot -Tpdf' for DOT -> PDF conversion")
+            else:
+                logger.verbose("use 'dot -Tsvg' and convert(1) for DOT -> PDF conversion")
 
 
 # integrate conversion tools
@@ -222,13 +262,12 @@ def convert_image(img_node, translator, src_fname=None):
 
     # in kernel builds, use 'make SPHINXOPTS=-v' to see verbose messages
 
-    kernellog.verbose(app, 'assert best format for: ' + img_node['uri'])
+    logger.verbose('assert best format for: ' + img_node['uri'])
 
     if in_ext == '.dot':
 
         if not dot_cmd:
-            kernellog.verbose(app,
-                              "dot from graphviz not available / include DOT raw.")
+            logger.verbose("dot from graphviz not available / include DOT raw.")
             img_node.replace_self(file2literal(src_fname))
 
         elif translator.builder.format == 'latex':
@@ -254,9 +293,12 @@ def convert_image(img_node, translator, src_fname=None):
     elif in_ext == '.svg':
 
         if translator.builder.format == 'latex':
-            if convert_cmd is None:
-                kernellog.verbose(app,
-                                  "no SVG to PDF conversion available / include SVG raw.")
+            if not inkscape_cmd and convert_cmd is None:
+                logger.warning(
+                    "no SVG to PDF conversion available / include SVG raw.\n"
+                    "Including large raw SVGs can cause xelatex error.\n"
+                    "Install Inkscape (preferred) or ImageMagick."
+                )
                 img_node.replace_self(file2literal(src_fname))
             else:
                 dst_fname = path.join(translator.builder.outdir, fname + '.pdf')
@@ -269,19 +311,25 @@ def convert_image(img_node, translator, src_fname=None):
         _name = dst_fname[len(str(translator.builder.outdir)) + 1:]
 
         if isNewer(dst_fname, src_fname):
-            kernellog.verbose(app,
-                              "convert: {out}/%s already exists and is newer" % _name)
+            logger.verbose("convert: {out}/%s already exists and is newer" % _name)
 
         else:
             ok = False
             mkdir(path.dirname(dst_fname))
 
             if in_ext == '.dot':
-                kernellog.verbose(app, 'convert DOT to: {out}/' + _name)
-                ok = dot2format(app, src_fname, dst_fname)
+                logger.verbose('convert DOT to: {out}/' + _name)
+                if translator.builder.format == 'latex' and not dot_Tpdf:
+                    svg_fname = path.join(translator.builder.outdir, fname + '.svg')
+                    ok1 = dot2format(app, src_fname, svg_fname)
+                    ok2 = svg2pdf_by_rsvg(app, svg_fname, dst_fname)
+                    ok = ok1 and ok2
+
+                else:
+                    ok = dot2format(app, src_fname, dst_fname)
 
             elif in_ext == '.svg':
-                kernellog.verbose(app, 'convert SVG to: {out}/' + _name)
+                logger.verbose('convert SVG to: {out}/' + _name)
                 ok = svg2pdf(app, src_fname, dst_fname)
 
             if not ok:
@@ -310,27 +358,77 @@ def dot2format(app, dot_fname, out_fname):
     with open(out_fname, "w") as out:
         exit_code = subprocess.call(cmd, stdout = out)
         if exit_code != 0:
-            kernellog.warn(app,
+            logger.warning(
                           "Error #%d when calling: %s" % (exit_code, " ".join(cmd)))
     return bool(exit_code == 0)
 
 def svg2pdf(app, svg_fname, pdf_fname):
-    """Converts SVG to PDF with ``convert(1)`` command.
+    """Converts SVG to PDF with ``inkscape(1)`` or ``convert(1)`` command.
 
-    Uses ``convert(1)`` from ImageMagick (https://www.imagemagick.org) for
-    conversion.  Returns ``True`` on success and ``False`` if an error occurred.
+    Uses ``inkscape(1)`` from Inkscape (https://inkscape.org/) or ``convert(1)``
+    from ImageMagick (https://www.imagemagick.org) for conversion.
+    Returns ``True`` on success and ``False`` if an error occurred.
 
     * ``svg_fname`` pathname of the input SVG file with extension (``.svg``)
     * ``pdf_name``  pathname of the output PDF file with extension (``.pdf``)
 
     """
     cmd = [convert_cmd, svg_fname, pdf_fname]
-    # use stdout and stderr from parent
-    exit_code = subprocess.call(cmd)
+    cmd_name = 'convert(1)'
+
+    if inkscape_cmd:
+        cmd_name = 'inkscape(1)'
+        if inkscape_ver_one:
+            cmd = [inkscape_cmd, '-o', pdf_fname, svg_fname]
+        else:
+            cmd = [inkscape_cmd, '-z', '--export-pdf=%s' % pdf_fname, svg_fname]
+
+    try:
+        warning_msg = subprocess.check_output(cmd, stderr=subprocess.STDOUT)
+        exit_code = 0
+    except subprocess.CalledProcessError as err:
+        warning_msg = err.output
+        exit_code = err.returncode
+        pass
+
     if exit_code != 0:
-        kernellog.warn(app, "Error #%d when calling: %s" % (exit_code, " ".join(cmd)))
+        logger.warning("Error #%d when calling: %s" %
+                            (exit_code, " ".join(cmd)))
+        if warning_msg:
+            logger.warning( "Warning msg from %s: %s" %
+                                (cmd_name, str(warning_msg, 'utf-8')))
+    elif warning_msg:
+        logger.verbose("Warning msg from %s (likely harmless):\n%s" %
+                            (cmd_name, str(warning_msg, 'utf-8')))
+
     return bool(exit_code == 0)
 
+def svg2pdf_by_rsvg(app, svg_fname, pdf_fname):
+    """Convert SVG to PDF with ``rsvg-convert(1)`` command.
+
+    * ``svg_fname`` pathname of input SVG file, including extension ``.svg``
+    * ``pdf_fname`` pathname of output PDF file, including extension ``.pdf``
+
+    Input SVG file should be the one generated by ``dot2format()``.
+    SVG -> PDF conversion is done by ``rsvg-convert(1)``.
+
+    If ``rsvg-convert(1)`` is unavailable, fall back to ``svg2pdf()``.
+
+    """
+
+    if rsvg_convert_cmd is None:
+        ok = svg2pdf(app, svg_fname, pdf_fname)
+    else:
+        cmd = [rsvg_convert_cmd, '--format=pdf', '-o', pdf_fname, svg_fname]
+        # use stdout and stderr from parent
+        exit_code = subprocess.call(cmd)
+        if exit_code != 0:
+            logger.warning("Error #%d when calling: %s" %
+                                (exit_code, " ".join(cmd)))
+        ok = bool(exit_code == 0)
+
+    return ok
+
 
 # image handling
 # ---------------------
@@ -348,7 +446,7 @@ class kernel_image(nodes.image):
     pass
 
 class KernelImage(images.Image):
-    u"""KernelImage directive
+    """KernelImage directive
 
     Earns everything from ``.. image::`` directive, except *remote URI* and
     *glob* pattern. The KernelImage wraps a image node into a
@@ -384,7 +482,7 @@ class kernel_figure(nodes.figure):
     """Node for ``kernel-figure`` directive."""
 
 class KernelFigure(Figure):
-    u"""KernelImage directive
+    """KernelImage directive
 
     Earns everything from ``.. figure::`` directive, except *remote URI* and
     *glob* pattern.  The KernelFigure wraps a figure node into a kernel_figure
@@ -421,15 +519,15 @@ def visit_kernel_render(self, node):
     app = self.builder.app
     srclang = node.get('srclang')
 
-    kernellog.verbose(app, 'visit kernel-render node lang: "%s"' % (srclang))
+    logger.verbose('visit kernel-render node lang: "%s"' % srclang)
 
     tmp_ext = RENDER_MARKUP_EXT.get(srclang, None)
     if tmp_ext is None:
-        kernellog.warn(app, 'kernel-render: "%s" unknown / include raw.' % (srclang))
+        logger.warning( 'kernel-render: "%s" unknown / include raw.' % srclang)
         return
 
     if not dot_cmd and tmp_ext == '.dot':
-        kernellog.verbose(app, "dot from graphviz not available / include raw.")
+        logger.verbose("dot from graphviz not available / include raw.")
         return
 
     literal_block = node[0]
@@ -460,7 +558,7 @@ class kernel_render(nodes.General, nodes.Inline, nodes.Element):
     pass
 
 class KernelRender(Figure):
-    u"""KernelRender directive
+    """KernelRender directive
 
     Render content by external tool.  Has all the options known from the
     *figure*  directive, plus option ``caption``.  If ``caption`` has a
@@ -540,7 +638,7 @@ def add_kernel_figure_to_std_domain(app, doctree):
     docname = app.env.docname
     labels = std.data["labels"]
 
-    for name, explicit in iteritems(doctree.nametypes):
+    for name, explicit in doctree.nametypes.items():
         if not explicit:
             continue
         labelid = doctree.nameids[name]
diff --git a/tools/docker/Dockerfile b/tools/docker/Dockerfile
index 8e166eda4bc..50d71f5bf90 100644
--- a/tools/docker/Dockerfile
+++ b/tools/docker/Dockerfile
@@ -90,6 +90,7 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
 	help2man \
 	iasl \
 	imagemagick \
+	inkscape \
 	inetutils-telnet \
 	iputils-ping \
 	libconfuse-dev \
-- 
2.34.1



More information about the U-Boot mailing list