Repository navigation
feat(pdf): extract PDF text layer with bounded stream decoding - #30
Merged
Merged
Conversation
Pure-Rust PDF support behind the pdf feature (hayro-syntax, no image decoders). Text runs are placed with the text/graphics state and rebuilt into paragraphs, font-size headings and bullet lists; fonts map via ToUnicode CMaps, simple-font encodings and Differences. Security (T7): every stream in the file is decoded under max_pdf_stream_size / max_pdf_decoded_size before hayro parses it, so object-stream bombs are caught; interpreted content is budgeted per use (form XObject reuse); indirect filters and encrypted files are refused. New builder limits max_pdf_pages, max_pdf_stream_size, max_pdf_decoded_size; warnings truncated_pdf, pdf_unmapped_font, pdf_no_text.
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.
Requested by Михайло · project thread
What changed
PDFs now convert to Markdown instead of returning
UnsupportedFormat(pdffeature; already on in the CLIall_formatsbuild and the Python/Node bindings). WASM still excludespdf.hayro-syntax0.8 withdefault-features = false(adds onlyrustc-hash+smallvec) andminiz_oxidefor bounded inflate.Tm,CTM,Tc/Tw/Tz/TL/Ts,TJkerning), then rebuilt into lines, paragraphs (gap vs. the page's most common line pitch), font-size headings, and bullet lists. Hyphenated line breaks are joined, ligatures are expanded, and form XObjects are followed.ToUnicodeCMaps (bfchar/bfrange, codespace code lengths), WinAnsi/MacRoman/Standard plus/Differencesglyph names. Type0 fonts with noToUnicodeare skipped with a warning rather than emitting CID garbage.Extraction:titlecomes from/Info /Title,stats.pagesis filled, andtrack_sourcesrecords the page per block. New warnings:truncated_pdf,pdf_unmapped_font,pdf_no_text.Limitsfields (also builder setters, CLI flags--max-pdf-*, PythonLimits(...)args, NodemaxPdf*options):max_pdf_pages(10 000),max_pdf_stream_size(64 MiB),max_pdf_decoded_size(256 MiB). PDF extraction also honourstimeoutandmax_output_sizefrom feat(limits): unified Limits with timeout, output, node and depth caps #27.UnsupportedFormat.Why
This is a roadmap initiative: PDF is the most common document format agents get, and it was the biggest format gap.
Security (new threat T8, invariant S19)
hayro-syntax has no decode limits, and it decodes object streams by itself while resolving objects. A bomb disguised as an object stream would exhaust memory before markkit sees a page. Mitigations:
streamkeyword in the raw bytes is found, its/Filterchain is read from the dictionary text (#xxescapes and abbreviations handled), and the chain is decoded with markkit's own bounded Flate, LZW, RunLength, AHx and A85 decoders. Each stage is measured, and the document is rejected past the per-stream or total cap.decoded()./Contents, cannot multiply CPU. Forms have a depth limit of 8 and are cycle-safe.qnesting is capped and CMap ranges are never expanded. The call deadline is checked per page and in the operator loop./Filter, and encrypted files, since the scan would see only ciphertext.Also fixed while merging: with only
pdf(or onlymarkdown) enabled, extraction skipped the bounded renderer, somax_output_sizeand the deadline were not applied to rendering. Covered bypdf_security::output_cap_rejects.Before / After
Before:
After, on a LibreOffice-exported PDF (Type0 fonts with ToUnicode):
On a 4 000-paragraph reportlab PDF (290 KB), output is 1.43 MB: 0.17 s, against 0.30 s for
pdftotexton the same file.Tests:
pdf_integration(19),pdf_security(23, including timeout, output cap, truncation, mutation and random-byte proptests), plus unit tests for the decoders, CMap and glyph names; CLI flag-mapping test and a Node invalid-option test for the new limits.cargo test --workspace --all-features, clippy-D warnings(all features, default, pdf-only, no features) andcheck_okfpass locally.Risk
Checklist
Limitsfields; thepdffeature's behavior changes from an error to extraction)🤖 Generated with Claude Code