Coverage for gws-app/gws/lib/pdf/__init__.py: 70%
53 statements
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-05 13:35 +0200
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-05 13:35 +0200
1"""PDF utilities.
3Functions to combine PDF files and convert them to images, used mainly by the printer.
4Combining is done with ``pypdf``, conversion to images with Ghostscript (``gs``).
6- ``overlay`` merges the pages of one PDF on top of the pages of another,
7- ``concat`` joins several PDFs into one,
8- ``page_count`` returns the number of pages,
9- ``to_image_path`` renders a page as PNG or JPEG.
11Example::
13 import gws.lib.pdf
15 gws.lib.pdf.overlay('/tmp/map.pdf', '/tmp/frame.pdf', '/tmp/page.pdf')
16 gws.lib.pdf.concat(['/tmp/page1.pdf', '/tmp/page2.pdf'], '/tmp/all.pdf')
17 gws.lib.pdf.to_image_path('/tmp/all.pdf', '/tmp/preview.png', (400, 300))
18"""
20import pypdf
21import gws.lib.mime
22import gws.lib.osx
23import gws.lib.image
26def overlay(a_path: str, b_path: str, out_path: str) -> str:
27 """Overlay two PDFs page by page.
29 Each page of ``b`` is placed on top of the page with the same number in ``a``.
30 The output has as many pages as ``a``; pages of ``a`` without a counterpart in ``b`` are copied unchanged.
32 Args:
33 a_path: Path to the bottom PDF.
34 b_path: Path to the top PDF.
35 out_path: Path to the output PDF.
37 Returns:
38 Path to the output PDF.
39 """
41 fa = open(a_path, 'rb')
42 fb = open(b_path, 'rb')
44 ra = pypdf.PdfReader(fa)
45 rb = pypdf.PdfReader(fb)
47 w = pypdf.PdfWriter()
49 for n, page in enumerate(ra.pages):
50 other = None
51 try:
52 other = rb.pages[n]
53 except IndexError:
54 pass
55 if other:
56 # https://github.com/py-pdf/pypdf/issues/2139
57 page.transfer_rotation_to_content()
58 page.merge_page(other)
59 w.add_page(page)
61 with open(out_path, 'wb') as out_fp:
62 w.write(out_fp)
64 fa.close()
65 fb.close()
67 return out_path
70def concat(paths: list[str], out_path: str) -> str:
71 """Concatenate multiple PDFs into one.
73 If only one path is given, nothing is written and that path is returned.
75 Args:
76 paths: Paths to the PDFs.
77 out_path: Path to the output PDF.
79 Returns:
80 Path to the concatenated PDF.
81 """
83 # only one path given - just return it
84 if len(paths) == 1:
85 return paths[0]
87 # NB: readers must be kept around until the writer is done
89 files = [open(p, 'rb') for p in paths]
90 readers = [pypdf.PdfReader(fp) for fp in files]
92 w = pypdf.PdfWriter()
94 for r in readers:
95 w.append_pages_from_reader(r)
97 with open(out_path, 'wb') as out_fp:
98 w.write(out_fp)
100 for fp in files:
101 fp.close()
103 return out_path
106def page_count(path: str) -> int:
107 """Return the number of pages in a PDF.
109 Args:
110 path: Path to the PDF.
112 Returns:
113 The number of pages.
114 """
116 with open(path, 'rb') as fp:
117 r = pypdf.PdfReader(fp)
118 return len(r.pages)
121def to_image_path(
122 in_path: str,
123 out_path: str,
124 size: gws.Size,
125 mime_type: str = gws.lib.mime.PNG,
126 page: int = 1,
127) -> str:
128 """Render a PDF page as an image, using Ghostscript.
130 The page is scaled to fit the given size.
132 Args:
133 in_path: Path to the input PDF.
134 out_path: Path to the output image.
135 size: Size of the output image in points.
136 mime_type: Mime type of the output image, either PNG or JPEG.
137 page: Page number to convert, starting with 1.
139 Returns:
140 Path to the output image.
142 Raises:
143 ``ValueError``: If the mime type is not supported.
144 ``gws.lib.osx.Error``: If Ghostscript fails.
145 """
147 if mime_type == gws.lib.mime.PNG:
148 device = 'png16m'
149 elif mime_type == gws.lib.mime.JPEG:
150 device = 'jpeg'
151 else:
152 raise ValueError(f'invalid mime type {mime_type!r}')
154 w, h = size
155 cmd = [
156 'gs',
157 '-q',
158 f'-dNOPAUSE',
159 f'-dBATCH',
160 f'-dFirstPage={page}',
161 f'-dLastPage={page}',
162 f'-dDEVICEWIDTHPOINTS={w}',
163 f'-dDEVICEHEIGHTPOINTS={h}',
164 f'-dPDFFitPage=true',
165 f'-sDEVICE={device}',
166 f'-dTextAlphaBits=4',
167 f'-dGraphicsAlphaBits=4',
168 f'-sOutputFile={out_path}',
169 f'{in_path}',
170 ]
172 gws.log.debug(' '.join(cmd))
173 gws.lib.osx.run(cmd)
175 return out_path