diff --git a/crates/pdf/src/ccitt.rs b/crates/pdf/src/ccitt.rs new file mode 100644 index 00000000..ebd4cad7 --- /dev/null +++ b/crates/pdf/src/ccitt.rs @@ -0,0 +1,798 @@ +//! `CCITTFaxDecode` (ISO 32000-1 §7.4.6): ITU-T T.4 Group 3 one-dimensional (Modified +//! Huffman) and two-dimensional (Modified READ) coding and ITU-T T.6 Group 4 (Modified Modified +//! READ), implemented from the ITU-T recommendations. The result is 1-bit rows, `0` black +//! unless `BlackIs1`. + +use std::collections::HashMap; +use std::sync::OnceLock; + +/// The decoding parameters (`/DecodeParms`). +#[derive(Clone, Debug, PartialEq)] +pub struct Params { + /// `< 0` Group 4, `0` Group 3 1-D, `> 0` Group 3 mixed 1-D / 2-D. + pub k: i64, + pub end_of_line: bool, + pub byte_align: bool, + pub columns: usize, + /// `0`: as many rows as the data holds. + pub rows: usize, + pub end_of_block: bool, + pub black_is_1: bool, +} + +impl Default for Params { + fn default() -> Self { + Params { k: 0, end_of_line: false, byte_align: false, columns: 1728, rows: 0, end_of_block: true, black_is_1: false } + } +} + +/// White terminating codes (T.4 Table 2), run lengths 0–63, as (code, length). +const WHITE_TERM: [(u16, u8); 64] = [ + (0b00110101, 8), + (0b000111, 6), + (0b0111, 4), + (0b1000, 4), + (0b1011, 4), + (0b1100, 4), + (0b1110, 4), + (0b1111, 4), + (0b10011, 5), + (0b10100, 5), + (0b00111, 5), + (0b01000, 5), + (0b001000, 6), + (0b000011, 6), + (0b110100, 6), + (0b110101, 6), + (0b101010, 6), + (0b101011, 6), + (0b0100111, 7), + (0b0001100, 7), + (0b0001000, 7), + (0b0010111, 7), + (0b0000011, 7), + (0b0000100, 7), + (0b0101000, 7), + (0b0101011, 7), + (0b0010011, 7), + (0b0100100, 7), + (0b0011000, 7), + (0b00000010, 8), + (0b00000011, 8), + (0b00011010, 8), + (0b00011011, 8), + (0b00010010, 8), + (0b00010011, 8), + (0b00010100, 8), + (0b00010101, 8), + (0b00010110, 8), + (0b00010111, 8), + (0b00101000, 8), + (0b00101001, 8), + (0b00101010, 8), + (0b00101011, 8), + (0b00101100, 8), + (0b00101101, 8), + (0b00000100, 8), + (0b00000101, 8), + (0b00001010, 8), + (0b00001011, 8), + (0b01010010, 8), + (0b01010011, 8), + (0b01010100, 8), + (0b01010101, 8), + (0b00100100, 8), + (0b00100101, 8), + (0b01011000, 8), + (0b01011001, 8), + (0b01011010, 8), + (0b01011011, 8), + (0b01001010, 8), + (0b01001011, 8), + (0b00110010, 8), + (0b00110011, 8), + (0b00110100, 8), +]; + +/// White make-up codes (T.4 Table 3), run lengths 64, 128, … 1728. +const WHITE_MAKEUP: [(u16, u8); 27] = [ + (0b11011, 5), + (0b10010, 5), + (0b010111, 6), + (0b0110111, 7), + (0b00110110, 8), + (0b00110111, 8), + (0b01100100, 8), + (0b01100101, 8), + (0b01101000, 8), + (0b01100111, 8), + (0b011001100, 9), + (0b011001101, 9), + (0b011010010, 9), + (0b011010011, 9), + (0b011010100, 9), + (0b011010101, 9), + (0b011010110, 9), + (0b011010111, 9), + (0b011011000, 9), + (0b011011001, 9), + (0b011011010, 9), + (0b011011011, 9), + (0b010011000, 9), + (0b010011001, 9), + (0b010011010, 9), + (0b011000, 6), + (0b010011011, 9), +]; + +/// Black terminating codes (T.4 Table 2), run lengths 0–63. +const BLACK_TERM: [(u16, u8); 64] = [ + (0b0000110111, 10), + (0b010, 3), + (0b11, 2), + (0b10, 2), + (0b011, 3), + (0b0011, 4), + (0b0010, 4), + (0b00011, 5), + (0b000101, 6), + (0b000100, 6), + (0b0000100, 7), + (0b0000101, 7), + (0b0000111, 7), + (0b00000100, 8), + (0b00000111, 8), + (0b000011000, 9), + (0b0000010111, 10), + (0b0000011000, 10), + (0b0000001000, 10), + (0b00001100111, 11), + (0b00001101000, 11), + (0b00001101100, 11), + (0b00000110111, 11), + (0b00000101000, 11), + (0b00000010111, 11), + (0b00000011000, 11), + (0b000011001010, 12), + (0b000011001011, 12), + (0b000011001100, 12), + (0b000011001101, 12), + (0b000001101000, 12), + (0b000001101001, 12), + (0b000001101010, 12), + (0b000001101011, 12), + (0b000011010010, 12), + (0b000011010011, 12), + (0b000011010100, 12), + (0b000011010101, 12), + (0b000011010110, 12), + (0b000011010111, 12), + (0b000001101100, 12), + (0b000001101101, 12), + (0b000011011010, 12), + (0b000011011011, 12), + (0b000001010100, 12), + (0b000001010101, 12), + (0b000001010110, 12), + (0b000001010111, 12), + (0b000001100100, 12), + (0b000001100101, 12), + (0b000001010010, 12), + (0b000001010011, 12), + (0b000000100100, 12), + (0b000000110111, 12), + (0b000000111000, 12), + (0b000000100111, 12), + (0b000000101000, 12), + (0b000001011000, 12), + (0b000001011001, 12), + (0b000000101011, 12), + (0b000000101100, 12), + (0b000001011010, 12), + (0b000001100110, 12), + (0b000001100111, 12), +]; + +/// Black make-up codes (T.4 Table 3), run lengths 64, 128, … 1728. +const BLACK_MAKEUP: [(u16, u8); 27] = [ + (0b0000001111, 10), + (0b000011001000, 12), + (0b000011001001, 12), + (0b000001011011, 12), + (0b000000110011, 12), + (0b000000110100, 12), + (0b000000110101, 12), + (0b0000001101100, 13), + (0b0000001101101, 13), + (0b0000001001010, 13), + (0b0000001001011, 13), + (0b0000001001100, 13), + (0b0000001001101, 13), + (0b0000001110010, 13), + (0b0000001110011, 13), + (0b0000001110100, 13), + (0b0000001110101, 13), + (0b0000001110110, 13), + (0b0000001110111, 13), + (0b0000001010010, 13), + (0b0000001010011, 13), + (0b0000001010100, 13), + (0b0000001010101, 13), + (0b0000001011010, 13), + (0b0000001011011, 13), + (0b0000001100100, 13), + (0b0000001100101, 13), +]; + +/// Extended make-up codes for both colours (T.4 Table 3a), run lengths 1792, 1856, … 2560. +const EXT_MAKEUP: [(u16, u8); 13] = [ + (0b00000001000, 11), + (0b00000001100, 11), + (0b00000001101, 11), + (0b000000010010, 12), + (0b000000010011, 12), + (0b000000010100, 12), + (0b000000010101, 12), + (0b000000010110, 12), + (0b000000010111, 12), + (0b000000011100, 12), + (0b000000011101, 12), + (0b000000011110, 12), + (0b000000011111, 12), +]; + +/// A colour's code table: (length, code) → run length. +type Table = HashMap<(u8, u16), u32>; + +fn table(term: &[(u16, u8); 64], makeup: &[(u16, u8); 27]) -> Table { + let mut t = HashMap::new(); + for (run, (code, len)) in term.iter().enumerate() { + t.insert((*len, *code), run as u32); + } + for (i, (code, len)) in makeup.iter().enumerate() { + t.insert((*len, *code), 64 * (i as u32 + 1)); + } + for (i, (code, len)) in EXT_MAKEUP.iter().enumerate() { + t.insert((*len, *code), 1792 + 64 * i as u32); + } + t +} + +fn tables() -> &'static (Table, Table) { + static T: OnceLock<(Table, Table)> = OnceLock::new(); + T.get_or_init(|| (table(&WHITE_TERM, &WHITE_MAKEUP), table(&BLACK_TERM, &BLACK_MAKEUP))) +} + +/// The code (code, length) for a run of `run` (≥ 0, any length) as make-up codes then a +/// terminating code (used by the test encoder). +#[cfg(test)] +pub(crate) fn run_codes(white: bool, mut run: usize) -> Vec<(u16, u8)> { + let (term, makeup) = if white { (&WHITE_TERM, &WHITE_MAKEUP) } else { (&BLACK_TERM, &BLACK_MAKEUP) }; + let mut out = vec![]; + while run >= 2560 { + out.push(EXT_MAKEUP[12]); + run -= 2560; + } + if run >= 1792 { + out.push(EXT_MAKEUP[(run - 1792) / 64]); + run -= 1792 + (run - 1792) / 64 * 64; + } else if run >= 64 { + out.push(makeup[run / 64 - 1]); + run %= 64; + } + out.push(term[run]); + out +} + +struct Reader<'a> { + data: &'a [u8], + bit: usize, +} + +impl Reader<'_> { + fn bit_at(&self, i: usize) -> u16 { + self.data.get(i / 8).map(|b| ((b >> (7 - i % 8)) & 1) as u16).unwrap_or(0) + } + + fn peek(&self, n: usize) -> u16 { + (0..n).fold(0, |v, k| (v << 1) | self.bit_at(self.bit + k)) + } + + fn eof(&self) -> bool { + self.bit >= self.data.len() * 8 + } + + fn take(&mut self, n: usize) -> u16 { + let v = self.peek(n); + self.bit += n; + v + } + + fn align(&mut self) { + self.bit = self.bit.div_ceil(8) * 8; + } + + /// An EOL (eleven or more 0 bits then 1) at the read position: consume it. + fn eol(&mut self) -> bool { + let mut zeros = 0; + while zeros < 64 && self.bit + zeros < self.data.len() * 8 && self.bit_at(self.bit + zeros) == 0 { + zeros += 1; + } + if zeros >= 11 && self.bit + zeros < self.data.len() * 8 { + self.bit += zeros + 1; + return true; + } + false + } + + /// One run (make-up codes then a terminating code), `None` on an invalid code. + fn run(&mut self, t: &Table) -> Option { + let mut total = 0u32; + loop { + if self.eof() { + return None; + } + let mut found = None; + for len in 2..=13u8 { + if let Some(r) = t.get(&(len, self.peek(len as usize))) { + found = Some((len, *r)); + break; + } + } + let (len, r) = found?; + self.bit += len as usize; + total = total.saturating_add(r); + if r < 64 { + return Some(total); + } + } + } +} + +/// 2-D mode codes (T.4 Table 4). +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum Mode { + Pass, + Horizontal, + Vertical(i64), + Eol, +} + +fn mode(r: &mut Reader) -> Option { + if r.eof() { + return None; + } + let m = if r.peek(1) == 1 { + r.bit += 1; + Mode::Vertical(0) + } else if r.peek(3) == 0b011 { + r.bit += 3; + Mode::Vertical(1) + } else if r.peek(3) == 0b010 { + r.bit += 3; + Mode::Vertical(-1) + } else if r.peek(3) == 0b001 { + r.bit += 3; + Mode::Horizontal + } else if r.peek(4) == 0b0001 { + r.bit += 4; + Mode::Pass + } else if r.peek(6) == 0b000011 { + r.bit += 6; + Mode::Vertical(2) + } else if r.peek(6) == 0b000010 { + r.bit += 6; + Mode::Vertical(-2) + } else if r.peek(7) == 0b0000011 { + r.bit += 7; + Mode::Vertical(3) + } else if r.peek(7) == 0b0000010 { + r.bit += 7; + Mode::Vertical(-3) + } else if r.eol() { + Mode::Eol + } else { + return None; + }; + Some(m) +} + +/// The first changing element of `refl` after `a0` whose colour is the opposite of `white` +/// (index parity: even indices start black runs), and the one after it. +fn b1b2(refl: &[usize], a0: i64, white: bool, columns: usize) -> (usize, usize) { + let want = if white { 0 } else { 1 }; + let mut i = 0; + while i < refl.len() && (refl[i] as i64 <= a0 || i % 2 != want) { + i += 1; + } + let b1 = refl.get(i).copied().unwrap_or(columns); + let b2 = refl.get(i + 1).copied().unwrap_or(columns); + (b1, b2) +} + +/// Decode a 1-D (Modified Huffman) row into changing elements. +fn row_1d(r: &mut Reader, columns: usize) -> Option> { + let (wt, bt) = tables(); + let mut changes = vec![]; + let mut a0 = 0usize; + let mut white = true; + while a0 < columns { + let run = r.run(if white { wt } else { bt })? as usize; + a0 = (a0 + run).min(columns); + changes.push(a0); + white = !white; + } + Some(changes) +} + +/// Decode a 2-D row against the reference row's changing elements. `Err(true)` at an EOL. +fn row_2d(r: &mut Reader, refl: &[usize], columns: usize) -> Result, bool> { + let (wt, bt) = tables(); + let mut changes: Vec = vec![]; + let mut a0: i64 = -1; + let mut white = true; + let mut guard = 0; + while a0 < columns as i64 { + guard += 1; + if guard > 4 * columns + 16 { + return Err(false); + } + let (b1, b2) = b1b2(refl, a0, white, columns); + match mode(r).ok_or(false)? { + Mode::Pass => { + // The run continues under b2 (no change in the coding line). + a0 = b2 as i64; + } + Mode::Horizontal => { + let start = a0.max(0) as usize; + let r1 = r.run(if white { wt } else { bt }).ok_or(false)? as usize; + let r2 = r.run(if white { bt } else { wt }).ok_or(false)? as usize; + let a1 = (start + r1).min(columns); + let a2 = (a1 + r2).min(columns); + changes.push(a1); + changes.push(a2); + a0 = a2 as i64; + } + Mode::Vertical(d) => { + let a1 = (b1 as i64 + d).clamp(0, columns as i64); + if a1 < a0 { + return Err(false); + } + changes.push(a1 as usize); + a0 = a1; + white = !white; + } + Mode::Eol => return Err(true), + } + } + Ok(changes) +} + +/// Changing elements → packed bits (`black` bit value for black pixels). +fn pack(changes: &[usize], columns: usize, black_is_1: bool, out: &mut Vec) { + let row_bytes = columns.div_ceil(8); + let base = out.len(); + out.resize(base + row_bytes, if black_is_1 { 0 } else { 0xFF }); + let mut x = 0usize; + let mut white = true; + for &c in changes.iter().chain(std::iter::once(&columns)) { + let c = c.min(columns); + if !white { + for p in x..c { + let byte = &mut out[base + p / 8]; + let m = 0x80u8 >> (p % 8); + if black_is_1 { *byte |= m } else { *byte &= !m } + } + } + x = x.max(c); + white = !white; + if x >= columns { + break; + } + } +} + +/// Decode CCITT fax data. Damaged data ends the image at the last good row (rows already +/// decoded are kept); `None` only when no row decodes. +pub fn decode(data: &[u8], p: &Params) -> Option> { + let columns = p.columns.clamp(1, 1 << 16); + let max_rows = if p.rows > 0 { p.rows.min(1 << 20) } else { 1 << 20 }; + let mut r = Reader { data, bit: 0 }; + let mut out = vec![]; + let mut refl: Vec = vec![]; + let mut rows = 0; + while rows < max_rows && !r.eof() { + let line: Option> = if p.k < 0 { + if p.byte_align { + r.align(); + } + // An error is EOFB (two EOLs) or damage. + row_2d(&mut r, &refl, columns).ok() + } else { + // Group 3: optional fill and EOL before each row, then a tag bit when K > 0. + let had_eol = r.eol(); + if !had_eol && p.byte_align { + r.align(); + r.eol(); + } + // RTC: six consecutive EOLs (the first already consumed). + if had_eol && p.end_of_block { + let save = r.bit; + if r.eol() { + break; + } + r.bit = save; + } + if r.eof() { + break; + } + let two_d = p.k > 0 && r.take(1) == 0; + if two_d { row_2d(&mut r, &refl, columns).ok() } else { row_1d(&mut r, columns) } + }; + let Some(line) = line else { break }; + pack(&line, columns, p.black_is_1, &mut out); + refl = line; + rows += 1; + } + // Rows missing from damaged or short data stay white. + if p.rows > 0 && rows > 0 && rows < p.rows { + let row_bytes = columns.div_ceil(8); + out.resize(p.rows.min(1 << 20) * row_bytes, if p.black_is_1 { 0 } else { 0xFF }); + } + (rows > 0).then_some(out) +} + +#[cfg(test)] +pub(crate) mod tests { + use super::*; + + /// A bit writer for the test encoder. + #[derive(Default)] + pub(crate) struct W { + pub bytes: Vec, + n: usize, + } + + impl W { + pub fn put(&mut self, code: u16, len: u8) { + for k in (0..len).rev() { + if self.n.is_multiple_of(8) { + self.bytes.push(0); + } + if (code >> k) & 1 == 1 { + *self.bytes.last_mut().unwrap() |= 0x80 >> (self.n % 8); + } + self.n += 1; + } + } + fn run(&mut self, white: bool, run: usize) { + for (c, l) in run_codes(white, run) { + self.put(c, l); + } + } + } + + /// Changing elements of a row of pixels (`true` = black). + fn changes(row: &[bool]) -> Vec { + let mut out = vec![]; + let mut cur = false; + for (i, &b) in row.iter().enumerate() { + if b != cur { + out.push(i); + cur = b; + } + } + out + } + + fn encode_1d(w: &mut W, row: &[bool]) { + let cols = row.len(); + let mut ch = changes(row); + ch.push(cols); + let mut a0 = 0; + let mut white = true; + for c in ch { + w.run(white, c - a0); + a0 = c; + white = !white; + if a0 >= cols { + break; + } + } + } + + fn encode_2d(w: &mut W, row: &[bool], refl: &[usize]) { + let cols = row.len(); + let cur = changes(row); + let mut a0: i64 = -1; + let mut white = true; + let next = |a0: i64| cur.iter().copied().find(|&c| c as i64 > a0).unwrap_or(cols); + while a0 < cols as i64 { + let a1 = next(a0); + let (b1, b2) = b1b2(refl, a0, white, cols); + if b2 < a1 { + w.put(0b0001, 4); + a0 = b2 as i64; + } else if (a1 as i64 - b1 as i64).abs() <= 3 { + let d = a1 as i64 - b1 as i64; + let (c, l) = match d { + 0 => (1, 1), + 1 => (0b011, 3), + -1 => (0b010, 3), + 2 => (0b000011, 6), + -2 => (0b000010, 6), + 3 => (0b0000011, 7), + _ => (0b0000010, 7), + }; + w.put(c, l); + a0 = a1 as i64; + white = !white; + } else { + let a2 = next(a1 as i64); + w.put(0b001, 3); + let start = a0.max(0) as usize; + w.run(white, a1 - start); + w.run(!white, a2 - a1); + a0 = a2 as i64; + } + } + } + + pub(crate) fn encode(img: &[Vec], k: i64) -> Vec { + let mut w = W::default(); + let mut refl: Vec = vec![]; + for (i, row) in img.iter().enumerate() { + match k { + k if k < 0 => encode_2d(&mut w, row, &refl), + 0 => { + w.put(1, 12); + encode_1d(&mut w, row); + } + _ => { + w.put(1, 12); + if i as i64 % k == 0 { + w.put(1, 1); + encode_1d(&mut w, row); + } else { + w.put(0, 1); + encode_2d(&mut w, row, &refl); + } + } + } + refl = changes(row); + } + if k < 0 { + w.put(1, 12); + w.put(1, 12); + } + w.bytes + } + + pub(crate) fn pattern(cols: usize, rows: usize, seed: u32) -> Vec> { + let mut s = seed; + let mut rnd = || { + s = s.wrapping_mul(1_103_515_245).wrapping_add(12345); + (s >> 16) & 0x7FFF + }; + let mut img = vec![]; + let mut prev = vec![false; cols]; + for y in 0..rows { + let mut row = prev.clone(); + // Mostly like the row above (2-D coding's case), with random edits and runs. + for _ in 0..(rnd() % 5) { + let a = rnd() as usize % cols; + let len = 1 + rnd() as usize % (cols / 3).max(1); + let v = rnd() % 2 == 0; + for p in row.iter_mut().skip(a).take(len) { + *p = v; + } + } + if y % 7 == 3 { + row.iter_mut().for_each(|p| *p = rnd() % 3 == 0); + } + img.push(row.clone()); + prev = row; + } + img + } + + fn unpack(data: &[u8], cols: usize, rows: usize) -> Vec> { + let rb = cols.div_ceil(8); + (0..rows).map(|y| (0..cols).map(|x| data[y * rb + x / 8] & (0x80 >> (x % 8)) == 0).collect()).collect() + } + + #[test] + fn code_tables_are_prefix_free() { + for (white, term, makeup) in [(true, &WHITE_TERM, &WHITE_MAKEUP), (false, &BLACK_TERM, &BLACK_MAKEUP)] { + let mut all: Vec<(u16, u8)> = term.iter().chain(makeup.iter()).chain(EXT_MAKEUP.iter()).copied().collect(); + all.push((1, 12)); // EOL + let mut kraft = 0.0; + for (i, a) in all.iter().enumerate() { + kraft += 0.5f64.powi(a.1 as i32); + for (j, b) in all.iter().enumerate() { + if i != j && a.1 <= b.1 { + assert_ne!(b.0 >> (b.1 - a.1), a.0, "white {white}: {a:?} is a prefix of {b:?}"); + } + } + } + assert!(kraft <= 1.0, "white {white}: Kraft sum {kraft}"); + } + } + + #[test] + fn round_trips_group_3_and_group_4() { + for (cols, rows, seed) in [(1, 3, 1), (8, 8, 2), (37, 29, 3), (300, 40, 4), (2600, 6, 5)] { + let img = pattern(cols, rows, seed); + for k in [-1, 0, 1, 3] { + let data = encode(&img, k); + let p = Params { k, columns: cols, rows, ..Params::default() }; + let out = decode(&data, &p).unwrap_or_else(|| panic!("{cols}×{rows} K {k}: no rows")); + assert_eq!(unpack(&out, cols, rows), img, "{cols}×{rows} K {k}"); + // Rows unknown: stop at the end of the data / EOFB. + let out = decode(&data, &Params { rows: 0, ..p.clone() }).unwrap(); + assert_eq!(unpack(&out, cols, rows), img, "{cols}×{rows} K {k}, no /Rows"); + // BlackIs1 inverts. + let inv = decode(&data, &Params { black_is_1: true, ..p }).unwrap(); + assert!(inv.iter().zip(&decode(&data, &Params { k, columns: cols, rows, ..Params::default() }).unwrap()).all(|(a, b)| a ^ b == 0xFF)); + } + } + } + + fn hex(s: &str) -> Vec { + (0..s.len()).step_by(2).map(|i| u8::from_str_radix(&s[i..i + 2], 16).unwrap()).collect() + } + + /// Data from an independent encoder (libtiff's `tiffcp -c g4 / g3:1d / g3:2d / g3:1d:fill`, + /// run once as an external fixture generator) for a 61×23 generated image (1 = black). + #[test] + fn decodes_an_independent_encoders_output() { + let raw = hex( + "0000000000000008380000000000000042d05803696223a87fc00003697fffe87f00007ffd7fffe87f00007ffd7fffe87f00007fffffffe87f00007ffffffff85cc00128608c10505cc001f860007ff0fffffffffffffff8fffffffffffdfff8fffffffffffffff8f8040003fffffff84140e4180f5002d04140e4180f5002d05ffdfc180f5000005ffdfc180f5000005ffdfc180f5000005ffdf000000ff800270ca82855a00d78ffecf82855a00c00ffecf82855a00c00", + ); + let cases = [ + ( + -1, + "52f3408da23e4747111f34c8f91d17447cc2308c4082588c447fe2223e11ac13ffff88fe3943845f08a72872acf8541f0ac0412ff11e22108e844445c82b0f8e4e6555608e223a12f17408fe58e50e51ca1c104bffffffc8e150ffc447ffffffffc21116449b29cd76088e88e8e223a3688e88e88f8455191d11d8420f88ffffc4e06ffffffc004004", + ), + ( + 0, + "0014b4002f2c8008ead0f8eb10fd31f1d3a1f850a21d0e8004710463e3a7438103a0011c75820e870207400238eb041d0e040e800471d61a43a0011c7586a0008e879f0d3a1d5f8a3c5887438008e879f0cafd028e0026a0b40026a0ac3838009a82d0009a9cd540d20023ac43ae9d67ec743a3a1f1d0e0023ac43ae9d67ec743a3a1f1d0e0023a1c28e3cfd8e87558008e870a38f3f63a1d560023a1c28e3cfd8e87558008e870a39a8126002e9ebdd0e875887568743a1f1d531d0ec004d428fb9e21d5a1d0e87c754fc004d428fb9e21d5a1d0e87c754fc", + ), + ( + 4, + "001a5a001179800c75687c75887e98f8e9d0fc28510e87400288c447fe2223c00638eb041d0e040e8005ff800c71d61a43a00171800c743cf869d0eafc51e2c43a1c005fc47888423800cd416800520ac3c0066a0b400293995540063ac43ae9d67ec743a3a1f1d0e002ffffff800c743851c79fb1d0eab0017fff0018e870a38f3f63a1d56002f84222c893001ba7af743a1d621d5a1d0e87c754c743b0010420f88ffffc4e06e003350a3ee7887568743a1f1d53f0", + ), + ( + 0, + "00014b4001796400011d5a1f1d621fa63e3a743f0a1443a1d000011c4118f8e9d0e040e800011c75820e87020740011c75820e87020740011c758690e800011c7586a000011d0f3e1a743abf1478b10e8700011d0f3e195fa051c0013505a001350561c1c0013505a0013539aa81a400011d621d74eb3f63a1d1d0f8e870011d621d74eb3f63a1d1d0f8e870011d0e1471e7ec743aac00011d0e1471e7ec743aac00011d0e1471e7ec743aac00011d0e14735024c00174f5ee8743ac43ab43a1d0f8ea98e8760001350a3ee7887568743a1f1d53f001350a3ee7887568743a1f1d53f0", + ), + ]; + for (i, (k, data)) in cases.iter().enumerate() { + let p = Params { k: *k, columns: 61, rows: 23, black_is_1: true, end_of_line: *k >= 0, ..Params::default() }; + let out = decode(&hex(data), &p).unwrap(); + assert_eq!(out, raw, "case {i} (K {k})"); + let out = decode(&hex(data), &Params { rows: 0, ..p }).unwrap(); + assert_eq!(out, raw, "case {i} (K {k}), no /Rows"); + } + } + + #[test] + fn damaged_data_keeps_the_good_rows() { + let img = pattern(64, 20, 9); + let data = encode(&img, -1); + for cut in [0, 1, data.len() / 3, data.len() - 3] { + let out = decode(&data[..cut], &Params { k: -1, columns: 64, rows: 20, ..Params::default() }); + if let Some(out) = out { + assert_eq!(out.len(), 20 * 8); + } + } + let mut garbage = data.clone(); + for (i, b) in garbage.iter_mut().enumerate() { + *b ^= (i * 37) as u8; + } + for k in [-1, 0, 2] { + let _ = decode(&garbage, &Params { k, columns: 64, rows: 20, ..Params::default() }); + let _ = decode(&garbage, &Params { k, columns: 1, rows: 0, ..Params::default() }); + } + } +} diff --git a/crates/pdf/src/image.rs b/crates/pdf/src/image.rs index 0f0a481d..5e6c49a5 100644 --- a/crates/pdf/src/image.rs +++ b/crates/pdf/src/image.rs @@ -100,7 +100,7 @@ fn samples(file: &File, d: &Dict, raw: &[u8]) -> Result<(Vec, Option) let jpeg = decode_stream(file, &d2, raw).ok_or("image (filters)")?; jpeg_decode(&jpeg).map(|(b, n)| (b, Some(n))) } - Some(f @ ("JPXDecode" | "CCITTFaxDecode" | "JBIG2Decode" | "CCF")) => Err(format!("image ({f})")), + Some(f @ ("JPXDecode" | "JBIG2Decode")) => Err(format!("image ({f})")), _ => decode_stream(file, d, raw).map(|b| (b, None)).ok_or_else(|| "image (filters)".to_string()), } } diff --git a/crates/pdf/src/lib.rs b/crates/pdf/src/lib.rs index 985a7083..3db854fc 100644 --- a/crates/pdf/src/lib.rs +++ b/crates/pdf/src/lib.rs @@ -8,6 +8,7 @@ //! (Illustrator layers), with content outside them gathered into groups of its own. mod build; +mod ccitt; mod cff; mod color; mod encoding; @@ -196,4 +197,6 @@ mod tests; #[cfg(test)] mod tests_groups; #[cfg(test)] +mod tests_import; +#[cfg(test)] mod tests_shading; diff --git a/crates/pdf/src/object.rs b/crates/pdf/src/object.rs index e24f6ec8..ffd9f9bc 100644 --- a/crates/pdf/src/object.rs +++ b/crates/pdf/src/object.rs @@ -328,6 +328,7 @@ pub fn decode_stream(file: &File, d: &Dict, raw: &[u8]) -> Option> { lzw(&data, parms.get(i).and_then(|p| p.as_ref()).and_then(|p| p.get("EarlyChange")).and_then(Obj::num).unwrap_or(1.0) != 0.0) } "RunLengthDecode" | "RL" => run_length(&data), + "CCITTFaxDecode" | "CCF" => crate::ccitt::decode(&data, &ccitt_params(file, parms.get(i).and_then(|p| p.as_ref())))?, _ => return None, }; if let Some(Some(p)) = parms.get(i) @@ -339,6 +340,24 @@ pub fn decode_stream(file: &File, d: &Dict, raw: &[u8]) -> Option> { Some(data) } +/// `/DecodeParms` of a `CCITTFaxDecode` filter. +fn ccitt_params(file: &File, d: Option<&Dict>) -> crate::ccitt::Params { + let mut p = crate::ccitt::Params::default(); + let Some(d) = d else { return p }; + let flag = |k: &str, default: bool| match file.get(d, k) { + Some(Obj::Bool(b)) => *b, + _ => default, + }; + p.k = file.get_num(d, "K").unwrap_or(0.0) as i64; + p.columns = file.get_num(d, "Columns").map(|c| c.max(1.0) as usize).unwrap_or(1728); + p.rows = file.get_num(d, "Rows").map(|r| r.max(0.0) as usize).unwrap_or(0); + p.end_of_line = flag("EndOfLine", false); + p.byte_align = flag("EncodedByteAlign", false); + p.end_of_block = flag("EndOfBlock", true); + p.black_is_1 = flag("BlackIs1", false); + p +} + pub fn inflate(data: &[u8]) -> Option> { miniz_oxide::inflate::decompress_to_vec_zlib(data).ok().or_else(|| miniz_oxide::inflate::decompress_to_vec(data).ok()).or_else(|| { // Truncated streams: keep what inflates. @@ -569,7 +588,7 @@ impl File { Obj::Dict(d) => { let save = lx.pos; lx.skip_ws(); - if data[lx.pos..].starts_with(b"stream") { + if data.get(lx.pos..).is_some_and(|d| d.starts_with(b"stream")) { let mut s = lx.pos + 6; if data.get(s) == Some(&b'\r') { s += 1; @@ -578,8 +597,9 @@ impl File { s += 1; } let declared = d.get("Length").and_then(Obj::num).map(|l| l as usize); + let s = s.min(data.len()); let end = match declared.filter(|l| { - s + l <= data.len() && { + s.checked_add(*l).is_some_and(|e| e <= data.len()) && { let mut k = s + l; while k < data.len() && is_white(data[k]) { k += 1; @@ -599,6 +619,7 @@ impl File { e } }; + let end = end.max(s); i = end; Obj::Stream(d, data[s..end].to_vec()) } else { diff --git a/crates/pdf/src/ps.rs b/crates/pdf/src/ps.rs index b9b0bff4..1a980f6f 100644 --- a/crates/pdf/src/ps.rs +++ b/crates/pdf/src/ps.rs @@ -309,6 +309,8 @@ pub(crate) struct Interp { gs: GState, saved: Vec, budget: usize, + /// Nested procedure calls (recursion is cut off at [`MAX_CALL_DEPTH`]). + calls: usize, page: [f64; 4], pub unknown: Vec, } @@ -330,6 +332,9 @@ fn matrix_val(m: Affine) -> V { array(m.as_coeffs().iter().map(|c| V::Num(*c)).collect()) } +/// The deepest procedure nesting run (deeper recursion stops the program). +const MAX_CALL_DEPTH: usize = 256; + impl Interp { pub fn new(page: [f64; 4]) -> Interp { Interp { @@ -351,6 +356,7 @@ impl Interp { }, saved: vec![], budget: 8_000_000, + calls: 0, page, unknown: vec![], } @@ -396,13 +402,25 @@ impl Interp { fn exec_obj(&mut self, v: V, src: &mut Src) -> Result<(), Flow> { match v { V::Proc(p) => { + if self.calls >= MAX_CALL_DEPTH { + self.b.skip("PostScript (recursion limit)"); + return Err(Flow::Quit); + } + self.calls += 1; + let mut r = Ok(()); for x in p.iter() { match x { - V::Name(n) => self.exec_name(n, src)?, + V::Name(n) => { + r = self.exec_name(n, src); + if r.is_err() { + break; + } + } other => self.push(other.clone()), } } - Ok(()) + self.calls -= 1; + r } V::Name(n) => self.exec_name(&n, src), other => { diff --git a/crates/pdf/src/tests_import.rs b/crates/pdf/src/tests_import.rs new file mode 100644 index 00000000..3709d4ff --- /dev/null +++ b/crates/pdf/src/tests_import.rs @@ -0,0 +1,214 @@ +//! M13.12: CCITT images in PDF pages, EPS text and parser robustness over truncated and +//! corrupt inputs (generated fixtures). + +use super::*; +use crate::tests::{page_pdf, render}; + +/// An image XObject of `img` (black = true) CCITT-encoded with `k`, drawn over the page. +fn ccitt_page(img: &[Vec], k: i64, mask: bool) -> Doc { + let (w, h) = (img[0].len(), img.len()); + let data = crate::ccitt::tests::encode(img, k); + let kind = if mask { "/ImageMask true" } else { "/ColorSpace /DeviceGray /BitsPerComponent 1" }; + let bytes = page_pdf( + "0 0 1 rg q 200 0 0 100 0 0 cm /Im Do Q", + "/XObject << /Im 10 0 R >>", + vec![( + 10, + format!( + "<< /Type /XObject /Subtype /Image /Width {w} /Height {h} {kind} /Filter /CCITTFaxDecode /DecodeParms << /K {k} /Columns {w} /Rows {h} /EndOfLine {} >> >>", + k >= 0 + ), + Some(data), + )], + ); + let doc = parse(&bytes).unwrap(); + assert!(doc.skipped.is_empty(), "{:?}", doc.skipped); + doc +} + +#[test] +fn ccitt_images_and_stencil_masks() { + // Left half black, a white bar on rows 4..6 (20×10 pixels → 10 PDF units per pixel). + let img: Vec> = (0..10).map(|y| (0..20).map(|x| x < 10 && !(4..6).contains(&y)).collect()).collect(); + for k in [-1, 0, 2] { + let doc = ccitt_page(&img, k, false); + let r = render(&doc); + let px = |x: usize, y: usize| r.get(x as i64 * 10 + 5, y as i64 * 10 + 5); + assert!(px(2, 2)[0] < 0.01 && px(2, 2)[3] > 0.99, "K {k}: black {:?}", px(2, 2)); + assert!(px(15, 2)[0] > 0.99, "K {k}: white {:?}", px(15, 2)); + assert!(px(2, 4)[0] > 0.99, "K {k}: the white bar {:?}", px(2, 4)); + // Stencil mask: black (sample 0) pixels paint the fill colour, the rest is clear. + let doc = ccitt_page(&img, k, true); + let r = render(&doc); + let px = |x: usize, y: usize| r.get(x as i64 * 10 + 5, y as i64 * 10 + 5); + assert!(px(2, 2)[2] > 0.99 && px(2, 2)[3] > 0.99, "K {k}: mask {:?}", px(2, 2)); + assert_eq!(px(15, 2)[3], 0.0, "K {k}"); + } +} + +/// Byte-level mutations of `bytes`: truncations, flipped bytes, deleted and duplicated +/// spans (deterministic). +fn mutations(bytes: &[u8]) -> Vec> { + let mut out = vec![]; + let n = bytes.len(); + for k in 1..24 { + out.push(bytes[..n * k / 24].to_vec()); + } + let mut s = 0x9E37_79B9u32; + let mut rnd = move || { + s ^= s << 13; + s ^= s >> 17; + s ^= s << 5; + s + }; + for _ in 0..60 { + let mut b = bytes.to_vec(); + for _ in 0..1 + rnd() % 6 { + let i = rnd() as usize % n; + b[i] = match rnd() % 4 { + 0 => b[i] ^ 0xFF, + 1 => b'0' + (rnd() % 10) as u8, + 2 => b"[]<>()/{}% \n"[(rnd() % 12) as usize], + _ => rnd() as u8, + }; + } + out.push(b); + } + for _ in 0..30 { + let mut b = bytes.to_vec(); + let i = rnd() as usize % n; + let len = (rnd() as usize % 64).min(n - i); + if rnd() % 2 == 0 { + b.drain(i..i + len); + } else { + let span = b[i..i + len].to_vec(); + b.splice(i..i, span); + } + out.push(b); + } + out +} + +/// Parse and render every page of every mutation: errors are fine, panics are not. +fn survive(name: &str, bytes: &[u8]) { + for (i, m) in mutations(bytes).into_iter().enumerate() { + let r = std::panic::catch_unwind(|| { + let pages = page_count(&m); + for p in 0..pages.clamp(1, 3) { + if let Ok(doc) = parse_page(&m, p) { + let (w, h) = doc.pixel_size(); + let s = 64.0 / (w.max(h).max(1) as f64); + let _ = effectcraft_svg::rasterize(&doc, 64, 64, s); + let _ = layer_names(&doc); + } + } + }); + assert!(r.is_ok(), "{name}: mutation {i} panicked"); + } +} + +#[test] +fn truncated_and_corrupt_pdfs_never_panic() { + survive("sample", &crate::tests::sample_pdf(false)); + survive("object streams", &crate::tests::sample_pdf(true)); + let mut fonts = crate::tests::square_font_objs(); + fonts.push((13, "<< /Type /XObject /Subtype /Image /Width 2 /Height 2 /ColorSpace /DeviceRGB /BitsPerComponent 8 >>".into(), Some(vec![255; 12]))); + survive( + "text and images", + &page_pdf("BT /F1 40 Tf 10 10 Td (AAA) Tj ET q 50 0 0 50 0 0 cm /Im Do Q", "/Font << /F1 10 0 R >> /XObject << /Im 13 0 R >>", fonts), + ); + let img: Vec> = (0..8).map(|y| (0..16).map(|x| (x + y) % 3 == 0).collect()).collect(); + let data = crate::ccitt::tests::encode(&img, -1); + survive( + "ccitt", + &page_pdf( + "q 100 0 0 50 0 0 cm /Im Do Q", + "/XObject << /Im 10 0 R >>", + vec![( + 10, + "<< /Type /XObject /Subtype /Image /Width 16 /Height 8 /ImageMask true /Filter /CCITTFaxDecode /DecodeParms << /K -1 /Columns 16 >> >>".into(), + Some(data), + )], + ), + ); +} + +#[test] +fn hostile_structures_return_errors_or_draw_nothing() { + // Not a PDF / EPS at all, empty, and headers alone. + for b in [&b""[..], b"%PDF-", b"%PDF-1.7\n%%EOF", b"%!PS-Adobe-3.0 EPSF-3.0\n", b"\xC5\xD0\xD3\xC6", b"hello"] { + let _ = parse(b); + let _ = page_count(b); + } + assert_eq!(parse(b"hello"), Err(Error::NotVector)); + assert_eq!(parse(b"%PDF-1.7\n%%EOF"), Err(Error::NoPages)); + // A page tree that refers to itself, a content stream that is a reference loop, deep + // form recursion and absurd numbers. + let objs: crate::tests::Objs = vec![ + (1, "<< /Type /Catalog /Pages 2 0 R >>".into(), None), + (2, "<< /Type /Pages /Kids [2 0 R 3 0 R] /Count 1 >>".into(), None), + (3, "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 1e300 -1e300] /Contents 5 0 R /Resources << /XObject << /F 4 0 R >> >> >>".into(), None), + ( + 4, + "<< /Type /XObject /Subtype /Form /BBox [0 0 1 1] /Resources << /XObject << /F 4 0 R >> >> >>".into(), + Some(b"/F Do 1e308 1e308 m 1e308 0 l f".to_vec()), + ), + (5, "<< >>".into(), Some(b"/F Do q q q q Q Q Q Q Q Q 99999999999 w [1 0 0 1 0 0] cm".to_vec())), + ]; + let bytes = crate::write::pdf(&objs, 1); + if let Ok(doc) = parse(&bytes) { + let _ = effectcraft_svg::rasterize(&doc, 32, 32, 1e-300); + } + // Images with absurd sizes and filters. + let bytes = page_pdf( + "/A Do /B Do /C Do /D Do", + "/XObject << /A 10 0 R /B 11 0 R /C 12 0 R /D 13 0 R >>", + vec![ + (10, "<< /Subtype /Image /Width 100000 /Height 100000 /BitsPerComponent 8 /ColorSpace /DeviceRGB >>".into(), Some(vec![0; 4])), + (11, "<< /Subtype /Image /Width 4 /Height 4 /BitsPerComponent 7 /ColorSpace /DeviceRGB >>".into(), Some(vec![0; 4])), + ( + 12, + "<< /Subtype /Image /Width 4 /Height 4 /BitsPerComponent 1 /ImageMask true /Filter /CCITTFaxDecode /DecodeParms << /K -1 /Columns 0 >> >>" + .into(), + Some(vec![0xFF; 4]), + ), + (13, "<< /Subtype /Image /Width 4 /Height 4 /BitsPerComponent 8 /ColorSpace [/Indexed /DeviceRGB 300 <00>] >>".into(), Some(vec![9; 16])), + ], + ); + let doc = parse(&bytes).unwrap(); + let _ = render(&doc); + assert!(!doc.skipped.is_empty()); +} + +#[test] +fn truncated_and_corrupt_eps_never_panic() { + let eps = b"%!PS-Adobe-3.0 EPSF-3.0\n%%BoundingBox: 0 0 100 100\n\ +/sq { newpath 0 0 moveto 10 0 lineto 10 10 lineto closepath } bind def\n\ +1 0 0 setrgbcolor 0 1 9 { gsave dup 10 mul 0 translate sq fill grestore } for\n\ +/Helvetica findfont 12 scalefont setfont 10 50 moveto (Hello) show\n\ +[1 2] 0 setdash 0 0 moveto 100 100 lineto stroke\n\ +1 1 1 { pop } repeat { exit } loop 0 0 50 0 360 arc closepath eofill\n%%EOF\n"; + survive("eps", eps); + // Stack underflow, unbalanced procedures, runaway loops and huge repeats. + for prog in [ + &b"pop pop pop add mul def"[..], + b"{ { { { {", + b"} } ] >> )", + b"{ } loop", + b"0 1 1e9 { pop } for", + b"1e9 { 1 } repeat", + b"/a { a } def a", + b"(unterminated", + b"