effectcraft/crates/pdf/src/object.rs

730 lines
25 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! 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<u8>),
Array(Vec<Obj>),
Dict(Dict),
Ref(u32, u16),
/// A stream: its dictionary and raw (still encoded) data.
Stream(Dict, Vec<u8>),
/// A content-stream operator (or any bare keyword).
Op(String),
}
pub type Dict = HashMap<String, Obj>;
impl Obj {
pub fn num(&self) -> Option<f64> {
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<Obj> {
self.next_depth(0)
}
fn next_depth(&mut self, depth: usize) -> Option<Obj> {
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<u8> {
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<f64> {
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::<f64>().ok().or_else(|| s.trim_start_matches(['+', '-']).parse::<f64>().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<Vec<u8>> {
let filters: Vec<String> = 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<Option<Dict>> = 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<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.
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<u8> {
let mut hex: Vec<u8> = 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<u8> {
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<u8> {
let mut out = vec![];
let mut table: Vec<Vec<u8>> = (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<Vec<u8>> = 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<u8> {
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<u8> {
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<u32, Obj>,
pub trailers: Vec<Dict>,
}
fn find(hay: &[u8], needle: &[u8], from: usize) -> Option<usize> {
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::<u32>().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<u8>)> = 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<f64> {
self.get(d, key)?.num()
}
/// Numbers of an array (references resolved).
pub fn nums(&self, o: &Obj) -> Vec<f64> {
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"))
}
}