diff --git a/crates/redact/src/interp.rs b/crates/redact/src/interp.rs index e0f8963d90..57829889de 100644 --- a/crates/redact/src/interp.rs +++ b/crates/redact/src/interp.rs @@ -1077,7 +1077,7 @@ fn xobject( return None; } // No page: annotation appearances draw through a page's resources, never through a - // form's. The form's own patterns and soft masks are still counted. + // form's. The form's own soft masks are still counted. if let Err(error) = crate::type3::prune_type3_from_resources(doc, &mut cleaned_res, content, None) { out.error = Some(error); return None; diff --git a/crates/redact/src/tests/surviving_references.rs b/crates/redact/src/tests/surviving_references.rs index 0864c701bd..7723cc1baa 100644 --- a/crates/redact/src/tests/surviving_references.rs +++ b/crates/redact/src/tests/surviving_references.rs @@ -395,36 +395,9 @@ fn saved_first_page_lists_font(doc: &Document, name: &[u8]) -> bool { res.get(b"Font").and_then(|f| saved.resolve(f).as_dict().map(|fonts| fonts.contains(name))).unwrap_or(false) } -/// A tiling pattern without /Resources shows glyph B of a second Type 3 font, which the page lists -/// but its content never selects. The cell draws through the page's resources, so the rewritten -/// page still lists that font and B's program stays. -#[test] -fn a_type3_font_only_a_pattern_without_resources_selects_stays_on_the_page() { - let mut doc = pdf(vec![ - b"<< /Type /Catalog /Pages 2 0 R >>".to_vec(), - b"<< /Type /Pages /Kids [3 0 R] /Count 1 >>".to_vec(), - b"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 300 300] /Contents 4 0 R /Resources 5 0 R >>".to_vec(), - stream("", b"BT /T3 10 Tf 10 100 Td (AC) Tj ET /Pattern cs /P1 scn 200 200 50 50 re f"), - b"<< /Font << /T3 6 0 R /T4 7 0 R >> /Pattern << /P1 8 0 R >> >>".to_vec(), - type3_abc(9), - type3_abc(12), - stream("/Type /Pattern /PatternType 1 /PaintType 1 /TilingType 1 /BBox [0 0 20 20] /XStep 20 /YStep 20", b"BT /T4 10 Tf 2 5 Td (B) Tj ET"), - stream("", b"% REDACTED_GLYPH_A"), - stream("", b"% UNUSED_GLYPH_B"), - stream("", b"% PAGE_GLYPH_C"), - stream("", b"% UNUSED_T4_GLYPH_A"), - stream("", b"% PATTERN_GLYPH_B"), - stream("", b"% UNUSED_T4_GLYPH_C"), - ]); - mark(&mut doc, 0, &[[9.0, 95.0, 14.9, 110.0]], ""); - assert_eq!(apply(&mut doc, None).unwrap().glyphs, 1); - assert!(!full_save_holds(&doc, b"REDACTED_GLYPH_A"), "the redacted glyph program survived"); - assert!(full_save_holds(&doc, b"PATTERN_GLYPH_B"), "the glyph the pattern shows was unlinked"); - assert!(saved_first_page_lists_font(&doc, b"T4"), "the page no longer lists the font the pattern selects"); -} - -/// The same with the group of a soft mask without /Resources: the page still lists the second -/// font, and the glyph the mask shows stays. +/// The group of a soft mask without /Resources shows glyph B of a second Type 3 font, which the +/// page lists but its content never selects. The group draws through the page's resources, so the +/// rewritten page still lists that font and B's program stays. #[test] fn a_type3_font_only_a_soft_mask_without_resources_selects_stays_on_the_page() { let mut doc = pdf(vec![ @@ -454,18 +427,22 @@ fn a_type3_font_only_a_soft_mask_without_resources_selects_stays_on_the_page() { assert!(saved_first_page_lists_font(&doc, b"T4"), "the page no longer lists the font the soft mask selects"); } -/// The cell of a tiling pattern the page fills with shows glyph B: its program is kept. +/// The cell of a tiling pattern another page fills with shows glyph B: its program is kept. (On +/// the redacted page itself, a pattern that shows text refuses the redaction: `pattern_guard`.) #[test] -fn a_type3_glyph_a_tiling_pattern_shows_is_kept() { +fn a_type3_glyph_a_tiling_pattern_on_another_page_shows_is_kept() { let mut doc = pdf(vec![ b"<< /Type /Catalog /Pages 2 0 R >>".to_vec(), - b"<< /Type /Pages /Kids [3 0 R] /Count 1 >>".to_vec(), + b"<< /Type /Pages /Kids [3 0 R 6 0 R] /Count 2 >>".to_vec(), b"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 300 300] /Contents 4 0 R /Resources 5 0 R >>".to_vec(), - stream("", b"BT /T3 10 Tf 10 100 Td (AC) Tj ET /Pattern cs /P1 scn 200 200 50 50 re f"), - b"<< /Font << /T3 6 0 R >> /Pattern << /P1 7 0 R >> >>".to_vec(), - type3_abc(8), + stream("", b"BT /T3 10 Tf 10 100 Td (AC) Tj ET"), + b"<< /Font << /T3 9 0 R >> >>".to_vec(), + b"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 300 300] /Contents 7 0 R /Resources 8 0 R >>".to_vec(), + stream("", b"/Pattern cs /P1 scn 200 200 50 50 re f"), + b"<< /Pattern << /P1 10 0 R >> >>".to_vec(), + type3_abc(11), stream( - "/Type /Pattern /PatternType 1 /PaintType 1 /TilingType 1 /BBox [0 0 20 20] /XStep 20 /YStep 20 /Resources << /Font << /T3 6 0 R >> >>", + "/Type /Pattern /PatternType 1 /PaintType 1 /TilingType 1 /BBox [0 0 20 20] /XStep 20 /YStep 20 /Resources << /Font << /T3 9 0 R >> >>", b"BT /T3 10 Tf 2 5 Td (B) Tj ET", ), stream("", b"% REDACTED_GLYPH_A"), diff --git a/crates/redact/src/type3.rs b/crates/redact/src/type3.rs index 32145c426a..1d8d315d76 100644 --- a/crates/redact/src/type3.rs +++ b/crates/redact/src/type3.rs @@ -401,19 +401,19 @@ fn resourceless_appearances(doc: &Document, page: &Dict) -> Result>, Ok(out) } -/// Add to `ops` those of the tiling patterns and soft mask groups without resources of their own -/// that `ops` select (`scn`, `SCN`, `gs`), and of the forms and further ones those draw in turn: -/// they draw through `res`, as [`scan_stream_object`] reads them. The forms that the first `known` -/// operators draw are in `ops` already. -fn add_pattern_and_mask_ops(doc: &Document, res: &Dict, ops: &mut Vec, known: usize) -> Result<(), CosError> { +/// Add to `ops` those of the soft mask groups without resources of their own that `ops` set +/// (`gs`), and of the forms and further ones those draw in turn: they draw through `res`, as +/// [`scan_stream_object`] reads them. The forms that the first `known` operators draw are in `ops` +/// already. Tiling patterns are not read here: a tiling pattern must have resources of its own, +/// so its cell draws through them, and Apply Redactions refuses one without them +/// (`pattern_guard`). +fn add_soft_mask_ops(doc: &Document, res: &Dict, ops: &mut Vec, known: usize) -> Result<(), CosError> { let xobjects = res_dict(doc, res, b"XObject"); - let patterns = res_dict(doc, res, b"Pattern"); let states = res_dict(doc, res, b"ExtGState"); let mut seen = HashSet::new(); let mut cursor = 0; while let Some(op) = ops.get(cursor) { let selected = match op.op.as_slice() { - b"scn" | b"SCN" => op.last_name().and_then(|n| patterns.get(n)).and_then(Object::as_ref), b"gs" => op .name(0) .and_then(|n| states.get(n)) @@ -438,7 +438,7 @@ fn add_pattern_and_mask_ops(doc: &Document, res: &Dict, ops: &mut Vec 0 || ops.len().saturating_add(parsed.ops.len()) > 2_000_000 { - return Err(CosError::Syntax { offset: 0, detail: "redaction could not fully inspect pattern or soft mask content".into() }); + return Err(CosError::Syntax { offset: 0, detail: "redaction could not fully inspect soft mask content".into() }); } ops.extend(parsed.ops); } @@ -489,9 +489,9 @@ pub(crate) fn prune_type3_from_resources(doc: &Document, res: &mut Dict, content // The operators of forms that draw through these resources count too: dropping a font a child // form still shows would leave its text without one. let mut ops = crate::interp::resource_ops(doc, res, content)?; - // So do those of a page's annotation appearances, and of tiling patterns and soft mask groups, - // without resources of their own. They are read only when there is a Type 3 font to keep, so - // other files are not refused over them. + // So do those of a page's annotation appearances, and of soft mask groups, without resources of + // their own. They are read only when there is a Type 3 font to keep, so other files are not + // refused over them. if fonts.iter().any(|(_, f)| doc.resolve(f).as_dict().is_some_and(|d| d.name(b"Subtype") == Some(b"Type3"))) { if let Some(page) = page { for data in resourceless_appearances(doc, page)? { @@ -499,7 +499,7 @@ pub(crate) fn prune_type3_from_resources(doc: &Document, res: &mut Dict, content } } let known = ops.len(); - add_pattern_and_mask_ops(doc, res, &mut ops, known)?; + add_soft_mask_ops(doc, res, &mut ops, known)?; } let mut drawn_font_names = HashSet::new();