Merge branch 'pdf-conversion' into docs-next

PDF-generation improvements from Akira Yokasawa; Akira says:

This patch set improves conversions of DOT -> PDF and SVG -> PDF
for PDF docs.

* DOT -> PDF conversion

Current scheme uses "dot -Tpdf" (of graphviz).

Cons:
  - openSUSE's dot(1) does not support -Tpdf.
  - Other distro's dot(1) generates PDFs with unnecessarily wide
    margins for inclusion into LaTeX docs.

Patch 1/4 changes the route to the following two steps:

  1. DOT -> SVG by "dot -Tsvg"
  2. SVG -> PDF by "rsvg-convert -f pdf" with fallback to convert(1)

Pros:
  - Improved portability across distros
  - Less space around graphs in final PDF documents

Con:
  - On systems without rsvg-convert, generated PDF will be of raster
    image.

Patch 2/4 avoids raster-image PDF by using "dot -Tpdf" on systems where
the option is available.

* SVG -> PDF conversion

Current scheme uses convert(1) (of ImageMagick)

Cons:
  - Generated PDFs are of raster image.  Some of them look blurry.
  - Raster images tend to be large in size.
  - convert(1) delegates SVG decoding to rsvg-convert(1).
    It doesn't cover full range of Inkscape-specific SVG features
    and fails to convert some of SVG figures properly.

Improper conversions are observed with SVGs listed below (incomplete,
conversion quality depends on the version of rsvg-convert):
  - Documentation/userspace-api/media/v4l/selection.svg
  - Documentation/userspace-api/media/v4l/vbi_525.svg
  - Documentation/userspace-api/media/v4l/vbi_625.svg
  - Documentation/userspace-api/media/v4l/vbi_hsync.svg
  - Documentation/admin-guide/blockdev/drbd/DRBD-8.3-data-packets.svg
  - Documentation/admin-guide/blockdev/drbd/DRBD-data-packages.svg

If you have Inkscape installed as well, convert(1) delegates SVG
decoding to inkscape(1) rather than to rsvg-convert(1) and SVGs listed
above can be rendered properly.

So if Inkscape is required for converting those SVGs properly, why not
use it directly in the first place?

Patches 3/4 and 4/4 add code to utilize inkscape(1) for SVG -> PDF
conversion when it is available.  They don't modify any existing
requirements for kernel-doc.

Patch 3/4 adds the alternative route of SVG -> PDF conversion by
inkscape(1).
Patch 4/4 delegates warning messages from inkscape(1) to kernellog.verbose
as they are likely harmless in command-line uses.

Pros:
  - Generated PDFs are of vector graphics.
  - Vector graphics tends to be smaller in size and looks nicer when
    zoomed in.
  - SVGs drawn by Inkscape are fully supported.

On systems without Inkscape, no regression is expected by these two
patches.
This commit is contained in:
Jonathan Corbet 2022-02-09 17:29:05 -07:00
commit f647de4b02
1 changed files with 121 additions and 13 deletions

View File

