diff --git a/crates/redact/src/interp.rs b/crates/redact/src/interp.rs index 460085a320..9b0dc842cb 100644 --- a/crates/redact/src/interp.rs +++ b/crates/redact/src/interp.rs @@ -68,6 +68,9 @@ struct Gs { scale: f64, leading: f64, rise: f64, + /// A Type 3 font's glyph box in text space per unit of font size: its `/FontBBox` through its + /// whole `/FontMatrix`, translation included, which the metrics' box leaves out. + glyph_box: Option<[f64; 4]>, } /// What processing a scope produced. @@ -205,6 +208,24 @@ fn is_vertical(doc: &Document, font: &Dict) -> bool { } } +/// Where a Type 3 font's glyphs can paint, in text space per unit of font size: its `/FontBBox` +/// through its whole `/FontMatrix`. The renderer applies the whole matrix, translation and skew +/// included, so a box from its scale alone can lie beside the glyph a mark covers. +fn type3_glyph_box(doc: &Document, font: &Dict) -> Option<[f64; 4]> { + if font.name(b"Subtype") != Some(b"Type3".as_slice()) { + return None; + } + let numbers = |key: &[u8]| -> Vec { + font.get(key) + .map(|v| doc.resolve(v)) + .and_then(|v| v.as_array().map(|a| a.iter().filter_map(|x| doc.resolve(x).as_f64()).collect())) + .unwrap_or_default() + }; + let (matrix, bbox) = (numbers(b"FontMatrix"), numbers(b"FontBBox")); + let (&[a, b, c, d, e, f], &[x0, y0, x1, y1]) = (matrix.as_slice(), bbox.as_slice()) else { return None }; + Some(Matrix([a, b, c, d, e, f]).bbox([x0.min(x1), y0.min(y1), x0.max(x1), y0.max(y1)])) +} + fn fresh_name(scope: &mut Scope<'_>, xobjects: &Dict) -> Vec { let mut i = scope.used_names.len() + 1; loop { @@ -238,6 +259,7 @@ pub(crate) fn process(doc: &mut Document, scope: &mut Scope<'_>, streams: &[Vec< scale: 1.0, leading: 0.0, rise: 0.0, + glyph_box: None, }; process_from(doc, scope, streams, resources, gs) } @@ -456,8 +478,10 @@ impl Walk { } b"Tf" => self.select_font(doc, scope, op), _ => { - if let Some((font, vertical, size)) = op.name(0).and_then(|n| extgstate_font(doc, scope, &self.ext_gstates, n, &self.fallback)) { - (self.gs.font, self.gs.vertical, self.gs.size) = (font, vertical, size); + if let Some((font, vertical, size, glyph_box)) = + op.name(0).and_then(|n| extgstate_font(doc, scope, &self.ext_gstates, n, &self.fallback)) + { + (self.gs.font, self.gs.vertical, self.gs.size, self.gs.glyph_box) = (font, vertical, size, glyph_box); } } } @@ -468,6 +492,7 @@ impl Walk { let Some(name) = op.name(0) else { return }; let font_dict = self.fonts_res.get(name).and_then(|f| doc.resolve(f).as_dict().cloned()); self.gs.vertical = font_dict.as_ref().is_some_and(|d| is_vertical(doc, d)); + self.gs.glyph_box = font_dict.as_ref().and_then(|d| type3_glyph_box(doc, d)); self.gs.font = match scope.fonts.get(name) { Some(f) => f.clone(), None => { @@ -672,6 +697,11 @@ fn show(scope: &Scope<'_>, gs: &Gs, tm: &mut Matrix, items: &[Object]) -> (Vec, bool, f64, Option<[f64; 4]>); + /// The font, writing direction and size an ExtGState's `/Font [font size]` entry sets, as `Tf` /// would (ISO 32000-2 §8.4.5); `None` when `name` sets no font. The metrics are cached with the /// fonts `Tf` names, under a key no resource name can take (a name cannot hold a null byte). @@ -337,13 +340,14 @@ pub(super) fn extgstate_font( ext_gstates: &Dict, name: &[u8], fallback: &Rc, -) -> Option<(Rc, bool, f64)> { +) -> Option { let state = doc.resolve(ext_gstates.get(name)?).as_dict().cloned()?; let entry = doc.resolve(state.get(b"Font")?).as_array().cloned()?; let [font, size] = entry.as_slice() else { return None }; let size = doc.resolve(size).as_f64()?; let font_dict = doc.resolve(font).as_dict().cloned(); let vertical = font_dict.as_ref().is_some_and(|d| is_vertical(doc, d)); + let glyph_box = font_dict.as_ref().and_then(|d| type3_glyph_box(doc, d)); let key = [b"\0".as_slice(), name].concat(); let metrics = match scope.fonts.get(&key) { Some(m) => m.clone(), @@ -353,5 +357,5 @@ pub(super) fn extgstate_font( m } }; - Some((metrics, vertical, size)) + Some((metrics, vertical, size, glyph_box)) } diff --git a/crates/redact/src/tests/text_state.rs b/crates/redact/src/tests/text_state.rs index 4e888f262c..5d968fd402 100644 --- a/crates/redact/src/tests/text_state.rs +++ b/crates/redact/src/tests/text_state.rs @@ -25,3 +25,16 @@ fn a_form_inherits_the_text_state_of_the_page_that_draws_it() { let r = apply(&mut doc, None).unwrap(); assert_eq!((r.glyphs, r.forms_rewritten), (6, 1), "{r:?}"); } + +/// A Type 3 font whose matrix moves its glyphs up 5 pt (at 10 pt): the renderer draws the `A` at +/// y 105–115, but the box from the matrix's scale alone put it at y 99–110, so a mark over the +/// top of the drawn glyph neither removed it nor saw it left behind. +#[test] +fn a_type3_glyph_is_placed_with_its_whole_font_matrix() { + let font = b"<< /Type /Font /Subtype /Type3 /FontBBox [0 0 1000 1000] /FontMatrix [0.001 0 0 0.001 0 0.5] /FirstChar 65 /LastChar 65 /Widths [500] /Encoding << /Differences [65 /A] >> /CharProcs << /A 8 0 R >> >>"; + let mut doc = one_page(b"BT /T3 10 Tf 10 100 Td (A) Tj ET", "/Font << /T3 7 0 R >>", vec![font.to_vec(), stream("", b"0 0 1000 1000 re f")]); + let top = [5.0, 111.0, 20.0, 116.0]; + assert_eq!(under(&mut doc, 0, &[top]), 1, "the verifier sees the drawn glyph"); + mark(&mut doc, 0, &[top], ""); + assert_eq!(apply(&mut doc, None).unwrap().glyphs, 1); +}