From 60925a85b69999aecb34100728cc15ee8ee03539 Mon Sep 17 00:00:00 2001 From: Combiler01 Date: Thu, 15 Oct 2026 00:24:17 +0530 Subject: [PATCH] fix: address reviewer feedback regarding [GS operator related] --- Cargo.lock | 1 + crates/edit/Cargo.toml | 1 + crates/edit/src/images.rs | 83 ++++++++++++++++++++++++++-------- crates/edit/src/lib.rs | 2 +- crates/edit/src/tests.rs | 95 +++++++++++++++++++++++++++++++++++++++ 5 files changed, 163 insertions(+), 19 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index df58c57d21..52485e1e32 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4027,6 +4027,7 @@ dependencies = [ "pdfcraft-cos", "pdfcraft-fonts", "pdfcraft-model", + "pdfcraft-render", "thiserror 2.0.21", "unicode-bidi", ] diff --git a/crates/edit/Cargo.toml b/crates/edit/Cargo.toml index 4eb2d6132b..f04f5ba17e 100644 --- a/crates/edit/Cargo.toml +++ b/crates/edit/Cargo.toml @@ -22,3 +22,4 @@ unicode-bidi = "0.3" [dev-dependencies] hayro-syntax = { workspace = true } +pdfcraft-render = { workspace = true } diff --git a/crates/edit/src/images.rs b/crates/edit/src/images.rs index 56d6ea56d7..fdc62b026f 100644 --- a/crates/edit/src/images.rs +++ b/crates/edit/src/images.rs @@ -10,6 +10,27 @@ use std::sync::Arc; use crate::EditError; +/// The identity of a soft mask (`/SMask`) in the PDF graphics state. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum MaskIdentity { + /// Indirect reference to a mask dictionary or stream. + Ref(u32, u16), + /// Indirect reference to the transparency group (`/G`) in a mask dictionary. + Group(u32, u16), + /// Explicit named mask or resource name. + Named(Vec), +} + +impl std::fmt::Display for MaskIdentity { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + MaskIdentity::Ref(num, generation) => write!(f, "{num} {generation} R"), + MaskIdentity::Group(num, generation) => write!(f, "group {num} {generation} R"), + MaskIdentity::Named(name) => write!(f, "{}", String::from_utf8_lossy(name)), + } + } +} + /// An image drawn on a page. #[derive(Clone, Debug, PartialEq)] pub struct PageImage { @@ -30,6 +51,8 @@ pub struct PageImage { /// Pixel size; zero for vector/grouped Form artwork. pub width: u32, pub height: u32, + /// Active soft mask identity at the image placement, if any. + pub smask: Option, stream: usize, op: usize, /// CTM before `Do`, excluding a Form's own Matrix. @@ -213,12 +236,16 @@ impl ImageState { pub fn page_images(doc: &Document, page: usize) -> Result, EditError> { let p = page_of(doc, page)?; let xo = xobjects(doc, &p.dict); + let res = p.dict.get(b"Resources").map(|r| doc.resolve(r)).and_then(|r| r.as_dict().cloned()).unwrap_or_default(); + let ext_gstates = res.get(b"ExtGState").map(|g| doc.resolve(g)).and_then(|g| g.as_dict().cloned()).unwrap_or_default(); let mut out = Vec::new(); // The graphics state carries over from one stream to the next. let mut state = ImageState::new(Matrix::IDENTITY, p.crop(doc), Vec::new()); + let mut tracker = ExtendedGStateTracker::default(); for (si, (_, data)) in streams(doc, &p.dict).into_iter().enumerate() { for (i, op) in parse(&data).ops.iter().enumerate() { state.apply(op)?; + tracker.step(op, &ext_gstates, doc)?; let ctm = state.placement.matrix; if op.is("Do") { let Some(name) = op.name(0) else { continue }; @@ -274,6 +301,7 @@ pub fn page_images(doc: &Document, page: usize) -> Result, EditEr is_form, width: if is_form { 0 } else { s.dict.int(b"Width").unwrap_or(0).clamp(0, u32::MAX as i64) as u32 }, height: if is_form { 0 } else { s.dict.int(b"Height").unwrap_or(0).clamp(0, u32::MAX as i64) as u32 }, + smask: tracker.current_state.smask.clone(), stream: si, op: i, draw_matrix: ctm.0, @@ -347,6 +375,7 @@ pub fn reading_images(doc: &Document, page: usize) -> Result, Edi is_form: false, width: s.dict.int(b"Width").unwrap_or(0).max(0) as u32, height: s.dict.int(b"Height").unwrap_or(0).max(0) as u32, + smask: None, stream, op: i, draw_matrix: ctm.0, @@ -548,7 +577,7 @@ struct EffectiveGState { ca: Option, ca_stroke: Option, bm: Option>, - smask: Option>, + smask: Option, } impl EffectiveGState { @@ -564,11 +593,8 @@ impl EffectiveGState { self.bm.as_deref().unwrap_or(b"Normal") } - fn effective_smask(&self) -> Option<&[u8]> { - match self.smask.as_deref() { - Some(b"None") | None => None, - Some(other) => Some(other), - } + fn effective_smask(&self) -> Option<&MaskIdentity> { + self.smask.as_ref() } fn visually_differs(&self, other: &EffectiveGState) -> bool { @@ -578,7 +604,7 @@ impl EffectiveGState { || self.effective_smask() != other.effective_smask() } - fn merge_dict(&mut self, dict: &Dict, doc: &Document) { + fn merge_dict(&mut self, dict: &Dict, doc: &Document, gs_name: Option<&[u8]>) { if let Some(val) = dict.get(b"ca").map(|v| doc.resolve(v)).and_then(|v| v.as_f64()) { self.ca = Some(val); } @@ -594,11 +620,28 @@ impl EffectiveGState { self.bm = Some(first_name); } } - if let Some(smask_obj) = dict.get(b"SMask").map(|v| doc.resolve(v)) { - if let Some(name) = smask_obj.as_name() { - self.smask = Some(name.to_vec()); - } else if smask_obj.as_dict().is_some() { - self.smask = Some(b"CustomSMask".to_vec()); + if let Some(raw_smask) = dict.get(b"SMask") { + let resolved = doc.resolve(raw_smask); + if let Some(name) = resolved.as_name() { + if name == b"None" { + self.smask = None; + } else { + self.smask = Some(MaskIdentity::Named(name.to_vec())); + } + } else if let Some(mask_dict) = resolved.as_dict() { + if let Some(gr) = mask_dict.get(b"G").and_then(|g| g.as_ref()) { + self.smask = Some(MaskIdentity::Group(gr.num, gr.generation)); + } else if let Some(r) = raw_smask.as_ref() { + self.smask = Some(MaskIdentity::Ref(r.num, r.generation)); + } else if let Some(name) = mask_dict.get(b"Name").map(|n| doc.resolve(n)).and_then(|n| n.as_name().map(|s| s.to_vec())) { + self.smask = Some(MaskIdentity::Named(name)); + } else if let Some(name) = gs_name { + self.smask = Some(MaskIdentity::Named(name.to_vec())); + } else { + self.smask = Some(MaskIdentity::Named(b"CustomMask".to_vec())); + } + } else if let Some(r) = raw_smask.as_ref() { + self.smask = Some(MaskIdentity::Ref(r.num, r.generation)); } } } @@ -615,7 +658,7 @@ impl ExtendedGStateTracker { fn step(&mut self, op: &Op, ext_gstates: &Dict, doc: &Document) -> Result<(), EditError> { match op.op.as_slice() { b"q" => { - if self.stack.len() >= 256 { + if self.stack.len() >= 4096 { return Err(EditError::Invalid("the image graphics state is too deeply nested".into())); } self.stack.push((self.current_state.clone(), self.active_gs_ops.clone())); @@ -629,7 +672,7 @@ impl ExtendedGStateTracker { b"gs" => { if let Some(name) = op.name(0) { if let Some(dict) = ext_gstates.get(name).map(|v| doc.resolve(v)).and_then(|v| v.as_dict().cloned()) { - self.current_state.merge_dict(&dict, doc); + self.current_state.merge_dict(&dict, doc, Some(name)); } else if self.current_state.ca.is_none() { self.current_state.ca = Some(0.5); } @@ -672,7 +715,7 @@ fn extract_scope_state(ops: &[Op], start: usize, target: usize) -> Result