Detect struck-through (deleted) text in PDFs and scanned document images.
Project description
pdf-strikethrough-detect
Detect struck-through (deleted) text in PDFs and scanned document images.
Strikethrough detection is a surprisingly unserved niche: most "redline"/diff tools assume clean
born-digital PDFs and fall apart on scans — which is the real-world case. pdf-strikethrough-detect
handles both, does the hard part (scanned images) with a tiny CPU model, and makes no cloud calls.
import pdf_strikethrough as st
# born-digital PDF — exact, no OCR
for w in st.strikethroughs_in_pdf("contract.pdf"):
print(w["page"], repr(w["chars"]), "partial" if w["partial"] else "full")
Install
pip install pdf-strikethrough-detect
Pure pip, no system binaries required: the CNN runs on ONNX Runtime (CPU) and the ~318 KB model ships inside the wheel. Extras:
pip install "pdf-strikethrough-detect[markdown]" # clean_markdown() via pymupdf4llm
pip install "pdf-strikethrough-detect[rapidocr]" # free scanned-word OCR backend (no binary)
pip install "pdf-strikethrough-detect[tesseract]" # word-level OCR (also needs the tesseract binary)
Native / born-digital PDFs — exact
In a born-digital PDF a strikethrough is a vector drawing (a line or thin rect over the text), so detection is exact ground truth — no OCR, no model, no guessing. Both vector-rule and filled-rect strikethrough styles are handled.
import pdf_strikethrough as st
for w in st.strikethroughs_in_pdf("contract.pdf"):
print(w["page"], repr(w["chars"])) # 'chars' = the struck substring
print(st.clean_markdown("contract.pdf")) # surviving text, deletions removed (needs [markdown])
Each record: {page, text, chars, char_span, partial, bbox_frac, coverage, verdict, final}.
Partial strikes (semi- of semi-monthly) are resolved to a char range. bbox_frac is in
fractions of the rendered page (rotation-aware), so it maps directly onto a rendered pixmap.
Two native detectors, both base-PyMuPDF only (no pymupdf4llm), selected by method:
st.strikethroughs_in_pdf("contract.pdf", method="vector") # stroke geometry (default) —
# precise partial-char spans
st.strikethroughs_in_pdf("contract.pdf", method="flag") # MuPDF's FZ_STEXT_STRIKEOUT signal —
# also catches font-attribute strikes
st.strikethroughs_in_pdf("contract.pdf", method="both") # union — maximum recall
Validated across domains. On 12 public redline PDFs (federal & state regulations, court
rules, procurement clauses, municipal codes, university policy; 33k struck words),
99.9–100% of vector detections are independently confirmed by MuPDF's strikeout signal,
and the flag method adds ~2% more words (font-attribute strikes and edge cases) — use
method="both" to capture them. pymupdf4llm is not used for detection at all; it is only an
optional [markdown] extra for richer layout in clean_markdown().
Any PDF — routed per page, scanned pages use OCR + CNN
import pdf_strikethrough as st
from pdf_strikethrough.ocr import rapidocr_backend
from pdf_strikethrough.scanned import ScanConfig
res = st.detect_pdf("mixed.pdf",
ocr=rapidocr_backend(), # for scanned pages
scan_config=ScanConfig.confidence_free())
struck = [w for w in res["words"] if w["final"]] # struck words (boxes, char spans)
markdown = res["markdown"] # deletions as ~~struck~~
clean = res["clean_text"] # surviving text, deletions removed
passages = res["passages"] # grouped deletion sections
detect_pdf classifies each page native-vs-scanned, runs the exact path on native pages
(native_method="vector"|"flag"|"both") and the geometry→OCR→CNN pipeline on scanned ones, and
assembles markdown / clean_text / passages for both page kinds from its own strike
decisions (so the text and the word records always agree — no dependence on an external markdown
engine). Already have an Azure Document Intelligence result? Pass di_result=... (the REST JSON
dict, an {'analyzeResult': ...} envelope, or sdk_result.as_dict()) to skip re-OCR and use
DI's word boxes.
Scanned pages with no OCR backend raise OcrRequiredError by default; pass
on_missing_ocr="skip" to skip them (with a warning in res["warnings"]) and still get
everything from the native pages. Password-protected PDFs raise EncryptedPdfError.
clean_markdown()remains a separate, higher-fidelity native-only path that borrows pymupdf4llm's layout (headings, paragraphs).detect_pdf'smarkdownis layout-plain but works uniformly on scanned pages too.
Choosing an OCR backend
The geometry + CNN carry the detection and are OCR-independent; OCR only supplies word boxes to attribute strikes to, plus a confidence prior. Benchmarked on a heavily-edited document (Azure DI as reference):
| Backend | Setup | Struck regions | Spatial agreement | Word granularity |
|---|---|---|---|---|
| Azure Document Intelligence | cloud, paid | reference | — | exact word boxes |
| RapidOCR | pip, no binary |
100% covered | ~99% | ~4× coarser (phrase-level) |
| Tesseract | needs system binary | — | — | genuine word-level |
Use ScanConfig.confidence_free() with RapidOCR (its confidences cluster near 1.0 and don't
separate struck from clean text); the default ScanConfig() is calibrated to Azure DI, whose
struck words drop to 0.43–0.94. The DI-decoupled classifier reproduces the original Azure-DI
pipeline to 99.5% (1477 vs 1484 struck words on the validation doc).
Low-level building blocks
gray = st.render_page_gray(doc[0], dpi=200) # HxW grayscale; RGB/float arrays are coerced
lines = st.strike_lines(gray, dpi=200) # OCR-free stroke geometry (strike/underline/rule)
# pass the dpi the image was rendered/scanned at
# word boxes are PAGE FRACTIONS in [0,1], origin top-left — not pixels
p = st.score_word(gray, (0.12, 0.34, 0.38, 0.36)) # CNN strike probability (0..1)
from pdf_strikethrough.ocr import Word
recs = st.detect_scanned_image(gray, [Word("foo", (0.12, 0.34, 0.38, 0.36), 0.6)])
CLI
pdf-strikethrough detect contract.pdf # native pages (scanned pages are
# skipped with a warning)
pdf-strikethrough detect scan.pdf --ocr rapidocr # include scanned pages
pdf-strikethrough detect doc.pdf --method both # max-recall native detection
pdf-strikethrough detect doc.pdf --json out.json # full struck words + passages
pdf-strikethrough detect doc.pdf --clean-text clean.txt # surviving text, deletions removed
pdf-strikethrough detect doc.pdf --markdown marked.md # deletions as ~~struck~~
How it works
- Native: merged horizontal vector strokes through a word's middle band (excludes under/over- lines); coverage ≥ 50% → struck, partials resolved to a char range.
- Scanned geometry (
lines.py): per-angle morphological opening extracts stroke fragments, collinear fragments are stitched, then strict filters (spine fill, stroke run-thickness, angle/length) separate real strikes from bold crossbars and serif-glyph chains. - CNN (
cnn.py, StrikeNet, 79k params): resolves pixel-ambiguous cases — a thin strike over an ascender-less word is pixel-identical to a glyph chain, and only a learned model tells them apart. Ships as ONNX; setPDF_STRIKETHROUGH_MODEL_DIRto use your own weights. - Attribution (
scanned.py): assigns strokes to OCR words with char spans and full/partial resolution, plus a visual-row "orphan" pass for words the detector's stroke evidence missed.
License
MIT.
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file pdf_strikethrough_detect-0.4.1.tar.gz.
File metadata
- Download URL: pdf_strikethrough_detect-0.4.1.tar.gz
- Upload date:
- Size: 345.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
712018adb2a7b274abdbe51782d132322a094255bce88951cbca95fcd7eddf4a
|
|
| MD5 |
cffb787e21cb1d575a7676a3a0cd56fa
|
|
| BLAKE2b-256 |
9ad88d41e54f9f74ec88cca5045d86ce07a4699b2b8298c375b3ec6a5055e43e
|
Provenance
The following attestation bundles were made for pdf_strikethrough_detect-0.4.1.tar.gz:
Publisher:
publish.yml on niles-liu/pdf-strikethrough-detect
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pdf_strikethrough_detect-0.4.1.tar.gz -
Subject digest:
712018adb2a7b274abdbe51782d132322a094255bce88951cbca95fcd7eddf4a - Sigstore transparency entry: 2082203390
- Sigstore integration time:
-
Permalink:
niles-liu/pdf-strikethrough-detect@5ffe65be314ac37f1843e4ee468f4650c88fc2b1 -
Branch / Tag:
refs/tags/v0.4.1 - Owner: https://github.com/niles-liu
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@5ffe65be314ac37f1843e4ee468f4650c88fc2b1 -
Trigger Event:
release
-
Statement type:
File details
Details for the file pdf_strikethrough_detect-0.4.1-py3-none-any.whl.
File metadata
- Download URL: pdf_strikethrough_detect-0.4.1-py3-none-any.whl
- Upload date:
- Size: 336.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
9bd090670da887a1acd528c801c89209c4bfe9a80fa35004f87d41e07d760c6d
|
|
| MD5 |
4bde8c391c8b83c77f5f77103ce2b1df
|
|
| BLAKE2b-256 |
a255d5c1d9b6ec56b17647db9d1dc2bf1e0654293bbbe6af491c3562f1ead06a
|
Provenance
The following attestation bundles were made for pdf_strikethrough_detect-0.4.1-py3-none-any.whl:
Publisher:
publish.yml on niles-liu/pdf-strikethrough-detect
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pdf_strikethrough_detect-0.4.1-py3-none-any.whl -
Subject digest:
9bd090670da887a1acd528c801c89209c4bfe9a80fa35004f87d41e07d760c6d - Sigstore transparency entry: 2082203397
- Sigstore integration time:
-
Permalink:
niles-liu/pdf-strikethrough-detect@5ffe65be314ac37f1843e4ee468f4650c88fc2b1 -
Branch / Tag:
refs/tags/v0.4.1 - Owner: https://github.com/niles-liu
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@5ffe65be314ac37f1843e4ee468f4650c88fc2b1 -
Trigger Event:
release
-
Statement type: