diff --git a/crates/a11y/src/alt.rs b/crates/a11y/src/alt.rs index 77dda106ed..55c0857878 100644 --- a/crates/a11y/src/alt.rs +++ b/crates/a11y/src/alt.rs @@ -452,8 +452,11 @@ fn artifact_marked_content( for op in pieces.parse().ops { if let Some(id) = marked_id(doc, &props, &op, ids) { // What follows the damage is unknown: a string or comment the damage left open runs - // on into the next stream, so what reads as an operator after it may not be one. - if damage.is_some_and(|d| pieces.piece_of(op.span.start) > d) { + // on into the next stream, so what reads as an operator after it may not be one. An + // operator whose operands start before a stream not read whole and whose keyword comes + // after it takes them from around what wasn't read: that stream may end them itself. + let (first, last) = pieces.pieces_of(&op); + if damage.is_some_and(|d| last > d) || read.get(first..last).is_some_and(|r| r.iter().any(|r| *r != Read::Whole)) { return Ok(Marked::Damaged); } found.insert(id); diff --git a/crates/a11y/src/tests.rs b/crates/a11y/src/tests.rs index 960eb39d3d..d8566eddf1 100644 --- a/crates/a11y/src/tests.rs +++ b/crates/a11y/src/tests.rs @@ -1220,6 +1220,40 @@ fn a_tags_content_after_a_damaged_stream_is_not_rewritten() { } } +/// A page may split its content between an operator's operands and its keyword. When the operands +/// of the figure's `BDC` end the stream before one damaged from its start or one that doesn't +/// decode, the `BDC` the page reads is in what wasn't read: joined without it, those operands and +/// the next tag's read as one `BDC`, which reaches into the stream after it. Neither marking +/// changes anything, the structure tree included. +#[test] +fn a_tags_operands_before_a_stream_not_read_whole_are_not_rewritten() { + use pdfcraft_cos::{Dict, ObjRef, Object, Stream}; + let rest = b" BDC q 40 0 0 20 10 150 cm /Im Do Q EMC"; + for damaged in [true, false] { + let mut doc = figures_doc(); + doc.set(ObjRef::new(4, 0), Stream::from_raw(Dict::new(), b"/Figure << /MCID 0 >>".to_vec())); + let hidden = if damaged { + damaged_flate(b"", rest) + } else { + let filter = doc.add(Object::name("FlateDecode")); + let mut s = Stream::flate(Dict::new(), rest); + s.dict.set(b"Filter".to_vec(), Object::Ref(filter)); + s + }; + let hidden = doc.add(hidden); + let after = doc.add(Object::Stream(Stream::from_raw(Dict::new(), b"/Photo << /MCID 1 >> BDC 100 100 50 30 re f EMC".to_vec()))); + let listed = Object::Array(vec![Object::Ref(ObjRef::new(4, 0)), Object::Ref(hidden), Object::Ref(after)]); + doc.update_dict(ObjRef::new(3, 0), |p| p.set(b"Contents".to_vec(), listed)).unwrap(); + let state = |doc: &Document| { + (doc.get(ObjRef::new(3, 0)).as_dict().unwrap().get(b"Contents").cloned(), doc.get(ObjRef::new(9, 0)), doc.get(ObjRef::new(6, 0))) + }; + let before = state(&doc); + assert_eq!(mark_tag_artifact(&mut doc, ObjRef::new(7, 0)), Err(ArtifactError::Damaged), "damaged: {damaged}"); + assert_eq!(mark_decorative(&mut doc, ObjRef::new(7, 0)), Err(AltError::Damaged), "damaged: {damaged}"); + assert_eq!(state(&doc), before, "damaged: {damaged}"); + } +} + /// A tag's content not found on the page may be in a stream that doesn't decode: it isn't known /// to be off the page, so neither marking changes anything, the structure tree included. #[test] diff --git a/crates/automation/src/tools.rs b/crates/automation/src/tools.rs index e206f042bc..c5b841860e 100644 --- a/crates/automation/src/tools.rs +++ b/crates/automation/src/tools.rs @@ -1285,7 +1285,7 @@ pub fn tools() -> Vec { "property": { "type": "string", "enum": ["type", "title", "actual_text", "alternate_text", "language", "summary", "scope"] }, "value": { "type": "string" }, }), &["doc", "object", "generation", "property"])), - t("accessibility_mark_tag_artifact", "Mark tag content as artifact", "Mark a reachable leaf structure tag's direct marked content as an artifact and remove the tag from the reading structure. Use object and generation from accessibility_tags. Tags with child structure elements, no direct marked content, content not found on its page, or content that may be in or after a damaged content stream are rejected. Undoable.") + t("accessibility_mark_tag_artifact", "Mark tag content as artifact", "Mark a reachable leaf structure tag's direct marked content as an artifact and remove the tag from the reading structure. Use object and generation from accessibility_tags. Tags with child structure elements, no direct marked content, content not found on its page, or content that a damaged or undecodable content stream may hide or change are rejected. Undoable.") .with(schema(json!({ "doc": doc(), "object": { "type": "integer", "minimum": 1 }, "generation": { "type": "integer", "minimum": 0, "maximum": 65535 },