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.1.tar.gz (345.6 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.1-py3-none-any.whl (336.0 kB view details)

Uploaded Python 3

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

Hashes for pdf_strikethrough_detect-0.4.1.tar.gz
Algorithm Hash digest
SHA256 712018adb2a7b274abdbe51782d132322a094255bce88951cbca95fcd7eddf4a
MD5 cffb787e21cb1d575a7676a3a0cd56fa
BLAKE2b-256 9ad88d41e54f9f74ec88cca5045d86ce07a4699b2b8298c375b3ec6a5055e43e

See more details on using hashes here.

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

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.1-py3-none-any.whl.

File metadata

File hashes

Hashes for pdf_strikethrough_detect-0.4.1-py3-none-any.whl
Algorithm Hash digest
SHA256 9bd090670da887a1acd528c801c89209c4bfe9a80fa35004f87d41e07d760c6d
MD5 4bde8c391c8b83c77f5f77103ce2b1df
BLAKE2b-256 a255d5c1d9b6ec56b17647db9d1dc2bf1e0654293bbbe6af491c3562f1ead06a

See more details on using hashes here.

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

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