From f52c92882d9b1aa95733107274cd64c6501fbb10 Mon Sep 17 00:00:00 2001 From: Zeyu Li Date: Thu, 15 Oct 2026 14:32:38 +0800 Subject: [PATCH] M9: signature labels follow the interface language, and one the font can't draw is refused, never written as ? (#1951) * M9: signature labels follow the interface language, and one the font can't draw is refused, never written as ? The labels of a visible signature ("Digitally signed by", "Reason: ", ...) were written in WinAnsi unchecked, and every caller passed the English defaults. Now: - sign: a drawn label is checked as the details are, and one Helvetica can't draw is refused (SignError::Unsupported) naming the label and the character. AppearanceLabels::fields and ::first_undrawable say which labels a caller can use. - ui-egui: signing stamps the labels in the interface language when Helvetica can draw all of them, otherwise all in English (never a mix) with a note under the preview. The preview uses the same labels, the stamp's line layout and the signing time. - automation: sign_document takes optional labels and appearance; a label the font can't draw, an unknown key or one without a visible signature is InvalidArgs. - Catalogs: the five labels and the note in every language; refresh hashes. Follows up the note on #1112. * Refresh catalog hashes after merging main --------- Co-authored-by: Brandon Thomas --- ATTRIBUTION.toml | 42 ++++---- Cargo.lock | 2 +- ROADMAP.md | 1 + crates/automation/src/signing.rs | 69 ++++++++++++++ crates/automation/src/tools.rs | 27 +++++- crates/automation/tests/automation.rs | 83 ++++++++++++++++ crates/engine/src/lib.rs | 2 +- crates/sign/src/pdf.rs | 33 +++++++ crates/sign/tests/pdf.rs | 52 ++++++++++ crates/ui-egui/src/i18n/ar.tsv | 6 ++ crates/ui-egui/src/i18n/bg.tsv | 6 ++ crates/ui-egui/src/i18n/cs.tsv | 6 ++ crates/ui-egui/src/i18n/de.tsv | 6 ++ crates/ui-egui/src/i18n/el.tsv | 6 ++ crates/ui-egui/src/i18n/es.tsv | 6 ++ crates/ui-egui/src/i18n/fr.tsv | 6 ++ crates/ui-egui/src/i18n/hu.tsv | 6 ++ crates/ui-egui/src/i18n/it.tsv | 6 ++ crates/ui-egui/src/i18n/ja.tsv | 6 ++ crates/ui-egui/src/i18n/kk.tsv | 6 ++ crates/ui-egui/src/i18n/lt.tsv | 6 ++ crates/ui-egui/src/i18n/mod.rs | 28 ++++++ crates/ui-egui/src/i18n/pl.tsv | 6 ++ crates/ui-egui/src/i18n/pt-br.tsv | 6 ++ crates/ui-egui/src/i18n/ru.tsv | 6 ++ crates/ui-egui/src/i18n/sl.tsv | 6 ++ crates/ui-egui/src/i18n/te.tsv | 6 ++ crates/ui-egui/src/i18n/tr.tsv | 6 ++ crates/ui-egui/src/i18n/uk.tsv | 6 ++ crates/ui-egui/src/i18n/zh-hans.tsv | 6 ++ crates/ui-egui/src/i18n/zh-hant.tsv | 6 ++ crates/ui-egui/src/sign_ui.rs | 132 ++++++++++++++++++++++---- crates/ui-egui/tests/signing.rs | 54 +++++++++++ parity/acrobat-features.toml | 4 +- 34 files changed, 611 insertions(+), 44 deletions(-) diff --git a/ATTRIBUTION.toml b/ATTRIBUTION.toml index 290dea2ccf..0e01f271d3 100644 --- a/ATTRIBUTION.toml +++ b/ATTRIBUTION.toml @@ -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 = "a4ab30e3f0e7e65c93dadf7c4dd88b15efbad5ae1f218d77f639cf490c900bdb" +sha256 = "348b7dbb18ca419dbdaf05c6701bb96754a5989c178bec1e971164e2e15c6c43" # 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 = "3ed54e2ed759fbcf7fbff39a70820de78a7e79ce0cd8133e0d870ad0f4b62ac3" +sha256 = "bea733dbdd4cdcdd2f9c1d7c278a382ee51188ee37f01c8a049c35e953151869" [[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 = "74ce914c682d54480f986a007ae4140ec19bdfa2ac9729418f04a8779a98330b" +sha256 = "6d5f5923f73d01fa6166a43b7221834ebb39002ce2d826084c10a4d3d89526a9" [[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 = "06bb8f0ce964ac41355c589ba278c941b281aca249741672d513ca2033bd818d" +sha256 = "9cea4cc5aa0289e609b00779acc1709c33488fe0a0b59d60a124e877c714eedc" [[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 = "615c578b6da67dc5c23fe6f290464aabb327da68027bd7ebd4e065a61f047b4b" +sha256 = "b6c62e0b859917175c80d7fa0a9174a3d445458b8d16ff85260a224ccc10204d" [[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 = "c7156ba4a06220d697c908e173f6bbd9ef1475069f90dcb8f72bc5a620e51cb3" +sha256 = "8ee80cd976b0a43f601628fa48a9b09bcc4a1e66b4df6870597ee0b1027d897e" [[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 = "8286c0e834e0a87fc1909dba2d974aa8993230e5cdb9ce89abdab3cd1ecf0d22" +sha256 = "656b2e9550c747e44bffd2437443a5dcd946841831e9708bcb93c5dd919221f5" [[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 = "8b65824b11221ec61fc42a32093d61f301a57e206f69e166c491f26333cd593f" +sha256 = "f7bb9f7b48c8759ca2e9a4fac8d991b5d656ef0c0563b81b320b4d127bb474a9" [[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 = "85eb2eb9f43b773fe76747b51d288a590a1d5251b19c189e818c38bc88a5781c" +sha256 = "e2ab1fe22706c4121c7e9a0d56da6404e61d7430433b915f22129c4733da3ed1" [[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 = "32c0f3141dab1aa960690e38a9414b88252a8477e9dd9c19e6b21a7dd099c6b0" +sha256 = "657dcdbecc7e105e03a82a0f7903b0e51c4120f8c4a793254216df989fb1f893" [[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 = "8b47ba586f779b4fd42d483b52a81260e52e10c4135fe541f6a62148f9b64812" +sha256 = "75282238445a2873e322059a90795463033ec4dfbc30b83c21d20bcde3680f8f" [[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 = "0a9864a30413a28ba90c11eb4b2760686adf0cb7d060365c2936a5e0c1201009" +sha256 = "653525be13d7fb18b3dcc6eb05656bb9d2c573dea2c488b21aa82b319ed4b67c" [[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 = "09e2c640908567e8a56ec9ac857ad5fb1e09d4c3ad68b1ad873f098a55396bb5" +sha256 = "d49788ad569aee627cd4386799b58334f03115c2e6977af3f02305a6771bc403" [[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 = "7d36195963293e514d9a40242a8fd2aaed479fa4642fed13f9648916a5b1fde1" +sha256 = "5fa60b06689a8c33fb0540dd3cd4c996064629066b2b116a3ded535c73405192" [[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 = "9c42561f5279e1e5c14b23e35328c69d800750ade22981e9524fc748412328bd" +sha256 = "2ac70113c4dd3182c566681f301f0fa0da77c57231ddcad514db6f67cd4bb8e9" [[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 = "64bba70d41277aeba09c1af425347e67586aa52d5d7758148c679af208a6a63b" +sha256 = "c61ad71750d8246f09d8e9ee1229d77662b96c7dba886e3ce7b46083e3bac406" [[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 = "f055211d06eee77906ddea5e53c7f29c7b110ee58a031a8359a4ce7485edfe46" +sha256 = "f675541c61cf5fb70cc63e95ffe1e56c5c63e9c311d1c321852aa2e3313b7833" [[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 = "eecf66414e14f29b989e7b6de748810609002dc62b71e7789a2eb2daed5ec845" +sha256 = "cdb9a5d7e51db6b62c1d85a77e42faa73932425791b06fad836333f1ee21b7fe" [[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 = "34ce3af08c6afc0bbc3ec37ec1923aa4f8a51db44b4f08d6b45674e91cd1bed4" +sha256 = "d4d0591fab5d328a5d34346ffc7abe1dbaeed8241a97f9624763b95126ab78fa" [[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 = "9d76dc284dd281e3d7a2fb7ea7636ae2c3650519713665f02bf96e8c65d86374" +sha256 = "d2d0588abbb6ae03347d197c58dc18d97a18bc479580216a14501be292254e8f" [[asset]] @@ -3363,4 +3363,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 = "fb217509688595c77fcf8c5d256adb931a7a21c87c23dcc31bc79ba5710a4ffc" +sha256 = "2a2d8aa07d2c6de2f55b9d7de1fb299c63843c217013c930c288be4ce68c6b87" diff --git a/Cargo.lock b/Cargo.lock index 59fbc32dbd..b881689e5b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4218,8 +4218,8 @@ dependencies = [ "pdfcraft-fonts", "pdfcraft-forms", "pdfcraft-model", - "pdfcraft-xfa", "pdfcraft-testkit", + "pdfcraft-xfa", "thiserror 2.0.21", ] diff --git a/ROADMAP.md b/ROADMAP.md index e45a076600..db201d7726 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -158,6 +158,7 @@ Ranked; detail and the full milestone table in [docs/roadmap.md](docs/roadmap.md - **2026-10-12 (M3, page-label navigation):** The page box displays logical labels and keeps the current page when its unchanged value is submitted, including duplicate labels. Editable-control regressions also cover numeric fallback, invalid input, active typing and focused same-document replacement with changed labels or fewer pages. Clean file-watcher reload preserves the chosen scrolling mode, preventing a Single Page reader from jumping to page 1 during replacement. Estimates unchanged. - **2026-10-13 (community, M2/M6, form-heavy documents):** An image whose pixels hold at most 16 bits of samples (an 8-bit Separation or DeviceN spot-colour image, a 1-4-bit gray or indexed one) is converted once per possible pixel value instead of once per pixel (vendored hayro-interpret patch 28). A fillable character sheet whose 3304 × 4105 DeviceN background ran a PostScript tint transform per pixel drew each page in 3.8 s and 430 MB, on opening and again after every field was filled; it now draws in 0.11 s and 100 MB, pixel for pixel the same. The inspector places widgets without `/P` in one pass over the pages (a form of 8,000 such fields opened in 1.17 s), and the Fields panel pairs its rows with the form through one map a frame and draws only the rows in view (8,000 fields: about 15× less CPU a frame with the panel open). Regressions fail before each fix. +- **2026-10-14 (M9, signature appearance labels):** A visible signature's labels ("Digitally signed by", "Reason: ", …) follow the interface language when Helvetica can draw all of them, and fall back to English as a set otherwise, with a note under the preview; the preview shows the same labels and the signing time. A drawn label the font can't draw is refused naming the label and character, never written as `?`. `sign_document` takes `labels` and `appearance`. Sign, automation and kittest regressions cover German, Chinese and the refusals. Estimates unchanged. - **2026-10-13 (M3, native-opening candidate):** Chosen tools survive preparation, conversion and password retries; startup intake waits for pending preparation. Regression tests cover completion, cancellation and document identity. Estimates unchanged. - **2026-10-13 (community, M3, #531 touchpad momentum):** On Wayland a two-finger swipe keeps gliding after the fingers lift and slows to a stop, measured from the gesture's last 0.1 s and decaying with a 0.35 s time constant through the #759 1:1 path. Any wheel input, click, key, zoom or edge ends it (fingers resting on the touchpad don't yet: the vendored winit binds no Wayland hold gestures); single-page paging, macOS (which sends its own momentum), the web, and Animation effects Off never glide. Unit tests for the physics and two headless view tests. Estimates unchanged. - **2026-10-12 (M10, Windows printing without PowerShell, #1107):** craft-print now prints on Windows in-process: printers from EnumPrinters/GetDefaultPrinter, jobs drawn as vectors by Windows.Data.Pdf into the XPS print API with a driver-validated PrintTicket (exact paper, orientation, no scaling), so printing works where App Control for Business or AppLocker run PowerShell in Constrained Language Mode. Portrait and landscape sheets still print as one job (turned with /Rotate); a sheet size the driver lacks is refused rather than shrunk. Pins craft-libs f03ec27 (craft-libs#28); deny.toml allows the storytold/winprint.rs fork and adds a webpki-root-certs licence exception and a RUSTSEC-2024-0370 ignore for winprint's build-dependencies. An earlier craft-print revision was checked in Windows Sandbox under enforced WDAC: printers listed, CLI and desktop jobs spooled, and no powershell.exe in the 4688 audit. The final pinned revision passed craft-libs Windows CI, including a real paused-queue job; that Sandbox run has not been repeated. Estimates unchanged. diff --git a/crates/automation/src/signing.rs b/crates/automation/src/signing.rs index 2d0281fc00..07bf6758cd 100644 --- a/crates/automation/src/signing.rs +++ b/crates/automation/src/signing.rs @@ -17,6 +17,62 @@ fn time(secs: i64) -> sign::Time { sign::Time::from_unix(secs) } +/// The longest label `sign_document` takes (characters): a label is a few words. +const MAX_LABEL_CHARS: usize = 200; + +/// `appearance`: which items the visible signature shows; those not given keep the default +/// (name, date and labels on; reason, location and distinguished name off). +fn appearance_arg(v: &Value) -> Result { + const KEYS: &str = "name, date, reason, location, distinguished_name, labels"; + let o = v.as_object().ok_or_else(|| bad(format!("appearance must be an object of true/false items ({KEYS})")))?; + let mut shown = sign::Appearance::default(); + for (key, value) in o { + let on = value.as_bool().ok_or_else(|| bad(format!("appearance.{key} must be true or false")))?; + let item = match key.as_str() { + "name" => &mut shown.name, + "date" => &mut shown.date, + "reason" => &mut shown.reason, + "location" => &mut shown.location, + "distinguished_name" => &mut shown.distinguished_name, + "labels" => &mut shown.labels, + other => return Err(bad(format!("unknown appearance item {other:?} ({KEYS})"))), + }; + *item = on; + } + Ok(shown) +} + +/// `labels`: the words of the visible signature's labels; those not given keep the English. Each +/// must be drawable in the signature's font. Nobody sees a preview here, so a label it can't draw +/// is refused even when this signature doesn't show it. +fn labels_arg(v: &Value) -> Result { + const KEYS: &str = "signed_by, distinguished_name, reason, location, date"; + let o = v.as_object().ok_or_else(|| bad(format!("labels must be an object of strings ({KEYS})")))?; + let mut labels = sign::AppearanceLabels::default(); + for (key, value) in o { + let text = value.as_str().ok_or_else(|| bad(format!("labels.{key} must be a string")))?; + if text.chars().count() > MAX_LABEL_CHARS { + return Err(bad(format!("labels.{key} is longer than {MAX_LABEL_CHARS} characters"))); + } + let label = match key.as_str() { + "signed_by" => &mut labels.signed_by, + "distinguished_name" => &mut labels.distinguished_name, + "reason" => &mut labels.reason, + "location" => &mut labels.location, + "date" => &mut labels.date, + other => return Err(bad(format!("unknown label {other:?} ({KEYS})"))), + }; + *label = text.to_string(); + } + if let Some((field, c)) = labels.first_undrawable() { + return Err(bad(format!( + "labels.{field}: {}", + pdfcraft_engine::undrawable_message(&format!("the {field} label of the visible signature"), c) + ))); + } + Ok(labels) +} + pub(crate) fn cert_json(c: &Certificate) -> Value { json!({ "name": c.display_name(), @@ -177,6 +233,17 @@ impl Automation { Some(g) if rect.is_none() && field.is_none() => return Err(bad(format!("graphic {g} needs a visible signature: pass rect or field"))), Some(g) => Some(self.signature_graphic(g)?), }; + let visible = rect.is_some() || field.is_some(); + let appearance = match a.get("appearance") { + None | Some(Value::Null) => sign::Appearance::default(), + Some(_) if !visible => return Err(bad("appearance needs a visible signature: pass rect or field")), + Some(v) => appearance_arg(v)?, + }; + let appearance_labels = match a.get("labels") { + None | Some(Value::Null) => sign::AppearanceLabels::default(), + Some(_) if !visible => return Err(bad("labels needs a visible signature: pass rect or field")), + Some(v) => labels_arg(v)?, + }; let out = self.resolve(a.str("out")?, true)?; let opts = SignOptions { field, @@ -186,6 +253,8 @@ impl Automation { location: a.opt_str("location")?.map(str::to_string), contact: a.opt_str("contact")?.map(str::to_string), certify, + appearance, + appearance_labels, graphic, ..SignOptions::default() }; diff --git a/crates/automation/src/tools.rs b/crates/automation/src/tools.rs index 9a37863304..d49fd7d433 100644 --- a/crates/automation/src/tools.rs +++ b/crates/automation/src/tools.rs @@ -692,7 +692,7 @@ pub fn tools() -> Vec { .ro() .cmd("sign.digital") .with(schema(json!({}), &[])), - t("sign_document", "Sign a document", "Sign with a digital ID (a .p12/.pfx path, or on macOS a Keychain identity: \"keychain:\" from sign_keychain_ids, or on Windows a store identity: \"windows:\" from sign_windows_ids) and save the signed file to `out` (signing always saves, as in Acrobat; the document then shows the signed file). Sign an existing empty signature field (`field`), or a new one on `page` at `rect` (omit rect for an invisible signature). certify: no_changes, form_fill or comments makes a certification signature. graphic shows a picture where the signer's name would be large (Acrobat: Configure Signature Appearance): {\"image\": path} (PNG or JPEG), {\"text\": name} (typed in the Fill & Sign script font) or {\"strokes\": [[[x, y], …], …]} (y up, the drawing pad 1 wide); it needs a visible signature. PAdES B-B, SHA-256 (SHA-384 for P-384 keys).") + t("sign_document", "Sign a document", "Sign with a digital ID (a .p12/.pfx path, or on macOS a Keychain identity: \"keychain:\" from sign_keychain_ids, or on Windows a store identity: \"windows:\" from sign_windows_ids) and save the signed file to `out` (signing always saves, as in Acrobat; the document then shows the signed file). Sign an existing empty signature field (`field`), or a new one on `page` at `rect` (omit rect for an invisible signature). certify: no_changes, form_fill or comments makes a certification signature. graphic shows a picture where the signer's name would be large (Acrobat: Configure Signature Appearance): {\"image\": path} (PNG or JPEG), {\"text\": name} (typed in the Fill & Sign script font) or {\"strokes\": [[[x, y], …], …]} (y up, the drawing pad 1 wide); it needs a visible signature. appearance chooses what a visible signature shows (default: name, date and labels; reason, location and distinguished_name are off unless turned on). labels gives the words of its labels, e.g. in another language (default English: \"Digitally signed by\", \"DN: \", \"Reason: \", \"Location: \", \"Date: \"; include the separator); each must be in Western European characters (Windows-1252), the only ones the signature's font draws, or the call is refused naming the label and character. PAdES B-B, SHA-256 (SHA-384 for P-384 keys).") .cmd("sign.digital") .with(schema( json!({ @@ -717,6 +717,31 @@ pub fn tools() -> Vec { "minProperties": 1, "maxProperties": 1, }, + "appearance": { + "type": "object", + "description": "What a visible signature shows; items not given keep their default.", + "properties": { + "name": { "type": "boolean", "description": "Default true." }, + "date": { "type": "boolean", "description": "Default true." }, + "reason": { "type": "boolean", "description": "Default false." }, + "location": { "type": "boolean", "description": "Default false." }, + "distinguished_name": { "type": "boolean", "description": "Default false." }, + "labels": { "type": "boolean", "description": "Default true." }, + }, + "additionalProperties": false, + }, + "labels": { + "type": "object", + "description": "The words of a visible signature's labels (at most 200 characters each, Western European characters only); labels not given stay English.", + "properties": { + "signed_by": { "type": "string", "description": "Above the name. Default \"Digitally signed by\"." }, + "distinguished_name": { "type": "string", "description": "Before the certificate's subject. Default \"DN: \"." }, + "reason": { "type": "string", "description": "Default \"Reason: \"." }, + "location": { "type": "string", "description": "Default \"Location: \"." }, + "date": { "type": "string", "description": "Default \"Date: \"." }, + }, + "additionalProperties": false, + }, "out": { "type": "string", "description": "Where to save the signed document." }, }), &["doc", "id", "out"], diff --git a/crates/automation/tests/automation.rs b/crates/automation/tests/automation.rs index 8ff0a754f2..65b8265e99 100644 --- a/crates/automation/tests/automation.rs +++ b/crates/automation/tests/automation.rs @@ -4194,6 +4194,89 @@ fn digital_ids_signing_and_validation_through_tools() { assert_eq!(ok(&mut a, "sign_list", json!({ "doc": doc }))["signatures"][0]["status"], "unknown"); } +/// The decoded text of every visible signature appearance (Helvetica forms) in a signed file. +fn signature_appearances(path: &Path) -> String { + use pdfcraft_cos::{Document, ObjRef, Object}; + let doc = Document::open(std::sync::Arc::new(std::fs::read(path).unwrap())).unwrap(); + let mut text = String::new(); + for num in doc.object_numbers() { + if let Object::Stream(s) = &*doc.get(ObjRef { num, generation: 0 }) + && s.dict.name(b"Subtype") == Some(b"Form") + { + let content = String::from_utf8_lossy(&s.decoded().unwrap()).into_owned(); + if content.contains("/Helv") { + text.push_str(&content); + } + } + } + text +} + +/// `sign_document` takes the words of a visible signature's labels (say, in German) and which +/// items it shows, as the interface does. A label the signature's font can't draw is refused, +/// naming the label, the character and what is allowed, before anything is written. +#[test] +fn signing_with_translated_labels_through_tools() { + let dir = workdir("signing-labels"); + let mut a = auto(&dir); + ok(&mut a, "sign_id_create", json!({ "name": "Ada Lovelace", "key": "p256", "password": "secret1", "path": "ada.p12" })); + let open = |a: &mut Automation| ok(a, "doc_open", json!({ "path": "a.pdf" }))["doc"].as_u64().unwrap(); + let doc = open(&mut a); + let args = |doc: u64, extra: Value, out: &str| { + let mut args = json!({ "doc": doc, "id": "ada.p12", "password": "secret1", "page": 1, "rect": [10, 150, 190, 290], "reason": "Genehmigt", "location": "Berlin", "out": out }); + for (k, v) in extra.as_object().unwrap() { + args[k] = v.clone(); + } + args + }; + for (extra, why, says) in [ + ( + json!({ "labels": { "signed_by": "数字签名者" } }), + "a label Helvetica can't draw", + "labels.signed_by: the standard fonts can't draw \"数\" (U+6570)", + ), + (json!({ "labels": { "reason": "Причина: " } }), "even one this signature doesn't show", "labels.reason"), + (json!({ "labels": { "signer": "Von" } }), "an unknown label", "unknown label \"signer\""), + (json!({ "labels": { "date": 1 } }), "not a string", "labels.date must be a string"), + (json!({ "labels": "Datum" }), "not an object", "labels must be an object"), + (json!({ "labels": { "date": "x".repeat(201) } }), "too long", "longer than 200"), + (json!({ "appearance": { "reason": "yes" } }), "not true or false", "appearance.reason must be true or false"), + (json!({ "appearance": { "logo": true } }), "an unknown item", "unknown appearance item"), + (json!({ "rect": null, "labels": { "date": "Datum: " } }), "labels for an invisible signature", "needs a visible signature"), + (json!({ "rect": null, "appearance": { "reason": true } }), "an appearance for an invisible one", "needs a visible signature"), + ] { + let r = a.call("sign_document", &args(doc, extra, "bad.pdf")); + assert!(matches!(&r, Err(ToolError::InvalidArgs(m)) if m.contains(says)), "{why}: {r:?}"); + } + let r = a.call("sign_document", &args(doc, json!({ "labels": { "signed_by": "数字签名者" } }), "bad.pdf")); + assert!(matches!(&r, Err(ToolError::InvalidArgs(m)) if m.contains("signed_by label") && m.contains("Western European")), "{r:?}"); + assert!(!dir.join("bad.pdf").exists(), "nothing is written for a refused call"); + + // German labels, with the reason and the location shown: stamped as given. + let german = json!({ + "labels": { "signed_by": "Digital signiert von", "reason": "Grund: ", "location": "Ort: ", "date": "Datum: " }, + "appearance": { "reason": true, "location": true }, + }); + let r = ok(&mut a, "sign_document", args(doc, german, "de.pdf")); + assert_eq!(r["signature"]["reason"], "Genehmigt"); + let stamped = signature_appearances(&dir.join("de.pdf")); + // Each line starts with its label (a long one wraps after it). + for word in ["(Digital", "(Grund:", "Genehmigt", "(Ort:", "Berlin", "(Datum:"] { + assert!(stamped.contains(word), "{word}: {stamped}"); + } + assert!(!stamped.contains("Reason") && !stamped.contains("Date:"), "{stamped}"); + + // Labels omitted keep the English; the labels turned off show the values alone. + let doc = open(&mut a); + ok(&mut a, "sign_document", args(doc, json!({ "labels": { "date": "Datum: " }, "appearance": { "reason": true } }), "mixed.pdf")); + let stamped = signature_appearances(&dir.join("mixed.pdf")); + assert!(stamped.contains("(Reason:") && stamped.contains("(Datum:") && !stamped.contains("Berlin"), "{stamped}"); + let doc = open(&mut a); + ok(&mut a, "sign_document", args(doc, json!({ "appearance": { "reason": true, "labels": false } }), "bare.pdf")); + let stamped = signature_appearances(&dir.join("bare.pdf")); + assert!(stamped.contains("(Genehmigt)") && !stamped.contains("Reason") && !stamped.contains("Date"), "{stamped}"); +} + #[test] fn optimizing_through_tools() { let dir = workdir("optimize"); diff --git a/crates/engine/src/lib.rs b/crates/engine/src/lib.rs index 42ebe340b7..03b3e5a92d 100644 --- a/crates/engine/src/lib.rs +++ b/crates/engine/src/lib.rs @@ -84,7 +84,7 @@ pub enum ImageEdit { }, Delete, } -pub use pdfcraft_fonts::{MAX_SIGNATURE_CHARS, ScriptOutline, script_missing, script_outline}; +pub use pdfcraft_fonts::{MAX_SIGNATURE_CHARS, ScriptOutline, script_missing, script_outline, undrawable_message}; /// Fill & Sign: `text` in the script font as a typed signature, its left edge at `at` (user /// space, vertically centred) and `height` points tall. `None` for text with no outlines. diff --git a/crates/sign/src/pdf.rs b/crates/sign/src/pdf.rs index e0ad1702b0..69e5f48082 100644 --- a/crates/sign/src/pdf.rs +++ b/crates/sign/src/pdf.rs @@ -1607,6 +1607,27 @@ impl Default for AppearanceLabels { } } +impl AppearanceLabels { + /// Each label with its field name (`signed_by`, `distinguished_name`, `reason`, `location`, + /// `date`), in the order the visible signature shows them. + pub fn fields(&self) -> [(&'static str, &str); 5] { + [ + ("signed_by", self.signed_by.as_str()), + ("distinguished_name", self.distinguished_name.as_str()), + ("reason", self.reason.as_str()), + ("location", self.location.as_str()), + ("date", self.date.as_str()), + ] + } + + /// The first label, with its field name, that has a character the visible signature's font + /// can't draw, and that character. Signing refuses such a label when it is shown, so an + /// interface checks its translated labels here and falls back to [`Default`] if one fails. + pub fn first_undrawable(&self) -> Option<(&'static str, char)> { + self.fields().into_iter().find_map(|(field, text)| pdfcraft_fonts::first_undrawable(text).map(|c| (field, c))) + } +} + #[derive(Clone, Debug, Default)] pub struct SignOptions { /// Sign this existing, unsigned signature field. @@ -1950,6 +1971,7 @@ pub fn display_date(pdf: &str) -> String { /// Helvetica (the signer's name, the certificate's subject, the reason or the location) has a /// character Helvetica can't draw: signing invisibly, or leaving that part out, still works. A /// graphic doesn't change this: the name it replaces on the left is still among the details. +/// A label that is drawn ([`AppearanceLabels`], the caller's words) is refused the same way. fn appearance(doc: &mut Document, rect: [f64; 4], name: &str, cert: &Certificate, opts: &SignOptions) -> Result { use pdfcraft_fonts::{first_undrawable, helvetica_width, literal, undrawable_message, win_ansi, wrap}; let (w, h) = ((rect[2] - rect[0]).max(0.0), (rect[3] - rect[1]).max(0.0)); @@ -1970,6 +1992,17 @@ fn appearance(doc: &mut Document, rect: [f64; 4], name: &str, cert: &Certificate ))); } } + // The labels are the caller's words (a translation), checked as the details are: only + // those that are drawn, as each is drawn only beside its detail. + let has = |v: Option<&str>| v.is_some_and(|v| !v.is_empty()); + let drawn = [a.name, a.distinguished_name, a.reason && has(opts.reason.as_deref()), a.location && has(opts.location.as_deref()), a.date]; + let mut labels = opts.appearance_labels.fields().into_iter().zip(drawn).filter(|&(_, on)| a.labels && on); + if let Some((field, c)) = labels.find_map(|((field, text), _)| first_undrawable(text).map(|c| (field, c))) { + return Err(SignError::Unsupported(format!( + "{}; pass that label in Western European characters, or leave the labels out of the visible signature", + undrawable_message(&format!("the {field} label of the visible signature"), c) + ))); + } } let mut lines: Vec = Vec::new(); let label = |l: &str, v: &str| if a.labels { format!("{l}{v}") } else { v.to_string() }; diff --git a/crates/sign/tests/pdf.rs b/crates/sign/tests/pdf.rs index 1bff181203..505051f45d 100644 --- a/crates/sign/tests/pdf.rs +++ b/crates/sign/tests/pdf.rs @@ -1852,6 +1852,58 @@ fn visible_appearance_text_helvetica_cannot_draw_is_refused() { assert_eq!(s.reason.as_deref(), Some("同意此文件")); } +/// The labels are the caller's words (a translation), so they are checked as the details are: a +/// label Helvetica can't draw is refused with the label and the character, never drawn as `?`. +/// Only labels that are drawn count: one whose detail is hidden, or empty, or with the labels +/// turned off or the signature invisible, signs. Labels it can draw are what the stamp says. +#[test] +fn appearance_labels_helvetica_cannot_draw_are_refused() { + use pdfcraft_sign::{Appearance, AppearanceLabels}; + let id = pkcs12::open(&data("ec-p256.p12"), "test").unwrap(); + assert_eq!(AppearanceLabels::default().first_undrawable(), None, "the English labels are drawable"); + let chinese = AppearanceLabels { signed_by: "数字签名者".into(), reason: "原因:".into(), ..AppearanceLabels::default() }; + assert_eq!(chinese.first_undrawable(), Some(("signed_by", '数'))); + let sign = |labels: AppearanceLabels, appearance: Appearance, o: SignOptions| { + pdfcraft_sign::sign(&open(&fixture()), &id, &SignOptions { appearance_labels: labels, appearance, ..o }) + }; + + // The name is shown with its label (the default appearance): refused, saying which and what. + let err = sign(chinese.clone(), Appearance::default(), opts()).unwrap_err(); + let m = err.to_string(); + assert!(matches!(err, SignError::Unsupported(_)), "{err:?}"); + assert!(m.contains("\"数\" (U+6570)") && m.contains("the signed_by label") && m.contains("leave the labels out"), "{m}"); + let russian_date = AppearanceLabels { date: "Дата: ".into(), ..AppearanceLabels::default() }; + let m = sign(russian_date.clone(), Appearance::default(), opts()).unwrap_err().to_string(); + assert!(m.contains("the date label") && m.contains("U+0414"), "{m}"); + let reason_only = AppearanceLabels { reason: "原因:".into(), ..AppearanceLabels::default() }; + let shown = Appearance { reason: true, ..Appearance::default() }; + assert!(sign(reason_only.clone(), shown.clone(), opts()).unwrap_err().to_string().contains("the reason label")); + + // Not drawn: the reason hidden, or empty; the labels off; the signature invisible. + sign(reason_only.clone(), Appearance::default(), opts()).unwrap(); + sign(reason_only.clone(), shown.clone(), SignOptions { reason: None, ..opts() }).unwrap(); + sign(reason_only, shown, SignOptions { reason: Some(String::new()), ..opts() }).unwrap(); + sign(chinese.clone(), Appearance { labels: false, ..Appearance::default() }, opts()).unwrap(); + sign(russian_date, Appearance { date: false, ..Appearance::default() }, opts()).unwrap(); + sign(chinese, Appearance::default(), SignOptions { rect: None, ..opts() }).unwrap(); + + // Labels Helvetica can draw are stamped as given, in place of the English ones. + let german = AppearanceLabels { + signed_by: "Digital signiert von".into(), + distinguished_name: "DN: ".into(), + reason: "Grund: ".into(), + location: "Ort: ".into(), + date: "Datum: ".into(), + }; + let all = Appearance { reason: true, location: true, ..Appearance::default() }; + let signed = sign(german, all, opts()).unwrap(); + let (content, _) = appearance_of(&signed, "Signature1"); + for word in ["(Digital", "(Grund:", "(Ort:", "(Datum:"] { + assert!(content.contains(word), "{word}: {content}"); + } + assert!(!content.contains("Reason") && !content.contains("Date:") && !content.contains('?'), "{content}"); +} + /// Every sentence this crate says about a signature is listed in `WORDINGS`, and every sentence on /// that list is one it still says: the details are written as `Detail::plain`/`Detail::of`, the /// sentences the modules that read a signature word for themselves are pushed as notes and quirks, diff --git a/crates/ui-egui/src/i18n/ar.tsv b/crates/ui-egui/src/i18n/ar.tsv index 093757cdc9..f3964f2e50 100644 --- a/crates/ui-egui/src/i18n/ar.tsv +++ b/crates/ui-egui/src/i18n/ar.tsv @@ -855,6 +855,12 @@ Windows store · مخزن Windows · Issued by: صادرة عن: , Expires: ، تنتهي: + Digitally signed by موقع رقميا من + DN: DN: + Reason: السبب: + Location: الموقع: + Date: التاريخ: + The labels are in English: the signature's font can't draw them in this language. التسميات بالإنجليزية: لا يستطيع خط التوقيع رسمها بهذه اللغة. No digital IDs yet. لا توجد معرّفات رقمية بعد. Configure New Digital ID إعداد معرّف رقمي جديد Remove "{name}" from this list? The file itself is not deleted. إزالة "{name}" من هذه القائمة؟ لن يُحذف الملف نفسه. diff --git a/crates/ui-egui/src/i18n/bg.tsv b/crates/ui-egui/src/i18n/bg.tsv index e75a7bc087..08d0d2cab4 100644 --- a/crates/ui-egui/src/i18n/bg.tsv +++ b/crates/ui-egui/src/i18n/bg.tsv @@ -530,6 +530,12 @@ Windows store · Хранилище на Windows · Issued by: Издаден от: , Expires: , валиден до: + Digitally signed by Цифрово подписано от + DN: DN: + Reason: Причина: + Location: Местоположение: + Date: Дата: + The labels are in English: the signature's font can't draw them in this language. Етикетите са на английски: шрифтът на подписа не може да ги изобрази на този език. No digital IDs yet. Все още няма цифрови идентификатори. Configure New Digital ID Конфигурирай нов цифров идентификатор Remove "{name}" from this list? The file itself is not deleted. Да се премахне ли „{name}“ от този списък? Самият файл няма да бъде изтрит. diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv index 6037cf2816..fd07b2f9f4 100644 --- a/crates/ui-egui/src/i18n/cs.tsv +++ b/crates/ui-egui/src/i18n/cs.tsv @@ -929,6 +929,12 @@ key Space Mezerník Windows store · Úložiště Windows · Issued by: Vydal: , Expires: , platnost do: + Digitally signed by Digitálně podepsal + DN: DN: + Reason: Důvod: + Location: Místo: + Date: Datum: + The labels are in English: the signature's font can't draw them in this language. Popisky jsou v angličtině: písmo podpisu je v tomto jazyce nedokáže vykreslit. No digital IDs yet. Zatím žádná digitální ID. Configure New Digital ID Nastavit nové digitální ID Remove "{name}" from this list? The file itself is not deleted. Odebrat „{name}“ z tohoto seznamu? Samotný soubor se nesmaže. diff --git a/crates/ui-egui/src/i18n/de.tsv b/crates/ui-egui/src/i18n/de.tsv index ee0a5418cb..2a4f790853 100644 --- a/crates/ui-egui/src/i18n/de.tsv +++ b/crates/ui-egui/src/i18n/de.tsv @@ -833,6 +833,12 @@ Windows store · Windows-Zertifikatspeicher · Issued by: Ausgestellt von: , Expires: , läuft ab: + Digitally signed by Digital signiert von + DN: DN: + Reason: Grund: + Location: Ort: + Date: Datum: + The labels are in English: the signature's font can't draw them in this language. Die Beschriftungen sind auf Englisch: Die Schrift der Signatur kann sie in dieser Sprache nicht darstellen. No digital IDs yet. Noch keine digitalen IDs. Configure New Digital ID Neue digitale ID einrichten Remove "{name}" from this list? The file itself is not deleted. „{name}“ aus dieser Liste entfernen? Die Datei selbst wird nicht gelöscht. diff --git a/crates/ui-egui/src/i18n/el.tsv b/crates/ui-egui/src/i18n/el.tsv index 679c2e2e98..f1d8b87d38 100644 --- a/crates/ui-egui/src/i18n/el.tsv +++ b/crates/ui-egui/src/i18n/el.tsv @@ -464,6 +464,12 @@ Choose the digital ID that you want to use for signing: Επιλέξτε την ψηφιακή ταυτότητα με την οποία θέλετε να υπογράψετε: Issued by: Εκδότης: , Expires: , Λήξη: + Digitally signed by Ψηφιακή υπογραφή από + DN: DN: + Reason: Αιτία: + Location: Τοποθεσία: + Date: Ημερομηνία: + The labels are in English: the signature's font can't draw them in this language. Οι ετικέτες είναι στα αγγλικά: η γραμματοσειρά της υπογραφής δεν μπορεί να τις σχεδιάσει σε αυτή τη γλώσσα. No digital IDs yet. Δεν υπάρχουν ακόμη ψηφιακές ταυτότητες. Configure New Digital ID Ρύθμιση νέας ψηφιακής ταυτότητας Remove "{name}" from this list? The file itself is not deleted. Αφαίρεση του «{name}» από αυτή τη λίστα; Το ίδιο το αρχείο δεν διαγράφεται. diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv index 3ca46ff114..df2feb390f 100644 --- a/crates/ui-egui/src/i18n/es.tsv +++ b/crates/ui-egui/src/i18n/es.tsv @@ -849,6 +849,12 @@ Windows store · Almacén de Windows · Issued by: Emitido por: , Expires: , caduca: + Digitally signed by Firmado digitalmente por + DN: DN: + Reason: Motivo: + Location: Ubicación: + Date: Fecha: + The labels are in English: the signature's font can't draw them in this language. Las etiquetas están en inglés: la fuente de la firma no puede dibujarlas en este idioma. No digital IDs yet. Aún no hay ningún ID digital. Configure New Digital ID Configurar un nuevo ID digital Remove "{name}" from this list? The file itself is not deleted. ¿Quitar «{name}» de esta lista? El archivo en sí no se elimina. diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv index 1408fddf8f..f13e724789 100644 --- a/crates/ui-egui/src/i18n/fr.tsv +++ b/crates/ui-egui/src/i18n/fr.tsv @@ -829,6 +829,12 @@ Windows store · Magasin Windows · Issued by: Émis par : , Expires: , expire : + Digitally signed by Signé numériquement par + DN: DN : + Reason: Motif : + Location: Lieu : + Date: Date : + The labels are in English: the signature's font can't draw them in this language. Les libellés sont en anglais : la police de la signature ne peut pas les afficher dans cette langue. No digital IDs yet. Aucun ID numérique pour l'instant. Configure New Digital ID Configurer un nouvel ID numérique Remove "{name}" from this list? The file itself is not deleted. Retirer « {name} » de cette liste ? Le fichier lui-même n’est pas supprimé. diff --git a/crates/ui-egui/src/i18n/hu.tsv b/crates/ui-egui/src/i18n/hu.tsv index 0ca2ec150a..2b3d942dac 100644 --- a/crates/ui-egui/src/i18n/hu.tsv +++ b/crates/ui-egui/src/i18n/hu.tsv @@ -826,6 +826,12 @@ Windows store · Windows-tároló · Issued by: Kiállította: , Expires: , lejár: + Digitally signed by Digitálisan aláírta + DN: DN: + Reason: Indok: + Location: Hely: + Date: Dátum: + The labels are in English: the signature's font can't draw them in this language. A címkék angolul jelennek meg: az aláírás betűtípusa ezen a nyelven nem tudja megjeleníteni őket. No digital IDs yet. Még nincs digitális azonosító. Configure New Digital ID Új digitális azonosító beállítása Remove "{name}" from this list? The file itself is not deleted. Eltávolítja a(z) „{name}” elemet erről a listáról? Maga a fájl nem törlődik. diff --git a/crates/ui-egui/src/i18n/it.tsv b/crates/ui-egui/src/i18n/it.tsv index d762d9869d..6bed842550 100644 --- a/crates/ui-egui/src/i18n/it.tsv +++ b/crates/ui-egui/src/i18n/it.tsv @@ -852,6 +852,12 @@ Windows store · Archivio Windows · Issued by: Rilasciato da: , Expires: , Scadenza: + Digitally signed by Firmato digitalmente da + DN: DN: + Reason: Motivo: + Location: Luogo: + Date: Data: + The labels are in English: the signature's font can't draw them in this language. Le etichette sono in inglese: il carattere della firma non può disegnarle in questa lingua. No digital IDs yet. Ancora nessun ID digitale. Configure New Digital ID Configura nuovo ID digitale Remove "{name}" from this list? The file itself is not deleted. Rimuovere "{name}" da questo elenco? Il file non viene eliminato. diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv index 4a80d277f7..83d8e64767 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -856,6 +856,12 @@ Windows store · Windows 証明書ストア · Issued by: 発行者: , Expires: 、有効期限: + Digitally signed by デジタル署名者 + DN: DN: + Reason: 理由: + Location: 場所: + Date: 日付: + The labels are in English: the signature's font can't draw them in this language. 署名のフォントではこの言語のラベルを描画できないため、ラベルは英語になります。 No digital IDs yet. デジタル ID はまだありません。 Configure New Digital ID 新しいデジタル ID を設定 Remove "{name}" from this list? The file itself is not deleted. 「{name}」をこの一覧から削除しますか?ファイル自体は削除されません。 diff --git a/crates/ui-egui/src/i18n/kk.tsv b/crates/ui-egui/src/i18n/kk.tsv index 67631f3374..02da45449e 100644 --- a/crates/ui-egui/src/i18n/kk.tsv +++ b/crates/ui-egui/src/i18n/kk.tsv @@ -811,6 +811,12 @@ Windows store · Windows қоймасы · Issued by: Шығарушы: , Expires: , Жарамдылық мерзімі: + Digitally signed by Цифрлық қолтаңба қойған + DN: DN: + Reason: Себебі: + Location: Орны: + Date: Күні: + The labels are in English: the signature's font can't draw them in this language. Белгілер ағылшын тілінде: қолтаңба қаріпі оларды бұл тілде сала алмайды. No digital IDs yet. Әзірге цифрлық ID жоқ. Configure New Digital ID Жаңа цифрлық ID теңшеу Remove "{name}" from this list? The file itself is not deleted. «{name}» осы тізімнен алып тасталсын ба? Файлдың өзі жойылмайды. diff --git a/crates/ui-egui/src/i18n/lt.tsv b/crates/ui-egui/src/i18n/lt.tsv index f67f0bd8c8..e3d0fca16c 100644 --- a/crates/ui-egui/src/i18n/lt.tsv +++ b/crates/ui-egui/src/i18n/lt.tsv @@ -197,6 +197,12 @@ · password-protected · apsaugotas slaptažodžiu (none) (nėra) , Expires: , galiojimo pabaiga: + Digitally signed by Skaitmeniniu parašu pasirašė + DN: DN: + Reason: Priežastis: + Location: Vieta: + Date: Data: + The labels are in English: the signature's font can't draw them in this language. Etiketės rodomos angliškai: parašo šriftas negali jų atvaizduoti šia kalba. 1 difference found Rastas 1 skirtumas 1 file 1 failas 1 file done 1 failas apdorotas diff --git a/crates/ui-egui/src/i18n/mod.rs b/crates/ui-egui/src/i18n/mod.rs index b9a264f216..9257018a7b 100644 --- a/crates/ui-egui/src/i18n/mod.rs +++ b/crates/ui-egui/src/i18n/mod.rs @@ -941,6 +941,26 @@ mod tests { } } + /// Every catalog says the labels of a visible signature, which are looked up by the sign + /// crate's own English words (so the `tl!` scan can't see them), and the note shown when they + /// fall back to English. Each label that runs into its value keeps a separator at its end. + #[test] + fn signature_appearance_labels_are_in_every_catalog() { + let english = pdfcraft_engine::sign::AppearanceLabels::default(); + let note = "The labels are in English: the signature's font can't draw them in this language."; + for language in LANGUAGES.iter().filter(|language| !language.source.is_empty()) { + let lang = Lang::from_code(language.code).expect("registered language"); + assert!(has(lang, note), "{} lacks the labels note", language.code); + for (field, label) in english.fields() { + assert!(has(lang, label), "{} lacks the signature label {field}: {label:?}", language.code); + let said = tr(lang, label); + if field != "signed_by" && !lang.rtl() { + assert!(said.ends_with(' ') || said.ends_with(':'), "{}: the {field} label runs into its value: {said:?}", language.code); + } + } + } + } + /// Japanese translates every `tl!("…")` literal in the UI source, so a new string can't ship in /// English by accident (the same check French, German, Russian and Simplified Chinese have). #[test] @@ -1370,6 +1390,8 @@ mod tests { "ΔLOC", "+Bin", "−Bin", + // The distinguished name of a certificate, abbreviated as in X.500. + "DN: ", ]; let (entries, errors) = parse_entries(cs.0.source, cs.0.plural_forms()); assert!(errors.is_empty(), "{errors:?}"); @@ -1482,6 +1504,8 @@ mod tests { "−Bin", "Model", "Home / End", + // The distinguished name of a certificate, abbreviated as in X.500. + "DN: ", ]; let (entries, errors) = parse_entries(tr_lang.0.source, tr_lang.0.plural_forms()); assert!(errors.is_empty(), "Turkish catalog errors: {errors:?}"); @@ -2934,6 +2958,8 @@ mod tests { "ΔLOC", "+Bin", "−Bin", + // The distinguished name of a certificate, abbreviated as in X.500. + "DN: ", ]; let unchanged: Vec<_> = entries .iter() @@ -3012,6 +3038,8 @@ mod tests { "ΔLOC", "+Bin", "−Bin", + // The distinguished name of a certificate, abbreviated as in X.500. + "DN: ", ]; let unchanged: Vec<_> = entries .iter() diff --git a/crates/ui-egui/src/i18n/pl.tsv b/crates/ui-egui/src/i18n/pl.tsv index dc28911163..339fff515d 100644 --- a/crates/ui-egui/src/i18n/pl.tsv +++ b/crates/ui-egui/src/i18n/pl.tsv @@ -847,6 +847,12 @@ Windows store · Magazyn Windows · Issued by: Wystawca: , Expires: , Wygasa: + Digitally signed by Podpisany cyfrowo przez + DN: DN: + Reason: Powód: + Location: Lokalizacja: + Date: Data: + The labels are in English: the signature's font can't draw them in this language. Etykiety są po angielsku: czcionka podpisu nie potrafi ich wyświetlić w tym języku. No digital IDs yet. Brak identyfikatorów cyfrowych. Configure New Digital ID Skonfiguruj nowy identyfikator cyfrowy Remove "{name}" from this list? The file itself is not deleted. Usunąć „{name}” z tej listy? Sam plik nie zostanie usunięty. diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv index fe507b6b45..f2652f52f4 100644 --- a/crates/ui-egui/src/i18n/pt-br.tsv +++ b/crates/ui-egui/src/i18n/pt-br.tsv @@ -484,6 +484,12 @@ Keychain · Chaveiro · Issued by: Emitido por: , Expires: , Expira em: + Digitally signed by Assinado digitalmente por + DN: DN: + Reason: Motivo: + Location: Local: + Date: Data: + The labels are in English: the signature's font can't draw them in this language. Os rótulos estão em inglês: a fonte da assinatura não consegue desenhá-los neste idioma. No digital IDs yet. Nenhuma ID digital ainda. Configure New Digital ID Configurar nova ID digital Remove "{name}" from this list? The file itself is not deleted. Remover "{name}" desta lista? O arquivo em si não é excluído. diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv index d8d23a3adb..0779819336 100644 --- a/crates/ui-egui/src/i18n/ru.tsv +++ b/crates/ui-egui/src/i18n/ru.tsv @@ -849,6 +849,12 @@ Windows store · Хранилище Windows · Issued by: Выдан: , Expires: , Истекает: + Digitally signed by Цифровая подпись + DN: DN: + Reason: Причина: + Location: Местоположение: + Date: Дата: + The labels are in English: the signature's font can't draw them in this language. Метки на английском: шрифт подписи не может отобразить их на этом языке. No digital IDs yet. Нет цифровых идентификаторов. Configure New Digital ID Настроить новый цифровой идентификатор Remove "{name}" from this list? The file itself is not deleted. Убрать «{name}» из этого списка? Сам файл не удаляется. diff --git a/crates/ui-egui/src/i18n/sl.tsv b/crates/ui-egui/src/i18n/sl.tsv index c0bb304593..5bb21ea6f3 100644 --- a/crates/ui-egui/src/i18n/sl.tsv +++ b/crates/ui-egui/src/i18n/sl.tsv @@ -29,6 +29,12 @@ +Bin +Bin +LOC +LOC , Expires: , poteče: + Digitally signed by Digitalno podpisal + DN: DN: + Reason: Razlog: + Location: Lokacija: + Date: Datum: + The labels are in English: the signature's font can't draw them in this language. Oznake so v angleščini: pisava podpisa jih v tem jeziku ne more prikazati. 1 difference found 1 razlika najdena 1 file 1 datoteka 1 file done 1 datoteka dokončana diff --git a/crates/ui-egui/src/i18n/te.tsv b/crates/ui-egui/src/i18n/te.tsv index 5bd6fa1506..76273e9207 100644 --- a/crates/ui-egui/src/i18n/te.tsv +++ b/crates/ui-egui/src/i18n/te.tsv @@ -828,6 +828,12 @@ comment menu Edit సవరించు Keychain · Keychain · Issued by: జారీ చేసినవారు: , Expires: , గడువు: + Digitally signed by డిజిటల్ సంతకం చేసినవారు + DN: DN: + Reason: కారణం: + Location: స్థానం: + Date: తేదీ: + The labels are in English: the signature's font can't draw them in this language. లేబుళ్లు ఆంగ్లంలో ఉన్నాయి: సంతకం ఫాంట్ ఈ భాషలో వాటిని చూపించలేదు. No digital IDs yet. ఇంకా డిజిటల్ ఐడీలు ఏవీ లేవు. Configure New Digital ID కొత్త డిజిటల్ ఐడీని కాన్ఫిగర్ చేయి Remove "{name}" from this list? The file itself is not deleted. ఈ జాబితా నుండి "{name}"ని తీసివేయాలా? ఫైల్ తొలగించబడదు. diff --git a/crates/ui-egui/src/i18n/tr.tsv b/crates/ui-egui/src/i18n/tr.tsv index b400033d0b..05d6015d03 100644 --- a/crates/ui-egui/src/i18n/tr.tsv +++ b/crates/ui-egui/src/i18n/tr.tsv @@ -810,6 +810,12 @@ Windows store · Windows depolama alanı · Issued by: Veren: , Expires: , Son kullanma tarihi: + Digitally signed by Dijital olarak imzalayan + DN: DN: + Reason: Neden: + Location: Konum: + Date: Tarih: + The labels are in English: the signature's font can't draw them in this language. Etiketler İngilizce: imzanın yazı tipi bu dilde onları çizemiyor. No digital IDs yet. Henüz dijital kimlik yok. Configure New Digital ID Yeni Dijital Kimlik Yapılandır Remove "{name}" from this list? The file itself is not deleted. "{name}" bu listeden kaldırılsın mı? Dosyanın kendisi silinmez. diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv index 5dbbf01a61..53613bd179 100644 --- a/crates/ui-egui/src/i18n/uk.tsv +++ b/crates/ui-egui/src/i18n/uk.tsv @@ -853,6 +853,12 @@ Windows store · Сховище Windows · Issued by: Видавець: , Expires: , Дійсний до: + Digitally signed by Цифровий підпис + DN: DN: + Reason: Причина: + Location: Місце: + Date: Дата: + The labels are in English: the signature's font can't draw them in this language. Мітки англійською: шрифт підпису не може відобразити їх цією мовою. No digital IDs yet. Ще немає цифрових посвідчень. Configure New Digital ID Налаштувати нове цифрове посвідчення Remove "{name}" from this list? The file itself is not deleted. Прибрати «{name}» з цього списку? Сам файл не видаляється. diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv index c24a7fea7f..ea9c2214c8 100644 --- a/crates/ui-egui/src/i18n/zh-hans.tsv +++ b/crates/ui-egui/src/i18n/zh-hans.tsv @@ -537,6 +537,12 @@ Windows store · Windows 证书存储 · Issued by: 颁发者: , Expires: ,有效期至: + Digitally signed by 数字签名者 + DN: DN: + Reason: 原因: + Location: 位置: + Date: 日期: + The labels are in English: the signature's font can't draw them in this language. 签名所用的字体无法显示此语言的标签,因此标签使用英文。 No digital IDs yet. 尚无数字 ID。 Configure New Digital ID 配置新数字 ID Remove "{name}" from this list? The file itself is not deleted. 从此列表中移除“{name}”?文件本身不会被删除。 diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv index 6aa5270ab3..e019740a33 100644 --- a/crates/ui-egui/src/i18n/zh-hant.tsv +++ b/crates/ui-egui/src/i18n/zh-hant.tsv @@ -489,6 +489,12 @@ Windows store · Windows 憑證存放區 · Issued by: 頒發者: , Expires: ,有效期至: + Digitally signed by 數位簽署者 + DN: DN: + Reason: 原因: + Location: 位置: + Date: 日期: + The labels are in English: the signature's font can't draw them in this language. 簽名使用的字型無法顯示此語言的標籤,因此標籤以英文顯示。 No digital IDs yet. 尚無數位 ID。 Configure New Digital ID 配置新數位 ID Remove "{name}" from this list? The file itself is not deleted. 從此清單中移除「{name}」?檔案本身不會被刪除。 diff --git a/crates/ui-egui/src/sign_ui.rs b/crates/ui-egui/src/sign_ui.rs index 14aa45160e..98a511de15 100644 --- a/crates/ui-egui/src/sign_ui.rs +++ b/crates/ui-egui/src/sign_ui.rs @@ -5,7 +5,7 @@ use std::path::PathBuf; use egui::{Align, Color32, CornerRadius, Layout, Pos2, Rect, Stroke, vec2}; -use pdfcraft_engine::sign::{self, Appearance, Certificate, DigitalId, Modification, Name, PrivateKey}; +use pdfcraft_engine::sign::{self, Appearance, AppearanceLabels, Certificate, DigitalId, Modification, Name, PrivateKey}; use pdfcraft_engine::{SignOptions, SignatureImage, SignatureInfo, SignatureStatus}; use crate::canvas::{DocView, PageXform}; @@ -434,6 +434,8 @@ impl PdfCraftApp { location: some(&d.location), certify: d.certify, appearance: d.appearance.clone(), + // The labels the preview shows: in the interface language, or all in English. + appearance_labels: appearance_labels(crate::i18n::current()).0, graphic, ..SignOptions::default() }; @@ -816,12 +818,67 @@ fn graphic_of(signature: Option<&SavedSig>, g: &SignGraphic) -> Option } } -/// The signature as it will look: the graphic (or the name) on the left, the details beside it. +/// The words of a visible signature's labels in `lang`, and whether they fell back to English. +/// +/// The labels are stamped into the document, so they are all in `lang` or all in English, never a +/// mix. English when `lang` is English; when it is written right to left (its catalog is kept in +/// display order, not the order a PDF stores text in); when one label has no translation; or when +/// the signature's font can't draw one (Chinese, Japanese, Cyrillic, Greek in Helvetica), which +/// signing would refuse. The English words are the sign crate's own, so they can't drift apart. +pub(crate) fn appearance_labels(lang: crate::i18n::Lang) -> (AppearanceLabels, bool) { + let english = AppearanceLabels::default(); + if lang == crate::i18n::Lang::EN { + return (english, false); + } + let said = |s: &str| (!lang.rtl() && crate::i18n::has(lang, s)).then(|| crate::i18n::tr(lang, s).to_string()); + let translated = (|| { + Some(AppearanceLabels { + signed_by: said(&english.signed_by)?, + distinguished_name: said(&english.distinguished_name)?, + reason: said(&english.reason)?, + location: said(&english.location)?, + date: said(&english.date)?, + }) + })(); + match translated.filter(|l| l.first_undrawable().is_none()) { + Some(labels) => (labels, false), + None => (english, true), + } +} + +/// The detail lines of a visible signature, as `sign::pdf` stamps them (the distinguished name +/// aside, which the dialog doesn't know before the ID is opened): the label above the name, then +/// the reason, location and date, each after its label. `reason` and `location` are as typed. +fn detail_lines(a: &Appearance, labels: &AppearanceLabels, name: &str, reason: &str, location: &str, date: &str) -> Vec { + let label = |l: &str, v: &str| if a.labels { format!("{l}{v}") } else { v.to_string() }; + let mut lines = Vec::new(); + if a.name { + if a.labels { + lines.push(labels.signed_by.clone()); + } + lines.push(name.to_string()); + } + if a.reason && !reason.trim().is_empty() { + lines.push(label(&labels.reason, reason.trim())); + } + if a.location && !location.trim().is_empty() { + lines.push(label(&labels.location, location.trim())); + } + if a.date { + lines.push(label(&labels.date, date)); + } + lines.retain(|l| !l.is_empty()); + lines +} + +/// The signature as it will look: the graphic (or the name) on the left, the details beside it +/// (`lines`, from [`detail_lines`] with the labels signing will stamp). fn preview( ui: &mut egui::Ui, t: &Tokens, name: &str, - d: &SignDraft, + a: &Appearance, + lines: &[String], graphic: Option<&SavedSig>, cache: &mut Option<(SavedSig, egui::TextureHandle)>, ) { @@ -829,20 +886,6 @@ fn preview( let bg = if t.dark() { Color32::from_gray(0xF4) } else { Color32::WHITE }; ui.painter().rect(rect, CornerRadius::same(4), bg, Stroke::new(1.0, t.border), egui::StrokeKind::Inside); let inner = rect.shrink(10.0); - let a = &d.appearance; - let mut lines = Vec::new(); - if a.name { - lines.push(if a.labels { format!("Digitally signed by {name}") } else { name.to_string() }); - } - if a.reason && !d.reason.trim().is_empty() { - lines.push(format!("{}{}", if a.labels { "Reason: " } else { "" }, d.reason.trim())); - } - if a.location && !d.location.trim().is_empty() { - lines.push(format!("{}{}", if a.labels { "Location: " } else { "" }, d.location.trim())); - } - if a.date { - lines.push(format!("{}{}", if a.labels { "Date: " } else { "" }, "(the signing time)")); - } // As in the signature itself: a graphic with no details beside it fills the box. let alone = graphic.is_some() && lines.is_empty(); let half = Rect::from_min_size(inner.min, vec2(inner.width() * 0.5 - 6.0, inner.height())); @@ -859,6 +902,8 @@ fn preview( fn sign_as(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens) -> bool { let ids = app.digital_ids.clone(); + // The time as the signature will show it (signing takes the session clock then). + let date = sign::pdf::display_date(&app.session.signing_date()); let Some(d) = app.sign_draft.as_mut() else { return true }; let Some(entry) = d.selected.and_then(|i| ids.get(i)).cloned() else { d.step = SignStep::Choose; @@ -879,7 +924,16 @@ fn sign_as(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens) -> bool { if d.rect.is_some() || d.field.is_some() { ui.label(egui::RichText::new(tl!("Appearance")).font(theme::semibold(12.5))); let graphic = graphic_of(app.signature.as_ref(), &d.graphic); - preview(ui, t, &entry.name, d, graphic.as_ref(), &mut app.sign_graphic_preview); + let (labels, in_english) = appearance_labels(crate::i18n::current()); + let lines = detail_lines(&d.appearance, &labels, &entry.name, &d.reason, &d.location, &date); + preview(ui, t, &entry.name, &d.appearance, &lines, graphic.as_ref(), &mut app.sign_graphic_preview); + if in_english && d.appearance.labels { + ui.label( + egui::RichText::new(tl!("The labels are in English: the signature's font can't draw them in this language.")) + .small() + .color(t.text_muted), + ); + } ui.horizontal_wrapped(|ui| { let a = &mut d.appearance; ui.add(widgets::checkbox(&mut a.name, tl!("Name"))); @@ -1456,4 +1510,46 @@ mod tests { assert!(fresh.digital_ids.is_empty(), "the removal survives a restart: {:?}", fresh.digital_ids); let _ = std::fs::remove_dir_all(&dir); } + + /// The labels a signature is stamped with are all in the interface language, or all in + /// English when the signature's font can't draw them there (or the language is right to + /// left): never a mix, and never a label signing would refuse. + #[test] + fn appearance_labels_are_translated_or_all_english() { + use crate::i18n::{Lang, tr}; + let english = AppearanceLabels::default(); + assert_eq!(appearance_labels(Lang::EN), (english.clone(), false)); + for code in ["de", "es", "fr", "it", "pt-br", "pl", "tr", "sl", "hu"] { + let lang = Lang::from_code(code).unwrap(); + let (labels, in_english) = appearance_labels(lang); + assert!(!in_english, "{code}: {labels:?}"); + for ((field, ours), (_, source)) in labels.fields().into_iter().zip(english.fields()) { + assert_eq!(ours, tr(lang, source), "{code}: {field}"); + } + } + let (de, _) = appearance_labels(Lang::from_code("de").unwrap()); + assert_eq!((de.signed_by.as_str(), de.reason.as_str()), ("Digital signiert von", "Grund: ")); + for code in ["zh-hans", "zh-hant", "ja", "ru", "uk", "bg", "kk", "el", "te", "cs", "lt", "ar"] { + assert_eq!(appearance_labels(Lang::from_code(code).unwrap()), (english.clone(), true), "{code}"); + } + for lang in Lang::all() { + let (labels, in_english) = appearance_labels(lang); + assert_eq!(labels.first_undrawable(), None, "{}: signing would refuse {labels:?}", lang.code()); + assert_eq!(in_english, lang != Lang::EN && labels == english, "{}", lang.code()); + } + } + + /// The preview's details are the stamp's: the label above the name, then each detail after + /// its label, in the words given, and only the values when the labels are off. + #[test] + fn preview_lines_use_the_labels_signing_stamps() { + let (de, _) = appearance_labels(crate::i18n::Lang::from_code("de").unwrap()); + let all = Appearance { reason: true, location: true, ..Appearance::default() }; + let lines = detail_lines(&all, &de, "Ada", " Genehmigt ", "Berlin", "2026.10.14 09:30:00 Z"); + assert_eq!(lines, ["Digital signiert von", "Ada", "Grund: Genehmigt", "Ort: Berlin", "Datum: 2026.10.14 09:30:00 Z"]); + let bare = Appearance { labels: false, ..all.clone() }; + assert_eq!(detail_lines(&bare, &de, "Ada", "Genehmigt", "", "D"), ["Ada", "Genehmigt", "D"]); + let nothing = Appearance { name: false, date: false, ..all }; + assert!(detail_lines(¬hing, &de, "Ada", " ", "", "D").is_empty()); + } } diff --git a/crates/ui-egui/tests/signing.rs b/crates/ui-egui/tests/signing.rs index a5b5cf513c..f783c5cd18 100644 --- a/crates/ui-egui/tests/signing.rs +++ b/crates/ui-egui/tests/signing.rs @@ -315,6 +315,60 @@ fn the_fill_and_sign_signature_or_an_image_shows_in_a_digital_signature() { assert_eq!(fresh.sign_graphic, SignGraphic::Name); } +/// The decoded text of every visible signature appearance (Helvetica forms) in a signed file. +fn stamped(path: &std::path::Path) -> String { + use pdfcraft_cos::{Document, ObjRef, Object}; + let doc = Document::open(std::sync::Arc::new(std::fs::read(path).unwrap())).unwrap(); + let mut text = String::new(); + for num in doc.object_numbers() { + if let Object::Stream(s) = &*doc.get(ObjRef { num, generation: 0 }) + && s.dict.name(b"Subtype") == Some(b"Form") + { + let content = String::from_utf8_lossy(&s.decoded().unwrap()).into_owned(); + if content.contains("/Helv") { + text.push_str(&content); + } + } + } + text +} + +/// A signature signed in German is stamped with German labels. In Simplified Chinese, whose +/// labels the signature's font can't draw, it is stamped with English ones and the dialog says +/// so under the preview: signing neither fails nor writes `?` for them. +#[test] +fn the_signature_labels_follow_the_interface_language() { + use pdfcraft_ui_egui::i18n::{Lang, tr}; + let p12 = concat!(env!("CARGO_MANIFEST_DIR"), "/../sign/tests/data/ec-p256.p12"); + let cert = pdfcraft_engine::sign::pkcs12::open(&std::fs::read(p12).unwrap(), "test").unwrap().certificate; + let note = "The labels are in English: the signature's font can't draw them in this language."; + for (code, labels, in_english) in [("de", ["(Digital", "(Grund:", "(Datum:"], false), ("zh-hans", ["(Digitally", "(Reason:", "(Date:"], true)] { + let dir = dir().join(code); + std::fs::create_dir_all(&dir).unwrap(); + let mut h = harness(dir.clone()); + let lang = Lang::from_code(code).unwrap(); + h.state_mut().set_option("language", code).unwrap(); + h.state_mut().start_signing(0, Some([20.0, 110.0, 280.0, 180.0]), None, None); + let entry = h.state_mut().add_digital_id(p12, &cert); + { + let d = h.state_mut().sign_draft.as_mut().unwrap(); + (d.selected, d.step, d.password, d.reason) = (Some(entry), SignStep::SignAs, "test".into(), "OK".into()); + d.appearance.reason = true; + } + h.run_steps(3); + assert_eq!(h.query_all_by_label(tr(lang, note)).count(), usize::from(in_english), "{code}: the note under the preview"); + field(&h, tr(lang, "Sign")).click(); + h.run_steps(6); + assert_eq!(h.state().sign_draft.as_ref().and_then(|d| d.error.clone()), None, "{code}"); + let content = stamped(&dir.join("contract_signed.pdf")); + for label in labels { + assert!(content.contains(label), "{code}: {label}: {content}"); + } + assert!(!content.contains('?'), "{code}: {content}"); + assert_eq!(content.contains("Digitally") || content.contains("Reason"), in_english, "{code}: {content}"); + } +} + #[test] fn os_store_identities_are_not_persisted() { let mut app = PdfCraftApp::new(); diff --git a/parity/acrobat-features.toml b/parity/acrobat-features.toml index d2da67304b..a3e4cbaef0 100644 --- a/parity/acrobat-features.toml +++ b/parity/acrobat-features.toml @@ -5998,8 +5998,8 @@ milestone = "M9" status = "partial" commands = ["sign.digital"] tools = ["sign_document"] -evidence = ["crates/ui-egui/tests/signing.rs::drawing_a_signature_creating_an_id_signing_and_trusting", "crates/ui-egui/tests/signing.rs::the_fill_and_sign_signature_or_an_image_shows_in_a_digital_signature", "crates/sign/tests/pdf.rs::visible_signatures_show_a_drawn_typed_or_image_graphic", "crates/sign/tests/pdf.rs::malformed_signature_graphics_are_refused_not_drawn", "crates/sign/tests/pdf.rs::an_encrypted_documents_signature_graphic_is_encrypted_with_it", "crates/automation/tests/automation.rs::signing_with_an_image_typed_or_drawn_graphic_through_tools"] -notes = "text items (name, date, reason, location, DN, labels), and a graphic where the large name would be: the saved Fill & Sign signature (drawn, typed or image) or an imported PNG/JPEG (white paper made transparent, as in Fill & Sign), remembered for the next signing; alone it fills the box. Missing: named saved appearances (one remembered choice only), a PDF page as the graphic, the logo/watermark background, drawing a new signature inside the dialog" +evidence = ["crates/ui-egui/tests/signing.rs::drawing_a_signature_creating_an_id_signing_and_trusting", "crates/ui-egui/tests/signing.rs::the_fill_and_sign_signature_or_an_image_shows_in_a_digital_signature", "crates/sign/tests/pdf.rs::visible_signatures_show_a_drawn_typed_or_image_graphic", "crates/sign/tests/pdf.rs::malformed_signature_graphics_are_refused_not_drawn", "crates/sign/tests/pdf.rs::an_encrypted_documents_signature_graphic_is_encrypted_with_it", "crates/automation/tests/automation.rs::signing_with_an_image_typed_or_drawn_graphic_through_tools", "crates/sign/tests/pdf.rs::appearance_labels_helvetica_cannot_draw_are_refused", "crates/ui-egui/tests/signing.rs::the_signature_labels_follow_the_interface_language", "crates/automation/tests/automation.rs::signing_with_translated_labels_through_tools"] +notes = "text items (name, date, reason, location, DN, labels), and a graphic where the large name would be: the saved Fill & Sign signature (drawn, typed or image) or an imported PNG/JPEG (white paper made transparent, as in Fill & Sign), remembered for the next signing; alone it fills the box. The labels are stamped in the interface language when Helvetica (WinAnsi) can draw all of them (German, Spanish, French, Italian, Portuguese, Polish, Turkish, Slovene, Hungarian), otherwise all in English with a note under the preview, which shows the same labels and the signing time; sign_document takes labels and the shown items (appearance), and a drawn label the font can't draw is refused, never written as ?. Missing: labels in non-Latin scripts (needs an embedded font), the DN line in the preview, named saved appearances (one remembered choice only), a PDF page as the graphic, the logo/watermark background, drawing a new signature inside the dialog" [[feature]] id = "sign.reason-location"