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

1"""PDF utilities. 

2 

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``). 

5 

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. 

10 

11Example:: 

12 

13 import gws.lib.pdf 

14 

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""" 

19 

20import pypdf 

21import gws.lib.mime 

22import gws.lib.osx 

23import gws.lib.image 

24 

25 

26def overlay(a_path: str, b_path: str, out_path: str) -> str: 

27 """Overlay two PDFs page by page. 

28 

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. 

31 

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. 

36 

37 Returns: 

38 Path to the output PDF. 

39 """ 

40 

41 fa = open(a_path, 'rb') 

42 fb = open(b_path, 'rb') 

43 

44 ra = pypdf.PdfReader(fa) 

45 rb = pypdf.PdfReader(fb) 

46 

47 w = pypdf.PdfWriter() 

48 

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) 

60 

61 with open(out_path, 'wb') as out_fp: 

62 w.write(out_fp) 

63 

64 fa.close() 

65 fb.close() 

66 

67 return out_path 

68 

69 

70def concat(paths: list[str], out_path: str) -> str: 

71 """Concatenate multiple PDFs into one. 

72 

73 If only one path is given, nothing is written and that path is returned. 

74 

75 Args: 

76 paths: Paths to the PDFs. 

77 out_path: Path to the output PDF. 

78 

79 Returns: 

80 Path to the concatenated PDF. 

81 """ 

82 

83 # only one path given - just return it 

84 if len(paths) == 1: 

85 return paths[0] 

86 

87 # NB: readers must be kept around until the writer is done 

88 

89 files = [open(p, 'rb') for p in paths] 

90 readers = [pypdf.PdfReader(fp) for fp in files] 

91 

92 w = pypdf.PdfWriter() 

93 

94 for r in readers: 

95 w.append_pages_from_reader(r) 

96 

97 with open(out_path, 'wb') as out_fp: 

98 w.write(out_fp) 

99 

100 for fp in files: 

101 fp.close() 

102 

103 return out_path 

104 

105 

106def page_count(path: str) -> int: 

107 """Return the number of pages in a PDF. 

108 

109 Args: 

110 path: Path to the PDF. 

111 

112 Returns: 

113 The number of pages. 

114 """ 

115 

116 with open(path, 'rb') as fp: 

117 r = pypdf.PdfReader(fp) 

118 return len(r.pages) 

119 

120 

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. 

129 

130 The page is scaled to fit the given size. 

131 

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. 

138 

139 Returns: 

140 Path to the output image. 

141 

142 Raises: 

143 ``ValueError``: If the mime type is not supported. 

144 ``gws.lib.osx.Error``: If Ghostscript fails. 

145 """ 

146 

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}') 

153 

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 ] 

171 

172 gws.log.debug(' '.join(cmd)) 

173 gws.lib.osx.run(cmd) 

174 

175 return out_path