mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-13 14:22:25 +00:00
Type 3 pruning leaves tiling patterns to the pattern guard
A tiling pattern must have /Resources of its own, so its cell never draws through the page's. Since #3680, Apply Redactions refuses a page whose tiling pattern shows text or has no /Resources (pattern_guard), so prune_type3_from_resources reads only soft mask groups now. Remove Hidden Information runs no pattern guard: there, a page font that only such a malformed pattern uses goes, as it did before this PR. unlink_type3 still follows a pattern's cell, for patterns on pages the redaction does not touch. Its test now puts the pattern on a second page.
This commit is contained in:
1 parent
c3b1a7fd5f
commit
f0e400c1ef
3 files changed
+27
-50
No files matched your search
@@ -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;
|
||||
|
||||
@@ -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"),
|
||||
|
||||
+12
-12
@@ -401,19 +401,19 @@ fn resourceless_appearances(doc: &Document, page: &Dict) -> Result<Vec<Vec<u8>>,
|
||||
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<pdfcraft_content::Op>, 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<pdfcraft_content::Op>, 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<pdfcraft_c
|
||||
}
|
||||
let parsed = pdfcraft_content::parse(&s.decoded()?);
|
||||
if parsed.skipped > 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();
|
||||
|
||||
Reference in new issue
Block a user