fix(pdf): abort early on scanned PDFs with an OCR hint - #130
Merged
Conversation
A scanned (image-only) PDF only failed at the very end of the pipeline, after Docling had processed the whole book in technical mode, and the error blamed "Unicode sanitization" instead of telling the user to run OCR. Real user report: minutes of waiting, then an unactionable message. Probe the first 5 pages with pdftotext before picking an extractor and fail immediately with an ocrmypdf hint. Without pdftotext on PATH the probe is skipped, so nothing regresses. Also tighten the PDF chain's `if text:` checks to `text.strip()`: pypdf returns "\n\n\n..." for image-only pages, which is truthy, so extraction was silently marked successful with no real text. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Dependency Review✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.Scanned FilesNone |
16 tasks
virgiliojr94
added a commit
that referenced
this pull request
Aug 10, 2026
Nothing in the README, SKILL.md or docs/ mentioned scanned PDFs or OCR, so the one case that cannot work was the one case with no documentation. A user hit it in practice, waited through a full extraction pass, and had to guess what to do next. The extractor now stops on the first pages (#130), but the failure should not be where people learn the limit. Adds a note under the PDF extractor table and an FAQ entry, both pointing at ocrmypdf, and states that not running OCR is a deliberate choice rather than a gap. Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
virgiliojr94
added a commit
that referenced
this pull request
Aug 12, 2026
The use-case link invited accounts without stating that two already exist. The strongest evidence this project has — a book that became a survey of 300+ engineers, and a stalled run that turned into #130, reported by someone who does not write software — was one click away and unnamed. The README line now leads with what happened and keeps the invitation second. docs/usage.md gains the same, since the site serves that page to people looking for how to run it and never mentioned use cases at all. No names or companies here: crediting the fact is evidence, crediting the author is README real estate, which stays reserved for sponsors. Full credits live in the index. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
16 tasks
jimewu
added a commit
to jimewu/pi-doc-to-skill
that referenced
this pull request
Aug 17, 2026
…ack node_modules Ports the code-level fixes from virgiliojr94/book-to-skill commits b4b3733..903d102 that the fork core was missing (local core files were identical to the fork point): - extractor: expand ~ in input paths (virgiliojr94#118); skip unreadable source instead of aborting the batch (virgiliojr94#120); detect ToC in any source, not just the first (virgiliojr94#114); whitespace-separated CJK ToC headers (virgiliojr94#112) - extract: count supplementary-plane CJK in token estimate (virgiliojr94#136); stop unbalanced code fence swallowing later headings (virgiliojr94#135); count numbered headings as chapters when they carry a chapter's weight (virgiliojr94#149); report which method produced the chapter count (virgiliojr94#150) - security: harden extraction workdir against symlink/ownership (virgiliojr94#141); DOCX leaf parsers self-defend against XXE (virgiliojr94#99); scan nested chapters and report markdown left out of scope (virgiliojr94#115) - pdf: precise Roman page-number pattern, strip running headers only at page edges, apply pdftotext cleanup to pypdf/pdfminer paths too, abort early on scanned PDFs with an OCR hint (virgiliojr94#113/virgiliojr94#119/virgiliojr94#101/virgiliojr94#130) - discovery_tax: reuse the extractor's multilingual ToC detection (virgiliojr94#138) - scan: match tool-call control tokens, not the words 'tool call' (virgiliojr94#152) - deps: diagnose modules installed in an isolated environment (pipx) (virgiliojr94#151) Excludes upstream branding/output changes (banner→intro+support note, GitHub publishing feature) pending review. Keeps the fork's notes/ and indexes/ scan coverage in tools/scan_generated_skill.py. Also: - untrack node_modules (14,794 files accidentally committed in af2c1fb); add to .gitignore - fix pre-existing ruff E9/F nits (f-strings, unused pytest imports) - +16 upstream test files; suite: 499 passed
jimewu
added a commit
to jimewu/pi-doc-to-skill
that referenced
this pull request
Aug 17, 2026
…ack node_modules Ports the code-level fixes from virgiliojr94/book-to-skill commits 9c95f41..903d102 that the fork core was missing (local core files were identical to the fork point): - extractor: expand ~ in input paths (virgiliojr94#118); skip unreadable source instead of aborting the batch (virgiliojr94#120); detect ToC in any source, not just the first (virgiliojr94#114); whitespace-separated CJK ToC headers (virgiliojr94#112) - extract: count supplementary-plane CJK in token estimate (virgiliojr94#136); stop unbalanced code fence swallowing later headings (virgiliojr94#135); count numbered headings as chapters when they carry a chapter's weight (virgiliojr94#149); report which method produced the chapter count (virgiliojr94#150) - security: harden extraction workdir against symlink/ownership (virgiliojr94#141); DOCX leaf parsers self-defend against XXE (virgiliojr94#99); scan nested chapters and report markdown left out of scope (virgiliojr94#115) - pdf: precise Roman page-number pattern, strip running headers only at page edges, apply pdftotext cleanup to pypdf/pdfminer paths too, abort early on scanned PDFs with an OCR hint (virgiliojr94#113/virgiliojr94#119/virgiliojr94#101/virgiliojr94#130) - discovery_tax: reuse the extractor's multilingual ToC detection (virgiliojr94#138) - scan: match tool-call control tokens, not the words 'tool call' (virgiliojr94#152) - deps: diagnose modules installed in an isolated environment (pipx) (virgiliojr94#151) Excludes upstream branding/output changes (banner→intro+support note, GitHub publishing feature) pending review. Keeps the fork's notes/ and indexes/ scan coverage in tools/scan_generated_skill.py. Also: - untrack node_modules (14,794 files accidentally committed in abc75a8); add to .gitignore - fix pre-existing ruff E9/F nits (f-strings, unused pytest imports) - +16 upstream test files; suite: 499 passed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary (Required)
A scanned (image-only) PDF used to fail only at the very end of the pipeline — after Docling had processed the entire book in technical mode — and the error blamed "Unicode sanitization" instead of saying "this is a scan, run OCR". This probes the first 5 pages up front and fails in ~0.01s with an actionable
ocrmypdfhint.Type of change (Required)
What it does (Required)
--mode technical, then pdftotext → pypdf → pdfminer) before the finalif not text.strip()guard inextract_single_fileraised — with a message about Unicode sanitization that never mentions OCR. Root cause: there was no pre-flight check, and the guard sat at the end of the function.if text:at four call sites.extract_with_pypdfjoins per-page strings, so an image-only PDF yields"\n\n\n…"— truthy — and extraction was marked successful (method="pypdf") with no real text. Nowif text and text.strip():.Motivation & context (Required)
Reported directly by a non-technical user who ran book-to-skill on a scanned PDF: it "spent ages trying to identify it and only then said it couldn't read it", burning agent time and tokens on a file that was never going to work. Her suggestion was exactly this — bail on the first pages.
Closes #n/a — reported in a live conversation, not via an issue.
How it works (Required for anything non-trivial)
looks_image_only()shells out topdftotext -f 1 -l 5and returns True when those pages produce no non-whitespace text.extract_single_filecalls it as the first thing in the.pdfbranch, before the extractor is chosen, so Docling never starts on a scan.Deliberately conservative:
ocrmypdf.pdftotexton PATH the probe returns False and the normal chain (plus the unchanged final empty-text guard) runs exactly as before. Same for a timeout or any subprocess error.Known limit, accepted: a book whose first 5 pages are images but whose body is real text would now be rejected. I could not construct a realistic instance of that, and the trade is a fast honest failure against minutes of work ending in a misleading message.
Evidence (Required)
TestLooksImageOnly— 4 cases: whitespace-only first pages ⇒ True and the probe is capped at-l 5; real text ⇒ False; nopdftotexton PATH ⇒ False (probe skipped); and an end-to-endextract_single_fileon a.pdfraisingExtractionErrorwhose message contains both "scanned" and "ocrmypdf".text.pdf(8 pages of text) andscan.pdf(8 pages of drawn rectangles, zero text operators) — and ran the real extraction path on both.Before this change, the same
scan.pdfreached the end of the chain and raisedExtracted text from scan.pdf contained no visible content after Unicode sanitization.— and in--mode technicalonly after Docling had walked every page.Checklist (Required — all must be checked)
masterand has no merge conflictspytest -qis green on a clean checkoutruff check .is cleanpython3 tools/validate_skill.py SKILL.mdpasses (ifSKILL.mdchanged) — n/a,SKILL.mduntouchedfix:,feat:,docs:… — it becomes the changelog entry; do NOT editCHANGELOG.mdby hand)SKILL.mdbloat without justificationNotes for reviewers (Optional)
The
text.strip()tightening at the four call sites is strictly a second line of defense — with the probe in place it should be unreachable for scans — butif text:on a whitespace string was wrong on its own terms, so I fixed it rather than leaving a latent silent-success path for any future extractor that returns blank pages.