Accessibility: identify structure objects in checker findings (#1207)
* Accessibility: identify structure objects in checker findings * Include object generation in accessibility findings
This commit is contained in:
parent
0824f44daf
commit
fe2eadb211
|
|
@ -422,6 +422,8 @@ impl Status {
|
|||
pub struct Finding {
|
||||
/// 0-based page, when the problem is on a page.
|
||||
pub page: Option<usize>,
|
||||
/// Indirect structure element that caused the finding, when one is available.
|
||||
pub object: Option<pdfcraft_cos::ObjRef>,
|
||||
pub message: String,
|
||||
/// The English sentence with a hole, such as `{p}` or `{what}`, where each value went.
|
||||
pub pattern: &'static str,
|
||||
|
|
@ -432,13 +434,13 @@ pub struct Finding {
|
|||
impl Finding {
|
||||
/// A finding whose sentence says the same thing however the document is put together.
|
||||
pub fn plain(page: Option<usize>, sentence: &'static str) -> Self {
|
||||
Finding { page, message: sentence.to_owned(), pattern: sentence, values: Vec::new() }
|
||||
Finding { page, object: None, message: sentence.to_owned(), pattern: sentence, values: Vec::new() }
|
||||
}
|
||||
|
||||
/// A finding built from a wording and what goes in its holes. The English `message` is filled
|
||||
/// in here, so it and `pattern` can never drift apart.
|
||||
pub fn worded(page: Option<usize>, pattern: &'static str, values: Vec<(&'static str, String)>) -> Self {
|
||||
Finding { page, message: say(pattern, &values), pattern, values }
|
||||
Finding { page, object: None, message: say(pattern, &values), pattern, values }
|
||||
}
|
||||
|
||||
/// A finding that says where the problem is and, in `{what}`, what is wrong, each worded
|
||||
|
|
@ -450,7 +452,7 @@ impl Finding {
|
|||
pub fn nested(page: Option<usize>, pattern: &'static str, what: &'static str, mut values: Vec<(&'static str, String)>) -> Self {
|
||||
let message = say(&pattern.replace("{what}", what), &values);
|
||||
values.push(("{what}", what.to_owned()));
|
||||
Finding { page, message, pattern, values }
|
||||
Finding { page, object: None, message, pattern, values }
|
||||
}
|
||||
|
||||
/// This finding as one sentence, said in the language `words` translates into.
|
||||
|
|
|
|||
|
|
@ -154,6 +154,8 @@ fn kind_of(ty: &[u8]) -> Kind {
|
|||
/// is filled in as the walk goes on.
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct Frame {
|
||||
/// Indirect object holding this tag, if it is not an inline dictionary.
|
||||
object: Option<ObjRef>,
|
||||
/// The standard type after the role map, or the type as written when it maps to nothing.
|
||||
ty: Vec<u8>,
|
||||
kind: Kind,
|
||||
|
|
@ -314,7 +316,7 @@ impl Walker<'_> {
|
|||
}
|
||||
Object::Int(_) => self.mark_content(),
|
||||
Object::Ref(r) => self.reference(*r, depth, page, v),
|
||||
Object::Dict(_) => self.dict(loc, depth, page, v),
|
||||
Object::Dict(_) => self.dict(loc, depth, page, None, v),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
|
@ -326,13 +328,13 @@ impl Walker<'_> {
|
|||
}
|
||||
let target = self.doc.get(r);
|
||||
if target.as_dict().is_some() {
|
||||
self.dict(&Loc { owner: target, path: Vec::new() }, depth, page, v);
|
||||
self.dict(&Loc { owner: target, path: Vec::new() }, depth, page, Some(r), v);
|
||||
}
|
||||
}
|
||||
|
||||
/// A dictionary. Marked content and object references only mark the open element; a
|
||||
/// dictionary with /S is an element, and its /K kids are read inside it.
|
||||
fn dict<V: Visitor>(&mut self, loc: &Loc, depth: usize, page: Option<usize>, v: &mut V) {
|
||||
fn dict<V: Visitor>(&mut self, loc: &Loc, depth: usize, page: Option<usize>, object: Option<ObjRef>, v: &mut V) {
|
||||
let Some(d) = loc.value().and_then(|o| o.as_dict()) else { return };
|
||||
match d.name(b"Type") {
|
||||
Some(b"MCR") => return self.mark_content(),
|
||||
|
|
@ -356,6 +358,7 @@ impl Walker<'_> {
|
|||
let kind = kind_of(&ty);
|
||||
let attrs = if matches!(kind, Kind::Table | Kind::Td | Kind::Th) { self.attr_dicts(d) } else { Vec::new() };
|
||||
let frame = Frame {
|
||||
object,
|
||||
ty,
|
||||
kind,
|
||||
index: self.opened,
|
||||
|
|
@ -463,13 +466,15 @@ fn row_owner(path: &[Frame]) -> Option<usize> {
|
|||
fn worded(e: &Frame, what: &'static str, extra: Vec<(&'static str, String)>) -> Finding {
|
||||
let mut values = extra;
|
||||
values.push(("{ty}", String::from_utf8_lossy(&e.ty).into_owned()));
|
||||
match e.page {
|
||||
let mut finding = match e.page {
|
||||
Some(p) => {
|
||||
values.push(("{p}", p.saturating_add(1).to_string()));
|
||||
Finding::nested(e.page, "{ty} element on page {p}: {what}", what, values)
|
||||
}
|
||||
None => Finding::nested(None, "{ty} element: {what}", what, values),
|
||||
}
|
||||
};
|
||||
finding.object = e.object;
|
||||
finding
|
||||
}
|
||||
|
||||
/// A finding whose `{what}` wording has no holes of its own.
|
||||
|
|
|
|||
|
|
@ -216,6 +216,16 @@ fn cycles_shared_kids_and_missing_kids_are_read_once() {
|
|||
assert_eq!(r.result(Rule::FiguresAltText).unwrap().findings.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn structure_findings_identify_indirect_elements_without_guessing_inline_objects() {
|
||||
let doc = structure_doc(&["<< /S /Document /K [4 0 R << /S /Figure >>] >>".into(), "<< /S /Figure >>".into()]);
|
||||
let r = checked(&doc, &[Rule::FiguresAltText]);
|
||||
let found = &r.result(Rule::FiguresAltText).unwrap().findings;
|
||||
assert_eq!(found.len(), 2);
|
||||
assert_eq!(found[0].object.map(|r| r.num), Some(4));
|
||||
assert_eq!(found[1].object, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nesting_is_read_to_the_depth_limit_and_no_further() {
|
||||
// Sections, each the kid of the one before, ending in a figure without alternate text. The
|
||||
|
|
|
|||
|
|
@ -31,7 +31,18 @@ pub(crate) fn report_json(r: &Report) -> Value {
|
|||
.results
|
||||
.iter()
|
||||
.map(|x| {
|
||||
let findings: Vec<Value> = x.findings.iter().map(|f| json!({ "page": f.page.map(|p| p + 1), "message": f.message })).collect();
|
||||
let findings: Vec<Value> = x
|
||||
.findings
|
||||
.iter()
|
||||
.map(|f| {
|
||||
json!({
|
||||
"page": f.page.map(|p| p + 1),
|
||||
"object": f.object.map(|r| r.num),
|
||||
"object_generation": f.object.map(|r| r.generation),
|
||||
"message": f.message,
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
json!({ "rule": x.rule.id(), "name": x.rule.name(), "category": category_id(x.rule.category()), "status": status_id(x.status), "findings": findings })
|
||||
})
|
||||
.collect();
|
||||
|
|
|
|||
|
|
@ -1565,6 +1565,12 @@ trailer << /Root 1 0 R >>
|
|||
)
|
||||
.unwrap();
|
||||
let fd = ok(&mut a, "doc_open", json!({ "path": "figures.pdf" }))["doc"].as_u64().unwrap();
|
||||
let findings = ok(&mut a, "accessibility_check", json!({ "doc": fd, "rules": ["figures-alt-text"] }));
|
||||
let figures_rule = findings["results"].as_array().unwrap().iter().find(|x| x["rule"] == "figures-alt-text").unwrap();
|
||||
assert_eq!(figures_rule["findings"][0]["object"], 7);
|
||||
assert_eq!(figures_rule["findings"][0]["object_generation"], 0);
|
||||
assert_eq!(figures_rule["findings"][1]["object"], 8);
|
||||
assert_eq!(figures_rule["findings"][1]["object_generation"], 0);
|
||||
let figs = ok(&mut a, "accessibility_figures", json!({ "doc": fd }));
|
||||
assert_eq!(figs["count"], 2);
|
||||
assert_eq!(figs["figures"][0]["rect"], json!([10.0, 30.0, 50.0, 50.0]), "top-left-origin points");
|
||||
|
|
|
|||
|
|
@ -8374,8 +8374,8 @@ milestone = "M12"
|
|||
status = "shipped"
|
||||
commands = ["a11y.check"]
|
||||
tools = ["accessibility_check"]
|
||||
evidence = ["crates/a11y/src/tests.rs::an_untagged_document_fails_the_document_rules", "crates/a11y/src/tests.rs::a_well_tagged_document_passes", "crates/ui-egui/tests/accessibility.rs::checking_fixing_skipping_and_reporting", "crates/automation/tests/automation.rs::accessibility_check_report_and_fixes_through_tools"]
|
||||
notes = "full check: options dialog (report, page range, 32 rules by category, 31 on by default), results panel by category with Passed / Failed / Needs manual check / Skipped and findings that go to their page"
|
||||
evidence = ["crates/a11y/src/tests.rs::an_untagged_document_fails_the_document_rules", "crates/a11y/src/tests.rs::a_well_tagged_document_passes", "crates/a11y/src/tests.rs::structure_findings_identify_indirect_elements_without_guessing_inline_objects", "crates/ui-egui/tests/accessibility.rs::checking_fixing_skipping_and_reporting", "crates/automation/tests/automation.rs::accessibility_check_report_and_fixes_through_tools"]
|
||||
notes = "full check: options dialog (report, page range, 32 rules by category, 31 on by default), results panel by category with Passed / Failed / Needs manual check / Skipped and findings that go to their page. Structure findings expose the indirect tag object number and generation through accessibility_check, when available, for precise follow-up in the Tags Panel."
|
||||
|
||||
[[feature]]
|
||||
id = "a11y.checker-report"
|
||||
|
|
|
|||
Loading…
Reference in New Issue