@ -31,10 +31,13 @@ u"""
* ``dot(1)``: Graphviz (https://www.graphviz.org). If Graphviz is not * ``dot(1)``: Graphviz (https://www.graphviz.org). If Graphviz is not
available, the DOT language is inserted as literal-block. 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: * SVG to PDF: To generate PDF, you need at least one of this tools:
- ``convert(1)``: ImageMagick (https://www.imagemagick.org) - ``convert(1)``: ImageMagick (https://www.imagemagick.org)
- ``inkscape(1)``: Inkscape (https://inkscape.org/)
List of customizations: List of customizations:
@ -49,6 +52,7 @@ import os
from os import path from os import path
import subprocess import subprocess
from hashlib import sha1 from hashlib import sha1
import re
from docutils import nodes from docutils import nodes
from docutils.statemachine import ViewList from docutils.statemachine import ViewList
from docutils.parsers.rst import directives from docutils.parsers.rst import directives
@ -109,10 +113,20 @@ def pass_handle(self, node): # pylint: disable=W0613
# Graphviz's dot(1) support # Graphviz's dot(1) support
dot_cmd = None dot_cmd = None
# dot(1) -Tpdf should be used
dot_Tpdf = False
# ImageMagick' convert(1) support # ImageMagick' convert(1) support
convert_cmd = None 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): def setup(app):
# check toolchain first # check toolchain first
@ -160,23 +174,62 @@ def setupTools(app):
This function is called once, when the builder is initiated. This function is called once, when the builder is initiated.
""" """
global dot_cmd, convert_cmd # pylint: disable=W0603 global dot_cmd, dot_Tpdf, convert_cmd, rsvg_convert_cmd # pylint: disable=W0603
global inkscape_cmd, inkscape_ver_one # pylint: disable=W0603
kernellog.verbose(app, "kfigure: check installed tools ...") kernellog.verbose(app, "kfigure: check installed tools ...")
dot_cmd = which('dot') dot_cmd = which('dot')
convert_cmd = which('convert') convert_cmd = which('convert')
rsvg_convert_cmd = which('rsvg-convert')
inkscape_cmd = which('inkscape')
if dot_cmd: if dot_cmd:
kernellog.verbose(app, "use dot(1) from: " + dot_cmd) kernellog.verbose(app, "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: else:
kernellog.warn(app, "dot(1) not found, for better output quality install " kernellog.warn(app, "dot(1) not found, for better output quality install "
"graphviz from https://www.graphviz.org") "graphviz from https://www.graphviz.org")
if convert_cmd: if inkscape_cmd:
kernellog.verbose(app, "use convert(1) from: " + convert_cmd) kernellog.verbose(app, "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: else:
kernellog.warn(app, if convert_cmd:
"convert(1) not found, for SVG to PDF conversion install " kernellog.verbose(app, "use convert(1) from: " + convert_cmd)
"ImageMagick (https://www.imagemagick.org)") else:
kernellog.warn(app,
"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:
kernellog.verbose(app, "use rsvg-convert(1) from: " + rsvg_convert_cmd)
kernellog.verbose(app, "use 'dot -Tsvg' and rsvg-convert(1) for DOT -> PDF conversion")
dot_Tpdf = False
else:
kernellog.verbose(app,
"rsvg-convert(1) not found.\n"
" SVG rendering of convert(1) is done by ImageMagick-native renderer.")
if dot_Tpdf:
kernellog.verbose(app, "use 'dot -Tpdf' for DOT -> PDF conversion")
else:
kernellog.verbose(app, "use 'dot -Tsvg' and convert(1) for DOT -> PDF conversion")
# integrate conversion tools # integrate conversion tools
@ -242,7 +295,7 @@ def convert_image(img_node, translator, src_fname=None):
elif in_ext == '.svg': elif in_ext == '.svg':
if translator.builder.format == 'latex': if translator.builder.format == 'latex':
if convert_cmd is None: if not inkscape_cmd and convert_cmd is None:
kernellog.verbose(app, kernellog.verbose(app,
"no SVG to PDF conversion available / include SVG raw.") "no SVG to PDF conversion available / include SVG raw.")
img_node.replace_self(file2literal(src_fname)) img_node.replace_self(file2literal(src_fname))
@ -266,7 +319,14 @@ def convert_image(img_node, translator, src_fname=None):
if in_ext == '.dot': if in_ext == '.dot':
kernellog.verbose(app, 'convert DOT to: {out}/' + _name) kernellog.verbose(app, 'convert DOT to: {out}/' + _name)
ok = dot2format(app, src_fname, dst_fname) 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': elif in_ext == '.svg':
kernellog.verbose(app, 'convert SVG to: {out}/' + _name) kernellog.verbose(app, 'convert SVG to: {out}/' + _name)
@ -303,22 +363,70 @@ def dot2format(app, dot_fname, out_fname):
return bool(exit_code == 0) return bool(exit_code == 0)
def svg2pdf(app, svg_fname, pdf_fname): 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 Uses ``inkscape(1)`` from Inkscape (https://inkscape.org/) or ``convert(1)``
conversion. Returns ``True`` on success and ``False`` if an error occurred. 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``) * ``svg_fname`` pathname of the input SVG file with extension (``.svg``)
* ``pdf_name`` pathname of the output PDF file with extension (``.pdf``) * ``pdf_name`` pathname of the output PDF file with extension (``.pdf``)
""" """
cmd = [convert_cmd, svg_fname, pdf_fname] cmd = [convert_cmd, svg_fname, pdf_fname]
# use stdout and stderr from parent cmd_name = 'convert(1)'
exit_code = subprocess.call(cmd)
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: if exit_code != 0:
kernellog.warn(app, "Error #%d when calling: %s" % (exit_code, " ".join(cmd))) kernellog.warn(app, "Error #%d when calling: %s" % (exit_code, " ".join(cmd)))
if warning_msg:
kernellog.warn(app, "Warning msg from %s: %s"
% (cmd_name, str(warning_msg, 'utf-8')))
elif warning_msg:
kernellog.verbose(app, "Warning msg from %s (likely harmless):\n%s"
% (cmd_name, str(warning_msg, 'utf-8')))
return bool(exit_code == 0) 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:
kernellog.warn(app, "Error #%d when calling: %s" % (exit_code, " ".join(cmd)))
ok = bool(exit_code == 0)
return ok
# image handling # image handling
# --------------------- # ---------------------