pdf-io 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
pdf_io/__init__.py ADDED
@@ -0,0 +1,144 @@
1
+ """pdf_io: create PDF files with the Python standard library only.
2
+
3
+ Basic usage::
4
+
5
+ from pdf_io import svg_to_pdf, convert_file
6
+
7
+ pdf_bytes = svg_to_pdf('<svg xmlns="http://www.w3.org/2000/svg" ...>...</svg>')
8
+ convert_file("drawing.svg", "drawing.pdf")
9
+ """
10
+
11
+ import os
12
+ from collections.abc import Iterable
13
+ from typing import IO, Any
14
+
15
+ from .canvas import Canvas, Document, LinearGradient, Paint, Pattern, RadialGradient
16
+ from .colormaps import Colormap
17
+ from .colors import CMYK, Color, Spot
18
+ from .document import SVGDocument, SVGParseError
19
+ from .fontdb import FontDB
20
+ from .geometry import Matrix, Path
21
+ from .renderer import render
22
+ from .textlayout import Content, Span, TextLayout
23
+
24
+ __all__ = ["svg_to_pdf", "svg_to_png", "convert_file", "convert", "SVGParseError",
25
+ "SVGDocument",
26
+ "Document", "Canvas", "LinearGradient", "RadialGradient", "Pattern",
27
+ "Colormap", "CMYK", "Spot", "Matrix", "Path", "Span", "TextLayout",
28
+ "Color", "Paint", "Content", "__version__"]
29
+
30
+ __version__ = "0.1.0"
31
+
32
+
33
+ def svg_to_pdf(svg: str | bytes, base_dir: str | os.PathLike[str] | None = None,
34
+ compress: bool = True, font_dirs: Iterable[str | os.PathLike[str]] | None = None,
35
+ system_fonts: bool = True, filters: bool = False,
36
+ jpeg_quality: int | None = None, pdf_a: str | None = None) -> bytes:
37
+ """Convert SVG source to PDF.
38
+
39
+ :param svg: SVG document as ``str`` or ``bytes`` (gzip-compressed SVGZ
40
+ data is accepted too).
41
+ :param base_dir: directory used to resolve relative file references
42
+ (``<image href="photo.png">``). When ``None`` only ``data:`` URIs
43
+ are loaded. Otherwise any local file the process can read may be
44
+ embedded (``../`` paths, absolute paths, ``file:`` URLs), so use
45
+ ``None`` for untrusted input.
46
+ :param compress: compress PDF streams with zlib.
47
+ :param font_dirs: extra directories searched for ``.ttf``/``.otf``/
48
+ ``.ttc``/``.woff`` fonts (searched before the system directories).
49
+ :param system_fonts: also use fonts installed on the system. Disable
50
+ for output that does not depend on the machine.
51
+ :param filters: apply SVG filter effects (off by default). Filtered
52
+ elements are rasterized, which requires numpy. A
53
+ ``data-filters="true|false"`` attribute on an element (inherited by
54
+ its descendants) overrides this setting.
55
+ :param jpeg_quality: store rasterized content (``data-rasterize``,
56
+ filters) as JPEG of this quality (1-100) instead of lossless. A
57
+ ``data-jpeg-quality`` attribute overrides it per element.
58
+ :param pdf_a: ``"2b"``, ``"2u"`` or ``"3b"`` to write a PDF/A file
59
+ (see :class:`Document`).
60
+ :returns: the PDF file as ``bytes``.
61
+ :raises SVGParseError: if the input is not a valid SVG document.
62
+ """
63
+ document = SVGDocument(svg)
64
+ font_db = FontDB(font_dirs=font_dirs, system_fonts=system_fonts)
65
+ return render(document, base_dir=base_dir, compress=compress, font_db=font_db,
66
+ filters=filters, jpeg_quality=jpeg_quality, pdf_a=pdf_a)
67
+
68
+
69
+ def svg_to_png(svg: str | bytes, dpi: float = 96, background: Any = None,
70
+ base_dir: str | os.PathLike[str] | None = None,
71
+ font_dirs: Iterable[str | os.PathLike[str]] | None = None,
72
+ system_fonts: bool = True, filters: bool = False) -> bytes:
73
+ """Render SVG source to PNG bytes (requires numpy).
74
+
75
+ ``dpi`` 96 gives one pixel per CSS pixel. ``background`` is a color, or
76
+ ``None`` for a transparent image. ``filters`` enables SVG filter
77
+ effects (see :func:`svg_to_pdf`).
78
+ """
79
+ from .canvas import Document as _Document
80
+ from .renderer import Renderer, svg_size
81
+
82
+ document = SVGDocument(svg)
83
+ doc = _Document(font_db=FontDB(font_dirs=font_dirs, system_fonts=system_fonts),
84
+ base_dir=base_dir, filters=filters)
85
+ width, height = svg_size(document)
86
+ page = doc.add_page(width, height, unit="px")
87
+ Renderer(document, page, base_dir=base_dir).draw()
88
+ return page.to_png(dpi, background)
89
+
90
+
91
+ def convert_file(input_path: str | os.PathLike[str],
92
+ output_path: str | os.PathLike[str] | None = None, compress: bool = True,
93
+ **options: Any) -> str | os.PathLike[str]:
94
+ """Convert the SVG file ``input_path`` and write the PDF.
95
+
96
+ ``output_path`` defaults to ``input_path`` with a ``.pdf`` extension.
97
+ Returns the output path. Extra keyword ``options`` (``base_dir``,
98
+ ``font_dirs``, ``system_fonts``, ``filters``, ``jpeg_quality``,
99
+ ``pdf_a``) are passed to :func:`svg_to_pdf`; ``base_dir`` defaults to the
100
+ directory of ``input_path``.
101
+ """
102
+ with open(input_path, "rb") as fh:
103
+ data = fh.read()
104
+ if output_path is None:
105
+ root, _ = os.path.splitext(input_path)
106
+ output_path = root + ".pdf"
107
+ options.setdefault("base_dir", os.path.dirname(os.path.abspath(input_path)))
108
+ pdf = svg_to_pdf(data, compress=compress, **options)
109
+ with open(output_path, "wb") as fh:
110
+ fh.write(pdf)
111
+ return output_path
112
+
113
+
114
+ def convert(source: str | bytes | os.PathLike[str] | IO[bytes],
115
+ output: str | os.PathLike[str] | IO[bytes] | None = None,
116
+ base_dir: str | os.PathLike[str] | None = None, compress: bool = True,
117
+ **options: Any) -> bytes:
118
+ """Flexible conversion helper.
119
+
120
+ ``source`` may be SVG text/bytes, a path to an SVG file, or a binary
121
+ file-like object. ``output`` may be ``None`` (return bytes), a path, or
122
+ a binary file-like object. The PDF bytes are always returned. Extra
123
+ keyword ``options`` are passed to :func:`svg_to_pdf`.
124
+ """
125
+ if hasattr(source, "read"):
126
+ data = source.read()
127
+ elif isinstance(source, (bytes, bytearray)):
128
+ data = bytes(source)
129
+ elif isinstance(source, str) and source.lstrip()[:1] == "<":
130
+ data = source
131
+ else:
132
+ path = os.fspath(source)
133
+ with open(path, "rb") as fh:
134
+ data = fh.read()
135
+ if base_dir is None:
136
+ base_dir = os.path.dirname(os.path.abspath(path))
137
+ pdf = svg_to_pdf(data, base_dir=base_dir, compress=compress, **options)
138
+ if output is not None:
139
+ if hasattr(output, "write"):
140
+ output.write(pdf)
141
+ else:
142
+ with open(os.fspath(output), "wb") as fh:
143
+ fh.write(pdf)
144
+ return pdf
pdf_io/__main__.py ADDED
@@ -0,0 +1,96 @@
1
+ """Command line interface: ``python -m pdf_io input.svg [output.pdf]``."""
2
+
3
+ import argparse
4
+ import os
5
+ import sys
6
+
7
+ from . import SVGParseError, __version__, svg_to_pdf, svg_to_png
8
+ from .raster import RasterizationError
9
+
10
+
11
+ def main(argv=None):
12
+ parser = argparse.ArgumentParser(
13
+ prog="pdf_io", description="Convert an SVG file to PDF (or PNG).")
14
+ parser.add_argument("input", help="input SVG file, or '-' for stdin")
15
+ parser.add_argument("output", nargs="?",
16
+ help="output PDF file ('-' for stdout); defaults to the "
17
+ "input name with a .pdf extension")
18
+ parser.add_argument("--no-compress", action="store_true",
19
+ help="write uncompressed content streams")
20
+ parser.add_argument("--font-dir", action="append", default=[], metavar="DIR",
21
+ help="extra directory to search for fonts (repeatable)")
22
+ parser.add_argument("--no-system-fonts", action="store_true",
23
+ help="do not use fonts installed on the system")
24
+ parser.add_argument("--filters", action="store_true",
25
+ help="apply SVG filter effects (rasterized; requires numpy)")
26
+ parser.add_argument("--jpeg-quality", type=int, default=None, metavar="N",
27
+ help="store rasterized content as JPEG of quality N (1-100)")
28
+ parser.add_argument("--pdf-a", choices=["2b", "2u", "3b"], default=None,
29
+ help="write a PDF/A file of this conformance level")
30
+ parser.add_argument("--png", action="store_true",
31
+ help="write a PNG image instead of a PDF (requires numpy)")
32
+ parser.add_argument("--dpi", type=float, default=96,
33
+ help="resolution of --png output (default 96: 1px per CSS px)")
34
+ parser.add_argument("--version", action="version", version="%(prog)s " + __version__)
35
+ args = parser.parse_args(argv)
36
+ if not args.dpi > 0:
37
+ parser.error("--dpi must be positive")
38
+ if args.png:
39
+ pdf_only = [flag for flag, used in (("--pdf-a", args.pdf_a is not None),
40
+ ("--jpeg-quality", args.jpeg_quality is not None),
41
+ ("--no-compress", args.no_compress)) if used]
42
+ if pdf_only:
43
+ parser.error(f"{', '.join(pdf_only)} cannot be used with --png")
44
+
45
+ try:
46
+ if args.input == "-":
47
+ data = sys.stdin.buffer.read()
48
+ base_dir = os.getcwd()
49
+ else:
50
+ with open(args.input, "rb") as fh:
51
+ data = fh.read()
52
+ base_dir = os.path.dirname(os.path.abspath(args.input))
53
+ except OSError as exc:
54
+ parser.exit(1, f"pdf_io: cannot read input: {exc}\n")
55
+
56
+ try:
57
+ if args.png:
58
+ pdf = svg_to_png(data, dpi=args.dpi, base_dir=base_dir, font_dirs=args.font_dir,
59
+ system_fonts=not args.no_system_fonts, filters=args.filters)
60
+ else:
61
+ pdf = svg_to_pdf(data, base_dir=base_dir, compress=not args.no_compress,
62
+ font_dirs=args.font_dir, system_fonts=not args.no_system_fonts,
63
+ filters=args.filters, jpeg_quality=args.jpeg_quality,
64
+ pdf_a=args.pdf_a)
65
+ except (SVGParseError, RasterizationError, ValueError) as exc:
66
+ parser.exit(1, f"pdf_io: {exc}\n")
67
+
68
+ output = args.output
69
+ if output is None:
70
+ if args.input == "-":
71
+ output = "-"
72
+ else:
73
+ output = os.path.splitext(args.input)[0] + (".png" if args.png else ".pdf")
74
+ if output == "-":
75
+ if sys.stdout is None: # file descriptor 1 is closed
76
+ parser.exit(1, "pdf_io: cannot write output: standard output is closed\n")
77
+ try:
78
+ sys.stdout.buffer.write(pdf)
79
+ sys.stdout.buffer.flush()
80
+ except BrokenPipeError:
81
+ # The reader went away; keep Python's exit flush from failing too.
82
+ os.dup2(os.open(os.devnull, os.O_WRONLY), sys.stdout.fileno())
83
+ return 1
84
+ except OSError as exc:
85
+ parser.exit(1, f"pdf_io: cannot write output: {exc}\n")
86
+ else:
87
+ try:
88
+ with open(output, "wb") as fh:
89
+ fh.write(pdf)
90
+ except OSError as exc:
91
+ parser.exit(1, f"pdf_io: cannot write output: {exc}\n")
92
+ return 0
93
+
94
+
95
+ if __name__ == "__main__":
96
+ sys.exit(main())