//! What the interpreter doesn't tell the import, read from the file itself: the optional content //! groups (layers) with their states and nesting, which group each marked-content sequence belongs //! to, the overprint parameters each path and each stretch of text is painted with, the Separation //! and DeviceN inks they are painted in (by their colorant names, which the interpreter drops), and //! the names of the fonts. //! //! hayro-interpret reports a marked-content sequence by its tag alone and skips the content of //! groups that are off. So each page's content is walked here in the interpreter's order (page //! content, then form XObjects as they are drawn, skipping what it skips): the k-th `BDC`/`BMC` it //! reports is the k-th one found here (its tag checks that), and the k-th path it fills or strokes //! is the k-th one painted here (the page's count checks that). To import content that is off as //! hidden layers, [`all_on`] gives the file an update that turns every group on. use std::collections::{HashMap, HashSet}; use crate::import_color::items; use craft_pdf::interpret::CacheKey; use craft_pdf::syntax::Pdf; use craft_pdf::syntax::content::TypedIter; use craft_pdf::syntax::content::ops::TypedInstruction; use craft_pdf::syntax::object::{Array, Dict, FromBytes, MaybeRef, Name, Object, ObjectIdentifier, Stream, dict_or_stream}; use craft_pdf::syntax::page::{Page, Resources}; use crate::PdfError; use crate::import_color::ink_names; /// The interpreter's limit on nested form XObjects. const MAX_DEPTH: u32 = 50; /// The most graphics states saved at once (PDF's own limit for nesting is 28; files nest far deeper). const MAX_STATES: usize = 65_536; /// The most content operators walked per page (a guard against forms drawn within each other /// many times over): the art past it can't be told apart by group. A drawing of a million paths /// takes a few million; the walk reads about 15 million a second, twice as fast as the /// interpreter draws them. const MAX_OPS: usize = 64_000_000; /// The most bytes of form XObject content walked per page, a form drawn again counted again: /// [`MAX_OPS`] guards content that is mostly operators, this content that isn't (a form of long /// strings, inline image data or spaces). An operator takes some 10 to 40 bytes (a curve's six /// numbers about 40), so a page of ordinary content within `MAX_OPS` walks up to about this much; /// a page that draws a form decoding to 64 MiB a thousand times (#3569, which hung) stops at its /// 33rd reading. The interpreter reads every form again too, and nothing else bounds what it /// decodes. (2 GiB: within a 32-bit `usize`, for the web build.) const MAX_FORM_CONTENT: usize = 2 << 30; /// The most marked-content sequences noted per page (24 bytes each): past it the rest of the /// page isn't told apart either. const MAX_TAGS: usize = 4_000_000; /// The most groups read. const MAX_GROUPS: usize = 10_000; /// Match the interpreter's ordinary content window and maximum incomplete instruction. const CONTENT_WINDOW: usize = 64 << 10; const MAX_WINDOW: usize = 256 << 20; /// The most Separation and DeviceN inks noted per page. const MAX_INKS: usize = 4096; /// The most changes of ink noted per page in the paths, and in the text (24 bytes each). const MAX_INK_CHANGES: usize = 2_000_000; /// The deepest layers nest in the configuration's `/Order`. pub(crate) const MAX_NESTING: usize = 32; /// An optional content group: a layer. #[derive(Clone, Debug, PartialEq)] pub(crate) struct Ocg { pub name: String, /// On in the file's default configuration (shown by viewers). pub on: bool, /// Printed (its print usage isn't off). pub print: bool, pub locked: bool, /// The group it is listed under in the layer order (a sublayer of it). pub parent: Option, } /// The file's optional content groups. #[derive(Default)] pub(crate) struct Ocgs { pub list: Vec, index: HashMap, /// The groups that are off (as the interpreter decides it). off: HashSet, /// The groups whose view state applies when the file opens (an `/AS` entry for viewing). view_auto: HashSet, /// The groups placed in the layer order. placed: HashSet, } impl Ocgs { /// The groups listed in the catalog's `/OCProperties`, with their default states. pub fn read(pdf: &Pdf) -> Self { let mut o = Self::default(); let xref = pdf.xref(); let Some(props) = xref.get::>(xref.root_id()).and_then(|c| c.get::>(b"OCProperties")) else { return o; }; let config = props.get::>(b"D").unwrap_or_default(); let refs = |d: &Dict<'_>, key: &[u8]| -> Vec { d.get::>(key) .map(|a| items(&a).filter_map(|i| i.as_obj_ref()).map(ObjectIdentifier::from).take(MAX_GROUPS).collect()) .unwrap_or_default() }; let all = refs(&props, b"OCGs"); if config.get::>(b"BaseState").is_some_and(|n| n.as_ref() == b"OFF") { o.off.extend(all.iter().copied()); } for id in refs(&config, b"ON") { o.off.remove(&id); } o.off.extend(refs(&config, b"OFF")); let auto = config.get::>(b"AS").map(|a| a.iter::>().take(MAX_GROUPS).collect::>()).unwrap_or_default(); for d in auto.iter().filter(|d| d.get::>(b"Event").is_some_and(|e| e.as_ref() == b"View")) { o.view_auto.extend(refs(d, b"OCGs")); } let locked: HashSet<_> = refs(&config, b"Locked").into_iter().collect(); for id in all { if let Some(d) = xref.get::>(id) { o.add(id, &d, locked.contains(&id)); } } if let Some(order) = config.get::>(b"Order") { o.nest(&order, None, 0, xref, &mut 0); } o } /// Read the layer order `order` (listed under group `parent`): a group followed by an array /// lists that array's groups as its sublayers; an array starting with a label lists its /// groups under the label, which isn't a group (they go under `parent`). A group listed twice /// stays where it is first listed. `seen`: the entries read so far. fn nest(&mut self, order: &Array<'_>, parent: Option, depth: usize, xref: &craft_pdf::syntax::xref::XRef, seen: &mut usize) { let mut prev = None; for item in items(order) { *seen += 1; if *seen > 4 * MAX_GROUPS { return; } let sub = match item { MaybeRef::Ref(r) => { let id = ObjectIdentifier::from(r); let sub = xref.get::>(id); if sub.is_none() { prev = self.place(id, parent); } sub } MaybeRef::NotRef(o) => o.into_array(), }; if let Some(sub) = sub && depth < MAX_NESTING { let label = matches!(items(&sub).next(), Some(MaybeRef::NotRef(Object::String(_)))); let under = prev.take().filter(|_| !label).or(parent); self.nest(&sub, under, depth + 1, xref, seen); } } } /// Put group `id` under `parent` in the layer order → its index, unless it isn't a group or /// is already placed. Each group is placed after its parent, so the nesting has no cycles. fn place(&mut self, id: ObjectIdentifier, parent: Option) -> Option { let i = *self.index.get(&id)?; let placed = self.placed.insert(i); let g = self.list.get_mut(i).filter(|_| placed)?; g.parent = parent; Some(i) } /// Note group `id` (its dictionary `d`) → its index. fn add(&mut self, id: ObjectIdentifier, d: &Dict<'_>, locked: bool) -> usize { if let Some(i) = self.index.get(&id) { return *i; } let name = d.get::>(b"Name").map(|s| text_string(s.as_bytes())).unwrap_or_default(); let print = usage(d, b"Print", b"PrintState") != Some(false); // A view state applies when the configuration says so on opening; an off one hides the // group all the same (it says how the group is meant to be seen). let view = usage(d, b"View", b"ViewState"); let on = match view { Some(v) if self.view_auto.contains(&id) => v, Some(false) => false, _ => !self.off.contains(&id), }; let name = if name.is_empty() { format!("Layer {}", self.list.len() + 1) } else { name }; self.list.push(Ocg { name, on, print, locked, parent: None }); self.index.insert(id, self.list.len() - 1); self.list.len() - 1 } /// Does the interpreter leave out the art of some group (off in the default configuration)? pub fn skips_any(&self) -> bool { self.index.keys().any(|id| self.off.contains(id)) } /// The group content marked with `/OC` (`d`: the group or membership dictionary, `id` its /// object) belongs to: a membership dictionary's first group. fn group_of(&mut self, d: &Dict<'_>, id: ObjectIdentifier, xref: &craft_pdf::syntax::xref::XRef) -> Option { if d.get::>(b"Type").is_some_and(|t| t.as_ref() == b"OCMD") { let first = d.get::>(b"OCGs").and_then(|a| items(&a).find_map(|i| i.as_obj_ref())).or_else(|| d.get_ref(b"OCGs"))?; let id = ObjectIdentifier::from(first); return self.note(id, &xref.get::>(id)?); } self.note(id, d) } /// The index of group `id` (dictionary `d`), noted if new (up to [`MAX_GROUPS`]). fn note(&mut self, id: ObjectIdentifier, d: &Dict<'_>) -> Option { match self.index.get(&id) { Some(i) => Some(*i), None => (self.list.len() < MAX_GROUPS).then(|| self.add(id, d, false)), } } } /// The state (`ON` → true, `OFF` → false) group `d`'s usage `category` gives in `key`. fn usage(d: &Dict<'_>, category: &[u8], key: &[u8]) -> Option { let state = d.get::>(b"Usage")?.get::>(category)?.get::>(key)?; match state.as_ref() { b"ON" => Some(true), b"OFF" => Some(false), _ => None, } } /// A PDF text string: UTF-16 (with its byte order mark), UTF-8 (with one) or PDFDocEncoding /// (ISO 32000-1 Annex D, Table D.2). pub(crate) fn text_string(b: &[u8]) -> String { if let Some(rest) = b.strip_prefix(&[0xFE, 0xFF]) { let units: Vec = rest.as_chunks::<2>().0.iter().map(|c| u16::from_be_bytes(*c)).collect(); return String::from_utf16_lossy(&units); } if let Some(rest) = b.strip_prefix(&[0xEF, 0xBB, 0xBF]) { return String::from_utf8_lossy(rest).into_owned(); } b.iter().map(|&c| pdf_doc_char(c)).collect() } /// Byte `c` of a PDFDocEncoding string. It matches Latin-1 except at 0x18 to 0x1F (accents) and /// 0x80 to 0xA0 (punctuation, ligatures, letters and the euro sign); 0x9F has no character and /// stays as it is. fn pdf_doc_char(c: u8) -> char { const ACCENTS: [char; 8] = ['\u{2D8}', '\u{2C7}', '\u{2C6}', '\u{2D9}', '\u{2DD}', '\u{2DB}', '\u{2DA}', '\u{2DC}']; const HIGH: [char; 33] = [ '\u{2022}', '\u{2020}', '\u{2021}', '\u{2026}', '\u{2014}', '\u{2013}', '\u{192}', '\u{2044}', '\u{2039}', '\u{203A}', '\u{2212}', '\u{2030}', '\u{201E}', '\u{201C}', '\u{201D}', '\u{2018}', '\u{2019}', '\u{201A}', '\u{2122}', '\u{FB01}', '\u{FB02}', '\u{141}', '\u{152}', '\u{160}', '\u{178}', '\u{17D}', '\u{131}', '\u{142}', '\u{153}', '\u{161}', '\u{17E}', '\u{9F}', '\u{20AC}', ]; let mapped = match c { 0x18..=0x1F => ACCENTS.get(usize::from(c - 0x18)), 0x80..=0xA0 => HIGH.get(usize::from(c - 0x80)), _ => None, }; mapped.copied().unwrap_or(char::from(c)) } /// One page as the interpreter walks it. #[derive(Default)] pub(crate) struct Scan { /// Each marked-content sequence: its tag ([`tag_key`]) and the group (index into /// [`Ocgs::list`]) it marks. pub tags: Vec<(u64, Option)>, /// Content was incomplete or a scan limit was reached. No document may be returned from /// this scan, since its routing no longer describes every object the interpreter draws. pub cut: bool, /// Each fill and each stroke of a path the interpreter draws (a fill-and-stroke operator is /// a fill, then a stroke; `sh` is a fill): whether it overprints. pub paints: Vec, /// How the text shown before each of [`Self::paints`] (since the one before it) overprints, /// and last the text after them all: one more entry than `paints`. pub texts: Vec, /// Something on the page overprints (when nothing does, the overprint flags in `paints` and /// `texts` aren't read). pub overprints: bool, /// Something overprints with overprint mode 0, where a `DeviceCMYK` colour's zero components /// knock out; the document's overprint is the nonzero one (mode 1). pub zero_mode: bool, /// The colorant names of the Separation and DeviceN spaces the page sets (`cs`, `CS`), and of /// those of the shading patterns it sets (`scn`, `SCN`) and of the shadings it draws (`sh`), /// each once (none when the page uses more than [`MAX_INKS`] inks or changes ink more than /// [`MAX_INK_CHANGES`] times). pub inks: Vec>, /// The inks each of [`Self::paints`] is painted in. paint_inks: Changes, /// The inks of the text before each of [`Self::paints`], counted as [`Self::texts`] is: of its /// fills and of its strokes (indices into [`Self::inks`]), where all of its text shown in an /// ink is shown in the same one. text_inks: Changes<[Option; 2]>, } impl Scan { /// The colorant names of the ink the walk found `at` painted in: a path's, or the fills' (the /// `stroke`s') of the text before a path. `pattern`: the interpreter paints with a pattern, /// whose shading's ink it is (text painted with one isn't followed); else the color space's. pub fn ink(&self, at: Walked, stroke: bool, pattern: bool) -> Option<&[String]> { let i = match at { Walked::Path(k) => { let inks = self.paint_inks.at(k); if pattern { inks.pattern } else { inks.space } } Walked::Text(_) if pattern => None, Walked::Text(k) => self.text_inks.at(k)[usize::from(stroke)], }?; self.inks.get(usize::try_from(i).ok()?).map(Vec::as_slice) } } /// The inks a fill or a stroke paints in (indices into [`Scan::inks`]): that of the color space /// set last (`cs`, `CS`), and that of the shading of the pattern named last (`scn`, `SCN`). The /// interpreter's paint, a color or a pattern, says which of the two it is. #[derive(Clone, Copy, Debug, Default, PartialEq)] struct Inks { space: Option, pattern: Option, } /// What the interpreter draws, counted as the walk counts it. #[derive(Clone, Copy, Debug)] pub(crate) enum Walked { /// The `k`-th path ([`Scan::paints`]). Path(usize), /// The text shown before the `k`-th path ([`Scan::texts`]). Text(usize), } /// A value for each index from 0, kept as the indices where it changes (the default before the /// first). #[derive(Default)] struct Changes(Vec<(usize, T)>); impl Changes { /// The value is `v` from index `at` on (`at` past the indices set before). `false`: the value /// changes there and [`MAX_INK_CHANGES`] changes are kept already, so this one isn't kept. fn set(&mut self, at: usize, v: T) -> bool { if self.0.last().map_or(T::default(), |(_, last)| *last) == v { return true; } if self.0.len() >= MAX_INK_CHANGES { return false; } self.0.push((at, v)); true } /// The value at index `k`. fn at(&self, k: usize) -> T { let n = self.0.partition_point(|(at, _)| *at <= k); n.checked_sub(1).and_then(|i| self.0.get(i)).map_or(T::default(), |(_, v)| *v) } } /// The ink of the text shown between two painted paths: of its fills, or of its strokes. #[derive(Clone, Copy, Debug, Default, PartialEq)] enum RunInk { /// No text is shown in an ink. #[default] None, /// All of the text shown in an ink is shown in this one. One(u32), /// Text is shown in several inks, which can't be told apart by glyph. Several, } impl RunInk { /// With more text shown in `ink` (`None`: in no ink). fn and(self, ink: Option) -> Self { match (self, ink) { (_, None) => self, (Self::None, Some(i)) => Self::One(i), (Self::One(j), Some(i)) if i == j => self, _ => Self::Several, } } fn ink(self) -> Option { match self { Self::One(i) => Some(i), _ => None, } } } /// How the text shown between two painted paths overprints. #[derive(Clone, Copy, Debug, Default, PartialEq)] pub(crate) enum TextOverprint { /// No text is shown there. #[default] None, /// All of it is shown with these parameters. All { fill: bool, stroke: bool }, /// Text is shown with different parameters, which can't be told apart by glyph. Mixed, } /// What the walk follows of the graphics state: the overprint parameters (ISO 32000-1 §8.6.7; /// `OP`, `op` and `OPM` in Table 58) and the inks fills and strokes paint in. #[derive(Clone, Copy, Default)] struct State { stroke: bool, fill: bool, /// Overprint mode 1 (nonzero overprint). nonzero: bool, /// The inks of fills and of strokes. inks: [Inks; 2], } /// A marked-content tag as [`Scan::tags`] keeps it: its FNV-1a hash (the tags are only compared, /// and a page can have millions). pub(crate) fn tag_key(tag: &[u8]) -> u64 { tag.iter().fold(0xcbf2_9ce4_8422_2325, |h, &b| (h ^ u64::from(b)).wrapping_mul(0x0100_0000_01b3)) } /// [`MAX_OPS`], lowered by tests that need a page past it. #[cfg(not(test))] fn max_ops() -> usize { MAX_OPS } #[cfg(test)] thread_local! { pub(crate) static TEST_MAX_OPS: std::cell::Cell = const { std::cell::Cell::new(MAX_OPS) }; } #[cfg(test)] fn max_ops() -> usize { TEST_MAX_OPS.with(std::cell::Cell::get) } #[cfg(not(test))] fn max_window() -> usize { MAX_WINDOW } #[cfg(test)] thread_local! { pub(crate) static TEST_MAX_WINDOW: std::cell::Cell = const { std::cell::Cell::new(MAX_WINDOW) }; } #[cfg(test)] fn max_window() -> usize { TEST_MAX_WINDOW.with(std::cell::Cell::get) } pub(crate) fn incomplete_content() -> PdfError { PdfError::Parse("PDF content is incomplete or exceeds the reader's limits; no document was imported".into()) } /// Whitespace and comments after the final instruction do not make an incomplete tail. fn blank_tail(mut bytes: &[u8]) -> bool { loop { let first = bytes.iter().position(|b| !matches!(b, 0 | 9 | 10 | 12 | 13 | 32)).unwrap_or(bytes.len()); bytes = bytes.get(first..).unwrap_or_default(); if bytes.first() == Some(&b'%') { let end = bytes.iter().position(|b| matches!(b, b'\r' | b'\n')).unwrap_or(bytes.len()); bytes = bytes.get(end..).unwrap_or_default(); } else { return bytes.is_empty(); } } } struct Walker<'w, 'p> { ocgs: &'w mut Ocgs, /// The groups that are off for the interpreter (none once [`all_on`] turned them on). off: HashSet, xref: &'p craft_pdf::syntax::xref::XRef, fonts: &'w mut FontNames, visible: Vec, ops: usize, max_ops: usize, max_form_bytes: usize, /// The bytes of form content walked so far ([`MAX_FORM_CONTENT`]). form_content: usize, out: Scan, /// A form XObject hidden by its own optional content was met. hidden_form: bool, /// The saved graphics states' overprint parameters and inks, the current one last, pushed and /// popped as the interpreter does its states. states: Vec, /// The `q`s past [`MAX_STATES`] whose `Q` has not come. unkept: usize, /// The text shown since the last painted path. text: TextOverprint, /// The inks of the text shown since the last painted path: of its fills, of its strokes. text_inks: [RunInk; 2], /// The index in [`Scan::inks`] of each list of colorant names. ink_index: HashMap, u32>, /// More inks or changes of ink than are kept: the page's inks are left out. inks_cut: bool, } impl Walker<'_, '_> { fn is_visible(&self) -> bool { self.visible.last().copied().unwrap_or(true) } fn begin(&mut self, on: bool) { let v = self.is_visible() && on; self.visible.push(v); } /// Enter optional content `d` (object `id`): a group, or a membership dictionary whose policy /// decides from its groups. fn begin_oc(&mut self, d: &Dict<'_>, id: Option) { let membership = id.is_none() || d.get::>(b"Type").is_some_and(|t| t.as_ref() == b"OCMD"); let on = match id { Some(id) if !membership => !self.off.contains(&id), _ => { let ids: Vec = match d.get::>(b"OCGs") { Some(a) => items(&a).filter_map(|i| i.as_obj_ref()).map(ObjectIdentifier::from).collect(), None => d.get_ref(b"OCGs").map(ObjectIdentifier::from).into_iter().collect(), }; let on = |i: &ObjectIdentifier| !self.off.contains(i); ids.is_empty() || match d.get::>(b"P").as_deref() { Some(b"AllOn") => ids.iter().all(on), Some(b"AnyOff") => !ids.iter().all(on), Some(b"AllOff") => !ids.iter().any(on), _ => ids.iter().any(on), } } }; self.begin(on); } fn state(&self) -> State { self.states.last().copied().unwrap_or_default() } fn save(&mut self) { // Past this many nested states the new ones are not kept (a stream of `q` is cheap to write // and holds a state each); their `Q`s are counted off first. if self.states.len() >= MAX_STATES { self.unkept = self.unkept.saturating_add(1); return; } self.states.push(self.state()); } /// The interpreter never pops its first state. fn restore(&mut self) { if self.unkept > 0 { self.unkept -= 1; } else if self.states.len() > 1 { self.states.pop(); } } fn overprinting(&mut self, on: bool) { self.out.overprints |= on; self.out.zero_mode |= on && !self.state().nonzero; } /// The interpreter draws a path: a fill (or a `stroke`) of the current one. fn painted(&mut self, stroke: bool) { if !self.is_visible() { return; } let s = self.state(); let on = if stroke { s.stroke } else { s.fill }; self.overprinting(on); let k = self.out.paints.len(); self.out.paints.push(on); self.out.texts.push(std::mem::take(&mut self.text)); self.inks_cut |= !self.out.paint_inks.set(k, s.inks[usize::from(stroke)]); self.inks_cut |= !self.out.text_inks.set(k, std::mem::take(&mut self.text_inks).map(RunInk::ink)); } /// The interpreter shows text. fn shown(&mut self) { if !self.is_visible() { return; } let s = self.state(); self.overprinting(s.fill || s.stroke); let now = TextOverprint::All { fill: s.fill, stroke: s.stroke }; self.text = if self.text == TextOverprint::None || self.text == now { now } else { TextOverprint::Mixed }; // Text painted with a pattern isn't followed, so only the color spaces' inks count. for (run, inks) in self.text_inks.iter_mut().zip(s.inks) { *run = run.and(inks.space); } } /// The color space of fills (or of `stroke`s) is in ink `ink` from now on. fn set_space(&mut self, stroke: bool, ink: Option) { if let Some(s) = self.states.last_mut() { s.inks[usize::from(stroke)].space = ink; } } /// The pattern of fills (or of `stroke`s) is in ink `ink` from now on. fn set_pattern(&mut self, stroke: bool, ink: Option) { if let Some(s) = self.states.last_mut() { s.inks[usize::from(stroke)].pattern = ink; } } /// The ink of color space `name` (`cs`, `CS`): a Separation or DeviceN space of the resources. /// The names the interpreter reads as device and pattern spaces come first, as it reads them. fn space_ink(&mut self, name: &Name<'_>, res: &Resources<'_>) -> Option { if matches!(name.as_ref(), b"DeviceRGB" | b"RGB" | b"DeviceGray" | b"G" | b"DeviceCMYK" | b"CMYK" | b"CalCMYK" | b"Pattern") { return None; } self.ink_of(&res.get_color_space(name)?) } /// The ink pattern `name` paints in (`scn`, `SCN`): a shading pattern's (a dictionary, where a /// tiling pattern is a stream) is its shading's. fn pattern_ink(&mut self, name: &Name<'_>, res: &Resources<'_>) -> Option { let Object::Dict(p) = res.get_pattern(name)? else { return None }; self.shading_ink(&p.get::>(b"Shading")?) } /// The ink of a shading (a dictionary or a stream): its color space's. fn shading_ink(&mut self, shading: &Object<'_>) -> Option { let (d, _) = dict_or_stream(shading)?; self.ink_of(&d.get::>(b"ColorSpace")?) } /// The index in [`Scan::inks`] of color space `o` when it is a Separation or DeviceN array, its /// colorant names noted when new. fn ink_of(&mut self, o: &Object<'_>) -> Option { let Object::Array(a) = o else { return None }; let (_, names) = ink_names(&mut a.flex_iter())?; if let Some(i) = self.ink_index.get(&names) { return Some(*i); } if self.out.inks.len() >= MAX_INKS { self.inks_cut = true; return None; } let i = u32::try_from(self.out.inks.len()).ok()?; self.ink_index.insert(names.clone(), i); self.out.inks.push(names); Some(i) } fn note_fonts(&mut self, res: &Resources<'_>) { for key in res.fonts.keys() { if let Some(f) = res.fonts.get::>(key.as_ref()) && let Some(name) = font_name(&f) { self.fonts.entry(f.cache_key()).or_insert_with(|| { let embedded = embeds_glyphs(&f); FontNote { name, embedded, vertical: vertical_metrics(&f, embedded) } }); } } } fn walk(&mut self, data: &[u8], res: &Resources<'_>, depth: u32) { let mut iter = TypedIter::new(data); self.note_fonts(res); // The interpreter saves the state for each content stream, and restores what the stream // left saved when it ends. let saved = self.states.len(); self.save(); self.walk_ops(&mut iter, res, depth); self.out.cut |= !blank_tail(data.get(iter.committed()..).unwrap_or_default()); self.states.truncate(saved); } /// Scan the same stream windows as the interpreter without materializing the page. /// Graphics and optional-content state live across windows; page state is saved once. fn page(&mut self, page: &Page<'_>) { let resources = page.resources(); self.note_fonts(resources); let saved = self.states.len(); self.save(); let mut content = page.content_reader(); let mut window = Vec::new(); let limit = max_window(); let mut exhausted = false; loop { let (committed, needs_more) = { let mut ops = TypedIter::new_window(&window, exhausted); self.walk_ops(&mut ops, resources, 0); (ops.committed(), ops.needs_more()) }; if committed > window.len() { self.out.cut = true; break; } window.drain(..committed); if self.out.cut { break; } if !needs_more || exhausted { // No success on an undecidable tail or on a parser stop before the source ends. // The typed iterator does not expose every malformed-operator failure. self.out.cut |= !exhausted || !blank_tail(&window); break; } if window.len() >= limit { self.out.cut = true; break; } let want = window.len().max(CONTENT_WINDOW).min(limit.saturating_sub(window.len())); if window.try_reserve(want).is_err() { self.out.cut = true; break; } if content.read_into(&mut window, want) == 0 { exhausted = true; } } self.out.cut |= content.truncated(); self.states.truncate(saved); } fn walk_ops(&mut self, iter: &mut TypedIter<'_>, res: &Resources<'_>, depth: u32) { while let Some(op) = iter.next() { self.ops += 1; if self.ops > self.max_ops || self.out.tags.len() >= MAX_TAGS { self.out.cut = true; return; } match op { TypedInstruction::BeginMarkedContentWithProperties(bdc) => { let oc = match bdc.1 { Object::Name(n) => { res.properties.get_ref(n.as_ref()).map(|r| (res.properties.get::>(n.as_ref()).unwrap_or_default(), r)) } o => dict_or_stream(o) .and_then(|(props, _)| props.get_ref(b"OC").map(|r| (props.get::>(b"OC").unwrap_or_default(), r))), }; let group = match oc { Some((d, r)) => { let id = ObjectIdentifier::from(r); self.begin_oc(&d, Some(id)); self.ocgs.group_of(&d, id, self.xref) } None => { self.begin(true); None } }; self.out.tags.push((tag_key(bdc.0.as_ref()), group)); } TypedInstruction::BeginMarkedContent(bmc) => { self.begin(true); self.out.tags.push((tag_key(bmc.0.as_ref()), None)); } TypedInstruction::EndMarkedContent(_) => { self.visible.pop(); } TypedInstruction::SaveState(_) => self.save(), TypedInstruction::RestoreState(_) => self.restore(), TypedInstruction::SetGraphicsState(gs) => { if let Some(d) = res.get_ext_g_state(gs.0) && let Some(s) = self.states.last_mut() { let stroke = d.get::(b"OP"); s.stroke = stroke.unwrap_or(s.stroke); // Without `/op`, `/OP` sets both. s.fill = d.get::(b"op").or(stroke).unwrap_or(s.fill); s.nonzero = d.get::(b"OPM").map_or(s.nonzero, |m| m != 0); } } TypedInstruction::FillPathEvenOdd(_) | TypedInstruction::FillPathNonZero(_) | TypedInstruction::FillPathNonZeroCompatibility(_) => { self.painted(false); } TypedInstruction::StrokePath(_) | TypedInstruction::CloseAndStrokePath(_) => self.painted(true), TypedInstruction::FillAndStrokeEvenOdd(_) | TypedInstruction::FillAndStrokeNonZero(_) | TypedInstruction::CloseFillAndStrokeEvenOdd(_) | TypedInstruction::CloseFillAndStrokeNonZero(_) => { self.painted(false); self.painted(true); } // Drawn when the interpreter can read the shading (the page's count tells), as a // fill in a state of its own, in the shading's ink. TypedInstruction::Shading(s) => { if let Some(sh) = res.get_shading(s.0) { let ink = self.shading_ink(&sh); self.save(); self.set_pattern(false, ink); self.painted(false); self.restore(); } } TypedInstruction::ColorSpaceNonStroke(cs) => { let ink = self.space_ink(cs.0, res); self.set_space(false, ink); } TypedInstruction::ColorSpaceStroke(cs) => { let ink = self.space_ink(cs.0, res); self.set_space(true, ink); } // A pattern by name; without one, `scn` sets a color in the space set before. TypedInstruction::NonStrokeColorNamed(c) => { if let Some(name) = c.1 { let ink = self.pattern_ink(name, res); self.set_pattern(false, ink); } } TypedInstruction::StrokeColorNamed(c) => { if let Some(name) = c.1 { let ink = self.pattern_ink(name, res); self.set_pattern(true, ink); } } TypedInstruction::NonStrokeColorDeviceGray(_) | TypedInstruction::NonStrokeColorDeviceRgb(_) | TypedInstruction::NonStrokeColorCmyk(_) => { self.set_space(false, None); } TypedInstruction::StrokeColorDeviceGray(_) | TypedInstruction::StrokeColorDeviceRgb(_) | TypedInstruction::StrokeColorCmyk(_) => { self.set_space(true, None); } TypedInstruction::ShowText(_) | TypedInstruction::ShowTexts(_) | TypedInstruction::NextLineAndShowText(_) | TypedInstruction::ShowTextWithParameters(_) => self.shown(), TypedInstruction::XObject(x) => { if !self.is_visible() { continue; } let Some(s) = res.get_x_object(x.0) else { continue }; let d = s.dict(); if d.get::>(b"Subtype").as_deref() != Some(b"Form") || d.get::<[f32; 4]>(b"BBox").is_none() { continue; } if depth >= MAX_DEPTH { self.out.cut = true; return; } let Ok(data) = s.decoded() else { self.out.cut = true; return; }; self.form_content = self.form_content.saturating_add(data.len()); if self.form_content > MAX_FORM_CONTENT { self.out.cut = true; return; } // Ordinary filtered form decoders may return a prefix at their limit. // Reaching that bound cannot certify a complete form, even at an operator boundary. if !s.filters().is_empty() && data.len() >= self.max_form_bytes { self.out.cut = true; return; } let oc = d.get::>(b"OC"); if let Some(oc) = &oc { self.begin_oc(oc, d.get_ref(b"OC").map(ObjectIdentifier::from)); self.hidden_form |= !self.is_visible(); } if self.is_visible() { // (Its own resources, or without any those of what draws it: as the interpreter reads them.) let inner = Resources::from_scope(d.get::>(b"Resources"), res.clone()); self.save(); self.walk(data.as_ref(), &inner, depth + 1); self.restore(); if self.out.cut { return; } } if oc.is_some() { self.visible.pop(); } } _ => {} } } } } /// The fonts of a document by cache key. pub(crate) type FontNames = HashMap; /// What a document says of one of its fonts. #[derive(Debug)] pub(crate) struct FontNote { /// Its base (PostScript) font name. pub name: String, /// The file carries its glyphs ([`embeds_glyphs`]). pub embedded: bool, /// A composite font written vertically: its descendant's metrics. pub vertical: Option, } /// The most CIDs a vertical font's widths are read for (the most a CID-keyed font has). const MAX_CIDS: u32 = 65_536; /// A vertical composite font's metrics (ISO 32000-1 §9.7.4.3), in glyph space (1000 units an em): /// where each glyph's position vector puts the column's centre. #[derive(Debug, Default)] pub(crate) struct Vertical { /// The default width (`DW`, w0). default: f64, /// Widths by CID (`W`). widths: HashMap, /// The position vector's x by CID (`W2`'s vx). vx: HashMap, /// The CID of each glyph: `None` when they can't be told (a CID-keyed CFF font's charset /// decides, or a font the file doesn't carry is drawn with a stand-in's glyphs). cids: Option, } /// How glyph ids map back to CIDs. #[derive(Debug)] enum Cids { /// The same (`CIDToGIDMap` `/Identity`, or none). Identity, /// The first CID of each glyph a `CIDToGIDMap` stream maps. Map(HashMap), } impl Vertical { /// How far right of glyph `gid`'s origin (glyph space) the column's centre is: the position /// vector's vx from `W2`, else half the glyph's width (w0 from `W`, or `DW`). pub fn centre(&self, gid: u32) -> f64 { let cid = match &self.cids { Some(Cids::Identity) => Some(gid), Some(Cids::Map(m)) => m.get(&gid).copied(), None => None, }; let w0 = |cid: Option| cid.and_then(|c| self.widths.get(&c)).copied().unwrap_or(self.default) * 0.5; cid.and_then(|c| self.vx.get(&c)).copied().unwrap_or_else(|| w0(cid)) } } /// The vertical metrics of font `f` when it is a composite font written vertically (its CMap's /// writing mode is 1), from its descendant CIDFont. `embedded`: the file carries its glyphs. pub(crate) fn vertical_metrics(f: &Dict<'_>, embedded: bool) -> Option { if f.get::>(b"Subtype")?.as_ref() != b"Type0" { return None; } let vertical = match f.get::>(b"Encoding") { Some(n) => n.as_ref().ends_with(b"-V"), None => f.get::>(b"Encoding").is_some_and(|s| s.dict().get::(b"WMode") == Some(1)), }; if !vertical { return None; } let d = f.get::>(b"DescendantFonts").and_then(|a| a.iter::>().next())?; let finite = |v: f64| v.is_finite() && v.abs() < 1e5; let default = d.get::(b"DW").filter(|w| finite(*w)).unwrap_or(1000.0); let mut widths = HashMap::new(); if let Some(w) = d.get::>(b"W") { cid_ranges(&w, 1, |cid, v| { if let [w0] = v { widths.insert(cid, *w0); } }); } let mut vx = HashMap::new(); if let Some(w2) = d.get::>(b"W2") { cid_ranges(&w2, 3, |cid, v| { if let [_, x, _] = v { vx.insert(cid, *x); } }); } widths.retain(|_, w| finite(*w)); vx.retain(|_, x| finite(*x)); let true_type = d.get::>(b"Subtype").is_some_and(|s| s.as_ref() == b"CIDFontType2"); let cids = if !embedded || !true_type { None } else if let Some(s) = d.get::>(b"CIDToGIDMap") { // Two bytes a CID, the glyph it maps to. let data = s.decoded().ok()?; let mut m = HashMap::new(); for (cid, g) in (0..MAX_CIDS).zip(data.as_chunks::<2>().0) { m.entry(u32::from(u16::from_be_bytes(*g))).or_insert(cid); } Some(Cids::Map(m)) } else { Some(Cids::Identity) }; Some(Vertical { default, widths, vx, cids }) } /// Each CID a `W` or `W2` array (`per` numbers a CID) gives values for, and its values: `c [v…]` /// gives CIDs from c on their values in turn, `c_first c_last v…` the same values to each of the /// range. At most [`MAX_CIDS`] CIDs are read. fn cid_ranges(a: &Array<'_>, per: usize, mut each: impl FnMut(u32, &[f64])) { let mut it = a.flex_iter(); let mut read = 0; loop { if let Some((first, list)) = it.next::<(u32, Array<'_>)>() { let values: Vec = list.iter::().collect(); for (cid, v) in (first..=u32::MAX).zip(values.chunks_exact(per)) { read += 1; if read > MAX_CIDS { return; } each(cid, v); } } else if per == 1 && let Some((first, last, w)) = it.next::<(u32, u32, f64)>() { for cid in first..=last.min(first.saturating_add(MAX_CIDS)) { read += 1; if read > MAX_CIDS { return; } each(cid, &[w]); } } else if per == 3 && let Some((first, last, w, x, y)) = it.next::<(u32, u32, f64, f64, f64)>() { for cid in first..=last.min(first.saturating_add(MAX_CIDS)) { read += 1; if read > MAX_CIDS { return; } each(cid, &[w, x, y]); } } else { return; } } } /// The PostScript name of font `f`: its `BaseFont`. A composite (Type 0) font's `BaseFont` is its /// CIDFont's name, a hyphen and its CMap's (`Name-Identity-H`, ISO 32000-1 §9.7.6.1), so its /// descendant CIDFont's `BaseFont` is taken, the font program's own name (§9.7.4.1); without one, /// the `BaseFont` without the CMap's name on its end. fn font_name(f: &Dict<'_>) -> Option { let base = String::from_utf8_lossy(f.get::>(b"BaseFont")?.as_ref()).into_owned(); if f.get::>(b"Subtype").is_none_or(|s| s.as_ref() != b"Type0") { return Some(base); } let descendant = f.get::>(b"DescendantFonts").and_then(|a| a.iter::>().next()); if let Some(name) = descendant.and_then(|d| d.get::>(b"BaseFont")).filter(|n| !n.as_ref().is_empty()) { return Some(String::from_utf8_lossy(name.as_ref()).into_owned()); } let cmap = f.get::>(b"Encoding").map(|n| String::from_utf8_lossy(n.as_ref()).into_owned()); let stripped = cmap.and_then(|c| base.strip_suffix(c.as_str())?.strip_suffix('-').filter(|b| !b.is_empty()).map(str::to_string)); Some(stripped.unwrap_or(base)) } /// Whether font `f` carries its own glyphs: a font program in its descriptor (a composite font's in /// its descendant's), or a Type 3 font's glyph procedures. A font that doesn't is drawn with a /// stand-in, whose glyphs say nothing about which installed face the document means. fn embeds_glyphs(f: &Dict<'_>) -> bool { if f.get::>(b"Subtype").is_some_and(|s| s.as_ref() == b"Type3") { return true; } // A font program is a stream, so always an indirect object. let program = |d: &Dict<'_>| { d.get::>(b"FontDescriptor").is_some_and(|fd| [&b"FontFile"[..], b"FontFile2", b"FontFile3"].iter().any(|k| fd.get_ref(*k).is_some())) }; program(f) || f.get::>(b"DescendantFonts").and_then(|a| a.iter::>().next()).is_some_and(|d| program(&d)) } /// Walk `page` as the interpreter will: `all_on` when [`all_on`] turned every group on. Fonts /// found are noted in `fonts` (their cache key → base font name, and whether the file carries its /// glyphs). pub(crate) fn scan_page(page: &Page<'_>, ocgs: &mut Ocgs, all_on: bool, fonts: &mut FontNames, max_form_bytes: usize) -> Result { let scan = walk_page(page, ocgs, all_on, fonts, max_form_bytes).out; if scan.cut { Err(incomplete_content()) } else { Ok(scan) } } fn walk_page<'w, 'p>(page: &Page<'p>, ocgs: &'w mut Ocgs, all_on: bool, fonts: &'w mut FontNames, max_form_bytes: usize) -> Walker<'w, 'p> { let off = if all_on { HashSet::new() } else { ocgs.off.clone() }; let mut w = Walker { ocgs, off, xref: page.xref(), fonts, visible: vec![], ops: 0, max_ops: max_ops(), max_form_bytes, form_content: 0, out: Scan::default(), hidden_form: false, states: vec![State::default()], unkept: 0, text: TextOverprint::None, text_inks: [RunInk::None; 2], ink_index: HashMap::new(), inks_cut: false, }; w.page(page); w.inks_cut |= !w.out.text_inks.set(w.out.paints.len(), w.text_inks.map(RunInk::ink)); if w.inks_cut { w.out.inks.clear(); } w.out.texts.push(w.text); w } /// Does `page` draw a form XObject that is off by its own optional content (not a marked-content /// sequence)? Its art couldn't be told apart by group once every group is on. pub(crate) fn hides_forms(page: &Page<'_>, ocgs: &mut Ocgs, max_form_bytes: usize) -> Result { let mut fonts = HashMap::new(); let walk = walk_page(page, ocgs, false, &mut fonts, max_form_bytes); if walk.out.cut { Err(incomplete_content()) } else { Ok(walk.hidden_form) } } /// `bytes` with an update appended that drops the catalog's `/OCProperties`, so every group /// draws; `None` when the file's structure can't take one. pub(crate) fn all_on(bytes: &[u8], pdf: &Pdf) -> Option> { let root = pdf.xref().root_id(); let catalog = pdf.xref().get::>(root)?; let body = catalog.data().strip_suffix(b">>")?; let prev = last_xref(bytes)?; let trailer = trailer_at(bytes, prev)?; let inner = trailer.strip_prefix(b"<<")?.strip_suffix(b">>")?; let mut out = bytes.to_vec(); if !out.ends_with(b"\n") { out.push(b'\n'); } let at = out.len(); out.extend(format!("{} {} obj\n", root.obj_number, root.gen_number).bytes()); out.extend_from_slice(body); // A later key wins: the groups' configuration is gone, so the interpreter draws them all. out.extend_from_slice(b" /OCProperties null >>\nendobj\n"); let xref = out.len(); out.extend(format!("xref\n{} 1\n{at:010} {:05} n \ntrailer\n<<", root.obj_number, root.gen_number).bytes()); out.extend_from_slice(inner); out.extend(format!(" /Prev {prev} >>\nstartxref\n{xref}\n%%EOF\n").bytes()); Some(out) } /// The offset the file's last `startxref` points to. fn last_xref(bytes: &[u8]) -> Option { let at = bytes.windows(9).rposition(|w| w == b"startxref")?; let rest = bytes.get(at + 9..)?; let digits: String = rest.iter().skip_while(|c| c.is_ascii_whitespace()).take_while(|c| c.is_ascii_digit()).map(|&c| c as char).collect(); digits.parse().ok().filter(|&p: &usize| p < bytes.len()) } /// The trailer dictionary of the cross-reference section at `at` (a table's `trailer`, or a /// cross-reference stream's dictionary), as written. fn trailer_at(bytes: &[u8], at: usize) -> Option<&[u8]> { let rest = bytes.get(at..)?; let from = if rest.trim_ascii_start().starts_with(b"xref") { rest.windows(7).position(|w| w == b"trailer")? } else { 0 }; let open = from + rest.get(from..)?.windows(2).position(|w| w == b"<<")?; let d = Dict::from_bytes(rest.get(open..)?)?; Some(d.data()) }