inkgrid 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.
- inkgrid-0.1.0/LICENSE +21 -0
- inkgrid-0.1.0/PKG-INFO +404 -0
- inkgrid-0.1.0/README.md +374 -0
- inkgrid-0.1.0/pyproject.toml +247 -0
- inkgrid-0.1.0/pyproject.toml.orig +155 -0
- inkgrid-0.1.0/src/inkgrid/__init__.py +100 -0
- inkgrid-0.1.0/src/inkgrid/__main__.py +5 -0
- inkgrid-0.1.0/src/inkgrid/api.py +167 -0
- inkgrid-0.1.0/src/inkgrid/cli.py +225 -0
- inkgrid-0.1.0/src/inkgrid/core/__init__.py +1 -0
- inkgrid-0.1.0/src/inkgrid/core/assemble.py +320 -0
- inkgrid-0.1.0/src/inkgrid/core/calls.py +184 -0
- inkgrid-0.1.0/src/inkgrid/core/furniture.py +292 -0
- inkgrid-0.1.0/src/inkgrid/core/glossary.py +305 -0
- inkgrid-0.1.0/src/inkgrid/core/joins.py +191 -0
- inkgrid-0.1.0/src/inkgrid/core/layout.py +211 -0
- inkgrid-0.1.0/src/inkgrid/core/lexicon.py +301 -0
- inkgrid-0.1.0/src/inkgrid/core/lines.py +159 -0
- inkgrid-0.1.0/src/inkgrid/core/notes.py +132 -0
- inkgrid-0.1.0/src/inkgrid/core/order.py +52 -0
- inkgrid-0.1.0/src/inkgrid/core/pipeline.py +135 -0
- inkgrid-0.1.0/src/inkgrid/core/prose.py +180 -0
- inkgrid-0.1.0/src/inkgrid/core/tables/__init__.py +1 -0
- inkgrid-0.1.0/src/inkgrid/core/tables/chart.py +251 -0
- inkgrid-0.1.0/src/inkgrid/core/tables/corridor.py +942 -0
- inkgrid-0.1.0/src/inkgrid/core/tables/grid.py +84 -0
- inkgrid-0.1.0/src/inkgrid/core/tables/lattice.py +294 -0
- inkgrid-0.1.0/src/inkgrid/core/tables/pages.py +19 -0
- inkgrid-0.1.0/src/inkgrid/core/tables/proto.py +122 -0
- inkgrid-0.1.0/src/inkgrid/core/tables/shape.py +65 -0
- inkgrid-0.1.0/src/inkgrid/core/text.py +34 -0
- inkgrid-0.1.0/src/inkgrid/core/view.py +49 -0
- inkgrid-0.1.0/src/inkgrid/errors.py +33 -0
- inkgrid-0.1.0/src/inkgrid/model/__init__.py +64 -0
- inkgrid-0.1.0/src/inkgrid/model/base.py +27 -0
- inkgrid-0.1.0/src/inkgrid/model/canonical.py +34 -0
- inkgrid-0.1.0/src/inkgrid/model/config.py +112 -0
- inkgrid-0.1.0/src/inkgrid/model/document.py +458 -0
- inkgrid-0.1.0/src/inkgrid/model/export.py +165 -0
- inkgrid-0.1.0/src/inkgrid/model/findings.py +106 -0
- inkgrid-0.1.0/src/inkgrid/model/geometry.py +211 -0
- inkgrid-0.1.0/src/inkgrid/model/invariants.py +297 -0
- inkgrid-0.1.0/src/inkgrid/model/lattice.py +73 -0
- inkgrid-0.1.0/src/inkgrid/model/page.py +194 -0
- inkgrid-0.1.0/src/inkgrid/model/verification.py +211 -0
- inkgrid-0.1.0/src/inkgrid/py.typed +0 -0
- inkgrid-0.1.0/src/inkgrid/read/__init__.py +1 -0
- inkgrid-0.1.0/src/inkgrid/read/camelot_reader.py +239 -0
- inkgrid-0.1.0/src/inkgrid/read/glyphs.py +296 -0
- inkgrid-0.1.0/src/inkgrid/read/page_findings.py +100 -0
- inkgrid-0.1.0/src/inkgrid/read/pymupdf_reader.py +508 -0
- inkgrid-0.1.0/src/inkgrid/read/raw.py +84 -0
- inkgrid-0.1.0/src/inkgrid/read/rules.py +135 -0
- inkgrid-0.1.0/src/inkgrid/read/source.py +67 -0
- inkgrid-0.1.0/src/inkgrid/read/words.py +290 -0
- inkgrid-0.1.0/src/inkgrid/render/__init__.py +1 -0
- inkgrid-0.1.0/src/inkgrid/render/inspector.py +247 -0
- inkgrid-0.1.0/src/inkgrid/verify/__init__.py +5 -0
- inkgrid-0.1.0/src/inkgrid/verify/checks.py +416 -0
- inkgrid-0.1.0/src/inkgrid/verify/ink.py +164 -0
- inkgrid-0.1.0/src/inkgrid/verify/ownership.py +410 -0
- inkgrid-0.1.0/src/inkgrid/verify/pdfium_reader.py +256 -0
- inkgrid-0.1.0/src/inkgrid/verify/report.py +197 -0
- inkgrid-0.1.0/src/inkgrid/verify/rules.py +108 -0
inkgrid-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Abdul-Muizz Anwar
|
|
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.
|
inkgrid-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: inkgrid
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Exact tables and text from born-digital PDFs, read from the text layer and drawn rules, and verified by a second PDF engine.
|
|
5
|
+
Keywords: pdf,tables,text-extraction,pymupdf,camelot,document-parsing
|
|
6
|
+
Author: Abdul-Muizz Anwar
|
|
7
|
+
Author-email: Abdul-Muizz Anwar <abdulmuizz1310@outlook.com>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
17
|
+
Classifier: Topic :: Text Processing
|
|
18
|
+
Classifier: Typing :: Typed
|
|
19
|
+
Requires-Dist: camelot-py>=2.0.0,<3
|
|
20
|
+
Requires-Dist: opencv-python-headless>=4.14.0.94,!=5.0.0.93
|
|
21
|
+
Requires-Dist: pydantic>=2.11,<3
|
|
22
|
+
Requires-Dist: pymupdf>=1.28.2,<2
|
|
23
|
+
Requires-Dist: pypdfium2>=5.13.0,<6
|
|
24
|
+
Requires-Python: >=3.12
|
|
25
|
+
Project-URL: Homepage, https://github.com/Abdul-Muizz1310/inkgrid
|
|
26
|
+
Project-URL: Source, https://github.com/Abdul-Muizz1310/inkgrid
|
|
27
|
+
Project-URL: Issues, https://github.com/Abdul-Muizz1310/inkgrid/issues
|
|
28
|
+
Project-URL: Changelog, https://github.com/Abdul-Muizz1310/inkgrid/blob/main/CHANGELOG.md
|
|
29
|
+
Description-Content-Type: text/markdown
|
|
30
|
+
|
|
31
|
+
<h1 align="center">inkgrid</h1>
|
|
32
|
+
|
|
33
|
+
<p align="center">Exact tables and text from born-digital PDFs, read from the text layer and the drawn rules, and checked by a second PDF engine.</p>
|
|
34
|
+
|
|
35
|
+
<p align="center">
|
|
36
|
+
<a href="https://github.com/Abdul-Muizz1310/inkgrid/blob/v0.1.0/WHY.md">Why</a> •
|
|
37
|
+
<a href="https://github.com/Abdul-Muizz1310/inkgrid/blob/v0.1.0/docs/ARCHITECTURE.md">Architecture</a> •
|
|
38
|
+
<a href="https://github.com/Abdul-Muizz1310/inkgrid/blob/v0.1.0/docs/DEMO.md">Demo Script</a> •
|
|
39
|
+
<a href="https://github.com/Abdul-Muizz1310/inkgrid/blob/v0.1.0/docs/specs/00-design.md">Design</a>
|
|
40
|
+
</p>
|
|
41
|
+
|
|
42
|
+
<p align="center">
|
|
43
|
+
<a href="https://github.com/Abdul-Muizz1310/inkgrid/actions/workflows/ci.yml"><img alt="CI" src="https://img.shields.io/github/actions/workflow/status/Abdul-Muizz1310/inkgrid/ci.yml?branch=main&label=ci"></a>
|
|
44
|
+
<a href="https://github.com/Abdul-Muizz1310/inkgrid/blob/v0.1.0/LICENSE"><img alt="License: MIT" src="https://img.shields.io/badge/license-MIT-blue"></a>
|
|
45
|
+
</p>
|
|
46
|
+
|
|
47
|
+
> **Status: alpha, 0.1.0 (milestones M0 to M6 complete).**
|
|
48
|
+
> Today inkgrid reads a PDF into a validated `Document`: every word in exactly one block, in reading
|
|
49
|
+
> order, with running headers and footers set apart, tables, ruled or not, as explicit cell grids,
|
|
50
|
+
> glossaries as definitions, and footnote calls linked to their notes. Tables and sentences that run
|
|
51
|
+
> onto the next page are joined. `inkgrid.verify` then grades the document against its PDF with a
|
|
52
|
+
> second engine, PDFium. The roadmap is in
|
|
53
|
+
> [`docs/specs/00-design.md`](https://github.com/Abdul-Muizz1310/inkgrid/blob/v0.1.0/docs/specs/00-design.md) § 14.
|
|
54
|
+
|
|
55
|
+
## What it does
|
|
56
|
+
|
|
57
|
+
inkgrid reads born-digital PDFs, the kind produced by a word processor or a report generator, through
|
|
58
|
+
their text layer and their vector drawings. It never recognizes characters from pixels, so it never
|
|
59
|
+
misreads one.
|
|
60
|
+
|
|
61
|
+
When complete, it returns every word of a document in exactly one block (heading, paragraph, list
|
|
62
|
+
item, footnote, definition, table, or page furniture), in reading order. Tables come back as explicit
|
|
63
|
+
cell grids, with merged cells as spans and headers carried across page breaks, marked as carried.
|
|
64
|
+
Links record the relationships the page prints: footnote calls to their notes, and tables that
|
|
65
|
+
continue onto the next page.
|
|
66
|
+
|
|
67
|
+
Shipped so far (M0 to M4):
|
|
68
|
+
|
|
69
|
+
- **Words rebuilt from characters.** A superscript marker printed tight against a value stays its own
|
|
70
|
+
word (`$0.40` and `2`, never `$0.402`). A font change in the middle of a word keeps it one word.
|
|
71
|
+
- **Drawn rules** from the page's vector paths, including cell outlines drawn as rectangles.
|
|
72
|
+
- **Nothing degraded silently.** Each of these comes back as a typed finding:
|
|
73
|
+
- image-only pages, and pages MuPDF cannot load or decode;
|
|
74
|
+
- text a clip path or the CropBox hides;
|
|
75
|
+
- glyphs with no Unicode mapping, and Type 3 fonts, whose glyphs are pictures;
|
|
76
|
+
- text in the layer that the page never shows: render modes 3 and 7, or fully transparent text.
|
|
77
|
+
That is the shape of an OCR layer, or of hidden prompt injection.
|
|
78
|
+
- **Reading order across columns.** Two prose columns read left column, then right. A gutter alone
|
|
79
|
+
does not make columns, because the narrowest table columns sit as close as a label and its text:
|
|
80
|
+
columns need a gutter that persists over several lines and prose on both sides. Tables and
|
|
81
|
+
hanging-indent lists keep row order.
|
|
82
|
+
- **Running headers, footers, and page numbers** found by recurrence across pages and set apart as
|
|
83
|
+
furniture, line by line. A line inside a ruled table is never furniture, and furniture runs from
|
|
84
|
+
the page's edge, so a table's spanning header or stub banner in the page band stays in its table.
|
|
85
|
+
- **Typed blocks:** headings with their section numbers and levels, paragraphs, list items, and
|
|
86
|
+
footnotes, with every line-end hyphen join recorded so it can be undone.
|
|
87
|
+
- **Ruled tables as cell grids.** Camelot's lattice parser reads the pages whose rules run both ways,
|
|
88
|
+
through its cell edge flags, never its DataFrame. A merged cell is one cell with a span, so a value
|
|
89
|
+
is never copied into the positions it covers. Leading value-free rows become header rows, and
|
|
90
|
+
full-width rows become banner rows. A box around a paragraph, a furniture label, or a frame ruled
|
|
91
|
+
around columns of running text is not a table. Nor is a grid over a table already read (a page
|
|
92
|
+
panel, an open frame), and a page border around a table reads as the table alone. The page's own
|
|
93
|
+
drawn rules, thin grey fills included, split a cell they divide.
|
|
94
|
+
Tables export as GFM Markdown, HTML with `colspan`/`rowspan`, and dense rows that flag every
|
|
95
|
+
repeated value.
|
|
96
|
+
- **Unruled tables from whitespace.** Tables set without column rules (SIX, LSE, Euronext) come from
|
|
97
|
+
the whitespace between the value rows' cells, which stays put however a column is aligned. Every row
|
|
98
|
+
votes on where each boundary sits, wrapped lines join their row, a drawn rule ends a row or a cell,
|
|
99
|
+
and two values never share a cell: rows that could only be read by fusing two values stay text,
|
|
100
|
+
with a `table_left_as_text` warning.
|
|
101
|
+
- **Charts are not tables.** Bars whose lengths are in one proportion to the numbers they carry, or a
|
|
102
|
+
numeric axis with a tick at every label, keep a chart's labels as text (`chart_left_as_text`),
|
|
103
|
+
in either table reader. A diagonal watermark is no cell's.
|
|
104
|
+
- **Glossaries and note lists.** Inside a definitions section, an entry that opens with a term becomes
|
|
105
|
+
a definition block with its `term` and `body`: a quoted term (`“ABBO” means …`), a bold one
|
|
106
|
+
(`Available for Distribution:`), or one set in a column of its own (SIX's glossary). Outside such a
|
|
107
|
+
section only a quoted term with a defining verb counts. A two-column grid of note labels beside prose
|
|
108
|
+
becomes footnotes (`X2 | Connection rebate: …`, where a table prints `EMDI^X2`), and inside a
|
|
109
|
+
section, or under its own `Legend` banner, a grid of terms beside prose becomes definitions.
|
|
110
|
+
- **Footnote calls linked to their notes.** Notes are found in every form the measured documents print
|
|
111
|
+
them: small type opening with a number, a number or mark opening its note at the text's own size
|
|
112
|
+
(`^ Contra to …`, `2 Each month …`), a raised number glued to its first word (`1A surcharge`), and
|
|
113
|
+
ruled tables titled `Footnotes`. A note must be called somewhere in the document, so numbered
|
|
114
|
+
clauses are never read as notes. Calls come as superscripts, as parenthesised numbers at body size
|
|
115
|
+
(`Customer (2)(8)(9)`), and as named references (`see footnote 27`). Each call ends resolved to its
|
|
116
|
+
note, unresolved (with a `call_unresolved` warning), or rejected by a named drafting convention:
|
|
117
|
+
`five (5)`, `Section 202(a)(11)`, `except (1)`, and `; (2) if …` are never calls.
|
|
118
|
+
- **Tables and sentences across pages.** A table that runs onto the next page without its header
|
|
119
|
+
carries the header as cells that own no words, linked to the part it continues. A reprinted header
|
|
120
|
+
links the parts without carrying. A sentence broken by the page joins into one paragraph with a
|
|
121
|
+
region on each page.
|
|
122
|
+
- **Markdown and an HTML inspector.** The inspector draws every block over its rendered page, for
|
|
123
|
+
looking at a reading rather than trusting it, and a verification report's defects over the page.
|
|
124
|
+
- **An independent verifier.** `inkgrid verify` re-reads the PDF with PDFium and grades the document
|
|
125
|
+
character by character: nothing lost, invented, or doubled; every cell holding the ink inside it;
|
|
126
|
+
no drawn rule dividing a cell; every value bound to one cell. It exits 1 on any defect. On the 42
|
|
127
|
+
measured fee schedules it accounts for all 1,817,075 characters and reports 8 cells, among them two
|
|
128
|
+
values from two columns read as one word, and a note mark in the column after its word. On
|
|
129
|
+
ICDAR-2013 and olmOCR-bench's table pages it names the ways the two engines read a page
|
|
130
|
+
differently (ligatures, unmappable glyphs, overprinted titles), so what it still reports there is
|
|
131
|
+
inkgrid's own errors: [`docs/specs/11-verify-on-public-corpora.md`](https://github.com/Abdul-Muizz1310/inkgrid/blob/v0.1.0/docs/specs/11-verify-on-public-corpora.md) § 5 lists them.
|
|
132
|
+
- **The full output contract,** `inkgrid.document/1`, with its invariants enforced: every word owned
|
|
133
|
+
exactly once, cells that tile their grid exactly, and block text spelled from its own words in
|
|
134
|
+
their order.
|
|
135
|
+
|
|
136
|
+
### Known limitations
|
|
137
|
+
|
|
138
|
+
- **Text is judged hidden from the text layer alone.** Text covered by an opaque shape or image,
|
|
139
|
+
white text on a white page, and text too small to read all read as visible. The verifier reads the
|
|
140
|
+
text layer too, so it does not catch them either.
|
|
141
|
+
- **Errors the verifier finds on public corpora.** On the 31 ICDAR-2013 and olmOCR-bench documents
|
|
142
|
+
where the verifier found inkgrid's own errors, 25 defects remain of the baseline's 316. One is a
|
|
143
|
+
table title straddling Camelot's header-row edge (practice us-008). The other 24 are places where
|
|
144
|
+
the two engines read the text layer differently and the verifier has no benign class for yet: a
|
|
145
|
+
symbol font's glyphs, dashes and tildes on one old page, a word PDFium does not show, a run of
|
|
146
|
+
digits it reads as one value. The baseline run above counts all 316. The fixes since it
|
|
147
|
+
(overprinted text, Type 3 and Dingbats glyphs, thin rules drawn as fills, grids over frames and
|
|
148
|
+
charts, table text taken as furniture) were designed while looking at those documents, so the run
|
|
149
|
+
after them is labelled as tuned on them.
|
|
150
|
+
- **Left-to-right scripts only.** Right-to-left and bidirectional text is not reordered in v0.1.
|
|
151
|
+
- **Unruled tables have no row spans.** A label centred beside several rows splits across them
|
|
152
|
+
(`Charge per` / `executed order`), and a label whose value is centred beside it needs the rows to be
|
|
153
|
+
set apart by more than the label's own line pitch. A fee set on two lines around a one-line label
|
|
154
|
+
(`€10 per million on the value` / `above €20,000`) splits from it, never into another fee's cell.
|
|
155
|
+
- **A table needs a value.** A table of text alone with no ruled grid reads as paragraphs, and a
|
|
156
|
+
caption set directly above a table can join it as its first row.
|
|
157
|
+
- **A table beside a prose column can come apart.** Lines are grouped across the whole page before
|
|
158
|
+
columns are found, so on a two-column page a prose line level with two table rows can chain them
|
|
159
|
+
into one line. Those rows read as interleaved text, and the unruled stage reports them with
|
|
160
|
+
`table_left_as_text`.
|
|
161
|
+
- **A call resolves forward only**, and a label the document prints on several notes resolves only
|
|
162
|
+
on its own page or the next (for a table that continues, from its last part). A note printed before
|
|
163
|
+
its call, or far from a numbering that restarts, leaves the call unresolved, with a warning. A
|
|
164
|
+
parenthesised number anchored on a capitalised noun is a call by the drafting conventions, so an
|
|
165
|
+
in-text reference like `Criteria (2)` can link to note 2.
|
|
166
|
+
- **Only page breaks join.** A paragraph continued in the next column, and list items or notes that
|
|
167
|
+
continue on the next page, stay separate blocks.
|
|
168
|
+
- **Lists set in the Symbol font read as paragraphs.** Their bullet is a private-use character, which
|
|
169
|
+
the reader drops as invisible.
|
|
170
|
+
- **Untrusted PDFs belong in a separate process.** MuPDF parses in memory, and a library cannot bound
|
|
171
|
+
its memory or time. Read hostile input in a worker process with resource limits.
|
|
172
|
+
|
|
173
|
+
## The unique angle
|
|
174
|
+
|
|
175
|
+
Vision and OCR parsers read pixels, so they can misread a glyph or drop a value, and nothing downstream
|
|
176
|
+
can tell. inkgrid commits to four guarantees instead:
|
|
177
|
+
|
|
178
|
+
1. **No invented text.** Every output character is a codepoint the PDF encodes.
|
|
179
|
+
2. **No lost or doubled text.** Each document carries a proof that every word is owned exactly once.
|
|
180
|
+
3. **Independently verified structure.** A verifier re-reads the PDF with a different engine,
|
|
181
|
+
PDFium, without importing the code that built the document. Every character PDFium reads must
|
|
182
|
+
belong to exactly one word that holds it, every cell must hold the ink inside its rectangle, no
|
|
183
|
+
drawn rule may divide a cell, and every value must be bound to one cell. On the 42 fee schedules
|
|
184
|
+
of the look-back corpus, all 1,817,075 characters are accounted for, and 8 cells are reported
|
|
185
|
+
(`docs/specs/10-verify.md` § 0); re-checked for 0.1.0 on 2026-10-02, with the same result.
|
|
186
|
+
4. **Never silent.** Anything degraded becomes a typed finding on the result.
|
|
187
|
+
|
|
188
|
+
Whether this beats OCR and vision parsers at *table structure* was an open question, and the public
|
|
189
|
+
evidence favored the vision hybrids. The benchmark (below) bears that out for Docling, the strongest
|
|
190
|
+
hybrid measured. On 11 fee schedules no fix has seen, no difference between inkgrid's table structure
|
|
191
|
+
and Docling's is distinguishable from 0, but Docling places tables better, finds more of their values,
|
|
192
|
+
and misreads fewer glyphs; on the public sets, where inkgrid is tuned, Docling is ahead on every
|
|
193
|
+
headline score. inkgrid is ahead of unstructured on both, of marker on the held-out set, and of the
|
|
194
|
+
other text-layer readers on the scores the tables below mark. It does not beat Docling; what it offers
|
|
195
|
+
instead is the four guarantees, in a 20th to a 35th of Docling's time per page.
|
|
196
|
+
|
|
197
|
+
## Quick start
|
|
198
|
+
|
|
199
|
+
```bash
|
|
200
|
+
pip install inkgrid # or, in a uv project: uv add inkgrid
|
|
201
|
+
inkgrid read path/to/file.pdf -o doc.json --markdown out.md --inspector out.html
|
|
202
|
+
inkgrid verify path/to/file.pdf doc.json --inspector checked.html # exit 1 on any defect
|
|
203
|
+
inkgrid words path/to/file.pdf --pretty # the raw page model, for debugging a reading
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
To work on inkgrid itself, from a clone: `uv sync --all-groups`, then the same commands under
|
|
207
|
+
`uv run`.
|
|
208
|
+
|
|
209
|
+
From Python:
|
|
210
|
+
|
|
211
|
+
```python
|
|
212
|
+
import inkgrid
|
|
213
|
+
|
|
214
|
+
doc = inkgrid.read("fees.pdf") # a path, bytes, or a binary stream
|
|
215
|
+
for block in doc.blocks:
|
|
216
|
+
print(block.id, block.kind, block.text)
|
|
217
|
+
for finding in doc.findings:
|
|
218
|
+
print(finding.severity, finding.code, finding.detail)
|
|
219
|
+
print(doc.to_markdown())
|
|
220
|
+
|
|
221
|
+
doc = inkgrid.read("fees.pdf", strict=True) # raises StrictModeError on an error finding
|
|
222
|
+
|
|
223
|
+
report = inkgrid.verify(doc, "fees.pdf") # PDFium's reading against the document's
|
|
224
|
+
for defect in report.defects: # lost, invented, text, vrule, value, ...
|
|
225
|
+
print(defect.page, defect.code, defect.block, defect.cell, defect.detail)
|
|
226
|
+
print(report.ok)
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
`inkgrid.read_pages()` returns the raw page model (words, rules, measurements) for debugging. An
|
|
230
|
+
encrypted PDF takes `password=`. On the command line, the password is read from stdin with
|
|
231
|
+
`--password-stdin`, never from arguments, because arguments show up in the process list.
|
|
232
|
+
|
|
233
|
+
## Benchmarks / Evals
|
|
234
|
+
|
|
235
|
+
**The headline: 11 fee schedules no fix has seen** (M5d). Fee schedules from 11 exchange groups that
|
|
236
|
+
none of inkgrid's 42 tuning documents comes from, chosen by a rule fixed before any tool read them,
|
|
237
|
+
three seeded pages each: 31 pages and 53 tables. Each table's grid was drafted from the page's
|
|
238
|
+
rendering, never from any tool's reading; each cell's text is the PDF's own words; and every table,
|
|
239
|
+
with 200 cells glyph by glyph, was checked by hand. inkgrid reads them as it read the tuned run below.
|
|
240
|
+
Every tool reads each whole document (146 pages); only its tables on the scored pages count. Eleven
|
|
241
|
+
documents give wide intervals. The protocol:
|
|
242
|
+
[`docs/specs/16-held-out-fee-set.md`](https://github.com/Abdul-Muizz1310/inkgrid/blob/v0.1.0/docs/specs/16-held-out-fee-set.md); the full report, with every
|
|
243
|
+
metric and paired difference:
|
|
244
|
+
[`bench/results/2026-10-01-fb58b0a-heldout/report.md`](https://github.com/Abdul-Muizz1310/inkgrid/blob/v0.1.0/bench/results/2026-10-01-fb58b0a-heldout/report.md).
|
|
245
|
+
|
|
246
|
+
| Tool | Structure F | Table regions F | Values bound to all labels | Values found | Cell CER (lower is better) | Seconds per page |
|
|
247
|
+
|---|---|---|---|---|---|---|
|
|
248
|
+
| **inkgrid** | 0.647 [0.418, 0.849] | 0.818 [0.655, 0.945] | 0.579 [0.363, 0.793] | 0.704 [0.451, 0.899] | 0.098 [0.029, 0.240] | 0.294 [0.211, 0.434] |
|
|
249
|
+
| Docling | 0.708 [0.521, 0.867] | 0.895 [0.747, 1.000] † | 0.743 [0.593, 0.865] | 0.932 [0.834, 0.988] † | 0.024 [0.003, 0.055] † | 10.158 [9.054, 11.836] † |
|
|
250
|
+
| Docling, image-only (OCR) | 0.707 [0.519, 0.862] | 0.862 [0.661, 1.000] | 0.772 [0.592, 0.892] | 0.923 [0.817, 0.983] † | 0.035 [0.011, 0.083] | 18.404 [12.969, 22.064] † |
|
|
251
|
+
| marker | 0.234 [0.114, 0.364] † | 0.850 [0.709, 0.953] | 0.153 [0.047, 0.272] † | 0.459 [0.249, 0.647] | 0.465 [0.273, 0.618] † | 0.566 [0.489, 0.732] † |
|
|
252
|
+
| unstructured `hi_res` (OCR) | 0.336 [0.206, 0.473] † | 0.905 [0.839, 0.962] | 0.082 [0.017, 0.175] † | 0.612 [0.388, 0.810] | 0.259 [0.147, 0.352] | 5.327 [4.824, 6.194] † |
|
|
253
|
+
| Camelot | 0.476 [0.202, 0.725] | 0.725 [0.496, 0.900] | 0.350 [0.114, 0.627] † | 0.357 [0.115, 0.641] † | 0.308 [0.097, 0.635] † | 0.284 [0.263, 0.326] |
|
|
254
|
+
| PyMuPDF | 0.490 [0.259, 0.711] | 0.776 [0.558, 0.939] | 0.320 [0.105, 0.592] † | 0.343 [0.113, 0.622] † | 0.477 [0.222, 0.703] † | 0.070 [0.058, 0.086] † |
|
|
255
|
+
| pdfplumber | 0.498 [0.263, 0.711] | 0.714 [0.480, 0.900] | 0.320 [0.105, 0.592] † | 0.343 [0.113, 0.622] † | 0.520 [0.241, 0.760] † | 0.096 [0.080, 0.113] † |
|
|
256
|
+
| inkgrid on Tesseract's words | 0.368 [0.218, 0.532] † | 0.809 [0.673, 0.928] | 0.136 [0.028, 0.305] † | 0.468 [0.291, 0.650] † | 0.144 [0.071, 0.265] | 1.681 [1.575, 1.912] † |
|
|
257
|
+
|
|
258
|
+
A † marks a tool whose paired difference from inkgrid on that metric has an interval excluding 0.
|
|
259
|
+
"Values bound to all labels" and "values found" score the truth's access paths (each value with its row
|
|
260
|
+
and column labels); cell CER counts glyph errors on the 200 checked cells, each against the closest
|
|
261
|
+
cell of the tool's matching table, so a missed table costs all its glyphs. Docling reads the text layer
|
|
262
|
+
through its layout and table models, with OCR only where a page has none; its image-only run, and
|
|
263
|
+
unstructured, read every page with OCR; marker reads with OCR off. The heavy tools' seconds are a warm
|
|
264
|
+
process's on a 16-thread CPU, their models loaded.
|
|
265
|
+
|
|
266
|
+
- **Against the other text-layer readers** (Camelot, PyMuPDF, pdfplumber): inkgrid files more values
|
|
267
|
+
under all their labels (0.579 against 0.320 to 0.350) and finds more of them (0.704 against 0.343 to
|
|
268
|
+
0.357), misreads fewer glyphs (0.098 against 0.308 to 0.520), and its structure recall is higher
|
|
269
|
+
(0.628 against 0.402 to 0.443). Its structure F and table regions are not distinguishable from theirs.
|
|
270
|
+
- **Against Docling:** no difference in structure (F 0.647 against 0.708) or in values bound to all
|
|
271
|
+
their labels (0.579 against 0.743) is distinguishable from 0. Docling places tables better (regions F
|
|
272
|
+
0.895 against 0.818), finds more values (0.932 against 0.704), and misreads fewer glyphs (0.024
|
|
273
|
+
against 0.098), at 10.2 seconds a page to inkgrid's 0.29. On the image-only copy, Docling finds more
|
|
274
|
+
values (0.923); no other difference from inkgrid is distinguishable, at 18.4 seconds a page.
|
|
275
|
+
- **Against marker and unstructured `hi_res`:** inkgrid's structure (F 0.647 against 0.234 and 0.336)
|
|
276
|
+
and values bound to all their labels (0.579 against 0.153 and 0.082) are higher, and marker misreads
|
|
277
|
+
more glyphs (0.465); no difference in table regions is distinguishable.
|
|
278
|
+
- **What the text layer gives inkgrid:** fed Tesseract's words instead of the PDF's, inkgrid's
|
|
279
|
+
structure F falls from 0.647 to 0.368 and its values bound to all their labels from 0.579 to 0.136,
|
|
280
|
+
and it runs 5.7 times slower.
|
|
281
|
+
- **Where inkgrid fails:** on three of the 11 documents (Borsa Istanbul's data fees, JPX's participant
|
|
282
|
+
fees, LuxSE's listing fees) it binds no value to all its labels and gets few of the tables' cell
|
|
283
|
+
relations right; they hold 283 of its 407 misread glyphs.
|
|
284
|
+
- **The verifier** reports 8 cells on these documents, all on one (PSX's trading fees, pages 3 and 4),
|
|
285
|
+
each a word inkgrid's grid cuts in two. **Camelot refuses one document** whose permissions forbid
|
|
286
|
+
text extraction (SET's TFEX fees), scored as no tables; every other tool reads all 11.
|
|
287
|
+
- **The truth shares the text layer:** its cell text is the characters inkgrid and the other
|
|
288
|
+
text-layer readers also read, so an OCR tool can misread a glyph where they cannot. Values found and
|
|
289
|
+
cell CER compare the two kinds of tool on those terms.
|
|
290
|
+
|
|
291
|
+
**Two runs against other text-layer readers** (pdfplumber 0.11.10, PyMuPDF 1.28.2's
|
|
292
|
+
`find_tables()`, and Camelot 2.0.0's lattice, each with its defaults). The **baseline** is inkgrid's
|
|
293
|
+
reading before any fix for the reading errors its verifier found on these same documents; the one
|
|
294
|
+
change since M4 stops a crash on a Camelot table with no cells. The **tuned** run is its reading after
|
|
295
|
+
the M5c fixes, which were designed while looking at those errors, so it overstates how inkgrid reads
|
|
296
|
+
documents no fix has seen (DR-0023). The peers are measured again in each run, and their numbers are
|
|
297
|
+
identical. Each number is a point estimate with its 95% interval from 10,000 resamples of documents.
|
|
298
|
+
The protocol was fixed before anything was scored:
|
|
299
|
+
[`docs/specs/12-benchmark.md`](https://github.com/Abdul-Muizz1310/inkgrid/blob/v0.1.0/docs/specs/12-benchmark.md). The full reports, with every metric and
|
|
300
|
+
every paired difference, are [`bench/results/latest.md`](https://github.com/Abdul-Muizz1310/inkgrid/blob/v0.1.0/bench/results/latest.md) (tuned, with the
|
|
301
|
+
heavy tools below) and
|
|
302
|
+
[`bench/results/2026-09-29-b33b3cb/report.md`](https://github.com/Abdul-Muizz1310/inkgrid/blob/v0.1.0/bench/results/2026-09-29-b33b3cb/report.md) (baseline).
|
|
303
|
+
|
|
304
|
+
| Dataset (documents) | Metric | inkgrid, tuned | inkgrid, baseline | Camelot | PyMuPDF | pdfplumber |
|
|
305
|
+
|---|---|---|---|---|---|---|
|
|
306
|
+
| ICDAR-2013 competition (67) | Structure F, the competition's scorer | **0.788** [0.711, 0.861] | **0.777** [0.700, 0.850] | 0.637 [0.531, 0.735] | 0.611 [0.512, 0.702] | 0.591 [0.493, 0.683] |
|
|
307
|
+
| ICDAR-2013 competition (67) | F1-TEDS, Soric et al.'s protocol | 0.628 [0.466, 0.788] | 0.612 [0.455, 0.767] | 0.624 [0.494, 0.735] | 0.519 [0.384, 0.639] | 0.416 [0.287, 0.545] |
|
|
308
|
+
| ICDAR-2013 practice (58) | Structure F, the competition's scorer | **0.688** [0.596, 0.776] | **0.664** [0.569, 0.756] | 0.499 [0.381, 0.611] | 0.457 [0.346, 0.568] | 0.420 [0.314, 0.527] |
|
|
309
|
+
| ICDAR-2013 practice (58) | Values filed under their labels | **0.323** [0.199, 0.488] | **0.309** [0.188, 0.469] | 0.251 [0.142, 0.393] | 0.239 [0.135, 0.382] | 0.172 [0.093, 0.280] |
|
|
310
|
+
| olmOCR-bench tables (188) | Table tests passed | **0.486** [0.415, 0.558] | **0.481** [0.410, 0.552] | 0.273 [0.213, 0.338] | 0.321 [0.256, 0.390] | 0.294 [0.231, 0.361] |
|
|
311
|
+
|
|
312
|
+
A bold number is ahead of all three peers in its run, with every paired difference's interval
|
|
313
|
+
excluding 0.
|
|
314
|
+
|
|
315
|
+
- **Where inkgrid is ahead, in both runs:** table structure by the ICDAR competition's own scorer, on
|
|
316
|
+
both sets, mostly through recall; the olmOCR-bench table tests, on all 188 PDFs and on the 173 with
|
|
317
|
+
a text layer; and values filed under their row and column labels (the practice set's 5,603
|
|
318
|
+
checkable access paths). That last metric tops out at 0.868 even when the ground truth itself is
|
|
319
|
+
read as a tool, partly because some labels sit outside the value's row and column.
|
|
320
|
+
- **Where it is not:** under Soric et al.'s protocol no difference from Camelot or PyMuPDF is
|
|
321
|
+
distinguishable from 0 in either run, and inkgrid is ahead of pdfplumber on the three structure
|
|
322
|
+
scores but not on table boxes. At the baseline, its pooled structure precision on the competition
|
|
323
|
+
set was below Camelot's (0.794 against 0.934), and so was its region precision on the practice set
|
|
324
|
+
(0.873 against 0.940); in the tuned run neither difference is distinguishable from 0 (0.802, and
|
|
325
|
+
0.895).
|
|
326
|
+
- **Tuned against baseline:** every tuned number in the table is higher, but the two runs are not
|
|
327
|
+
paired here and each row's intervals overlap, so this benchmark does not show that the fixes
|
|
328
|
+
raised these scores. What they did remove, by design, is what the verifier finds on these
|
|
329
|
+
documents: 323 defects on 34 of the 313 at the baseline, 32 on 7 in the tuned run.
|
|
330
|
+
- **It is the slowest:** 0.35 to 0.53 seconds per page in the tuned run (0.34 to 0.55 at the
|
|
331
|
+
baseline), against 0.28 to 0.36 for Camelot and 0.07 to 0.12 for PyMuPDF and pdfplumber (timings
|
|
332
|
+
move about 10% between two runs of the same code). inkgrid runs Camelot's lattice for ruled grids,
|
|
333
|
+
so its comparison with Camelot measures what inkgrid adds on top of it.
|
|
334
|
+
- **No tool crashed** on any of the 313 documents in either run. Soric et al.'s own released
|
|
335
|
+
predictions, re-scored here with their evaluator, reproduce their results within 0.01 on 15 of 16
|
|
336
|
+
numbers (PyMuPDF's F1-TEDS misses by 0.00002).
|
|
337
|
+
|
|
338
|
+
**OCR, vision, and hybrid parsers on the public sets** (M5d-1). The tuned run again, at `bea4c95`,
|
|
339
|
+
with Docling (by default and on an image-only copy of each PDF), marker (OCR off), unstructured
|
|
340
|
+
`hi_res` (Tesseract), and inkgrid on Tesseract's words added; inkgrid's and the three peers' counts are
|
|
341
|
+
identical to the first tuned run's. inkgrid is tuned on these documents and the heavy tools are not,
|
|
342
|
+
so the comparison flatters inkgrid. A † marks a tool whose paired difference from inkgrid has an
|
|
343
|
+
interval excluding 0. The full report is [`bench/results/latest.md`](https://github.com/Abdul-Muizz1310/inkgrid/blob/v0.1.0/bench/results/latest.md).
|
|
344
|
+
|
|
345
|
+
| Dataset (documents) | Metric | inkgrid, tuned | Docling | Docling, image-only (OCR) | marker | unstructured `hi_res` (OCR) | inkgrid on Tesseract's words |
|
|
346
|
+
|---|---|---|---|---|---|---|---|
|
|
347
|
+
| ICDAR-2013 competition (67) | Structure F, the competition's scorer | 0.788 [0.711, 0.861] | 0.901 [0.857, 0.937] † | 0.880 [0.827, 0.924] † | 0.768 [0.697, 0.833] | 0.467 [0.398, 0.534] † | 0.354 [0.280, 0.428] † |
|
|
348
|
+
| ICDAR-2013 competition (67) | F1-TEDS, Soric et al.'s protocol | 0.628 [0.466, 0.788] | 0.894 [0.846, 0.934] † | 0.881 [0.829, 0.923] † | 0.759 [0.682, 0.827] | 0.585 [0.510, 0.654] | 0.341 [0.245, 0.446] † |
|
|
349
|
+
| ICDAR-2013 practice (58) | Structure F, the competition's scorer | 0.688 [0.596, 0.776] | 0.847 [0.775, 0.910] † | 0.829 [0.763, 0.888] † | 0.783 [0.708, 0.850] † | 0.468 [0.397, 0.541] † | 0.328 [0.253, 0.404] † |
|
|
350
|
+
| ICDAR-2013 practice (58) | Values filed under their labels | 0.323 [0.199, 0.488] | 0.494 [0.329, 0.708] † | 0.473 [0.314, 0.672] † | 0.303 [0.183, 0.464] | 0.074 [0.035, 0.131] † | 0.065 [0.024, 0.126] † |
|
|
351
|
+
| olmOCR-bench tables (188) | Table tests passed | 0.486 [0.415, 0.558] | 0.744 [0.692, 0.792] † | 0.736 [0.683, 0.787] † | 0.460 [0.397, 0.521] | 0.413 [0.359, 0.468] | 0.303 [0.252, 0.358] † |
|
|
352
|
+
| ICDAR-2013 practice (58) | Seconds per page | 0.427 [0.374, 0.486] | 8.254 [6.876, 9.904] † | 13.525 [12.634, 14.583] † | 0.451 [0.440, 0.464] | 5.186 [4.962, 5.443] † | 2.226 [2.070, 2.391] † |
|
|
353
|
+
|
|
354
|
+
- **Docling is ahead of inkgrid on every one of these**, in both its runs: structure F by 0.09 to
|
|
355
|
+
0.16, F1-TEDS by 0.25 to 0.27, values filed under their labels by 0.15 to 0.17, and olmOCR-bench's
|
|
356
|
+
table tests by 0.25 to 0.26, at 8.3 and 13.5 seconds a page on the practice set to inkgrid's 0.43.
|
|
357
|
+
- **marker** is ahead on the practice set's structure (0.783 against 0.688); no other difference
|
|
358
|
+
from inkgrid is distinguishable, its speed included. On the held-out fee set it is far behind.
|
|
359
|
+
- **unstructured `hi_res`** trails inkgrid on structure, on both sets, and on values filed under
|
|
360
|
+
their labels; on F1-TEDS and olmOCR-bench no difference is distinguishable.
|
|
361
|
+
- **On Tesseract's words**, inkgrid trails itself on every metric (structure F 0.354 and 0.328,
|
|
362
|
+
against 0.788 and 0.688): the text layer, not the gridders alone, is what puts inkgrid where it is.
|
|
363
|
+
- No heavy tool crashed or timed out on any of the 313 documents.
|
|
364
|
+
|
|
365
|
+
Not measured: FinTabNet, of which no official source of the PDFs remains.
|
|
366
|
+
|
|
367
|
+
## Architecture
|
|
368
|
+
|
|
369
|
+
A pure core sits between two thin shells. Only `inkgrid.read` touches MuPDF and Camelot, and only
|
|
370
|
+
`inkgrid.verify`'s reader touches PDFium; a test enforces the layer table on the import graph. The
|
|
371
|
+
verifier cannot import the code that builds the output, so its independence is structural. Details:
|
|
372
|
+
[`docs/ARCHITECTURE.md`](https://github.com/Abdul-Muizz1310/inkgrid/blob/v0.1.0/docs/ARCHITECTURE.md).
|
|
373
|
+
|
|
374
|
+
## Tech stack
|
|
375
|
+
|
|
376
|
+
Python 3.12+, PyMuPDF 1.28 (text layer and drawings), Pydantic v2 (frozen, validated models), uv
|
|
377
|
+
with the `uv_build` backend, Ruff, mypy `--strict`, pytest 9 with Hypothesis, and GitHub Actions.
|
|
378
|
+
Camelot 2.0 reads ruled tables. The default engine is `lattice="combined"`, because it never fused or
|
|
379
|
+
split a cell across 42 measured fee schedules. `"vector"` is about twice as fast end to end, but it
|
|
380
|
+
malformed cells on 3% of their ruled pages, mostly tables drawn as Word draws borders. `"raster"` is
|
|
381
|
+
also available. pypdfium2 5.13 (PDFium) powers the independent verifier.
|
|
382
|
+
|
|
383
|
+
## Deployment
|
|
384
|
+
|
|
385
|
+
inkgrid is a library, so there is nothing to deploy. A release is a version tag: `release.yml` checks
|
|
386
|
+
that the tag is the project's final version with a dated changelog section, runs the lint, type, and
|
|
387
|
+
test gates and the distribution smoke tests on Linux (CI on `main` covers the other platforms), builds
|
|
388
|
+
the sdist and wheel, and uploads those files to PyPI with the repository's API token; a `testpypi-v`
|
|
389
|
+
tag rehearses the same on TestPyPI (`docs/specs/17-release.md`). Versions are bumped with
|
|
390
|
+
`uv version --bump`.
|
|
391
|
+
|
|
392
|
+
## License
|
|
393
|
+
|
|
394
|
+
inkgrid's own code is MIT; see [LICENSE](https://github.com/Abdul-Muizz1310/inkgrid/blob/v0.1.0/LICENSE).
|
|
395
|
+
|
|
396
|
+
**Read this before you ship inkgrid in a product.** inkgrid requires
|
|
397
|
+
[PyMuPDF](https://pymupdf.readthedocs.io/en/latest/about.html#license-and-copyright), which Artifex
|
|
398
|
+
dual-licenses under the GNU AGPL-3.0 or a commercial license. Software distributed or served over a
|
|
399
|
+
network with inkgrid must meet the AGPL's terms for the combined work, unless you hold Artifex's
|
|
400
|
+
commercial license. This is not legal advice.
|
|
401
|
+
|
|
402
|
+
The other dependencies are permissively licensed: Camelot (MIT), Pydantic (MIT), OpenCV (Apache-2.0),
|
|
403
|
+
pypdfium2 (Apache-2.0 or BSD-3-Clause), pandas and NumPy (BSD-3-Clause). OpenCV is pinned away from
|
|
404
|
+
5.0.0.93, whose bundled FFmpeg lacks the CVE-2026-8461 fix.
|