opendockit 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.
- opendockit-0.1.0/.gitignore +24 -0
- opendockit-0.1.0/CHANGELOG.md +27 -0
- opendockit-0.1.0/LICENSE +21 -0
- opendockit-0.1.0/PKG-INFO +205 -0
- opendockit-0.1.0/README.md +160 -0
- opendockit-0.1.0/docs/decisions/core.md +130 -0
- opendockit-0.1.0/docs/decisions/csv.md +19 -0
- opendockit-0.1.0/docs/decisions/detect.md +24 -0
- opendockit-0.1.0/docs/decisions/encoding.md +19 -0
- opendockit-0.1.0/docs/decisions/extractors.md +70 -0
- opendockit-0.1.0/docs/decisions/html.md +34 -0
- opendockit-0.1.0/docs/decisions/hwp.md +45 -0
- opendockit-0.1.0/docs/decisions/images.md +35 -0
- opendockit-0.1.0/docs/decisions/mail.md +20 -0
- opendockit-0.1.0/docs/decisions/office-charts.md +19 -0
- opendockit-0.1.0/docs/decisions/office-limits.md +14 -0
- opendockit-0.1.0/docs/decisions/office-lists.md +25 -0
- opendockit-0.1.0/docs/decisions/office-math.md +16 -0
- opendockit-0.1.0/docs/decisions/office.md +46 -0
- opendockit-0.1.0/docs/decisions/pdf-borderless.md +26 -0
- opendockit-0.1.0/docs/decisions/pdf-cells.md +27 -0
- opendockit-0.1.0/docs/decisions/pdf-flow.md +41 -0
- opendockit-0.1.0/docs/decisions/pdf-fonts.md +14 -0
- opendockit-0.1.0/docs/decisions/pdf-images.md +13 -0
- opendockit-0.1.0/docs/decisions/pdf-limits.md +39 -0
- opendockit-0.1.0/docs/decisions/pdf-rows.md +33 -0
- opendockit-0.1.0/docs/decisions/pdf-tables.md +28 -0
- opendockit-0.1.0/docs/decisions/pdf.md +54 -0
- opendockit-0.1.0/docs/decisions/spreadsheet-cells.md +52 -0
- opendockit-0.1.0/docs/decisions/spreadsheet-tables.md +13 -0
- opendockit-0.1.0/docs/decisions/spreadsheet.md +60 -0
- opendockit-0.1.0/docs/design.md +114 -0
- opendockit-0.1.0/docs/formats.md +298 -0
- opendockit-0.1.0/docs/images.md +111 -0
- opendockit-0.1.0/docs/integration-guide.md +314 -0
- opendockit-0.1.0/docs/limitations.md +147 -0
- opendockit-0.1.0/docs/options.md +81 -0
- opendockit-0.1.0/docs/server.md +123 -0
- opendockit-0.1.0/pyproject.toml +112 -0
- opendockit-0.1.0/src/opendockit/__init__.py +69 -0
- opendockit-0.1.0/src/opendockit/__main__.py +9 -0
- opendockit-0.1.0/src/opendockit/cli.py +135 -0
- opendockit-0.1.0/src/opendockit/converter.py +562 -0
- opendockit-0.1.0/src/opendockit/describe.py +371 -0
- opendockit-0.1.0/src/opendockit/detect.py +550 -0
- opendockit-0.1.0/src/opendockit/exceptions.py +23 -0
- opendockit-0.1.0/src/opendockit/extractors/__init__.py +38 -0
- opendockit-0.1.0/src/opendockit/extractors/_equation.py +479 -0
- opendockit-0.1.0/src/opendockit/extractors/_shared.py +1684 -0
- opendockit-0.1.0/src/opendockit/extractors/docx.py +1687 -0
- opendockit-0.1.0/src/opendockit/extractors/html.py +2585 -0
- opendockit-0.1.0/src/opendockit/extractors/hwp.py +1063 -0
- opendockit-0.1.0/src/opendockit/extractors/hwpx.py +870 -0
- opendockit-0.1.0/src/opendockit/extractors/image.py +39 -0
- opendockit-0.1.0/src/opendockit/extractors/pdf.py +9091 -0
- opendockit-0.1.0/src/opendockit/extractors/pptx.py +940 -0
- opendockit-0.1.0/src/opendockit/extractors/spreadsheet.py +4117 -0
- opendockit-0.1.0/src/opendockit/extractors/text.py +1620 -0
- opendockit-0.1.0/src/opendockit/image.py +340 -0
- opendockit-0.1.0/src/opendockit/isolated.py +212 -0
- opendockit-0.1.0/src/opendockit/markdown.py +535 -0
- opendockit-0.1.0/src/opendockit/models.py +348 -0
- opendockit-0.1.0/src/opendockit/py.typed +0 -0
- opendockit-0.1.0/tests/__init__.py +10 -0
- opendockit-0.1.0/tests/builder.py +653 -0
- opendockit-0.1.0/tests/conftest.py +13 -0
- opendockit-0.1.0/tests/test_cli.py +213 -0
- opendockit-0.1.0/tests/test_converter.py +464 -0
- opendockit-0.1.0/tests/test_core_front_cut_and_tiff_pixels.py +85 -0
- opendockit-0.1.0/tests/test_core_header_tables_cell_code_and_terminal_codes.py +123 -0
- opendockit-0.1.0/tests/test_core_html.py +361 -0
- opendockit-0.1.0/tests/test_core_html_blocks.py +102 -0
- opendockit-0.1.0/tests/test_core_html_layout_cells.py +188 -0
- opendockit-0.1.0/tests/test_core_html_mail_hidden_and_frames.py +379 -0
- opendockit-0.1.0/tests/test_core_html_raw_text_and_removal.py +151 -0
- opendockit-0.1.0/tests/test_core_image_and_assembly.py +187 -0
- opendockit-0.1.0/tests/test_core_mail_and_detect.py +281 -0
- opendockit-0.1.0/tests/test_core_mail_bodies_and_events.py +97 -0
- opendockit-0.1.0/tests/test_core_mail_parts_and_icons.py +386 -0
- opendockit-0.1.0/tests/test_core_mailboxes_and_charsets.py +241 -0
- opendockit-0.1.0/tests/test_core_malformed_mail_and_html.py +147 -0
- opendockit-0.1.0/tests/test_core_mbox_markdown_and_api.py +126 -0
- opendockit-0.1.0/tests/test_core_round44.py +170 -0
- opendockit-0.1.0/tests/test_core_round45.py +166 -0
- opendockit-0.1.0/tests/test_core_round46.py +39 -0
- opendockit-0.1.0/tests/test_core_round47.py +58 -0
- opendockit-0.1.0/tests/test_core_round48.py +92 -0
- opendockit-0.1.0/tests/test_core_sheet_layouts.py +193 -0
- opendockit-0.1.0/tests/test_core_spreadsheet.py +527 -0
- opendockit-0.1.0/tests/test_core_text_and_mail.py +440 -0
- opendockit-0.1.0/tests/test_core_th_rows_front_tables_and_linear_time.py +118 -0
- opendockit-0.1.0/tests/test_core_word_lists_cell_blocks_and_mail_lines.py +133 -0
- opendockit-0.1.0/tests/test_detect.py +665 -0
- opendockit-0.1.0/tests/test_equation.py +217 -0
- opendockit-0.1.0/tests/test_hwp.py +1354 -0
- opendockit-0.1.0/tests/test_hwp_heads_and_shapes.py +454 -0
- opendockit-0.1.0/tests/test_hwp_object_only_numbers.py +88 -0
- opendockit-0.1.0/tests/test_hwp_robustness.py +393 -0
- opendockit-0.1.0/tests/test_image.py +1660 -0
- opendockit-0.1.0/tests/test_isolated.py +222 -0
- opendockit-0.1.0/tests/test_live_vision.py +221 -0
- opendockit-0.1.0/tests/test_markdown.py +795 -0
- opendockit-0.1.0/tests/test_office.py +3374 -0
- opendockit-0.1.0/tests/test_office_cells_and_notes.py +139 -0
- opendockit-0.1.0/tests/test_office_chart_units_and_ole.py +305 -0
- opendockit-0.1.0/tests/test_office_comments.py +201 -0
- opendockit-0.1.0/tests/test_office_drawing_line_breaks.py +84 -0
- opendockit-0.1.0/tests/test_office_equations.py +196 -0
- opendockit-0.1.0/tests/test_office_revisions_and_numbering.py +246 -0
- opendockit-0.1.0/tests/test_office_robustness.py +293 -0
- opendockit-0.1.0/tests/test_office_round43.py +185 -0
- opendockit-0.1.0/tests/test_office_round44.py +144 -0
- opendockit-0.1.0/tests/test_office_round45.py +263 -0
- opendockit-0.1.0/tests/test_office_scaling_and_note_order.py +174 -0
- opendockit-0.1.0/tests/test_office_symbols_fields_notes.py +343 -0
- opendockit-0.1.0/tests/test_office_table_char_limit.py +158 -0
- opendockit-0.1.0/tests/test_pdf.py +3551 -0
- opendockit-0.1.0/tests/test_pdf_attachments.py +41 -0
- opendockit-0.1.0/tests/test_pdf_cell_wraps.py +79 -0
- opendockit-0.1.0/tests/test_pdf_character_spacing.py +39 -0
- opendockit-0.1.0/tests/test_pdf_fonts.py +121 -0
- opendockit-0.1.0/tests/test_pdf_form_fields.py +124 -0
- opendockit-0.1.0/tests/test_pdf_form_grids.py +39 -0
- opendockit-0.1.0/tests/test_pdf_forms_and_amounts.py +141 -0
- opendockit-0.1.0/tests/test_pdf_highlights.py +373 -0
- opendockit-0.1.0/tests/test_pdf_images.py +169 -0
- opendockit-0.1.0/tests/test_pdf_label_value_rows.py +421 -0
- opendockit-0.1.0/tests/test_pdf_nested_tables.py +97 -0
- opendockit-0.1.0/tests/test_pdf_page_boxes.py +31 -0
- opendockit-0.1.0/tests/test_pdf_pictures_beside_text.py +70 -0
- opendockit-0.1.0/tests/test_pdf_reading_order.py +351 -0
- opendockit-0.1.0/tests/test_pdf_resource_limits.py +792 -0
- opendockit-0.1.0/tests/test_pdf_rotated_pages.py +99 -0
- opendockit-0.1.0/tests/test_pdf_round44.py +179 -0
- opendockit-0.1.0/tests/test_pdf_round45.py +201 -0
- opendockit-0.1.0/tests/test_pdf_round46.py +256 -0
- opendockit-0.1.0/tests/test_pdf_round47.py +55 -0
- opendockit-0.1.0/tests/test_pdf_round48.py +194 -0
- opendockit-0.1.0/tests/test_pdf_ruled_wrapped_cells.py +121 -0
- opendockit-0.1.0/tests/test_pdf_shaded_tables.py +278 -0
- opendockit-0.1.0/tests/test_pdf_table_columns.py +1041 -0
- opendockit-0.1.0/tests/test_pdf_toc_and_checkboxes.py +458 -0
- opendockit-0.1.0/tests/test_quality.py +739 -0
- opendockit-0.1.0/tests/test_spreadsheet.py +2359 -0
- opendockit-0.1.0/tests/test_spreadsheet_cell_values_and_controls.py +489 -0
- opendockit-0.1.0/tests/test_spreadsheet_damaged_parts.py +206 -0
- opendockit-0.1.0/tests/test_spreadsheet_notes_pictures_and_times.py +234 -0
- opendockit-0.1.0/tests/test_text_html.py +1607 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*.egg-info/
|
|
5
|
+
build/
|
|
6
|
+
dist/
|
|
7
|
+
.venv/
|
|
8
|
+
|
|
9
|
+
# Tools
|
|
10
|
+
.pytest_cache/
|
|
11
|
+
.ruff_cache/
|
|
12
|
+
.mypy_cache/
|
|
13
|
+
.coverage
|
|
14
|
+
htmlcov/
|
|
15
|
+
|
|
16
|
+
# Editors / OS
|
|
17
|
+
.idea/
|
|
18
|
+
.vscode/
|
|
19
|
+
.DS_Store
|
|
20
|
+
|
|
21
|
+
# Local only: sample documents and backups never go into git
|
|
22
|
+
data/
|
|
23
|
+
.backup/
|
|
24
|
+
.env
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented here.
|
|
4
|
+
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project uses
|
|
5
|
+
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
|
+
|
|
7
|
+
## [Unreleased]
|
|
8
|
+
|
|
9
|
+
## [0.1.0] - 2026-11-12
|
|
10
|
+
|
|
11
|
+
First public release.
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- `convert`, `convert_file` and `aconvert`: PDF, DOCX, PPTX, XLSX, XLS, Excel 2003 XML, CSV, HWP, HWPX, HTML/MHTML,
|
|
16
|
+
email (`.eml`, mbox), TXT, Markdown and image files to Markdown.
|
|
17
|
+
- `ConversionResult` with per-page / slide / sheet sections, metadata, warnings, extracted images and a character count.
|
|
18
|
+
- `ConvertOptions` with a character budget (`max_chars`) that stops conversion instead of truncating its output.
|
|
19
|
+
- `IsolatedConverter`: conversion in disposable worker processes with memory and time limits.
|
|
20
|
+
- `describe_images` and `OpenAIImageDescriber` (optional `openai` extra) to describe pictures and transcribe scanned pages;
|
|
21
|
+
`OpenAIImageDescriber.check()` to fail fast on a missing extra or API key.
|
|
22
|
+
- `opendockit` command-line tool.
|
|
23
|
+
- `examples/`: sample documents for every format with their converted output.
|
|
24
|
+
- Optional `heic` extra for HEIC/HEIF images (kept out of the default install because its wheels bundle GPL/LGPL codecs).
|
|
25
|
+
|
|
26
|
+
[Unreleased]: https://github.com/hyeonsang010716/OpenDocKit/compare/v0.1.0...HEAD
|
|
27
|
+
[0.1.0]: https://github.com/hyeonsang010716/OpenDocKit/releases/tag/v0.1.0
|
opendockit-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 OpenDocKit contributors
|
|
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.
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: opendockit
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Convert PDF, Office, HWP/HWPX, HTML, email, spreadsheets and images into clean, LLM-ready Markdown.
|
|
5
|
+
Project-URL: Homepage, https://github.com/hyeonsang010716/OpenDocKit
|
|
6
|
+
Project-URL: Documentation, https://github.com/hyeonsang010716/OpenDocKit/tree/main/docs
|
|
7
|
+
Project-URL: Issues, https://github.com/hyeonsang010716/OpenDocKit/issues
|
|
8
|
+
Project-URL: Changelog, https://github.com/hyeonsang010716/OpenDocKit/blob/main/CHANGELOG.md
|
|
9
|
+
Author: OpenDocKit contributors
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: document-conversion,docx,hwp,hwpx,llm,markdown,pdf,pptx,rag,xlsx
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Operating System :: MacOS
|
|
16
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
22
|
+
Classifier: Topic :: Office/Business :: Office Suites
|
|
23
|
+
Classifier: Topic :: Text Processing :: Markup :: Markdown
|
|
24
|
+
Classifier: Typing :: Typed
|
|
25
|
+
Requires-Python: >=3.12
|
|
26
|
+
Requires-Dist: beautifulsoup4!=4.14.0,>=4.13
|
|
27
|
+
Requires-Dist: charset-normalizer>=3.5
|
|
28
|
+
Requires-Dist: lxml>=6.0
|
|
29
|
+
Requires-Dist: olefile>=0.47
|
|
30
|
+
Requires-Dist: openpyxl>=3.1.4
|
|
31
|
+
Requires-Dist: pdfminer-six>=20251230
|
|
32
|
+
Requires-Dist: pdfplumber>=0.11.9
|
|
33
|
+
Requires-Dist: pillow>=11.3
|
|
34
|
+
Requires-Dist: pypdfium2>=4.30
|
|
35
|
+
Requires-Dist: python-docx>=1.1
|
|
36
|
+
Requires-Dist: python-pptx>=1.0
|
|
37
|
+
Requires-Dist: xlrd>=2.0.1
|
|
38
|
+
Provides-Extra: heic
|
|
39
|
+
Requires-Dist: pillow-heif>=0.16; extra == 'heic'
|
|
40
|
+
Provides-Extra: openai
|
|
41
|
+
Requires-Dist: httpx>=0.27; extra == 'openai'
|
|
42
|
+
Requires-Dist: langchain-core>=1.0; extra == 'openai'
|
|
43
|
+
Requires-Dist: langchain-openai>=1.0; extra == 'openai'
|
|
44
|
+
Description-Content-Type: text/markdown
|
|
45
|
+
|
|
46
|
+
<h1 align="center">OpenDocKit</h1>
|
|
47
|
+
|
|
48
|
+
<p align="center">
|
|
49
|
+
<b>Turn any document into clean, LLM-ready Markdown.</b><br>
|
|
50
|
+
PDF · DOCX · PPTX · XLSX · HWP/HWPX · HTML · email · CSV · images — one function, no GPU, MIT licensed.
|
|
51
|
+
</p>
|
|
52
|
+
|
|
53
|
+
<p align="center">
|
|
54
|
+
<a href="https://pypi.org/project/opendockit/"><img alt="PyPI" src="https://img.shields.io/pypi/v/opendockit"></a>
|
|
55
|
+
<a href="https://pypi.org/project/opendockit/"><img alt="Python" src="https://img.shields.io/pypi/pyversions/opendockit"></a>
|
|
56
|
+
<a href="https://github.com/hyeonsang010716/OpenDocKit/actions/workflows/ci.yml"><img alt="CI" src="https://github.com/hyeonsang010716/OpenDocKit/actions/workflows/ci.yml/badge.svg"></a>
|
|
57
|
+
<a href="https://github.com/hyeonsang010716/OpenDocKit/blob/main/LICENSE"><img alt="License: MIT" src="https://img.shields.io/badge/license-MIT-blue"></a>
|
|
58
|
+
</p>
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
from opendockit import convert_file
|
|
64
|
+
|
|
65
|
+
result = convert_file("quarterly-report.hwp")
|
|
66
|
+
|
|
67
|
+
result.text # Markdown: headings, tables, lists, footnotes, LaTeX equations
|
|
68
|
+
result.sections # per page / slide / sheet — for chunking and citations ("page 3")
|
|
69
|
+
result.warnings # what was skipped or approximated, in plain sentences
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Why OpenDocKit
|
|
73
|
+
|
|
74
|
+
Most converters handle the happy path. OpenDocKit is built for the files people actually upload.
|
|
75
|
+
|
|
76
|
+
- **Structure, not just text.** Merged table cells, multi-column PDFs, borderless tables, nested lists, footnotes,
|
|
77
|
+
speaker notes, native charts as data tables, SmartArt as nested lists, Word / PowerPoint / Hancom equations as LaTeX.
|
|
78
|
+
- **First-class Korean documents.** A from-scratch HWP 5.x parser, HWPX, CP949 detection, NFC normalization, and the
|
|
79
|
+
form-heavy tables Korean offices produce. Most general-purpose converters cannot read these at all.
|
|
80
|
+
- **Detects by content, not extension.** An HTML table saved as `.xls`, a CSV with a title block, an MHTML page named
|
|
81
|
+
`.doc` — each is read as what it really is, and a warning tells you.
|
|
82
|
+
- **Safe on untrusted uploads.** Zip bombs, runaway merged-cell counts, recursive PDF objects and multi-gigabyte images
|
|
83
|
+
are bounded. `IsolatedConverter` runs every file in a disposable worker process with memory and time limits.
|
|
84
|
+
- **Images in place.** Pictures, charts and scanned pages are extracted, filtered, and — if you plug in a vision
|
|
85
|
+
model — described right where they appeared in the document.
|
|
86
|
+
- **A hard character budget.** `max_chars` *stops* work instead of truncating afterwards, so a file that will be
|
|
87
|
+
rejected anyway costs neither parsing time nor vision-model calls.
|
|
88
|
+
- **Permissive by default.** MIT licensed; everything a plain `pip install opendockit` brings in is MIT / BSD /
|
|
89
|
+
Apache-style (no PyMuPDF, no pyhwp), with no ML models and no GPU. The only copyleft code is behind the opt-in
|
|
90
|
+
`heic` extra.
|
|
91
|
+
|
|
92
|
+
## Installation
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
pip install opendockit # conversion only — never touches the network
|
|
96
|
+
pip install "opendockit[openai]" # + image description with a vision model
|
|
97
|
+
pip install "opendockit[heic]" # + HEIC/HEIF images (pulls in GPL/LGPL-licensed codec libraries)
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Requires Python 3.12 or newer on Linux or macOS. Windows is not supported yet for HTML and email
|
|
101
|
+
([why](https://github.com/hyeonsang010716/OpenDocKit/blob/main/docs/limitations.md#platforms)).
|
|
102
|
+
|
|
103
|
+
## Quick start
|
|
104
|
+
|
|
105
|
+
### Convert
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
from opendockit import convert, convert_file
|
|
109
|
+
|
|
110
|
+
result = convert_file("report.pdf") # from a path
|
|
111
|
+
result = convert(data, "report.pdf") # from bytes (the filename is only a hint)
|
|
112
|
+
|
|
113
|
+
print(result.text)
|
|
114
|
+
print(result.char_count, result.exceeded) # size of the output, and whether the limit stopped it
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
`convert` is synchronous, offline and deterministic. Pictures are collected in `result.images` and the text keeps an
|
|
118
|
+
`[Image]` placeholder where each one was.
|
|
119
|
+
|
|
120
|
+
### Describe images with a vision model
|
|
121
|
+
|
|
122
|
+
```python
|
|
123
|
+
from opendockit import OpenAIImageDescriber, aconvert
|
|
124
|
+
|
|
125
|
+
result = await aconvert(data, "report.pdf", describer=OpenAIImageDescriber()) # reads OPENAI_API_KEY
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Charts become data tables, diagrams become lists, scanned pages are transcribed, and decorative images are dropped.
|
|
129
|
+
`OpenAIImageDescriber(model)` accepts any LangChain chat model, and anything with an
|
|
130
|
+
`async describe(image) -> str` method works as a describer. See [docs/images.md](https://github.com/hyeonsang010716/OpenDocKit/blob/main/docs/images.md).
|
|
131
|
+
|
|
132
|
+
### Serve it
|
|
133
|
+
|
|
134
|
+
```python
|
|
135
|
+
from opendockit import ConversionError, IsolatedConverter
|
|
136
|
+
|
|
137
|
+
async with IsolatedConverter(workers=2) as converter: # 2 GB and 2 minutes per file by default
|
|
138
|
+
try:
|
|
139
|
+
result = await converter.convert(data, filename)
|
|
140
|
+
except ConversionError as exc:
|
|
141
|
+
print(exc.code, exc) # stable code + a message safe to show users
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
One bad file ends one worker; your server and the next upload carry on. See [docs/server.md](https://github.com/hyeonsang010716/OpenDocKit/blob/main/docs/server.md).
|
|
145
|
+
|
|
146
|
+
### Command line
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
opendockit report.pdf # Markdown on stdout, warnings on stderr
|
|
150
|
+
opendockit report.pdf --json # sections, metadata, warnings, images
|
|
151
|
+
opendockit report.pdf --count # just the character count
|
|
152
|
+
opendockit scan.pdf --describe # describe images with a vision model (uses OPENAI_API_KEY)
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## Supported formats
|
|
156
|
+
|
|
157
|
+
| Format | Sections | Highlights |
|
|
158
|
+
| --- | --- | --- |
|
|
159
|
+
| PDF | page | reading order across columns, ruled and borderless tables, form field values, headings from font size, repeated header/footer removal, scanned pages via vision model |
|
|
160
|
+
| DOCX | document | heading styles, list numbering, merged cells, charts → tables, SmartArt, footnotes/endnotes, equations → LaTeX, comments |
|
|
161
|
+
| PPTX | slide | on-screen order, bullet levels, tables, charts → tables, SmartArt, equations, speaker notes |
|
|
162
|
+
| XLSX · XLS · Excel 2003 XML · CSV | sheet | several tables per sheet, merged cells, number and date formats, charts, cell comments |
|
|
163
|
+
| HWP · HWPX | document | outline headings, numbering, merged cells, captions, footnotes, equations → LaTeX |
|
|
164
|
+
| HTML · MHTML | document | tables with rowspan/colspan, layout tables unwrapped, MathML → LaTeX, hidden elements removed |
|
|
165
|
+
| Email (`.eml`, mbox) | document | headers, HTML or plain body, forwarded messages, calendar invites |
|
|
166
|
+
| TXT · Markdown | document | encoding detection (UTF-8, CP949, UTF-16, Windows-1252) |
|
|
167
|
+
| Images | document / page | JPEG, PNG, WebP, GIF, BMP, TIFF, AVIF, and HEIC with the `heic` extra — described by a vision model |
|
|
168
|
+
|
|
169
|
+
Not supported: legacy `.doc`/`.ppt`, RTF, ODF. The full list of what each format restores is in
|
|
170
|
+
[docs/formats.md](https://github.com/hyeonsang010716/OpenDocKit/blob/main/docs/formats.md); the honest list of what it cannot do is in [docs/limitations.md](https://github.com/hyeonsang010716/OpenDocKit/blob/main/docs/limitations.md).
|
|
171
|
+
|
|
172
|
+
## Examples
|
|
173
|
+
|
|
174
|
+
[`examples/`](https://github.com/hyeonsang010716/OpenDocKit/tree/main/examples/) has one sample document per format and the Markdown OpenDocKit produces from each — with and
|
|
175
|
+
without a vision model — so you can judge the output before installing anything.
|
|
176
|
+
|
|
177
|
+
## Documentation
|
|
178
|
+
|
|
179
|
+
| | |
|
|
180
|
+
| --- | --- |
|
|
181
|
+
| [Formats](https://github.com/hyeonsang010716/OpenDocKit/blob/main/docs/formats.md) | What is extracted from each format and how it is rendered |
|
|
182
|
+
| [Options](https://github.com/hyeonsang010716/OpenDocKit/blob/main/docs/options.md) | `ConvertOptions`, exceptions and error codes |
|
|
183
|
+
| [Images](https://github.com/hyeonsang010716/OpenDocKit/blob/main/docs/images.md) | Extraction, filtering, vision models, custom describers |
|
|
184
|
+
| [Running on a server](https://github.com/hyeonsang010716/OpenDocKit/blob/main/docs/server.md) | `IsolatedConverter`, resource limits, the character budget |
|
|
185
|
+
| [Integration guide](https://github.com/hyeonsang010716/OpenDocKit/blob/main/docs/integration-guide.md) | What an upload UI should tell its users |
|
|
186
|
+
| [Design](https://github.com/hyeonsang010716/OpenDocKit/blob/main/docs/design.md) | Why it is built this way, and how it was verified |
|
|
187
|
+
| [Limitations](https://github.com/hyeonsang010716/OpenDocKit/blob/main/docs/limitations.md) | Known gaps, stated plainly |
|
|
188
|
+
| [Decision records](https://github.com/hyeonsang010716/OpenDocKit/tree/main/docs/decisions/) | Every deliberate trade-off, with the test that pins it |
|
|
189
|
+
|
|
190
|
+
## Contributing
|
|
191
|
+
|
|
192
|
+
Contributions are welcome — bug reports with a minimal reproducing document are especially valuable.
|
|
193
|
+
Start with [CONTRIBUTING.md](https://github.com/hyeonsang010716/OpenDocKit/blob/main/CONTRIBUTING.md).
|
|
194
|
+
|
|
195
|
+
```bash
|
|
196
|
+
git clone https://github.com/hyeonsang010716/OpenDocKit.git && cd OpenDocKit
|
|
197
|
+
uv sync
|
|
198
|
+
uv run pytest -n auto -m "not timing" # the bulk of the suite, in parallel
|
|
199
|
+
uv run pytest -m timing # tests that measure time, one at a time
|
|
200
|
+
uv run ruff check
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
## License
|
|
204
|
+
|
|
205
|
+
[MIT](https://github.com/hyeonsang010716/OpenDocKit/blob/main/LICENSE)
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
<h1 align="center">OpenDocKit</h1>
|
|
2
|
+
|
|
3
|
+
<p align="center">
|
|
4
|
+
<b>Turn any document into clean, LLM-ready Markdown.</b><br>
|
|
5
|
+
PDF · DOCX · PPTX · XLSX · HWP/HWPX · HTML · email · CSV · images — one function, no GPU, MIT licensed.
|
|
6
|
+
</p>
|
|
7
|
+
|
|
8
|
+
<p align="center">
|
|
9
|
+
<a href="https://pypi.org/project/opendockit/"><img alt="PyPI" src="https://img.shields.io/pypi/v/opendockit"></a>
|
|
10
|
+
<a href="https://pypi.org/project/opendockit/"><img alt="Python" src="https://img.shields.io/pypi/pyversions/opendockit"></a>
|
|
11
|
+
<a href="https://github.com/hyeonsang010716/OpenDocKit/actions/workflows/ci.yml"><img alt="CI" src="https://github.com/hyeonsang010716/OpenDocKit/actions/workflows/ci.yml/badge.svg"></a>
|
|
12
|
+
<a href="https://github.com/hyeonsang010716/OpenDocKit/blob/main/LICENSE"><img alt="License: MIT" src="https://img.shields.io/badge/license-MIT-blue"></a>
|
|
13
|
+
</p>
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
```python
|
|
18
|
+
from opendockit import convert_file
|
|
19
|
+
|
|
20
|
+
result = convert_file("quarterly-report.hwp")
|
|
21
|
+
|
|
22
|
+
result.text # Markdown: headings, tables, lists, footnotes, LaTeX equations
|
|
23
|
+
result.sections # per page / slide / sheet — for chunking and citations ("page 3")
|
|
24
|
+
result.warnings # what was skipped or approximated, in plain sentences
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Why OpenDocKit
|
|
28
|
+
|
|
29
|
+
Most converters handle the happy path. OpenDocKit is built for the files people actually upload.
|
|
30
|
+
|
|
31
|
+
- **Structure, not just text.** Merged table cells, multi-column PDFs, borderless tables, nested lists, footnotes,
|
|
32
|
+
speaker notes, native charts as data tables, SmartArt as nested lists, Word / PowerPoint / Hancom equations as LaTeX.
|
|
33
|
+
- **First-class Korean documents.** A from-scratch HWP 5.x parser, HWPX, CP949 detection, NFC normalization, and the
|
|
34
|
+
form-heavy tables Korean offices produce. Most general-purpose converters cannot read these at all.
|
|
35
|
+
- **Detects by content, not extension.** An HTML table saved as `.xls`, a CSV with a title block, an MHTML page named
|
|
36
|
+
`.doc` — each is read as what it really is, and a warning tells you.
|
|
37
|
+
- **Safe on untrusted uploads.** Zip bombs, runaway merged-cell counts, recursive PDF objects and multi-gigabyte images
|
|
38
|
+
are bounded. `IsolatedConverter` runs every file in a disposable worker process with memory and time limits.
|
|
39
|
+
- **Images in place.** Pictures, charts and scanned pages are extracted, filtered, and — if you plug in a vision
|
|
40
|
+
model — described right where they appeared in the document.
|
|
41
|
+
- **A hard character budget.** `max_chars` *stops* work instead of truncating afterwards, so a file that will be
|
|
42
|
+
rejected anyway costs neither parsing time nor vision-model calls.
|
|
43
|
+
- **Permissive by default.** MIT licensed; everything a plain `pip install opendockit` brings in is MIT / BSD /
|
|
44
|
+
Apache-style (no PyMuPDF, no pyhwp), with no ML models and no GPU. The only copyleft code is behind the opt-in
|
|
45
|
+
`heic` extra.
|
|
46
|
+
|
|
47
|
+
## Installation
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
pip install opendockit # conversion only — never touches the network
|
|
51
|
+
pip install "opendockit[openai]" # + image description with a vision model
|
|
52
|
+
pip install "opendockit[heic]" # + HEIC/HEIF images (pulls in GPL/LGPL-licensed codec libraries)
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Requires Python 3.12 or newer on Linux or macOS. Windows is not supported yet for HTML and email
|
|
56
|
+
([why](https://github.com/hyeonsang010716/OpenDocKit/blob/main/docs/limitations.md#platforms)).
|
|
57
|
+
|
|
58
|
+
## Quick start
|
|
59
|
+
|
|
60
|
+
### Convert
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
from opendockit import convert, convert_file
|
|
64
|
+
|
|
65
|
+
result = convert_file("report.pdf") # from a path
|
|
66
|
+
result = convert(data, "report.pdf") # from bytes (the filename is only a hint)
|
|
67
|
+
|
|
68
|
+
print(result.text)
|
|
69
|
+
print(result.char_count, result.exceeded) # size of the output, and whether the limit stopped it
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
`convert` is synchronous, offline and deterministic. Pictures are collected in `result.images` and the text keeps an
|
|
73
|
+
`[Image]` placeholder where each one was.
|
|
74
|
+
|
|
75
|
+
### Describe images with a vision model
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
from opendockit import OpenAIImageDescriber, aconvert
|
|
79
|
+
|
|
80
|
+
result = await aconvert(data, "report.pdf", describer=OpenAIImageDescriber()) # reads OPENAI_API_KEY
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Charts become data tables, diagrams become lists, scanned pages are transcribed, and decorative images are dropped.
|
|
84
|
+
`OpenAIImageDescriber(model)` accepts any LangChain chat model, and anything with an
|
|
85
|
+
`async describe(image) -> str` method works as a describer. See [docs/images.md](https://github.com/hyeonsang010716/OpenDocKit/blob/main/docs/images.md).
|
|
86
|
+
|
|
87
|
+
### Serve it
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
from opendockit import ConversionError, IsolatedConverter
|
|
91
|
+
|
|
92
|
+
async with IsolatedConverter(workers=2) as converter: # 2 GB and 2 minutes per file by default
|
|
93
|
+
try:
|
|
94
|
+
result = await converter.convert(data, filename)
|
|
95
|
+
except ConversionError as exc:
|
|
96
|
+
print(exc.code, exc) # stable code + a message safe to show users
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
One bad file ends one worker; your server and the next upload carry on. See [docs/server.md](https://github.com/hyeonsang010716/OpenDocKit/blob/main/docs/server.md).
|
|
100
|
+
|
|
101
|
+
### Command line
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
opendockit report.pdf # Markdown on stdout, warnings on stderr
|
|
105
|
+
opendockit report.pdf --json # sections, metadata, warnings, images
|
|
106
|
+
opendockit report.pdf --count # just the character count
|
|
107
|
+
opendockit scan.pdf --describe # describe images with a vision model (uses OPENAI_API_KEY)
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Supported formats
|
|
111
|
+
|
|
112
|
+
| Format | Sections | Highlights |
|
|
113
|
+
| --- | --- | --- |
|
|
114
|
+
| PDF | page | reading order across columns, ruled and borderless tables, form field values, headings from font size, repeated header/footer removal, scanned pages via vision model |
|
|
115
|
+
| DOCX | document | heading styles, list numbering, merged cells, charts → tables, SmartArt, footnotes/endnotes, equations → LaTeX, comments |
|
|
116
|
+
| PPTX | slide | on-screen order, bullet levels, tables, charts → tables, SmartArt, equations, speaker notes |
|
|
117
|
+
| XLSX · XLS · Excel 2003 XML · CSV | sheet | several tables per sheet, merged cells, number and date formats, charts, cell comments |
|
|
118
|
+
| HWP · HWPX | document | outline headings, numbering, merged cells, captions, footnotes, equations → LaTeX |
|
|
119
|
+
| HTML · MHTML | document | tables with rowspan/colspan, layout tables unwrapped, MathML → LaTeX, hidden elements removed |
|
|
120
|
+
| Email (`.eml`, mbox) | document | headers, HTML or plain body, forwarded messages, calendar invites |
|
|
121
|
+
| TXT · Markdown | document | encoding detection (UTF-8, CP949, UTF-16, Windows-1252) |
|
|
122
|
+
| Images | document / page | JPEG, PNG, WebP, GIF, BMP, TIFF, AVIF, and HEIC with the `heic` extra — described by a vision model |
|
|
123
|
+
|
|
124
|
+
Not supported: legacy `.doc`/`.ppt`, RTF, ODF. The full list of what each format restores is in
|
|
125
|
+
[docs/formats.md](https://github.com/hyeonsang010716/OpenDocKit/blob/main/docs/formats.md); the honest list of what it cannot do is in [docs/limitations.md](https://github.com/hyeonsang010716/OpenDocKit/blob/main/docs/limitations.md).
|
|
126
|
+
|
|
127
|
+
## Examples
|
|
128
|
+
|
|
129
|
+
[`examples/`](https://github.com/hyeonsang010716/OpenDocKit/tree/main/examples/) has one sample document per format and the Markdown OpenDocKit produces from each — with and
|
|
130
|
+
without a vision model — so you can judge the output before installing anything.
|
|
131
|
+
|
|
132
|
+
## Documentation
|
|
133
|
+
|
|
134
|
+
| | |
|
|
135
|
+
| --- | --- |
|
|
136
|
+
| [Formats](https://github.com/hyeonsang010716/OpenDocKit/blob/main/docs/formats.md) | What is extracted from each format and how it is rendered |
|
|
137
|
+
| [Options](https://github.com/hyeonsang010716/OpenDocKit/blob/main/docs/options.md) | `ConvertOptions`, exceptions and error codes |
|
|
138
|
+
| [Images](https://github.com/hyeonsang010716/OpenDocKit/blob/main/docs/images.md) | Extraction, filtering, vision models, custom describers |
|
|
139
|
+
| [Running on a server](https://github.com/hyeonsang010716/OpenDocKit/blob/main/docs/server.md) | `IsolatedConverter`, resource limits, the character budget |
|
|
140
|
+
| [Integration guide](https://github.com/hyeonsang010716/OpenDocKit/blob/main/docs/integration-guide.md) | What an upload UI should tell its users |
|
|
141
|
+
| [Design](https://github.com/hyeonsang010716/OpenDocKit/blob/main/docs/design.md) | Why it is built this way, and how it was verified |
|
|
142
|
+
| [Limitations](https://github.com/hyeonsang010716/OpenDocKit/blob/main/docs/limitations.md) | Known gaps, stated plainly |
|
|
143
|
+
| [Decision records](https://github.com/hyeonsang010716/OpenDocKit/tree/main/docs/decisions/) | Every deliberate trade-off, with the test that pins it |
|
|
144
|
+
|
|
145
|
+
## Contributing
|
|
146
|
+
|
|
147
|
+
Contributions are welcome — bug reports with a minimal reproducing document are especially valuable.
|
|
148
|
+
Start with [CONTRIBUTING.md](https://github.com/hyeonsang010716/OpenDocKit/blob/main/CONTRIBUTING.md).
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
git clone https://github.com/hyeonsang010716/OpenDocKit.git && cd OpenDocKit
|
|
152
|
+
uv sync
|
|
153
|
+
uv run pytest -n auto -m "not timing" # the bulk of the suite, in parallel
|
|
154
|
+
uv run pytest -m timing # tests that measure time, one at a time
|
|
155
|
+
uv run ruff check
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
## License
|
|
159
|
+
|
|
160
|
+
[MIT](https://github.com/hyeonsang010716/OpenDocKit/blob/main/LICENSE)
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# Core decisions
|
|
2
|
+
|
|
3
|
+
Guide for agents and developers working on `src/opendockit/`: package structure, dependency direction and the decisions shared by every format. Usage, options and limits are in the [README](../../README.md), [options.md](../options.md) and [limitations.md](../limitations.md).
|
|
4
|
+
|
|
5
|
+
## Structure and dependency direction
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
src/opendockit/
|
|
9
|
+
├── __init__.py # public API (convert, aconvert, IsolatedConverter, describe_images, OpenAIImageDescriber, ConvertOptions, exceptions)
|
|
10
|
+
├── cli.py # command line: opendockit <file> [--json] [--describe]
|
|
11
|
+
├── converter.py # size check → detect → extractor → clean_text → image filtering → render (fill placeholders, assembly, character count, limit verdict)
|
|
12
|
+
├── isolated.py # isolated conversion for servers (IsolatedConverter): a new worker process per file, memory and time limits, a crash fails only that file
|
|
13
|
+
├── image.py # image decoding, downscaling, PNG/JPEG re-encoding, image-file magic bytes
|
|
14
|
+
├── describe.py # vision-model descriptions (LangChain ChatOpenAI). The only module that uses the network
|
|
15
|
+
├── detect.py # magic-byte detection. The extension is used only to tell the text family apart (TXT, MD, CSV, HTML) (decisions in detect.md)
|
|
16
|
+
├── markdown.py # clean_text · render_table · TableGrid · join_blocks — shared by every format
|
|
17
|
+
├── models.py
|
|
18
|
+
├── exceptions.py
|
|
19
|
+
└── extractors/ # per format `(bytes, ConvertOptions) -> Extraction`. Registered in EXTRACTORS in __init__.py
|
|
20
|
+
├── _shared.py # Strict OOXML detection (`is_strict_package`, DOCX/PPTX), OOXML charts and SmartArt (PPTX/DOCX/HWPX), Office equations (OMML → LaTeX, DOCX/PPTX), automatic number strings, paragraph heads (`paragraph_head`), ruby text and list levels inside a cell (DOCX/HWP/HWPX), chart tables inside a cell (`embedded_table`), Hangul (HWP) bullet indent levels (`BulletNesting`, HWP/HWPX), symbol-font mapping (`symbol_font_text` — including ordinary letters in Symbol), character limit inside tables (`TableBudget`, DOCX/HWP/HWPX), DrawingML `a:br` line breaks (`drawing_paragraph_lines`, PPTX/DOCX/XLSX/charts), comment lines (`comment_line`, DOCX/PPTX/HWPX/Excel)
|
|
21
|
+
├── _equation.py # Hancom equation script → LaTeX (HWP/HWPX). The original script when it cannot be converted
|
|
22
|
+
├── docx.py
|
|
23
|
+
├── html.py
|
|
24
|
+
├── hwp.py
|
|
25
|
+
├── hwpx.py
|
|
26
|
+
├── image.py
|
|
27
|
+
├── pdf.py
|
|
28
|
+
├── pptx.py
|
|
29
|
+
├── spreadsheet.py
|
|
30
|
+
└── text.py
|
|
31
|
+
|
|
32
|
+
tests/ # unit tests; sample builders are in tests/builder.py
|
|
33
|
+
docs/decisions/ # deliberate decisions per format (file list in "Deliberate decisions" below)
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
For use on a server see [server.md](../server.md) (conversion must go through `IsolatedConverter`).
|
|
37
|
+
|
|
38
|
+
- `extractors/*` imports only `markdown`, `models` and `exceptions`. Extractors share with each other only the decoding functions of `text` (including MIME header blocks and unpacking MIME mail) and `extractors/_shared.py` / `_equation.py`. Warning filtering is `models.quiet_warnings` (spreadsheet, image, html). Exceptions: DOCX `w:altChunk` uses `html.extract_html`, `is_mhtml` and `text.extract_text` as they are (altChunk row in [office.md](office.md)). The image extractor uses `image.split_tiff_pages` (multi-page TIFF, image section of [extractors.md](extractors.md)).
|
|
39
|
+
- An extractor registers images **as the original bytes** with `Extraction.add_image()` and puts the placeholder it gets back (`⟦IMG:<key>:n⟧`) at the right place in the body.
|
|
40
|
+
Decoding, size filtering, de-duplication and limits happen in one place only, `converter._prepare_images`.
|
|
41
|
+
- `convert` does not use the network (deterministic, usable in a process pool). Vision-model calls live only in `describe.py`, and once descriptions are filled in
|
|
42
|
+
`converter.render` assembles again from `Section.template`. A failed description is a warning, not an exception.
|
|
43
|
+
- The describer builds `langchain_openai.ChatOpenAI` itself (key from `OPENAI_API_KEY`, default model `describe.DEFAULT_MODEL`) or takes a LangChain chat model from the caller.
|
|
44
|
+
- Extractors produce **sections only**. NFC normalization, whitespace cleanup, section heads (`## Slide N`) and the limit verdict are done by `converter` — not in extractors.
|
|
45
|
+
An extractor reports the character count with `Extraction.count()` for each unit it reads (PDF page, Excel/CSV row, PPTX slide, DOCX/HWP/HWPX top-level paragraph or table — large tables per cell through `TableBudget`)
|
|
46
|
+
and stops reading when it gets the exceeded signal (True). TXT and MD are read in one go; for HTML only large documents get their front part read first.
|
|
47
|
+
- Tables must be built with `render_table`/`TableGrid`, so that the merged-cell rules (Markdown repeats vertically and keeps the first cell horizontally; a table that still has horizontal spans becomes HTML)
|
|
48
|
+
do not differ per format. A format that has span information passes the span counts to `TableGrid` (it does not repeat values itself). Exception: Excel spreads merges over the cells and is always Markdown ([spreadsheet-tables.md](spreadsheet-tables.md)).
|
|
49
|
+
- User-facing text (exception messages, `warnings`) is English, fixed wording, and never contains file content or parser error text
|
|
50
|
+
(the cause is chained through `__cause__`). `warnings` carry no more than the sheet name or page number needed to say where.
|
|
51
|
+
|
|
52
|
+
## Adding a new format
|
|
53
|
+
|
|
54
|
+
1. Add a value to `models.DocumentFormat` → 2. content detection in `detect.py` → 3. write `extractors/<format>.py` and register it in `EXTRACTORS` →
|
|
55
|
+
4. a sample builder in `tests/builder.py` and `tests/test_<format>.py` → 5. update the README table.
|
|
56
|
+
|
|
57
|
+
## Deliberate decisions (do not revert)
|
|
58
|
+
|
|
59
|
+
Only the common decisions (detection, limits, pictures, assembly, description) are here. The rest:
|
|
60
|
+
|
|
61
|
+
- [extractors.md](extractors.md) — per-format decisions (PDF, PPTX, DOCX, HWP/HWPX, spreadsheets, HTML, text, mail, images)
|
|
62
|
+
- DOCX / PPTX
|
|
63
|
+
- [office.md](office.md) — DOCX and PPTX
|
|
64
|
+
- [office-lists.md](office-lists.md) — numbering and lists
|
|
65
|
+
- [office-math.md](office-math.md) — equations
|
|
66
|
+
- [office-charts.md](office-charts.md) — chart value tables, axes, units
|
|
67
|
+
- [office-limits.md](office-limits.md) — processing time
|
|
68
|
+
- PDF
|
|
69
|
+
- [pdf.md](pdf.md) — PDF
|
|
70
|
+
- [pdf-flow.md](pdf-flow.md) — reading order, two columns, labels beside a table, superscripts and subscripts
|
|
71
|
+
- [pdf-rows.md](pdf-rows.md) — joining fragments of the same row, pulling up pushed-down lines
|
|
72
|
+
- [pdf-tables.md](pdf-tables.md) — splitting table cells, header rows of continued tables, tables inside tables, checkboxes
|
|
73
|
+
- [pdf-borderless.md](pdf-borderless.md) — wide-cell rows of borderless tables, the empty top-left cell, narrow gaps between cells
|
|
74
|
+
- [pdf-limits.md](pdf-limits.md) — resource limits, page boxes, attachments
|
|
75
|
+
- [pdf-images.md](pdf-images.md) — pictures drawn in strips, pictures inside table cells, photo grids
|
|
76
|
+
- [pdf-fonts.md](pdf-fonts.md) — fonts without widths, symbol-font checkboxes, CMaps of built-in encodings, lower bound on fragment character width
|
|
77
|
+
- [pdf-cells.md](pdf-cells.md) — joining lines of text in ruled-table cells, filtering highlighter marks, tables made of cell shading
|
|
78
|
+
- HWP / HWPX
|
|
79
|
+
- [hwp.md](hwp.md)
|
|
80
|
+
- Excel / CSV
|
|
81
|
+
- [spreadsheet.md](spreadsheet.md) — spreadsheets
|
|
82
|
+
- [csv.md](csv.md) — CSV
|
|
83
|
+
- [spreadsheet-tables.md](spreadsheet-tables.md) — splitting tables, side-by-side tables, summary cells, filling merges, the first column of the column limit
|
|
84
|
+
- [spreadsheet-cells.md](spreadsheet-cells.md) — error cell values, pictures in cells, form controls, sheet SmartArt, default-password XLS, cell comments, the cell of a floating picture, time formats, parts the converter reads itself
|
|
85
|
+
- Text family
|
|
86
|
+
- [encoding.md](encoding.md) — encoding detection
|
|
87
|
+
- [mail.md](mail.md) — mail
|
|
88
|
+
- [html.md](html.md) — HTML blocks (aside, quotes, checklists, item/value tables), exceptions for hidden elements, lazy-loading placeholder pictures, MHTML frames, inline SVG labels, `data:` URI pictures in MD
|
|
89
|
+
- [images.md](images.md) — images, picture descriptions (the describer), placeholders
|
|
90
|
+
- [detect.md](detect.md) — format detection (magic bytes, ZIP, OLE, telling the text family apart, MHTML, mail, extension warnings)
|
|
91
|
+
|
|
92
|
+
| Decision | Why | Pinned by |
|
|
93
|
+
| --- | --- | --- |
|
|
94
|
+
| Markdown tables repeat a vertically merged value on every row | Each row carries by itself which category its values belong to (no information loss) | `test_markdown.py::TestTableGrid` |
|
|
95
|
+
| Only a table that still has horizontally spanning cells after empty rows and columns are removed becomes an HTML table (rowspan/colspan, th in the first row, one row per line). A full-width merge of the title row at the top alone does not switch it. Cell text and the picture descriptions of that cell stay as written and only table tags are neutralized with `<` (`markdown.neutralize_table_tags`) — the result is not rendered on screen but fed to an LLM, so escaping everything is noise. Applies to the formats that pass span counts to `TableGrid` (HWP, HWPX, DOCX, PPTX, HTML with a single header row) | A Markdown table cannot draw a horizontal merge, so nobody could tell what the empty cell in '\| 합계 \| \| 4개 과제 \|' (Total \| \| 4 tasks) was. A table with only vertical merges loses nothing through repetition, so it is not switched (existing output kept). Keeps a `</td>` in cell text from being confused with a cell boundary. In 295 real documents all 312 HTML tables had matching cell counts; 142 limit verdicts × 3, 0 violations | `test_markdown.py::TestMergedTableAsHtml`, `test_converter.py::TestAssembly::test_picture_in_an_html_table_cell_keeps_its_text` |
|
|
96
|
+
| A table inside a table (HWP, HWPX, DOCX; for HTML see [extractors.md](extractors.md)) is not flattened: it is handed to the outer `TableGrid.embed`, which leaves a placeholder (private-use characters) in the cell text. The outer table becomes HTML (a one-line HTML table inside the cell); a one-column box becomes paragraphs + a standalone table. While rendering text that goes somewhere other than cell text, such as a header inside a cell, the position is marked as one that cannot take it (None) and the table is `flatten`ed ('cell / cell'). A table inside a footnote or endnote is `_shared.note_table` ('cell / cell; cell / cell'). Copies made for merged cells drop picture and table placeholders (`merged_copy`). The caption of an inner table stays with the table wherever it is | 'cell / cell' lost the row and column structure. A placeholder that leaks into a footnote is never resolved and is erased, so the content disappears. A table in a merged cell came out once per row. The inner table's caption was dropped, so a picture in the caption cost a description and then vanished | `test_markdown.py::TestMergedTableAsHtml` (4 nested cases), `test_hwp.py::TestNestedTables` (including footnotes), `test_quality.py::test_docx_nested_table_keeps_its_rows_and_columns`, `test_hwp.py::TestTableInsideFootnote`, `test_office_symbols_fields_notes.py::TestDocxTableInsideFootnote` |
|
|
97
|
+
| PyMuPDF and pyhwp are not used | AGPL | — |
|
|
98
|
+
| clean_text removes first, then NFC | In the other order the characters on both sides of a removed one combined anew and NFC broke (real PDF) | `test_markdown.py::test_result_is_nfc_even_when_removed_character_separated_combining_mark` |
|
|
99
|
+
| Private-use bullets of Symbol/Wingdings (U+F0B7 etc.) are not removed but turned into Unicode symbols | The list markers of a Word PDF disappeared | `test_quality.py::test_symbol_font_bullets_become_unicode_bullets` |
|
|
100
|
+
| Only Latin ligatures (U+FB00–FB06) are expanded (`fi`→`fi`). NFKC is not used | 'first' in a typeset PDF became a different word from 'first' (964 places in one book PDF). NFKC would also change ①, ㈜ and ² in Korean documents | `test_markdown.py::TestCleanText::test_expands_latin_ligatures_but_keeps_other_compatibility_characters` |
|
|
101
|
+
| `TableGrid` limits span counts and builds the grid only from rows and columns where cells are placed. Spans are clipped at render time to the table's real extent (the last row/column where a cell was placed or that was announced with `reach`), and the number of rows dropped by the grid cell limit is reported through `omitted_rows()` so the extractor warns (`_shared.omitted_rows_warning`, HTML included). A cell that arrives without an address (`place_next`, HTML) looks for a free position starting at the column placed last in that row | Trusting span counts and cell addresses lets a few KB of input use seconds and gigabytes. It is blocked in one common place, not per format. rowSpan=5000 in a 2-row table made 4,999 fake rows, and gridSpan=10000 in the top cell left only 499 of 1,000 rows (no warning). Searching from column 0 took 56 seconds for one row of 50,000 cells | `test_markdown.py::TestTableGridLimits`, `test_office_robustness.py::TestSpansPastTheTable`, `test_hwp_robustness.py::TestSpansPastTheTable`, `test_core_html.py::TestRowsLeftOutByTheGridLimit`, `test_markdown.py::TestTableGridLimits::test_cells_placed_in_order_on_a_long_row_take_linear_time` |
|
|
102
|
+
| The character limit (`max_chars`) is a criterion for stopping, not for cutting. Once exceeded, the result comes back with `exceeded=True` and the remaining pages, rows and pictures are not processed | Spending page analysis, picture decoding and vision-model calls on a document that will be rejected anyway is waste (130,000-row Excel 7.6 s → 0.4 s; 40 scanned pages 40 calls → 7). The former `max_output_chars` (cutting) and `truncated` were replaced by this | `test_converter.py::test_exceeding_the_character_limit_sets_exceeded_and_cuts_the_text`, `test_pdf.py::test_stops_reading_pages_once_the_character_limit_is_exceeded`, `test_image.py::test_description_stops_once_the_character_limit_is_exceeded` |
|
|
103
|
+
| `exceeded` must be exactly "character count of the full conversion > limit". Mid-way verdicts use only values that are never larger than the real one: during extraction `markdown.solid_length` (excluding whitespace, placeholders and characters that will be removed; NFC), during description the body assembled with every remaining picture erased including its alt text (`describe._floor_length`; when there is clearly room, `converter.image_length_bound` skips the assembly). `render` does not inherit `result.exceeded` and decides again every time | Counting high blocks documents at or under the limit as exceeded. Counting by raw length made 4 documents with N = L come out exceeded, and judging by the body length at an intermediate point made 48 documents come out exceeded whose pictures were later erased as decorative and so shrank | `test_converter.py::TestLimitIsExact`, `test_image.py::test_placeholders_of_images_that_turn_out_decorative_do_not_count_against_the_limit`, `test_retry_does_not_count_already_described_images_twice`, `TestDecorativePictureWithAltText::test_limit_is_exact_when_a_picture_with_alt_text_gets_any_answer` |
|
|
104
|
+
| `char_count` includes picture descriptions and `render` counts again every time. The total of a batch (several files) is added up by the caller — this library works per single file | It is the criterion the front end uses to limit uploads | `test_image.py::test_char_count_includes_image_descriptions_once_described` |
|
|
105
|
+
| When the limit is exceeded, `sections[].text` is cut at the same point as `text` (`_cut_sections`; sections after the cut have empty text). The section list and `template` stay as they are | Only `text` was cut and the sections kept everything, so chunking and source attribution received content beyond the limit. Erasing the originals would shrink the character count on re-assembly and flip `exceeded` | `test_converter.py::TestAuditedAssemblyGaps::test_sections_are_cut_where_the_text_is_cut` and 2 more |
|
|
106
|
+
| In documents that are not Markdown (anything but TXT/MD) an unpaired last ``` line is neutralized as `\```` (`clean_text`). Paired fences stay code blocks | A ``` in a single paragraph turned every section after it into a code block and whitespace cleanup stopped | `test_markdown.py::TestUnclosedCodeFence`, `test_converter.py::TestAuditedAssemblyGaps::test_stray_code_fence_in_a_docx_does_not_swallow_the_rest` |
|
|
107
|
+
| A line holding a picture placeholder is a table row (folded to one line) only when it starts and ends with `\|` (including MD `\|a\|b\|`) or has the shape `<tr>…</tr>`. Known accepted gap: a paragraph of the form `\|…\|` is taken for a table row | A picture description was folded to one line and glued onto a paragraph that starts with `\|` or `<tr>`. Only lines ending in ` \|` were looked at, so a `\|a\|b\|` table broke at the picture | `test_converter.py::TestAuditedAssemblyGaps::test_paragraph_that_starts_like_a_table_row_gets_a_picture_block`, `test_core_text_and_mail.py::TestPictureInACompactMarkdownTable` |
|
|
108
|
+
| Superscripts and subscripts become `10^6`, `10^{-3}`, `H_2O` in one place, `_shared.format_script`. Only scripts of at most 10 characters that start with a letter, digit or sign are marked; footnote marks, symbols, long text and ordinal suffixes after a number (21st) stay as they are. Scripts containing Hangul, Han characters or kana are not marked (`1,000원`, same rule as PDF). Sources: HTML sup/sub, DOCX w:vertAlign and LibreOffice w:position + small w:sz (at most 0.8× the paragraph size), PPTX baseline, HWPX hh:supscript/subscript, HWP character-shape attribute bits 15 and 16, XLSX rich text (full load only), XLS font escapement, `<Sup>`/`<Sub>` of Excel 2003 XML (`spreadsheet._xml_rich_text`) | Glued on as plain text, 10⁶ read as 106. Marking `[1]`, `†` and `: 125` as well produces hundreds of noise marks per wiki document. The HWP script bits (taken from hwplib) were confirmed on real files against the HWPX twin of the same document (hh:supscript) | `test_text_html.py::test_html_superscript_and_subscript_are_marked`, `test_html_superscript_with_hangul_or_hanja_is_left_as_plain_text`, `test_html_ordinal_suffix_after_a_number_is_not_marked_as_superscript`, `test_office.py::TestSuperscriptAndTextBoxes` (including 2 LibreOffice position cases), `test_hwp.py::TestSuperscript`, `test_spreadsheet.py::TestSuperscriptInCells`, `test_core_spreadsheet.py::TestXmlSpreadsheetScripts` |
|
|
109
|
+
| Private-use characters are removed in the supplementary planes too (U+F0000–10FFFD) (`markdown._PRIVATE_USE`). Mapping Hancom font ordinals (U+F02B1–) → ① is not done for lack of evidence | Characters of Hancom-only fonts stayed as meaningless tokens | `test_markdown.py::TestCleanText::test_removes_supplementary_private_use_characters` |
|
|
110
|
+
| PDF superscripts and subscripts are inferred from position (`pdf._scripted_text`): characters at most 0.8× the line's ordinary character size whose baseline is off by at least 0.2 upward or 0.1 downward (relative to the character size). Only marks are inserted into pdfminer's line text; lines whose character order differs from the line text, table cells and pages extracted the simple way are left alone. Runs containing Hangul or Han characters are not marked | A PDF has no script information. Rebuilding the line text changed the order and spacing of overlapping accents. A raised unit in a designed document became '12,345^{백만원}'. Of 2,377 marked lines in 20 public papers, a sample of 45 lines had 0 misjudgments | `test_pdf.py::TestSuperscriptByPosition` |
|
|
111
|
+
| `convert` takes bytes, bytearray and memoryview; anything else is a `TypeError` (pointing to convert_file). A ConvertOptions in the file-name position, a file name that is not a str and an options value that is not ConvertOptions are also a `TypeError` before conversion (convert, aconvert, convert_file; `converter._check_arguments`) | Path strings and memoryviews were rejected as a 'corrupted file'. `convert(data, ConvertOptions(...))` raised TypeError only after running the whole conversion (222 s) | `test_converter.py::TestInputTypes`, `test_core_mbox_markdown_and_api.py::TestArgumentsInTheWrongPlace` |
|
|
112
|
+
| `ConvertOptions.__post_init__` rejects values below the limit minimums (`models._OPTION_MINIMUMS`) with a `ValueError` (English). 0 for `max_chars`, `max_images`, `min_image_side` and `pdf_time_budget` means 'allow none'; `max_pages`, `max_sheet_cols`, `max_file_bytes` and the like must be at least 1. The type is checked too (`models._checked_number`, `TypeError`): None only for `max_chars`, `max_pages`, `max_images` and `pdf_time_budget`; a whole-number float for an integer limit (`5e5`) is accepted and converted to int; bool, strings and fractions are rejected. NaN and infinity are a `ValueError` (no time limit is `pdf_time_budget=None`) | `max_chars=-5` cut the last five characters and became 'exceeded', and `max_images=-1` dropped only the last picture. `max_sheet_cols=None` rejected an Excel file as 'corrupted' and `max_chars=2.5` was a TypeError in the middle of conversion. `pdf_render_scale=nan` passed the minimum comparison and a healthy PDF became 'corrupted' | `test_converter.py::TestLimits::test_negative_or_meaningless_zero_limits_are_rejected_when_options_are_made`, `test_limit_of_the_wrong_type_is_rejected_when_options_are_made`, `test_whole_number_float_limit_becomes_an_integer`, `test_non_finite_number_is_rejected_when_options_are_made` |
|
|
113
|
+
| The single-cell top row of a two-column table is a table title (a paragraph above the table; `markdown._title_of`, common to `render_table` and `TableGrid` HTML) only when just the left cell is filled, the row below has non-numeric text in both cells and there is a numeric cell within the 5 rows below that. HTML, which merges multi-row headers itself, uses the same test through `markdown.table_title`. A ruled table in a PDF that continues from the previous page uses `render_table(merged_titles_only=True)` — a title only when the remaining cells are merged cells (None). Known accepted gap: the title row of a two-column table without numeric cells stays a header row | Titles were taken out only for 3 or more columns, so '부서별 인원 현황 \| ' (Headcount by department) became the header row and '부서 \| 인원' (Department \| Headcount) a data row (XLSX merged title, CSV, HTML colspan=2). A '구분 \| ' header row and two-column tables with text only stay as they are | `test_markdown.py::TestTwoColumnTitleRow`, `TestTitleRowOnlyFromMergedCells`, `test_text_html.py::TestTitleRowInsideMultiRowHeader::test_two_column_table_with_a_spanning_title_row` |
|
|
114
|
+
| `clean_text` does not collapse consecutive blank lines inside paired fences (```) (TXT/MD and every other format alike). After an unpaired last fence it does. Assembly of a section with pictures (`converter._render_template` → `_collapse_blank_lines`) skips the inside of fences by the same pairing | Two blank lines between functions in an MD code block became one. Assembly collapsed the blank lines of the whole section again, so the same happened only in sections with pictures | `test_markdown.py::TestBlankLinesInsideCodeFences`, `test_core_image_and_assembly.py::TestBlankLinesInCodeNextToAPicture` |
|
|
115
|
+
| Warning filtering inside the library uses `models.quiet_warnings` instead of `warnings.catch_warnings` (narrow category and module; inserted under a lock with a use count and only that entry removed at the end). Known accepted gap: while the block is open, warnings of the same category and module from other threads are ignored too, and overlap with a `catch_warnings` outside the library cannot be prevented (Python 3.12) | `catch_warnings` is not thread-safe, so after overlapping XLSX conversions the ignore filter for openpyxl's UserWarning stayed in the process for good. Serializing whole conversions with a lock would make concurrent conversions sequential | `test_core_spreadsheet.py::TestOpenpyxlWarnings::test_overlapping_conversions_on_threads_leave_the_filters_as_they_were` |
|
|
116
|
+
| On a server, convert with `IsolatedConverter`: each worker gets its own `ProcessPoolExecutor(max_workers=1, max_tasks_per_child=1)`, started with forkserver (this package preloaded; spawn when unavailable). On a timeout, `BrokenProcessPool` or a cancelled request the worker is killed directly through the private `_processes` and the pool is rebuilt. On shutdown, `shutdown(wait=True)` is awaited in the default thread pool, and until it returns the pool's workers keep being killed (`isolated._shut_down`): the manager thread starts a replacement whenever a worker exits, so one can appear after the first kill. Memory or time overruns and a dead worker are `DocumentTooLargeError` (`TOO_COMPLEX_MESSAGE`) | Run on a server thread, a memory bomb or a crashing C extension takes the whole server down, and a thread cannot be stopped. With fork, the child can hang in a server that runs DB and asyncio threads. Without preloading a new worker takes 0.2 s, with it 5 ms, so a new worker per file gives the memory back. With several files in one pool, killing one fails the others too. `shutdown(wait=False)` emptied `_processes` while the management thread was refilling workers and that thread died with a TypeError (3 times per 15 tests). Known accepted gap: the forkserver preload list is one per process, so it overwrites another code's list A worker killed between writing its result and releasing the result queue's write lock left the lock held; a replacement worker then blocked on it while announcing its exit, and `aclose()` never returned (a hung run about once in four when the whole suite ran in parallel in a Linux container). | `test_isolated.py` (the memory limit only on Linux — macOS does not accept `RLIMIT_AS`, so it is skipped), `test_isolated.py::TestIsolatedConverter::test_stopping_does_not_wait_for_a_worker_blocked_on_the_result_queue` |
|
|
117
|
+
|
|
118
|
+
## Commands
|
|
119
|
+
|
|
120
|
+
Run from the repository root.
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
uv sync # install dependencies
|
|
124
|
+
uv run pytest # unit tests
|
|
125
|
+
uv run pytest -n auto -m "not timing" # everything but the timing tests, in parallel
|
|
126
|
+
uv run pytest -m timing # tests that measure time, one at a time
|
|
127
|
+
uv run ruff check
|
|
128
|
+
uv run opendockit <file> [--json] [--describe] # --describe calls the vision model (needs OPENAI_API_KEY, costs money)
|
|
129
|
+
# To check the shape of a conversion, convert a real document with the CLI above (do not commit personal or copyrighted documents)
|
|
130
|
+
```
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# CSV decisions
|
|
2
|
+
|
|
3
|
+
Deliberate decisions of the CSV extractor (`spreadsheet.extract_csv`) about delimiters, quotes and line verdicts. Split off from [spreadsheet.md](spreadsheet.md);
|
|
4
|
+
the rule is the same: do not revert the decisions in this table. To change one, change its pinning test first and write the reason in the same row. Value formatting is,
|
|
5
|
+
together with Excel, in [spreadsheet.md](spreadsheet.md); splitting tables within a sheet is in [spreadsheet-tables.md](spreadsheet-tables.md).
|
|
6
|
+
|
|
7
|
+
## CSV
|
|
8
|
+
|
|
9
|
+
| Decision | Why | Pinned by |
|
|
10
|
+
| --- | --- | --- |
|
|
11
|
+
| A row on which the csv module fails is read again with a quote-aware split (`_quoted_record`; only dialects with the doubled-quote rule and no escape character); when the quote closes, only that row is used and the csv module resumes from the next line (no warning). Only when the quote never closes before the end of the file is the rest, from that line on, split by delimiter alone (warning). `csv.field_size_limit` is not raised | An unclosed quote swallowed everything after it into one cell, and once that passed the limit (131,072 characters) the whole file was rejected. The limit is a process-global setting. The delimiter-only split used instead also cut healthy cells over the limit (long body text) at every comma, so the column limit dropped the rest and the author cell (348 KB → 2,410 characters, false warning) | `test_office.py::TestCsvWithBrokenQuotes` |
|
|
12
|
+
| CSV uses all of the Sniffer's delimiter, quotechar and skipinitialspace (the single-quote and comma verdicts are changed only under the conditions of the rows below) | `, "서울시, 강남구"` was split into two cells | `test_spreadsheet.py::TestCsvDialect` |
|
|
13
|
+
| A single quote in CSV is accepted as the quote character only when every cell containing it is wrapped whole (`_wraps_whole_fields`) | The Sniffer picked `'` because of a single `'목표'`, and the cell `'24년 상반기` swallowed the delimiter. `'홍길동','1,000'` is still read | `test_spreadsheet.py::TestCsvDialect::test_apostrophe_inside_a_field_is_not_a_quote_character`, `test_single_quote_as_quote_character` |
|
|
14
|
+
| Even when the Sniffer picked the comma (or failed), if the number of cells split by tab, semicolon or pipe is the same on every line of the sample (2 or more cells; in the order tab → semicolon → pipe), that delimiter is used (`_steadier_delimiter`). For a single-line file, tab only. When double quotes wrap cells whole under the comma but not under the candidate, the comma stays. A file whose cell count differs per line is changed only for TSV that omits trailing empty cells (row below) and the following case, otherwise the Sniffer's choice stands: when there are two or more lines with the maximum candidate cell count (3 or more cells), they are the majority, and from the first of them (the header) on the comma cell count is uneven, that candidate is used (`_delimiter_of_most_lines`; leading title/meta lines and short lines are ignored). When the comma cell count is even from the header on, the `;`/`\|` are enumerations inside values, so comma. Known accepted gap: the `.tsv` extension is not passed to the extractor and so is not used, and an unquoted comma CSV whose every line contains the same number of semicolons or pipes is split by those | With the same number of commas per line the Sniffer picked the comma, so `서울\t1,234\t5,678` became `서울 1 \| 234 5 \| 678`, and decimal commas (`1,5`) and word enumerations were split into two cells. Title/meta lines of bank and ERP exports (`Umsatzanzeige;…`, `고객 목록`) or a single line omitting trailing empty cells made the cell count uneven, and `-1.234,56` was split into `-1.234 \| 56` (the earlier decision 'when the cell count differs per line, keep the Sniffer's choice' was changed with the owner's approval) | `test_spreadsheet.py::TestCsvDialect::test_delimiter_with_a_steady_column_count_wins_over_commas_inside_values`, `test_tab_delimiter_is_found_in_a_file_longer_than_the_sample`, `test_comma_file_with_other_delimiters_inside_values_stays_comma_delimited`, `TestCsvDelimiterOfMostLines` |
|
|
15
|
+
| TSV whose tab count differs per line because trailing empty cells are omitted: when the Sniffer picked the comma, the comma cell count is uneven, the first line has a tab and every line containing a comma has a tab (2 or more lines with tabs; only when double quotes wrap cells whole by tab), it is tab (`_tab_on_every_comma_line`) | The amount `1,100,000` in Excel/ERP TSV exports was split at the thousands separators and the columns shifted. A comma CSV has lines with commas but no tab, or no tab in the header | `test_spreadsheet.py::TestCsvDialect::test_tab_file_whose_rows_omit_trailing_empty_cells_is_tab_delimited`, `test_comma_file_with_some_tabs_stays_comma_delimited` |
|
|
16
|
+
| A CSV line with more delimiters than the column limit is cut before parsing to the column limit + one 'overflow marker' cell. Lines containing the quote character and lines inside an open quoted cell (decided by the parity of the quote count) are not cut (Known accepted gap: such lines stay large). When the first line is only `sep=X`, that delimiter is used and the line removed (the Sniffer is restricted to that delimiter) | A single line with 22.5 million commas (45 MB) built every cell object: 5.2 s, 1.55 GB (→ 0.4 s). Excel's `sep=;` marker line became the header and was not used for the delimiter verdict | `test_spreadsheet.py::TestCsvLimitsAndHints` |
|
|
17
|
+
| When a whole CSV cell is `="…"` (the Excel idiom for keeping leading zeros), it is unwrapped to the text inside (`""` → `"`). Other formulas stay as they are | `="00123"` came out as is | `test_spreadsheet.py::TestCsvExcelTextFormula` |
|
|
18
|
+
| Only when every backslash in the sample (first 8,192 characters) is in front of a character that MySQL `INTO OUTFILE` escapes (`"`, `\`, `N`, `0`, `n`, `t`, `r`, `b`, `Z`, the delimiter, a line break), there is a place where `\"` is followed by a character that is not the end of a cell, and the sample read with `\"`→`""` and `\\`→`\` has the same cell count on every line with double quotes wrapping cells whole, is the file read that way (`_escapes_quotes_with_backslashes`). `\N` (NULL) and the like stay literal. Known accepted gap: when `\"` occurs only after the sample it is not recognized, and a file mixing both rules (`"Is it \"ok\"?"` and a path ending `"c:\"`) is read with the doubled-quote rule as before | Read only with the doubled-quote rule, `"27\" 모니터"` became `27\ 모니터"`. Reading an ordinary CSV with other backslashes (`C:\temp`, `\d+`) as escapes loses characters | `test_core_spreadsheet.py::TestCsvBackslashEscapes` |
|
|
19
|
+
| Even with `.csv`/`.tsv`, when the very start after BOM and whitespace is an HTML document tag (`<!doctype html`, `<html`, `<table`, `<head`, `<body`, `<style`, `<meta`) and the first 64 KB contain a table tag (`<table`, `<tr`, `<td`) together with its closing tag, the file is read as HTML with an extension warning (`detect._is_html_table_export`). A real CSV whose first cell starts with a tag (`<b>굵게</b>,…`, `<table>,<tr>` without closing table tags) is CSV. `.txt` stays text (among text formats the extension decides) | When an 'Excel download' serves an HTML table as `.csv`, the tags became cell text and `1,000` was split into two cells. Of 2,035 real CSV/TSV files from systems, 0 changed to HTML (owner's approval, conservative) | `test_core_text_and_mail.py::TestHtmlTableServedAsCsv` |
|