Skip to main content

Detect struck-through (deleted) text in PDFs and scanned document images.

Project description

pdf-strikethrough-detect

PyPI Python versions CI License: MIT

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's markdown is 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; set PDF_STRIKETHROUGH_MODEL_DIR to 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

pdf_strikethrough_detect-0.4.0.tar.gz (335.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

pdf_strikethrough_detect-0.4.0-py3-none-any.whl (332.0 kB view details)

Uploaded Python 3

File details

Details for the file pdf_strikethrough_detect-0.4.0.tar.gz.

File metadata

  • Download URL: pdf_strikethrough_detect-0.4.0.tar.gz
  • Upload date:
  • Size: 335.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for pdf_strikethrough_detect-0.4.0.tar.gz
Algorithm Hash digest
SHA256 de3a841ffe2ee83897a689ba39a8d28590ebda845a463f62d16a927c1ae22904
MD5 014bff3d04ae5e7fe51e6a15382a9363
BLAKE2b-256 02027590f9631d0194703ce36e095f7117be999197ad99b05430300013d5f10c

See more details on using hashes here.

Provenance

The following attestation bundles were made for pdf_strikethrough_detect-0.4.0.tar.gz:

Publisher: publish.yml on niles-liu/pdf-strikethrough-detect

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file pdf_strikethrough_detect-0.4.0-py3-none-any.whl.

File metadata

File hashes

Hashes for pdf_strikethrough_detect-0.4.0-py3-none-any.whl
Algorithm Hash digest
SHA256 380d50ca8b4d4bb6f798799a1db300159e44a53289109256d26b71cc599ced14
MD5 183c262f90c4c03cdeb4a1e04bcc7c12
BLAKE2b-256 74dad1d95a2506029c74989f84d326537fc6ee27952fa039eb11636674aa2cbc

See more details on using hashes here.

Provenance

The following attestation bundles were made for pdf_strikethrough_detect-0.4.0-py3-none-any.whl:

Publisher: publish.yml on niles-liu/pdf-strikethrough-detect

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page