Files
pdfcraft/crates/filters
Grzegorz MartensandClaude Opus 5.5 f175beb25a M11: PDF Optimizer stores black-and-white images as CCITT Group 4 when smaller
pdfcraft_filters::encode_g4 encodes packed 1-bit rows as Group 4 (ITU-T
T.6, /K -1) through the fax crate (MIT, already in the tree via tiff),
with rows capped at 32,767 samples so its 16-bit positions can't
overflow; round trips are checked against hayro-ccitt.

The optimizer (Settings::mono, CCITT G4 by default, so Reduce File Size
too) re-encodes a 1-bit image of one colour component, or an image
mask, stored with no filter or general-purpose ones, when that is
smaller as stored. The samples decode to the same bits, so colour
space, decode array and masks stay. PDF Optimizer gets a Monochrome
Images row and doc_optimize a mono option. optimize.mono-compression
goes from planned to partial (JBIG2 and monochrome downsampling not
yet).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BfMGHQNgdq5yf5tKZHGR3z
2026-10-29 20:47:39 +01:00
..
2026-10-07 16:03:57 -04:00

pdfcraft-filters

PDF stream filters: the decoders and encoders behind /Filter and /DecodeParms.

  • Layer: L0, standalone. No dependency on any other PdfCraft crate. cos depends 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 flate2 with the pure-Rust miniz_oxide backend), TIFF 6.0 §14 (predictor 2) and RFC 2083 §6 (PNG filters). CCITT Group 4 encoding (ITU-T T.6) is the fax crate's (MIT, already in the tree through tiff), checked here by round trips through an independent decoder.
  • #![forbid(unsafe_code)]. Builds for wasm32-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
  • chain is applied first to last, as in a /Filter array.
  • Image codecs (DCT, JPX, JBIG2, CCITT), Crypt and unknown filters anywhere in a chain give Unsupported: callers keep such data encoded. So does a chain longer than MAX_CHAIN (8) filters, before any stage runs. Decryption belongs to crypt.
  • max_output bounds every intermediate buffer (including the pre-predictor buffer) and gives LimitExceeded(max_output). This also holds when Flate retries raw deflate after a misleading zlib header, in both strict and tolerant decoding.
  • decode_counted and decode_tolerant_counted are for a caller charging a budget (pdfcraft_cos::DecodeBudget). max_output bounds 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_tolerant turns Corrupt into "what was decoded so far" plus partial = true, then runs the rest of the chain on it (predictors are applied to partial output too). LimitExceeded and Unsupported are 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 here (Unsupported; renderers decode 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: Group 4 output decoded by hayro-ccitt (dev-dependency, 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 from crates/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, CCITT G3/G4 decode, DCT encode), CCITT Group 3 encoding and Flate encode levels other than 6.