PyMuPDF 1.24.11__cp38-abi3-win_amd64.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.
Files changed (99) hide show
  1. PyMuPDF-1.24.11.dist-info/COPYING +1 -0
  2. PyMuPDF-1.24.11.dist-info/METADATA +83 -0
  3. PyMuPDF-1.24.11.dist-info/README.md +60 -0
  4. PyMuPDF-1.24.11.dist-info/RECORD +99 -0
  5. PyMuPDF-1.24.11.dist-info/WHEEL +4 -0
  6. PyMuPDF-1.24.11.dist-info/entry_points.txt +3 -0
  7. fitz/__init__.py +11 -0
  8. fitz/table.py +2 -0
  9. fitz/utils.py +2 -0
  10. pymupdf/__init__.py +22122 -0
  11. pymupdf/__main__.py +1146 -0
  12. pymupdf/_apply_pages.py +253 -0
  13. pymupdf/_build.py +1 -0
  14. pymupdf/_extra.pyd +0 -0
  15. pymupdf/_mupdf.pyd +0 -0
  16. pymupdf/extra.py +214 -0
  17. pymupdf/mupdf-devel/include/mupdf/fitz/archive.h +444 -0
  18. pymupdf/mupdf-devel/include/mupdf/fitz/band-writer.h +117 -0
  19. pymupdf/mupdf-devel/include/mupdf/fitz/bidi.h +90 -0
  20. pymupdf/mupdf-devel/include/mupdf/fitz/bitmap.h +175 -0
  21. pymupdf/mupdf-devel/include/mupdf/fitz/buffer.h +250 -0
  22. pymupdf/mupdf-devel/include/mupdf/fitz/color.h +427 -0
  23. pymupdf/mupdf-devel/include/mupdf/fitz/compress.h +88 -0
  24. pymupdf/mupdf-devel/include/mupdf/fitz/compressed-buffer.h +185 -0
  25. pymupdf/mupdf-devel/include/mupdf/fitz/config.h +223 -0
  26. pymupdf/mupdf-devel/include/mupdf/fitz/context.h +1006 -0
  27. pymupdf/mupdf-devel/include/mupdf/fitz/crypt.h +270 -0
  28. pymupdf/mupdf-devel/include/mupdf/fitz/device.h +654 -0
  29. pymupdf/mupdf-devel/include/mupdf/fitz/display-list.h +142 -0
  30. pymupdf/mupdf-devel/include/mupdf/fitz/document.h +1108 -0
  31. pymupdf/mupdf-devel/include/mupdf/fitz/export.h +52 -0
  32. pymupdf/mupdf-devel/include/mupdf/fitz/filter.h +263 -0
  33. pymupdf/mupdf-devel/include/mupdf/fitz/font.h +815 -0
  34. pymupdf/mupdf-devel/include/mupdf/fitz/geometry.h +818 -0
  35. pymupdf/mupdf-devel/include/mupdf/fitz/getopt.h +128 -0
  36. pymupdf/mupdf-devel/include/mupdf/fitz/glyph-cache.h +96 -0
  37. pymupdf/mupdf-devel/include/mupdf/fitz/glyph.h +81 -0
  38. pymupdf/mupdf-devel/include/mupdf/fitz/hash.h +126 -0
  39. pymupdf/mupdf-devel/include/mupdf/fitz/heap-imp.h +163 -0
  40. pymupdf/mupdf-devel/include/mupdf/fitz/heap.h +140 -0
  41. pymupdf/mupdf-devel/include/mupdf/fitz/image.h +443 -0
  42. pymupdf/mupdf-devel/include/mupdf/fitz/link.h +130 -0
  43. pymupdf/mupdf-devel/include/mupdf/fitz/log.h +61 -0
  44. pymupdf/mupdf-devel/include/mupdf/fitz/outline.h +228 -0
  45. pymupdf/mupdf-devel/include/mupdf/fitz/output-svg.h +64 -0
  46. pymupdf/mupdf-devel/include/mupdf/fitz/output.h +415 -0
  47. pymupdf/mupdf-devel/include/mupdf/fitz/path.h +447 -0
  48. pymupdf/mupdf-devel/include/mupdf/fitz/pixmap.h +501 -0
  49. pymupdf/mupdf-devel/include/mupdf/fitz/pool.h +68 -0
  50. pymupdf/mupdf-devel/include/mupdf/fitz/separation.h +138 -0
  51. pymupdf/mupdf-devel/include/mupdf/fitz/shade.h +233 -0
  52. pymupdf/mupdf-devel/include/mupdf/fitz/store.h +456 -0
  53. pymupdf/mupdf-devel/include/mupdf/fitz/story-writer.h +209 -0
  54. pymupdf/mupdf-devel/include/mupdf/fitz/story.h +232 -0
  55. pymupdf/mupdf-devel/include/mupdf/fitz/stream.h +646 -0
  56. pymupdf/mupdf-devel/include/mupdf/fitz/string-util.h +286 -0
  57. pymupdf/mupdf-devel/include/mupdf/fitz/structured-text.h +365 -0
  58. pymupdf/mupdf-devel/include/mupdf/fitz/system.h +459 -0
  59. pymupdf/mupdf-devel/include/mupdf/fitz/text.h +209 -0
  60. pymupdf/mupdf-devel/include/mupdf/fitz/track-usage.h +57 -0
  61. pymupdf/mupdf-devel/include/mupdf/fitz/transition.h +76 -0
  62. pymupdf/mupdf-devel/include/mupdf/fitz/tree.h +62 -0
  63. pymupdf/mupdf-devel/include/mupdf/fitz/types.h +41 -0
  64. pymupdf/mupdf-devel/include/mupdf/fitz/util.h +151 -0
  65. pymupdf/mupdf-devel/include/mupdf/fitz/version.h +31 -0
  66. pymupdf/mupdf-devel/include/mupdf/fitz/write-pixmap.h +499 -0
  67. pymupdf/mupdf-devel/include/mupdf/fitz/writer.h +266 -0
  68. pymupdf/mupdf-devel/include/mupdf/fitz/xml.h +397 -0
  69. pymupdf/mupdf-devel/include/mupdf/fitz.h +97 -0
  70. pymupdf/mupdf-devel/include/mupdf/helpers/mu-office-lib.h +666 -0
  71. pymupdf/mupdf-devel/include/mupdf/helpers/mu-threads.h +280 -0
  72. pymupdf/mupdf-devel/include/mupdf/helpers/pkcs7-openssl.h +48 -0
  73. pymupdf/mupdf-devel/include/mupdf/html.h +53 -0
  74. pymupdf/mupdf-devel/include/mupdf/memento.h +423 -0
  75. pymupdf/mupdf-devel/include/mupdf/pdf/annot.h +965 -0
  76. pymupdf/mupdf-devel/include/mupdf/pdf/clean.h +48 -0
  77. pymupdf/mupdf-devel/include/mupdf/pdf/cmap.h +145 -0
  78. pymupdf/mupdf-devel/include/mupdf/pdf/crypt.h +107 -0
  79. pymupdf/mupdf-devel/include/mupdf/pdf/document.h +842 -0
  80. pymupdf/mupdf-devel/include/mupdf/pdf/event.h +167 -0
  81. pymupdf/mupdf-devel/include/mupdf/pdf/font.h +168 -0
  82. pymupdf/mupdf-devel/include/mupdf/pdf/form.h +380 -0
  83. pymupdf/mupdf-devel/include/mupdf/pdf/image-rewriter.h +78 -0
  84. pymupdf/mupdf-devel/include/mupdf/pdf/interpret.h +492 -0
  85. pymupdf/mupdf-devel/include/mupdf/pdf/javascript.h +43 -0
  86. pymupdf/mupdf-devel/include/mupdf/pdf/name-table.h +596 -0
  87. pymupdf/mupdf-devel/include/mupdf/pdf/object.h +435 -0
  88. pymupdf/mupdf-devel/include/mupdf/pdf/page.h +316 -0
  89. pymupdf/mupdf-devel/include/mupdf/pdf/parse.h +61 -0
  90. pymupdf/mupdf-devel/include/mupdf/pdf/resource.h +133 -0
  91. pymupdf/mupdf-devel/include/mupdf/pdf/xref.h +300 -0
  92. pymupdf/mupdf-devel/include/mupdf/pdf.h +57 -0
  93. pymupdf/mupdf-devel/include/mupdf/ucdn.h +452 -0
  94. pymupdf/mupdf-devel/lib/mupdfcpp64.lib +0 -0
  95. pymupdf/mupdf.py +58506 -0
  96. pymupdf/mupdfcpp64.dll +0 -0
  97. pymupdf/pymupdf.py +2 -0
  98. pymupdf/table.py +2355 -0
  99. pymupdf/utils.py +5880 -0
