M12: tag unstructured AcroForm widgets (#1451)

* M12: tag unstructured AcroForm widgets

* i18n: translate form tag command

* i18n: repair form-field label merges

* fix(i18n): refresh translation catalog hashes

* fix(i18n): preserve main translations after rebase

* Merge main; tag a widget listed on several pages once, docs say existing trees are extended

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Merge main; add the Vietnamese catalog entry for Tag form fields

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Earl Co
2026-10-15 03:24:55 -04:00
committed by GitHub
co-authored by Claude Opus 5.5 Brandon Thomas
parent 416c85579b
commit afa50999ef
38 changed files with 344 additions and 30 deletions
+22 -22
View File
@@ -7,7 +7,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Greek UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "48add323da3a6a7643e8019692712e135d57fc996cf8c91397ca155c128a231b"
sha256 = "9976cffa76284284c79e3270dcffac1c393148e8c0ee7113d5793fedcbb698ec"
# ATTRIBUTION.toml: one entry per asset (AGENTS.md §1). The source of truth for provenance and licences.
# ATTRIBUTION.md is generated from this file: `cargo xtask assets --write`. Checked by `cargo xtask assets` (part of `xtask ci`).
@@ -30,7 +30,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Simplified Chinese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "dacff0f922e0b5c05614dd424e8d2d42ef50ebf88617f9fb46d6397368831c62"
sha256 = "4597a7131ac94a4893e1f84b6585b4f63d54551795c7c17af1f0c7b60178cd38"
[[asset]]
path = "crates/ui-egui/src/i18n/zh-hant.tsv"
@@ -41,7 +41,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Traditional Chinese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "050af73f413f418a884779b200399a1f34c60b23f319376720387c4ab8a68bde"
sha256 = "47af30988b5776a138033de15549df77e198cfe1cc2285e37f94d0af872b259d"
[[asset]]
path = "crates/ui-egui/src/i18n/cs.tsv"
@@ -52,7 +52,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Czech UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "d10d93d5d8a4263f76ec4ff2b422cec460b462811b05e5835bb8e11a8eca420c"
sha256 = "faee91b7cb2cee41b831779ad6309952d399b83709007b336ca116a506525dec"
[[asset]]
path = "crates/ui-egui/src/i18n/pt-br.tsv"
@@ -63,7 +63,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Brazilian Portuguese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "8f4b89f85ef458195501f8b94c6ccc49f2613d780e5f96a49c6376f326e21255"
sha256 = "13bd6b4176147feafd9b1ddff71aa75778c1e01119537ce5e36f6da3284c89b1"
[[asset]]
path = "crates/ui-egui/src/i18n/es.tsv"
@@ -74,7 +74,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Spanish UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "cccacda996b0864810ca50ab5bbe011f086558d29d3d6b85343c6473774ef3d6"
sha256 = "897fbcf310a56be8b8d5b2b23dfe2c45d4f3c565792c2d28b58161c435d4aa5c"
[[asset]]
path = "crates/ui-egui/src/i18n/fr.tsv"
@@ -85,7 +85,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "French UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "1ab4346ba91874eddc9800c15a40d46e2399cbbf90bba4481785e25961b14602"
sha256 = "c593532d581c2bae30f0c03be3261256fcae4b703ab154a2cb5e9026247874bd"
[[asset]]
path = "crates/ui-egui/src/i18n/de.tsv"
@@ -96,7 +96,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "German UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "58e9d3d0bdedc03dfd515c767dc7e83d211b54b52783402357c36d9e4012062b"
sha256 = "a724e544044561df9880a68a924dc2225716095c995ebbe93420e0ddd599835a"
[[asset]]
path = "crates/ui-egui/src/i18n/te.tsv"
@@ -107,7 +107,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Telugu UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "1ab3877b027c600ba3cec01523eac90172098ce800ac32fbee93a98a039e0e71"
sha256 = "e92ba4c24b2183d90f007f3d50ef1d90ae10e82aea4d45b8f838967cf7db1bde"
[[asset]]
path = "crates/ui-egui/src/i18n/it.tsv"
@@ -118,7 +118,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Italian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "93a5c2631cfb458bdd06d12b20783c0e9f70d73ef6cc007651824ff04a7ff772"
sha256 = "e1b873432f3bcd2ccadfbdf90b912e47af8d733062d4d34bd0d338f05b57c6c2"
[[asset]]
path = "crates/ui-egui/src/i18n/pl.tsv"
@@ -129,7 +129,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Polish UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "dccace4145bf5be6961e03413e1baac3022b23343fb8ec1bb80e4b771b9b15c1"
sha256 = "7ebdede05e7d63b127232e5e3ebb36220a529f12ad73badbfd2c155ecafb31b6"
[[asset]]
path = "crates/ui-egui/src/i18n/ru.tsv"
@@ -140,7 +140,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Russian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "980f345f77d501e1413b734dd343f70a3fc535752a118647e1c3c71719008672"
sha256 = "66897289a489e2b65052d9fb21238e882c6d9cf6652d346b072f0e837367ec23"
[[asset]]
path = "crates/ui-egui/src/i18n/hu.tsv"
@@ -151,7 +151,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Hungarian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "ba02397a61f224605fda9fd17416b5e364724c932392ebdda34153310190c07a"
sha256 = "a946a8756b80dbb0505b07dbfe0c01ae4260deab6d3b0beb37b43552eef03536"
[[asset]]
path = "crates/ui-egui/src/i18n/bg.tsv"
@@ -162,7 +162,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Bulgarian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "d59371e117366bf36ff8627db5abc355d354dc73b61d09e20c78028a492c2518"
sha256 = "0deac0ca8e57642d7d43971fc94d151e500d5234ba0f221a25a0b3754150fef0"
[[asset]]
path = "crates/ui-egui/src/i18n/kk.tsv"
@@ -173,7 +173,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Kazakh UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "c39f98e74458221d474623a67345e54623307a7cb6396d4927439b5cacfec831"
sha256 = "67aac3be3f312649900310dfdafcdd8233b463e0366185bfe13d97f30eca4df8"
[[asset]]
path = "crates/ui-egui/src/i18n/ja.tsv"
@@ -184,7 +184,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Japanese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "ac27ed3c24f09c5e727ecce94de2313621394457f6cc03b7ebd440331ae62b99"
sha256 = "d2131a65c052b25b72c796bcc235de88c3d662814f015de666a5eb112889944f"
[[asset]]
path = "crates/ui-egui/src/i18n/uk.tsv"
@@ -195,7 +195,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Ukrainian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "b312eadaea1f306399c9285b528bca282a204c559885546cbd1213509a6f08fe"
sha256 = "320cb208f7e61750480c96e95b3bebe4000efb4bf5218668c3c8d7d630369ec2"
[[asset]]
path = "crates/ui-egui/src/i18n/ar.tsv"
@@ -206,7 +206,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Arabic UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "a0f9d57e72d92ff5c8822c38681774b4cd49c6c2a8d174322c3e8e2404bd4e33"
sha256 = "b32f085ffd99fd92e7e2457c44aa51bd7a0831734008fa46c542f4691fa4cb30"
[[asset]]
path = "crates/ui-egui/src/i18n/tr.tsv"
@@ -217,7 +217,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Turkish UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "4f0505cf19f18e9152d692861dec6ee9bf1127055e799a65a3305e9d0681a9da"
sha256 = "9ec9155f36d5b5e36ef1b27646ee72dc625cde0e6ba07c54d633d6aaabe45c8f"
[[asset]]
path = "crates/ui-egui/src/i18n/lt.tsv"
@@ -228,7 +228,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Lithuanian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "f8972322dbe61d739bef4a29e3a4a2a16ac267652375413646273e1c1a917515"
sha256 = "93039a5d4f5b78e29cb6f0212fec4c705fc9a47bc2d89dcf6b00de07d505a608"
[[asset]]
@@ -240,7 +240,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Vietnamese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "a05bf2b649ce3fcfa2f990871daa2b97a6464db7b313f606868688806d23f8e1"
sha256 = "4cc6a7220f27ce4fcf9787f56d763b6e2aa4e29a682111fd8ab21dac5689f99e"
[[asset]]
path = "assets/icons/accessibility.svg"
@@ -3363,7 +3363,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Slovene UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "0c6a8713138e05dd8a2996d7e2da097a03e321e5212a4071bbe58c4c846179bf"
sha256 = "39b64b099db9a6d4d54d1a67f2923dbce0baea9ade737eb8925db8a1faf66ac3"
[[asset]]
path = "vendor/vello_common/assets/probe.png"
+138
View File
@@ -0,0 +1,138 @@
//! Conservative tagging for AcroForm widgets.
//!
//! It creates `/Form` structure elements and `/OBJR` children so assistive technology can find
//! widgets. An existing direct `/Document` root and direct `/ParentTree /Nums` are extended;
//! unusual tree layouts are refused before any edit is made.
use pdfcraft_cos::{Dict, Document, ObjRef, Object};
fn reference(object: Option<&Object>, what: &str) -> Result<ObjRef, String> {
object.and_then(Object::as_ref).ok_or_else(|| format!("the {what} must be an indirect reference"))
}
fn nums(doc: &Document, parent_tree: ObjRef) -> Result<Vec<Object>, String> {
let tree = doc.get(parent_tree);
let tree = tree.as_dict().ok_or("the structure parent tree is invalid")?;
tree.get(b"Nums")
.map(|value| doc.resolve(value))
.as_deref()
.and_then(Object::as_array)
.cloned()
.ok_or_else(|| "the structure parent tree must use a direct Nums array".to_string())
}
/// Add `/Form` tags for every page widget that does not already have a structure parent.
///
/// A new structure tree is written for an untagged form. A conventional existing tree is
/// extended by adding the form tags to its direct `/Document` element and fresh entries to its
/// direct `/ParentTree /Nums` array. Text, figures, links, and non-widget annotations remain
/// outside this operation and need separate review.
pub fn tag_form_fields(doc: &mut Document) -> Result<usize, String> {
let root = doc.root().ok_or("the PDF has no catalog")?;
let catalog_object = doc.get(root);
let catalog = catalog_object.as_dict().ok_or("the PDF catalog is invalid")?;
if !catalog.contains(b"AcroForm") {
return Err("the PDF has no AcroForm fields".into());
}
let existing_root = catalog.get(b"StructTreeRoot").cloned();
let mark_info = catalog.get(b"MarkInfo").map(|v| doc.resolve(v)).as_deref().and_then(Object::as_dict).cloned().unwrap_or_default();
let pages = pdfcraft_model::pages(doc);
let mut widgets = Vec::new();
// A widget listed in more than one page's /Annots is tagged once (one key, one /Form).
let mut seen = std::collections::HashSet::new();
for page in &pages {
let annots = page.dict.get(b"Annots").map(|a| doc.resolve(a)).as_deref().and_then(Object::as_array).cloned().unwrap_or_default();
for annot in annots {
let Some(reference) = annot.as_ref() else { continue };
let resolved = doc.get(reference);
let Some(dict) = resolved.as_dict() else { continue };
if dict.name(b"Subtype") == Some(b"Widget") && !dict.contains(b"StructParent") && seen.insert(reference) {
widgets.push((page.obj, reference));
}
}
}
if widgets.is_empty() {
return Err("the AcroForm has no untagged page widgets".into());
}
let (struct_root, document_ref, parent_tree, mut parent_nums, first_key, new_tree) = if let Some(existing_root) = existing_root {
let struct_root = reference(Some(&existing_root), "structure tree root")?;
let structure_object = doc.get(struct_root);
let structure = structure_object.as_dict().ok_or("the structure tree root is invalid")?;
let document_ref = reference(structure.get(b"K"), "structure tree root K")?;
let document = doc.get(document_ref);
if document.as_dict().is_none_or(|d| d.name(b"S") != Some(b"Document")) {
return Err("the structure tree root must point directly to a Document element".into());
}
let parent_tree = reference(structure.get(b"ParentTree"), "structure parent tree")?;
let parent_nums = nums(doc, parent_tree)?;
if parent_nums.len() % 2 != 0 {
return Err("the structure parent tree has an odd number of entries".into());
}
let mut greatest = -1_i64;
for pair in parent_nums.as_chunks::<2>().0 {
let Some(key) = pair[0].as_int() else { return Err("the structure parent tree has a non-integer key".into()) };
if key < 0 {
return Err("the structure parent tree has a negative key".into());
}
greatest = greatest.max(key);
}
let first_key = greatest.checked_add(1).ok_or("the structure parent tree has no available key")?;
(struct_root, document_ref, parent_tree, parent_nums, first_key, false)
} else {
let mut root_dict = Dict::new();
root_dict.set(b"Type".to_vec(), Object::Name(b"StructTreeRoot".to_vec()));
let struct_root = doc.add(Object::Dict(root_dict));
let mut document = Dict::new();
document.set(b"S".to_vec(), Object::Name(b"Document".to_vec()));
document.set(b"P".to_vec(), Object::Ref(struct_root));
let document_ref = doc.add(Object::Dict(document));
let parent_tree = doc.add(Object::Dict(Dict::new()));
(struct_root, document_ref, parent_tree, Vec::new(), 0, true)
};
let mut kids = Vec::with_capacity(widgets.len());
for (offset, (page, widget)) in widgets.iter().copied().enumerate() {
let key = first_key.checked_add(i64::try_from(offset).map_err(|_| "too many form widgets")?).ok_or("too many form widgets")?;
let mut objr = Dict::new();
objr.set(b"Type".to_vec(), Object::Name(b"OBJR".to_vec()));
objr.set(b"Obj".to_vec(), Object::Ref(widget));
objr.set(b"Pg".to_vec(), Object::Ref(page));
let mut form = Dict::new();
form.set(b"S".to_vec(), Object::Name(b"Form".to_vec()));
form.set(b"P".to_vec(), Object::Ref(document_ref));
form.set(b"Pg".to_vec(), Object::Ref(page));
form.set(b"K".to_vec(), Object::Dict(objr));
let form_ref = doc.add(Object::Dict(form));
doc.update_dict(widget, |d| d.set(b"StructParent".to_vec(), Object::Int(key))).map_err(|e| e.to_string())?;
parent_nums.extend([Object::Int(key), Object::Ref(form_ref)]);
kids.push(Object::Ref(form_ref));
}
doc.update_dict(document_ref, |d| {
let old = d.remove(b"K").unwrap_or(Object::Array(Vec::new()));
let mut all = match old {
Object::Array(items) => items,
item => vec![item],
};
all.extend(kids);
d.set(b"K".to_vec(), Object::Array(all));
})
.map_err(|e| e.to_string())?;
doc.update_dict(parent_tree, |d| d.set(b"Nums".to_vec(), Object::Array(parent_nums))).map_err(|e| e.to_string())?;
if new_tree {
doc.update_dict(struct_root, |d| {
d.set(b"K".to_vec(), Object::Ref(document_ref));
d.set(b"ParentTree".to_vec(), Object::Ref(parent_tree));
})
.map_err(|e| e.to_string())?;
}
let mut mark_info = mark_info;
mark_info.set(b"Marked".to_vec(), Object::Bool(true));
doc.update_dict(root, |d| {
d.set(b"MarkInfo".to_vec(), Object::Dict(mark_info));
if new_tree {
d.set(b"StructTreeRoot".to_vec(), Object::Ref(struct_root));
}
})
.map_err(|e| e.to_string())?;
Ok(widgets.len())
}
+2
View File
@@ -14,6 +14,7 @@ use pdfcraft_cos::Document;
mod alt;
mod autotag;
mod autotag_fields;
mod content;
mod headings;
mod report;
@@ -22,6 +23,7 @@ mod tags;
pub use alt::{AltError, ArtifactError, Figure, figures, mark_decorative, mark_tag_artifact, set_alt};
pub use autotag::{AutoTagError, AutoTagResult, autotag_text};
pub use autotag_fields::tag_form_fields;
pub use headings::{Heading, headings};
pub use report::{REPORT_WORDS, report_html};
pub use tags::{TagError, TagInfo, TagPlacement, TagProperty, move_tag, set_tag_property, tags};
+72
View File
@@ -96,6 +96,78 @@ fn autotag_text_refuses_paint_and_does_not_modify_the_document() {
assert_eq!(autotag_text(&mut pdf(&good(), "")), Err(AutoTagError::AlreadyTagged));
}
#[test]
fn autotag_form_fields_writes_widget_structure_and_parent_tree() {
let mut doc = pdf(
&[
"<< /Type /Catalog /Pages 2 0 R /AcroForm << /Fields [5 0 R] >> >>".into(),
"<< /Type /Pages /Kids [3 0 R] /Count 1 >>".into(),
"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 200 200] /Annots [5 0 R 6 0 R] >>".into(),
"<< /Length 0 >>\nstream\n\nendstream".into(),
"<< /FT /Tx /T (name) /Kids [6 0 R] >>".into(),
"<< /Type /Annot /Subtype /Widget /Parent 5 0 R /Rect [20 60 120 80] >>".into(),
],
"",
);
assert_eq!(tag_form_fields(&mut doc).unwrap(), 1);
let report = check(&doc, &Options { rules: [Rule::TaggedPdf, Rule::TaggedFormFields].into(), pages: None });
assert_eq!(status(&report, Rule::TaggedPdf), Status::Passed);
assert_eq!(status(&report, Rule::TaggedFormFields), Status::Passed);
let tree = doc.get(doc.root().unwrap()).as_dict().unwrap().reference(b"StructTreeRoot").unwrap();
assert!(doc.get(tree).as_dict().unwrap().contains(b"ParentTree"));
let widget = pdfcraft_model::pages(&doc)[0].dict.get(b"Annots").and_then(|a| doc.resolve(a).as_array().cloned()).unwrap()[1].as_ref().unwrap();
assert_eq!(doc.get(widget).as_dict().unwrap().get(b"StructParent").and_then(Object::as_int), Some(0));
let written = pdfcraft_cos::write_full(&doc, &pdfcraft_cos::SaveOptions::default()).unwrap();
let reopened = Document::open(Arc::new(written)).unwrap();
assert_eq!(status(&check(&reopened, &Options { rules: [Rule::TaggedFormFields].into(), pages: None }), Rule::TaggedFormFields), Status::Passed);
}
#[test]
fn autotag_form_fields_tags_a_widget_listed_on_two_pages_once() {
let mut doc = pdf(
&[
"<< /Type /Catalog /Pages 2 0 R /AcroForm << /Fields [5 0 R] >> >>".into(),
"<< /Type /Pages /Kids [3 0 R 6 0 R] /Count 2 >>".into(),
"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 200 200] /Annots [5 0 R] >>".into(),
"<< /Length 0 >>\nstream\n\nendstream".into(),
"<< /Type /Annot /Subtype /Widget /FT /Tx /T (name) /Rect [20 60 120 80] >>".into(),
"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 200 200] /Annots [5 0 R] >>".into(),
],
"",
);
assert_eq!(tag_form_fields(&mut doc).unwrap(), 1);
let tree = doc.get(doc.root().unwrap()).as_dict().unwrap().reference(b"StructTreeRoot").unwrap();
let parent_tree = doc.get(tree).as_dict().unwrap().reference(b"ParentTree").unwrap();
let nums = doc.get(parent_tree).as_dict().unwrap().get(b"Nums").and_then(|n| n.as_array().cloned()).unwrap();
assert_eq!(nums.len(), 2, "one key and one /Form element for the shared widget");
}
#[test]
fn autotag_form_fields_extends_a_direct_existing_structure_tree() {
let mut doc = pdf(
&[
"<< /Type /Catalog /Pages 2 0 R /AcroForm << /Fields [5 0 R] >> /MarkInfo << /Marked true >> /StructTreeRoot 6 0 R >>".into(),
"<< /Type /Pages /Kids [3 0 R] /Count 1 >>".into(),
"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 200 200] /Annots [5 0 R] /StructParents 0 >>".into(),
"<< /Length 0 >>\nstream\n\nendstream".into(),
"<< /Type /Annot /Subtype /Widget /FT /Tx /T (name) /Rect [20 60 120 80] >>".into(),
"<< /Type /StructTreeRoot /K 7 0 R /ParentTree 8 0 R >>".into(),
"<< /S /Document /P 6 0 R /K 9 0 R >>".into(),
"<< /Nums [0 [9 0 R]] >>".into(),
"<< /S /P /P 7 0 R /Pg 3 0 R /K 0 >>".into(),
],
"",
);
assert_eq!(tag_form_fields(&mut doc).unwrap(), 1);
let widget = pdfcraft_model::pages(&doc)[0].dict.get(b"Annots").and_then(|a| doc.resolve(a).as_array().cloned()).unwrap()[0].as_ref().unwrap();
assert_eq!(doc.get(widget).as_dict().unwrap().get(b"StructParent").and_then(Object::as_int), Some(1));
let report = check(&doc, &Options { rules: [Rule::TaggedFormFields].into(), pages: None });
assert_eq!(status(&report, Rule::TaggedFormFields), Status::Passed);
let document_object = doc.get(pdfcraft_cos::ObjRef::new(7, 0));
let document = document_object.as_dict().unwrap();
assert_eq!(document.get(b"K").and_then(Object::as_array).unwrap().len(), 2, "the existing paragraph and new form remain siblings");
}
#[test]
fn an_untagged_document_fails_the_document_rules() {
let doc = pdf(
+13
View File
@@ -217,6 +217,19 @@ impl Automation {
}))
}
pub(crate) fn accessibility_autotag_form_fields(&mut self, a: &Args) -> Result<Value> {
let id = self.doc(a)?.id;
self.session.apply(id, pdfcraft_engine::Edit::AutoTagFormFields).map_err(failed)?;
let report = self
.doc(a)?
.accessibility_check(&Options { rules: [Rule::TaggedFormFields].into(), pages: None })
.ok_or_else(|| failed("the document can't be read"))?;
let result = report.result(Rule::TaggedFormFields).ok_or_else(|| failed("the form field check is unavailable"))?;
Ok(
json!({ "status": status_id(result.status), "remaining_findings": result.findings.len(), "note": "Only form widgets were tagged. Review text, figures, links, reading order, and other document content separately." }),
)
}
pub(crate) fn accessibility_autotag_preview(&self, a: &Args) -> Result<Value> {
let page = self.page(a)?;
let preview = self.doc(a)?.autotag_preview(page).map_err(failed)?;
+1
View File
@@ -765,6 +765,7 @@ impl Automation {
"accessibility_check" => self.a11y_check(&a)?,
"accessibility_autotag_preview" => self.accessibility_autotag_preview(&a)?,
"accessibility_autotag_apply" => self.accessibility_autotag_apply(&a)?,
"accessibility_autotag_form_fields" => self.accessibility_autotag_form_fields(&a)?,
"ocr_recognize" => self.ocr_recognize(&a)?,
"js_run" => self.js_run(&a)?,
"js_document_scripts" => self.js_document_scripts(&a)?,
+3
View File
@@ -928,6 +928,9 @@ pub fn tools() -> Vec<ToolDef> {
.with(schema(json!({ "doc": doc(), "page": { "type": "integer", "minimum": 1 } }), &["doc", "page"])),
t("accessibility_autotag_apply", "Automatically tag text-only PDF", "Write marked content, a Document structure element, paragraph tags and a parent tree for every text object in an untagged, text-only PDF. Refuses graphics, images, annotations, forms, existing tags and malformed streams before changing anything. Review all resulting roles; this does not certify accessibility. Undoable.")
.with(schema(json!({ "doc": doc() }), &["doc"])),
t("accessibility_autotag_form_fields", "Tag form fields", "Write `/Form` tags, widget structure-parent keys, and a parent tree for all page widgets in an untagged AcroForm. Existing tag trees are refused for review. Text, figures, links, and other content are not tagged.")
.cmd("a11y.autotag_fields")
.with(schema(json!({ "doc": doc() }), &["doc"])),
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")
+16 -1
View File
@@ -291,7 +291,22 @@ fn autotag_apply_writes_reviewable_tags_and_survives_save_reopen() {
let pages = pdfcraft_model::pages(&reopened);
assert_eq!(pages.len(), 2);
assert!(pages.iter().all(|page| page.dict.contains(b"StructParents")));
assert!(reopened.get(reopened.root().unwrap()).as_dict().unwrap().contains(b"StructTreeRoot"));
}
#[test]
fn autotag_form_fields_writes_reviewable_widget_tags() {
let dir = workdir("autotag-form-fields");
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 tagged = ok(&mut a, "accessibility_autotag_form_fields", json!({ "doc": doc }));
assert_eq!(tagged["status"], "passed");
assert_eq!(tagged["remaining_findings"], 0);
ok(&mut a, "doc_save", json!({ "doc": doc, "path": "tagged.pdf" }));
let reopened = ok(&mut a, "doc_open", json!({ "path": "tagged.pdf" }))["doc"].as_u64().unwrap();
let check = ok(&mut a, "accessibility_check", json!({ "doc": reopened, "rules": ["tagged-form-fields"] }));
let result = check["results"].as_array().unwrap().iter().find(|result| result["rule"] == "tagged-form-fields").unwrap();
assert_eq!(result["status"], "passed");
}
/// A one-page PDF whose form holds one text field named `name`.
+1
View File
@@ -472,6 +472,7 @@ pub static TOOL_GROUPS: &[ToolGroup] = &[
title: "Accessibility",
items: &[
item("Automatically tag PDF", "tag", "a11y.autotag", Ready),
item("Tag form fields", "tag", "a11y.autotag_fields", Ready),
item("Change reading options", "book-open", "a11y.reading_options", Ready),
item("Check for accessibility", "accessibility", "a11y.check", Ready),
item("Open accessibility report", "file-text", "a11y.report", Ready),
+1
View File
@@ -336,6 +336,7 @@ pub const COMMANDS: &[CommandSpec] = &[
c("a11y.report", "Open accessibility report", None, None, Document, "file-text"),
c("a11y.reading_options", "Change reading options…", None, None, Document, "book-open"),
c("a11y.alt_text", "Add alternate text…", None, None, Modification, "image"),
c("a11y.autotag_fields", "Tag form fields", None, None, Modification, "tag"),
c("a11y.tags", "Tags panel", None, None, Document, "tag"),
c("export.all_images", "Export all images…", FILE, None, Copying, "image"),
c("edit.header_footer", "Add header & footer…", None, None, Modification, "heading"),
+7
View File
@@ -1471,6 +1471,8 @@ pub enum Edit {
figure: u32,
alt: Option<String>,
},
/// Add `/Form` tags and object references for all widgets in an untagged AcroForm.
AutoTagFormFields,
/// Change one property on a reachable indirect structure tag.
SetTagProperty {
object: pdfcraft_cos::ObjRef,
@@ -1961,6 +1963,7 @@ impl Edit {
Edit::SetInitialView(_) => Label::of("Change initial view"),
Edit::AutoTagText => Label::of("Automatically tag text"),
Edit::SetAltText { .. } => Label::of("Set alternate text"),
Edit::AutoTagFormFields => Label::of("Tag form fields"),
Edit::SetTagProperty { .. } => Label::of("Change tag properties"),
Edit::MoveTag { .. } => Label::of("Move object"),
Edit::MarkDecorative { .. } => Label::of("Mark figure as decorative"),
@@ -2156,6 +2159,7 @@ fn check_permission(edit: &Edit, p: &pdfcraft_cos::Permissions) -> Result<(), Ed
| Edit::SetInitialView(_)
| Edit::AutoTagText
| Edit::SetAltText { .. }
| Edit::AutoTagFormFields
| Edit::SetTagProperty { .. }
| Edit::MoveTag { .. }
| Edit::MarkDecorative { .. }
@@ -2444,6 +2448,9 @@ fn run_edit(doc: &mut pdfcraft_cos::Document, edit: &Edit, cx: &mut EditCtx) ->
let r = pdfcraft_cos::ObjRef::new(*figure, doc.generation(*figure));
a11y::set_alt(doc, r, alt.as_deref()).map_err(|e| EditError::Accessibility(e.to_string()))?;
}
Edit::AutoTagFormFields => {
a11y::tag_form_fields(doc).map_err(EditError::Accessibility)?;
}
Edit::SetTagProperty { object, property, value } => {
a11y::set_tag_property(doc, *object, *property, value.as_deref()).map_err(|e| EditError::Accessibility(e.to_string()))?;
}
+6
View File
@@ -608,6 +608,12 @@ pub(crate) fn alt_body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens) ->
}
impl PdfCraftApp {
/// Prepare for accessibility ▸ Tag form fields.
pub(crate) fn autotag_form_fields(&mut self) {
self.apply_edit(pdfcraft_engine::Edit::AutoTagFormFields);
self.choose_right_panel(Some(RightPanel::Tags));
}
/// Prepare for accessibility ▸ Add alternate text.
pub(crate) fn start_alt_text(&mut self) {
let Some((_, id)) = self.active_ids() else { return };
+1
View File
@@ -424,6 +424,7 @@ impl PdfCraftApp {
}
}
"a11y.alt_text" => self.start_alt_text(),
"a11y.autotag_fields" => self.autotag_form_fields(),
"a11y.tags" => self.choose_right_panel(Some(RightPanel::Tags)),
"a11y.reading_options" => self.dialog = Some(Dialog::Properties(crate::PropsTab::Advanced)),
"export.all_images" => self.dialog = Some(Dialog::Export(crate::export_ui::ExportKind::AllImages)),
+1
View File
@@ -2394,6 +2394,7 @@ animation effects Off معطّلة
This document is trying to open a local file: يحاول هذا المستند فتح ملف محلي:
Move tag earlier نقل العلامة للأعلى
Move tag later نقل العلامة للأسفل
Tag form fields وضع علامات على حقول النموذج
Mark as artifact وضع علامة كعنصر زخرفي
Remove this leaf tag from reading order and mark its direct content as an artifact أزل هذه العلامة من ترتيب القراءة وضع علامة على محتواها المباشر كعنصر زخرفي
Mark content as artifact وضع علامة على المحتوى كعنصر زخرفي
Can't render this file because it contains an unexpected character in line 6 and column 47.
+1
View File
@@ -2336,6 +2336,7 @@ animation effects Off Изключени
This document is trying to open a local file: Този документ се опитва да отвори локален файл:
Move tag earlier Преместване на етикета по-рано
Move tag later Преместване на етикета по-късно
Tag form fields Маркиране на полетата на формуляра
Mark as artifact Маркиране като артефакт
Remove this leaf tag from reading order and mark its direct content as an artifact Премахнете този листов етикет от реда на четене и маркирайте прякото му съдържание като артефакт
Mark content as artifact Маркиране на съдържание като артефакт
Can't render this file because it contains an unexpected character in line 8 and column 16.
+1
View File
@@ -2447,6 +2447,7 @@ animation effects Off Vypnuto
This document is trying to open a local file: Tento dokument se pokouší otevřít místní soubor:
Move tag earlier Přesunout značku dříve
Move tag later Přesunout značku později
Tag form fields Označit pole formuláře
Mark as artifact Označit jako artefakt
Remove this leaf tag from reading order and mark its direct content as an artifact Odeberte tuto značku listu z pořadí čtení a označte její přímý obsah jako artefakt
Mark content as artifact Označit obsah jako artefakt
Can't render this file because it contains an unexpected character in line 6 and column 47.
+1
View File
@@ -2374,6 +2374,7 @@ animation effects Off Aus
This document is trying to open a local file: Dieses Dokument versucht, eine lokale Datei zu öffnen:
Move tag earlier Tag früher verschieben
Move tag later Tag später verschieben
Tag form fields Formularfelder taggen
Mark as artifact Als Artefakt markieren
Remove this leaf tag from reading order and mark its direct content as an artifact Dieses Blatt-Tag aus der Lesereihenfolge entfernen und seinen direkten Inhalt als Artefakt markieren
Mark content as artifact Inhalt als Artefakt markieren
Can't render this file because it contains an unexpected character in line 6 and column 51.
+1
View File
@@ -2383,6 +2383,7 @@ animation effects Off Ανενεργά
This document is trying to open a local file: Αυτό το έγγραφο προσπαθεί να ανοίξει ένα τοπικό αρχείο:
Move tag earlier Μετακίνηση ετικέτας νωρίτερα
Move tag later Μετακίνηση ετικέτας αργότερα
Tag form fields Επισήμανση πεδίων φόρμας
Mark as artifact Σήμανση ως τεχνούργημα
Remove this leaf tag from reading order and mark its direct content as an artifact Αφαιρέστε αυτήν την ετικέτα φύλλου από τη σειρά ανάγνωσης και σημειώστε το άμεσο περιεχόμενό της ως τεχνούργημα
Mark content as artifact Σήμανση περιεχομένου ως τεχνούργημα
Can't render this file because it contains an unexpected character in line 475 and column 9.
+1
View File
@@ -2513,6 +2513,7 @@ animation effects Off Desactivados
This document is trying to open a local file: Este documento intenta abrir un archivo local:
Move tag earlier Mover etiqueta antes
Move tag later Mover etiqueta después
Tag form fields Etiquetar campos de formulario
Mark as artifact Marcar como artefacto
Remove this leaf tag from reading order and mark its direct content as an artifact Quite esta etiqueta hoja del orden de lectura y marque su contenido directo como artefacto
Mark content as artifact Marcar contenido como artefacto
Can't render this file because it contains an unexpected character in line 6 and column 60.
+1
View File
@@ -2373,6 +2373,7 @@ animation effects Off Désactivés
This document is trying to open a local file: Ce document tente d’ouvrir un fichier local :
Move tag earlier Déplacer la balise plus tôt
Move tag later Déplacer la balise plus tard
Tag form fields Baliser les champs de formulaire
Mark as artifact Marquer comme artefact
Remove this leaf tag from reading order and mark its direct content as an artifact Retirez cette balise feuille de l’ordre de lecture et marquez son contenu direct comme artefact
Mark content as artifact Marquer le contenu comme artefact
Can't render this file because it contains an unexpected character in line 6 and column 39.
+1
View File
@@ -2372,6 +2372,7 @@ animation effects Off Ki
This document is trying to open a local file: Ez a dokumentum egy helyi fájlt próbál megnyitni:
Move tag earlier Címke korábbra helyezése
Move tag later Címke későbbre helyezése
Tag form fields Űrlapmezők címkézése
Mark as artifact Megjelölés műtárgyként
Remove this leaf tag from reading order and mark its direct content as an artifact Távolítsa el ezt a levélcímkét az olvasási sorrendből, és jelölje meg a közvetlen tartalmát műtárgyként
Mark content as artifact Tartalom megjelölése műtárgyként
Can't render this file because it contains an unexpected character in line 7 and column 44.
+1
View File
@@ -2382,6 +2382,7 @@ animation effects Off Disattivati
This document is trying to open a local file: Questo documento sta tentando di aprire un file locale:
Move tag earlier Sposta tag prima
Move tag later Sposta tag dopo
Tag form fields Assegna tag ai campi del modulo
Mark as artifact Segna come artefatto
Remove this leaf tag from reading order and mark its direct content as an artifact Rimuovi questo tag foglia dall'ordine di lettura e segna il suo contenuto diretto come artefatto
Mark content as artifact Segna il contenuto come artefatto
Can't render this file because it contains an unexpected character in line 6 and column 47.
+1
View File
@@ -2373,6 +2373,7 @@ animation effects Off オフ
This document is trying to open a local file: この文書はローカルファイルを開こうとしています:
Move tag earlier タグを前へ移動
Move tag later タグを後へ移動
Tag form fields フォームフィールドにタグを付ける
Mark as artifact アーティファクトとしてマーク
Remove this leaf tag from reading order and mark its direct content as an artifact このリーフタグを読み上げ順序から削除し、その直接のコンテンツをアーティファクトとしてマークします
Mark content as artifact コンテンツをアーティファクトとしてマーク
Can't render this file because it contains an unexpected character in line 6 and column 47.
+1
View File
@@ -2371,6 +2371,7 @@ animation effects Off Өшірулі
This document is trying to open a local file: Бұл құжат жергілікті файлды ашуға әрекеттеніп жатыр:
Move tag earlier Тегті ертерек жылжыту
Move tag later Тегті кейінірек жылжыту
Tag form fields Пішін өрістерін тегтеу
Mark as artifact Артефакт ретінде белгілеу
Remove this leaf tag from reading order and mark its direct content as an artifact Осы жапырақ тегін оқу ретінен алып тастап, оның тікелей мазмұнын артефакт ретінде белгілеңіз
Mark content as artifact Мазмұнды артефакт ретінде белгілеу
Can't render this file because it contains an unexpected character in line 824 and column 9.
+1
View File
@@ -2617,6 +2617,7 @@ key Space Tarpas
This document is trying to open a local file: Šis dokumentas bando atverti vietinį failą:
Move tag earlier Perkelti žymą anksčiau
Move tag later Perkelti žymą vėliau
Tag form fields Žymėti formos laukus
Mark as artifact Pažymėti kaip artefaktą
Remove this leaf tag from reading order and mark its direct content as an artifact Pašalinkite šią lapo žymą iš skaitymo tvarkos ir pažymėkite jos tiesioginį turinį kaip artefaktą
Mark content as artifact Pažymėti turinį kaip artefaktą
Can't render this file because it contains an unexpected character in line 401 and column 9.
+1
View File
@@ -2353,6 +2353,7 @@ animation effects Off Wyłączone
This document is trying to open a local file: Ten dokument próbuje otworzyć plik lokalny:
Move tag earlier Przenieś znacznik wcześniej
Move tag later Przenieś znacznik później
Tag form fields Oznacz pola formularza
Mark as artifact Oznacz jako artefakt
Remove this leaf tag from reading order and mark its direct content as an artifact Usuń ten znacznik liścia z kolejności odczytu i oznacz jego bezpośrednią zawartość jako artefakt
Mark content as artifact Oznacz zawartość jako artefakt
Can't render this file because it contains an unexpected character in line 6 and column 47.
+1
View File
@@ -2387,6 +2387,7 @@ animation effects Off Desativados
This document is trying to open a local file: Este documento está tentando abrir um arquivo local:
Move tag earlier Mover tag para antes
Move tag later Mover tag para depois
Tag form fields Marcar campos de formulário
Mark as artifact Marcar como artefato
Remove this leaf tag from reading order and mark its direct content as an artifact Remova esta tag folha da ordem de leitura e marque seu conteúdo direto como artefato
Mark content as artifact Marcar conteúdo como artefato
Can't render this file because it contains an unexpected character in line 9 and column 16.
+1
View File
@@ -2286,6 +2286,7 @@ animation effects Off Выкл
This document is trying to open a local file: Этот документ пытается открыть локальный файл:
Move tag earlier Переместить тег раньше
Move tag later Переместить тег позже
Tag form fields Пометить поля формы
Mark as artifact Пометить как артефакт
Remove this leaf tag from reading order and mark its direct content as an artifact Удалите этот конечный тег из порядка чтения и пометьте его непосредственное содержимое как артефакт
Mark content as artifact Пометить содержимое как артефакт
Can't render this file because it contains an unexpected character in line 9 and column 16.
+1
View File
@@ -2378,6 +2378,7 @@ animation effects Off Izklopljeni
This document is trying to open a local file: Ta dokument poskuša odpreti lokalno datoteko:
Move tag earlier Premakni oznako prej
Move tag later Premakni oznako pozneje
Tag form fields Označi polja obrazca
Mark as artifact Označi kot artefakt
Remove this leaf tag from reading order and mark its direct content as an artifact Odstranite to listno oznako iz vrstnega reda branja in označite njeno neposredno vsebino kot artefakt
Mark content as artifact Označi vsebino kot artefakt
Can't render this file because it contains an unexpected character in line 9 and column 16.
+1
View File
@@ -2286,6 +2286,7 @@ animation effects Off ఆఫ్
This document is trying to open a local file: ఈ పత్రం ఒక స్థానిక ఫైల్‌ను తెరవడానికి ప్రయత్నిస్తోంది:
Move tag earlier ట్యాగ్‌ను ముందుకు తరలించు
Move tag later ట్యాగ్‌ను వెనుకకు తరలించు
Tag form fields ఫారమ్ ఫీల్డ్‌లకు ట్యాగ్‌లు జోడించు
Mark as artifact ఆర్టిఫాక్ట్‌గా గుర్తించు
Remove this leaf tag from reading order and mark its direct content as an artifact ఈ లీఫ్ ట్యాగ్‌ను పఠన క్రమం నుంచి తీసివేసి, దాని ప్రత్యక్ష కంటెంట్‌ను ఆర్టిఫాక్ట్‌గా గుర్తించండి
Mark content as artifact కంటెంట్‌ను ఆర్టిఫాక్ట్‌గా గుర్తించు
Can't render this file because it contains an unexpected character in line 13 and column 16.
+1
View File
@@ -2369,6 +2369,7 @@ animation effects Off Kapalı
This document is trying to open a local file: Bu belge yerel bir dosya açmaya çalışıyor:
Move tag earlier Etiketi daha erken taşı
Move tag later Etiketi daha geç taşı
Tag form fields Form alanlarını etiketle
Mark as artifact Yapıt olarak işaretle
Remove this leaf tag from reading order and mark its direct content as an artifact Bu yaprak etiketini okuma sırasından kaldırın ve doğrudan içeriğini yapıt olarak işaretleyin
Mark content as artifact İçeriği yapıt olarak işaretle
Can't render this file because it contains an unexpected character in line 823 and column 9.
+1
View File
@@ -2395,6 +2395,7 @@ animation effects Off Вимкнено
This document is trying to open a local file: Цей документ намагається відкрити локальний файл:
Move tag earlier Перемістити тег раніше
Move tag later Перемістити тег пізніше
Tag form fields Позначити поля форми
Mark as artifact Позначити як артефакт
Remove this leaf tag from reading order and mark its direct content as an artifact Вилучіть цей кінцевий тег із порядку читання та позначте його безпосередній вміст як артефакт
Mark content as artifact Позначити вміст як артефакт
Can't render this file because it contains an unexpected character in line 6 and column 47.
+1
View File
@@ -2592,6 +2592,7 @@ key End End
key Space Space
Move tag earlier Chuyển thẻ lên trước
Move tag later Chuyển thẻ ra sau
Tag form fields Gắn thẻ các trường biểu mẫu
Only allow this if you trust the source document. Chỉ cho phép nếu bạn tin tưởng tài liệu nguồn.
Open this local PDF? Mở PDF cục bộ này?
This document is trying to open a local file: Tài liệu này đang cố mở một tệp cục bộ:
Can't render this file because it contains an unexpected character in line 487 and column 12.
+1
View File
@@ -2337,6 +2337,7 @@ animation effects Off 关
This document is trying to open a local file: 此文档正尝试打开本地文件:
Move tag earlier 将标签前移
Move tag later 将标签后移
Tag form fields 标记表单字段
Mark as artifact 标记为工件
Remove this leaf tag from reading order and mark its direct content as an artifact 从阅读顺序中移除此叶标签,并将其直接内容标记为工件
Mark content as artifact 将内容标记为工件
Can't render this file because it contains an unexpected character in line 9 and column 16.
+1
View File
@@ -2249,6 +2249,7 @@ animation effects Off 關
This document is trying to open a local file: 此文件正嘗試開啟本機檔案:
Move tag earlier 將標記前移
Move tag later 將標記後移
Tag form fields 標記表單欄位
Mark as artifact 標記為成品
Remove this leaf tag from reading order and mark its direct content as an artifact 從閱讀順序中移除此葉標記,並將其直接內容標記為成品
Mark content as artifact 將內容標記為成品
Can't render this file because it contains an unexpected character in line 9 and column 16.
+30
View File
@@ -18,6 +18,14 @@ endstream endobj
trailer << /Root 1 0 R >>
%%EOF";
const FORM_FIXTURE: &[u8] = b"%PDF-1.7
1 0 obj << /Type /Catalog /Pages 2 0 R /AcroForm << /Fields [4 0 R] >> >> endobj
2 0 obj << /Type /Pages /Kids [3 0 R] /Count 1 >> endobj
3 0 obj << /Type /Page /Parent 2 0 R /MediaBox [0 0 200 100] /Annots [4 0 R] >> endobj
4 0 obj << /Type /Annot /Subtype /Widget /FT /Tx /T (Name) /Rect [20 40 120 60] /P 3 0 R >> endobj
trailer << /Root 1 0 R >>
%%EOF";
fn status(h: &Harness<'static, PdfCraftApp>, rule: Rule) -> Status {
h.state().a11y.report.as_ref().unwrap().1.result(rule).unwrap().status
}
@@ -210,6 +218,28 @@ fn a_saved_page_range_past_the_document_still_checks_its_pages() {
assert_eq!(h.state().a11y.options.as_ref().and_then(|o| o.pages.clone()), Some(vec![0]));
}
#[test]
fn tagging_form_fields_opens_the_tags_panel() {
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("form.pdf", None, FORM_FIXTURE.to_vec()).unwrap();
app
});
h.run_steps(3);
assert!(h.state_mut().execute("a11y.autotag_fields"));
h.run_steps(3);
assert_eq!(h.state().right, Some(RightPanel::Tags));
let id = h.state().views[0].id;
let report = h
.state()
.session
.get(id)
.unwrap()
.accessibility_check(&pdfcraft_engine::a11y::Options { rules: [Rule::TaggedFormFields].into(), pages: None });
assert_eq!(report.unwrap().result(Rule::TaggedFormFields).unwrap().status, Status::Passed);
}
/// Two tagged figures: an image without alternate text and a bar (role-mapped Photo) with.
const FIGURES: &[u8] = b"%PDF-1.7
1 0 obj << /Type /Catalog /Pages 2 0 R /MarkInfo << /Marked true >> /StructTreeRoot 5 0 R >> endobj
+4 -2
View File
@@ -198,8 +198,10 @@ update the numbers.
### 11. Accessibility: only text-only automatic structure creation
- **Missing:** auto-tag mixed text, images, tables, lists, links and fields; connect preview heading
proposals to Apply; visual Order and Content panels, Reading Order tool, table editor, Make Accessible action,
- **Missing:** apply auto-tag proposals to marked content and a valid parent tree, tag fields
into structure trees with unusual layouts (only a direct Document root with a direct
ParentTree Nums array is extended),
visual Tags, Order and Content panels, Reading Order tool, table editor, Make Accessible action,
Read Out Loud; full keyboard operation of PdfCraft itself and a screen-reader audit.
- **Evidence:** all 32 checker rules, figure alt text, tag property and sibling order edits, and
a bounded auto-tag preview exist. Apply writes valid structure and parent-tree links for
+5 -5
View File
@@ -8876,11 +8876,11 @@ title = "Autotag form fields"
area = "M"
tier = "P1"
milestone = "M12"
status = "planned"
commands = []
tools = []
evidence = []
notes = ""
status = "partial"
commands = ["a11y.autotag_fields"]
tools = ["accessibility_autotag_form_fields"]
evidence = ["crates/a11y/src/tests.rs::autotag_form_fields_writes_widget_structure_and_parent_tree", "crates/automation/tests/automation.rs::autotag_form_fields_writes_reviewable_widget_tags"]
notes = "For an untagged AcroForm, writes Form structure elements, OBJR references, widget StructParent keys and ParentTree entries. An existing tree is extended when its root points directly to a Document element and its ParentTree uses a direct Nums array; other layouts are refused before any edit. A widget listed on several pages is tagged once. Text, figures, links and other content remain untagged."
[[feature]]
id = "a11y.reading-order-tool"