mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-12 17:31:35 +00:00
M7: ungroup refuses a nested form, Type 3 font or tiling pattern that inherits renamed resources
A Form XObject, Type 3 font or tiling pattern without /Resources of its own is drawn by the renderer with the resources of whatever draws it (vendor/hayro-interpret x_object.rs, font/type3.rs, pattern.rs chain them with Resources::from_parent). Ungroup moves the form's resources into the page's under new names, so such an item drawn from the page afterwards would find the page's resources instead: another font, or none. Ungroup now refuses when the merge renamed anything and the form's content draws such an item. A form without resources of its own is unaffected (it already uses the page's), and so are images and shading patterns. Raised by @Lcoding15 in #3067 (from #3222). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01R3niW7kVeT8mi7TpWc8eAJ
This commit is contained in:
1 parent
363c76db87
commit
30a285ae67
2 files changed
+155
No files matched your search
@@ -9069,6 +9069,124 @@ fn ungroup_art(content: &[u8], group: bool, build: impl FnOnce(&mut Document) ->
|
||||
images::change_image(&mut doc, 0, 0, &images::ImageChange::Ungroup).map(|_| ())
|
||||
}
|
||||
|
||||
/// A nested form, Type 3 font or tiling pattern without `/Resources` takes its names from the
|
||||
/// form drawing it (the renderer chains them). Once the form's names are renamed into the page's,
|
||||
/// it would find the page's instead (the page's Courier `F1`, not the form's Times), so ungroup
|
||||
/// refuses. With resources of its own it is unaffected, and so is a form without resources of
|
||||
/// its own, whose names are already the page's.
|
||||
#[test]
|
||||
fn ungroup_refuses_nested_content_that_inherits_renamed_resources() {
|
||||
let times = || {
|
||||
let mut d = Dict::new();
|
||||
d.set(b"Type".to_vec(), Object::name("Font"));
|
||||
d.set(b"Subtype".to_vec(), Object::name("Type1"));
|
||||
d.set(b"BaseFont".to_vec(), Object::name("Times-Roman"));
|
||||
Object::Dict(d)
|
||||
};
|
||||
let nested = |doc: &mut Document, kind: &str, own: bool| -> (&'static str, Object) {
|
||||
let mut d = Dict::new();
|
||||
if own {
|
||||
d.set(b"Resources".to_vec(), Object::Dict(Dict::new()));
|
||||
}
|
||||
match kind {
|
||||
"form" => {
|
||||
d.set(b"Subtype".to_vec(), Object::name("Form"));
|
||||
d.set(b"BBox".to_vec(), Object::Array([0, 0, 100, 50].map(Object::Int).to_vec()));
|
||||
("XObject", Object::Ref(doc.add(Object::Stream(Stream::flate(d, b"BT /F1 9 Tf (n) Tj ET")))))
|
||||
}
|
||||
"type3" => {
|
||||
d.set(b"Type".to_vec(), Object::name("Font"));
|
||||
d.set(b"Subtype".to_vec(), Object::name("Type3"));
|
||||
("Font", Object::Dict(d))
|
||||
}
|
||||
_ => {
|
||||
d.set(b"PatternType".to_vec(), Object::Int(1));
|
||||
("Pattern", Object::Ref(doc.add(Object::Stream(Stream::flate(d, b"BT /F1 9 Tf (n) Tj ET")))))
|
||||
}
|
||||
}
|
||||
};
|
||||
let content = |kind: &str| -> &'static [u8] {
|
||||
match kind {
|
||||
"form" => b"BT /F1 9 Tf (a) Tj ET /N Do",
|
||||
"type3" => b"BT /F1 9 Tf (a) Tj /N 9 Tf (b) Tj ET",
|
||||
// An uncoloured pattern: the name comes after the colour.
|
||||
_ => b"BT /F1 9 Tf (a) Tj ET [/Pattern /DeviceGray] cs 0.5 /N scn 0 0 10 10 re f",
|
||||
}
|
||||
};
|
||||
let run = |kind: &'static str, own: bool| {
|
||||
ungroup_art(content(kind), false, |doc| {
|
||||
let (cat, obj) = nested(doc, kind, own);
|
||||
let mut fonts = Dict::new();
|
||||
fonts.set(b"F1".to_vec(), times());
|
||||
let mut res = Dict::new();
|
||||
if cat == "Font" {
|
||||
fonts.set(b"N".to_vec(), obj);
|
||||
} else {
|
||||
let mut d = Dict::new();
|
||||
d.set(b"N".to_vec(), obj);
|
||||
res.set(cat.as_bytes().to_vec(), Object::Dict(d));
|
||||
}
|
||||
res.set(b"Font".to_vec(), Object::Dict(fonts));
|
||||
res
|
||||
})
|
||||
};
|
||||
for kind in ["form", "type3", "pattern"] {
|
||||
let r = run(kind, false);
|
||||
assert!(matches!(&r, Err(EditError::Invalid(m)) if m.contains("uses its resources")), "{kind}: {r:?}");
|
||||
assert!(run(kind, true).is_ok(), "{kind} with resources of its own");
|
||||
}
|
||||
|
||||
// An image and a shading pattern have no content to look names up for.
|
||||
let r = ungroup_art(b"BT /F1 9 Tf (a) Tj ET /Im Do /Pattern cs /Sh scn 0 0 10 10 re f", false, |doc| {
|
||||
let mut img = Dict::new();
|
||||
img.set(b"Subtype".to_vec(), Object::name("Image"));
|
||||
img.set(b"Width".to_vec(), Object::Int(1));
|
||||
img.set(b"Height".to_vec(), Object::Int(1));
|
||||
img.set(b"ColorSpace".to_vec(), Object::name("DeviceGray"));
|
||||
img.set(b"BitsPerComponent".to_vec(), Object::Int(8));
|
||||
let img = doc.add(Object::Stream(Stream::flate(img, b"@")));
|
||||
let mut shading = Dict::new();
|
||||
shading.set(b"ShadingType".to_vec(), Object::Int(2));
|
||||
let mut pattern = Dict::new();
|
||||
pattern.set(b"PatternType".to_vec(), Object::Int(2));
|
||||
pattern.set(b"Shading".to_vec(), Object::Dict(shading));
|
||||
let mut res = Dict::new();
|
||||
let mut d = Dict::new();
|
||||
d.set(b"Im".to_vec(), Object::Ref(img));
|
||||
res.set(b"XObject".to_vec(), Object::Dict(d));
|
||||
let mut d = Dict::new();
|
||||
d.set(b"Sh".to_vec(), Object::Dict(pattern));
|
||||
res.set(b"Pattern".to_vec(), Object::Dict(d));
|
||||
let mut d = Dict::new();
|
||||
d.set(b"F1".to_vec(), times());
|
||||
res.set(b"Font".to_vec(), Object::Dict(d));
|
||||
res
|
||||
});
|
||||
assert!(r.is_ok(), "an image and a shading pattern: {r:?}");
|
||||
|
||||
// The outer form has no resources: the nested form already finds the page's, as it will.
|
||||
let mut doc = fixture();
|
||||
let mut inner = Dict::new();
|
||||
inner.set(b"Subtype".to_vec(), Object::name("Form"));
|
||||
inner.set(b"BBox".to_vec(), Object::Array([0, 0, 100, 50].map(Object::Int).to_vec()));
|
||||
let inner = doc.add(Object::Stream(Stream::flate(inner, b"BT /F1 9 Tf (n) Tj ET")));
|
||||
let mut outer = Dict::new();
|
||||
outer.set(b"Subtype".to_vec(), Object::name("Form"));
|
||||
outer.set(b"BBox".to_vec(), Object::Array([0, 0, 100, 50].map(Object::Int).to_vec()));
|
||||
let outer = doc.add(Object::Stream(Stream::flate(outer, b"/N Do")));
|
||||
doc.update_dict(pdfcraft_cos::ObjRef::new(6, 0), |d| {
|
||||
let mut xo = Dict::new();
|
||||
xo.set(b"Art".to_vec(), Object::Ref(outer));
|
||||
xo.set(b"N".to_vec(), Object::Ref(inner));
|
||||
d.set(b"XObject".to_vec(), Object::Dict(xo));
|
||||
})
|
||||
.unwrap();
|
||||
doc.set(pdfcraft_cos::ObjRef::new(7, 0), Object::Stream(Stream::flate(Dict::new(), b"q /Art Do Q")));
|
||||
let mut doc = reopen(&doc);
|
||||
let art = images::page_images(&doc, 0).unwrap().iter().position(|i| i.name == "Art").unwrap();
|
||||
images::change_image(&mut doc, 0, art, &images::ImageChange::Ungroup).unwrap();
|
||||
}
|
||||
|
||||
/// #3067 review: a form whose marked content or text objects don't balance within its own
|
||||
/// stream is refused: inlined, an extra EMC would close what the page opened, and one left open
|
||||
/// would take in the page's later content.
|
||||
|
||||
@@ -122,6 +122,13 @@ pub(crate) fn ungroup_ops(doc: &Document, resources: &Dict, name: &[u8], translu
|
||||
let renamed = |cat: &[u8], name: &[u8]| -> Option<Vec<u8>> {
|
||||
renames.iter().find(|(c, _)| *c == cat).and_then(|(_, m)| m.iter().find(|(from, _)| from == name)).map(|(_, to)| to.clone())
|
||||
};
|
||||
// A nested form, Type 3 font or tiling pattern without resources of its own looks its names
|
||||
// up in the form's (the renderer chains them to whatever draws it). Drawn from the page, it
|
||||
// would find the page's, which differ wherever a name was renamed (every form resource that
|
||||
// isn't already the page's, the nested item's own entry included).
|
||||
if !renames.is_empty() && ops.iter().any(|op| inherits_resources(doc, &form_res, op)) {
|
||||
return Err(refuse("it draws a nested form, Type 3 font or pattern that uses its resources"));
|
||||
}
|
||||
let mut depth = 0usize;
|
||||
// Marked content and text objects must balance within the form's own stream: an extra EMC
|
||||
// would close one the page opened, and one left open would take in the page's later content.
|
||||
@@ -248,6 +255,36 @@ struct BlendWalk {
|
||||
/// its own `/ExtGState`, a tiling pattern through its content. A nested form or pattern that can't
|
||||
/// be read, is nested too deep or draws itself counts as blending, so the caller refuses rather
|
||||
/// than guesses.
|
||||
/// Whether `op` draws a Form XObject (`Do`), Type 3 font (`Tf`) or tiling pattern (`scn`/`SCN`) from
|
||||
/// `resources` that has no `/Resources` of its own, and so inherits the ones it is drawn with.
|
||||
fn inherits_resources(doc: &Document, resources: &Dict, op: &Op) -> bool {
|
||||
let (cat, i): (&[u8], usize) = match op.op.as_slice() {
|
||||
b"Do" => (b"XObject", 0),
|
||||
b"Tf" => (b"Font", 0),
|
||||
b"scn" | b"SCN" => match op.operands.len().checked_sub(1) {
|
||||
Some(i) => (b"Pattern", i),
|
||||
None => return false,
|
||||
},
|
||||
_ => return false,
|
||||
};
|
||||
let Some(Object::Name(name)) = op.operands.get(i) else { return false };
|
||||
let Some(entry) = resources.get(cat).map(|d| doc.resolve(d)).and_then(|d| d.as_dict().and_then(|d| d.get(name).cloned())) else {
|
||||
return false;
|
||||
};
|
||||
let dict = match &*doc.resolve(&entry) {
|
||||
Object::Stream(s) => s.dict.clone(),
|
||||
Object::Dict(d) => d.clone(),
|
||||
_ => return false,
|
||||
};
|
||||
let drawn = match cat {
|
||||
b"XObject" => dict.name(b"Subtype") == Some(b"Form"),
|
||||
b"Font" => dict.name(b"Subtype") == Some(b"Type3"),
|
||||
// A shading pattern has no content of its own.
|
||||
_ => dict.int(b"PatternType") == Some(1),
|
||||
};
|
||||
drawn && dict.get(b"Resources").is_none()
|
||||
}
|
||||
|
||||
fn blends_inside(doc: &Document, resources: &Dict, ops: &[Op], depth: usize, walk: &mut BlendWalk) -> bool {
|
||||
let category = |cat: &[u8]| resources.get(cat).map(|x| doc.resolve(x)).and_then(|x| x.as_dict().cloned()).unwrap_or_default();
|
||||
let (states, xobjects, patterns) = (category(b"ExtGState"), category(b"XObject"), category(b"Pattern"));
|
||||
|
||||
Reference in new issue
Block a user