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 +144 -0
- pdf_io/__main__.py +96 -0
- pdf_io/canvas.py +2659 -0
- pdf_io/cff.py +608 -0
- pdf_io/colormaps.py +264 -0
- pdf_io/colors.py +354 -0
- pdf_io/css.py +355 -0
- pdf_io/document.py +341 -0
- pdf_io/draw.py +1277 -0
- pdf_io/filters.py +338 -0
- pdf_io/fontdb.py +257 -0
- pdf_io/fonts.py +412 -0
- pdf_io/geometry.py +735 -0
- pdf_io/heatmap.py +440 -0
- pdf_io/images.py +329 -0
- pdf_io/jpeg.py +667 -0
- pdf_io/lines.py +458 -0
- pdf_io/markers.py +491 -0
- pdf_io/opentype.py +279 -0
- pdf_io/pdf.py +255 -0
- pdf_io/pdfa.py +163 -0
- pdf_io/png.py +111 -0
- pdf_io/py.typed +0 -0
- pdf_io/raster.py +1858 -0
- pdf_io/rasterfonts.py +243 -0
- pdf_io/rasterlines.py +410 -0
- pdf_io/renderer.py +1958 -0
- pdf_io/sfnt.py +702 -0
- pdf_io/textlayout.py +756 -0
- pdf_io/units.py +57 -0
- pdf_io-0.1.0.dist-info/METADATA +142 -0
- pdf_io-0.1.0.dist-info/RECORD +35 -0
- pdf_io-0.1.0.dist-info/WHEEL +4 -0
- pdf_io-0.1.0.dist-info/entry_points.txt +2 -0
- pdf_io-0.1.0.dist-info/licenses/LICENSE +28 -0
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())
|