//! PDF objects (ISO 32000-1 §7.2–7.5): the lexer, the object model, stream filters and the //! indirect-object table. //! //! The table is built by scanning the file for `n g obj` headers (later definitions win, as //! incremental updates do) and by unpacking object streams, so damaged or rewritten //! cross-reference sections do not matter. use std::collections::HashMap; #[derive(Clone, Debug, PartialEq)] pub enum Obj { Null, Bool(bool), Num(f64), Name(String), Str(Vec), Array(Vec), Dict(Dict), Ref(u32, u16), /// A stream: its dictionary and raw (still encoded) data. Stream(Dict, Vec), /// A content-stream operator (or any bare keyword). Op(String), } pub type Dict = HashMap; impl Obj { pub fn num(&self) -> Option { match self { Obj::Num(n) => Some(*n), _ => None, } } pub fn name(&self) -> Option<&str> { match self { Obj::Name(n) => Some(n), _ => None, } } pub fn array(&self) -> Option<&[Obj]> { match self { Obj::Array(a) => Some(a), _ => None, } } pub fn dict(&self) -> Option<&Dict> { match self { Obj::Dict(d) | Obj::Stream(d, _) => Some(d), _ => None, } } } pub fn is_white(c: u8) -> bool { matches!(c, 0 | 9 | 10 | 12 | 13 | 32) } pub fn is_delim(c: u8) -> bool { matches!(c, b'(' | b')' | b'<' | b'>' | b'[' | b']' | b'{' | b'}' | b'/' | b'%') } /// A byte lexer producing PDF objects (arrays and dictionaries nested). pub struct Lexer<'a> { pub data: &'a [u8], pub pos: usize, } impl<'a> Lexer<'a> { pub fn new(data: &'a [u8], pos: usize) -> Self { Lexer { data, pos } } pub fn skip_ws(&mut self) { while self.pos < self.data.len() { let c = self.data[self.pos]; if is_white(c) { self.pos += 1; } else if c == b'%' { while self.pos < self.data.len() && !matches!(self.data[self.pos], b'\n' | b'\r') { self.pos += 1; } } else { break; } } } fn word(&mut self) -> &'a [u8] { let s = self.pos; while self.pos < self.data.len() && !is_white(self.data[self.pos]) && !is_delim(self.data[self.pos]) { self.pos += 1; } &self.data[s..self.pos] } /// The next object, `None` at the end. `]` and `>>` come back as `Op`s. pub fn next(&mut self) -> Option { self.next_depth(0) } fn next_depth(&mut self, depth: usize) -> Option { self.skip_ws(); let c = *self.data.get(self.pos)?; match c { b'/' => { self.pos += 1; let w = self.word(); Some(Obj::Name(decode_name(w))) } b'(' => Some(Obj::Str(self.literal_string())), b'<' if self.data.get(self.pos + 1) == Some(&b'<') => { self.pos += 2; let mut d = Dict::new(); loop { if depth > 64 { return Some(Obj::Null); } let k = self.next_depth(depth + 1); match k { None => break, Some(Obj::Op(o)) if o == ">>" => break, Some(Obj::Name(k)) => { let v = self.next_depth(depth + 1).unwrap_or(Obj::Null); let v = self.maybe_ref(v); d.insert(k, v); } Some(_) => {} } } Some(Obj::Dict(d)) } b'<' => { self.pos += 1; let mut hex = vec![]; while self.pos < self.data.len() && self.data[self.pos] != b'>' { let h = self.data[self.pos]; if h.is_ascii_hexdigit() { hex.push(h); } self.pos += 1; } self.pos += 1; if hex.len() % 2 == 1 { hex.push(b'0'); } Some(Obj::Str(hex.chunks(2).map(|p| (hexv(p[0]) << 4) | hexv(p[1])).collect())) } b'>' if self.data.get(self.pos + 1) == Some(&b'>') => { self.pos += 2; Some(Obj::Op(">>".into())) } b'[' => { self.pos += 1; let mut a = vec![]; loop { if depth > 64 { return Some(Obj::Null); } match self.next_depth(depth + 1) { None => break, Some(Obj::Op(o)) if o == "]" => break, Some(v) => { let v = self.maybe_ref(v); a.push(v); } } } Some(Obj::Array(a)) } b']' | b'{' | b'}' | b')' | b'>' => { self.pos += 1; Some(Obj::Op((c as char).to_string())) } _ => { let w = self.word(); if w.is_empty() { self.pos += 1; return Some(Obj::Op(String::new())); } Some(match w { b"true" => Obj::Bool(true), b"false" => Obj::Bool(false), b"null" => Obj::Null, _ => match parse_num(w) { Some(n) => Obj::Num(n), None => Obj::Op(String::from_utf8_lossy(w).into_owned()), }, }) } } } /// `n g R` after a number inside arrays and dictionaries. fn maybe_ref(&mut self, v: Obj) -> Obj { let Obj::Num(n) = v else { return v }; if n.fract() != 0.0 || n < 0.0 { return v; } let save = self.pos; self.skip_ws(); let gp = self.pos; let g = self.word(); if let Some(gn) = parse_num(g).filter(|g| g.fract() == 0.0 && *g >= 0.0) { self.skip_ws(); if self.data.get(self.pos) == Some(&b'R') && self.data.get(self.pos + 1).is_none_or(|c| is_white(*c) || is_delim(*c)) { self.pos += 1; return Obj::Ref(n as u32, gn as u16); } } let _ = gp; self.pos = save; v } fn literal_string(&mut self) -> Vec { self.pos += 1; let mut out = vec![]; let mut depth = 1; while self.pos < self.data.len() { let c = self.data[self.pos]; self.pos += 1; match c { b'\\' => { let Some(&e) = self.data.get(self.pos) else { break }; self.pos += 1; match e { b'n' => out.push(b'\n'), b'r' => out.push(b'\r'), b't' => out.push(b'\t'), b'b' => out.push(8), b'f' => out.push(12), b'\r' => { if self.data.get(self.pos) == Some(&b'\n') { self.pos += 1; } } b'\n' => {} b'0'..=b'7' => { let mut v = (e - b'0') as u32; for _ in 0..2 { match self.data.get(self.pos) { Some(d @ b'0'..=b'7') => { v = v * 8 + (d - b'0') as u32; self.pos += 1; } _ => break, } } out.push(v as u8); } other => out.push(other), } } b'(' => { depth += 1; out.push(c); } b')' => { depth -= 1; if depth == 0 { break; } out.push(c); } _ => out.push(c), } } out } } fn hexv(c: u8) -> u8 { match c { b'0'..=b'9' => c - b'0', b'a'..=b'f' => c - b'a' + 10, b'A'..=b'F' => c - b'A' + 10, _ => 0, } } fn decode_name(w: &[u8]) -> String { let mut out = vec![]; let mut i = 0; while i < w.len() { if w[i] == b'#' && w.get(i + 1).is_some_and(u8::is_ascii_hexdigit) && w.get(i + 2).is_some_and(u8::is_ascii_hexdigit) { out.push((hexv(w[i + 1]) << 4) | hexv(w[i + 2])); i += 3; } else { out.push(w[i]); i += 1; } } String::from_utf8_lossy(&out).into_owned() } pub fn parse_num(w: &[u8]) -> Option { if w.is_empty() || !w.iter().all(|c| c.is_ascii_digit() || matches!(c, b'+' | b'-' | b'.')) || !w.iter().any(u8::is_ascii_digit) { return None; } let s = std::str::from_utf8(w).ok()?; // "--5" and similar malformed numbers: take what parses. s.parse::().ok().or_else(|| s.trim_start_matches(['+', '-']).parse::().ok().map(|v| if s.starts_with('-') { -v } else { v })) } // ------------------------------------------------------------------ filters /// Decode a stream's data through its `/Filter` chain. Unknown filters (images: DCT, JPX, CCITT, /// JBIG2) give `None`. pub fn decode_stream(file: &File, d: &Dict, raw: &[u8]) -> Option> { let filters: Vec = match d.get("Filter").map(|f| file.resolve(f)) { None => vec![], Some(Obj::Name(n)) => vec![n.clone()], Some(Obj::Array(a)) => a.iter().filter_map(|f| file.resolve(f).name().map(str::to_string)).collect(), _ => vec![], }; let parms: Vec> = match d.get("DecodeParms").or_else(|| d.get("DP")).map(|p| file.resolve(p)) { Some(Obj::Dict(p)) => vec![Some(p.clone())], Some(Obj::Array(a)) => a.iter().map(|p| file.resolve(p).dict().cloned()).collect(), _ => vec![], }; let mut data = raw.to_vec(); for (i, f) in filters.iter().enumerate() { data = match f.as_str() { "FlateDecode" | "Fl" => inflate(&data)?, "ASCIIHexDecode" | "AHx" => ascii_hex(&data), "ASCII85Decode" | "A85" => ascii85(&data), "LZWDecode" | "LZW" => { 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) && matches!(f.as_str(), "FlateDecode" | "Fl" | "LZWDecode" | "LZW") { data = predict(&data, p); } } 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. let mut d = miniz_oxide::inflate::stream::InflateState::new_boxed(miniz_oxide::DataFormat::Zlib); let mut out = vec![0u8; data.len() * 8 + 1024]; let r = miniz_oxide::inflate::stream::inflate(&mut d, data, &mut out, miniz_oxide::MZFlush::Finish); (r.bytes_written > 0).then(|| out[..r.bytes_written].to_vec()) }) } fn ascii_hex(data: &[u8]) -> Vec { let mut hex: Vec = data.iter().copied().take_while(|c| *c != b'>').filter(u8::is_ascii_hexdigit).collect(); if hex.len() % 2 == 1 { hex.push(b'0'); } hex.chunks(2).map(|p| (hexv(p[0]) << 4) | hexv(p[1])).collect() } pub fn ascii85(data: &[u8]) -> Vec { let mut out = vec![]; let mut group = [0u32; 5]; let mut n = 0; let mut i = 0; if data.starts_with(b"<~") { i = 2; } while i < data.len() { let c = data[i]; i += 1; match c { b'~' => break, b'z' if n == 0 => out.extend_from_slice(&[0; 4]), b'!'..=b'u' => { group[n] = (c - b'!') as u32; n += 1; if n == 5 { let v = group.iter().fold(0u32, |a, &d| a.wrapping_mul(85).wrapping_add(d)); out.extend_from_slice(&v.to_be_bytes()); n = 0; } } _ => {} } } if n > 1 { for g in group.iter_mut().skip(n) { *g = 84; } let v = group.iter().fold(0u32, |a, &d| a.wrapping_mul(85).wrapping_add(d)); out.extend_from_slice(&v.to_be_bytes()[..n - 1]); } out } fn lzw(data: &[u8], early: bool) -> Vec { let mut out = vec![]; let mut table: Vec> = (0..=255u16).map(|b| vec![b as u8]).collect(); table.push(vec![]); table.push(vec![]); let (mut bits, mut acc, mut nbits) = (9u32, 0u32, 0u32); let mut prev: Option> = None; for &b in data { acc = (acc << 8) | b as u32; nbits += 8; while nbits >= bits { let code = ((acc >> (nbits - bits)) & ((1 << bits) - 1)) as usize; nbits -= bits; match code { 256 => { table.truncate(258); bits = 9; prev = None; continue; } 257 => return out, _ => {} } let entry = if code < table.len() { table[code].clone() } else if let Some(p) = &prev { let mut e = p.clone(); e.push(p[0]); e } else { return out; }; out.extend_from_slice(&entry); if let Some(p) = prev { let mut e = p; e.push(entry[0]); table.push(e); } prev = Some(entry); let limit = table.len() + usize::from(early); if limit >= (1 << bits) && bits < 12 { bits += 1; } } } out } fn run_length(data: &[u8]) -> Vec { let mut out = vec![]; let mut i = 0; while i < data.len() { let l = data[i] as usize; i += 1; if l < 128 { out.extend_from_slice(&data[i.min(data.len())..(i + l + 1).min(data.len())]); i += l + 1; } else if l > 128 { if let Some(&b) = data.get(i) { out.extend(std::iter::repeat_n(b, 257 - l)); } i += 1; } else { break; } } out } /// PNG / TIFF predictors (`/Predictor`, `/Colors`, `/BitsPerComponent`, `/Columns`). fn predict(data: &[u8], p: &Dict) -> Vec { let g = |k: &str, d: f64| p.get(k).and_then(Obj::num).unwrap_or(d) as usize; let pred = g("Predictor", 1.0); if pred < 10 { return data.to_vec(); } let bpp = (g("Colors", 1.0) * g("BitsPerComponent", 8.0)).div_ceil(8).max(1); let row = (g("Columns", 1.0) * g("Colors", 1.0) * g("BitsPerComponent", 8.0)).div_ceil(8); let mut out = Vec::with_capacity(data.len()); let mut prev = vec![0u8; row]; for chunk in data.chunks(row + 1) { if chunk.len() < row + 1 { break; } let ft = chunk[0]; let mut cur = chunk[1..].to_vec(); for i in 0..row { let a = if i >= bpp { cur[i - bpp] as i32 } else { 0 }; let b = prev[i] as i32; let c = if i >= bpp { prev[i - bpp] as i32 } else { 0 }; let add = match ft { 1 => a, 2 => b, 3 => (a + b) / 2, 4 => { let pp = a + b - c; let (pa, pb, pc) = ((pp - a).abs(), (pp - b).abs(), (pp - c).abs()); if pa <= pb && pa <= pc { a } else if pb <= pc { b } else { c } } _ => 0, }; cur[i] = (cur[i] as i32 + add) as u8; } out.extend_from_slice(&cur); prev = cur; } out } // ------------------------------------------------------------------ the file /// Every indirect object of a PDF file, plus the trailer dictionaries found. pub struct File { pub objects: HashMap, pub trailers: Vec, } fn find(hay: &[u8], needle: &[u8], from: usize) -> Option { if from >= hay.len() || needle.is_empty() { return None; } hay[from..].windows(needle.len()).position(|w| w == needle).map(|p| p + from) } impl File { pub fn parse(data: &[u8]) -> File { let mut f = File { objects: HashMap::new(), trailers: vec![] }; let mut i = 0; while let Some(p) = find(data, b"obj", i) { i = p + 3; if p >= 3 && &data[p - 3..p] == b"end" { continue; } if data.get(p + 3).is_some_and(|c| !is_white(*c) && !is_delim(*c)) { continue; } // Walk back over "num gen ". let mut q = p; let back_ws = |q: &mut usize| { let s = *q; while *q > 0 && is_white(data[*q - 1]) { *q -= 1; } *q < s }; let back_digits = |q: &mut usize| { let e = *q; while *q > 0 && data[*q - 1].is_ascii_digit() { *q -= 1; } (*q < e).then(|| std::str::from_utf8(&data[*q..e]).ok()?.parse::().ok()).flatten() }; if !back_ws(&mut q) { continue; } let Some(_gen) = back_digits(&mut q) else { continue }; if !back_ws(&mut q) { continue; } let Some(num) = back_digits(&mut q) else { continue }; if q > 0 && !is_white(data[q - 1]) && !is_delim(data[q - 1]) { continue; } let mut lx = Lexer::new(data, p + 3); let Some(obj) = lx.next() else { continue }; let obj = lx.maybe_ref(obj); let obj = match obj { Obj::Dict(d) => { let save = lx.pos; lx.skip_ws(); 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; } if data.get(s) == Some(&b'\n') { 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.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; } data[k..].starts_with(b"endstream") } }) { Some(l) => s + l, None => { let mut e = find(data, b"endstream", s).unwrap_or(data.len()); if e > s && data[e - 1] == b'\n' { e -= 1; } if e > s && data[e - 1] == b'\r' { e -= 1; } e } }; let end = end.max(s); i = end; Obj::Stream(d, data[s..end].to_vec()) } else { lx.pos = save; Obj::Dict(d) } } o => o, }; if let Obj::Stream(d, _) = &obj && d.get("Type").and_then(Obj::name) == Some("XRef") { f.trailers.push(d.clone()); } f.objects.insert(num, obj); } // Trailer dictionaries of classic cross-reference sections. let mut i = 0; while let Some(p) = find(data, b"trailer", i) { i = p + 7; let mut lx = Lexer::new(data, i); if let Some(Obj::Dict(d)) = lx.next() { f.trailers.push(d); } } // Object streams. let streams: Vec<(Dict, Vec)> = f .objects .values() .filter_map(|o| match o { Obj::Stream(d, raw) if d.get("Type").and_then(Obj::name) == Some("ObjStm") => Some((d.clone(), raw.clone())), _ => None, }) .collect(); for (d, raw) in streams { let Some(dec) = decode_stream(&f, &d, &raw) else { continue }; let n = d.get("N").and_then(Obj::num).unwrap_or(0.0) as usize; let first = d.get("First").and_then(Obj::num).unwrap_or(0.0) as usize; let mut lx = Lexer::new(&dec, 0); let mut heads = vec![]; for _ in 0..n.min(100_000) { let (Some(Obj::Num(num)), Some(Obj::Num(off))) = (lx.next(), lx.next()) else { break }; heads.push((num as u32, off as usize)); } for (num, off) in heads { if f.objects.contains_key(&num) || first + off >= dec.len() { continue; } let mut lx = Lexer::new(&dec, first + off); if let Some(o) = lx.next() { let o = lx.maybe_ref(o); f.objects.insert(num, o); } } } f } /// Follow references (bounded). pub fn resolve<'a>(&'a self, o: &'a Obj) -> &'a Obj { let mut o = o; for _ in 0..32 { match o { Obj::Ref(n, _) => match self.objects.get(n) { Some(x) => o = x, None => return &Obj::Null, }, _ => return o, } } &Obj::Null } pub fn get<'a>(&'a self, d: &'a Dict, key: &str) -> Option<&'a Obj> { d.get(key).map(|o| self.resolve(o)).filter(|o| !matches!(o, Obj::Null)) } pub fn get_dict<'a>(&'a self, d: &'a Dict, key: &str) -> Option<&'a Dict> { self.get(d, key)?.dict() } pub fn get_num(&self, d: &Dict, key: &str) -> Option { self.get(d, key)?.num() } /// Numbers of an array (references resolved). pub fn nums(&self, o: &Obj) -> Vec { self.resolve(o).array().map(|a| a.iter().filter_map(|x| self.resolve(x).num()).collect()).unwrap_or_default() } /// The document catalog. pub fn catalog(&self) -> Option<&Dict> { for t in self.trailers.iter().rev() { if let Some(Obj::Dict(d)) = t.get("Root").map(|r| self.resolve(r)) { return Some(d); } } let mut cats: Vec<(&u32, &Obj)> = self.objects.iter().filter(|(_, o)| matches!(o, Obj::Dict(d) if d.get("Type").and_then(Obj::name) == Some("Catalog"))).collect(); cats.sort_by_key(|(n, _)| **n); cats.last().and_then(|(_, o)| o.dict()) } pub fn encrypted(&self) -> bool { self.trailers.iter().any(|t| t.contains_key("Encrypt")) } }