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.
- PyMuPDF-1.24.11.dist-info/COPYING +1 -0
- PyMuPDF-1.24.11.dist-info/METADATA +83 -0
- PyMuPDF-1.24.11.dist-info/README.md +60 -0
- PyMuPDF-1.24.11.dist-info/RECORD +99 -0
- PyMuPDF-1.24.11.dist-info/WHEEL +4 -0
- PyMuPDF-1.24.11.dist-info/entry_points.txt +3 -0
- fitz/__init__.py +11 -0
- fitz/table.py +2 -0
- fitz/utils.py +2 -0
- pymupdf/__init__.py +22122 -0
- pymupdf/__main__.py +1146 -0
- pymupdf/_apply_pages.py +253 -0
- pymupdf/_build.py +1 -0
- pymupdf/_extra.pyd +0 -0
- pymupdf/_mupdf.pyd +0 -0
- pymupdf/extra.py +214 -0
- pymupdf/mupdf-devel/include/mupdf/fitz/archive.h +444 -0
- pymupdf/mupdf-devel/include/mupdf/fitz/band-writer.h +117 -0
- pymupdf/mupdf-devel/include/mupdf/fitz/bidi.h +90 -0
- pymupdf/mupdf-devel/include/mupdf/fitz/bitmap.h +175 -0
- pymupdf/mupdf-devel/include/mupdf/fitz/buffer.h +250 -0
- pymupdf/mupdf-devel/include/mupdf/fitz/color.h +427 -0
- pymupdf/mupdf-devel/include/mupdf/fitz/compress.h +88 -0
- pymupdf/mupdf-devel/include/mupdf/fitz/compressed-buffer.h +185 -0
- pymupdf/mupdf-devel/include/mupdf/fitz/config.h +223 -0
- pymupdf/mupdf-devel/include/mupdf/fitz/context.h +1006 -0
- pymupdf/mupdf-devel/include/mupdf/fitz/crypt.h +270 -0
- pymupdf/mupdf-devel/include/mupdf/fitz/device.h +654 -0
- pymupdf/mupdf-devel/include/mupdf/fitz/display-list.h +142 -0
- pymupdf/mupdf-devel/include/mupdf/fitz/document.h +1108 -0
- pymupdf/mupdf-devel/include/mupdf/fitz/export.h +52 -0
- pymupdf/mupdf-devel/include/mupdf/fitz/filter.h +263 -0
- pymupdf/mupdf-devel/include/mupdf/fitz/font.h +815 -0
- pymupdf/mupdf-devel/include/mupdf/fitz/geometry.h +818 -0
- pymupdf/mupdf-devel/include/mupdf/fitz/getopt.h +128 -0
- pymupdf/mupdf-devel/include/mupdf/fitz/glyph-cache.h +96 -0
- pymupdf/mupdf-devel/include/mupdf/fitz/glyph.h +81 -0
- pymupdf/mupdf-devel/include/mupdf/fitz/hash.h +126 -0
- pymupdf/mupdf-devel/include/mupdf/fitz/heap-imp.h +163 -0
- pymupdf/mupdf-devel/include/mupdf/fitz/heap.h +140 -0
- pymupdf/mupdf-devel/include/mupdf/fitz/image.h +443 -0
- pymupdf/mupdf-devel/include/mupdf/fitz/link.h +130 -0
- pymupdf/mupdf-devel/include/mupdf/fitz/log.h +61 -0
- pymupdf/mupdf-devel/include/mupdf/fitz/outline.h +228 -0
- pymupdf/mupdf-devel/include/mupdf/fitz/output-svg.h +64 -0
- pymupdf/mupdf-devel/include/mupdf/fitz/output.h +415 -0
- pymupdf/mupdf-devel/include/mupdf/fitz/path.h +447 -0
- pymupdf/mupdf-devel/include/mupdf/fitz/pixmap.h +501 -0
- pymupdf/mupdf-devel/include/mupdf/fitz/pool.h +68 -0
- pymupdf/mupdf-devel/include/mupdf/fitz/separation.h +138 -0
- pymupdf/mupdf-devel/include/mupdf/fitz/shade.h +233 -0
- pymupdf/mupdf-devel/include/mupdf/fitz/store.h +456 -0
- pymupdf/mupdf-devel/include/mupdf/fitz/story-writer.h +209 -0
- pymupdf/mupdf-devel/include/mupdf/fitz/story.h +232 -0
- pymupdf/mupdf-devel/include/mupdf/fitz/stream.h +646 -0
- pymupdf/mupdf-devel/include/mupdf/fitz/string-util.h +286 -0
- pymupdf/mupdf-devel/include/mupdf/fitz/structured-text.h +365 -0
- pymupdf/mupdf-devel/include/mupdf/fitz/system.h +459 -0
- pymupdf/mupdf-devel/include/mupdf/fitz/text.h +209 -0
- pymupdf/mupdf-devel/include/mupdf/fitz/track-usage.h +57 -0
- pymupdf/mupdf-devel/include/mupdf/fitz/transition.h +76 -0
- pymupdf/mupdf-devel/include/mupdf/fitz/tree.h +62 -0
- pymupdf/mupdf-devel/include/mupdf/fitz/types.h +41 -0
- pymupdf/mupdf-devel/include/mupdf/fitz/util.h +151 -0
- pymupdf/mupdf-devel/include/mupdf/fitz/version.h +31 -0
- pymupdf/mupdf-devel/include/mupdf/fitz/write-pixmap.h +499 -0
- pymupdf/mupdf-devel/include/mupdf/fitz/writer.h +266 -0
- pymupdf/mupdf-devel/include/mupdf/fitz/xml.h +397 -0
- pymupdf/mupdf-devel/include/mupdf/fitz.h +97 -0
- pymupdf/mupdf-devel/include/mupdf/helpers/mu-office-lib.h +666 -0
- pymupdf/mupdf-devel/include/mupdf/helpers/mu-threads.h +280 -0
- pymupdf/mupdf-devel/include/mupdf/helpers/pkcs7-openssl.h +48 -0
- pymupdf/mupdf-devel/include/mupdf/html.h +53 -0
- pymupdf/mupdf-devel/include/mupdf/memento.h +423 -0
- pymupdf/mupdf-devel/include/mupdf/pdf/annot.h +965 -0
- pymupdf/mupdf-devel/include/mupdf/pdf/clean.h +48 -0
- pymupdf/mupdf-devel/include/mupdf/pdf/cmap.h +145 -0
- pymupdf/mupdf-devel/include/mupdf/pdf/crypt.h +107 -0
- pymupdf/mupdf-devel/include/mupdf/pdf/document.h +842 -0
- pymupdf/mupdf-devel/include/mupdf/pdf/event.h +167 -0
- pymupdf/mupdf-devel/include/mupdf/pdf/font.h +168 -0
- pymupdf/mupdf-devel/include/mupdf/pdf/form.h +380 -0
- pymupdf/mupdf-devel/include/mupdf/pdf/image-rewriter.h +78 -0
- pymupdf/mupdf-devel/include/mupdf/pdf/interpret.h +492 -0
- pymupdf/mupdf-devel/include/mupdf/pdf/javascript.h +43 -0
- pymupdf/mupdf-devel/include/mupdf/pdf/name-table.h +596 -0
- pymupdf/mupdf-devel/include/mupdf/pdf/object.h +435 -0
- pymupdf/mupdf-devel/include/mupdf/pdf/page.h +316 -0
- pymupdf/mupdf-devel/include/mupdf/pdf/parse.h +61 -0
- pymupdf/mupdf-devel/include/mupdf/pdf/resource.h +133 -0
- pymupdf/mupdf-devel/include/mupdf/pdf/xref.h +300 -0
- pymupdf/mupdf-devel/include/mupdf/pdf.h +57 -0
- pymupdf/mupdf-devel/include/mupdf/ucdn.h +452 -0
- pymupdf/mupdf-devel/lib/mupdfcpp64.lib +0 -0
- pymupdf/mupdf.py +58506 -0
- pymupdf/mupdfcpp64.dll +0 -0
- pymupdf/pymupdf.py +2 -0
- pymupdf/table.py +2355 -0
- 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()
|