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
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,6 +52,7 @@ import os
from os import path
import subprocess
from hashlib import sha1
import re
from docutils import nodes
from docutils.statemachine import ViewList
from docutils.parsers.rst import directives
@ -109,10 +113,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
@ -160,23 +174,62 @@ def setupTools(app):
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 ...")
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)
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)
if inkscape_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:
kernellog.warn(app,
"convert(1) not found, for SVG to PDF conversion install "
"ImageMagick (https://www.imagemagick.org)")
if convert_cmd:
kernellog.verbose(app, "use convert(1) from: " + convert_cmd)
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
@ -242,7 +295,7 @@ def convert_image(img_node, translator, src_fname=None):
elif in_ext == '.svg':
if translator.builder.format == 'latex':
if convert_cmd is None:
if not inkscape_cmd and convert_cmd is None:
kernellog.verbose(app,
"no SVG to PDF conversion available / include SVG raw.")
img_node.replace_self(file2literal(src_fname))
@ -266,7 +319,14 @@ def convert_image(img_node, translator, src_fname=None):
if in_ext == '.dot':
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':
kernellog.verbose(app, 'convert SVG to: {out}/' + _name)
@ -303,22 +363,70 @@ def dot2format(app, dot_fname, out_fname):
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)))
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)
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
# ---------------------