fix(redact): read text-state operands from the end, as the renderer does (#5951)

This commit is contained in:
Dylan M. Taylor authored and GitHub committed 2026-11-09 18:36:34 +08:00
1 parent 44a26f4b93
commit ca56433f5f
2 files changed
+31 -10

No files matched your search

+17 -10
View File
@@ -591,8 +591,10 @@ impl Walk {
}
fn select_font(&mut self, doc: &Document, scope: &mut Scope<'_>, op: &Op) {
self.gs.size = op.num(1).unwrap_or(self.gs.size);
let Some(name) = op.name(0) else { return };
// `/F1 12 Tf` takes its last two operands, as the renderer does: a surplus before them
// is ignored, not read in their place.
self.gs.size = op.nums::<1>().map_or(self.gs.size, |[size]| size);
let Some(name) = op.operands.len().checked_sub(2).and_then(|i| op.name(i)) 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.type3_inherits = font_dict.as_ref().is_some_and(|d| type3_without_resources(doc, d));
@@ -609,17 +611,20 @@ impl Walk {
/// Text state and text-positioning operators.
fn text_state(&mut self, op: &Op) {
let gs = &mut self.gs;
// The operand immediately before the operator, as the renderer reads it: surplus operands
// before it must not move where glyphs are placed (`1 50 Ts` raises text by 50, not 1).
let last = op.nums::<1>().map(|[v]| v);
match op.op.as_slice() {
b"BT" => {
self.tm = Matrix::IDENTITY;
self.tlm = Matrix::IDENTITY;
}
b"Tc" => gs.char_spacing = op.num(0).unwrap_or(0.0),
b"Tw" => gs.word_spacing = op.num(0).unwrap_or(0.0),
b"Tz" => gs.scale = op.num(0).unwrap_or(100.0) / 100.0,
b"TL" => gs.leading = op.num(0).unwrap_or(0.0),
b"Ts" => gs.rise = op.num(0).unwrap_or(0.0),
b"Tr" => gs.render_mode = op.num(0).unwrap_or(0.0) as i64,
b"Tc" => gs.char_spacing = last.unwrap_or(0.0),
b"Tw" => gs.word_spacing = last.unwrap_or(0.0),
b"Tz" => gs.scale = last.unwrap_or(100.0) / 100.0,
b"TL" => gs.leading = last.unwrap_or(0.0),
b"Ts" => gs.rise = last.unwrap_or(0.0),
b"Tr" => gs.render_mode = last.unwrap_or(0.0) as i64,
b"Td" | b"TD" => {
if let Some([x, y]) = op.nums::<2>() {
if op.is("TD") {
@@ -656,8 +661,10 @@ impl Walk {
}
let o = op.op.as_slice();
if o == b"\"" {
self.gs.word_spacing = op.num(0).unwrap_or(self.gs.word_spacing);
self.gs.char_spacing = op.num(1).unwrap_or(self.gs.char_spacing);
// `aw ac string "`: the two numbers before the string, whatever surplus precedes them.
let at = |back: usize| op.operands.len().checked_sub(back).and_then(|i| op.num(i));
self.gs.word_spacing = at(3).unwrap_or(self.gs.word_spacing);
self.gs.char_spacing = at(2).unwrap_or(self.gs.char_spacing);
}
if matches!(o, b"'" | b"\"") {
self.tlm = Matrix::translate(0.0, -self.gs.leading).then(&self.tlm);
+14
View File
@@ -25,3 +25,17 @@ 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:?}");
}
/// `1 50 Ts` raises text by 50, the operand before the operator, as the renderer reads it; the
/// surplus `1` before it is ignored. Reading the first operand put the glyphs 49 pt too low, so a
/// mark over the text the page shows missed it.
#[test]
fn surplus_text_state_operands_do_not_move_the_glyphs() {
let mut doc = one_page(b"BT /F1 10 Tf 1 50 Ts 10 100 Td (SECRET) Tj ET", "", vec![]);
assert_eq!(under(&mut doc, 0, &[SECRET_AT_10_100]), 0, "nothing is drawn at y 100");
let raised = [5.0, 145.0, 45.0, 160.0];
assert_eq!(under(&mut doc, 0, &[raised]), 6);
mark(&mut doc, 0, &[raised], "");
assert_eq!(apply(&mut doc, None).unwrap().glyphs, 6);
assert!(!content(&doc, 0).contains("SECRET"));
}