M13.12: pdf CCITT G3/G4 images, parser robustness tests and panic fixes

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Brandon Thomas 2026-10-03 08:54:05 -04:00
parent 3d536cfe5a
commit d0df520bc1
6 changed files with 1059 additions and 5 deletions

798
crates/pdf/src/ccitt.rs Normal file
View File

@ -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<u32> {
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<Mode> {
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<Vec<usize>> {
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<Vec<usize>, bool> {
let (wt, bt) = tables();
let mut changes: Vec<usize> = 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<u8>) {
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<Vec<u8>> {
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<usize> = vec![];
let mut rows = 0;
while rows < max_rows && !r.eof() {
let line: Option<Vec<usize>> = 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<u8>,
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<usize> {
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<bool>], k: i64) -> Vec<u8> {
let mut w = W::default();
let mut refl: Vec<usize> = 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<Vec<bool>> {
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<Vec<bool>> {
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<u8> {
(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() });
}
}
}

View File

@ -100,7 +100,7 @@ fn samples(file: &File, d: &Dict, raw: &[u8]) -> Result<(Vec<u8>, Option<usize>)
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()),
}
}

View File

@ -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;

View File

@ -328,6 +328,7 @@ pub fn decode_stream(file: &File, d: &Dict, raw: &[u8]) -> Option<Vec<u8>> {
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<Vec<u8>> {
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<Vec<u8>> {
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 {

View File

@ -309,6 +309,8 @@ pub(crate) struct Interp {
gs: GState,
saved: Vec<GState>,
budget: usize,
/// Nested procedure calls (recursion is cut off at [`MAX_CALL_DEPTH`]).
calls: usize,
page: [f64; 4],
pub unknown: Vec<String>,
}
@ -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 => {

View File

@ -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<bool>], 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<Vec<bool>> = (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<Vec<u8>> {
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<Vec<bool>> = (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"<abc",
b"100 100 scale 0 0 moveto 1e308 1e308 lineto stroke",
] {
let mut b = b"%!PS-Adobe-3.0 EPSF-3.0\n%%BoundingBox: 0 0 10 10\n".to_vec();
b.extend_from_slice(prog);
let r = std::panic::catch_unwind(|| {
if let Ok(doc) = parse(&b) {
let _ = effectcraft_svg::rasterize(&doc, 10, 10, 1.0);
}
});
assert!(r.is_ok(), "{}", String::from_utf8_lossy(prog));
}
}