Merge remote-tracking branch 'origin/main' into feat/all-tools-expand-view

# Conflicts:
#	ATTRIBUTION.toml
#	crates/ui-egui/src/i18n/lt.tsv
#	crates/ui-egui/src/i18n/vi.tsv
This commit is contained in:
alehel committed 2026-10-15 16:54:41 +02:00
commit cd16644a64
15 files changed
+196 -61

No files matched your search

+2 -2
View File
@@ -27,8 +27,8 @@ Every asset PdfCraft includes, bundles or uses to build its published material,
| `crates/ui-egui/src/i18n/uk.tsv` | Ukrainian interface translation catalog | PdfCraft contributors | MIT OR Apache-2.0 | Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources | Ukrainian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs |
| `crates/ui-egui/src/i18n/ar.tsv` | Arabic interface translation catalog | PdfCraft contributors | MIT OR Apache-2.0 | Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources | Arabic UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs |
| `crates/ui-egui/src/i18n/tr.tsv` | Turkish interface translation catalog | PdfCraft contributors | MIT OR Apache-2.0 | Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources | Turkish UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs |
| `crates/ui-egui/src/i18n/lt.tsv` | Lithuanian interface translation catalog | PdfCraft contributors | MIT OR Apache-2.0 | Contributor-original: clean-room translations of PdfCraft's English UI labels and user-facing messages; reviewed by a native Lithuanian-speaking contributor | Lithuanian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs |
| `crates/ui-egui/src/i18n/vi.tsv` | Vietnamese interface translation catalog | PdfCraft contributors | MIT OR Apache-2.0 | Contributor-original: clean-room translations of PdfCraft's English UI labels; PhotoCraft and VectorCraft Vietnamese catalogs used only as translation memory when English source and semantic context matched; reviewed by a native Vietnamese-speaking contributor | Vietnamese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs |
| `crates/ui-egui/src/i18n/lt.tsv` | Lithuanian interface translation catalog | PdfCraft contributors | MIT OR Apache-2.0 | Contributor-original: clean-room translations of PdfCraft's English UI labels and user-facing messages; original catalog reviewed by a native Lithuanian-speaking contributor; 2026-10-15 coverage additions are AI-assisted and not separately native-reviewed | Lithuanian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs |
| `crates/ui-egui/src/i18n/vi.tsv` | Vietnamese interface translation catalog | PdfCraft contributors | MIT OR Apache-2.0 | Contributor-original: clean-room translations of PdfCraft's English UI labels; PhotoCraft and VectorCraft Vietnamese catalogs used only as translation memory when English source and semantic context matched; original catalog reviewed by a native Vietnamese-speaking contributor; 2026-10-15 coverage additions are AI-assisted and not separately native-reviewed | Vietnamese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs |
| `assets/icons/accessibility.svg` | Lucide icon "accessibility" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/accessibility.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/align-left.svg` | Lucide icon "align-left" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/align-left.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/align-center.svg` | Lucide icon "align-center" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/align-center.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
+5 -5
View File
@@ -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 = "12a568bf7a626ff3e34009150894eabe3c0927589756d8635fc9bae19638a512"
sha256 = "08e35bcca452daaa26efc66a6cbbf75794673cc59ebf42a97ce4180cc6ecfa0c"
[[asset]]
path = "crates/ui-egui/src/i18n/cs.tsv"
@@ -223,24 +223,24 @@ sha256 = "9d879f455313d528d2d8c91e080a1fa199f50d901c7691f7b6dc3f15be73fd1a"
path = "crates/ui-egui/src/i18n/lt.tsv"
title = "Lithuanian interface translation catalog"
author = "PdfCraft contributors"
source = "Contributor-original: clean-room translations of PdfCraft's English UI labels and user-facing messages; reviewed by a native Lithuanian-speaking contributor"
source = "Contributor-original: clean-room translations of PdfCraft's English UI labels and user-facing messages; original catalog reviewed by a native Lithuanian-speaking contributor; 2026-10-15 coverage additions are AI-assisted and not separately native-reviewed"
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 = "b300a148c46c122b84ee9b978979ad41c3b4fbb220a488989bb991e0062d9885"
sha256 = "e9ecf10658e7c02e2c59e3fb354e561ade437ce9938cb08ebdc9488184efb59e"
[[asset]]
path = "crates/ui-egui/src/i18n/vi.tsv"
title = "Vietnamese interface translation catalog"
author = "PdfCraft contributors"
source = "Contributor-original: clean-room translations of PdfCraft's English UI labels; PhotoCraft and VectorCraft Vietnamese catalogs used only as translation memory when English source and semantic context matched; reviewed by a native Vietnamese-speaking contributor"
source = "Contributor-original: clean-room translations of PdfCraft's English UI labels; PhotoCraft and VectorCraft Vietnamese catalogs used only as translation memory when English source and semantic context matched; original catalog reviewed by a native Vietnamese-speaking contributor; 2026-10-15 coverage additions are AI-assisted and not separately native-reviewed"
licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Vietnamese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "4c09041b54acbdb7c5fe16409b2fa699d260200fec8600000d668c9af7636f8c"
sha256 = "29677be4697aa6feeb1d4251795960e525a3a47ce3381a32b0157f488542b30e"
[[asset]]
path = "assets/icons/accessibility.svg"
+46 -2
View File
@@ -932,11 +932,24 @@ impl Automation {
// A save that adds or removes encryption, or must not keep the old revision (Sanitize,
// redaction), is rewritten in full even when an incremental save was asked for.
let incremental = !full && bytes.starts_with(&self.doc(a)?.saved_bytes());
self.publish_save(id, &target, bytes, incremental, write)
}
/// Validate the prepared bytes before publishing them or marking the document saved.
fn publish_save(
&mut self,
id: DocId,
target: &Path,
bytes: Arc<Vec<u8>>,
incremental: bool,
write: impl FnOnce(&Path, &[u8]) -> Result<()>,
) -> Result<Value> {
self.session.check_save(id, &bytes).map_err(failed)?;
write(&target, &bytes)?;
write(target, &bytes)?;
let path = target.to_string_lossy().into_owned();
self.session.mark_saved(id, bytes.clone(), Some(path.clone())).map_err(failed)?;
Ok(json!({ "path": path, "bytes": bytes.len(), "incremental": incremental, "document": summary(self.doc(a)?) }))
let doc = self.session.get(id).ok_or_else(|| failed("the saved document is no longer open"))?;
Ok(json!({ "path": path, "bytes": bytes.len(), "incremental": incremental, "document": summary(doc) }))
}
fn doc_revisions(&mut self, a: &Args) -> Result<Value> {
@@ -2958,6 +2971,37 @@ mod tests {
}
}
#[test]
fn invalid_save_bytes_never_reach_publication_or_mark_saved() {
let dir = staging_dir("invalid-save-bytes");
let target = dir.join("out.pdf");
let mut a = Automation::new().with_root(&dir).unwrap();
let id = search_document(&mut a, 2);
json(&mut a, "doc_save", json!({"doc": id.0, "path": "out.pdf"}));
let old = std::fs::read(&target).unwrap();
json(&mut a, "page_rotate", json!({"doc": id.0, "pages": [1], "degrees": 90}));
let before = summary(a.session.get(id).unwrap());
// Exercise both parse failure and a valid PDF that loses a page. This does not
// depend on a malformed input remaining unrepaired by future reader improvements.
for bytes in [b"not a PDF".to_vec(), numbered_pages(1)] {
let mut published = false;
let result = a.publish_save(id, &target, Arc::new(bytes), false, |_, _| {
published = true;
Ok(())
});
assert!(result.is_err(), "invalid output must be refused");
assert!(result.unwrap_err().to_string().contains("could not be reopened"));
assert!(!published, "validation must precede the writer");
assert_eq!(summary(a.session.get(id).unwrap()), before, "failed validation must preserve document state");
assert_eq!(std::fs::read(&target).unwrap(), old);
assert_eq!(listing(&dir), ["out.pdf"]);
}
json(&mut a, "doc_save", json!({"doc": id.0}));
assert!(!a.session.get(id).unwrap().dirty);
assert_ne!(std::fs::read(&target).unwrap(), old);
std::fs::remove_dir_all(dir).unwrap();
}
#[test]
fn a_failed_save_sync_keeps_the_document_dirty_and_can_be_retried() {
let dir = staging_dir("sync-dirty");
+17 -9
View File
@@ -711,11 +711,10 @@ fn an_in_place_save_reports_a_required_rewrite_as_not_incremental() {
}
}
/// Issue #1698: a save whose bytes don't reopen is refused before it touches the file. The
/// catalog's `/Pages` is not a page tree, so the reader finds the page by scanning, while a
/// full rewrite keeps only what the catalog reaches and drops it.
/// #1664 repairs the missing page tree on open. The former #1698 failure fixture now
/// saves successfully, keeping its page and pixels through a full rewrite.
#[test]
fn a_save_that_does_not_reopen_leaves_the_file_as_it_was() {
fn a_recovered_page_tree_survives_a_full_save() {
let dir = workdir("save-verify-before-write");
let objs: [&[u8]; 4] = [
b"<< /Type /Catalog /Pages 3 0 R >>",
@@ -743,10 +742,19 @@ fn a_save_that_does_not_reopen_leaves_the_file_as_it_was() {
let opened = ok(&mut a, "doc_open", json!({ "path": "damaged.pdf" }));
assert_eq!(opened["pages"], 1, "{opened}");
let doc = opened["doc"].as_u64().unwrap();
let e = err(&mut a, "doc_save", json!({ "doc": doc, "full": true }));
assert!(e.to_string().contains("could not be reopened"), "{e}");
let (width, height, pixels) = a.render_pixels(doc, 1, 1.0).unwrap();
assert!(pixels.as_ref().as_chunks::<4>().0.iter().any(|p| p[..3] != [255, 255, 255]), "the original page is not blank");
let saved = ok(&mut a, "doc_save", json!({ "doc": doc, "full": true }));
assert_eq!(saved["document"]["dirty"], false);
let on_disk = std::fs::read(dir.join("damaged.pdf")).unwrap();
assert!(on_disk == original, "the file was replaced by {} bytes that don't reopen", on_disk.len());
assert_ne!(on_disk, original, "a full save writes the repaired page tree");
let mut reopened = auto(&dir);
let opened = ok(&mut reopened, "doc_open", json!({ "path": "damaged.pdf" }));
assert_eq!(opened["pages"], 1);
let again = opened["doc"].as_u64().unwrap();
let (new_width, new_height, new_pixels) = reopened.render_pixels(again, 1, 1.0).unwrap();
assert_eq!((new_width, new_height), (width, height));
assert_eq!(new_pixels.as_ref(), pixels.as_ref(), "saving must preserve the recovered page's drawing");
}
/// organize.field-name-conflicts: combining files whose forms share a field name keeps the
@@ -4527,11 +4535,11 @@ fn signature_appearances(path: &Path) -> String {
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" }));
ok(&mut a, "sign_id_create", json!({ "name": "Ada Lovelace", "key": "p256", "password": "secret12", "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 });
let mut args = json!({ "doc": doc, "id": "ada.p12", "password": "secret12", "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();
}
+9 -3
View File
@@ -2397,7 +2397,8 @@ pub fn document_area(app: &mut PdfCraftApp, index: usize, ui: &mut egui::Ui) {
// Paging owns the vertical axis in single-page view; leave it to the pager.
// The rest of a gesture that turned the page is swallowed too, or it would pan the new page
// away from the edge it was shown at.
if single_paging || (!view.continuous && view.wheel.gesture_turned()) {
let paging_owns_vertical = single_paging || (!view.continuous && view.wheel.gesture_turned());
if paging_owns_vertical {
precise_scroll.y = 0.0;
}
if now < view.precise_scroll_until {
@@ -2410,7 +2411,7 @@ pub fn document_area(app: &mut PdfCraftApp, index: usize, ui: &mut egui::Ui) {
// dialog has the input, or animations are off; any click, key, zoom or other scrolling ends it.
// Asked of the preference, not of egui's animation time, which test harnesses zero.
let animates = app.animation_effects.animates(app.motion.reduced);
let may_glide = !single_paging && !middle_gesture && navigation_enabled && unobstructed && animates;
let may_glide = !paging_owns_vertical && !middle_gesture && navigation_enabled && unobstructed && animates;
let over_page = ui.rect_contains_pointer(avail);
ui.input(|i| view.fling.read(&i.events, now, may_glide && over_page));
let interrupted = ui.input(|i| i.pointer.any_pressed() || i.events.iter().any(|e| matches!(e, egui::Event::Key { pressed: true, .. })));
@@ -5107,6 +5108,7 @@ trailer << /Root 1 0 R >>
fn explicit_search_jumps_record_history_and_keep_match_destination() {
for step in [None, Some(true), Some(false)] {
let mut v = history_search_view();
v.page_input = "draft in progress".into();
v.goto_point = Some((3, Some(0.8), Some(0.9), Vec2::ZERO));
if let Some(forward) = step {
v.find_step(forward);
@@ -5115,7 +5117,11 @@ trailer << /Root 1 0 R >>
}
assert_eq!(v.back, vec![0], "explicit search jump must remember its origin");
assert_eq!(v.goto, Some((3, 0.5)), "keep the match offset instead of the page top");
assert_eq!(v.page_input, "4");
assert_eq!(v.current, 3);
assert_eq!(v.current_page_label(None), "4");
// Chrome refreshes the idle page box; navigation must not overwrite a draft
// that may still be focused. DocView does not own the editor's focus state.
assert_eq!(v.page_input, "draft in progress");
assert!(v.goto_point.is_none(), "an earlier link destination cannot override the search offset");
v.go_to_match(1);
assert_eq!(v.back, vec![0], "same-page matches do not duplicate history");
+7
View File
@@ -2678,3 +2678,10 @@ key Space Tarpas
Protect & Standardize Apsauga ir standartizavimas
Customize & Automate Pritaikymas ir automatizavimas
No tools match your search. Paieškos neatitinka joks įrankis.
# AI-assisted additions for concurrently merged UI features (2026-10-15).
# These additions have not had separate native-speaker review.
Mouse wheel scroll speed Pelės ratuko slinkimo greitis
The password must have at least 6 characters. Slaptažodį turi sudaryti bent 6 simboliai.
This file changed on disk. Reload it to see the new version; your unsaved changes will be lost. Šis failas diske pasikeitė. Įkelkite jį iš naujo, kad pamatytumėte naują versiją; neįrašyti pakeitimai bus prarasti.
theme Appearance Išvaizda
Can't render this file because it contains an unexpected character in line 404 and column 9.
+67 -1
View File
@@ -1,7 +1,7 @@
# Vietnamese (vi) catalog. Columns, separated by a TAB: context <TAB> source <TAB> translation
#
# Clean-room translations from PdfCraft's English UI labels. Reviewed by a native
# Vietnamese-speaking contributor. PhotoCraft/VectorCraft Vietnamese catalogs were used only as
# Vietnamese-speaking contributor for the original catalog; later additions are marked below. PhotoCraft/VectorCraft Vietnamese catalogs were used only as
# translation memory when the English source and its meaning in context matched. Product and
# format names (PdfCraft, PDF, PDF/A, OCR, XFA, Word…) remain names where appropriate.
# Plural rule: `plural_none` — every @plural row lists exactly one form.
@@ -2621,3 +2621,69 @@ key Space Space
Protect & Standardize Bảo vệ và chuẩn hóa
Customize & Automate Tùy chỉnh và tự động hóa
No tools match your search. Không có công cụ nào khớp với tìm kiếm.
# AI-assisted additions for concurrently merged UI features (2026-10-15).
# These additions have not had separate native-speaker review.
Characters Ký tự
Characters without spaces Ký tự không tính dấu cách
Counting words… {n} pages read Đang đếm từ… đã đọc {n} trang
Crop Image Xén hình ảnh
Current page only Chỉ trang hiện tại
DN: Tên phân biệt:
Date: Ngày:
Digitally signed by Được ký số bởi
Exclude image/OCR text Loại trừ văn bản trong ảnh/OCR
Exclude tagged code blocks Loại trừ khối mã đã gắn thẻ
Exclude tagged tables Loại trừ bảng đã gắn thẻ
Exclusions use PDF tags. Untagged tables and code remain included. Image exclusion also removes invisible text. Việc loại trừ dựa trên thẻ PDF. Bảng và mã chưa gắn thẻ vẫn được tính. Loại trừ hình ảnh cũng loại bỏ văn bản ẩn.
Fill colour: Màu tô:
Font size Cỡ chữ
Full Screen Toàn màn hình
Location: Vị trí:
Make properties default Đặt thuộc tính làm mặc định
Mark as artifact Đánh dấu là thành phần trang trí
Mark content as artifact Đánh dấu nội dung là thành phần trang trí
Merge image fragments Gộp các mảnh hình ảnh
Mobile Di động
New file only Chỉ tệp mới
New file: {name} Tệp mới: {name}
Next tab Thẻ tiếp theo
No printers were found. Check that the printer is installed and the print spooler is running, then open Print again. Không tìm thấy máy in. Kiểm tra xem máy in đã được cài đặt và dịch vụ hàng đợi in đang chạy, rồi mở lại hộp thoại In.
Not now Để sau
Old file: {name} Tệp cũ: {name}
Opacity: Độ mờ:
Open in Full Screen mode? Mở ở chế độ toàn màn hình?
Optimizing… merging image fragments Đang tối ưu hóa… gộp các mảnh hình ảnh
Outline colour: Màu đường viền:
Page properties Thuộc tính trang
Pages counted Số trang đã đếm
Pages selected Số trang đã chọn
Previous tab Thẻ trước
Print as image In dưới dạng hình ảnh
Reason: Lý do:
Redaction mark appearance Hình thức dấu xóa nội dung nhạy cảm
Remove Crop Bỏ xén
Remove this leaf tag from reading order and mark its direct content as an artifact Xóa thẻ lá này khỏi thứ tự đọc và đánh dấu nội dung trực tiếp của nó là thành phần trang trí
Resolution: Độ phân giải:
Save anyway Vẫn lưu
Save over the changed file? Lưu đè lên tệp đã thay đổi?
Scroll speed (mouse wheel and arrow keys) Tốc độ cuộn (con lăn chuột và phím mũi tên)
Set page transitions Đặt hiệu ứng chuyển trang
Shown while the pointer is over a mark. Hiển thị khi con trỏ nằm trên một dấu.
Side by side Cạnh nhau
Some content could not be classified safely and was kept in the count. Một số nội dung không thể được phân loại an toàn và vẫn được tính.
Some pages contain no countable text. Scanned images need OCR first. Một số trang không có văn bản có thể đếm. Ảnh quét cần được nhận dạng văn bản (OCR) trước.
Standard Tiêu chuẩn
The labels are in English: the signature's font can't draw them in this language. Các nhãn được hiển thị bằng tiếng Anh: phông chữ của chữ ký không thể hiển thị chúng bằng ngôn ngữ này.
The password must have at least 8 characters. Mật khẩu phải có ít nhất 8 ký tự.
This document asks to open in Full Screen mode: the page fills the screen and nothing else shows. Press Esc at any time to leave it. Tài liệu này yêu cầu mở ở chế độ toàn màn hình: trang chiếm toàn bộ màn hình và không hiển thị nội dung nào khác. Nhấn Esc bất kỳ lúc nào để thoát.
This file changed on disk. Reload shows the new version and drops your unsaved changes; Save replaces it with this version. Tệp này đã thay đổi trên đĩa. Tải lại sẽ hiển thị phiên bản mới và bỏ các thay đổi chưa lưu của bạn; Lưu sẽ thay thế tệp bằng phiên bản này.
Trim the picture with its handles; Enter or Done applies Xén hình bằng các tay nắm; nhấn Enter hoặc Xong để áp dụng
Use it when a printer skips text, shading or transparency. The print is no longer searchable text; with Print in grayscale, the pictures are grey. Dùng khi máy in bỏ sót văn bản, chuyển sắc hoặc độ trong suốt. Bản in không còn chứa văn bản có thể tìm kiếm; với In thang độ xám, hình ảnh sẽ có màu xám.
What takes up the space in this file, by kind of content. Dung lượng mà từng loại nội dung chiếm trong tệp này.
Word count Số từ
Word counting stopped unexpectedly. Việc đếm từ đã dừng đột ngột.
Word count… Đếm từ…
Words Từ
“{name}” changed on disk after it was opened or last saved. Saving now replaces that newer version with this one. “{name}” đã thay đổi trên đĩa sau khi được mở hoặc lưu lần cuối. Lưu lúc này sẽ thay thế phiên bản mới hơn đó bằng phiên bản này.
@id file.word_count Đếm từ…
Can't render this file because it contains an unexpected character in line 490 and column 12.
-2
View File
@@ -787,7 +787,6 @@
Redaction code: 遮蔽代碼:
Font: 字型:
Font size: 字型大小:
Font size: 字號:
Font size 字號
Auto-size text to fit redaction region 自動調整文字大小以適應遮蔽內容區域
Font colour: 字型顏色:
@@ -1322,7 +1321,6 @@
Sort comments by: 註解排序依據:
Create PDF Comment Summary 建立 PDF 註解摘要
Revert to the previously saved version of “{name}”? Changes since then can't be undone afterwards. 還原到「{name}」的上次儲存版本?此後的更改將無法復原。
Revert to the previously saved version of “{name}”? Changes since then can't be undone afterwards. 還原到“{name}”的上次儲存版本?此後的更改將無法復原。
Save over the changed file? 覆寫已變更的檔案?
“{name}” changed on disk after it was opened or last saved. Saving now replaces that newer version with this one. 「{name}」在開啟或上次儲存後已在磁碟上變更。現在儲存會以此版本取代那個較新的版本。
Save anyway 仍要儲存
Can't render this file because it contains an unexpected character in line 9 and column 16.
+3
View File
@@ -367,6 +367,9 @@ fn font_definitions_for_platform(prefer_hans: bool, platform: FontPolicyPlatform
add(&mut fonts, "JetBrainsMono", include_bytes!("../../../assets/fonts/JetBrainsMono-Regular.ttf"));
fonts.families.entry(FontFamily::Proportional).or_default().insert(0, "Inter".to_owned());
fonts.families.entry(FontFamily::Monospace).or_default().insert(0, "JetBrainsMono".to_owned());
// Keep missing Latin/Cyrillic letters in the app's own faces (for example Bulgarian ѝ,
// absent from JetBrains Mono), before egui's defaults or optional system/CJK fallbacks.
fonts.families.entry(FontFamily::Monospace).or_default().insert(1, "Inter".to_owned());
// The same static bytes pdfcraft-fonts uses for Japanese/Chinese text in PDFs: one copy, not two.
for face in pdfcraft_fonts::ui_cjk_fonts(prefer_hans) {
let name = face.name();
+10 -1
View File
@@ -1603,7 +1603,15 @@ fn page_size_shows_while_the_pointer_is_in_the_bottom_left_quadrant() {
#[test]
fn navigation_history_commands_and_rail_are_per_tab_and_non_editing() {
for os in [egui::os::OperatingSystem::Nix, egui::os::OperatingSystem::Mac, egui::os::OperatingSystem::Windows] {
navigation_history_for_os(os);
}
}
fn navigation_history_for_os(os: egui::os::OperatingSystem) {
let (mut h, c) = harness();
h.ctx.set_os(os);
h.run_steps(2);
let enabled =
|list: Value, id: &str| list["commands"].as_array().unwrap().iter().find(|cmd| cmd["id"] == id).unwrap()["enabled"].as_bool().unwrap();
assert!(!enabled(ok(&mut h, &c, "ui.commands", json!({})), "view.previous_view"));
@@ -1617,7 +1625,8 @@ fn navigation_history_commands_and_rail_are_per_tab_and_non_editing() {
ok(&mut h, &c, "ui.set", json!({"key": "page", "value": "4"}));
assert!(enabled(ok(&mut h, &c, "ui.commands", json!({})), "view.previous_view"));
// The real rail button is accessible and invokes the same history, not the adjacent page.
ok(&mut h, &c, "ui.click", json!({"label": &format!("Previous view ({})", if cfg!(target_os = "macos") { "⌘[" } else { "Ctrl+[" })}));
let shortcut = if h.ctx.os() == egui::os::OperatingSystem::Mac { "⌘[" } else { "Ctrl+[" };
ok(&mut h, &c, "ui.click", json!({"label": &format!("Previous view ({shortcut})")}));
assert_eq!(h.state().views[0].current, 0);
assert!(enabled(ok(&mut h, &c, "ui.commands", json!({})), "view.next_view"));
ok(&mut h, &c, "ui.command", json!({"id": "view.next_view"}));
+4 -1
View File
@@ -1777,9 +1777,12 @@ fn files_dropped_beside_the_pages_panel_still_open_as_documents() {
h.hover_at(egui::pos2(thumb.left() - 300.0, thumb.center().y));
h.run_steps(2);
drop_files(&mut h, vec![Dropped("other.pdf", fixture(1))]);
h.run_steps(3);
// Native opening prepares the document on a worker; three UI frames need not give it
// any CPU time. Wait for the result, as the other dropped-document regression does.
support::wait_until(&mut h, "document dropped beside the Pages panel", |app| app.views.len() == 2 && app.password_prompt.is_none());
assert_eq!(h.state().views.len(), 2);
assert_eq!(texts_of(h.state(), 0), ["Page 1", "Page 2"]);
assert_eq!(texts_of(h.state(), 1), ["Page 1"], "the dropped PDF opens as its own document");
}
fn grid_zoom(h: &Harness<'static, PdfCraftApp>) -> f32 {
+7 -2
View File
@@ -291,12 +291,16 @@ fn ui_fonts_work_without_craft_fonts() {
}
/// Latin- and Cyrillic-script catalogs (Czech, Brazilian Portuguese, German, Spanish, French, Russian, Bulgarian, Hungarian, Ukrainian, Italian, Slovene) are drawn entirely by the app's own faces
/// (Inter, JetBrains Mono): no letter falls through to egui's defaults or a CJK fallback. (egui's
/// (Inter, JetBrains Mono with Inter fallback): no letter falls through to egui's defaults or a CJK fallback. (egui's
/// `has_glyphs` can't answer this: with only the primary face it is also the replacement face.)
#[test]
fn primary_ui_fonts_cover_latin_and_cyrillic_catalogs() {
use skrifa::MetadataProvider as _;
let defs = theme::font_definitions();
assert_eq!(&defs.families[&FontFamily::Monospace][..2], ["JetBrainsMono", "Inter"]);
let fallback = &defs.font_data["Inter"];
let fallback_font = skrifa::FontRef::from_index(&fallback.font, fallback.index).unwrap();
let fallback_charmap = fallback_font.charmap();
for (code, catalog) in [
("cs", include_str!("../src/i18n/cs.tsv")),
("el", include_str!("../src/i18n/el.tsv")),
@@ -325,7 +329,8 @@ fn primary_ui_fonts_cover_latin_and_cyrillic_catalogs() {
let data = &defs.font_data[name];
let font = skrifa::FontRef::from_index(&data.font, data.index).unwrap();
let charmap = font.charmap();
let mut missing: Vec<char> = text.chars().filter(|c| charmap.map(*c).is_none()).collect();
let mut missing: Vec<char> =
text.chars().filter(|c| charmap.map(*c).is_none() && (name != "JetBrainsMono" || fallback_charmap.map(*c).is_none())).collect();
missing.sort_unstable();
missing.dedup();
assert!(missing.is_empty(), "{code}: {name} lacks {missing:?}");
+5 -3
View File
@@ -244,14 +244,16 @@ fn print_dialog_shows_the_printer_driver_options() {
h.get_by_label("Printer properties");
h.get_by_label("Media");
h.get_by_label("Paper tray");
assert_eq!(h.state().print_draft.job("form.pdf").options, [("InputSlot".to_string(), "Tray2".to_string())]);
// The selected sheet's media is always sent, independently of changed driver properties.
let media = vec![("media".to_string(), "Letter".to_string())];
assert_eq!(h.state().print_draft.job("form.pdf").options, [("InputSlot".to_string(), "Tray2".to_string()), media[0].clone()]);
h.get_by_label("Reset to printer defaults").click();
h.run_steps(2);
assert!(h.state().print_draft.job("form.pdf").options.is_empty(), "defaults are not sent");
assert_eq!(h.state().print_draft.job("form.pdf").options, media, "driver defaults are not sent; selected paper is retained");
// Options read for another printer never go with this one's job.
h.state_mut().print_draft.driver_choices.insert("InputSlot".into(), "Tray2".into());
h.state_mut().print_draft.printer = Some("Other_Queue".into());
assert!(h.state().print_draft.job("form.pdf").options.is_empty());
assert_eq!(h.state().print_draft.job("form.pdf").options, media, "another queue must not inherit driver options");
}
/// The desktop app lists printers in the background (PowerShell on Windows takes a second or
+2
View File
@@ -347,6 +347,7 @@ fn precise_touchpad_wheel_scrolls_one_to_one() {
/// Steps `frames` frames and returns how far the document scrolled down over them, from the
/// pages on screen in each pair of consecutive frames (a page can leave the screen on the way).
#[cfg(not(target_os = "macos"))]
fn scroll_over(h: &mut Harness<'static, PdfCraftApp>, frames: usize) -> f32 {
let tops = |h: &Harness<'static, PdfCraftApp>| (0..3).map(|p| rect(h, p).map(|r| r.min.y)).collect::<Vec<_>>();
let mut moved = 0.0;
@@ -363,6 +364,7 @@ fn scroll_over(h: &mut Harness<'static, PdfCraftApp>, frames: usize) -> f32 {
/// A two-finger swipe down the page, 25 points a frame for six frames, lifted the frame after
/// (the axis stop Wayland sends), at 300 % so the document is long enough to glide in: how far
/// it scrolled by the frame after the lift (the page is drawn a frame behind its scroll).
#[cfg(not(target_os = "macos"))]
fn swipe_and_lift(h: &mut Harness<'static, PdfCraftApp>) -> f32 {
let at = h.state().views[0].viewport_rect().center();
h.hover_at(at);
+12 -30
View File
@@ -2620,10 +2620,8 @@ milestone = "M4"
status = "shipped"
commands = ["page.organize"]
tools = []
evidence = ["crates/ui-egui/tests/editing.rs::shift_click_selects_a_range_and_moves_it", "crates/ui-egui/tests/editing.rs::organize_tool_opens_the_overview_directly_and_can_be_reentered", "crates/ui-egui/tests/editing.rs::organize_opens_on_the_current_page", "crates/ui-egui/tests/control.rs::organize_overview_is_reachable_through_the_control_channel"]
notes = "Opening the Organize pages tool enters the overview directly, on the current page: selected and scrolled into view (#1910). Hovered, focused and selected pages expose individual rotate/delete actions; the toolbar acts on the selection."
evidence = ["crates/ui-egui/tests/editing.rs::shift_click_selects_a_range_and_moves_it", "crates/ui-egui/tests/editing.rs::organize_tool_opens_the_overview_directly_and_can_be_reentered", "crates/ui-egui/tests/control.rs::organize_overview_is_reachable_through_the_control_channel", "crates/ui-egui/tests/editing.rs::organize_panel_comes_with_the_grid_and_closes_it", "crates/ui-egui/tests/editing.rs::leaving_organize_goes_back_to_the_workspace_panel"]
notes = "Opening the Organize pages tool enters the overview directly. Hovered, focused and selected pages expose individual rotate/delete actions; the toolbar acts on the selection. The left panel is laid out as Acrobat's Organize pages panel: Pages selected (typed ranges, or All, Even, Odd, Landscape or Portrait pages; empty, it shows the current page), rotate, delete, extract, Insert (a blank page, pages from a file, duplicates), Replace, Split, Bates numbering, Set page boxes, Crop pages, Print pages and Page labels. Closing it, by its Close, the grid's Close or opening a page, returns to the workspace's own panel (Edit, Convert, E-Sign or All tools). Page transitions, Page templates and Page properties are listed but planned."
evidence = ["crates/ui-egui/tests/editing.rs::shift_click_selects_a_range_and_moves_it", "crates/ui-egui/tests/editing.rs::organize_tool_opens_the_overview_directly_and_can_be_reentered", "crates/ui-egui/tests/editing.rs::organize_opens_on_the_current_page", "crates/ui-egui/tests/control.rs::organize_overview_is_reachable_through_the_control_channel", "crates/ui-egui/tests/editing.rs::organize_panel_comes_with_the_grid_and_closes_it", "crates/ui-egui/tests/editing.rs::leaving_organize_goes_back_to_the_workspace_panel"]
notes = "Opening the Organize pages tool enters the overview directly, on the current page: selected and scrolled into view (#1910). Hovered, focused and selected pages expose individual rotate/delete actions; the toolbar acts on the selection. The left panel is laid out as Acrobat's Organize pages panel: Pages selected (typed ranges, or All, Even, Odd, Landscape or Portrait pages; empty, it shows the current page), rotate, delete, extract, Insert (a blank page, pages from a file, duplicates), Replace, Split, Bates numbering, Set page boxes, Crop pages, Print pages and Page labels. Closing it, by its Close, the grid's Close or opening a page, returns to the workspace's own panel (Edit, Convert, E-Sign or All tools). Page transitions, Page templates and Page properties are listed but planned."
[[feature]]
id = "organize.multi-select-pages"
@@ -3197,11 +3195,8 @@ tier = "P2"
milestone = "M4"
status = "partial"
commands = []
tools = ["doc_info"]
evidence = ["crates/render/src/inspect.rs::page_transitions_and_display_times_are_read", "crates/automation/tests/automation.rs::doc_info_lists_page_transitions_and_display_times"]
notes = "Read only: each page's /Trans (style, duration, dimension, motion, direction, Fly scale and backdrop) and /Dur display time are read with the spec's defaults for what is missing or out of range, and doc_info lists them (DocInfo::transitions). Not yet: playing them in full screen, auto-advancing on /Dur, or the Page Transitions dialog that sets them"
tools = []
evidence = []
tools = ["doc_info", "page_transition_set"]
evidence = ["crates/render/src/inspect.rs::page_transitions_and_display_times_are_read", "crates/automation/tests/automation.rs::doc_info_lists_page_transitions_and_display_times", "crates/organize/src/tests.rs::page_transitions_are_set_removed_and_checked", "crates/automation/tests/automation.rs::page_transitions_are_set_and_removed_through_the_tools"]
notes = "Read and set, not played: each page's /Trans (style, duration, dimension, motion, direction, Fly scale and backdrop) and /Dur display time are read with the spec's defaults and listed by doc_info (DocInfo::transitions); page_transition_set (Edit::SetTransition, one undo step) writes or removes them on chosen pages with the values checked first. Not yet: playing them in full screen, auto-advancing on /Dur, and the Page Transitions dialog"
[[feature]]
@@ -4421,10 +4416,8 @@ milestone = "M6"
status = "shipped"
commands = ["form.fields"]
tools = ["form_fill", "form_fields"]
evidence = ["crates/ui-egui/tests/forms.rs::typing_into_a_text_field_and_tabbing_on", "crates/ui-egui/tests/forms.rs::auto_size_fields_shrink_and_wrap_typed_text_to_fit_their_box", "crates/forms/src/tests.rs::text_fields_get_new_appearances", "crates/engine/src/tests.rs::filling_a_form_shows_saves_and_undoes", "crates/automation/tests/automation.rs::orphan_form_fields_through_tools"]
evidence = ["crates/ui-egui/tests/forms.rs::typing_into_a_text_field_and_tabbing_on", "crates/ui-egui/tests/forms.rs::auto_size_fields_shrink_and_wrap_typed_text_to_fit_their_box", "crates/forms/src/tests.rs::text_fields_get_new_appearances", "crates/engine/src/tests.rs::filling_a_form_shows_saves_and_undoes", "crates/automation/tests/automation.rs::orphan_form_fields_through_tools", "crates/forms/src/tests.rs::fields_sharing_a_name_take_the_same_value"]
notes = "in-place editor; Enter or clicking away commits, Escape cancels; auto-size (0 Tf) fields shrink, and multiline ones wrap, the text being typed to fit the box, on screen as on the page (the screen measures in the interface font, so its size can be a little smaller than the page's Helvetica fit); fixed sizes are kept and clip, as in Acrobat; no format/keystroke JavaScript yet (M6.4)"
evidence = ["crates/ui-egui/tests/forms.rs::typing_into_a_text_field_and_tabbing_on", "crates/forms/src/tests.rs::text_fields_get_new_appearances", "crates/engine/src/tests.rs::filling_a_form_shows_saves_and_undoes", "crates/automation/tests/automation.rs::orphan_form_fields_through_tools", "crates/forms/src/tests.rs::fields_sharing_a_name_take_the_same_value"]
notes = "in-place editor; Enter or clicking away commits, Escape cancels; no format/keystroke JavaScript yet (M6.4)"
[[feature]]
id = "form.fill-checkbox-radio"
@@ -5311,10 +5304,8 @@ milestone = "M5"
status = "shipped"
commands = ["sign.fill.signature", "sign.fill.initials", "sign.fill.signature.change", "sign.fill.initials.change", "sign.fill.signature.remove", "sign.fill.initials.remove"]
tools = ["fill_sign_add", "comment_edit", "comment_image_preview"]
evidence = ["crates/ui-egui/tests/fill_sign.rs::drawn_signatures_resize_from_their_corners", "crates/ui-egui/tests/fill_sign.rs::drawn_and_typed_signatures_stay_upright_on_rotated_pages", "crates/automation/tests/automation.rs::typed_signatures_stay_upright_on_rotated_pages", "crates/ui-egui/tests/fill_sign.rs::image_signature_corners_restore_original_aspect_after_edge_resize_and_reopen", "crates/ui-egui/src/comments.rs::proportional_corners_keep_the_opposite_anchor_and_edges_stretch_one_axis", "crates/ui-egui/tests/fill_sign.rs::image_signature_live_resize_and_move_preserve_background_and_hide_moving_handles", "crates/ui-egui/tests/fill_sign.rs::embedded_image_signature_live_gestures_follow_document_and_view_rotation", "crates/automation/tests/automation.rs::image_signature_preview_layers_are_read_only_and_survive_encrypted_save", "crates/engine/src/signature_image.rs::embedded_signature_preview_preserves_alpha_without_editing_the_document", "crates/engine/src/signature_image.rs::oversized_embedded_signature_preview_is_rejected_before_decoding", "crates/ui-egui/tests/fill_sign.rs::image_signature_preview_follows_pointer_at_page_size_with_zoom_and_rotation", "crates/ui-egui/tests/fill_sign.rs::image_signature_placement_selects_resize_handles_and_requires_reselecting_to_repeat", "crates/ui-egui/tests/fill_sign.rs::typed_signatures_and_initials", "crates/ui-egui/tests/fill_sign.rs::signing_draws_a_signature_once_and_places_it", "crates/ui-egui/tests/fill_sign.rs::changing_saved_signatures_and_initials_preserves_placed_marks", "crates/ui-egui/tests/fill_sign.rs::changing_drawn_signatures_can_be_cancelled_or_switched_to_type", "crates/ui-egui/tests/fill_sign.rs::saved_signature_cards_remove_and_add_without_changing_the_document", "crates/ui-egui/tests/fill_sign.rs::long_typed_names_fit_the_placed_signature_and_keep_every_outline", "crates/ui-egui/src/fill_sign.rs::long_signature_previews_have_clear_margins", "crates/ui-egui/tests/control.rs::saved_signature_can_be_changed_through_the_control_channel", "crates/fonts/src/script.rs::a_name_becomes_outlines", "crates/fonts/src/script.rs::long_names_keep_every_glyph_with_bounded_outline_work", "crates/automation/tests/automation.rs::fill_and_sign_through_tools", "crates/ui-egui/tests/fill_sign.rs::image_signatures_and_initials_can_be_imported_placed_and_remembered", "crates/ui-egui/tests/fill_sign.rs::image_import_cancel_clear_and_errors_preserve_saved_signatures", "crates/engine/src/signature_image.rs::image_signatures_preserve_alpha_and_fit_the_signature_box", "crates/engine/src/signature_image.rs::invalid_and_oversized_signature_images_are_rejected", "crates/engine/src/signature_image.rs::large_signature_images_are_scaled_down", "crates/engine/src/signature_image.rs::jpeg_signatures_are_normalized_to_png", "crates/engine/src/signature_image.rs::jpeg_signature_orientation_is_applied_before_preview_and_embedding", "crates/engine/src/signature_image.rs::very_wide_image_signatures_keep_the_requested_rectangle", "crates/automation/tests/automation.rs::image_signatures_through_tools_preserve_transparency_and_survive_save", "crates/ui-egui/tests/fill_sign.rs::fill_and_sign_tools_pick_up_placed_marks_to_move_and_resize_them", "crates/ui-egui/tests/fill_sign.rs::select_tool_picks_up_a_signature_over_a_form_field", "crates/ui-egui/tests/fill_sign.rs::scanned_signatures_are_framed_cleaned_adjusted_placed_and_remembered", "crates/ui-egui/tests/fill_sign.rs::scan_cancel_errors_and_stale_answers_preserve_saved_signatures", "crates/automation/tests/automation.rs::scanned_signatures_through_tools_drop_the_paper_and_report_bad_arguments", "crates/engine/src/signature_scan.rs::a_photo_is_scanned_into_a_trimmed_transparent_signature_in_its_ink_colour", "crates/ui-egui/tests/fill_sign.rs::undo_button_and_ctrl_z_take_back_one_drawn_stroke_at_a_time", "crates/ui-egui/src/fill_sign.rs::undo_removes_the_last_stroke_and_skips_empty_ones"]
notes = "signature and initials, typed (Dancing Script outlines as filled paths, no font embedded), drawn (an Undo button and Ctrl/Cmd+Z in the pad take back the last stroke; Clear empties it) or imported from local PNG/JPEG images; all three are placed upright and anchored as displayed on pages with /Rotate (#299); saved in settings with preview cards and add/change/remove controls; changes leave placed annotations untouched; images preserve alpha, aspect ratio and EXIF orientation, fit within 150 pt by 32 pt (24 pt for initials), and are normalized without source paths or metadata; image previews follow the pointer at placement size with zoom and rotation; signatures/initials place once then select the new annotation; while a Fill & Sign tool is active, marks already placed are picked up as with Select (hover shows the move cursor, a click selects, a drag moves, the selected mark's handles resize) and the tool places only away from them; embedded image signatures resize and move live, hiding the frame during movement and restoring it on release; corner handles preserve the original aspect ratio of the embedded image with the opposite corner anchored, while edge midpoint handles stretch width or height independently; cached background layers preserve page content and alpha, including reopened/encrypted documents; Escape cancels a gesture without committing; stamps resize through selection handles or comment_edit with their original appearance; native and web asynchronous pickers; fill_sign_add accepts path instead of text; image imports capped at 4 MiB, 32 megapixels and 8192 pixels per side, larger than 4 megapixels or 4096 pixels per side scaled down to fit (#1075); Scan tab (fill_sign_add scan/sensitivity headless): a phone photo of a signature on paper (PNG/JPEG within the same image limits, EXIF-oriented, kept at most 4 megapixels) is framed automatically (all words; table, sheet edge and dust ignored; a sideways signature turned upright), the frame can be moved, resized or redrawn and the photo turned in quarter turns, and an ink-sensitivity slider (0-100) controls how faint ink is kept; the paper is estimated by a morphological closing, so shadows and uneven light are removed, and the result is trimmed ink in its own colour on transparency, saved like an imported image. Still missing: perspective correction of a skewed sheet, live camera capture, keyboard control of the frame, and processing off the UI thread (bounded by the 4-megapixel working copy; quick 900 px previews while dragging)"
evidence = ["crates/ui-egui/tests/fill_sign.rs::drawn_signatures_resize_from_their_corners", "crates/ui-egui/tests/fill_sign.rs::drawn_and_typed_signatures_stay_upright_on_rotated_pages", "crates/automation/tests/automation.rs::typed_signatures_stay_upright_on_rotated_pages", "crates/ui-egui/tests/fill_sign.rs::image_signature_corners_restore_original_aspect_after_edge_resize_and_reopen", "crates/ui-egui/src/comments.rs::proportional_corners_keep_the_opposite_anchor_and_edges_stretch_one_axis", "crates/ui-egui/src/comments.rs::shift_keeps_the_current_proportions_unless_the_box_has_its_own", "crates/ui-egui/tests/fill_sign.rs::image_signature_live_resize_and_move_preserve_background_and_hide_moving_handles", "crates/ui-egui/tests/fill_sign.rs::embedded_image_signature_live_gestures_follow_document_and_view_rotation", "crates/automation/tests/automation.rs::image_signature_preview_layers_are_read_only_and_survive_encrypted_save", "crates/engine/src/signature_image.rs::embedded_signature_preview_preserves_alpha_without_editing_the_document", "crates/engine/src/signature_image.rs::oversized_embedded_signature_preview_is_rejected_before_decoding", "crates/ui-egui/tests/fill_sign.rs::image_signature_preview_follows_pointer_at_page_size_with_zoom_and_rotation", "crates/ui-egui/tests/fill_sign.rs::image_signature_placement_selects_resize_handles_and_requires_reselecting_to_repeat", "crates/ui-egui/tests/fill_sign.rs::typed_signatures_and_initials", "crates/ui-egui/tests/fill_sign.rs::signing_draws_a_signature_once_and_places_it", "crates/ui-egui/tests/fill_sign.rs::changing_saved_signatures_and_initials_preserves_placed_marks", "crates/ui-egui/tests/fill_sign.rs::changing_drawn_signatures_can_be_cancelled_or_switched_to_type", "crates/ui-egui/tests/fill_sign.rs::saved_signature_cards_remove_and_add_without_changing_the_document", "crates/ui-egui/tests/fill_sign.rs::long_typed_names_fit_the_placed_signature_and_keep_every_outline", "crates/ui-egui/src/fill_sign.rs::long_signature_previews_have_clear_margins", "crates/ui-egui/tests/control.rs::saved_signature_can_be_changed_through_the_control_channel", "crates/fonts/src/script.rs::a_name_becomes_outlines", "crates/fonts/src/script.rs::long_names_keep_every_glyph_with_bounded_outline_work", "crates/automation/tests/automation.rs::fill_and_sign_through_tools", "crates/ui-egui/tests/fill_sign.rs::image_signatures_and_initials_can_be_imported_placed_and_remembered", "crates/ui-egui/tests/fill_sign.rs::image_import_cancel_clear_and_errors_preserve_saved_signatures", "crates/engine/src/signature_image.rs::image_signatures_preserve_alpha_and_fit_the_signature_box", "crates/engine/src/signature_image.rs::invalid_and_oversized_signature_images_are_rejected", "crates/engine/src/signature_image.rs::large_signature_images_are_scaled_down", "crates/engine/src/signature_image.rs::jpeg_signatures_are_normalized_to_png", "crates/engine/src/signature_image.rs::jpeg_signature_orientation_is_applied_before_preview_and_embedding", "crates/engine/src/signature_image.rs::very_wide_image_signatures_keep_the_requested_rectangle", "crates/automation/tests/automation.rs::image_signatures_through_tools_preserve_transparency_and_survive_save", "crates/ui-egui/tests/fill_sign.rs::fill_and_sign_tools_pick_up_placed_marks_to_move_and_resize_them", "crates/ui-egui/tests/fill_sign.rs::select_tool_picks_up_a_signature_over_a_form_field", "crates/ui-egui/tests/fill_sign.rs::scanned_signatures_are_framed_cleaned_adjusted_placed_and_remembered", "crates/ui-egui/tests/fill_sign.rs::scan_cancel_errors_and_stale_answers_preserve_saved_signatures", "crates/automation/tests/automation.rs::scanned_signatures_through_tools_drop_the_paper_and_report_bad_arguments", "crates/engine/src/signature_scan.rs::a_photo_is_scanned_into_a_trimmed_transparent_signature_in_its_ink_colour"]
notes = "signature and initials, typed (Dancing Script outlines as filled paths, no font embedded), drawn or imported from local PNG/JPEG images; all three are placed upright and anchored as displayed on pages with /Rotate (#299); saved in settings with preview cards and add/change/remove controls; changes leave placed annotations untouched; images preserve alpha, aspect ratio and EXIF orientation, fit within 150 pt by 32 pt (24 pt for initials), and are normalized without source paths or metadata; image previews follow the pointer at placement size with zoom and rotation; signatures/initials place once then select the new annotation; while a Fill & Sign tool is active, marks already placed are picked up as with Select (hover shows the move cursor, a click selects, a drag moves, the selected mark's handles resize) and the tool places only away from them; embedded image signatures resize and move live, hiding the frame during movement and restoring it on release; corner handles preserve the original aspect ratio of the embedded image with the opposite corner anchored, while edge midpoint handles stretch width or height independently; cached background layers preserve page content and alpha, including reopened/encrypted documents; Escape cancels a gesture without committing; stamps resize through selection handles or comment_edit with their original appearance; holding Shift on a corner handle keeps any resizable comment's current proportions (#2199); native and web asynchronous pickers; fill_sign_add accepts path instead of text; image imports capped at 4 MiB, 32 megapixels and 8192 pixels per side, larger than 4 megapixels or 4096 pixels per side scaled down to fit (#1075); Scan tab (fill_sign_add scan/sensitivity headless): a phone photo of a signature on paper (PNG/JPEG within the same image limits, EXIF-oriented, kept at most 4 megapixels) is framed automatically (all words; table, sheet edge and dust ignored; a sideways signature turned upright), the frame can be moved, resized or redrawn and the photo turned in quarter turns, and an ink-sensitivity slider (0-100) controls how faint ink is kept; the paper is estimated by a morphological closing, so shadows and uneven light are removed, and the result is trimmed ink in its own colour on transparency, saved like an imported image. Still missing: perspective correction of a skewed sheet, live camera capture, keyboard control of the frame, and processing off the UI thread (bounded by the 4-megapixel working copy; quick 900 px previews while dragging)"
evidence = ["crates/ui-egui/tests/fill_sign.rs::drawn_signatures_resize_from_their_corners", "crates/ui-egui/tests/fill_sign.rs::drawn_and_typed_signatures_stay_upright_on_rotated_pages", "crates/automation/tests/automation.rs::typed_signatures_stay_upright_on_rotated_pages", "crates/ui-egui/tests/fill_sign.rs::image_signature_corners_restore_original_aspect_after_edge_resize_and_reopen", "crates/ui-egui/src/comments.rs::proportional_corners_keep_the_opposite_anchor_and_edges_stretch_one_axis", "crates/ui-egui/tests/fill_sign.rs::image_signature_live_resize_and_move_preserve_background_and_hide_moving_handles", "crates/ui-egui/tests/fill_sign.rs::embedded_image_signature_live_gestures_follow_document_and_view_rotation", "crates/automation/tests/automation.rs::image_signature_preview_layers_are_read_only_and_survive_encrypted_save", "crates/engine/src/signature_image.rs::embedded_signature_preview_preserves_alpha_without_editing_the_document", "crates/engine/src/signature_image.rs::oversized_embedded_signature_preview_is_rejected_before_decoding", "crates/ui-egui/tests/fill_sign.rs::image_signature_preview_follows_pointer_at_page_size_with_zoom_and_rotation", "crates/ui-egui/tests/fill_sign.rs::image_signature_placement_selects_resize_handles_and_requires_reselecting_to_repeat", "crates/ui-egui/tests/fill_sign.rs::typed_signatures_and_initials", "crates/ui-egui/tests/fill_sign.rs::signing_draws_a_signature_once_and_places_it", "crates/ui-egui/tests/fill_sign.rs::changing_saved_signatures_and_initials_preserves_placed_marks", "crates/ui-egui/tests/fill_sign.rs::changing_drawn_signatures_can_be_cancelled_or_switched_to_type", "crates/ui-egui/tests/fill_sign.rs::saved_signature_cards_remove_and_add_without_changing_the_document", "crates/ui-egui/tests/fill_sign.rs::long_typed_names_fit_the_placed_signature_and_keep_every_outline", "crates/ui-egui/src/fill_sign.rs::long_signature_previews_have_clear_margins", "crates/ui-egui/tests/control.rs::saved_signature_can_be_changed_through_the_control_channel", "crates/fonts/src/script.rs::a_name_becomes_outlines", "crates/fonts/src/script.rs::long_names_keep_every_glyph_with_bounded_outline_work", "crates/automation/tests/automation.rs::fill_and_sign_through_tools", "crates/ui-egui/tests/fill_sign.rs::image_signatures_and_initials_can_be_imported_placed_and_remembered", "crates/ui-egui/tests/fill_sign.rs::image_import_cancel_clear_and_errors_preserve_saved_signatures", "crates/engine/src/signature_image.rs::image_signatures_preserve_alpha_and_fit_the_signature_box", "crates/engine/src/signature_image.rs::invalid_and_oversized_signature_images_are_rejected", "crates/engine/src/signature_image.rs::large_signature_images_are_scaled_down", "crates/engine/src/signature_image.rs::jpeg_signatures_are_normalized_to_png", "crates/engine/src/signature_image.rs::jpeg_signature_orientation_is_applied_before_preview_and_embedding", "crates/engine/src/signature_image.rs::very_wide_image_signatures_keep_the_requested_rectangle", "crates/automation/tests/automation.rs::image_signatures_through_tools_preserve_transparency_and_survive_save", "crates/ui-egui/tests/fill_sign.rs::fill_and_sign_tools_pick_up_placed_marks_to_move_and_resize_them", "crates/ui-egui/tests/fill_sign.rs::select_tool_picks_up_a_signature_over_a_form_field", "crates/ui-egui/tests/fill_sign.rs::scanned_signatures_are_framed_cleaned_adjusted_placed_and_remembered", "crates/ui-egui/tests/fill_sign.rs::scan_cancel_errors_and_stale_answers_preserve_saved_signatures", "crates/automation/tests/automation.rs::scanned_signatures_through_tools_drop_the_paper_and_report_bad_arguments", "crates/engine/src/signature_scan.rs::a_photo_is_scanned_into_a_trimmed_transparent_signature_in_its_ink_colour", "crates/ui-egui/tests/fill_sign.rs::undo_button_and_ctrl_z_take_back_one_drawn_stroke_at_a_time", "crates/ui-egui/src/fill_sign.rs::undo_removes_the_last_stroke_and_skips_empty_ones", "crates/ui-egui/src/comments.rs::shift_keeps_the_current_proportions_unless_the_box_has_its_own"]
notes = "signature and initials, typed (Dancing Script outlines as filled paths, no font embedded), drawn (an Undo button and Ctrl/Cmd+Z in the pad take back the last stroke; Clear empties it) or imported from local PNG/JPEG images; all three are placed upright and anchored as displayed on pages with /Rotate (#299); saved in settings with preview cards and add/change/remove controls; changes leave placed annotations untouched; images preserve alpha, aspect ratio and EXIF orientation, fit within 150 pt by 32 pt (24 pt for initials), and are normalized without source paths or metadata; image previews follow the pointer at placement size with zoom and rotation; signatures/initials place once then select the new annotation; while a Fill & Sign tool is active, marks already placed are picked up as with Select (hover shows the move cursor, a click selects, a drag moves, the selected mark's handles resize) and the tool places only away from them; embedded image signatures resize and move live, hiding the frame during movement and restoring it on release; corner handles preserve the original aspect ratio of the embedded image with the opposite corner anchored, while edge midpoint handles stretch width or height independently; cached background layers preserve page content and alpha, including reopened/encrypted documents; Escape cancels a gesture without committing; stamps resize through selection handles or comment_edit with their original appearance; holding Shift on a corner handle keeps any resizable comment's current proportions (#2199); native and web asynchronous pickers; fill_sign_add accepts path instead of text; image imports capped at 4 MiB, 32 megapixels and 8192 pixels per side, larger than 4 megapixels or 4096 pixels per side scaled down to fit (#1075); Scan tab (fill_sign_add scan/sensitivity headless): a phone photo of a signature on paper (PNG/JPEG within the same image limits, EXIF-oriented, kept at most 4 megapixels) is framed automatically (all words; table, sheet edge and dust ignored; a sideways signature turned upright), the frame can be moved, resized or redrawn and the photo turned in quarter turns, and an ink-sensitivity slider (0-100) controls how faint ink is kept; the paper is estimated by a morphological closing, so shadows and uneven light are removed, and the result is trimmed ink in its own colour on transparency, saved like an imported image. Still missing: perspective correction of a skewed sheet, live camera capture, keyboard control of the frame, and processing off the UI thread (bounded by the 4-megapixel working copy; quick 900 px previews while dragging)"
[[feature]]
id = "form.fill-sign-flatten"
@@ -5821,10 +5812,8 @@ milestone = "M8"
status = "partial"
commands = []
tools = ["doc_audit_redactions"]
evidence = ["crates/audit/src/tests.rs::faux_redaction_is_found", "crates/audit/src/tests.rs::background_before_text_is_not_flagged", "crates/audit/src/tests.rs::light_shading_after_text_is_not_flagged", "crates/audit/src/tests.rs::graphics_state_carries_across_streams", "crates/audit/src/tests.rs::audit_fails_closed_on_undecodable_stream", "crates/audit/src/tests.rs::unapplied_redact_mark_is_found", "crates/audit/src/tests.rs::applied_redaction_at_the_end_of_a_line_is_not_flagged", "crates/audit/src/tests.rs::faux_redaction_at_the_end_of_a_line_is_found", "crates/audit/src/tests.rs::faux_redaction_over_a_short_value_after_a_long_label_is_found", "crates/audit/src/tests.rs::faux_redaction_over_mirrored_text_is_found", "crates/audit/src/tests.rs::faux_redaction_over_a_partial_value_after_a_long_label_is_found", "crates/audit/src/tests.rs::faux_redaction_over_a_partial_value_after_a_long_label_on_a_rotated_page_is_found", "crates/audit/src/tests.rs::faux_redactions_with_multiple_covers_lines_and_spacing_are_found", "crates/automation/tests/automation.rs::auditing_faux_redactions_through_tools", "crates/automation/tests/automation.rs::an_applied_redaction_at_the_end_of_a_line_audits_clean", "crates/audit/src/tests.rs::rectangle_drawn_as_a_path_covers_text_like_re", "crates/audit/src/tests.rs::non_rectangular_path_is_not_a_cover"]
notes = "New pdfcraft-audit crate. A filled rectangle (re + fill, or a single m/l subpath whose corners form an axis-aligned rectangle, #1510; opaque) counts as a cover only when painted after the text it overlaps and dark enough to hide it (WCAG contrast < 3.0); page backgrounds, table shading and highlights are not flagged. Coverage is measured on the words a line draws, not its box, so the gap an applied redaction leaves is not counted as hidden text; a cover over 40% of the line's box still counts when it has a positive-area intersection with one word, so a short value after a long label is found. A cover over less than 40% of both a line's box and its words (a short value in the middle of a long line) is still not flagged. Graphics state carries across a page's content streams. Fails closed: undecodable content is an error, never a clean bill. Also flags /Redact annotations never applied. CLI: pdfcraft-cli audit (fails when findings exist)."
evidence = ["crates/audit/src/tests.rs::faux_redaction_is_found", "crates/audit/src/tests.rs::background_before_text_is_not_flagged", "crates/audit/src/tests.rs::light_shading_after_text_is_not_flagged", "crates/audit/src/tests.rs::graphics_state_carries_across_streams", "crates/audit/src/tests.rs::audit_fails_closed_on_undecodable_stream", "crates/audit/src/tests.rs::unapplied_redact_mark_is_found", "crates/automation/tests/automation.rs::auditing_faux_redactions_through_tools", "apps/pdfcraft-cli/tests/audit.rs::audit_rectangles_match_the_tool_in_top_left_points", "crates/audit/src/tests.rs::rectangle_drawn_as_a_path_covers_text_like_re", "crates/audit/src/tests.rs::non_rectangular_path_is_not_a_cover"]
notes = "New pdfcraft-audit crate. A filled rectangle (re + fill, or a single m/l subpath whose corners form an axis-aligned rectangle, #1510; opaque) counts as a cover only when painted after the text it overlaps and dark enough to hide it (WCAG contrast < 3.0); page backgrounds, table shading and highlights are not flagged. Graphics state carries across a page's content streams. Fails closed: undecodable content is an error, never a clean bill. Also flags /Redact annotations never applied. CLI: pdfcraft-cli audit (fails when findings exist). Tool and CLI report rectangles in points from the top-left of the displayed page, after /Rotate and rounded to 0.01 pt, like the other tools."
evidence = ["crates/audit/src/tests.rs::faux_redaction_is_found", "crates/audit/src/tests.rs::background_before_text_is_not_flagged", "crates/audit/src/tests.rs::light_shading_after_text_is_not_flagged", "crates/audit/src/tests.rs::graphics_state_carries_across_streams", "crates/audit/src/tests.rs::audit_fails_closed_on_undecodable_stream", "crates/audit/src/tests.rs::unapplied_redact_mark_is_found", "crates/audit/src/tests.rs::applied_redaction_at_the_end_of_a_line_is_not_flagged", "crates/audit/src/tests.rs::faux_redaction_at_the_end_of_a_line_is_found", "crates/audit/src/tests.rs::faux_redaction_over_a_short_value_after_a_long_label_is_found", "crates/audit/src/tests.rs::faux_redaction_over_mirrored_text_is_found", "crates/audit/src/tests.rs::faux_redaction_over_a_partial_value_after_a_long_label_is_found", "crates/audit/src/tests.rs::faux_redaction_over_a_partial_value_after_a_long_label_on_a_rotated_page_is_found", "crates/audit/src/tests.rs::faux_redactions_with_multiple_covers_lines_and_spacing_are_found", "crates/automation/tests/automation.rs::auditing_faux_redactions_through_tools", "crates/automation/tests/automation.rs::an_applied_redaction_at_the_end_of_a_line_audits_clean", "crates/audit/src/tests.rs::rectangle_drawn_as_a_path_covers_text_like_re", "crates/audit/src/tests.rs::non_rectangular_path_is_not_a_cover", "apps/pdfcraft-cli/tests/audit.rs::audit_rectangles_match_the_tool_in_top_left_points"]
notes = "New pdfcraft-audit crate. A filled rectangle (re + fill, or a single m/l subpath whose corners form an axis-aligned rectangle, #1510; opaque) counts as a cover only when painted after the text it overlaps and dark enough to hide it (WCAG contrast < 3.0); page backgrounds, table shading and highlights are not flagged. Coverage is measured on the words a line draws, not its box, so the gap an applied redaction leaves is not counted as hidden text; a cover over 40% of the line's box still counts when it has a positive-area intersection with one word, so a short value after a long label is found. A cover over less than 40% of both a line's box and its words (a short value in the middle of a long line) is still not flagged. Graphics state carries across a page's content streams. Fails closed: undecodable content is an error, never a clean bill. Also flags /Redact annotations never applied. CLI: pdfcraft-cli audit (fails when findings exist). Tool and CLI report rectangles in points from the top-left of the displayed page, after /Rotate and rounded to 0.01 pt, like the other tools."
[[feature]]
id = "protect.remove-hidden-on-close"
@@ -7273,8 +7262,6 @@ commands = ["optimize.advanced"]
tools = ["doc_optimize"]
evidence = ["crates/ui-egui/tests/create.rs::the_pdf_optimizer_dialog_saves_an_optimized_copy", "crates/automation/tests/automation.rs::optimizing_through_tools", "crates/ui-egui/src/optimize_ui.rs::the_progress_card_shows_the_stage_and_cancels", "crates/engine/src/tests.rs::an_optimize_job_reports_its_stages_and_can_be_cancelled", "crates/optimize/tests/optimize.rs::images_and_forms_nothing_draws_are_dropped", "crates/automation/tests/automation.rs::a_deleted_image_is_not_kept_by_reduce_or_optimize", "crates/ui-egui/tests/create.rs::the_pdf_optimizer_dialog_picks_a_preset", "crates/automation/tests/automation.rs::a_page_printed_in_strips_gets_small_through_tools", "crates/optimize/tests/optimize.rs::the_mobile_preset_makes_a_printed_page_small"]
notes = "Settings menu with the Standard and Mobile presets (Custom once an option changes; doc_optimize preset), Images (with Merge image fragments: a page picture drawn as one-pixel strips, as some PDF printers write it, is put back together so it can be resampled), Discard Objects, Discard User Data, Clean Up (also drops images and forms the pages list but never draw, such as deleted images); runs in the background with a progress bar (per image) and Cancel; no Fonts or Transparency panels, no saved custom presets, no space audit"
evidence = ["crates/ui-egui/tests/create.rs::the_pdf_optimizer_dialog_saves_an_optimized_copy", "crates/automation/tests/automation.rs::optimizing_through_tools", "crates/ui-egui/src/optimize_ui.rs::the_progress_card_shows_the_stage_and_cancels", "crates/engine/src/tests.rs::an_optimize_job_reports_its_stages_and_can_be_cancelled", "crates/optimize/tests/optimize.rs::images_and_forms_nothing_draws_are_dropped", "crates/automation/tests/automation.rs::a_deleted_image_is_not_kept_by_reduce_or_optimize"]
notes = "Images, Discard Objects, Discard User Data, Clean Up (also drops images and forms the pages list but never draw, such as deleted images); runs in the background with a progress bar (per image) and Cancel; no Fonts or Transparency panels"
[[feature]]
id = "optimize.downsample-images"
@@ -7285,10 +7272,8 @@ milestone = "M11"
status = "partial"
commands = ["optimize.advanced"]
tools = ["doc_optimize"]
evidence = ["crates/optimize/tests/optimize.rs::images_are_measured_where_drawn_and_downsampled", "crates/optimize/tests/optimize.rs::settings_choose_what_happens", "crates/automation/tests/automation.rs::optimizing_through_tools", "crates/optimize/tests/optimize.rs::a_page_picture_printed_as_one_pixel_strips_is_merged_into_one_image", "crates/optimize/tests/optimize.rs::images_that_do_not_tile_a_picture_are_left_alone"]
notes = "colour and grayscale, bicubic, effective resolution measured where drawn; image fragments stacked edge to edge are merged first (side-by-side tiles not yet); monochrome not yet"
evidence = ["crates/optimize/tests/optimize.rs::images_are_measured_where_drawn_and_downsampled", "crates/optimize/tests/optimize.rs::a_deeply_nested_form_is_measured_at_its_largest_use", "crates/optimize/tests/optimize.rs::images_are_not_resampled_when_some_uses_could_not_be_measured", "crates/optimize/tests/optimize.rs::settings_choose_what_happens", "crates/automation/tests/automation.rs::optimizing_through_tools"]
notes = "colour and grayscale, bicubic, effective resolution measured where drawn (every use, through nested forms up to 12 deep; when a use can't be measured, nothing is resampled); monochrome not yet"
evidence = ["crates/optimize/tests/optimize.rs::images_are_measured_where_drawn_and_downsampled", "crates/optimize/tests/optimize.rs::settings_choose_what_happens", "crates/automation/tests/automation.rs::optimizing_through_tools", "crates/optimize/tests/optimize.rs::a_page_picture_printed_as_one_pixel_strips_is_merged_into_one_image", "crates/optimize/tests/optimize.rs::images_that_do_not_tile_a_picture_are_left_alone", "crates/optimize/tests/optimize.rs::a_deeply_nested_form_is_measured_at_its_largest_use", "crates/optimize/tests/optimize.rs::images_are_not_resampled_when_some_uses_could_not_be_measured"]
notes = "colour and grayscale, bicubic, effective resolution measured where drawn (every use, through nested forms up to 12 deep; when a use can't be measured, nothing is resampled); image fragments stacked edge to edge are merged first (side-by-side tiles not yet); monochrome not yet"
[[feature]]
id = "optimize.recompress-images"
@@ -9952,8 +9937,6 @@ status = "partial"
commands = ["app.preferences"]
tools = []
evidence = ["crates/ui-egui/src/commands.rs::localized_shortcut_labels_follow_language_and_platform", "crates/ui-egui/src/i18n/mod.rs::keyboard_key_names_have_a_separate_context_and_english_fallback", "crates/ui-egui/tests/commands.rs::localized_shortcuts_update_immediately_and_keep_command_ids_and_key_bindings", "crates/ui-egui/src/i18n/mod.rs::windows_display_language_resolves_chinese_scripts", "crates/ui-egui/src/i18n/mod.rs::neutral_locale_does_not_override_display_language", "crates/ui-egui/src/i18n/mod.rs::windows_display_language_query_returns_a_locale_tag", "crates/ui-egui/src/i18n/mod.rs::language_persists_and_invalid_input_keeps_current_language", "crates/ui-egui/src/i18n/mod.rs::bundled_catalogs_are_consistent", "crates/ui-egui/src/i18n/mod.rs::arabic_is_registered_and_persists", "crates/ui-egui/src/i18n/mod.rs::arabic_lookups_are_in_display_order", "crates/ui-egui/src/i18n/mod.rs::arabic_plurals_follow_the_six_cldr_forms", "crates/ui-egui/src/i18n/mod.rs::arabic_covers_commands_and_catalogue", "crates/ui-egui/src/i18n/mod.rs::arabic_covers_ui_literals", "crates/ui-egui/src/i18n/mod.rs::arabic_labels_have_glyphs", "crates/ui-egui/src/bidi.rs::rtl_labels_keep_placeholders_whole", "crates/ui-egui/src/i18n/mod.rs::japanese_covers_commands_and_catalogue", "crates/ui-egui/src/i18n/mod.rs::simplified_chinese_covers_commands_and_catalogue", "crates/ui-egui/src/i18n/mod.rs::simplified_chinese_covers_ui_literals", "crates/ui-egui/src/i18n/mod.rs::simplified_chinese_history_and_diagnostics_preserve_user_values", "crates/ui-egui/tests/control.rs::simplified_chinese_signature_prompts_and_errors_keep_document_state", "crates/ui-egui/tests/control.rs::language_switch_preserves_document_and_command_ids", "crates/ui-egui/tests/control.rs::japanese_controls_and_search_keep_command_ids", "crates/ui-egui/tests/control.rs::japanese_dialogs_errors_and_custom_action_names", "crates/ui-egui/src/i18n/mod.rs::french_is_registered", "crates/ui-egui/src/i18n/mod.rs::french_covers_commands_and_catalogue", "crates/ui-egui/src/i18n/mod.rs::french_covers_ui_literals", "crates/ui-egui/src/i18n/mod.rs::french_history_and_diagnostics_preserve_user_values", "crates/ui-egui/src/i18n/mod.rs::german_certificate_subject_has_a_distinct_context", "crates/ui-egui/src/widgets.rs::search_placeholder_never_overlaps_the_shortcut", "crates/ui-egui/src/home.rs::translated_card_description_leaves_room_for_the_action", "crates/ui-egui/tests/control.rs::german_preferences_and_search_keep_command_ids", "crates/ui-egui/src/i18n/mod.rs::telugu_covers_commands_and_catalogue", "crates/ui-egui/src/i18n/mod.rs::telugu_labels_have_glyphs", "crates/ui-egui/src/i18n/mod.rs::hungarian_is_registered", "crates/ui-egui/src/i18n/mod.rs::hungarian_covers_commands_and_catalogue", "crates/ui-egui/src/i18n/mod.rs::hungarian_covers_ui_literals", "crates/ui-egui/src/i18n/mod.rs::hungarian_history_and_diagnostics_preserve_user_values", "crates/ui-egui/src/i18n/mod.rs::ukrainian_is_registered_and_persists", "crates/ui-egui/src/i18n/mod.rs::ukrainian_integer_plurals_cover_teens_and_large_counts", "crates/ui-egui/src/i18n/mod.rs::ukrainian_calendar_uses_month_case_for_dates", "crates/ui-egui/src/i18n/mod.rs::ukrainian_catalog_does_not_leave_english_ui_text", "crates/ui-egui/src/i18n/mod.rs::ukrainian_covers_commands_and_catalogue", "crates/ui-egui/src/i18n/mod.rs::ukrainian_covers_ui_literals", "crates/ui-egui/src/i18n/mod.rs::ukrainian_history_and_diagnostics_preserve_user_values", "crates/ui-egui/src/i18n/mod.rs::spanish_covers_ui_literals", "crates/ui-egui/src/i18n/mod.rs::spanish_covers_what_the_engine_says", "crates/ui-egui/src/i18n/mod.rs::spanish_keeps_the_holes_in_what_the_engine_says", "crates/print/src/tests.rs::wordings_are_said"]
notes = "Shortcut modifiers and key names use a dedicated catalog context across toolbar, menus, palette, Help and tooltips, with English fallback and macOS symbols preserved. Preferences switches Auto (system language)/English/Japanese/Simplified Chinese/Traditional Chinese/Czech/Brazilian Portuguese/German/Spanish/French/Telugu/Hungarian/Ukrainian/Italian/Arabic and persists; ui.set language auto|en|ja|zh-hans|zh-hant|cs|pt-br|de|es|fr|te|hu|uk|it|ar and ui.state expose it. TSV catalogs per language (same system as PhotoCraft); every dialog, panel and notice goes through tl!. Japanese, Traditional Chinese, Simplified Chinese, German, Spanish, French and Hungarian cover commands, dialogs, panels, shortcuts and history labels; Czech and Brazilian Portuguese the menus. French locales (fr, fr_FR, fr_CA, fr_BE) select the French catalog, German locales (de, de_DE, de_AT, de_CH) the German one, Hungarian locales (hu, hu_HU) the Hungarian one. German search hints reserve shortcut space, Home cards bound translated text, certificate Subject has its own context, and inserted tool names keep their capitalization. The sentences the layers below word for themselves (accessibility findings and the saved report, PDF/A problems, signature details, page-range mistakes, edit labels) are handed over apart from the document's own values and go through the catalog too, through said(&i18n::words); Spanish translates them and the other catalogs fall back to English. OS error details, and faults rather than mistakes, stay as they are. Auto detects the Windows display language after explicitly named supported locale overrides; neutral C/POSIX launcher locales are skipped on Windows, with Chinese script/region mapping and English fallback. Arabic (ar and every ar-* locale) covers commands, dialogs and panels with the six CLDR plural forms; its labels are put into display order when the catalog loads, because egui joins Arabic letters but lays words out left to right. Missing: a right-to-left layout (Arabic labels sit in the left-to-right layout, a wrapped Arabic paragraph breaks at the wrong end, and text typed into a field is not reordered), Czech and Brazilian Portuguese dialogs, Chinese and Telugu font delivery in releases (the pinned craft-fonts revision has an Arabic face; its Noto Sans CJK SC is never embedded, AGENTS.md §1.1), the engine's sentences in the other catalogs, a full localization audit."
evidence = ["crates/ui-egui/src/commands.rs::localized_shortcut_labels_follow_language_and_platform", "crates/ui-egui/src/i18n/mod.rs::keyboard_key_names_have_a_separate_context_and_english_fallback", "crates/ui-egui/tests/commands.rs::localized_shortcuts_update_immediately_and_keep_command_ids_and_key_bindings", "crates/ui-egui/src/i18n/mod.rs::windows_display_language_resolves_chinese_scripts", "crates/ui-egui/src/i18n/mod.rs::neutral_locale_does_not_override_display_language", "crates/ui-egui/src/i18n/mod.rs::windows_display_language_query_returns_a_locale_tag", "crates/ui-egui/src/i18n/mod.rs::language_persists_and_invalid_input_keeps_current_language", "crates/ui-egui/src/i18n/mod.rs::bundled_catalogs_are_consistent", "crates/ui-egui/src/i18n/mod.rs::arabic_is_registered_and_persists", "crates/ui-egui/src/i18n/mod.rs::arabic_lookups_are_in_display_order", "crates/ui-egui/src/i18n/mod.rs::arabic_plurals_follow_the_six_cldr_forms", "crates/ui-egui/src/i18n/mod.rs::arabic_covers_commands_and_catalogue", "crates/ui-egui/src/i18n/mod.rs::arabic_covers_ui_literals", "crates/ui-egui/src/i18n/mod.rs::arabic_labels_have_glyphs", "crates/ui-egui/src/bidi.rs::rtl_labels_keep_placeholders_whole", "crates/ui-egui/src/i18n/mod.rs::japanese_covers_commands_and_catalogue", "crates/ui-egui/src/i18n/mod.rs::simplified_chinese_covers_commands_and_catalogue", "crates/ui-egui/src/i18n/mod.rs::simplified_chinese_covers_ui_literals", "crates/ui-egui/src/i18n/mod.rs::simplified_chinese_history_and_diagnostics_preserve_user_values", "crates/ui-egui/tests/control.rs::simplified_chinese_signature_prompts_and_errors_keep_document_state", "crates/ui-egui/tests/control.rs::language_switch_preserves_document_and_command_ids", "crates/ui-egui/tests/control.rs::japanese_controls_and_search_keep_command_ids", "crates/ui-egui/tests/control.rs::japanese_dialogs_errors_and_custom_action_names", "crates/ui-egui/src/i18n/mod.rs::french_is_registered", "crates/ui-egui/src/i18n/mod.rs::french_covers_commands_and_catalogue", "crates/ui-egui/src/i18n/mod.rs::french_covers_ui_literals", "crates/ui-egui/src/i18n/mod.rs::french_history_and_diagnostics_preserve_user_values", "crates/ui-egui/src/i18n/mod.rs::german_certificate_subject_has_a_distinct_context", "crates/ui-egui/src/widgets.rs::search_placeholder_never_overlaps_the_shortcut", "crates/ui-egui/src/home.rs::translated_card_description_leaves_room_for_the_action", "crates/ui-egui/tests/control.rs::german_preferences_and_search_keep_command_ids", "crates/ui-egui/src/i18n/mod.rs::telugu_covers_commands_and_catalogue", "crates/ui-egui/src/i18n/mod.rs::telugu_labels_have_glyphs", "crates/ui-egui/src/i18n/mod.rs::hungarian_is_registered", "crates/ui-egui/src/i18n/mod.rs::hungarian_covers_commands_and_catalogue", "crates/ui-egui/src/i18n/mod.rs::hungarian_covers_ui_literals", "crates/ui-egui/src/i18n/mod.rs::hungarian_history_and_diagnostics_preserve_user_values", "crates/ui-egui/src/i18n/mod.rs::ukrainian_is_registered_and_persists", "crates/ui-egui/src/i18n/mod.rs::ukrainian_integer_plurals_cover_teens_and_large_counts", "crates/ui-egui/src/i18n/mod.rs::ukrainian_calendar_uses_month_case_for_dates", "crates/ui-egui/src/i18n/mod.rs::ukrainian_catalog_does_not_leave_english_ui_text", "crates/ui-egui/src/i18n/mod.rs::ukrainian_covers_commands_and_catalogue", "crates/ui-egui/src/i18n/mod.rs::ukrainian_covers_ui_literals", "crates/ui-egui/src/i18n/mod.rs::ukrainian_history_and_diagnostics_preserve_user_values", "crates/ui-egui/src/i18n/mod.rs::spanish_covers_what_the_engine_says", "crates/ui-egui/src/i18n/mod.rs::spanish_keeps_the_holes_in_what_the_engine_says", "crates/print/src/tests.rs::wordings_are_said"]
notes = "Shortcut modifiers and key names use a dedicated catalog context across toolbar, menus, palette, Help and tooltips, with English fallback and macOS symbols preserved. Preferences switches Auto (system language)/English/Japanese/Simplified Chinese/Traditional Chinese/Czech/Brazilian Portuguese/German/Spanish/French/Telugu/Hungarian/Ukrainian/Italian/Arabic and persists; ui.set language auto|en|ja|zh-hans|zh-hant|cs|pt-br|de|es|fr|te|hu|uk|it|ar and ui.state expose it. TSV catalogs per language (same system as PhotoCraft); every dialog, panel and notice goes through tl!. Japanese, Traditional Chinese, Simplified Chinese, German, Spanish, French and Hungarian cover commands, dialogs, panels, shortcuts and history labels; Czech and Brazilian Portuguese the menus. French locales (fr, fr_FR, fr_CA, fr_BE) select the French catalog, German locales (de, de_DE, de_AT, de_CH) the German one, Hungarian locales (hu, hu_HU) the Hungarian one. German search hints reserve shortcut space, Home cards bound translated text, certificate Subject has its own context, and inserted tool names keep their capitalization. The sentences the layers below word for themselves (accessibility findings and the saved report, PDF/A problems, signature details, page-range mistakes, edit labels) are handed over apart from the document's own values and go through the catalog too, through said(&i18n::words); Spanish and Italian translate them and the other catalogs fall back to English. OS error details, and faults rather than mistakes, stay as they are. Auto detects the Windows display language after explicitly named supported locale overrides; neutral C/POSIX launcher locales are skipped on Windows, with Chinese script/region mapping and English fallback. Arabic (ar and every ar-* locale) covers commands, dialogs and panels with the six CLDR plural forms; its labels are put into display order when the catalog loads, because egui joins Arabic letters but lays words out left to right. Missing: a right-to-left layout (Arabic labels sit in the left-to-right layout, a wrapped Arabic paragraph breaks at the wrong end, and text typed into a field is not reordered), Czech and Brazilian Portuguese dialogs, Chinese and Telugu font delivery in releases (the pinned craft-fonts revision has an Arabic face; its Noto Sans CJK SC is never embedded, AGENTS.md §1.1), the engine's sentences in the other catalogs, a full localization audit."
[[feature]]
@@ -10110,8 +10093,7 @@ status = "partial"
commands = []
tools = []
evidence = []
notes = "Release builds provide signed and notarized macOS artifacts and code-signed Windows installers when signing credentials are configured (packaging/macos/package.sh; packaging/windows/sign.ps1; README.md). Linux AppImage, Flatpak, deb and rpm packages ship, but are not code-signed. Windows MSI creates a Start menu shortcut, an optional (default on) desktop shortcut and an optional (default on, INSTALLPDFCONTEXTMENU) \"Edit with PdfCraft\" File Explorer menu entry for PDF files (#682), and confirms full-UI completion with Finish (#143); packaging/windows/test-msi.ps1 checks the compiled MSI and windows-arm64.yml checks installation/removal. PDF files use a dedicated embedded document icon when PdfCraft is chosen as the default reader (confirmed in Windows Explorer); first-page thumbnails remain unsupported."
notes = "Windows MSI creates a Start menu shortcut, an optional (default on) desktop shortcut, an optional (default on, INSTALLPDFCONTEXTMENU) \"Edit with PdfCraft\" File Explorer menu entry for PDF files (#682) and an optional (default on, INSTALLIMAGECONTEXTMENU) \"Create PDF with PdfCraft…\" entry for images; an update keeps whichever of the three the installed version has (#1058, #305). It confirms full-UI completion with Finish (#143); packaging/windows/test-msi.ps1 checks the compiled MSI and windows-arm64.yml checks installation/removal. PDF files use a dedicated embedded document icon when PdfCraft is chosen as the default reader (confirmed in Windows Explorer); first-page thumbnails remain unsupported. Signed installers across platforms remain planned."
notes = "Release builds provide signed and notarized macOS artifacts and code-signed Windows installers when signing credentials are configured (packaging/macos/package.sh; packaging/windows/sign.ps1; README.md). Linux AppImage, Flatpak, deb and rpm packages ship, but are not code-signed. Windows MSI creates a Start menu shortcut, an optional (default on) desktop shortcut, an optional (default on, INSTALLPDFCONTEXTMENU) \"Edit with PdfCraft\" File Explorer menu entry for PDF files (#682) and an optional (default on, INSTALLIMAGECONTEXTMENU) \"Create PDF with PdfCraft…\" entry for images; an update keeps whichever of the three the installed version has (#1058, #305). It confirms full-UI completion with Finish (#143); packaging/windows/test-msi.ps1 checks the compiled MSI and windows-arm64.yml checks installation/removal. PDF files use a dedicated embedded document icon when PdfCraft is chosen as the default reader (confirmed in Windows Explorer); first-page thumbnails remain unsupported."
[[feature]]
id = "misc.web-deploy"