Files
vectorcraft/crates/pdf/src/syntax.rs
T
bflatasticandClaude Opus 5.5 8961824876 M4.86: PDF page thumbnails and fast web view (linearised files)
Embed Page Thumbnails gives every page a /Thumb image: the page drawn on
white, 106 px on its long side, without the layers the page leaves out
(the engine draws them; vectorcraft_pdf::page_areas says what each page
shows; PDF/X files get grey ones). Optimize for Fast Web View writes a
linearised file: the objects are put in linearised order and numbering,
encrypted when a password asks (the encryption dictionary goes with the
catalog, PDF layers' groups too), then laid out with the linearization
dictionary, both cross-reference tables and the page offset and shared
object hint tables (the hint stream encrypted like any other). Their
"not applied yet" warnings are gone. Reading the objects back uses the
lexer of encrypt.rs (syntax.rs adds what rewriting needs).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 18:05:36 +07:00

77 lines
3.1 KiB
Rust

//! A written file's indirect objects read for rewriting it (the linearisation and the page
//! thumbnails), on the lexer of [`crate::encrypt`]: where each object's parts lie, what it refers
//! to, and the references to renumber.
use crate::encrypt::{Lexer, Obj, Tok, int, stream_spans};
impl Obj<'_> {
/// Every object this one refers to, in file order, leaving out the values of the dictionary
/// keys in `skip` (`Parent`).
pub(crate) fn refs(&self, skip: &[&[u8]], out: &mut Vec<u32>) {
match self {
Self::Ref(n, _) => out.push(*n),
Self::Dict { entries, .. } => entries.iter().filter(|(k, _)| !skip.contains(k)).for_each(|(_, v)| v.refs(skip, out)),
Self::Array(items) => items.iter().for_each(|v| v.refs(skip, out)),
_ => {}
}
}
}
/// An indirect object of a file, with where its parts lie.
pub(crate) struct Indirect<'a> {
pub num: u32,
pub generation: u16,
pub value: Obj<'a>,
/// Where the value (a stream's dictionary) starts and ends.
pub value_span: (usize, usize),
/// Where `endobj` starts.
pub endobj: usize,
}
/// The indirect object at `off` of `pdf` (a stream's length must be direct); `None` when it can't
/// be read.
pub(crate) fn indirect(pdf: &[u8], off: usize) -> Option<Indirect<'_>> {
let mut lx = Lexer::at(pdf, off);
let (num, generation) = lx.header()?;
let start = lx.clone().next()?.1;
let value = lx.object(0)?;
// The value ends before the white space ahead of the next token (`stream` or `endobj`).
let (next, next_at, _) = lx.clone().next()?;
let end = next_at - pdf.get(..next_at)?.iter().rev().take_while(|b| b.is_ascii_whitespace()).count();
let endobj = match next {
Tok::Word(b"endobj") => next_at,
_ => {
let (_, (_, data_end)) = stream_spans(pdf, &mut lx, &value).ok()??;
let mut lx = Lexer::at(pdf, data_end);
let (Tok::Word(b"endstream"), ..) = lx.next()? else { return None };
let (Tok::Word(b"endobj"), at, _) = lx.next()? else { return None };
at
}
};
Some(Indirect { num, generation, value, value_span: (start, end.max(start)), endobj })
}
/// The references (`n g R`) in `pdf[start..end]` (an object's value: no stream data), outside
/// strings: span and object number.
pub(crate) fn ref_spans(pdf: &[u8], start: usize, end: usize) -> Vec<(usize, usize, u32)> {
let mut out = vec![];
let mut lx = Lexer::at(pdf, start);
// The last two tokens when they are numbers: (start, value).
let mut last: [Option<(usize, i64)>; 2] = [None, None];
while let Some((tok, s, e)) = lx.next().filter(|t| t.1 < end) {
match tok {
Tok::Word(b"R") => {
if let [Some((at, n)), Some(_)] = last
&& let Ok(n) = u32::try_from(n)
{
out.push((at, e, n));
}
last = [None, None];
}
Tok::Word(w) => last = [last[1], int(w).map(|v| (s, v))],
_ => last = [None, None],
}
}
out
}