mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-12 05:42:52 +00:00
M8: a test of its own for each operand read the fix changes
Review of the whole change found reads it fixes that no test failed without: each was put back to main's read alone, and the tests passed. Each now has a case that fails without it: - the proof's `Tf` size (text starting outside the region reaches into it only at its size) and font (an undecodable font's text leaves the region unverifiable), and the strings a `TJ` carries; - the proof's `l`, `c`, `v` and `y` points after a surplus, from a point outside the region, and the walk's `m`, `l`, `c`, `v` and `y` points beside a mark, each alone; - the property list a second page sharing the resources still names; - a Type 3 font an ExtGState's `/Font` sets, kept by the page and its glyph by the font, and one without resources of its own, which keeps the page's pattern its glyph may paint with; - a Type 3 font only a soft mask's group selects, the mask's ExtGState named after a surplus; - the named property lists read once, with each `BDC` naming another entry, so that it is the lists, not each name's answer, that are kept. And the walk's `cs` says why it tells a Pattern space by the name `/Pattern` alone. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
This commit is contained in:
1 parent
5e2552dcb0
commit
042371bda9
5 files changed
+163
-4
No files matched your search
@@ -571,6 +571,11 @@ impl Walk {
|
||||
Some(op)
|
||||
}
|
||||
b"cs" | b"CS" => {
|
||||
// A Pattern space is told by the name `/Pattern` alone: one named through the
|
||||
// resources' `/ColorSpace` clears the pattern, which the app's renderer keeps and
|
||||
// Poppler doesn't (`cs` resets the colour, ISO 32000-2 §8.6.8). Looking the name up
|
||||
// at each `cs` wants a cache for the page: a `/ColorSpace` too large for the
|
||||
// document to keep is parsed again at each read.
|
||||
if op.last_name() != Some(b"Pattern") {
|
||||
if o == b"cs" {
|
||||
self.gs.fill_pattern = None;
|
||||
|
||||
@@ -190,6 +190,19 @@ fn a_paths_points_after_a_surplus_number_are_where_the_renderer_draws_them() {
|
||||
mark(&mut doc, 0, &[[0.0, 0.0, 20.0, 20.0]], "");
|
||||
assert_eq!(apply(&mut doc, None).unwrap().paths_removed, 1, "{}", String::from_utf8_lossy(curve));
|
||||
}
|
||||
// A line, and curves, from a point beside the mark whose points, after a surplus, stay beside
|
||||
// it: read from the front, the surplus was a point under it.
|
||||
for curve in [
|
||||
&b"100 100 500 m 510 510 l S"[..],
|
||||
b"500 500 m 100 100 510 l S",
|
||||
b"100 500 m 100 100 500 110 500 120 500 c f",
|
||||
b"100 500 m 100 110 500 120 500 v f",
|
||||
b"100 500 m 100 110 500 120 500 y f",
|
||||
] {
|
||||
let mut doc = one_page(curve, "", vec![]);
|
||||
mark(&mut doc, 0, &[[90.0, 90.0, 150.0, 150.0]], "");
|
||||
assert_eq!(apply(&mut doc, None).unwrap().paths_removed, 0, "{}", String::from_utf8_lossy(curve));
|
||||
}
|
||||
}
|
||||
|
||||
/// An `MCID` given by reference: in a named property list, and in a structure element's `/MCR`.
|
||||
@@ -236,3 +249,92 @@ fn an_mcid_given_by_reference_loses_what_redaction_removed() {
|
||||
let other = doc.get(ObjRef::new(9, 0)).as_dict().cloned().unwrap();
|
||||
assert!(other.get(b"ActualText").is_some(), "{other:?}");
|
||||
}
|
||||
|
||||
/// The resources of page `page`, resolved.
|
||||
fn resources_of(doc: &Document, page: usize) -> Dict {
|
||||
let page = pdfcraft_model::pages(doc).swap_remove(page);
|
||||
page.dict.get(b"Resources").map(|r| doc.resolve(r)).and_then(|r| r.as_dict().cloned()).unwrap_or_default()
|
||||
}
|
||||
|
||||
/// `dict`'s dictionary `key`, resolved.
|
||||
fn dict_in(doc: &Document, dict: &Dict, key: &[u8]) -> Dict {
|
||||
dict.get(key).map(|d| doc.resolve(d)).and_then(|d| d.as_dict().cloned()).unwrap_or_default()
|
||||
}
|
||||
|
||||
/// A property list a second page sharing the resources names after a surplus operand, once the
|
||||
/// first page's block that named it was redacted: read from the front, the second page's name went
|
||||
/// unseen and the list was retired from under it.
|
||||
#[test]
|
||||
fn a_property_list_another_page_names_after_a_surplus_operand_is_kept() {
|
||||
use pdfcraft_cos::Stream;
|
||||
let mut doc = one_page(
|
||||
b"/Span /P0 BDC BT /F1 10 Tf 10 200 Td (SECRET) Tj ET EMC",
|
||||
"/Properties << /P0 7 0 R >>",
|
||||
vec![b"<< /ActualText (SECRET) >>".to_vec()],
|
||||
);
|
||||
let shared = doc.add(Object::Dict(resources_of(&doc, 0)));
|
||||
doc.update_dict(ObjRef::new(3, 0), |d| d.set(b"Resources".to_vec(), Object::Ref(shared))).unwrap();
|
||||
let mut second = doc.get(ObjRef::new(3, 0)).as_dict().cloned().unwrap();
|
||||
let contents = doc.add(Object::Stream(Stream::from_raw(Dict::new(), b"(x) /Span /P0 BDC BT /F1 10 Tf 10 100 Td (Other) Tj ET EMC".to_vec())));
|
||||
second.set(b"Contents".to_vec(), Object::Ref(contents));
|
||||
let second = doc.add(Object::Dict(second));
|
||||
doc.update_dict(ObjRef::new(2, 0), |d| {
|
||||
d.set(b"Kids".to_vec(), Object::Array(vec![Object::Ref(ObjRef::new(3, 0)), Object::Ref(second)]));
|
||||
d.set(b"Count".to_vec(), Object::Int(2));
|
||||
})
|
||||
.unwrap();
|
||||
mark(&mut doc, 0, &[[5.0, 195.0, 60.0, 212.0]], "");
|
||||
apply(&mut doc, Some(&[0])).unwrap();
|
||||
let properties = dict_in(&doc, &resources_of(&doc, 1), b"Properties");
|
||||
assert!(properties.contains(b"P0"), "the second page's property list stays: {properties:?}");
|
||||
}
|
||||
|
||||
/// A Type 3 font set only by an ExtGState's `/Font`, selected after a surplus operand: read from the
|
||||
/// front, the cleanups took it for undrawn, dropped it from the page and unlinked its glyph.
|
||||
#[test]
|
||||
fn a_type3_font_an_extgstate_selects_after_a_surplus_operand_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 /Page /Parent 2 0 R /MediaBox [0 0 300 300] /Contents 4 0 R /Resources << /Font << /F1 5 0 R /T3 7 0 R >> /ExtGState << /GS1 << /Font [7 0 R 12] >> >> >> >>".to_vec(),
|
||||
stream("", b"BT /F1 10 Tf 20 260 Td (SECRET) Tj ET BT (x) /GS1 gs 20 20 Td (a) Tj ET"),
|
||||
FONT.replace("95 0 R", "6 0 R").into_bytes(),
|
||||
widths(),
|
||||
b"<< /Type /Font /Subtype /Type3 /FontBBox [0 0 10 10] /FontMatrix [0.1 0 0 0.1 0 0] /CharProcs << /a 8 0 R /b 9 0 R >> \
|
||||
/Encoding << /Differences [97 /a /b] >> /FirstChar 97 /LastChar 98 /Widths [10 10] >>"
|
||||
.to_vec(),
|
||||
stream("", b"10 0 d0 0 0 10 10 re f"),
|
||||
stream("", b"10 0 d0 0 0 5 5 re f"),
|
||||
]);
|
||||
mark(&mut doc, 0, &[[0.0, 200.0, 300.0, 300.0]], "");
|
||||
assert_eq!(apply(&mut doc, None).unwrap().glyphs, 6);
|
||||
let fonts = dict_in(&doc, &resources_of(&doc, 0), b"Font");
|
||||
assert!(fonts.contains(b"T3"), "the page still lists the font: {fonts:?}");
|
||||
let font = doc.get(ObjRef::new(7, 0)).as_dict().cloned().unwrap_or_default();
|
||||
let procs = dict_in(&doc, &font, b"CharProcs");
|
||||
assert!(procs.contains(b"a"), "the glyph the page draws stays: {procs:?}");
|
||||
}
|
||||
|
||||
/// A Type 3 font without resources of its own, set by an ExtGState selected after a surplus operand:
|
||||
/// its glyph paints through the page's resources, so it may paint with the page's pattern, which
|
||||
/// stays once the page's own fill with it was redacted (the text itself fills with grey). Read from the front, the font was taken
|
||||
/// for one with resources, and the pattern was pruned.
|
||||
#[test]
|
||||
fn a_type3_font_without_resources_an_extgstate_selects_after_a_surplus_operand_keeps_the_pattern() {
|
||||
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 << /Font << /F1 5 0 R >> /Pattern << /P0 9 0 R >> /ExtGState << /GS1 << /Font [7 0 R 12] >> >> >> >>".to_vec(),
|
||||
stream("", b"/Pattern cs /P0 scn 10 260 20 20 re f 0 g BT (x) /GS1 gs 150 20 Td (a) Tj ET"),
|
||||
FONT.replace("95 0 R", "6 0 R").into_bytes(),
|
||||
widths(),
|
||||
b"<< /Type /Font /Subtype /Type3 /FontBBox [0 0 10 10] /FontMatrix [0.1 0 0 0.1 0 0] /CharProcs << /a 8 0 R >> \
|
||||
/Encoding << /Differences [97 /a] >> /FirstChar 97 /LastChar 97 /Widths [10] >>"
|
||||
.to_vec(),
|
||||
stream("", b"10 0 d0 /Pattern cs /P0 scn 0 0 10 10 re f"),
|
||||
stream("/PatternType 1 /PaintType 1 /TilingType 1 /BBox [0 0 10 10] /XStep 10 /YStep 10 /Resources << >>", b"0 0 5 5 re f"),
|
||||
]);
|
||||
mark(&mut doc, 0, &[[0.0, 250.0, 40.0, 290.0]], "");
|
||||
let r = apply(&mut doc, None).unwrap();
|
||||
assert_eq!((r.paths_removed, r.patterns_removed), (1, 0));
|
||||
}
|
||||
@@ -783,6 +783,37 @@ 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 same soft mask, its ExtGState selected after a surplus operand (`(x) /GS1 gs`), which the
|
||||
/// renderer ignores: read from the front, the cleanups never read the group, and dropped the font
|
||||
/// it selects and its glyph.
|
||||
#[test]
|
||||
fn a_type3_font_only_a_soft_mask_selected_after_a_surplus_operand_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 (x) /GS1 gs 150 150 100 100 re f"),
|
||||
b"<< /Font << /T3 6 0 R /T4 7 0 R >> /ExtGState << /GS1 << /Type /ExtGState /SMask << /Type /Mask /S /Luminosity /G 8 0 R >> >> >> >>"
|
||||
.to_vec(),
|
||||
type3_abc(9),
|
||||
type3_abc(12),
|
||||
stream(
|
||||
"/Type /XObject /Subtype /Form /BBox [0 0 300 300] /Group << /S /Transparency /CS /DeviceGray >>",
|
||||
b"1 g BT /T4 40 Tf 160 180 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"% MASK_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"MASK_GLYPH_B"), "the glyph the soft mask shows was unlinked");
|
||||
assert!(saved_first_page_lists_font(&doc, b"T4"), "the page no longer lists the font the soft mask selects");
|
||||
}
|
||||
|
||||
/// The same group, its content behind a predictor whose `/DecodeParms` are given by reference:
|
||||
/// the page's cleanup reads it through them, and keeps the font it selects.
|
||||
#[test]
|
||||
|
||||
@@ -912,6 +912,8 @@ fn the_proof_reads_operands_from_the_end() {
|
||||
stream("/Type /XObject /Subtype /Form /BBox [0 0 300 300] /Resources << /XObject << /Im1 8 0 R >> >>", b"q 50 0 0 12 10 98 cm (x) /Im1 Do Q");
|
||||
for (content, spec) in [
|
||||
(&b"BT (x) /F1 10 Tf 10 100 Td (SECRETVALUE) Tj ET"[..], Spec::default()),
|
||||
// The size: text starting outside the region reaches into it only at its size.
|
||||
(b"BT (x) /F1 10 Tf -60 100 Td (AAAAAAAAAAAAAAAAAAAAAAAAAAAA) Tj ET", Spec::default()),
|
||||
(b"BT /F1 10 Tf 10 100 Td (x) [(SECRETVALUE)] TJ ET", Spec::default()),
|
||||
(b"(x) /Fm1 Do", Spec { resources: "/XObject << /Fm1 7 0 R >>", extra: vec![form.clone()], ..Spec::default() }),
|
||||
// An image under the mark, and one in a form under it.
|
||||
@@ -960,6 +962,11 @@ fn the_proof_reads_a_paths_points_from_the_end() {
|
||||
b"0 0 1 rg 9 200 200 m 9 20 98 40 100 60 108 c h f",
|
||||
b"0 0 1 rg 9 200 200 m 9 20 98 60 108 v h f",
|
||||
b"0 0 1 rg 9 200 200 m 9 20 98 60 108 y h f",
|
||||
// A segment after a surplus, from a point outside the region.
|
||||
b"0 0 1 rg 200 200 m 200 20 100 l h f",
|
||||
b"0 0 1 rg 200 200 m 200 20 100 30 100 40 100 c h f",
|
||||
b"0 0 1 rg 200 200 m 200 20 100 40 100 v h f",
|
||||
b"0 0 1 rg 200 200 m 200 20 100 40 100 y h f",
|
||||
] {
|
||||
let mut doc = marked_content(content, Spec::default());
|
||||
doc.require_full_save();
|
||||
@@ -968,6 +975,19 @@ fn the_proof_reads_a_paths_points_from_the_end() {
|
||||
}
|
||||
}
|
||||
|
||||
/// Which font a `Tf` selects, and which strings a `TJ` shows, are read from the end too: text an
|
||||
/// undecodable font shows after a surplus operand, away from the mark, leaves the region
|
||||
/// unverifiable. Read from the front, no font was set, or no string was shown, and it passed.
|
||||
#[test]
|
||||
fn the_proof_reads_the_font_and_the_strings_from_the_end() {
|
||||
for content in [&b"BT (x) /F2 10 Tf 10 200 Td <00410042> Tj ET"[..], b"BT /F2 10 Tf 10 200 Td (x) [<00410042>] TJ ET"] {
|
||||
let mut doc = open(build_with(content, opaque_spec()));
|
||||
mark(&mut doc, &[[150.0, 20.0, 200.0, 40.0]]);
|
||||
let (_, proof) = run(&mut doc);
|
||||
assert_eq!(proof.entries[0].status, Status::Unverifiable, "{}: {}", String::from_utf8_lossy(content), proof.to_text());
|
||||
}
|
||||
}
|
||||
|
||||
/// An image a Type 3 glyph (one without resources of its own, drawing through the page's) draws
|
||||
/// after a surplus operand can't be placed, as one it draws without one can't: the region is
|
||||
/// unverifiable.
|
||||
|
||||
@@ -77,13 +77,14 @@ fn about_as_long(many: Duration, one: Duration) -> bool {
|
||||
}
|
||||
|
||||
/// Written when the tags cleanup resolved the page's `/Properties` at every named `BDC`: 20,000 of
|
||||
/// them with 5,000 entries took 28 s against 0.07 s with one entry (optimized).
|
||||
/// them with 5,000 entries took 28 s against 0.07 s with one entry (optimized). Each `BDC` here
|
||||
/// names another entry, so that it is the lists, not each name's answer, that are read once.
|
||||
#[test]
|
||||
fn named_property_lists_are_read_once_for_a_page() {
|
||||
let _serial = one_at_a_time();
|
||||
let ops = b"/P /M0 BDC EMC ".repeat(5_000);
|
||||
let (one, many) = (redaction(|| properties(1, &ops)), redaction(|| properties(2_000, &ops)));
|
||||
assert!(about_as_long(many, one), "2,000 entries {many:?}, one {one:?}");
|
||||
let ops: Vec<u8> = (0..5_000).map(|i| format!("/P /M{i} BDC EMC ")).collect::<String>().into_bytes();
|
||||
let (one, many) = (redaction(|| properties(1, &ops)), redaction(|| properties(5_000, &ops)));
|
||||
assert!(about_as_long(many, one), "5,000 entries {many:?}, one {one:?}");
|
||||
}
|
||||
|
||||
/// Written when the tags cleanup read a named property list's `MCID` by reference at each `BDC`: a
|
||||
|
||||
Reference in new issue
Block a user