docx-from-pdf 0.1__tar.gz
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-0.1/LICENSE +7 -0
- docx_from_pdf-0.1/MANIFEST.in +7 -0
- docx_from_pdf-0.1/PKG-INFO +103 -0
- docx_from_pdf-0.1/README.md +74 -0
- docx_from_pdf-0.1/docx_from_pdf/__init__.py +3 -0
- docx_from_pdf-0.1/docx_from_pdf/backend/__init__.py +1 -0
- docx_from_pdf-0.1/docx_from_pdf/backend/pdfium.py +440 -0
- docx_from_pdf-0.1/docx_from_pdf/common/Block.py +144 -0
- docx_from_pdf-0.1/docx_from_pdf/common/Collection.py +359 -0
- docx_from_pdf-0.1/docx_from_pdf/common/Element.py +307 -0
- docx_from_pdf-0.1/docx_from_pdf/common/__init__.py +0 -0
- docx_from_pdf-0.1/docx_from_pdf/common/algorithm.py +403 -0
- docx_from_pdf-0.1/docx_from_pdf/common/constants.py +90 -0
- docx_from_pdf-0.1/docx_from_pdf/common/docx.py +593 -0
- docx_from_pdf-0.1/docx_from_pdf/common/geometry.py +206 -0
- docx_from_pdf-0.1/docx_from_pdf/common/share.py +319 -0
- docx_from_pdf-0.1/docx_from_pdf/converter.py +473 -0
- docx_from_pdf-0.1/docx_from_pdf/font/Fonts.py +243 -0
- docx_from_pdf-0.1/docx_from_pdf/font/__init__.py +0 -0
- docx_from_pdf-0.1/docx_from_pdf/gui/App.py +37 -0
- docx_from_pdf-0.1/docx_from_pdf/gui/MainFrame.py +147 -0
- docx_from_pdf-0.1/docx_from_pdf/gui/__init__.py +0 -0
- docx_from_pdf-0.1/docx_from_pdf/image/Image.py +112 -0
- docx_from_pdf-0.1/docx_from_pdf/image/ImageBlock.py +81 -0
- docx_from_pdf-0.1/docx_from_pdf/image/ImageSpan.py +27 -0
- docx_from_pdf-0.1/docx_from_pdf/image/ImagesExtractor.py +117 -0
- docx_from_pdf-0.1/docx_from_pdf/image/__init__.py +0 -0
- docx_from_pdf-0.1/docx_from_pdf/layout/Blocks.py +650 -0
- docx_from_pdf-0.1/docx_from_pdf/layout/Column.py +49 -0
- docx_from_pdf-0.1/docx_from_pdf/layout/Layout.py +177 -0
- docx_from_pdf-0.1/docx_from_pdf/layout/Section.py +97 -0
- docx_from_pdf-0.1/docx_from_pdf/layout/Sections.py +91 -0
- docx_from_pdf-0.1/docx_from_pdf/layout/__init__.py +0 -0
- docx_from_pdf-0.1/docx_from_pdf/main.py +135 -0
- docx_from_pdf-0.1/docx_from_pdf/page/BasePage.py +27 -0
- docx_from_pdf-0.1/docx_from_pdf/page/Page.py +211 -0
- docx_from_pdf-0.1/docx_from_pdf/page/Pages.py +90 -0
- docx_from_pdf-0.1/docx_from_pdf/page/RawPage.py +279 -0
- docx_from_pdf-0.1/docx_from_pdf/page/RawPageFactory.py +21 -0
- docx_from_pdf-0.1/docx_from_pdf/page/RawPagePdfium.py +66 -0
- docx_from_pdf-0.1/docx_from_pdf/page/__init__.py +0 -0
- docx_from_pdf-0.1/docx_from_pdf/shape/Path.py +405 -0
- docx_from_pdf-0.1/docx_from_pdf/shape/Paths.py +142 -0
- docx_from_pdf-0.1/docx_from_pdf/shape/Shape.py +365 -0
- docx_from_pdf-0.1/docx_from_pdf/shape/Shapes.py +241 -0
- docx_from_pdf-0.1/docx_from_pdf/shape/__init__.py +0 -0
- docx_from_pdf-0.1/docx_from_pdf/table/Border.py +419 -0
- docx_from_pdf-0.1/docx_from_pdf/table/Cell.py +165 -0
- docx_from_pdf-0.1/docx_from_pdf/table/Cells.py +27 -0
- docx_from_pdf-0.1/docx_from_pdf/table/Row.py +78 -0
- docx_from_pdf-0.1/docx_from_pdf/table/Rows.py +25 -0
- docx_from_pdf-0.1/docx_from_pdf/table/TableBlock.py +174 -0
- docx_from_pdf-0.1/docx_from_pdf/table/TableStructure.py +633 -0
- docx_from_pdf-0.1/docx_from_pdf/table/TablesConstructor.py +382 -0
- docx_from_pdf-0.1/docx_from_pdf/table/__init__.py +0 -0
- docx_from_pdf-0.1/docx_from_pdf/text/Char.py +65 -0
- docx_from_pdf-0.1/docx_from_pdf/text/Line.py +183 -0
- docx_from_pdf-0.1/docx_from_pdf/text/Lines.py +281 -0
- docx_from_pdf-0.1/docx_from_pdf/text/Spans.py +59 -0
- docx_from_pdf-0.1/docx_from_pdf/text/TextBlock.py +502 -0
- docx_from_pdf-0.1/docx_from_pdf/text/TextSpan.py +426 -0
- docx_from_pdf-0.1/docx_from_pdf/text/__init__.py +0 -0
- docx_from_pdf-0.1/docx_from_pdf.egg-info/PKG-INFO +103 -0
- docx_from_pdf-0.1/docx_from_pdf.egg-info/SOURCES.txt +106 -0
- docx_from_pdf-0.1/docx_from_pdf.egg-info/dependency_links.txt +1 -0
- docx_from_pdf-0.1/docx_from_pdf.egg-info/entry_points.txt +2 -0
- docx_from_pdf-0.1/docx_from_pdf.egg-info/not-zip-safe +1 -0
- docx_from_pdf-0.1/docx_from_pdf.egg-info/requires.txt +7 -0
- docx_from_pdf-0.1/docx_from_pdf.egg-info/top_level.txt +1 -0
- docx_from_pdf-0.1/pyproject.toml +3 -0
- docx_from_pdf-0.1/requirements.txt +7 -0
- docx_from_pdf-0.1/setup.cfg +4 -0
- docx_from_pdf-0.1/setup.py +57 -0
- docx_from_pdf-0.1/test/samples/demo-blank.pdf +0 -0
- docx_from_pdf-0.1/test/samples/demo-image-colorspace.pdf +0 -0
- docx_from_pdf-0.1/test/samples/demo-image-floating.pdf +0 -0
- docx_from_pdf-0.1/test/samples/demo-image-overlap.pdf +0 -0
- docx_from_pdf-0.1/test/samples/demo-image-rotation.pdf +0 -0
- docx_from_pdf-0.1/test/samples/demo-image-vector-graphic.pdf +0 -0
- docx_from_pdf-0.1/test/samples/demo-image.pdf +0 -0
- docx_from_pdf-0.1/test/samples/demo-issue-340.pdf +0 -0
- docx_from_pdf-0.1/test/samples/demo-issue-346.pdf +0 -0
- docx_from_pdf-0.1/test/samples/demo-path-transformation.pdf +0 -0
- docx_from_pdf-0.1/test/samples/demo-section-spacing.pdf +0 -0
- docx_from_pdf-0.1/test/samples/demo-section.pdf +0 -0
- docx_from_pdf-0.1/test/samples/demo-table-align-borders.pdf +1671 -2
- docx_from_pdf-0.1/test/samples/demo-table-border-style.pdf +0 -0
- docx_from_pdf-0.1/test/samples/demo-table-bottom.pdf +0 -0
- docx_from_pdf-0.1/test/samples/demo-table-close-underline.pdf +0 -0
- docx_from_pdf-0.1/test/samples/demo-table-empty-cell.pdf +0 -0
- docx_from_pdf-0.1/test/samples/demo-table-lattice-one-cell.pdf +0 -0
- docx_from_pdf-0.1/test/samples/demo-table-lattice.pdf +0 -0
- docx_from_pdf-0.1/test/samples/demo-table-nested.pdf +0 -0
- docx_from_pdf-0.1/test/samples/demo-table-shading-highlight.pdf +0 -0
- docx_from_pdf-0.1/test/samples/demo-table-shading.pdf +0 -0
- docx_from_pdf-0.1/test/samples/demo-table-stream.pdf +197 -0
- docx_from_pdf-0.1/test/samples/demo-table-weird.pdf +0 -0
- docx_from_pdf-0.1/test/samples/demo-table.pdf +0 -0
- docx_from_pdf-0.1/test/samples/demo-text-alignment.pdf +0 -0
- docx_from_pdf-0.1/test/samples/demo-text-hidden.pdf +0 -0
- docx_from_pdf-0.1/test/samples/demo-text-scaling.pdf +0 -0
- docx_from_pdf-0.1/test/samples/demo-text-unnamed-fonts.pdf +0 -0
- docx_from_pdf-0.1/test/samples/demo-text-vertical.pdf +0 -0
- docx_from_pdf-0.1/test/samples/demo-text.pdf +0 -0
- docx_from_pdf-0.1/test/samples/demo-whisper_2_3.pdf +0 -0
- docx_from_pdf-0.1/test/samples/demo.pdf +0 -0
- docx_from_pdf-0.1/test/test.py +513 -0
- docx_from_pdf-0.1/version.txt +1 -0
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
Copyright (c) 2026 Artifex Software, Inc.
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
4
|
+
|
|
5
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
6
|
+
|
|
7
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: docx-from-pdf
|
|
3
|
+
Version: 0.1
|
|
4
|
+
Summary: Python library and CLI for converting PDF files to DOCX.
|
|
5
|
+
Home-page: https://github.com/michael-provision/docx-from-pdf/
|
|
6
|
+
Author: Michael Ellis
|
|
7
|
+
License: MIT
|
|
8
|
+
Keywords: pdf-to-word,pdf-to-docx,docx-from-pdf
|
|
9
|
+
Requires-Python: >=3.10
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Requires-Dist: pypdfium2>=5.11.0
|
|
13
|
+
Requires-Dist: Pillow>=10.0.0
|
|
14
|
+
Requires-Dist: python-docx>=0.8.10
|
|
15
|
+
Requires-Dist: fonttools>=4.24.0
|
|
16
|
+
Requires-Dist: numpy>=1.17.2
|
|
17
|
+
Requires-Dist: opencv-python-headless>=4.5
|
|
18
|
+
Requires-Dist: fire>=0.3.0
|
|
19
|
+
Dynamic: author
|
|
20
|
+
Dynamic: description
|
|
21
|
+
Dynamic: description-content-type
|
|
22
|
+
Dynamic: home-page
|
|
23
|
+
Dynamic: keywords
|
|
24
|
+
Dynamic: license
|
|
25
|
+
Dynamic: license-file
|
|
26
|
+
Dynamic: requires-dist
|
|
27
|
+
Dynamic: requires-python
|
|
28
|
+
Dynamic: summary
|
|
29
|
+
|
|
30
|
+
# docx-from-pdf
|
|
31
|
+
|
|
32
|
+
A Python library and CLI for converting PDF files into editable DOCX documents.
|
|
33
|
+
|
|
34
|
+
`docx-from-pdf` uses PDFium for PDF parsing and rendering. It converts text,
|
|
35
|
+
tables, images, vector paths, and section layout into DOCX output with
|
|
36
|
+
permissive runtime dependencies.
|
|
37
|
+
|
|
38
|
+
This project uses PDFium through `pypdfium2`; it is not affiliated with PDFium,
|
|
39
|
+
Chromium, or Google.
|
|
40
|
+
|
|
41
|
+
## Install
|
|
42
|
+
|
|
43
|
+
Install from PyPI:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
pip install docx-from-pdf
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Install from source:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
pip install git+https://github.com/michael-provision/docx-from-pdf.git
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Install local development dependencies:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
uv venv
|
|
59
|
+
uv pip install -r requirements.txt
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Usage
|
|
63
|
+
|
|
64
|
+
Convert a PDF from Python:
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
from docx_from_pdf import Converter
|
|
68
|
+
|
|
69
|
+
converter = Converter("input.pdf")
|
|
70
|
+
converter.convert("output.docx")
|
|
71
|
+
converter.close()
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Convert a PDF from the CLI:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
docx-from-pdf convert input.pdf output.docx
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Extract tables:
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
from docx_from_pdf import Converter
|
|
84
|
+
|
|
85
|
+
converter = Converter("input.pdf")
|
|
86
|
+
tables = converter.extract_tables()
|
|
87
|
+
converter.close()
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Development
|
|
91
|
+
|
|
92
|
+
Run the test suite:
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
PYTHONPATH=. uv run --no-project --with-requirements requirements.txt --with pytest pytest -q test/test.py
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Some visual-comparison tests render DOCX files through LibreOffice on non-Windows
|
|
99
|
+
systems, so LibreOffice must be installed for the full suite.
|
|
100
|
+
|
|
101
|
+
## License
|
|
102
|
+
|
|
103
|
+
MIT
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# docx-from-pdf
|
|
2
|
+
|
|
3
|
+
A Python library and CLI for converting PDF files into editable DOCX documents.
|
|
4
|
+
|
|
5
|
+
`docx-from-pdf` uses PDFium for PDF parsing and rendering. It converts text,
|
|
6
|
+
tables, images, vector paths, and section layout into DOCX output with
|
|
7
|
+
permissive runtime dependencies.
|
|
8
|
+
|
|
9
|
+
This project uses PDFium through `pypdfium2`; it is not affiliated with PDFium,
|
|
10
|
+
Chromium, or Google.
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
Install from PyPI:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
pip install docx-from-pdf
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Install from source:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
pip install git+https://github.com/michael-provision/docx-from-pdf.git
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Install local development dependencies:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
uv venv
|
|
30
|
+
uv pip install -r requirements.txt
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Usage
|
|
34
|
+
|
|
35
|
+
Convert a PDF from Python:
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
from docx_from_pdf import Converter
|
|
39
|
+
|
|
40
|
+
converter = Converter("input.pdf")
|
|
41
|
+
converter.convert("output.docx")
|
|
42
|
+
converter.close()
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Convert a PDF from the CLI:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
docx-from-pdf convert input.pdf output.docx
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Extract tables:
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
from docx_from_pdf import Converter
|
|
55
|
+
|
|
56
|
+
converter = Converter("input.pdf")
|
|
57
|
+
tables = converter.extract_tables()
|
|
58
|
+
converter.close()
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Development
|
|
62
|
+
|
|
63
|
+
Run the test suite:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
PYTHONPATH=. uv run --no-project --with-requirements requirements.txt --with pytest pytest -q test/test.py
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Some visual-comparison tests render DOCX files through LibreOffice on non-Windows
|
|
70
|
+
systems, so LibreOffice must be installed for the full suite.
|
|
71
|
+
|
|
72
|
+
## License
|
|
73
|
+
|
|
74
|
+
MIT
|
|
@@ -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)
|