pdf-html 0.1.0__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.
- pdf_html-0.1.0/.gitignore +244 -0
- pdf_html-0.1.0/.vscode/settings.json +3 -0
- pdf_html-0.1.0/LICENSE +21 -0
- pdf_html-0.1.0/PKG-INFO +296 -0
- pdf_html-0.1.0/README.md +266 -0
- pdf_html-0.1.0/TUTORIAL.md +87 -0
- pdf_html-0.1.0/arch.mmd +44 -0
- pdf_html-0.1.0/logo.png +0 -0
- pdf_html-0.1.0/pyproject.toml +53 -0
- pdf_html-0.1.0/src/pdf_html/__init__.py +7 -0
- pdf_html-0.1.0/src/pdf_html/ast.py +124 -0
- pdf_html-0.1.0/src/pdf_html/callout_detector.py +6 -0
- pdf_html-0.1.0/src/pdf_html/cli.py +147 -0
- pdf_html-0.1.0/src/pdf_html/extractor.py +272 -0
- pdf_html-0.1.0/src/pdf_html/header_footer.py +118 -0
- pdf_html-0.1.0/src/pdf_html/list_parser.py +158 -0
- pdf_html-0.1.0/src/pdf_html/reading_order.py +154 -0
- pdf_html-0.1.0/src/pdf_html/renderer.py +220 -0
- pdf_html-0.1.0/src/pdf_html/structure.py +218 -0
- pdf_html-0.1.0/src/pdf_html/style_profiler.py +117 -0
- pdf_html-0.1.0/src/pdf_html/table_reconstructor.py +237 -0
- pdf_html-0.1.0/tests/fixtures/brochure.pdf +80 -0
- pdf_html-0.1.0/tests/fixtures/make_fixtures.py +195 -0
- pdf_html-0.1.0/tests/fixtures/report.pdf +93 -0
- pdf_html-0.1.0/tests/fixtures/slides.pdf +93 -0
- pdf_html-0.1.0/tests/fixtures/table.pdf +74 -0
- pdf_html-0.1.0/tests/fixtures/two_column.pdf +74 -0
- pdf_html-0.1.0/tests/test_extractor.py +121 -0
- pdf_html-0.1.0/tests/test_header_footer.py +72 -0
- pdf_html-0.1.0/tests/test_pipeline.py +134 -0
- pdf_html-0.1.0/tests/test_reading_order.py +68 -0
- pdf_html-0.1.0/tests/test_structure.py +108 -0
- pdf_html-0.1.0/tests/test_style_profiler.py +58 -0
- pdf_html-0.1.0/tests/test_table_reconstructor.py +162 -0
- pdf_html-0.1.0/uv.lock +375 -0
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
# Byte-compiled / optimized / DLL files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[codz]
|
|
4
|
+
*$py.class
|
|
5
|
+
|
|
6
|
+
# C extensions
|
|
7
|
+
*.so
|
|
8
|
+
|
|
9
|
+
# Distribution / packaging
|
|
10
|
+
.Python
|
|
11
|
+
build/
|
|
12
|
+
develop-eggs/
|
|
13
|
+
dist/
|
|
14
|
+
downloads/
|
|
15
|
+
eggs/
|
|
16
|
+
.eggs/
|
|
17
|
+
lib/
|
|
18
|
+
lib64/
|
|
19
|
+
parts/
|
|
20
|
+
sdist/
|
|
21
|
+
var/
|
|
22
|
+
wheels/
|
|
23
|
+
share/python-wheels/
|
|
24
|
+
*.egg-info/
|
|
25
|
+
.installed.cfg
|
|
26
|
+
*.egg
|
|
27
|
+
MANIFEST
|
|
28
|
+
|
|
29
|
+
# PyInstaller
|
|
30
|
+
# Usually these files are written by a python script from a template
|
|
31
|
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
32
|
+
*.manifest
|
|
33
|
+
*.spec
|
|
34
|
+
|
|
35
|
+
# Installer logs
|
|
36
|
+
pip-log.txt
|
|
37
|
+
pip-delete-this-directory.txt
|
|
38
|
+
|
|
39
|
+
# Unit test / coverage reports
|
|
40
|
+
htmlcov/
|
|
41
|
+
.tox/
|
|
42
|
+
.nox/
|
|
43
|
+
.coverage
|
|
44
|
+
.coverage.*
|
|
45
|
+
.cache
|
|
46
|
+
nosetests.xml
|
|
47
|
+
coverage.xml
|
|
48
|
+
*.cover
|
|
49
|
+
*.py.cover
|
|
50
|
+
*.lcov
|
|
51
|
+
.hypothesis/
|
|
52
|
+
.pytest_cache/
|
|
53
|
+
cover/
|
|
54
|
+
|
|
55
|
+
# Translations
|
|
56
|
+
*.mo
|
|
57
|
+
*.pot
|
|
58
|
+
|
|
59
|
+
# Django stuff:
|
|
60
|
+
*.log
|
|
61
|
+
local_settings.py
|
|
62
|
+
db.sqlite3
|
|
63
|
+
db.sqlite3-journal
|
|
64
|
+
|
|
65
|
+
# Flask stuff:
|
|
66
|
+
instance/
|
|
67
|
+
.webassets-cache
|
|
68
|
+
|
|
69
|
+
# Scrapy stuff:
|
|
70
|
+
.scrapy
|
|
71
|
+
|
|
72
|
+
# Sphinx documentation
|
|
73
|
+
docs/_build/
|
|
74
|
+
|
|
75
|
+
# PyBuilder
|
|
76
|
+
.pybuilder/
|
|
77
|
+
target/
|
|
78
|
+
|
|
79
|
+
# Jupyter Notebook
|
|
80
|
+
.ipynb_checkpoints
|
|
81
|
+
|
|
82
|
+
# IPython
|
|
83
|
+
profile_default/
|
|
84
|
+
ipython_config.py
|
|
85
|
+
|
|
86
|
+
# pyenv
|
|
87
|
+
# For a library or package, you might want to ignore these files since the code is
|
|
88
|
+
# intended to run in multiple environments; otherwise, check them in:
|
|
89
|
+
# .python-version
|
|
90
|
+
|
|
91
|
+
# pipenv
|
|
92
|
+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
|
93
|
+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
|
94
|
+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
|
95
|
+
# install all needed dependencies.
|
|
96
|
+
# Pipfile.lock
|
|
97
|
+
|
|
98
|
+
# UV
|
|
99
|
+
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
|
|
100
|
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
101
|
+
# commonly ignored for libraries.
|
|
102
|
+
# uv.lock
|
|
103
|
+
|
|
104
|
+
# poetry
|
|
105
|
+
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
|
106
|
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
107
|
+
# commonly ignored for libraries.
|
|
108
|
+
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
|
109
|
+
# poetry.lock
|
|
110
|
+
# poetry.toml
|
|
111
|
+
|
|
112
|
+
# pdm
|
|
113
|
+
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
|
114
|
+
# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
|
|
115
|
+
# https://pdm-project.org/en/latest/usage/project/#working-with-version-control
|
|
116
|
+
# pdm.lock
|
|
117
|
+
# pdm.toml
|
|
118
|
+
.pdm-python
|
|
119
|
+
.pdm-build/
|
|
120
|
+
|
|
121
|
+
# pixi
|
|
122
|
+
# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
|
|
123
|
+
# pixi.lock
|
|
124
|
+
# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
|
|
125
|
+
# in the .venv directory. It is recommended not to include this directory in version control.
|
|
126
|
+
.pixi/*
|
|
127
|
+
!.pixi/config.toml
|
|
128
|
+
|
|
129
|
+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
|
130
|
+
__pypackages__/
|
|
131
|
+
|
|
132
|
+
# Celery stuff
|
|
133
|
+
celerybeat-schedule*
|
|
134
|
+
celerybeat.pid
|
|
135
|
+
|
|
136
|
+
# Redis
|
|
137
|
+
*.rdb
|
|
138
|
+
*.aof
|
|
139
|
+
*.pid
|
|
140
|
+
|
|
141
|
+
# RabbitMQ
|
|
142
|
+
mnesia/
|
|
143
|
+
rabbitmq/
|
|
144
|
+
rabbitmq-data/
|
|
145
|
+
|
|
146
|
+
# ActiveMQ
|
|
147
|
+
activemq-data/
|
|
148
|
+
|
|
149
|
+
# SageMath parsed files
|
|
150
|
+
*.sage.py
|
|
151
|
+
|
|
152
|
+
# Environments
|
|
153
|
+
.env
|
|
154
|
+
.envrc
|
|
155
|
+
.venv
|
|
156
|
+
env/
|
|
157
|
+
venv/
|
|
158
|
+
ENV/
|
|
159
|
+
env.bak/
|
|
160
|
+
venv.bak/
|
|
161
|
+
|
|
162
|
+
# Spyder project settings
|
|
163
|
+
.spyderproject
|
|
164
|
+
.spyproject
|
|
165
|
+
|
|
166
|
+
# Rope project settings
|
|
167
|
+
.ropeproject
|
|
168
|
+
|
|
169
|
+
# mkdocs documentation
|
|
170
|
+
/site
|
|
171
|
+
|
|
172
|
+
# mypy
|
|
173
|
+
.mypy_cache/
|
|
174
|
+
.dmypy.json
|
|
175
|
+
dmypy.json
|
|
176
|
+
|
|
177
|
+
# Pyre type checker
|
|
178
|
+
.pyre/
|
|
179
|
+
|
|
180
|
+
# pytype static type analyzer
|
|
181
|
+
.pytype/
|
|
182
|
+
|
|
183
|
+
# Cython debug symbols
|
|
184
|
+
cython_debug/
|
|
185
|
+
|
|
186
|
+
# PyCharm
|
|
187
|
+
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
|
188
|
+
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
|
189
|
+
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
|
190
|
+
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
|
191
|
+
# .idea/
|
|
192
|
+
|
|
193
|
+
# Abstra
|
|
194
|
+
# Abstra is an AI-powered process automation framework.
|
|
195
|
+
# Ignore directories containing user credentials, local state, and settings.
|
|
196
|
+
# Learn more at https://abstra.io/docs
|
|
197
|
+
.abstra/
|
|
198
|
+
|
|
199
|
+
# Visual Studio Code
|
|
200
|
+
# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
|
|
201
|
+
# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
|
|
202
|
+
# and can be added to the global gitignore or merged into this file. However, if you prefer,
|
|
203
|
+
# you could uncomment the following to ignore the entire vscode folder
|
|
204
|
+
# .vscode/
|
|
205
|
+
# Temporary file for partial code execution
|
|
206
|
+
tempCodeRunnerFile.py
|
|
207
|
+
|
|
208
|
+
# Ruff stuff:
|
|
209
|
+
.ruff_cache/
|
|
210
|
+
|
|
211
|
+
# PyPI configuration file
|
|
212
|
+
.pypirc
|
|
213
|
+
|
|
214
|
+
# Marimo
|
|
215
|
+
marimo/_static/
|
|
216
|
+
marimo/_lsp/
|
|
217
|
+
__marimo__/
|
|
218
|
+
|
|
219
|
+
# Streamlit
|
|
220
|
+
.streamlit/secrets.toml
|
|
221
|
+
|
|
222
|
+
# Internal development files (workflow guide ยง4)
|
|
223
|
+
REQS.md
|
|
224
|
+
.github/instructions/workflow-instructions.md
|
|
225
|
+
.github/*instructions.md
|
|
226
|
+
|
|
227
|
+
tests/fixtures/*.html
|
|
228
|
+
# Only the deterministic generated fixtures are committed; ad-hoc test PDFs
|
|
229
|
+
# (often copyrighted course material) stay local.
|
|
230
|
+
tests/fixtures/*.pdf
|
|
231
|
+
tests/fixtures/*.PDF
|
|
232
|
+
!tests/fixtures/report.pdf
|
|
233
|
+
!tests/fixtures/two_column.pdf
|
|
234
|
+
!tests/fixtures/slides.pdf
|
|
235
|
+
!tests/fixtures/brochure.pdf
|
|
236
|
+
!tests/fixtures/table.pdf
|
|
237
|
+
chain*
|
|
238
|
+
|
|
239
|
+
# Local conversion artifacts at repo root (often copyrighted sources โ never commit)
|
|
240
|
+
/*.pdf
|
|
241
|
+
/*.html
|
|
242
|
+
|
|
243
|
+
# macOS
|
|
244
|
+
.DS_Store
|
pdf_html-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Omar (Top Gear Technologies LLC)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
pdf_html-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: pdf-html
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Early-stage geometry-first PDF-to-HTML converter for text-based PDFs โ verbatim text, source typography, zero images.
|
|
5
|
+
Project-URL: Homepage, https://github.com/OmarMWarraich/pdf-html
|
|
6
|
+
Project-URL: Repository, https://github.com/OmarMWarraich/pdf-html
|
|
7
|
+
Project-URL: Issues, https://github.com/OmarMWarraich/pdf-html/issues
|
|
8
|
+
Project-URL: Documentation, https://github.com/OmarMWarraich/pdf-html#readme
|
|
9
|
+
Author: Omar Warraich
|
|
10
|
+
License: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: converter,document-conversion,geometry-first,html,pdf,pdf-to-html,pymupdf,semantic-html,text-based-pdf
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Environment :: Console
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
17
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
18
|
+
Classifier: Operating System :: OS Independent
|
|
19
|
+
Classifier: Programming Language :: Python :: 3
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
24
|
+
Classifier: Topic :: Office/Business
|
|
25
|
+
Classifier: Topic :: Text Processing :: Markup :: HTML
|
|
26
|
+
Classifier: Topic :: Utilities
|
|
27
|
+
Requires-Python: >=3.10
|
|
28
|
+
Requires-Dist: pymupdf>=1.24
|
|
29
|
+
Description-Content-Type: text/markdown
|
|
30
|
+
|
|
31
|
+
<div align="center">
|
|
32
|
+
|
|
33
|
+
<img src="logo.png" alt="pdf-html logo" width="120" height="120" />
|
|
34
|
+
|
|
35
|
+
# pdf-html
|
|
36
|
+
|
|
37
|
+
**Early-stage geometry-first PDF โ HTML converter for text-based PDFs โ verbatim text, source typography, zero images.**
|
|
38
|
+
|
|
39
|
+
[](https://www.python.org/)
|
|
40
|
+
[](https://pymupdf.readthedocs.io/)
|
|
41
|
+
[](#-license)
|
|
42
|
+
[](#-testing)
|
|
43
|
+
[](#)
|
|
44
|
+
[](#-design-principles)
|
|
45
|
+
|
|
46
|
+
*One command in. One elegant, dependency-free HTML file out.*
|
|
47
|
+
|
|
48
|
+
</div>
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## ๐ฏ What it does
|
|
53
|
+
|
|
54
|
+
`pdf-html` is an **early-stage, geometry-first PDF-to-HTML converter** for **text-based PDFs**. It rebuilds the document as a **single self-contained HTML5 file** that mirrors the source document's look and structure:
|
|
55
|
+
|
|
56
|
+
- โ
Best for: text-based PDFs with real text layers, headings, lists, tables, and multi-column layouts
|
|
57
|
+
- โ ๏ธ Not a universal OCR-first converter: scanned/image-heavy PDFs still need an OCR pre-pass or a dedicated workflow
|
|
58
|
+
|
|
59
|
+
- ๐ท๏ธ **Heading hierarchy** โ font-size tiers become real `<h1>`โ`<h6>`
|
|
60
|
+
- ๐จ **Typography & color** โ the page CSS is derived from the document's own fonts, sizes, and palette
|
|
61
|
+
- ๐ **Tables** โ ruled tables are rebuilt as real `<table>` elements, including lists *inside* cells and rows that continue across page breaks
|
|
62
|
+
- ๐ **Lists** โ bullets and numbered items become nested `<ul>`/`<ol>` (indent decides nesting)
|
|
63
|
+
- ๐งญ **Reading order** โ multi-column layouts are re-linearized column by column
|
|
64
|
+
- โ๏ธ **Page furniture** โ repeated running headers/footers and page numbers are stripped
|
|
65
|
+
- ๐ **Text is verbatim** โ never summarized, reworded, or reordered within a block
|
|
66
|
+
- ๐ซ **No images, ever** โ image content is dropped by design; text alone carries the document
|
|
67
|
+
|
|
68
|
+
## ๐งญ Early-stage scope
|
|
69
|
+
|
|
70
|
+
This is an **early-stage, geometry-first conversion tool for text-based PDFs**. It is designed to be reliable and deterministic where the PDF has a usable text layer, but it is not a universal OCR-first converter for scanned documents, forms, or arbitrary image-heavy PDFs.
|
|
71
|
+
|
|
72
|
+
### What it does well
|
|
73
|
+
|
|
74
|
+
- text-based PDFs with real text layers
|
|
75
|
+
- styled HTML with verbatim text
|
|
76
|
+
- heading hierarchy, lists, and multi-column layout reconstruction
|
|
77
|
+
- ruled table reconstruction with cell-aware content flow
|
|
78
|
+
|
|
79
|
+
### What it does not yet do well
|
|
80
|
+
|
|
81
|
+
- OCR and scanned-document support are planned as separate, opt-in paths
|
|
82
|
+
- arbitrary image-heavy PDFs or forms with weak text layers
|
|
83
|
+
- pixel-perfect visual reconstruction of every layout edge case
|
|
84
|
+
|
|
85
|
+
## โก Quick start
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
# install (Python 3.10+)
|
|
89
|
+
uv pip install . # or: pip install .
|
|
90
|
+
|
|
91
|
+
# convert
|
|
92
|
+
pdf-html report.pdf -o report.html
|
|
93
|
+
|
|
94
|
+
# open report.html in any browser โ no external assets needed
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## ๐ฅ๏ธ CLI reference
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
pdf-html INPUT.pdf -o out.html [--extractor pymupdf|pdftotext]
|
|
101
|
+
[--style auto|default] [--paginate] [--no-tables] [--no-callouts]
|
|
102
|
+
[--keep-headers] [--allow-scanned]
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
| Flag | Default | What it does |
|
|
106
|
+
|---|---|---|
|
|
107
|
+
| `-o, --output` | *(required)* | Path of the HTML file to write |
|
|
108
|
+
| `--extractor` | `pymupdf` | Text extraction backend (`pdftotext` fallback planned) |
|
|
109
|
+
| `--style` | `auto` | `auto` derives CSS from the document's own fonts/sizes/colors; `default` uses a clean built-in theme |
|
|
110
|
+
| `--paginate` | off | Wrap each PDF page in a `<section class="sheet">` |
|
|
111
|
+
| `--no-tables` | off | Disable table reconstruction (table text flows as paragraphs) |
|
|
112
|
+
| `--keep-headers` | off | Keep repeated running headers/footers |
|
|
113
|
+
| `--allow-scanned` | off | Convert scanned/image PDFs instead of exiting with an OCR hint |
|
|
114
|
+
|
|
115
|
+
## ๐๏ธ Architecture
|
|
116
|
+
|
|
117
|
+
A deterministic pipeline โ every stage is a small, pure, individually testable module:
|
|
118
|
+
|
|
119
|
+
<details>
|
|
120
|
+
<summary>๐ <b>Pipeline diagram</b> โ click to enlarge (click again to close) ยท <a href="https://mermaid.live/view#pako:eNqNlV9vmzAUxb_KFQ97WVnzp1nbbKq2No3ykDYIMqnS2IOxL8GqsZExa7uq330GQgismZIHJzb28Tm_XJtXhyqGzhQc13VDSZWM-WYaSgBBXlRhpoDiseyaBFOcgsTCaCJCWU2PhXqiCdEGln45CSAvoo0mWQLB8GfohAW7GDPbUpzAEMJiMIjO4fbZSlATOr_qNeWHcY3UcCVhfd2OerP5t1fIE5LZrZmiJ9ZVhGIK-9L0rJwXOvDWLrx9aNeVwnsLt7sr_TXSp1d5RmQOHyFW0kCKhjBiSFfLioPrXlnNegwl62cdNVnRtiy-hFGT9UYgkfCBpNkX8LSKucAjci_mB-0vkDDUp3OlDG4jGM2zjMtN13bgtRoJPu9JBOZFYOOm1ogUe4Gc_8HGdzIMi9FgOE4-NyMZEWgMdjdZ1GgC7yCacQcNMhg3gsuqwI6g4a8O0vAtDZscVpptk1AlilQCFUVuAf1DZe23WrF2e3JrElkyPtpjYKkWuyrJULsUhYCMZyi43KPwTuSzJjK1bczOwyLGQQxnTfCgki70MZUQzPYOAP_dN7yTghka3PlNaix5-99pUpnbjQiem7yLZukdxLy0s8EjOt9ClmjhMijEqRJdke_BulWJ1FPfsH1eKcwULVK0Z86W2fByZMU32Py-Foo-9swFs6rQll71ZVUO0p906JMLmDSZfTsb9TEFd_-fgis1mpOHKZGGU1is75YTe4twWRYH3ARB1_3qxx4VQzbuO3dZZXc4AHsmPiUm7XH176vgVqgX_PaherCY192gBuSvOt319nb2V_VxndXdtd_pWqr14nvnBJwUdUo4s6-GV6e6_MuXxPb6d97e_gLHTOOG">open full screen โ</a></summary>
|
|
121
|
+
<br/>
|
|
122
|
+
|
|
123
|
+
```mermaid
|
|
124
|
+
---
|
|
125
|
+
config:
|
|
126
|
+
layout: elk
|
|
127
|
+
theme: neutral
|
|
128
|
+
---
|
|
129
|
+
flowchart LR
|
|
130
|
+
subgraph S1["๐ฅ 1 ยท Extract"]
|
|
131
|
+
direction TB
|
|
132
|
+
PDF@{ shape: doc, label: "๐ PDF" }
|
|
133
|
+
EX@{ shape: rect, label: "Extractor<br/>spans + font metadata" }
|
|
134
|
+
PDF --> EX
|
|
135
|
+
end
|
|
136
|
+
subgraph S2["๐งน 2 ยท Clean & Profile"]
|
|
137
|
+
direction TB
|
|
138
|
+
HF@{ shape: rect, label: "Header/Footer<br/>stripping" }
|
|
139
|
+
SP@{ shape: hex, label: "Style Profiler<br/>body size ยท h1โh6 ยท palette" }
|
|
140
|
+
HF --> SP
|
|
141
|
+
end
|
|
142
|
+
subgraph S3["๐งญ 3 ยท Layout"]
|
|
143
|
+
direction TB
|
|
144
|
+
RO@{ shape: rect, label: "Reading Order<br/>column clustering" }
|
|
145
|
+
TR@{ shape: fr-rect, label: "Table Reconstructor<br/>per-cell pipeline" }
|
|
146
|
+
end
|
|
147
|
+
subgraph S4["๐๏ธ 4 ยท Structure"]
|
|
148
|
+
direction TB
|
|
149
|
+
SD@{ shape: div-rect, label: "Structure Detector<br/>headings ยท paragraphs ยท lists" }
|
|
150
|
+
LP@{ shape: rect, label: "List Parser<br/>nested ul/ol" }
|
|
151
|
+
AST@{ shape: bow-rect, label: "AST<br/>Document โ Page โ Blocks" }
|
|
152
|
+
SD --> LP --> AST
|
|
153
|
+
end
|
|
154
|
+
subgraph S5["๐จ 5 ยท Render"]
|
|
155
|
+
direction TB
|
|
156
|
+
RN@{ shape: rect, label: "Renderer<br/>semantic HTML5 + inline CSS" }
|
|
157
|
+
OUT@{ shape: tag-doc, label: "๐ out.html" }
|
|
158
|
+
RN --> OUT
|
|
159
|
+
end
|
|
160
|
+
EX --> HF
|
|
161
|
+
SP --> RO
|
|
162
|
+
SP --> TR
|
|
163
|
+
RO --> SD
|
|
164
|
+
TR --> SD
|
|
165
|
+
AST --> RN
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
</details>
|
|
169
|
+
|
|
170
|
+
| Module | Responsibility |
|
|
171
|
+
|---|---|
|
|
172
|
+
| `extractor.py` | `TextExtractor` ABC; PyMuPDF backend reads per-span size/weight/color/bbox and detects table regions |
|
|
173
|
+
| `header_footer.py` | Strips spans repeating on โฅ 60% of pages in the top/bottom 10% bands |
|
|
174
|
+
| `style_profiler.py` | Character-weighted font-size histogram โ body size, heading tiers, color palette |
|
|
175
|
+
| `reading_order.py` | Column detection via x-gap clustering, with a card-grid fallback and straddle guard |
|
|
176
|
+
| `table_reconstructor.py` | Assigns spans to detected cells, runs the full pipeline *inside each cell*, merges cross-page rows |
|
|
177
|
+
| `structure.py` | Classifies lines into headings/paragraphs/list items from geometry + font cues |
|
|
178
|
+
| `list_parser.py` | Indent-based nesting; glyph style only picks `ul` vs `ol`; markers stripped, text verbatim |
|
|
179
|
+
| `ast.py` | Typed document model โ `Document โ Page โ Block`, runs carry inline style |
|
|
180
|
+
| `renderer.py` | Single-file HTML5 with one `<style>` block and CSS variables from the profile |
|
|
181
|
+
|
|
182
|
+
## ๐ Table reconstruction highlights
|
|
183
|
+
|
|
184
|
+
The hardest part of PDF โ HTML is tables. `pdf-html`:
|
|
185
|
+
|
|
186
|
+
1. ๐ Detects ruled tables geometrically (PyMuPDF `find_tables()`) at extraction time
|
|
187
|
+
2. ๐ Assigns the page's *styled* spans to cells by bounding box โ inline bold/color/size survive
|
|
188
|
+
3. ๐ Runs the normal line โ paragraph โ list pipeline **inside every cell**, so bullets in cells become real nested lists
|
|
189
|
+
4. ๐งต Merges rows that continue across page breaks (empty-first-cell fragments) back into one row โ even resuming mid-list-item
|
|
190
|
+
5. ๐ท๏ธ Promotes a bold-only first row to a `<th>` header row
|
|
191
|
+
|
|
192
|
+
## ๐งญ Design principles
|
|
193
|
+
|
|
194
|
+
| Principle | Meaning |
|
|
195
|
+
|---|---|
|
|
196
|
+
| ๐งฎ **Pure geometry, no AI** | All structure is inferred from font metadata and bounding boxes. No NLP, no LLM, no document-specific regexes |
|
|
197
|
+
| ๐ **Text is sacred** | Output text is verbatim; only `& < >` are escaped |
|
|
198
|
+
| ๐ซ **No images** | Spans overlapping image rects are dropped; `<img>` is never emitted |
|
|
199
|
+
| ๐ช **Graceful degradation** | Heuristic failures only affect styling โ never text content or order |
|
|
200
|
+
| ๐ง **Tunable & testable** | Every heuristic threshold is a named module-level constant with focused unit tests |
|
|
201
|
+
|
|
202
|
+
## โ๏ธ How it compares
|
|
203
|
+
|
|
204
|
+
Every PDF converter picks a trade-off. `pdf-html` optimizes for **semantic, reflowable, styled HTML with a verbatim-text guarantee** โ a square none of the established tools occupy:
|
|
205
|
+
|
|
206
|
+
| Tool | Output | Semantic structure | Keeps typography | Deterministic | Footprint |
|
|
207
|
+
|---|---|:---:|:---:|:---:|---|
|
|
208
|
+
| **pdf-html** | Self-contained HTML5 | โ
`h1โh6`, `ul/ol`, `table` | โ
CSS derived from the source | โ
| ~30 MB (PyMuPDF only) |
|
|
209
|
+
| [pdf2htmlEX](https://github.com/pdf2htmlEX/pdf2htmlEX) | Pixel-faithful HTML | โ positioned glyphs | โ
visually | โ
| C++ toolchain |
|
|
210
|
+
| [Poppler pdftohtml](https://poppler.freedesktop.org/) | Positioned divs / bare text | โ | โ ๏ธ partial | โ
| system package |
|
|
211
|
+
| [pymupdf4llm](https://pypi.org/project/pymupdf4llm/) | Markdown for LLM ingestion | โ ๏ธ headings & lists | โ discarded | โ
| ~30 MB |
|
|
212
|
+
| [marker-pdf](https://pypi.org/project/marker-pdf/) | Markdown/JSON via ML | โ
| โ discarded | โ model-dependent | GB-scale models, GPU-friendly |
|
|
213
|
+
| [docling](https://pypi.org/project/docling/) | Markdown/HTML/JSON via ML | โ
| โ discarded | โ model-dependent | GB-scale models |
|
|
214
|
+
| [unstructured](https://pypi.org/project/unstructured/) | Element JSON for RAG | โ ๏ธ element types | โ | โ ๏ธ | heavy optional deps |
|
|
215
|
+
| Adobe PDF Services | Structured JSON/HTML | โ
| โ ๏ธ | โ | cloud API, paid |
|
|
216
|
+
|
|
217
|
+
**When to choose pdf-html** โ you want a *readable, reflowable* document that still looks like the original, produced offline, reproducibly, with text you can trust character-for-character (text-based PDFs, tables included, even across page breaks).
|
|
218
|
+
|
|
219
|
+
**When to choose something else** โ you need pixel-perfect visual replicas (pdf2htmlEX), OCR-heavy scanned document conversion (marker, docling), or RAG-oriented element JSON (unstructured).
|
|
220
|
+
|
|
221
|
+
## ๐งช Testing
|
|
222
|
+
|
|
223
|
+
56 tests cover every pipeline stage plus end-to-end CLI runs over deterministic fixture PDFs
|
|
224
|
+
(report, two-column paper, slide deck, brochure, ruled table):
|
|
225
|
+
|
|
226
|
+
```bash
|
|
227
|
+
uv sync # dev deps (pytest, reportlab)
|
|
228
|
+
uv run pytest # run the suite
|
|
229
|
+
uv run python tests/fixtures/make_fixtures.py # regenerate fixture PDFs
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
Verbatim-ness is asserted mechanically: every source string drawn into a fixture must appear in the rendered HTML.
|
|
233
|
+
|
|
234
|
+
## ๐ Project structure
|
|
235
|
+
|
|
236
|
+
```
|
|
237
|
+
pdf-html/
|
|
238
|
+
โโโ src/pdf_html/
|
|
239
|
+
โ โโโ cli.py # argparse CLI โ pipeline โ HTML
|
|
240
|
+
โ โโโ extractor.py # PyMuPDF span + table-region extraction
|
|
241
|
+
โ โโโ header_footer.py # repeated page-furniture stripping
|
|
242
|
+
โ โโโ style_profiler.py # font-size histogram โ style profile
|
|
243
|
+
โ โโโ reading_order.py # column clustering & span ordering
|
|
244
|
+
โ โโโ table_reconstructor.py # cell assignment, per-cell pipeline, row merging
|
|
245
|
+
โ โโโ structure.py # heading / paragraph / list classification
|
|
246
|
+
โ โโโ list_parser.py # nested list folding
|
|
247
|
+
โ โโโ ast.py # typed document model
|
|
248
|
+
โ โโโ renderer.py # semantic HTML5 + derived CSS
|
|
249
|
+
โโโ tests/ # 56 tests + deterministic PDF fixtures
|
|
250
|
+
โโโ README.md
|
|
251
|
+
โโโ TUTORIAL.md # step-by-step usage guide
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
## ๐ฃ๏ธ Roadmap
|
|
255
|
+
|
|
256
|
+
- [x] Ruled-table reconstruction with cross-page row merging
|
|
257
|
+
- [x] Column-aware reading order with card-grid detection
|
|
258
|
+
- [x] Repeated header/footer stripping
|
|
259
|
+
- [ ] Borderless-table detection (whitespace-gap heuristic)
|
|
260
|
+
- [ ] Callout/aside detection (`--no-callouts` flag already reserved)
|
|
261
|
+
- [ ] Dependency-free `pdftotext` fallback extractor
|
|
262
|
+
- [ ] `colspan`/`rowspan` from merged-cell geometry
|
|
263
|
+
|
|
264
|
+
## ๐ค Contributing
|
|
265
|
+
|
|
266
|
+
Contributions welcome! Ground rules:
|
|
267
|
+
|
|
268
|
+
- ๐ Python 3.10+, type hints throughout
|
|
269
|
+
- ๐ฆ PyMuPDF is the **only** hard runtime dependency
|
|
270
|
+
- ๐งฉ Keep heuristics small, pure, and individually testable; thresholds as named constants
|
|
271
|
+
- โ
One feature per commit (`feat|fix|docs|refactor|chore: ...`); update README/TUTORIAL with any user-facing change
|
|
272
|
+
- ๐งช `uv run pytest` must stay green โ fixtures are the contract
|
|
273
|
+
|
|
274
|
+
## ๐ Releases
|
|
275
|
+
|
|
276
|
+
This project is intentionally published as an **early-stage v0.x** tool: the core pipeline is solid for text-based PDFs, but it is not a universal PDF converter for scanned pages, forms, or OCR-heavy corpora.
|
|
277
|
+
|
|
278
|
+
```bash
|
|
279
|
+
# 1. bump version in pyproject.toml
|
|
280
|
+
uv build # 2. artifacts land in dist/
|
|
281
|
+
uv publish # 3. push to PyPI (or twine upload dist/*)
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
## ๐ License
|
|
285
|
+
|
|
286
|
+
MIT โ see [pyproject.toml](pyproject.toml).
|
|
287
|
+
|
|
288
|
+
---
|
|
289
|
+
|
|
290
|
+
<div align="center">
|
|
291
|
+
|
|
292
|
+
**Built with ๐ + ๐ โ early-stage geometry over guesswork.**
|
|
293
|
+
|
|
294
|
+
*If this project helped you, consider giving it a โญ!*
|
|
295
|
+
|
|
296
|
+
</div>
|