mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-01 22:19:10 -06:00
decode_ccitt reads byte-aligned Group 3 whose data has end-of-line codes but none before the first row (not as T.4 sends them) on bytes. At /K 0 with the fill ending each code on a byte, about one small picture in five is then read as other rows; at fax width almost none are, as such pages come out short and are refused. Export already said so. The optimizer, when it resamples such an image or stores it again as Group 4 from behind another filter, and redaction, when it clears pixels in one, write those rows back, and nothing said that. Black-box, PdfCraft's renderer draws such an image exactly as decode_ccitt reads it (all 42 misread of 200 seeded pictures, behind ASCII85Decode too), so the written image is what PdfCraft showed before. Refusing such data instead (a code after nearly every row but none first) would also refuse some ordinary Modified Huffman images, whose rows can meet 11 zeros by chance (0.5 to 5% of small ones in a black-box probe), so the limit is stated rather than guarded: in decode_ccitt's doc, the filters, optimize and redact READMEs, and the core.image-filters and mono-compression notes. The optimize README's and the mono-compression note's "not yet: Group 3 to Group 4 without resampling" now say a lone Group 3 image: one behind another filter is stored again as Group 4. Also from the review: docs/roadmap.md keeps #4335's status line under this PR's, rather than replacing it; the filters README's tests and the core.image-filters evidence name the Group 3 layout test; the filters crate's description names Group 3 and 4 decoding. From reviews with the pdfcraft-review skill. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01P6yUtvWGPsJpMLyh6pxSXG
pdfcraft-filters
PDF stream filters: the decoders and encoders behind /Filter and /DecodeParms.
- Layer: L0, standalone. No dependency on any other PdfCraft crate.
cosdepends on it (architecture §3 rule 3). - Licence: MIT OR Apache-2.0. Clean-room: written from ISO 32000-2 §7.4, RFC 1950/1951 (through
flate2with the pure-Rustminiz_oxidebackend), TIFF 6.0 §14 (predictor 2) and RFC 2083 §6 (PNG filters). CCITT Group 4 encoding (ITU-T T.6) is thefaxcrate's (MIT, already in the tree throughtiff), checked here by round trips through an independent decoder; CCITT decoding (T.4 and T.6) is thehayro-ccittcrate's (MIT OR Apache-2.0, already in the tree through hayro), bounded here. #![forbid(unsafe_code)]. Builds forwasm32-unknown-unknown.
API
pub enum Filter { Flate, Lzw, AsciiHex, Ascii85, RunLength, Dct, Jpx, Jbig2, CcittFax, Crypt, Unknown(String) }
Filter::from_name(b"FlateDecode" | b"Fl" | ...) -> Filter // full names and inline-image abbreviations
Filter::name(&self) -> &str; Filter::is_image_codec(&self) -> bool
pub struct Params { predictor, colors, bits_per_component, columns, early_change } // i64s; Default = 1, 1, 8, 1, 1
pub enum FilterError { Corrupt { filter, detail }, LimitExceeded(usize), Unsupported(String) }
decode(chain, data, max_output) -> Result<Vec<u8>, FilterError>
decode_counted(chain, data, max_output) -> Result<(Vec<u8>, usize /*decoded*/), (FilterError, usize /*decoded*/)>
decode_tolerant(chain, data, max_output) -> Result<(Vec<u8>, bool /*partial*/), FilterError>
decode_tolerant_counted(chain, data, max_output) -> Result<(Vec<u8>, bool /*partial*/, usize /*decoded*/), (FilterError, usize /*decoded*/)>
encode(filter, params, data) -> Result<Vec<u8>, FilterError>
encode_flate(data) -> Vec<u8> // zlib, level 6
encode_g4(data, columns, rows) -> Result<Vec<u8>, FilterError> // 1-bit rows as CCITT Group 4; columns <= MAX_G4_COLUMNS
pub struct CcittParams { k, columns, rows, end_of_line, encoded_byte_align, end_of_block, black_is_1 } // Table 11; Default = its defaults
decode_ccitt(data, ¶ms, max_rows) -> Result<Vec<u8>, FilterError> // CCITT fax as CCITTFaxDecode gives it; columns <= MAX_CCITT_COLUMNS
chainis applied first to last, as in a/Filterarray.- Image codecs (DCT, JPX, JBIG2, CCITT),
Cryptand unknown filters anywhere in a chain giveUnsupported: callers keep such data encoded. So does a chain longer thanMAX_CHAIN(8) filters, before any stage runs. Decryption belongs tocrypt. max_outputbounds every intermediate buffer (including the pre-predictor buffer) and givesLimitExceeded(max_output). This also holds when Flate retries raw deflate after a misleading zlib header, in both strict and tolerant decoding.decode_countedanddecode_tolerant_countedare for a caller charging a budget (pdfcraft_cos::DecodeBudget).max_outputbounds all stages together (each stage gets what the earlier ones left), and they give the bytes every stage that ran decoded, on success and on failure. A stage's output counts as its decoder produced it, before a predictor or a later stage shrinks it, and a failed decode pays for what it decoded first.decode_tolerantturnsCorruptinto "what was decoded so far" pluspartial = true, then runs the rest of the chain on it (predictors are applied to partial output too).LimitExceededandUnsupportedare still errors.
Behaviour
| Filter | Decode | Encode |
|---|---|---|
| FlateDecode | zlib header checked, then inflated raw: a missing or wrong Adler-32 is tolerated; no valid header → raw deflate; trailing bytes ignored; preset dictionary → Corrupt; truncated → Corrupt (partial in tolerant mode) | zlib level 6 |
| LZWDecode | 9–12-bit MSB-first codes, clear (256), EOD (257), EarlyChange 0/1; missing EOD/initial clear tolerated; a full table stops growing |
greedy, leading clear, clear-table when the table fills, EOD |
| Predictors (Flate/LZW) | 2 = TIFF (1/2/4/8/16 bpc), 10–15 = PNG with per-row tags 0–4; partial final rows decoded; bad params or tags → Corrupt | 2, 10 None, 11 Sub, 12 Up, 13 Average, 14 Paeth, 15 per-row optimum (min sum of signed residuals) |
| ASCIIHexDecode | white space ignored, odd digit padded with 0, > EOD (optional), other characters → Corrupt |
upper case, newline every 64 digits, > |
| ASCII85Decode | <~ prefix, z, white space, ~> (or bare ~, or none) EOD, final partial group; z inside a group, a 1-char final group, overflow or bad characters → Corrupt |
z for zero groups, newline every 75 characters, ~> |
| RunLengthDecode | 0–127 literal, 129–255 repeat, 128 EOD (optional); truncated run → Corrupt | runs of ≥2 repeat, literals up to 128, 128 |
| CCITTFaxDecode | not in a chain (Unsupported); decode_ccitt decodes Group 3 and Group 4 on its own, as rows of 1-bit samples each on a new byte, black 0 unless black_is_1, at most max_rows rows (fewer with /Rows), the rows before any damage, up to 1,048,576 samples a row; Group 3 rows on bytes are read by their end-of-line codes when the data starts with one and on bytes otherwise, whatever end_of_line says, so data with codes but none before the first row (not as T.4 sends them) is read on bytes, as the renderer draws it, and at k 0 may come out as other rows (pdfcraft_cos::Document::decoded_image calls it for a fax image, after the filters before it) |
encode_g4: Group 4 (/K -1) with an end-of-block mark, 0 bits coded black so /BlackIs1 false decodes the same bits; rows packed MSB first, each on a new byte, padding bits and trailing bytes ignored; up to 32,767 samples a row (the encoder counts positions in 16 bits); no rows or columns, or too wide → Unsupported, too few bytes → Corrupt |
Tests
- Unit tests per filter with hand-computed vectors (the ISO 32000-2 LZW example, RFC 1951 stored blocks, PNG/TIFF rows).
- LZW is cross-checked against
weezl(dev-dependency) in both directions for both early-change settings. tests/proptests.rs: encode→decode round trips for every encodable filter × predictor × colors × bpc × columns, two-filter chains, output-limit checks, and fuzz-style no-panic tests (random bytes, random/nonsense params, bit-flipped valid streams).ccitt.rs:decode_ccittgives back the rowsencode_g4was given (with/BlackIs1,/Rowsand a row limit), refuses bad widths, and bounds its output for any input; byte-aligned Group 3 in each layout, over 32 seeded pictures, decodes to its picture, and a byte short to fewer rows. Group 4 output decoded byhayro-ccitt(an independent decoder) matches the input bits: a text-like page, padding bits, the widest row, and property tests over bitmaps of noise and of runs; any input returns rather than panics.fuzz/: a detached cargo-fuzz crate (cargo +nightly fuzz run decode, run fromcrates/filters/).
Status
M1.1/M1.2 for the general-purpose filters are done. Not yet here: the image codecs (DCT via zune-jpeg, JPX via hayro-jpeg2000, JBIG2, DCT encode), CCITT Group 3 encoding and Flate encode levels other than 6.