| The native entry point checks the size cap and the %PDF- header, then parses inside catch_unwind and returns errors rather than panicking. | Read from the code | superdocuments/src/pdf/mod.rs:62-86 | native_pages_of_bytes checks MAX_PDF_BYTES and the header, then wraps Document::parse and markdown_pages in catch_unwind. |
| The filter chain supports FlateDecode, ASCIIHexDecode, ASCII85Decode and RunLengthDecode. Any other filter returns no data for that stream. | Read from the code | superdocuments/src/pdf/parser/decode.rs:266-300 | decode_within applies each named stage in order. LZWDecode, the image filters and any undecodable stage hit the default arm and return None. The doc comment on decode_within states this. |
| The parser has no decryption code. /Encrypt appears only in the AcroForm repair path, which skips encrypted files. | Read from the code | superdocuments/src/pdf/acroform.rs:57 | A search of superdocuments/src/pdf/parser finds no Encrypt, password or RC4/AES handling. README.md format matrix (PDF row) says there is no decryption code. The only encrypted-file test is acroform.rs:430, encrypted_pdf_is_skipped. |
| Character codes decode through the ToUnicode CMap first, then the /Differences map, then a Latin-1 fallback for single-byte codes at 0x20 and above. | Read from the code | superdocuments/src/pdf/parser/parse.rs:1814-1830 | CMap::decode looks up map then enc, and falls back to the byte as a character only when the code width is one and the code is at least 0x20. page_fonts (parse.rs:2127-2170) builds the ToUnicode map and the Differences map for every font. |
Headings are classified by font size against the body size, by bold short lines, and by numbered section prefixes, and are written as a single-level # prefix. | Read from the code | superdocuments/src/pdf/parser/parse.rs:347-358 | is_heading gates each signal to lines under 150 characters, and to under 90 for bold and numbered lines. emit_lines writes the prefix # only (parse.rs:977). No level 2 or deeper heading is produced. |
| Tables are emitted as HTML table markup with colspan for cells that run into empty columns. | Read from the code | superdocuments/src/pdf/parser/parse.rs:828-870 | emit_grid_table writes <table> (line 851), one <tr> per row and one <td> per cell, and adds colspan when the next columns are empty and the text reaches past their midpoint. |
| Reading order is the order of the content stream. The text accumulator does not sort text by position. | Read from the code | superdocuments/src/pdf/parser/parse.rs:1557-1560 | The doc comment on the accumulator says document order is the stream order, with no sort and no binary search. Columns are detected only inside table candidates (columns_from_gutters, parse.rs:560). |
| A picture painted with Do is written as an image marker at its place in the document flow. | Read from the code | superdocuments/src/pdf/parser/parse.rs:968-972 | emit_lines writes  for an image marker line. The Do operator (parse.rs:1752) places the marker where the image is painted. |
| Image listing returns JPEG and JPEG 2000 bytes unchanged, Flate or unfiltered 8-bit gray and RGB images as PNG with stored deflate blocks, and other images as raw samples. | Read from the code | superdocuments/src/pdf/images.rs:1-20, 262-300 | The module doc lists the encodings. The match on the last filter handles DCTDecode (JPEG bytes unchanged when no filter precedes it, or Flate only), JPXDecode, and the default arm, which uses encode_png only when bpc is 8 and the colour space is DeviceGray or DeviceRGB. |
| Object streams and xref streams are loaded natively, and a refused object stream is decoded only once. | Covered by a test | superdocuments/src/pdf/parser/xref_tests.rs:234 and superdocuments/src/pdf/parser/xref_tests.rs:650 | Named tests: xref_stream_with_object_stream_loads_compressed_catalog and refused_object_stream_is_decoded_once. Not run for this sheet. Also flate_xref_stream_with_png_predictor_is_undone (xref_tests.rs:245). |
| Embedded image listing agrees with poppler's pdfimages on the recorded fixtures, with zero disagreements. | Measured | superdocuments/benches/pdf-images/results-images-mixed.txt and superdocuments/benches/pdf-images/results-inherited-predictor.txt | Seven images in all: six in the mixed file and one in the inherited-predictor file, 0 disagreements in both. compare_pdfimages.py compares page, size, colour space, bits, encoding category, JPEG bytes and decoded PNG pixels. The results files carry no date or poppler version, and the run was not repeated here. |
| On a PDF whose text is rotated 15 degrees, the native parser returns the text with a character error rate of 0.000 in about 8 ms with process start. pdftotext scores 0.686 and MarkItDown 0.264. | Measured | PDF text and OCR benchmark; webfold-bench repository, tests/pdf-extractors-v1/results/SUMMARY.md | Eight PDFs from superdocuments/tests/fixtures; character error rate against each file's expected text with HTML tags removed first; median of 6 calls after 1 warm-up, on a shared 4-core machine at a load average of 12 to 30; Poppler 24.02.0, Tesseract 5.3.4 and MarkItDown 0.1.9. The Webfold figures are a superdocuments build with the PDF parser changes, run through its example programs. One fixture; the text layer is intact, so this is not a rotated scan. |
| Scanned pages are recovered through the OCR fallback: 0.023 on a blurred scan where pdftotext and MarkItDown score 0.628, and 0.000 on a plain scan where they return no text. | Measured | PDF text and OCR benchmark; webfold-bench repository, tests/pdf-extractors-v1/results/SUMMARY.md | Eight PDFs from superdocuments/tests/fixtures; character error rate against each file's expected text with HTML tags removed first; median of 6 calls after 1 warm-up, on a shared 4-core machine at a load average of 12 to 30; Poppler 24.02.0, Tesseract 5.3.4 and MarkItDown 0.1.9. The route ties Tesseract run directly on page 1, so the gain is over text extractors, not over OCR. The Webfold rows come from a build with an interim OCR page rule that was never committed, not the rule that merged. The results record no commit hash for that build. Eight small fixtures made for the repository. |
| The native parser reads the five born-digital fixtures, and the text page of the mixed file. It gets little from the two scans. | Measured | PDF text and OCR benchmark; webfold-bench repository, tests/pdf-extractors-v1/results/SUMMARY.md | Eight PDFs from superdocuments/tests/fixtures; character error rate against each file's expected text with HTML tags removed first; median of 6 calls after 1 warm-up, on a shared 4-core machine at a load average of 12 to 30; Poppler 24.02.0, Tesseract 5.3.4 and MarkItDown 0.1.9. Native-only error rates: 0.000 on the invoice, rotated page, borderless table and ASCII85 file, 0.141 on two columns (the floor, because the reference omits a sentence the PDF contains), 0.202 on the mixed file, 0.581 and 0.955 on the two scans. The 14 October run, which showed an empty native output on three born-digital files, is withdrawn: its parser did not decode ASCII85 streams. |
The webfold CLI reads PDFs with pdf and convert, with OCR flags that set environment variables. | Read from the code | webfold/src/main.rs (the pdf arm, from line 800) | The usage string is webfold pdf <file.pdf> [--no-ocr] [--chunk n] [--ocr-lang eng+chi_sim] [--ocr-max-pages n] [--ocr-max-dim px] [--ocr-timeout secs]. Each --ocr-* flag sets WEBFOLD_OCR_* for the call, and --no-ocr turns OCR off. convert (main.rs, the convert arm) also routes .pdf through pdf_to_markdown and takes --no-ocr. |
| The CLI decides OCR page by page: a page with no text, or a scan whose only text is a stamp, is OCRed. Born-digital pages keep their native text. | Read from the code | webfold/src/cli/document.rs (pdf_to_markdown, pdf_to_markdown_opts); superdocuments/src/pdf/page_profile.rs | The page rule treats a page as needing OCR when it has no alphanumeric text, or when images cover half the page or more and its text is under 0.4 alphanumeric characters per 1000 pt². webfold pdf --no-ocr starts neither pdftoppm nor tesseract and names on stderr the pages it did not read. The OCR options are still set by WEBFOLD_OCR_* variables or the --ocr-* flags. |
| arXiv PDFs convert through the fallback path on real papers | Covered by a test | webfold/src/arxiv/mod.rs (tests module); live check 2026-11-07 | With the HTML route forced to 404 against live arXiv, 1706.03762v1 (29k characters), hep-th/9901001 (20k) and 2401.04088v1 (22k) converted. The live check was a throwaway test and is not committed; the committed tests use a generated PDF and the shipped example PDF. The fallback is in Webfold's main branch. |