strikethrough-ocr 0.3.3__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.
- strikethrough_ocr-0.3.3/.gitignore +10 -0
- strikethrough_ocr-0.3.3/Makefile +18 -0
- strikethrough_ocr-0.3.3/PKG-INFO +179 -0
- strikethrough_ocr-0.3.3/README.md +157 -0
- strikethrough_ocr-0.3.3/___dev/.gitignore +0 -0
- strikethrough_ocr-0.3.3/___dev/.notes +2 -0
- strikethrough_ocr-0.3.3/___dev/COMMIT_CHANGES.sh +31 -0
- strikethrough_ocr-0.3.3/___dev/NOTES.md +25 -0
- strikethrough_ocr-0.3.3/demo.sh +18 -0
- strikethrough_ocr-0.3.3/examples/image01.png +0 -0
- strikethrough_ocr-0.3.3/examples/image02.png +0 -0
- strikethrough_ocr-0.3.3/examples/image03.png +0 -0
- strikethrough_ocr-0.3.3/examples/image04.png +0 -0
- strikethrough_ocr-0.3.3/examples/image05.png +0 -0
- strikethrough_ocr-0.3.3/examples/image06.png +0 -0
- strikethrough_ocr-0.3.3/examples/image07.png +0 -0
- strikethrough_ocr-0.3.3/examples/image08.png +0 -0
- strikethrough_ocr-0.3.3/examples/image09.png +0 -0
- strikethrough_ocr-0.3.3/examples/image10.png +0 -0
- strikethrough_ocr-0.3.3/examples/image11.png +0 -0
- strikethrough_ocr-0.3.3/examples/image12.png +0 -0
- strikethrough_ocr-0.3.3/examples/image13.png +0 -0
- strikethrough_ocr-0.3.3/examples/image14.png +0 -0
- strikethrough_ocr-0.3.3/examples/image15.png +0 -0
- strikethrough_ocr-0.3.3/pyproject.toml +43 -0
- strikethrough_ocr-0.3.3/src/strikethrough_ocr/__init__.py +42 -0
- strikethrough_ocr-0.3.3/src/strikethrough_ocr/markdown.py +973 -0
- strikethrough_ocr-0.3.3/src/strikethrough_ocr/scan.py +1225 -0
- strikethrough_ocr-0.3.3/src/strikethrough_ocr/strikethrough_ocr.py +139 -0
- strikethrough_ocr-0.3.3/tests/conftest.py +122 -0
- strikethrough_ocr-0.3.3/tests/fixtures/page.overlay.html +93 -0
- strikethrough_ocr-0.3.3/tests/test_cli.py +63 -0
- strikethrough_ocr-0.3.3/tests/test_document.py +147 -0
- strikethrough_ocr-0.3.3/tests/test_golden.py +41 -0
- strikethrough_ocr-0.3.3/tests/test_integration.py +37 -0
- strikethrough_ocr-0.3.3/tests/test_scan_geometry.py +62 -0
- strikethrough_ocr-0.3.3/tests/test_scan_ink.py +217 -0
- strikethrough_ocr-0.3.3/uv.lock +370 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
.PHONY: all
|
|
2
|
+
all: build test
|
|
3
|
+
|
|
4
|
+
.PHONY: build
|
|
5
|
+
build:
|
|
6
|
+
uv build
|
|
7
|
+
|
|
8
|
+
.PHONY: test
|
|
9
|
+
test:
|
|
10
|
+
uv run pytest
|
|
11
|
+
|
|
12
|
+
# uv publish uploads every file in dist/, including builds of old
|
|
13
|
+
# versions; publish only the artifacts of the current version.
|
|
14
|
+
VERSION := $(shell sed -n 's/^version = "\(.*\)"/\1/p' pyproject.toml)
|
|
15
|
+
|
|
16
|
+
.PHONY: publish
|
|
17
|
+
publish: build test
|
|
18
|
+
uv publish dist/strikethrough_ocr-$(VERSION)-py3-none-any.whl dist/strikethrough_ocr-$(VERSION).tar.gz
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: strikethrough-ocr
|
|
3
|
+
Version: 0.3.3
|
|
4
|
+
Summary: OCR for scanned pages that preserves styling: bold, colour, underline and strike-through.
|
|
5
|
+
Author-email: Frank Eickhoff <info@frankeickhoff.de>
|
|
6
|
+
Keywords: contracts,ocr,opencv,scanned-documents,strike-through,strikethrough,tesseract
|
|
7
|
+
Classifier: Development Status :: 4 - Beta
|
|
8
|
+
Classifier: Environment :: Console
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
14
|
+
Classifier: Topic :: Multimedia :: Graphics :: Capture :: Scanners
|
|
15
|
+
Classifier: Topic :: Scientific/Engineering :: Image Recognition
|
|
16
|
+
Classifier: Topic :: Text Processing :: Markup :: Markdown
|
|
17
|
+
Requires-Python: >=3.11
|
|
18
|
+
Requires-Dist: numpy>=2
|
|
19
|
+
Requires-Dist: opencv-python-headless>=4.11
|
|
20
|
+
Requires-Dist: pytesseract>=0.3.13
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
|
|
23
|
+
# strikethrough-ocr
|
|
24
|
+
|
|
25
|
+
Most OCR pipelines silently drop text decorations like color, underline, strike-through.
|
|
26
|
+
|
|
27
|
+
StrikethroughOCR tries to preserves them.
|
|
28
|
+
|
|
29
|
+
## Installation
|
|
30
|
+
|
|
31
|
+
```sh
|
|
32
|
+
uv tool install strikethrough-ocr # or: pip install strikethrough-ocr
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Tesseract itself must be on `PATH`:
|
|
36
|
+
|
|
37
|
+
```sh
|
|
38
|
+
brew install tesseract # macOS
|
|
39
|
+
apt install tesseract-ocr # Debian/Ubuntu
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Command line
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
strikethrough-ocr [OPTIONS] INPUT.png
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Without options it writes the HTML overlay next to the image:
|
|
49
|
+
|
|
50
|
+
```sh
|
|
51
|
+
strikethrough-ocr page.png # -> page.scan-preview.html (overlay)
|
|
52
|
+
strikethrough-ocr -to html page.png # -> page.scan.html (clean document)
|
|
53
|
+
strikethrough-ocr -to md page.png # -> page.scan.md
|
|
54
|
+
strikethrough-ocr page.png -o out/ # choose the output directory
|
|
55
|
+
strikethrough-ocr page.png --stdout # print instead of writing
|
|
56
|
+
strikethrough-ocr page.png --lang deu+eng # Tesseract language(s)
|
|
57
|
+
strikethrough-ocr page.png --json # also write page.scan.json
|
|
58
|
+
strikethrough-ocr page.png --debug # also write page.debug.png
|
|
59
|
+
strikethrough-ocr page.png -v # log second-pass OCR decisions
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
| option | effect |
|
|
63
|
+
|---|---|
|
|
64
|
+
| `-to {preview,html,md}` | output format; `preview` (default) writes the overlay as `<name>.scan-preview.html`, `html` writes the clean document as `<name>.scan.html`, `md` writes `<name>.scan.md` |
|
|
65
|
+
| `-o`, `--out-dir` | output directory (default: next to the image) |
|
|
66
|
+
| `--stdout` | print the output instead of writing a file |
|
|
67
|
+
| `--lang` | Tesseract language(s), e.g. `deu+eng` (default `eng`) |
|
|
68
|
+
| `--psm` | Tesseract page segmentation mode (default 3) |
|
|
69
|
+
| `--bold-factor` | stroke width relative to regular text that counts as bold (default 1.2) |
|
|
70
|
+
| `--json` | also write `<name>.scan.json` with the page model as records |
|
|
71
|
+
| `--debug` | also write `<name>.debug.png` with the detections drawn on the scan |
|
|
72
|
+
| `--min-conf` | `-to html`, `-to md`: drop words below this OCR confidence |
|
|
73
|
+
| `--no-color` | `-to html`, `-to md`: do not emit colour spans |
|
|
74
|
+
| `--no-headings` | `-to html`, `-to md`: large text stays a paragraph |
|
|
75
|
+
| `-v`, `--verbose` | log second-pass OCR and layout decisions |
|
|
76
|
+
|
|
77
|
+
The HTML overlay references the scan by relative path for its slider, so
|
|
78
|
+
keep the two files together or the slider shows nothing.
|
|
79
|
+
|
|
80
|
+
## Library
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
from strikethrough_ocr import process, render_html, html_to_markdown, \
|
|
84
|
+
html_to_document_html
|
|
85
|
+
|
|
86
|
+
model = process("page.png", lang="eng") # run the OCR pipeline
|
|
87
|
+
overlay = render_html(model, "page", "page.png") # styled HTML overlay
|
|
88
|
+
md = html_to_markdown(overlay) # Markdown in reading order
|
|
89
|
+
doc = html_to_document_html(overlay) # clean document HTML
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
`process()` returns a `PageModel`: words (text, box, baseline, font size,
|
|
93
|
+
colour, bold, underline, strike, decoration colour, confidence), table
|
|
94
|
+
rules, decoration strokes and graphics. `model_to_json()` turns it into
|
|
95
|
+
plain records; `draw_debug()` paints the detections onto the scan image.
|
|
96
|
+
|
|
97
|
+
## Development
|
|
98
|
+
|
|
99
|
+
```sh
|
|
100
|
+
uv sync --group dev # install with the test dependencies
|
|
101
|
+
uv run pytest # unit tests and golden files
|
|
102
|
+
UPDATE_GOLDENS=1 uv run pytest # rewrite tests/golden and the overlay fixture
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
The golden tests pin all three renderings of one synthetic fixture page
|
|
106
|
+
(`tests/conftest.py` builds it): the overlay it renders must equal
|
|
107
|
+
`tests/fixtures/page.overlay.html`, and the Markdown and document HTML
|
|
108
|
+
produced from that fixture must equal the files under `tests/golden/`.
|
|
109
|
+
After a deliberate change to a renderer, regenerate them and review the
|
|
110
|
+
diff. One end-to-end test runs the real pipeline and is skipped when no
|
|
111
|
+
`tesseract` binary is on PATH.
|
|
112
|
+
|
|
113
|
+
## How it works
|
|
114
|
+
|
|
115
|
+
1. **Ink mask.** The page is thresholded on each pixel's *distance from
|
|
116
|
+
white*, `255 − min(B, G, R)`, so black and coloured ink are both caught.
|
|
117
|
+
The glyph size measured on this mask sets the working scale: a scan
|
|
118
|
+
whose print is smaller than the 300 dpi reference is enlarged (up to 4x)
|
|
119
|
+
before anything else, because Tesseract and every stroke measure need
|
|
120
|
+
the pixels. Highlighter bands pass the ink threshold too; they are
|
|
121
|
+
recognised as *bright, broad* fills — every print ink is dark, and no
|
|
122
|
+
stroke of print is as tall as a marker band — and whitened away, so OCR
|
|
123
|
+
sees paper while the letters on top of them stay.
|
|
124
|
+
2. **Straight strokes.** Morphological opening finds long horizontal and
|
|
125
|
+
vertical strokes. A stroke with clear paper on both sides is a **table
|
|
126
|
+
rule**; every other horizontal stroke is a **decoration candidate**
|
|
127
|
+
(underline or strike-through). The paper test follows the stroke's ink
|
|
128
|
+
column by column, so a slightly slanted strike-through does not pass
|
|
129
|
+
for a rule. A stroke crossed by vertical rules belongs to a table
|
|
130
|
+
lattice: it is split at the crossings, and only a piece whose glyphs
|
|
131
|
+
pass through it (a strike drawn flush onto a row boundary fuses with
|
|
132
|
+
it into one line) stays a decoration. Length is deliberately no
|
|
133
|
+
criterion — a struck-through paragraph line is as long as a rule.
|
|
134
|
+
3. **OCR.** Strikes are removed in two ways (Telea inpainting and
|
|
135
|
+
column-wise erasure, which fail on different glyphs) and Tesseract reads
|
|
136
|
+
the cleaned page as hOCR, which carries a baseline and font size per row.
|
|
137
|
+
Weak lines are cut out and read again in isolation.
|
|
138
|
+
4. **Per-word style.** Colour is the median of the *darkest* ink in the
|
|
139
|
+
word box (darkest by luminance, so the fringe where a glyph blends into
|
|
140
|
+
a marker or the paper does not vote); a decoration's colour comes from
|
|
141
|
+
the stroke's own pixels, or the glyphs a red line crosses would outvote
|
|
142
|
+
the line. Bold compares the measured stroke width (2 × area / perimeter of the
|
|
143
|
+
word's core ink) against a regular-weight model fitted to the page.
|
|
144
|
+
Decoration strokes are assigned to the words they span: through the
|
|
145
|
+
x-height it is a strike, at the baseline an underline. Struck lines are
|
|
146
|
+
re-read on both cleaned variants and the most confident reading wins.
|
|
147
|
+
5. **Graphics.** Oversized coloured lettering (logos) and ink no confident
|
|
148
|
+
word claims are cropped from the scan and embedded as PNG data URIs.
|
|
149
|
+
|
|
150
|
+
In the Markdown output, connected rules become tables (merged cells hold
|
|
151
|
+
their content in the top-left grid cell), large text becomes headings,
|
|
152
|
+
bold is `**text**`, strike-through `~~text~~`, underline `<u>text</u>`
|
|
153
|
+
and colour `<span style="color:#rrggbb">…</span>`; colour and underline
|
|
154
|
+
have no Markdown syntax, so they are inline HTML. The document HTML
|
|
155
|
+
output shares the same layout analysis and writes it the way pandoc
|
|
156
|
+
would: `<p>`, `<h1..h3>` with an id, `<table>` with a `<colgroup>`,
|
|
157
|
+
`<strong>`/`<del>`/`<u>`/`<span>` inline, and consecutive paragraphs
|
|
158
|
+
labelled `(vi)`, `(b)` or `3.` folded into `<ol start type>` lists —
|
|
159
|
+
unless the label itself is struck through, because deleted text keeps
|
|
160
|
+
its literal label.
|
|
161
|
+
|
|
162
|
+
## Limits and assumptions
|
|
163
|
+
|
|
164
|
+
* Geometry constants are set for a 300 dpi scan and scaled to the page's
|
|
165
|
+
*measured* glyph size; a scan with smaller print is enlarged internally
|
|
166
|
+
(up to 4x) first. Below roughly 8 px of x-height there is not enough
|
|
167
|
+
signal left even so, and the bold measure goes first.
|
|
168
|
+
* The page is assumed to be deskewed; a rotated page breaks the rule
|
|
169
|
+
detection.
|
|
170
|
+
* Bold is relative: a page set entirely in bold has no regular reference
|
|
171
|
+
and renders as regular. Italic is not detected.
|
|
172
|
+
* Underline and strike-through are found as straight strokes at least
|
|
173
|
+
40 px long (at 300 dpi); a wavy or hand-drawn line is missed, and a
|
|
174
|
+
heavy or doubled strike hides too much of the glyphs for the text to be
|
|
175
|
+
recovered. A marker stroke as tall as the text it covers is taken for a
|
|
176
|
+
highlight, not a strike.
|
|
177
|
+
* The HTML overlay is a *reproduction of the page*, not a document: words
|
|
178
|
+
are absolutely positioned and the reading order is Tesseract's. The
|
|
179
|
+
document HTML and Markdown outputs are the document view.
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# strikethrough-ocr
|
|
2
|
+
|
|
3
|
+
Most OCR pipelines silently drop text decorations like color, underline, strike-through.
|
|
4
|
+
|
|
5
|
+
StrikethroughOCR tries to preserves them.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
uv tool install strikethrough-ocr # or: pip install strikethrough-ocr
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Tesseract itself must be on `PATH`:
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
brew install tesseract # macOS
|
|
17
|
+
apt install tesseract-ocr # Debian/Ubuntu
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Command line
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
strikethrough-ocr [OPTIONS] INPUT.png
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Without options it writes the HTML overlay next to the image:
|
|
27
|
+
|
|
28
|
+
```sh
|
|
29
|
+
strikethrough-ocr page.png # -> page.scan-preview.html (overlay)
|
|
30
|
+
strikethrough-ocr -to html page.png # -> page.scan.html (clean document)
|
|
31
|
+
strikethrough-ocr -to md page.png # -> page.scan.md
|
|
32
|
+
strikethrough-ocr page.png -o out/ # choose the output directory
|
|
33
|
+
strikethrough-ocr page.png --stdout # print instead of writing
|
|
34
|
+
strikethrough-ocr page.png --lang deu+eng # Tesseract language(s)
|
|
35
|
+
strikethrough-ocr page.png --json # also write page.scan.json
|
|
36
|
+
strikethrough-ocr page.png --debug # also write page.debug.png
|
|
37
|
+
strikethrough-ocr page.png -v # log second-pass OCR decisions
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
| option | effect |
|
|
41
|
+
|---|---|
|
|
42
|
+
| `-to {preview,html,md}` | output format; `preview` (default) writes the overlay as `<name>.scan-preview.html`, `html` writes the clean document as `<name>.scan.html`, `md` writes `<name>.scan.md` |
|
|
43
|
+
| `-o`, `--out-dir` | output directory (default: next to the image) |
|
|
44
|
+
| `--stdout` | print the output instead of writing a file |
|
|
45
|
+
| `--lang` | Tesseract language(s), e.g. `deu+eng` (default `eng`) |
|
|
46
|
+
| `--psm` | Tesseract page segmentation mode (default 3) |
|
|
47
|
+
| `--bold-factor` | stroke width relative to regular text that counts as bold (default 1.2) |
|
|
48
|
+
| `--json` | also write `<name>.scan.json` with the page model as records |
|
|
49
|
+
| `--debug` | also write `<name>.debug.png` with the detections drawn on the scan |
|
|
50
|
+
| `--min-conf` | `-to html`, `-to md`: drop words below this OCR confidence |
|
|
51
|
+
| `--no-color` | `-to html`, `-to md`: do not emit colour spans |
|
|
52
|
+
| `--no-headings` | `-to html`, `-to md`: large text stays a paragraph |
|
|
53
|
+
| `-v`, `--verbose` | log second-pass OCR and layout decisions |
|
|
54
|
+
|
|
55
|
+
The HTML overlay references the scan by relative path for its slider, so
|
|
56
|
+
keep the two files together or the slider shows nothing.
|
|
57
|
+
|
|
58
|
+
## Library
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
from strikethrough_ocr import process, render_html, html_to_markdown, \
|
|
62
|
+
html_to_document_html
|
|
63
|
+
|
|
64
|
+
model = process("page.png", lang="eng") # run the OCR pipeline
|
|
65
|
+
overlay = render_html(model, "page", "page.png") # styled HTML overlay
|
|
66
|
+
md = html_to_markdown(overlay) # Markdown in reading order
|
|
67
|
+
doc = html_to_document_html(overlay) # clean document HTML
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
`process()` returns a `PageModel`: words (text, box, baseline, font size,
|
|
71
|
+
colour, bold, underline, strike, decoration colour, confidence), table
|
|
72
|
+
rules, decoration strokes and graphics. `model_to_json()` turns it into
|
|
73
|
+
plain records; `draw_debug()` paints the detections onto the scan image.
|
|
74
|
+
|
|
75
|
+
## Development
|
|
76
|
+
|
|
77
|
+
```sh
|
|
78
|
+
uv sync --group dev # install with the test dependencies
|
|
79
|
+
uv run pytest # unit tests and golden files
|
|
80
|
+
UPDATE_GOLDENS=1 uv run pytest # rewrite tests/golden and the overlay fixture
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
The golden tests pin all three renderings of one synthetic fixture page
|
|
84
|
+
(`tests/conftest.py` builds it): the overlay it renders must equal
|
|
85
|
+
`tests/fixtures/page.overlay.html`, and the Markdown and document HTML
|
|
86
|
+
produced from that fixture must equal the files under `tests/golden/`.
|
|
87
|
+
After a deliberate change to a renderer, regenerate them and review the
|
|
88
|
+
diff. One end-to-end test runs the real pipeline and is skipped when no
|
|
89
|
+
`tesseract` binary is on PATH.
|
|
90
|
+
|
|
91
|
+
## How it works
|
|
92
|
+
|
|
93
|
+
1. **Ink mask.** The page is thresholded on each pixel's *distance from
|
|
94
|
+
white*, `255 − min(B, G, R)`, so black and coloured ink are both caught.
|
|
95
|
+
The glyph size measured on this mask sets the working scale: a scan
|
|
96
|
+
whose print is smaller than the 300 dpi reference is enlarged (up to 4x)
|
|
97
|
+
before anything else, because Tesseract and every stroke measure need
|
|
98
|
+
the pixels. Highlighter bands pass the ink threshold too; they are
|
|
99
|
+
recognised as *bright, broad* fills — every print ink is dark, and no
|
|
100
|
+
stroke of print is as tall as a marker band — and whitened away, so OCR
|
|
101
|
+
sees paper while the letters on top of them stay.
|
|
102
|
+
2. **Straight strokes.** Morphological opening finds long horizontal and
|
|
103
|
+
vertical strokes. A stroke with clear paper on both sides is a **table
|
|
104
|
+
rule**; every other horizontal stroke is a **decoration candidate**
|
|
105
|
+
(underline or strike-through). The paper test follows the stroke's ink
|
|
106
|
+
column by column, so a slightly slanted strike-through does not pass
|
|
107
|
+
for a rule. A stroke crossed by vertical rules belongs to a table
|
|
108
|
+
lattice: it is split at the crossings, and only a piece whose glyphs
|
|
109
|
+
pass through it (a strike drawn flush onto a row boundary fuses with
|
|
110
|
+
it into one line) stays a decoration. Length is deliberately no
|
|
111
|
+
criterion — a struck-through paragraph line is as long as a rule.
|
|
112
|
+
3. **OCR.** Strikes are removed in two ways (Telea inpainting and
|
|
113
|
+
column-wise erasure, which fail on different glyphs) and Tesseract reads
|
|
114
|
+
the cleaned page as hOCR, which carries a baseline and font size per row.
|
|
115
|
+
Weak lines are cut out and read again in isolation.
|
|
116
|
+
4. **Per-word style.** Colour is the median of the *darkest* ink in the
|
|
117
|
+
word box (darkest by luminance, so the fringe where a glyph blends into
|
|
118
|
+
a marker or the paper does not vote); a decoration's colour comes from
|
|
119
|
+
the stroke's own pixels, or the glyphs a red line crosses would outvote
|
|
120
|
+
the line. Bold compares the measured stroke width (2 × area / perimeter of the
|
|
121
|
+
word's core ink) against a regular-weight model fitted to the page.
|
|
122
|
+
Decoration strokes are assigned to the words they span: through the
|
|
123
|
+
x-height it is a strike, at the baseline an underline. Struck lines are
|
|
124
|
+
re-read on both cleaned variants and the most confident reading wins.
|
|
125
|
+
5. **Graphics.** Oversized coloured lettering (logos) and ink no confident
|
|
126
|
+
word claims are cropped from the scan and embedded as PNG data URIs.
|
|
127
|
+
|
|
128
|
+
In the Markdown output, connected rules become tables (merged cells hold
|
|
129
|
+
their content in the top-left grid cell), large text becomes headings,
|
|
130
|
+
bold is `**text**`, strike-through `~~text~~`, underline `<u>text</u>`
|
|
131
|
+
and colour `<span style="color:#rrggbb">…</span>`; colour and underline
|
|
132
|
+
have no Markdown syntax, so they are inline HTML. The document HTML
|
|
133
|
+
output shares the same layout analysis and writes it the way pandoc
|
|
134
|
+
would: `<p>`, `<h1..h3>` with an id, `<table>` with a `<colgroup>`,
|
|
135
|
+
`<strong>`/`<del>`/`<u>`/`<span>` inline, and consecutive paragraphs
|
|
136
|
+
labelled `(vi)`, `(b)` or `3.` folded into `<ol start type>` lists —
|
|
137
|
+
unless the label itself is struck through, because deleted text keeps
|
|
138
|
+
its literal label.
|
|
139
|
+
|
|
140
|
+
## Limits and assumptions
|
|
141
|
+
|
|
142
|
+
* Geometry constants are set for a 300 dpi scan and scaled to the page's
|
|
143
|
+
*measured* glyph size; a scan with smaller print is enlarged internally
|
|
144
|
+
(up to 4x) first. Below roughly 8 px of x-height there is not enough
|
|
145
|
+
signal left even so, and the bold measure goes first.
|
|
146
|
+
* The page is assumed to be deskewed; a rotated page breaks the rule
|
|
147
|
+
detection.
|
|
148
|
+
* Bold is relative: a page set entirely in bold has no regular reference
|
|
149
|
+
and renders as regular. Italic is not detected.
|
|
150
|
+
* Underline and strike-through are found as straight strokes at least
|
|
151
|
+
40 px long (at 300 dpi); a wavy or hand-drawn line is missed, and a
|
|
152
|
+
heavy or doubled strike hides too much of the glyphs for the text to be
|
|
153
|
+
recovered. A marker stroke as tall as the text it covers is taken for a
|
|
154
|
+
highlight, not a strike.
|
|
155
|
+
* The HTML overlay is a *reproduction of the page*, not a document: words
|
|
156
|
+
are absolutely positioned and the reading order is Tesseract's. The
|
|
157
|
+
document HTML and Markdown outputs are the document view.
|
|
File without changes
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Commit the remaining working tree of the strikethrough-ocr repository.
|
|
3
|
+
set -euo pipefail
|
|
4
|
+
|
|
5
|
+
cd "$(dirname "$0")/.."
|
|
6
|
+
|
|
7
|
+
git add src/strikethrough_ocr/strikethrough_ocr.py
|
|
8
|
+
git commit -m "fix: add the renamed CLI module
|
|
9
|
+
|
|
10
|
+
The console script entry point already refers to
|
|
11
|
+
strikethrough_ocr.strikethrough_ocr, but the renamed module file itself
|
|
12
|
+
was never committed."
|
|
13
|
+
|
|
14
|
+
git add tests/conftest.py
|
|
15
|
+
git add tests/test_golden.py
|
|
16
|
+
git add tests/test_scan_ink.py
|
|
17
|
+
git add tests/test_scan_geometry.py
|
|
18
|
+
git add tests/test_document.py
|
|
19
|
+
git add tests/test_cli.py
|
|
20
|
+
git add tests/test_integration.py
|
|
21
|
+
git add tests/fixtures/page.overlay.html
|
|
22
|
+
# the golden files match the *.scan.* ignore pattern, so they are added explicitly
|
|
23
|
+
git add -f tests/golden/page.scan.md
|
|
24
|
+
git add -f tests/golden/page.scan.html
|
|
25
|
+
git commit -m "test: add unit tests, fixtures and golden files
|
|
26
|
+
|
|
27
|
+
A synthetic fixture page pins all three renderings: the overlay must
|
|
28
|
+
match tests/fixtures and the Markdown and document HTML must match
|
|
29
|
+
tests/golden. Unit tests cover ink analysis, stroke classification,
|
|
30
|
+
decoration assignment and the CLI; an end-to-end test skips without
|
|
31
|
+
tesseract."
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
|
|
2
|
+
These are my personal notes.
|
|
3
|
+
The ___dev/ folder is entirely for development.
|
|
4
|
+
For commits or generated documentation, never mention any planning or investigation details as described in ___dev/.
|
|
5
|
+
|
|
6
|
+
🤖 Please ignore everything below the next line.
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
install from local
|
|
12
|
+
|
|
13
|
+
uv tool install . 2>&1 | tail -3 && which strikethrough-ocr && strikethrough-ocr --version
|
|
14
|
+
|
|
15
|
+
uv tool install --force .
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
cd strikethrough-ocr
|
|
20
|
+
|
|
21
|
+
uv build
|
|
22
|
+
|
|
23
|
+
uv publish --token pypi-<your-api-token> # token from pypi.org/manage/account/token
|
|
24
|
+
|
|
25
|
+
~/.pypirc
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
|
|
3
|
+
mkdir -p ./out
|
|
4
|
+
rm -rf ./out/*
|
|
5
|
+
|
|
6
|
+
for INPUT in $(find ./pages -type f \( -name '*.png' -o -name '*.jpg' \) | sort); do
|
|
7
|
+
echo $INPUT
|
|
8
|
+
strikethrough-ocr ${INPUT} --debug --json -o ./out
|
|
9
|
+
strikethrough-ocr ${INPUT} -to html -o ./out
|
|
10
|
+
strikethrough-ocr ${INPUT} -to md -o ./out
|
|
11
|
+
done
|
|
12
|
+
|
|
13
|
+
for INPUT in $(find ./examples -type f \( -name '*.png' -o -name '*.jpg' \) | sort); do
|
|
14
|
+
echo $INPUT
|
|
15
|
+
strikethrough-ocr ${INPUT} --debug --json -o ./out
|
|
16
|
+
strikethrough-ocr ${INPUT} -to html -o ./out
|
|
17
|
+
strikethrough-ocr ${INPUT} -to md -o ./out
|
|
18
|
+
done
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "strikethrough-ocr"
|
|
3
|
+
version = "0.3.3"
|
|
4
|
+
description = "OCR for scanned pages that preserves styling: bold, colour, underline and strike-through."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.11"
|
|
7
|
+
authors = [
|
|
8
|
+
{ name = "Frank Eickhoff", email = "info@frankeickhoff.de" },
|
|
9
|
+
]
|
|
10
|
+
keywords = ["ocr", "strikethrough", "strike-through", "tesseract", "opencv", "scanned-documents", "contracts"]
|
|
11
|
+
classifiers = [
|
|
12
|
+
"Development Status :: 4 - Beta",
|
|
13
|
+
"Environment :: Console",
|
|
14
|
+
"Intended Audience :: Developers",
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"Programming Language :: Python :: 3.11",
|
|
17
|
+
"Programming Language :: Python :: 3.12",
|
|
18
|
+
"Programming Language :: Python :: 3.13",
|
|
19
|
+
"Topic :: Multimedia :: Graphics :: Capture :: Scanners",
|
|
20
|
+
"Topic :: Scientific/Engineering :: Image Recognition",
|
|
21
|
+
"Topic :: Text Processing :: Markup :: Markdown",
|
|
22
|
+
]
|
|
23
|
+
dependencies = [
|
|
24
|
+
"opencv-python-headless>=4.11",
|
|
25
|
+
"numpy>=2",
|
|
26
|
+
"pytesseract>=0.3.13",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
[project.scripts]
|
|
30
|
+
strikethrough-ocr = "strikethrough_ocr.strikethrough_ocr:main"
|
|
31
|
+
|
|
32
|
+
[dependency-groups]
|
|
33
|
+
dev = ["pytest>=8"]
|
|
34
|
+
|
|
35
|
+
[tool.pytest.ini_options]
|
|
36
|
+
testpaths = ["tests"]
|
|
37
|
+
|
|
38
|
+
[build-system]
|
|
39
|
+
requires = ["hatchling"]
|
|
40
|
+
build-backend = "hatchling.build"
|
|
41
|
+
|
|
42
|
+
[tool.hatch.build.targets.wheel]
|
|
43
|
+
packages = ["src/strikethrough_ocr"]
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""strikethrough-ocr -- OCR for scanned pages that preserves styling.
|
|
2
|
+
|
|
3
|
+
Library use:
|
|
4
|
+
|
|
5
|
+
from strikethrough_ocr import process, render_html, html_to_markdown
|
|
6
|
+
|
|
7
|
+
model = process("page.png", lang="eng") # run the OCR pipeline
|
|
8
|
+
overlay = render_html(model, "page", "page.png") # styled HTML overlay
|
|
9
|
+
md = html_to_markdown(overlay) # Markdown in reading order
|
|
10
|
+
doc = html_to_document_html(overlay) # clean document HTML
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
__version__ = "0.3.3"
|
|
14
|
+
|
|
15
|
+
from .scan import (
|
|
16
|
+
PageModel,
|
|
17
|
+
draw_debug,
|
|
18
|
+
model_to_json,
|
|
19
|
+
process,
|
|
20
|
+
render_html,
|
|
21
|
+
)
|
|
22
|
+
from .markdown import (
|
|
23
|
+
html_to_document_html,
|
|
24
|
+
html_to_markdown,
|
|
25
|
+
page_to_html,
|
|
26
|
+
page_to_markdown,
|
|
27
|
+
parse_page,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
__all__ = [
|
|
31
|
+
"__version__",
|
|
32
|
+
"PageModel",
|
|
33
|
+
"draw_debug",
|
|
34
|
+
"model_to_json",
|
|
35
|
+
"process",
|
|
36
|
+
"render_html",
|
|
37
|
+
"html_to_document_html",
|
|
38
|
+
"html_to_markdown",
|
|
39
|
+
"page_to_html",
|
|
40
|
+
"page_to_markdown",
|
|
41
|
+
"parse_page",
|
|
42
|
+
]
|