pymupdf/__main__.py ADDED
@@ -0,0 +1,1146 @@
1
+ # -----------------------------------------------------------------------------
2
+ # Copyright 2020-2022, Harald Lieder, mailto:harald.lieder@outlook.com
3
+ # License: GNU AFFERO GPL 3.0, https://www.gnu.org/licenses/agpl-3.0.html
4
+ # Part of "PyMuPDF", Python bindings for "MuPDF" (http://mupdf.com), a
5
+ # lightweight PDF, XPS, and E-book viewer, renderer and toolkit which is
6
+ # maintained and developed by Artifex Software, Inc. https://artifex.com.
7
+ # -----------------------------------------------------------------------------
8
+ import argparse
9
+ import bisect
10
+ import os
11
+ import sys
12
+ import statistics
13
+ from typing import Dict, List, Set
14
+
15
+ from . import pymupdf
16
+
17
+ def mycenter(x):
18
+ return (" %s " % x).center(75, "-")
19
+
20
+
21
+ def recoverpix(doc, item):
22
+ """Return image for a given XREF."""
23
+ x = item[0] # xref of PDF image
24
+ s = item[1] # xref of its /SMask
25
+ if s == 0: # no smask: use direct image output
26
+ return doc.extract_image(x)
27
+
28
+ def getimage(pix):
29
+ if pix.colorspace.n != 4:
30
+ return pix
31
+ tpix = pymupdf.Pixmap(pymupdf.csRGB, pix)
32
+ return tpix
33
+
34
+ # we need to reconstruct the alpha channel with the smask
35
+ pix1 = pymupdf.Pixmap(doc, x)
36
+ pix2 = pymupdf.Pixmap(doc, s) # create pixmap of the /SMask entry
37
+
38
+ """Sanity check:
39
+ - both pixmaps must have the same rectangle
40
+ - both pixmaps must have alpha=0
41
+ - pix2 must consist of 1 byte per pixel
42
+ """
43
+ if not (pix1.irect == pix2.irect and pix1.alpha == pix2.alpha == 0 and pix2.n == 1):
44
+ pymupdf.message("Warning: unsupported /SMask %i for %i:" % (s, x))
45
+ pymupdf.message(pix2)
46
+ pix2 = None
47
+ return getimage(pix1) # return the pixmap as is
48
+
49
+ pix = pymupdf.Pixmap(pix1) # copy of pix1, with an alpha channel added
50
+ pix.set_alpha(pix2.samples) # treat pix2.samples as the alpha values
51
+ pix1 = pix2 = None # free temp pixmaps
52
+
53
+ # we may need to adjust something for CMYK pixmaps here:
54
+ return getimage(pix)
55
+
56
+
57
+ def open_file(filename, password, show=False, pdf=True):
58
+ """Open and authenticate a document."""
59
+ doc = pymupdf.open(filename)
60
+ if not doc.is_pdf and pdf is True:
61
+ sys.exit("this command supports PDF files only")
62
+ rc = -1
63
+ if not doc.needs_pass:
64
+ return doc
65
+ if password:
66
+ rc = doc.authenticate(password)
67
+ if not rc:
68
+ sys.exit("authentication unsuccessful")
69
+ if show is True:
70
+ pymupdf.message("authenticated as %s" % "owner" if rc > 2 else "user")
71
+ else:
72
+ sys.exit("'%s' requires a password" % doc.name)
73
+ return doc
74
+
75
+
76
+ def print_dict(item):
77
+ """Print a Python dictionary."""
78
+ l = max([len(k) for k in item.keys()]) + 1
79
+ for k, v in item.items():
80
+ msg = "%s: %s" % (k.rjust(l), v)
81
+ pymupdf.message(msg)
82
+
83
+
84
+ def print_xref(doc, xref):
85
+ """Print an object given by XREF number.
86
+
87
+ Simulate the PDF source in "pretty" format.
88
+ For a stream also print its size.
89
+ """
90
+ pymupdf.message("%i 0 obj" % xref)
91
+ xref_str = doc.xref_object(xref)
92
+ pymupdf.message(xref_str)
93
+ if doc.xref_is_stream(xref):
94
+ temp = xref_str.split()
95
+ try:
96
+ idx = temp.index("/Length") + 1
97
+ size = temp[idx]
98
+ if size.endswith("0 R"):
99
+ size = "unknown"
100
+ except Exception:
101
+ size = "unknown"
102
+ pymupdf.message("stream\n...%s bytes" % size)
103
+ pymupdf.message("endstream")
104
+ pymupdf.message("endobj")
105
+
106
+
107
+ def get_list(rlist, limit, what="page"):
108
+ """Transform a page / xref specification into a list of integers.
109
+
110
+ Args
111
+ ----
112
+ rlist: (str) the specification
113
+ limit: maximum number, i.e. number of pages, number of objects
114
+ what: a string to be used in error messages
115
+ Returns
116
+ -------
117
+ A list of integers representing the specification.
118
+ """
119
+ N = str(limit - 1)
120
+ rlist = rlist.replace("N", N).replace(" ", "")
121
+ rlist_arr = rlist.split(",")
122
+ out_list = []
123
+ for seq, item in enumerate(rlist_arr):
124
+ n = seq + 1
125
+ if item.isdecimal(): # a single integer
126
+ i = int(item)
127
+ if 1 <= i < limit:
128
+ out_list.append(int(item))
129
+ else:
130
+ sys.exit("bad %s specification at item %i" % (what, n))
131
+ continue
132
+ try: # this must be a range now, and all of the following must work:
133
+ i1, i2 = item.split("-") # will fail if not 2 items produced
134
+ i1 = int(i1) # will fail on non-integers
135
+ i2 = int(i2)
136
+ except Exception:
137
+ sys.exit("bad %s range specification at item %i" % (what, n))
138
+
139
+ if not (1 <= i1 < limit and 1 <= i2 < limit):
140
+ sys.exit("bad %s range specification at item %i" % (what, n))
141
+
142
+ if i1 == i2: # just in case: a range of equal numbers
143
+ out_list.append(i1)
144
+ continue
145
+
146
+ if i1 < i2: # first less than second
147
+ out_list += list(range(i1, i2 + 1))
148
+ else: # first larger than second
149
+ out_list += list(range(i1, i2 - 1, -1))
150
+
151
+ return out_list
152
+
153
+
154
+ def show(args):
155
+ doc = open_file(args.input, args.password, True)
156
+ size = os.path.getsize(args.input) / 1024
157
+ flag = "KB"
158
+ if size > 1000:
159
+ size /= 1024
160
+ flag = "MB"
161
+ size = round(size, 1)
162
+ meta = doc.metadata # pylint: disable=no-member
163
+ pymupdf.message(
164
+ "'%s', pages: %i, objects: %i, %g %s, %s, encryption: %s"
165
+ % (
166
+ args.input,
167
+ doc.page_count,
168
+ doc.xref_length() - 1,
169
+ size,
170
+ flag,
171
+ meta["format"],
172
+ meta["encryption"],
173
+ )
174
+ )
175
+ n = doc.is_form_pdf
176
+ if n > 0:
177
+ s = doc.get_sigflags()
178
+ pymupdf.message(
179
+ "document contains %i root form fields and is %ssigned"
180
+ % (n, "not " if s != 3 else "")
181
+ )
182
+ n = doc.embfile_count()
183
+ if n > 0:
184
+ pymupdf.message("document contains %i embedded files" % n)
185
+ pymupdf.message()
186
+ if args.catalog:
187
+ pymupdf.message(mycenter("PDF catalog"))
188
+ xref = doc.pdf_catalog()
189
+ print_xref(doc, xref)
190
+ pymupdf.message()
191
+ if args.metadata:
192
+ pymupdf.message(mycenter("PDF metadata"))
193
+ print_dict(doc.metadata) # pylint: disable=no-member
194
+ pymupdf.message()
195
+ if args.xrefs:
196
+ pymupdf.message(mycenter("object information"))
197
+ xrefl = get_list(args.xrefs, doc.xref_length(), what="xref")
198
+ for xref in xrefl:
199
+ print_xref(doc, xref)
200
+ pymupdf.message()
201
+ if args.pages:
202
+ pymupdf.message(mycenter("page information"))
203
+ pagel = get_list(args.pages, doc.page_count + 1)
204
+ for pno in pagel:
205
+ n = pno - 1
206
+ xref = doc.page_xref(n)
207
+ pymupdf.message("Page %i:" % pno)
208
+ print_xref(doc, xref)
209
+ pymupdf.message()
210
+ if args.trailer:
211
+ pymupdf.message(mycenter("PDF trailer"))
212
+ pymupdf.message(doc.pdf_trailer())
213
+ pymupdf.message()
214
+ doc.close()
215
+
216
+
217
+ def clean(args):
218
+ doc = open_file(args.input, args.password, pdf=True)
219
+ encryption = args.encryption
220
+ encrypt = ("keep", "none", "rc4-40", "rc4-128", "aes-128", "aes-256").index(
221
+ encryption
222
+ )
223
+
224
+ if not args.pages: # simple cleaning
225
+ doc.save(
226
+ args.output,
227
+ garbage=args.garbage,
228
+ deflate=args.compress,
229
+ pretty=args.pretty,
230
+ clean=args.sanitize,
231
+ ascii=args.ascii,
232
+ linear=args.linear,
233
+ encryption=encrypt,
234
+ owner_pw=args.owner,
235
+ user_pw=args.user,
236
+ permissions=args.permission,
237
+ )
238
+ return
239
+
240
+ # create sub document from page numbers
241
+ pages = get_list(args.pages, doc.page_count + 1)
242
+ outdoc = pymupdf.open()
243
+ for pno in pages:
244
+ n = pno - 1
245
+ outdoc.insert_pdf(doc, from_page=n, to_page=n)
246
+ outdoc.save(
247
+ args.output,
248
+ garbage=args.garbage,
249
+ deflate=args.compress,
250
+ pretty=args.pretty,
251
+ clean=args.sanitize,
252
+ ascii=args.ascii,
253
+ linear=args.linear,
254
+ encryption=encrypt,
255
+ owner_pw=args.owner,
256
+ user_pw=args.user,
257
+ permissions=args.permission,
258
+ )
259
+ doc.close()
260
+ outdoc.close()
261
+ return
262
+
263
+
264
+ def doc_join(args):
265
+ """Join pages from several PDF documents."""
266
+ doc_list = args.input # a list of input PDFs
267
+ doc = pymupdf.open() # output PDF
268
+ for src_item in doc_list: # process one input PDF
269
+ src_list = src_item.split(",")
270
+ password = src_list[1] if len(src_list) > 1 else None
271
+ src = open_file(src_list[0], password, pdf=True)
272
+ pages = ",".join(src_list[2:]) # get 'pages' specifications
273
+ if pages: # if anything there, retrieve a list of desired pages
274
+ page_list = get_list(",".join(src_list[2:]), src.page_count + 1)
275
+ else: # take all pages
276
+ page_list = range(1, src.page_count + 1)
277
+ for i in page_list:
278
+ doc.insert_pdf(src, from_page=i - 1, to_page=i - 1) # copy each source page
279
+ src.close()
280
+
281
+ doc.save(args.output, garbage=4, deflate=True)
282
+ doc.close()
283
+
284
+
285
+ def embedded_copy(args):
286
+ """Copy embedded files between PDFs."""
287
+ doc = open_file(args.input, args.password, pdf=True)
288
+ if not doc.can_save_incrementally() and (
289
+ not args.output or args.output == args.input
290
+ ):
291
+ sys.exit("cannot save PDF incrementally")
292
+ src = open_file(args.source, args.pwdsource)
293
+ names = set(args.name) if args.name else set()
294
+ src_names = set(src.embfile_names())
295
+ if names:
296
+ if not names <= src_names:
297
+ sys.exit("not all names are contained in source")
298
+ else:
299
+ names = src_names
300
+ if not names:
301
+ sys.exit("nothing to copy")
302
+ intersect = names & set(doc.embfile_names()) # any equal name already in target?
303
+ if intersect:
304
+ sys.exit("following names already exist in receiving PDF: %s" % str(intersect))
305
+
306
+ for item in names:
307
+ info = src.embfile_info(item)
308
+ buff = src.embfile_get(item)
309
+ doc.embfile_add(
310
+ item,
311
+ buff,
312
+ filename=info["filename"],
313
+ ufilename=info["ufilename"],
314
+ desc=info["desc"],
315
+ )
316
+ pymupdf.message("copied entry '%s' from '%s'" % (item, src.name))
317
+ src.close()
318
+ if args.output and args.output != args.input:
319
+ doc.save(args.output, garbage=3)
320
+ else:
321
+ doc.saveIncr()
322
+ doc.close()
323
+
324
+
325
+ def embedded_del(args):
326
+ """Delete an embedded file entry."""
327
+ doc = open_file(args.input, args.password, pdf=True)
328
+ if not doc.can_save_incrementally() and (
329
+ not args.output or args.output == args.input
330
+ ):
331
+ sys.exit("cannot save PDF incrementally")
332
+
333
+ exception_types = (ValueError, pymupdf.mupdf.FzErrorBase)
334
+ if pymupdf.mupdf_version_tuple < (1, 24):
335
+ exception_types = ValueError
336
+ try:
337
+ doc.embfile_del(args.name)
338
+ except exception_types as e: # pylint: disable=catching-non-exception
339
+ sys.exit(f'no such embedded file {args.name!r}: {e}')
340
+ if not args.output or args.output == args.input:
341
+ doc.saveIncr()
342
+ else:
343
+ doc.save(args.output, garbage=1)
344
+ doc.close()
345
+
346
+
347
+ def embedded_get(args):
348
+ """Retrieve contents of an embedded file."""
349
+ doc = open_file(args.input, args.password, pdf=True)
350
+ exception_types = (ValueError, pymupdf.mupdf.FzErrorBase)
351
+ if pymupdf.mupdf_version_tuple < (1, 24):
352
+ exception_types = ValueError
353
+ try:
354
+ stream = doc.embfile_get(args.name)
355
+ d = doc.embfile_info(args.name)
356
+ except exception_types as e: # pylint: disable=catching-non-exception
357
+ sys.exit(f'no such embedded file {args.name!r}: {e}')
358
+ filename = args.output if args.output else d["filename"]
359
+ with open(filename, "wb") as output:
360
+ output.write(stream)
361
+ pymupdf.message("saved entry '%s' as '%s'" % (args.name, filename))
362
+ doc.close()
363
+
364
+
365
+ def embedded_add(args):
366
+ """Insert a new embedded file."""
367
+ doc = open_file(args.input, args.password, pdf=True)
368
+ if not doc.can_save_incrementally() and (
369
+ args.output is None or args.output == args.input
370
+ ):
371
+ sys.exit("cannot save PDF incrementally")
372
+
373
+ try:
374
+ doc.embfile_del(args.name)
375
+ sys.exit("entry '%s' already exists" % args.name)
376
+ except Exception:
377
+ pass
378
+
379
+ if not os.path.exists(args.path) or not os.path.isfile(args.path):
380
+ sys.exit("no such file '%s'" % args.path)
381
+ with open(args.path, "rb") as f:
382
+ stream = f.read()
383
+ filename = args.path
384
+ ufilename = filename
385
+ if not args.desc:
386
+ desc = filename
387
+ else:
388
+ desc = args.desc
389
+ doc.embfile_add(
390
+ args.name, stream, filename=filename, ufilename=ufilename, desc=desc
391
+ )
392
+ if not args.output or args.output == args.input:
393
+ doc.saveIncr()
394
+ else:
395
+ doc.save(args.output, garbage=3)
396
+ doc.close()
397
+
398
+
399
+ def embedded_upd(args):
400
+ """Update contents or metadata of an embedded file."""
401
+ doc = open_file(args.input, args.password, pdf=True)
402
+ if not doc.can_save_incrementally() and (
403
+ args.output is None or args.output == args.input
404
+ ):
405
+ sys.exit("cannot save PDF incrementally")
406
+
407
+ try:
408
+ doc.embfile_info(args.name)
409
+ except Exception:
410
+ sys.exit("no such embedded file '%s'" % args.name)
411
+
412
+ if (
413
+ args.path is not None
414
+ and os.path.exists(args.path)
415
+ and os.path.isfile(args.path)
416
+ ):
417
+ with open(args.path, "rb") as f:
418
+ stream = f.read()
419
+ else:
420
+ stream = None
421
+
422
+ if args.filename:
423
+ filename = args.filename
424
+ else:
425
+ filename = None
426
+
427
+ if args.ufilename:
428
+ ufilename = args.ufilename
429
+ elif args.filename:
430
+ ufilename = args.filename
431
+ else:
432
+ ufilename = None
433
+
434
+ if args.desc:
435
+ desc = args.desc
436
+ else:
437
+ desc = None
438
+
439
+ doc.embfile_upd(
440
+ args.name, stream, filename=filename, ufilename=ufilename, desc=desc
441
+ )
442
+ if args.output is None or args.output == args.input:
443
+ doc.saveIncr()
444
+ else:
445
+ doc.save(args.output, garbage=3)
446
+ doc.close()
447
+
448
+
449
+ def embedded_list(args):
450
+ """List embedded files."""
451
+ doc = open_file(args.input, args.password, pdf=True)
452
+ names = doc.embfile_names()
453
+ if args.name is not None:
454
+ if args.name not in names:
455
+ sys.exit("no such embedded file '%s'" % args.name)
456
+ else:
457
+ pymupdf.message()
458
+ pymupdf.message(
459
+ "printing 1 of %i embedded file%s:"
460
+ % (len(names), "s" if len(names) > 1 else "")
461
+ )
462
+ pymupdf.message()
463
+ print_dict(doc.embfile_info(args.name))
464
+ pymupdf.message()
465
+ return
466
+ if not names:
467
+ pymupdf.message("'%s' contains no embedded files" % doc.name)
468
+ return
469
+ if len(names) > 1:
470
+ msg = "'%s' contains the following %i embedded files" % (doc.name, len(names))
471
+ else:
472
+ msg = "'%s' contains the following embedded file" % doc.name
473
+ pymupdf.message(msg)
474
+ pymupdf.message()
475
+ for name in names:
476
+ if not args.detail:
477
+ pymupdf.message(name)
478
+ continue
479
+ _ = doc.embfile_info(name)
480
+ print_dict(doc.embfile_info(name))
481
+ pymupdf.message()
482
+ doc.close()
483
+
484
+
485
+ def extract_objects(args):
486
+ """Extract images and / or fonts from a PDF."""
487
+ if not args.fonts and not args.images:
488
+ sys.exit("neither fonts nor images requested")
489
+ doc = open_file(args.input, args.password, pdf=True)
490
+
491
+ if args.pages:
492
+ pages = get_list(args.pages, doc.page_count + 1)
493
+ else:
494
+ pages = range(1, doc.page_count + 1)
495
+
496
+ if not args.output:
497
+ out_dir = os.path.abspath(os.curdir)
498
+ else:
499
+ out_dir = args.output
500
+ if not (os.path.exists(out_dir) and os.path.isdir(out_dir)):
501
+ sys.exit("output directory %s does not exist" % out_dir)
502
+
503
+ font_xrefs = set() # already saved fonts
504
+ image_xrefs = set() # already saved images
505
+
506
+ for pno in pages:
507
+ if args.fonts:
508
+ itemlist = doc.get_page_fonts(pno - 1)
509
+ for item in itemlist:
510
+ xref = item[0]
511
+ if xref not in font_xrefs:
512
+ font_xrefs.add(xref)
513
+ fontname, ext, _, buffer = doc.extract_font(xref)
514
+ if ext == "n/a" or not buffer:
515
+ continue
516
+ outname = os.path.join(
517
+ out_dir, f"{fontname.replace(' ', '-')}-{xref}.{ext}"
518
+ )
519
+ with open(outname, "wb") as outfile:
520
+ outfile.write(buffer)
521
+ buffer = None
522
+ if args.images:
523
+ itemlist = doc.get_page_images(pno - 1)
524
+ for item in itemlist:
525
+ xref = item[0]
526
+ if xref not in image_xrefs:
527
+ image_xrefs.add(xref)
528
+ pix = recoverpix(doc, item)
529
+ if type(pix) is dict:
530
+ ext = pix["ext"]
531
+ imgdata = pix["image"]
532
+ outname = os.path.join(out_dir, "img-%i.%s" % (xref, ext))
533
+ with open(outname, "wb") as outfile:
534
+ outfile.write(imgdata)
535
+ else:
536
+ outname = os.path.join(out_dir, "img-%i.png" % xref)
537
+ pix2 = (
538
+ pix
539
+ if pix.colorspace.n < 4
540
+ else pymupdf.Pixmap(pymupdf.csRGB, pix)
541
+ )
542
+ pix2.save(outname)
543
+
544
+ if args.fonts:
545
+ pymupdf.message("saved %i fonts to '%s'" % (len(font_xrefs), out_dir))
546
+ if args.images:
547
+ pymupdf.message("saved %i images to '%s'" % (len(image_xrefs), out_dir))
548
+ doc.close()
549
+
550
+
551
+ def page_simple(page, textout, GRID, fontsize, noformfeed, skip_empty, flags):
552
+ eop = b"\n" if noformfeed else bytes([12])
553
+ text = page.get_text("text", flags=flags)
554
+ if not text:
555
+ if not skip_empty:
556
+ textout.write(eop) # write formfeed
557
+ return
558
+ textout.write(text.encode("utf8", errors="surrogatepass"))
559
+ textout.write(eop)
560
+ return
561
+
562
+
563
+ def page_blocksort(page, textout, GRID, fontsize, noformfeed, skip_empty, flags):
564
+ eop = b"\n" if noformfeed else bytes([12])
565
+ blocks = page.get_text("blocks", flags=flags)
566
+ if blocks == []:
567
+ if not skip_empty:
568
+ textout.write(eop) # write formfeed
569
+ return
570
+ blocks.sort(key=lambda b: (b[3], b[0]))
571
+ for b in blocks:
572
+ textout.write(b[4].encode("utf8", errors="surrogatepass"))
573
+ textout.write(eop)
574
+ return
575
+
576
+
577
+ def page_layout(page, textout, GRID, fontsize, noformfeed, skip_empty, flags):
578
+ eop = b"\n" if noformfeed else bytes([12])
579
+
580
+ # --------------------------------------------------------------------
581
+ def find_line_index(values: List[int], value: int) -> int:
582
+ """Find the right row coordinate.
583
+
584
+ Args:
585
+ values: (list) y-coordinates of rows.
586
+ value: (int) lookup for this value (y-origin of char).
587
+ Returns:
588
+ y-ccordinate of appropriate line for value.
589
+ """
590
+ i = bisect.bisect_right(values, value)
591
+ if i:
592
+ return values[i - 1]
593
+ raise RuntimeError("Line for %g not found in %s" % (value, values))
594
+
595
+ # --------------------------------------------------------------------
596
+ def curate_rows(rows: Set[int], GRID) -> List:
597
+ rows = list(rows)
598
+ rows.sort() # sort ascending
599
+ nrows = [rows[0]]
600
+ for h in rows[1:]:
601
+ if h >= nrows[-1] + GRID: # only keep significant differences
602
+ nrows.append(h)
603
+ return nrows # curated list of line bottom coordinates
604
+
605
+ def process_blocks(blocks: List[Dict], page: pymupdf.Page):
606
+ rows = set()
607
+ page_width = page.rect.width
608
+ page_height = page.rect.height
609
+ rowheight = page_height
610
+ left = page_width
611
+ right = 0
612
+ chars = []
613
+ for block in blocks:
614
+ for line in block["lines"]:
615
+ if line["dir"] != (1, 0): # ignore non-horizontal text
616
+ continue
617
+ x0, y0, x1, y1 = line["bbox"]
618
+ if y1 < 0 or y0 > page.rect.height: # ignore if outside CropBox
619
+ continue
620
+ # upd row height
621
+ height = y1 - y0
622
+
623
+ if rowheight > height:
624
+ rowheight = height
625
+ for span in line["spans"]:
626
+ if span["size"] <= fontsize:
627
+ continue
628
+ for c in span["chars"]:
629
+ x0, _, x1, _ = c["bbox"]
630
+ cwidth = x1 - x0
631
+ ox, oy = c["origin"]
632
+ oy = int(round(oy))
633
+ rows.add(oy)
634
+ ch = c["c"]
635
+ if left > ox and ch != " ":
636
+ left = ox # update left coordinate
637
+ if right < x1:
638
+ right = x1 # update right coordinate
639
+ # handle ligatures:
640
+ if cwidth == 0 and chars != []: # potential ligature
641
+ old_ch, old_ox, old_oy, old_cwidth = chars[-1]
642
+ if old_oy == oy: # ligature
643
+ if old_ch != chr(0xFB00): # previous "ff" char lig?
644
+ lig = joinligature(old_ch + ch) # no
645
+ # convert to one of the 3-char ligatures:
646
+ elif ch == "i":
647
+ lig = chr(0xFB03) # "ffi"
648
+ elif ch == "l":
649
+ lig = chr(0xFB04) # "ffl"
650
+ else: # something wrong, leave old char in place
651
+ lig = old_ch
652
+ chars[-1] = (lig, old_ox, old_oy, old_cwidth)
653
+ continue
654
+ chars.append((ch, ox, oy, cwidth)) # all chars on page
655
+ return chars, rows, left, right, rowheight
656
+
657
+ def joinligature(lig: str) -> str:
658
+ """Return ligature character for a given pair / triple of characters.
659
+
660
+ Args:
661
+ lig: (str) 2/3 characters, e.g. "ff"
662
+ Returns:
663
+ Ligature, e.g. "ff" -> chr(0xFB00)
664
+ """
665
+
666
+ if lig == "ff":
667
+ return chr(0xFB00)
668
+ elif lig == "fi":
669
+ return chr(0xFB01)
670
+ elif lig == "fl":
671
+ return chr(0xFB02)
672
+ elif lig == "ffi":
673
+ return chr(0xFB03)
674
+ elif lig == "ffl":
675
+ return chr(0xFB04)
676
+ elif lig == "ft":
677
+ return chr(0xFB05)
678
+ elif lig == "st":
679
+ return chr(0xFB06)
680
+ return lig
681
+
682
+ # --------------------------------------------------------------------
683
+ def make_textline(left, slot, minslot, lchars):
684
+ """Produce the text of one output line.
685
+
686
+ Args:
687
+ left: (float) left most coordinate used on page
688
+ slot: (float) avg width of one character in any font in use.
689
+ minslot: (float) min width for the characters in this line.
690
+ chars: (list[tuple]) characters of this line.
691
+ Returns:
692
+ text: (str) text string for this line
693
+ """
694
+ text = "" # we output this
695
+ old_char = ""
696
+ old_x1 = 0 # end coordinate of last char
697
+ old_ox = 0 # x-origin of last char
698
+ if minslot <= pymupdf.EPSILON:
699
+ raise RuntimeError("program error: minslot too small = %g" % minslot)
700
+
701
+ for c in lchars: # loop over characters
702
+ char, ox, _, cwidth = c
703
+ ox = ox - left # its (relative) start coordinate
704
+ x1 = ox + cwidth # ending coordinate
705
+
706
+ # eliminate overprint effect
707
+ if old_char == char and ox - old_ox <= cwidth * 0.2:
708
+ continue
709
+
710
+ # omit spaces overlapping previous char
711
+ if char == " " and (old_x1 - ox) / cwidth > 0.8:
712
+ continue
713
+
714
+ old_char = char
715
+ # close enough to previous?
716
+ if ox < old_x1 + minslot: # assume char adjacent to previous
717
+ text += char # append to output
718
+ old_x1 = x1 # new end coord
719
+ old_ox = ox # new origin.x
720
+ continue
721
+
722
+ # else next char starts after some gap:
723
+ # fill in right number of spaces, so char is positioned
724
+ # in the right slot of the line
725
+ if char == " ": # rest relevant for non-space only
726
+ continue
727
+ delta = int(ox / slot) - len(text)
728
+ if ox > old_x1 and delta > 1:
729
+ text += " " * delta
730
+ # now append char
731
+ text += char
732
+ old_x1 = x1 # new end coordinate
733
+ old_ox = ox # new origin
734
+ return text.rstrip()
735
+
736
+ # extract page text by single characters ("rawdict")
737
+ blocks = page.get_text("rawdict", flags=flags)["blocks"]
738
+ chars, rows, left, right, rowheight = process_blocks(blocks, page)
739
+
740
+ if chars == []:
741
+ if not skip_empty:
742
+ textout.write(eop) # write formfeed
743
+ return
744
+ # compute list of line coordinates - ignoring small (GRID) differences
745
+ rows = curate_rows(rows, GRID)
746
+
747
+ # sort all chars by x-coordinates, so every line will receive char info,
748
+ # sorted from left to right.
749
+ chars.sort(key=lambda c: c[1])
750
+
751
+ # populate the lines with their char info
752
+ lines = {} # key: y1-ccordinate, value: char list
753
+ for c in chars:
754
+ _, _, oy, _ = c
755
+ y = find_line_index(rows, oy) # y-coord of the right line
756
+ lchars = lines.get(y, []) # read line chars so far
757
+ lchars.append(c) # append this char
758
+ lines[y] = lchars # write back to line
759
+
760
+ # ensure line coordinates are ascending
761
+ keys = list(lines.keys())
762
+ keys.sort()
763
+
764
+ # -------------------------------------------------------------------------
765
+ # Compute "char resolution" for the page: the char width corresponding to
766
+ # 1 text char position on output - call it 'slot'.
767
+ # For each line, compute median of its char widths. The minimum across all
768
+ # lines is 'slot'.
769
+ # The minimum char width of each line is used to determine if spaces must
770
+ # be inserted in between two characters.
771
+ # -------------------------------------------------------------------------
772
+ slot = right - left
773
+ minslots = {}
774
+ for k in keys:
775
+ lchars = lines[k]
776
+ ccount = len(lchars)
777
+ if ccount < 2:
778
+ minslots[k] = 1
779
+ continue
780
+ widths = [c[3] for c in lchars]
781
+ widths.sort()
782
+ this_slot = statistics.median(widths) # take median value
783
+ if this_slot < slot:
784
+ slot = this_slot
785
+ minslots[k] = widths[0]
786
+
787
+ # compute line advance in text output
788
+ rowheight = rowheight * (rows[-1] - rows[0]) / (rowheight * len(rows)) * 1.2
789
+ rowpos = rows[0] # first line positioned here
790
+ textout.write(b"\n")
791
+ for k in keys: # walk through the lines
792
+ while rowpos < k: # honor distance between lines
793
+ textout.write(b"\n")
794
+ rowpos += rowheight
795
+ text = make_textline(left, slot, minslots[k], lines[k])
796
+ textout.write((text + "\n").encode("utf8", errors="surrogatepass"))
797
+ rowpos = k + rowheight
798
+
799
+ textout.write(eop) # write formfeed
800
+
801
+
802
+ def gettext(args):
803
+ doc = open_file(args.input, args.password, pdf=False)
804
+ pagel = get_list(args.pages, doc.page_count + 1)
805
+ output = args.output
806
+ if output is None:
807
+ filename, _ = os.path.splitext(doc.name)
808
+ output = filename + ".txt"
809
+ with open(output, "wb") as textout:
810
+ flags = pymupdf.TEXT_PRESERVE_LIGATURES | pymupdf.TEXT_PRESERVE_WHITESPACE
811
+ if args.convert_white:
812
+ flags ^= pymupdf.TEXT_PRESERVE_WHITESPACE
813
+ if args.noligatures:
814
+ flags ^= pymupdf.TEXT_PRESERVE_LIGATURES
815
+ if args.extra_spaces:
816
+ flags ^= pymupdf.TEXT_INHIBIT_SPACES
817
+ func = {
818
+ "simple": page_simple,
819
+ "blocks": page_blocksort,
820
+ "layout": page_layout,
821
+ }
822
+ for pno in pagel:
823
+ page = doc[pno - 1]
824
+ func[args.mode](
825
+ page,
826
+ textout,
827
+ args.grid,
828
+ args.fontsize,
829
+ args.noformfeed,
830
+ args.skip_empty,
831
+ flags=flags,
832
+ )
833
+
834
+
835
+ def _internal(args):
836
+ pymupdf.message('This is from PyMuPDF message().')
837
+ pymupdf.log('This is from PyMuPDF log().')
838
+
839
+ def main():
840
+ """Define command configurations."""
841
+ parser = argparse.ArgumentParser(
842
+ prog="pymupdf",
843
+ description=mycenter("Basic PyMuPDF Functions"),
844
+ )
845
+ subps = parser.add_subparsers(
846
+ title="Subcommands", help="Enter 'command -h' for subcommand specific help"
847
+ )
848
+
849
+ # -------------------------------------------------------------------------
850
+ # 'show' command
851
+ # -------------------------------------------------------------------------
852
+ ps_show = subps.add_parser("show", description=mycenter("display PDF information"))
853
+ ps_show.add_argument("input", type=str, help="PDF filename")
854
+ ps_show.add_argument("-password", help="password")
855
+ ps_show.add_argument("-catalog", action="store_true", help="show PDF catalog")
856
+ ps_show.add_argument("-trailer", action="store_true", help="show PDF trailer")
857
+ ps_show.add_argument("-metadata", action="store_true", help="show PDF metadata")
858
+ ps_show.add_argument(
859
+ "-xrefs", type=str, help="show selected objects, format: 1,5-7,N"
860
+ )
861
+ ps_show.add_argument(
862
+ "-pages", type=str, help="show selected pages, format: 1,5-7,50-N"
863
+ )
864
+ ps_show.set_defaults(func=show)
865
+
866
+ # -------------------------------------------------------------------------
867
+ # 'clean' command
868
+ # -------------------------------------------------------------------------
869
+ ps_clean = subps.add_parser(
870
+ "clean", description=mycenter("optimize PDF, or create sub-PDF if pages given")
871
+ )
872
+ ps_clean.add_argument("input", type=str, help="PDF filename")
873
+ ps_clean.add_argument("output", type=str, help="output PDF filename")
874
+ ps_clean.add_argument("-password", help="password")
875
+
876
+ ps_clean.add_argument(
877
+ "-encryption",
878
+ help="encryption method",
879
+ choices=("keep", "none", "rc4-40", "rc4-128", "aes-128", "aes-256"),
880
+ default="none",
881
+ )
882
+
883
+ ps_clean.add_argument("-owner", type=str, help="owner password")
884
+ ps_clean.add_argument("-user", type=str, help="user password")
885
+
886
+ ps_clean.add_argument(
887
+ "-garbage",
888
+ type=int,
889
+ help="garbage collection level",
890
+ choices=range(5),
891
+ default=0,
892
+ )
893
+
894
+ ps_clean.add_argument(
895
+ "-compress",
896
+ action="store_true",
897
+ default=False,
898
+ help="compress (deflate) output",
899
+ )
900
+
901
+ ps_clean.add_argument(
902
+ "-ascii", action="store_true", default=False, help="ASCII encode binary data"
903
+ )
904
+
905
+ ps_clean.add_argument(
906
+ "-linear",
907
+ action="store_true",
908
+ default=False,
909
+ help="format for fast web display",
910
+ )
911
+
912
+ ps_clean.add_argument(
913
+ "-permission", type=int, default=-1, help="integer with permission levels"
914
+ )
915
+
916
+ ps_clean.add_argument(
917
+ "-sanitize",
918
+ action="store_true",
919
+ default=False,
920
+ help="sanitize / clean contents",
921
+ )
922
+ ps_clean.add_argument(
923
+ "-pretty", action="store_true", default=False, help="prettify PDF structure"
924
+ )
925
+ ps_clean.add_argument(
926
+ "-pages", help="output selected pages pages, format: 1,5-7,50-N"
927
+ )
928
+ ps_clean.set_defaults(func=clean)
929
+
930
+ # -------------------------------------------------------------------------
931
+ # 'join' command
932
+ # -------------------------------------------------------------------------
933
+ ps_join = subps.add_parser(
934
+ "join",
935
+ description=mycenter("join PDF documents"),
936
+ epilog="specify each input as 'filename[,password[,pages]]'",
937
+ )
938
+ ps_join.add_argument("input", nargs="*", help="input filenames")
939
+ ps_join.add_argument("-output", required=True, help="output filename")
940
+ ps_join.set_defaults(func=doc_join)
941
+
942
+ # -------------------------------------------------------------------------
943
+ # 'extract' command
944
+ # -------------------------------------------------------------------------
945
+ ps_extract = subps.add_parser(
946
+ "extract", description=mycenter("extract images and fonts to disk")
947
+ )
948
+ ps_extract.add_argument("input", type=str, help="PDF filename")
949
+ ps_extract.add_argument("-images", action="store_true", help="extract images")
950
+ ps_extract.add_argument("-fonts", action="store_true", help="extract fonts")
951
+ ps_extract.add_argument(
952
+ "-output", help="folder to receive output, defaults to current"
953
+ )
954
+ ps_extract.add_argument("-password", help="password")
955
+ ps_extract.add_argument(
956
+ "-pages", type=str, help="consider these pages only, format: 1,5-7,50-N"
957
+ )
958
+ ps_extract.set_defaults(func=extract_objects)
959
+
960
+ # -------------------------------------------------------------------------
961
+ # 'embed-info'
962
+ # -------------------------------------------------------------------------
963
+ ps_show = subps.add_parser(
964
+ "embed-info", description=mycenter("list embedded files")
965
+ )
966
+ ps_show.add_argument("input", help="PDF filename")
967
+ ps_show.add_argument("-name", help="if given, report only this one")
968
+ ps_show.add_argument("-detail", action="store_true", help="detail information")
969
+ ps_show.add_argument("-password", help="password")
970
+ ps_show.set_defaults(func=embedded_list)
971
+
972
+ # -------------------------------------------------------------------------
973
+ # 'embed-add' command
974
+ # -------------------------------------------------------------------------
975
+ ps_embed_add = subps.add_parser(
976
+ "embed-add", description=mycenter("add embedded file")
977
+ )
978
+ ps_embed_add.add_argument("input", help="PDF filename")
979
+ ps_embed_add.add_argument("-password", help="password")
980
+ ps_embed_add.add_argument(
981
+ "-output", help="output PDF filename, incremental save if none"
982
+ )
983
+ ps_embed_add.add_argument("-name", required=True, help="name of new entry")
984
+ ps_embed_add.add_argument("-path", required=True, help="path to data for new entry")
985
+ ps_embed_add.add_argument("-desc", help="description of new entry")
986
+ ps_embed_add.set_defaults(func=embedded_add)
987
+
988
+ # -------------------------------------------------------------------------
989
+ # 'embed-del' command
990
+ # -------------------------------------------------------------------------
991
+ ps_embed_del = subps.add_parser(
992
+ "embed-del", description=mycenter("delete embedded file")
993
+ )
994
+ ps_embed_del.add_argument("input", help="PDF filename")
995
+ ps_embed_del.add_argument("-password", help="password")
996
+ ps_embed_del.add_argument(
997
+ "-output", help="output PDF filename, incremental save if none"
998
+ )
999
+ ps_embed_del.add_argument("-name", required=True, help="name of entry to delete")
1000
+ ps_embed_del.set_defaults(func=embedded_del)
1001
+
1002
+ # -------------------------------------------------------------------------
1003
+ # 'embed-upd' command
1004
+ # -------------------------------------------------------------------------
1005
+ ps_embed_upd = subps.add_parser(
1006
+ "embed-upd",
1007
+ description=mycenter("update embedded file"),
1008
+ epilog="except '-name' all parameters are optional",
1009
+ )
1010
+ ps_embed_upd.add_argument("input", help="PDF filename")
1011
+ ps_embed_upd.add_argument("-name", required=True, help="name of entry")
1012
+ ps_embed_upd.add_argument("-password", help="password")
1013
+ ps_embed_upd.add_argument(
1014
+ "-output", help="Output PDF filename, incremental save if none"
1015
+ )
1016
+ ps_embed_upd.add_argument("-path", help="path to new data for entry")
1017
+ ps_embed_upd.add_argument("-filename", help="new filename to store in entry")
1018
+ ps_embed_upd.add_argument(
1019
+ "-ufilename", help="new unicode filename to store in entry"
1020
+ )
1021
+ ps_embed_upd.add_argument("-desc", help="new description to store in entry")
1022
+ ps_embed_upd.set_defaults(func=embedded_upd)
1023
+
1024
+ # -------------------------------------------------------------------------
1025
+ # 'embed-extract' command
1026
+ # -------------------------------------------------------------------------
1027
+ ps_embed_extract = subps.add_parser(
1028
+ "embed-extract", description=mycenter("extract embedded file to disk")
1029
+ )
1030
+ ps_embed_extract.add_argument("input", type=str, help="PDF filename")
1031
+ ps_embed_extract.add_argument("-name", required=True, help="name of entry")
1032
+ ps_embed_extract.add_argument("-password", help="password")
1033
+ ps_embed_extract.add_argument(
1034
+ "-output", help="output filename, default is stored name"
1035
+ )
1036
+ ps_embed_extract.set_defaults(func=embedded_get)
1037
+
1038
+ # -------------------------------------------------------------------------
1039
+ # 'embed-copy' command
1040
+ # -------------------------------------------------------------------------
1041
+ ps_embed_copy = subps.add_parser(
1042
+ "embed-copy", description=mycenter("copy embedded files between PDFs")
1043
+ )
1044
+ ps_embed_copy.add_argument("input", type=str, help="PDF to receive embedded files")
1045
+ ps_embed_copy.add_argument("-password", help="password of input")
1046
+ ps_embed_copy.add_argument(
1047
+ "-output", help="output PDF, incremental save to 'input' if omitted"
1048
+ )
1049
+ ps_embed_copy.add_argument(
1050
+ "-source", required=True, help="copy embedded files from here"
1051
+ )
1052
+ ps_embed_copy.add_argument("-pwdsource", help="password of 'source' PDF")
1053
+ ps_embed_copy.add_argument(
1054
+ "-name", nargs="*", help="restrict copy to these entries"
1055
+ )
1056
+ ps_embed_copy.set_defaults(func=embedded_copy)
1057
+
1058
+ # -------------------------------------------------------------------------
1059
+ # 'textlayout' command
1060
+ # -------------------------------------------------------------------------
1061
+ ps_gettext = subps.add_parser(
1062
+ "gettext", description=mycenter("extract text in various formatting modes")
1063
+ )
1064
+ ps_gettext.add_argument("input", type=str, help="input document filename")
1065
+ ps_gettext.add_argument("-password", help="password for input document")
1066
+ ps_gettext.add_argument(
1067
+ "-mode",
1068
+ type=str,
1069
+ help="mode: simple, block sort, or layout (default)",
1070
+ choices=("simple", "blocks", "layout"),
1071
+ default="layout",
1072
+ )
1073
+ ps_gettext.add_argument(
1074
+ "-pages",
1075
+ type=str,
1076
+ help="select pages, format: 1,5-7,50-N",
1077
+ default="1-N",
1078
+ )
1079
+ ps_gettext.add_argument(
1080
+ "-noligatures",
1081
+ action="store_true",
1082
+ help="expand ligature characters (default False)",
1083
+ default=False,
1084
+ )
1085
+ ps_gettext.add_argument(
1086
+ "-convert-white",
1087
+ action="store_true",
1088
+ help="convert whitespace characters to white (default False)",
1089
+ default=False,
1090
+ )
1091
+ ps_gettext.add_argument(
1092
+ "-extra-spaces",
1093
+ action="store_true",
1094
+ help="fill gaps with spaces (default False)",
1095
+ default=False,
1096
+ )
1097
+ ps_gettext.add_argument(
1098
+ "-noformfeed",
1099
+ action="store_true",
1100
+ help="write linefeeds, no formfeeds (default False)",
1101
+ default=False,
1102
+ )
1103
+ ps_gettext.add_argument(
1104
+ "-skip-empty",
1105
+ action="store_true",
1106
+ help="suppress pages with no text (default False)",
1107
+ default=False,
1108
+ )
1109
+ ps_gettext.add_argument(
1110
+ "-output",
1111
+ help="store text in this file (default inputfilename.txt)",
1112
+ )
1113
+ ps_gettext.add_argument(
1114
+ "-grid",
1115
+ type=float,
1116
+ help="merge lines if closer than this (default 2)",
1117
+ default=2,
1118
+ )
1119
+ ps_gettext.add_argument(
1120
+ "-fontsize",
1121
+ type=float,
1122
+ help="only include text with a larger fontsize (default 3)",
1123
+ default=3,
1124
+ )
1125
+ ps_gettext.set_defaults(func=gettext)
1126
+
1127
+ # -------------------------------------------------------------------------
1128
+ # '_internal' command
1129
+ # -------------------------------------------------------------------------
1130
+ ps_internal = subps.add_parser(
1131
+ "internal", description=mycenter("internal testing")
1132
+ )
1133
+ ps_internal.set_defaults(func=_internal)
1134
+
1135
+ # -------------------------------------------------------------------------
1136
+ # start program
1137
+ # -------------------------------------------------------------------------
1138
+ args = parser.parse_args() # create parameter arguments class
1139
+ if not hasattr(args, "func"): # no function selected
1140
+ parser.print_help() # so print top level help
1141
+ else:
1142
+ args.func(args) # execute requested command
1143
+
1144
+
1145
+ if __name__ == "__main__":
1146
+ main()