mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-14 19:27:04 -04:00
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:
1 parent
fe2eadb211
commit
b65b7adb01
32 files changed
+277
-26
No files matched your search
+20
-20
@@ -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"
|
||||
@@ -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();
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
@@ -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 })
|
||||
}
|
||||
|
||||
@@ -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"] }
|
||||
@@ -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 {
|
||||
|
||||
@@ -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" => {
|
||||
|
||||
@@ -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"])),
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)?;
|
||||
}
|
||||
|
||||
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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"
|
||||
|
||||
Reference in new issue
Block a user