mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-24 10:08:00 -06:00
A BDC may name its property list in /Resources /Properties instead of writing it inline. Marking a figure decorative and placing figures only matched inline lists, so a figure tagged through a named list kept its MCID content and parent-tree entry while its element was removed. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Brandon Thomas <echelon@gmail.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
Brandon Thomas
parent
3a4de17707
commit
178d9a5882
+20
-6
@@ -140,6 +140,22 @@ fn mcids(d: &Dict) -> Vec<i64> {
|
||||
out
|
||||
}
|
||||
|
||||
/// The page's named property lists (`/Resources /Properties`), which a `BDC` can name instead of
|
||||
/// writing its property list inline.
|
||||
fn page_properties(doc: &Document, page: &Dict) -> Dict {
|
||||
let res = page.get(b"Resources").map(|r| doc.resolve(r)).and_then(|r| r.as_dict().cloned()).unwrap_or_default();
|
||||
res.get(b"Properties").map(|p| doc.resolve(p)).and_then(|p| p.as_dict().cloned()).unwrap_or_default()
|
||||
}
|
||||
|
||||
/// The marked-content id a `BDC` opens, whether its property list is inline or named in `props`.
|
||||
fn bdc_mcid(doc: &Document, props: &Dict, op: &pdfcraft_content::Op) -> Option<i64> {
|
||||
match op.operands.get(1)? {
|
||||
Object::Dict(d) => d.get(b"MCID").and_then(Object::as_int),
|
||||
Object::Name(n) => props.get(n).map(|x| doc.resolve(x)).and_then(|x| x.as_dict().and_then(|d| d.get(b"MCID").and_then(Object::as_int))),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn page_content(doc: &Document, page: &Dict) -> Vec<u8> {
|
||||
let Some(c) = page.get(b"Contents") else { return Vec::new() };
|
||||
match &*doc.resolve(c) {
|
||||
@@ -170,6 +186,7 @@ fn union(a: Option<[f64; 4]>, b: [f64; 4]) -> [f64; 4] {
|
||||
fn content_bbox(doc: &Document, page: &Dict, ids: &HashSet<i64>) -> Option<[f64; 4]> {
|
||||
let res = page.get(b"Resources").map(|r| doc.resolve(r)).and_then(|r| r.as_dict().cloned()).unwrap_or_default();
|
||||
let xobjects = res.get(b"XObject").map(|x| doc.resolve(x)).and_then(|x| x.as_dict().cloned()).unwrap_or_default();
|
||||
let props = page_properties(doc, page);
|
||||
let mut ctm = vec![Matrix([1.0, 0.0, 0.0, 1.0, 0.0, 0.0])];
|
||||
let mut marks: Vec<bool> = Vec::new();
|
||||
let mut bbox = None;
|
||||
@@ -190,9 +207,7 @@ fn content_bbox(doc: &Document, page: &Dict, ids: &HashSet<i64>) -> Option<[f64;
|
||||
}
|
||||
}
|
||||
b"BMC" => marks.push(false),
|
||||
b"BDC" => marks.push(
|
||||
matches!(op.operands.get(1), Some(Object::Dict(d)) if d.get(b"MCID").and_then(Object::as_int).is_some_and(|n| ids.contains(&n))),
|
||||
),
|
||||
b"BDC" => marks.push(bdc_mcid(doc, &props, &op).is_some_and(|n| ids.contains(&n))),
|
||||
b"EMC" => {
|
||||
marks.pop();
|
||||
}
|
||||
@@ -295,12 +310,11 @@ pub fn mark_decorative(doc: &mut Document, obj: ObjRef) -> Result<(), AltError>
|
||||
let pages = pdfcraft_model::pages(doc);
|
||||
let pg = d.get(b"Pg").and_then(Object::as_ref).or_else(|| elements(doc).into_iter().find(|e| e.object == obj).and_then(|e| e.page));
|
||||
if let Some(page) = pg.and_then(|p| pages.iter().find(|x| x.obj == p)).filter(|_| !ids.is_empty()) {
|
||||
let props = page_properties(doc, &page.dict);
|
||||
let mut ops = pdfcraft_content::parse(&page_content(doc, &page.dict)).ops;
|
||||
let mut changed = false;
|
||||
for op in ops.iter_mut() {
|
||||
let ours = op.op == b"BDC"
|
||||
&& matches!(op.operands.get(1), Some(Object::Dict(m)) if m.get(b"MCID").and_then(Object::as_int).is_some_and(|n| ids.contains(&n)));
|
||||
if ours {
|
||||
if op.op == b"BDC" && bdc_mcid(doc, &props, op).is_some_and(|n| ids.contains(&n)) {
|
||||
*op = pdfcraft_content::Op::new("BMC", vec![Object::name("Artifact")]);
|
||||
changed = true;
|
||||
}
|
||||
|
||||
@@ -624,6 +624,62 @@ fn marking_decorative_survives_a_parent_tree_with_an_unpaired_key() {
|
||||
}
|
||||
}
|
||||
|
||||
/// `figures_doc` with its property lists named in the page resources (one inline, one indirect)
|
||||
/// instead of written inline in the content.
|
||||
fn named_figures_doc() -> Document {
|
||||
pdf(
|
||||
&[
|
||||
"<< /Type /Catalog /Pages 2 0 R /MarkInfo << /Marked true >> /StructTreeRoot 5 0 R >>".into(),
|
||||
"<< /Type /Pages /Kids [3 0 R] /Count 1 >>".into(),
|
||||
"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 200 200] /Contents 4 0 R /StructParents 0 \
|
||||
/Resources << /XObject << /Im 10 0 R >> /Properties << /P0 << /MCID 0 >> /P1 11 0 R /Other << /Note (kept) >> >> >> >>"
|
||||
.into(),
|
||||
stream(
|
||||
"/Figure /P0 BDC q 40 0 0 20 10 150 cm /Im Do Q EMC \
|
||||
/Photo /P1 BDC 100 100 50 30 re f EMC",
|
||||
),
|
||||
"<< /Type /StructTreeRoot /K 6 0 R /ParentTree 9 0 R /RoleMap << /Photo /Figure >> >>".into(),
|
||||
"<< /S /Document /P 5 0 R /K [7 0 R 8 0 R] >>".into(),
|
||||
"<< /S /Figure /P 6 0 R /Pg 3 0 R /K 0 >>".into(),
|
||||
"<< /S /Photo /P 6 0 R /Pg 3 0 R /Alt (A bar) /K 1 >>".into(),
|
||||
"<< /Nums [0 [7 0 R 8 0 R]] >>".into(),
|
||||
"<< /Type /XObject /Subtype /Image /Width 1 /Height 1 /ColorSpace /DeviceGray /BitsPerComponent 8 /Length 1 >>\nstream\n\u{0}\nendstream"
|
||||
.into(),
|
||||
"<< /MCID 1 >>".into(),
|
||||
],
|
||||
"",
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn named_property_lists_are_found_and_marked_decorative_like_inline_ones() {
|
||||
let (mut inline, mut named) = (figures_doc(), named_figures_doc());
|
||||
let place = |doc: &Document| figures(doc).into_iter().map(|f| (f.obj, f.page, f.alt, f.bbox)).collect::<Vec<_>>();
|
||||
assert_eq!(place(&named), place(&inline), "both figures, placed where their content is drawn");
|
||||
for doc in [&mut inline, &mut named] {
|
||||
mark_decorative(doc, pdfcraft_cos::ObjRef::new(8, 0)).unwrap();
|
||||
let left = figures(doc);
|
||||
assert_eq!((left.len(), left[0].obj.num, left[0].bbox), (1, 7, Some([10.0, 150.0, 50.0, 170.0])));
|
||||
let pages = pdfcraft_model::pages(doc);
|
||||
let c = doc.resolve(pages[0].dict.get(b"Contents").unwrap());
|
||||
let Object::Stream(c) = &*c else { panic!("one content stream") };
|
||||
let text = String::from_utf8(c.decoded().unwrap()).unwrap();
|
||||
assert!(text.contains("/Artifact BMC") && !text.contains("/P1") && !text.contains("/MCID 1"), "{text}");
|
||||
assert!(text.contains("/Im Do"), "the drawing itself stays: {text}");
|
||||
let nums = doc.get(pdfcraft_cos::ObjRef::new(9, 0)).as_dict().and_then(|d| d.get(b"Nums").cloned());
|
||||
assert_eq!(
|
||||
nums,
|
||||
Some(Object::Array(vec![Object::Int(0), Object::Array(vec![Object::Ref(pdfcraft_cos::ObjRef::new(7, 0)), Object::Null])])),
|
||||
"the parent tree no longer points MCID 1 at the removed figure"
|
||||
);
|
||||
let r = check(doc, &Options { rules: [Rule::FiguresAltText, Rule::TaggedContent].into(), pages: None });
|
||||
assert_eq!(r.result(Rule::TaggedContent).unwrap().status, Status::Passed, "{:?}", r.results);
|
||||
}
|
||||
let props = pdfcraft_model::pages(&named)[0].dict.get(b"Resources").and_then(|r| named.resolve(r).as_dict().cloned());
|
||||
let props = props.and_then(|r| r.get(b"Properties").and_then(|p| named.resolve(p).as_dict().cloned())).unwrap();
|
||||
assert!(props.contains(b"P0") && props.contains(b"P1") && props.contains(b"Other"), "resources are left as they were");
|
||||
}
|
||||
|
||||
fn ref_(n: u32) -> Option<pdfcraft_cos::ObjRef> {
|
||||
Some(pdfcraft_cos::ObjRef::new(n, 0))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user