PyMuPDF 1.26.6__cp310-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.
- fitz/__init__.py +13 -0
- fitz/table.py +2 -0
- fitz/utils.py +2 -0
- pymupdf/__init__.py +25899 -0
- pymupdf/__main__.py +1140 -0
- pymupdf/_apply_pages.py +253 -0
- pymupdf/_build.py +8 -0
- pymupdf/_extra.pyd +0 -0
- pymupdf/_mupdf.pyd +0 -0
- pymupdf/_wxcolors.py +562 -0
- pymupdf/extra.py +220 -0
- pymupdf/mupdf-devel/include/mupdf/classes.h +19999 -0
- pymupdf/mupdf-devel/include/mupdf/classes2.h +12244 -0
- pymupdf/mupdf-devel/include/mupdf/exceptions.h +118 -0
- pymupdf/mupdf-devel/include/mupdf/extra.h +189 -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/barcode.h +138 -0
- pymupdf/mupdf-devel/include/mupdf/fitz/bidi.h +90 -0
- pymupdf/mupdf-devel/include/mupdf/fitz/bitmap.h +188 -0
- pymupdf/mupdf-devel/include/mupdf/fitz/buffer.h +250 -0
- pymupdf/mupdf-devel/include/mupdf/fitz/color.h +433 -0
- pymupdf/mupdf-devel/include/mupdf/fitz/compress.h +132 -0
- pymupdf/mupdf-devel/include/mupdf/fitz/compressed-buffer.h +194 -0
- pymupdf/mupdf-devel/include/mupdf/fitz/config.h +302 -0
- pymupdf/mupdf-devel/include/mupdf/fitz/context.h +1064 -0
- pymupdf/mupdf-devel/include/mupdf/fitz/crypt.h +270 -0
- pymupdf/mupdf-devel/include/mupdf/fitz/deskew.h +46 -0
- pymupdf/mupdf-devel/include/mupdf/fitz/device.h +652 -0
- pymupdf/mupdf-devel/include/mupdf/fitz/display-list.h +142 -0
- pymupdf/mupdf-devel/include/mupdf/fitz/document.h +1103 -0
- pymupdf/mupdf-devel/include/mupdf/fitz/export.h +52 -0
- pymupdf/mupdf-devel/include/mupdf/fitz/filter.h +269 -0
- pymupdf/mupdf-devel/include/mupdf/fitz/font.h +858 -0
- pymupdf/mupdf-devel/include/mupdf/fitz/geometry.h +885 -0
- pymupdf/mupdf-devel/include/mupdf/fitz/getopt.h +134 -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 +190 -0
- pymupdf/mupdf-devel/include/mupdf/fitz/heap.h +143 -0
- pymupdf/mupdf-devel/include/mupdf/fitz/image.h +451 -0
- pymupdf/mupdf-devel/include/mupdf/fitz/json.h +98 -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 +254 -0
- pymupdf/mupdf-devel/include/mupdf/fitz/output-svg.h +64 -0
- pymupdf/mupdf-devel/include/mupdf/fitz/output.h +448 -0
- pymupdf/mupdf-devel/include/mupdf/fitz/path.h +477 -0
- pymupdf/mupdf-devel/include/mupdf/fitz/pixmap.h +516 -0
- pymupdf/mupdf-devel/include/mupdf/fitz/pool.h +102 -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 +465 -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 +335 -0
- pymupdf/mupdf-devel/include/mupdf/fitz/structured-text.h +924 -0
- pymupdf/mupdf-devel/include/mupdf/fitz/system.h +495 -0
- pymupdf/mupdf-devel/include/mupdf/fitz/text.h +210 -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 +159 -0
- pymupdf/mupdf-devel/include/mupdf/fitz/version.h +31 -0
- pymupdf/mupdf-devel/include/mupdf/fitz/write-pixmap.h +507 -0
- pymupdf/mupdf-devel/include/mupdf/fitz/writer.h +269 -0
- pymupdf/mupdf-devel/include/mupdf/fitz/xml.h +435 -0
- pymupdf/mupdf-devel/include/mupdf/fitz.h +101 -0
- pymupdf/mupdf-devel/include/mupdf/functions.h +13819 -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/internal.h +29 -0
- pymupdf/mupdf-devel/include/mupdf/memento.h +425 -0
- pymupdf/mupdf-devel/include/mupdf/pdf/annot.h +1015 -0
- pymupdf/mupdf-devel/include/mupdf/pdf/clean.h +68 -0
- pymupdf/mupdf-devel/include/mupdf/pdf/cmap.h +145 -0
- pymupdf/mupdf-devel/include/mupdf/pdf/crypt.h +108 -0
- pymupdf/mupdf-devel/include/mupdf/pdf/document.h +902 -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 +381 -0
- pymupdf/mupdf-devel/include/mupdf/pdf/image-rewriter.h +78 -0
- pymupdf/mupdf-devel/include/mupdf/pdf/interpret.h +518 -0
- pymupdf/mupdf-devel/include/mupdf/pdf/javascript.h +43 -0
- pymupdf/mupdf-devel/include/mupdf/pdf/name-table.h +621 -0
- pymupdf/mupdf-devel/include/mupdf/pdf/object.h +445 -0
- pymupdf/mupdf-devel/include/mupdf/pdf/page.h +327 -0
- pymupdf/mupdf-devel/include/mupdf/pdf/parse.h +61 -0
- pymupdf/mupdf-devel/include/mupdf/pdf/recolor.h +49 -0
- pymupdf/mupdf-devel/include/mupdf/pdf/resource.h +144 -0
- pymupdf/mupdf-devel/include/mupdf/pdf/xref.h +300 -0
- pymupdf/mupdf-devel/include/mupdf/pdf/zugferd.h +49 -0
- pymupdf/mupdf-devel/include/mupdf/pdf.h +59 -0
- pymupdf/mupdf-devel/include/mupdf/ucdn.h +464 -0
- pymupdf/mupdf-devel/lib/libmuthreads.lib +0 -0
- pymupdf/mupdf-devel/lib/mupdfcpp64.lib +0 -0
- pymupdf/mupdf.py +63667 -0
- pymupdf/mupdfcpp64.dll +0 -0
- pymupdf/pymupdf.py +2 -0
- pymupdf/table.py +2697 -0
- pymupdf/utils.py +1169 -0
- pymupdf-1.26.6.dist-info/COPYING +1 -0
- pymupdf-1.26.6.dist-info/METADATA +83 -0
- pymupdf-1.26.6.dist-info/README.md +60 -0
- pymupdf-1.26.6.dist-info/RECORD +112 -0
- pymupdf-1.26.6.dist-info/WHEEL +4 -0
- pymupdf-1.26.6.dist-info/entry_points.txt +3 -0
pymupdf/utils.py
ADDED
|
@@ -0,0 +1,1169 @@
|
|
|
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
|
+
#
|
|
5
|
+
# Part of "PyMuPDF", a Python binding for "MuPDF" (http://mupdf.com), a
|
|
6
|
+
# lightweight PDF, XPS, and E-book viewer, renderer and toolkit which is
|
|
7
|
+
# maintained and developed by Artifex Software, Inc. https://artifex.com.
|
|
8
|
+
# ------------------------------------------------------------------------
|
|
9
|
+
import math
|
|
10
|
+
import typing
|
|
11
|
+
import weakref
|
|
12
|
+
|
|
13
|
+
try:
|
|
14
|
+
from . import pymupdf
|
|
15
|
+
except Exception:
|
|
16
|
+
import pymupdf
|
|
17
|
+
try:
|
|
18
|
+
from . import mupdf
|
|
19
|
+
except Exception:
|
|
20
|
+
import mupdf
|
|
21
|
+
|
|
22
|
+
_format_g = pymupdf.format_g
|
|
23
|
+
|
|
24
|
+
g_exceptions_verbose = pymupdf.g_exceptions_verbose
|
|
25
|
+
|
|
26
|
+
point_like = "point_like"
|
|
27
|
+
rect_like = "rect_like"
|
|
28
|
+
matrix_like = "matrix_like"
|
|
29
|
+
quad_like = "quad_like"
|
|
30
|
+
|
|
31
|
+
# ByteString is gone from typing in 3.14.
|
|
32
|
+
# collections.abc.Buffer available from 3.12 only
|
|
33
|
+
try:
|
|
34
|
+
ByteString = typing.ByteString
|
|
35
|
+
except AttributeError:
|
|
36
|
+
# pylint: disable=unsupported-binary-operation
|
|
37
|
+
ByteString = bytes | bytearray | memoryview
|
|
38
|
+
|
|
39
|
+
AnyType = typing.Any
|
|
40
|
+
OptInt = typing.Union[int, None]
|
|
41
|
+
OptFloat = typing.Optional[float]
|
|
42
|
+
OptStr = typing.Optional[str]
|
|
43
|
+
OptDict = typing.Optional[dict]
|
|
44
|
+
OptBytes = typing.Optional[ByteString]
|
|
45
|
+
OptSeq = typing.Optional[typing.Sequence]
|
|
46
|
+
|
|
47
|
+
"""
|
|
48
|
+
This is a collection of functions to extend PyMupdf.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def get_text_blocks(
|
|
53
|
+
page: pymupdf.Page,
|
|
54
|
+
clip: rect_like = None,
|
|
55
|
+
flags: OptInt = None,
|
|
56
|
+
textpage: pymupdf.TextPage = None,
|
|
57
|
+
sort: bool = False,
|
|
58
|
+
) -> list:
|
|
59
|
+
"""Return the text blocks on a page.
|
|
60
|
+
|
|
61
|
+
Notes:
|
|
62
|
+
Lines in a block are concatenated with line breaks.
|
|
63
|
+
Args:
|
|
64
|
+
flags: (int) control the amount of data parsed into the textpage.
|
|
65
|
+
Returns:
|
|
66
|
+
A list of the blocks. Each item contains the containing rectangle
|
|
67
|
+
coordinates, text lines, running block number and block type.
|
|
68
|
+
"""
|
|
69
|
+
pymupdf.CheckParent(page)
|
|
70
|
+
if flags is None:
|
|
71
|
+
flags = pymupdf.TEXTFLAGS_BLOCKS
|
|
72
|
+
tp = textpage
|
|
73
|
+
if tp is None:
|
|
74
|
+
tp = page.get_textpage(clip=clip, flags=flags)
|
|
75
|
+
elif getattr(tp, "parent") != page:
|
|
76
|
+
raise ValueError("not a textpage of this page")
|
|
77
|
+
|
|
78
|
+
blocks = tp.extractBLOCKS()
|
|
79
|
+
if textpage is None:
|
|
80
|
+
del tp
|
|
81
|
+
if sort:
|
|
82
|
+
blocks.sort(key=lambda b: (b[3], b[0]))
|
|
83
|
+
return blocks
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def get_text_words(
|
|
87
|
+
page: pymupdf.Page,
|
|
88
|
+
clip: rect_like = None,
|
|
89
|
+
flags: OptInt = None,
|
|
90
|
+
textpage: pymupdf.TextPage = None,
|
|
91
|
+
sort: bool = False,
|
|
92
|
+
delimiters=None,
|
|
93
|
+
tolerance=3,
|
|
94
|
+
) -> list:
|
|
95
|
+
"""Return the text words as a list with the bbox for each word.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
page: pymupdf.Page
|
|
99
|
+
clip: (rect-like) area on page to consider
|
|
100
|
+
flags: (int) control the amount of data parsed into the textpage.
|
|
101
|
+
textpage: (pymupdf.TextPage) either passed-in or None.
|
|
102
|
+
sort: (bool) sort the words in reading sequence.
|
|
103
|
+
delimiters: (str,list) characters to use as word delimiters.
|
|
104
|
+
tolerance: (float) consider words to be part of the same line if
|
|
105
|
+
top or bottom coordinate are not larger than this. Relevant
|
|
106
|
+
only if sort=True.
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
Word tuples (x0, y0, x1, y1, "word", bno, lno, wno).
|
|
110
|
+
"""
|
|
111
|
+
|
|
112
|
+
def sort_words(words):
|
|
113
|
+
"""Sort words line-wise, forgiving small deviations."""
|
|
114
|
+
words.sort(key=lambda w: (w[3], w[0]))
|
|
115
|
+
nwords = [] # final word list
|
|
116
|
+
line = [words[0]] # collects words roughly in same line
|
|
117
|
+
lrect = pymupdf.Rect(words[0][:4]) # start the line rectangle
|
|
118
|
+
for w in words[1:]:
|
|
119
|
+
wrect = pymupdf.Rect(w[:4])
|
|
120
|
+
if (
|
|
121
|
+
abs(wrect.y0 - lrect.y0) <= tolerance
|
|
122
|
+
or abs(wrect.y1 - lrect.y1) <= tolerance
|
|
123
|
+
):
|
|
124
|
+
line.append(w)
|
|
125
|
+
lrect |= wrect
|
|
126
|
+
else:
|
|
127
|
+
line.sort(key=lambda w: w[0]) # sort words in line l-t-r
|
|
128
|
+
nwords.extend(line) # append to final words list
|
|
129
|
+
line = [w] # start next line
|
|
130
|
+
lrect = wrect # start next line rect
|
|
131
|
+
|
|
132
|
+
line.sort(key=lambda w: w[0]) # sort words in line l-t-r
|
|
133
|
+
nwords.extend(line) # append to final words list
|
|
134
|
+
|
|
135
|
+
return nwords
|
|
136
|
+
|
|
137
|
+
pymupdf.CheckParent(page)
|
|
138
|
+
if flags is None:
|
|
139
|
+
flags = pymupdf.TEXTFLAGS_WORDS
|
|
140
|
+
tp = textpage
|
|
141
|
+
if tp is None:
|
|
142
|
+
tp = page.get_textpage(clip=clip, flags=flags)
|
|
143
|
+
elif getattr(tp, "parent") != page:
|
|
144
|
+
raise ValueError("not a textpage of this page")
|
|
145
|
+
|
|
146
|
+
words = tp.extractWORDS(delimiters)
|
|
147
|
+
|
|
148
|
+
# if textpage was given, we subselect the words in clip
|
|
149
|
+
if textpage is not None and clip is not None:
|
|
150
|
+
# sub-select words contained in clip
|
|
151
|
+
clip = pymupdf.Rect(clip)
|
|
152
|
+
words = [
|
|
153
|
+
w for w in words if abs(clip & w[:4]) >= 0.5 * abs(pymupdf.Rect(w[:4]))
|
|
154
|
+
]
|
|
155
|
+
|
|
156
|
+
if textpage is None:
|
|
157
|
+
del tp
|
|
158
|
+
if words and sort:
|
|
159
|
+
# advanced sort if any words found
|
|
160
|
+
words = sort_words(words)
|
|
161
|
+
|
|
162
|
+
return words
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def get_sorted_text(
|
|
166
|
+
page: pymupdf.Page,
|
|
167
|
+
clip: rect_like = None,
|
|
168
|
+
flags: OptInt = None,
|
|
169
|
+
textpage: pymupdf.TextPage = None,
|
|
170
|
+
tolerance=3,
|
|
171
|
+
) -> str:
|
|
172
|
+
"""Extract plain text avoiding unacceptable line breaks.
|
|
173
|
+
|
|
174
|
+
Text contained in clip will be sorted in reading sequence. Some effort
|
|
175
|
+
is also spent to simulate layout vertically and horizontally.
|
|
176
|
+
|
|
177
|
+
Args:
|
|
178
|
+
page: pymupdf.Page
|
|
179
|
+
clip: (rect-like) only consider text inside
|
|
180
|
+
flags: (int) text extraction flags
|
|
181
|
+
textpage: pymupdf.TextPage
|
|
182
|
+
tolerance: (float) consider words to be on the same line if their top
|
|
183
|
+
or bottom coordinates do not differ more than this.
|
|
184
|
+
|
|
185
|
+
Notes:
|
|
186
|
+
If a TextPage is provided, all text is checked for being inside clip
|
|
187
|
+
with at least 50% of its bbox.
|
|
188
|
+
This allows to use some "global" TextPage in conjunction with sub-
|
|
189
|
+
selecting words in parts of the defined TextPage rectangle.
|
|
190
|
+
|
|
191
|
+
Returns:
|
|
192
|
+
A text string in reading sequence. Left indentation of each line,
|
|
193
|
+
inter-line and inter-word distances strive to reflect the layout.
|
|
194
|
+
"""
|
|
195
|
+
|
|
196
|
+
def line_text(clip, line):
|
|
197
|
+
"""Create the string of one text line.
|
|
198
|
+
|
|
199
|
+
We are trying to simulate some horizontal layout here, too.
|
|
200
|
+
|
|
201
|
+
Args:
|
|
202
|
+
clip: (pymupdf.Rect) the area from which all text is being read.
|
|
203
|
+
line: (list) word tuples (rect, text) contained in the line
|
|
204
|
+
Returns:
|
|
205
|
+
Text in this line. Generated from words in 'line'. Distance from
|
|
206
|
+
predecessor is translated to multiple spaces, thus simulating
|
|
207
|
+
text indentations and large horizontal distances.
|
|
208
|
+
"""
|
|
209
|
+
line.sort(key=lambda w: w[0].x0)
|
|
210
|
+
ltext = "" # text in the line
|
|
211
|
+
x1 = clip.x0 # end coordinate of ltext
|
|
212
|
+
lrect = pymupdf.EMPTY_RECT() # bbox of this line
|
|
213
|
+
for r, t in line:
|
|
214
|
+
lrect |= r # update line bbox
|
|
215
|
+
# convert distance to previous word to multiple spaces
|
|
216
|
+
dist = max(
|
|
217
|
+
int(round((r.x0 - x1) / r.width * len(t))),
|
|
218
|
+
0 if (x1 == clip.x0 or r.x0 <= x1) else 1,
|
|
219
|
+
) # number of space characters
|
|
220
|
+
|
|
221
|
+
ltext += " " * dist + t # append word string
|
|
222
|
+
x1 = r.x1 # update new end position
|
|
223
|
+
return ltext
|
|
224
|
+
|
|
225
|
+
# Extract words in correct sequence first.
|
|
226
|
+
words = [
|
|
227
|
+
(pymupdf.Rect(w[:4]), w[4])
|
|
228
|
+
for w in get_text_words(
|
|
229
|
+
page,
|
|
230
|
+
clip=clip,
|
|
231
|
+
flags=flags,
|
|
232
|
+
textpage=textpage,
|
|
233
|
+
sort=True,
|
|
234
|
+
tolerance=tolerance,
|
|
235
|
+
)
|
|
236
|
+
]
|
|
237
|
+
|
|
238
|
+
if not words: # no text present
|
|
239
|
+
return ""
|
|
240
|
+
totalbox = pymupdf.EMPTY_RECT() # area covering all text
|
|
241
|
+
for wr, text in words:
|
|
242
|
+
totalbox |= wr
|
|
243
|
+
|
|
244
|
+
lines = [] # list of reconstituted lines
|
|
245
|
+
line = [words[0]] # current line
|
|
246
|
+
lrect = words[0][0] # the line's rectangle
|
|
247
|
+
|
|
248
|
+
# walk through the words
|
|
249
|
+
for wr, text in words[1:]: # start with second word
|
|
250
|
+
w0r, _ = line[-1] # read previous word in current line
|
|
251
|
+
|
|
252
|
+
# if this word matches top or bottom of the line, append it
|
|
253
|
+
if abs(lrect.y0 - wr.y0) <= tolerance or abs(lrect.y1 - wr.y1) <= tolerance:
|
|
254
|
+
line.append((wr, text))
|
|
255
|
+
lrect |= wr
|
|
256
|
+
else:
|
|
257
|
+
# output current line and re-initialize
|
|
258
|
+
ltext = line_text(totalbox, line)
|
|
259
|
+
lines.append((lrect, ltext))
|
|
260
|
+
line = [(wr, text)]
|
|
261
|
+
lrect = wr
|
|
262
|
+
|
|
263
|
+
# also append unfinished last line
|
|
264
|
+
ltext = line_text(totalbox, line)
|
|
265
|
+
lines.append((lrect, ltext))
|
|
266
|
+
|
|
267
|
+
# sort all lines vertically
|
|
268
|
+
lines.sort(key=lambda l: (l[0].y1))
|
|
269
|
+
|
|
270
|
+
text = lines[0][1] # text of first line
|
|
271
|
+
y1 = lines[0][0].y1 # its bottom coordinate
|
|
272
|
+
for lrect, ltext in lines[1:]:
|
|
273
|
+
distance = min(int(round((lrect.y0 - y1) / lrect.height)), 5)
|
|
274
|
+
breaks = "\n" * (distance + 1)
|
|
275
|
+
text += breaks + ltext
|
|
276
|
+
y1 = lrect.y1
|
|
277
|
+
|
|
278
|
+
# return text in clip
|
|
279
|
+
return text
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def get_textbox(
|
|
283
|
+
page: pymupdf.Page,
|
|
284
|
+
rect: rect_like,
|
|
285
|
+
textpage: pymupdf.TextPage = None,
|
|
286
|
+
) -> str:
|
|
287
|
+
tp = textpage
|
|
288
|
+
if tp is None:
|
|
289
|
+
tp = page.get_textpage()
|
|
290
|
+
elif getattr(tp, "parent") != page:
|
|
291
|
+
raise ValueError("not a textpage of this page")
|
|
292
|
+
rc = tp.extractTextbox(rect)
|
|
293
|
+
if textpage is None:
|
|
294
|
+
del tp
|
|
295
|
+
return rc
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def get_text_selection(
|
|
299
|
+
page: pymupdf.Page,
|
|
300
|
+
p1: point_like,
|
|
301
|
+
p2: point_like,
|
|
302
|
+
clip: rect_like = None,
|
|
303
|
+
textpage: pymupdf.TextPage = None,
|
|
304
|
+
):
|
|
305
|
+
pymupdf.CheckParent(page)
|
|
306
|
+
tp = textpage
|
|
307
|
+
if tp is None:
|
|
308
|
+
tp = page.get_textpage(clip=clip, flags=pymupdf.TEXT_DEHYPHENATE)
|
|
309
|
+
elif getattr(tp, "parent") != page:
|
|
310
|
+
raise ValueError("not a textpage of this page")
|
|
311
|
+
rc = tp.extractSelection(p1, p2)
|
|
312
|
+
if textpage is None:
|
|
313
|
+
del tp
|
|
314
|
+
return rc
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def get_textpage_ocr(
|
|
318
|
+
page: pymupdf.Page,
|
|
319
|
+
flags: int = 0,
|
|
320
|
+
language: str = "eng",
|
|
321
|
+
dpi: int = 72,
|
|
322
|
+
full: bool = False,
|
|
323
|
+
tessdata: str = None,
|
|
324
|
+
) -> pymupdf.TextPage:
|
|
325
|
+
"""Create a Textpage from combined results of normal and OCR text parsing.
|
|
326
|
+
|
|
327
|
+
Args:
|
|
328
|
+
flags: (int) control content becoming part of the result.
|
|
329
|
+
language: (str) specify expected language(s). Default is "eng" (English).
|
|
330
|
+
dpi: (int) resolution in dpi, default 72.
|
|
331
|
+
full: (bool) whether to OCR the full page image, or only its images (default)
|
|
332
|
+
"""
|
|
333
|
+
pymupdf.CheckParent(page)
|
|
334
|
+
tessdata = pymupdf.get_tessdata(tessdata)
|
|
335
|
+
|
|
336
|
+
def full_ocr(page, dpi, language, flags):
|
|
337
|
+
zoom = dpi / 72
|
|
338
|
+
mat = pymupdf.Matrix(zoom, zoom)
|
|
339
|
+
pix = page.get_pixmap(matrix=mat)
|
|
340
|
+
ocr_pdf = pymupdf.Document(
|
|
341
|
+
"pdf",
|
|
342
|
+
pix.pdfocr_tobytes(
|
|
343
|
+
compress=False,
|
|
344
|
+
language=language,
|
|
345
|
+
tessdata=tessdata,
|
|
346
|
+
),
|
|
347
|
+
)
|
|
348
|
+
ocr_page = ocr_pdf.load_page(0)
|
|
349
|
+
unzoom = page.rect.width / ocr_page.rect.width
|
|
350
|
+
ctm = pymupdf.Matrix(unzoom, unzoom) * page.derotation_matrix
|
|
351
|
+
tpage = ocr_page.get_textpage(flags=flags, matrix=ctm)
|
|
352
|
+
ocr_pdf.close()
|
|
353
|
+
pix = None
|
|
354
|
+
tpage.parent = weakref.proxy(page)
|
|
355
|
+
return tpage
|
|
356
|
+
|
|
357
|
+
# if OCR for the full page, OCR its pixmap @ desired dpi
|
|
358
|
+
if full:
|
|
359
|
+
return full_ocr(page, dpi, language, flags)
|
|
360
|
+
|
|
361
|
+
# For partial OCR, make a normal textpage, then extend it with text that
|
|
362
|
+
# is OCRed from each image.
|
|
363
|
+
# Because of this, we need the images flag bit set ON.
|
|
364
|
+
tpage = page.get_textpage(flags=flags)
|
|
365
|
+
for block in page.get_text("dict", flags=pymupdf.TEXT_PRESERVE_IMAGES)["blocks"]:
|
|
366
|
+
if block["type"] != 1: # only look at images
|
|
367
|
+
continue
|
|
368
|
+
bbox = pymupdf.Rect(block["bbox"])
|
|
369
|
+
if bbox.width <= 3 or bbox.height <= 3: # ignore tiny stuff
|
|
370
|
+
continue
|
|
371
|
+
try:
|
|
372
|
+
pix = pymupdf.Pixmap(block["image"]) # get image pixmap
|
|
373
|
+
if pix.n - pix.alpha != 3: # we need to convert this to RGB!
|
|
374
|
+
pix = pymupdf.Pixmap(pymupdf.csRGB, pix)
|
|
375
|
+
if pix.alpha: # must remove alpha channel
|
|
376
|
+
pix = pymupdf.Pixmap(pix, 0)
|
|
377
|
+
imgdoc = pymupdf.Document(
|
|
378
|
+
"pdf",
|
|
379
|
+
pix.pdfocr_tobytes(language=language, tessdata=tessdata),
|
|
380
|
+
) # pdf with OCRed page
|
|
381
|
+
imgpage = imgdoc.load_page(0) # read image as a page
|
|
382
|
+
pix = None
|
|
383
|
+
# compute matrix to transform coordinates back to that of 'page'
|
|
384
|
+
imgrect = imgpage.rect # page size of image PDF
|
|
385
|
+
shrink = pymupdf.Matrix(1 / imgrect.width, 1 / imgrect.height)
|
|
386
|
+
mat = shrink * block["transform"]
|
|
387
|
+
imgpage.extend_textpage(tpage, flags=0, matrix=mat)
|
|
388
|
+
imgdoc.close()
|
|
389
|
+
except (RuntimeError, mupdf.FzErrorBase):
|
|
390
|
+
if 0 and g_exceptions_verbose:
|
|
391
|
+
# Don't show exception info here because it can happen in
|
|
392
|
+
# normal operation (see test_3842b).
|
|
393
|
+
pymupdf.exception_info()
|
|
394
|
+
tpage = None
|
|
395
|
+
pymupdf.message("Falling back to full page OCR")
|
|
396
|
+
return full_ocr(page, dpi, language, flags)
|
|
397
|
+
|
|
398
|
+
return tpage
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
def get_text(
|
|
402
|
+
page: pymupdf.Page,
|
|
403
|
+
option: str = "text",
|
|
404
|
+
*,
|
|
405
|
+
clip: rect_like = None,
|
|
406
|
+
flags: OptInt = None,
|
|
407
|
+
textpage: pymupdf.TextPage = None,
|
|
408
|
+
sort: bool = False,
|
|
409
|
+
delimiters=None,
|
|
410
|
+
tolerance=3,
|
|
411
|
+
):
|
|
412
|
+
"""Extract text from a page or an annotation.
|
|
413
|
+
|
|
414
|
+
This is a unifying wrapper for various methods of the pymupdf.TextPage class.
|
|
415
|
+
|
|
416
|
+
Args:
|
|
417
|
+
option: (str) text, words, blocks, html, dict, json, rawdict, xhtml or xml.
|
|
418
|
+
clip: (rect-like) restrict output to this area.
|
|
419
|
+
flags: bit switches to e.g. exclude images or decompose ligatures.
|
|
420
|
+
textpage: reuse this pymupdf.TextPage and make no new one. If specified,
|
|
421
|
+
'flags' and 'clip' are ignored.
|
|
422
|
+
|
|
423
|
+
Returns:
|
|
424
|
+
the output of methods get_text_words / get_text_blocks or pymupdf.TextPage
|
|
425
|
+
methods extractText, extractHTML, extractDICT, extractJSON, extractRAWDICT,
|
|
426
|
+
extractXHTML or etractXML respectively.
|
|
427
|
+
Default and misspelling choice is "text".
|
|
428
|
+
"""
|
|
429
|
+
formats = {
|
|
430
|
+
"text": pymupdf.TEXTFLAGS_TEXT,
|
|
431
|
+
"html": pymupdf.TEXTFLAGS_HTML,
|
|
432
|
+
"json": pymupdf.TEXTFLAGS_DICT,
|
|
433
|
+
"rawjson": pymupdf.TEXTFLAGS_RAWDICT,
|
|
434
|
+
"xml": pymupdf.TEXTFLAGS_XML,
|
|
435
|
+
"xhtml": pymupdf.TEXTFLAGS_XHTML,
|
|
436
|
+
"dict": pymupdf.TEXTFLAGS_DICT,
|
|
437
|
+
"rawdict": pymupdf.TEXTFLAGS_RAWDICT,
|
|
438
|
+
"words": pymupdf.TEXTFLAGS_WORDS,
|
|
439
|
+
"blocks": pymupdf.TEXTFLAGS_BLOCKS,
|
|
440
|
+
}
|
|
441
|
+
option = option.lower()
|
|
442
|
+
assert option in formats
|
|
443
|
+
if option not in formats:
|
|
444
|
+
option = "text"
|
|
445
|
+
if flags is None:
|
|
446
|
+
flags = formats[option]
|
|
447
|
+
|
|
448
|
+
if option == "words":
|
|
449
|
+
return get_text_words(
|
|
450
|
+
page,
|
|
451
|
+
clip=clip,
|
|
452
|
+
flags=flags,
|
|
453
|
+
textpage=textpage,
|
|
454
|
+
sort=sort,
|
|
455
|
+
delimiters=delimiters,
|
|
456
|
+
)
|
|
457
|
+
if option == "blocks":
|
|
458
|
+
return get_text_blocks(
|
|
459
|
+
page, clip=clip, flags=flags, textpage=textpage, sort=sort
|
|
460
|
+
)
|
|
461
|
+
|
|
462
|
+
if option == "text" and sort:
|
|
463
|
+
return get_sorted_text(
|
|
464
|
+
page,
|
|
465
|
+
clip=clip,
|
|
466
|
+
flags=flags,
|
|
467
|
+
textpage=textpage,
|
|
468
|
+
tolerance=tolerance,
|
|
469
|
+
)
|
|
470
|
+
|
|
471
|
+
pymupdf.CheckParent(page)
|
|
472
|
+
cb = None
|
|
473
|
+
if option in ("html", "xml", "xhtml"): # no clipping for MuPDF functions
|
|
474
|
+
clip = page.cropbox
|
|
475
|
+
if clip is not None:
|
|
476
|
+
clip = pymupdf.Rect(clip)
|
|
477
|
+
cb = None
|
|
478
|
+
elif type(page) is pymupdf.Page:
|
|
479
|
+
cb = page.cropbox
|
|
480
|
+
# pymupdf.TextPage with or without images
|
|
481
|
+
tp = textpage
|
|
482
|
+
#pymupdf.exception_info()
|
|
483
|
+
if tp is None:
|
|
484
|
+
tp = page.get_textpage(clip=clip, flags=flags)
|
|
485
|
+
elif getattr(tp, "parent") != page:
|
|
486
|
+
raise ValueError("not a textpage of this page")
|
|
487
|
+
#pymupdf.log( '{option=}')
|
|
488
|
+
if option == "json":
|
|
489
|
+
t = tp.extractJSON(cb=cb, sort=sort)
|
|
490
|
+
elif option == "rawjson":
|
|
491
|
+
t = tp.extractRAWJSON(cb=cb, sort=sort)
|
|
492
|
+
elif option == "dict":
|
|
493
|
+
t = tp.extractDICT(cb=cb, sort=sort)
|
|
494
|
+
elif option == "rawdict":
|
|
495
|
+
t = tp.extractRAWDICT(cb=cb, sort=sort)
|
|
496
|
+
elif option == "html":
|
|
497
|
+
t = tp.extractHTML()
|
|
498
|
+
elif option == "xml":
|
|
499
|
+
t = tp.extractXML()
|
|
500
|
+
elif option == "xhtml":
|
|
501
|
+
t = tp.extractXHTML()
|
|
502
|
+
else:
|
|
503
|
+
t = tp.extractText(sort=sort)
|
|
504
|
+
|
|
505
|
+
if textpage is None:
|
|
506
|
+
del tp
|
|
507
|
+
return t
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
def getLinkDict(ln, document=None) -> dict:
|
|
511
|
+
if isinstance(ln, pymupdf.Outline):
|
|
512
|
+
dest = ln.destination(document)
|
|
513
|
+
elif isinstance(ln, pymupdf.Link):
|
|
514
|
+
dest = ln.dest
|
|
515
|
+
else:
|
|
516
|
+
assert 0, f'Unexpected {type(ln)=}.'
|
|
517
|
+
nl = {"kind": dest.kind, "xref": 0}
|
|
518
|
+
try:
|
|
519
|
+
if hasattr(ln, 'rect'):
|
|
520
|
+
nl["from"] = ln.rect
|
|
521
|
+
except Exception:
|
|
522
|
+
# This seems to happen quite often in PyMuPDF/tests.
|
|
523
|
+
if g_exceptions_verbose >= 2: pymupdf.exception_info()
|
|
524
|
+
pass
|
|
525
|
+
pnt = pymupdf.Point(0, 0)
|
|
526
|
+
if dest.flags & pymupdf.LINK_FLAG_L_VALID:
|
|
527
|
+
pnt.x = dest.lt.x
|
|
528
|
+
if dest.flags & pymupdf.LINK_FLAG_T_VALID:
|
|
529
|
+
pnt.y = dest.lt.y
|
|
530
|
+
|
|
531
|
+
if dest.kind == pymupdf.LINK_URI:
|
|
532
|
+
nl["uri"] = dest.uri
|
|
533
|
+
|
|
534
|
+
elif dest.kind == pymupdf.LINK_GOTO:
|
|
535
|
+
nl["page"] = dest.page
|
|
536
|
+
nl["to"] = pnt
|
|
537
|
+
if dest.flags & pymupdf.LINK_FLAG_R_IS_ZOOM:
|
|
538
|
+
nl["zoom"] = dest.rb.x
|
|
539
|
+
else:
|
|
540
|
+
nl["zoom"] = 0.0
|
|
541
|
+
|
|
542
|
+
elif dest.kind == pymupdf.LINK_GOTOR:
|
|
543
|
+
nl["file"] = dest.file_spec.replace("\\", "/")
|
|
544
|
+
nl["page"] = dest.page
|
|
545
|
+
if dest.page < 0:
|
|
546
|
+
nl["to"] = dest.dest
|
|
547
|
+
else:
|
|
548
|
+
nl["to"] = pnt
|
|
549
|
+
if dest.flags & pymupdf.LINK_FLAG_R_IS_ZOOM:
|
|
550
|
+
nl["zoom"] = dest.rb.x
|
|
551
|
+
else:
|
|
552
|
+
nl["zoom"] = 0.0
|
|
553
|
+
|
|
554
|
+
elif dest.kind == pymupdf.LINK_LAUNCH:
|
|
555
|
+
nl["file"] = dest.file_spec.replace("\\", "/")
|
|
556
|
+
|
|
557
|
+
elif dest.kind == pymupdf.LINK_NAMED:
|
|
558
|
+
# The dicts should not have same key(s).
|
|
559
|
+
assert not (dest.named.keys() & nl.keys())
|
|
560
|
+
nl.update(dest.named)
|
|
561
|
+
if 'to' in nl:
|
|
562
|
+
nl['to'] = pymupdf.Point(nl['to'])
|
|
563
|
+
|
|
564
|
+
else:
|
|
565
|
+
nl["page"] = dest.page
|
|
566
|
+
return nl
|
|
567
|
+
|
|
568
|
+
|
|
569
|
+
def getDestStr(xref: int, ddict: dict) -> str:
|
|
570
|
+
"""Calculate the PDF action string.
|
|
571
|
+
|
|
572
|
+
Notes:
|
|
573
|
+
Supports Link annotations and outline items (bookmarks).
|
|
574
|
+
"""
|
|
575
|
+
if not ddict:
|
|
576
|
+
return ""
|
|
577
|
+
str_goto = lambda a, b, c, d: f"/A<</S/GoTo/D[{a} 0 R/XYZ {_format_g((b, c, d))}]>>"
|
|
578
|
+
str_gotor1 = lambda a, b, c, d, e, f: f"/A<</S/GoToR/D[{a} /XYZ {_format_g((b, c, d))}]/F<</F{e}/UF{f}/Type/Filespec>>>>"
|
|
579
|
+
str_gotor2 = lambda a, b, c: f"/A<</S/GoToR/D{a}/F<</F{b}/UF{c}/Type/Filespec>>>>"
|
|
580
|
+
str_launch = lambda a, b: f"/A<</S/Launch/F<</F{a}/UF{b}/Type/Filespec>>>>"
|
|
581
|
+
str_uri = lambda a: f"/A<</S/URI/URI{a}>>"
|
|
582
|
+
|
|
583
|
+
if type(ddict) in (int, float):
|
|
584
|
+
dest = str_goto(xref, 0, ddict, 0)
|
|
585
|
+
return dest
|
|
586
|
+
d_kind = ddict.get("kind", pymupdf.LINK_NONE)
|
|
587
|
+
|
|
588
|
+
if d_kind == pymupdf.LINK_NONE:
|
|
589
|
+
return ""
|
|
590
|
+
|
|
591
|
+
if ddict["kind"] == pymupdf.LINK_GOTO:
|
|
592
|
+
d_zoom = ddict.get("zoom", 0)
|
|
593
|
+
to = ddict.get("to", pymupdf.Point(0, 0))
|
|
594
|
+
d_left, d_top = to
|
|
595
|
+
dest = str_goto(xref, d_left, d_top, d_zoom)
|
|
596
|
+
return dest
|
|
597
|
+
|
|
598
|
+
if ddict["kind"] == pymupdf.LINK_URI:
|
|
599
|
+
dest = str_uri(pymupdf.get_pdf_str(ddict["uri"]),)
|
|
600
|
+
return dest
|
|
601
|
+
|
|
602
|
+
if ddict["kind"] == pymupdf.LINK_LAUNCH:
|
|
603
|
+
fspec = pymupdf.get_pdf_str(ddict["file"])
|
|
604
|
+
dest = str_launch(fspec, fspec)
|
|
605
|
+
return dest
|
|
606
|
+
|
|
607
|
+
if ddict["kind"] == pymupdf.LINK_GOTOR and ddict["page"] < 0:
|
|
608
|
+
fspec = pymupdf.get_pdf_str(ddict["file"])
|
|
609
|
+
dest = str_gotor2(pymupdf.get_pdf_str(ddict["to"]), fspec, fspec)
|
|
610
|
+
return dest
|
|
611
|
+
|
|
612
|
+
if ddict["kind"] == pymupdf.LINK_GOTOR and ddict["page"] >= 0:
|
|
613
|
+
fspec = pymupdf.get_pdf_str(ddict["file"])
|
|
614
|
+
dest = str_gotor1(
|
|
615
|
+
ddict["page"],
|
|
616
|
+
ddict["to"].x,
|
|
617
|
+
ddict["to"].y,
|
|
618
|
+
ddict["zoom"],
|
|
619
|
+
fspec,
|
|
620
|
+
fspec,
|
|
621
|
+
)
|
|
622
|
+
return dest
|
|
623
|
+
|
|
624
|
+
return ""
|
|
625
|
+
|
|
626
|
+
|
|
627
|
+
def getLinkText(page: pymupdf.Page, lnk: dict) -> str:
|
|
628
|
+
# --------------------------------------------------------------------------
|
|
629
|
+
# define skeletons for /Annots object texts
|
|
630
|
+
# --------------------------------------------------------------------------
|
|
631
|
+
ctm = page.transformation_matrix
|
|
632
|
+
ictm = ~ctm
|
|
633
|
+
r = lnk["from"]
|
|
634
|
+
rect = _format_g(tuple(r * ictm))
|
|
635
|
+
|
|
636
|
+
annot = ""
|
|
637
|
+
if lnk["kind"] == pymupdf.LINK_GOTO:
|
|
638
|
+
if lnk["page"] >= 0:
|
|
639
|
+
txt = pymupdf.annot_skel["goto1"] # annot_goto
|
|
640
|
+
pno = lnk["page"]
|
|
641
|
+
xref = page.parent.page_xref(pno)
|
|
642
|
+
pnt = lnk.get("to", pymupdf.Point(0, 0)) # destination point
|
|
643
|
+
dest_page = page.parent[pno]
|
|
644
|
+
dest_ctm = dest_page.transformation_matrix
|
|
645
|
+
dest_ictm = ~dest_ctm
|
|
646
|
+
ipnt = pnt * dest_ictm
|
|
647
|
+
annot = txt(xref, ipnt.x, ipnt.y, lnk.get("zoom", 0), rect)
|
|
648
|
+
else:
|
|
649
|
+
txt = pymupdf.annot_skel["goto2"] # annot_goto_n
|
|
650
|
+
annot = txt(pymupdf.get_pdf_str(lnk["to"]), rect)
|
|
651
|
+
|
|
652
|
+
elif lnk["kind"] == pymupdf.LINK_GOTOR:
|
|
653
|
+
if lnk["page"] >= 0:
|
|
654
|
+
txt = pymupdf.annot_skel["gotor1"] # annot_gotor
|
|
655
|
+
pnt = lnk.get("to", pymupdf.Point(0, 0)) # destination point
|
|
656
|
+
if type(pnt) is not pymupdf.Point:
|
|
657
|
+
pnt = pymupdf.Point(0, 0)
|
|
658
|
+
annot = txt(
|
|
659
|
+
lnk["page"],
|
|
660
|
+
pnt.x,
|
|
661
|
+
pnt.y,
|
|
662
|
+
lnk.get("zoom", 0),
|
|
663
|
+
lnk["file"],
|
|
664
|
+
lnk["file"],
|
|
665
|
+
rect,
|
|
666
|
+
)
|
|
667
|
+
else:
|
|
668
|
+
txt = pymupdf.annot_skel["gotor2"] # annot_gotor_n
|
|
669
|
+
annot = txt(pymupdf.get_pdf_str(lnk["to"]), lnk["file"], rect)
|
|
670
|
+
|
|
671
|
+
elif lnk["kind"] == pymupdf.LINK_LAUNCH:
|
|
672
|
+
txt = pymupdf.annot_skel["launch"] # annot_launch
|
|
673
|
+
annot = txt(lnk["file"], lnk["file"], rect)
|
|
674
|
+
|
|
675
|
+
elif lnk["kind"] == pymupdf.LINK_URI:
|
|
676
|
+
txt = pymupdf.annot_skel["uri"] # txt = annot_uri
|
|
677
|
+
annot = txt(lnk["uri"], rect)
|
|
678
|
+
|
|
679
|
+
elif lnk["kind"] == pymupdf.LINK_NAMED:
|
|
680
|
+
txt = pymupdf.annot_skel["named"] # annot_named
|
|
681
|
+
lname = lnk.get("name") # check presence of key
|
|
682
|
+
if lname is None: # if missing, fall back to alternative
|
|
683
|
+
lname = lnk["nameddest"]
|
|
684
|
+
annot = txt(lname, rect)
|
|
685
|
+
if not annot:
|
|
686
|
+
return annot
|
|
687
|
+
|
|
688
|
+
# add a /NM PDF key to the object definition
|
|
689
|
+
link_names = dict( # existing ids and their xref
|
|
690
|
+
[(x[0], x[2]) for x in page.annot_xrefs() if x[1] == pymupdf.PDF_ANNOT_LINK] # pylint: disable=no-member
|
|
691
|
+
)
|
|
692
|
+
|
|
693
|
+
old_name = lnk.get("id", "") # id value in the argument
|
|
694
|
+
|
|
695
|
+
if old_name and (lnk["xref"], old_name) in link_names.items():
|
|
696
|
+
name = old_name # no new name if this is an update only
|
|
697
|
+
else:
|
|
698
|
+
i = 0
|
|
699
|
+
stem = pymupdf.TOOLS.set_annot_stem() + "-L%i"
|
|
700
|
+
while True:
|
|
701
|
+
name = stem % i
|
|
702
|
+
if name not in link_names.values():
|
|
703
|
+
break
|
|
704
|
+
i += 1
|
|
705
|
+
# add /NM key to object definition
|
|
706
|
+
annot = annot.replace("/Link", "/Link/NM(%s)" % name)
|
|
707
|
+
return annot
|
|
708
|
+
|
|
709
|
+
|
|
710
|
+
# ----------------------------------------------------------------------
|
|
711
|
+
# Name: wx.lib.colourdb.py
|
|
712
|
+
# Purpose: Adds a bunch of colour names and RGB values to the
|
|
713
|
+
# colour database so they can be found by name
|
|
714
|
+
#
|
|
715
|
+
# Author: Robin Dunn
|
|
716
|
+
#
|
|
717
|
+
# Created: 13-March-2001
|
|
718
|
+
# Copyright: (c) 2001-2017 by Total Control Software
|
|
719
|
+
# Licence: wxWindows license
|
|
720
|
+
# Tags: phoenix-port, unittest, documented
|
|
721
|
+
# ----------------------------------------------------------------------
|
|
722
|
+
|
|
723
|
+
|
|
724
|
+
def getColorList() -> list:
|
|
725
|
+
"""
|
|
726
|
+
Returns a list of upper-case colour names.
|
|
727
|
+
:rtype: list of strings
|
|
728
|
+
"""
|
|
729
|
+
return [name for name, r, g, b in pymupdf.colors_wx_list()]
|
|
730
|
+
|
|
731
|
+
|
|
732
|
+
def getColorInfoList() -> list:
|
|
733
|
+
"""
|
|
734
|
+
Returns list of (name, red, gree, blue) tuples, where:
|
|
735
|
+
name: upper-case color name.
|
|
736
|
+
read, green, blue: integers in range 0..255.
|
|
737
|
+
:rtype: list of tuples
|
|
738
|
+
"""
|
|
739
|
+
return pymupdf.colors_wx_list()
|
|
740
|
+
|
|
741
|
+
|
|
742
|
+
def getColor(name: str) -> tuple:
|
|
743
|
+
"""Retrieve RGB color in PDF format by name.
|
|
744
|
+
|
|
745
|
+
Returns:
|
|
746
|
+
a triple of floats in range 0 to 1. In case of name-not-found, "white" is returned.
|
|
747
|
+
"""
|
|
748
|
+
return pymupdf.colors_pdf_dict().get(name.lower(), (1, 1, 1))
|
|
749
|
+
|
|
750
|
+
|
|
751
|
+
def getColorHSV(name: str) -> tuple:
|
|
752
|
+
"""Retrieve the hue, saturation, value triple of a color name.
|
|
753
|
+
|
|
754
|
+
Returns:
|
|
755
|
+
a triple (degree, percent, percent). If not found (-1, -1, -1) is returned.
|
|
756
|
+
"""
|
|
757
|
+
try:
|
|
758
|
+
x = getColorInfoList()[getColorList().index(name.upper())]
|
|
759
|
+
except Exception:
|
|
760
|
+
if g_exceptions_verbose: pymupdf.exception_info()
|
|
761
|
+
return (-1, -1, -1)
|
|
762
|
+
|
|
763
|
+
r = x[1] / 255.0
|
|
764
|
+
g = x[2] / 255.0
|
|
765
|
+
b = x[3] / 255.0
|
|
766
|
+
cmax = max(r, g, b)
|
|
767
|
+
V = round(cmax * 100, 1)
|
|
768
|
+
cmin = min(r, g, b)
|
|
769
|
+
delta = cmax - cmin
|
|
770
|
+
if delta == 0:
|
|
771
|
+
hue = 0
|
|
772
|
+
elif cmax == r:
|
|
773
|
+
hue = 60.0 * (((g - b) / delta) % 6)
|
|
774
|
+
elif cmax == g:
|
|
775
|
+
hue = 60.0 * (((b - r) / delta) + 2)
|
|
776
|
+
else:
|
|
777
|
+
hue = 60.0 * (((r - g) / delta) + 4)
|
|
778
|
+
|
|
779
|
+
H = int(round(hue))
|
|
780
|
+
|
|
781
|
+
if cmax == 0:
|
|
782
|
+
sat = 0
|
|
783
|
+
else:
|
|
784
|
+
sat = delta / cmax
|
|
785
|
+
S = int(round(sat * 100))
|
|
786
|
+
|
|
787
|
+
return (H, S, V)
|
|
788
|
+
|
|
789
|
+
|
|
790
|
+
def _get_font_properties(doc: pymupdf.Document, xref: int) -> tuple:
|
|
791
|
+
fontname, ext, stype, buffer = doc.extract_font(xref)
|
|
792
|
+
asc = 0.8
|
|
793
|
+
dsc = -0.2
|
|
794
|
+
if ext == "":
|
|
795
|
+
return fontname, ext, stype, asc, dsc
|
|
796
|
+
|
|
797
|
+
if buffer:
|
|
798
|
+
try:
|
|
799
|
+
font = pymupdf.Font(fontbuffer=buffer)
|
|
800
|
+
asc = font.ascender
|
|
801
|
+
dsc = font.descender
|
|
802
|
+
bbox = font.bbox
|
|
803
|
+
if asc - dsc < 1:
|
|
804
|
+
if bbox.y0 < dsc:
|
|
805
|
+
dsc = bbox.y0
|
|
806
|
+
asc = 1 - dsc
|
|
807
|
+
except Exception:
|
|
808
|
+
pymupdf.exception_info()
|
|
809
|
+
asc *= 1.2
|
|
810
|
+
dsc *= 1.2
|
|
811
|
+
return fontname, ext, stype, asc, dsc
|
|
812
|
+
if ext != "n/a":
|
|
813
|
+
try:
|
|
814
|
+
font = pymupdf.Font(fontname)
|
|
815
|
+
asc = font.ascender
|
|
816
|
+
dsc = font.descender
|
|
817
|
+
except Exception:
|
|
818
|
+
pymupdf.exception_info()
|
|
819
|
+
asc *= 1.2
|
|
820
|
+
dsc *= 1.2
|
|
821
|
+
else:
|
|
822
|
+
asc *= 1.2
|
|
823
|
+
dsc *= 1.2
|
|
824
|
+
return fontname, ext, stype, asc, dsc
|
|
825
|
+
|
|
826
|
+
|
|
827
|
+
def _show_fz_text( text):
|
|
828
|
+
#if mupdf_cppyy:
|
|
829
|
+
# assert isinstance( text, cppyy.gbl.mupdf.Text)
|
|
830
|
+
#else:
|
|
831
|
+
# assert isinstance( text, mupdf.Text)
|
|
832
|
+
num_spans = 0
|
|
833
|
+
num_chars = 0
|
|
834
|
+
span = text.m_internal.head
|
|
835
|
+
while 1:
|
|
836
|
+
if not span:
|
|
837
|
+
break
|
|
838
|
+
num_spans += 1
|
|
839
|
+
num_chars += span.len
|
|
840
|
+
span = span.next
|
|
841
|
+
return f'num_spans={num_spans} num_chars={num_chars}'
|
|
842
|
+
|
|
843
|
+
|
|
844
|
+
"""
|
|
845
|
+
Handle page labels for PDF documents.
|
|
846
|
+
|
|
847
|
+
Reading
|
|
848
|
+
-------
|
|
849
|
+
* compute the label of a page
|
|
850
|
+
* find page number(s) having the given label.
|
|
851
|
+
|
|
852
|
+
Writing
|
|
853
|
+
-------
|
|
854
|
+
Supports setting (defining) page labels for PDF documents.
|
|
855
|
+
|
|
856
|
+
A big Thank You goes to WILLIAM CHAPMAN who contributed the idea and
|
|
857
|
+
significant parts of the following code during late December 2020
|
|
858
|
+
through early January 2021.
|
|
859
|
+
"""
|
|
860
|
+
|
|
861
|
+
|
|
862
|
+
def rule_dict(item):
|
|
863
|
+
"""Make a Python dict from a PDF page label rule.
|
|
864
|
+
|
|
865
|
+
Args:
|
|
866
|
+
item -- a tuple (pno, rule) with the start page number and the rule
|
|
867
|
+
string like <</S/D...>>.
|
|
868
|
+
Returns:
|
|
869
|
+
A dict like
|
|
870
|
+
{'startpage': int, 'prefix': str, 'style': str, 'firstpagenum': int}.
|
|
871
|
+
"""
|
|
872
|
+
# Jorj McKie, 2021-01-06
|
|
873
|
+
|
|
874
|
+
pno, rule = item
|
|
875
|
+
rule = rule[2:-2].split("/")[1:] # strip "<<" and ">>"
|
|
876
|
+
d = {"startpage": pno, "prefix": "", "firstpagenum": 1}
|
|
877
|
+
skip = False
|
|
878
|
+
for i, item in enumerate(rule): # pylint: disable=redefined-argument-from-local
|
|
879
|
+
if skip: # this item has already been processed
|
|
880
|
+
skip = False # deactivate skipping again
|
|
881
|
+
continue
|
|
882
|
+
if item == "S": # style specification
|
|
883
|
+
d["style"] = rule[i + 1] # next item has the style
|
|
884
|
+
skip = True # do not process next item again
|
|
885
|
+
continue
|
|
886
|
+
if item.startswith("P"): # prefix specification: extract the string
|
|
887
|
+
x = item[1:].replace("(", "").replace(")", "")
|
|
888
|
+
d["prefix"] = x
|
|
889
|
+
continue
|
|
890
|
+
if item.startswith("St"): # start page number specification
|
|
891
|
+
x = int(item[2:])
|
|
892
|
+
d["firstpagenum"] = x
|
|
893
|
+
return d
|
|
894
|
+
|
|
895
|
+
|
|
896
|
+
def get_label_pno(pgNo, labels):
|
|
897
|
+
"""Return the label for this page number.
|
|
898
|
+
|
|
899
|
+
Args:
|
|
900
|
+
pgNo: page number, 0-based.
|
|
901
|
+
labels: result of doc._get_page_labels().
|
|
902
|
+
Returns:
|
|
903
|
+
The label (str) of the page number. Errors return an empty string.
|
|
904
|
+
"""
|
|
905
|
+
# Jorj McKie, 2021-01-06
|
|
906
|
+
|
|
907
|
+
item = [x for x in labels if x[0] <= pgNo][-1]
|
|
908
|
+
rule = rule_dict(item)
|
|
909
|
+
prefix = rule.get("prefix", "")
|
|
910
|
+
style = rule.get("style", "")
|
|
911
|
+
# make sure we start at 0 when enumerating the alphabet
|
|
912
|
+
delta = -1 if style in ("a", "A") else 0
|
|
913
|
+
pagenumber = pgNo - rule["startpage"] + rule["firstpagenum"] + delta
|
|
914
|
+
return construct_label(style, prefix, pagenumber)
|
|
915
|
+
|
|
916
|
+
|
|
917
|
+
def construct_label(style, prefix, pno) -> str:
|
|
918
|
+
"""Construct a label based on style, prefix and page number."""
|
|
919
|
+
# William Chapman, 2021-01-06
|
|
920
|
+
|
|
921
|
+
n_str = ""
|
|
922
|
+
if style == "D":
|
|
923
|
+
n_str = str(pno)
|
|
924
|
+
elif style == "r":
|
|
925
|
+
n_str = integerToRoman(pno).lower()
|
|
926
|
+
elif style == "R":
|
|
927
|
+
n_str = integerToRoman(pno).upper()
|
|
928
|
+
elif style == "a":
|
|
929
|
+
n_str = integerToLetter(pno).lower()
|
|
930
|
+
elif style == "A":
|
|
931
|
+
n_str = integerToLetter(pno).upper()
|
|
932
|
+
result = prefix + n_str
|
|
933
|
+
return result
|
|
934
|
+
|
|
935
|
+
|
|
936
|
+
def integerToLetter(i) -> str:
|
|
937
|
+
"""Returns letter sequence string for integer i."""
|
|
938
|
+
# William Chapman, Jorj McKie, 2021-01-06
|
|
939
|
+
import string
|
|
940
|
+
ls = string.ascii_uppercase
|
|
941
|
+
n, a = 1, i
|
|
942
|
+
while pow(26, n) <= a:
|
|
943
|
+
a -= int(math.pow(26, n))
|
|
944
|
+
n += 1
|
|
945
|
+
|
|
946
|
+
str_t = ""
|
|
947
|
+
for j in reversed(range(n)):
|
|
948
|
+
f, g = divmod(a, int(math.pow(26, j)))
|
|
949
|
+
str_t += ls[f]
|
|
950
|
+
a = g
|
|
951
|
+
return str_t
|
|
952
|
+
|
|
953
|
+
|
|
954
|
+
def integerToRoman(num: int) -> str:
|
|
955
|
+
"""Return roman numeral for an integer."""
|
|
956
|
+
# William Chapman, Jorj McKie, 2021-01-06
|
|
957
|
+
|
|
958
|
+
roman = (
|
|
959
|
+
(1000, "M"),
|
|
960
|
+
(900, "CM"),
|
|
961
|
+
(500, "D"),
|
|
962
|
+
(400, "CD"),
|
|
963
|
+
(100, "C"),
|
|
964
|
+
(90, "XC"),
|
|
965
|
+
(50, "L"),
|
|
966
|
+
(40, "XL"),
|
|
967
|
+
(10, "X"),
|
|
968
|
+
(9, "IX"),
|
|
969
|
+
(5, "V"),
|
|
970
|
+
(4, "IV"),
|
|
971
|
+
(1, "I"),
|
|
972
|
+
)
|
|
973
|
+
|
|
974
|
+
def roman_num(num):
|
|
975
|
+
for r, ltr in roman:
|
|
976
|
+
x, _ = divmod(num, r)
|
|
977
|
+
yield ltr * x
|
|
978
|
+
num -= r * x
|
|
979
|
+
if num <= 0:
|
|
980
|
+
break
|
|
981
|
+
|
|
982
|
+
return "".join([a for a in roman_num(num)])
|
|
983
|
+
|
|
984
|
+
|
|
985
|
+
# -------------------------------------------------------------------
|
|
986
|
+
# Functions to recover the quad contained in a text extraction bbox
|
|
987
|
+
# -------------------------------------------------------------------
|
|
988
|
+
def recover_bbox_quad(line_dir: tuple, span: dict, bbox: tuple) -> pymupdf.Quad:
|
|
989
|
+
"""Compute the quad located inside the bbox.
|
|
990
|
+
|
|
991
|
+
The bbox may be any of the resp. tuples occurring inside the given span.
|
|
992
|
+
|
|
993
|
+
Args:
|
|
994
|
+
line_dir: (tuple) 'line["dir"]' of the owning line or None.
|
|
995
|
+
span: (dict) the span. May be from get_texttrace() method.
|
|
996
|
+
bbox: (tuple) the bbox of the span or any of its characters.
|
|
997
|
+
Returns:
|
|
998
|
+
The quad which is wrapped by the bbox.
|
|
999
|
+
"""
|
|
1000
|
+
if line_dir is None:
|
|
1001
|
+
line_dir = span["dir"]
|
|
1002
|
+
cos, sin = line_dir
|
|
1003
|
+
bbox = pymupdf.Rect(bbox) # make it a rect
|
|
1004
|
+
if pymupdf.TOOLS.set_small_glyph_heights(): # ==> just fontsize as height
|
|
1005
|
+
d = 1
|
|
1006
|
+
else:
|
|
1007
|
+
d = span["ascender"] - span["descender"]
|
|
1008
|
+
|
|
1009
|
+
height = d * span["size"] # the quad's rectangle height
|
|
1010
|
+
# The following are distances from the bbox corners, at which we find the
|
|
1011
|
+
# respective quad points. The computation depends on in which quadrant the
|
|
1012
|
+
# text writing angle is located.
|
|
1013
|
+
hs = height * sin
|
|
1014
|
+
hc = height * cos
|
|
1015
|
+
if hc >= 0 and hs <= 0: # quadrant 1
|
|
1016
|
+
ul = bbox.bl - (0, hc)
|
|
1017
|
+
ur = bbox.tr + (hs, 0)
|
|
1018
|
+
ll = bbox.bl - (hs, 0)
|
|
1019
|
+
lr = bbox.tr + (0, hc)
|
|
1020
|
+
elif hc <= 0 and hs <= 0: # quadrant 2
|
|
1021
|
+
ul = bbox.br + (hs, 0)
|
|
1022
|
+
ur = bbox.tl - (0, hc)
|
|
1023
|
+
ll = bbox.br + (0, hc)
|
|
1024
|
+
lr = bbox.tl - (hs, 0)
|
|
1025
|
+
elif hc <= 0 and hs >= 0: # quadrant 3
|
|
1026
|
+
ul = bbox.tr - (0, hc)
|
|
1027
|
+
ur = bbox.bl + (hs, 0)
|
|
1028
|
+
ll = bbox.tr - (hs, 0)
|
|
1029
|
+
lr = bbox.bl + (0, hc)
|
|
1030
|
+
else: # quadrant 4
|
|
1031
|
+
ul = bbox.tl + (hs, 0)
|
|
1032
|
+
ur = bbox.br - (0, hc)
|
|
1033
|
+
ll = bbox.tl + (0, hc)
|
|
1034
|
+
lr = bbox.br - (hs, 0)
|
|
1035
|
+
return pymupdf.Quad(ul, ur, ll, lr)
|
|
1036
|
+
|
|
1037
|
+
|
|
1038
|
+
def recover_quad(line_dir: tuple, span: dict) -> pymupdf.Quad:
|
|
1039
|
+
"""Recover the quadrilateral of a text span.
|
|
1040
|
+
|
|
1041
|
+
Args:
|
|
1042
|
+
line_dir: (tuple) 'line["dir"]' of the owning line.
|
|
1043
|
+
span: the span.
|
|
1044
|
+
Returns:
|
|
1045
|
+
The quadrilateral enveloping the span's text.
|
|
1046
|
+
"""
|
|
1047
|
+
if type(line_dir) is not tuple or len(line_dir) != 2:
|
|
1048
|
+
raise ValueError("bad line dir argument")
|
|
1049
|
+
if type(span) is not dict:
|
|
1050
|
+
raise ValueError("bad span argument")
|
|
1051
|
+
return recover_bbox_quad(line_dir, span, span["bbox"])
|
|
1052
|
+
|
|
1053
|
+
|
|
1054
|
+
def recover_line_quad(line: dict, spans: list = None) -> pymupdf.Quad:
|
|
1055
|
+
"""Calculate the line quad for 'dict' / 'rawdict' text extractions.
|
|
1056
|
+
|
|
1057
|
+
The lower quad points are those of the first, resp. last span quad.
|
|
1058
|
+
The upper points are determined by the maximum span quad height.
|
|
1059
|
+
From this, compute a rect with bottom-left in (0, 0), convert this to a
|
|
1060
|
+
quad and rotate and shift back to cover the text of the spans.
|
|
1061
|
+
|
|
1062
|
+
Args:
|
|
1063
|
+
spans: (list, optional) sub-list of spans to consider.
|
|
1064
|
+
Returns:
|
|
1065
|
+
pymupdf.Quad covering selected spans.
|
|
1066
|
+
"""
|
|
1067
|
+
if spans is None: # no sub-selection
|
|
1068
|
+
spans = line["spans"] # all spans
|
|
1069
|
+
if len(spans) == 0:
|
|
1070
|
+
raise ValueError("bad span list")
|
|
1071
|
+
line_dir = line["dir"] # text direction
|
|
1072
|
+
cos, sin = line_dir
|
|
1073
|
+
q0 = recover_quad(line_dir, spans[0]) # quad of first span
|
|
1074
|
+
if len(spans) > 1: # get quad of last span
|
|
1075
|
+
q1 = recover_quad(line_dir, spans[-1])
|
|
1076
|
+
else:
|
|
1077
|
+
q1 = q0 # last = first
|
|
1078
|
+
|
|
1079
|
+
line_ll = q0.ll # lower-left of line quad
|
|
1080
|
+
line_lr = q1.lr # lower-right of line quad
|
|
1081
|
+
|
|
1082
|
+
mat0 = pymupdf.planish_line(line_ll, line_lr)
|
|
1083
|
+
|
|
1084
|
+
# map base line to x-axis such that line_ll goes to (0, 0)
|
|
1085
|
+
x_lr = line_lr * mat0
|
|
1086
|
+
|
|
1087
|
+
small = pymupdf.TOOLS.set_small_glyph_heights() # small glyph heights?
|
|
1088
|
+
|
|
1089
|
+
h = max(
|
|
1090
|
+
[s["size"] * (1 if small else (s["ascender"] - s["descender"])) for s in spans]
|
|
1091
|
+
)
|
|
1092
|
+
|
|
1093
|
+
line_rect = pymupdf.Rect(0, -h, x_lr.x, 0) # line rectangle
|
|
1094
|
+
line_quad = line_rect.quad # make it a quad and:
|
|
1095
|
+
line_quad *= ~mat0
|
|
1096
|
+
return line_quad
|
|
1097
|
+
|
|
1098
|
+
|
|
1099
|
+
def recover_span_quad(line_dir: tuple, span: dict, chars: list = None) -> pymupdf.Quad:
|
|
1100
|
+
"""Calculate the span quad for 'dict' / 'rawdict' text extractions.
|
|
1101
|
+
|
|
1102
|
+
Notes:
|
|
1103
|
+
There are two execution paths:
|
|
1104
|
+
1. For the full span quad, the result of 'recover_quad' is returned.
|
|
1105
|
+
2. For the quad of a sub-list of characters, the char quads are
|
|
1106
|
+
computed and joined. This is only supported for the "rawdict"
|
|
1107
|
+
extraction option.
|
|
1108
|
+
|
|
1109
|
+
Args:
|
|
1110
|
+
line_dir: (tuple) 'line["dir"]' of the owning line.
|
|
1111
|
+
span: (dict) the span.
|
|
1112
|
+
chars: (list, optional) sub-list of characters to consider.
|
|
1113
|
+
Returns:
|
|
1114
|
+
pymupdf.Quad covering selected characters.
|
|
1115
|
+
"""
|
|
1116
|
+
if line_dir is None: # must be a span from get_texttrace()
|
|
1117
|
+
line_dir = span["dir"]
|
|
1118
|
+
if chars is None: # no sub-selection
|
|
1119
|
+
return recover_quad(line_dir, span)
|
|
1120
|
+
if "chars" not in span.keys():
|
|
1121
|
+
raise ValueError("need 'rawdict' option to sub-select chars")
|
|
1122
|
+
|
|
1123
|
+
q0 = recover_char_quad(line_dir, span, chars[0]) # quad of first char
|
|
1124
|
+
if len(chars) > 1: # get quad of last char
|
|
1125
|
+
q1 = recover_char_quad(line_dir, span, chars[-1])
|
|
1126
|
+
else:
|
|
1127
|
+
q1 = q0 # last = first
|
|
1128
|
+
|
|
1129
|
+
span_ll = q0.ll # lower-left of span quad
|
|
1130
|
+
span_lr = q1.lr # lower-right of span quad
|
|
1131
|
+
mat0 = pymupdf.planish_line(span_ll, span_lr)
|
|
1132
|
+
# map base line to x-axis such that span_ll goes to (0, 0)
|
|
1133
|
+
x_lr = span_lr * mat0
|
|
1134
|
+
|
|
1135
|
+
small = pymupdf.TOOLS.set_small_glyph_heights() # small glyph heights?
|
|
1136
|
+
h = span["size"] * (1 if small else (span["ascender"] - span["descender"]))
|
|
1137
|
+
|
|
1138
|
+
span_rect = pymupdf.Rect(0, -h, x_lr.x, 0) # line rectangle
|
|
1139
|
+
span_quad = span_rect.quad # make it a quad and:
|
|
1140
|
+
span_quad *= ~mat0 # rotate back and shift back
|
|
1141
|
+
return span_quad
|
|
1142
|
+
|
|
1143
|
+
|
|
1144
|
+
def recover_char_quad(line_dir: tuple, span: dict, char: dict) -> pymupdf.Quad:
|
|
1145
|
+
"""Recover the quadrilateral of a text character.
|
|
1146
|
+
|
|
1147
|
+
This requires the "rawdict" option of text extraction.
|
|
1148
|
+
|
|
1149
|
+
Args:
|
|
1150
|
+
line_dir: (tuple) 'line["dir"]' of the span's line.
|
|
1151
|
+
span: (dict) the span dict.
|
|
1152
|
+
char: (dict) the character dict.
|
|
1153
|
+
Returns:
|
|
1154
|
+
The quadrilateral enveloping the character.
|
|
1155
|
+
"""
|
|
1156
|
+
if line_dir is None:
|
|
1157
|
+
line_dir = span["dir"]
|
|
1158
|
+
if type(line_dir) is not tuple or len(line_dir) != 2:
|
|
1159
|
+
raise ValueError("bad line dir argument")
|
|
1160
|
+
if type(span) is not dict:
|
|
1161
|
+
raise ValueError("bad span argument")
|
|
1162
|
+
if type(char) is dict:
|
|
1163
|
+
bbox = pymupdf.Rect(char["bbox"])
|
|
1164
|
+
elif type(char) is tuple:
|
|
1165
|
+
bbox = pymupdf.Rect(char[3])
|
|
1166
|
+
else:
|
|
1167
|
+
raise ValueError("bad span argument")
|
|
1168
|
+
|
|
1169
|
+
return recover_bbox_quad(line_dir, span, bbox)
|