docx-from-pdf 0.1__py3-none-any.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.
- docx_from_pdf/__init__.py +3 -0
- docx_from_pdf/backend/__init__.py +1 -0
- docx_from_pdf/backend/pdfium.py +440 -0
- docx_from_pdf/common/Block.py +144 -0
- docx_from_pdf/common/Collection.py +359 -0
- docx_from_pdf/common/Element.py +307 -0
- docx_from_pdf/common/__init__.py +0 -0
- docx_from_pdf/common/algorithm.py +403 -0
- docx_from_pdf/common/constants.py +90 -0
- docx_from_pdf/common/docx.py +593 -0
- docx_from_pdf/common/geometry.py +206 -0
- docx_from_pdf/common/share.py +319 -0
- docx_from_pdf/converter.py +473 -0
- docx_from_pdf/font/Fonts.py +243 -0
- docx_from_pdf/font/__init__.py +0 -0
- docx_from_pdf/gui/App.py +37 -0
- docx_from_pdf/gui/MainFrame.py +147 -0
- docx_from_pdf/gui/__init__.py +0 -0
- docx_from_pdf/image/Image.py +112 -0
- docx_from_pdf/image/ImageBlock.py +81 -0
- docx_from_pdf/image/ImageSpan.py +27 -0
- docx_from_pdf/image/ImagesExtractor.py +117 -0
- docx_from_pdf/image/__init__.py +0 -0
- docx_from_pdf/layout/Blocks.py +650 -0
- docx_from_pdf/layout/Column.py +49 -0
- docx_from_pdf/layout/Layout.py +177 -0
- docx_from_pdf/layout/Section.py +97 -0
- docx_from_pdf/layout/Sections.py +91 -0
- docx_from_pdf/layout/__init__.py +0 -0
- docx_from_pdf/main.py +135 -0
- docx_from_pdf/page/BasePage.py +27 -0
- docx_from_pdf/page/Page.py +211 -0
- docx_from_pdf/page/Pages.py +90 -0
- docx_from_pdf/page/RawPage.py +279 -0
- docx_from_pdf/page/RawPageFactory.py +21 -0
- docx_from_pdf/page/RawPagePdfium.py +66 -0
- docx_from_pdf/page/__init__.py +0 -0
- docx_from_pdf/shape/Path.py +405 -0
- docx_from_pdf/shape/Paths.py +142 -0
- docx_from_pdf/shape/Shape.py +365 -0
- docx_from_pdf/shape/Shapes.py +241 -0
- docx_from_pdf/shape/__init__.py +0 -0
- docx_from_pdf/table/Border.py +419 -0
- docx_from_pdf/table/Cell.py +165 -0
- docx_from_pdf/table/Cells.py +27 -0
- docx_from_pdf/table/Row.py +78 -0
- docx_from_pdf/table/Rows.py +25 -0
- docx_from_pdf/table/TableBlock.py +174 -0
- docx_from_pdf/table/TableStructure.py +633 -0
- docx_from_pdf/table/TablesConstructor.py +382 -0
- docx_from_pdf/table/__init__.py +0 -0
- docx_from_pdf/text/Char.py +65 -0
- docx_from_pdf/text/Line.py +183 -0
- docx_from_pdf/text/Lines.py +281 -0
- docx_from_pdf/text/Spans.py +59 -0
- docx_from_pdf/text/TextBlock.py +502 -0
- docx_from_pdf/text/TextSpan.py +426 -0
- docx_from_pdf/text/__init__.py +0 -0
- docx_from_pdf-0.1.dist-info/METADATA +103 -0
- docx_from_pdf-0.1.dist-info/RECORD +64 -0
- docx_from_pdf-0.1.dist-info/WHEEL +5 -0
- docx_from_pdf-0.1.dist-info/entry_points.txt +2 -0
- docx_from_pdf-0.1.dist-info/licenses/LICENSE +7 -0
- docx_from_pdf-0.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""PDF backend implementations."""
|
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
"""PDFium-backed document and page primitives."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from ctypes import byref, c_double, c_float, c_int, c_uint, create_string_buffer
|
|
6
|
+
from io import BytesIO
|
|
7
|
+
from math import cos, pi, sin, sqrt
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
import pypdfium2 as pdfium
|
|
11
|
+
import pypdfium2.raw as pdfium_c
|
|
12
|
+
|
|
13
|
+
from ..common.geometry import Matrix, Rect
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class PdfiumDocument:
|
|
17
|
+
"""Document wrapper exposing the subset of behavior used by the converter."""
|
|
18
|
+
|
|
19
|
+
def __init__(self, pdf_file: str | None = None, password: str | None = None, stream: bytes | None = None):
|
|
20
|
+
self._document = pdfium.PdfDocument(stream if stream is not None else Path(pdf_file), password=password or None)
|
|
21
|
+
self.needs_pass = False
|
|
22
|
+
|
|
23
|
+
def __len__(self):
|
|
24
|
+
return len(self._document)
|
|
25
|
+
|
|
26
|
+
def __iter__(self):
|
|
27
|
+
for page_index in range(len(self)):
|
|
28
|
+
yield self[page_index]
|
|
29
|
+
|
|
30
|
+
def __getitem__(self, page_index: int):
|
|
31
|
+
return PdfiumPage(self, page_index)
|
|
32
|
+
|
|
33
|
+
def authenticate(self, password: str):
|
|
34
|
+
return True
|
|
35
|
+
|
|
36
|
+
def close(self):
|
|
37
|
+
self._document.close()
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class PdfiumPage:
|
|
41
|
+
"""Page wrapper with extraction helpers."""
|
|
42
|
+
|
|
43
|
+
def __init__(self, document: PdfiumDocument, page_index: int):
|
|
44
|
+
self.parent = document
|
|
45
|
+
self.page_index = page_index
|
|
46
|
+
self._page = document._document[page_index]
|
|
47
|
+
self._pdf_cropbox = self._page.get_cropbox()
|
|
48
|
+
crop_left, crop_bottom, crop_right, crop_top = self._pdf_cropbox
|
|
49
|
+
self.width = crop_right - crop_left
|
|
50
|
+
self.height = crop_top - crop_bottom
|
|
51
|
+
self.rect = Rect(0.0, 0.0, self.width, self.height)
|
|
52
|
+
self.cropbox = self.rect
|
|
53
|
+
self.rotation = self._page.get_rotation()
|
|
54
|
+
self.rotation_matrix = Matrix()
|
|
55
|
+
|
|
56
|
+
def close(self):
|
|
57
|
+
self._page.close()
|
|
58
|
+
|
|
59
|
+
def extract_text_blocks(self, sort=None):
|
|
60
|
+
text_page = self._page.get_textpage()
|
|
61
|
+
chars = self._extract_chars(text_page)
|
|
62
|
+
if sort:
|
|
63
|
+
chars.sort(key=lambda char: (char["bbox"][1], char["bbox"][0]))
|
|
64
|
+
lines = self._group_chars_into_lines(chars)
|
|
65
|
+
return [{"type": 0, "bbox": self._bbox_for_lines(lines), "lines": lines}] if lines else []
|
|
66
|
+
|
|
67
|
+
def extract_images(self, clip_image_res_ratio: float = 3.0):
|
|
68
|
+
images = []
|
|
69
|
+
for image_object in self._page.get_objects(filter=[pdfium_c.FPDF_PAGEOBJ_IMAGE], max_depth=3):
|
|
70
|
+
bbox = self._pdf_bounds_to_page_rect(image_object.get_bounds())
|
|
71
|
+
if bbox.get_area() <= 4:
|
|
72
|
+
continue
|
|
73
|
+
image_bytes, width, height = self.render_clip_to_png(
|
|
74
|
+
bbox,
|
|
75
|
+
zoom=clip_image_res_ratio,
|
|
76
|
+
rm_text=True,
|
|
77
|
+
rm_image=False,
|
|
78
|
+
)
|
|
79
|
+
images.append(
|
|
80
|
+
{
|
|
81
|
+
"type": 1,
|
|
82
|
+
"bbox": tuple(bbox),
|
|
83
|
+
"width": width,
|
|
84
|
+
"height": height,
|
|
85
|
+
"image": image_bytes,
|
|
86
|
+
}
|
|
87
|
+
)
|
|
88
|
+
return images
|
|
89
|
+
|
|
90
|
+
def extract_paths(self):
|
|
91
|
+
paths = []
|
|
92
|
+
for path_object in self._page.get_objects(filter=[pdfium_c.FPDF_PAGEOBJ_PATH], max_depth=3):
|
|
93
|
+
items = self._extract_path_items(path_object)
|
|
94
|
+
if not items:
|
|
95
|
+
continue
|
|
96
|
+
fill_mode = c_int()
|
|
97
|
+
stroke = c_int()
|
|
98
|
+
pdfium_c.FPDFPath_GetDrawMode(path_object, byref(fill_mode), byref(stroke))
|
|
99
|
+
path_type = ""
|
|
100
|
+
if fill_mode.value:
|
|
101
|
+
path_type += "f"
|
|
102
|
+
if stroke.value:
|
|
103
|
+
path_type += "s"
|
|
104
|
+
if not path_type:
|
|
105
|
+
continue
|
|
106
|
+
stroke_width = c_float()
|
|
107
|
+
pdfium_c.FPDFPageObj_GetStrokeWidth(path_object, byref(stroke_width))
|
|
108
|
+
paths.append(
|
|
109
|
+
{
|
|
110
|
+
"type": path_type,
|
|
111
|
+
"items": items,
|
|
112
|
+
"width": max(0.1, float(stroke_width.value)),
|
|
113
|
+
"color": self._get_object_color(path_object, stroke=True),
|
|
114
|
+
"fill": self._get_object_color(path_object, stroke=False),
|
|
115
|
+
"closePath": any(item[0] == "close" for item in items),
|
|
116
|
+
}
|
|
117
|
+
)
|
|
118
|
+
return paths
|
|
119
|
+
|
|
120
|
+
def render_clip_to_png(self, bbox=None, zoom: float = 3.0, rm_text: bool = False, rm_image: bool = False):
|
|
121
|
+
clip = Rect(bbox) if bbox is not None else self.rect
|
|
122
|
+
crop = (
|
|
123
|
+
max(0.0, clip.x0),
|
|
124
|
+
max(0.0, self.height - clip.y1),
|
|
125
|
+
max(0.0, self.width - clip.x1),
|
|
126
|
+
max(0.0, clip.y0),
|
|
127
|
+
)
|
|
128
|
+
deactivated_objects = self._deactivate_page_objects(rm_text=rm_text, rm_image=rm_image)
|
|
129
|
+
try:
|
|
130
|
+
bitmap = self._page.render(scale=zoom, crop=crop, rev_byteorder=True)
|
|
131
|
+
pil_image = bitmap.to_pil()
|
|
132
|
+
output = BytesIO()
|
|
133
|
+
pil_image.save(output, format="PNG")
|
|
134
|
+
return output.getvalue(), pil_image.width, pil_image.height
|
|
135
|
+
finally:
|
|
136
|
+
for page_object in deactivated_objects:
|
|
137
|
+
pdfium_c.FPDFPageObj_SetIsActive(page_object, 1)
|
|
138
|
+
|
|
139
|
+
def _extract_chars(self, text_page):
|
|
140
|
+
chars = []
|
|
141
|
+
for char_index in range(text_page.count_chars()):
|
|
142
|
+
character = self._xml_compatible_text(text_page.get_text_range(char_index, 1))
|
|
143
|
+
if not character:
|
|
144
|
+
continue
|
|
145
|
+
bbox = self._pdf_bounds_to_page_rect(text_page.get_charbox(char_index, loose=True))
|
|
146
|
+
if bbox.is_empty:
|
|
147
|
+
continue
|
|
148
|
+
origin_x = c_double()
|
|
149
|
+
origin_y = c_double()
|
|
150
|
+
pdfium_c.FPDFText_GetCharOrigin(text_page, char_index, byref(origin_x), byref(origin_y))
|
|
151
|
+
font_name = self._font_name(text_page, char_index)
|
|
152
|
+
font_size = self._effective_font_size(text_page, char_index)
|
|
153
|
+
chars.append(
|
|
154
|
+
{
|
|
155
|
+
"c": character,
|
|
156
|
+
"bbox": tuple(bbox),
|
|
157
|
+
"origin": (float(origin_x.value), self.height - float(origin_y.value)),
|
|
158
|
+
"dir": self._text_direction(text_page, char_index),
|
|
159
|
+
"font": font_name,
|
|
160
|
+
"size": font_size,
|
|
161
|
+
"color": self._text_color(text_page, char_index),
|
|
162
|
+
"flags": self._font_flags(text_page, char_index, font_name),
|
|
163
|
+
}
|
|
164
|
+
)
|
|
165
|
+
return chars
|
|
166
|
+
|
|
167
|
+
def _group_chars_into_lines(self, chars):
|
|
168
|
+
lines = []
|
|
169
|
+
for char in chars:
|
|
170
|
+
for line in reversed(lines[-8:]):
|
|
171
|
+
if line["_dir"] != char["dir"]:
|
|
172
|
+
continue
|
|
173
|
+
if abs(line["_center"] - self._line_center(char)) <= self._line_grouping_threshold(char):
|
|
174
|
+
line["_chars"].append(char)
|
|
175
|
+
line["_center"] = self._weighted_line_center(line["_chars"])
|
|
176
|
+
break
|
|
177
|
+
else:
|
|
178
|
+
lines.append({"_center": self._line_center(char), "_dir": char["dir"], "_chars": [char]})
|
|
179
|
+
|
|
180
|
+
raw_lines = []
|
|
181
|
+
for line in sorted(lines, key=self._line_sort_key):
|
|
182
|
+
line_chars = self._sort_line_chars(line["_chars"], line["_dir"])
|
|
183
|
+
for segment_chars in self._split_line_chars_by_gap(line_chars, line["_dir"]):
|
|
184
|
+
self._add_missing_spaces(segment_chars, line["_dir"])
|
|
185
|
+
spans = self._group_chars_into_spans(segment_chars)
|
|
186
|
+
raw_lines.append({"wmode": 0, "dir": line["_dir"], "bbox": self._bbox_for_spans(spans), "spans": spans})
|
|
187
|
+
return raw_lines
|
|
188
|
+
|
|
189
|
+
@staticmethod
|
|
190
|
+
def _split_line_chars_by_gap(chars, direction):
|
|
191
|
+
if not chars:
|
|
192
|
+
return []
|
|
193
|
+
groups = [[chars[0]]]
|
|
194
|
+
for char in chars[1:]:
|
|
195
|
+
previous_char = groups[-1][-1]
|
|
196
|
+
if direction == (0.0, -1.0):
|
|
197
|
+
gap = previous_char["bbox"][1] - char["bbox"][3]
|
|
198
|
+
else:
|
|
199
|
+
gap = char["bbox"][0] - previous_char["bbox"][2]
|
|
200
|
+
threshold = max(8.0, min(PdfiumPage._char_extent(previous_char), PdfiumPage._char_extent(char)) * 1.2)
|
|
201
|
+
if gap > threshold:
|
|
202
|
+
groups.append([char])
|
|
203
|
+
else:
|
|
204
|
+
groups[-1].append(char)
|
|
205
|
+
return groups
|
|
206
|
+
|
|
207
|
+
@staticmethod
|
|
208
|
+
def _add_missing_spaces(chars, direction):
|
|
209
|
+
for previous_char, char in zip(chars, chars[1:]):
|
|
210
|
+
if previous_char["c"].isspace() or char["c"].isspace():
|
|
211
|
+
continue
|
|
212
|
+
if direction == (0.0, -1.0):
|
|
213
|
+
gap = previous_char["bbox"][1] - char["bbox"][3]
|
|
214
|
+
else:
|
|
215
|
+
gap = char["bbox"][0] - previous_char["bbox"][2]
|
|
216
|
+
threshold = max(1.5, min(PdfiumPage._char_advance_extent(previous_char), PdfiumPage._char_advance_extent(char)) * 0.35)
|
|
217
|
+
if gap > threshold:
|
|
218
|
+
previous_char["c"] += " "
|
|
219
|
+
|
|
220
|
+
@staticmethod
|
|
221
|
+
def _group_chars_into_spans(chars):
|
|
222
|
+
spans = []
|
|
223
|
+
current = None
|
|
224
|
+
for char in chars:
|
|
225
|
+
key = (char["font"], round(char["size"], 2), char["color"], char["flags"])
|
|
226
|
+
if current is None or current["_key"] != key:
|
|
227
|
+
current = {
|
|
228
|
+
"_key": key,
|
|
229
|
+
"font": char["font"],
|
|
230
|
+
"size": char["size"],
|
|
231
|
+
"color": char["color"],
|
|
232
|
+
"flags": char["flags"],
|
|
233
|
+
"chars": [],
|
|
234
|
+
}
|
|
235
|
+
spans.append(current)
|
|
236
|
+
current["chars"].append({"c": char["c"], "bbox": char["bbox"], "origin": char["origin"]})
|
|
237
|
+
for span in spans:
|
|
238
|
+
span["bbox"] = PdfiumPage._bbox_for_chars(span["chars"])
|
|
239
|
+
del span["_key"]
|
|
240
|
+
return spans
|
|
241
|
+
|
|
242
|
+
def _extract_path_items(self, path_object):
|
|
243
|
+
matrix = path_object.get_matrix()
|
|
244
|
+
items = []
|
|
245
|
+
cursor = None
|
|
246
|
+
bezier_points = []
|
|
247
|
+
segment_count = pdfium_c.FPDFPath_CountSegments(path_object)
|
|
248
|
+
for segment_index in range(segment_count):
|
|
249
|
+
segment = pdfium_c.FPDFPath_GetPathSegment(path_object, segment_index)
|
|
250
|
+
segment_type = pdfium_c.FPDFPathSegment_GetType(segment)
|
|
251
|
+
x = c_float()
|
|
252
|
+
y = c_float()
|
|
253
|
+
pdfium_c.FPDFPathSegment_GetPoint(segment, byref(x), byref(y))
|
|
254
|
+
point = self._matrix_point_to_page_point(matrix, x.value, y.value)
|
|
255
|
+
if segment_type == pdfium_c.FPDF_SEGMENT_MOVETO:
|
|
256
|
+
cursor = point
|
|
257
|
+
bezier_points = []
|
|
258
|
+
elif segment_type == pdfium_c.FPDF_SEGMENT_LINETO and cursor is not None:
|
|
259
|
+
items.append(("l", cursor, point))
|
|
260
|
+
cursor = point
|
|
261
|
+
bezier_points = []
|
|
262
|
+
elif segment_type == pdfium_c.FPDF_SEGMENT_BEZIERTO and cursor is not None:
|
|
263
|
+
bezier_points.append(point)
|
|
264
|
+
if len(bezier_points) == 3:
|
|
265
|
+
items.append(("c", cursor, bezier_points[0], bezier_points[1], bezier_points[2]))
|
|
266
|
+
cursor = bezier_points[2]
|
|
267
|
+
bezier_points = []
|
|
268
|
+
if pdfium_c.FPDFPathSegment_GetClose(segment):
|
|
269
|
+
items.append(("close",))
|
|
270
|
+
return items
|
|
271
|
+
|
|
272
|
+
def _deactivate_page_objects(self, rm_text: bool, rm_image: bool):
|
|
273
|
+
if not rm_text and not rm_image:
|
|
274
|
+
return []
|
|
275
|
+
filters = []
|
|
276
|
+
if rm_text:
|
|
277
|
+
filters.append(pdfium_c.FPDF_PAGEOBJ_TEXT)
|
|
278
|
+
if rm_image:
|
|
279
|
+
filters.append(pdfium_c.FPDF_PAGEOBJ_IMAGE)
|
|
280
|
+
deactivated_objects = []
|
|
281
|
+
for page_object in self._page.get_objects(filter=filters, max_depth=3):
|
|
282
|
+
active = c_int()
|
|
283
|
+
pdfium_c.FPDFPageObj_GetIsActive(page_object, byref(active))
|
|
284
|
+
if active.value:
|
|
285
|
+
pdfium_c.FPDFPageObj_SetIsActive(page_object, 0)
|
|
286
|
+
deactivated_objects.append(page_object)
|
|
287
|
+
return deactivated_objects
|
|
288
|
+
|
|
289
|
+
def _matrix_point_to_page_point(self, matrix, x: float, y: float):
|
|
290
|
+
page_x = x * matrix.a + y * matrix.c + matrix.e
|
|
291
|
+
page_y = x * matrix.b + y * matrix.d + matrix.f
|
|
292
|
+
crop_left, _, _, crop_top = self._pdf_cropbox
|
|
293
|
+
return (float(page_x - crop_left), float(crop_top - page_y))
|
|
294
|
+
|
|
295
|
+
def _pdf_bounds_to_page_rect(self, bounds):
|
|
296
|
+
left, bottom, right, top = bounds
|
|
297
|
+
crop_left, _, _, crop_top = self._pdf_cropbox
|
|
298
|
+
return Rect(left - crop_left, crop_top - top, right - crop_left, crop_top - bottom)
|
|
299
|
+
|
|
300
|
+
@staticmethod
|
|
301
|
+
def _effective_font_size(text_page, char_index: int):
|
|
302
|
+
font_size = float(pdfium_c.FPDFText_GetFontSize(text_page, char_index) or 12.0)
|
|
303
|
+
matrix = pdfium_c.FS_MATRIX()
|
|
304
|
+
if not pdfium_c.FPDFText_GetMatrix(text_page, char_index, byref(matrix)):
|
|
305
|
+
return font_size
|
|
306
|
+
vertical_scale = sqrt(matrix.c * matrix.c + matrix.d * matrix.d)
|
|
307
|
+
return font_size * vertical_scale if vertical_scale else font_size
|
|
308
|
+
|
|
309
|
+
@staticmethod
|
|
310
|
+
def _font_name(text_page, char_index: int):
|
|
311
|
+
buffer = create_string_buffer(512)
|
|
312
|
+
flags = c_int()
|
|
313
|
+
length = pdfium_c.FPDFText_GetFontInfo(text_page, char_index, buffer, len(buffer), byref(flags))
|
|
314
|
+
return buffer.value[:length].decode("utf-8", errors="ignore").split("+")[-1] or "Arial"
|
|
315
|
+
|
|
316
|
+
@staticmethod
|
|
317
|
+
def _font_flags(text_page, char_index: int, font_name: str):
|
|
318
|
+
flags = 0
|
|
319
|
+
if "Italic" in font_name or "Oblique" in font_name:
|
|
320
|
+
flags |= 2**1
|
|
321
|
+
if "Bold" in font_name or pdfium_c.FPDFText_GetFontWeight(text_page, char_index) >= 600:
|
|
322
|
+
flags |= 2**4
|
|
323
|
+
return flags
|
|
324
|
+
|
|
325
|
+
@staticmethod
|
|
326
|
+
def _text_direction(text_page, char_index: int):
|
|
327
|
+
angle = float(pdfium_c.FPDFText_GetCharAngle(text_page, char_index)) % (2 * pi)
|
|
328
|
+
if angle <= 0.1 or abs(angle - 2 * pi) <= 0.1:
|
|
329
|
+
return (1.0, 0.0)
|
|
330
|
+
if abs(angle - 1.5 * pi) <= 0.1:
|
|
331
|
+
return (0.0, -1.0)
|
|
332
|
+
return (round(cos(angle), 3), round(sin(angle), 3))
|
|
333
|
+
|
|
334
|
+
@staticmethod
|
|
335
|
+
def _text_color(text_page, char_index: int):
|
|
336
|
+
red = c_uint()
|
|
337
|
+
green = c_uint()
|
|
338
|
+
blue = c_uint()
|
|
339
|
+
alpha = c_uint()
|
|
340
|
+
if not pdfium_c.FPDFText_GetFillColor(text_page, char_index, byref(red), byref(green), byref(blue), byref(alpha)):
|
|
341
|
+
return 0
|
|
342
|
+
return (red.value << 16) + (green.value << 8) + blue.value
|
|
343
|
+
|
|
344
|
+
@staticmethod
|
|
345
|
+
def _get_object_color(path_object, stroke: bool):
|
|
346
|
+
red = c_uint()
|
|
347
|
+
green = c_uint()
|
|
348
|
+
blue = c_uint()
|
|
349
|
+
alpha = c_uint()
|
|
350
|
+
color_function = pdfium_c.FPDFPageObj_GetStrokeColor if stroke else pdfium_c.FPDFPageObj_GetFillColor
|
|
351
|
+
if not color_function(path_object, byref(red), byref(green), byref(blue), byref(alpha)):
|
|
352
|
+
return None
|
|
353
|
+
return (red.value / 255.0, green.value / 255.0, blue.value / 255.0)
|
|
354
|
+
|
|
355
|
+
@staticmethod
|
|
356
|
+
def _xml_compatible_text(text: str):
|
|
357
|
+
return "".join(char for char in text if PdfiumPage._is_xml_compatible_character(char))
|
|
358
|
+
|
|
359
|
+
@staticmethod
|
|
360
|
+
def _is_xml_compatible_character(char: str):
|
|
361
|
+
codepoint = ord(char)
|
|
362
|
+
return (
|
|
363
|
+
codepoint in (0x9, 0xA, 0xD)
|
|
364
|
+
or 0x20 <= codepoint <= 0xD7FF
|
|
365
|
+
or 0xE000 <= codepoint <= 0xFFFD
|
|
366
|
+
or 0x10000 <= codepoint <= 0x10FFFF
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
@staticmethod
|
|
370
|
+
def _vertical_center(bbox):
|
|
371
|
+
return (bbox[1] + bbox[3]) / 2.0
|
|
372
|
+
|
|
373
|
+
@staticmethod
|
|
374
|
+
def _horizontal_center(bbox):
|
|
375
|
+
return (bbox[0] + bbox[2]) / 2.0
|
|
376
|
+
|
|
377
|
+
@staticmethod
|
|
378
|
+
def _line_center(char):
|
|
379
|
+
if char["dir"] == (0.0, -1.0):
|
|
380
|
+
return PdfiumPage._horizontal_center(char["bbox"])
|
|
381
|
+
return PdfiumPage._vertical_center(char["bbox"])
|
|
382
|
+
|
|
383
|
+
@staticmethod
|
|
384
|
+
def _char_extent(char):
|
|
385
|
+
if char["dir"] == (0.0, -1.0):
|
|
386
|
+
return max(1.0, char["bbox"][2] - char["bbox"][0])
|
|
387
|
+
return max(1.0, char["bbox"][3] - char["bbox"][1])
|
|
388
|
+
|
|
389
|
+
@staticmethod
|
|
390
|
+
def _char_advance_extent(char):
|
|
391
|
+
if char["dir"] == (0.0, -1.0):
|
|
392
|
+
return max(1.0, char["bbox"][3] - char["bbox"][1])
|
|
393
|
+
return max(1.0, char["bbox"][2] - char["bbox"][0])
|
|
394
|
+
|
|
395
|
+
@staticmethod
|
|
396
|
+
def _line_grouping_threshold(char):
|
|
397
|
+
return max(1.5, PdfiumPage._char_extent(char) * 0.35)
|
|
398
|
+
|
|
399
|
+
@staticmethod
|
|
400
|
+
def _weighted_line_center(chars):
|
|
401
|
+
if chars[0]["dir"] == (0.0, -1.0):
|
|
402
|
+
return sum(PdfiumPage._horizontal_center(char["bbox"]) * PdfiumPage._char_extent(char) for char in chars) / sum(
|
|
403
|
+
PdfiumPage._char_extent(char) for char in chars
|
|
404
|
+
)
|
|
405
|
+
return sum(PdfiumPage._vertical_center(char["bbox"]) * PdfiumPage._char_extent(char) for char in chars) / sum(
|
|
406
|
+
PdfiumPage._char_extent(char) for char in chars
|
|
407
|
+
)
|
|
408
|
+
|
|
409
|
+
@staticmethod
|
|
410
|
+
def _line_sort_key(line):
|
|
411
|
+
if line["_dir"] == (0.0, -1.0):
|
|
412
|
+
return (min(char["bbox"][0] for char in line["_chars"]), min(char["bbox"][1] for char in line["_chars"]))
|
|
413
|
+
return (line["_center"], min(char["bbox"][0] for char in line["_chars"]))
|
|
414
|
+
|
|
415
|
+
@staticmethod
|
|
416
|
+
def _sort_line_chars(chars, direction):
|
|
417
|
+
if direction == (0.0, -1.0):
|
|
418
|
+
return sorted(chars, key=lambda item: (-item["bbox"][1], item["bbox"][0]))
|
|
419
|
+
return sorted(chars, key=lambda item: item["bbox"][0])
|
|
420
|
+
|
|
421
|
+
@staticmethod
|
|
422
|
+
def _bbox_for_chars(chars):
|
|
423
|
+
bbox = Rect()
|
|
424
|
+
for char in chars:
|
|
425
|
+
bbox |= Rect(char["bbox"])
|
|
426
|
+
return tuple(bbox)
|
|
427
|
+
|
|
428
|
+
@staticmethod
|
|
429
|
+
def _bbox_for_spans(spans):
|
|
430
|
+
bbox = Rect()
|
|
431
|
+
for span in spans:
|
|
432
|
+
bbox |= Rect(span["bbox"])
|
|
433
|
+
return tuple(bbox)
|
|
434
|
+
|
|
435
|
+
@staticmethod
|
|
436
|
+
def _bbox_for_lines(lines):
|
|
437
|
+
bbox = Rect()
|
|
438
|
+
for line in lines:
|
|
439
|
+
bbox |= Rect(line["bbox"])
|
|
440
|
+
return tuple(bbox)
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
'''Base class for text/image/table blocks.
|
|
4
|
+
'''
|
|
5
|
+
|
|
6
|
+
from .share import BlockType, TextAlignment
|
|
7
|
+
from .Element import Element
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Block(Element):
|
|
11
|
+
'''Base class for text/image/table blocks.
|
|
12
|
+
|
|
13
|
+
Attributes:
|
|
14
|
+
raw (dict): initialize object from raw properties.
|
|
15
|
+
parent (optional): parent object that this block belongs to.
|
|
16
|
+
'''
|
|
17
|
+
def __init__(self, raw:dict=None, parent=None):
|
|
18
|
+
self._type = BlockType.UNDEFINED
|
|
19
|
+
|
|
20
|
+
# horizontal spacing
|
|
21
|
+
if raw is None: raw = {}
|
|
22
|
+
self.alignment = self._get_alignment(raw.get('alignment', 0))
|
|
23
|
+
self.left_space = raw.get('left_space', 0.0)
|
|
24
|
+
self.right_space = raw.get('right_space', 0.0)
|
|
25
|
+
self.first_line_space = raw.get('first_line_space', 0.0)
|
|
26
|
+
|
|
27
|
+
# RELATIVE position of tab stops
|
|
28
|
+
self.tab_stops = raw.get('tab_stops', [])
|
|
29
|
+
|
|
30
|
+
# vertical spacing
|
|
31
|
+
self.before_space = raw.get('before_space', 0.0)
|
|
32
|
+
self.after_space = raw.get('after_space', 0.0)
|
|
33
|
+
self.line_space = raw.get('line_space', 0.0)
|
|
34
|
+
self.line_space_type = raw.get('line_space_type', 1) # 0-exactly, 1-relatively
|
|
35
|
+
|
|
36
|
+
super().__init__(raw, parent)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def is_text_block(self):
|
|
41
|
+
'''Whether test block.'''
|
|
42
|
+
return self._type==BlockType.TEXT
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
def is_inline_image_block(self):
|
|
46
|
+
'''Whether inline image block.'''
|
|
47
|
+
return self._type==BlockType.IMAGE
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def is_float_image_block(self):
|
|
51
|
+
'''Whether float image block.'''
|
|
52
|
+
return self._type==BlockType.FLOAT_IMAGE
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def is_image_block(self):
|
|
56
|
+
'''Whether inline or float image block.'''
|
|
57
|
+
return self.is_inline_image_block or self.is_float_image_block
|
|
58
|
+
|
|
59
|
+
@property
|
|
60
|
+
def is_text_image_block(self):
|
|
61
|
+
'''Whether text block or inline image block.'''
|
|
62
|
+
return self.is_text_block or self.is_inline_image_block
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def is_lattice_table_block(self):
|
|
66
|
+
'''Whether lattice table (explicit table borders) block.'''
|
|
67
|
+
return self._type==BlockType.LATTICE_TABLE
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
def is_stream_table_block(self):
|
|
71
|
+
'''Whether stream table (implied by table content) block.'''
|
|
72
|
+
return self._type==BlockType.STREAM_TABLE
|
|
73
|
+
|
|
74
|
+
@property
|
|
75
|
+
def is_table_block(self):
|
|
76
|
+
'''Whether table (lattice or stream) block.'''
|
|
77
|
+
return self.is_lattice_table_block or self.is_stream_table_block
|
|
78
|
+
|
|
79
|
+
def set_text_block(self):
|
|
80
|
+
'''Set block type.'''
|
|
81
|
+
self._type = BlockType.TEXT
|
|
82
|
+
|
|
83
|
+
def set_inline_image_block(self):
|
|
84
|
+
'''Set block type.'''
|
|
85
|
+
self._type = BlockType.IMAGE
|
|
86
|
+
|
|
87
|
+
def set_float_image_block(self):
|
|
88
|
+
'''Set block type.'''
|
|
89
|
+
self._type = BlockType.FLOAT_IMAGE
|
|
90
|
+
|
|
91
|
+
def set_lattice_table_block(self):
|
|
92
|
+
'''Set block type.'''
|
|
93
|
+
self._type = BlockType.LATTICE_TABLE
|
|
94
|
+
|
|
95
|
+
def set_stream_table_block(self):
|
|
96
|
+
'''Set block type.'''
|
|
97
|
+
self._type = BlockType.STREAM_TABLE
|
|
98
|
+
|
|
99
|
+
def _get_alignment(self, mode:int):
|
|
100
|
+
for t in TextAlignment:
|
|
101
|
+
if t.value==mode:
|
|
102
|
+
return t
|
|
103
|
+
return TextAlignment.LEFT
|
|
104
|
+
|
|
105
|
+
def parse_horizontal_spacing(self, bbox, *args):
|
|
106
|
+
"""Set left alignment, and calculate left space.
|
|
107
|
+
|
|
108
|
+
Override by :obj:`docx_from_pdf.text.TextBlock`.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
bbox (Rect): boundary box of this block.
|
|
112
|
+
"""
|
|
113
|
+
# NOTE: in PDFium CS, horizontal text direction is same with positive x-axis,
|
|
114
|
+
# while vertical text is on the contrary, so use f = -1 here
|
|
115
|
+
idx, f = (0, 1.0) if self.is_horizontal_text else (3, -1.0)
|
|
116
|
+
self.alignment = TextAlignment.LEFT
|
|
117
|
+
self.left_space = (self.bbox[idx] - bbox[idx]) * f
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def store(self):
|
|
121
|
+
'''Store attributes in json format.'''
|
|
122
|
+
res = super().store()
|
|
123
|
+
res.update({
|
|
124
|
+
'type' : self._type.value,
|
|
125
|
+
'alignment' : self.alignment.value,
|
|
126
|
+
'left_space' : self.left_space,
|
|
127
|
+
'right_space' : self.right_space,
|
|
128
|
+
'first_line_space' : self.first_line_space,
|
|
129
|
+
'before_space' : self.before_space,
|
|
130
|
+
'after_space' : self.after_space,
|
|
131
|
+
'line_space' : self.line_space,
|
|
132
|
+
'line_space_type' : self.line_space_type,
|
|
133
|
+
'tab_stops' : self.tab_stops
|
|
134
|
+
})
|
|
135
|
+
return res
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def make_docx(self, *args, **kwargs):
|
|
139
|
+
"""Create associated docx element.
|
|
140
|
+
|
|
141
|
+
Raises:
|
|
142
|
+
NotImplementedError
|
|
143
|
+
"""
|
|
144
|
+
raise NotImplementedError
|