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.
Files changed (112) hide show
  1. fitz/__init__.py +13 -0
  2. fitz/table.py +2 -0
  3. fitz/utils.py +2 -0
  4. pymupdf/__init__.py +25899 -0
  5. pymupdf/__main__.py +1140 -0
  6. pymupdf/_apply_pages.py +253 -0
  7. pymupdf/_build.py +8 -0
  8. pymupdf/_extra.pyd +0 -0
  9. pymupdf/_mupdf.pyd +0 -0
  10. pymupdf/_wxcolors.py +562 -0
  11. pymupdf/extra.py +220 -0
  12. pymupdf/mupdf-devel/include/mupdf/classes.h +19999 -0
  13. pymupdf/mupdf-devel/include/mupdf/classes2.h +12244 -0
  14. pymupdf/mupdf-devel/include/mupdf/exceptions.h +118 -0
  15. pymupdf/mupdf-devel/include/mupdf/extra.h +189 -0
  16. pymupdf/mupdf-devel/include/mupdf/fitz/archive.h +444 -0
  17. pymupdf/mupdf-devel/include/mupdf/fitz/band-writer.h +117 -0
  18. pymupdf/mupdf-devel/include/mupdf/fitz/barcode.h +138 -0
  19. pymupdf/mupdf-devel/include/mupdf/fitz/bidi.h +90 -0
  20. pymupdf/mupdf-devel/include/mupdf/fitz/bitmap.h +188 -0
  21. pymupdf/mupdf-devel/include/mupdf/fitz/buffer.h +250 -0
  22. pymupdf/mupdf-devel/include/mupdf/fitz/color.h +433 -0
  23. pymupdf/mupdf-devel/include/mupdf/fitz/compress.h +132 -0
  24. pymupdf/mupdf-devel/include/mupdf/fitz/compressed-buffer.h +194 -0
  25. pymupdf/mupdf-devel/include/mupdf/fitz/config.h +302 -0
  26. pymupdf/mupdf-devel/include/mupdf/fitz/context.h +1064 -0
  27. pymupdf/mupdf-devel/include/mupdf/fitz/crypt.h +270 -0
  28. pymupdf/mupdf-devel/include/mupdf/fitz/deskew.h +46 -0
  29. pymupdf/mupdf-devel/include/mupdf/fitz/device.h +652 -0
  30. pymupdf/mupdf-devel/include/mupdf/fitz/display-list.h +142 -0
  31. pymupdf/mupdf-devel/include/mupdf/fitz/document.h +1103 -0
  32. pymupdf/mupdf-devel/include/mupdf/fitz/export.h +52 -0
  33. pymupdf/mupdf-devel/include/mupdf/fitz/filter.h +269 -0
  34. pymupdf/mupdf-devel/include/mupdf/fitz/font.h +858 -0
  35. pymupdf/mupdf-devel/include/mupdf/fitz/geometry.h +885 -0
  36. pymupdf/mupdf-devel/include/mupdf/fitz/getopt.h +134 -0
  37. pymupdf/mupdf-devel/include/mupdf/fitz/glyph-cache.h +96 -0
  38. pymupdf/mupdf-devel/include/mupdf/fitz/glyph.h +81 -0
  39. pymupdf/mupdf-devel/include/mupdf/fitz/hash.h +126 -0
  40. pymupdf/mupdf-devel/include/mupdf/fitz/heap-imp.h +190 -0
  41. pymupdf/mupdf-devel/include/mupdf/fitz/heap.h +143 -0
  42. pymupdf/mupdf-devel/include/mupdf/fitz/image.h +451 -0
  43. pymupdf/mupdf-devel/include/mupdf/fitz/json.h +98 -0
  44. pymupdf/mupdf-devel/include/mupdf/fitz/link.h +130 -0
  45. pymupdf/mupdf-devel/include/mupdf/fitz/log.h +61 -0
  46. pymupdf/mupdf-devel/include/mupdf/fitz/outline.h +254 -0
  47. pymupdf/mupdf-devel/include/mupdf/fitz/output-svg.h +64 -0
  48. pymupdf/mupdf-devel/include/mupdf/fitz/output.h +448 -0
  49. pymupdf/mupdf-devel/include/mupdf/fitz/path.h +477 -0
  50. pymupdf/mupdf-devel/include/mupdf/fitz/pixmap.h +516 -0
  51. pymupdf/mupdf-devel/include/mupdf/fitz/pool.h +102 -0
  52. pymupdf/mupdf-devel/include/mupdf/fitz/separation.h +138 -0
  53. pymupdf/mupdf-devel/include/mupdf/fitz/shade.h +233 -0
  54. pymupdf/mupdf-devel/include/mupdf/fitz/store.h +465 -0
  55. pymupdf/mupdf-devel/include/mupdf/fitz/story-writer.h +209 -0
  56. pymupdf/mupdf-devel/include/mupdf/fitz/story.h +232 -0
  57. pymupdf/mupdf-devel/include/mupdf/fitz/stream.h +646 -0
  58. pymupdf/mupdf-devel/include/mupdf/fitz/string-util.h +335 -0
  59. pymupdf/mupdf-devel/include/mupdf/fitz/structured-text.h +924 -0
  60. pymupdf/mupdf-devel/include/mupdf/fitz/system.h +495 -0
  61. pymupdf/mupdf-devel/include/mupdf/fitz/text.h +210 -0
  62. pymupdf/mupdf-devel/include/mupdf/fitz/track-usage.h +57 -0
  63. pymupdf/mupdf-devel/include/mupdf/fitz/transition.h +76 -0
  64. pymupdf/mupdf-devel/include/mupdf/fitz/tree.h +62 -0
  65. pymupdf/mupdf-devel/include/mupdf/fitz/types.h +41 -0
  66. pymupdf/mupdf-devel/include/mupdf/fitz/util.h +159 -0
  67. pymupdf/mupdf-devel/include/mupdf/fitz/version.h +31 -0
  68. pymupdf/mupdf-devel/include/mupdf/fitz/write-pixmap.h +507 -0
  69. pymupdf/mupdf-devel/include/mupdf/fitz/writer.h +269 -0
  70. pymupdf/mupdf-devel/include/mupdf/fitz/xml.h +435 -0
  71. pymupdf/mupdf-devel/include/mupdf/fitz.h +101 -0
  72. pymupdf/mupdf-devel/include/mupdf/functions.h +13819 -0
  73. pymupdf/mupdf-devel/include/mupdf/helpers/mu-office-lib.h +666 -0
  74. pymupdf/mupdf-devel/include/mupdf/helpers/mu-threads.h +280 -0
  75. pymupdf/mupdf-devel/include/mupdf/helpers/pkcs7-openssl.h +48 -0
  76. pymupdf/mupdf-devel/include/mupdf/html.h +53 -0
  77. pymupdf/mupdf-devel/include/mupdf/internal.h +29 -0
  78. pymupdf/mupdf-devel/include/mupdf/memento.h +425 -0
  79. pymupdf/mupdf-devel/include/mupdf/pdf/annot.h +1015 -0
  80. pymupdf/mupdf-devel/include/mupdf/pdf/clean.h +68 -0
  81. pymupdf/mupdf-devel/include/mupdf/pdf/cmap.h +145 -0
  82. pymupdf/mupdf-devel/include/mupdf/pdf/crypt.h +108 -0
  83. pymupdf/mupdf-devel/include/mupdf/pdf/document.h +902 -0
  84. pymupdf/mupdf-devel/include/mupdf/pdf/event.h +167 -0
  85. pymupdf/mupdf-devel/include/mupdf/pdf/font.h +168 -0
  86. pymupdf/mupdf-devel/include/mupdf/pdf/form.h +381 -0
  87. pymupdf/mupdf-devel/include/mupdf/pdf/image-rewriter.h +78 -0
  88. pymupdf/mupdf-devel/include/mupdf/pdf/interpret.h +518 -0
  89. pymupdf/mupdf-devel/include/mupdf/pdf/javascript.h +43 -0
  90. pymupdf/mupdf-devel/include/mupdf/pdf/name-table.h +621 -0
  91. pymupdf/mupdf-devel/include/mupdf/pdf/object.h +445 -0
  92. pymupdf/mupdf-devel/include/mupdf/pdf/page.h +327 -0
  93. pymupdf/mupdf-devel/include/mupdf/pdf/parse.h +61 -0
  94. pymupdf/mupdf-devel/include/mupdf/pdf/recolor.h +49 -0
  95. pymupdf/mupdf-devel/include/mupdf/pdf/resource.h +144 -0
  96. pymupdf/mupdf-devel/include/mupdf/pdf/xref.h +300 -0
  97. pymupdf/mupdf-devel/include/mupdf/pdf/zugferd.h +49 -0
  98. pymupdf/mupdf-devel/include/mupdf/pdf.h +59 -0
  99. pymupdf/mupdf-devel/include/mupdf/ucdn.h +464 -0
  100. pymupdf/mupdf-devel/lib/libmuthreads.lib +0 -0
  101. pymupdf/mupdf-devel/lib/mupdfcpp64.lib +0 -0
  102. pymupdf/mupdf.py +63667 -0
  103. pymupdf/mupdfcpp64.dll +0 -0
  104. pymupdf/pymupdf.py +2 -0
  105. pymupdf/table.py +2697 -0
  106. pymupdf/utils.py +1169 -0
  107. pymupdf-1.26.6.dist-info/COPYING +1 -0
  108. pymupdf-1.26.6.dist-info/METADATA +83 -0
  109. pymupdf-1.26.6.dist-info/README.md +60 -0
  110. pymupdf-1.26.6.dist-info/RECORD +112 -0
  111. pymupdf-1.26.6.dist-info/WHEEL +4 -0
  112. 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)