From 05a862995d7dfc733eeb11e9113a8582e1b4d3ca Mon Sep 17 00:00:00 2001 From: Earl Co Date: Mon, 12 Oct 2026 07:29:25 +0200 Subject: [PATCH] Accessibility: show figure findings on the page (#1280) * Highlight locatable accessibility figure findings * M12: locate annotation and form accessibility findings * M12: keep one contributor profile entry --------- Co-authored-by: Brandon Thomas --- ROADMAP.md | 2 ++ crates/a11y/src/content.rs | 24 +++++++++++++++++++--- crates/a11y/src/lib.rs | 29 +++++++++++++++++++++++---- crates/a11y/src/tests.rs | 16 +++++++++++++++ crates/automation/src/a11y.rs | 1 + crates/automation/src/tools.rs | 2 +- crates/automation/tests/automation.rs | 15 ++++++++++++++ crates/ui-egui/src/a11y_ui.rs | 12 ++++++++--- crates/ui-egui/tests/accessibility.rs | 19 ++++++++++++++++++ docs/roadmap.md | 2 +- parity/acrobat-features.toml | 4 ++-- 11 files changed, 112 insertions(+), 14 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index e8cd3614..45a9919b 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -137,6 +137,8 @@ Ranked; detail and the full milestone table in [docs/roadmap.md](docs/roadmap.md ## Progress log +- **2026-10-12 (M12, accessibility finding locations):** Checker findings for figures, annotations and form widgets carry verified page bounds. Selecting one flashes its location; the headless check returns the same PDF-space rectangle. Malformed annotation rectangles remain page-only findings. Guided review and tagging estimates unchanged. + - **2026-10-12 (M14, gap 13, large-file memory):** Measured on synthetic 1 GiB and 100,000-page files. Edits let go of the previous working file (working bytes, display bytes, renderer) before writing the next, so a 1 GiB file peaks at two copies while editing instead of three (five edits: 3.1 → 2.1 GiB, 16 → 8.5 s). Saving still peaks at three; reading from disk on demand is blocked on the renderer (gap 2). Estimates unchanged. - **2026-10-12 (M2, short text redraws):** Same-line redraws that restart a matching three-glyph prefix after more than one glyph-height of progress split before placement, so short doubled words no longer interleave. The existing redraw-position tolerance is reused; controls cover negative TJ movement, combining marks, RTL, CJK, vertical and turned text. Estimates unchanged. - **2026-10-11 (M10.5, focused follow-up to #599):** Carry the architectural/ANSI/large ISO paper presets, current-page matching at 100%, and previewed PDF scale into the current print dialog and `doc_print`. Retains the #847/#1007 spooler and background discovery. Driver substitution, physical calibration, vector Windows printing and managed-endpoint restrictions remain separate gaps. Overall estimates unchanged. diff --git a/crates/a11y/src/content.rs b/crates/a11y/src/content.rs index a6910823..e8a7db37 100644 --- a/crates/a11y/src/content.rs +++ b/crates/a11y/src/content.rs @@ -218,6 +218,20 @@ fn subtype_name(d: &Dict) -> String { String::from_utf8_lossy(d.name(b"Subtype").unwrap_or(b"Unknown")).into_owned() } +/// An annotation's rectangle is already in page user space. Ignore missing, malformed and +/// degenerate rectangles rather than sending the checker to an arbitrary place on the page. +fn annotation_rect(doc: &Document, annot: &Dict) -> Option<[f64; 4]> { + let rect = annot.get(b"Rect").map(|r| doc.resolve(r))?; + let values = rect.as_array()?; + let [x0, y0, x1, y1] = values.as_slice() else { return None }; + let [x0, y0, x1, y1] = [x0, y0, x1, y1].map(|v| doc.resolve(v).as_f64()); + let (x0, y0, x1, y1) = (x0?, y0?, x1?, y1?); + if ![x0, y0, x1, y1].iter().all(|v| v.is_finite()) || x0 == x1 || y0 == y1 { + return None; + } + Some([x0.min(x1), y0.min(y1), x0.max(x1), y0.max(y1)]) +} + pub(crate) fn page_rule(doc: &Document, rule: Rule, pages: &[Page], list: &[usize], scan: &Scan, tagged: bool) -> Vec { let mut out = Vec::new(); let annots = |p: &Page| -> Vec<(ObjRef, Dict)> { @@ -260,7 +274,9 @@ pub(crate) fn page_rule(doc: &Document, rule: Rule, pages: &[Page], list: &[usiz values.push(("{}", subtype_name(&a))); "{} annotation on page {p} isn't tagged" }; - out.push(Finding::worded(Some(p), pattern, values)); + let mut finding = Finding::worded(Some(p), pattern, values); + finding.bbox = annotation_rect(doc, &a); + out.push(finding); } } } @@ -313,11 +329,13 @@ pub(crate) fn page_rule(doc: &Document, rule: Rule, pages: &[Page], list: &[usiz } let fd = doc.get(field).as_dict().cloned().unwrap_or_default(); if text_of(doc, &fd, b"TU").is_none() { - out.push(Finding::worded( + let mut finding = Finding::worded( Some(p), "Field \"{}\" on page {p} has no description (tooltip)", vec![("{}", full_name(doc, field)), ("{p}", (p + 1).to_string())], - )); + ); + finding.bbox = annotation_rect(doc, &a); + out.push(finding); } } } diff --git a/crates/a11y/src/lib.rs b/crates/a11y/src/lib.rs index 8154cd70..e7d8223e 100644 --- a/crates/a11y/src/lib.rs +++ b/crates/a11y/src/lib.rs @@ -8,7 +8,7 @@ #![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)] -use std::collections::BTreeSet; +use std::collections::{BTreeSet, HashMap}; use pdfcraft_cos::Document; @@ -426,6 +426,8 @@ pub struct Finding { pub page: Option, /// Indirect structure element that caused the finding, when one is available. pub object: Option, + /// Bounds in PDF user space when the affected content can be located safely. + pub bbox: Option<[f64; 4]>, pub message: String, /// The English sentence with a hole, such as `{p}` or `{what}`, where each value went. pub pattern: &'static str, @@ -436,13 +438,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, sentence: &'static str) -> Self { - Finding { page, object: None, message: sentence.to_owned(), pattern: sentence, values: Vec::new() } + Finding { page, object: None, bbox: 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, pattern: &'static str, values: Vec<(&'static str, String)>) -> Self { - Finding { page, object: None, message: say(pattern, &values), pattern, values } + Finding { page, object: None, bbox: None, message: say(pattern, &values), pattern, values } } /// A finding that says where the problem is and, in `{what}`, what is wrong, each worded @@ -454,7 +456,7 @@ impl Finding { pub fn nested(page: Option, 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, object: None, message, pattern, values } + Finding { page, object: None, bbox: None, message, pattern, values } } /// This finding as one sentence, said in the language `words` translates into. @@ -616,6 +618,7 @@ pub fn check(doc: &Document, options: &Options) -> Report { let tagged = structure::has_struct_tree(doc); let scan = content::Scan::run(doc, &pages, &page_list); let mut tree_rules = structure::run(doc, &pages, &options.rules); + let mut figure_bounds: Option> = None; let mut results = Vec::with_capacity(32); for rule in Rule::ALL { if !options.rules.contains(&rule) { @@ -640,6 +643,24 @@ pub fn check(doc: &Document, options: &Options) -> Report { } _ => tree_rules.remove(&rule).unwrap_or_default(), }; + if rule.category() == Category::AlternateText && findings.iter().any(|f| f.object.is_some()) { + let bounds = figure_bounds.get_or_insert_with(|| { + figures(doc) + .into_iter() + .filter_map(|figure| { + let (page, bbox) = (figure.page?, figure.bbox?); + (bbox.iter().all(|v| v.is_finite()) && bbox[0] < bbox[2] && bbox[1] < bbox[3]).then_some((figure.obj, (page, bbox))) + }) + .collect() + }); + for finding in &mut findings { + if let Some((page, bbox)) = finding.object.and_then(|obj| bounds.get(&obj)) + && finding.page == Some(*page) + { + finding.bbox = Some(*bbox); + } + } + } if total > MAX_FINDINGS { let more = total - MAX_FINDINGS; findings.truncate(MAX_FINDINGS); diff --git a/crates/a11y/src/tests.rs b/crates/a11y/src/tests.rs index 3d320e9b..abd11731 100644 --- a/crates/a11y/src/tests.rs +++ b/crates/a11y/src/tests.rs @@ -342,6 +342,19 @@ fn page_content_and_annotation_problems_are_found() { assert_eq!(got, [Rule::TaggedContent, Rule::TaggedAnnotations, Rule::TabOrder, Rule::FieldDescriptions]); assert_eq!(r.result(Rule::TaggedContent).unwrap().findings[0].message, "Page 1: 1 untagged content item"); assert!(r.result(Rule::FieldDescriptions).unwrap().findings[0].message.contains("\"name\"")); + assert_eq!(r.result(Rule::TaggedAnnotations).unwrap().findings[0].bbox, Some([20.0, 20.0, 80.0, 40.0])); + assert_eq!(r.result(Rule::FieldDescriptions).unwrap().findings[0].bbox, Some([20.0, 60.0, 120.0, 80.0])); +} + +#[test] +fn annotation_findings_only_locate_valid_rectangles() { + let cases = [("[80 40 20 20]", Some([20.0, 20.0, 80.0, 40.0])), ("[20 20 20 40]", None), ("[20 20 80]", None), ("[20 20 80 (bad)]", None)]; + for (rect, expected) in cases { + let mut objs = good(); + objs[8] = format!("<< /Type /Annot /Subtype /Link /Rect {rect} >>"); + let r = checked(&pdf(&objs, "/Info 8 0 R"), &[Rule::TaggedAnnotations]); + assert_eq!(r.result(Rule::TaggedAnnotations).unwrap().findings[0].bbox, expected, "{rect}"); + } } #[test] @@ -430,6 +443,9 @@ fn figures_are_listed_with_their_alt_text_and_place() { assert_eq!(f.len(), 2, "role-mapped Photo counts"); assert_eq!((f[0].page, f[0].alt.as_deref(), f[0].bbox), (Some(0), None, Some([10.0, 150.0, 50.0, 170.0]))); assert_eq!((f[1].alt.as_deref(), f[1].bbox), (Some("A bar"), Some([100.0, 100.0, 150.0, 130.0]))); + let report = check(&doc, &Options { rules: [Rule::FiguresAltText].into(), pages: None }); + let finding = &report.result(Rule::FiguresAltText).unwrap().findings[0]; + assert_eq!((finding.object, finding.page, finding.bbox), (Some(f[0].obj), Some(0), f[0].bbox)); } #[test] diff --git a/crates/automation/src/a11y.rs b/crates/automation/src/a11y.rs index 5384127d..ccf8128d 100644 --- a/crates/automation/src/a11y.rs +++ b/crates/automation/src/a11y.rs @@ -41,6 +41,7 @@ pub(crate) fn report_json(r: &Report) -> Value { "page": f.page.map(|p| p + 1), "object": f.object.map(|r| r.num), "object_generation": f.object.map(|r| r.generation), + "bbox_user_space": f.bbox, "message": f.message, }) }) diff --git a/crates/automation/src/tools.rs b/crates/automation/src/tools.rs index 740eb752..68bceb97 100644 --- a/crates/automation/src/tools.rs +++ b/crates/automation/src/tools.rs @@ -842,7 +842,7 @@ pub fn tools() -> Vec { }), &["doc", "folder"], )), - t("accessibility_check", "Check for accessibility", "Run the Accessibility Checker's full check (32 rules in 7 categories: document, page_content, forms, alternate_text, tables, lists, headings). Each rule is passed, failed (with findings and pages), manual (needs a person) or skipped. Colour contrast is off unless all is true; rules (ids such as tagged-pdf, figures-alt-text) or categories narrow the check; pages limit the page rules.") + t("accessibility_check", "Check for accessibility", "Run the Accessibility Checker's full check (32 rules in 7 categories: document, page_content, forms, alternate_text, tables, lists, headings). Each rule is passed, failed (with findings and pages), manual (needs a person) or skipped. Findings on locatable figures, annotations and form widgets include bbox_user_space in PDF coordinates. Colour contrast is off unless all is true; rules (ids such as tagged-pdf, figures-alt-text) or categories narrow the check; pages limit the page rules.") .ro() .cmd("a11y.check") .with(schema( diff --git a/crates/automation/tests/automation.rs b/crates/automation/tests/automation.rs index c75b8be7..30765cc0 100644 --- a/crates/automation/tests/automation.rs +++ b/crates/automation/tests/automation.rs @@ -72,6 +72,19 @@ fn form_pdf(name: &str) -> Vec { bytes } +#[test] +fn accessibility_check_locates_field_description_on_page() { + let dir = workdir("accessibility-field-location"); + std::fs::write(dir.join("field.pdf"), form_pdf("name")).unwrap(); + let mut a = auto(&dir); + let doc = ok(&mut a, "doc_open", json!({ "path": "field.pdf" }))["doc"].as_u64().unwrap(); + let report = ok(&mut a, "accessibility_check", json!({ "doc": doc, "rules": ["field-descriptions"] })); + let rule = report["results"].as_array().unwrap().iter().find(|r| r["rule"] == "field-descriptions").unwrap(); + assert_eq!(rule["status"], "failed"); + assert_eq!(rule["findings"][0]["page"], 1); + assert_eq!(rule["findings"][0]["bbox_user_space"], json!([60.0, 60.0, 200.0, 80.0])); +} + fn ok(a: &mut Automation, tool: &str, args: Value) -> Value { match a.call(tool, &args) { Ok(mut c) => match c.remove(0) { @@ -1705,8 +1718,10 @@ trailer << /Root 1 0 R >> 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"][0]["bbox_user_space"], json!([10.0, 150.0, 50.0, 170.0])); assert_eq!(figures_rule["findings"][1]["object"], 8); assert_eq!(figures_rule["findings"][1]["object_generation"], 0); + assert_eq!(figures_rule["findings"][1]["bbox_user_space"], json!([100.0, 100.0, 150.0, 130.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"); diff --git a/crates/ui-egui/src/a11y_ui.rs b/crates/ui-egui/src/a11y_ui.rs index 40505cc4..1e7ab215 100644 --- a/crates/ui-egui/src/a11y_ui.rs +++ b/crates/ui-egui/src/a11y_ui.rs @@ -59,7 +59,7 @@ pub(crate) enum PanelAction { CheckAgain, ShowReport, Options, - GoTo(usize), + GoTo(usize, Option<[f64; 4]>), } /// Rules with an automatic (or guided) fix. @@ -294,7 +294,7 @@ pub(crate) fn panel(ui: &mut egui::Ui, t: &Tokens, state: &mut A11yState, doc: D if let Some(p) = f.page && l.on_hover_text(tl!("Go to the page")).clicked() { - action = Some(PanelAction::GoTo(p)); + action = Some(PanelAction::GoTo(p, f.bbox)); } } }); @@ -372,7 +372,13 @@ impl PdfCraftApp { PanelAction::CheckAgain => self.run_accessibility_check(), PanelAction::Options => self.dialog = Some(Dialog::AccessibilityOptions), PanelAction::ShowReport => self.show_accessibility_report(), - PanelAction::GoTo(p) => self.views[index].go_to_page(p), + PanelAction::GoTo(p, bbox) => { + let view = &mut self.views[index]; + view.go_to_page(p); + if let Some(bbox) = bbox { + view.flash = Some((p, bbox.map(|v| v as f32), 0.0)); + } + } PanelAction::Skip(rule) => { self.a11y_skipped.insert(rule); if let Some((_, r)) = self.a11y.report.as_mut() diff --git a/crates/ui-egui/tests/accessibility.rs b/crates/ui-egui/tests/accessibility.rs index b4be8841..f8c69c81 100644 --- a/crates/ui-egui/tests/accessibility.rs +++ b/crates/ui-egui/tests/accessibility.rs @@ -107,6 +107,25 @@ endstream endobj trailer << /Root 1 0 R >> %%EOF"; +#[test] +fn checker_shows_a_figure_finding_on_its_page() { + let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).build_eframe(move |_cc| { + let mut app = PdfCraftApp::new(); + app.set_option("language", "en").unwrap(); + app.open_bytes("figures.pdf", None, FIGURES.to_vec()).unwrap(); + app + }); + h.run_steps(4); + h.state_mut().run_accessibility_check(); + h.run_steps(3); + h.get_by_label("Figures alternate text - Failed").click(); + h.run_steps(2); + h.get_by_label_contains("Figure element on page 1: no alternate text").click(); + h.run_steps(1); + let flash = h.state().views[0].flash.map(|(page, rect, _)| (page, rect)); + assert_eq!(flash, Some((0, [10.0, 150.0, 50.0, 170.0]))); +} + #[test] fn setting_alternate_text_figure_by_figure() { let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).build_eframe(move |_cc| { diff --git a/docs/roadmap.md b/docs/roadmap.md index 143ba103..546df8de 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -92,7 +92,7 @@ total matches the parity estimate. | M9 | Signatures (PAdES, validation) | 160–280 | 60% | 40–70 | Done: new `sign` crate (DER, X.509, CMS, PKCS #12 on RustCrypto; aws-lc-rs for RSA private keys); PAdES B-B signing (visible, invisible, existing fields, certification with DocMDP); validation with trust store and changes-after-signing classification; self-signed digital IDs; Signatures panel, message bar, sign dialogs; agent tools. Checked with pdfsig and OpenSSL.
Done since: macOS Keychain and Windows Current User Personal store signing (software-backed CNG keys), certificate viewer, signed documents protected from rewrites. Missing: timestamps (B-T), LTV (DSS, OCSP, CRL), FieldMDP, smart cards, PKCS #11 | | M10 | OCR, create, export, print | 200–350 | 33% | 120–215 | Done: create from blank/text/PNG/JPEG/TIFF (multi-page)/GIF/BMP; export PNG/JPEG/TIFF and text; Print (Acrobat's sizing, n-up, booklet, poster, comments & forms, preview, the CUPS and Windows spoolers, print-ready PDF).
Done since: embedded/72/custom DPI choices for image imports; export all images; OCR (searchable image for pages, ranges and multiple files); single-sided cut-and-stack imposition with cut marks, through Print and doc_print; architectural/ANSI/large ISO sheet presets, Match page at 100% and PDF-scale preview (#599 follow-up); Create PDF from multiple files (PDFs, images and text, edited as pages in the grid); OCR can also read pictures on pages that have text, without adding that text twice (#1028). Done since: Word invoice export uses borderless inferred tables, floating positioned text/artwork only where text overlaps an image or is rotated, and per-page sizes, covered by synthetic invoice automation regressions (#773). Missing: OCR languages beyond Latin, editable-text OCR output, full Office layout fidelity/import, web printing | | M11 | Optimize, preflight, PDF/A/X/UA, print production | 200–350 | 18% | 80–140 | Done: new `optimize` crate: Reduce File Size and the PDF Optimizer (images measured where drawn, bicubic downsampling, JPEG/ZIP recompression only when smaller, discard objects and user data, Flate clean-up, resource merging, object streams). Missing: fonts and transparency panels, space audit, preflight, PDF/A/X/UA | -| M12 | Accessibility, compare, measure, search, XFA | 200–380 | 30% | 90–160 | Done: new `a11y` crate with the Accessibility Checker (all 32 rules, report, Fix/Skip/Explain, options dialog and results panel, agent tools).
Done since: 2D distance, perimeter and area measurements with persistent viewport calibration, snapping, live information and CSV export. Missing: autotag, Tags/Order/Content panels, Reading Order tool, alt-text workflow, compare, geospatial/3D measurement, search index, XFA | +| M12 | Accessibility, compare, measure, search, XFA | 200–380 | 30% | 90–160 | Done: new `a11y` crate with the Accessibility Checker (all 32 rules, report, Fix/Skip/Explain, options dialog and results panel, agent tools). Locatable figure, annotation and form findings highlight their page bounds.
Done since: 2D distance, perimeter and area measurements with persistent viewport calibration, snapping, live information and CSV export. Missing: autotag, Tags/Order/Content panels, Reading Order tool, alt-text workflow, compare, geospatial/3D measurement, search index, XFA | | M13 | Automation (MCP, Action Wizard, CLI) + AI providers | 60–120 | 45% | 25–45 | Done: MCP resources (document info, text, page images); headless tool table (123 tools incl. signing, optimizing, initial view, links, stamps, data exchange, comment review, forms authoring and scripts, redaction, sanitize, print, add content), opt-in MCP server over stdio, CLI `run`/`tools` (closed stdout pipes exit cleanly), UI control channel with drag. Missing: Action Wizard, AI providers
Done since: CLI run and MCP reject missing, empty, repeated and option-valued --root arguments before any tool/script execution or MCP initialization. A single-hyphen directory name remains valid; synthetic CLI regressions preserve a would-be overwritten file. `text_find` also caps the per-page match ranges it materializes at the remaining result limit plus one continuation match. | | M14 | 1.0 polish: performance, localization, installers | 120–250 | 15% | 80–150 | Done: PhotoCraft's translation system (`i18n/`: TSV catalogs, `tl!`, command-id and plural entries, system-language detection, strict catalog tests); every dialog, panel and notice goes through `tl!`. Catalogs of ≈ 2,000–2,140 entries for Japanese, Simplified and Traditional Chinese, Russian, Bulgarian, German, Spanish, French, Telugu, Hungarian, Ukrainian, Italian, Brazilian Portuguese and Arabic (logical-order catalog put into display order; layout not yet mirrored); Czech covers the menus. Signed and notarized macOS builds, signed Windows installers (x64, x86, ARM64), AppImage, deb, rpm, Flatpak and FreeBSD packages. #307 (bounded large-PDF memory): shared resource indexes and parser metadata, lazy inspection, bounded caches; a 9,156-page file inspects in ≈ 1.8 s at ≈ 500 MiB instead of exceeding 10 GiB. Edits let go of the previous working file before writing the next (a 1 GiB file peaks at two copies while editing, not three; gap 13). Shortcut display uses localized modifier/key names across menus, toolbar, palette, Help and tooltips (#496). Missing: a right-to-left layout, a Chinese UI face in releases, Hindi/Indonesian/Korean/Vietnamese, file-backed input and global memory budgets, performance budgets, keyboard-only operation | | M13 | Automation (MCP, Action Wizard, CLI) + AI providers | 60–120 | 45% | 25–45 | Done: MCP resources (document info, text, page images); headless tool table (123 tools incl. signing, optimizing, initial view, links, stamps, data exchange, comment review, forms authoring and scripts, redaction, sanitize, print, add content), opt-in MCP server over stdio, CLI `run`/`tools` (closed stdout pipes exit cleanly), UI control channel with drag. Missing: Action Wizard, AI providers | diff --git a/parity/acrobat-features.toml b/parity/acrobat-features.toml index 0f2ede79..34747f74 100644 --- a/parity/acrobat-features.toml +++ b/parity/acrobat-features.toml @@ -8390,8 +8390,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/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." +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/a11y/src/tests.rs::figures_are_listed_with_their_alt_text_and_place", "crates/a11y/src/tests.rs::annotation_findings_only_locate_valid_rectangles", "crates/automation/tests/automation.rs::accessibility_check_locates_field_description_on_page", "crates/ui-egui/tests/accessibility.rs::checking_fixing_skipping_and_reporting", "crates/ui-egui/tests/accessibility.rs::checker_shows_a_figure_finding_on_its_page", "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. Locatable figure, annotation and form-widget findings flash their bounds on the page; accessibility_check exposes those bounds in PDF user space. Other findings navigate 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"