Accessibility: edit structure tag properties through tools (#1210)

* Accessibility: edit structure tag properties through tools

* Change tag properties: listed in EDIT_LABELS and translated in every catalog; refresh hashes

---------

Co-authored-by: Brandon Thomas <[email protected]>
This commit is contained in:
Earl CoandBrandon Thomas authored and GitHub committed 2026-10-11 19:36:00 -04:00
1 parent fe2eadb211
commit b65b7adb01
32 files changed
+277 -26

No files matched your search

+20 -20
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 = "bd7d565bff34f6f73b90b0884f480b9f39b106d5c96953e17c68619f03385a57"
sha256 = "380a21460c6fff6df7ae0a2ef563e5df5a8320dba53bcae483f9172a0123d69d"
# 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 = "de11e8f3b126092b584beb8b22ea8952ff6cbc79d42c417663843be93c807bc4"
sha256 = "19fb102db12d172a4bfca18e22b2acae1fd51b36adc80b2ace5e5aa66b8249da"
[[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 = "8ddfd156833e29d8584dab85520264d8e5d77bfcdb5ea994577acc1a9644936b"
sha256 = "d7c8c50f5113f5bcb46dcce711c79701a5d7d9e05927146d0b2de9d99dbf1d92"
[[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 = "67abad667c05dc10bc9bffab5302978bc73b8314f63f78b543cdece4b5cea6d7"
sha256 = "4d8119b38701c0b178d36afe4df0ad6b1cfd78a5a164062d615133b9b205e515"
[[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 = "a8d35613c888904ce6e984560c06d54a3c6f9828fa840c733eb9f2f893b8c0ee"
sha256 = "79f305d74e4a58897de55abe2b4ae5f3b3651e6946bb377fff5d7747648a60f5"
[[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 = "9f8a39143000bc909ee24617518c41f3aefaaeace341002e403d94a9eb51c984"
sha256 = "7d7e1ce33764d838993a26afaf68d66c8e39e9dfc1f238954bd6dd2380798fe4"
[[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 = "a099eb8c6c4bdbd62dc32efef97b4bef718c0170492e6921568d48e46499c964"
sha256 = "83f3287fe2158900793aee2425b56d058573b8b62f274bb3da935a55646ebe44"
[[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 = "e59dc41d7628503fd241689efb69641bcf31733a204f3cd492ac88d66c76c6a0"
sha256 = "f79bb5ebbec462b3c662fb6ac2175b0983dd8d153b8d1b672fa18e13f0576d70"
[[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 = "c6ab6587ad3ecd56efb019a18eb71f92fca33c43ead8eb9fc3262b8c56e5beb1"
sha256 = "add4434d161501a2d1848f4b672922ddd66c49dd17d3d14a4d0e385feb43285f"
[[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 = "1fa0e8bba340d78ecf2a3c9ae319b34e99811082950ebf4aee5e7f5eb0bdab22"
sha256 = "01b5915d4d35318d6ecc82d9f99ffee41155a5c7ac358e5c6eaa269474b8aede"
[[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 = "4297893b78fa25368d9bb6e10485fb3ede866ed470e528001de0120ea2f0915b"
sha256 = "780a710c4baaff41e2cf2e290d77ea09baa179b3566eea0a9be0292f61ca2cb4"
[[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 = "a84ccfd96c9704d1630d6b42b1c50298a0ea5791b063ff2bbe1ccc993f87cc47"
sha256 = "d7547d9f20841485234618a260c7982fef9214c7eb70b132780e3b1737075ce0"
[[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 = "da6aee2a47ec15193a3d88f8116f2b81ec1ac815b9481eb9c36d48518625df30"
sha256 = "d31d3addabfe9553f94760f6e267ab10c244738c90ce66aaf30f0eb36b6ec865"
[[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 = "122cc1b0a346a8ff4cb7ede77775afbd326e82c9bfd6bda06f895ac100702144"
sha256 = "58dae1284c97aac4f3a142804ac5c685a8a7c35918f3ae156862e3abc1223c17"
[[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 = "5eb3ccd3c7613fd54b4a217ddaf32f29a37efb29373787a3178209ae3aca12a5"
sha256 = "9ce954e1ee23680d23bb400e45386f32c3337e92efe69c822530baa79b9894cf"
[[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 = "ffbf8a2c117d75b008361e882c96c9d7cad8844e791800a60174eacf96e3c89a"
sha256 = "e7120ab2b8499639534931873526019c7ba8c90e2dc5cec64e631bdbaa046775"
[[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 = "424e5f16bead85250d142e791b5e8a874c9905025be052ed8aef1f1645e5585a"
sha256 = "03f9087ecd0f95b238a4aa057038988d9f4fe18f7b21919e3fd87332a53fcdb2"
[[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 = "63d26bec54aa6e9f9c4b6e4dc16b872a41ace7feec825c888633992ca1507148"
sha256 = "0c6c11f737cd9ad73bfb7f362ca0754fc010d7d0f54726344fe49ae97cc5a0ad"
[[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 = "0b330a5e739159d02a8214af4655e954cba59861ca30b914614ffb0f2735e52d"
sha256 = "4d1b9618321a3fa05896eec924f54c8d265742729032335c18863724333b74b6"
[[asset]]
path = "assets/icons/accessibility.svg"
@@ -3339,4 +3339,4 @@ 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 = "7071df48ebdb362914f404deebebc84252550d10d08f333892584230efa6c1d6"
sha256 = "9fe1f01f044766434ccaad74556a3af5ec99591d61f5d4d2a4967c7bfbfc92c3"
+1 -1
View File
@@ -48,7 +48,7 @@ pub(crate) fn standard(doc: &Document, role_map: &Dict, s: &[u8]) -> Vec<u8> {
}
/// Indirect structure elements in document order, with their page.
fn elements(doc: &Document) -> Vec<(ObjRef, Dict, Option<ObjRef>)> {
pub(crate) fn elements(doc: &Document) -> Vec<(ObjRef, Dict, Option<ObjRef>)> {
let Some(root) = tree_root(doc) else { return Vec::new() };
let mut out = Vec::new();
let mut seen = HashSet::new();
+2
View File
@@ -17,10 +17,12 @@ mod content;
mod headings;
mod report;
mod structure;
mod tags;
pub use alt::{AltError, Figure, figures, mark_decorative, set_alt};
pub use headings::{Heading, headings};
pub use report::{REPORT_WORDS, report_html};
pub use tags::{TagError, TagInfo, TagProperty, set_tag_property, tags};
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Category {
+111
View File
@@ -0,0 +1,111 @@
//! Properties of reachable indirect structure elements.
use pdfcraft_cos::{CosError, Document, ObjRef, Object, PdfString};
use crate::alt;
/// A tag that can be addressed by an edit. Inline dictionaries have no stable object reference.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct TagInfo {
pub object: ObjRef,
pub kind: String,
pub title: Option<String>,
pub actual_text: Option<String>,
pub alt: Option<String>,
pub language: Option<String>,
/// Zero-based page when `/Pg` is known.
pub page: Option<usize>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum TagProperty {
Type,
Title,
ActualText,
AlternateText,
Language,
}
impl TagProperty {
pub fn from_id(id: &str) -> Option<Self> {
match id {
"type" => Some(Self::Type),
"title" => Some(Self::Title),
"actual_text" => Some(Self::ActualText),
"alternate_text" => Some(Self::AlternateText),
"language" => Some(Self::Language),
_ => None,
}
}
fn key(self) -> &'static [u8] {
match self {
Self::Type => b"S",
Self::Title => b"T",
Self::ActualText => b"ActualText",
Self::AlternateText => b"Alt",
Self::Language => b"Lang",
}
}
}
#[derive(Debug, thiserror::Error, PartialEq)]
pub enum TagError {
#[error("object {0} is not a reachable indirect structure tag")]
NotATag(ObjRef),
#[error("a tag type must be a nonempty PDF name using letters, digits, colon, period, underscore or hyphen")]
InvalidType,
#[error("tag property text is too long (maximum 65536 bytes)")]
TooLong,
#[error(transparent)]
Cos(#[from] CosError),
}
fn text(doc: &Document, d: &pdfcraft_cos::Dict, key: &[u8]) -> Option<String> {
d.get(key).map(|o| doc.resolve(o)).and_then(|o| o.as_string().map(PdfString::to_text))
}
/// List reachable, indirect tags in structure-tree order.
pub fn tags(doc: &Document) -> Vec<TagInfo> {
let pages = pdfcraft_model::pages(doc);
let page_index: std::collections::HashMap<ObjRef, usize> = pages.iter().enumerate().map(|(i, p)| (p.obj, i)).collect();
alt::elements(doc)
.into_iter()
.filter_map(|(object, d, page)| {
Some(TagInfo {
object,
kind: String::from_utf8_lossy(d.name(b"S")?).into_owned(),
title: text(doc, &d, b"T"),
actual_text: text(doc, &d, b"ActualText"),
alt: text(doc, &d, b"Alt"),
language: text(doc, &d, b"Lang"),
page: page.and_then(|p| page_index.get(&p).copied()),
})
})
.collect()
}
/// Change one property of a reachable tag; `None` removes a text property.
pub fn set_tag_property(doc: &mut Document, object: ObjRef, property: TagProperty, value: Option<&str>) -> Result<(), TagError> {
if !alt::elements(doc).iter().any(|(r, _, _)| *r == object) {
return Err(TagError::NotATag(object));
}
// ActualText may intentionally contain leading or trailing whitespace; preserve it.
let value = value.filter(|s| !s.is_empty());
if value.is_some_and(|s| s.len() > 65_536) {
return Err(TagError::TooLong);
}
if property == TagProperty::Type
&& !value.is_some_and(|s| s.len() <= 127 && s.as_bytes().iter().all(|b| b.is_ascii_alphanumeric() || matches!(b, b':' | b'.' | b'_' | b'-')))
{
return Err(TagError::InvalidType);
}
doc.update_dict(object, |d| match value {
Some(s) if property == TagProperty::Type => d.set(property.key().to_vec(), Object::name(s)),
Some(s) => d.set(property.key().to_vec(), Object::String(PdfString::text(s))),
None => {
d.remove(property.key());
}
})?;
Ok(())
}
+32
View File
@@ -164,6 +164,38 @@ fn structure_doc(objects: &[String]) -> Document {
pdf(&objs, "")
}
#[test]
fn reachable_tag_properties_round_trip_and_reject_unrelated_objects() {
let mut doc =
structure_doc(&["<< /S /Document /K [4 0 R] >>".into(), "<< /S /Figure /T (Old) /Alt (A chart) >>".into(), "<< /S /Figure >>".into()]);
let tag = pdfcraft_cos::ObjRef::new(4, 0);
let listed = tags(&doc);
assert_eq!(listed.len(), 2);
assert_eq!(listed[1].object, tag);
assert_eq!(listed[1].title.as_deref(), Some("Old"));
for (property, value) in [
(TagProperty::Type, "P"),
(TagProperty::Title, "Figure title"),
(TagProperty::ActualText, " Revenue increased "),
(TagProperty::AlternateText, "A rising chart"),
(TagProperty::Language, "en-GB"),
] {
set_tag_property(&mut doc, tag, property, Some(value)).unwrap();
}
let edited = &tags(&doc)[1];
assert_eq!(edited.kind, "P");
assert_eq!(edited.title.as_deref(), Some("Figure title"));
assert_eq!(edited.actual_text.as_deref(), Some(" Revenue increased "));
assert_eq!(edited.alt.as_deref(), Some("A rising chart"));
assert_eq!(edited.language.as_deref(), Some("en-GB"));
set_tag_property(&mut doc, tag, TagProperty::Title, None).unwrap();
assert_eq!(tags(&doc)[1].title, None);
assert!(matches!(set_tag_property(&mut doc, tag, TagProperty::Type, None), Err(TagError::InvalidType)));
assert!(matches!(set_tag_property(&mut doc, tag, TagProperty::Type, Some("bad type")), Err(TagError::InvalidType)));
assert!(matches!(set_tag_property(&mut doc, pdfcraft_cos::ObjRef::new(5, 0), TagProperty::Title, Some("No")), Err(TagError::NotATag(_))));
assert!(matches!(set_tag_property(&mut doc, pdfcraft_cos::ObjRef::new(4, 1), TagProperty::Title, Some("No")), Err(TagError::NotATag(_))));
}
fn checked(doc: &Document, rules: &[Rule]) -> Report {
check(doc, &Options { rules: rules.iter().copied().collect(), pages: None })
}
+1 -1
View File
@@ -10,6 +10,7 @@ workspace = true
[dependencies]
pdfcraft-engine = { workspace = true }
pdfcraft-cos = { workspace = true }
pdfcraft-render = { workspace = true }
pdfcraft-organize = { workspace = true }
pdfcraft-platform = { workspace = true }
@@ -25,7 +26,6 @@ mcp = ["dep:base64"]
[dev-dependencies]
pdfcraft-fonts = { workspace = true }
pdfcraft-cos = { workspace = true }
pdfcraft-model = { workspace = true }
pdfcraft-xfa = { workspace = true }
image = { version = "0.25", default-features = false, features = ["jpeg"] }
+34
View File
@@ -146,6 +146,40 @@ impl Automation {
self.session.apply(id, edit).map_err(failed)?;
self.accessibility_figures(a)
}
pub(crate) fn accessibility_tags(&self, a: &Args) -> Result<Value> {
let tags: Vec<Value> = self
.doc(a)?
.tags()
.into_iter()
.map(|t| {
json!({
"object": t.object.num, "generation": t.object.generation, "type": t.kind,
"title": t.title, "actual_text": t.actual_text, "alternate_text": t.alt,
"language": t.language, "page": t.page.map(|p| p + 1),
})
})
.collect();
Ok(json!({ "count": tags.len(), "tags": tags }))
}
pub(crate) fn accessibility_set_tag_property(&mut self, a: &Args) -> Result<Value> {
let object = u32::try_from(a.int("object")?).map_err(|_| ToolError::InvalidArgs("object must be a positive tag object number".into()))?;
if object == 0 {
return Err(ToolError::InvalidArgs("object must be a positive tag object number".into()));
}
let generation = u16::try_from(a.int("generation")?)
.map_err(|_| ToolError::InvalidArgs("generation must be a PDF object generation from accessibility_tags".into()))?;
let property_id = a.str("property")?;
let property = pdfcraft_engine::a11y::TagProperty::from_id(property_id)
.ok_or_else(|| ToolError::InvalidArgs(format!("unknown tag property {property_id:?}")))?;
let value = a.opt_str("value")?.map(str::to_owned);
let id = self.doc(a)?.id;
self.session
.apply(id, pdfcraft_engine::Edit::SetTagProperty { object: pdfcraft_cos::ObjRef::new(object, generation), property, value })
.map_err(failed)?;
self.accessibility_tags(a)
}
}
impl Automation {
+2
View File
@@ -643,6 +643,8 @@ impl Automation {
"accessibility_fix" => self.a11y_fix(&a)?,
"accessibility_figures" => self.accessibility_figures(&a)?,
"accessibility_set_alt" => self.accessibility_set_alt(&a)?,
"accessibility_tags" => self.accessibility_tags(&a)?,
"accessibility_set_tag_property" => self.accessibility_set_tag_property(&a)?,
"sign_id_create" => self.sign_id_create(&a)?,
"sign_document" => self.sign_document(&a)?,
"sign_keychain_ids" => {
+10
View File
@@ -875,6 +875,16 @@ pub fn tools() -> Vec<ToolDef> {
t("accessibility_set_alt", "Set alternate text", "Set a figure's alternate text (alt; empty or omitted clears it), or mark it decorative (decorative: true: its content becomes an artifact and the figure leaves the tags). figure is a number from accessibility_figures. Returns the figures. Undoable.")
.cmd("a11y.alt_text")
.with(schema(json!({ "doc": doc(), "figure": { "type": "integer", "minimum": 1 }, "alt": { "type": "string" }, "decorative": { "type": "boolean" } }), &["doc", "figure"])),
t("accessibility_tags", "List structure tags", "List reachable indirect structure tags in document order with PDF object number, generation, page, type, title, actual text, alternate text and language. Inline tag dictionaries have no stable object reference and are not listed.")
.ro()
.with(schema(json!({ "doc": doc() }), &["doc"])),
t("accessibility_set_tag_property", "Edit tag property", "Set or clear one property of a reachable indirect structure tag. Use object and generation from accessibility_tags. Properties: type, title, actual_text, alternate_text, language. Empty or omitted value clears text properties; type requires a nonempty PDF name. Undoable.")
.with(schema(json!({
"doc": doc(), "object": { "type": "integer", "minimum": 1 },
"generation": { "type": "integer", "minimum": 0, "maximum": 65535 },
"property": { "type": "string", "enum": ["type", "title", "actual_text", "alternate_text", "language"] },
"value": { "type": "string" },
}), &["doc", "object", "generation", "property"])),
t("form_merge_data", "Merge data files into spreadsheet", "Collect the field values of form data files (FDF, XFDF) or filled-in PDF forms into one CSV file at path: a column per field name, a row per file. Returns the row and column counts.")
.cmd("form.merge_data")
.with(schema(json!({ "paths": { "type": "array", "items": { "type": "string" }, "minItems": 1 }, "path": { "type": "string" } }), &["paths", "path"])),
+23
View File
@@ -1565,6 +1565,29 @@ trailer << /Root 1 0 R >>
)
.unwrap();
let fd = ok(&mut a, "doc_open", json!({ "path": "figures.pdf" }))["doc"].as_u64().unwrap();
let tags = ok(&mut a, "accessibility_tags", json!({ "doc": fd }));
assert_eq!(tags["count"], 3);
assert_eq!(tags["tags"][1]["object"], 7);
assert_eq!(tags["tags"][1]["type"], "Figure");
let changed = ok(
&mut a,
"accessibility_set_tag_property",
json!({
"doc": fd, "object": 7, "generation": 0, "property": "title", "value": "First shape"
}),
);
assert_eq!(changed["tags"][1]["title"], "First shape");
assert_eq!(ok(&mut a, "edit_undo", json!({ "doc": fd }))["undone"], "Change tag properties");
assert_eq!(ok(&mut a, "accessibility_tags", json!({ "doc": fd }))["tags"][1]["title"], Value::Null);
assert!(matches!(
a.call(
"accessibility_set_tag_property",
&json!({
"doc": fd, "object": 7, "generation": 1, "property": "title", "value": "Wrong generation"
})
),
Err(ToolError::Failed(_))
));
let findings = ok(&mut a, "accessibility_check", json!({ "doc": fd, "rules": ["figures-alt-text"] }));
let figures_rule = findings["results"].as_array().unwrap().iter().find(|x| x["rule"] == "figures-alt-text").unwrap();
assert_eq!(figures_rule["findings"][0]["object"], 7);
+17
View File
@@ -472,6 +472,11 @@ impl Document {
self.editor.as_ref().map(|e| a11y::figures(&e.cos)).unwrap_or_default()
}
/// Reachable indirect structure tags, in document order.
pub fn tags(&self) -> Vec<a11y::TagInfo> {
self.editor.as_ref().map(|e| a11y::tags(&e.cos)).unwrap_or_default()
}
/// The edit that fixes `rule`, for the rules with an automatic fix: the document language
/// (`value` is the language), the title (`value` replaces it; else the current title or the
/// file name) and the tab order.
@@ -1184,6 +1189,12 @@ pub enum Edit {
figure: u32,
alt: Option<String>,
},
/// Change one property on a reachable indirect structure tag.
SetTagProperty {
object: pdfcraft_cos::ObjRef,
property: a11y::TagProperty,
value: Option<String>,
},
/// Add alternate text ▸ Decorative figure: the figure's content becomes an artifact.
MarkDecorative {
figure: u32,
@@ -1438,6 +1449,7 @@ impl std::fmt::Display for Label {
/// [`BATCH_LABELS`]).
pub const EDIT_LABELS: &[&str] = &[
"Rotate page",
"Change tag properties",
"Rotate pages",
"Delete page",
"Delete pages",
@@ -1639,6 +1651,7 @@ impl Edit {
Edit::SetTabOrder { .. } | Edit::MoveInTabOrder { .. } => Label::of("Set tab order"),
Edit::SetInitialView(_) => Label::of("Change initial view"),
Edit::SetAltText { .. } => Label::of("Set alternate text"),
Edit::SetTagProperty { .. } => Label::of("Change tag properties"),
Edit::MarkDecorative { .. } => Label::of("Mark figure as decorative"),
Edit::AddOcrText { .. } => Label::of("Recognize text"),
Edit::ApplyScriptChanges { .. } => Label::of("Run JavaScript"),
@@ -1824,6 +1837,7 @@ fn check_permission(edit: &Edit, p: &pdfcraft_cos::Permissions) -> Result<(), Ed
| Edit::MoveInTabOrder { .. }
| Edit::SetInitialView(_)
| Edit::SetAltText { .. }
| Edit::SetTagProperty { .. }
| Edit::MarkDecorative { .. }
| Edit::AddOcrText { .. }
| Edit::SetDocumentScript { .. }
@@ -2093,6 +2107,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::SetTagProperty { object, property, value } => {
a11y::set_tag_property(doc, *object, *property, value.as_deref()).map_err(|e| EditError::Accessibility(e.to_string()))?;
}
Edit::EditTextLine { page, line, text } => {
pdfcraft_edit::replace_line(doc, *page, *line, text)?;
}
+1
View File
@@ -2275,3 +2275,4 @@ scroll speed Fast سريعة
This printer's driver doesn't take two-sided settings from other programs, so its own setting applies. Change it in Properties… لا يقبل برنامج تشغيل هذه الطابعة إعدادات الطباعة على الوجهين من البرامج الأخرى، لذا يُطبَّق إعداده الخاص. غيّره من خصائص…
Tools panel side جانب لوحة الأدوات
Add comment إضافة تعليق
Change tag properties تغيير خصائص الوسم
Can't render this file because it contains an unexpected character in line 6 and column 47.
+1
View File
@@ -2217,3 +2217,4 @@ scroll speed Fast Висока
This printer's driver doesn't take two-sided settings from other programs, so its own setting applies. Change it in Properties… Драйверът на този принтер не приема настройки за двустранен печат от други програми и прилага собствената си настройка. Променете я от Свойства…
Tools panel side Страна на панела с инструменти
Add comment Добавяне на коментар
Change tag properties Промяна на свойствата на етикета
Can't render this file because it contains an unexpected character in line 8 and column 16.
+1
View File
@@ -2331,3 +2331,4 @@ scroll speed Fast Rychlá
The fonts PdfCraft writes with can't draw “{c}” ({code}), so it shows as ? in every PDF viewer. The text is saved. Písma, kterými PdfCraft píše, neumí zobrazit „{c}“ ({code}), proto se ve všech prohlížečích PDF zobrazí jako ?. Text se uloží.
Tools panel side Strana panelu nástrojů
Add comment Přidat komentář
Change tag properties Změnit vlastnosti značky
Can't render this file because it contains an unexpected character in line 6 and column 47.
+1
View File
@@ -2255,3 +2255,4 @@ scroll speed Fast Schnell
This printer's driver doesn't take two-sided settings from other programs, so its own setting applies. Change it in Properties… Der Treiber dieses Druckers übernimmt keine Einstellungen für beidseitigen Druck von anderen Programmen, daher gilt seine eigene Einstellung. Ändern unter Eigenschaften…
Tools panel side Seite des Werkzeugbereichs
Add comment Kommentar hinzufügen
Change tag properties Tag-Eigenschaften ändern
Can't render this file because it contains an unexpected character in line 6 and column 51.
+1
View File
@@ -2273,3 +2273,4 @@ scroll speed Fast Γρήγορη
The fonts PdfCraft writes with can't draw “{c}” ({code}), so it shows as ? in every PDF viewer. The text is saved. Οι γραμματοσειρές με τις οποίες γράφει το PdfCraft δεν μπορούν να σχεδιάσουν το «{c}» ({code}), γι' αυτό εμφανίζεται ως ? σε κάθε πρόγραμμα προβολής PDF. Το κείμενο αποθηκεύεται.
Tools panel side Πλευρά πλαισίου εργαλείων
Add comment Προσθήκη σχολίου
Change tag properties Αλλαγή ιδιοτήτων ετικέτας
Can't render this file because it contains an unexpected character in line 477 and column 12.
+1
View File
@@ -2387,3 +2387,4 @@ scroll speed Fast Rápida
This printer's driver doesn't take two-sided settings from other programs, so its own setting applies. Change it in Properties… El controlador de esta impresora no acepta los ajustes de doble cara de otros programas, así que se aplica su propio ajuste. Cámbielo en Propiedades…
Tools panel side Lado del panel de herramientas
Add comment Añadir comentario
Change tag properties Cambiar propiedades de la etiqueta
Can't render this file because it contains an unexpected character in line 6 and column 60.
+1
View File
@@ -2254,3 +2254,4 @@ scroll speed Fast Rapide
This printer's driver doesn't take two-sided settings from other programs, so its own setting applies. Change it in Properties… Le pilote de cette imprimante n'accepte pas les réglages recto verso des autres programmes ; son propre réglage s'applique. Modifiez-le dans Propriétés…
Tools panel side Côté du panneau Outils
Add comment Ajouter un commentaire
Change tag properties Modifier les propriétés de la balise
Can't render this file because it contains an unexpected character in line 6 and column 39.
+1
View File
@@ -2253,3 +2253,4 @@ scroll speed Fast Gyors
This printer's driver doesn't take two-sided settings from other programs, so its own setting applies. Change it in Properties… A nyomtató illesztőprogramja nem fogadja el más programoktól a kétoldalas nyomtatás beállítását, ezért a saját beállítása érvényes. Módosítás: Tulajdonságok…
Tools panel side Az Eszközök panel oldala
Add comment Megjegyzés hozzáadása
Change tag properties Címke tulajdonságainak módosítása
Can't render this file because it contains an unexpected character in line 7 and column 44.
+1
View File
@@ -2263,3 +2263,4 @@ scroll speed Fast Veloce
This printer's driver doesn't take two-sided settings from other programs, so its own setting applies. Change it in Properties… Il driver di questa stampante non accetta le impostazioni fronte-retro da altri programmi, quindi vale la sua impostazione. Modificala in Proprietà…
Tools panel side Lato del pannello Strumenti
Add comment Aggiungi commento
Change tag properties Modifica proprietà del tag
Can't render this file because it contains an unexpected character in line 6 and column 47.
+1
View File
@@ -2254,3 +2254,4 @@ scroll speed Fast 速い
This printer's driver doesn't take two-sided settings from other programs, so its own setting applies. Change it in Properties… このプリンターのドライバーは他のプログラムからの両面印刷の設定を受け付けないため、ドライバー自身の設定が使われます。変更先: プロパティ…
Tools panel side ツールパネルの位置
Add comment コメントを追加
Change tag properties タグのプロパティを変更
Can't render this file because it contains an unexpected character in line 6 and column 47.
+1
View File
@@ -2255,3 +2255,4 @@ scroll speed Fast Жылдам
The fonts PdfCraft writes with can't draw “{c}” ({code}), so it shows as ? in every PDF viewer. The text is saved. PdfCraft жазатын қаріптер «{c}» ({code}) таңбасын сала алмайды, сондықтан ол барлық PDF қарау құралдарында ? болып көрінеді. Мәтін сақталады.
Tools panel side Құралдар панелінің жағы
Add comment Түсініктеме қосу
Change tag properties Тег қасиеттерін өзгерту
Can't render this file because it contains an unexpected character in line 813 and column 12.
+1
View File
@@ -2237,3 +2237,4 @@ scroll speed Fast Szybka
This printer's driver doesn't take two-sided settings from other programs, so its own setting applies. Change it in Properties… Sterownik tej drukarki nie przyjmuje ustawień druku dwustronnego z innych programów, więc obowiązuje jego własne ustawienie. Zmień je w oknie Właściwości…
Tools panel side Strona panelu narzędzi
Add comment Dodaj komentarz
Change tag properties Zmień właściwości znacznika
Can't render this file because it contains an unexpected character in line 6 and column 47.
+1
View File
@@ -2268,3 +2268,4 @@ scroll speed Fast Rápida
This printer's driver doesn't take two-sided settings from other programs, so its own setting applies. Change it in Properties… O driver desta impressora não aceita configurações de frente e verso de outros programas, então vale a configuração dele. Altere em Propriedades…
Tools panel side Lado do painel Ferramentas
Add comment Adicionar comentário
Change tag properties Alterar propriedades da tag
Can't render this file because it contains an unexpected character in line 9 and column 16.
+1
View File
@@ -2167,3 +2167,4 @@ scroll speed Fast Высокая
This printer's driver doesn't take two-sided settings from other programs, so its own setting applies. Change it in Properties… Драйвер этого принтера не принимает настройки двусторонней печати от других программ, поэтому действует его собственная настройка. Измените её в окне Свойства…
Tools panel side Сторона панели инструментов
Add comment Добавить комментарий
Change tag properties Изменить свойства тега
Can't render this file because it contains an unexpected character in line 9 and column 16.
+1
View File
@@ -2262,3 +2262,4 @@ scroll speed Fast Hitro
The fonts PdfCraft writes with can't draw “{c}” ({code}), so it shows as ? in every PDF viewer. The text is saved. Pisave, s katerimi piše PdfCraft, ne morejo prikazati »{c}« ({code}), zato se v vseh pregledovalnikih PDF prikaže kot ?. Besedilo se shrani.
Tools panel side Stran podokna z orodji
Add comment Dodaj komentar
Change tag properties Spremeni lastnosti oznake
Can't render this file because it contains an unexpected character in line 9 and column 16.
+1
View File
@@ -2170,3 +2170,4 @@ scroll speed Fast వేగంగా
Black and white నలుపు తెలుపు
Tools panel side టూల్స్ ప్యానెల్ వైపు
Add comment వ్యాఖ్యను జోడించు
Change tag properties ట్యాగ్ లక్షణాలను మార్చు
Can't render this file because it contains an unexpected character in line 13 and column 16.
+1
View File
@@ -2253,3 +2253,4 @@ scroll speed Fast Hızlı
The fonts PdfCraft writes with can't draw “{c}” ({code}), so it shows as ? in every PDF viewer. The text is saved. PdfCraft'ın yazdığı yazı tipleri “{c}” ({code}) karakterini çizemiyor, bu yüzden her PDF görüntüleyicide ? olarak görünür. Metin kaydedilir.
Tools panel side Araçlar panelinin yeri
Add comment Yorum ekle
Change tag properties Etiket özelliklerini değiştir
Can't render this file because it contains an unexpected character in line 811 and column 12.
+1
View File
@@ -2276,3 +2276,4 @@ scroll speed Fast Швидка
This printer's driver doesn't take two-sided settings from other programs, so its own setting applies. Change it in Properties… Драйвер цього принтера не приймає налаштування двобічного друку від інших програм, тому діє його власне налаштування. Змініть його у вікні Властивості…
Tools panel side Бік панелі інструментів
Add comment Додати коментар
Change tag properties Змінити властивості тегу
Can't render this file because it contains an unexpected character in line 6 and column 47.
+1
View File
@@ -2218,3 +2218,4 @@ scroll speed Fast 快
This printer's driver doesn't take two-sided settings from other programs, so its own setting applies. Change it in Properties… 此打印机的驱动程序不接受其他程序的双面打印设置,因此使用其自身设置。更改位置:属性…
Tools panel side 工具面板位置
Add comment 添加注释
Change tag properties 更改标签属性
Can't render this file because it contains an unexpected character in line 9 and column 16.
+1
View File
@@ -2131,3 +2131,4 @@ scroll speed Fast 快
This printer's driver doesn't take two-sided settings from other programs, so its own setting applies. Change it in Properties… 此印表機的驅動程式不接受其他程式的雙面列印設定,因此使用其自身設定。變更位置:屬性…
Tools panel side 工具面板位置
Add comment 新增註解
Change tag properties 變更標籤屬性
Can't render this file because it contains an unexpected character in line 9 and column 16.
+4 -4
View File
@@ -8839,11 +8839,11 @@ title = "Tag properties (type, title, actual text, alternate text, language)"
area = "M"
tier = "P1"
milestone = "M12"
status = "planned"
status = "partial"
commands = []
tools = []
evidence = []
notes = ""
tools = ["accessibility_tags", "accessibility_set_tag_property"]
evidence = ["crates/a11y/src/tests.rs::reachable_tag_properties_round_trip_and_reject_unrelated_objects", "crates/automation/tests/automation.rs::accessibility_check_report_and_fixes_through_tools"]
notes = "The engine and headless tools list reachable indirect tags and edit type, title, actual text, alternate text and language with undo. An on-screen tag properties editor and inline tag dictionary support remain planned with the Tags Panel (#982)."
[[feature]]
id = "a11y.role-map"