mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-14 19:27:04 -04:00
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 <[email protected]>
This commit is contained in:
1 parent
1970935587
commit
05a862995d
11 files changed
+112
-14
No files matched your search
@@ -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.
|
||||
|
||||
@@ -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<Finding> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+25
-4
@@ -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<usize>,
|
||||
/// Indirect structure element that caused the finding, when one is available.
|
||||
pub object: Option<pdfcraft_cos::ObjRef>,
|
||||
/// 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<usize>, 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<usize>, 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<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, 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<HashMap<pdfcraft_cos::ObjRef, (usize, [f64; 4])>> = 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);
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
})
|
||||
|
||||
@@ -842,7 +842,7 @@ pub fn tools() -> Vec<ToolDef> {
|
||||
}),
|
||||
&["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(
|
||||
|
||||
@@ -72,6 +72,19 @@ fn form_pdf(name: &str) -> Vec<u8> {
|
||||
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");
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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| {
|
||||
|
||||
+1
-1
@@ -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.<br>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).<br>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).<br>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.<br>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<br>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 |
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in new issue
Block a user