From a0d68dafd2752168490a422b94fcbdad4a2f8b39 Mon Sep 17 00:00:00 2001 From: spacekomet <11369225+spacekomet@users.noreply.github.com> Date: Sun, 11 Oct 2026 22:09:24 +0200 Subject: [PATCH 1/9] =?UTF-8?q?M14:=20Slovenian=20(Sloven=C5=A1=C4=8Dina)?= =?UTF-8?q?=20interface=20language=20(#1091)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add Slovenian translation * Slovene: a date language for the date picker; drop two stale PRINTCRAFT_MODELS rows * Slovene: rows for the labels that landed alongside this catalog; refresh hashes --------- Co-authored-by: spacekomet <1369225+spacekomet@users.noreply.github.com> Co-authored-by: Brandon Thomas --- ATTRIBUTION.md | 3 +- ATTRIBUTION.toml | 11 + crates/engine/src/dates.rs | 24 +- crates/ui-egui/src/i18n/mod.rs | 133 +- crates/ui-egui/src/i18n/sl.tsv | 2257 ++++++++++++++++++++++++++++++++ crates/ui-egui/tests/fonts.rs | 3 +- docs/localization.md | 7 +- 7 files changed, 2431 insertions(+), 7 deletions(-) create mode 100644 crates/ui-egui/src/i18n/sl.tsv diff --git a/ATTRIBUTION.md b/ATTRIBUTION.md index f4974b09..b0742370 100644 --- a/ATTRIBUTION.md +++ b/ATTRIBUTION.md @@ -4,7 +4,7 @@ Every asset PdfCraft includes, bundles or uses to build its published material, with its author, source and licence. The policy is in [AGENTS.md](AGENTS.md) §1. The machine-readable list, with SHA-256 hashes, is [ATTRIBUTION.toml](ATTRIBUTION.toml). Licence texts are kept beside the assets and summarised in [NOTICE](NOTICE). -## In this repository (265) +## In this repository (266) | Asset | Title | Author | Licence | Source | Used for | |---|---|---|---|---|---| @@ -273,6 +273,7 @@ Every asset PdfCraft includes, bundles or uses to build its published material, | `packaging/macos/dmg/background.tiff` | PdfCraft macOS DMG window background (rendered) | @XusBadia | MIT OR Apache-2.0 | Rendered from packaging/macos/dmg/background.svg by packaging/macos/dmg/generate.py | 1x + 2x HiDPI 16-colour palette TIFF copied into the macOS DMG by packaging/macos/package.sh; includes the app icon and the free LittleCMS built-in sRGB profile | | `crates/sign/tests/data/x509-rsa-sha1.pdf` | One-page PDF with a legacy adbe.x509.rsa_sha1 signature by the RSA test key | PdfCraft contributors | MIT OR Apache-2.0 | Contributor-original: hand-written PDF objects; signature made with `openssl dgst -sha1 -sign` over its byte ranges (see crates/sign/tests/data/README.md) | Signature validation tests (crates/sign/tests/pdf.rs): the legacy adbe.x509.rsa_sha1 format | | `assets/icons/monitor.svg` | Lucide icon "monitor" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/monitor.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs | +| `crates/ui-egui/src/i18n/sl.tsv` | Slovene 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 | Slovene UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs | ## Compiled in through dependencies (6) diff --git a/ATTRIBUTION.toml b/ATTRIBUTION.toml index 2c705f00..c0c4a095 100644 --- a/ATTRIBUTION.toml +++ b/ATTRIBUTION.toml @@ -3296,3 +3296,14 @@ licence_file = "assets/icons/LICENSE-lucide.txt" kind = "icon" usage = "UI icon, embedded via crates/ui-egui/src/icon_data.rs" sha256 = "d1a443233345724859de8e4fb48968ba91e885e12ddbd710701548bd91b39428" + +[[asset]] +path = "crates/ui-egui/src/i18n/sl.tsv" +title = "Slovene interface translation catalog" +author = "PdfCraft contributors" +source = "Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources" +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 = "8590f5acce1da70d6a3ce58de073bf4f86815651ff74a59625980c6c52e051b7" diff --git a/crates/engine/src/dates.rs b/crates/engine/src/dates.rs index d12e812b..ebda6a5f 100644 --- a/crates/engine/src/dates.rs +++ b/crates/engine/src/dates.rs @@ -56,7 +56,7 @@ const AR_DAYS: [&str; 7] = ["الأحد", "الاثنين", "الثلاثاء", /// The languages month and weekday names can be written in (Preferences ▸ Date format ▸ Language): /// the interface languages, in the same order and with the same names. -pub const DATE_LANGUAGES: [DateLanguage; 17] = [ +pub const DATE_LANGUAGES: [DateLanguage; 18] = [ DateLanguage { code: "en", name: "English", @@ -263,6 +263,28 @@ pub const DATE_LANGUAGES: [DateLanguage; 17] = [ days: ["niedziela", "poniedziałek", "wtorek", "środa", "czwartek", "piątek", "sobota"], days_short: ["niedz.", "pon.", "wt.", "śr.", "czw.", "pt.", "sob."], }, + DateLanguage { + code: "sl", + name: "Slovenščina", + months: ["januar", "februar", "marec", "april", "maj", "junij", "julij", "avgust", "september", "oktober", "november", "december"], + months_with_day: Some([ + "januarja", + "februarja", + "marca", + "aprila", + "maja", + "junija", + "julija", + "avgusta", + "septembra", + "oktobra", + "novembra", + "decembra", + ]), + months_short: ["jan.", "feb.", "mar.", "apr.", "maj", "jun.", "jul.", "avg.", "sep.", "okt.", "nov.", "dec."], + days: ["nedelja", "ponedeljek", "torek", "sreda", "četrtek", "petek", "sobota"], + days_short: ["ned.", "pon.", "tor.", "sre.", "čet.", "pet.", "sob."], + }, ]; /// The date language for `code` (any case), or `None` when there's none. diff --git a/crates/ui-egui/src/i18n/mod.rs b/crates/ui-egui/src/i18n/mod.rs index 447416c3..247d11b8 100644 --- a/crates/ui-egui/src/i18n/mod.rs +++ b/crates/ui-egui/src/i18n/mod.rs @@ -125,8 +125,18 @@ fn plural_polish(n: u64) -> usize { } } +/// Slovene (CLDR `sl`): one (1, 101, 201 ...) | two (2, 102 ...) | few (3, 4, 103 ...) | other. +fn plural_sl(n: u64) -> usize { + match n % 100 { + 1 => 0, + 2 => 1, + 3 | 4 => 2, + _ => 3, + } +} + /// The registry. English first: it is the fallback and the source language. -pub static LANGUAGES: [LangInfo; 17] = [ +pub static LANGUAGES: [LangInfo; 18] = [ LangInfo { code: "en", name: "English", source: "", plural: plural_one_other, rtl: false, catalog: OnceLock::new() }, LangInfo { code: "ja", name: "日本語", source: include_str!("ja.tsv"), plural: plural_none, rtl: false, catalog: OnceLock::new() }, // Simplified Chinese; `zh`, `zh-CN`, `zh-SG` and `zh-Hans-*` locales resolve here (see `candidates`). @@ -177,6 +187,8 @@ pub static LANGUAGES: [LangInfo; 17] = [ LangInfo { code: "ar", name: "العربية", source: include_str!("ar.tsv"), plural: plural_arabic, rtl: true, catalog: OnceLock::new() }, // Polish; every `pl-*` locale (`pl-pl`, `pl_pl.UTF-8`) resolves here. LangInfo { code: "pl", name: "Polski", source: include_str!("pl.tsv"), plural: plural_polish, rtl: false, catalog: OnceLock::new() }, + // Slovene; every `sl-*` locale (`sl-SI`) resolves here. Four plural forms (one, two, few, other). + LangInfo { code: "sl", name: "Slovenščina", source: include_str!("sl.tsv"), plural: plural_sl, rtl: false, catalog: OnceLock::new() }, ]; impl LangInfo { @@ -1897,6 +1909,125 @@ mod tests { set_current(Lang::EN); } + #[test] + fn slovene_is_registered_and_persists() { + let sl = Lang::from_code("sl").expect("sl registered"); + assert_eq!(sl.name(), "Slovenščina"); + assert_eq!(normalize_pref("SL"), Some("sl")); + for tag in ["sl", "sl-SI", "sl_SI.UTF-8", "SL-si", "sl_SI@variant"] { + assert_eq!(lang_from_tag(tag), Some(sl), "{tag}"); + } + assert_eq!(first_supported("sl-SI\r\nen-US"), Some(sl)); + assert_eq!(tr(sl, "File"), "Datoteka"); + assert_eq!(tr(sl, "Save as…"), "Shrani kot…"); + assert_eq!(tr(sl, "Bookmarks"), "Zaznamki"); + assert_eq!(tr(sl, "Layers"), "Plasti"); + assert_eq!(tr(sl, "Poročilo {n}.pdf"), "Poročilo {n}.pdf"); + assert_eq!(tr_ctx(sl, "comment menu", "Edit"), "Uredi"); + assert_eq!(tr_ctx(sl, "signature pad", "Type"), "Vtipkaj"); + assert_eq!(tr_ctx(sl, "action wizard", "Start"), "Začni"); + // one (1), two (2), few (3, 4), other (0, 5+) + assert_eq!((0..=5).map(|n| (sl.0.plural)(n)).collect::>(), [3, 0, 1, 2, 2, 3]); + let mut app = crate::PdfCraftApp::default(); + app.set_option("language", "SL").unwrap(); + assert_eq!(app.language, "sl"); + let mut restored = crate::PdfCraftApp::default(); + restored.restore(&app.persist()); + assert_eq!(restored.language, "sl"); + assert!(restored.set_option("language", "xx").is_err()); + assert_eq!(restored.language, "sl"); + } + + #[test] + fn slovene_plurals_cover_dual_few_teens_and_large_counts() { + let sl = Lang::from_code("sl").expect("sl registered"); + for (counts, page, field) in [ + (vec![1, 101, 201, 1001, 1101], "stran", "polje"), + (vec![2, 102, 202, 1002], "strani", "polji"), + (vec![3, 4, 103, 104, 1003], "strani", "polja"), + (vec![0, 5, 10, 11, 12, 13, 14, 15, 20, 21, 22, 23, 24, 25, 31, 100, 111, 112, 121, 122, 1000, u64::MAX], "strani", "polj"), + ] { + for n in counts { + assert_eq!(trn(sl, n, "{n} page", "{n} pages"), format!("{n} {page}")); + assert_eq!(trn(sl, n, "{n} field", "{n} fields"), format!("{n} {field}")); + } + } + } + + #[test] + fn slovene_diagnostics_preserve_user_values() { + let sl = Lang::from_code("sl").expect("sl registered"); + set_current(sl); + assert_eq!(action_label("Custom action {n}"), "Custom action {n}"); + assert_eq!(fmt(t("{n} pages selected"), &[("n", "3")]), "Izbranih strani: 3"); + assert_eq!(tr(sl, "CheckBox"), "Potrditveno polje"); + set_current(Lang::EN); + } + + /// Slovene translates every registered command and every All tools group, section and item. + #[test] + fn slovene_covers_commands_and_catalogue() { + let sl = Lang::from_code("sl").expect("sl registered"); + for command in pdfcraft_engine::commands::COMMANDS { + assert!(has(sl, command.label), "missing command: {}", command.label); + if let Some(menu) = command.menu { + assert!(has(sl, menu), "missing menu: {menu}"); + } + } + for group in pdfcraft_engine::catalog::TOOL_GROUPS { + assert!(has(sl, group.label), "missing group: {}", group.label); + for section in group.sections { + assert!(has(sl, section.title), "missing section: {}", section.title); + for item in section.items { + assert!(has(sl, item.label), "missing item: {}", item.label); + } + } + } + } + + /// New tl!("literal") labels must not silently fall back to English. + #[test] + fn slovene_covers_ui_literals() { + let sl = Lang::from_code("sl").expect("sl registered"); + let mut stack = vec![std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src")]; + let mut literals = std::collections::BTreeSet::new(); + while let Some(dir) = stack.pop() { + for entry in std::fs::read_dir(dir).expect("UI source directory") { + let path = entry.expect("UI source entry").path(); + if path.is_dir() { + if path.file_name().is_some_and(|name| name != "i18n") { + stack.push(path); + } + } else if path.extension().is_some_and(|ext| ext == "rs") { + let source = std::fs::read_to_string(path).expect("UI source file").replace("\r\n", "\n"); + let mut rest = source.split("#[cfg(test)]\nmod ").next().unwrap_or_default(); + while let Some((_, after)) = rest.split_once("tl!(\"") { + let mut escaped = false; + let end = after + .char_indices() + .find_map(|(i, c)| { + if c == '"' && !escaped { + return Some(i); + } + escaped = c == '\\' && !escaped; + None + }) + .expect("closed tl! literal"); + let (raw, tail) = after.split_at(end); + if tail.starts_with("\")") { + let label: String = serde_json::from_str(&format!("\"{raw}\"")).expect("UI literal escapes"); + literals.insert(label); + } + rest = tail.strip_prefix('"').expect("closing quote"); + } + } + } + } + assert!(literals.len() > 900, "source scan found only {} literals", literals.len()); + let missing: Vec<_> = literals.iter().filter(|label| !has(sl, label)).collect(); + assert!(missing.is_empty(), "untranslated Slovene UI literals: {missing:#?}"); + } + #[test] fn bulgarian_is_registered() { let bg = Lang::from_code("bg").expect("bg registered"); diff --git a/crates/ui-egui/src/i18n/sl.tsv b/crates/ui-egui/src/i18n/sl.tsv new file mode 100644 index 00000000..c6805c34 --- /dev/null +++ b/crates/ui-egui/src/i18n/sl.tsv @@ -0,0 +1,2257 @@ +# Slovene (sl) catalog. Format: see the header of ja.tsv +# (context source translation; @id, @plural and other contexts; escapes \n \t \\). +# Plural forms (see `plural_sl` in i18n/mod.rs): one (1, 101 ...) | two (2, 102 ...) | few (3, 4, 103 ...) | other. +# +# Clean-room: every entry was written from the plain meaning of the English label using ordinary +# Slovene computing vocabulary. No proprietary localisation resource, string table or product UI +# (Acrobat's included) was consulted or copied; add entries the same way. +# Product and technology names (PdfCraft, PDF, Acrobat JavaScript ...) are not translated; +# the trailing "…" of dialog commands is kept. + + Menu Meni + File Datoteka + Edit Urejanje + Pages Strani + View Pogled + Help Pomoč + Preferences Nastavitve + Preferences… Nastavitve… + Interface language Jezik vmesnika +@plural {n} page|{n} pages {n} stran|{n} strani|{n} strani|{n} strani + · not fixable here · tu ni mogoče popraviti + (page {p}) (stran {p}) + (searching… {s} of {p} pages) (iskanje… {s} od {p} strani) + ({s} not exported: {first}) (neizvoženih: {s}; {first}) + on page {p} na strani {p} + · password-protected · zaščiteno z geslom + % of all commits % vseh commitov + (none) (brez) + +Bin +Bin + +LOC +LOC + , Expires: , poteče: + 1 difference found 1 razlika najdena + 1 file 1 datoteka + 1 file done 1 datoteka dokončana + 1 file needs attention 1 datoteka zahteva pozornost + 1 instance 1 pojavitev + 1 issue 1 težava + 1 mark 1 oznaka + 1 page 1 stran + 1 page selected 1 stran izbrana + 1 page selected. 1 stran izbrana. + 180 degrees 180 stopinj + 9 digit, A letter, O letter or digit, X any character 9 števka, A črka, O črka ali števka, X poljuben znak + ; 1 page already had text ; 1 stran je že imela besedilo + ; failed: {list} ; neuspelo: {list} + ; {n} pages already had text ; strani, ki so že imele besedilo: {n} + ? of {n} ? od {n} + @{login}: {prs} PRs, {commits} commits, +{added} / −{deleted} lines (Δ {delta}), +{bin_added} / −{bin_deleted} binary assets, {first} – {last} @{login}: {prs} PR-jev, {commits} commitov, +{added} / −{deleted} vrstic (Δ {delta}), +{bin_added} / −{bin_deleted} binarnih virov, {first} – {last} + A button Gumb + A clean-room, open-source PDF application written in Rust. MIT OR Apache-2.0. Odprtokodna aplikacija za PDF, razvita po postopku clean-room in napisana v jeziku Rust. MIT OR Apache-2.0. + A diagonal CONFIDENTIAL watermark on every page. Diagonalni vodni žig ZAUPNO na vsaki strani. + A document needs at least one page: copy instead Dokument mora imeti vsaj eno stran: namesto tega kopirajte + A form field has additional actions Polje obrazca ima dodatna dejanja + A link Povezava + A script Skript + A stream refers to an external file Tok se sklicuje na zunanjo datoteko + A stream uses LZW compression Tok uporablja stiskanje LZW + A summary describes the table's purpose and layout. Add a Summary attribute to each table. Povzetek opisuje namen in postavitev tabele. Vsaki tabeli dodajte atribut Summary. + AI assistant Pomočnik za UI + About O programu + About PdfCraft O programu PdfCraft + Accepted Potrjeno + Accessibility Dostopnost + Accessibility Checker Preverjanje dostopnosti + Accessibility Checker Options Možnosti preverjanja dostopnosti + Accessibility permission flag Dovoljenje za dostopnost + Accessibility permission flag is set Dovoljenje za dostopnost je nastavljeno + Acrobat 3.0 and later (40-bit RC4) Acrobat 3.0 in novejši (40-bitni RC4) + Acrobat 5.0 and later (128-bit RC4) Acrobat 5.0 in novejši (128-bitni RC4) + Acrobat 7.0 and later (128-bit AES) Acrobat 7.0 in novejši (128-bitni AES) + Acrobat X and later (256-bit AES) Acrobat X in novejši (256-bitni AES) + Action Wizard Čarovnik za dejanja + Action Wizard… Čarovnik za dejanja… + Action name: Ime dejanja: + Actions Dejanja + Actions list Seznam dejanj + Actual Size Dejanska velikost + Actual size Dejanska velikost + Add Dodaj + Add Bates numbering… Dodaj Batesovo oštevilčenje… + Add Page Numbers Dodaj številke strani + Add Step Dodaj korak + Add a button Dodaj gumb + Add a callout Dodaj oblaček + Add a checkbox Dodaj potrditveno polje + Add a comment Dodaj komentar + Add a date field Dodaj polje za datum + Add a digital signature field Dodaj polje za digitalni podpis + Add a drop-down list Dodaj spustni seznam + Add a list box Dodaj polje seznama + Add a radio button Dodaj izbirni gumb + Add a reply Dodaj odgovor + Add a stamp Dodaj žig + Add a sticky note Dodaj opombo + Add a sticky note here Dodaj opombo tu + Add a text box Dodaj tekst + Add a text field Dodaj besedilno polje + Add alternate text Dodaj nadomestno besedilo + Add alternate text… Dodaj nadomestno besedilo… + Add an Action Dodaj dejanje + Add an image field Dodaj polje za sliko + Add arrow Dodaj puščico + Add at least two files Dodajte vsaj dve datoteki + Add background Dodaj ozadje + Add background… Dodaj ozadje… + Add bookmark Dodaj zaznamek + Add callout Dodaj namig + Add checkmark Dodaj kljukico + Add cloud Dodaj oblak + Add comments Dodaj komentarje + Add connected lines Dodaj povezane črte + Add content Dodaj vsebino + Add cross Dodaj X + Add dot Dodaj piko + Add drawing Dodaj risbo + Add field Dodaj polje + Add file attachment Dodaj priponko + Add files to combine Dodaj datoteke za združitev + Add files… Dodaj datoteke… + Add folder Dodaj mapo + Add folder and subfolders Dodaj mapo in podmape + Add folder and subfolders… Dodaj mapo in podmape… + Add folder… Dodaj mapo… + Add footer Dodaj nogo + Add form components Dodaj komponente obrazca + Add header Dodaj glavo + Add header & footer Dodaj glavo in nogo + Add header & footer… Dodaj glavo in nogo… + Add highlight Dodaj označbo + Add image Dodaj sliko + Add image… Dodaj sliko… + Add inserted text Dodaj vstavljeno besedilo + Add line Dodaj črto + Add link Dodaj povezavo + Add open documents Dodaj odprte dokumente + Add or edit links Dodaj ali uredi povezave + Add oval Dodaj elipso + Add polygon Dodaj mnogokotnik + Add printer marks Dodaj tiskarske oznake + Add rectangle Dodaj pravokotnik + Add redaction mark Dodaj oznako za počrnitev + Add search index Dodaj iskalni indeks + Add signature Dodaj podpis + Add squiggly underline Dodaj valovito podčrtavo + Add stamp Dodaj žig + Add sticky note Dodaj opombo + Add strikethrough Dodaj prečrtanje + Add text Dodaj besedilo + Add text box Dodaj besedilni okvir + Add the PDFs to combine Dodajte datoteke PDF za združitev + Add the choices people pick from. Dodajte možne izbire. + Add the steps this action runs, in order. Dodajte korake, ki jih to dejanje izvede, po vrstnem redu. + Add to Trusted Certificates Dodaj med zaupanja vredna potrdila + Add to trusted certificates Dodaj med zaupanja vredna potrdila + Add underline Dodaj podčrtavo + Add watermark Dodaj vodni žig + Add watermark… Dodaj vodni žig… + Add {what} Dodaj {what} + Added 1 comment 1 komentar dodan + Added more than once Večkrat dodano + Added the first {n} PDFs in {folder} Dodanih je prvih {n} datotek PDF v mapi {folder} + Added {n} comments Dodanih komentarjev: {n} + Adding images arrives on the web with file pickers for images Dodajanje slik bo v spletni različici na voljo z izbirniki slikovnih datotek + Adds invisible text over each word; the page image is not changed Doda nevidno besedilo čez vsako besedo; slika strani se ne spremeni + Advanced Napredno + Advanced Options {chevron} Napredne možnosti {chevron} + Advanced optimization Napredna optimizacija + Advanced options Napredne možnosti + Advanced search… Napredno iskanje… + Algorithm Algoritem + Align Poravnaj + Align fields Poravnaj polja + Align left Poravnaj levo + Align right Poravnaj desno + Alignment: Poravnava: + All Vse + All annotations are tagged Vse opombe so označene + All document contents Vsa vsebina dokumenta + All document contents except metadata Vsa vsebina dokumenta razen metapodatkov + All form fields are tagged Vsa polja obrazca so označena + All form fields have description Vsa polja obrazca imajo opis + All multimedia objects are tagged Vsi večpredstavnostni objekti so označeni + All open documents Vsi odprti dokumenti + All page content is tagged Vsa vsebina strani je označena + All pages Vse strani + All pages in document Vse strani v dokumentu + All pages in range Vse strani v obsegu + All tools Vsa orodja + Allow Rich Text Formatting Dovoli oblikovano besedilo (Rich Text) + Allow user to enter custom text Uporabniku dovoli vnos poljubnega besedila + Allowed Dovoljeno + Alternate Text Nadomestno besedilo + Alternate text Nadomestno besedilo + Alternate text inside an element that already has alternate text is never read. Remove the inner alternate text. Nadomestno besedilo znotraj elementa, ki že ima nadomestno besedilo, se nikoli ne prebere. Odstranite notranje nadomestno besedilo. + Alternate text must be associated with some content Nadomestno besedilo mora biti povezano z vsebino + Alternate text on an element that contains a link or field hides that annotation from assistive technology. Move the alternate text. Nadomestno besedilo na elementu, ki vsebuje povezavo ali polje, skrije to opombo pred podpornimi tehnologijami. Premaknite nadomestno besedilo. + Alternate text on an element without page content is never reached. Remove it or attach the element to content. Nadomestno besedilo na elementu brez vsebine strani ni nikoli dosegljivo. Odstranite ga ali element povežite z vsebino. + Alternate text should not hide annotation Nadomestno besedilo ne bi smelo skriti opombe + Alternate text that will never be read Nadomestno besedilo nikoli ne bo prebrano + An action is already running Dejanje že poteka + An action with this name already exists. Dejanje s tem imenom že obstaja. + An embedded file has no AFRelationship Vdelana datoteka nima AFRelationship + An image asks to be interpolated Slika zahteva interpolacijo + An image has alternates or OPI information Slika ima nadomestke ali podatke OPI + An open-source PDF workbench — local, private, and scriptable. Odprtokodno delovno okolje za PDF — lokalno, zasebno in skriptabilno. + Angle Kot + Animation that flashes can cause seizures. Check scripts and media for flicker. Animacije, ki utripajo, lahko sprožijo napade. Preverite skripte in predstavnostne vsebine glede utripanja. + Annotations, form fill-in, and digital signatures Opombe, izpolnjevanje obrazcev in digitalni podpisi + Another file dialog is still open. Finish with it first. Odprto je že eno pogovorno okno. Najprej ga zaprite. + Any Poljubno + Any except extracting pages Vse razen izvlečenja strani + Anyone opening the document will need this password. It's applied when you save. Vsak, ki odpre dokument, bo potreboval to geslo. Vključi se ob shranjevanju. + Appear behind page Prikaži za stranjo + Appear on top of page Prikaži nad stranjo + Appearance Videz + Application Program + Applied 1 redaction mark. Save to remove the content from the file. Uveljavljena je 1 oznaka za počrnitev. Shranite, da se vsebina odstrani iz datoteke. + Applied {n} redaction marks. Save to remove the content from the file. Uveljavljenih oznak za počrnitev: {n}. Shranite, da se vsebina odstrani iz datoteke. + Applies to 1 page Velja za 1 stran + Applies to {n} pages Velja za strani: {n} + Apply Uporabi + Apply PDF standards Uporabi PDF standarde + Apply redactions Uveljavi počrnitev + Apply redactions… Uveljavi počrnitev… + Apply scale Uporabi merilo + Appropriate nesting Ustrezno gnezdenje + April april + Arbitrary Mask Poljubna maska + Archive documents Arhiviraj dokumente + Area Ploščina + Arrow Puščica + ArtCraft website Spletna stran ArtCraft + Ask about this document Vprašaj o tem dokumentu + Asks GitHub for the latest release. Nothing is downloaded or installed automatically. Vpraša GitHub za najnovejšo izdajo. Ničesar se ne prenese ali namesti samodejno. + Associated with content Povezano z vsebino + At least one signature has problems. Vsaj en podpis ima težave. + At least one signature is invalid. Vsaj en podpis je neveljaven. + At most Največ + Attach a file Pripni datoteko + Attach file Pripni datoteko + Attachment Priponka + Attachments Priponke + Attachments Panel and Page Razdelek in stran s priponkami + Audit space usage Preglej porabo prostora + Audit space usage… Preglej porabo prostora… + August avgust + Author Avtor + Auto Samodejno + Auto portrait/landscape Samodejno pokončno/ležeče + Auto-rotate pages Samodejno obrni strani + Auto-size text to fit redaction region Samodejno prilagodi velikost besedila območju počrnitve + Automatically tag PDF Samodejno označi PDF + Available Na voljo + Back Nazaj + Back side only Samo hrbtna stran + Back to all tools Nazaj na vsa orodja + Back to the document Nazaj na dokument + Background Ozadje + Basic constraints Osnovne omejitve + Bates numbering Batesovo oštevilčenje + Before each selected page (select pages in Organize) Pred vsako izbrano stranjo (strani izberite v načinu Organiziraj) + Before the number Pred številko + Beveled Prirezan rob + Bicubic downsampling to Bicubic zmanjšanje vzorčenja na + Binary assets added Dodani binarni viri + Binary assets removed Odstranjeni binarni viri + Binding Vezava + Binding: Vezava: + Black Črna + Blank page Prazna stran + Blue Modra + Bold Krepko + Booklet Knjižica + Booklet subset: Podmnožica knjižice: + Bookmark options Možnosti zaznamkov + Bookmarks Zaznamki + Bookmarks Panel and Page Razdelek in stran z zaznamki + Bookmarks are present in large documents Zaznamki so prisotni v velikih dokumentih + Border Color: Barva obrobe: + Borders and Colors Obrobe in barve + Both Oboje + Both sides Obe strani + Bottom Spodaj + Bottom: Spodaj: + Box Okvir + Bring your own model (off by default) Uporabite lasten model (privzeto izklopljeno) + Browse… Prebrskaj… + Button Gumb + Button Properties Lastnosti gumba + Button Style: Slog gumba: + Button is checked by default Gumb je privzeto označen + Buttons with the same name and value are selected in unison Izbirni gumbi z istim imenom in vrednostjo se izberejo hkrati + Bytes Bajti + Calculate Izračunaj + Calibrate from two points Umeri z dvema točkama + Callout Oblaček + Can't be read: {e} Ni mogoče prebrati: {e} + Cancel Prekliči + Cancelled Preklicano + Cancelling… Preklicevanje… + Case-sensitive Razlikuj velike in male črke + Category: Kategorija: + Center Sredinsko + Center Footer Text Besedilo noge na sredini + Center Header Text Besedilo glave na sredini + Center horizontally Na sredino vodoravno + Center vertically Na sredino navpično + Center window on screen Postavi okno na sredino zaslona + Centre Sredina + Certificate Viewer Pregledovalnik potrdil + Certificates Potrdila + Certified by {by} Overil: {by} + Certify Overi + Certify (invisible signature) Overi (nevidni podpis) + Certify (visible signature) Overi (vidni podpis) + Change comment properties Spremeni lastnosti komentarja + Change document properties Spremeni lastnosti dokumenta + Change field properties Spremeni lastnosti polja + Change initial view Spremeni začetni pogled + Change initials Spremeni začetnice + Change link properties Spremeni lastnosti povezave + Change reading options Spremeni možnosti branja + Change reading options… Spremeni možnosti branja… + Change settings… Spremeni nastavitve… + Change signature Spremeni podpis + Change {key} Spremeni: {key} + Change {what} Spremeni: {what} + Changes allowed Dovoljene spremembe + Changing the document Spreminjanje dokumenta + Character encoding Kodiranje znakov + Character spacing Razmik med znaki + Check Preveri + Check Again Ponovno preveri + Check Box Properties Lastnosti potrditvenega polja + Check Box Style: Slog potrditvenega polja: + Check again Ponovno preveri + Check box is checked by default Potrditveno polje je privzeto označeno + Check for accessibility Preveri dostopnost + Check for accessibility… Preveri dostopnost… + Check for updates Preveri posodobitve + Check for updates… Preveri posodobitve… + Check spelling Preveri črkovanje + Check that the tags read in the order a sighted reader would. Use the Reading Order tool or the Tags panel to correct it. Preverite, ali se oznake berejo v vrstnem redu, v katerem bi jih bral bralec. Za popravek uporabite orodje Vrstni red branja ali razdelek Oznake. + Check the pages of {name}: {why} Preverite strani dokumenta {name}: {why} + CheckBox Potrditveno polje + Checkbox Potrditveno polje + Checked Označeno + Checking Options ({on} of 32 in all categories) Možnosti preverjanja ({on} od 32 v vseh kategorijah) + Checking for a newer version… Preverjanje za novejšo različico… + Checkmark Kljukica + Choose a PNG or JPEG image (up to 4 MiB). Transparency is preserved. Izberite sliko PNG ali JPEG (do 4 MiB). Prosojnost je ohranjena. + Choose a folder for the exported files Izberite mapo za izvožene datoteke + Choose a folder for the extracted pages Izberite mapo za izvlečene strani + Choose a folder for the results Izberite mapo za rezultate + Choose a folder for the searchable files Izberite mapo za iskalne datoteke + Choose a folder for the split files Izberite mapo za razdeljene datoteke + Choose a folder on this computer to pin Izberite mapo v tem računalniku, ki jo želite pripeti + Choose a layout: Izberite postavitev: + Choose a stamp, then click on the page to place it. Izberite žig, nato kliknite željeno lokacijo. + Choose an image Izberite sliko + Choose images Izberite slike + Choose the digital ID that you want to use for signing: Izberite digitalno identiteto, s katero želite podpisati: + Circle Krog + Clean Up Počisti + Clear Počisti + Clear All Počisti vse + Clear Recent Files Počisti nedavne datoteke + Clear all Počisti vse + Clear form Počisti obrazec + Clear redaction marks Počisti oznake za počrnitev + Clear selection / close find Počisti izbor / zapri iskanje + Click on the page to add a {tool}, or drag to set its size Kliknite na stran, da dodate: {tool}, ali nastavite velikost + Click on the page to add text; each click starts a new box, and ✓ finishes. Drag items to move them, drag a corner to resize, double-click text to edit it. Kliknite na stran, da dodate besedilo; vsak klik začne nov okvir, ✓ ga zaključi. Elemente premikate z vlečenjem, velikost spremenite z vlečenjem vogala, besedilo uredite z dvoklikom. + Click text or an image to edit it Kliknite besedilo ali sliko, da jo uredite + Click the first point to begin. Kliknite prvo točko, da začnete. + Click to sort, drag to move the column. Drag its edge to resize it. Kliknite za razvrščanje, povlecite za premik stolpca. Povlecite rob za spremembo širine. + Click vertices. Enter or double-click finishes a perimeter or area. Click the first point to close an area. Kliknite na točke. Enter ali dvoklik zaključi obseg ali ploščino. Kliknite prvo točko, da sklenete ploščino. + Clipboard Odložišče + Clockwise 90 degrees 90 stopinj desno + Close Zapri + Close (Esc) Zapri (Esc) + Close all Zapri vse + Close file Zapri datoteko + Close panel Zapri razdelek + Cloud Oblak + Collapse Strni + Collapse all bookmarks Strni vse zaznamke + Collate Zloži po vrsti + Color Images Barvne slike + Color Spaces Barvni prostori + Color contrast Barvni kontrast + Color: Barva: + Colour Barva + Comb of Glavnik za + Combine Združi + Combine files Združi datoteke + Combine files… Združi datoteke… + Combined {n} files Združenih datotek: {n} + Combo Kombinirano polje + Coming in milestone Prihaja v fazi + Coming in milestone {m}. Items marked Ready work today. Prihaja v fazi {m}. Elementi, označeni z Omogočeno, delujejo že danes. + Command Ukaz + Comment and form data Podatki komentarjev in obrazcev + Comment list Seznam komentarjev + Commenting Komentiranje + Commenting, filling in form fields, and signing existing signature fields Komentiranje, izpolnjevanje polj obrazca in podpisovanje obstoječih podpisnih polj + Comments Komentarji + Comments & Forms Komentarji in obrazci + Comments and markups Komentarji in označbe + Comments only Samo komentarji + Comments panel Razdelek s komentarji + Comments, links and fields must be in the tag tree to be announced. Tag the annotations. Komentarji, povezave in polja morajo biti v drevesu oznak, da jih podporne tehnologije izgovorijo. Označite opombe. + Commit selected value immediately Takoj potrdi izbrano vrednost + Commits Commiti + Common Properties Skupne lastnosti + Company Podjetje + Compare Primerjaj + Compare Files Primerjava datotek + Compare files Primerjaj datoteke + Compare files… Primerjaj datoteke… + Compare this file with an older version: Compare files… in All tools. To datoteko primerjajte s starejšo različico: Primerjaj datoteke… v meniju Vsa orodja. + Compatibility Združljivost + Completed Končano + Compress a PDF Stisni PDF + Compress document structure (object streams) Stisni strukturo dokumenta (tokovi objektov) + Compression Stiskanje + Configure New Digital ID Nastavi novo digitalno identiteto + Configure a Digital ID for Signing Nastavite digitalno identiteto za podpisovanje + Confirm Password Potrdite geslo + Conformance level: Raven skladnosti: + Connected lines Povezane črte + Console Konzola + Content Streams Tokovi vsebine + Content copying Kopiranje vsebine + Content copying for accessibility Kopiranje vsebine za dostopnost + Continue Nadaljuj + Continuous scrolling Neprekinjeno pomikanje + Contributors Sodelavci + Convert Pretvori + Convert colors Pretvori barve + Copied {what} Kopirano: {what} + Copies: Kopije: + Copy Kopiraj + Copy or cut pages first Najprej kopiraj ali izrež strani + Copy pages Kopiraj strani + Copy selected text Kopiraj izbrano besedilo + Copy text Kopiraj besedilo + Correct recognized text Popravi prepoznano besedilo + Could not save: {e} Shranjevanje ni uspelo: {e} + Couldn't add {name}: {e} Datoteke {name} ni bilo mogoče dodati: {e} + Couldn't check for updates: {e} Posodobitev ni bilo mogoče preveriti: {e} + Couldn't combine files: {e} Datotek ni bilo mogoče združiti: {e} + Couldn't create a PDF: {e} PDF-ja ni bilo mogoče ustvariti: {e} + Couldn't download {name}: {e} Datoteke {name} ni bilo mogoče prenesti: {e} + Couldn't extract pages: {e} Strani ni bilo mogoče izvleči: {e} + Couldn't fit the visible content: {e} Vidne vsebine ni bilo mogoče prilagoditi: {e} + Couldn't import the signature image: {e} Slike podpisa ni bilo mogoče uvoziti: {e} + Couldn't open revision {n}: {e} Revizije {n} ni bilo mogoče odpreti: {e} + Couldn't open the result: {e} Rezultata ni bilo mogoče odpreti: {e} + Couldn't open the signed version: {e} Podpisane različice ni bilo mogoče odpreti: {e} + Couldn't open {name}: {e} Datoteke {name} ni bilo mogoče odpreti: {e} + Couldn't optimize the file Datoteke ni bilo mogoče optimizirati + Couldn't optimize the file: {e} Datoteke ni bilo mogoče optimizirati: {e} + Couldn't read {name}: {e} Datoteke {name} ni bilo mogoče prebrati: {e} + Couldn't recover {name}: {e} Datoteke {name} ni bilo mogoče obnoviti: {e} + Couldn't save the image: {e} Slike ni bilo mogoče shraniti: {e} + Couldn't save {name}: {e} Datoteke {name} ni bilo mogoče shraniti: {e} + Couldn't save: {e} Shranjevanje ni uspelo: {e} + Couldn't show the file picker. Please try again. Izbirnika datotek ni bilo mogoče prikazati. Poskusite znova. + Couldn't split: {e} Razdelitev ni uspela: {e} + Couldn't summarize comments: {e} Komentarjev ni bilo mogoče povzeti: {e} + Couldn't take the snapshot: {e} Posnetka ni bilo mogoče usvariti: {e} + Couldn't use {name}: {e} Datoteke {name} ni bilo mogoče uporabiti: {e} + Couldn't write {name}: {e} Datoteke {name} ni bilo mogoče zapisati: {e} + Couldn't write {path}: {e} Poti {path} ni bilo mogoče zapisati: {e} + Counterclockwise 90 degrees 90 stopinj v levo + Country/Region Država/regija + Create Ustvari + Create Custom Stamp Ustvari lasten žig + Create Link Ustvari povezavo + Create PDF Comment Summary Ustvari povzetek komentarjev PDF + Create PDF comment summary… Ustvari povzetek komentarjev PDF… + Create PDF from clipboard Ustvari PDF iz odložišča + Create PDF from file… Ustvari PDF iz datoteke… + Create PDF from images Ustvari PDF iz slik + Create PDF from images… Ustvari PDF iz slik… + Create PDF from multiple files… Ustvari PDF iz več datotek… + Create a PDF Ustvari PDF + Create a new Digital ID (self-signed, saved to a password-protected file) Ustvari novo digitalno identiteto (samopodpisano, shranjeno v datoteko, zaščiteno z geslom) + Create accessibility report Ustvari poročilo o dostopnosti + Create custom stamp… Ustvari lasten žig… + Create from Ustvari iz + Create link Ustvari povezavo + Create links from URLs Ustvari povezave iz naslovov URL + Create {what} Ustvari: {what} + Created 1 link Ustvarjena je 1 povezava + Created a PDF from the first {n} files Ustvarjen PDF iz prvih datotek: {n} + Created a PDF from {n} files Ustvarjen PDF iz datotek: {n} + Created a PDF from {n} files; left out: {names} Ustvarjen PDF iz datotek: {n}; izpuščeno: {names} + Created a comment summary Ustvarjen je povzetek komentarjev + Created {n} links Ustvarjenih povezav: {n} + Creates 1 file from {n} pages. Ustvari 1 datoteko iz strani: {n}. + Creates {f} files from {n} pages. Ustvari datoteke: {f}, iz strani: {n}. + Credit Cards Kreditne kartice + Crop (% trimmed from each side) Obreži (% odrezano z vsake strani) + Crop page Obreži stran + Crop pages Obreži strani + Cross Križec + Currency symbol: Simbol valute: + Current page Trenutna stran + Current page ({n}) Trenutna stran ({n}) + Current page — type a page number or label (such as iv) and press Enter Trenutna stran — vnesite številko ali oznako strani (npr. iv) in pritisnite Enter + Custom Po meri + Custom scale: Merilo po meri: + Custom stamps arrive on the web with file pickers for images Lastni žigi bodo v spletni različici na voljo z izbirniki slikovnih datotek + Custom text: Besedilo po meri: + Custom: Po meri: + Cut Izreži + Cut and stack Razreži in zloži + Cut marks Rezalne oznake + Cut pages Izreži strani + Cut {what} Izreži: {what} + DPI sets the printed page size without resampling the image. DPI določa velikost natisnjene strani brez ponovnega vzorčenja slike. + Dark gray Temno siva + Dark gray theme Temno siva tema + Dashed Črtkano + Date Datum + Date field Polje za datum + Dates Datumi + December December + Decimal places: Decimalna mesta: + Decimals Decimalke + Declared conformance: {shown} Deklarirana skladnost: {shown} + Decorative figure Dekorativna slika + Default Privzeto + Default Value: Privzeta vrednost: + Default page display Privzet prikaz strani + Default workspace mode Privzeti način delovnega prostora + Default zoom Privzeto povečanje + Default: Privzeto: + Delete Izbriši + Delete bookmark Izbriši zaznamek + Delete comment Izbriši komentar + Delete content Izbriši vsebino + Delete document JavaScript Izbriši JavaScript dokumenta + Delete field Izbriši polje + Delete image Izbriši sliko + Delete link Izbriši povezavo + Delete page Izbriši stran + Delete pages Izbriši strani + Delete pages (Delete) Izbriši strani (Delete) + Delete pages after extracting Izbriši strani po izvlečenju + Delete selected fields Izbriši izbrana polja + Delete selected pages (Organize) Izbriši izbrane strani (Organiziraj) + Delete stamp Izbriši žig + Delete {o} Izbriši: {o} + Deleted Izbrisano + Describe the figure Opišite sliko + Description Opis + Description: Opis: + Details Podrobnosti + Detect form fields Zaznaj polja obrazca + Detected 1 form field Zaznano je 1 polje obrazca + Detected {n} form fields Zaznanih polj obrazca: {n} + Device colour is used, but there is no PDF/A output intent Uporabljena je barva naprave, vendar ni izhodnega namena PDF/A + DeviceCMYK is used: it needs a CMYK output intent (not added automatically) Uporabljen je DeviceCMYK: potrebuje izhodni namen CMYK (ni dodan samodejno) + Diagnostic details Diagnostične podrobnosti + Diamond Romb + Digital ID Digitalna identiteta + Digital ID password Geslo digitalne identitete + Digital Signature Properties Lastnosti digitalnega podpisa + Digital signature Digitalni podpis + Digitally sign Digitalno podpiši + Direction: Smer: + Discard Zavrzi + Discard Objects Zavrzi objekte + Discard User Data Zavrzi uporabniške podatke + Discard all comments, forms and multimedia Zavrzi vse komentarje, obrazce in večpredstavnostne vsebine + Discard all links, actions and JavaScript Zavrzi vse povezave, dejanja in JavaScript + Discard all object data (file attachments) Zavrzi vse podatke objektov (priponke datotek) + Discard alternate images Zavrzi nadomestne slike + Discard bookmarks Zavrzi zaznamke + Discard document information and metadata Zavrzi podatke o dokumentu in metapodatke + Discard document tags Zavrzi oznake dokumenta + Discard embedded page thumbnails Zavrzi vdelane sličice strani + Discard embedded print settings Zavrzi vdelane nastavitve tiskanja + Discard hidden layer content Zavrzi vsebino skritih plasti + Discard hidden text Zavrzi skrito besedilo + Discard private data of other applications Zavrzi zasebne podatke drugih programov + Dismiss Opusti + Display name Prikazno ime + Display theme Tema prikaza + Distance Razdalja + Distinguished name Razločevalno ime + Distribute Porazdeli + Distribute fields Porazdeli polja + Document Dokument + Document JavaScripts JavaScripti dokumenta + Document JavaScripts… JavaScripti dokumenta… + Document Overhead Režija dokumenta + Document Properties Lastnosti dokumenta + Document Title Naslov dokumenta + Document actions Dejanja dokumenta + Document and markups Dokument in označbe + Document and stamps Dokument in žigi + Document assembly Sestavljanje dokumenta + Document has appropriate color contrast Dokument ima ustrezen barvni kontrast + Document is not image-only PDF Dokument ni PDF samo s slikami + Document is tagged PDF Dokument je označen PDF + Document language Jezik dokumenta + Document properties (⌘D) Lastnosti dokumenta (⌘D) + Document properties… Lastnosti dokumenta… + Document sanitized. Save to finish: saving rewrites the whole file. Dokument je očiščen. Shranite za dokončanje: shranjevanje prepiše celotno datoteko. + Document structure provides a logical reading order Struktura dokumenta zagotavlja logičen vrstni red branja + Document title is showing in title bar Naslov dokumenta je prikazan v naslovni vrstici + Documents and view Dokumenti in pogled + Documents of more than 20 pages need bookmarks for navigation. Add bookmarks for the main sections. Dokumenti z več kot 20 stranmi potrebujejo zaznamke za navigacijo. Dodajte zaznamke za glavne razdelke. + Don't save Ne shrani + Done Opravljeno + Dot Pika + Double-click Dvoklik + Download Prenesi + Downloaded {name} Preneseno: {name} + Downloading attachments on the web arrives with M3.10 ({n} bytes ready) Prenos priponk v spletni različici pride z M3.10 (pripravljenih bajtov: {n}) + Downsample to Zmanjšaj vzorčenje na + Drag a rectangle around the area to copy Povlecite pravokotnik okoli območja, ki ga želite kopirati + Drag a rectangle on a page to crop it; double-click a page for Set Page Boxes Povlecite pravokotnik na strani, da jo obrežete; dvokliknite stran za nastavitev okvirjev strani + Drag a rectangle to create a link; double-click a link to edit it Povlecite pravokotnik, da ustvarite povezavo; dvokliknite povezavo, da jo uredite + Drag to draw the area where the signature should appear. Povlecite, da narišete območje, kjer naj se prikaže podpis. + Drag to reorder Povlecite za spremembo vrstnega reda + Draw Riši + Draw a cloud Nariši oblak + Draw a line Nariši črto + Draw a polygon Nariši mnogokotnik + Draw a rectangle Nariši pravokotnik + Draw an arrow Nariši puščico + Draw an oval Nariši elipso + Draw connected lines Nariši povezane črte + Draw freehand Riši prostoročno + Draw your {what} below. Spodaj narišite: {what}. + Drawing Risba + Drawing scale Merilo risbe + Drop PDFs or folders here, or choose them. Spustite datoteke PDF ali mape sem ali jih izberite. + Drop-down list Spustni seznam + Dropdown Properties Lastnosti spustnega seznama + Duplicate "{name}" onto: Podvoji „{name}“ na: + Duplicate Field Podvoji polje + Duplicate field Podvoji polje + Duplicate page Podvoji stran + Duplicate pages Podvoji strani + Duplicate… Podvoji… + Dynamic Dinamično + Dynamic XFA form laid out from its template: {pages}, {fields}. Dinamični obrazec XFA, razporejen iz predloge: {pages}, {fields}. + E-Sign E-podpis + Each field needs a tooltip that says what to enter. Add one in Field Properties > General > Tooltip. Vsako polje potrebuje namig, ki pove, kaj vnesti. Dodajte ga v Lastnosti polja > Splošno > Namig. + Each file holds as many of the {n} pages as fit. Vsaka datoteka vsebuje toliko od {n} strani, kot jih gre vanjo. + Each image is measured where pages draw it; an image is replaced only if the result is smaller. Vsaka slika je izmerjena tam, kjer jo strani izrišejo; slika se zamenja le, če je rezultat manjši. + Each image once, named after the document and page. JPEG images are saved unchanged; others as PNG. Vsaka slika enkrat, poimenovana po dokumentu in strani. Slike JPEG so shranjene nespremenjene, ostale kot PNG. + Earlier in tab order Prej v vrstnem redu tabulatorja + Edit Action Uredi dejanje + Edit a PDF Uredi PDF + Edit comment Uredi komentar + Edit content Uredi vsebino + Edit document JavaScript Uredi JavaScript dokumenta + Edit fields Uredi polja + Edit image Uredi sliko + Edit script of {name} Uredi skript: {name} + Edit text Uredi besedilo + Edit text & images Uredi besedilo in slike + Edit text… Uredi besedilo… + Editing Urejanje + Edit… Uredi… + Email Address E-poštni naslov + Email Addresses E-poštni naslovi + Embedded Files Vdelane datoteke + Embedded files must be PDF/A files themselves (use PDF/A-3 for others) Vdelane datoteke morajo biti same datoteke PDF/A (za druge uporabite PDF/A-3) + Enable Acrobat JavaScript Omogoči Acrobat JavaScript + Enable copying of text, images, and other content Omogoči kopiranje besedila, slik in druge vsebine + Enable text access for screen reader devices for the visually impaired Omogoči dostop do besedila bralnikom zaslona za slepe in slabovidne + Encrypt Šifriraj + Encrypt with certificate Šifriraj s potrdilom + Encryption Šifriranje + Endpoint Končna točka + Endpoints Končne točke + Enhance scanned file Izboljšaj skenirano datoteko + Enter the password that opens it Vnesite geslo, s katerim se odpre + Enter the permissions password to allow combining Vnesite geslo za dovoljenja, da omogočite združevanje + Entire page Cela stran + Erase Izbriši + Erase drawings Izbriši risbe + Eraser Radirka + Even Pages Only Samo sode strani + Even and Odd Pages Sode in lihe strani + Even pages Sode strani + Even pages only Samo sode strani + Every PDF in the folder Vsak PDF v mapi + Every PDF in the folder and the folders inside it Vsak PDF v mapi in v mapah v njej + Everything on a page must be tagged or marked as an artifact (decoration, headers, footers). Tag the untagged content. Vse na strani mora biti označeno ali označeno kot artefakt (okras, glave, noge). Označite neoznačeno vsebino. + Example: {sample} Primer: {sample} + Exclude images smaller than Izključi slike, manjše od + Execute a menu item Izvedi element menija + Expand Razširi + Expand all bookmarks Razširi vse zaznamke + Expand top-level bookmarks Razširi zaznamke najvišje ravni + Explain Razloži + Export Izvozi + Export All Images Izvozi vse slike + Export Value: {on} Izvozna vrednost: {on} + Export a PDF Izvozi PDF + Export all comments to data file… Izvozi vse komentarje v podatkovno datoteko… + Export all images Izvozi vse slike + Export all images… Izvozi vse slike… + Export all to data file… Izvozi vse v podatkovno datoteko… + Export certificate Izvozi potrdilo + Export certificate… Izvozi potrdilo… + Export comments Izvozi komentarje + Export data Izvozi podatke + Export form data Izvozi podatke obrazca + Export form data… Izvozi podatke obrazca… + Export measurements Izvozi meritve + Export measurements as CSV Izvozi meritve kot CSV + Export stopped: {e} Izvoz se je ustavil: {e} + Export to Izvozi v + Export to HTML… Izvozi v HTML… + Export to Image Izvozi v sliko + Export to RTF… Izvozi v RTF… + Export to Text Izvozi v besedilo + Export to Word… Izvozi v Word… + Export to image… Izvozi v sliko… + Export to text… Izvozi v besedilo… + Exported 1 image Izvožena je 1 slika + Exported the text of 1 page Izvoženo je besedilo 1 strani + Exported the text of {n} pages Izvoženo je besedilo strani: {n} + Exported to {path} Izvoženo v {path} + Exported {n} images Izvoženih slik: {n} + Exporting… {d} of {t} Izvažanje… {d} od {t} + Export… Izvozi… + Extended Graphics States Razširjena grafična stanja + Extract Izvleci + Extract pages Izvleci strani + Extract pages as separate files Izvleci strani kot ločene datoteke + Extract pages to a new document Izvleci strani v nov dokument + Extract pages… Izvleci strani… + Extracted 1 page Izvlečena je 1 stran + Extracted {n} pages Izvlečenih strani: {n} + Failed Ni uspelo + February Februar + Field Polje + Field descriptions Opisi polj + Field is used for file selection Polje se uporablja za izbiro datoteke + Field list Seznam polj + Field names (comma-separated) Imena polj (ločena z vejicami) + Field names with + - * / and parentheses; put \\ before spaces in names. Imena polj z + - * / in oklepaji; pred presledke v imenih postavite \\. + Field properties Lastnosti polja + Field properties… Lastnosti polja… + Field value is in range: Vrednost polja je v obsegu: + Field value is not validated Vrednost polja ni preverjena + Field: {field} on page {p} Polje: {field} na strani {p} + Fields Polja + Fields to reset (comma-separated; empty: all) Polja za ponastavitev (ločena z vejicami; prazno: vsa) + Figure {i} of {n}{page} Slika {i} od {n}{page} + Figures alternate text Nadomestno besedilo slik + Figures require alternate text Slike zahtevajo nadomestno besedilo + File (.p12, .pfx) Datoteka (.p12, .pfx) + File Name Ime datoteke + File attachments Priponke datotek + File name Ime datoteke + File size Velikost datoteke + Files are combined from top to bottom. Leave Pages empty to take every page. Datoteke se združijo od zgoraj navzdol. Če pustite Strani prazno, se vzamejo vse strani. + Files are combined in this order. Leave Pages empty to take every page. Datoteke se združijo v tem vrstnem redu. Če pustite Strani prazno, se vzamejo vse strani. + Files for {name} Datoteke za {name} + Files you open in PdfCraft appear here. Drop a PDF anywhere to open it. Datoteke, ki jih odprete v PdfCraftu, se prikažejo tukaj. Spustite PDF kamor koli, da ga odprete. + Fill Izpolni + Fill & Sign Izpolni in podpiši + Fill & Sign: add text Izpolni in podpiši: dodaj besedilo + Fill & Sign: change initials Izpolni in podpiši: spremeni začetnice + Fill & Sign: change signature Izpolni in podpiši: spremeni podpis + Fill & Sign: checkmark Izpolni in podpiši: kljukica + Fill & Sign: cross Izpolni in podpiši: križec + Fill & Sign: date Izpolni in podpiši: datum + Fill & Sign: dot Izpolni in podpiši: pika + Fill & Sign: initials Izpolni in podpiši: začetnice + Fill & Sign: line Izpolni in podpiši: črta + Fill & Sign: remove saved initials Izpolni in podpiši: odstrani shranjene začetnice + Fill & Sign: remove saved signature Izpolni in podpiši: odstrani shranjeni podpis + Fill & Sign: sign Izpolni in podpiši: podpiši + Fill Color: Barva polnila: + Fill in {name} Izpolni: {name} + Filling in form fields and signing existing signature fields Izpolnjevanje polj obrazca in podpisovanje obstoječih podpisnih polj + Filling of form fields Izpolnjevanje polj obrazca + Filter comments Filtriraj komentarje + Find options Možnosti iskanja + Find text Poišči besedilo + Find text and redact Poišči besedilo in počrni + Find text and redact… Poišči besedilo in počrni… + Find tools and commands Poišči orodja in ukaze + Find tools and commands… Poišči orodja in ukaze… + Find… Poišči… + First Prva + First / last page Prva / zadnja stran + First commit Prvi commit + Fit Prilagodi + Fit Height Prilagodi višini + Fit Page Prilagodi strani + Fit Visible Prilagodi vidnemu + Fit Width Prilagodi širini + Fit one full page Prilagodi eni celi strani + Fit to height Prilagodi višini + Fit to page Prilagodi strani + Fit to width Prilagodi širini + Fit to width scrolling Pomikanje s prilagoditvijo širini + Fit visible Prilagodi vidnemu + Fix Popravi + Fix hairlines Popravi tanke črte + Fix reading order Popravi vrstni red branja + Fixed what could be fixed; 1 problem remains Popravljeno, kar je bilo mogoče; ostaja 1 težava + Fixed what could be fixed; {n} problems remain Popravljeno, kar je bilo mogoče; preostalih težav: {n} + Flatten Splošči + Flatten comments Splošči komentarje + Flatten form fields Splošči polja obrazca + Flip Horizontal Prezrcali vodoravno + Flip Vertical Prezrcali navpično + Flip horizontal Prezrcali vodoravno + Flip image Prezrcali sliko + Flip on long edge Obrni po dolgem robu + Flip on short edge Obrni po kratkem robu + Flip vertical Prezrcali navpično + Folders Mape + Font Size: Velikost pisave: + Font colour: Barva pisave: + Font size: Velikost pisave: + Font: Pisava: + Font: Helvetica Pisava: Helvetica + Fonts Pisave + Fonts must map their characters to Unicode so text can be read and copied. Fix the fonts or recreate the PDF. Pisave morajo svoje znake preslikati v Unicode, da je besedilo mogoče brati in kopirati. Popravite pisave ali ponovno ustvarite PDF. + Form data and PDF forms Podatki obrazca in obrazci PDF + Form fields Polja obrazca + Form fields are flattened: their values stay visible. Saving rewrites the whole file. Polja obrazca so sploščena: njihove vrednosti ostanejo vidne. Shranjevanje prepiše celotno datoteko. + Form fields must be in the tag tree (Form tags) to be announced. Tag the form fields. Polja obrazca morajo biti v drevesu oznak (oznake Form), da jih podporne tehnologije izgovorijo. Označite polja obrazca. + Form fields only Samo polja obrazca + Form fields panel Razdelek s polji obrazca + Form fill-in and digital signatures Izpolnjevanje obrazcev in digitalni podpisi + Format Oblika + Format text Oblikuj besedilo + Format: Oblika: + Forms Obrazci + Formulas and similar non-text elements need alternate text. Add alternate text to them. Formule in podobni nebesedilni elementi potrebujejo nadomestno besedilo. Dodajte jim nadomestno besedilo. + From Od + From a PDF page or an image Iz strani PDF ali slike + From colour Iz barve + From {file} Iz: {file} + From: Od: + Front side only Samo sprednja stran + Full screen mode Celozaslonski način + General Splošno + Geospatial location Geoprostorska lokacija + Get help, share feedback and follow development on Discord. Poiščite pomoč, delite povratne informacije in spremljajte razvoj na Discordu. + Go to a page view Pojdi na pogled strani + Go to page {p} Pojdi na stran {p} + Go to page… Pojdi na stran… + Go to the page Pojdi na stran + Go to this result Pojdi na ta rezultat + Grab bag Razno + Gray Siva + Grayscale Images Sivinske slike + Green Zelena + H / Space (hold) H / preslednica (drži) + HTML web page Spletna stran HTML + Hand (H) Roka (H) + Has JavaScript actions (run in M6) Vsebuje dejanja JavaScript (izvajajo se v M6) + Header and footer Glava in noga + Header cells (TH) tell listeners what each column or row holds. Mark the header cells. Celice glave (TH) povedo bralniku, kaj vsebuje posamezen stolpec ali vrstica. Označite celice glave. + Headers Glave + Heading levels shouldn't skip (an H1 followed by an H3). Retag the headings. Ravni naslovov ne smejo preskakovati (H1, ki mu sledi H3). Ponovno označite naslove. + Headings Naslovi + Height Višina + Height: Višina: + Hidden layers Skrite plasti + Hidden text Skrito besedilo + Hide a field Skrij polje + Hide all comments Skrij vse komentarje + Hide field highlights Skrij osvetlitev polj + Hide layer Skrij plast + Hide menu bar Skrij menijsko vrstico + Hide password Skrij geslo + Hide toolbars Skrij orodne vrstice + Hide window controls Skrij kontrolnike okna + Hides annotation Skrije opombo + High Visoka + High Resolution Visoka ločljivost + High resolution Visoka ločljivost + Highlight Označi + Highlight Style: Slog označbe: + Highlight fields Označi polja + Highlight text Označi besedilo + Home Domov + Home / End Home / End + Horizontal Vodoravno + Horizontal reversed Vodoravno obrnjeno + Horizontal scale Vodoravno merilo + Horizontally Vodoravno + Icon Ikona + Identity Identiteta + Image Slika + Image (PNG) Slika (PNG) + Image field Polje za sliko + Image-only PDF PDF samo s slikami + Images Slike + Images and text Slike in besedilo + Images and text (converted to PDF) Slike in besedilo (pretvorjeno v PDF) + Import comments… Uvozi komentarje… + Import data Uvozi podatke + Import form data… Uvozi podatke obrazca… + Import {name} Uvozi: {name} + Importing data arrives on the web with file pickers for data files Uvoz podatkov bo v spletni različici na voljo z izbirniki podatkovnih datotek + In multiple files V več datotekah + In this file V tej datoteki + Includes unsaved changes Vključuje neshranjene spremembe + Incorrect password. Try again. Napačno geslo. Poskusite znova. + Indent Zamik + Index Kazalo + Initial View Začetni pogled + Initials Začetnice + Insert Vstavi + Insert Date Vstavi datum + Insert Page Number Vstavi številko strani + Insert a blank page after the selection Vstavi prazno stran + Insert a file at the end Vstavi datoteko na konec + Insert a file before page {label} Vstavi datoteko pred stran {label} + Insert blank page Vstavi prazno stran + Insert pages Vstavi strani + Insert pages from a file… Vstavi strani iz datoteke… + Insert pages from file… Vstavi strani iz datoteke… + Insert pages from {name} Vstavi strani iz: {name} + Insert text Vstavi besedilo + Inserted Vstavljeno + Inserted text Vstavljeno besedilo + Inserting, deleting, and rotating pages Vstavljanje, brisanje in vrtenje strani + Inset Vtisnjeno + Intended usage Predvidena uporaba + Intersection Presek + Intersections Preseki + Invert Obrni barve + Invisible Rectangle Nevidni pravokotnik + Irregular tables confuse cell navigation. Give every row the same number of cells (counting spans). Nepravilne tabele zmedejo premikanje po celicah. Vsaka vrstica naj ima enako število celic (vključno z razponi). + Issued by Izdal + Issued by: Izdal: + Issued to Izdano za + Issuer Izdajatelj + Italic Ležeče + Item List Seznam elementov + Item: Element: + Items marked Ready work today. Elementi, označeni s Pripravljeno, delujejo že danes. + Its security settings don't allow copying pages Njene varnostne nastavitve ne dovoljujejo kopiranja strani + January Januar + JavaScript JavaScript + JavaScript Console Konzola JavaScript + JavaScript console… Konzola JavaScript… + JavaScript is turned off (Preferences ▸ JavaScript). JavaScript je izklopljen (Nastavitve > JavaScript). + JavaScript, e.g. getField("total").value JavaScript, npr. getField("total").value + Join our Discord Discord kanal + Join the ArtCraft Discord Pridružite se Discordu ArtCraft + Join the ArtCraft community Pridružite se skupnosti ArtCraft + July Julij + June Junij + Justify Obojestransko poravnano + Keep tool selected Ohrani izbrano orodje + Keep tool selected: on Ohrani izbrano orodje: vklopljeno + Key Algorithm Algoritem ključa + Key usage Uporaba ključa + Keyboard shortcuts Bližnjice na tipkovnici + Keychain · Keychain · + Keywords Ključne besede + LI must be a child of L LI mora biti podrejen element elementa L + Label Oznaka + Labels Oznake + Labels: {preview}. Later pages keep their labels. Oznake: {preview}. Poznejše strani obdržijo svoje oznake. + Landscape Ležeče + Landscape Pages Ležeče strani + Landscape and Portrait Pages Ležeče in pokončne strani + Landscape pages Ležeče strani + Language Jezik + Last Zadnja + Last commit Zadnji commit + Later Prekliči + Later in tab order Pozneje v vrstnem redu tabulatorja + Layers Plasti + Layers Panel and Page Razdelek in stran s plastmi + Layout and Magnification Postavitev in povečava + Lbl and LBody Lbl in LBody + Lbl and LBody must be children of LI Lbl in LBody morata biti podrejena elementa elementa LI + Left Levo + Left Edge Levi rob + Left Footer Text Besedilo noge na levi + Left Header Text Besedilo glave na levi + Left: Levo: + Light blue Svetlo modra + Light gray Svetlo siva + Light theme Svetla tema + Limit of Omejitev + Line Črta + Line Style: Slog črte: + Line Thickness: Debelina črte: + Line delta Sprememba vrstic + Line spacing Razmik med vrsticami + Line thickness Debelina črte + Lines +/− Vrstice +/− + Lines added Dodane vrstice + Lines deleted Izbrisane vrstice + Link Povezava + Link Action Dejanje povezave + Link Appearance Videz povezave + Link Properties Lastnosti povezave + Link Type: Vrsta povezave: + Links, actions and JavaScripts Povezave, dejanja in JavaScripti + List Seznam + List Box Properties Lastnosti seznamskega polja + List box Seznamsko polje + List items Elementi seznama + List items belong in a list. Fix the list's tags. Elementi seznama sodijo v seznam. Popravite oznake seznama. + List labels and bodies belong in a list item. Fix the list's tags. Oznake in telesa seznama sodijo v element seznama. Popravite oznake seznama. + Lists Seznami + Location Lokacija + Lock comment Zakleni komentar + Locked Zaklenjeno + Logical Reading Order Logični vrstni red branja + Looking for PDFs… Iskanje datotek PDF… + Low Nizka + Low Resolution (150 dpi) Nizka ločljivost (150 dpi) + Low resolution Nizka ločljivost + Magnification Povečava + Make Current Properties Default Nastavi kot privzeto + Make accessible Naredi dostopno + Make scans searchable, then make them smaller. Skenirane dokumente naredite iskalne, nato pa jih zmanjšajte. + Manage embedded index Upravljaj vdelani indeks + March Marec + Margins (in) Robovi (palci) + Mark Page Range Označi obseg strani + Mark all Označi vse + Mark as Confidential Označi kot zaupno + Mark as comments Označi kot komentarje + Mark differences Označi razlike + Mark figure as decorative Označi sliko kot dekorativno + Mark for redaction Označi za počrnitev + Mark pages for redaction Označi strani za počrnitev + Mark with checkmark Označi s kljukico + Marked Označeno + Markup Označba + Marquee zoom Povečava z okvirjem + Mask: Maska: + Match field sizes Uskladi velikosti polj + Maximum Največ + May Maj + Measure Izmeri + Measure area Izmeri ploščino + Measure distance Izmeri razdaljo + Measure objects Izmeri objekte + Measure perimeter Izmeri obseg + Measurement information Podatki o meritvah + Measurement snapping Pripenjanje meritev + Measuring tool Orodje za merjenje + Medium Srednja + Merge data files into spreadsheet… Združi podatkovne datoteke v preglednico… + Merged 1 file into {path} Združena je 1 datoteka v {path} + Merged PRs Združeni PR-ji + Merged {n} files into {path} Združenih datotek: {n}, v {path} + Merging data files needs the desktop app Združevanje podatkovnih datotek zahteva namizno aplikacijo + Metadata Metapodatki + Microsoft Word (.docx) Microsoft Word (.docx) + Midpoint Središčna točka + Midpoints Središčne točke + Minimum Najmanj + Model Model + Models Modeli + Modified Spremenjeno + Modify page Spremeni stran + More Več + More options Več možnosti + More options: Več možnosti: + More ways to add files Več načinov dodajanja datotek + Mouse Down Miška navzdol + Mouse Enter Miška vstopi + Mouse Exit Miška izstopi + Mouse Up Miška navzgor + Move bookmark Premakni zaznamek + Move comment Premakni komentar + Move down Premakni navzdol + Move earlier Premakni prej + Move image Premakni sliko + Move later Premakni pozneje + Move page Premakni stran + Move pages Premakni strani + Move up Premakni navzgor + Multi-line Večvrstično + Multiple Več + Multiple files Več datotek + Multiple selection Večkratna izbira + Name Ime + Name (A–Z) Ime (A–Ž) + Name on new comments Ime na novih komentarjih + Name: Ime: + Named Destinations Poimenovani cilji + Navigation links Navigacijske povezave + Navigation links are not repetitive Navigacijske povezave se ne ponavljajo + Navigation tab Zavihek za navigacijo + Needs manual check Zahteva ročni pregled + Negative number style: Slog negativnih števil: + Nested alternate text Gnezdeno nadomestno besedilo + New Action Novo dejanje + New Action… Novo dejanje… + New blank PDF Nov prazen PDF + New bookmark Nov zaznamek + New bookmark (⌘B) Nov zaznamek (⌘B) + New file: Nova datoteka: + New {tool} comments will look like this one Novi komentarji ({tool}) bodo videti tako kot ta + Next (⌘G) Naslednja (⌘G) + Next / previous match Naslednje / prejšnje ujemanje + Next figure Naslednja slika + Next month Naslednji mesec + Next page Naslednja stran + Next sheet Naslednji list + Next view Naslednji pogled + No Ne + No PDFs found in {folder} V mapi {folder} ni najdenih datotek PDF + No PDFs in this folder yet V tej mapi še ni datotek PDF + No actions Ni dejanj + No changes allowed Spremembe niso dovoljene + No changes to save Ni sprememb za shranjevanje + No check has run yet. Preverjanje še ni bilo izvedeno. + No color Brez barve + No colour Brez barve + No comments match. Noben komentar se ne ujema. + No comments yet. Komentarjev še ni. + No contributor data was built into this copy. V to kopijo ni vgrajenih podatkov o sodelujočih. + No differences in the text. V besedilu ni razlik. + No digital IDs yet. Digitalnih identitet še ni. + No documents are open Noben dokument ni odprt + No file chosen Nobena datoteka ni izbrana + No fonts are referenced by the pages. Strani se ne sklicujejo na nobeno pisavo. + No form fields were detected Polj obrazca ni bilo zaznati + No inaccessible scripts Ni nedostopnih skriptov + No issues found Težav ni bilo najdenih + No limit Brez omejitve + No matches Ni zadetkov + No matches. Ni zadetkov. + No matching tools Ni ustreznih orodij + No model credits were built into this copy. V to kopijo ni vgrajenih zahval modelom. + No pages match those choices Nobena stran se ne ujema s temi izbirami + No pages selected Nobena stran ni izbrana + No preview Ni predogleda + No problems found: the document conforms to {level}. Težav ni bilo najdenih: dokument je skladen z {level}. + No recent files Ni nedavnih datotek + No security Brez zaščite + No sheets Ni listov + No status has been set. Stanje ni nastavljeno. + No unlinked web addresses were found in the text V besedilu ni nepovezanih spletnih naslovov + None Brez + None (prefix only) Brez (samo predpona) + None found Ni najdeno + None of those files can be made into a PDF; use PDFs, images or .txt files Nobene od teh datotek ni mogoče pretvoriti v PDF; uporabite datoteke PDF, slike ali .txt + None — everything is allowed Brez — vse je dovoljeno + Not allowed Ni dovoljeno + Not present Ni prisotno + Note Zapisek + Nothing to redo Ni dejanj za ponovitev + Nothing to undo Ni dejanj za razveljavitev + November November + Number Številka + Number every page in the footer. Oštevilči vsako stran v nogi. + Number of pages Število strani + Number pages Oštevilči strani + Number pages… Oštevilči strani… + OK V redu + October Oktober + Odd Pages Only Samo lihe strani + Odd pages Lihe strani + Odd pages only Samo lihe strani + Off Izklopljeno + Old file: Stara datoteka: + On Blur Ob izgubi fokusa + On Focus Ob fokusu + On the web, open or drop an image to convert it V spletni različici odprite ali spustite sliko za pretvorbo + One {f} file per page, named after the document. Ena datoteka {f} na stran, poimenovana po dokumentu. + Only continue if you trust this document. An address can carry information from the document, such as what you typed into its form. Nadaljujte samo, če temu dokumentu zaupate. Naslov lahko prenaša podatke iz dokumenta, na primer to, kar ste vnesli v njegov obrazec. + Only the document's owner can change its security (open it with the permissions password) Varnostne nastavitve lahko spremeni samo lastnik dokumenta (odprite ga z geslom za dovoljenja) + Only the page content changes: links, comments, form fields and bookmarks on the original pages stay. Spremeni se samo vsebina strani: povezave, komentarji, polja obrazca in zaznamki na izvirnih straneh ostanejo. + Opacity Neprosojnost + Open Odpri + Open Recent Odpri nedavne + Open a PDF (⌘O) Odpri PDF (⌘O) + Open a PDF to measure it. Odprite PDF, da ga izmerite. + Open a document first Najprej odprite dokument + Open a web link Odpri spletno povezavo + Open a web page Odpri spletno stran + Open accessibility report Odpri poročilo o dostopnosti + Open email app Odpri e-poštni program + Open file Odpri datoteko + Open in Full Screen mode Odpri v celozaslonskem načinu + Open in a new tab Odpri v novem zavihku + Open link Odpri povezavo + Open the file as it was saved then Odpri datoteko, kakršna je bila takrat shranjena + Open the older version first Najprej odprite starejšo različico + Open this web page? Odpreti to spletno stran? + Open to page Odpri na strani + Opened with Odprto z + Opened, but its security settings don't allow combining. Enter the permissions password. Odprto, vendar njegove varnostne nastavitve ne dovoljujejo združevanja. Vnesite geslo za dovoljenja. + Open… Odpri… + Optimization cancelled Optimizacija preklicana + Optimization is already running Optimizacija že poteka + Optimize Optimiziraj + Optimize PDF… Optimiziraj PDF… + Optimize Scanned Documents Optimiziraj skenirane dokumente + Optimize scanned documents Optimiziraj skenirane dokumente + Optimize scanned documents… Optimiziraj skenirane dokumente… + Optimizing… cleaning up Optimiziranje… čiščenje + Optimizing… image {d} of {t} Optimiziranje… slika {d} od {t} + Optimizing… merging identical objects Optimiziranje… združevanje enakih objektov + Optimizing… removing user data Optimiziranje… odstranjevanje uporabniških podatkov + Optimizing… writing the copy Optimiziranje… zapisovanje kopije + Optional Neobvezno + Optional: needs an AI provider you configure Neobvezno: potrebuje ponudnika umetne inteligence, ki ga nastavite + Options Možnosti + Options… Možnosti… + Orange Oranžna + Organization Name Ime organizacije + Organizational Unit Organizacijska enota + Organize pages Organiziraj strani + Orientation: Usmerjenost: + Original: replace pages Izvirnik: zamenjaj strani + Other elements alternate text Nadomestno besedilo drugih elementov + Other elements that require alternate text Drugi elementi, ki zahtevajo nadomestno besedilo + Outdent Zmanjšaj zamik + Outline Oris + Output Izhod + Output appears here. Izhod se prikaže tukaj. + Output intent: {list} Izhodni namen: {list} + Output preview Predogled izhoda + Oval Elipsa + Overlap: Prekrivanje: + Owner password (no restrictions apply) Geslo lastnika (omejitve ne veljajo) + PDF Optimizer Optimizator PDF + PDF or image PDF ali slika + PDF producer Izdelovalec PDF + PDF version Različica PDF + PDF, images and text PDF, slike in besedilo + PDF/A… PDF/A… + PRs PR-ji + Page Stran + Page Content Vsebina strani + Page Only Samo stran + Page Range Obseg strani + Page Range Options Možnosti obsega strani + Page Sizing & Handling Velikost in obravnava strani + Page display Prikaz strani + Page display: {layout} Prikaz strani: {layout} + Page does not require timed responses Stran ne zahteva časovno omejenih odzivov + Page grid Mreža strani + Page labels Oznake strani + Page layout Postavitev strani + Page navigation Krmarjenje po straneh + Page number Številka strani + Page options Možnosti strani + Page order: Vrstni red strani: + Page size Velikost strani + Page thumbnails Pregled strani + Page transitions Prehodi strani + Page will not cause screen flicker Stran ne bo povzročala utripanja zaslona + Page {label} Stran {label} + Page {p} of {n} Stran {p} od {n} + Pages Panel and Page Razdelek in stran s stranmi + Pages from Strani iz + Pages of {name} to combine, e.g. 1-3, 6 Strani datoteke {name} za združitev, npr. 1-3, 6 + Pages per file Strani na datoteko + Pages per sheet: Strani na list: + Pages that are only pictures of text can't be read aloud or searched. Recognize text (OCR) to add a text layer. Strani, ki so samo slike besedila, ni mogoče prebrati na glas ali iskati. Prepoznajte besedilo (OCR), da dodate besedilno plast. + Pages to Print Strani za tiskanje + Pages with links, comments or fields should tab in structure order. The fix sets each page's tab order to Use Document Structure. Strani s povezavami, komentarji ali polji naj se pomikajo s tabulatorjem po vrstnem redu strukture. Popravek nastavi vrstni red tabulatorja vsake strani na Uporabi strukturo dokumenta. + Pages: Strani: + Paper: Papir: + Part of Del + Passed Uspešno + Password Geslo + Password protection will be applied when you save Zaščita z geslom bo uporabljena ob shranjevanju + Password required Potrebno je geslo + Password security Varnost z geslom + Password-protected Zaščiteno z geslom + Paste after Prilepi za + Paste pages Prilepi strani + Path Pot + Paths Poti + Patterns Vzorci + Patterns (graphics objects) Vzorci (grafični objekti) + PdfCraft didn't shut down normally. These documents had changes that were autosaved: PdfCraft se ni zaprl običajno. Ti dokumenti so imeli samodejno shranjene spremembe: + PdfCraft on GitHub PdfCraft na GitHubu + PdfCraft web page Spletna stran PdfCraft + PdfCraft works offline. No telemetry, no account, and no cloud processing unless you add a provider. PdfCraft deluje brez povezave. Brez telemetrije, brez računa in brez obdelave v oblaku, razen če dodate ponudnika. + PdfCraft {c} is up to date. PdfCraft {c} je posodobljen. + PdfCraft {v} is available. Na voljo je PdfCraft {v}. + People may need more time to respond. Check that nothing has a time limit. Ljudje bodo morda potrebovali več časa za odziv. Preverite, da nič nima časovne omejitve. + Percentage Odstotek + Perimeter Obseg + Permitted actions after certifying Dovoljena dejanja po overitvi + Phone Number Telefonska številka + Phone Numbers Telefonske številke + Pick a tool in the quick bar, or type above. Izberite orodje v hitri vrstici ali tipkajte zgoraj. + Pictures need alternate text that describes them, or must be marked decorative. Add alternate text to each figure. Slike potrebujejo nadomestno besedilo, ki jih opisuje, ali pa morajo biti označene kot dekorativne. Vsaki sliki dodajte nadomestno besedilo. + Pin a folder to Home Pripni mapo na domačo stran + Pin a folder to list its newest PDFs here, such as where your scanner or a synced cloud drive saves them. Pripnite mapo, da se tukaj prikažejo njeni najnovejši PDF-ji, na primer tja, kamor shranjuje vaš skener ali sinhroniziran pogon v oblaku. + Pin a folder… Pripni mapo… + Pin folder to Home… Pripni mapo na domačo stran… + Pink Roza + Pinned folders need the desktop app Pripete mape zahtevajo namizno aplikacijo + Pinned {folder} to Home Mapa {folder} je pripeta na domačo stran + Place saved {what} Postavi shranjeno: {what} + Plain text in reading order; pages are separated by form feeds. Navadno besedilo v vrstnem redu branja; strani so ločene z znaki za prelom strani. + Planned Načrtovano + Planned for milestone Načrtovano za mejnik + Planned for milestone {m} — open to see what it will include Načrtovano za mejnik {m} — odprite, da vidite, kaj bo vključeval + Planned · {m} Načrtovano · {m} + Points from the page's bottom-left corner. Točke od spodnjega levega kota strani. + Polygon Mnogokotnik + Polyline Lomljenica + Portable mode is off: {folder} can't be written ({error}). Settings are kept in your user folder instead. Prenosni način je izklopljen: v {folder} ni mogoče pisati ({error}). Nastavitve se namesto tega hranijo v vaši uporabniški mapi. + Portrait Pokončno + Portrait Pages Pokončne strani + Portrait pages Pokončne strani + Position Položaj + Post Vstavi + PostScript / EPS PostScript / EPS + PostScript XObjects aren't allowed Objekti XObject PostScript niso dovoljeni + Poster Plakat + Prefix Predpona + Preflight Predpreverjanje + Prepare a form Pripravi obrazec + Prepare for Distribution Pripravi za distribucijo + Prepare for accessibility Pripravi za dostopnost + Prepare for distribution Pripravi za distribucijo + Prepare for distribution… Pripravi za distribucijo… + Presentation (.pptx) Predstavitev (.pptx) + Preview Predogled + Preview: page {p} of {n} Predogled: stran {p} od {n} + Previous (⇧⌘G) Prejšnja (⇧⌘G) + Previous / next page Prejšnja / naslednja stran + Previous figure Prejšnja slika + Previous month Prejšnji mesec + Previous page Prejšnja stran + Previous sheet Prejšnji list + Previous view Prejšnji pogled + Primary language Glavni jezik + Print Natisni + Print (⌘P) Natisni (⌘P) + Print in grayscale Natisni v sivinah + Print page border Natisni rob strani + Print production Priprava za tisk + Print single-sided. Keep the sheets in order, cut at the marks, then stack the piles left to right, top to bottom. Natisnite enostransko. Liste ohranite v vrstnem redu, razrežite po oznakah, nato zložite kupčke od leve proti desni, od zgoraj navzdol. + Print, NextPage, PrevPage, FirstPage or LastPage Print, NextPage, PrevPage, FirstPage ali LastPage + Printer: Tiskalnik: + Printing Tiskanje + Printing allowed Tiskanje dovoljeno + Print… Natisni… + Privacy Zasebnost + Private data of other applications Zasebni podatki drugih programov + Properties… Lastnosti… + Protect Zaščiti + Protect Using Password Zaščiti z geslom + Protect a PDF Zaščiti PDF + Protect using password… Zaščiti z geslom… + Protect with password Zaščiti z geslom + Protected: the combined file won't keep its security settings Zaščiteno: združena datoteka ne bo obdržala varnostnih nastavitev + Public key Javni ključ + Purple Vijolična + PushButton Potisni gumb + Quality Kakovost + Radio Izbirni gumb + Radio Button Properties Lastnosti izbirnega gumba + Radio button Izbirni gumb + Re-type Password Ponovno vnesite geslo + Read Branje + Read Only Samo za branje + Read mode Način branja + Ready Omogočeno + Real name Pravo ime + Reason Razlog + Recent Nedavno + Recognize Text Prepoznaj besedilo + Recognize text Prepoznaj besedilo + Recognize text in multiple files… Prepoznaj besedilo v več datotekah… + Recognize text… Prepoznaj besedilo… + Recognized 1 word Prepoznana je 1 beseda + Recognized {n} words Prepoznanih besed: {n} + Recognized {w} words in 1 file Prepoznanih besed: {w}, v 1 datoteki + Recognized {w} words in {n} files Prepoznanih besed: {w}, v datotekah: {n} + Recognizing text… file {d} of {t} Prepoznavanje besedila… datoteka {d} od {t} + Recognizing text… page {d} of {t} Prepoznavanje besedila… stran {d} od {t} + Recommended tools Priporočena orodja + Recover Obnovi + Recover unsaved documents? Obnoviti neshranjene dokumente? + Rectangle Pravokotnik + Red Rdeča + Redact Počrni + Redact a PDF Počrni PDF + Redact all Počrni vse + Redact pages Počrni strani + Redact pages… Počrni strani… + Redact text and images Počrni besedilo in slike + Redacted area fill colour: Barva polnila počrnjenega območja: + Redaction Tool Properties Lastnosti orodja za počrnitev + Redaction properties… Lastnosti počrnitve… + Redid {label} Ponovljeno: {label} + Redo Ponovi + Redo {action} Ponovi: {action} + Reduce file size Zmanjšaj velikost datoteke + Reduce file size… Zmanjšaj velikost datoteke… + Regularity Pravilnost + Rejected Zavrnjeno + Reliable character encoding is provided Zagotovljeno je zanesljivo kodiranje znakov + Remove Odstrani + Remove all links Odstrani vse povezave + Remove background Odstrani ozadje + Remove checkmark Odstrani kljukico + Remove from Recent Odstrani iz nedavnih + Remove header & footer Odstrani glavo in nogo + Remove hidden information Odstrani skrite podatke + Remove hidden information and flatten comments and form fields, so recipients see only the pages. Odstranite skrite podatke ter sploščite komentarje in polja obrazca, da prejemniki vidijo samo strani. + Remove hidden information, metadata, scripts and embedded content, and rewrite the files. Odstranite skrite podatke, metapodatke, skripte in vdelano vsebino ter prepišite datoteke. + Remove hidden information… Odstrani skrite podatke… + Remove invalid links and bookmarks Odstrani neveljavne povezave in zaznamke + Remove redaction marks Odstrani oznake za počrnitev + Remove saved {what} Odstrani shranjeno: {what} + Remove security Odstrani zaščito + Remove unreferenced named destinations Odstrani nereferencirane poimenovane cilje + Remove unused objects and merge identical ones Odstrani neuporabljene objekte in združi enake + Remove watermark Odstrani vodni žig + Remove {name} Odstrani: {name} + Remove {name} from Recent Odstrani {name} iz nedavnih + Remove {n} files Odstrani datoteke: {n} + Removed 1 hidden item. Save to remove them from the file. Odstranjen je 1 skriti element. Shranite, da ga odstranite iz datoteke. + Removed {n} hidden items. Save to remove them from the file. Odstranjenih skritih elementov: {n}. Shranite, da jih odstranite iz datoteke. + Rename Preimenuj + Rename bookmark Preimenuj zaznamek + Rendering: hayro (bootstrap) · UI: egui · Icons: Lucide (ISC) · Fonts: Inter, JetBrains Mono, Dancing Script (OFL) Izris: hayro (bootstrap) · Vmesnik: egui · Ikone: Lucide (ISC) · Pisave: Inter, JetBrains Mono, Dancing Script (OFL) + Reopen the files that were open when PdfCraft last closed Ponovno odpri datoteke, ki so bile odprte ob zadnjem zaprtju PdfCrafta + Repair log Dnevnik popravil + Repairs Popravila + Repeat overlay text Ponovi besedilo prekrivanja + Repeated navigation links should be skippable. Check links that appear on every page. Ponavljajoče se navigacijske povezave naj bi bilo mogoče preskočiti. Preverite povezave, ki se pojavijo na vsaki strani. + Replace Zamenjaj + Replace Image… Zamenjaj sliko… + Replace Pages Zamenjaj strani + Replace image Zamenjaj sliko + Replace page Zamenjaj stran + Replace pages Zamenjaj strani + Replace pages… Zamenjaj strani… + Replace text Zamenjaj besedilo + Replaced Zamenjano + Replacement text Nadomestno besedilo + Replacement: pages of {name} Zamenjava: strani datoteke {name} + Replaces the existing one on those pages. Nadomesti obstoječe na teh straneh. + Replacing images arrives on the web with image file pickers Zamenjava slik bo v spletni različici na voljo z izbirniki slikovnih datotek + Replacing images on page {p} arrives on the web with image pickers ({i}) Zamenjava slik na strani {p} bo v spletni različici na voljo z izbirniki slik ({i}) + Reply Odgovori + Report Poročilo + Report Options Možnosti poročila + Report… Poročilo… + Required Obvezno + Requires user to enter a password for: Zahteva, da uporabnik vnese geslo za: + Reset a form Ponastavi obrazec + Reset columns Ponastavi stolpce + Resize comment Spremeni velikost komentarja + Resize window to initial page Prilagodi velikost okna začetni strani + Resolution Ločljivost + Restrictions Omejitve + Retain existing Ohrani obstoječe + Reverse pages Obrni vrstni red strani + Reverse the order Obrni vrstni red + Revert Povrni + Revert to the previously saved version of “{name}”? Changes since then can't be undone afterwards. Povrniti na prej shranjeno različico dokumenta „{name}“? Sprememb od takrat pozneje ni mogoče razveljaviti. + Reverted to the last saved version Povrnjeno na zadnjo shranjeno različico + Review Pregled + Review History Zgodovina pregledov + Revisions Revizije + Rich Text Format (.rtf) Rich Text Format (.rtf) + Right Desno + Right Edge Desni rob + Right Footer Text Besedilo noge na desni + Right Header Text Besedilo glave na desni + Rotate Zavrti + Rotate Clockwise Zavrti desno + Rotate Counterclockwise Zavrti levo + Rotate Pages Zavrti strani + Rotate clockwise Zavrti desno + Rotate counterclockwise Zavrti levo + Rotate image Zavrti sliko + Rotate page Zavrti stran + Rotate pages Zavrti strani + Rotate pages clockwise Zavrti strani desno + Rotate pages counterclockwise Zavrti strani levo + Rotate pages… Zavrti strani… + Rotate view Zavrti pogled + Rotate view clockwise Zavrti pogled desno + Rotate view clockwise (⇧⌘+) Zavrti pogled desno (⇧⌘+) + Rotate view counterclockwise Zavrti pogled levo + Rotate: Zavrti: + Rotation Rotacija + Rows Vrstice + Run Zaženi + Run JavaScript Zaženi JavaScript + Run a JavaScript Zaženi JavaScript + Run form script Zaženi skript obrazca + Running action… file {d} of {t} Izvajanje dejanja… datoteka {d} od {t} + SHA-1 digest Izvleček SHA-1 + SHA-256 digest Izvleček SHA-256 + Sanitize Očisti + Sanitize Documents Očisti dokumente + Sanitize document Očisti dokument + Sanitize document… Očisti dokument… + Sanitizing removes hidden information from the document: metadata, file attachments, comments, form fields (flattened), hidden text and layers, bookmarks, links, actions and scripts, and private application data. Čiščenje odstrani skrite podatke iz dokumenta: metapodatke, priponke datotek, komentarje, polja obrazca (sploščena), skrito besedilo in plasti, zaznamke, povezave, dejanja in skripte ter zasebne podatke programov. + Save Shrani + Save & Close Shrani in zapri + Save (M4) Shrani (M4) + Save Image As… Shrani sliko kot… + Save as PDF Shrani kot PDF + Save as PDF/A Shrani kot PDF/A + Save as PDF/A-1b Shrani kot PDF/A-1b + Save as PDF/A-2b Shrani kot PDF/A-2b + Save as PDF/A-3b Shrani kot PDF/A-3b + Save as PDF/UA Shrani kot PDF/UA + Save as PDF/X Shrani kot PDF/X + Save as… Shrani kot… + Save attachment… Shrani priponko… + Save changes to “{name}” before closing? Ali želite shraniti spremembe v „{name}“? + Save image Shrani sliko + Save pages Shrani strani + Save the accessibility report Shrani poročilo o dostopnosti + Save the document afterwards: saving rewrites the whole file so no trace of the removed content stays in it. Nato shranite dokument: shranjevanje prepiše celotno datoteko, tako da v njej ne ostane nobena sled odstranjene vsebine. + Save the document afterwards; saving rewrites the whole file so nothing removed stays in it. Nato shranite dokument; shranjevanje prepiše celotno datoteko, tako da v njej ne ostane nič odstranjenega. + Save the spreadsheet Shrani preglednico + Save these pages as one PDF Shrani te strani kot en PDF + Saved measurements Shranjene meritve + Saved measurements. Shranjene meritve. + Saved the print-ready PDF to {path} Za tisk pripravljen PDF je shranjen v {path} + Saved {name} Shranjeno: {name} + Saved {place}: {before} → {after} ({pct}% smaller){detail} Shranjeno {place}: {before} → {after} ({pct}% manjše){detail} + Saved, but reopening failed: {e} Shranjeno, vendar ponovno odpiranje ni uspelo: {e} + Scale relative to target page Merilo glede na ciljno stran + Scan & OCR Skeniranje in OCR + Screen flicker Utripanje zaslona + Screen readers choose their voice from the document's language. Set it in Document Properties > Advanced > Language. Bralniki zaslona izberejo glas glede na jezik dokumenta. Nastavite ga v Lastnosti dokumenta > Napredno > Jezik. + Script Name: Ime skripta: + Script: Skript: + Scripted content must be usable with a keyboard and assistive technology. Check the document's scripts. Vsebina s skripti mora biti uporabna s tipkovnico in podpornimi tehnologijami. Preverite skripte dokumenta. + Scripts Skripti + Scripts run in a sandbox without file or network access. A script that asks to open a web page needs your permission first. With JavaScript off, Acrobat's standard format, validate and calculate functions still work. Skripti se izvajajo v peskovniku brez dostopa do datotek ali omrežja. Skript, ki želi odpreti spletno stran, najprej potrebuje vaše dovoljenje. Ko je JavaScript izklopljen, Acrobatove standardne funkcije za oblikovanje, preverjanje in izračun še vedno delujejo. + Scripts run in a sandbox without file or network access. With JavaScript off, Acrobat's standard format, validate and calculate functions still work. Skripti se izvajajo v peskovniku brez dostopa do datotek ali omrežja. Ko je JavaScript izklopljen, Acrobatove standardne funkcije za oblikovanje, preverjanje in izračun še vedno delujejo. + Scroll long text Pomikaj dolgo besedilo + Search Iskanje + Search comments Išči po komentarjih + Search tools and commands… Poišči orodja in ukaze… + Searchable Image (Exact) Iskalna slika (natančna) + Searching… {s} of {p} pages Iskanje… {s} od {p} strani + Searching… {s}/{p} Iskanje… {s}/{p} + Security Varnost + Security method Varnostna metoda + Security properties Varnostne lastnosti + Security properties… Varnostne lastnosti… + Security settings Varnostne nastavitve + Security settings must allow assistive technology to read the content ("Enable text access for screen reader devices"). Change the document's security to allow it. Varnostne nastavitve morajo podpornim tehnologijam omogočati branje vsebine („Omogoči dostop do besedila bralnikom zaslona“). Spremenite varnost dokumenta, da to dovolite. + Security will be removed when you save Zaščita bo odstranjena ob shranjevanju + Select (V) Izberi (V) + Select Action: Izberite dejanje: + Select All Izberi vse + Select Icon Izberite ikono + Select Trigger: Izberite sprožilec: + Select a file first Najprej izberite datoteko + Select a file for the stamp Izberite datoteko za žig + Select a password-protected file first Najprej izberite z geslom zaščiteno datoteko + Select a word Izberi besedo + Select all pages (Organize) Izberi vse strani (Organiziraj) + Select data files to merge Izberite podatkovne datoteke za združitev + Select files to compare Izberite datoteke za primerjavo + Select format category: Izberite kategorijo oblike: + Select pages Izberi strani + Select the items to remove from this document. Izberite elemente, ki jih želite odstraniti iz tega dokumenta. + Selected image files: {count} Izbrane slikovne datoteke: {count} + Selected pages Izbrane strani + Selection Izbor + Self-signed Samopodpisano + Sensitivity Občutljivost + Sent to {printer} Poslano na {printer} + Sent to {printer}: {msg} Poslano na {printer}: {msg} + Separator style: Slog ločila: + September September + Serial number Serijska številka + Set Alternate Text Nastavi nadomestno besedilo + Set Fields to Same Size Nastavi polja na enako velikost + Set Page Boxes Nastavi robove strani + Set alternate text Nastavi nadomestno besedilo + Set bookmark destination Nastavi cilj zaznamka + Set document title Nastavi naslov dokumenta + Set layer visibility Nastavi vidnost plasti + Set measurement scale Nastavi merilo meritev + Set page boxes Nastavi robove strani + Set page boxes… Nastavi robove strani… + Set properties Nastavi lastnosti + Set status Nastavi status + Set status Accepted Nastavi status Sprejeto + Set status Cancelled Nastavi status Preklicano + Set status Completed Nastavi status Končano + Set status None Nastavi status Brez + Set status Rejected Nastavi status Zavrnjeno + Set status Unmarked Nastavi status Neoznačeno + Set tab order Nastavi vrstni red tabulatorja + Set the image of {name} Nastavi sliko za {name} + Set to current page ({n}) Nastavi na trenutno stran ({n}) + Set to zero Nastavi na nič + Settings Nastavitve + Shadings Senčenja + Sheet {s} of {n} List {s} od {n} + Shift constrains to 45 degrees. Backspace removes a vertex. Escape cancels. Shift omeji na 45 stopinj. Backspace odstrani oglišče. Escape prekliče. + Show Pokaži + Show Report Pokaži poročilo + Show a field Pokaži polje + Show all Pokaži vse + Show all comments Pokaži vse komentarje + Show certificate… Pokaži potrdilo… + Show cover page in two-page view Pokaži naslovnico v dvostranskem pogledu + Show layer Pokaži plast + Show less Pokaži manj + Show password Pokaži geslo + Show the accessibility report Pokaži poročilo o dostopnosti + Show this dialog when the Checker starts Pokaži to okno ob zagonu preverjanja + Show {n} more Pokaži še {n} + Show: Pokaži: + Shrink oversized pages Skrči prevelike strani + Side panels Stranski razdelki + Sign Podpiši + Sign Here Podpišite tukaj + Sign this field… Podpiši to polje… + Sign with a Digital ID Podpiši z digitalno identiteto + Sign yourself Podpišite sami + Signature Podpis + Signature Details Podrobnosti podpisa + Signature is INVALID: Podpis je NEVELJAVEN: + Signature is valid: Podpis je veljaven: + Signature panel Razdelek s podpisom + Signature validity is UNKNOWN: Veljavnost podpisa je NEZNANA: + Signatures Podpisi + Signatures panel Razdelek s podpisi + Signed and all signatures are valid. Podpisano in vsi podpisi so veljavni. + Signed and saved to {path} Podpisano in shranjeno v {path} + Signer Podpisnik + Signing time Čas podpisa + Simplified field notation: Poenostavljen zapis polj: + Single Page Ena stran + Single Page Continuous Ena stran, neprekinjeno + Single file Ena datoteka + Single page Ena stran + Single word or phrase Ena beseda ali besedna zveza + Size Velikost + Skip Rule Pravilo preskoka + Skipped Preskočeno + Snap to drawing Pripni na risbo + Snapping Pripenjanje + Social Security Number Številka socialnega zavarovanja + Social Security Numbers Številke socialnega zavarovanja + Solid Polno + Some page content couldn't be read; snapping uses the rest. Nekaterih vsebin strani ni bilo mogoče prebrati; pripenjanje uporablja preostale. + Sort Razvrsti + Sort by Razvrsti po + Sort comments by: Razvrsti komentarje po: + Sort items Razvrsti elemente + Source Vir + Space Audit Pregled prostora + Special Posebno + Split Razdeli + Split document Razdeli dokument + Split document… Razdeli dokument… + Split into files… Razdeli na datoteke… + Spreadsheet (.xlsx) Preglednica (.xlsx) + Square Kvadrat + Squiggly Valovito + Squiggly underline Valovita podčrtava + Squiggly underline text Valovito podčrtaj besedilo + Stamp Žig + Stamp palette Paleta žigov + Stamps Žigi + Standard Business Standardno poslovno + Standard fonts; text outside Windows-1252 isn't supported yet. Standardne pisave; besedilo zunaj nabora Windows-1252 še ni podprto. + Standards Standardi + Star Zvezda + Start Začetek + Start Checking Začni preverjanje + Start page number Številka začetne strani + Status Stanje + Steps Koraki + Sticky note Opomba + Strength: {s} Moč: {s} + Strikethrough Prečrtanje + Strikethrough text Prečrtaj besedilo + Strong Močno + Structure Info Podatki o strukturi + Style Slog + Subject Zadeva + Submit a form Oddaj obrazec + Subset Podmnožica + Summarize Povzemi + Summarize Options Možnosti povzemanja + Summarize comments Povzemi komentarje + Summary Povzetek + Switch light / dark theme Preklopi svetlo / temno temo + Switch to two-page view first to show the cover page Za prikaz naslovnice najprej preklopite na dvostranski pogled + TH and TD TH in TD + TH and TD must be children of TR TH in TD morata biti podrejena elementa elementa TR + TR must be a child of Table, THead, TBody, or TFoot TR mora biti podrejen element elementa Table, THead, TBody ali TFoot + Tab order Vrstni red tabulatorja + Tab order by columns Vrstni red tabulatorja po stolpcih + Tab order by rows Vrstni red tabulatorja po vrsticah + Tab order is consistent with structure order Vrstni red tabulatorja se ujema z vrstnim redom strukture + Tab order: by columns Vrstni red tabulatorja: po stolpcih + Tab order: by document structure Vrstni red tabulatorja: po strukturi dokumenta + Tab order: by rows Vrstni red tabulatorja: po vrsticah + Tab order: move a field with its arrows. Vrstni red tabulatorja: polje premaknite z njegovimi puščicami. + Table Tabela + Table cells belong in a row. Fix the table's tags. Celice tabele sodijo v vrstico. Popravite oznake tabele. + Table rows belong directly in a table or its head, body or foot. Fix the table's tags. Vrstice tabele sodijo neposredno v tabelo ali njeno glavo, telo ali nogo. Popravite oznake tabele. + Tables Tabele + Tables must contain the same number of columns in each row and rows in each column Tabele morajo imeti v vsaki vrstici enako število stolpcev in v vsakem stolpcu enako število vrstic + Tables must have a summary Tabele morajo imeti povzetek + Tables should have headers Tabele naj bi imele glave + Tagged PDF Označen PDF + Tagged annotations Označene opombe + Tagged content Označena vsebina + Tagged form fields Označena polja obrazca + Tagged multimedia Označene večpredstavnostne vsebine + Tags give the document a logical structure that assistive technology follows. Tag the document (Automatically tag PDF). Oznake dajo dokumentu logično strukturo, ki ji sledijo podporne tehnologije. Označite dokument (Samodejno označi PDF). + Take a snapshot Ustvari posnetek + Text Besedilo + Text (plain) Besedilo (navadno) + Text Color: Barva besedila: + Text Field Properties Lastnosti besedilnega polja + Text alignment: Poravnava besedila: + Text box Besedilni okvir + Text field Besedilno polje + Text in images isn't found; recognise text (OCR) first. Besedilo v slikah ni najdeno; najprej prepoznajte besedilo (OCR). + Text is compared word by word; the differences are shown on the new file. Besedilo se primerja po besedah; razlike so prikazane na novi datoteki. + Text language is specified Jezik besedila je določen + Text must contrast enough with its background for people with low vision. Check the colours by eye or with a contrast tool. Besedilo mora imeti dovolj kontrasta z ozadjem za slabovidne ljudi. Barve preverite na oko ali z orodjem za kontrast. + Text recognition is already running Prepoznavanje besedila že poteka + Text recognition isn't installed: its models are missing (run `cargo xtask models`, or set PDFCRAFT_MODELS). Prepoznavanje besedila ni nameščeno: manjkajo njegovi modeli (zaženite `cargo xtask models` ali nastavite PDFCRAFT_MODELS). + Text to find Besedilo za iskanje + Text to insert Besedilo za vstavljanje + That didn't work: an internal error stopped it ({m}). To ni uspelo: ustavila ga je notranja napaka ({m}). + That didn't work: an internal error stopped it ({m}). Your documents are unchanged. To ni uspelo: ustavila ga je notranja napaka ({m}). Vaši dokumenti so nespremenjeni. + The Keychain's digital IDs couldn't be listed: {e} Digitalnih identitet iz Keychaina ni bilo mogoče prikazati: {e} + The Windows store's digital IDs couldn't be listed: {e} Digitalnih identitet iz shrambe Windows ni bilo mogoče prikazati: {e} + The checker found 1 problem which may prevent the document from being fully accessible. Preverjanje je našlo 1 težavo, ki morda preprečuje popolno dostopnost dokumenta. + The checker found no problems in this document. Preverjanje v tem dokumentu ni našlo težav. + The checker found {n} problems which may prevent the document from being fully accessible. Preverjanje je našlo težave: {n}, ki morda preprečujejo popolno dostopnost dokumenta. + The clipboard has no image or text Odložišče nima slike ali besedila + The document changed while you were choosing a file, so nothing was changed. Dokument se je spremenil, medtem ko ste izbirali datoteko, zato ni bilo spremenjeno nič. + The document has additional actions (/AA) Dokument ima dodatna dejanja (/AA) + The document has document-level JavaScript Dokument vsebuje JavaScript na ravni dokumenta + The document has no XMP metadata Dokument nima metapodatkov XMP + The document is encrypted Dokument je šifriran + The document now conforms to {level}; save it to keep the changes Dokument je zdaj skladen z {level}; shranite ga, da obdržite spremembe + The document opens without a password; this password is needed to change the restrictions. It's applied when you save. Dokument se odpre brez gesla; to geslo je potrebno za spremembo omejitev. Uporabi se ob shranjevanju. + The document was closed before it could be saved. Dokument je bil zaprt, preden ga je bilo mogoče shraniti. + The document's security settings don't allow filling in form fields Varnostne nastavitve dokumenta ne dovoljujejo izpolnjevanja polj obrazca + The document's security settings don't allow this change Varnostne nastavitve dokumenta ne dovoljujejo te spremembe + The form asks to be submitted to {u}; PdfCraft doesn't send form data. Save the document to keep your entries. Obrazec želi biti oddan na {u}; PdfCraft ne pošilja podatkov obrazcev. Shranite dokument, da obdržite svoje vnose. + The form asks viewers to make appearances (NeedAppearances) Obrazec zahteva, da gledalci ustvarijo videze (NeedAppearances) + The key is in the Windows certificate store, which may ask to allow PdfCraft to use it. Ključ je v shrambi potrdil Windows, ki lahko zahteva dovoljenje, da ga PdfCraft uporabi. + The key is in the macOS Keychain, which may ask to allow PdfCraft to use it. Ključ je v macOS Keychain, ki lahko zahteva dovoljenje, da ga PdfCraft uporabi. + The newest {n} of {total} PDFs Najnovejših {n} od {total} datotek PDF + The operation failed. Operacija ni uspela. + The pages were extracted but not deleted, because the document changed while you were choosing a folder. Strani so bile izvlečene, a ne izbrisane, ker se je dokument spremenil, medtem ko ste izbirali mapo. + The password is only used to read the file; it isn't saved. Geslo se uporabi samo za branje datoteke; ni shranjeno. + The passwords don't match. Gesli se ne ujemata. + The replacement file doesn't have that many pages. Nadomestna datoteka nima toliko strani. + The same password is tried on each file. Isto geslo se poskusi pri vsaki datoteki. + The selected area has been copied ({w} × {h} pixels) Izbrano območje je kopirano ({w} × {h} slikovnih pik) + The signature will be invisible. Podpis bo nevidni. + The text is the same Besedilo je enako + The title in the metadata and the document information differ Naslov v metapodatkih in podatkih o dokumentu se razlikujeta + The value is shown as typed. Vrednost je prikazana, kot je vnesena. + The window should show a meaningful title rather than the file name. Set a title and Initial View > Show: Document Title. Okno naj prikazuje smiseln naslov namesto imena datoteke. Nastavite naslov in Začetni pogled > Pokaži: Naslov dokumenta. + There are no redaction marks (mark text, areas or pages first) Ni oznak za počrnitev (najprej označite besedilo, območja ali strani) + There are no redaction marks to apply Ni oznak za počrnitev, ki bi jih bilo mogoče uveljaviti + Thick Debelo + Thickness Debelina + Thin Tanko + This area has too many overlapping paths; intersection snapping is limited. To območje ima preveč prekrivajočih se poti; pripenjanje na preseke je omejeno. + This certificate is in your list of trusted certificates. To potrdilo je na vašem seznamu zaupanja vrednih potrdil. + This certificate is not trusted. Signatures made with it show an unknown identity. Temu potrdilu se ne zaupa. Podpisi, narejeni z njim, kažejo neznano identiteto. + This certificate is trusted through a certificate above it in the chain. Temu potrdilu se zaupa prek potrdila nad njim v verigi. + This comment's appearance can't be changed yet. Videza tega komentarja še ni mogoče spremeniti. + This compatibility level supports only plain ASCII passwords. Ta raven združljivosti podpira samo navadna gesla ASCII. + This dialog shows the details of a certificate and its chain. To okno prikazuje podrobnosti potrdila in njegove verige. + This document can't be checked: it couldn't be read for editing Tega dokumenta ni mogoče preveriti: ni bilo mogoče prebrati za urejanje + This document contains {n} interactive form fields. Ta dokument vsebuje interaktivna polja obrazca: {n}. + This document has no attachments. Ta dokument nima priponk. + This document has no bookmarks. Ta dokument nima zaznamkov. + This document has no comments to flatten Ta dokument nima komentarjev za sploščitev + This document has no document-level scripts. Ta dokument nima skriptov na ravni dokumenta. + This document has no form fields to flatten Ta dokument nima polj obrazca za sploščitev + This document has no form fields you can fill in Ta dokument nima polj obrazca, ki bi jih lahko izpolnili + This document has no form fields. Ta dokument nima polj obrazca. + This document has no layers. Ta dokument nima plasti. + This document has no pages. Ta dokument nima strani. + This document has no signatures. Ta dokument nima podpisov. + This document has no tagged figures. Ta dokument nima označenih slik. + This document has no {kind} to change Ta dokument nima ničesar za spremembo: {kind} + This document hasn't been checked yet. Ta dokument še ni bil preverjen. + This document is secured. Some changes are restricted by its security settings. Ta dokument je zaščiten. Nekatere spremembe so omejene zaradi varnostnih nastavitev. + This document isn't password-protected Ta dokument ni zaščiten z geslom + This drawing exceeds the snapping limit; only the first paths are used. Ta risba presega mejo pripenjanja; uporabljene so samo prve poti. + This field is locked. Unlock it to change its properties. To polje je zaklenjeno. Odklenite ga, da spremenite njegove lastnosti. + This file was damaged and has been repaired. Saving keeps the repaired version. Ta datoteka je bila poškodovana in je bila popravljena. Shranjevanje obdrži popravljeno različico. + This folder isn't available right now. It may have moved, or its drive or cloud sync is offline. Ta mapa trenutno ni na voljo. Morda je bila premaknjena ali pa je njen pogon ali sinhronizacija v oblaku brez povezave. + This form also contains XFA data, which PdfCraft doesn't read yet. You can fill its fields, but Acrobat may show the XFA values instead. Ta obrazec vsebuje tudi podatke XFA, ki jih PdfCraft še ne bere. Polja lahko izpolnite, vendar Acrobat morda prikaže vrednosti XFA. + This is a dynamic XFA form, which PdfCraft can't display yet. What you see is the file's placeholder page. To je dinamični obrazec XFA, ki ga PdfCraft še ne more prikazati. Tisto, kar vidite, je nadomestna stran datoteke. + This measurement needs at least {n} points. Ta meritev potrebuje vsaj točk: {n}. + This page couldn't be displayed.\n{e} Te strani ni bilo mogoče prikazati.\n{e} + This web address mixes letters from different alphabets, a common way to imitate another site's name. Ta spletni naslov meša črke iz različnih abeced, kar je pogost način posnemanja imena druge strani. + This web address uses letters from another alphabet, which can look like familiar ones. Ta spletni naslov uporablja črke iz druge abecede, ki so lahko podobne znanim črkam. + Thumbnails Sličice + Tile scale: Merilo ploščic: + Time Čas + Timed responses Časovno omejeni odzivi + Timestamp Časovni žig + Title Naslov + To: Do: + Toggle fit page / fit width Preklopi prilagoditev strani / širini + Tool Orodje + Tooltip: Namig: + Top Zgoraj + Top-level bookmarks ({n}) Zaznamki najvišje ravni ({n}) + Total Skupaj + Translate Prevedi + Trust Zaupanje + Try the form as people filling it in will see it Preizkusite obrazec tako, kot ga bodo videli ljudje, ki ga izpolnjujejo + Two-Up (Cover Page) Dve strani (z naslovnico) + Two-Up (Facing) Dve strani (nasproti) + Two-Up Continuous (Cover Page) Dve strani, neprekinjeno (z naslovnico) + Two-Up Continuous (Facing) Dve strani, neprekinjeno (nasproti) + Two-page view Dvostranski pogled + Two-sided: Obojestransko: + Type Vrsta + Type Password Vnesite geslo + Type a password. Vnesite geslo. + Type text Vtipkaj besedilo + Type the web address. Vnesite spletni naslov. + Type your {what}. Vnesite: {what}. + Typewriter Pisalni stroj + URL to submit to Naslov URL za oddajo + Unchecked Neoznačeno + Underline Podčrtaj + Underline text Podčrtaj besedilo + Undid {label} Razveljavljeno: {label} + Undo Razveljavi + Undo {action} Razveljavi: {action} + Unit Enota + Units Enote + Unknown Neznano + Unknown author Neznan avtor + Unlock Odkleni + Unlock comment Odkleni komentar + Unlock {name} Odkleni: {name} + Unlock {n} files Odkleni datoteke: {n} + Unlocked {n} of {m} files Odklenjenih datotek: {n} od {m} + Unlocked: the combined file won't be password-protected Odklenjeno: združena datoteka ne bo zaščitena z geslom + Unlock… Odkleni… + Unpin Odpni + Unpin {folder} Odpni: {folder} + Unplaced Nepostavljeno + Unsupported measurements Nepodprte meritve + Up Navzgor + Update Posodobi + Update background Posodobi ozadje + Update background… Posodobi ozadje… + Update header & footer Posodobi glavo in nogo + Update header & footer… Posodobi glavo in nogo… + Update watermark Posodobi vodni žig + Update watermark… Posodobi vodni žig… + Use 72 DPI (one point per pixel) Uporabi 72 DPI (ena točka na slikovno piko) + Use Flate to encode streams that are not encoded Za kodiranje nekodiranih tokov uporabi Flate + Use JavaScript Uporabi JavaScript + Use a Digital ID from a file Uporabi digitalno identiteto iz datoteke + Use a certificate Uporabi potrdilo + Use custom DPI Uporabi lasten DPI + Use each image's embedded DPI; use 72 DPI when it is absent. Uporabi vdelani DPI vsake slike; če ga ni, uporabi 72 DPI. + Use guided actions Uporabi vodena dejanja + Use image resolution Uporabi ločljivost slike + Use now Uporabi zdaj + Use overlay text Uporabi besedilo prekrivanja + Use print production Uporabi pripravo za tisk + Use system setting Uporabi sistemsko nastavitev + Use {what} Uporabi: {what} + Used when a PDF doesn't ask for a layout or zoom. Continuous scrolling never snaps between pages. Uporabi se, kadar PDF ne zahteva postavitve ali povečave. Neprekinjeno pomikanje se nikoli ne pripne med strani. + Used when opening PDFs. An explicit launch or control mode takes precedence for the session. Uporabi se pri odpiranju datotek PDF. Izrecen način zagona ali upravljanja ima za sejo prednost. + User Interface Options Možnosti uporabniškega vmesnika + User password Geslo uporabnika + Username Uporabniško ime + Valid Veljavno + Valid for 5 years, for digital signatures. Self-signed IDs are usually not trusted by others. Velja 5 let, za digitalne podpise. Samopodpisanim identitetam drugi običajno ne zaupajo. + Valid from Velja od + Valid to Velja do + Validate Preveri veljavnost + Validate all Preveri vse + Validate all signatures Preveri vse podpise + Validity ends Veljavnost se konča + Validity starts Veljavnost se začne + Value is not calculated Vrednost ni izračunana + Value is the Vrednost je + Value: Vrednost: + Verify Preveri + Verify to see what the document needs. Preverite, da vidite, kaj dokument potrebuje. + Version Različica + Version {v} Različica {v} + Vertical Navpično + Vertical reversed Navpično obrnjeno + Vertically Navpično + Video, sound and rich media must be tagged to be announced. Tag the multimedia annotations. Video, zvok in bogate predstavnostne vsebine morajo biti označeni, da jih podporne tehnologije izgovorijo. Označite večpredstavnostne opombe. + View less Pokaži manj + View more Pokaži več + View revision {n} Poglej revizijo {n} + View signed version Poglej podpisano različico + Viewing Ogled + Viewport Vidno območje + Viewport rectangle in page points (x, y, width, height) Pravokotnik vidnega območja v točkah strani (x, y, širina, višina) + Visible Rectangle Viden pravokotnik + Visual Vizualno + Warnings Opozorila + Watermark Vodni žig + Weak Šibko + Welcome to PdfCraft Dobrodošli v PdfCraftu + What word or phrase would you like to search for? Katero besedo ali besedno zvezo želite poiskati? + Whole words only Samo cele besede + Width Širina + Width: Širina: + Window Options Možnosti okna + Windows store · Shramba Windows · + Write this email? Napisati to e-pošto? + Wrong password Napačno geslo + Wrote 1 file to {dir} V {dir} je zapisana 1 datoteka + Wrote {n} files to {dir} Zapisanih datotek: {n}, v {dir} + X Object Forms Obrazci XObject + XFA form: its fields and its XFA data are kept in step, so other viewers show what you fill in. Obrazec XFA: njegova polja in podatki XFA so usklajeni, zato drugi pregledovalniki prikažejo to, kar izpolnite. + Yellow Rumena + Yes Da + You are about to apply 1 redaction mark. Text, images and drawings under the marks, and comments and form fields that overlap them, are removed permanently. Uveljavili boste 1 oznako za počrnitev. Besedilo, slike in risbe pod oznakami ter komentarji in polja obrazca, ki se z njimi prekrivajo, bodo trajno odstranjeni. + You are about to apply {n} redaction marks. Text, images and drawings under the marks, and comments and form fields that overlap them, are removed permanently. Uveljavili boste oznake za počrnitev: {n}. Besedilo, slike in risbe pod oznakami ter komentarji in polja obrazca, ki se z njimi prekrivajo, bodo trajno odstranjeni. + You can pin up to {n} folders. Unpin one first. Pripnete lahko do {n} map. Najprej eno odpnite. + You have version {c}. All releases are listed at {page}. Imate različico {c}. Vse izdaje so navedene na {page}. + You have version {c}. Download the new version from its release page. Imate različico {c}. Novo različico prenesite z njene strani izdaje. + Your changes will be lost if you don't save them. Vaše spremembe bodo izgubljene, če jih ne shranite. + Your name Vaše ime + ZIP ZIP + Zip Code Poštna številka + Zip Code + 4 Poštna številka + 4 + Zoom Povečava + Zoom in Povečaj + Zoom in (⌘+) Povečaj (⌘+) + Zoom in / out (also pinch or ⌘-scroll) Povečaj / pomanjšaj (tudi s ščipanjem ali ⌘ + pomikanjem) + Zoom out Pomanjšaj + Zoom out (⌘−) Pomanjšaj (⌘−) + Zoom to page level Povečaj na raven strani + `{command}` {when} `{command}` {when} + a bookmark cannot be moved inside itself zaznamka ni mogoče premakniti vase + a document must keep at least one page dokument mora obdržati vsaj eno stran + average povprečje + background ozadje + characters znaki + e.g. My stamps npr. Moji žigi + e.g. en-US npr. sl-SI + for images above za slike nad + header or footer glava ali noga + initials začetnice + is available je na voljo + is not available yet še ni na voljo + just now pravkar + maximum največ + minimum najmanj + needs an AI provider (off by default) potrebuje ponudnika umetne inteligence (privzeto izklopljeno) + no such document ni takega dokumenta + none brez + nothing in the file matches this document nič v datoteki se ne ujema s tem dokumentom + nothing to redo ni dejanj za ponovitev + nothing to undo ni dejanj za razveljavitev + of the following fields: naslednjih polj: + of {name} od {name} + of {pages} od {pages} + on page {label} na strani {label} + open to see what it will include odprite, da vidite, kaj bo vključeval + pages strani + product (x) zmnožek (x) + ships in milestone {m} izide v mejniku {m} + signature podpis + sorted ascending razvrščeno naraščajoče + sorted descending razvrščeno padajoče + sum (+) vsota (+) + the document has no form fields dokument nima polj obrazca + the document has no page tree dokument nima drevesa strani + the document has no tags dokument nima oznak + the document is protected by a password dokument je zaščiten z geslom + the image is empty slika je prazna + the password is incorrect geslo je napačno + the title must not be empty naslov ne sme biti prazen + there are no pages to print ni strani za tiskanje + there are no redaction marks to apply ni oznak za počrnitev, ki bi jih bilo mogoče uveljaviti + this document is signed: rewriting it would invalidate its signatures (save it incrementally instead) ta dokument je podpisan: prepis bi razveljavil njegove podpise (namesto tega ga shranite inkrementalno) + this file isn't XFDF, FDF, XML or text form data ta datoteka ni podatkov obrazca XFDF, FDF, XML ali besedilnih + to do + to {n} do {n} + watermark vodni žig + yesterday včeraj + {0}: {1} {0}: {1} + {c} of {n}{more} {c} od {n}{more} + {dpi} pixels/inch {dpi} slikovnih pik/palec + {field}: {e} {field}: {e} + {file} is too large for a stamp (at most {n} MB) Datoteka {file} je prevelika za žig (največ {n} MB) + {folder} is already pinned to Home Mapa {folder} je že pripeta na domačo stran + {kind} Properties Lastnosti: {kind} + {kind} field Polje: {kind} + {kind} · page {p} {kind} · stran {p} + {kind} — click to show it on the page {kind} — kliknite, da ga prikažete na strani + {k} of {n} {k} od {n} + {label} (1 issue) {label} (1 težava) + {label} ({n} issues) {label} (težav: {n}) + {label} failed: {e} {label} ni uspelo: {e} + {month} {day}, {y} {day}. {month} {y} + {month} {y} {month} {y} + {msg} to {dir} {msg} v {dir} + {name} (default) {name} (privzeto) + {name} can't be combined: {why} Datoteke {name} ni mogoče združiti: {why} + {name} has no action {name} nima dejanja + {name} is read-only {name} je samo za branje + {name} submits the form to {url}; PdfCraft doesn't send form data. Save the document to keep your entries. {name} odda obrazec na {url}; PdfCraft ne pošilja podatkov obrazcev. Shranite dokument, da obdržite svoje vnose. + {name} — unsaved changes {name} — neshranjene spremembe + {name}: choosing images arrives on the web with file pickers for images {name}: izbiranje slik bo v spletni različici na voljo z izbirniki slikovnih datotek + {name}: the {other} action isn't supported yet {name}: dejanje {other} še ni podprto + {n} contributors · {c} commits Sodelujočih: {n} · commitov: {c} + {n} days ago pred {n} dnevi + {n} differences found Najdenih razlik: {n} + {n} files Datotek: {n} + {n} files done Dokončanih datotek: {n} + {n} files need attention Datotek, ki zahtevajo pozornost: {n} + {n} h ago pred {n} h + {n} instances Pojavitev: {n} + {n} issues Težav: {n} + {n} marks Oznak: {n} + {n} match(es) marked for redaction. Ujemanj, označenih za počrnitev: {n}. + {n} measurements use an unsupported format and are left out. Meritve z nepodprto obliko so izpuščene: {n}. + {n} min ago pred {n} min + {n} pages Strani: {n} + {n} pages selected Izbranih strani: {n} + {n} pages selected. Izbranih strani: {n}. + {n} selected Izbrano: {n} + {n} {kind} {kind}: {n} + {rule} - {status} {rule} - {status} + {s} action Dejanj: {s} + {s} actions run in the JavaScript engine (M6) Dejanja ({s}) se izvajajo v pogonu JavaScript (M6) + {verb} Background Ozadje: {verb} + {verb} Header and Footer Glava in noga: {verb} + {verb} Watermark Vodni žig: {verb} + {verb} as "{name}" {verb} kot „{name}“ + {who} in this document tried to open an address PdfCraft won't open: {e}. {who} v tem dokumentu je poskusil odpreti naslov, ki ga PdfCraft ne odpre: {e}. + {who} in this document tried to open an address PdfCraft won't open: {e}. Only web (http, https) and email (mailto) links open from documents. {who} v tem dokumentu je poskusil odpreti naslov, ki ga PdfCraft ne odpre: {e}. Iz dokumentov se odpirajo samo spletne (http, https) in e-poštne (mailto) povezave. + {who} in this document wants to open this address in your browser: {who} v tem dokumentu želi ta naslov odpreti v vašem brskalniku: + {who} in this document wants to open your email app with this address: {who} v tem dokumentu želi odpreti vaš e-poštni program s tem naslovom: + {words} on {pages} {words} na {pages} + ΔLOC ΔLOC + — (protection is applied when you save) — (zaščita se uporabi ob shranjevanju) + “{name}” is protected. Enter a password to open it. „{name}“ je zaščiten. Za odpiranje vnesite geslo. + −Bin −Bin + −LOC −LOC +action wizard Start Začni +certificate Subject Imetnik +comment menu Edit Uredi +signature pad Type Vtipkaj +@plural {n} field|{n} fields {n} polje|{n} polji|{n} polja|{n} polj + ({n} at {dpi} dpi, the largest size they can be drawn at) ({n} pri {dpi} dpi, največja velikost, v kateri se lahko izrišejo) + A selected field has no widget on any page, so the fields can't be edited together. Izbrano polje nima gradnika na nobeni strani, zato polj ni mogoče urejati skupaj. + Butt Raven rob + Character limit 0 means no limit. A comb of characters needs a limit. Omejitev znakov 0 pomeni brez omejitve. Glavnik znakov potrebuje omejitev. + Character limit: Omejitev znakov: + Closed arrow Zaprta puščica + Comb of characters Glavnik znakov + Couldn't open the printer's preferences: {e} Nastavitev tiskalnika ni bilo mogoče odpreti: {e} + Custom font Pisava po meri + Date format Oblika datuma + Document properties ({key}) Lastnosti dokumenta ({key}) + Fill & Sign dates Datumi pri Izpolni in podpiši + Flatten Fill & Sign when saving Splošči Izpolni in podpiši ob shranjevanju + Font size 0 uses automatic sizing. Velikost pisave 0 pomeni samodejno velikost. + Import list… Uvozi seznam… + Larger pages Večje strani + Line ending Konec črte + Mixed Mešano + Multiple words or phrases Več besed ali besednih zvez + New bookmark ({key}) Nov zaznamek ({key}) + New bookmarks from structure Novi zaznamki iz strukture + Next ({key}) Naslednja ({key}) + One word or phrase per line Ena beseda ali besedna zveza na vrstico + Only checked properties will change. Mixed means the selected fields have different values. Spremenile se bodo samo označene lastnosti. Mešano pomeni, da imajo izbrana polja različne vrednosti. + Open a PDF ({key}) Odpri PDF ({key}) + Open arrow Odprta puščica + Opens the printer driver's preferences. They are saved for every print from this computer; the copies, two-sided, grayscale and paper chosen here still apply. Odpre nastavitve gonilnika tiskalnika. Shranjene so za vsako tiskanje s tega računalnika; kopije, obojestransko, sivine in papir, izbrani tukaj, še vedno veljajo. + Pattern Vzorec + PdfCraft is drawing without a graphics processor, so it may be slow. Updating the graphics driver may help. PdfCraft riše brez grafičnega procesorja, zato je lahko počasen. Pomaga lahko posodobitev grafičnega gonilnika. + Previous ({key}) Prejšnja ({key}) + Print ({key}) Natisni ({key}) + Printer properties Lastnosti tiskalnika + Redaction code: Koda počrnitve: + Reset page size Ponastavi velikost strani + Reset to printer defaults Ponastavi na privzete nastavitve tiskalnika + Reverse closed arrow Obrnjena zaprta puščica + Reverse open arrow Obrnjena odprta puščica + Rotate view clockwise ({key}) Zavrti pogled v smeri urnega kazalca ({key}) + Rotation: Rotacija: + Same as interface ({language}) Enako kot vmesnik ({language}) + Scroll tabs left Pomakni zavihke levo + Scroll tabs right Pomakni zavihke desno + Shared Field Properties Skupne lastnosti polj + Shift- or Ctrl/Command-click to select several fields. S Shift ali Ctrl/Command in klikom izberete več polj. + Slash Poševnica + Smaller pages Manjše strani + Text recognition isn't installed: its model files are missing. Reinstall PdfCraft, or set PDFCRAFT_MODELS to the folder that holds them. Prepoznavanje besedila ni nameščeno: manjkajo datoteke njegovih modelov. Ponovno namestite PdfCraft ali nastavite PDFCRAFT_MODELS na mapo, v kateri so. + Text, marks and signatures become part of the page. Other comments stay editable. Besedilo, oznake in podpisi postanejo del strani. Drugi komentarji ostanejo urejljivi. + The document changed while you were editing field properties. Dokument se je spremenil med urejanjem lastnosti polja. + The document is no longer open. Dokument ni več odprt. + The field properties could not be changed. Lastnosti polja ni bilo mogoče spremeniti. + The field properties dialog is no longer open. Pogovorno okno z lastnostmi polja ni več odprto. + The field {name} no longer exists. Polje {name} ne obstaja več. + The printer driver's own settings for this print: paper tray, paper type, finishing and more. Lastne nastavitve gonilnika tiskalnika za to tiskanje: pladenj za papir, vrsta papirja, dodelava in drugo. + The selected fields could not be found. Select them again. Izbranih polj ni bilo mogoče najti. Izberite jih znova. + The standard fonts can't draw “{c}” ({code}). Only Western European characters can be added as text for now. Standardne pisave ne morejo izrisati znaka „{c}“ ({code}). Zaenkrat je kot besedilo mogoče dodati samo zahodnoevropske znake. + This printer offers no settings of its own. Ta tiskalnik nima lastnih nastavitev. + To change locked fields, also set Locked to No. Za spremembo zaklenjenih polj nastavite tudi Zaklenjeno na Ne. + Today: {date} Danes: {date} + Zoom in ({key}) Povečaj ({key}) + Zoom in / out (also pinch or {key}-scroll) Povečaj / pomanjšaj (tudi s ščipanjem ali {key} + pomikanjem) + Zoom out ({key}) Pomanjšaj ({key}) + the text recognition models are not installed (reinstall PdfCraft, or set PDFCRAFT_MODELS to the folder that holds them) modeli za prepoznavanje besedila niso nameščeni (ponovno namestite PdfCraft ali nastavite PDFCRAFT_MODELS na mapo, v kateri so) + yyyy or yy year · m or mm month · mmm or mmmm month name · d or dd day · ddd or dddd weekday. H, h, M, s and t are time letters and can't be used on their own: put {escape} before a letter to show it as is. yyyy ali yy leto · m ali mm mesec · mmm ali mmmm ime meseca · d ali dd dan · ddd ali dddd dan v tednu. H, h, M, s in t so časovne črke in jih ni mogoče uporabiti samostojno: pred črko postavite {escape}, da se prikaže tako, kot je. + {chars} can't be written into the PDF yet: Fill & Sign text is Western European only. Pick a numeric format such as dd.mm.yyyy. Znakov {chars} še ni mogoče zapisati v PDF: besedilo Izpolni in podpiši je samo zahodnoevropsko. Izberite številčno obliko, na primer dd.mm.yyyy. + {name} is too large for a word list (at most 1 MB). Datoteka {name} je prevelika za seznam besed (največ 1 MB). + {n} fields selected Izbranih polj: {n} + {n} word(s) or phrase(s) Besed ali besednih zvez: {n} + Auto ({percent}%) Samodejno ({percent} %) + Auto follows the system's text size. Pages always render at the display's full resolution. Samodejno sledi velikosti besedila v sistemu. Strani se vedno izrišejo v polni ločljivosti zaslona. + Check the pages Preveri strani + Collapse all Strni vse + Column select Izbira stolpcev + Drag the selection to move it. Esc clears the selection. Povlecite izbor, da ga premaknete. Esc počisti izbor. + Expand all Razširi vse + Files are combined in the order shown. Drag them to change it. Datoteke se združijo v prikazanem vrstnem redu. Povlecite jih, da ga spremenite. + Fill colour Barva polnila + Fill colour: none Barva polnila: brez + Finish or discard the text being edited before selecting several objects. Dokončajte ali opustite urejano besedilo, preden izberete več objektov. + Grid view Pogled mreže + Interface size Velikost vmesnika + List view Pogled seznama + Move object Premakni objekt + Move objects Premakni objekte + No fill Brez polnila + Pages: {k} of {n} Strani: {k} od {n} + Preview unavailable: {e} Predogled ni na voljo: {e} + Remove page Odstrani stran + Remove {n} pages Odstrani strani: {n} + Saved {place}: {before} → {after} (not smaller){detail} Shranjeno {place}: {before} → {after} (ni manjše){detail} + Select at most 1000 objects at a time. Naenkrat lahko izberete največ 1000 objektov. + Shift-click to select several objects. Drag empty space to select an area. S Shift in klikom izberete več objektov. Povlecite po praznem prostoru, da izberete območje. + Some bookmarks are not shown here: the panel lists a limited number, nested a limited depth. Nekateri zaznamki tu niso prikazani: seznam prikaže omejeno število zaznamkov do omejene globine gnezdenja. + Switch to List view to change the pages it takes. Preklopite na pogled seznama, da spremenite strani, ki jih zajame. + Text used as a clipping path cannot be moved with this selection. Besedila, uporabljenega kot pot izrezovanja, s tem izborom ni mogoče premakniti. + These objects could not be moved. Select them again and try a smaller move. Teh objektov ni bilo mogoče premakniti. Izberite jih znova in poskusite z manjšim premikom. + This file is already as small as it can be made ({size}). No copy was saved. Ta datoteka je že tako majhna, kot je mogoče ({size}). Kopija ni bila shranjena. + This page contains objects that cannot be selected together. Ta stran vsebuje objekte, ki jih ni mogoče izbrati skupaj. + Vertical text cannot be moved with this selection. Navpičnega besedila s tem izborom ni mogoče premakniti. +key Ctrl Ctrl +key Delete Delete +key End End +key Esc Esc +key Home Home +key Shift Shift +key Space Preslednica +@plural {n} object selected|{n} objects selected {n} izbran objekt|{n} izbrana objekta|{n} izbrani objekti|{n} izbranih objektov + Looking for printers… Iskanje tiskalnikov… + Sending to {printer}… Pošiljanje na {printer}… + Float panel Odtrgaj razdelek + Inspector Inšpektor + Move to group Premakni v skupino + Panels Razdelki + Reset panel layout Ponastavi postavitev razdelkov + Resize panel window Spremeni velikost okna razdelka + Resize panels Spremeni velikost razdelkov + Tools Orodja + +# Added for labels that landed alongside this catalog (2026-10-11). + Appearance mode Način videza + Light Svetlo + Dark Temno + Active Aktivno + Dark theme Temna tema + Appearance settings… Nastavitve videza … + Save ({key}) Shrani ({key}) + Select page {label} Izberi stran {label} + Rotate page {label} counterclockwise Zasukaj stran {label} v nasprotni smeri urinega kazalca + Rotate page {label} clockwise Zasukaj stran {label} v smeri urinega kazalca + Delete page {label} Izbriši stran {label} + Offset from centre Zamik od središča + Graphic Grafika + Signer's name Ime podpisnika + Fill & Sign signature Podpis iz orodja Izpolni in podpiši + Choose image… Izberi sliko … + Enter permissions password… Vnesi geslo za dovoljenja … + Permissions password Geslo za dovoljenja + “{name}” has security restrictions. Enter its permissions password to lift them. Dokument »{name}« ima varnostne omejitve. Vnesite geslo za dovoljenja, da jih odpravite. + Restrictions lifted Omejitve so odpravljene + Couldn't unlock {name}: {e} Dokumenta {name} ni bilo mogoče odkleniti: {e} + This document has no restrictions to lift Ta dokument nima omejitev, ki bi jih bilo treba odpraviti + The document's security settings don't allow copying text and images Varnostne nastavitve dokumenta ne dovolijo kopiranja besedila in slik + Add the files to combine Dodajte datoteke za združevanje + Leave out files that can't be read? Izpustim datoteke, ki jih ni mogoče prebrati? + These files can't be read, so they won't be in the combined PDF: Teh datotek ni mogoče prebrati, zato ne bodo v združenem PDF-ju: + Combine the rest Združi ostale + and {n} more in še {n} + Drop PDFs, images, text files or folders here, or choose them. Sem povlecite datoteke PDF, slike, besedilne datoteke ali mape ali pa jih izberite. + Every PDF, image and text file in the folder Vse datoteke PDF, slike in besedilne datoteke v mapi + Every PDF, image and text file in the folder and the folders inside it Vse datoteke PDF, slike in besedilne datoteke v mapi in njenih podmapah + No PDFs, images or text files found in {folder} V mapi {folder} ni datotek PDF, slik ali besedilnih datotek + Added the first {n} files in {folder} Dodanih je prvih {n} datotek iz mape {folder} + Margins Robovi + For page sizes and margins. Za velikosti strani in robove. + PDF page: {size} Stran PDF: {size} + Match page at 100% Prilagodi strani pri 100 % + PDF scale: {scale}% Merilo PDF: {scale} % + Printer paper and scaling may differ. Check driver settings. Papir in merilo tiskalnika se lahko razlikujeta. Preverite nastavitve gonilnika. + Drawing scale changes. Use Actual size for full-scale plans. Merilo risbe se spremeni. Za načrte v polnem merilu uporabite Dejansko velikost. + Reload documents when their files change on disk Znova naloži dokumente, ko se njihove datoteke na disku spremenijo + This file changed on disk. Reload it to see the new version; your unsaved changes will be lost. Ta datoteka se je na disku spremenila. Znova jo naložite, da vidite novo različico; neshranjene spremembe bodo izgubljene. + Reload Znova naloži + Couldn't reload {name}: {e} Datoteke {name} ni bilo mogoče znova naložiti: {e} + Mouse wheel scroll speed Hitrost drsenja s kolescem miške +scroll speed Slow Počasi +scroll speed Normal Običajno +scroll speed Fast Hitro + Enhance camera image Izboljšaj fotografijo s kamere + Drag the corners onto the document's edges, then choose Enhance page. Povlecite vogale na robove dokumenta, nato izberite Izboljšaj stran. + Enhance page Izboljšaj stran + Done adding text Dodajanje besedila končano + Discard this text (Esc) Zavrzi to besedilo (Esc) + Clean-up Čiščenje + Original Izvirnik + Greyscale Sivine + Black and white Črno-belo diff --git a/crates/ui-egui/tests/fonts.rs b/crates/ui-egui/tests/fonts.rs index 3ccfdf37..bdc53ecf 100644 --- a/crates/ui-egui/tests/fonts.rs +++ b/crates/ui-egui/tests/fonts.rs @@ -218,7 +218,7 @@ fn ui_fonts_work_without_craft_fonts() { theme::install_fonts(&ctx); } -/// Latin- and Cyrillic-script catalogs (Czech, Brazilian Portuguese, German, Spanish, French, Russian, Bulgarian, Hungarian, Ukrainian, Italian) are drawn entirely by the app's own faces +/// 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 /// `has_glyphs` can't answer this: with only the primary face it is also the replacement face.) #[test] @@ -236,6 +236,7 @@ fn primary_ui_fonts_cover_latin_and_cyrillic_catalogs() { ("hu", include_str!("../src/i18n/hu.tsv")), ("uk", include_str!("../src/i18n/uk.tsv")), ("it", include_str!("../src/i18n/it.tsv")), + ("sl", include_str!("../src/i18n/sl.tsv")), ] { let mut text = String::from("áčďéěíňóřšťúůýžÁČĎÉĚÍŇÓŘŠŤÚŮÝŽãõçâêôàÃÕÇÂÊÔÀñÑüÜäÄöÖőűß„“¿¡«»…ґҐєЄіІїЇ"); for line in catalog.lines().filter(|l| !l.starts_with('#')) { diff --git a/docs/localization.md b/docs/localization.md index 456c0824..b19eda5c 100644 --- a/docs/localization.md +++ b/docs/localization.md @@ -1,10 +1,10 @@ # Interface language -Choose **Menu > Edit > Preferences…** (Command-comma on macOS, Ctrl-comma elsewhere) **> Interface language** and select **Auto**, **English**, **日本語**, **简体中文**, **繁體中文**, **Русский**, **Български**, **Čeština**, **Português (Brasil)**, **Deutsch**, **Español**, **Français**, **తెలుగు**, **Magyar**, **Українська**, **Italiano** or **العربية**. The change applies immediately and persists between launches. Command ids, document contents and file names are unchanged. +Choose **Menu > Edit > Preferences…** (Command-comma on macOS, Ctrl-comma elsewhere) **> Interface language** and select **Auto**, **English**, **日本語**, **简体中文**, **繁體中文**, **Русский**, **Български**, **Čeština**, **Português (Brasil)**, **Deutsch**, **Español**, **Français**, **తెలుగు**, **Magyar**, **Українська**, **Italiano**, **العربية** or **Slovenščina**. The change applies immediately and persists between launches. Command ids, document contents and file names are unchanged. Displayed keyboard shortcuts use the same active interface language. For example, German shows `Strg+K` and `Strg+Umschalt+S` in the toolbar, menus, command palette, tooltips and Keyboard shortcuts dialog; macOS keeps its modifier symbols (`⌘`, `⇧`, `⌃`). Key names have their own `key` catalog context, so the Delete key can read `Entf` while the command remains `Löschen`. A missing key-name entry falls back to the English key name. Actual bindings and machine-facing command ids remain stable. -**Auto** (the default) follows the system language: `LC_ALL`, `LC_MESSAGES` or `LANG`, then the preferred-languages list on macOS. Any Portuguese locale (`pt_BR`, `pt_PT`) uses the Brazilian catalog, any French locale (`fr`, `fr_FR`, `fr_CA`, `fr_BE`) the French one, any German locale (`de`, `de_DE`, `de_AT`, `de_CH`) the German one, and Chinese locales for Taiwan, Hong Kong and Macau (`zh_TW`, `zh_HK`, `zh_MO`, `zh-Hant`) the Traditional Chinese one; other Chinese locales (`zh`, `zh_CN`, `zh_SG`, `zh-Hans`) the Simplified Chinese one. Telugu locales (`te`, `te_IN`) use the Telugu catalog, Bulgarian locales (`bg`, `bg_BG`) the Bulgarian one, and Hungarian locales (`hu`, `hu_HU`) the Hungarian one, and Ukrainian locales (`uk`, `uk_UA`) the Ukrainian one, and Italian locales (`it`, `it_IT`, `it_CH`) the Italian one, and Arabic locales (`ar`, `ar_MA`, `ar_EG`, `ar_SA`, …) the Arabic one. A system language without a catalog shows English. On Windows, **Auto** uses the Windows display languages, in preference order, when no supported locale is selected by those environment variables. Neutral `C`/`POSIX` locale values (including `C.UTF-8`) inherited from a launcher do not override the Windows display language; explicitly named supported locales such as `fr_FR.UTF-8` still do. The query asks Windows directly (`GetUserPreferredUILanguages`, through the `sys-locale` crate) without starting a process, and is cached for the process. If detection fails or the display language has no catalog, PdfCraft shows English. +**Auto** (the default) follows the system language: `LC_ALL`, `LC_MESSAGES` or `LANG`, then the preferred-languages list on macOS. Any Portuguese locale (`pt_BR`, `pt_PT`) uses the Brazilian catalog, any French locale (`fr`, `fr_FR`, `fr_CA`, `fr_BE`) the French one, any German locale (`de`, `de_DE`, `de_AT`, `de_CH`) the German one, and Chinese locales for Taiwan, Hong Kong and Macau (`zh_TW`, `zh_HK`, `zh_MO`, `zh-Hant`) the Traditional Chinese one; other Chinese locales (`zh`, `zh_CN`, `zh_SG`, `zh-Hans`) the Simplified Chinese one. Telugu locales (`te`, `te_IN`) use the Telugu catalog, Bulgarian locales (`bg`, `bg_BG`) the Bulgarian one, and Hungarian locales (`hu`, `hu_HU`) the Hungarian one, and Ukrainian locales (`uk`, `uk_UA`) the Ukrainian one, and Italian locales (`it`, `it_IT`, `it_CH`) the Italian one, and Arabic locales (`ar`, `ar_MA`, `ar_EG`, `ar_SA`, …) the Arabic one, and Slovenian locales (`sl`, `sl_SI`) the Slovenian one. A system language without a catalog shows English. On Windows, **Auto** uses the Windows display languages, in preference order, when no supported locale is selected by those environment variables. Neutral `C`/`POSIX` locale values (including `C.UTF-8`) inherited from a launcher do not override the Windows display language; explicitly named supported locales such as `fr_FR.UTF-8` still do. The query asks Windows directly (`GetUserPreferredUILanguages`, through the `sys-locale` crate) without starting a process, and is cached for the process. If detection fails or the display language has no catalog, PdfCraft shows English. The control channel exposes the setting through `ui.set`: @@ -14,12 +14,13 @@ The control channel exposes the setting through `ui.set`: The value is `auto` or a language code (`en`, `ja`, `zh-hans`, `zh-hant`, `ru`, `bg`, `cs`, `pt-br`, `de`, `es`, `fr`, `te`, `hu`, `uk`, `it`, `ar`), in any case. `ui.state` reports `language` as written in the preferences (`auto`, `en`, `ja`, `zh-hans`, `zh-hant`, `ru`, `bg`, `cs`, `pt-br`, `de`, `es`, `fr`, `te`, `hu`, `uk`, `it` or `ar`). Unknown values return an error without changing the current setting. Preferences saved before this setting existed follow the system language. -Dialogs, panels, menus, notices and history labels go through the catalog (`tl!`). Japanese, Traditional Chinese, Simplified Chinese, Russian, Bulgarian, German, Spanish, French, Telugu, Hungarian, Ukrainian, Italian, Brazilian Portuguese and Arabic translate them (about 2,000–2,140 entries each); Czech covers the menus so far. Untranslated labels use English. Error details that come from the engine or the operating system are shown as they are, inside a translated frame ("操作「…」に失敗しました: …"). The command palette matches the translated label, the English label and the command id. Vertical Japanese PDF rendering is an existing viewer feature; this change does not add vertical text editing. Per-language coverage, script support and what each language still needs are in [localization-parity.md](localization-parity.md). +Dialogs, panels, menus, notices and history labels go through the catalog (`tl!`). Japanese, Traditional Chinese, Simplified Chinese, Russian, Bulgarian, German, Spanish, French, Telugu, Hungarian, Ukrainian, Italian, Brazilian Portuguese, Arabic and Slovenian translate them (about 2,000–2,190 entries each); Czech covers the menus so far. Untranslated labels use English. Error details that come from the engine or the operating system are shown as they are, inside a translated frame ("操作「…」に失敗しました: …"). The command palette matches the translated label, the English label and the command id. Vertical Japanese PDF rendering is an existing viewer feature; this change does not add vertical text editing. Per-language coverage, script support and what each language still needs are in [localization-parity.md](localization-parity.md). Whole sentences that the layers below word — what the accessibility checker found, what the PDF/A verifier reports, what a signature's details say, what is wrong with the pages to print, what an edit is called in the history — are translated too, because those layers hand the sentence over apart from the document's own words. See [Sentences the engine words](#sentences-the-engine-words). The Ukrainian catalog has 2,047 entries, written from the meaning of the English labels using Ukrainian computing vocabulary. Commands use infinitives; fixed-count messages use count-neutral wording. Plural-aware messages use the three [CLDR Ukrainian integer forms](https://www.unicode.org/cldr/charts/48/supplemental/language_plural_rules.html#uk): `1 сторінка`, `2 сторінки`, `5 сторінок`; 11–14 take the third form, while 21 and 22 return to the first and second. Calendar dates use genitive month names (`9 жовтня 2026`), while headings use standalone names (`Жовтень 2026`). Home recommendation cards grow to fit translated text; the toolbar search hint truncates before the shortcut, with the full label available on hover. +Japanese interface text uses BIZ UDPGothic from [craft-fonts](https://github.com/storytold/craft-fonts), an optional build input that every release includes (`CRAFT_FONTS_DIR`; see the README). A build made without it has no Japanese face, so Japanese labels show replacement boxes. Chinese is drawn with the same craft-fonts CJK faces (in Simplified Chinese the `Hans` faces come first, so a line never mixes faces with different baselines), so a character none of them has shows a replacement box (and every Chinese character does in a build without craft-fonts). Russian, Bulgarian, Czech, Brazilian Portuguese, German, Spanish, French, Hungarian, Italian and Slovenian need only the bundled Latin faces (tested in `tests/fonts.rs`). Slovenian has four plural forms: one (1, 101, 201…), two (2, 102…), few (3, 4, 103…) and other (everything else, including 0, 5–20 and 21+), so `@plural` entries list four forms (`1 stran`, `2 strani`, `3 strani`, `5 strani`). Hungarian uses the same one/other plural rule as English: 1 is singular, and every other count — including 0 — is the other form. After a numeral the noun stays singular (`1 oldal`, `2 oldal`). Telugu is drawn with a craft-fonts `Telu` face (Noto Sans Telugu, kept in the web build too), whose conjuncts egui shapes; a build without one shows Telugu labels as replacement boxes, and the release-pinned craft-fonts input has no Telugu face yet. Japanese interface text uses BIZ UDPGothic from [craft-fonts](https://github.com/storytold/craft-fonts), an optional build input that every release includes (`CRAFT_FONTS_DIR`; see the README). A build made without it has no Japanese face, so Japanese labels show replacement boxes. Chinese is drawn with the same craft-fonts CJK faces (in Simplified Chinese the `Hans` faces come first, so a line never mixes faces with different baselines), so a character none of them has shows a replacement box (and every Chinese character does in a build without craft-fonts). Until craft-fonts has an allowed `Hans` face, the desktop app draws a Chinese interface with a Chinese face already installed on the machine (Microsoft YaHei or JhengHei on Windows, PingFang, Hiragino Sans GB or STHeiti on macOS, WenQuanYi or Droid Sans Fallback on Linux; never Noto CJK or Source Han), placed before the Japanese faces so a line keeps one face (#826). The web build has no installed fonts to fall back on. Russian, Bulgarian, Czech, Brazilian Portuguese, German, Spanish, French, Hungarian and Italian need only the bundled Latin faces (tested in `tests/fonts.rs`). Hungarian uses the same one/other plural rule as English: 1 is singular, and every other count — including 0 — is the other form. After a numeral the noun stays singular (`1 oldal`, `2 oldal`). Telugu is drawn with a craft-fonts `Telu` face (Noto Sans Telugu, kept in the web build too), whose conjuncts egui shapes; a build without one shows Telugu labels as replacement boxes, and the release-pinned craft-fonts input has no Telugu face yet. The Brazilian Portuguese catalog has 2,000 entries: the ~1,826-entry gist André Cardoso posted in [#288](https://github.com/storytold/pdfcraft/issues/288), the labels added since, and a review of the existing wording. Portuguese, like French, treats 0 and 1 as singular (`1 página`, `2 páginas`). From 58ae136267189c723b0f12eb2f7eb02ec3c3fac5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=CE=93=CE=B9=E1=BF=B6=CF=81=CE=B3=CE=BF=CF=82=20=28George?= =?UTF-8?q?=29?= Date: Sun, 11 Oct 2026 23:16:40 +0300 Subject: [PATCH 2/9] Add Greek (el) interface localization (#994) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(i18n): add Greek localization * Greek: same place in both language lists, covered by the font test; Μαΐ * Greek: rows for the labels that landed alongside this catalog; refresh hashes --------- Co-authored-by: George Athenakis <2944375+Camthalion77@users.noreply.github.com> Co-authored-by: Brandon Thomas --- ATTRIBUTION.md | 3 +- ATTRIBUTION.toml | 11 + crates/engine/src/dates.rs | 11 +- crates/ui-egui/src/i18n/el.tsv | 2268 ++++++++++++++++++++++++++++++++ crates/ui-egui/src/i18n/mod.rs | 3 +- crates/ui-egui/tests/fonts.rs | 1 + 6 files changed, 2294 insertions(+), 3 deletions(-) create mode 100644 crates/ui-egui/src/i18n/el.tsv diff --git a/ATTRIBUTION.md b/ATTRIBUTION.md index b0742370..edcb9c04 100644 --- a/ATTRIBUTION.md +++ b/ATTRIBUTION.md @@ -4,10 +4,11 @@ Every asset PdfCraft includes, bundles or uses to build its published material, with its author, source and licence. The policy is in [AGENTS.md](AGENTS.md) §1. The machine-readable list, with SHA-256 hashes, is [ATTRIBUTION.toml](ATTRIBUTION.toml). Licence texts are kept beside the assets and summarised in [NOTICE](NOTICE). -## In this repository (266) +## In this repository (267) | Asset | Title | Author | Licence | Source | Used for | |---|---|---|---|---|---| +| `crates/ui-egui/src/i18n/el.tsv` | Greek interface translation catalog | Camthalion77 and contributors | MIT OR Apache-2.0 | Contributor-original: clean-room translations of PdfCraft English UI labels | Greek UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs | | `crates/ui-egui/src/i18n/zh-hans.tsv` | Simplified Chinese 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 | Simplified Chinese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs | | `crates/ui-egui/src/i18n/zh-hant.tsv` | Traditional Chinese (Taiwan) 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 | Traditional Chinese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs | | `crates/ui-egui/src/i18n/cs.tsv` | Czech 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 | Czech UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs | diff --git a/ATTRIBUTION.toml b/ATTRIBUTION.toml index c0c4a095..821939c0 100644 --- a/ATTRIBUTION.toml +++ b/ATTRIBUTION.toml @@ -1,3 +1,14 @@ +[[asset]] +path = "crates/ui-egui/src/i18n/el.tsv" +title = "Greek interface translation catalog" +author = "Camthalion77 and contributors" +source = "Contributor-original: clean-room translations of PdfCraft English UI labels" +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 = "23a9402d3ebbcff69aed133706e6e428a804ae06a7dba4923892cca5c98e92d1" + # 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`). # diff --git a/crates/engine/src/dates.rs b/crates/engine/src/dates.rs index ebda6a5f..0feb9eaa 100644 --- a/crates/engine/src/dates.rs +++ b/crates/engine/src/dates.rs @@ -56,7 +56,7 @@ const AR_DAYS: [&str; 7] = ["الأحد", "الاثنين", "الثلاثاء", /// The languages month and weekday names can be written in (Preferences ▸ Date format ▸ Language): /// the interface languages, in the same order and with the same names. -pub const DATE_LANGUAGES: [DateLanguage; 18] = [ +pub const DATE_LANGUAGES: [DateLanguage; 19] = [ DateLanguage { code: "en", name: "English", @@ -285,6 +285,15 @@ pub const DATE_LANGUAGES: [DateLanguage; 18] = [ days: ["nedelja", "ponedeljek", "torek", "sreda", "četrtek", "petek", "sobota"], days_short: ["ned.", "pon.", "tor.", "sre.", "čet.", "pet.", "sob."], }, + DateLanguage { + code: "el", + name: "Ελληνικά", + months: ["Ιανουάριος", "Φεβρουάριος", "Μάρτιος", "Απρίλιος", "Μάιος", "Ιούνιος", "Ιούλιος", "Αύγουστος", "Σεπτέμβριος", "Οκτώβριος", "Νοέμβριος", "Δεκέμβριος"], + months_with_day: Some(["Ιανουαρίου", "Φεβρουαρίου", "Μαρτίου", "Απριλίου", "Μαΐου", "Ιουνίου", "Ιουλίου", "Αυγούστου", "Σεπτεμβρίου", "Οκτωβρίου", "Νοεμβρίου", "Δεκεμβρίου"]), + months_short: ["Ιαν", "Φεβ", "Μαρ", "Απρ", "Μαΐ", "Ιουν", "Ιουλ", "Αυγ", "Σεπ", "Οκτ", "Νοε", "Δεκ"], + days: ["Κυριακή", "Δευτέρα", "Τρίτη", "Τετάρτη", "Πέμπτη", "Παρασκευή", "Σάββατο"], + days_short: ["Κυρ", "Δευ", "Τρί", "Τετ", "Πέμ", "Παρ", "Σάβ"], + }, ]; /// The date language for `code` (any case), or `None` when there's none. diff --git a/crates/ui-egui/src/i18n/el.tsv b/crates/ui-egui/src/i18n/el.tsv new file mode 100644 index 00000000..3c6c2ab4 --- /dev/null +++ b/crates/ui-egui/src/i18n/el.tsv @@ -0,0 +1,2268 @@ +# Greek (el) interface catalog. Columns: context English source Greek translation. +# Partial, clean-room initial contribution. Entries not yet translated fall back to English. +# Translate from English meanings; retain placeholders and trailing ellipses. +# Greek plural forms: one (1) | other (0, 2+). + Menu Μενού + File Αρχείο + Edit Επεξεργασία + Pages Σελίδες + View Προβολή + Help Βοήθεια + Preferences Προτιμήσεις + Preferences… Προτιμήσεις… + Interface language Γλώσσα περιβάλλοντος + Interface size Μέγεθος περιβάλλοντος + Auto ({percent}%) Αυτόματο ({percent}%) + Auto follows the system's text size. Pages always render at the display's full resolution. Η αυτόματη ρύθμιση ακολουθεί το μέγεθος κειμένου του συστήματος. Οι σελίδες αποδίδονται πάντα στην πλήρη ανάλυση της οθόνης. + Identity Ταυτότητα + Name on new comments Όνομα στα νέα σχόλια + Open… Άνοιγμα… + Open Recent Άνοιγμα πρόσφατων + Clear Recent Files Εκκαθάριση πρόσφατων αρχείων + Remove from Recent Αφαίρεση από τα πρόσφατα + Remove {name} from Recent Αφαίρεση του {name} από τα πρόσφατα + Pin folder to Home… Καρφίτσωμα φακέλου στην αρχική… + Folders Φάκελοι + Pin a folder… Καρφίτσωμα φακέλου… + Pin a folder to Home Καρφίτσωμα φακέλου στην αρχική + Unpin Ξεκαρφίτσωμα + Unpin {folder} Ξεκαρφίτσωμα του {folder} + Looking for PDFs… Αναζήτηση αρχείων PDF… + No PDFs in this folder yet Δεν υπάρχουν ακόμη PDF σε αυτόν τον φάκελο + Show less Εμφάνιση λιγότερων + Show {n} more Εμφάνιση {n} ακόμη + The newest {n} of {total} PDFs Τα {n} νεότερα από {total} PDF + Pinned folders need the desktop app Οι καρφιτσωμένοι φάκελοι απαιτούν την εφαρμογή υπολογιστή + Choose a folder on this computer to pin Επιλέξτε έναν φάκελο από αυτόν τον υπολογιστή για καρφίτσωμα + {folder} is already pinned to Home Ο φάκελος {folder} είναι ήδη καρφιτσωμένος στην αρχική + You can pin up to {n} folders. Unpin one first. Μπορείτε να καρφιτσώσετε έως {n} φακέλους. Ξεκαρφιτσώστε πρώτα έναν. + Pinned {folder} to Home Ο φάκελος {folder} καρφιτσώθηκε στην αρχική + No recent files Δεν υπάρχουν πρόσφατα αρχεία + New blank PDF Νέο κενό PDF + Create PDF from file… Δημιουργία PDF από αρχείο… + Create PDF from images… Δημιουργία PDF από εικόνες… + Create PDF from multiple files… Δημιουργία PDF από πολλά αρχεία… + Create PDF from clipboard Δημιουργία PDF από το πρόχειρο + Combine files… Συνένωση αρχείων… + Save Αποθήκευση + Save as… Αποθήκευση ως… + Close file Κλείσιμο αρχείου + Close all Κλείσιμο όλων + Revert Επαναφορά στην αποθηκευμένη έκδοση + Print… Εκτύπωση… + Document properties… Ιδιότητες εγγράφου… + Undo Αναίρεση + Redo Επανάληψη + Find… Εύρεση… + Advanced search… Σύνθετη αναζήτηση… + Copy pages Αντιγραφή σελίδων + Cut pages Αποκοπή σελίδων + Paste pages Επικόλληση σελίδων + Fit visible Προσαρμογή στο ορατό τμήμα + Marquee zoom Μεγέθυνση επιλεγμένης περιοχής + Take a snapshot Λήψη στιγμιότυπου + Column select Επιλογή στήλης + Full screen mode Λειτουργία πλήρους οθόνης + Read mode Λειτουργία ανάγνωσης + Switch light / dark theme Εναλλαγή φωτεινού / σκοτεινού θέματος + Comments panel Πίνακας σχολίων + Form fields panel Πίνακας πεδίων φόρμας + Clear form Εκκαθάριση φόρμας + Find tools and commands… Αναζήτηση εργαλείων και εντολών… + Zoom Μεγέθυνση + Actual size Πραγματικό μέγεθος + Zoom to page level Προσαρμογή στη σελίδα + Fit to width Προσαρμογή στο πλάτος + Display theme Θέμα εμφάνισης + Side panels Πλευρικοί πίνακες + Enable Acrobat JavaScript Ενεργοποίηση Acrobat JavaScript + OK Εντάξει + Join our Discord Ελάτε στο Discord μας + Close Κλείσιμο + Next / previous match Επόμενο / προηγούμενο αποτέλεσμα + Copy selected text Αντιγραφή επιλεγμένου κειμένου + Double-click Διπλό κλικ + Select a word Επιλογή λέξης + Clear selection / close find Κατάργηση επιλογής / κλείσιμο αναζήτησης + Zoom in / out (also pinch or {key}-scroll) Μεγέθυνση / σμίκρυνση (επίσης με χειρονομία ή κύλιση με {key}) + Rotate view Περιστροφή προβολής + First / last page Πρώτη / τελευταία σελίδα + Previous / next page Προηγούμενη / επόμενη σελίδα + Delete selected pages (Organize) Διαγραφή επιλεγμένων σελίδων (Οργάνωση) + Select all pages (Organize) Επιλογή όλων των σελίδων (Οργάνωση) + Home Αρχική + Open Άνοιγμα + Open a PDF ({key}) Άνοιγμα PDF ({key}) + Dark gray theme Σκούρο γκρι θέμα + Light theme Φωτεινό θέμα + All tools Όλα τα εργαλεία + Read Ανάγνωση + Convert Μετατροπή + E-Sign Ηλεκτρονική υπογραφή + Print ({key}) Εκτύπωση ({key}) + Save (M4) Αποθήκευση (M4) + Document properties ({key}) Ιδιότητες εγγράφου ({key}) + Find tools and commands Αναζήτηση εργαλείων και εντολών + Fit to height Προσαρμογή στο ύψος + Zoom in Μεγέθυνση + Zoom out Σμίκρυνση + Rotate view clockwise Περιστροφή προβολής δεξιόστροφα + Rotate view counterclockwise Περιστροφή προβολής αριστερόστροφα + Page navigation Πλοήγηση σελίδων + Previous view Προηγούμενη προβολή + Next view Επόμενη προβολή + Page display Εμφάνιση σελίδων + Continuous scrolling Συνεχής κύλιση + Two-page view Προβολή δύο σελίδων + Single page Μονή σελίδα + Show cover page in two-page view Εμφάνιση εξωφύλλου στην προβολή δύο σελίδων + Use system setting Χρήση ρύθμισης συστήματος + Light gray Ανοιχτό γκρι + Dark gray Σκούρο γκρι + Comments Σχόλια + Bookmarks Σελιδοδείκτες + Fields Πεδία + Layers Επίπεδα + Attachments Συνημμένα + Signatures Υπογραφές + Accessibility Checker Έλεγχος προσβασιμότητας + Search Αναζήτηση + Compare Σύγκριση + Page thumbnails Μικρογραφίες σελίδων + Form fields Πεδία φόρμας + Zoom out ({key}) Σμίκρυνση ({key}) + Zoom in ({key}) Μεγέθυνση ({key}) + Rotate view clockwise ({key}) Περιστροφή προβολής δεξιόστροφα ({key}) + Toggle fit page / fit width Εναλλαγή προσαρμογής στη σελίδα / στο πλάτος + Next page Επόμενη σελίδα + Previous page Προηγούμενη σελίδα + Welcome to PdfCraft Καλώς ορίσατε στο PdfCraft + An open-source PDF workbench — local, private, and scriptable. Ένα εργαλείο PDF ανοιχτού κώδικα — τοπικό, ιδιωτικό και αυτοματοποιήσιμο. + Join the ArtCraft community Γίνετε μέλος της κοινότητας ArtCraft + Get help, share feedback and follow development on Discord. Βρείτε βοήθεια, μοιραστείτε σχόλια και παρακολουθήστε την ανάπτυξη στο Discord. + Recommended tools Προτεινόμενα εργαλεία + Use now Χρήση τώρα + Open file Άνοιγμα αρχείου + Recent Πρόσφατα + Privacy Απόρρητο + PdfCraft works offline. No telemetry, no account, and no cloud processing unless you add a provider. Το PdfCraft λειτουργεί χωρίς σύνδεση στο διαδίκτυο. Χωρίς τηλεμετρία, λογαριασμό ή επεξεργασία στο νέφος, εκτός αν προσθέσετε εσείς υπηρεσία. + Date Ημερομηνία + Rotate Περιστροφή + Delete Διαγραφή + Cancel Ακύρωση + Apply Εφαρμογή + Password Κωδικός πρόσβασης + Download Λήψη + Add Προσθήκη + Name Όνομα + Remove Αφαίρεση + Size Μέγεθος + Document Έγγραφο + Settings Ρυθμίσεις + Author Συντάκτης + Type Τύπος + Title Τίτλος + Subject Θέμα + Keywords Λέξεις-κλειδιά + Language Γλώσσα + Export Εξαγωγή + Cut Αποκοπή + Copy Αντιγραφή + Page Σελίδα + Date format Μορφή ημερομηνίας + Create Δημιουργία + Browse… Περιήγηση… + Discard Απόρριψη + Yes Ναι + No Όχι + Done Ολοκληρώθηκε + Continue Συνέχεια + Back Πίσω + Details Λεπτομέρειες + More Περισσότερα + Edit text Επεξεργασία κειμένου + Add text Προσθήκη κειμένου + Edit image Επεξεργασία εικόνας + Add image Προσθήκη εικόνας + Insert text Εισαγωγή κειμένου + Insert Εισαγωγή + Insert pages Εισαγωγή σελίδων + Delete pages Διαγραφή σελίδων + Extract pages Εξαγωγή σελίδων + Split Διαχωρισμός + Combine Συνένωση + Organize pages Οργάνωση σελίδων + Page number Αριθμός σελίδας + All pages Όλες οι σελίδες + Selected pages Επιλεγμένες σελίδες + Current page Τρέχουσα σελίδα + Odd pages Μονές σελίδες + Even pages Ζυγές σελίδες + Portrait Κατακόρυφος + Landscape Οριζόντιος + Page size Μέγεθος σελίδας + Width Πλάτος + Height Ύψος + Position Θέση + Rotation Περιστροφή + Rotate pages Περιστροφή σελίδων + Crop pages Περικοπή σελίδων + Add bookmark Προσθήκη σελιδοδείκτη + Delete bookmark Διαγραφή σελιδοδείκτη + Rename Μετονομασία + Rename bookmark Μετονομασία σελιδοδείκτη + Replace Αντικατάσταση + Select pages Επιλογή σελίδων + Selection Επιλογή + Metadata Μεταδεδομένα + Description Περιγραφή + Modified Τροποποιήθηκε + File size Μέγεθος αρχείου + File name Όνομα αρχείου + Location Τοποθεσία + Security Ασφάλεια + Protect Προστασία + Encrypt Κρυπτογράφηση + Encryption Κρυπτογράφηση + Unlock Ξεκλείδωμα + Sign Υπογραφή + Digital signature Ψηφιακή υπογραφή + Certificates Πιστοποιητικά + Valid Έγκυρο + Signature Υπογραφή + Add signature Προσθήκη υπογραφής + Fill & Sign Συμπλήρωση και υπογραφή + Forms Φόρμες + Text field Πεδίο κειμένου + Checkbox Πλαίσιο ελέγχου + Radio button Κουμπί επιλογής + Button Κουμπί + Required Υποχρεωτικό + Optional Προαιρετικό + Appearance Εμφάνιση + Opacity Αδιαφάνεια + Bold Έντονα + Italic Πλάγια + Underline Υπογράμμιση + Align left Στοίχιση αριστερά + Align right Στοίχιση δεξιά + Justify Πλήρης στοίχιση + Highlight Επισήμανση + Highlight text Επισήμανση κειμένου + Strikethrough Διακριτή διαγραφή + Stamp Σφραγίδα + Stamps Σφραγίδες + Delete comment Διαγραφή σχολίου + Reply Απάντηση + Flatten Ενσωμάτωση στο περιεχόμενο + Print Εκτύπωση + Optimize Βελτιστοποίηση + Compression Συμπίεση + Quality Ποιότητα + Resolution Ανάλυση + Recognize text Αναγνώριση κειμένου + Advanced Για προχωρημένους + General Γενικά + About Πληροφορίες + About PdfCraft Σχετικά με το PdfCraft + Check for updates Έλεγχος για ενημερώσεις + Keyboard shortcuts Συντομεύσεις πληκτρολογίου + Default Προεπιλογή + Custom Προσαρμοσμένο + Show Εμφάνιση + Off Ανενεργό + Completed Ολοκληρώθηκε + Failed Αποτυχία + Page {p} of {n} Σελίδα {p} από {n} + Flatten Fill & Sign when saving Ενσωμάτωση συμπληρώσεων και υπογραφών κατά την αποθήκευση + Text, marks and signatures become part of the page. Other comments stay editable. Το κείμενο, τα σημάδια και οι υπογραφές γίνονται μέρος της σελίδας. Τα υπόλοιπα σχόλια παραμένουν επεξεργάσιμα. + Current page — type a page number or label (such as iv) and press Enter Τρέχουσα σελίδα — πληκτρολογήστε αριθμό ή ετικέτα σελίδας (π.χ. iv) και πατήστε Enter + Files you open in PdfCraft appear here. Drop a PDF anywhere to open it. Τα αρχεία που ανοίγετε στο PdfCraft εμφανίζονται εδώ. Σύρετε ένα PDF οπουδήποτε για να το ανοίξετε. + Scripts run in a sandbox without file or network access. With JavaScript off, Acrobat's standard format, validate and calculate functions still work. Τα σενάρια εκτελούνται σε απομονωμένο περιβάλλον χωρίς πρόσβαση σε αρχεία ή δίκτυο. Με απενεργοποιημένη τη JavaScript, οι τυπικές λειτουργίες μορφοποίησης, επικύρωσης και υπολογισμού του Acrobat εξακολουθούν να λειτουργούν. + Move up Μετακίνηση πάνω + Move down Μετακίνηση κάτω + Clear Εκκαθάριση + More options Περισσότερες επιλογές + Options Επιλογές + Show all Εμφάνιση όλων + Expand Ανάπτυξη + Collapse Σύμπτυξη + Duplicate page Διπλασιασμός σελίδας + Remove page Αφαίρεση σελίδας + Delete page Διαγραφή σελίδας + Rotate page Περιστροφή σελίδας + Crop page Περικοπή σελίδας + Page labels Ετικέτες σελίδων + Number of pages Αριθμός σελίδων + Page layout Διάταξη σελίδας + Units Μονάδες + Percentage Ποσοστό + Angle Γωνία + Horizontal Οριζόντια + Vertical Κατακόρυφα + Left Αριστερά + Right Δεξιά + Top Πάνω + Bottom Κάτω + Center Κέντρο + Start Αρχή + Version Έκδοση + PDF version Έκδοση PDF + Security settings Ρυθμίσεις ασφαλείας + Password required Απαιτείται κωδικός πρόσβασης + Signer Υπογράφων + Issuer Εκδότης + Issued by Εκδόθηκε από + Issued to Εκδόθηκε για + Serial number Σειριακός αριθμός + Valid from Ισχύει από + Unknown Άγνωστο + Validate Επικύρωση + Redact Απόκρυψη ευαίσθητου περιεχομένου + Apply redactions Εφαρμογή οριστικών αποκρύψεων + Sanitize Εκκαθάριση ευαίσθητων δεδομένων + Accessibility Προσβασιμότητα + Headings Επικεφαλίδες + Tagged PDF PDF με ετικέτες δομής + Document language Γλώσσα εγγράφου + List box Πλαίσιο λίστας + Date field Πεδίο ημερομηνίας + Import data Εισαγωγή δεδομένων + Export data Εξαγωγή δεδομένων + Sticky note Αυτοκόλλητη σημείωση + Text box Πλαίσιο κειμένου + Callout Πλαίσιο επεξήγησης + Squiggly Κυματιστή υπογράμμιση + Eraser Γόμα + Rectangle Ορθογώνιο + Line Γραμμή + Arrow Βέλος + Polygon Πολύγωνο + Polyline Πολυγραμμή + Cloud Σύννεφο + Rejected Απορρίφθηκε + Review Ανασκόπηση + Status Κατάσταση + Edit comment Επεξεργασία σχολίου + Sort by Ταξινόμηση κατά + Find text Εύρεση κειμένου + No matches Δεν βρέθηκαν αντιστοιχίες + Copy text Αντιγραφή κειμένου + Replace image Αντικατάσταση εικόνας + Images Εικόνες + Image Εικόνα + Flip horizontal Οριζόντια αναστροφή + Flip vertical Κατακόρυφη αναστροφή + Align Στοίχιση + Distribute Κατανομή + Show layer Εμφάνιση επιπέδου + Hide layer Απόκρυψη επιπέδου + Collate Συρραφή αντιγράφων + Fit to page Προσαρμογή στη σελίδα + Reverse pages Αντιστροφή σειράς σελίδων + Reduce file size Μείωση μεγέθους αρχείου + Add watermark Προσθήκη υδατογραφήματος + Watermark Υδατογράφημα + Remove watermark Αφαίρεση υδατογραφήματος + Background Φόντο + Add background Προσθήκη φόντου + Remove background Αφαίρεση φόντου + Header and footer Κεφαλίδα και υποσέλιδο + Preview Προεπισκόπηση + Rotate clockwise Δεξιόστροφη περιστροφή + Rotate counterclockwise Αριστερόστροφη περιστροφή + Open link Άνοιγμα συνδέσμου + Add link Προσθήκη συνδέσμου + Contributors Συντελεστές + Auto Αυτόματα + Don't save Να μην αποθηκευτεί + Ready Έτοιμο + Rotate Clockwise Περιστροφή δεξιόστροφα + Rotate Counterclockwise Περιστροφή αριστερόστροφα + Flip Horizontal Οριζόντια αναστροφή + Flip Vertical Κατακόρυφη αναστροφή + Replace Image… Αντικατάσταση εικόνας… + Save Image As… Αποθήκευση εικόνας ως… + No check has run yet. Δεν έχει πραγματοποιηθεί ακόμη έλεγχος. + Checking for a newer version… Έλεγχος για νεότερη έκδοση… + PdfCraft {v} is available. Η έκδοση {v} του PdfCraft είναι διαθέσιμη. + You have version {c}. Download the new version from its release page. Έχετε την έκδοση {c}. Κάντε λήψη της νέας έκδοσης από τη σελίδα εκδόσεών της. + PdfCraft {c} is up to date. Το PdfCraft {c} είναι ενημερωμένο. + Couldn't check for updates: {e} Δεν ήταν δυνατός ο έλεγχος για ενημερώσεις: {e} + You have version {c}. All releases are listed at {page}. Έχετε την έκδοση {c}. Όλες οι εκδόσεις βρίσκονται στη διεύθυνση {page}. + Asks GitHub for the latest release. Nothing is downloaded or installed automatically. Αναζητά την τελευταία έκδοση στο GitHub. Δεν γίνεται αυτόματα λήψη ή εγκατάσταση. + Later Αργότερα + none κανένα + Declared conformance: {shown} Δηλωμένη συμμόρφωση: {shown} + Output intent: {list} Προφίλ χρωματικής εξόδου: {list} + Conformance level: Επίπεδο συμμόρφωσης: + No problems found: the document conforms to {level}. Δεν εντοπίστηκαν προβλήματα: το έγγραφο συμμορφώνεται με το επίπεδο {level}. + (page {p}) (σελίδα {p}) + · not fixable here · δεν διορθώνεται εδώ + Verify Επαλήθευση + Box Πλαίσιο + Set to zero Ορισμός σε μηδέν + Current page ({n}) Τρέχουσα σελίδα ({n}) + Subset Υποσύνολο + Update Ενημέρωση + {verb} Header and Footer {verb} κεφαλίδας και υποσέλιδου + {verb} Watermark {verb} υδατογραφήματος + {verb} Background {verb} φόντου + Margins (in) Περιθώρια (ίντσες) + Left Header Text Κείμενο αριστερής κεφαλίδας + Center Header Text Κείμενο κεντρικής κεφαλίδας + Right Header Text Κείμενο δεξιάς κεφαλίδας + Left Footer Text Κείμενο αριστερού υποσέλιδου + Center Footer Text Κείμενο κεντρικού υποσέλιδου + Right Footer Text Κείμενο δεξιού υποσέλιδου + Insert Page Number Εισαγωγή αριθμού σελίδας + Insert Date Εισαγωγή ημερομηνίας + Start page number Αρχικός αριθμός σελίδας + Source Πηγή + Appear behind page Εμφάνιση πίσω από τη σελίδα + Appear on top of page Εμφάνιση πάνω από τη σελίδα + From colour Από χρώμα + Page Range Options Επιλογές περιοχής σελίδων + Preview: page {p} of {n} Προεπισκόπηση: σελίδα {p} από {n} + Applies to 1 page Εφαρμόζεται σε 1 σελίδα + Applies to {n} pages Εφαρμόζεται σε {n} σελίδες + Replaces the existing one on those pages. Αντικαθιστά το υπάρχον στοιχείο στις συγκεκριμένες σελίδες. + PDF or image PDF ή εικόνα + No file chosen Δεν έχει επιλεγεί αρχείο + Scale relative to target page Κλίμακα ως προς τη σελίδα προορισμού + Link Properties Ιδιότητες συνδέσμου + Create Link Δημιουργία συνδέσμου + Link Appearance Εμφάνιση συνδέσμου + Link Type: Τύπος συνδέσμου: + Visible Rectangle Ορατό ορθογώνιο + Invisible Rectangle Αόρατο ορθογώνιο + Highlight Style: Στυλ επισήμανσης: + Invert Αντιστροφή + Outline Περίγραμμα + Inset Εσοχή + Line Thickness: Πάχος γραμμής: + Thin Λεπτό + Thick Παχύ + Line Style: Στυλ γραμμής: + Solid Συνεχής + Dashed Διακεκομμένη + Color: Χρώμα: + Link Action Ενέργεια συνδέσμου + Open a web page Άνοιγμα ιστοσελίδας + Go to a page view Μετάβαση σε προβολή σελίδας + Type the web address. Πληκτρολογήστε τη διεύθυνση της ιστοσελίδας. + This document has no signatures. Αυτό το έγγραφο δεν περιέχει υπογραφές. + The Keychain's digital IDs couldn't be listed: {e} Δεν ήταν δυνατή η εμφάνιση των ψηφιακών ταυτοτήτων της Κλειδοθήκης: {e} + Signed and saved to {path} Υπογράφηκε και αποθηκεύτηκε στο {path} + Couldn't open revision {n}: {e} Δεν ήταν δυνατό το άνοιγμα της αναθεώρησης {n}: {e} + Couldn't open the signed version: {e} Δεν ήταν δυνατό το άνοιγμα της υπογεγραμμένης έκδοσης: {e} + Sign with a Digital ID Υπογραφή με ψηφιακή ταυτότητα + Choose the digital ID that you want to use for signing: Επιλέξτε την ψηφιακή ταυτότητα με την οποία θέλετε να υπογράψετε: + Issued by: Εκδότης: + , Expires: , Λήξη: + No digital IDs yet. Δεν υπάρχουν ακόμη ψηφιακές ταυτότητες. + Configure New Digital ID Ρύθμιση νέας ψηφιακής ταυτότητας + Configure a Digital ID for Signing Ρύθμιση ψηφιακής ταυτότητας για υπογραφή + Use a Digital ID from a file Χρήση ψηφιακής ταυτότητας από αρχείο + Create a new Digital ID (self-signed, saved to a password-protected file) Δημιουργία νέας ψηφιακής ταυτότητας (αυτοϋπογεγραμμένης, αποθηκευμένης σε αρχείο με κωδικό πρόσβασης) + Organizational Unit Οργανωτική μονάδα + Organization Name Επωνυμία οργανισμού + Email Address Διεύθυνση email + Country/Region Χώρα/Περιοχή + Key Algorithm Αλγόριθμος κλειδιού + Confirm Password Επιβεβαίωση κωδικού πρόσβασης + Valid for 5 years, for digital signatures. Self-signed IDs are usually not trusted by others. Ισχύει για 5 χρόνια για ψηφιακές υπογραφές. Οι αυτοϋπογεγραμμένες ταυτότητες συνήθως δεν θεωρούνται αξιόπιστες από άλλους. + File (.p12, .pfx) Αρχείο (.p12, .pfx) + Digital ID Ψηφιακή ταυτότητα + {verb} as "{name}" {verb} ως «{name}» + Certify Πιστοποίηση + The signature will be invisible. Η υπογραφή θα είναι αόρατη. + Distinguished name Διακεκριμένο όνομα + Labels Ετικέτες + Permitted actions after certifying Επιτρεπόμενες ενέργειες μετά την πιστοποίηση + No changes allowed Δεν επιτρέπονται αλλαγές + Annotations, form fill-in, and digital signatures Σημειώσεις, συμπλήρωση φορμών και ψηφιακές υπογραφές + Form fill-in and digital signatures Συμπλήρωση φορμών και ψηφιακές υπογραφές + The key is in the macOS Keychain, which may ask to allow PdfCraft to use it. Το κλειδί βρίσκεται στην Κλειδοθήκη του macOS, η οποία ενδέχεται να ζητήσει άδεια χρήσης από το PdfCraft. + The key is in the Windows certificate store, which may ask to allow PdfCraft to use it. Το κλειδί βρίσκεται στο χώρο αποθήκευσης πιστοποιητικών των Windows, ο οποίος ενδέχεται να ζητήσει άδεια χρήσης από το PdfCraft. + Digital ID password Κωδικός πρόσβασης ψηφιακής ταυτότητας + Validate all Επικύρωση όλων + Sign this field… Υπογραφή αυτού του πεδίου… + Signature is valid: Η υπογραφή είναι έγκυρη: + Signature validity is UNKNOWN: Η εγκυρότητα της υπογραφής είναι ΑΓΝΩΣΤΗ: + Signature is INVALID: Η υπογραφή είναι ΜΗ ΕΓΚΥΡΗ: + Signature Details Λεπτομέρειες υπογραφής + Reason Αιτία + Signing time Ώρα υπογραφής + Algorithm Αλγόριθμος + Field Πεδίο + Field: {field} on page {p} Πεδίο: {field} στη σελίδα {p} + Add to trusted certificates Προσθήκη στα αξιόπιστα πιστοποιητικά + View signed version Προβολή υπογεγραμμένης έκδοσης + Show certificate… Εμφάνιση πιστοποιητικού… + Export certificate… Εξαγωγή πιστοποιητικού… + Certificate Viewer Προβολή πιστοποιητικού + This dialog shows the details of a certificate and its chain. Αυτό το παράθυρο εμφανίζει τις λεπτομέρειες ενός πιστοποιητικού και της αλυσίδας του. + Trust Εμπιστοσύνη + Valid to Έγκυρο έως + Intended usage Προβλεπόμενη χρήση + Any Οποιαδήποτε + Validity starts Έναρξη ισχύος + Validity ends Λήξη ισχύος + Public key Δημόσιο κλειδί + Key usage Χρήση κλειδιού + Not present Δεν υπάρχει + Self-signed Αυτοϋπογεγραμμένο + Edit Action Επεξεργασία ενέργειας + New Action Νέα ενέργεια + Action name: Όνομα ενέργειας: + Description: Περιγραφή: + Steps Βήματα + Add the steps this action runs, in order. Προσθέστε με τη σειρά τα βήματα που εκτελεί αυτή η ενέργεια. + Add Step Προσθήκη βήματος + An action with this name already exists. Υπάρχει ήδη ενέργεια με αυτό το όνομα. + New Action… Νέα ενέργεια… + Edit… Επεξεργασία… + Add header Προσθήκη κεφαλίδας + Add footer Προσθήκη υποσέλιδου + Set document title Ορισμός τίτλου εγγράφου + Run a JavaScript Εκτέλεση JavaScript + Prepare for Distribution Προετοιμασία για διανομή + Remove hidden information and flatten comments and form fields, so recipients see only the pages. Αφαιρεί κρυφές πληροφορίες και ενσωματώνει οριστικά σχόλια και πεδία φορμών, ώστε οι παραλήπτες να βλέπουν μόνο τις σελίδες. + Optimize Scanned Documents Βελτιστοποίηση σαρωμένων εγγράφων + Make scans searchable, then make them smaller. Κάνει τις σαρώσεις αναζητήσιμες και στη συνέχεια μειώνει το μέγεθός τους. + Add Page Numbers Προσθήκη αρίθμησης σελίδων + Number every page in the footer. Προσθέτει αριθμό σε κάθε σελίδα, στο υποσέλιδο. + Mark as Confidential Σήμανση ως εμπιστευτικού + A diagonal CONFIDENTIAL watermark on every page. Προσθέτει διαγώνιο υδατογράφημα «ΕΜΠΙΣΤΕΥΤΙΚΟ» σε κάθε σελίδα. + Sanitize Documents Αφαίρεση ευαίσθητων δεδομένων από έγγραφα + Remove hidden information, metadata, scripts and embedded content, and rewrite the files. Αφαιρεί κρυφές πληροφορίες, μεταδεδομένα, δέσμες ενεργειών και ενσωματωμένο περιεχόμενο και δημιουργεί εκ νέου τα αρχεία. + Type text Πληκτρολόγηση κειμένου + Text to insert Κείμενο προς εισαγωγή + Replacement text Κείμενο αντικατάστασης + Edit text… Επεξεργασία κειμένου… + Add a sticky note here Προσθήκη σημείωσης εδώ + {kind} Properties Ιδιότητες: {kind} + Review History Ιστορικό ελέγχου + This comment's appearance can't be changed yet. Δεν είναι ακόμη δυνατή η αλλαγή της εμφάνισης αυτού του σχολίου. + Icon Εικονίδιο + Thickness Πάχος + No status has been set. Δεν έχει οριστεί κατάσταση. + Compare Files Σύγκριση αρχείων + Old file: Παλαιό αρχείο: + Open the older version first Ανοίξτε πρώτα την παλαιότερη έκδοση + New file: Νέο αρχείο: + Text is compared word by word; the differences are shown on the new file. Το κείμενο συγκρίνεται λέξη προς λέξη και οι διαφορές εμφανίζονται στο νέο αρχείο. + Compare this file with an older version: Compare files… in All tools. Συγκρίνετε αυτό το αρχείο με παλαιότερη έκδοση: «Σύγκριση αρχείων…» στα «Όλα τα εργαλεία». + No differences in the text. Δεν εντοπίστηκαν διαφορές στο κείμενο. + {n} {kind} {n} {kind} + Inserted Προστέθηκε + Deleted Διαγράφηκε + Replaced Αντικαταστάθηκε + Visual Οπτικό + Mark as comments Επισήμανση ως σχόλια + Report… Αναφορά… + {kind} · page {p} {kind} · σελίδα {p} + Set Alternate Text Ορισμός εναλλακτικού κειμένου + This document has no tagged figures. Αυτό το έγγραφο δεν περιέχει εικόνες με ετικέτες δομής. + on page {p} στη σελίδα {p} + Figure {i} of {n}{page} Εικόνα {i} από {n}{page} + No preview Δεν υπάρχει προεπισκόπηση + Alternate text Εναλλακτικό κείμενο + Describe the figure Περιγράψτε την εικόνα + Decorative figure Διακοσμητική εικόνα + Previous figure Προηγούμενη εικόνα + Next figure Επόμενη εικόνα + Save & Close Αποθήκευση και κλείσιμο + Accessibility Checker Options Επιλογές ελέγχου προσβασιμότητας + Report Options Επιλογές αναφοράς + Create accessibility report Δημιουργία αναφοράς προσβασιμότητας + Page Range Εύρος σελίδων + All pages in document Όλες οι σελίδες του εγγράφου + Checking Options ({on} of 32 in all categories) Επιλογές ελέγχου ({on} από 32 σε όλες τις κατηγορίες) + Show this dialog when the Checker starts Εμφάνιση αυτού του παραθύρου κατά την έναρξη του ελέγχου + Select All Επιλογή όλων + Clear All Εκκαθάριση όλων + Start Checking Έναρξη ελέγχου + This document hasn't been checked yet. Αυτό το έγγραφο δεν έχει ελεγχθεί ακόμη. + No issues found Δεν εντοπίστηκαν προβλήματα + 1 issue 1 πρόβλημα + {n} issues {n} προβλήματα + {label} (1 issue) {label} (1 πρόβλημα) + {label} ({n} issues) {label} ({n} προβλήματα) + Check again Επανέλεγχος + Report Αναφορά + Show the accessibility report Εμφάνιση αναφοράς προσβασιμότητας + {rule} - {status} {rule} - {status} + Passed Επιτυχής έλεγχος + Needs manual check Απαιτείται χειροκίνητος έλεγχος + Skipped Παραλείφθηκε + Fix Διόρθωση + Skip Rule Παράλειψη κανόνα + Explain Επεξήγηση + Check Again Επανέλεγχος + Show Report Εμφάνιση αναφοράς + Options… Επιλογές… + Go to the page Μετάβαση στη σελίδα + Page Content Περιεχόμενο σελίδας + Alternate Text Εναλλακτικό κείμενο + Tables Πίνακες + Lists Λίστες + Accessibility permission flag Σήμανση άδειας προσβασιμότητας + Accessibility permission flag is set Η άδεια προσβασιμότητας είναι ενεργοποιημένη + Security settings must allow assistive technology to read the content ("Enable text access for screen reader devices"). Change the document's security to allow it. Οι ρυθμίσεις ασφαλείας πρέπει να επιτρέπουν την ανάγνωση από βοηθητικές τεχνολογίες («Ενεργοποίηση πρόσβασης στο κείμενο για προγράμματα ανάγνωσης οθόνης»). Αλλάξτε τις ρυθμίσεις ασφαλείας του εγγράφου. + Image-only PDF PDF μόνο με εικόνες + Document is not image-only PDF Το PDF δεν αποτελείται αποκλειστικά από εικόνες + Pages that are only pictures of text can't be read aloud or searched. Recognize text (OCR) to add a text layer. Οι σελίδες που περιέχουν μόνο εικόνες κειμένου δεν μπορούν να αναγνωστούν φωναχτά ή να αναζητηθούν. Χρησιμοποιήστε αναγνώριση κειμένου (OCR) για να προσθέσετε επίπεδο κειμένου. + Document is tagged PDF Το έγγραφο είναι PDF με ετικέτες δομής + Tags give the document a logical structure that assistive technology follows. Tag the document (Automatically tag PDF). Οι ετικέτες δίνουν στο έγγραφο λογική δομή που ακολουθούν οι βοηθητικές τεχνολογίες. Προσθέστε ετικέτες στο έγγραφο («Αυτόματη προσθήκη ετικετών PDF»). + Logical Reading Order Λογική σειρά ανάγνωσης + Document structure provides a logical reading order Η δομή του εγγράφου ορίζει λογική σειρά ανάγνωσης + Check that the tags read in the order a sighted reader would. Use the Reading Order tool or the Tags panel to correct it. Ελέγξτε ότι οι ετικέτες ακολουθούν τη σειρά ανάγνωσης ενός βλέποντος χρήστη. Χρησιμοποιήστε το εργαλείο «Σειρά ανάγνωσης» ή τον πίνακα «Ετικέτες» για διόρθωση. + Primary language Κύρια γλώσσα + Text language is specified Έχει καθοριστεί η γλώσσα του κειμένου + Screen readers choose their voice from the document's language. Set it in Document Properties > Advanced > Language. Τα προγράμματα ανάγνωσης οθόνης επιλέγουν φωνή σύμφωνα με τη γλώσσα του εγγράφου. Ορίστε την στις Ιδιότητες εγγράφου > Για προχωρημένους > Γλώσσα. + Document title is showing in title bar Ο τίτλος του εγγράφου εμφανίζεται στη γραμμή τίτλου + The window should show a meaningful title rather than the file name. Set a title and Initial View > Show: Document Title. Το παράθυρο πρέπει να εμφανίζει περιγραφικό τίτλο αντί για το όνομα αρχείου. Ορίστε τίτλο και επιλέξτε Αρχική προβολή > Εμφάνιση: Τίτλος εγγράφου. + Bookmarks are present in large documents Υπάρχουν σελιδοδείκτες σε μεγάλα έγγραφα + Documents of more than 20 pages need bookmarks for navigation. Add bookmarks for the main sections. Τα έγγραφα με περισσότερες από 20 σελίδες χρειάζονται σελιδοδείκτες για πλοήγηση. Προσθέστε σελιδοδείκτες στις κύριες ενότητες. + Color contrast Αντίθεση χρωμάτων + Document has appropriate color contrast Το έγγραφο έχει κατάλληλη χρωματική αντίθεση + Text must contrast enough with its background for people with low vision. Check the colours by eye or with a contrast tool. Το κείμενο πρέπει να ξεχωρίζει επαρκώς από το φόντο για άτομα με μειωμένη όραση. Ελέγξτε τα χρώματα οπτικά ή με εργαλείο αντίθεσης. + Tagged content Περιεχόμενο με ετικέτες δομής + All page content is tagged Όλο το περιεχόμενο των σελίδων έχει ετικέτες δομής + Everything on a page must be tagged or marked as an artifact (decoration, headers, footers). Tag the untagged content. Κάθε στοιχείο της σελίδας πρέπει να έχει ετικέτα δομής ή να επισημαίνεται ως διακοσμητικό (όπως κεφαλίδες και υποσέλιδα). Προσθέστε ετικέτες στο περιεχόμενο που δεν έχει. + Tagged annotations Σχόλια με ετικέτες δομής + All annotations are tagged Όλα τα σχόλια έχουν ετικέτες δομής + Comments, links and fields must be in the tag tree to be announced. Tag the annotations. Τα σχόλια, οι σύνδεσμοι και τα πεδία πρέπει να περιλαμβάνονται στο δέντρο ετικετών για να αναγγέλλονται. Προσθέστε τους ετικέτες. + Tab order Σειρά μετακίνησης με Tab + Tab order is consistent with structure order Η σειρά μετακίνησης με Tab ακολουθεί τη δομή του εγγράφου + Pages with links, comments or fields should tab in structure order. The fix sets each page's tab order to Use Document Structure. Στις σελίδες με συνδέσμους, σχόλια ή πεδία, το πλήκτρο Tab πρέπει να ακολουθεί τη σειρά της δομής. Η διόρθωση ορίζει τη σειρά σε «Χρήση δομής εγγράφου». + Character encoding Κωδικοποίηση χαρακτήρων + Reliable character encoding is provided Παρέχεται αξιόπιστη κωδικοποίηση χαρακτήρων + Fonts must map their characters to Unicode so text can be read and copied. Fix the fonts or recreate the PDF. Οι γραμματοσειρές πρέπει να αντιστοιχίζουν τους χαρακτήρες τους σε Unicode, ώστε το κείμενο να διαβάζεται και να αντιγράφεται. Διορθώστε τις γραμματοσειρές ή δημιουργήστε ξανά το PDF. + Tagged multimedia Πολυμέσα με ετικέτες δομής + All multimedia objects are tagged Όλα τα αντικείμενα πολυμέσων έχουν ετικέτες δομής + Video, sound and rich media must be tagged to be announced. Tag the multimedia annotations. Το βίντεο, ο ήχος και το εμπλουτισμένο περιεχόμενο πρέπει να έχουν ετικέτες για να αναγγέλλονται. Προσθέστε ετικέτες στα σχόλια πολυμέσων. + Screen flicker Τρεμόπαιγμα οθόνης + Page will not cause screen flicker Η σελίδα δεν προκαλεί τρεμόπαιγμα οθόνης + Animation that flashes can cause seizures. Check scripts and media for flicker. Οι κινούμενες εικόνες με έντονο αναβόσβημα μπορεί να προκαλέσουν επιληπτικές κρίσεις. Ελέγξτε τα σενάρια και τα πολυμέσα για τρεμόπαιγμα. + Scripts Σενάρια εντολών + No inaccessible scripts Δεν υπάρχουν μη προσβάσιμα σενάρια εντολών + Timed responses Απαντήσεις με χρονικό περιορισμό + Page does not require timed responses Η σελίδα δεν απαιτεί απαντήσεις μέσα σε χρονικό όριο + People may need more time to respond. Check that nothing has a time limit. Οι χρήστες μπορεί να χρειάζονται περισσότερο χρόνο για να απαντήσουν. Βεβαιωθείτε ότι δεν υπάρχει χρονικό όριο. + Navigation links Σύνδεσμοι πλοήγησης + Navigation links are not repetitive Οι σύνδεσμοι πλοήγησης δεν επαναλαμβάνονται + Repeated navigation links should be skippable. Check links that appear on every page. Οι επαναλαμβανόμενοι σύνδεσμοι πλοήγησης πρέπει να μπορούν να παραλειφθούν. Ελέγξτε τους συνδέσμους που εμφανίζονται σε κάθε σελίδα. + Tagged form fields Πεδία φόρμας με ετικέτες δομής + All form fields are tagged Όλα τα πεδία φόρμας έχουν ετικέτες δομής + Form fields must be in the tag tree (Form tags) to be announced. Tag the form fields. Τα πεδία φόρμας πρέπει να υπάρχουν στο δέντρο ετικετών (ετικέτες Form), για να αναγγέλλονται. Προσθέστε ετικέτες στα πεδία. + Field descriptions Περιγραφές πεδίων + All form fields have description Όλα τα πεδία φόρμας έχουν περιγραφή + Each field needs a tooltip that says what to enter. Add one in Field Properties > General > Tooltip. Κάθε πεδίο χρειάζεται επεξηγηματικό κείμενο που αναφέρει τι πρέπει να συμπληρωθεί. Προσθέστε το στις Ιδιότητες πεδίου > Γενικά > Συμβουλή εργαλείου. + Figures alternate text Εναλλακτικό κείμενο εικόνων + Figures require alternate text Οι εικόνες απαιτούν εναλλακτικό κείμενο + Pictures need alternate text that describes them, or must be marked decorative. Add alternate text to each figure. Οι εικόνες χρειάζονται περιγραφικό εναλλακτικό κείμενο ή πρέπει να επισημανθούν ως διακοσμητικές. Προσθέστε εναλλακτικό κείμενο σε κάθε εικόνα. + Nested alternate text Ένθετο εναλλακτικό κείμενο + Alternate text that will never be read Εναλλακτικό κείμενο που δεν θα αναγνωστεί ποτέ + Alternate text inside an element that already has alternate text is never read. Remove the inner alternate text. Το εναλλακτικό κείμενο μέσα σε στοιχείο που διαθέτει ήδη εναλλακτικό κείμενο δεν αναγιγνώσκεται. Αφαιρέστε το εσωτερικό. + Associated with content Συσχέτιση με περιεχόμενο + Alternate text must be associated with some content Το εναλλακτικό κείμενο πρέπει να συνδέεται με περιεχόμενο + Alternate text on an element without page content is never reached. Remove it or attach the element to content. Το εναλλακτικό κείμενο στοιχείου χωρίς περιεχόμενο σελίδας δεν είναι προσβάσιμο. Αφαιρέστε το ή συνδέστε το στοιχείο με περιεχόμενο. + Hides annotation Απόκρυψη σχολίου + Alternate text should not hide annotation Το εναλλακτικό κείμενο δεν πρέπει να αποκρύπτει σχόλια + Alternate text on an element that contains a link or field hides that annotation from assistive technology. Move the alternate text. Το εναλλακτικό κείμενο σε στοιχείο που περιέχει σύνδεσμο ή πεδίο αποκρύπτει αυτό το στοιχείο από τις βοηθητικές τεχνολογίες. Μετακινήστε το εναλλακτικό κείμενο. + Other elements alternate text Εναλλακτικό κείμενο άλλων στοιχείων + Other elements that require alternate text Άλλα στοιχεία που απαιτούν εναλλακτικό κείμενο + Formulas and similar non-text elements need alternate text. Add alternate text to them. Οι τύποι και παρόμοια μη κειμενικά στοιχεία χρειάζονται εναλλακτικό κείμενο. Προσθέστε το σε αυτά. + Rows Γραμμές + TR must be a child of Table, THead, TBody, or TFoot Το TR πρέπει να είναι θυγατρικό στοιχείο των Table, THead, TBody ή TFoot + Headers Κεφαλίδες + Tables should have headers Οι πίνακες πρέπει να διαθέτουν κεφαλίδες + Header cells (TH) tell listeners what each column or row holds. Mark the header cells. Τα κελιά κεφαλίδων (TH) ενημερώνουν για το περιεχόμενο κάθε στήλης ή γραμμής. Επισημάνετε τα κελιά κεφαλίδας. + Regularity Ομοιομορφία + Tables must contain the same number of columns in each row and rows in each column Οι πίνακες πρέπει να έχουν ίδιο αριθμό στηλών ανά γραμμή και γραμμών ανά στήλη + Irregular tables confuse cell navigation. Give every row the same number of cells (counting spans). Οι ανομοιόμορφοι πίνακες δυσκολεύουν την πλοήγηση στα κελιά. Δώστε σε κάθε γραμμή ίδιο πλήθος κελιών, συνυπολογίζοντας τα συγχωνευμένα. + Summary Σύνοψη + Tables must have a summary Οι πίνακες πρέπει να έχουν σύνοψη + A summary describes the table's purpose and layout. Add a Summary attribute to each table. Η σύνοψη περιγράφει τον σκοπό και τη διάταξη του πίνακα. Προσθέστε ιδιότητα «Σύνοψη» σε κάθε πίνακα. + List items Στοιχεία λίστας + LI must be a child of L Το LI πρέπει να είναι θυγατρικό στοιχείο του L + List items belong in a list. Fix the list's tags. Τα στοιχεία λίστας πρέπει να ανήκουν σε λίστα. Διορθώστε τις ετικέτες της λίστας. + Lbl and LBody Lbl και LBody + Lbl and LBody must be children of LI Τα Lbl και LBody πρέπει να είναι θυγατρικά στοιχεία του LI + List labels and bodies belong in a list item. Fix the list's tags. Οι ετικέτες και τα σώματα λίστας πρέπει να ανήκουν σε στοιχείο λίστας. Διορθώστε τις ετικέτες δομής της. + Appropriate nesting Σωστή εμφώλευση + Heading levels shouldn't skip (an H1 followed by an H3). Retag the headings. Τα επίπεδα επικεφαλίδων δεν πρέπει να παραλείπονται (π.χ. H1 ακολουθούμενο από H3). Διορθώστε τις ετικέτες των επικεφαλίδων. + TH and TD TH και TD + TH and TD must be children of TR Τα TH και TD πρέπει να είναι θυγατρικά στοιχεία του TR + Mark Page Range Επισήμανση εύρους σελίδων + Pages from Σελίδες από + Single word or phrase Μία λέξη ή φράση + Text to find Κείμενο προς αναζήτηση + Patterns Μοτίβα + Phone Numbers Αριθμοί τηλεφώνου + Email Addresses Διευθύνσεις email + Credit Cards Πιστωτικές κάρτες + Social Security Numbers Αριθμοί κοινωνικής ασφάλισης + Dates Ημερομηνίες + Text in images isn't found; recognise text (OCR) first. Το κείμενο μέσα σε εικόνες δεν εντοπίζεται. Πραγματοποιήστε πρώτα αναγνώριση κειμένου (OCR). + No matches. Δεν βρέθηκαν αντιστοιχίες. + Some bookmarks are not shown here: the panel lists a limited number, nested a limited depth. Ορισμένοι σελιδοδείκτες δεν εμφανίζονται εδώ: ο πίνακας παρουσιάζει περιορισμένο αριθμό και βάθος εμφώλευσης. + {n} match(es) marked for redaction. {n} αντιστοιχίες επισημάνθηκαν για απόκρυψη. + Multiple words or phrases Πολλές λέξεις ή φράσεις + One word or phrase per line Μία λέξη ή φράση ανά γραμμή + Import list… Εισαγωγή λίστας… + {n} word(s) or phrase(s) {n} λέξεις ή φράσεις + {name} is too large for a word list (at most 1 MB). Το αρχείο {name} είναι πολύ μεγάλο για λίστα λέξεων (μέγιστο 1 MB). + Table cells belong in a row. Fix the table's tags. Τα κελιά του πίνακα πρέπει να βρίσκονται μέσα σε γραμμή. Διορθώστε τις ετικέτες του πίνακα. + Redaction Tool Properties Ιδιότητες εργαλείου απόκρυψης + Redacted area fill colour: Χρώμα γεμίσματος περιοχής απόκρυψης: + Use overlay text Χρήση κειμένου επικάλυψης + Custom text: Προσαρμοσμένο κείμενο: + Redaction code: Κωδικός απόκρυψης: + Font: Γραμματοσειρά: + Font size: Μέγεθος γραμματοσειράς: + Auto-size text to fit redaction region Αυτόματη προσαρμογή μεγέθους κειμένου στην περιοχή απόκρυψης + Font colour: Χρώμα γραμματοσειράς: + Repeat overlay text Επανάληψη κειμένου επικάλυψης + Text alignment: Στοίχιση κειμένου: + You are about to apply 1 redaction mark. Text, images and drawings under the marks, and comments and form fields that overlap them, are removed permanently. Πρόκειται να εφαρμόσετε 1 επισήμανση απόκρυψης. Το κείμενο, οι εικόνες και τα σχέδια κάτω από τις επισημάνσεις, καθώς και τα σχόλια και τα πεδία φόρμας που επικαλύπτονται, θα αφαιρεθούν οριστικά. + You are about to apply {n} redaction marks. Text, images and drawings under the marks, and comments and form fields that overlap them, are removed permanently. Πρόκειται να εφαρμόσετε {n} επισημάνσεις απόκρυψης. Το κείμενο, οι εικόνες και τα σχέδια κάτω από τις επισημάνσεις, καθώς και τα σχόλια και τα πεδία φόρμας που επικαλύπτονται, θα αφαιρεθούν οριστικά. + Save the document afterwards: saving rewrites the whole file so no trace of the removed content stays in it. Αποθηκεύστε έπειτα το έγγραφο: η αποθήκευση επανεγγράφει ολόκληρο το αρχείο, ώστε να μη μείνει κανένα ίχνος του περιεχομένου που αφαιρέθηκε. + Select the items to remove from this document. Επιλέξτε τα στοιχεία που θα αφαιρεθούν από αυτό το έγγραφο. + File attachments Συνημμένα αρχεία + Comments and markups Σχόλια και επισημάνσεις + Hidden text Κρυφό κείμενο + Hidden layers Κρυφά επίπεδα + Links, actions and JavaScripts Σύνδεσμοι, ενέργειες και σενάρια JavaScript + Private data of other applications Ιδιωτικά δεδομένα άλλων εφαρμογών + None found Δεν βρέθηκε κανένα + Form fields are flattened: their values stay visible. Saving rewrites the whole file. Τα πεδία φόρμας ενσωματώνονται στο περιεχόμενο και οι τιμές τους παραμένουν ορατές. Η αποθήκευση επανεγγράφει ολόκληρο το αρχείο. + Sanitizing removes hidden information from the document: metadata, file attachments, comments, form fields (flattened), hidden text and layers, bookmarks, links, actions and scripts, and private application data. Η εκκαθάριση αφαιρεί κρυφές πληροφορίες από το έγγραφο: μεταδεδομένα, συνημμένα, σχόλια, πεδία φόρμας (με ενσωμάτωση στο περιεχόμενο), κρυφό κείμενο και επίπεδα, σελιδοδείκτες, συνδέσμους, ενέργειες, σενάρια και ιδιωτικά δεδομένα εφαρμογών. + Save the document afterwards; saving rewrites the whole file so nothing removed stays in it. Αποθηκεύστε έπειτα το έγγραφο. Η αποθήκευση επανεγγράφει ολόκληρο το αρχείο, ώστε να μη διατηρηθεί τίποτα από όσα αφαιρέθηκαν. + Printer: Εκτυπωτής: + Save as PDF Αποθήκευση ως PDF + {name} (default) {name} (προεπιλογή) + Copies: Αντίγραφα: + Print in grayscale Εκτύπωση σε αποχρώσεις του γκρι + Two-sided: Εκτύπωση διπλής όψης: + Flip on long edge Αναστροφή στη μεγάλη πλευρά + Flip on short edge Αναστροφή στη μικρή πλευρά + Paper: Χαρτί: + Pages to Print Σελίδες προς εκτύπωση + More options: Περισσότερες επιλογές: + All pages in range Όλες οι σελίδες του εύρους + Odd pages only Μόνο μονές σελίδες + Even pages only Μόνο ζυγές σελίδες + Page Sizing & Handling Μέγεθος και διαχείριση σελίδων + Poster Αφίσα + Multiple Πολλαπλές σελίδες + Booklet Φυλλάδιο + Fit Προσαρμογή + Shrink oversized pages Σμίκρυνση υπερμεγέθων σελίδων + Custom scale: Προσαρμοσμένη κλίμακα: + Tile scale: Κλίμακα πλακιδίων: + Overlap: Επικάλυψη: + Cut marks Σημάδια κοπής + Pages per sheet: Σελίδες ανά φύλλο: + Page order: Σειρά σελίδων: + Horizontal reversed Οριζόντια αντίστροφα + Vertical reversed Κατακόρυφα αντίστροφα + Print page border Εκτύπωση περιγράμματος σελίδας + Auto-rotate pages Αυτόματη περιστροφή σελίδων + Booklet subset: Επιλογή όψεων φυλλαδίου: + Both sides Και οι δύο όψεις + Front side only Μόνο η μπροστινή όψη + Back side only Μόνο η πίσω όψη + Binding: Βιβλιοδεσία: + Orientation: Προσανατολισμός: + Auto portrait/landscape Αυτόματα κατακόρυφος/οριζόντιος + Comments & Forms Σχόλια και φόρμες + Document and markups Έγγραφο και επισημάνσεις + Document and stamps Έγγραφο και σφραγίδες + Form fields only Μόνο πεδία φόρμας + Previous sheet Προηγούμενο φύλλο + Next sheet Επόμενο φύλλο + No sheets Δεν υπάρχουν φύλλα + Sheet {s} of {n} Φύλλο {s} από {n} + Removed 1 hidden item. Save to remove them from the file. Αφαιρέθηκε 1 κρυφό στοιχείο. Αποθηκεύστε για να αφαιρεθεί από το αρχείο. + Removed {n} hidden items. Save to remove them from the file. Αφαιρέθηκαν {n} κρυφά στοιχεία. Αποθηκεύστε για να αφαιρεθούν από το αρχείο. + Document sanitized. Save to finish: saving rewrites the whole file. Το έγγραφο εκκαθαρίστηκε. Αποθηκεύστε για ολοκλήρωση: η αποθήκευση επανεγγράφει ολόκληρο το αρχείο. + Applied 1 redaction mark. Save to remove the content from the file. Εφαρμόστηκε 1 επισήμανση απόκρυψης. Αποθηκεύστε για να αφαιρεθεί το περιεχόμενο από το αρχείο. + Applied {n} redaction marks. Save to remove the content from the file. Εφαρμόστηκαν {n} επισημάνσεις απόκρυψης. Αποθηκεύστε για να αφαιρεθεί το περιεχόμενο από το αρχείο. + JavaScript Console Κονσόλα JavaScript + JavaScript is turned off (Preferences ▸ JavaScript). Η JavaScript είναι απενεργοποιημένη (Προτιμήσεις > JavaScript). + Output appears here. Η έξοδος εμφανίζεται εδώ. + Script Name: Όνομα σεναρίου: + This document has no document-level scripts. Αυτό το έγγραφο δεν περιέχει σενάρια σε επίπεδο εγγράφου. + No form fields were detected Δεν εντοπίστηκαν πεδία φόρμας + Detected 1 form field Εντοπίστηκε 1 πεδίο φόρμας + Detected {n} form fields Εντοπίστηκαν {n} πεδία φόρμας + {field}: {e} {field}: {e} + Output Έξοδος + Searchable Image (Exact) Αναζητήσιμη εικόνα (ακριβής) + Adds invisible text over each word; the page image is not changed Προσθέτει αόρατο κείμενο πάνω από κάθε λέξη, χωρίς να αλλάζει την εικόνα της σελίδας + Downsample to Μείωση ανάλυσης σε + Text recognition isn't installed: its model files are missing. Reinstall PdfCraft, or set PDFCRAFT_MODELS to the folder that holds them. Η αναγνώριση κειμένου δεν είναι εγκατεστημένη: λείπουν τα αρχεία μοντέλων. Εγκαταστήστε ξανά το PdfCraft ή ορίστε το PDFCRAFT_MODELS στον φάκελο που τα περιέχει. + Planned · {m} Προγραμματισμένο · {m} + Planned for milestone {m} — open to see what it will include Προγραμματισμένο για το ορόσημο {m} — ανοίξτε για να δείτε τι θα περιλαμβάνει + Coming in milestone {m}. Items marked Ready work today. Αναμένεται στο ορόσημο {m}. Τα στοιχεία με ένδειξη «Έτοιμο» λειτουργούν ήδη. + 1 mark 1 επισήμανση + {n} marks {n} επισημάνσεις + Dynamic Δυναμικό + Sign Here Υπογράψτε εδώ + Standard Business Τυπική επαγγελματική + Choose a stamp, then click on the page to place it. Επιλέξτε μια σφραγίδα και έπειτα κάντε κλικ στη σελίδα για να την τοποθετήσετε. + Note Σημείωση + Attachment Συνημμένο + Typewriter Γραφομηχανή + Checked Επιλεγμένο + Unchecked Μη επιλεγμένο + Marked Επισημασμένο + Locked Κλειδωμένο + Search comments Αναζήτηση σχολίων + Add a comment Προσθήκη σχολίου + No comments yet. Δεν υπάρχουν ακόμη σχόλια. + Pick a tool in the quick bar, or type above. Επιλέξτε εργαλείο από τη γραμμή γρήγορης πρόσβασης ή πληκτρολογήστε παραπάνω. + No comments match. Δεν βρέθηκαν σχόλια που να ταιριάζουν. + Unknown author Άγνωστος συντάκτης + No colour Χωρίς χρώμα + Remove checkmark Αφαίρεση σημαδιού επιλογής + Mark with checkmark Σήμανση με σημάδι επιλογής + Add a reply Προσθήκη απάντησης + Post Δημοσίευση + Page {label} Σελίδα {label} + {kind} — click to show it on the page {kind} — κάντε κλικ για εμφάνιση στη σελίδα + Set status Ορισμός κατάστασης + Properties… Ιδιότητες… + Make Current Properties Default Ορισμός τρεχουσών ιδιοτήτων ως προεπιλογών + Colour Χρώμα + Show all comments Εμφάνιση όλων των σχολίων + Create PDF comment summary… Δημιουργία σύνοψης σχολίων σε PDF… + Export all to data file… Εξαγωγή όλων σε αρχείο δεδομένων… + Filter comments Φιλτράρισμα σχολίων + Accepted Αποδεκτό + Cancelled Ακυρώθηκε + Yellow Κίτρινο + Orange Πορτοκαλί + Red Κόκκινο + Pink Ροζ + Purple Μοβ + Blue Μπλε + Light blue Ανοιχτό μπλε + Green Πράσινο + Gray Γκρι + Black Μαύρο + Bookmark options Επιλογές σελιδοδεικτών + Expand all bookmarks Ανάπτυξη όλων των σελιδοδεικτών + Expand top-level bookmarks Ανάπτυξη σελιδοδεικτών πρώτου επιπέδου + Collapse all bookmarks Σύμπτυξη όλων των σελιδοδεικτών + New bookmark ({key}) Νέος σελιδοδείκτης ({key}) + This document has no bookmarks. Αυτό το έγγραφο δεν έχει σελιδοδείκτες. + This document has no layers. Αυτό το έγγραφο δεν έχει επίπεδα. + This document has no attachments. Αυτό το έγγραφο δεν έχει συνημμένα. + Set layer visibility Ορισμός ορατότητας επιπέδου + on page {label} στη σελίδα {label} + Save attachment… Αποθήκευση συνημμένου… + Open in a new tab Άνοιγμα σε νέα καρτέλα + This document has no pages. Αυτό το έγγραφο δεν έχει σελίδες. + Searching… {s}/{p} Αναζήτηση… {s}/{p} + {c} of {n}{more} {c} από {n}{more} + Previous ({key}) Προηγούμενο ({key}) + Next ({key}) Επόμενο ({key}) + Find options Επιλογές αναζήτησης + Whole words only Μόνο ολόκληρες λέξεις + Case-sensitive Διάκριση πεζών/κεφαλαίων + Close (Esc) Κλείσιμο (Esc) + Center horizontally Οριζόντιο κεντράρισμα + Center vertically Κατακόρυφο κεντράρισμα + Horizontally Οριζόντια + Vertically Κατακόρυφα + Set Fields to Same Size Ίδιο μέγεθος πεδίων + Both Και τα δύο + Duplicate… Δημιουργία αντιγράφου… + Landscape pages Οριζόντιες σελίδες + Portrait pages Κατακόρυφες σελίδες + Move earlier Μετακίνηση προς τα προηγούμενα + Move later Μετακίνηση προς τα επόμενα + Back to the document Επιστροφή στο έγγραφο + Protect Using Password Προστασία με κωδικό πρόσβασης + Requires user to enter a password for: Απαίτηση εισαγωγής κωδικού πρόσβασης για: + Viewing Προβολή + Editing Επεξεργασία + Type Password Πληκτρολογήστε κωδικό πρόσβασης + Re-type Password Πληκτρολογήστε ξανά τον κωδικό πρόσβασης + Strength: {s} Ισχύς: {s} + Strong Ισχυρός + Medium Μέτριος + Weak Αδύναμος + Advanced Options {chevron} Σύνθετες επιλογές {chevron} + Compatibility Συμβατότητα + Acrobat X and later (256-bit AES) Acrobat X και νεότερες εκδόσεις (AES 256 bit) + Acrobat 7.0 and later (128-bit AES) Acrobat 7.0 και νεότερες εκδόσεις (AES 128 bit) + Acrobat 5.0 and later (128-bit RC4) Acrobat 5.0 και νεότερες εκδόσεις (RC4 128 bit) + Acrobat 3.0 and later (40-bit RC4) Acrobat 3.0 και νεότερες εκδόσεις (RC4 40 bit) + All document contents Όλο το περιεχόμενο του εγγράφου + All document contents except metadata Όλο το περιεχόμενο του εγγράφου εκτός από τα μεταδεδομένα + Printing allowed Επιτρέπεται η εκτύπωση + Low Resolution (150 dpi) Χαμηλή ανάλυση (150 dpi) + Changes allowed Επιτρεπόμενες αλλαγές + Inserting, deleting, and rotating pages Εισαγωγή, διαγραφή και περιστροφή σελίδων + Filling in form fields and signing existing signature fields Συμπλήρωση πεδίων φόρμας και υπογραφή υπαρχόντων πεδίων υπογραφής + Commenting, filling in form fields, and signing existing signature fields Προσθήκη σχολίων, συμπλήρωση πεδίων φόρμας και υπογραφή υπαρχόντων πεδίων υπογραφής + Any except extracting pages Όλες εκτός από την εξαγωγή σελίδων + Enable copying of text, images, and other content Να επιτρέπεται η αντιγραφή κειμένου, εικόνων και άλλου περιεχομένου + Enable text access for screen reader devices for the visually impaired Να επιτρέπεται η πρόσβαση στο κείμενο από προγράμματα ανάγνωσης οθόνης για άτομα με προβλήματα όρασης + Type a password. Πληκτρολογήστε έναν κωδικό πρόσβασης. + The passwords don't match. Οι κωδικοί πρόσβασης δεν ταιριάζουν. + This compatibility level supports only plain ASCII passwords. Αυτό το επίπεδο συμβατότητας υποστηρίζει μόνο κωδικούς πρόσβασης με χαρακτήρες ASCII. + Anyone opening the document will need this password. It's applied when you save. Όποιος ανοίγει το έγγραφο θα χρειάζεται αυτόν τον κωδικό. Η προστασία εφαρμόζεται κατά την αποθήκευση. + The document opens without a password; this password is needed to change the restrictions. It's applied when you save. Το έγγραφο ανοίγει χωρίς κωδικό. Ο κωδικός απαιτείται για αλλαγή των περιορισμών και εφαρμόζεται κατά την αποθήκευση. + Recover unsaved documents? Ανάκτηση μη αποθηκευμένων εγγράφων; + PdfCraft didn't shut down normally. These documents had changes that were autosaved: Το PdfCraft δεν τερματίστηκε κανονικά. Στα παρακάτω έγγραφα αποθηκεύτηκαν αυτόματα αλλαγές: + just now μόλις τώρα + {n} min ago πριν από {n} λεπτά + {n} h ago πριν από {n} ώρες + · password-protected · προστατευμένο με κωδικό + Recover Ανάκτηση + Version {v} Έκδοση {v} + A clean-room, open-source PDF application written in Rust. MIT OR Apache-2.0. Μια εφαρμογή PDF ανοικτού κώδικα, αναπτυγμένη ανεξάρτητα σε Rust. Άδεια MIT ή Apache-2.0. + Part of Μέρος του + Summarize Options Επιλογές σύνοψης + Choose a layout: Επιλέξτε διάταξη: + Comments only Μόνο σχόλια + 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}»; Οι μεταγενέστερες αλλαγές δεν θα μπορούν να αναιρεθούν. + Number pages Αρίθμηση σελίδων + Style Στυλ + None (prefix only) Κανένα (μόνο πρόθεμα) + Prefix Πρόθεμα + Labels: {preview}. Later pages keep their labels. Ετικέτες: {preview}. Οι επόμενες σελίδες διατηρούν τις ετικέτες τους. + Even and Odd Pages Ζυγές και μονές σελίδες + Even Pages Only Μόνο ζυγές σελίδες + Odd Pages Only Μόνο μονές σελίδες + Landscape and Portrait Pages Οριζόντιες και κατακόρυφες σελίδες + Landscape Pages Οριζόντιες σελίδες + Portrait Pages Κατακόρυφες σελίδες + Duplicate Field Δημιουργία αντιγράφου πεδίου + Duplicate "{name}" onto: Αντιγραφή του «{name}» σε: + 1 page selected. Επιλέχθηκε 1 σελίδα. + {n} pages selected. Επιλέχθηκαν {n} σελίδες. + Delete pages after extracting Διαγραφή σελίδων μετά την εξαγωγή + Extract pages as separate files Εξαγωγή σελίδων ως χωριστά αρχεία + Rotate Pages Περιστροφή σελίδων + Direction: Κατεύθυνση: + Clockwise 90 degrees 90 μοίρες δεξιόστροφα + Counterclockwise 90 degrees 90 μοίρες αριστερόστροφα + 180 degrees 180 μοίρες + Pages: Σελίδες: + All Όλες + From Από + Rotate: Περιστροφή: + Repairs Επιδιορθώσεις + Repair log Αρχείο καταγραφής επιδιορθώσεων + Split document Διαχωρισμός εγγράφου + Pages per file Σελίδες ανά αρχείο + At most Το πολύ + Top-level bookmarks ({n}) Σελιδοδείκτες πρώτου επιπέδου ({n}) + Before each selected page (select pages in Organize) Πριν από κάθε επιλεγμένη σελίδα (επιλέξτε σελίδες στην Οργάνωση) + Creates 1 file from {n} pages. Δημιουργεί 1 αρχείο από {n} σελίδες. + Creates {f} files from {n} pages. Δημιουργεί {f} αρχεία από {n} σελίδες. + Each file holds as many of the {n} pages as fit. Κάθε αρχείο περιέχει όσες από τις {n} σελίδες χωρούν. + Replace Pages Αντικατάσταση σελίδων + Original: replace pages Αρχικό: αντικατάσταση σελίδων + to έως + to {n} έως {n} + Replacement: pages of {name} Αντικατάσταση: σελίδες του {name} + of {name} του {name} + Only the page content changes: links, comments, form fields and bookmarks on the original pages stay. Αλλάζει μόνο το περιεχόμενο των σελίδων: οι σύνδεσμοι, τα σχόλια, τα πεδία φόρμας και οι σελιδοδείκτες των αρχικών σελίδων διατηρούνται. + The replacement file doesn't have that many pages. Το αρχείο αντικατάστασης δεν έχει τόσες σελίδες. + Document Properties Ιδιότητες εγγράφου + Initial View Αρχική προβολή + Fonts Γραμματοσειρές + Application Εφαρμογή + PDF producer Πρόγραμμα παραγωγής PDF + Layout and Magnification Διάταξη και μεγέθυνση + Navigation tab Καρτέλα πλοήγησης + Page Only Μόνο σελίδα + Bookmarks Panel and Page Πίνακας σελιδοδεικτών και σελίδα + Pages Panel and Page Πίνακας σελίδων και σελίδα + Attachments Panel and Page Πίνακας συνημμένων και σελίδα + Layers Panel and Page Πίνακας επιπέδων και σελίδα + Single Page Μία σελίδα + Single Page Continuous Συνεχής προβολή μίας σελίδας + Two-Up (Facing) Δύο αντικριστές σελίδες + Two-Up Continuous (Facing) Συνεχής προβολή αντικριστών σελίδων + Two-Up (Cover Page) Δύο σελίδες (με εξώφυλλο) + Two-Up Continuous (Cover Page) Συνεχής προβολή δύο σελίδων (με εξώφυλλο) + Magnification Μεγέθυνση + Actual Size Πραγματικό μέγεθος + Fit Page Προσαρμογή σελίδας + Fit Width Προσαρμογή στο πλάτος + Fit Height Προσαρμογή στο ύψος + Fit Visible Προσαρμογή στο ορατό περιεχόμενο + Open to page Άνοιγμα στη σελίδα + of {pages} από {pages} + Window Options Επιλογές παραθύρου + Resize window to initial page Προσαρμογή μεγέθους παραθύρου στην αρχική σελίδα + Center window on screen Κεντράρισμα παραθύρου στην οθόνη + Open in Full Screen mode Άνοιγμα σε πλήρη οθόνη + Show: Εμφάνιση: + File Name Όνομα αρχείου + Document Title Τίτλος εγγράφου + User Interface Options Επιλογές περιβάλλοντος χρήστη + Hide menu bar Απόκρυψη γραμμής μενού + Hide toolbars Απόκρυψη γραμμών εργαλείων + Hide window controls Απόκρυψη στοιχείων ελέγχου παραθύρου + Security method Μέθοδος ασφάλειας + No security Χωρίς προστασία + Restrictions Περιορισμοί + None — everything is allowed Κανένας — επιτρέπονται τα πάντα + Password security Προστασία με κωδικό πρόσβασης + Opened with Άνοιγμα με + — (protection is applied when you save) — (η προστασία εφαρμόζεται κατά την αποθήκευση) + Owner password (no restrictions apply) Κωδικός κατόχου (χωρίς περιορισμούς) + User password Κωδικός χρήστη + Change settings… Αλλαγή ρυθμίσεων… + Printing Εκτύπωση + Not allowed Δεν επιτρέπεται + Allowed Επιτρέπεται + High resolution Υψηλή ανάλυση + Low resolution Χαμηλή ανάλυση + Changing the document Τροποποίηση εγγράφου + Document assembly Σύνθεση εγγράφου + Content copying Αντιγραφή περιεχομένου + Content copying for accessibility Αντιγραφή περιεχομένου για προσβασιμότητα + Commenting Προσθήκη σχολίων + Filling of form fields Συμπλήρωση πεδίων φόρμας + No fonts are referenced by the pages. Οι σελίδες δεν αναφέρονται σε γραμματοσειρές. + Revisions Αναθεωρήσεις + View revision {n} Προβολή αναθεώρησης {n} + Open the file as it was saved then Άνοιγμα του αρχείου όπως είχε αποθηκευτεί τότε + Binding Βιβλιοδεσία + Left Edge Αριστερή πλευρά + Right Edge Δεξιά πλευρά + Saved {place}: {before} → {after} ({pct}% smaller){detail} Αποθηκεύτηκε στο {place}: {before} → {after} ({pct}% μικρότερο){detail} + This file is already as small as it can be made ({size}). No copy was saved. Το αρχείο έχει ήδη το μικρότερο δυνατό μέγεθος ({size}). Δεν αποθηκεύτηκε αντίγραφο. + Saved {place}: {before} → {after} (not smaller){detail} Αποθηκεύτηκε στο {place}: {before} → {after} (χωρίς μείωση μεγέθους){detail} + Couldn't save the image: {e} Δεν ήταν δυνατή η αποθήκευση της εικόνας: {e} + Adding images arrives on the web with file pickers for images Η προσθήκη εικόνων θα διατεθεί στη διαδικτυακή έκδοση με επιλογή αρχείων εικόνας + Replacing images on page {p} arrives on the web with image pickers ({i}) Η αντικατάσταση εικόνων στη σελίδα {p} θα διατεθεί στη διαδικτυακή έκδοση με επιλογή εικόνων ({i}) + {name}: choosing images arrives on the web with file pickers for images {name}: η επιλογή εικόνων θα διατεθεί στη διαδικτυακή έκδοση με επιλογή αρχείων εικόνας + Replacing images arrives on the web with image file pickers Η αντικατάσταση εικόνων θα διατεθεί στη διαδικτυακή έκδοση με επιλογή αρχείων εικόνας + Couldn't create a PDF: {e} Δεν ήταν δυνατή η δημιουργία PDF: {e} + The clipboard has no image or text Το πρόχειρο δεν περιέχει εικόνα ή κείμενο + On the web, open or drop an image to convert it Στη διαδικτυακή έκδοση, ανοίξτε ή σύρετε μια εικόνα για μετατροπή + Couldn't optimize the file: {e} Δεν ήταν δυνατή η βελτιστοποίηση του αρχείου: {e} + Couldn't write {path}: {e} Δεν ήταν δυνατή η εγγραφή στο {path}: {e} + The form asks to be submitted to {u}; PdfCraft doesn't send form data. Save the document to keep your entries. Η φόρμα ζητά υποβολή στο {u}, αλλά το PdfCraft δεν αποστέλλει δεδομένα φόρμας. Αποθηκεύστε το έγγραφο για να διατηρήσετε τις καταχωρίσεις σας. + {s} actions run in the JavaScript engine (M6) {s} ενέργειες εκτελούνται στη μηχανή JavaScript (M6) + {name}: the {other} action isn't supported yet {name}: η ενέργεια {other} δεν υποστηρίζεται ακόμη + {name} submits the form to {url}; PdfCraft doesn't send form data. Save the document to keep your entries. Το {name} υποβάλλει τη φόρμα στο {url}, αλλά το PdfCraft δεν αποστέλλει δεδομένα φόρμας. Αποθηκεύστε το έγγραφο για να διατηρήσετε τις καταχωρίσεις σας. + Export stopped: {e} Η εξαγωγή διακόπηκε: {e} + Exported 1 image Εξήχθη 1 εικόνα + Exported {n} images Εξήχθησαν {n} εικόνες + ({s} not exported: {first}) ({s} δεν εξήχθησαν: {first}) + Exported the text of 1 page Εξήχθη το κείμενο 1 σελίδας + Exported the text of {n} pages Εξήχθη το κείμενο {n} σελίδων + {msg} to {dir} {msg} στο {dir} + Exporting… {d} of {t} Εξαγωγή… {d} από {t} + Choose a folder for the exported files Επιλέξτε φάκελο για τα εξαγόμενα αρχεία + New {tool} comments will look like this one Τα νέα σχόλια του εργαλείου {tool} θα έχουν αυτή την εμφάνιση + This document can't be checked: it couldn't be read for editing Δεν είναι δυνατός ο έλεγχος αυτού του εγγράφου: δεν ήταν δυνατή η ανάγνωσή του για επεξεργασία + The checker found no problems in this document. Ο έλεγχος δεν εντόπισε προβλήματα σε αυτό το έγγραφο. + The checker found 1 problem which may prevent the document from being fully accessible. Ο έλεγχος εντόπισε 1 πρόβλημα που ενδέχεται να εμποδίζει την πλήρη προσβασιμότητα του εγγράφου. + The checker found {n} problems which may prevent the document from being fully accessible. Ο έλεγχος εντόπισε {n} προβλήματα που ενδέχεται να εμποδίζουν την πλήρη προσβασιμότητα του εγγράφου. + The text is the same Το κείμενο είναι ίδιο + 1 difference found Βρέθηκε 1 διαφορά + {n} differences found Βρέθηκαν {n} διαφορές + Added 1 comment Προστέθηκε 1 σχόλιο + Added {n} comments Προστέθηκαν {n} σχόλια + Choose a folder for the results Επιλέξτε φάκελο για τα αποτελέσματα + Files for {name} Αρχεία για το {name} + An action is already running Μια ενέργεια εκτελείται ήδη + Running action… file {d} of {t} Εκτέλεση ενέργειας… αρχείο {d} από {t} + No unlinked web addresses were found in the text Δεν βρέθηκαν διευθύνσεις ιστού χωρίς σύνδεσμο στο κείμενο + Created 1 link Δημιουργήθηκε 1 σύνδεσμος + Created {n} links Δημιουργήθηκαν {n} σύνδεσμοι + Text recognition is already running Η αναγνώριση κειμένου εκτελείται ήδη + Optimizing… removing user data Βελτιστοποίηση… αφαίρεση δεδομένων χρήστη + Optimizing… image {d} of {t} Βελτιστοποίηση… εικόνα {d} από {t} + Optimizing… cleaning up Βελτιστοποίηση… εκκαθάριση + Optimizing… merging identical objects Βελτιστοποίηση… συγχώνευση πανομοιότυπων αντικειμένων + Optimizing… writing the copy Βελτιστοποίηση… εγγραφή αντιγράφου + Cancelling… Ακύρωση… + Optimization is already running Η βελτιστοποίηση εκτελείται ήδη + Optimization cancelled Η βελτιστοποίηση ακυρώθηκε + Couldn't optimize the file Δεν ήταν δυνατή η βελτιστοποίηση του αρχείου + Recognizing text… file {d} of {t} Αναγνώριση κειμένου… αρχείο {d} από {t} + Recognizing text… page {d} of {t} Αναγνώριση κειμένου… σελίδα {d} από {t} + Recognized 1 word Αναγνωρίστηκε 1 λέξη + Recognized {n} words Αναγνωρίστηκαν {n} λέξεις + {words} on {pages} {words} σε {pages} + ; 1 page already had text · 1 σελίδα περιείχε ήδη κείμενο + ; {n} pages already had text · {n} σελίδες περιείχαν ήδη κείμενο + Recognized {w} words in 1 file Αναγνωρίστηκαν {w} λέξεις σε 1 αρχείο + Recognized {w} words in {n} files Αναγνωρίστηκαν {w} λέξεις σε {n} αρχεία + ; failed: {list} · αποτυχία: {list} + 1 file done Ολοκληρώθηκε 1 αρχείο + {n} files done Ολοκληρώθηκαν {n} αρχεία + The selected area has been copied ({w} × {h} pixels) Η επιλεγμένη περιοχή αντιγράφηκε ({w} × {h} εικονοστοιχεία) + Couldn't take the snapshot: {e} Δεν ήταν δυνατή η λήψη στιγμιότυπου: {e} + Couldn't recover {name}: {e} Δεν ήταν δυνατή η ανάκτηση του {name}: {e} + The document now conforms to {level}; save it to keep the changes Το έγγραφο συμμορφώνεται πλέον με το {level}· αποθηκεύστε το για να διατηρήσετε τις αλλαγές + Fixed what could be fixed; 1 problem remains Διορθώθηκαν όσα προβλήματα ήταν δυνατό· απομένει 1 πρόβλημα + Fixed what could be fixed; {n} problems remain Διορθώθηκαν όσα προβλήματα ήταν δυνατό· απομένουν {n} προβλήματα + Custom stamps arrive on the web with file pickers for images Οι προσαρμοσμένες σφραγίδες θα διατεθούν στη διαδικτυακή έκδοση με επιλογή αρχείων εικόνας + {file} is too large for a stamp (at most {n} MB) Το {file} είναι πολύ μεγάλο για σφραγίδα (μέγιστο μέγεθος {n} MB) + Sent to {printer} Στάλθηκε στον εκτυπωτή {printer} + Sent to {printer}: {msg} Στάλθηκε στον εκτυπωτή {printer}: {msg} + Choose a folder for the searchable files Επιλέξτε φάκελο για τα αρχεία με δυνατότητα αναζήτησης + Files are combined from top to bottom. Leave Pages empty to take every page. Τα αρχεία συνδυάζονται από πάνω προς τα κάτω. Αφήστε κενό το πεδίο Σελίδες για να συμπεριληφθούν όλες. + Unlock… Ξεκλείδωμα… + Select a password-protected file first Επιλέξτε πρώτα αρχείο προστατευμένο με κωδικό πρόσβασης + Enter the permissions password to allow combining Εισαγάγετε τον κωδικό δικαιωμάτων για να επιτραπεί ο συνδυασμός + Enter the password that opens it Εισαγάγετε τον κωδικό πρόσβασης για το άνοιγμά του + Unlock {name} Ξεκλείδωμα του {name} + Unlock {n} files Ξεκλείδωμα {n} αρχείων + The same password is tried on each file. Ο ίδιος κωδικός πρόσβασης δοκιμάζεται σε κάθε αρχείο. + Hide password Απόκρυψη κωδικού πρόσβασης + Show password Εμφάνιση κωδικού πρόσβασης + Wrong password Λανθασμένος κωδικός πρόσβασης + The password is only used to read the file; it isn't saved. Ο κωδικός χρησιμοποιείται μόνο για την ανάγνωση του αρχείου και δεν αποθηκεύεται. + Opened, but its security settings don't allow combining. Enter the permissions password. Το αρχείο άνοιξε, αλλά οι ρυθμίσεις ασφαλείας δεν επιτρέπουν τον συνδυασμό. Εισαγάγετε τον κωδικό δικαιωμάτων. + Unlocked: the combined file won't be password-protected Ξεκλειδώθηκε: το συνδυασμένο αρχείο δεν θα προστατεύεται με κωδικό πρόσβασης + Unlocked {n} of {m} files Ξεκλειδώθηκαν {n} από {m} αρχεία + Drop PDFs or folders here, or choose them. Σύρετε εδώ PDF ή φακέλους ή επιλέξτε τα. + Add folder… Προσθήκη φακέλου… + Add folder and subfolders… Προσθήκη φακέλου και υποφακέλων… + More ways to add files Περισσότεροι τρόποι προσθήκης αρχείων + Every PDF in the folder Όλα τα PDF του φακέλου + Every PDF in the folder and the folders inside it Όλα τα PDF του φακέλου και των υποφακέλων του + Add folder Προσθήκη φακέλου + Add folder and subfolders Προσθήκη φακέλου και υποφακέλων + No PDFs found in {folder} Δεν βρέθηκαν PDF στον φάκελο {folder} + Added the first {n} PDFs in {folder} Προστέθηκαν τα πρώτα {n} PDF του φακέλου {folder} + Click to sort, drag to move the column. Drag its edge to resize it. Κάντε κλικ για ταξινόμηση ή σύρετε τη στήλη για μετακίνηση. Σύρετε το άκρο της για αλλαγή πλάτους. + Reset columns Επαναφορά στηλών + Remove {n} files Αφαίρεση {n} αρχείων + {n} selected {n} επιλεγμένα + sorted ascending ταξινομημένα σε αύξουσα σειρά + sorted descending ταξινομημένα σε φθίνουσα σειρά + Protected: the combined file won't keep its security settings Προστατευμένο: το συνδυασμένο αρχείο δεν θα διατηρήσει τις ρυθμίσεις ασφαλείας του + Add open documents Προσθήκη ανοικτών εγγράφων + All open documents Όλα τα ανοικτά έγγραφα + No documents are open Δεν υπάρχουν ανοικτά έγγραφα + Select a file first Επιλέξτε πρώτα αρχείο + 1 file 1 αρχείο + {n} files {n} αρχεία + 1 file needs attention 1 αρχείο χρειάζεται έλεγχο + {n} files need attention {n} αρχεία χρειάζονται έλεγχο + {name} can't be combined: {why} Το {name} δεν μπορεί να συνδυαστεί: {why} + Check the pages of {name}: {why} Ελέγξτε τις σελίδες του {name}: {why} + Add the PDFs to combine Προσθέστε τα PDF που θέλετε να συνδυάσετε + Warnings Προειδοποιήσεις + Drag to reorder Σύρετε για αλλαγή σειράς + {k} of {n} {k} από {n} + ? of {n} ? από {n} + Added more than once Προστέθηκε περισσότερες από μία φορές + yesterday χθες + {n} days ago πριν από {n} ημέρες + No pages selected Δεν επιλέχθηκαν σελίδες + Password-protected Προστατευμένο με κωδικό πρόσβασης + Its security settings don't allow copying pages Οι ρυθμίσεις ασφαλείας του δεν επιτρέπουν την αντιγραφή σελίδων + Can't be read: {e} Δεν είναι δυνατή η ανάγνωση: {e} + Includes unsaved changes Περιλαμβάνει μη αποθηκευμένες αλλαγές + Pages of {name} to combine, e.g. 1-3, 6 Σελίδες του {name} για συνδυασμό, π.χ. 1-3, 6 + Remove {name} Αφαίρεση του {name} + Add files… Προσθήκη αρχείων… + Add at least two files Προσθέστε τουλάχιστον δύο αρχεία + Couldn't add {name}: {e} Δεν ήταν δυνατή η προσθήκη του {name}: {e} + Couldn't combine files: {e} Δεν ήταν δυνατός ο συνδυασμός αρχείων: {e} + Export comments Εξαγωγή σχολίων + Export form data Εξαγωγή δεδομένων φόρμας + Comment and form data Δεδομένα σχολίων και φόρμας + Select data files to merge Επιλέξτε αρχεία δεδομένων για συγχώνευση + Form data and PDF forms Δεδομένα φόρμας και φόρμες PDF + Save the spreadsheet Αποθήκευση υπολογιστικού φύλλου + Save the accessibility report Αποθήκευση αναφοράς προσβασιμότητας + Save image Αποθήκευση εικόνας + Export certificate Εξαγωγή πιστοποιητικού + Choose a folder for the extracted pages Επιλέξτε φάκελο για τις εξαγόμενες σελίδες + Choose a folder for the split files Επιλέξτε φάκελο για τα διαχωρισμένα αρχεία + Couldn't use {name}: {e} Δεν ήταν δυνατή η χρήση του {name}: {e} + Couldn't extract pages: {e} Δεν ήταν δυνατή η εξαγωγή σελίδων: {e} + Couldn't write {name}: {e} Δεν ήταν δυνατή η εγγραφή του {name}: {e} + Wrote 1 file to {dir} Γράφτηκε 1 αρχείο στον φάκελο {dir} + Wrote {n} files to {dir} Γράφτηκαν {n} αρχεία στον φάκελο {dir} + No pages match those choices Καμία σελίδα δεν αντιστοιχεί σε αυτές τις επιλογές + Couldn't split: {e} Δεν ήταν δυνατός ο διαχωρισμός: {e} + Couldn't summarize comments: {e} Δεν ήταν δυνατή η σύνοψη σχολίων: {e} + Couldn't open the result: {e} Δεν ήταν δυνατό το άνοιγμα του αποτελέσματος: {e} + Importing data arrives on the web with file pickers for data files Η εισαγωγή δεδομένων θα διατεθεί στη διαδικτυακή έκδοση με επιλογή αρχείων δεδομένων + Exported to {path} Εξήχθη στο {path} + Merging data files needs the desktop app Η συγχώνευση αρχείων δεδομένων απαιτεί την εφαρμογή υπολογιστή + Merged 1 file into {path} Συγχωνεύτηκε 1 αρχείο στο {path} + Merged {n} files into {path} Συγχωνεύτηκαν {n} αρχεία στο {path} + Combined {n} files Συνδυάστηκαν {n} αρχεία + Extracted 1 page Εξήχθη 1 σελίδα + Extracted {n} pages Εξήχθησαν {n} σελίδες + Created a comment summary Δημιουργήθηκε σύνοψη σχολίων + Hide field highlights Απόκρυψη επισήμανσης πεδίων + Highlight fields Επισήμανση πεδίων + Select (V) Επιλογή (V) + Hand (H) Χέρι (H) + Delete pages (Delete) Διαγραφή σελίδων (Delete) + Insert a blank page after the selection Εισαγωγή κενής σελίδας μετά την επιλογή + Insert pages from a file… Εισαγωγή σελίδων από αρχείο… + Extract pages to a new document Εξαγωγή σελίδων σε νέο έγγραφο + Split into files… Διαχωρισμός σε αρχεία… + Paste after Επικόλληση μετά + Set to current page ({n}) Ορισμός στην τρέχουσα σελίδα ({n}) + Indent Αύξηση εσοχής + Outdent Μείωση εσοχής + This document has no form fields. Αυτό το έγγραφο δεν περιέχει πεδία φόρμας. + Tab order: move a field with its arrows. Σειρά μετακίνησης με Tab: μετακινήστε ένα πεδίο με τα βέλη του. + Unplaced Χωρίς τοποθέτηση + Has JavaScript actions (run in M6) Περιέχει ενέργειες JavaScript (εκτελούνται στο M6) + None Κανένα + Delete selected fields Διαγραφή επιλεγμένων πεδίων + Diagnostic details Διαγνωστικές λεπτομέρειες + The operation failed. Η λειτουργία απέτυχε. + no such document δεν υπάρχει τέτοιο έγγραφο + nothing to undo δεν υπάρχει ενέργεια προς αναίρεση + nothing to redo δεν υπάρχει ενέργεια προς επανάληψη + this document is signed: rewriting it would invalidate its signatures (save it incrementally instead) το έγγραφο είναι υπογεγραμμένο: η επανεγγραφή του θα ακύρωνε τις υπογραφές του (αποθηκεύστε το με σταδιακή ενημέρωση) + the document is protected by a password το έγγραφο προστατεύεται με κωδικό πρόσβασης + the password is incorrect ο κωδικός πρόσβασης είναι λανθασμένος + the document has no page tree το έγγραφο δεν διαθέτει δομή σελίδων + a document must keep at least one page το έγγραφο πρέπει να διατηρεί τουλάχιστον μία σελίδα + a bookmark cannot be moved inside itself ένας σελιδοδείκτης δεν μπορεί να μετακινηθεί μέσα στον εαυτό του + the title must not be empty ο τίτλος δεν επιτρέπεται να είναι κενός + the document has no form fields το έγγραφο δεν περιέχει πεδία φόρμας + there are no redaction marks to apply δεν υπάρχουν σημάνσεις απόκρυψης προς εφαρμογή + there are no pages to print δεν υπάρχουν σελίδες προς εκτύπωση + the document has no tags το έγγραφο δεν περιέχει ετικέτες δομής + {0}: {1} {0}: {1} + the text recognition models are not installed (reinstall PdfCraft, or set PDFCRAFT_MODELS to the folder that holds them) τα μοντέλα αναγνώρισης κειμένου δεν είναι εγκατεστημένα (επανεγκαταστήστε το PdfCraft ή ορίστε το PDFCRAFT_MODELS στον φάκελο που τα περιέχει) + the image is empty η εικόνα είναι κενή + this file isn't XFDF, FDF, XML or text form data αυτό το αρχείο δεν περιέχει δεδομένα φόρμας XFDF, FDF, XML ή κειμένου + nothing in the file matches this document κανένα στοιχείο του αρχείου δεν αντιστοιχεί στο έγγραφο + This page couldn't be displayed.\n{e} Δεν ήταν δυνατή η εμφάνιση αυτής της σελίδας.\n{e} + Go to page {p} Μετάβαση στη σελίδα {p} + {s} action Ενέργεια {s} + 1 page selected Επιλέχθηκε 1 σελίδα + {n} pages selected Επιλέχθηκαν {n} σελίδες + {name} — unsaved changes {name} — μη αποθηκευμένες αλλαγές + {kind} field Πεδίο {kind} + Select a file for the stamp Επιλέξτε αρχείο για τη σφραγίδα + JavaScript, e.g. getField("total").value JavaScript, π.χ. getField("total").value + Font: Helvetica Γραμματοσειρά: Helvetica + e.g. en-US π.χ. el-GR + Rendering: hayro (bootstrap) · UI: egui · Icons: Lucide (ISC) · Fonts: Inter, JetBrains Mono, Dancing Script (OFL) Απόδοση: hayro (bootstrap) · Περιβάλλον: egui · Εικονίδια: Lucide (ISC) · Γραμματοσειρές: Inter, JetBrains Mono, Dancing Script (OFL) + CheckBox Πλαίσιο ελέγχου + Radio Κουμπί επιλογής + PushButton Κουμπί εντολής + Combo Αναπτυσσόμενη λίστα + List Λίστα + The document has no XMP metadata Το έγγραφο δεν περιέχει μεταδεδομένα XMP + The title in the metadata and the document information differ Ο τίτλος στα μεταδεδομένα διαφέρει από τον τίτλο στις πληροφορίες εγγράφου + The document is encrypted Το έγγραφο είναι κρυπτογραφημένο + Device colour is used, but there is no PDF/A output intent Χρησιμοποιείται χρώμα συσκευής, αλλά δεν έχει οριστεί προφίλ εξόδου PDF/A + DeviceCMYK is used: it needs a CMYK output intent (not added automatically) Χρησιμοποιείται DeviceCMYK: απαιτείται προφίλ εξόδου CMYK (δεν προστίθεται αυτόματα) + The document has additional actions (/AA) Το έγγραφο περιέχει πρόσθετες ενέργειες (/AA) + The document has document-level JavaScript Το έγγραφο περιέχει JavaScript σε επίπεδο εγγράφου + The form asks viewers to make appearances (NeedAppearances) Η φόρμα απαιτεί από το πρόγραμμα προβολής να δημιουργεί εμφανίσεις πεδίων (NeedAppearances) + A form field has additional actions Ένα πεδίο φόρμας περιέχει πρόσθετες ενέργειες + A stream uses LZW compression Μια ροή χρησιμοποιεί συμπίεση LZW + A stream refers to an external file Μια ροή παραπέμπει σε εξωτερικό αρχείο + An image asks to be interpolated Μια εικόνα απαιτεί παρεμβολή + An image has alternates or OPI information Μια εικόνα περιέχει εναλλακτικές εκδόσεις ή πληροφορίες OPI + PostScript XObjects aren't allowed Δεν επιτρέπονται αντικείμενα XObject PostScript + Embedded files must be PDF/A files themselves (use PDF/A-3 for others) Τα ενσωματωμένα αρχεία πρέπει να είναι επίσης PDF/A (για άλλους τύπους χρησιμοποιήστε PDF/A-3) + An embedded file has no AFRelationship Ένα ενσωματωμένο αρχείο δεν έχει ορισμένο AFRelationship + Select Icon Επιλογή εικονιδίου + Choose images Επιλέξτε εικόνες + {dpi} pixels/inch {dpi} εικονοστοιχεία/ίντσα + Type your {what}. Πληκτρολογήστε {what}. + Draw your {what} below. Σχεδιάστε {what} παρακάτω. + Your name Το όνομά σας + Images and text (converted to PDF) Εικόνες και κείμενο (μετατροπή σε PDF) + Images and text Εικόνες και κείμενο + Check Box Style: Στυλ πλαισίου ελέγχου: + Button Style: Στυλ κουμπιού: + Check Σημάδι επιλογής + Circle Κύκλος + Diamond Ρόμβος + Square Τετράγωνο + Star Αστέρι + {month} {y} {month} {y} + Change {key} Αλλαγή {key} + Insert pages from {name} Εισαγωγή σελίδων από {name} + Fill in {name} Συμπλήρωση του {name} + Set the image of {name} Ορισμός εικόνας του {name} + Edit script of {name} Επεξεργασία δέσμης ενεργειών του {name} + Import {name} Εισαγωγή του {name} + Move bookmark Μετακίνηση σελιδοδείκτη + Set bookmark destination Ορισμός προορισμού σελιδοδείκτη + Add stamp Προσθήκη σφραγίδας + Erase Σβήσιμο + Lock comment Κλείδωμα σχολίου + Unlock comment Ξεκλείδωμα σχολίου + Move comment Μετακίνηση σχολίου + Resize comment Αλλαγή μεγέθους σχολίου + Change comment properties Αλλαγή ιδιοτήτων σχολίου + Add field Προσθήκη πεδίου + Change field properties Αλλαγή ιδιοτήτων πεδίου + Delete field Διαγραφή πεδίου + Duplicate field Δημιουργία αντιγράφου πεδίου + Set tab order Ορισμός σειράς μετακίνησης με Tab + Change initial view Αλλαγή αρχικής προβολής + Set alternate text Ορισμός εναλλακτικού κειμένου + Mark figure as decorative Σήμανση σχήματος ως διακοσμητικού + Run JavaScript Εκτέλεση JavaScript + Delete document JavaScript Διαγραφή JavaScript εγγράφου + Edit document JavaScript Επεξεργασία JavaScript εγγράφου + Move image Μετακίνηση εικόνας + Rotate image Περιστροφή εικόνας + Flip image Αναστροφή εικόνας + Delete image Διαγραφή εικόνας + Add header & footer Προσθήκη κεφαλίδας και υποσέλιδου + Update header & footer Ενημέρωση κεφαλίδας και υποσέλιδου + Update watermark Ενημέρωση υδατογραφήματος + Update background Ενημέρωση φόντου + Edit content Επεξεργασία περιεχομένου + Delete content Διαγραφή περιεχομένου + Remove redaction marks Αφαίρεση σημάνσεων απόκρυψης + Change link properties Αλλαγή ιδιοτήτων συνδέσμου + Delete link Διαγραφή συνδέσμου + Create link Δημιουργία συνδέσμου + Add sticky note Προσθήκη αυτοκόλλητης σημείωσης + Add highlight Προσθήκη επισήμανσης + Add underline Προσθήκη υπογράμμισης + Add strikethrough Προσθήκη διακριτής διαγραφής + Add squiggly underline Προσθήκη κυματιστής υπογράμμισης + Add rectangle Προσθήκη ορθογωνίου + Add oval Προσθήκη έλλειψης + Add arrow Προσθήκη βέλους + Add line Προσθήκη γραμμής + Add drawing Προσθήκη σχεδίου + Add text box Προσθήκη πλαισίου κειμένου + Add checkmark Προσθήκη σημαδιού επιλογής + Add cross Προσθήκη σταυρού + Add dot Προσθήκη τελείας + Add redaction mark Προσθήκη σήμανσης απόκρυψης + Add cloud Προσθήκη σύννεφου + Add polygon Προσθήκη πολυγώνου + Add connected lines Προσθήκη συνδεδεμένων γραμμών + Add callout Προσθήκη πλαισίου επεξήγησης + Add inserted text Προσθήκη εισαγόμενου κειμένου + Add file attachment Προσθήκη συνημμένου αρχείου + Set status Accepted Ορισμός κατάστασης: Αποδεκτό + Set status Rejected Ορισμός κατάστασης: Απορρίφθηκε + Set status Cancelled Ορισμός κατάστασης: Ακυρώθηκε + Set status Completed Ορισμός κατάστασης: Ολοκληρώθηκε + Set status None Ορισμός κατάστασης: Καμία + Set status Unmarked Ορισμός κατάστασης: Χωρίς σήμανση + Save as PDF/A-1b Αποθήκευση ως PDF/A-1b + Save as PDF/A-2b Αποθήκευση ως PDF/A-2b + Save as PDF/A-3b Αποθήκευση ως PDF/A-3b + Fill & Sign: change signature Συμπλήρωση και υπογραφή: αλλαγή υπογραφής + Fill & Sign: change initials Συμπλήρωση και υπογραφή: αλλαγή μονογραφής + Fill & Sign: remove saved signature Συμπλήρωση και υπογραφή: αφαίρεση αποθηκευμένης υπογραφής + Fill & Sign: remove saved initials Συμπλήρωση και υπογραφή: αφαίρεση αποθηκευμένης μονογραφής + Change signature Αλλαγή υπογραφής + Change initials Αλλαγή μονογραφής + Measure distance Μέτρηση απόστασης + Measure perimeter Μέτρηση περιμέτρου + Measure area Μέτρηση εμβαδού + Set measurement scale Ορισμός κλίμακας μέτρησης + Measurement information Πληροφορίες μέτρησης + Measurement snapping Προσκόλληση κατά τη μέτρηση + Export measurements as CSV Εξαγωγή μετρήσεων ως CSV + Distance Απόσταση + Perimeter Περίμετρος + Area Εμβαδόν + Drawing scale Κλίμακα σχεδίου + Snapping Προσκόλληση + Export measurements Εξαγωγή μετρήσεων + Open a PDF to measure it. Ανοίξτε ένα PDF για να πραγματοποιήσετε μετρήσεις. + Click vertices. Enter or double-click finishes a perimeter or area. Click the first point to close an area. Κάντε κλικ στις κορυφές. Με Enter ή διπλό κλικ ολοκληρώνεται η περίμετρος ή το εμβαδόν. Κάντε κλικ στο πρώτο σημείο για να κλείσετε την περιοχή. + Shift constrains to 45 degrees. Backspace removes a vertex. Escape cancels. Το Shift περιορίζει τις γωνίες σε βήματα των 45 μοιρών. Το Backspace αφαιρεί μια κορυφή. Το Escape ακυρώνει. + Click the first point to begin. Κάντε κλικ στο πρώτο σημείο για να ξεκινήσετε. + Snap to drawing Προσκόλληση στο σχέδιο + Endpoints Τελικά σημεία + Midpoints Μέσα τμημάτων + Intersections Τομές + Paths Διαδρομές + Sensitivity Ευαισθησία + Unit Μονάδα + Decimals Δεκαδικά ψηφία + Calibrate from two points Βαθμονόμηση με δύο σημεία + Entire page Ολόκληρη η σελίδα + Viewport rectangle in page points (x, y, width, height) Ορθογώνιο προβολής σε σημεία σελίδας (x, y, πλάτος, ύψος) + Viewport Περιοχή προβολής + Apply scale Εφαρμογή κλίμακας + Saved measurements Αποθηκευμένες μετρήσεις + Saved measurements. Οι μετρήσεις αποθηκεύτηκαν. + Endpoint Τελικό σημείο + Midpoint Μέσο τμήματος + Intersection Σημείο τομής + Path Διαδρομή + This drawing exceeds the snapping limit; only the first paths are used. Το σχέδιο υπερβαίνει το όριο προσκόλλησης· χρησιμοποιούνται μόνο οι πρώτες διαδρομές. + Some page content couldn't be read; snapping uses the rest. Δεν ήταν δυνατή η ανάγνωση μέρους του περιεχομένου της σελίδας· η προσκόλληση χρησιμοποιεί το υπόλοιπο. + Unsupported measurements Μη υποστηριζόμενες μετρήσεις + Label Ετικέτα + {n} measurements use an unsupported format and are left out. {n} μετρήσεις έχουν μη υποστηριζόμενη μορφή και παραλείπονται. + This area has too many overlapping paths; intersection snapping is limited. Αυτή η περιοχή έχει υπερβολικά πολλές επικαλυπτόμενες διαδρομές· η προσκόλληση σε τομές είναι περιορισμένη. + This measurement needs at least {n} points. Αυτή η μέτρηση απαιτεί τουλάχιστον {n} σημεία. + Switch to two-page view first to show the cover page Μεταβείτε πρώτα στην προβολή δύο σελίδων για να εμφανιστεί το εξώφυλλο + Page display: {layout} Εμφάνιση σελίδων: {layout} + Fit to width scrolling Κύλιση με προσαρμογή στο πλάτος + Fit one full page Προσαρμογή ολόκληρης σελίδας + Created a PDF from {n} files Δημιουργήθηκε PDF από {n} αρχεία + PDF, images and text PDF, εικόνες και κείμενο + Insert a file before page {label} Εισαγωγή αρχείου πριν από τη σελίδα {label} + Insert a file at the end Εισαγωγή αρχείου στο τέλος + Save pages Αποθήκευση σελίδων + Save these pages as one PDF Αποθήκευση αυτών των σελίδων σε ένα PDF + No changes to save Δεν υπάρχουν αλλαγές προς αποθήκευση + Created a PDF from {n} files; left out: {names} Δημιουργήθηκε PDF από {n} αρχεία· παραλείφθηκαν: {names} + Created a PDF from the first {n} files Δημιουργήθηκε PDF από τα πρώτα {n} αρχεία + None of those files can be made into a PDF; use PDFs, images or .txt files Κανένα από αυτά τα αρχεία δεν μπορεί να μετατραπεί σε PDF· χρησιμοποιήστε PDF, εικόνες ή αρχεία .txt + Choose a PNG or JPEG image (up to 4 MiB). Transparency is preserved. Επιλέξτε εικόνα PNG ή JPEG (έως 4 MiB). Η διαφάνεια διατηρείται. + Couldn't import the signature image: {e} Δεν ήταν δυνατή η εισαγωγή της εικόνας υπογραφής: {e} + Go to page… Μετάβαση στη σελίδα… + Reopen the files that were open when PdfCraft last closed Εκ νέου άνοιγμα των αρχείων που ήταν ανοιχτά κατά τον τελευταίο τερματισμό του PdfCraft + The standard fonts can't draw “{c}” ({code}). Only Western European characters can be added as text for now. Οι τυπικές γραμματοσειρές δεν μπορούν να εμφανίσουν τον χαρακτήρα «{c}» ({code}). Προς το παρόν μπορούν να προστεθούν ως κείμενο μόνο δυτικοευρωπαϊκοί χαρακτήρες. + Open arrow Ανοιχτή αιχμή βέλους + Closed arrow Κλειστή αιχμή βέλους + Butt Επίπεδο άκρο + Reverse open arrow Αντίστροφη ανοιχτή αιχμή βέλους + Reverse closed arrow Αντίστροφη κλειστή αιχμή βέλους + Slash Πλάγια γραμμή + Larger pages Μεγαλύτερες σελίδες + Smaller pages Μικρότερες σελίδες + Reset page size Επαναφορά μεγέθους σελίδας + Fill & Sign dates Ημερομηνίες συμπλήρωσης και υπογραφής + Pattern Μοτίβο + Today: {date} Σήμερα: {date} + yyyy or yy year · m or mm month · mmm or mmmm month name · d or dd day · ddd or dddd weekday. H, h, M, s and t are time letters and can't be used on their own: put {escape} before a letter to show it as is. yyyy ή yy έτος · m ή mm μήνας · mmm ή mmmm όνομα μήνα · d ή dd ημέρα · ddd ή dddd ημέρα εβδομάδας. Τα H, h, M, s και t είναι σύμβολα ώρας και δεν χρησιμοποιούνται μόνα τους: βάλτε {escape} πριν από ένα γράμμα για να εμφανιστεί αυτούσιο. + Same as interface ({language}) Ίδια με τη γλώσσα περιβάλλοντος ({language}) + {chars} can't be written into the PDF yet: Fill & Sign text is Western European only. Pick a numeric format such as dd.mm.yyyy. Οι χαρακτήρες {chars} δεν μπορούν ακόμη να εγγραφούν στο PDF: το κείμενο συμπλήρωσης και υπογραφής υποστηρίζει μόνο δυτικοευρωπαϊκούς χαρακτήρες. Επιλέξτε αριθμητική μορφή, όπως dd.mm.yyyy. + Opens the printer driver's preferences. They are saved for every print from this computer; the copies, two-sided, grayscale and paper chosen here still apply. Ανοίγει τις προτιμήσεις του προγράμματος οδήγησης εκτυπωτή. Αποθηκεύονται για κάθε εκτύπωση από αυτόν τον υπολογιστή· οι επιλογές αντιγράφων, εκτύπωσης διπλής όψης, κλίμακας του γκρι και χαρτιού εξακολουθούν να ισχύουν. + The printer driver's own settings for this print: paper tray, paper type, finishing and more. Ρυθμίσεις του προγράμματος οδήγησης εκτυπωτή για αυτή την εκτύπωση: δίσκος τροφοδοσίας, τύπος χαρτιού, τελική επεξεργασία και άλλα. + Couldn't open the printer's preferences: {e} Δεν ήταν δυνατό το άνοιγμα των προτιμήσεων του εκτυπωτή: {e} + Printer properties Ιδιότητες εκτυπωτή + Reset to printer defaults Επαναφορά προεπιλογών εκτυπωτή + This printer offers no settings of its own. Αυτός ο εκτυπωτής δεν διαθέτει πρόσθετες ρυθμίσεις. + % of all commits % όλων των καταχωρίσεων + +Bin +Δυαδ. + +LOC +Γραμμές + @{login}: {prs} PRs, {commits} commits, +{added} / −{deleted} lines (Δ {delta}), +{bin_added} / −{bin_deleted} binary assets, {first} – {last} @{login}: {prs} αιτήματα συγχώνευσης, {commits} καταχωρίσεις, +{added} / −{deleted} γραμμές (Δ {delta}), +{bin_added} / −{bin_deleted} δυαδικά στοιχεία, {first} – {last} + A button Ένα κουμπί + A link Ένας σύνδεσμος + A script Μια δέσμη ενεργειών + Add {what} Προσθήκη {what} + Align fields Στοίχιση πεδίων + Another file dialog is still open. Finish with it first. Ένα άλλο παράθυρο επιλογής αρχείου παραμένει ανοιχτό. Ολοκληρώστε πρώτα τη διαδικασία εκεί. + Basic constraints Βασικοί περιορισμοί + Binary assets added Δυαδικά στοιχεία που προστέθηκαν + Binary assets removed Δυαδικά στοιχεία που αφαιρέθηκαν + Change document properties Αλλαγή ιδιοτήτων εγγράφου + Change {what} Αλλαγή {what} + Commits Καταχωρίσεις + Company Εταιρεία + Couldn't show the file picker. Please try again. Δεν ήταν δυνατή η εμφάνιση του παραθύρου επιλογής αρχείου. Δοκιμάστε ξανά. + Create PDF from images Δημιουργία PDF από εικόνες + Cut and stack Κοπή και στοίβαξη + DPI sets the printed page size without resampling the image. Η τιμή DPI καθορίζει το μέγεθος της εκτυπωμένης σελίδας χωρίς επαναδειγματοληψία της εικόνας. + Default page display Προεπιλεγμένη εμφάνιση σελίδων + Default workspace mode Προεπιλεγμένη λειτουργία χώρου εργασίας + Default zoom Προεπιλεγμένη μεγέθυνση + Display name Εμφανιζόμενο όνομα + Distribute fields Ισοκατανομή πεδίων + Documents and view Έγγραφα και προβολή + Dynamic XFA form laid out from its template: {pages}, {fields}. Δυναμική φόρμα XFA με διάταξη από το πρότυπό της: {pages}, {fields}. + Files are combined in this order. Leave Pages empty to take every page. Τα αρχεία συνδυάζονται με αυτή τη σειρά. Αφήστε κενό το πεδίο «Σελίδες» για να συμπεριληφθούν όλες. + First Πρώτο + First commit Πρώτη καταχώριση + Grab bag Διάφορα + H / Space (hold) H / Διάστημα (κρατήστε πατημένο) + Home / End Home / End + Last Τελευταίο + Last commit Τελευταία καταχώριση + Line delta Μεταβολή γραμμών + Line ending Τέλος γραμμής + Lines +/− Γραμμές +/− + Lines added Γραμμές που προστέθηκαν + Lines deleted Γραμμές που διαγράφηκαν + Mark differences Επισήμανση διαφορών + Mark for redaction Σήμανση για οριστική απόκρυψη + Mark pages for redaction Σήμανση σελίδων για οριστική απόκρυψη + Match field sizes Εξίσωση μεγεθών πεδίων + Merged PRs Συγχωνευμένα αιτήματα αλλαγών + Model Μοντέλο + Models Μοντέλα + Name (A–Z) Όνομα (Α–Ω) + No contributor data was built into this copy. Αυτή η έκδοση δεν περιλαμβάνει δεδομένα συντελεστών. + No model credits were built into this copy. Αυτή η έκδοση δεν περιλαμβάνει πληροφορίες αναγνώρισης συνεισφορών μοντέλων. + Only continue if you trust this document. An address can carry information from the document, such as what you typed into its form. Συνεχίστε μόνο αν εμπιστεύεστε αυτό το έγγραφο. Μια διεύθυνση μπορεί να μεταφέρει πληροφορίες από το έγγραφο, όπως όσα πληκτρολογήσατε στη φόρμα του. + Open email app Άνοιγμα εφαρμογής ηλεκτρονικού ταχυδρομείου + Open this web page? Άνοιγμα αυτής της ιστοσελίδας; + PRs Αιτήματα αλλαγών + Place saved {what} Τοποθέτηση αποθηκευμένου στοιχείου: {what} + Print single-sided. Keep the sheets in order, cut at the marks, then stack the piles left to right, top to bottom. Εκτυπώστε στη μία όψη. Κρατήστε τα φύλλα με τη σωστή σειρά, κόψτε στις ενδείξεις και στοιβάξτε τις ομάδες από αριστερά προς τα δεξιά και από πάνω προς τα κάτω. + Real name Πραγματικό όνομα + Remove saved {what} Αφαίρεση αποθηκευμένου στοιχείου: {what} + Reverse the order Αντιστροφή σειράς + Run form script Εκτέλεση δέσμης ενεργειών φόρμας + SHA-1 digest Σύνοψη SHA-1 + SHA-256 digest Σύνοψη SHA-256 + Scripted content must be usable with a keyboard and assistive technology. Check the document's scripts. Το περιεχόμενο που ελέγχεται από δέσμες ενεργειών πρέπει να είναι προσβάσιμο με πληκτρολόγιο και υποστηρικτικές τεχνολογίες. Ελέγξτε τις δέσμες ενεργειών του εγγράφου. + Scripts run in a sandbox without file or network access. A script that asks to open a web page needs your permission first. With JavaScript off, Acrobat's standard format, validate and calculate functions still work. Οι δέσμες ενεργειών εκτελούνται σε απομονωμένο περιβάλλον χωρίς πρόσβαση σε αρχεία ή δίκτυο. Για το άνοιγμα ιστοσελίδας απαιτείται πρώτα η άδειά σας. Ακόμη και με απενεργοποιημένη τη JavaScript, οι τυπικές συναρτήσεις μορφοποίησης, επικύρωσης και υπολογισμού του Acrobat εξακολουθούν να λειτουργούν. + Selected image files: {count} Επιλεγμένα αρχεία εικόνας: {count} + Sign yourself Υπογράψτε εσείς + Sort Ταξινόμηση + Table Πίνακας + Table rows belong directly in a table or its head, body or foot. Fix the table's tags. Οι γραμμές πίνακα πρέπει να ανήκουν απευθείας στον πίνακα ή στην κεφαλίδα, το σώμα ή το υποσέλιδό του. Διορθώστε τις ετικέτες δομής του πίνακα. + Text recognition isn't installed: its models are missing (run `cargo xtask models`, or set PDFCRAFT_MODELS). Η αναγνώριση κειμένου δεν έχει εγκατασταθεί: λείπουν τα μοντέλα της (εκτελέστε `cargo xtask models` ή ορίστε το PDFCRAFT_MODELS). + That didn't work: an internal error stopped it ({m}). Η ενέργεια απέτυχε λόγω εσωτερικού σφάλματος ({m}). + The Windows store's digital IDs couldn't be listed: {e} Δεν ήταν δυνατή η ανάκτηση των ψηφιακών ταυτοτήτων από το αποθετήριο των Windows: {e} + The document changed while you were choosing a file, so nothing was changed. Το έγγραφο τροποποιήθηκε ενώ επιλέγατε αρχείο, οπότε δεν εφαρμόστηκε καμία αλλαγή. + The document was closed before it could be saved. Το έγγραφο έκλεισε προτού αποθηκευτεί. + The pages were extracted but not deleted, because the document changed while you were choosing a folder. Οι σελίδες εξήχθησαν αλλά δεν διαγράφηκαν, επειδή το έγγραφο τροποποιήθηκε ενώ επιλέγατε φάκελο. + This web address mixes letters from different alphabets, a common way to imitate another site's name. Αυτή η διεύθυνση χρησιμοποιεί γράμματα από διαφορετικά αλφάβητα, συνηθισμένη μέθοδος παραπλάνησης με ονόματα ιστοτόπων. + This web address uses letters from another alphabet, which can look like familiar ones. Αυτή η διεύθυνση περιέχει γράμματα άλλου αλφαβήτου που μοιάζουν με γνωστούς χαρακτήρες. + Use 72 DPI (one point per pixel) Χρήση 72 DPI (ένα σημείο ανά εικονοστοιχείο) + Use custom DPI Χρήση προσαρμοσμένης ανάλυσης DPI + Use each image's embedded DPI; use 72 DPI when it is absent. Χρήση της ενσωματωμένης ανάλυσης DPI κάθε εικόνας· αν δεν υπάρχει, χρήση 72 DPI. + Use image resolution Χρήση ανάλυσης εικόνας + Use {what} Χρήση {what} + Used when a PDF doesn't ask for a layout or zoom. Continuous scrolling never snaps between pages. Χρησιμοποιείται όταν το PDF δεν ορίζει διάταξη ή μεγέθυνση. Η συνεχής κύλιση δεν προσκολλάται στις σελίδες. + Used when opening PDFs. An explicit launch or control mode takes precedence for the session. Χρησιμοποιείται κατά το άνοιγμα PDF. Μια ρητά επιλεγμένη λειτουργία εκκίνησης ή ελέγχου υπερισχύει για την τρέχουσα συνεδρία. + Username Όνομα χρήστη + Write this email? Σύνταξη αυτού του email; + XFA form: its fields and its XFA data are kept in step, so other viewers show what you fill in. Φόρμα XFA: τα πεδία και τα δεδομένα XFA συγχρονίζονται, ώστε άλλα προγράμματα προβολής να εμφανίζουν όσα συμπληρώνετε. + {n} contributors · {c} commits {n} συντελεστές · {c} καταχωρίσεις + {who} in this document tried to open an address PdfCraft won't open: {e}. Το στοιχείο {who} σε αυτό το έγγραφο προσπάθησε να ανοίξει διεύθυνση που το PdfCraft δεν επιτρέπει: {e}. + {who} in this document tried to open an address PdfCraft won't open: {e}. Only web (http, https) and email (mailto) links open from documents. Το στοιχείο {who} σε αυτό το έγγραφο προσπάθησε να ανοίξει μη επιτρεπόμενη διεύθυνση: {e}. Από τα έγγραφα ανοίγουν μόνο σύνδεσμοι ιστού (http, https) και ηλεκτρονικού ταχυδρομείου (mailto). + {who} in this document wants to open this address in your browser: Το στοιχείο {who} σε αυτό το έγγραφο θέλει να ανοίξει αυτή τη διεύθυνση στο πρόγραμμα περιήγησης: + {who} in this document wants to open your email app with this address: Το στοιχείο {who} σε αυτό το έγγραφο θέλει να ανοίξει την εφαρμογή ηλεκτρονικού ταχυδρομείου με αυτή τη διεύθυνση: + ΔLOC Δγραμμές + −Bin −Δυαδ. + −LOC −Γραμμές + Scroll tabs left Κύλιση καρτελών προς τα αριστερά + Scroll tabs right Κύλιση καρτελών προς τα δεξιά + Shared Field Properties Κοινές ιδιότητες πεδίων + {n} fields selected Επιλέχθηκαν {n} πεδία + Mixed Μεικτές τιμές + Custom font Προσαρμοσμένη γραμματοσειρά + Only checked properties will change. Mixed means the selected fields have different values. Θα αλλάξουν μόνο οι επιλεγμένες ιδιότητες. Η ένδειξη «Μεικτές τιμές» σημαίνει ότι τα επιλεγμένα πεδία έχουν διαφορετικές τιμές. + Shift- or Ctrl/Command-click to select several fields. Χρησιμοποιήστε Shift ή Ctrl/Command μαζί με κλικ για να επιλέξετε πολλά πεδία. + Font size 0 uses automatic sizing. Το μέγεθος γραμματοσειράς 0 ενεργοποιεί την αυτόματη προσαρμογή. + Character limit: Όριο χαρακτήρων: + Character limit 0 means no limit. A comb of characters needs a limit. Το όριο 0 σημαίνει απεριόριστους χαρακτήρες. Η διάταξη χαρακτήρων σε χωριστά πλαίσια απαιτεί όριο. + To change locked fields, also set Locked to No. Για να αλλάξετε κλειδωμένα πεδία, ορίστε επίσης το «Κλειδωμένο» σε «Όχι». + Comb of characters Χαρακτήρες σε χωριστά πλαίσια + The document changed while you were editing field properties. Το έγγραφο τροποποιήθηκε ενώ επεξεργαζόσασταν τις ιδιότητες των πεδίων. + The document is no longer open. Το έγγραφο δεν είναι πλέον ανοιχτό. + The field properties dialog is no longer open. Το παράθυρο ιδιοτήτων πεδίων δεν είναι πλέον ανοιχτό. + The field properties could not be changed. Δεν ήταν δυνατή η αλλαγή των ιδιοτήτων πεδίων. + The selected fields could not be found. Select them again. Δεν βρέθηκαν τα επιλεγμένα πεδία. Επιλέξτε τα ξανά. + The field {name} no longer exists. Το πεδίο {name} δεν υπάρχει πλέον. + A selected field has no widget on any page, so the fields can't be edited together. Ένα επιλεγμένο πεδίο δεν διαθέτει στοιχείο ελέγχου σε καμία σελίδα, επομένως τα πεδία δεν μπορούν να επεξεργαστούν μαζί. + ({n} at {dpi} dpi, the largest size they can be drawn at) ({n} στα {dpi} dpi, το μεγαλύτερο μέγεθος στο οποίο μπορούν να αποδοθούν) + Drag the selection to move it. Esc clears the selection. Σύρετε την επιλογή για να τη μετακινήσετε. Το Esc καταργεί την επιλογή. + Shift-click to select several objects. Drag empty space to select an area. Κάντε κλικ με πατημένο το Shift για επιλογή πολλών αντικειμένων. Σύρετε σε κενό χώρο για να επιλέξετε περιοχή. + Select at most 1000 objects at a time. Επιλέξτε έως 1.000 αντικείμενα κάθε φορά. + Finish or discard the text being edited before selecting several objects. Ολοκληρώστε ή απορρίψτε την επεξεργασία κειμένου προτού επιλέξετε πολλά αντικείμενα. + Move object Μετακίνηση αντικειμένου + Move objects Μετακίνηση αντικειμένων + Vertical text cannot be moved with this selection. Δεν είναι δυνατή η μετακίνηση κατακόρυφου κειμένου με αυτή την επιλογή. + Text used as a clipping path cannot be moved with this selection. Δεν είναι δυνατή η μετακίνηση κειμένου που χρησιμοποιείται ως διαδρομή αποκοπής με αυτή την επιλογή. + These objects could not be moved. Select them again and try a smaller move. Δεν ήταν δυνατή η μετακίνηση αυτών των αντικειμένων. Επιλέξτε τα ξανά και δοκιμάστε μικρότερη μετακίνηση. + This page contains objects that cannot be selected together. Αυτή η σελίδα περιέχει αντικείμενα που δεν μπορούν να επιλεγούν μαζί. + Fill colour Χρώμα γεμίσματος + No fill Χωρίς γέμισμα + Fill colour: none Χρώμα γεμίσματος: κανένα + Grid view Προβολή πλέγματος + List view Προβολή λίστας + Files are combined in the order shown. Drag them to change it. Τα αρχεία συνδυάζονται με τη σειρά εμφάνισης. Σύρετέ τα για να την αλλάξετε. + Preview unavailable: {e} Η προεπισκόπηση δεν είναι διαθέσιμη: {e} + Pages: {k} of {n} Σελίδες: {k} από {n} + Check the pages Έλεγχος σελίδων + Switch to List view to change the pages it takes. Μεταβείτε στην προβολή λίστας για να αλλάξετε ποιες σελίδες θα συμπεριληφθούν. + Expand all Ανάπτυξη όλων + Collapse all Σύμπτυξη όλων + Remove {n} pages Αφαίρεση {n} σελίδων + Add an image field Προσθήκη πεδίου εικόνας + Add a date field Προσθήκη πεδίου ημερομηνίας + Add a digital signature field Προσθήκη πεδίου ψηφιακής υπογραφής + Digitally sign Ψηφιακή υπογραφή + Certify (visible signature) Πιστοποίηση (ορατή υπογραφή) + Certify (invisible signature) Πιστοποίηση (αόρατη υπογραφή) + Validate all signatures Επικύρωση όλων των υπογραφών + Signatures panel Πίνακας υπογραφών + Fill & Sign: add text Συμπλήρωση και υπογραφή: προσθήκη κειμένου + Fill & Sign: checkmark Συμπλήρωση και υπογραφή: σημάδι επιλογής + Fill & Sign: cross Συμπλήρωση και υπογραφή: σταυρός + Fill & Sign: dot Συμπλήρωση και υπογραφή: τελεία + Fill & Sign: line Συμπλήρωση και υπογραφή: γραμμή + Fill & Sign: date Συμπλήρωση και υπογραφή: ημερομηνία + Fill & Sign: sign Συμπλήρωση και υπογραφή: υπογραφή + Fill & Sign: initials Συμπλήρωση και υπογραφή: μονογραφή + Export to image… Εξαγωγή σε εικόνα… + Reduce file size… Μείωση μεγέθους αρχείου… + Optimize PDF… Βελτιστοποίηση PDF… + Export to text… Εξαγωγή σε κείμενο… + Export to Word… Εξαγωγή σε Word… + Export to HTML… Εξαγωγή σε HTML… + Export to RTF… Εξαγωγή σε RTF… + JavaScript console… Κονσόλα JavaScript… + Document JavaScripts… Σενάρια JavaScript εγγράφου… + Recognize text… Αναγνώριση κειμένου… + Recognize text in multiple files… Αναγνώριση κειμένου σε πολλά αρχεία… + Check for accessibility… Έλεγχος προσβασιμότητας… + Open accessibility report Άνοιγμα αναφοράς προσβασιμότητας + Change reading options… Αλλαγή επιλογών ανάγνωσης… + Add alternate text… Προσθήκη εναλλακτικού κειμένου… + Export all images… Εξαγωγή όλων των εικόνων… + Add header & footer… Προσθήκη κεφαλίδας και υποσέλιδου… + Update header & footer… Ενημέρωση κεφαλίδας και υποσέλιδου… + Remove header & footer Αφαίρεση κεφαλίδας και υποσέλιδου + Add watermark… Προσθήκη υδατογραφήματος… + Update watermark… Ενημέρωση υδατογραφήματος… + Add background… Προσθήκη φόντου… + Update background… Ενημέρωση φόντου… + Protect using password… Προστασία με κωδικό πρόσβασης… + Remove security Κατάργηση προστασίας + Security properties… Ιδιότητες ασφάλειας… + New bookmark Νέος σελιδοδείκτης + New bookmarks from structure Νέοι σελιδοδείκτες από τη δομή + Rotate pages clockwise Δεξιόστροφη περιστροφή σελίδων + Rotate pages counterclockwise Αριστερόστροφη περιστροφή σελίδων + Insert blank page Εισαγωγή κενής σελίδας + Rotate pages… Περιστροφή σελίδων… + Duplicate pages Δημιουργία αντιγράφων σελίδων + Set page boxes… Ορισμός πλαισίων σελίδας… + Insert pages from file… Εισαγωγή σελίδων από αρχείο… + Replace pages… Αντικατάσταση σελίδων… + Extract pages… Εξαγωγή σελίδων… + Split document… Διαχωρισμός εγγράφου… + Number pages… Αρίθμηση σελίδων… + Join the ArtCraft Discord Συμμετοχή στο Discord του ArtCraft + PdfCraft web page Ιστότοπος του PdfCraft + PdfCraft on GitHub PdfCraft στο GitHub + ArtCraft website Ιστότοπος ArtCraft + Check for updates… Έλεγχος για ενημερώσεις… + Export a PDF Εξαγωγή PDF + Export to Εξαγωγή σε + Microsoft Word (.docx) Microsoft Word (.docx) + Spreadsheet (.xlsx) Υπολογιστικό φύλλο (.xlsx) + Presentation (.pptx) Παρουσίαση (.pptx) + Image (PNG) Εικόνα (PNG) + Export all images Εξαγωγή όλων των εικόνων + HTML web page Ιστοσελίδα HTML + Rich Text Format (.rtf) Μορφή εμπλουτισμένου κειμένου (.rtf) + Text (plain) Απλό κείμενο + PostScript / EPS PostScript / EPS + Fill Συμπλήρωση + Checkmark Σημάδι επιλογής + Cross Σταυρός + Dot Τελεία + Initials Μονογραφή + Edit a PDF Επεξεργασία PDF + Modify page Τροποποίηση σελίδας + Add content Προσθήκη περιεχομένου + Text Κείμενο + Link Σύνδεσμος + Bates numbering Αρίθμηση Bates + Attach file Επισύναψη αρχείου + Create a PDF Δημιουργία PDF + Create from Δημιουργία από + Single file Ένα αρχείο + Multiple files Πολλά αρχεία + Clipboard Πρόχειρο + Blank page Κενή σελίδα + Combine files Συνδυασμός αρχείων + Add files to combine Προσθήκη αρχείων για συνδυασμό + Page options Επιλογές σελίδας + Page grid Πλέγμα σελίδων + Extract Εξαγωγή + Set page boxes Ορισμός πλαισίων σελίδας + Page transitions Μεταβάσεις σελίδων + Add comments Προσθήκη σχολίων + Markup Σημειώσεις επισήμανσης + Squiggly underline Κυματιστή υπογράμμιση + Drawing Σχεδίαση + Oval Έλλειψη + Comment list Λίστα σχολίων + Scan & OCR Σάρωση και OCR + In this file Σε αυτό το αρχείο + In multiple files Σε πολλά αρχεία + Enhance scanned file Βελτίωση σαρωμένου αρχείου + Correct recognized text Διόρθωση αναγνωρισμένου κειμένου + Protect a PDF Προστασία PDF + Protect with password Προστασία με κωδικό πρόσβασης + Remove hidden information Αφαίρεση κρυφών πληροφοριών + Advanced options Σύνθετες επιλογές + Encrypt with certificate Κρυπτογράφηση με πιστοποιητικό + Security properties Ιδιότητες ασφάλειας + Redact a PDF Απόκρυψη ευαίσθητου περιεχομένου PDF + Redact pages Απόκρυψη περιεχομένου σελίδων + Find text and redact Εύρεση και απόκρυψη κειμένου + Set properties Ορισμός ιδιοτήτων + Sanitize document Εκκαθάριση κρυφών δεδομένων εγγράφου + Compress a PDF Συμπίεση PDF + Advanced optimization Σύνθετη βελτιστοποίηση + Audit space usage Ανάλυση χρήσης χώρου + Field list Λίστα πεδίων + Field properties Ιδιότητες πεδίου + Tab order by rows Σειρά μετακίνησης με Tab κατά γραμμές + Tab order by columns Σειρά μετακίνησης με Tab κατά στήλες + Add form components Προσθήκη στοιχείων φόρμας + Drop-down list Αναπτυσσόμενη λίστα + Image field Πεδίο εικόνας + Stamp palette Παλέτα σφραγίδων + Use a certificate Χρήση πιστοποιητικού + Timestamp Χρονοσήμανση + Use print production Χρήση εργαλείων προεκτύπωσης + Print production Προεκτύπωση + Output preview Προεπισκόπηση εξόδου + Preflight Προέλεγχος + Convert colors Μετατροπή χρωμάτων + Add printer marks Προσθήκη σημαδιών εκτύπωσης + Fix hairlines Διόρθωση πολύ λεπτών γραμμών + Measure objects Μέτρηση αντικειμένων + Measure Μέτρηση + Measuring tool Εργαλείο μέτρησης + Geospatial location Γεωχωρική θέση + Compare files Σύγκριση αρχείων + Select files to compare Επιλογή αρχείων για σύγκριση + Use guided actions Χρήση καθοδηγούμενων ενεργειών + Actions list Λίστα ενεργειών + Action Wizard Οδηγός ενεργειών + Make accessible Βελτίωση προσβασιμότητας + Prepare for distribution Προετοιμασία για διανομή + Optimize scanned documents Βελτιστοποίηση σαρωμένων εγγράφων + Archive documents Αρχειοθέτηση εγγράφων + Prepare for accessibility Προετοιμασία για προσβασιμότητα + Automatically tag PDF Αυτόματη προσθήκη ετικετών δομής PDF + Change reading options Αλλαγή επιλογών ανάγνωσης + Check for accessibility Έλεγχος προσβασιμότητας + Add alternate text Προσθήκη εναλλακτικού κειμένου + Fix reading order Διόρθωση σειράς ανάγνωσης + Apply PDF standards Εφαρμογή προτύπων PDF + Standards Πρότυπα + Save as PDF/A Αποθήκευση ως PDF/A + Save as PDF/X Αποθήκευση ως PDF/X + Save as PDF/UA Αποθήκευση ως PDF/UA + Add search index Προσθήκη ευρετηρίου αναζήτησης + Index Ευρετήριο + Manage embedded index Διαχείριση ενσωματωμένου ευρετηρίου + Use JavaScript Χρήση JavaScript + JavaScript JavaScript + Console Κονσόλα + Document JavaScripts Σενάρια JavaScript εγγράφου + Document actions Ενέργειες εγγράφου + AI assistant Βοηθός τεχνητής νοημοσύνης + Bring your own model (off by default) Χρήση δικού σας μοντέλου (απενεργοποιημένο από προεπιλογή) + Summarize Σύνοψη + Ask about this document Ερώτηση σχετικά με αυτό το έγγραφο + Translate Μετάφραση + pages σελίδες + Planned Προγραμματισμένο + Planned for milestone Προγραμματισμένο για ορόσημο + open to see what it will include ανοίξτε για να δείτε τι θα περιλαμβάνει + Optional: needs an AI provider you configure Προαιρετικό: απαιτεί πάροχο τεχνητής νοημοσύνης που θα ρυθμίσετε + Coming in milestone Θα διατεθεί στο ορόσημο + Items marked Ready work today. Τα στοιχεία με την ένδειξη «Έτοιμο» λειτουργούν ήδη. + Edit fields Επεξεργασία πεδίων + Try the form as people filling it in will see it Δοκιμή της φόρμας όπως θα τη βλέπουν όσοι τη συμπληρώνουν + Redact all Απόκρυψη όλων των ευαίσθητων στοιχείων + Clear all Εκκαθάριση όλων + Move page Μετακίνηση σελίδας + Move pages Μετακίνηση σελίδων + Replace page Αντικατάσταση σελίδας + Replace pages Αντικατάσταση σελίδων + Run Εκτέλεση + Mark all Σήμανση όλων + Save changes to “{name}” before closing? Αποθήκευση αλλαγών στο «{name}» πριν από το κλείσιμο; + Your changes will be lost if you don't save them. Οι αλλαγές σας θα χαθούν εάν δεν τις αποθηκεύσετε. + “{name}” is protected. Enter a password to open it. Το «{name}» προστατεύεται. Εισαγάγετε κωδικό πρόσβασης για να το ανοίξετε. + Incorrect password. Try again. Λανθασμένος κωδικός πρόσβασης. Δοκιμάστε ξανά. + Recognize Text Αναγνώριση κειμένου + Password protection will be applied when you save Η προστασία με κωδικό θα εφαρμοστεί κατά την αποθήκευση + Saved the print-ready PDF to {path} Το PDF για εκτύπωση αποθηκεύτηκε στο {path} + Could not save: {e} Δεν ήταν δυνατή η αποθήκευση: {e} + Earlier in tab order Νωρίτερα στη σειρά μετακίνησης με Tab + Later in tab order Αργότερα στη σειρά μετακίνησης με Tab + From: Από: + To: Έως: + High Resolution Υψηλή ανάλυση + Verify to see what the document needs. Εκτελέστε επαλήθευση για να δείτε τι χρειάζεται το έγγραφο. + Set Page Boxes Ορισμός πλαισίων σελίδας + Signature panel Πίνακας υπογραφών + {month} {day}, {y} {day} {month} {y} + The document's security settings don't allow filling in form fields Οι ρυθμίσεις ασφάλειας του εγγράφου δεν επιτρέπουν τη συμπλήρωση πεδίων φόρμας + {name} is read-only Το {name} είναι μόνο για ανάγνωση + {name} has no action Το {name} δεν έχει ενέργεια + Previous month Προηγούμενος μήνας + Next month Επόμενος μήνας + January Ιανουάριος + February Φεβρουάριος + March Μάρτιος + April Απρίλιος + May Μάιος + June Ιούνιος + July Ιούλιος + August Αύγουστος + September Σεπτέμβριος + October Οκτώβριος + November Νοέμβριος + December Δεκέμβριος + This certificate is in your list of trusted certificates. Αυτό το πιστοποιητικό βρίσκεται στη λίστα αξιόπιστων πιστοποιητικών σας. + This certificate is trusted through a certificate above it in the chain. Αυτό το πιστοποιητικό θεωρείται αξιόπιστο μέσω άλλου πιστοποιητικού της αλυσίδας. + This certificate is not trusted. Signatures made with it show an unknown identity. Αυτό το πιστοποιητικό δεν είναι αξιόπιστο. Οι υπογραφές που δημιουργούνται με αυτό εμφανίζουν άγνωστη ταυτότητα. + Add to Trusted Certificates Προσθήκη στα αξιόπιστα πιστοποιητικά + Export… Εξαγωγή… + PDF Optimizer Βελτιστοποίηση PDF + Discard Objects Αφαίρεση αντικειμένων + Discard User Data Αφαίρεση δεδομένων χρήστη + Clean Up Εκκαθάριση + Color Images Έγχρωμες εικόνες + Grayscale Images Εικόνες σε κλίμακα του γκρι + Bicubic downsampling to Δικυβική μείωση ανάλυσης σε + for images above για εικόνες πάνω από + ZIP ZIP + Retain existing Διατήρηση υπάρχουσας + Minimum Ελάχιστη + Low Χαμηλή + High Υψηλή + Maximum Μέγιστη + Each image is measured where pages draw it; an image is replaced only if the result is smaller. Κάθε εικόνα μετριέται στο μέγεθος που προβάλλεται στις σελίδες και αντικαθίσταται μόνο εάν το αποτέλεσμα είναι μικρότερο. + Discard all links, actions and JavaScript Αφαίρεση όλων των συνδέσμων, ενεργειών και σεναρίων JavaScript + Discard alternate images Αφαίρεση εναλλακτικών εικόνων + Discard embedded page thumbnails Αφαίρεση ενσωματωμένων μικρογραφιών σελίδων + Discard document tags Αφαίρεση ετικετών δομής εγγράφου + Discard embedded print settings Αφαίρεση ενσωματωμένων ρυθμίσεων εκτύπωσης + Discard bookmarks Αφαίρεση σελιδοδεικτών + Discard hidden layer content Αφαίρεση περιεχομένου κρυφών επιπέδων + Discard all comments, forms and multimedia Αφαίρεση όλων των σχολίων, φορμών και πολυμέσων + Discard document information and metadata Αφαίρεση πληροφοριών και μεταδεδομένων εγγράφου + Discard all object data (file attachments) Αφαίρεση όλων των δεδομένων αντικειμένων (συνημμένα αρχεία) + Discard private data of other applications Αφαίρεση ιδιωτικών δεδομένων άλλων εφαρμογών + Discard hidden text Αφαίρεση κρυφού κειμένου + Use Flate to encode streams that are not encoded Χρήση συμπίεσης Flate για ροές που δεν είναι συμπιεσμένες + Remove invalid links and bookmarks Αφαίρεση μη έγκυρων συνδέσμων και σελιδοδεικτών + Remove unreferenced named destinations Αφαίρεση ονομασμένων προορισμών χωρίς αναφορές + Compress document structure (object streams) Συμπίεση δομής εγγράφου (ροές αντικειμένων) + Remove unused objects and merge identical ones Αφαίρεση αχρησιμοποίητων αντικειμένων και συγχώνευση πανομοιότυπων + Audit space usage… Έλεγχος χρήσης χώρου… + Space Audit Έλεγχος χώρου + Bytes Byte + Content Streams Ροές περιεχομένου + X Object Forms Φόρμες XObject + Structure Info Πληροφορίες δομής + Thumbnails Μικρογραφίες + Named Destinations Ονομασμένοι προορισμοί + Color Spaces Χρωματικοί χώροι + Shadings Σκιάσεις + Patterns (graphics objects) Μοτίβα (γραφικά αντικείμενα) + Extended Graphics States Εκτεταμένες καταστάσεις γραφικών + Embedded Files Ενσωματωμένα αρχεία + Document Overhead Επιπλέον δεδομένα εγγράφου + Total Σύνολο + Text Field Properties Ιδιότητες πεδίου κειμένου + Check Box Properties Ιδιότητες πλαισίου ελέγχου + Radio Button Properties Ιδιότητες κουμπιού επιλογής + Dropdown Properties Ιδιότητες αναπτυσσόμενης λίστας + List Box Properties Ιδιότητες πλαισίου λίστας + Button Properties Ιδιότητες κουμπιού + Digital Signature Properties Ιδιότητες ψηφιακής υπογραφής + Calculate Υπολογισμός + Actions Ενέργειες + This field is locked. Unlock it to change its properties. Το πεδίο είναι κλειδωμένο. Ξεκλειδώστε το για να αλλάξετε τις ιδιότητές του. + Tooltip: Συμβουλή εργαλείου: + Common Properties Κοινές ιδιότητες + Read Only Μόνο για ανάγνωση + Font Size: Μέγεθος γραμματοσειράς: + Text Color: Χρώμα κειμένου: + Borders and Colors Περιγράμματα και χρώματα + Border Color: Χρώμα περιγράμματος: + Fill Color: Χρώμα γεμίσματος: + No color Χωρίς χρώμα + Beveled Λοξότμητο + Left: Αριστερά: + Bottom: Κάτω: + Width: Πλάτος: + Height: Ύψος: + Rotation: Περιστροφή: + Points from the page's bottom-left corner. Σημεία από την κάτω αριστερή γωνία της σελίδας. + Alignment: Στοίχιση: + Default Value: Προεπιλεγμένη τιμή: + Multi-line Πολλές γραμμές + Scroll long text Κύλιση μεγάλου κειμένου + Allow Rich Text Formatting Να επιτρέπεται μορφοποίηση εμπλουτισμένου κειμένου + Limit of Όριο + characters χαρακτήρες + Field is used for file selection Το πεδίο χρησιμοποιείται για επιλογή αρχείου + Check spelling Ορθογραφικός έλεγχος + Comb of Διαχωρισμός σε θέσεις + Export Value: {on} Τιμή εξαγωγής: {on} + Check box is checked by default Το πλαίσιο ελέγχου είναι προεπιλεγμένο + Button is checked by default Το κουμπί είναι προεπιλεγμένο + Buttons with the same name and value are selected in unison Τα κουμπιά με ίδιο όνομα και τιμή επιλέγονται μαζί + Item: Στοιχείο: + Item List Λίστα στοιχείων + Up Πάνω + Delete {o} Διαγραφή {o} + Add the choices people pick from. Προσθέστε τις διαθέσιμες επιλογές για τους χρήστες. + Default: Προεπιλογή: + (none) (κανένα) + Sort items Ταξινόμηση στοιχείων + Allow user to enter custom text Να επιτρέπεται η εισαγωγή προσαρμοσμένου κειμένου + Multiple selection Πολλαπλή επιλογή + Commit selected value immediately Άμεση καταχώριση της επιλεγμένης τιμής + Number Αριθμός + Time Ώρα + Special Ειδικό + Select format category: Επιλέξτε κατηγορία μορφής: + The value is shown as typed. Η τιμή εμφανίζεται όπως πληκτρολογήθηκε. + Decimal places: Δεκαδικά ψηφία: + Separator style: Μορφή διαχωριστικών: + Currency symbol: Σύμβολο νομίσματος: + Before the number Πριν από τον αριθμό + Negative number style: Μορφή αρνητικών αριθμών: + Format: Μορφή: + Custom: Προσαρμοσμένο: + Zip Code Ταχυδρομικός κώδικας + Zip Code + 4 Ταχυδρομικός κώδικας + 4 + Phone Number Αριθμός τηλεφώνου + Social Security Number Αριθμός κοινωνικής ασφάλισης + Arbitrary Mask Προσαρμοσμένη μάσκα + Mask: Μάσκα: + 9 digit, A letter, O letter or digit, X any character 9 ψηφίο, A γράμμα, O γράμμα ή ψηφίο, X οποιοσδήποτε χαρακτήρας + Example: {sample} Παράδειγμα: {sample} + Field value is not validated Η τιμή του πεδίου δεν επικυρώνεται + Field value is in range: Η τιμή του πεδίου βρίσκεται στο εύρος: + Add an Action Προσθήκη ενέργειας + Select Trigger: Επιλέξτε συμβάν ενεργοποίησης: + Mouse Up Απελευθέρωση κουμπιού ποντικιού + Mouse Down Πάτημα κουμπιού ποντικιού + Mouse Enter Είσοδος δείκτη ποντικιού + Mouse Exit Έξοδος δείκτη ποντικιού + On Focus Κατά την εστίαση + On Blur Κατά την απώλεια εστίασης + Select Action: Επιλέξτε ενέργεια: + Open a web link Άνοιγμα συνδέσμου ιστού + Reset a form Επαναφορά φόρμας + Execute a menu item Εκτέλεση εντολής μενού + Show a field Εμφάνιση πεδίου + Hide a field Απόκρυψη πεδίου + Submit a form Υποβολή φόρμας + Fields to reset (comma-separated; empty: all) Πεδία προς επαναφορά (χωρισμένα με κόμματα· κενό: όλα) + Print, NextPage, PrevPage, FirstPage or LastPage Print, NextPage, PrevPage, FirstPage ή LastPage + Field names (comma-separated) Ονόματα πεδίων (χωρισμένα με κόμματα) + URL to submit to Διεύθυνση URL υποβολής + Script: Σενάριο εντολών: + Value: Τιμή: + No actions Καμία ενέργεια + Value is not calculated Δεν υπολογίζεται τιμή + Value is the Η τιμή είναι το + sum (+) άθροισμα (+) + product (x) γινόμενο (x) + average μέσος όρος + minimum ελάχιστο + maximum μέγιστο + of the following fields: των παρακάτω πεδίων: + Simplified field notation: Απλοποιημένη σύνταξη πεδίων: + Field names with + - * / and parentheses; put \\ before spaces in names. Ονόματα πεδίων με + - * / και παρενθέσεις· προσθέστε \\ πριν από τα κενά των ονομάτων. + Format text Μορφοποίηση κειμένου + Centre Κεντράρισμα + Standard fonts; text outside Windows-1252 isn't supported yet. Τυπικές γραμματοσειρές· κείμενο εκτός της κωδικοποίησης Windows-1252 δεν υποστηρίζεται ακόμη. + Crop (% trimmed from each side) Περικοπή (% αφαίρεσης από κάθε πλευρά) + Click on the page to add text; each click starts a new box, and ✓ finishes. Drag items to move them, drag a corner to resize, double-click text to edit it. Κάντε κλικ στη σελίδα για να προσθέσετε κείμενο· κάθε κλικ δημιουργεί νέο πλαίσιο και το ✓ ολοκληρώνει την εισαγωγή. Σύρετε τα στοιχεία για μετακίνηση, μια γωνία για αλλαγή μεγέθους ή κάντε διπλό κλικ στο κείμενο για επεξεργασία. + Choose an image Επιλογή εικόνας + Export to Image Εξαγωγή σε εικόνα + Export to Text Εξαγωγή σε κείμενο + Export All Images Εξαγωγή όλων των εικόνων + Format Μορφή + One {f} file per page, named after the document. Ένα αρχείο {f} ανά σελίδα, με όνομα βάσει του εγγράφου. + Exclude images smaller than Εξαίρεση εικόνων μικρότερων από + No limit Χωρίς όριο + Each image once, named after the document and page. JPEG images are saved unchanged; others as PNG. Κάθε εικόνα εξάγεται μία φορά με όνομα βάσει του εγγράφου και της σελίδας. Οι εικόνες JPEG αποθηκεύονται χωρίς αλλαγές, οι υπόλοιπες ως PNG. + Plain text in reading order; pages are separated by form feeds. Απλό κείμενο με τη σειρά ανάγνωσης· οι σελίδες χωρίζονται με χαρακτήρες αλλαγής σελίδας. + Create {what} Δημιουργία {what} + initials μονογραφή + signature υπογραφή + Create Custom Stamp Δημιουργία προσαρμοσμένης σφραγίδας + From {file} Από το {file} + Category: Κατηγορία: + e.g. My stamps π.χ. Οι σφραγίδες μου + Name: Όνομα: + Delete stamp Διαγραφή σφραγίδας + Create custom stamp… Δημιουργία προσαρμοσμένης σφραγίδας… + From a PDF page or an image Από σελίδα PDF ή εικόνα + Line spacing Διάστιχο + Character spacing Απόσταση χαρακτήρων + Horizontal scale Οριζόντια κλίμακα + Couldn't open {name}: {e} Δεν ήταν δυνατό το άνοιγμα του {name}: {e} + Couldn't read {name}: {e} Δεν ήταν δυνατή η ανάγνωση του {name}: {e} + Portable mode is off: {folder} can't be written ({error}). Settings are kept in your user folder instead. Η φορητή λειτουργία απενεργοποιήθηκε: δεν είναι δυνατή η εγγραφή στο {folder} ({error}). Οι ρυθμίσεις αποθηκεύονται στον φάκελο χρήστη. + PdfCraft is drawing without a graphics processor, so it may be slow. Updating the graphics driver may help. Το PdfCraft αποδίδει τα γραφικά χωρίς επιτάχυνση από επεξεργαστή γραφικών και ενδέχεται να είναι αργό. Μια ενημέρωση του προγράμματος οδήγησης γραφικών ίσως βοηθήσει. + Couldn't save: {e} Δεν ήταν δυνατή η αποθήκευση: {e} + Downloading attachments on the web arrives with M3.10 ({n} bytes ready) Η λήψη συνημμένων στη διαδικτυακή έκδοση θα διατεθεί με το M3.10 ({n} byte έτοιμα) + `{command}` {when} `{command}` {when} + ships in milestone {m} θα διατεθεί στο ορόσημο {m} + needs an AI provider (off by default) απαιτεί πάροχο ΤΝ (απενεργοποιημένο από προεπιλογή) + is available είναι διαθέσιμο + is not available yet δεν είναι ακόμη διαθέσιμο + {label} failed: {e} Αποτυχία {label}: {e} + Undid {label} Αναιρέθηκε: {label} + Redid {label} Επαναλήφθηκε: {label} + A document needs at least one page: copy instead Ένα έγγραφο πρέπει να έχει τουλάχιστον μία σελίδα: χρησιμοποιήστε αντιγραφή + Cut {what} Αποκόπηκε: {what} + Copied {what} Αντιγράφηκε: {what} + 1 page 1 σελίδα + {n} pages {n} σελίδες + Copy or cut pages first Αντιγράψτε ή αποκόψτε πρώτα σελίδες + Couldn't save {name}: {e} Δεν ήταν δυνατή η αποθήκευση του {name}: {e} + Saved {name} Αποθηκεύτηκε το {name} + Saved, but reopening failed: {e} Αποθηκεύτηκε, αλλά το εκ νέου άνοιγμα απέτυχε: {e} + Downloaded {name} Έγινε λήψη του {name} + Couldn't download {name}: {e} Δεν ήταν δυνατή η λήψη του {name}: {e} + Reverted to the last saved version Έγινε επαναφορά στην τελευταία αποθηκευμένη έκδοση + That didn't work: an internal error stopped it ({m}). Your documents are unchanged. Η ενέργεια διακόπηκε από εσωτερικό σφάλμα ({m}). Τα έγγραφά σας δεν άλλαξαν. + Nothing to undo Δεν υπάρχει ενέργεια για αναίρεση + Nothing to redo Δεν υπάρχει ενέργεια για επανάληψη + This document has no form fields you can fill in Το έγγραφο δεν έχει πεδία φόρμας που μπορούν να συμπληρωθούν + This document has no comments to flatten Το έγγραφο δεν έχει σχόλια για ενσωμάτωση + This document has no form fields to flatten Το έγγραφο δεν έχει πεδία φόρμας για ενσωμάτωση + There are no redaction marks (mark text, areas or pages first) Δεν υπάρχουν σημάνσεις απόκρυψης (σημάνετε πρώτα κείμενο, περιοχές ή σελίδες) + This document has no {kind} to change Το έγγραφο δεν έχει {kind} για τροποποίηση + header or footer κεφαλίδα ή υποσέλιδο + watermark υδατογράφημα + background φόντο + This document isn't password-protected Το έγγραφο δεν προστατεύεται με κωδικό + Only the document's owner can change its security (open it with the permissions password) Μόνο ο κάτοχος μπορεί να αλλάξει την προστασία (ανοίξτε το με τον κωδικό δικαιωμάτων) + The document's security settings don't allow this change Οι ρυθμίσεις ασφαλείας του εγγράφου δεν επιτρέπουν αυτή την αλλαγή + Open a document first Ανοίξτε πρώτα ένα έγγραφο + Security will be removed when you save Η προστασία θα αφαιρεθεί κατά την αποθήκευση + Click text or an image to edit it Κάντε κλικ σε κείμενο ή εικόνα για επεξεργασία + Drag a rectangle to create a link; double-click a link to edit it Σύρετε για να ορίσετε ορθογώνια περιοχή συνδέσμου· κάντε διπλό κλικ σε σύνδεσμο για επεξεργασία + There are no redaction marks to apply Δεν υπάρχουν σημάνσεις απόκρυψης για εφαρμογή + Drag to draw the area where the signature should appear. Σύρετε για να σχεδιάσετε την περιοχή εμφάνισης της υπογραφής. + Couldn't fit the visible content: {e} Δεν ήταν δυνατή η προσαρμογή του ορατού περιεχομένου: {e} + Drag a rectangle around the area to copy Σύρετε για να ορίσετε την ορθογώνια περιοχή αντιγραφής + Click on the page to add a {tool}, or drag to set its size Κάντε κλικ στη σελίδα για προσθήκη {tool} ή σύρετε για να ορίσετε το μέγεθος + Drag a rectangle on a page to crop it; double-click a page for Set Page Boxes Σύρετε για να ορίσετε περιοχή περικοπής· κάντε διπλό κλικ στη σελίδα για τις επιλογές πλαισίων σελίδας + Keep tool selected: on Διατήρηση επιλεγμένου εργαλείου: ενεργή + Keep tool selected Διατήρηση επιλεγμένου εργαλείου + Line thickness Πάχος γραμμής + What word or phrase would you like to search for? Ποια λέξη ή φράση θέλετε να αναζητήσετε; + Searching… {s} of {p} pages Αναζήτηση… {s} από {p} σελίδες + (searching… {s} of {p} pages) (αναζήτηση… {s} από {p} σελίδες) + 1 instance 1 εμφάνιση + {n} instances {n} εμφανίσεις + Go to this result Μετάβαση σε αυτό το αποτέλεσμα + Draw Σχεδίαση + Connected lines Συνδεδεμένες γραμμές + Certified by {by} Πιστοποιήθηκε από {by} + At least one signature is invalid. Τουλάχιστον μία υπογραφή είναι μη έγκυρη. + Signed and all signatures are valid. Το έγγραφο είναι υπογεγραμμένο και όλες οι υπογραφές είναι έγκυρες. + At least one signature has problems. Τουλάχιστον μία υπογραφή παρουσιάζει προβλήματα. + This document is secured. Some changes are restricted by its security settings. Το έγγραφο προστατεύεται. Ορισμένες αλλαγές περιορίζονται από τις ρυθμίσεις ασφαλείας του. + This is a dynamic XFA form, which PdfCraft can't display yet. What you see is the file's placeholder page. Πρόκειται για δυναμική φόρμα XFA, την οποία το PdfCraft δεν μπορεί ακόμη να εμφανίσει. Εμφανίζεται η προσωρινή σελίδα του αρχείου. + This form also contains XFA data, which PdfCraft doesn't read yet. You can fill its fields, but Acrobat may show the XFA values instead. Αυτή η φόρμα περιέχει επίσης δεδομένα XFA, τα οποία το PdfCraft δεν διαβάζει ακόμη. Μπορείτε να συμπληρώσετε τα πεδία της, αλλά το Acrobat ενδέχεται να εμφανίσει τις τιμές XFA. + This document contains {n} interactive form fields. Το έγγραφο περιέχει {n} διαδραστικά πεδία φόρμας. + This file was damaged and has been repaired. Saving keeps the repaired version. Το αρχείο ήταν κατεστραμμένο και επιδιορθώθηκε. Η αποθήκευση διατηρεί την επιδιορθωμένη έκδοση. + Dismiss Κλείσιμο ειδοποίησης + Pin a folder to list its newest PDFs here, such as where your scanner or a synced cloud drive saves them. Καρφιτσώστε έναν φάκελο για να εμφανίζονται εδώ τα νεότερα PDF του, όπως εκείνα που αποθηκεύει ο σαρωτής σας ή ένας συγχρονισμένος φάκελος στο νέφος. + This folder isn't available right now. It may have moved, or its drive or cloud sync is offline. Αυτός ο φάκελος δεν είναι διαθέσιμος αυτή τη στιγμή. Μπορεί να έχει μετακινηθεί ή ο δίσκος ή ο συγχρονισμός νέφους να είναι εκτός σύνδεσης. + Back to all tools Επιστροφή σε όλα τα εργαλεία + Close panel Κλείσιμο πίνακα + View less Λιγότερα + View more Περισσότερα + Available Διαθέσιμο + Search tools and commands… Αναζήτηση εργαλείων και εντολών… + No matching tools Δεν βρέθηκαν εργαλεία + Tool Εργαλείο + Command Εντολή + Add a sticky note Προσθήκη αυτοκόλλητης σημείωσης + Add a text box Προσθήκη πλαισίου κειμένου + Underline text Υπογράμμιση κειμένου + Strikethrough text Διακριτή διαγραφή κειμένου + Squiggly underline text Κυματιστή υπογράμμιση κειμένου + Draw freehand Σχεδίαση με ελεύθερο χέρι + Draw a line Σχεδίαση γραμμής + Draw an arrow Σχεδίαση βέλους + Draw a rectangle Σχεδίαση ορθογωνίου + Draw an oval Σχεδίαση έλλειψης + Draw a polygon Σχεδίαση πολυγώνου + Draw connected lines Σχεδίαση συνδεδεμένων γραμμών + Draw a cloud Σχεδίαση σύννεφου + Add a callout Προσθήκη επεξηγηματικού πλαισίου + Replace text Αντικατάσταση κειμένου + Attach a file Επισύναψη αρχείου + Erase drawings Σβήσιμο σχεδίων + Flatten comments Ενσωμάτωση σχολίων στο περιεχόμενο + Import comments… Εισαγωγή σχολίων… + Add a stamp Προσθήκη σφραγίδας + Export all comments to data file… Εξαγωγή όλων των σχολίων σε αρχείο δεδομένων… + Hide all comments Απόκρυψη όλων των σχολίων + Summarize comments Σύνοψη σχολίων + Import form data… Εισαγωγή δεδομένων φόρμας… + Export form data… Εξαγωγή δεδομένων φόρμας… + PDF/A… PDF/A… + Action Wizard… Οδηγός ενεργειών… + Prepare for distribution… Προετοιμασία για διανομή… + Optimize scanned documents… Βελτιστοποίηση σαρωμένων εγγράφων… + Compare files… Σύγκριση αρχείων… + Detect form fields Εντοπισμός πεδίων φόρμας + Merge data files into spreadsheet… Συγχώνευση αρχείων δεδομένων σε υπολογιστικό φύλλο… + Flatten form fields Ενσωμάτωση πεδίων φόρμας στο περιεχόμενο + Prepare a form Προετοιμασία φόρμας + Tab order: by rows Σειρά μετακίνησης με Tab: κατά γραμμές + Tab order: by columns Σειρά μετακίνησης με Tab: κατά στήλες + Tab order: by document structure Σειρά μετακίνησης με Tab: κατά δομή εγγράφου + Edit text & images Επεξεργασία κειμένου και εικόνων + Add or edit links Προσθήκη ή επεξεργασία συνδέσμων + Create links from URLs Δημιουργία συνδέσμων από URL + Remove all links Αφαίρεση όλων των συνδέσμων + Add Bates numbering… Προσθήκη αρίθμησης Bates… + Add image… Προσθήκη εικόνας… + Redact text and images Οριστική απόκρυψη κειμένου και εικόνων + Redact pages… Οριστική απόκρυψη σελίδων… + Find text and redact… Εύρεση και οριστική απόκρυψη κειμένου… + Redaction properties… Ιδιότητες οριστικής απόκρυψης… + Apply redactions… Εφαρμογή οριστικών αποκρύψεων… + Clear redaction marks Απαλοιφή επισημάνσεων απόκρυψης + Remove hidden information… Αφαίρεση κρυφών πληροφοριών… + Sanitize document… Εκκαθάριση εγγράφου… + Field properties… Ιδιότητες πεδίου… + Add a text field Προσθήκη πεδίου κειμένου + Add a checkbox Προσθήκη πλαισίου ελέγχου + Add a radio button Προσθήκη κουμπιού επιλογής + Add a drop-down list Προσθήκη αναπτυσσόμενης λίστας + Add a list box Προσθήκη πλαισίου λίστας + Add a button Προσθήκη κουμπιού + Keychain · Κλειδοθήκη · + Windows store · Αποθήκη πιστοποιητικών Windows · + on page {label} στη σελίδα {label} +@plural {n} field|{n} fields {n} πεδίο|{n} πεδία +@plural {n} page|{n} pages {n} σελίδα|{n} σελίδες +signature pad Type Πληκτρολόγηση +@plural {n} object selected|{n} objects selected {n} επιλεγμένο αντικείμενο|{n} επιλεγμένα αντικείμενα +key Ctrl Ctrl +key Shift Shift +key Delete Διαγραφή +key Esc Esc +key Home Αρχή +key End Τέλος +key Space Διάστημα + Certificate authority Αρχή πιστοποίησης + Creator Δημιουργός + Document properties (⌘D) Ιδιότητες εγγράφου (⌘D) + End entity Τελική οντότητα + Inserted text Εισαχθέν κείμενο + New bookmark (⌘B) Νέος σελιδοδείκτης (⌘B) + Next (⌘G) Επόμενο (⌘G) + Open a PDF (⌘O) Άνοιγμα PDF (⌘O) + Previous (⇧⌘G) Προηγούμενο (⇧⌘G) + Print (⌘P) Εκτύπωση (⌘P) + Producer Παραγωγός + Redo {action} Επανάληψη ενέργειας: {action} + Rotate view clockwise (⇧⌘+) Δεξιόστροφη περιστροφή προβολής (⇧⌘+) + Undo {action} Αναίρεση ενέργειας: {action} + Zoom in (⌘+) Μεγέθυνση (⌘+) + Zoom in / out (also pinch or ⌘-scroll) Μεγέθυνση / σμίκρυνση (επίσης με σύγκλιση δακτύλων ή κύλιση με ⌘) + Zoom out (⌘−) Σμίκρυνση (⌘−) +action wizard Start Έναρξη +calendar date April Απρίλιος +calendar date August Αύγουστος +calendar date December Δεκέμβριος +calendar date February Φεβρουάριος +calendar date January Ιανουάριος +calendar date July Ιούλιος +calendar date June Ιούνιος +calendar date March Μάρτιος +calendar date May Μάιος +calendar date November Νοέμβριος +calendar date October Οκτώβριος +calendar date September Σεπτέμβριος +certificate Subject Υποκείμενο πιστοποιητικού +comment menu Edit Επεξεργασία + +# Added for labels that landed alongside this catalog (2026-10-11). + Appearance mode Λειτουργία εμφάνισης + Light Ανοιχτό + Dark Σκούρο + Active Ενεργό + Dark theme Σκούρο θέμα + Appearance settings… Ρυθμίσεις εμφάνισης… + Save ({key}) Αποθήκευση ({key}) + Select page {label} Επιλογή σελίδας {label} + Rotate page {label} counterclockwise Περιστροφή σελίδας {label} αριστερόστροφα + Rotate page {label} clockwise Περιστροφή σελίδας {label} δεξιόστροφα + Delete page {label} Διαγραφή σελίδας {label} + Offset from centre Μετατόπιση από το κέντρο + Graphic Γραφικό + Signer's name Όνομα υπογράφοντος + Fill & Sign signature Υπογραφή από τη Συμπλήρωση και υπογραφή + Choose image… Επιλογή εικόνας… + Looking for printers… Αναζήτηση εκτυπωτών… + Sending to {printer}… Αποστολή στον {printer}… + Enter permissions password… Εισαγωγή κωδικού δικαιωμάτων… + Permissions password Κωδικός δικαιωμάτων + “{name}” has security restrictions. Enter its permissions password to lift them. Το «{name}» έχει περιορισμούς ασφαλείας. Εισαγάγετε τον κωδικό δικαιωμάτων για να τους αρθούν. + Restrictions lifted Οι περιορισμοί άρθηκαν + Couldn't unlock {name}: {e} Δεν ήταν δυνατό το ξεκλείδωμα του {name}: {e} + This document has no restrictions to lift Αυτό το έγγραφο δεν έχει περιορισμούς προς άρση + The document's security settings don't allow copying text and images Οι ρυθμίσεις ασφαλείας του εγγράφου δεν επιτρέπουν την αντιγραφή κειμένου και εικόνων + Add the files to combine Προσθέστε τα αρχεία προς συνδυασμό + Leave out files that can't be read? Να παραλειφθούν τα αρχεία που δεν διαβάζονται; + These files can't be read, so they won't be in the combined PDF: Αυτά τα αρχεία δεν διαβάζονται, οπότε δεν θα συμπεριληφθούν στο συνδυασμένο PDF: + Combine the rest Συνδυασμός των υπολοίπων + and {n} more και {n} ακόμη + Drop PDFs, images, text files or folders here, or choose them. Αποθέστε εδώ PDF, εικόνες, αρχεία κειμένου ή φακέλους ή επιλέξτε τα. + Every PDF, image and text file in the folder Όλα τα PDF, οι εικόνες και τα αρχεία κειμένου του φακέλου + Every PDF, image and text file in the folder and the folders inside it Όλα τα PDF, οι εικόνες και τα αρχεία κειμένου του φακέλου και των υποφακέλων του + No PDFs, images or text files found in {folder} Δεν βρέθηκαν PDF, εικόνες ή αρχεία κειμένου στο {folder} + Added the first {n} files in {folder} Προστέθηκαν τα πρώτα {n} αρχεία από το {folder} + Margins Περιθώρια + For page sizes and margins. Για μεγέθη σελίδων και περιθώρια. + Reset panel layout Επαναφορά διάταξης πλαισίων + Float panel Αποσύνδεση πλαισίου + Move to group Μετακίνηση σε ομάδα + Panels Πλαίσια + Resize panels Αλλαγή μεγέθους πλαισίων + Resize panel window Αλλαγή μεγέθους παραθύρου πλαισίου + Tools Εργαλεία + Inspector Επιθεωρητής + PDF page: {size} Σελίδα PDF: {size} + Match page at 100% Ταίριασμα σελίδας στο 100% + PDF scale: {scale}% Κλίμακα PDF: {scale}% + Printer paper and scaling may differ. Check driver settings. Το χαρτί και η κλίμακα του εκτυπωτή μπορεί να διαφέρουν. Ελέγξτε τις ρυθμίσεις του προγράμματος οδήγησης. + Drawing scale changes. Use Actual size for full-scale plans. Η κλίμακα του σχεδίου αλλάζει. Για σχέδια σε πλήρη κλίμακα χρησιμοποιήστε το Πραγματικό μέγεθος. + Reload documents when their files change on disk Επαναφόρτωση εγγράφων όταν αλλάζουν τα αρχεία τους στον δίσκο + This file changed on disk. Reload it to see the new version; your unsaved changes will be lost. Αυτό το αρχείο άλλαξε στον δίσκο. Επαναφορτώστε το για να δείτε τη νέα έκδοση· οι μη αποθηκευμένες αλλαγές θα χαθούν. + Reload Επαναφόρτωση + Couldn't reload {name}: {e} Δεν ήταν δυνατή η επαναφόρτωση του {name}: {e} + Mouse wheel scroll speed Ταχύτητα κύλισης τροχού ποντικιού +scroll speed Slow Αργή +scroll speed Normal Κανονική +scroll speed Fast Γρήγορη + Enhance camera image Βελτίωση εικόνας κάμερας + Drag the corners onto the document's edges, then choose Enhance page. Σύρετε τις γωνίες στις άκρες του εγγράφου και επιλέξτε Βελτίωση σελίδας. + Enhance page Βελτίωση σελίδας + Done adding text Τέλος προσθήκης κειμένου + Discard this text (Esc) Απόρριψη αυτού του κειμένου (Esc) + Clean-up Καθαρισμός + Original Πρωτότυπο + Greyscale Κλίμακα του γκρι + Black and white Ασπρόμαυρο diff --git a/crates/ui-egui/src/i18n/mod.rs b/crates/ui-egui/src/i18n/mod.rs index 247d11b8..b4346f15 100644 --- a/crates/ui-egui/src/i18n/mod.rs +++ b/crates/ui-egui/src/i18n/mod.rs @@ -136,7 +136,7 @@ fn plural_sl(n: u64) -> usize { } /// The registry. English first: it is the fallback and the source language. -pub static LANGUAGES: [LangInfo; 18] = [ +pub static LANGUAGES: [LangInfo; 19] = [ LangInfo { code: "en", name: "English", source: "", plural: plural_one_other, rtl: false, catalog: OnceLock::new() }, LangInfo { code: "ja", name: "日本語", source: include_str!("ja.tsv"), plural: plural_none, rtl: false, catalog: OnceLock::new() }, // Simplified Chinese; `zh`, `zh-CN`, `zh-SG` and `zh-Hans-*` locales resolve here (see `candidates`). @@ -189,6 +189,7 @@ pub static LANGUAGES: [LangInfo; 18] = [ LangInfo { code: "pl", name: "Polski", source: include_str!("pl.tsv"), plural: plural_polish, rtl: false, catalog: OnceLock::new() }, // Slovene; every `sl-*` locale (`sl-SI`) resolves here. Four plural forms (one, two, few, other). LangInfo { code: "sl", name: "Slovenščina", source: include_str!("sl.tsv"), plural: plural_sl, rtl: false, catalog: OnceLock::new() }, + LangInfo { code: "el", name: "Ελληνικά", source: include_str!("el.tsv"), plural: plural_one_other, rtl: false, catalog: OnceLock::new() }, ]; impl LangInfo { diff --git a/crates/ui-egui/tests/fonts.rs b/crates/ui-egui/tests/fonts.rs index bdc53ecf..0e4f8ba8 100644 --- a/crates/ui-egui/tests/fonts.rs +++ b/crates/ui-egui/tests/fonts.rs @@ -227,6 +227,7 @@ fn primary_ui_fonts_cover_latin_and_cyrillic_catalogs() { let defs = theme::font_definitions(); for (code, catalog) in [ ("cs", include_str!("../src/i18n/cs.tsv")), + ("el", include_str!("../src/i18n/el.tsv")), ("pt-br", include_str!("../src/i18n/pt-br.tsv")), ("de", include_str!("../src/i18n/de.tsv")), ("es", include_str!("../src/i18n/es.tsv")), From d1980ec99fb4d07addb34e215384ba288c5bdc94 Mon Sep 17 00:00:00 2001 From: pechnikov2026-star Date: Sun, 11 Oct 2026 23:22:16 +0300 Subject: [PATCH 3/9] M14: Kazakh interface language (#955) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * M14: Kazakh interface language A complete clean-room Kazakh (kk) translation of the interface: 2105 catalogue rows covering every English key (commands, tool catalogue, UI literals), with the plural forms and the DATE_LANGUAGES entry (months, months_short, weekdays) the other languages have. - Register 'kk' (Қазақша) in LANGUAGES; every kk-* locale resolves to it, and it appears in Preferences and the date-language picker. - Three coverage tests cloned from the Ukrainian battery: every registered command and All tools label, every tl! literal in the UI sources, and no untranslated English left (format-only strings excepted). - tests/fonts.rs covers the catalogue's Kazakh Cyrillic (әғқңөұүһі) against Inter and JetBrains Mono, which both carry the letters. - ATTRIBUTION.toml gains the kk.tsv entry (contributor-original, MIT OR Apache-2.0). cargo test -p pdfcraft-ui-egui (lib 171 + fonts 7), clippy -D warnings, cargo fmt --check and cargo xtask assets all pass. * Tests: give the Kazakh literal check its own doc comment and restore the Ukrainian one * Kazakh: rows for the labels that landed alongside this catalog; refresh hashes --------- Co-authored-by: pechnikov2026-star Co-authored-by: Brandon Thomas --- ATTRIBUTION.md | 3 +- ATTRIBUTION.toml | 11 + crates/engine/src/dates.rs | 11 +- crates/ui-egui/src/i18n/kk.tsv | 2250 ++++++++++++++++++++++++++++++++ crates/ui-egui/src/i18n/mod.rs | 110 +- crates/ui-egui/tests/fonts.rs | 3 +- 6 files changed, 2384 insertions(+), 4 deletions(-) create mode 100644 crates/ui-egui/src/i18n/kk.tsv diff --git a/ATTRIBUTION.md b/ATTRIBUTION.md index edcb9c04..351955f8 100644 --- a/ATTRIBUTION.md +++ b/ATTRIBUTION.md @@ -4,7 +4,7 @@ Every asset PdfCraft includes, bundles or uses to build its published material, with its author, source and licence. The policy is in [AGENTS.md](AGENTS.md) §1. The machine-readable list, with SHA-256 hashes, is [ATTRIBUTION.toml](ATTRIBUTION.toml). Licence texts are kept beside the assets and summarised in [NOTICE](NOTICE). -## In this repository (267) +## In this repository (268) | Asset | Title | Author | Licence | Source | Used for | |---|---|---|---|---|---| @@ -22,6 +22,7 @@ Every asset PdfCraft includes, bundles or uses to build its published material, | `crates/ui-egui/src/i18n/ru.tsv` | Russian 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 | Russian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs | | `crates/ui-egui/src/i18n/hu.tsv` | Hungarian 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 | Hungarian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs | | `crates/ui-egui/src/i18n/bg.tsv` | Bulgarian 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 | Bulgarian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs | +| `crates/ui-egui/src/i18n/kk.tsv` | Kazakh interface translation catalog | PdfCraft contributors | MIT OR Apache-2.0 | Contributor-original: clean-room translations of the English UI labels into Kazakh, no proprietary localisation resources | Kazakh UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs | | `crates/ui-egui/src/i18n/ja.tsv` | Japanese 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 | Japanese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs | | `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 | diff --git a/ATTRIBUTION.toml b/ATTRIBUTION.toml index 821939c0..f473ec05 100644 --- a/ATTRIBUTION.toml +++ b/ATTRIBUTION.toml @@ -164,6 +164,17 @@ kind = "translation" usage = "Bulgarian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" sha256 = "ca305b188abc8d9c261901dde10f7cd8518520983fdf88d2358374b352ad3d5a" +[[asset]] +path = "crates/ui-egui/src/i18n/kk.tsv" +title = "Kazakh interface translation catalog" +author = "PdfCraft contributors" +source = "Contributor-original: clean-room translations of the English UI labels into Kazakh, no proprietary localisation resources" +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 = "77afe7d7fa215c99e8e627a758178d79c51e08e33564e00b97b8840aeb91a9dd" + [[asset]] path = "crates/ui-egui/src/i18n/ja.tsv" title = "Japanese interface translation catalog" diff --git a/crates/engine/src/dates.rs b/crates/engine/src/dates.rs index 0feb9eaa..154ac965 100644 --- a/crates/engine/src/dates.rs +++ b/crates/engine/src/dates.rs @@ -56,7 +56,7 @@ const AR_DAYS: [&str; 7] = ["الأحد", "الاثنين", "الثلاثاء", /// The languages month and weekday names can be written in (Preferences ▸ Date format ▸ Language): /// the interface languages, in the same order and with the same names. -pub const DATE_LANGUAGES: [DateLanguage; 19] = [ +pub const DATE_LANGUAGES: [DateLanguage; 20] = [ DateLanguage { code: "en", name: "English", @@ -160,6 +160,15 @@ pub const DATE_LANGUAGES: [DateLanguage; 19] = [ days: ["воскресенье", "понедельник", "вторник", "среда", "четверг", "пятница", "суббота"], days_short: ["вс", "пн", "вт", "ср", "чт", "пт", "сб"], }, + DateLanguage { + code: "kk", + name: "Қазақша", + months: ["қаңтар", "ақпан", "наурыз", "сәуір", "мамыр", "маусым", "шілде", "тамыз", "қыркүйек", "қазан", "қараша", "желтоқсан"], + months_with_day: None, + months_short: ["қаң", "ақп", "нау", "сәу", "мам", "мау", "шіл", "там", "қыр", "қаз", "қар", "жел"], + days: ["жексенбі", "дүйсенбі", "сейсенбі", "сәрсенбі", "бейсенбі", "жұма", "сенбі"], + days_short: ["жк", "дс", "сс", "ср", "бс", "жм", "сб"], + }, DateLanguage { code: "bg", name: "Български", diff --git a/crates/ui-egui/src/i18n/kk.tsv b/crates/ui-egui/src/i18n/kk.tsv new file mode 100644 index 00000000..1cd217d0 --- /dev/null +++ b/crates/ui-egui/src/i18n/kk.tsv @@ -0,0 +1,2250 @@ + Menu Мәзір + File Файл + Edit Түзету + Pages Беттер + View Көрініс + Help Көмек + Preferences Параметрлер + Preferences… Параметрлер… + Interface language Интерфейс тілі + Identity Жеке деректер + Name on new comments Жаңа түсініктемелердегі аты + Open… Ашу… + Open Recent Соңғы файлдарды ашу + Pin folder to Home… Қалтаны Басты бетке бекіту… + Folders Қалталар + Pin a folder… Қалтаны бекіту… + Pin a folder to Home Қалтаны Басты бетке бекіту + Pin a folder to list its newest PDFs here, such as where your scanner or a synced cloud drive saves them. Қалтаны бекітіңіз — оның ең жаңа PDF файлдары осында көрсетіледі, мысалы, сканер немесе синхрондалатын бұлт дискісі файлдарды сақтайтын қалта. + Unpin Бекітуді алып тастау + Unpin {folder} {folder} бекітпесін алып тастау + Looking for PDFs… PDF файлдары ізделуде… + This folder isn't available right now. It may have moved, or its drive or cloud sync is offline. Бұл қалта қазір қолжетімді емес. Ол басқа жерге көшірілген, не болмаса оның дискісі мен бұлт синхроны желіден тыс болуы мүмкін. + No PDFs in this folder yet Бұл қалтада әзірге PDF файлдары жоқ + Show less Азырақ көрсету + Show {n} more Тағы {n} көрсету + The newest {n} of {total} PDFs Ең жаңа {n} PDF (барлығы {total}) + Pinned folders need the desktop app Бекітілген қалталар үшін жұмыс үстелі қосымшасы қажет + Choose a folder on this computer to pin Бекіту үшін осы компьютердегі бір қалтаны таңдаңыз + {folder} is already pinned to Home {folder} қалтасы Басты бетке бұрыннан бекітілген + You can pin up to {n} folders. Unpin one first. Ең көбі {n} қалта бекітуге болады. Алдымен біреуінің бекітпесін алып тастаңыз. + Pinned {folder} to Home {folder} Басты бетке бекітілді + No recent files Соңғы файлдар жоқ + New blank PDF Жаңа бос PDF + Create PDF from file… Файлдан PDF жасау… + Create PDF from images… Суреттерден PDF жасау… + Create PDF from clipboard Алмасу буферінен PDF жасау + Combine files… Файлдарды біріктіру… + Save Сақтау + Save as… Басқаша сақтау… + Close file Файлды жабу + Close all Барлығын жабу + Revert Сақталған нұсқаға қайту + Print… Басып шығару… + Document properties… Құжат сипаттары… + Undo Болдырмау + Redo Қайталау + Find… Іздеу… + Advanced search… Кеңейтілген іздеу… + Copy pages Беттерді көшіру + Cut pages Беттерді кесу + Paste pages Беттерді қою + Fit visible Көрінетін аймаққа сыйдыру + Marquee zoom Аймақты масштабтау + Take a snapshot Суретке түсіру + Full screen mode Толық экран режимі + Read mode Оқу режимі + Switch light / dark theme Ашық / қараңғы теманы ауыстыру + Comments panel Түсініктемелер панелі + Form fields panel Форма өрістері панелі + Clear form Форманы тазалау + Find tools and commands… Құралдар мен командаларды іздеу… + Zoom Масштаб + Actual size Нақты өлшем + Zoom to page level Бет деңгейіне масштабтау + Fit to width Еніне сыйдыру + Display theme Көрсету темасы + Side panels Бүйірлік панельдер + Enable Acrobat JavaScript Acrobat JavaScript-ті қосу + OK ОК + Join our Discord Discord-қа қосылыңыз + Close Жабу + Next / previous match Келесі / алдыңғы сәйкестік + Copy selected text Таңдалған мәтінді көшіру + Double-click Қос шерту + Select a word Сөзді таңдау + Clear selection / close find Таңдауды тазалау / іздеуді жабу + Rotate view Көріністі бұру + First / last page Бірінші / соңғы бет + Previous / next page Алдыңғы / келесі бет + Delete selected pages (Organize) Таңдалған беттерді жою (Ұйымдастыру) + Select all pages (Organize) Барлық беттерді таңдау (Ұйымдастыру) + Home Басты бет + Open Ашу + Dark gray theme Қою сұр тема + Light theme Ашық тема + All tools Барлық құралдар + Read Оқу + Convert Түрлендіру + E-Sign Электронды қол қою + Save (M4) Сақтау (M4) + Find tools and commands Құралдар мен командаларды іздеу + Fit to height Биіктігіне сыйдыру + Zoom in Үлкейту + Zoom out Кішірейту + Rotate view clockwise Көріністі сағат тілінің бағытымен бұру + Rotate view counterclockwise Көріністі сағат тіліне қарсы бұру + Page navigation Беттер бойынша навигация + Previous view Алдыңғы көрініс + Next view Келесі көрініс + Page display Беттерді көрсету + Continuous scrolling Үздіксіз айналдыру + Two-page view Екі беттік көрініс + Single page Бір бет + Show cover page in two-page view Екі беттік көріністе мұқаба бетін көрсету + Use system setting Жүйе параметрін пайдалану + Light gray Ашық сұр + Dark gray Қою сұр + Comments Түсініктемелер + Bookmarks Бетбелгілер + Fields Өрістер + Layers Қабаттар + Attachments Тіркемелер + Signatures Қолтаңбалар + Accessibility Checker Қолжетімділік тексеруіші + Search Іздеу + Compare Салыстыру + Page thumbnails Бет миниатюралары + Form fields Форма өрістері + Toggle fit page / fit width Бетке / еніне сыйдыруды ауыстыру + Next page Келесі бет + Previous page Алдыңғы бет + Current page — type a page number or label (such as iv) and press Enter Ағымдағы бет — бет нөмірін немесе белгісін (мысалы, iv) теріңіз де, Enter пернесін басыңыз + Welcome to PdfCraft PdfCraft-ге қош келдіңіз + An open-source PDF workbench — local, private, and scriptable. Ашық бастапқы кодты PDF жұмыс орны — жергілікті, жеке әрі скрипттерді қолдайтын. + Join the ArtCraft community ArtCraft қауымдастығына қосылыңыз + Get help, share feedback and follow development on Discord. Көмек алыңыз, пікір бөлісіңіз және Discord-тағы әзірлеуді қадағалаңыз. + Recommended tools Ұсынылған құралдар + Use now Дәл қазір пайдалану + Open file Файлды ашу + Recent Соңғы + Files you open in PdfCraft appear here. Drop a PDF anywhere to open it. PdfCraft-те ашқан файлдарыңыз осында көрсетіледі. PDF файлын ашу үшін оны кез келген жерге апарып тастаңыз. + Privacy Жекелік + PdfCraft works offline. No telemetry, no account, and no cloud processing unless you add a provider. PdfCraft офлайн жұмыс істейді. Провайдер қоспағаныңызша телеметрия да, тіркелгі де, бұлтта өңдеу де жоқ. + Scripts run in a sandbox without file or network access. With JavaScript off, Acrobat's standard format, validate and calculate functions still work. Скрипттер файлға да, желіге де қатынасы жоқ құмсалғышта орындалады. JavaScript өшірулі тұрса да, Acrobat-тың стандартты пішімдеу, тексеру және есептеу функциялары әлі де жұмыс істейді. + Back to all tools Барлық құралдарға оралу + Close panel Панельді жабу + View less Азырақ көру + View more Көбірек көру + Available Қолжетімді + Ready Дайын + Search tools and commands… Құралдар мен командаларды іздеу… + No matching tools Сәйкес құралдар жоқ + Tool Құрал + Command Команда + Add a sticky note Стикер қосу + Add a text box Мәтін блокын қосу + Highlight text Мәтінді бояп белгілеу + Underline text Мәтінді астын сызу + Strikethrough text Мәтінді сызып тастау + Squiggly underline text Мәтінді толқын сызықпен астын сызу + Draw freehand Қолмен салу + Draw a line Сызық салу + Draw an arrow Жебе салу + Draw a rectangle Тіктөртбұрыш салу + Draw an oval Эллипс салу + Draw a polygon Көпбұрыш салу + Draw connected lines Байланысқан сызықтар салу + Draw a cloud Бұлт салу + Add a callout Көрсеткішті мәтін қосу + Insert text Мәтін енгізу + Replace text Мәтінді ауыстыру + Attach a file Файлды тіркеу + Erase drawings Суреттерді өшіру + Flatten comments Түсініктемелерді жазықтау + Import comments… Түсініктемелерді импорттау… + Add a stamp Мөр қосу + Export all comments to data file… Барлық түсініктемені деректер файлына экспорттау… + Hide all comments Барлық түсініктемелерді жасыру + Summarize comments Түсініктемелерді қорытындылау + Import form data… Форма деректерін импорттау… + Export form data… Форма деректерін экспорттау… + PDF/A… PDF/A… + Action Wizard… Әрекеттер шебері… + Prepare for distribution… Таратуға дайындау… + Optimize scanned documents… Сканерленген құжаттарды оңтайландыру… + Compare files… Файлдарды салыстыру… + Detect form fields Форма өрістерін анықтау + Merge data files into spreadsheet… Деректер файлдарын электронды кестеге біріктіру… + Flatten form fields Форма өрістерін жазықтау + Prepare a form Форманы дайындау + Tab order: by rows Tab реті: жолдар бойынша + Tab order: by columns Tab реті: бағандар бойынша + Tab order: by document structure Tab реті: құжат құрылымы бойынша + Add text Мәтін қосу + Edit text & images Мәтін мен суреттерді түзету + Add or edit links Сілтемелерді қосу немесе түзету + Create links from URLs URL-ден сілтемелер жасау + Remove all links Барлық сілтемелерді алып тастау + Add Bates numbering… Bates нөмірлеуін қосу… + Add image… Сурет қосу… + Redact text and images Мәтін мен суреттерді жасыру + Redact pages… Беттерді жасыру… + Find text and redact… Мәтінді тауып, жасыру… + Redaction properties… Жасыру сипаттары… + Apply redactions… Жасыруларды қолдану… + Clear redaction marks Жасыру белгілерін тазалау + Remove hidden information… Жасырын ақпаратты алып тастау… + Sanitize document… Құжатты тазарту… + Field properties… Өріс сипаттары… + Add a text field Мәтін өрісін қосу + Add a checkbox Құсбелгі қосу + Add a radio button Ауыстырып қосқыш қосу + Add a drop-down list Ашылмалы тізім қосу + Add a list box Тізім қосу + Add a button Түйме қосу + Add an image field Сурет өрісін қосу + Add a date field Күн өрісін қосу + Add a digital signature field Цифрлық қолтаңба өрісін қосу + Digitally sign Цифрлық түрде қол қою + Certify (visible signature) Куәландыру (көрінетін қолтаңба) + Certify (invisible signature) Куәландыру (көрінбейтін қолтаңба) + Validate all signatures Барлық қолтаңбаларды тексеру + Signatures panel Қолтаңбалар панелі + Fill & Sign: add text Толтыру және қол қою: мәтін қосу + Fill & Sign: checkmark Толтыру және қол қою: құсбелгі + Fill & Sign: cross Толтыру және қол қою: крест + Fill & Sign: dot Толтыру және қол қою: нүкте + Fill & Sign: line Толтыру және қол қою: сызық + Fill & Sign: date Толтыру және қол қою: күн + Fill & Sign: sign Толтыру және қол қою: қол қою + Fill & Sign: initials Толтыру және қол қою: бастапқы әріптер + Export to image… Суретке экспорттау… + Reduce file size… Файл өлшемін кішірейту… + Optimize PDF… PDF оңтайландыру… + Export to text… Мәтінге экспорттау… + Export to Word… Word-ға экспорттау… + Export to HTML… HTML-ге экспорттау… + Export to RTF… RTF-ке экспорттау… + JavaScript console… JavaScript консолі… + Document JavaScripts… Құжат JavaScript-тері… + Recognize text… Мәтінді тану… + Recognize text in multiple files… Бірнеше файлда мәтінді тану… + Check for accessibility… Қолжетімділікті тексеру… + Open accessibility report Қолжетімділік есебін ашу + Change reading options… Оқу параметрлерін өзгерту… + Add alternate text… Балама мәтін қосу… + Export all images… Барлық суреттерді экспорттау… + Add header & footer… Жоғарғы және төменгі колонтитулдарды қосу… + Update header & footer… Жоғарғы және төменгі колонтитулдарды жаңарту… + Remove header & footer Жоғарғы және төменгі колонтитулдарды алып тастау + Add watermark… Су белгісін қосу… + Update watermark… Су белгісін жаңарту… + Remove watermark Су белгісін алып тастау + Add background… Фон қосу… + Update background… Фонды жаңарту… + Remove background Фонды алып тастау + Protect using password… Құпиясөз арқылы қорғау… + Remove security Қорғанысты алып тастау + Security properties… Қорғаныс сипаттары… + Organize pages Беттерді ұйымдастыру + New bookmark Жаңа бетбелгі + Rotate pages clockwise Беттерді сағат тілінің бағытымен бұру + Rotate pages counterclockwise Беттерді сағат тіліне қарсы бұру + Delete pages Беттерді жою + Insert blank page Бос бет енгізу + Rotate pages… Беттерді бұру… + Duplicate pages Беттерді көшірмелеу + Crop pages Беттерді қиып алу + Set page boxes… Бет жәшіктерін орнату… + Insert pages from file… Файлдан беттер енгізу… + Replace pages… Беттерді ауыстыру… + Extract pages… Беттерді шығарып алу… + Split document… Құжатты бөлу… + Number pages… Беттерді нөмірлеу… + Keyboard shortcuts Пернетақта тіркесімдері + Join the ArtCraft Discord ArtCraft Discord-ына қосылыңыз + PdfCraft web page PdfCraft веб-сайты + PdfCraft on GitHub GitHub-тағы PdfCraft + ArtCraft website ArtCraft веб-сайты + Check for updates… Жаңартуларды тексеру… + About PdfCraft PdfCraft туралы + Export a PDF PDF экспорттау + Export to Мынаған экспорттау + Microsoft Word (.docx) Microsoft Word (.docx) + Spreadsheet (.xlsx) Электрондық кесте (.xlsx) + Presentation (.pptx) Презентация (.pptx) + Image (PNG) Кескін (PNG) + Export all images Барлық кескіндерді экспорттау + HTML web page HTML веб-беті + Rich Text Format (.rtf) Бай мәтін пішімі (.rtf) + Text (plain) Мәтін (қарапайым) + PostScript / EPS PostScript / EPS + Fill & Sign Толтыру және қол қою + Fill Толтыру + Checkmark Дәлбелгі + Cross Крест + Dot Нүкте + Line Сызық + Date Күн + Signature Қолтаңба + Initials Бас әріптер + Edit a PDF PDF түзету + Modify page Бетті өзгерту + Rotate pages Беттерді бұру + Insert pages Беттерді енгізу + Extract pages Беттерді шығарып алу + Add content Мазмұн қосу + Text Мәтін + Image Кескін + Header and footer Жоғарғы және төменгі колонтитулдар + Watermark Су белгісі + Link Сілтеме + Bates numbering Бейтс нөмірлеу + Background Фон + Attach file Файлды тіркеу + Create a PDF PDF жасау + Create from Мынадан жасау + Single file Бір файл + Multiple files Бірнеше файл + Images Кескіндер + Clipboard Алмасу буфері + Blank page Бос бет + Combine files Файлдарды біріктіру + Combine Біріктіру + Add files to combine Біріктіруге файлдар қосу + Page options Бет параметрлері + Page grid Бет торы + Rotate Бұру + Delete Жою + Extract Шығарып алу + Insert Енгізу + Replace Ауыстыру + Split Бөлу + More Қосымша + Set page boxes Бет шектерін орнату + Page labels Бет белгілері + Page transitions Бет ауысуы + Add comments Түсініктеме қосу + Markup Белгілеу + Sticky note Жапсырма жазба + Underline Астын сызу + Strikethrough Сызып тастау + Squiggly underline Толқынды астын сызу + Text box Мәтін жәшігі + Drawing Сурет салу + Rectangle Тіктөртбұрыш + Oval Сопақ + Arrow Жебе + Cloud Бұлт + Stamp Мөр + Review Пікір алмасу + Comment list Түсініктемелер тізімі + Scan & OCR Сканерлеу және OCR + Recognize text Мәтінді тану + In this file Осы файлда + In multiple files Бірнеше файлда + Enhance scanned file Сканерленген файлды жақсарту + Correct recognized text Танылған мәтінді түзету + Protect a PDF PDF қорғау + Protect Қорғау + Protect with password Құпиясөзбен қорғау + Remove hidden information Жасырын ақпаратты алып тастау + Advanced options Кеңейтілген параметрлер + Encrypt with certificate Сертификатпен шифрлау + Security properties Қауіпсіздік сипаттары + Redact a PDF PDF мазмұнын жасыру + Redact Жасыру + Redact pages Беттерді жасыру + Find text and redact Мәтінді тауып жасыру + Set properties Сипаттарды орнату + Sanitize document Құжаттан құпия деректерді тазарту + Compress a PDF PDF сығу + Optimize Оңтайландыру + Reduce file size Файл өлшемін кішірейту + Advanced optimization Кеңейтілген оңтайландыру + Audit space usage Кеңістікті пайдалануды тексеру + Field list Өрістер тізімі + Field properties Өріс сипаттары + Tab order by rows Жолдар бойынша табуляция реті + Tab order by columns Бағандар бойынша табуляция реті + Import data Деректерді импорттау + Export data Деректерді экспорттау + Add form components Форма компоненттерін қосу + Text field Мәтін өрісі + Checkbox Құсбелгі + Radio button Ауыстырғыш + Drop-down list Ашылмалы тізім + List box Тізім + Button Түйме + Image field Кескін өрісі + Date field Күн өрісі + Digital signature Цифрлық қолтаңба + Stamps Мөрлер + Stamp palette Мөрлер палитрасы + Use a certificate Сертификатты пайдалану + Certificates Сертификаттар + Timestamp Уақыт белгісі + Use print production Басып шығаруға дайындауды пайдалану + Print production Басып шығаруға дайындау + Output preview Шығысты алдын ала көру + Preflight Басу алдындағы тексеру + Convert colors Түстерді түрлендіру + Add printer marks Баспа белгілерін қосу + Fix hairlines Жіңішке сызықтарды түзету + Measure objects Нысандарды өлшеу + Measure Өлшеу + Measuring tool Өлшеу құралы + Geospatial location Геокеңістіктік орналасу + Compare files Файлдарды салыстыру + Select files to compare Салыстыру үшін файлдарды таңдау + Use guided actions Қадамдық әрекеттерді пайдалану + Actions list Әрекеттер тізімі + Action Wizard Әрекеттер шебері + Make accessible Қолжетімді ету + Prepare for distribution Таратуға дайындау + Optimize scanned documents Сканерленген құжаттарды оңтайландыру + Archive documents Құжаттарды мұрағаттау + Prepare for accessibility Қолжетімділікке дайындау + Accessibility Қолжетімділік + Automatically tag PDF PDF-ке тегтерді автоматты қосу + Change reading options Оқу параметрлерін өзгерту + Check for accessibility Қолжетімділікті тексеру + Add alternate text Балама мәтін қосу + Fix reading order Оқу ретін түзету + Apply PDF standards PDF стандарттарын қолдану + Standards Стандарттар + Save as PDF/A PDF/A ретінде сақтау + Save as PDF/X PDF/X ретінде сақтау + Save as PDF/UA PDF/UA ретінде сақтау + Add search index Іздеу индексін қосу + Index Индекс + Manage embedded index Ендірілген индексті басқару + Use JavaScript JavaScript-ті пайдалану + JavaScript JavaScript + Console Консоль + Document JavaScripts Құжаттың JavaScript-тері + Document actions Құжат әрекеттері + AI assistant ЖИ көмекшісі + Bring your own model (off by default) Өз моделіңізді пайдаланыңыз (әдепкіде өшірулі) + Summarize Түйіндеу + Ask about this document Осы құжат туралы сұрау + Translate Аудару + pages бет + Planned Жоспарланған + Planned for milestone Кезеңге жоспарланған + open to see what it will include ашып, нені қамтитынын көріңіз + Optional: needs an AI provider you configure Міндетті емес: сіз теңшейтін ЖИ провайдері қажет + Coming in milestone Кезеңде пайда болады + Items marked Ready work today. «Дайын» деп белгіленген элементтер бүгін-ақ жұмыс істейді. + Preview Алдын ала көру + Edit fields Өрістерді түзету + Try the form as people filling it in will see it Форманы толтырушылар көретіндей көріністе сынап көріңіз + Redact all Барлығын жасыру + Clear all Барлығын тазарту + Rotate page Бетті бұру + Delete page Бетті жою + Move page Бетті жылжыту + Move pages Беттерді жылжыту + Duplicate page Бетті көшіру + Replace page Бетті ауыстыру + Replace pages Беттерді ауыстыру + Crop page Бетті қиып алу + Cancel Бас тарту + Don't save Сақтамау + Apply Қолдану + Run Орындау + Clear Тазарту + Sanitize Құпия деректерді тазарту + Mark all Барлығын белгілеу + Save changes to “{name}” before closing? «{name}» ішіндегі өзгерістерді жабу алдында сақтау керек пе? + Your changes will be lost if you don't save them. Сақтамасаңыз, өзгерістеріңіз жоғалады. + Password required Құпиясөз қажет + “{name}” is protected. Enter a password to open it. «{name}» қорғалған. Ашу үшін құпиясөзді енгізіңіз. + Password Құпиясөз + Incorrect password. Try again. Құпиясөз дұрыс емес. Қайталап көріңіз. + Recognize Text Мәтінді тану + Password protection will be applied when you save Құпиясөзбен қорғау сақтау кезінде қолданылады + Saved the print-ready PDF to {path} Басып шығаруға дайын PDF {path} жолына сақталды + Could not save: {e} Сақтау мүмкін болмады: {e} + Earlier in tab order Табуляция реті бойынша алдыңғы + Later in tab order Табуляция реті бойынша кейінгі + From: Бастап: + To: Дейін: + High Resolution Жоғары ажыратымдылық + Print Басып шығару + Verify to see what the document needs. Құжатқа не қажет екенін көру үшін тексеріңіз. + Set Page Boxes Бет шектерін орнату + Signature panel Қолтаңба панелі + {month} {day}, {y} {day} {month} {y} + The document's security settings don't allow filling in form fields Құжаттың қауіпсіздік параметрлері форма өрістерін толтыруға рұқсат бермейді + {name} is read-only {name} — тек оқуға арналған + {name} has no action {name} әрекеті жоқ + Previous month Алдыңғы ай + Next month Келесі ай + January Қаңтар + February Ақпан + March Наурыз + April Сәуір + May Мамыр + June Маусым + July Шілде + August Тамыз + September Қыркүйек + October Қазан + November Қараша + December Желтоқсан + Done Дайын + This certificate is in your list of trusted certificates. Бұл сертификат сіздің сенімді сертификаттар тізіміңізде бар. + This certificate is trusted through a certificate above it in the chain. Бұл сертификат тізбектегі жоғары тұрған сертификат арқылы сенімді саналады. + This certificate is not trusted. Signatures made with it show an unknown identity. Бұл сертификат сенімді емес. Онымен жасалған қолтаңбалар белгісіз тұлғаны көрсетеді. + Add to Trusted Certificates Сенімді сертификаттарға қосу + Export… Экспорттау… + PDF Optimizer PDF оңтайландырғышы + Discard Objects Нысандарды алып тастау + Discard User Data Пайдаланушы деректерін алып тастау + Clean Up Тазалау + Color Images Түсті кескіндер + Grayscale Images Сұр реңкті кескіндер + Bicubic downsampling to Бикубтық төмендету + for images above мұнан асатын кескіндер үшін + Compression Сығымдау + ZIP ZIP + Retain existing Бар тұрғанын сақтау + Minimum Минимум + Low Төмен + High Жоғары + Maximum Максимум + Each image is measured where pages draw it; an image is replaced only if the result is smaller. Әр кескін беттер оны сызатын жерде өлшенеді; кескін нәтиже кіші болғанда ғана ауыстырылады. + Discard all links, actions and JavaScript Барлық сілтемелерді, әрекеттерді және JavaScript-ті алып тастау + Discard alternate images Балама кескіндерді алып тастау + Discard embedded page thumbnails Ендірілген бет миниатюраларын алып тастау + Discard document tags Құжат тегтерін алып тастау + Discard embedded print settings Ендірілген басып шығару параметрлерін алып тастау + Discard bookmarks Бетбелгілерді алып тастау + Discard hidden layer content Жасырын қабат мазмұнын алып тастау + Discard all comments, forms and multimedia Барлық түсініктемелерді, формаларды және мультимедияны алып тастау + Discard document information and metadata Құжат туралы мәліметтерді және метадеректерді алып тастау + Discard all object data (file attachments) Барлық нысан деректерін (файл тіркемелерін) алып тастау + Discard private data of other applications Басқа қосымшалардың жеке деректерін алып тастау + Discard hidden text Жасырын мәтінді алып тастау + Use Flate to encode streams that are not encoded Кодталмаған ағындарды Flate көмегімен кодтау + Remove invalid links and bookmarks Жарамсыз сілтемелерді және бетбелгілерді алып тастау + Remove unreferenced named destinations Сілтеме берілмеген атаулы межелерді алып тастау + Compress document structure (object streams) Құжат құрылымын сығу (нысан ағындары) + Remove unused objects and merge identical ones Пайдаланылмаған нысандарды алып тастау және ұқсастарын біріктіру + Audit space usage… Кеңістікті пайдалануды тексеру… + Space Audit Кеңістікті пайдалану аудиті + Bytes Байт + Content Streams Мазмұн ағындары + X Object Forms XObject формалары + Structure Info Құрылым ақпараты + Thumbnails Миниатюралар + Named Destinations Атауланған мақсаттар + Color Spaces Түс кеңістіктері + Shadings Градиенттер + Patterns (graphics objects) Үлгілер (графикалық нысандар) + Extended Graphics States Кеңейтілген графикалық күйлер + Embedded Files Ендірілген файлдар + Document Overhead Құжаттың қосалқы деректері + Total Барлығы + Text Field Properties Мәтін өрісінің сипаттары + Check Box Properties Құсбелгінің сипаттары + Radio Button Properties Радио түймесінің сипаттары + Dropdown Properties Ашылмалы тізімнің сипаттары + List Box Properties Тізім жәшігінің сипаттары + Button Properties Түйменің сипаттары + Digital Signature Properties Цифрлық қолтаңбаның сипаттары + Position Орналасу + Options Параметрлер + Validate Тексеру + Calculate Есептеу + Actions Әрекеттер + This field is locked. Unlock it to change its properties. Бұл өріс құлыпталған. Сипаттарын өзгерту үшін оның құлпын ашыңыз. + Tooltip: Қалтқы: + Common Properties Жалпы сипаттар + Read Only Тек оқуға арналған + Required Міндетті + Font Size: Қаріп өлшемі: + Auto Авто + Text Color: Мәтін түсі: + Borders and Colors Жиектер мен түстер + Border Color: Жиек түсі: + Fill Color: Толтыру түсі: + No color Түс жоқ + Beveled Қиғаш + Left: Сол жақ: + Bottom: Төмен: + Width: Ені: + Height: Биіктігі: + Points from the page's bottom-left corner. Беттің төменгі сол жақ бұрышынан (пункттермен). + Alignment: Туралау: + Default Value: Әдепкі мәні: + Multi-line Көпжолды + Scroll long text Ұзын мәтінді айналдыру + Allow Rich Text Formatting Бай мәтін пішімдеуге рұқсат беру + Limit of Шегі: + characters таңба + Field is used for file selection Өріс файл таңдауға қолданылады + Check spelling Емлені тексеру + Comb of Ұяшықтар саны: + Export Value: {on} Экспорт мәні: {on} + Check box is checked by default Құсбелгі әдепкі бойынша белгіленген + Button is checked by default Түйме әдепкі бойынша таңдалады + Buttons with the same name and value are selected in unison Атауы мен мәні бірдей түймелер бірге таңдалады + Item: Элемент: + Item List Элементтер тізімі + Up Жоғары + Delete {o} {o} жою + Add the choices people pick from. Пайдаланушылар таңдайтын нұсқаларды қосыңыз. + Default: Әдепкі: + (none) (жоқ) + Sort items Элементтерді сұрыптау + Allow user to enter custom text Пайдаланушыға өз мәтінін енгізуге рұқсат беру + Multiple selection Көп таңдау + Commit selected value immediately Таңдалған мәнді бірден қолдану + Number Сан + Percentage Пайыз + Time Уақыт + Special Арнайы + Select format category: Пішім санатын таңдаңыз: + The value is shown as typed. Мән енгізілген күйінде көрсетіледі. + Decimal places: Ондық таңба саны: + Separator style: Бөлгіш стилі: + Currency symbol: Валюта таңбасы: + Before the number Сан алдында + Negative number style: Теріс сандар стилі: + Format: Пішім: + Custom: Өзіндік: + Zip Code Пошталық индекс + Zip Code + 4 Пошталық индекс + 4 + Phone Number Телефон нөмірі + Social Security Number Әлеуметтік қамсыздандыру нөмірі + Arbitrary Mask Еркін маска + Mask: Маска: + 9 digit, A letter, O letter or digit, X any character 9 — сан, A — әріп, O — әріп немесе сан, X — кез келген таңба + Example: {sample} Мысал: {sample} + Field value is not validated Өріс мәні тексерілмейді + Field value is in range: Өріс мәні келесі аралықта: + Add an Action Әрекет қосу + Select Trigger: Триггер таңдаңыз: + Mouse Up Тышқан батырмасын босату + Mouse Down Тышқан батырмасын басу + Mouse Enter Курсор кіруі + Mouse Exit Курсор шығуы + On Focus Фокус алу + On Blur Фокус жоғалту + Select Action: Әрекетті таңдаңыз: + Open a web link Веб-сілтемені ашу + Reset a form Форманы бастапқы күйге келтіру + Execute a menu item Мәзір пунктін орындау + Show a field Өрісті көрсету + Hide a field Өрісті жасыру + Submit a form Форманы жіберу + Fields to reset (comma-separated; empty: all) Бастапқы күйге келтірілетін өрістер (үтірмен бөлінген; бос: барлығы) + Print, NextPage, PrevPage, FirstPage or LastPage Print, NextPage, PrevPage, FirstPage немесе LastPage + Field names (comma-separated) Өріс атаулары (үтірмен бөлінген) + URL to submit to Жіберу үшін URL + Script: Скрипт: + Value: Мәні: + No actions Әрекеттер жоқ + Value is not calculated Мән есептелмейді + Value is the Мән — + sum (+) қосындысы (+) + product (x) көбейтіндісі (x) + average орташа + minimum минимум + maximum максимум + of the following fields: келесі өрістердің: + Simplified field notation: Жеңілдетілген өріс жазылуы: + Field names with + - * / and parentheses; put \\ before spaces in names. Құрамында + - * / және жақшалар бар өріс атаулары; атаудағы бос орынның алдында \\ қойыңыз. + Format text Мәтінді пішімдеу + Bold Жуан + Italic Көлбеу + Align left Солға туралау + Centre Ортаға туралау + Align right Оңға туралау + Justify Екі жаққа туралау + Standard fonts; text outside Windows-1252 isn't supported yet. Стандартты қаріптер; Windows-1252-ден тыс мәтін әзірге қолдау таппайды. + Edit image Суретті түзету + Rotate counterclockwise Сағат тілінің бағытына қарсы бұру + Rotate clockwise Сағат тілінің бағытымен бұру + Flip horizontal Жатыққа аудару + Flip vertical Тікке аудару + Replace image Суретті ауыстыру + Crop (% trimmed from each side) Қиып алу (әр жақтан %) + Click on the page to add text; each click starts a new box, and ✓ finishes. Drag items to move them, drag a corner to resize, double-click text to edit it. Мәтін қосу үшін бетті шертіңіз; әр шерту жаңа жәшік бастайды, ✓ аяқтайды. Элементтерді жылжыту үшін сүйреңіз, өлшемін өзгерту үшін бұрышынан тартыңыз, мәтінді түзету үшін қос шертіңіз. + Choose an image Суретті таңдаңыз + Export to Image Суретке экспорттау + Export to Text Мәтінге экспорттау + Export All Images Барлық суреттерді экспорттау + Resolution Ажыратымдылық + Format Пішім + Quality Сапа + One {f} file per page, named after the document. Бет сайын бір {f} файлы, құжаттың атауымен аталады. + Exclude images smaller than Мынадан кіші суреттерді қоспау + No limit Шектеу жоқ + Each image once, named after the document and page. JPEG images are saved unchanged; others as PNG. Әр сурет бір рет, құжат пен бет атауымен аталады. JPEG суреттері өзгеріссіз сақталады; қалғандары PNG болып сақталады. + Plain text in reading order; pages are separated by form feeds. Оқылу ретімен қарапайым мәтін; беттер бет ауыстыру таңбасымен бөлінеді. + Create {what} {what} жасау + initials инициалдар + signature қолтаңба + Create Custom Stamp Өзіндік мөр жасау + From {file} {file} файлынан + Category: Санаты: + e.g. My stamps мысалы, Менің мөрлерім + Name: Атауы: + Custom Өзіндік + Delete stamp Мөрді жою + Create custom stamp… Өзіндік мөр жасау… + From a PDF page or an image PDF бетінен немесе суреттен + Line spacing Жол аралығы + Character spacing Таңба аралығы + Horizontal scale Жатық масштаб + Rotate Clockwise Сағат тілінің бағытымен бұру + Rotate Counterclockwise Сағат тілінің бағытына қарсы бұру + Flip Horizontal Жатыққа аудару + Flip Vertical Тікке аудару + Replace Image… Суретті ауыстыру… + Save Image As… Суретті басқаша сақтау… + Check for updates Жаңартуларды тексеру + No check has run yet. Тексеру әлі жүргізілген жоқ. + Checking for a newer version… Жаңа нұсқа бар ма екені тексерілуде… + PdfCraft {v} is available. PdfCraft {v} қолжетімді. + You have version {c}. Download the new version from its release page. Сізде {c} нұсқасы бар. Жаңа нұсқаны шығарылым бетінен жүктеп алыңыз. + PdfCraft {c} is up to date. PdfCraft {c} нұсқасы ең соңғы. + Couldn't check for updates: {e} Жаңартуларды тексеру мүмкін болмады: {e} + You have version {c}. All releases are listed at {page}. Сізде {c} нұсқасы бар. Барлық шығарылымдар {page} бетінде көрсетілген. + Asks GitHub for the latest release. Nothing is downloaded or installed automatically. GitHub-тан соңғы шығарылымды сұрайды. Ештеңе автоматты түрде жүктелмейді немесе орнатылмайды. + Download Жүктеп алу + Later Кейінірек + none жоқ + Declared conformance: {shown} Жарияланған сәйкестік: {shown} + Output intent: {list} Шығыс профилі: {list} + Conformance level: Сәйкестік деңгейі: + No problems found: the document conforms to {level}. Мәселер табылмады: құжат {level} деңгейіне сәйкес. + (page {p}) (бет {p}) + · not fixable here · мұнда түзетуге болмайды + Verify Тексеру + Box Аумақ + Units Өлшем бірліктері + Set to zero Нөлге орнату + Current page ({n}) Ағымдағы бет ({n}) + Subset Ішкі жиын + Update Жаңарту + Add Қосу + {verb} Header and Footer Колонтитулдарды {verb} + {verb} Watermark Су белгісін {verb} + {verb} Background Фонды {verb} + Margins (in) Шеттер (дюйм) + Left Header Text Сол жақ үстіңгі колонтитул мәтіні + Center Header Text Ортаңғы үстіңгі колонтитул мәтіні + Right Header Text Оң жақ үстіңгі колонтитул мәтіні + Left Footer Text Сол жақ астыңғы колонтитул мәтіні + Center Footer Text Ортаңғы астыңғы колонтитул мәтіні + Right Footer Text Оң жақ астыңғы колонтитул мәтіні + Insert Page Number Бет нөмірін енгізу + Insert Date Күнді енгізу + Start page number Бастапқы бет нөмірі + Source Дереккөз + Fit to page Бетке сыйдыру + Rotation Бұрылу + Appear behind page Беттің артында көріну + Appear on top of page Беттің үстінде көріну + From colour Түстен + Page Range Options Беттер ауқымының параметрлері + Preview: page {p} of {n} Алдын ала қарау: {n} беттің {p}-сі + Applies to 1 page 1 бетке қолданылады + Applies to {n} pages {n} бетке қолданылады + Replaces the existing one on those pages. Осы беттерде бар элементті ауыстырады. + PDF or image PDF немесе сурет + No file chosen Файл таңдалмады + Page number Бет нөмірі + Scale relative to target page Мақсат бетіне қатысты масштаб + Link Properties Сілтеменің сипаттары + Create Link Сілтеме жасау + Link Appearance Сілтеменің көрінісі + Link Type: Сілтеме түрі: + Visible Rectangle Көрінетін тіктөртбұрыш + Invisible Rectangle Көрінбейтін тіктөртбұрыш + Highlight Style: Ерекшелеу стилі: + Invert Терістеу + Outline Контур + Inset Шегініс + Line Thickness: Сызық жуандығы: + Thin Жұқа + Thick Қалың + Line Style: Сызық стилі: + Solid Тұтас + Dashed Үзік + Color: Түс: + Link Action Сілтеме әрекеті + Open a web page Веб-бетті ашу + Go to a page view Бет көрінісіне өту + Type the web address. Веб-мекенжайын енгізіңіз. + This document has no signatures. Бұл құжатта қолтаңбалар жоқ. + The Keychain's digital IDs couldn't be listed: {e} Кілттер бауындағы цифрлық ID-лерді тізімдеу мүмкін болмады: {e} + Signed and saved to {path} Қол қойылып, {path} жолына сақталды + Couldn't open revision {n}: {e} {n} нұсқасын ашу мүмкін болмады: {e} + Couldn't open the signed version: {e} Қол қойылған нұсқаны ашу мүмкін болмады: {e} + Sign with a Digital ID Цифрлық ID арқылы қол қою + Choose the digital ID that you want to use for signing: Қол қою үшін пайдаланғыңыз келетін цифрлық ID-ді таңдаңыз: + Keychain · Кілттер бауы · + Windows store · Windows қоймасы · + Issued by: Шығарушы: + , Expires: , Жарамдылық мерзімі: + No digital IDs yet. Әзірге цифрлық ID жоқ. + Configure New Digital ID Жаңа цифрлық ID теңшеу + Continue Жалғастыру + Configure a Digital ID for Signing Қол қоюға арналған цифрлық ID теңшеу + Use a Digital ID from a file Файлдағы цифрлық ID-ді пайдалану + Create a new Digital ID (self-signed, saved to a password-protected file) Жаңа цифрлық ID жасау (өз-өзіне қол қойылған, құпиясөзбен қорғалған файлға сақталады) + Name Аты-жөні + Organizational Unit Ұйымдық бөлімше + Organization Name Ұйым атауы + Email Address Электрондық пошта мекенжайы + Country/Region Ел/Өңір + Key Algorithm Кілт алгоритмі + Confirm Password Құпиясөзді растау + Valid for 5 years, for digital signatures. Self-signed IDs are usually not trusted by others. 5 жылға жарамды, цифрлық қолтаңбалар үшін. Өз-өзіне қол қойылған ID-лерге әдетте басқалар сенбейді. + File (.p12, .pfx) Файл (.p12, .pfx) + Digital ID Цифрлық ID + Browse… Шолу… + {verb} as "{name}" «{name}» ретінде {verb} + Certify Куәландыру + The signature will be invisible. Қолтаңба көрінбейтін болады. + Distinguished name Ажырататын атау + Labels Белгілер + Permitted actions after certifying Куәландырғаннан кейінгі рұқсат етілген әрекеттер + No changes allowed Ешқандай өзгеріс рұқсат етілмейді + Annotations, form fill-in, and digital signatures Белгілеулер, форманы толтыру және цифрлық қолтаңбалар + Form fill-in and digital signatures Форманы толтыру және цифрлық қолтаңбалар + The key is in the macOS Keychain, which may ask to allow PdfCraft to use it. Кілт macOS Keychain ішінде; ол PdfCraft-ге оны пайдалануға рұқсат сұрауы мүмкін. + The key is in the Windows certificate store, which may ask to allow PdfCraft to use it. Кілт Windows сертификаттар қоймасында; ол PdfCraft-ге оны пайдалануға рұқсат сұрауы мүмкін. + Digital ID password Цифрлық ID құпиясөзі + Back Артқа + Validate all Барлығын тексеру + Sign this field… Бұл өріске қол қою… + Signature is valid: Қолтаңба жарамды: + Signature validity is UNKNOWN: Қолтаңбаның жарамдылығы БЕЛГІСІЗ: + Signature is INVALID: Қолтаңба ЖАРАМСЫЗ: + Signature Details Қолтаңба мәліметтері + Reason Себеп + Signing time Қол қою уақыты + Algorithm Алгоритм + Signer Қол қоюшы + Issued by Шығарушы + Valid Жарамды + Field Өріс + Field: {field} on page {p} Өріс: {field}, {p}-бетте + Add to trusted certificates Сенімді сертификаттарға қосу + View signed version Қол қойылған нұсқаны көру + Show certificate… Сертификатты көрсету… + Export certificate… Сертификатты экспорттау… + Certificate Viewer Сертификатты көру + This dialog shows the details of a certificate and its chain. Бұл терезе сертификат және оның тізбегі туралы мәліметтерді көрсетеді. + Trust Сенім + Issued to Кімге берілген + Valid from Жарамдылық басталуы + Valid to Жарамдылық аяқталуы + Intended usage Пайдалану мақсаты + Any Кез келген + Version Нұсқа + Serial number Сериялық нөмір + Issuer Шығарушы + Validity starts Жарамдылық басы + Validity ends Жарамдылық соңы + Public key Ашық кілт + Key usage Кілтті пайдалану + Not present Жоқ + Self-signed Өзіне-өзі қол қойылған + Edit Action Әрекетті түзету + New Action Жаңа әрекет + Action name: Әрекет атауы: + Description: Сипаттама: + Steps Қадамдар + Add the steps this action runs, in order. Бұл әрекет орындайтын қадамдарды ретімен қосыңыз. + Add Step Қадам қосу + An action with this name already exists. Осындай атаумен әрекет әлдеқашан бар. + New Action… Жаңа әрекет… + Edit… Түзету… + Add watermark Су белгісін қосу + Add header Жоғарғы колонтитулды қосу + Add footer Төменгі колонтитулды қосу + Set document title Құжат тақырыбын орнату + Run a JavaScript JavaScript орындау + Prepare for Distribution Таратуға дайындау + Remove hidden information and flatten comments and form fields, so recipients see only the pages. Жасырын ақпаратты жойып, түсініктемелер мен форма өрістерін жазықтайды, сонда алушылар тек беттерді көреді. + Optimize Scanned Documents Сканерленген құжаттарды оңтайландыру + Make scans searchable, then make them smaller. Сканерленгендерді іздеп табуға болатындай етіп, содан кейін өлшемін кішірейтеді. + Add Page Numbers Бет нөмірлерін қосу + Number every page in the footer. Әр бетті төменгі колонтитулда нөмірлейді. + Mark as Confidential Құпия деп белгілеу + A diagonal CONFIDENTIAL watermark on every page. Әр беттегі көлбеу CONFIDENTIAL су белгісі. + Sanitize Documents Құжаттарды тазарту + Remove hidden information, metadata, scripts and embedded content, and rewrite the files. Жасырын ақпаратты, метадеректерді, скрипттер мен енгізілген мазмұнды жойып, файлдарды қайта жазады. + Type text Мәтін теру + Text to insert Енгізілетін мәтін + Replacement text Ауыстыру мәтіні + Edit text… Мәтінді түзету… + Add a sticky note here Осында жабысқақ жазба қосу + {kind} Properties {kind} сипаттары + Appearance Сыртқы түрі + General Жалпы + Review History Қарау тарихы + This comment's appearance can't be changed yet. Бұл түсініктеменің сыртқы түрін әзірге өзгертуге болмайды. + Icon Таңбаша + Thickness Жуандық + Modified Өзгертілген + No status has been set. Күй орнатылмаған. + Compare Files Файлдарды салыстыру + Old file: Ескі файл: + Open the older version first Алдымен ескірек нұсқаны ашыңыз + New file: Жаңа файл: + Text is compared word by word; the differences are shown on the new file. Мәтін сөзбе-сөз салыстырылады; айырмашылықтар жаңа файлда көрсетіледі. + Compare this file with an older version: Compare files… in All tools. Бұл файлды ескірек нұсқасымен салыстырыңыз: «Барлық құралдар» ішіндегі «Файлдарды салыстыру…». + No differences in the text. Мәтіндегі айырмашылық жоқ. + {n} {kind} {n} {kind} + Inserted Енгізілген + Deleted Жойылған + Replaced Ауыстырылған + Visual Көрнекі + Mark as comments Түсініктемелер деп белгілеу + Report… Есеп… + {kind} · page {p} {kind} · {p}-бет + Set Alternate Text Балама мәтінді орнату + This document has no tagged figures. Бұл құжатта тегтелген суреттер жоқ. + on page {p} {p}-бетте + Figure {i} of {n}{page} Сурет {i}, барлығы {n}{page} + No preview Алдын ала қарау жоқ + Alternate text Балама мәтін + Describe the figure Суретті сипаттаңыз + Decorative figure Декоративті сурет + Previous figure Алдыңғы сурет + Next figure Келесі сурет + Save & Close Сақтау және жабу + Accessibility Checker Options Қолжетімділікті тексеру параметрлері + Report Options Есеп параметрлері + Create accessibility report Қолжетімділік есебін жасау + Page Range Беттер ауқымы + All pages in document Құжаттағы барлық беттер + Checking Options ({on} of 32 in all categories) Тексеру параметрлері (барлық санаттар бойынша 32-ден {on}) + Show this dialog when the Checker starts Тексеру басталғанда бұл терезені көрсету + Select All Барлығын таңдау + Clear All Барлығын тазалау + Start Checking Тексеруді бастау + This document hasn't been checked yet. Бұл құжат әлі тексерілмеген. + No issues found Мәселе табылмады + 1 issue 1 мәселе + {n} issues {n} мәселе + {label} (1 issue) {label} (1 мәселе) + {label} ({n} issues) {label} ({n} мәселе) + Check again Қайта тексеру + Report Есеп + Show the accessibility report Қолжетімділік есебін көрсету + Collapse Жинау + Expand Жаю + {rule} - {status} {rule} - {status} + Passed Сәтті + Failed Сәтсіз + Needs manual check Қолмен тексеру қажет + Skipped Өткізілді + Fix Жөндеу + Skip Rule Ережені өткізіп жіберу + Explain Түсіндіру + Check Again Қайта тексеру + Show Report Есепті көрсету + Options… Параметрлер… + Go to the page Бетке өту + Page Content Бет мазмұны + Forms Формалар + Alternate Text Балама мәтін + Tables Кестелер + Lists Тізімдер + Headings Тақырыптар + Accessibility permission flag Қолжетімділік рұқсатының жалаушасы + Accessibility permission flag is set Қолжетімділік рұқсатының жалаушасы орнатылған + Security settings must allow assistive technology to read the content ("Enable text access for screen reader devices"). Change the document's security to allow it. Қауіпсіздік параметрлері көмекші технологияларға мазмұнды оқуға рұқсат беруі тиіс («Экраннан оқу құралдары үшін мәтінге қолжетімділікті қосу»). Мұны рұқсат ету үшін құжаттың қауіпсіздігін өзгертіңіз. + Image-only PDF Тек суреттерден тұратын PDF + Document is not image-only PDF Құжат тек суреттерден тұратын PDF емес + Pages that are only pictures of text can't be read aloud or searched. Recognize text (OCR) to add a text layer. Мәтіннің суреті ғана болып табылатын беттерді дауыстап оқуға немесе іздеуге болмайды. Мәтін қабатын қосу үшін мәтінді тануыңыз (OCR). + Document is tagged PDF Құжат — тегтелген PDF + Tags give the document a logical structure that assistive technology follows. Tag the document (Automatically tag PDF). Тегтер құжатқа көмекші технологиялар ұстанатын логикалық құрылым береді. Құжатты тегтеңіз («PDF-ті автоматты түрде тегтеу»). + Logical Reading Order Логикалық оқу реті + Document structure provides a logical reading order Құжат құрылымы логикалық оқу ретін қамтамасыз етеді + Check that the tags read in the order a sighted reader would. Use the Reading Order tool or the Tags panel to correct it. Тегтер көретін оқырман оқитын ретпен оқылатынын тексеріңіз. Түзету үшін «Оқу реті» құралын немесе «Тегтер» панелін пайдаланыңыз. + Primary language Негізгі тіл + Text language is specified Мәтін тілі көрсетілген + Screen readers choose their voice from the document's language. Set it in Document Properties > Advanced > Language. Экраннан оқу құралдары дауысты құжаттың тілі бойынша таңдайды. Оны «Құжат сипаттары > Қосымша > Тіл» бөлімінде орнатыңыз. + Document title is showing in title bar Құжат тақырыбы атау жолағында көрсетіледі + The window should show a meaningful title rather than the file name. Set a title and Initial View > Show: Document Title. Терезе файл атауының орнына мазмұнды тақырыпты көрсетуі тиіс. Тақырыпты орнатып, «Бастапқы көрініс > Көрсету: Құжат тақырыбы» параметрін белгілеңіз. + Bookmarks are present in large documents Үлкен құжаттарда бетбелгілер бар + Documents of more than 20 pages need bookmarks for navigation. Add bookmarks for the main sections. 20 беттен асатын құжаттарда навигация үшін бетбелгілер қажет. Негізгі бөлімдерге бетбелгілер қосыңыз. + Color contrast Түс контрасты + Document has appropriate color contrast Құжатта тиісті түс контрасты бар + Text must contrast enough with its background for people with low vision. Check the colours by eye or with a contrast tool. Мәтін нашар көретін адамдар үшін фонмен жеткілікті түрде контрасттануы тиіс. Түстерді көзбен немесе контраст құралымен тексеріңіз. + Tagged content Тегтелген мазмұн + All page content is tagged Барлық бет мазмұны тегтелген + Everything on a page must be tagged or marked as an artifact (decoration, headers, footers). Tag the untagged content. Беттегінің бәрі тегтелген немесе артефакт (әшекей, колонтитулдар) ретінде белгіленуі тиіс. Тегтелмеген мазмұнды тегтеңіз. + Tagged annotations Тегтелген белгілеулер + All annotations are tagged Барлық белгілеулер тегтелген + Comments, links and fields must be in the tag tree to be announced. Tag the annotations. Түсініктемелер, сілтемелер мен өрістер дауыстап хабарлануы үшін тег ағашында болуы тиіс. Белгілеулерді тегтеңіз. + Tab order Tab реті + Tab order is consistent with structure order Tab реті құрылым ретіне сәйкес + Pages with links, comments or fields should tab in structure order. The fix sets each page's tab order to Use Document Structure. Сілтемелері, түсініктемелері немесе өрістері бар беттер құрылым ретімен табуляциялануы тиіс. Түзету әр беттің Tab ретін «Құжат құрылымын пайдалану» деп орнатады. + Character encoding Таңбаларды кодтау + Reliable character encoding is provided Сенімді таңбаларды кодтау қамтамасыз етілген + Fonts must map their characters to Unicode so text can be read and copied. Fix the fonts or recreate the PDF. Қаріптер таңбаларын Unicode-қа сәйкестендіруі тиіс, сонда мәтінді оқуға және көшіруге болады. Қаріптерді жөндеңіз немесе PDF-ті қайта жасаңыз. + Tagged multimedia Тегтелген мультимедиа + All multimedia objects are tagged Барлық мультимедиа нысандары тегтелген + Video, sound and rich media must be tagged to be announced. Tag the multimedia annotations. Видео, дыбыс және интерактивті медиа дауыстап хабарлануы үшін тегтелуі тиіс. Мультимедиа белгілеулерін тегтеңіз. + Screen flicker Экранның жыпылықтауы + Page will not cause screen flicker Бет экранның жыпылықтауын тудырмайды + Animation that flashes can cause seizures. Check scripts and media for flicker. Жыпылықтаған анимация құрыс ұстамаларын тудыруы мүмкін. Скрипттер мен медианы жыпылықтауға тексеріңіз. + Scripts Скрипттер + No inaccessible scripts Қолжетімсіз скрипттер жоқ + Timed responses Уақытпен шектеулі жауаптар + Page does not require timed responses Бет уақытпен шектеулі жауаптарды талап етпейді + People may need more time to respond. Check that nothing has a time limit. Адамдарға жауап беру үшін қосымша уақыт қажет болуы мүмкін. Уақыт шектеулері жоқ екенін тексеріңіз. + Navigation links Навигация сілтемелері + Navigation links are not repetitive Навигация сілтемелері қайталанбайды + Repeated navigation links should be skippable. Check links that appear on every page. Қайталанатын навигация сілтемелерін өткізіп жіберуге болатындай болуы тиіс. Әр бетте көрінетін сілтемелерді тексеріңіз. + Tagged form fields Тегтелген форма өрістері + All form fields are tagged Барлық форма өрістері тегтелген + Form fields must be in the tag tree (Form tags) to be announced. Tag the form fields. Форма өрістері дауыстап хабарлануы үшін тег ағашында (Form тегтерінде) болуы тиіс. Форма өрістерін тегтеңіз. + Field descriptions Өріс сипаттамалары + All form fields have description Барлық форма өрістерінде сипаттама бар + Each field needs a tooltip that says what to enter. Add one in Field Properties > General > Tooltip. Әр өріске не енгізу керектігін айтатын қалқымалы кеңес қажет. Оны «Өріс сипаттары > Жалпы > Қалқымалы кеңес» бөлімінде қосыңыз. + Figures alternate text Суреттердің балама мәтіні + Figures require alternate text Суреттерге балама мәтін қажет + Pictures need alternate text that describes them, or must be marked decorative. Add alternate text to each figure. Суреттерге оларды сипаттайтын балама мәтін қажет немесе декоративті деп белгіленуі тиіс. Әр суретке балама мәтін қосыңыз. + Nested alternate text Кірістірілген балама мәтін + Alternate text that will never be read Ешқашан оқылмайтын балама мәтін + Alternate text inside an element that already has alternate text is never read. Remove the inner alternate text. Балама мәтіні бар элементтің ішіндегі балама мәтін ешқашан оқылмайды. Ішкі балама мәтінді алып тастаңыз. + Associated with content Мазмұнмен байланысқан + Alternate text must be associated with some content Балама мәтін белгілі бір мазмұнмен байланысуы тиіс + Alternate text on an element without page content is never reached. Remove it or attach the element to content. Бет мазмұны жоқ элементтегі балама мәтін ешқашан дауыстап оқылмайды. Оны алып тастаңыз немесе элементті мазмұнға байланыстырыңыз. + Hides annotation Белгілеуді жасырады + Alternate text should not hide annotation Балама мәтін белгілеуді жасырмауы тиіс + Alternate text on an element that contains a link or field hides that annotation from assistive technology. Move the alternate text. Сілтеме немесе өріс бар элементтегі балама мәтін бұл белгілеуді көмекші технологиялардан жасырады. Балама мәтінді жылжытыңыз. + Other elements alternate text Басқа элементтердің балама мәтіні + Other elements that require alternate text Балама мәтінді қажет ететін басқа элементтер + Formulas and similar non-text elements need alternate text. Add alternate text to them. Формулалар мен ұқсас мәтінге жатпайтын элементтерге балама мәтін қажет. Оларға балама мәтін қосыңыз. + Rows Жолдар + TR must be a child of Table, THead, TBody, or TFoot TR Table, THead, TBody немесе TFoot ішінде болуы тиіс + Table cells belong in a row. Fix the table's tags. Кесте ұяшықтары жолда болуы тиіс. Кестенің тегтерін жөндеңіз. + Headers Тақырыптар + Tables should have headers Кестелерде тақырыптар болуы тиіс + Header cells (TH) tell listeners what each column or row holds. Mark the header cells. Тақырып ұяшықтары (TH) тыңдаушыға әр бағанның немесе жолдың нені сақтайтынын айтады. Тақырып ұяшықтарын белгілеңіз. + Regularity Біркелкілік + Tables must contain the same number of columns in each row and rows in each column Кестелердің әр жолындағы бағандар саны мен әр бағанындағы жолдар саны бірдей болуы тиіс + Irregular tables confuse cell navigation. Give every row the same number of cells (counting spans). Біркелкі емес кестелер ұяшықтар арасындағы навигацияны шатастырады. Әр жолдағы ұяшықтар санын бірдей етіңіз (біріктіруді есептегенде). + Summary Түйіндеме + Tables must have a summary Кестелерде түйіндеме болуы тиіс + A summary describes the table's purpose and layout. Add a Summary attribute to each table. Түйіндеме кестенің мақсаты мен құрылысын сипаттайды. Әр кестеге Summary атрибутын қосыңыз. + List items Тізім элементтері + LI must be a child of L LI L ішінде болуы тиіс + List items belong in a list. Fix the list's tags. Тізім элементтері тізімде болуы тиіс. Тізімнің тегтерін жөндеңіз. + Lbl and LBody Lbl және LBody + Lbl and LBody must be children of LI Lbl мен LBody LI ішінде болуы тиіс + List labels and bodies belong in a list item. Fix the list's tags. Тізім белгілері мен денелері тізім элементінде болуы тиіс. Тізімнің тегтерін жөндеңіз. + Appropriate nesting Тиісті кірістірілу + Heading levels shouldn't skip (an H1 followed by an H3). Retag the headings. Тақырып деңгейлері үзілмеуі тиіс (H1-ден кейін H3 келмеуі керек). Тақырыптарды қайта тегтеңіз. + TH and TD TH және TD + TH and TD must be children of TR TH мен TD TR ішінде болуы тиіс + Mark Page Range Беттер ауқымын белгілеу + Pages from Беттер бастап + Single word or phrase Жалғыз сөз немесе тіркес + Text to find Ізделетін мәтін + Patterns Үлгілер + Phone Numbers Телефон нөмірлері + Email Addresses Электрондық пошта адрестері + Credit Cards Несиелік карталар + Social Security Numbers Әлеуметтік сақтандыру нөмірлері + Dates Күндер + Text in images isn't found; recognise text (OCR) first. Суреттегі мәтін табылмайды; алдымен мәтінді тануыңыз (OCR). + No matches. Сәйкестік жоқ. + {n} match(es) marked for redaction. {n} сәйкестік жасыруға белгіленді. + Redaction Tool Properties Жасыру құралының сипаттары + Redacted area fill colour: Жасырылған аймақтың толтыру түсі: + Use overlay text Үстеме мәтінді пайдалану + Custom text: Жеке мәтін: + Font: Қаріп: + Font size: Қаріп өлшемі: + Auto-size text to fit redaction region Мәтін өлшемін жасыру аймағына автоматты бейімдеу + Font colour: Қаріп түсі: + Repeat overlay text Үстеме мәтінді қайталау + Text alignment: Мәтін туралануы: + Center Ортаға + Apply redactions Жасыруларды қолдану + You are about to apply 1 redaction mark. Text, images and drawings under the marks, and comments and form fields that overlap them, are removed permanently. Сіз қазір 1 жасыру белгісін қолданасыз. Белгілердің астындағы мәтін, кескіндер мен сызбалар, сондай-ақ олармен қабаттасқан түсініктемелер мен форма өрістері мәңгілікке жойылады. + You are about to apply {n} redaction marks. Text, images and drawings under the marks, and comments and form fields that overlap them, are removed permanently. Сіз қазір {n} жасыру белгісін қолданасыз. Белгілердің астындағы мәтін, кескіндер мен сызбалар, сондай-ақ олармен қабаттасқан түсініктемелер мен форма өрістері мәңгілікке жойылады. + Save the document afterwards: saving rewrites the whole file so no trace of the removed content stays in it. Содан кейін құжатты сақтаңыз: сақтау бүкіл файлды қайта жазады, сондықтан жойылған мазмұнның ізі онда қалмайды. + Select the items to remove from this document. Осы құжаттан алып тастайтын элементтерді таңдаңыз. + Metadata Метадеректер + File attachments Файл тіркемелері + Comments and markups Түсініктемелер мен белгілеулер + Hidden text Жасырын мәтін + Hidden layers Жасырын қабаттар + Links, actions and JavaScripts Сілтемелер, әрекеттер және JavaScript сценарийлері + Private data of other applications Басқа қосымшалардың жеке деректері + None found Ештеңе табылмады + Form fields are flattened: their values stay visible. Saving rewrites the whole file. Форма өрістері жазықталады: мәндері көрініп тұра береді. Сақтау бүкіл файлды қайта жазады. + Remove Алып тастау + Sanitizing removes hidden information from the document: metadata, file attachments, comments, form fields (flattened), hidden text and layers, bookmarks, links, actions and scripts, and private application data. Тазарту құжаттан жасырын ақпаратты жояды: метадеректер, файл тіркемелері, түсініктемелер, форма өрістері (жазықталады), жасырын мәтін мен қабаттар, бетбелгілер, сілтемелер, әрекеттер мен сценарийлер, сондай-ақ қосымшаның жеке деректері. + Save the document afterwards; saving rewrites the whole file so nothing removed stays in it. Содан кейін құжатты сақтаңыз; сақтау бүкіл файлды қайта жазады, сондықтан жойылған ешнәрсе онда қалмайды. + Printer: Принтер: + Save as PDF PDF ретінде сақтау + {name} (default) {name} (әдепкі) + Copies: Даналар: + Collate Даналар бойынша реттеу + Print in grayscale Сұр түспен басып шығару + Two-sided: Екі жақты: + Off Өшірулі + Flip on long edge Ұзын жиек бойынша аудару + Flip on short edge Қысқа жиек бойынша аудару + Paper: Қағаз: + Pages to Print Басып шығарылатын беттер + More options: Қосымша параметрлер: + All pages in range Диапазондағы барлық беттер + Odd pages only Тек тақ беттер + Even pages only Тек жұп беттер + Reverse pages Беттерді кері ретпен + Page Sizing & Handling Бет өлшемі мен өңдеуі + Size Өлшем + Poster Плакат + Multiple Бірнеше бет + Booklet Буклет + Fit Сыйдыру + Shrink oversized pages Тым үлкен беттерді кішірейту + Custom scale: Арнайы масштаб: + Tile scale: Фрагменттердің масштабы: + Overlap: Қабаттасу: + Cut marks Кесу белгілері + Pages per sheet: Бір парақтағы бет саны: + Page order: Беттердің реті: + Horizontal Көлденең + Horizontal reversed Көлденең (кері ретпен) + Vertical Тік + Vertical reversed Тік (кері ретпен) + Print page border Бет жиегін басып шығару + Auto-rotate pages Беттерді автоматты бұру + Booklet subset: Буклеттің бөлігі: + Both sides Екі жақ та + Front side only Тек алдыңғы жағы + Back side only Тек артқы жағы + Binding: Тігу жағы: + Left Сол жақ + Right Оң жақ + Orientation: Бағдар: + Auto portrait/landscape Автоматты тік/жатық + Portrait Тік + Landscape Жатық + Comments & Forms Түсініктемелер мен формалар + Document Құжат + Document and markups Құжат және белгілеулер + Document and stamps Құжат және мөрлер + Form fields only Тек форма өрістері + Previous sheet Алдыңғы парақ + Next sheet Келесі парақ + No sheets Парақтар жоқ + Sheet {s} of {n} Парақ {s} / {n} + Removed 1 hidden item. Save to remove them from the file. 1 жасырын элемент жойылды. Оны файлдан алып тастау үшін сақтаңыз. + Removed {n} hidden items. Save to remove them from the file. {n} жасырын элемент жойылды. Оларды файлдан алып тастау үшін сақтаңыз. + Document sanitized. Save to finish: saving rewrites the whole file. Құжат тазартылды. Аяқтау үшін сақтаңыз: сақтау бүкіл файлды қайта жазады. + Applied 1 redaction mark. Save to remove the content from the file. 1 жасыру белгісі қолданылды. Мазмұнды файлдан алып тастау үшін сақтаңыз. + Applied {n} redaction marks. Save to remove the content from the file. {n} жасыру белгісі қолданылды. Мазмұнды файлдан алып тастау үшін сақтаңыз. + JavaScript Console JavaScript консолі + JavaScript is turned off (Preferences ▸ JavaScript). JavaScript өшірулі (Параметрлер > JavaScript). + Output appears here. Шығыс осында пайда болады. + Script Name: Сценарийдің атауы: + This document has no document-level scripts. Бұл құжатта құжат деңгейіндегі сценарийлер жоқ. + No form fields were detected Форма өрістері анықталмады + Detected 1 form field 1 форма өрісі анықталды + Detected {n} form fields {n} форма өрісі анықталды + {field}: {e} {field}: {e} + Current page Ағымдағы бет + Selected pages Таңдалған беттер + Document language Құжат тілі + Output Шығыс + Searchable Image (Exact) Іздеуге болатын сурет (дәл) + Adds invisible text over each word; the page image is not changed Әр сөздің үстіне көрінбейтін мәтін қосады; бет суреті өзгермейді + Downsample to Ажыратымдылықты төмендету + Settings Параметрлер + Optional Міндетті емес + Planned · {m} Жоспарланған · {m} + Planned for milestone {m} — open to see what it will include {m} кезеңіне жоспарланған — не қамтитынын көру үшін ашыңыз + Coming in milestone {m}. Items marked Ready work today. {m} кезеңінде шығады. «Дайын» деп белгіленген элементтер қазір жұмыс істейді. + 1 mark 1 белгі + {n} marks {n} белгі + Dynamic Динамикалық + Sign Here Осында қол қойыңыз + Standard Business Стандартты іскерлік + Choose a stamp, then click on the page to place it. Мөрді таңдап, оны орналастыру үшін бетті шертіңіз. + Note Жазба + Polyline Сынық сызық + Attachment Тіркеме + Callout Көрсетпе + Typewriter Баспа машинасы + Checked Белгіленген + Unchecked Белгіленбеген + Marked Ерекшеленген + Locked Құлыпталған + Search comments Түсініктемелерді іздеу + Add a comment Түсініктеме қосу + No comments yet. Әзірге түсініктемелер жоқ. + Pick a tool in the quick bar, or type above. Жылдам тақтадан құрал таңдаңыз немесе жоғарыда енгізіңіз. + No comments match. Сәйкес түсініктемелер жоқ. + Unknown author Белгісіз автор + No colour Түсі жоқ + Remove checkmark Белгіні алып тастау + Mark with checkmark Белгі қою + Reply Жауап беру + Add a reply Жауап қосу + Post Жариялау + Page {label} Бет {label} + {kind} — click to show it on the page {kind} — бетте көрсету үшін шертіңіз + Set status Күйді орнату + Copy text Мәтінді көшіру + Properties… Сипаттар… + Make Current Properties Default Ағымдағы сипаттарды әдепкі етіп орнату + More options Қосымша параметрлер + Sort by Бойынша сұрыптау + Author Автор + Type Түрі + Colour Түс + Show all comments Барлық түсініктемелерді көрсету + Create PDF comment summary… Түсініктемелердің PDF қорытындысын жасау… + Export all to data file… Барлығын деректер файлына экспорттау… + Filter comments Түсініктемелерді сүзгілеу + Status Күй + Accepted Қабылданды + Rejected Қабылданбады + Cancelled Бас тартылды + Completed Аяқталды + Show all Барлығын көрсету + Yellow Сары + Orange Қызғылт сары + Red Қызыл + Pink Қызғылт + Purple Күлгін + Blue Көк + Light blue Ашық көк + Green Жасыл + Gray Сұр + Black Қара + Bookmark options Бетбелгі параметрлері + Expand all bookmarks Барлық бетбелгілерді жаю + Expand top-level bookmarks Жоғарғы деңгей бетбелгілерін жаю + Collapse all bookmarks Барлық бетбелгілерді жию + This document has no bookmarks. Бұл құжатта бетбелгілер жоқ. + This document has no layers. Бұл құжатта қабаттар жоқ. + This document has no attachments. Бұл құжатта тіркемелер жоқ. + Hide layer Қабатты жасыру + Show layer Қабатты көрсету + Set layer visibility Қабаттың көрінісін орнату + on page {label} {label} бетінде + Save attachment… Тіркемені сақтау… + Open in a new tab Жаңа қойындыда ашу + This document has no pages. Бұл құжатта беттер жоқ. + Find text Мәтінді іздеу + No matches Сәйкестік жоқ + Searching… {s}/{p} Ізделуде… {s}/{p} + {c} of {n}{more} {n} ішінен {c}{more} + Find options Іздеу параметрлері + Whole words only Тек толық сөздер + Case-sensitive Регистрді ескеру + Close (Esc) Жабу (Esc) + Align Туралау + Top Жоғары + Bottom Төмен + Center horizontally Көлденең бойынша ортаға + Center vertically Тік бойынша ортаға + Distribute Бірдей тарату + Horizontally Көлденең + Vertically Тік + Set Fields to Same Size Өрістерді бірдей өлшемге келтіру + Height Биіктік + Width Ені + Both Екеуі де + Duplicate… Көшірмесін жасау… + All pages Барлық беттер + Odd pages Тақ беттер + Even pages Жұп беттер + Landscape pages Жатық беттер + Portrait pages Тік беттер + Move earlier Алдыңға жылжыту + Move later Кейінге жылжыту + Back to the document Құжатқа оралу + Protect Using Password Құпиясөз арқылы қорғау + Requires user to enter a password for: Пайдаланушыдан келесі үшін құпиясөз енгізуді талап етеді: + Viewing Көру + Editing Түзету + Type Password Құпиясөзді енгізіңіз + Re-type Password Құпиясөзді қайта енгізіңіз + Strength: {s} Беріктігі: {s} + Strong Күшті + Medium Орташа + Weak Әлсіз + Advanced Options {chevron} Кеңейтілген параметрлер {chevron} + Compatibility Үйлесімділік + Acrobat X and later (256-bit AES) Acrobat X және кейінгілері (256-бит AES) + Acrobat 7.0 and later (128-bit AES) Acrobat 7.0 және кейінгілері (128-бит AES) + Acrobat 5.0 and later (128-bit RC4) Acrobat 5.0 және кейінгілері (128-бит RC4) + Acrobat 3.0 and later (40-bit RC4) Acrobat 3.0 және кейінгілері (40-бит RC4) + Encrypt Шифрлау + All document contents Құжаттың барлық мазмұны + All document contents except metadata Метадеректерден басқа құжаттың барлық мазмұны + Printing allowed Басып шығаруға рұқсат етілген + Low Resolution (150 dpi) Төмен ажыратымдылық (150 dpi) + Changes allowed Өзгертулерге рұқсат етілген + Inserting, deleting, and rotating pages Беттерді енгізу, жою және бұру + Filling in form fields and signing existing signature fields Форма өрістерін толтыру және бар қолтаңба өрістеріне қол қою + Commenting, filling in form fields, and signing existing signature fields Түсініктеме жазу, форма өрістерін толтыру және бар қолтаңба өрістеріне қол қою + Any except extracting pages Беттерді шығарып алудан басқа барлығы + Enable copying of text, images, and other content Мәтін, кескіндер және басқа да мазмұнды көшіруге рұқсат беру + Enable text access for screen reader devices for the visually impaired Көру қабілеті шектеулі адамдарға арналған экраннан оқу құрылғыларына мәтінге қатынасуға рұқсат беру + Type a password. Құпиясөзді енгізіңіз. + The passwords don't match. Құпиясөздер сәйкес емес. + This compatibility level supports only plain ASCII passwords. Бұл үйлесімділік деңгейі тек қарапайым ASCII құпиясөздерін қолдайды. + Anyone opening the document will need this password. It's applied when you save. Құжатты ашатын кез келген адамға осы құпиясөз қажет болады. Ол сақтау кезінде қолданылады. + The document opens without a password; this password is needed to change the restrictions. It's applied when you save. Құжат құпиясөзсіз ашылады; шектеулерді өзгерту үшін осы құпиясөз керек. Ол сақтау кезінде қолданылады. + Recover unsaved documents? Сақталмаған құжаттарды қалпына келтіру керек пе? + PdfCraft didn't shut down normally. These documents had changes that were autosaved: PdfCraft қалыпты жабылмады. Осы құжаттардың өзгерістері автосақталды: + just now жаңа ғана + {n} min ago {n} минут бұрын + {n} h ago {n} сағат бұрын + · password-protected · құпиясөзбен қорғалған + Recover Қалпына келтіру + Discard Елемеу + Version {v} Нұсқа {v} + A clean-room, open-source PDF application written in Rust. MIT OR Apache-2.0. Rust тілінде жазылған, clean-room әдісімен әзірленген ашық бастапқы кодты PDF қосымшасы. MIT OR Apache-2.0. + Part of Бөлігі + Summarize Options Қорытындылау параметрлері + Choose a layout: Макетті таңдаңыз: + Comments only Тек түсініктемелер + 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}» бұрын сақталған нұсқасына оралу керек пе? Осы уақыттан кейінгі өзгерістерді кейін болдырмауға болмайды. + Number pages Беттерді нөмірлеу + Style Стиль + None (prefix only) Жоқ (тек префикс) + Prefix Префикс + Start Басталуы + Labels: {preview}. Later pages keep their labels. Белгілер: {preview}. Кейінгі беттер өз белгілерін сақтайды. + Even and Odd Pages Жұп және тақ беттер + Even Pages Only Тек жұп беттер + Odd Pages Only Тек тақ беттер + Landscape and Portrait Pages Жатық және тік беттер + Landscape Pages Жатық беттер + Portrait Pages Тік беттер + Duplicate Field Өрістің көшірмесін жасау + Duplicate "{name}" onto: «{name}» көшірмесін келесіге жасау: + 1 page selected. 1 бет таңдалды. + {n} pages selected. {n} бет таңдалды. + Delete pages after extracting Шығарып алғаннан кейін беттерді жою + Extract pages as separate files Беттерді бөлек файлдар ретінде шығарып алу + Rotate Pages Беттерді бұру + Direction: Бағыт: + Clockwise 90 degrees Сағат тілінің бағытымен 90 градус + Counterclockwise 90 degrees Сағат тілінің бағытына қарсы 90 градус + 180 degrees 180 градус + Pages: Беттер: + All Барлығы + Selection Таңдалғандар + From Бастап + Rotate: Бұру: + Repairs Жөндеулер + Repair log Жөндеу журналы + Split document Құжатты бөлу + Number of pages Беттер саны + Pages per file Файлға бет саны + File size Файл өлшемі + At most Көп дегенде + Top-level bookmarks ({n}) Жоғарғы деңгейдегі бетбелгілер ({n}) + Before each selected page (select pages in Organize) Таңдалған әр беттің алдында (беттерді «Реттеу» ішінде таңдаңыз) + Creates 1 file from {n} pages. {n} беттен 1 файл жасалады. + Creates {f} files from {n} pages. {n} беттен {f} файл жасалады. + Each file holds as many of the {n} pages as fit. Әр файлға {n} беттің сыйғаны жазылады. + Replace Pages Беттерді ауыстыру + Original: replace pages Түпнұсқа: ауыстырылатын беттер + to дейін + to {n} {n} дейін + Replacement: pages of {name} Ауыстырғыш: {name} беттері + of {name} {name} ішінен + Only the page content changes: links, comments, form fields and bookmarks on the original pages stay. Тек бет мазмұны өзгереді: түпнұсқа беттердегі сілтемелер, түсініктемелер, форма өрістері мен бетбелгілер сақталады. + The replacement file doesn't have that many pages. Ауыстырғыш файлда соншалық бет жоқ. + Tagged PDF Белгіленген PDF + Document Properties Құжат сипаттары + Description Сипаттама + Initial View Бастапқы көрініс + Security Қауіпсіздік + Fonts Қаріптер + Advanced Қосымша + Title Атауы + Subject Тақырыбы + Keywords Кілт сөздер + Application Қолданба + PDF producer PDF өндірушісі + Layout and Magnification Орналасу және масштаб + Navigation tab Навигация қойындысы + Page Only Тек бет + Bookmarks Panel and Page Бетбелгілер панелі және бет + Pages Panel and Page Беттер панелі және бет + Attachments Panel and Page Тіркемелер панелі және бет + Layers Panel and Page Қабаттар панелі және бет + Page layout Бет орналасуы + Default Әдепкі + Single Page Бір бет + Single Page Continuous Бір бет, үздіксіз + Two-Up (Facing) Екі бет (қарама-қарсы) + Two-Up Continuous (Facing) Екі бет, үздіксіз (қарама-қарсы) + Two-Up (Cover Page) Екі бет (мұқаба бетімен) + Two-Up Continuous (Cover Page) Екі бет, үздіксіз (мұқаба бетімен) + Magnification Масштаб + Actual Size Нақты өлшем + Fit Page Бетке сыйдыру + Fit Width Еніне сыйдыру + Fit Height Биіктігіне сыйдыру + Fit Visible Көрінетін аймаққа сыйдыру + Open to page Бетке ашу + of {pages} барлығы {pages} + Window Options Терезе параметрлері + Resize window to initial page Терезені бастапқы бетке сай өлшемдеу + Center window on screen Терезені экран ортасына орналастыру + Open in Full Screen mode Толық экран режимінде ашу + Show: Көрсету: + File Name Файл атауы + Document Title Құжат атауы + User Interface Options Пайдаланушы интерфейсі параметрлері + Hide menu bar Мәзір жолағын жасыру + Hide toolbars Құралдар тақталарын жасыру + Hide window controls Терезе басқару элементтерін жасыру + Security method Қауіпсіздік әдісі + No security Қауіпсіздік жоқ + Restrictions Шектеулер + None — everything is allowed Жоқ — бәріне рұқсат етіледі + Password security Құпиясөзбен қорғау + Encryption Шифрлау + Opened with Мынамен ашылады + — (protection is applied when you save) — (қорғау сақтау кезінде қолданылады) + Owner password (no restrictions apply) Иелік құпиясөзі (шектеулер қолданылмайды) + User password Пайдаланушы құпиясөзі + Change settings… Параметрлерді өзгерту… + Printing Басып шығару + Not allowed Рұқсат етілмеген + Allowed Рұқсат етілген + High resolution Жоғары ажыратымдылық + Low resolution Төмен ажыратымдылық + Changing the document Құжатты өзгерту + Document assembly Құжаттарды біріктіру + Content copying Мазмұнды көшіру + Content copying for accessibility Мазмұнды қолжетімділік үшін көшіру + Commenting Түсініктемелер қалдыру + Filling of form fields Форма өрістерін толтыру + No fonts are referenced by the pages. Беттер ешбір қаріпке сілтеме жасамайды. + PDF version PDF нұсқасы + Location Орналасқан жері + Page size Бет өлшемі + Yes Иә + No Жоқ + Revisions Нұсқалар + View revision {n} {n} нұсқаны көру + Open the file as it was saved then Файлды сол сақтау сәтіндегі күйінде ашу + Binding Тігілуі + Left Edge Сол жиек + Right Edge Оң жиек + Language Тіл + Saved {place}: {before} → {after} ({pct}% smaller){detail} Сақталды {place}: {before} → {after} ({pct}%-ға кіші){detail} + Couldn't save the image: {e} Суретті сақтау мүмкін болмады: {e} + Adding images arrives on the web with file pickers for images Суреттерді қосу веб-нұсқада сурет файлдарын таңдау мүмкіндігімен келеді + Replacing images on page {p} arrives on the web with image pickers ({i}) {p} беттегі суреттерді ауыстыру веб-нұсқада сурет таңдау мүмкіндігімен келеді ({i}) + {name}: choosing images arrives on the web with file pickers for images {name}: суреттерді таңдау веб-нұсқада сурет файлдарын таңдау мүмкіндігімен келеді + Replacing images arrives on the web with image file pickers Суреттерді ауыстыру веб-нұсқада сурет файлдарын таңдау мүмкіндігімен келеді + Couldn't create a PDF: {e} PDF жасау мүмкін болмады: {e} + The clipboard has no image or text Алмасу буферінде сурет те, мәтін де жоқ + On the web, open or drop an image to convert it Веб-нұсқада түрлендіру үшін суретті ашыңыз немесе сүйреп әкеліп тастаңыз + Couldn't optimize the file: {e} Файлды оңтайландыру мүмкін болмады: {e} + Couldn't write {path}: {e} {path} жолына жазу мүмкін болмады: {e} + The form asks to be submitted to {u}; PdfCraft doesn't send form data. Save the document to keep your entries. Форма {u} адресіне жіберілуді сұрайды; PdfCraft форма деректерін жібермейді. Енгізген деректеріңізді сақтап қалу үшін құжатты сақтаңыз. + {s} actions run in the JavaScript engine (M6) {s} әрекет JavaScript қозғалтқышында орындалады (M6) + {name}: the {other} action isn't supported yet {name}: {other} әрекеті әлі қолдау көрсетілмейді + {name} submits the form to {url}; PdfCraft doesn't send form data. Save the document to keep your entries. {name} форманы {url} адресіне жібереді; PdfCraft форма деректерін жібермейді. Енгізген деректеріңізді сақтап қалу үшін құжатты сақтаңыз. + Export stopped: {e} Экспорт тоқтатылды: {e} + Exported 1 image 1 сурет экспортталды + Exported {n} images {n} сурет экспортталды + ({s} not exported: {first}) ({s} сурет экспортталмады, алғашқысы: {first}) + Exported the text of 1 page 1 беттің мәтіні экспортталды + Exported the text of {n} pages {n} беттің мәтіні экспортталды + {msg} to {dir} {msg} → {dir} + Exporting… {d} of {t} Экспортталуда… {d}/{t} + Choose a folder for the exported files Экспортталатын файлдар үшін қалта таңдаңыз + New {tool} comments will look like this one Жаңа «{tool}» түсініктемелері мынадай көрінеді + This document can't be checked: it couldn't be read for editing Бұл құжатты тексеруге болмайды: оны түзету үшін оқу мүмкін болмады + The checker found no problems in this document. Тексеру барысында бұл құжатта ешбір мәселе табылмады. + The checker found 1 problem which may prevent the document from being fully accessible. Тексеру барысында құжаттың толық қолжетімді болуына кедергі келтіруі мүмкін 1 мәселе табылды. + The checker found {n} problems which may prevent the document from being fully accessible. Тексеру барысында құжаттың толық қолжетімді болуына кедергі келтіруі мүмкін {n} мәселе табылды. + The text is the same Мәтін бірдей + 1 difference found 1 айырмашылық табылды + {n} differences found {n} айырмашылық табылды + Added 1 comment 1 түсініктеме қосылды + Added {n} comments {n} түсініктеме қосылды + Choose a folder for the results Нәтижелер үшін қалта таңдаңыз + Files for {name} {name} үшін файлдар + An action is already running Бір әрекет қазіргі уақытта орындалуда + Running action… file {d} of {t} Әрекет орындалуда… файл {d}/{t} + No unlinked web addresses were found in the text Мәтінде сілтемесіз веб-мекенжайлар табылмады + Created 1 link 1 сілтеме жасалды + Created {n} links {n} сілтеме жасалды + Text recognition is already running Мәтінді тану қазіргі уақытта орындалуда + Recognizing text… file {d} of {t} Мәтінді тану… файл {d}/{t} + Recognizing text… page {d} of {t} Мәтінді тану… бет {d}/{t} + Recognized 1 word 1 сөз танылды + Recognized {n} words {n} сөз танылды + {words} on {pages} {pages} бетте {words} сөз + ; 1 page already had text ; 1 бетте мәтін бұрыннан бар еді + ; {n} pages already had text ; {n} бетте мәтін бұрыннан бар еді + Recognized {w} words in 1 file 1 файлда {w} сөз танылды + Recognized {w} words in {n} files {n} файлда {w} сөз танылды + ; failed: {list} ; сәтсіз: {list} + 1 file done 1 файл өңделді + {n} files done {n} файл өңделді + The selected area has been copied ({w} × {h} pixels) Таңдалған аймақ көшірілді ({w} × {h} пиксель) + Couldn't take the snapshot: {e} Суретке түсіру мүмкін болмады: {e} + Couldn't recover {name}: {e} {name} файлын қалпына келтіру мүмкін болмады: {e} + The document now conforms to {level}; save it to keep the changes Құжат енді {level} стандартына сай келеді; өзгерістерді сақтап қалу үшін оны сақтаңыз + Fixed what could be fixed; 1 problem remains Түзетуге болатынының бәрі түзетілді; 1 мәселе қалды + Fixed what could be fixed; {n} problems remain Түзетуге болатынының бәрі түзетілді; {n} мәселе қалды + Custom stamps arrive on the web with file pickers for images Пайдаланушы мөрлері веб-нұсқада сурет файлдарын таңдау мүмкіндігімен келеді + {file} is too large for a stamp (at most {n} MB) {file} файлы мөр үшін тым үлкен (көп дегенде {n} МБ) + Sent to {printer} {printer} принтеріне жіберілді + Sent to {printer}: {msg} {printer} принтеріне жіберілді: {msg} + Choose a folder for the searchable files Іздеуге болатын файлдар үшін қалта таңдаңыз + Files are combined from top to bottom. Leave Pages empty to take every page. Файлдар жоғарыдан төмен қарай біріктіріледі. Барлық бетті алу үшін «Беттер» өрісін бос қалдырыңыз. + Unlock… Блоктан шығару… + Select a password-protected file first Алдымен құпиясөзбен қорғалған файлды таңдаңыз + Enter the permissions password to allow combining Біріктіруге рұқсат ету үшін рұқсаттар құпиясөзін енгізіңіз + Enter the password that opens it Оны ашатын құпиясөзді енгізіңіз + Unlock {name} {name} файлын блоктан шығару + Unlock {n} files {n} файлды блоктан шығару + The same password is tried on each file. Әр файлға сол бір құпиясөз сыналады. + Hide password Құпиясөзді жасыру + Show password Құпиясөзді көрсету + Wrong password Құпиясөз дұрыс емес + The password is only used to read the file; it isn't saved. Құпиясөз файлды оқу үшін ғана қолданылады; ол сақталмайды. + Unlock Блоктан шығару + Opened, but its security settings don't allow combining. Enter the permissions password. Ашылды, бірақ оның қауіпсіздік параметрлері біріктіруге рұқсат бермейді. Рұқсаттар құпиясөзін енгізіңіз. + Unlocked: the combined file won't be password-protected Блоктан шығарылды: біріктірілген файл құпиясөзбен қорғалмайды + Unlocked {n} of {m} files {m} файлдың {n}-і блоктан шығарылды + Drop PDFs or folders here, or choose them. PDF файлдарын немесе қалталарды осында тастаңыз не таңдаңыз. + Add folder… Қалта қосу… + Add folder and subfolders… Қалта мен ішкі қалталарды қосу… + More ways to add files Файлдарды қосудың басқа жолдары + Every PDF in the folder Қалтадағы барлық PDF + Every PDF in the folder and the folders inside it Қалтадағы және оның ішіндегі қалталардағы барлық PDF + Add folder Қалта қосу + Add folder and subfolders Қалта мен ішкі қалталарды қосу + No PDFs found in {folder} {folder} ішінен ешбір PDF табылмады + Added the first {n} PDFs in {folder} {folder} ішіндегі алғашқы {n} PDF қосылды + Click to sort, drag to move the column. Drag its edge to resize it. Сұрыптау үшін шертіңіз, бағанды жылжыту үшін сүйреңіз. Өлшемін өзгерту үшін бағанның шетін сүйреңіз. + Reset columns Бағандарды ысыру + Remove {n} files {n} файлды алып тастау + {n} selected Таңдалды: {n} + sorted ascending өсу ретімен сұрыпталған + sorted descending кему ретімен сұрыпталған + Protected: the combined file won't keep its security settings Қорғалған: біріктірілген файл өз қауіпсіздік параметрлерін сақтамайды + Add open documents Ашық құжаттарды қосу + All open documents Барлық ашық құжаттар + No documents are open Ашық құжаттар жоқ + Select a file first Алдымен файл таңдаңыз + 1 file 1 файл + {n} files {n} файл + 1 file needs attention 1 файл назар аударуды қажет етеді + {n} files need attention {n} файл назар аударуды қажет етеді + {name} can't be combined: {why} {name} біріктіріле алмайды: {why} + Check the pages of {name}: {why} {name} беттерін тексеріңіз: {why} + Add the PDFs to combine Біріктіру үшін PDF файлдарын қосыңыз + File name Файл атауы + Warnings Ескертулер + Drag to reorder Ретін өзгерту үшін сүйреңіз + {k} of {n} {k}/{n} + ? of {n} ?/{n} + Added more than once Бірнеше рет қосылған + yesterday кеше + {n} days ago {n} күн бұрын + No pages selected Беттер таңдалмады + Password-protected Құпиясөзбен қорғалған + Its security settings don't allow copying pages Оның қауіпсіздік параметрлері беттерді көшіруге рұқсат бермейді + Can't be read: {e} Оқу мүмкін емес: {e} + Includes unsaved changes Сақталмаған өзгерістері бар + Pages of {name} to combine, e.g. 1-3, 6 Біріктірілетін {name} беттері, мысалы: 1-3, 6 + Remove {name} {name} файлын алып тастау + Add files… Файлдарды қосу… + Add at least two files Кем дегенде екі файл қосыңыз + Couldn't add {name}: {e} {name} файлын қосу мүмкін болмады: {e} + Couldn't combine files: {e} Файлдарды біріктіру мүмкін болмады: {e} + Export comments Түсініктемелерді экспорттау + Export form data Форма деректерін экспорттау + Comment and form data Түсініктеме және форма деректері + Export Экспорттау + Select data files to merge Біріктіру үшін деректер файлдарын таңдаңыз + Form data and PDF forms Форма деректері және PDF формалары + Save the spreadsheet Электрондық кестені сақтау + Save the accessibility report Қолжетімділік есебін сақтау + Save image Суретті сақтау + Export certificate Сертификатты экспорттау + Choose a folder for the extracted pages Шығарып алынған беттер үшін қалта таңдаңыз + Choose a folder for the split files Бөлінген файлдар үшін қалта таңдаңыз + Couldn't use {name}: {e} {name} файлын пайдалану мүмкін болмады: {e} + Couldn't extract pages: {e} Беттерді шығарып алу мүмкін болмады: {e} + Couldn't write {name}: {e} {name} файлына жазу мүмкін болмады: {e} + Wrote 1 file to {dir} 1 файл {dir} ішіне жазылды + Wrote {n} files to {dir} {n} файл {dir} ішіне жазылды + No pages match those choices Ешбір бет бұл таңдауларға сәйкес келмейді + Couldn't split: {e} Бөлу мүмкін болмады: {e} + Couldn't summarize comments: {e} Түсініктемелер жиынтығын жасау мүмкін болмады: {e} + Couldn't open the result: {e} Нәтижені ашу мүмкін болмады: {e} + Importing data arrives on the web with file pickers for data files Деректерді импорттау веб-нұсқада деректер файлдарын таңдау мүмкіндігімен келеді + Exported to {path} Экспортталды: {path} + Merging data files needs the desktop app Деректер файлдарын біріктіруге жұмыс үстелі қолданбасы қажет + Merged 1 file into {path} 1 файл {path} ішіне біріктірілді + Merged {n} files into {path} {n} файл {path} ішіне біріктірілді + Combined {n} files {n} файл біріктірілді + Extracted 1 page 1 бет шығарып алынды + Extracted {n} pages {n} бет шығарып алынды + Created a comment summary Түсініктемелер жиынтығы жасалды + Couldn't open {name}: {e} {name} файлын ашу мүмкін болмады: {e} + Couldn't read {name}: {e} «{name}» оқылмады: {e} + Couldn't save: {e} Сақтау мүмкін болмады: {e} + Downloading attachments on the web arrives with M3.10 ({n} bytes ready) Тіркемелерді веб-нұсқада жүктеп алу M3.10-мен келеді ({n} байт дайын) + `{command}` {when} `{command}` {when} + ships in milestone {m} {m} кезеңінде шығады + needs an AI provider (off by default) ЖИ провайдерін қажет етеді (әдепкіде өшірулі) + is available қолжетімді + is not available yet әлі қолжетімді емес + {label} failed: {e} «{label}» сәтсіз аяқталды: {e} + Undid {label} Болдырылды: {label} + Redid {label} Қайталанды: {label} + A document needs at least one page: copy instead Құжатқа кемінде бір бет қажет: орнына көшіріңіз + Cut {what} Кесілді: {what} + Copied {what} Көшірілді: {what} + 1 page 1 бет + {n} pages {n} бет + Copy or cut pages first Алдымен беттерді көшіріңіз немесе кесіңіз + Couldn't save {name}: {e} «{name}» сақталмады: {e} + Saved {name} «{name}» сақталды + Saved, but reopening failed: {e} Сақталды, бірақ қайта ашу мүмкін болмады: {e} + Downloaded {name} «{name}» жүктеп алынды + Couldn't download {name}: {e} «{name}» жүктеп алынбады: {e} + Reverted to the last saved version Соңғы сақталған нұсқаға қайтарылды + That didn't work: an internal error stopped it ({m}). Your documents are unchanged. Әрекет сәтсіз аяқталды: оны ішкі қате тоқтатты ({m}). Құжаттарыңыз өзгеріссіз. + Nothing to undo Болдырмауға ештеңе жоқ + Nothing to redo Қайталауға ештеңе жоқ + This document has no form fields you can fill in Бұл құжатта толтыруға болатын форма өрістері жоқ + This document has no comments to flatten Бұл құжатта жазықтауға жататын түсініктемелер жоқ + This document has no form fields to flatten Бұл құжатта жазықтауға жататын форма өрістері жоқ + There are no redaction marks (mark text, areas or pages first) Жасыру белгілері жоқ (алдымен мәтінді, аймақтарды немесе беттерді белгілеңіз) + This document has no {kind} to change Бұл құжатта өзгертуге болатын {kind} жоқ + header or footer үстіңгі/астыңғы колонтитул + watermark су белгісі + background фон + This document isn't password-protected Бұл құжат құпиясөзбен қорғалмаған + Only the document's owner can change its security (open it with the permissions password) Қауіпсіздікті тек құжат иесі өзгерте алады (оны рұқсаттар құпиясөзімен ашыңыз) + The document's security settings don't allow this change Құжаттың қауіпсіздік параметрлері бұл өзгерісті рұқсат етпейді + Open a document first Алдымен құжатты ашыңыз + Security will be removed when you save Сақтаған кезде қауіпсіздік алынып тасталады + Click text or an image to edit it Түзету үшін мәтінге немесе кескінге шертіңіз + Drag a rectangle to create a link; double-click a link to edit it Сілтеме жасау үшін тіктөртбұрыш сүйреңіз; сілтемені түзету үшін оған екі рет шертіңіз + There are no redaction marks to apply Қолданатын жасыру белгілері жоқ + Drag to draw the area where the signature should appear. Қолтаңба пайда болатын аймақты сүйреп сызыңыз. + Couldn't fit the visible content: {e} Көрінетін мазмұнды сыйдыру мүмкін болмады: {e} + Drag a rectangle around the area to copy Көшіретін аймақтың айналасына тіктөртбұрыш сүйреңіз + Click on the page to add a {tool}, or drag to set its size {tool} қосу үшін бетке шертіңіз немесе өлшемін орнату үшін сүйреңіз + Drag a rectangle on a page to crop it; double-click a page for Set Page Boxes Бетті қиып алу үшін оған тіктөртбұрыш сүйреңіз; «Бет аумақтарын орнату» үшін бетке екі рет шертіңіз + Keep tool selected: on Құрал таңдалып тұрсын: қосулы + Keep tool selected Құрал таңдалып тұрсын + Opacity Мөлдірсіздік + Line thickness Сызық жуандығы + What word or phrase would you like to search for? Қандай сөзді немесе тіркесті іздегіңіз келеді? + Searching… {s} of {p} pages Ізделуде… {s}/{p} бет + (searching… {s} of {p} pages) (ізделуде… {s}/{p} бет) + 1 instance 1 сәйкестік + {n} instances {n} сәйкестік + Go to this result Осы нәтижеге өту + Highlight Бояп белгілеу + Draw Салу + Polygon Көпбұрыш + Connected lines Сынық сызық + Eraser Өшіргіш + Sign Қол қою + Certified by {by} Куәландырған: {by} + At least one signature is invalid. Кемінде бір қолтаңба жарамсыз. + Signed and all signatures are valid. Қол қойылды, барлық қолтаңбалар жарамды. + At least one signature has problems. Кемінде бір қолтаңбада мәселелер бар. + This document is secured. Some changes are restricted by its security settings. Бұл құжат қорғалған. Кейбір өзгерістер оның қауіпсіздік параметрлерімен шектеулі. + This is a dynamic XFA form, which PdfCraft can't display yet. What you see is the file's placeholder page. Бұл динамикалық XFA формасы, оны PdfCraft әлі көрсете алмайды. Көріп тұрғаныңыз — файлдың орынбасар беті. + This form also contains XFA data, which PdfCraft doesn't read yet. You can fill its fields, but Acrobat may show the XFA values instead. Бұл формада PdfCraft әлі оқымайтын XFA деректері де бар. Өрістерін толтыра аласыз, бірақ Acrobat орнына XFA мәндерін көрсетуі мүмкін. + This document contains {n} interactive form fields. Бұл құжат {n} интерактивті форма өрісін қамтиды. + This file was damaged and has been repaired. Saving keeps the repaired version. Бұл файл зақымдалған еді және жөнделді. Сақтау кезінде жөнделген нұсқа сақталады. + Dismiss Жабу + Hide field highlights Өрістердің бояп белгілеуін жасыру + Highlight fields Өрістерді бояп белгілеу + Security settings Қауіпсіздік параметрлері + Details Мәліметтер + Select (V) Таңдау (V) + Hand (H) Қол (H) + Delete pages (Delete) Беттерді жою (Delete) + Insert a blank page after the selection Таңдалған беттерден кейін бос бет енгізу + Insert pages from a file… Файлдан беттер енгізу… + Extract pages to a new document Беттерді жаңа құжатқа шығарып алу + Split into files… Файлдарға бөлу… + Select pages Беттерді таңдау + Cut Кесу + Copy Көшіру + Paste after Артына қою + Rename Атын өзгерту + Set to current page ({n}) Ағымдағы бетке орнату ({n}) + Move up Жоғары жылжыту + Move down Төмен жылжыту + Indent Шегіністі арттыру + Outdent Шегіністі азайту + This document has no form fields. Бұл құжатта форма өрістері жоқ. + Tab order: move a field with its arrows. Қойынды реті: өрісті жебелерімен жылжытыңыз. + Unplaced Орналастырылмаған + Has JavaScript actions (run in M6) JavaScript әрекеттері бар (M6-да орындалады) + Page Бет + None Жоқ + Delete selected fields Таңдалған өрістерді жою + Diagnostic details Диагностика мәліметтері + The operation failed. Операция сәтсіз аяқталды. + no such document мұндай құжат жоқ + nothing to undo болдырмауға ештеңе жоқ + nothing to redo қайталауға ештеңе жоқ + this document is signed: rewriting it would invalidate its signatures (save it incrementally instead) бұл құжат қол қойылған: қайта жазу оның қолтаңбаларын жарамсыз етеді (орнына қосымша сақтаңыз) + the document is protected by a password құжат құпиясөзбен қорғалған + the password is incorrect құпиясөз дұрыс емес + the document has no page tree құжатта беттер ағашы жоқ + a document must keep at least one page құжатта кемінде бір бет болуы тиіс + a bookmark cannot be moved inside itself бетбелгіні өз ішіне жылжытуға болмайды + the title must not be empty тақырып бос болмауы тиіс + the document has no form fields құжатта форма өрістері жоқ + there are no redaction marks to apply қолданатын жасыру белгілері жоқ + there are no pages to print басып шығаратын беттер жоқ + the document has no tags құжатта тегтер жоқ + {0}: {1} {0}: {1} + the image is empty кескін бос + this file isn't XFDF, FDF, XML or text form data бұл файл XFDF, FDF, XML немесе мәтіндік форма деректері емес + nothing in the file matches this document файлдағы ешнәрсе бұл құжатқа сәйкес келмейді + This page couldn't be displayed.\n{e} Бұл бет көрсетіле алмады.\n{e} + Go to page {p} {p}-бетке өту + {s} action {s} әрекеті + Page {p} of {n} Бет {p}/{n} + 1 page selected 1 бет таңдалды + {n} pages selected {n} бет таңдалды + {name} — unsaved changes {name} — сақталмаған өзгерістер + {kind} field {kind} өрісі + Select a file for the stamp Мөр үшін файл таңдаңыз + JavaScript, e.g. getField("total").value JavaScript, мысалы getField("total").value + Font: Helvetica Қаріп: Helvetica + e.g. en-US мысалы: kk-KZ + Rendering: hayro (bootstrap) · UI: egui · Icons: Lucide (ISC) · Fonts: Inter, JetBrains Mono, Dancing Script (OFL) Көрсету: hayro (бастапқы) · Интерфейс: egui · Белгішелер: Lucide (ISC) · Қаріптер: Inter, JetBrains Mono, Dancing Script (OFL) + CheckBox Құсбелгі + Radio Радио түймесі + PushButton Түйме + Combo Ашылатын тізім + List Тізім + Unknown Белгісіз + The document has no XMP metadata Құжатта XMP метадеректері жоқ + The title in the metadata and the document information differ Метадеректер мен құжат ақпаратындағы тақырыптар сәйкес келмейді + The document is encrypted Құжат шифрланған + Device colour is used, but there is no PDF/A output intent Құрылғы түсі қолданылады, бірақ PDF/A шығыс профилі жоқ + DeviceCMYK is used: it needs a CMYK output intent (not added automatically) DeviceCMYK қолданылады: CMYK шығыс профилі қажет (автоматты түрде қосылмайды) + The document has additional actions (/AA) Құжатта қосымша әрекеттер бар (/AA) + The document has document-level JavaScript Құжатта құжат деңгейіндегі JavaScript бар + The form asks viewers to make appearances (NeedAppearances) Форма қарау құралдарынан өріс көріністерін құруды сұрайды (NeedAppearances) + A form field has additional actions Форма өрісінде қосымша әрекеттер бар + A stream uses LZW compression Ағын LZW қысуын қолданады + A stream refers to an external file Ағын сыртқы файлға сілтеме жасайды + An image asks to be interpolated Кескін интерполяцияны сұрайды + An image has alternates or OPI information Кескінде баламалар немесе OPI ақпараты бар + PostScript XObjects aren't allowed PostScript XObject нысандарына рұқсат етілмейді + Embedded files must be PDF/A files themselves (use PDF/A-3 for others) Енгізілген файлдардың өздері PDF/A файлдары болуы тиіс (басқалары үшін PDF/A-3 қолданыңыз) + An embedded file has no AFRelationship Енгізілген файлда AFRelationship жоқ + Select Icon Белгішені таңдау + Choose images Кескіндерді таңдаңыз + {dpi} pixels/inch {dpi} пиксель/дюйм + Type your {what}. {what} теріңіз. + Draw your {what} below. Төменде {what} салыңыз. + Your name Атыңыз + Images and text (converted to PDF) Кескіндер мен мәтін (PDF-ке түрлендірілген) + Images and text Кескіндер мен мәтін + Check Box Style: Құсбелгі стилі: + Button Style: Түйме стилі: + Check Тексеру белгісі + Circle Шеңбер + Diamond Ромб + Square Шаршы + Star Жұлдыз + {month} {y} {month} {y} + Change {key} Өзгерту: {key} + Insert pages from {name} Беттерді {name} файлынан енгізу + Fill in {name} {name} толтыру + Set the image of {name} {name} кескінін орнату + Edit script of {name} {name} скриптін түзету + Import {name} {name} импорттау + Add bookmark Бетбелгі қосу + Rename bookmark Бетбелгі атауын өзгерту + Delete bookmark Бетбелгіні жою + Move bookmark Бетбелгіні жылжыту + Set bookmark destination Бетбелгінің баратын орнын орнату + Add stamp Мөр қосу + Delete comment Түсініктемені жою + Edit comment Түсініктемені түзету + Erase Өшіру + Lock comment Түсініктемені құлыптау + Unlock comment Түсініктеменің құлпын ашу + Move comment Түсініктемені жылжыту + Resize comment Түсініктеме өлшемін өзгерту + Change comment properties Түсініктеме сипаттарын өзгерту + Add field Өріс қосу + Change field properties Өріс сипаттарын өзгерту + Delete field Өрісті жою + Duplicate field Өрістің көшірмесін жасау + Set tab order Қойынды ретін орнату + Change initial view Бастапқы көріністі өзгерту + Set alternate text Балама мәтінді орнату + Mark figure as decorative Кескінді декоративті деп белгілеу + Run JavaScript JavaScript орындау + Delete document JavaScript Құжаттың JavaScript-ін жою + Edit document JavaScript Құжаттың JavaScript-ін түзету + Edit text Мәтінді түзету + Move image Кескінді жылжыту + Rotate image Кескінді бұру + Flip image Кескінді аудару + Delete image Кескінді жою + Add header & footer Үстіңгі және астыңғы колонтитулдарды қосу + Update header & footer Үстіңгі және астыңғы колонтитулдарды жаңарту + Update watermark Су белгісін жаңарту + Add background Фон қосу + Update background Фонды жаңарту + Add image Кескін қосу + Edit content Мазмұнды түзету + Delete content Мазмұнды жою + Remove redaction marks Жасыру белгілерін алып тастау + Add link Сілтеме қосу + Change link properties Сілтеме сипаттарын өзгерту + Delete link Сілтемені жою + Create link Сілтеме жасау + Flatten Жазықтау + Add sticky note Жапсырма қосу + Add highlight Бояп белгілеу қосу + Add underline Астын сызу қосу + Add strikethrough Арқылы сызу қосу + Add squiggly underline Толқыншақ астын сызу қосу + Add rectangle Тіктөртбұрыш қосу + Add oval Овал қосу + Add arrow Жебе қосу + Add line Сызық қосу + Add drawing Сурет салу + Add text box Мәтін жәшігін қосу + Add checkmark Белгі қосу + Add cross X белгісін қосу + Add dot Нүкте қосу + Add signature Қолтаңба қосу + Add redaction mark Жасыру белгісін қосу + Add cloud Бұлт қосу + Add polygon Көпбұрыш қосу + Add connected lines Сынық сызық қосу + Add callout Түсіндірме қосу + Add inserted text Енгізілген мәтін қосу + Add file attachment Файл тіркемесін қосу + Set status Accepted Күйді орнату: Қабылданды + Set status Rejected Күйді орнату: Қабылданбады + Set status Cancelled Күйді орнату: Бас тартылды + Set status Completed Күйді орнату: Аяқталды + Set status None Күйді орнату: Жоқ + Set status Unmarked Күйді орнату: Белгіленбеген + Save as PDF/A-1b PDF/A-1b ретінде сақтау + Save as PDF/A-2b PDF/A-2b ретінде сақтау + Save as PDF/A-3b PDF/A-3b ретінде сақтау + Fill & Sign: change signature Толтыру және қол қою: қолтаңбаны өзгерту + Fill & Sign: change initials Толтыру және қол қою: инициалдарды өзгерту + Fill & Sign: remove saved signature Толтыру және қол қою: сақталған қолтаңбаны алып тастау + Fill & Sign: remove saved initials Толтыру және қол қою: сақталған инициалдарды алып тастау + Change signature Қолтаңбаны өзгерту + Change initials Инициалдарды өзгерту + Measure distance Қашықтықты өлшеу + Measure perimeter Периметрді өлшеу + Measure area Ауданды өлшеу + Set measurement scale Өлшеу масштабын орнату + Measurement information Өлшеу ақпараты + Measurement snapping Өлшеуді түйістіру + Export measurements as CSV Өлшеулерді CSV ретінде экспорттау + Distance Қашықтық + Perimeter Периметр + Area Аудан + Drawing scale Сызба масштабы + Snapping Түйістіру + Export measurements Өлшемдерді экспорттау + Open a PDF to measure it. Өлшеу үшін PDF құжатын ашыңыз. + Click vertices. Enter or double-click finishes a perimeter or area. Click the first point to close an area. Шыңдарды шертіңіз. Enter немесе қос шерту периметр мен ауданды аяқтайды. Ауданды жабу үшін бірінші нүктені шертіңіз. + Shift constrains to 45 degrees. Backspace removes a vertex. Escape cancels. Shift бұрышты 45 градусқа шектейді. Backspace шыңды алып тастайды. Escape — бас тарту. + Angle Бұрыш + Click the first point to begin. Бастау үшін бірінші нүктені шертіңіз. + Snap to drawing Сызбаға түйістіру + Endpoints Шеткі нүктелер + Midpoints Орта нүктелер + Intersections Қиылыстар + Paths Контурлар + Sensitivity Сезімталдық + Unit Өлшем бірлігі + Decimals Ондық таңбалар + Calibrate from two points Екі нүктеден калибрлеу + Entire page Толық бет + Viewport rectangle in page points (x, y, width, height) Көрініс аймағының тіктөртбұрышы бет пункттерінде (x, y, ені, биіктігі) + Viewport Көрініс аймағы + Apply scale Масштабты қолдану + Saved measurements Сақталған өлшемдер + Saved measurements. Өлшемдер сақталды. + Endpoint Шеткі нүкте + Midpoint Орта нүкте + Intersection Қиылыс + Path Контур + This drawing exceeds the snapping limit; only the first paths are used. Бұл сызба түйістіру шегінен асады; тек алғашқы контурлар қолданылады. + Some page content couldn't be read; snapping uses the rest. Беттің кейбір мазмұнын оқу мүмкін болмады; түйістіру қалғанын қолданады. + Unsupported measurements Қолдау көрсетілмейтін өлшемдер + Label Белгі + {n} measurements use an unsupported format and are left out. {n} өлшем қолдау көрсетілмейтін пішімде; олар ескерілмейді. + This area has too many overlapping paths; intersection snapping is limited. Бұл аймақта бір-бірін жауып тұрған контурлар тым көп; қиылысқа түйістіру шектеулі. + This measurement needs at least {n} points. Бұл өлшемге кемінде {n} нүкте қажет. + Change {what} Өзгерту: {what} + Cut and stack Кесу және үю + Default workspace mode Әдепкі жұмыс кеңістігі режимі + Documents and view Құжаттар және көрініс + Open email app Email қосымшасын ашу + Open link Сілтемені ашу + Open this web page? Бұл веб-бет ашылсын ба? + Scripts run in a sandbox without file or network access. A script that asks to open a web page needs your permission first. With JavaScript off, Acrobat's standard format, validate and calculate functions still work. Сценарийлер файлдар мен желіге қатынасы жоқ оқшауланған ортада орындалады. Веб-бет ашуды сұрайтын сценарий алдымен сіздің рұқсатыңызды талап етеді. JavaScript өшірілген кезде де Acrobat-тың стандартты форматтау, тексеру және есептеу функциялары жұмыс істейді. + Sign yourself Өзіңіз қол қойыңыз + Used when opening PDFs. An explicit launch or control mode takes precedence for the session. PDF файлдарын ашқанда қолданылады. Сессия үшін нақты көрсетілген іске қосу немесе басқару режимі басымдыққа ие. + Write this email? Бұл email жазылсын ба? + {who} in this document wants to open this address in your browser: Осы құжаттағы {who} бұл адресті браузеріңізде ашқысы келеді: + {who} in this document wants to open your email app with this address: Осы құжаттағы {who} email қосымшаңызды бұл адресмен ашқысы келеді: + This web address mixes letters from different alphabets, a common way to imitate another site's name. Бұл веб-адрес әртүрлі әліпбилердің әріптерін араластырады — бұл басқа сайт атауына еліктеудің жиі кездесетін тәсілі. + This web address uses letters from another alphabet, which can look like familiar ones. Бұл веб-адрес басқа әліпбидің әріптерін қолданады, олар таныс әріптер сияқты көрінуі мүмкін. + Create PDF from images Суреттерден PDF жасау + Selected image files: {count} Таңдалған сурет файлдары: {count} + Use image resolution Сурет ажыратымдылығын қолдану + Use each image's embedded DPI; use 72 DPI when it is absent. Әр суреттің ендірілген DPI мәні қолданылады; болмағанда — 72 DPI. + Use 72 DPI (one point per pixel) 72 DPI қолдану (пиксельге бір пункт) + Use custom DPI Арнайы DPI қолдану + DPI sets the printed page size without resampling the image. DPI пиксельдер санын өзгертпей, басып шығарылатын беттің өлшемін орнатады. + Create Жасау + Dynamic XFA form laid out from its template: {pages}, {fields}. Ұлгісі бойынша құрылған динамикалық XFA формасы: {pages}, {fields}. + XFA form: its fields and its XFA data are kept in step, so other viewers show what you fill in. XFA формасы: өрістері мен XFA деректері өзара синхрондалып тұрады, сондықтан басқа қарау құралдары да сіз енгізген мәндерді көрсетеді. + Switch to two-page view first to show the cover page Мұқабаны көрсету үшін алдымен екі беттік көрініске ауысыңыз + Page display: {layout} Беттерді көрсету: {layout} + Fit to width scrolling Еніне сыйдыра айналдыру + Fit one full page Бір толық бетке сыйдыру + Default page display Әдепкі беттер көрсету + Default zoom Әдепкі масштаб + Used when a PDF doesn't ask for a layout or zoom. Continuous scrolling never snaps between pages. PDF макет пен масштабты талап етпегенде қолданылады. Үздіксіз айналдыру кезінде беттер арасында ешқашан түйістіру жасалмайды. + Choose a PNG or JPEG image (up to 4 MiB). Transparency is preserved. PNG немесе JPEG суретін таңдаңыз (4 MiB-қа дейін). Мөлдірлік сақталады. + Couldn't import the signature image: {e} Қолтаңба суретін импорттау мүмкін болмады: {e} +@plural {n} page|{n} pages {n} бет|{n} бет +@plural {n} field|{n} fields {n} өріс|{n} өріс + Create PDF from multiple files… Бірнеше файлдан PDF жасау… + Optimizing… removing user data Оңтайландыру… пайдаланушы деректерін жою + Optimizing… image {d} of {t} Оңтайландыру… сурет {d} / {t} + Optimizing… cleaning up Оңтайландыру… тазарту + Optimizing… merging identical objects Оңтайландыру… бірдей нысандарды біріктіру + Optimizing… writing the copy Оңтайландыру… көшірмені жазу + Cancelling… Бас тартылуда… + Created a PDF from {n} files {n} файлдан PDF жасалды + PDF, images and text PDF, суреттер және мәтін + Insert a file before page {label} Файлды {label} беттің алдына енгізу + Insert a file at the end Файлды соңына енгізу + Save pages Беттерді сақтау + Save these pages as one PDF Бұл беттерді бір PDF ретінде сақтау + No changes to save Сақтайтын өзгеріс жоқ + Created a PDF from {n} files; left out: {names} {n} файлдан PDF жасалды; қосылмағандары: {names} + Created a PDF from the first {n} files Алғашқы {n} файлдан PDF жасалды + Go to page… Бетке өту… + Optimization is already running Оңтайландыру әлдеқашан орындалуда + Optimization cancelled Оңтайландырудан бас тартылды + Couldn't optimize the file Файлды оңтайландыру мүмкін болмады + Files are combined in this order. Leave Pages empty to take every page. Файлдар осы ретпен біріктіріледі. Барлық бетті алу үшін «Беттер» өрісін бос қалдырыңыз. + Portable mode is off: {folder} can't be written ({error}). Settings are kept in your user folder instead. Портативті режим өшірулі: {folder} ішіне жазу мүмкін емес ({error}). Параметрлер орнына пайдаланушы қалтаңызда сақталады. + Show Көрсету + Sort Сұрыптау + Reverse the order Ретін кері өзгерту + Grab bag Әр түрлі + Table Кесте + Username Пайдаланушы аты + Display name Көрсетілетін аты + Real name Шын аты + Name (A–Z) Аты (А–Я) + Merged PRs Біріктірілген PR-лер + PRs PR-лер + Commits Коммиттер + Lines added Қосылған жолдар + +LOC +LOC + Lines deleted Жойылған жолдар + −LOC −LOC + Line delta Жолдар айырмасы + ΔLOC ΔLOC + Binary assets added Қосылған бинарлы ресурстар + +Bin +Bin + Binary assets removed Жойылған бинарлы ресурстар + −Bin −Bin + First commit Алғашқы коммит + First Алғашқы + Last commit Соңғы коммит + Last Соңғы + No contributor data was built into this copy. Бұл көшірмеге үлес қосушылар туралы деректер ендірілмеген. + No model credits were built into this copy. Бұл көшірмеге модельдер туралы деректер ендірілмеген. + {n} contributors · {c} commits {n} үлес қосушы · {c} коммит + Company Компания + Model Модель + % of all commits Барлық коммиттердің пайызы + Lines +/− Жолдар +/− + @{login}: {prs} PRs, {commits} commits, +{added} / −{deleted} lines (Δ {delta}), +{bin_added} / −{bin_deleted} binary assets, {first} – {last} @{login}: {prs} PR, {commits} коммит, +{added} / −{deleted} жол (Δ {delta}), +{bin_added} / −{bin_deleted} бинарлы ресурс, {first} – {last} + None of those files can be made into a PDF; use PDFs, images or .txt files Осы файлдардың ешбірінен PDF жасауға болмайды; PDF, суреттер немесе .txt файлдарын қолданыңыз + About Туралы + Contributors Үлес қосушылар + Models Модельдер + Only continue if you trust this document. An address can carry information from the document, such as what you typed into its form. Тек осы құжатқа сенетін болсаңыз ғана жалғастырыңыз. Адрес құжаттан ақпарат алып жүруі мүмкін, мысалы формаға енгізгендеріңіз. + Print single-sided. Keep the sheets in order, cut at the marks, then stack the piles left to right, top to bottom. Бір жақты басып шығарыңыз. Парақтардың ретін бұзбай, белгілер бойынша кесіңіз, содан кейін топтамаларды солдан оңға, үстінен астына үйіңіз. +comment menu Edit Түзету + Undo {action} Болдырмау: {action} + Redo {action} Қайталау: {action} +signature pad Type Теру +action wizard Start Бастау +calendar date January Қаңтар +calendar date February Ақпан +calendar date March Наурыз +calendar date April Сәуір +calendar date May Мамыр +calendar date June Маусым +calendar date July Шілде +calendar date August Тамыз +calendar date September Қыркүйек +calendar date October Қазан +calendar date November Қараша +calendar date December Желтоқсан + Reopen the files that were open when PdfCraft last closed PdfCraft соңғы рет жабылғанда ашық болған файлдарды қайта ашу + Clear Recent Files Соңғы файлдарды тазарту + Remove from Recent Соңғылардан алып тастау + Remove {name} from Recent {name} дегенді соңғылардан алып тастау + Inserted text Енгізілген мәтін + Add {what} Қосу: {what} + Use {what} Қолдану: {what} + Place saved {what} Орналастыру: сақталған {what} + Remove saved {what} Алып тастау: сақталған {what} + A link Сілтеме + A button Түйме + A script Сценарий + {who} in this document tried to open an address PdfCraft won't open: {e}. Осы құжаттағы {who} PdfCraft ашпайтын адресті ашпақ болды: {e}. + {who} in this document tried to open an address PdfCraft won't open: {e}. Only web (http, https) and email (mailto) links open from documents. Осы құжаттағы {who} PdfCraft ашпайтын адресті ашпақ болды: {e}. Құжаттардан тек веб (http, https) және email (mailto) сілтемелері ашылады. + Another file dialog is still open. Finish with it first. Басқа файл сұхбат терезесі әлі ашық тұр. Алдымен оны аяқтаңыз. + Couldn't show the file picker. Please try again. Файл таңдау терезесін көрсету мүмкін болмады. Қайталап көріңіз. + The document changed while you were choosing a file, so nothing was changed. Сіз файл таңдап жатқанда құжат өзгергендіктен, ешнәрсе өзгертілген жоқ. + The document was closed before it could be saved. Құжат сақталмай тұрып жабылды. + The pages were extracted but not deleted, because the document changed while you were choosing a folder. Беттер шығарып алынды, бірақ жойылмады, себебі сіз қалта таңдап жатқанда құжат өзгерді. + That didn't work: an internal error stopped it ({m}). Болмады: ішкі қате әрекетті тоқтатты ({m}). + The Windows store's digital IDs couldn't be listed: {e} Windows қоймасындағы цифрлық ID-лерді тізімдеу мүмкін болмады: {e} + Align fields Өрістерді туралау + Distribute fields Өрістерді біркелкі орналастыру + Match field sizes Өріс өлшемдерін теңестіру + Mark for redaction Жасыру үшін белгілеу + Mark pages for redaction Беттерді жасыру үшін белгілеу + Change document properties Құжат сипаттарын өзгерту + Mark differences Айырмашылықтарды белгілеу + Run form script Форма сценарийін орындау + Scripted content must be usable with a keyboard and assistive technology. Check the document's scripts. Сценарий арқылы жасалған мазмұн перентақта және көмекші технологиялар арқылы қолданылуға жарамды болуы тиіс. Құжаттың сценарийлерін тексеріңіз. + Table rows belong directly in a table or its head, body or foot. Fix the table's tags. Кесте жолдары кестенің өзінде немесе оның бас, дене, аяқ бөліктерінде тікелей орналуы тиіс. Кестенің тегтерін түзетіңіз. + Basic constraints Негізгі шектеулер + SHA-1 digest SHA-1 хеші + SHA-256 digest SHA-256 хеші + Squiggly Толқынды + Home / End Home / End + H / Space (hold) H / Space (ұстап тұру) + Rotation: Бұру: + Line ending Сызық ұшы + The standard fonts can't draw “{c}” ({code}). Only Western European characters can be added as text for now. Стандартты қаріптер «{c}» таңбасын ({code}) сала алмайды. Әзірге мәтін ретінде тек Батыс Еуропа таңбаларын қосуға болады. + Larger pages Үлкенірек беттер + Smaller pages Кішірек беттер + Reset page size Бет өлшемін қалпына келтіру + Flatten Fill & Sign when saving Сақтау кезінде «Толтыру және қол қою» мазмұнын жазықтау + Text, marks and signatures become part of the page. Other comments stay editable. Мәтін, белгілер мен қолтаңбалар беттің бөлігіне айналады. Басқа түсініктемелер өңделетін күйде қалады. + Date format Күн пішімі + Fill & Sign dates «Толтыру және қол қою» күндері + Pattern Шаблон + Today: {date} Бүгін: {date} + yyyy or yy year · m or mm month · mmm or mmmm month name · d or dd day · ddd or dddd weekday. H, h, M, s and t are time letters and can't be used on their own: put {escape} before a letter to show it as is. yyyy немесе yy — жыл · m немесе mm — ай · mmm немесе mmmm — ай атауы · d немесе dd — күн · ddd немесе dddd — аптаның күні. H, h, M, s және t — уақыт әріптері, оларды жеке қолдануға болмайды: әріпті сол күйінде көрсету үшін оның алдына {escape} қойыңыз. + Same as interface ({language}) Интерфейспен бірдей ({language}) + {chars} can't be written into the PDF yet: Fill & Sign text is Western European only. Pick a numeric format such as dd.mm.yyyy. {chars} таңбаларын әзірге PDF-ге жазуға болмайды: «Толтыру және қол қою» мәтіні тек Батыс Еуропа таңбаларын қолдайды. dd.mm.yyyy сияқты сандық пішімді таңдаңыз. + Opens the printer driver's preferences. They are saved for every print from this computer; the copies, two-sided, grayscale and paper chosen here still apply. Принтер драйверінің параметрлерін ашады. Олар осы компьютерден жасалатын әр басып шығару үшін сақталады; мұнда таңдалған дана саны, екі жақты басу, сұр түс режимі мен қағаз түрі әлі де қолданылады. + The printer driver's own settings for this print: paper tray, paper type, finishing and more. Осы басып шығаруға арналған принтер драйверінің дербес параметрлері: қағаз науасы, қағаз түрі, соңғы өңдеу және т.б. + Couldn't open the printer's preferences: {e} Принтердің параметрлерін ашу мүмкін болмады: {e} + Printer properties Принтер сипаттары + Reset to printer defaults Принтердің әдепкі параметрлеріне келтіру + This printer offers no settings of its own. Бұл принтердің дербес параметрлері жоқ. + ({n} at {dpi} dpi, the largest size they can be drawn at) ({n} — {dpi} dpi, сурет салынатын ең үлкен өлшем) + A selected field has no widget on any page, so the fields can't be edited together. Таңдалған өрістің ешбір бетте виджеті жоқ, сондықтан өрістерді бірге түзетуге болмайды. + Auto ({percent}%) Авто ({percent}%) + Auto follows the system's text size. Pages always render at the display's full resolution. Авто жүйенің мәтін өлшемін ұстанады. Беттер әрқашан дисплейдің толық ажыратымдылығында көрсетіледі. + Butt Кесілген + Certificate authority Сертификаттау орталығы + Character limit 0 means no limit. A comb of characters needs a limit. Таңба шегі 0 деген — шексіз. Таңбалар торы шекті қажет етеді. + Character limit: Таңба шегі: + Check the pages Беттерді белгілеу + Closed arrow Жабық көрсеткі + Collapse all Барлығын жию + Column select Баған бойынша таңдау + Comb of characters Таңбалар торы + Custom font Арнаулы қаріп + Document properties ({key}) Құжат сипаттары ({key}) + Drag the selection to move it. Esc clears the selection. Таңдауды жылжыту үшін сүйреңіз. Esc таңдауды алып тастайды. + End entity Соңғы субъект + Expand all Барлығын жаю + Files are combined in the order shown. Drag them to change it. Файлдар көрсетілген ретпен біріктіріледі. Ретін өзгерту үшін сүйреңіз. + Fill colour Толтыру түсі + Fill colour: none Толтыру түсі: жоқ + Finish or discard the text being edited before selecting several objects. Бірнеше объектіні таңдамас бұрын түзетіліп жатқан мәтінді аяқтаңыз немесе алып тастаңыз. + Font size 0 uses automatic sizing. Қаріп өлшемі 0 автоматты өлшемді қолданады. + Grid view Тор көрінісі + Import list… Тізімді импорттау… + Interface size Интерфейс өлшемі + List view Тізім көрінісі + Mixed Аралас + Move object Объектіні жылжыту + Move objects Объектілерді жылжыту + Multiple words or phrases Бірнеше сөз немесе тіркес + New bookmark ({key}) Жаңа бетбелгі ({key}) + New bookmarks from structure Құрылымнан жаңа бетбелгілер + Next ({key}) Келесі ({key}) + No fill Толтырусыз + One word or phrase per line Әр жолға бір сөз немесе тіркес + Only checked properties will change. Mixed means the selected fields have different values. Тек белгіленген сипаттар өзгереді. «Аралас» дегені — таңдалған өрістердің мәндері әртүрлі деген сөз. + Open a PDF ({key}) PDF ашу ({key}) + Open arrow Ашық көрсеткі + Pages: {k} of {n} Беттер: {k}, барлығы {n} + PdfCraft is drawing without a graphics processor, so it may be slow. Updating the graphics driver may help. PdfCraft графикалық процессорсыз сурет салуда, сондықтан баяу жұмыс істеуі мүмкін. Графикалық драйверді жаңарту көмектесуі мүмкін. + Preview unavailable: {e} Алдын ала көру қолжетімсіз: {e} + Previous ({key}) Алдыңғы ({key}) + Print ({key}) Басып шығару ({key}) + Redaction code: Жасыру коды: + Remove page Бетті алып тастау + Remove {n} pages {n} бетті алып тастау + Reverse closed arrow Кері жабық көрсеткі + Reverse open arrow Кері ашық көрсеткі + Rotate view clockwise ({key}) Көріністі сағат тілінің бағытымен бұру ({key}) + Saved {place}: {before} → {after} (not smaller){detail} {place} сақталды: {before} → {after} (кішіреймеген){detail} + Scroll tabs left Қойындыларды солға айналдыру + Scroll tabs right Қойындыларды оңға айналдыру + Select at most 1000 objects at a time. Бір мезгілде ең көбі 1000 объектіні таңдауға болады. + Shared Field Properties Ортақ өріс сипаттары + Shift- or Ctrl/Command-click to select several fields. Бірнеше өрісті таңдау үшін Shift немесе Ctrl/Command басып шертіңіз. + Shift-click to select several objects. Drag empty space to select an area. Бірнеше объектіні таңдау үшін Shift басып шертіңіз. Ауданды таңдау үшін бос орынды сүйреңіз. + Slash Қиғаш сызық + Some bookmarks are not shown here: the panel lists a limited number, nested a limited depth. Кейбір бетбелгілер осында көрсетілмеген: панель шектеулі санды, шектеулі тереңдікке дейін ғана тізеді. + Switch to List view to change the pages it takes. Қамтитын беттерін өзгерту үшін Тізім көрінісіне ауысыңыз. + Text recognition isn't installed: its model files are missing. Reinstall PdfCraft, or set PDFCRAFT_MODELS to the folder that holds them. Мәтінді тану орнатылмаған: модель файлдары жоқ. PdfCraft қайта орнатыңыз немесе PDFCRAFT_MODELS айнымалысына олар жатқан қалтаны көрсетіңіз. + Text used as a clipping path cannot be moved with this selection. Қиып алу жолы ретінде қолданылатын мәтін бұл таңдаумен жылжытылмайды. + The document changed while you were editing field properties. Өріс сипаттарын түзетіп жүргенде құжат өзгерді. + The document is no longer open. Құжат енді ашық емес. + The field properties could not be changed. Өріс сипаттарын өзгерту мүмкін болмады. + The field properties dialog is no longer open. Өріс сипаттарының диалогы енді ашық емес. + The field {name} no longer exists. {name} өрісі енді жоқ. + The selected fields could not be found. Select them again. Таңдалған өрістер табылмады. Оларды қайта таңдаңыз. + These objects could not be moved. Select them again and try a smaller move. Бұл объектілерді жылжыту мүмкін болмады. Оларды қайта таңдап, кішілеу жылжытуға тырысыңыз. + This file is already as small as it can be made ({size}). No copy was saved. Бұл файлды одан әрі кішірейту мүмкін емес ({size}). Көшірме сақталмады. + This page contains objects that cannot be selected together. Бұл бетте бірге таңдалмайтын объектілер бар. + To change locked fields, also set Locked to No. Бұғатталған өрістерді өзгерту үшін сонымен қатар «Бұғаттау» параметрін «Жоқ» етіп қойыңыз. + Vertical text cannot be moved with this selection. Тік мәтінді бұл таңдаумен жылжытуға болмайды. + Zoom in ({key}) Үлкейту ({key}) + Zoom in / out (also pinch or {key}-scroll) Үлкейту / кішірейту (сонымен қатар қысу немесе {key}-айналдыру) + Zoom out ({key}) Кішірейту ({key}) + the text recognition models are not installed (reinstall PdfCraft, or set PDFCRAFT_MODELS to the folder that holds them) мәтінді тану модельдері орнатылмаған (PdfCraft қайта орнатыңыз немесе PDFCRAFT_MODELS айнымалысына олар жатқан қалтаны көрсетіңіз) + {name} is too large for a word list (at most 1 MB). {name} сөз тізімі үшін тым үлкен (ең көбі 1 МБ). + {n} fields selected {n} өріс таңдалды + {n} word(s) or phrase(s) {n} сөз немесе тіркес +@plural {n} object selected|{n} objects selected {n} объект таңдалды|{n} объект таңдалды +certificate Subject Субъект +key Ctrl Ctrl +key Delete Delete +key End End +key Esc Esc +key Home Home +key Shift Shift +key Space Бос орын + +# Added for labels that landed alongside this catalog (2026-10-11). + Appearance mode Көрініс режимі + Light Ашық + Dark Күңгірт + Active Белсенді + Dark theme Күңгірт тақырып + Appearance settings… Көрініс параметрлері… + Save ({key}) Сақтау ({key}) + Select page {label} {label} бетін таңдау + Rotate page {label} counterclockwise {label} бетін сағат тіліне қарсы бұру + Rotate page {label} clockwise {label} бетін сағат тілімен бұру + Delete page {label} {label} бетін жою + Offset from centre Ортадан ығысу + Graphic Графика + Signer's name Қол қоюшының аты + Fill & Sign signature «Толтыру және қол қою» қолтаңбасы + Choose image… Сурет таңдау… + Looking for printers… Принтерлер ізделуде… + Sending to {printer}… {printer} құрылғысына жіберілуде… + Enter permissions password… Рұқсаттар құпиясөзін енгізу… + Permissions password Рұқсаттар құпиясөзі + “{name}” has security restrictions. Enter its permissions password to lift them. «{name}» құжатында қауіпсіздік шектеулері бар. Оларды алып тастау үшін рұқсаттар құпиясөзін енгізіңіз. + Restrictions lifted Шектеулер алынды + Couldn't unlock {name}: {e} {name} құлпын ашу мүмкін болмады: {e} + This document has no restrictions to lift Бұл құжатта алып тасталатын шектеулер жоқ + The document's security settings don't allow copying text and images Құжаттың қауіпсіздік параметрлері мәтін мен суреттерді көшіруге рұқсат бермейді + Add the files to combine Біріктірілетін файлдарды қосыңыз + Leave out files that can't be read? Оқылмайтын файлдарды қалдырып кету керек пе? + These files can't be read, so they won't be in the combined PDF: Бұл файлдар оқылмайды, сондықтан біріктірілген PDF-ке кірмейді: + Combine the rest Қалғанын біріктіру + and {n} more және тағы {n} + Drop PDFs, images, text files or folders here, or choose them. PDF, сурет, мәтін файлдарын немесе қалталарды осында тастаңыз не таңдаңыз. + Every PDF, image and text file in the folder Қалтадағы барлық PDF, сурет және мәтін файлдары + Every PDF, image and text file in the folder and the folders inside it Қалтадағы және оның ішкі қалталарындағы барлық PDF, сурет және мәтін файлдары + No PDFs, images or text files found in {folder} {folder} қалтасынан PDF, сурет не мәтін файлдары табылмады + Added the first {n} files in {folder} {folder} қалтасынан алғашқы {n} файл қосылды + Margins Шеттер + For page sizes and margins. Бет өлшемдері мен шеттері үшін. + Reset panel layout Панельдер орналасуын қалпына келтіру + Float panel Панельді бөлек терезеге шығару + Move to group Топқа жылжыту + Panels Панельдер + Resize panels Панельдер өлшемін өзгерту + Resize panel window Панель терезесінің өлшемін өзгерту + Tools Құралдар + Inspector Инспектор + PDF page: {size} PDF беті: {size} + Match page at 100% Бетке 100% сәйкестендіру + PDF scale: {scale}% PDF масштабы: {scale}% + Printer paper and scaling may differ. Check driver settings. Принтер қағазы мен масштабы өзгеше болуы мүмкін. Драйвер параметрлерін тексеріңіз. + Drawing scale changes. Use Actual size for full-scale plans. Сызба масштабы өзгереді. Толық масштабты жоспарлар үшін Нақты өлшемді пайдаланыңыз. + Reload documents when their files change on disk Дискідегі файлдары өзгергенде құжаттарды қайта жүктеу + This file changed on disk. Reload it to see the new version; your unsaved changes will be lost. Бұл файл дискіде өзгерді. Жаңа нұсқасын көру үшін оны қайта жүктеңіз; сақталмаған өзгерістер жоғалады. + Reload Қайта жүктеу + Couldn't reload {name}: {e} {name} қайта жүктеу мүмкін болмады: {e} + Mouse wheel scroll speed Тінтуір дөңгелегімен айналдыру жылдамдығы +scroll speed Slow Баяу +scroll speed Normal Қалыпты +scroll speed Fast Жылдам + Enhance camera image Камера суретін жақсарту + Drag the corners onto the document's edges, then choose Enhance page. Бұрыштарды құжаттың жиектеріне сүйреп, содан кейін «Бетті жақсарту» таңдаңыз. + Enhance page Бетті жақсарту + Done adding text Мәтін қосу аяқталды + Discard this text (Esc) Бұл мәтіннен бас тарту (Esc) + Clean-up Тазалау + Original Түпнұсқа + Greyscale Сұр реңктер + Black and white Ақ-қара diff --git a/crates/ui-egui/src/i18n/mod.rs b/crates/ui-egui/src/i18n/mod.rs index b4346f15..6c01bebd 100644 --- a/crates/ui-egui/src/i18n/mod.rs +++ b/crates/ui-egui/src/i18n/mod.rs @@ -136,7 +136,7 @@ fn plural_sl(n: u64) -> usize { } /// The registry. English first: it is the fallback and the source language. -pub static LANGUAGES: [LangInfo; 19] = [ +pub static LANGUAGES: [LangInfo; 20] = [ LangInfo { code: "en", name: "English", source: "", plural: plural_one_other, rtl: false, catalog: OnceLock::new() }, LangInfo { code: "ja", name: "日本語", source: include_str!("ja.tsv"), plural: plural_none, rtl: false, catalog: OnceLock::new() }, // Simplified Chinese; `zh`, `zh-CN`, `zh-SG` and `zh-Hans-*` locales resolve here (see `candidates`). @@ -166,6 +166,8 @@ pub static LANGUAGES: [LangInfo; 19] = [ LangInfo { code: "fr", name: "Français", source: include_str!("fr.tsv"), plural: plural_fr, rtl: false, catalog: OnceLock::new() }, // Russian; every `ru-*` locale (`ru-RU`, `ru-BY`, `ru-KZ` ...) resolves here. LangInfo { code: "ru", name: "Русский", source: include_str!("ru.tsv"), plural: plural_russian, rtl: false, catalog: OnceLock::new() }, + // Kazakh; every `kk-*` locale (`kk-KZ`, `kk-KZ.UTF-8`) resolves here. + LangInfo { code: "kk", name: "Қазақша", source: include_str!("kk.tsv"), plural: plural_one_other, rtl: false, catalog: OnceLock::new() }, // Bulgarian; every `bg-*` locale (`bg-BG`) resolves here. LangInfo { code: "bg", name: "Български", source: include_str!("bg.tsv"), plural: plural_one_other, rtl: false, catalog: OnceLock::new() @@ -2377,6 +2379,112 @@ mod tests { } } + /// Kazakh translates every registered command and every All tools label. + #[test] + fn kazakh_covers_commands_and_catalogue() { + let kk = Lang::from_code("kk").expect("kk registered"); + for command in pdfcraft_engine::commands::COMMANDS { + assert!(has(kk, command.label), "missing command: {}", command.label); + if let Some(menu) = command.menu { + assert!(has(kk, menu), "missing menu: {menu}"); + } + } + for group in pdfcraft_engine::catalog::TOOL_GROUPS { + assert!(has(kk, group.label), "missing group: {}", group.label); + for section in group.sections { + assert!(has(kk, section.title), "missing section: {}", section.title); + for item in section.items { + assert!(has(kk, item.label), "missing item: {}", item.label); + } + } + } + } + /// Preserve only product/format names, abbreviations and templates with no English words. + #[test] + fn kazakh_catalog_does_not_leave_english_ui_text() { + let kk = Lang::from_code("kk").expect("kk registered"); + let (entries, errors) = parse_entries(kk.0.source, kk.0.plural_forms()); + assert!(errors.is_empty(), "{errors:?}"); + let keep = [ + "OK", + "PDF/A…", + "Microsoft Word (.docx)", + "PostScript / EPS", + // Key-cap names, printed in English on Kazakh keyboards. + "Home / End", + "JavaScript", + "ZIP", + "{n} {kind}", + "{rule} - {status}", + "{field}: {e}", + "`{command}` {when}", + "{0}: {1}", + "{month} {y}", + "+LOC", + "−LOC", + "ΔLOC", + "+Bin", + "−Bin", + ]; + let unchanged: Vec<_> = entries + .iter() + .filter(|e| { + let key_cap = e.context == "key" && KEY_CAP_NAMES.contains(&e.source.as_str()); + e.source == e.translation && !keep.contains(&e.source.as_str()) && !key_cap + }) + .collect(); + assert!(unchanged.is_empty(), "untranslated Kazakh catalog entries: {unchanged:#?}"); + } + /// Kazakh translates every tl!("literal") in the UI source, including direct lookups and + /// multiline literals, following the complete-catalog check for Simplified Chinese. + #[test] + fn kazakh_covers_ui_literals() { + let kk = Lang::from_code("kk").expect("kk registered"); + let mut stack = vec![std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src")]; + let mut literals = std::collections::BTreeSet::new(); + while let Some(dir) = stack.pop() { + for entry in std::fs::read_dir(dir).expect("UI source directory") { + let path = entry.expect("UI source entry").path(); + if path.is_dir() { + if path.file_name().is_some_and(|name| name != "i18n") { + stack.push(path); + } + } else if path.extension().is_some_and(|ext| ext == "rs") { + let source = std::fs::read_to_string(path).expect("UI source file").replace("\r\n", "\n").replace("crate::i18n::t(", "tl!("); + let mut rest = source.split("#[cfg(test)]\nmod ").next().unwrap_or_default(); + while let Some((_, after)) = rest.split_once("tl!(") { + let after = after.trim_start(); + let Some(after) = after.strip_prefix('"') else { + rest = after; + continue; + }; + let mut escaped = false; + let end = after + .char_indices() + .find_map(|(i, c)| { + if c == '"' && !escaped { + return Some(i); + } + escaped = c == '\\' && !escaped; + None + }) + .expect("closed tl! literal"); + let (raw, tail) = after.split_at(end); + let closing = tail.strip_prefix('"').expect("closing quote").trim_start(); + let closing = closing.strip_prefix(',').unwrap_or(closing).trim_start(); + if closing.starts_with(')') { + let label: String = serde_json::from_str(&format!("\"{raw}\"")).expect("UI literal escapes"); + literals.insert(label); + } + rest = tail.strip_prefix('"').expect("closing quote"); + } + } + } + } + assert!(literals.len() > 900, "source scan found only {} literals", literals.len()); + let missing: Vec<_> = literals.iter().filter(|label| !has(kk, label)).collect(); + assert!(missing.is_empty(), "untranslated Kazakh UI literals: {missing:#?}"); + } /// Check direct lookups and multiline literals as well as ordinary tl!("literal") calls, /// following the complete-catalog check for Simplified Chinese. #[test] diff --git a/crates/ui-egui/tests/fonts.rs b/crates/ui-egui/tests/fonts.rs index 0e4f8ba8..dcf91594 100644 --- a/crates/ui-egui/tests/fonts.rs +++ b/crates/ui-egui/tests/fonts.rs @@ -233,13 +233,14 @@ fn primary_ui_fonts_cover_latin_and_cyrillic_catalogs() { ("es", include_str!("../src/i18n/es.tsv")), ("fr", include_str!("../src/i18n/fr.tsv")), ("ru", include_str!("../src/i18n/ru.tsv")), + ("kk", include_str!("../src/i18n/kk.tsv")), ("bg", include_str!("../src/i18n/bg.tsv")), ("hu", include_str!("../src/i18n/hu.tsv")), ("uk", include_str!("../src/i18n/uk.tsv")), ("it", include_str!("../src/i18n/it.tsv")), ("sl", include_str!("../src/i18n/sl.tsv")), ] { - let mut text = String::from("áčďéěíňóřšťúůýžÁČĎÉĚÍŇÓŘŠŤÚŮÝŽãõçâêôàÃÕÇÂÊÔÀñÑüÜäÄöÖőűß„“¿¡«»…ґҐєЄіІїЇ"); + let mut text = String::from("áčďéěíňóřšťúůýžÁČĎÉĚÍŇÓŘŠŤÚŮÝŽãõçâêôàÃÕÇÂÊÔÀñÑüÜäÄöÖőűß„“¿¡«»…ґҐєЄіІїЇәғқңөұүһӘҒҚҢӨҰҮҺ"); for line in catalog.lines().filter(|l| !l.starts_with('#')) { if let Some(translation) = line.split('\t').nth(2) { text.extend(translation.chars().filter(|c| !c.is_whitespace())); From 3d2bdfe71ada10f6bc39afc680dce61bbf080cbc Mon Sep 17 00:00:00 2001 From: Ufuk Efe Date: Sun, 11 Oct 2026 23:34:25 +0300 Subject: [PATCH 4/9] feat(i18n): add complete Turkish interface language support (#1057) * feat(i18n): add complete Turkish interface language support * Turkish: rows for the labels that landed alongside this catalog; refresh hashes --------- Co-authored-by: Brandon Thomas --- ATTRIBUTION.md | 3 +- ATTRIBUTION.toml | 11 + crates/engine/src/dates.rs | 11 +- crates/ui-egui/src/i18n/mod.rs | 95 +- crates/ui-egui/src/i18n/tr.tsv | 2248 ++++++++++++++++++++++++++++++++ crates/ui-egui/tests/fonts.rs | 1 + docs/localization.md | 6 +- 7 files changed, 2370 insertions(+), 5 deletions(-) create mode 100644 crates/ui-egui/src/i18n/tr.tsv diff --git a/ATTRIBUTION.md b/ATTRIBUTION.md index 351955f8..f1094e3c 100644 --- a/ATTRIBUTION.md +++ b/ATTRIBUTION.md @@ -4,7 +4,7 @@ Every asset PdfCraft includes, bundles or uses to build its published material, with its author, source and licence. The policy is in [AGENTS.md](AGENTS.md) §1. The machine-readable list, with SHA-256 hashes, is [ATTRIBUTION.toml](ATTRIBUTION.toml). Licence texts are kept beside the assets and summarised in [NOTICE](NOTICE). -## In this repository (268) +## In this repository (269) | Asset | Title | Author | Licence | Source | Used for | |---|---|---|---|---|---| @@ -26,6 +26,7 @@ Every asset PdfCraft includes, bundles or uses to build its published material, | `crates/ui-egui/src/i18n/ja.tsv` | Japanese 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 | Japanese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs | | `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 | | `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 | diff --git a/ATTRIBUTION.toml b/ATTRIBUTION.toml index f473ec05..80866b35 100644 --- a/ATTRIBUTION.toml +++ b/ATTRIBUTION.toml @@ -208,6 +208,17 @@ kind = "translation" usage = "Arabic UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" sha256 = "a8a66390b2d80b4708c4f854318b923eef7f0f5932439fb5ef25f2651980f24e" +[[asset]] +path = "crates/ui-egui/src/i18n/tr.tsv" +title = "Turkish interface translation catalog" +author = "PdfCraft contributors" +source = "Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources" +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 = "5688538845a80c154feabd096badbf097485dea8a7402fede4ef7d71466e9d6a" + [[asset]] path = "assets/icons/accessibility.svg" title = "Lucide icon \"accessibility\"" diff --git a/crates/engine/src/dates.rs b/crates/engine/src/dates.rs index 154ac965..195202a1 100644 --- a/crates/engine/src/dates.rs +++ b/crates/engine/src/dates.rs @@ -56,7 +56,7 @@ const AR_DAYS: [&str; 7] = ["الأحد", "الاثنين", "الثلاثاء", /// The languages month and weekday names can be written in (Preferences ▸ Date format ▸ Language): /// the interface languages, in the same order and with the same names. -pub const DATE_LANGUAGES: [DateLanguage; 20] = [ +pub const DATE_LANGUAGES: [DateLanguage; 21] = [ DateLanguage { code: "en", name: "English", @@ -303,6 +303,15 @@ pub const DATE_LANGUAGES: [DateLanguage; 20] = [ days: ["Κυριακή", "Δευτέρα", "Τρίτη", "Τετάρτη", "Πέμπτη", "Παρασκευή", "Σάββατο"], days_short: ["Κυρ", "Δευ", "Τρί", "Τετ", "Πέμ", "Παρ", "Σάβ"], }, + DateLanguage { + code: "tr", + name: "Türkçe", + months: ["Ocak", "Şubat", "Mart", "Nisan", "Mayıs", "Haziran", "Temmuz", "Ağustos", "Eylül", "Ekim", "Kasım", "Aralık"], + months_with_day: None, + months_short: ["Oca", "Şub", "Mar", "Nis", "May", "Haz", "Tem", "Ağu", "Eyl", "Eki", "Kas", "Ara"], + days: ["Pazar", "Pazartesi", "Salı", "Çarşamba", "Perşembe", "Cuma", "Cumartesi"], + days_short: ["Paz", "Pzt", "Sal", "Çar", "Per", "Cum", "Cmt"], + }, ]; /// The date language for `code` (any case), or `None` when there's none. diff --git a/crates/ui-egui/src/i18n/mod.rs b/crates/ui-egui/src/i18n/mod.rs index 6c01bebd..4c399246 100644 --- a/crates/ui-egui/src/i18n/mod.rs +++ b/crates/ui-egui/src/i18n/mod.rs @@ -136,7 +136,7 @@ fn plural_sl(n: u64) -> usize { } /// The registry. English first: it is the fallback and the source language. -pub static LANGUAGES: [LangInfo; 20] = [ +pub static LANGUAGES: [LangInfo; 21] = [ LangInfo { code: "en", name: "English", source: "", plural: plural_one_other, rtl: false, catalog: OnceLock::new() }, LangInfo { code: "ja", name: "日本語", source: include_str!("ja.tsv"), plural: plural_none, rtl: false, catalog: OnceLock::new() }, // Simplified Chinese; `zh`, `zh-CN`, `zh-SG` and `zh-Hans-*` locales resolve here (see `candidates`). @@ -192,6 +192,8 @@ pub static LANGUAGES: [LangInfo; 20] = [ // Slovene; every `sl-*` locale (`sl-SI`) resolves here. Four plural forms (one, two, few, other). LangInfo { code: "sl", name: "Slovenščina", source: include_str!("sl.tsv"), plural: plural_sl, rtl: false, catalog: OnceLock::new() }, LangInfo { code: "el", name: "Ελληνικά", source: include_str!("el.tsv"), plural: plural_one_other, rtl: false, catalog: OnceLock::new() }, + // Turkish; every `tr-*` locale (`tr-TR`, `tr_TR.UTF-8`) resolves here. + LangInfo { code: "tr", name: "Türkçe", source: include_str!("tr.tsv"), plural: plural_none, rtl: false, catalog: OnceLock::new() }, ]; impl LangInfo { @@ -841,7 +843,7 @@ mod tests { labels.insert(label.to_string()); } assert!(labels.len() > 20 && labels.contains("Comb of characters"), "the scan found the dialog's labels: {labels:?}"); - for code in ["bg", "cs", "de", "es", "fr", "hu", "it", "ja", "pt-br", "ru", "te", "uk", "zh-hans", "zh-hant"] { + for code in ["bg", "cs", "de", "es", "fr", "hu", "it", "ja", "pt-br", "ru", "te", "tr", "uk", "zh-hans", "zh-hant"] { let lang = Lang::from_code(code).expect("registered"); let missing: Vec<_> = labels.iter().filter(|l| !has(lang, l)).collect(); assert!(missing.is_empty(), "{code} lacks Shared Field Properties labels: {missing:#?}"); @@ -1391,6 +1393,95 @@ mod tests { set_current(Lang::EN); } + /// Turkish covers every registered menu title and menu command label, keeps ellipses, and + /// persists in preferences. + #[test] + fn turkish_covers_every_menu_label() { + let tr_lang = Lang::from_code("tr").expect("tr registered"); + assert_eq!(tr_lang.name(), "Türkçe"); + assert_eq!(tr(tr_lang, "File"), "Dosya"); + assert_eq!(tr(tr_lang, "Türkçe Belge.pdf"), "Türkçe Belge.pdf"); + assert_eq!((0..=6).map(|n| (tr_lang.0.plural)(n)).collect::>(), [0; 7]); + for spec in pdfcraft_engine::commands::COMMANDS { + let Some(menu) = spec.menu else { continue }; + assert!(has(tr_lang, menu), "Turkish lacks the menu title {menu:?}"); + assert!(has(tr_lang, spec.label), "Turkish lacks the {menu} menu label {:?} ({})", spec.label, spec.id); + } + const KEEP_AS_IS: &[&str] = &[ + "OK", + "PDF/A…", + "Microsoft Word (.docx)", + "PostScript / EPS", + "Preflight", + "JavaScript", + "ZIP", + "{n} {kind}", + "{rule} - {status}", + "Poster", + "{field}: {e}", + "`{command}` {when}", + "{0}: {1}", + "{month} {y}", + "+LOC", + "−LOC", + "ΔLOC", + "+Bin", + "−Bin", + "Model", + "Home / End", + ]; + let (entries, errors) = parse_entries(tr_lang.0.source, tr_lang.0.plural_forms()); + assert!(errors.is_empty(), "Turkish catalog errors: {errors:?}"); + for e in &entries { + assert!( + e.source != e.translation || KEEP_AS_IS.contains(&e.source.as_str()) || (e.context == "key" && KEY_CAP_NAMES.contains(&e.source.as_str())), + "{:?} is untranslated", + e.source + ); + assert!(!e.translation.contains("..."), "use … rather than three dots: {:?}", e.translation); + assert_eq!(e.source.starts_with(' '), e.translation.starts_with(' '), "leading space mismatch: {:?}", e.translation); + assert_eq!(e.source.ends_with(' '), e.translation.ends_with(' '), "trailing space mismatch: {:?}", e.translation); + } + let mut app = crate::PdfCraftApp::default(); + app.set_option("language", "tr").unwrap(); + assert!(app.set_option("language", "tu").is_err()); + assert_eq!(app.language, "tr"); + let mut restored = crate::PdfCraftApp::default(); + restored.restore(&app.persist()); + assert_eq!(restored.language, "tr"); + } + + /// Turkish translates every registered command, menu and All tools label. + #[test] + fn turkish_covers_commands_and_catalogue() { + let tr_lang = Lang::from_code("tr").expect("tr registered"); + for command in pdfcraft_engine::commands::COMMANDS { + assert!(has(tr_lang, command.label), "missing command: {}", command.label); + if let Some(menu) = command.menu { + assert!(has(tr_lang, menu), "missing menu: {menu}"); + } + } + for group in pdfcraft_engine::catalog::TOOL_GROUPS { + assert!(has(tr_lang, group.label), "missing group: {}", group.label); + for section in group.sections { + assert!(has(tr_lang, section.title), "missing section: {}", section.title); + for item in section.items { + assert!(has(tr_lang, item.label), "missing item: {}", item.label); + } + } + } + } + + /// Turkish translates every `tl!("…")` literal in the UI source. + #[test] + fn turkish_covers_ui_literals() { + let literals = ui_literals(); + assert!(literals.len() > 900, "source scan found only {} literals", literals.len()); + let tr_lang = Lang::from_code("tr").expect("tr registered"); + let missing: Vec<_> = literals.iter().filter(|label| !has(tr_lang, label)).collect(); + assert!(missing.is_empty(), "untranslated Turkish UI literals: {missing:#?}"); + } + #[test] fn simplified_chinese_is_registered() { let zh = Lang::from_code("zh-hans").expect("zh-hans registered"); diff --git a/crates/ui-egui/src/i18n/tr.tsv b/crates/ui-egui/src/i18n/tr.tsv new file mode 100644 index 00000000..f4c6213a --- /dev/null +++ b/crates/ui-egui/src/i18n/tr.tsv @@ -0,0 +1,2248 @@ +# Turkish (tr) catalog. Format: see the header of ja.tsv +# (context source translation; @id, @plural and other contexts; escapes \n \t \\). +# Plural forms (see `plural_none` in i18n/mod.rs): one form. +# +# Clean-room: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources. + + Menu Menü + File Dosya + Edit Düzen + Pages Sayfalar + View Görünüm + Help Yardım + Preferences Tercihler + Preferences… Tercihler… + Interface language Arayüz dili + Identity Kimlik + Name on new comments Yeni yorumlardaki ad + Open… Aç… + New blank PDF Yeni boş PDF + Create PDF from file… Dosyadan PDF oluştur… + Create PDF from images… Görsellerden PDF oluştur… + Create PDF from multiple files… Birden çok dosyadan PDF oluştur… + Create PDF from clipboard Panodan PDF oluştur + Combine files… Dosyaları birleştir… + Save Kaydet + Save as… Farklı kaydet… + Close file Dosyayı kapat + Close all Tümünü kapat + Revert Geri döndür + Print… Yazdır… + Document properties… Belge özellikleri… + Undo Geri al + Redo Yinele + Find… Bul… + Advanced search… Gelişmiş arama… + Copy pages Sayfaları kopyala + Cut pages Sayfaları kes + Paste pages Sayfaları yapıştır + Fit visible Görünene sığdır + Marquee zoom Seçim yakınlaştırma + Take a snapshot Anlık görüntü al + Full screen mode Tam ekran modu + Read mode Okuma modu + Switch light / dark theme Açık / koyu temaya geç + Comments panel Yorumlar paneli + Form fields panel Form alanları paneli + Clear form Formu temizle + Find tools and commands… Araçları ve komutları bul… + Zoom Yakınlaştırma + Actual size Gerçek boyut + Zoom to page level Sayfa düzeyine yakınlaştır + Fit to width Genişliğe sığdır + Display theme Görünüm teması + Side panels Yan paneller + Enable Acrobat JavaScript Acrobat JavaScript'i etkinleştir + Flatten Fill & Sign when saving Kaydederken Doldur ve İmzala'yı düzleştir + Text, marks and signatures become part of the page. Other comments stay editable. Metin, işaretler ve imzalar sayfanın bir parçası olur. Diğer yorumlar düzenlenebilir kalır. + OK Tamam + Join our Discord Discord'umuza katılın + Close Kapat + Next / previous match Sonraki / önceki eşleşme + Copy selected text Seçilen metni kopyala + Double-click Çift tıklama + Select a word Bir kelime seç + Clear selection / close find Seçimi temizle / bulmayı kapat + Zoom in / out (also pinch or {key}-scroll) Yakınlaştır / uzaklaştır (ayrıca kıstırma veya {key}+kaydırma) + Rotate view Görünümü döndür + First / last page İlk / son sayfa + Previous / next page Önceki / sonraki sayfa + Delete selected pages (Organize) Seçili sayfaları sil (Düzenle) + Select all pages (Organize) Tüm sayfaları seç (Düzenle) + Home Ana Sayfa + Open Aç + Open a PDF ({key}) Bir PDF aç ({key}) + Dark gray theme Koyu gri tema + Light theme Açık tema + All tools Tüm araçlar + Read Oku + Convert Dönüştür + E-Sign E-İmza + Print ({key}) Yazdır ({key}) + Save (M4) Kaydet (M4) + Document properties ({key}) Belge özellikleri ({key}) + Find tools and commands Araçları ve komutları bul + Fit to height Yüksekliğe sığdır + Zoom in Yakınlaştır + Zoom out Uzaklaştır + Rotate view clockwise Görünümü saat yönünde döndür + Rotate view counterclockwise Görünümü saat yönünün tersine döndür + Page navigation Sayfa gezintisi + Previous view Önceki görünüm + Next view Sonraki görünüm + Page display Sayfa görünümü + Continuous scrolling Sürekli kaydırma + Two-page view İki sayfa görünümü + Single page Tek sayfa + Show cover page in two-page view İki sayfa görünümünde kapak sayfasını göster + Use system setting Sistem ayarını kullan + Light gray Açık gri + Dark gray Koyu gri + Comments Yorumlar + Bookmarks Yer işaretleri + Fields Alanlar + Layers Katmanlar + Attachments Ekler + Signatures İmzalar + Accessibility Checker Erişilebilirlik Denetleyicisi + Search Ara + Compare Karşılaştır + Page thumbnails Sayfa küçük resimleri + Form fields Form alanları + Zoom out ({key}) Uzaklaştır ({key}) + Zoom in ({key}) Yakınlaştır ({key}) + Rotate view clockwise ({key}) Görünümü saat yönünde döndür ({key}) + Toggle fit page / fit width Sayfayı sığdır / genişliğe sığdır geçişi + Next page Sonraki sayfa + Previous page Önceki sayfa + Current page — type a page number or label (such as iv) and press Enter Geçerli sayfa — bir sayfa numarası veya etiketi (örneğin iv) yazıp Enter'a basın + Welcome to PdfCraft PdfCraft'a Hoş Geldiniz + An open-source PDF workbench — local, private, and scriptable. Açık kaynaklı bir PDF çalışma alanı — yerel, gizli ve komut dosyalarıyla yönetilebilir. + Join the ArtCraft community ArtCraft topluluğuna katılın + Get help, share feedback and follow development on Discord. Discord'da yardım alın, geri bildirim paylaşın ve geliştirmeyi takip edin. + Recommended tools Önerilen araçlar + Use now Şimdi kullan + Open file Dosya aç + Recent Son kullanılanlar + Files you open in PdfCraft appear here. Drop a PDF anywhere to open it. PdfCraft'ta açtığınız dosyalar burada görünür. Açmak için bir PDF'i herhangi bir yere bırakın. + Privacy Gizlilik + PdfCraft works offline. No telemetry, no account, and no cloud processing unless you add a provider. PdfCraft çevrimdışı çalışır. Bir sağlayıcı eklemediğiniz sürece telemetri yok, hesap yok ve bulut işleme yok. + Scripts run in a sandbox without file or network access. With JavaScript off, Acrobat's standard format, validate and calculate functions still work. Komut dosyaları dosya veya ağ erişimi olmadan yalıtılmış alanda (sandbox) çalışır. JavaScript kapalıyken bile Acrobat'ın standart biçimlendirme, doğrulama ve hesaplama işlevleri çalışmaya devam eder. + Back to all tools Tüm araçlara geri dön + Close panel Paneli kapat + View less Daha az göster + View more Daha fazla göster + Available Kullanılabilir + Ready Hazır + Search tools and commands… Araçları ve komutları ara… + No matching tools Eşleşen araç yok + Tool Araç + Command Komut + Add a sticky note Not ekle + Add a text box Metin kutusu ekle + Highlight text Metni vurgula + Underline text Metnin altını çiz + Strikethrough text Metnin üstünü çiz + Squiggly underline text Metnin altını dalgalı çiz + Draw freehand Serbest çizim yap + Draw a line Çizgi çiz + Draw an arrow Ok çiz + Draw a rectangle Dikdörtgen çiz + Draw an oval Elips çiz + Draw a polygon Çokgen çiz + Draw connected lines Bağlantılı çizgiler çiz + Draw a cloud Bulut çiz + Add a callout Açıklama balonu ekle + Insert text Metin ekle + Replace text Metni değiştir + Attach a file Dosya ekle + Erase drawings Çizimleri sil + Flatten comments Yorumları düzleştir + Import comments… Yorumları içe aktar… + Add a stamp Damga ekle + Export all comments to data file… Tüm yorumları veri dosyasına aktar… + Hide all comments Tüm yorumları gizle + Summarize comments Yorumları özetle + Import form data… Form verilerini içe aktar… + Export form data… Form verilerini dışa aktar… + PDF/A… PDF/A… + Action Wizard… Eylem Sihirbazı… + Prepare for distribution… Dağıtım için hazırla… + Optimize scanned documents… Taranmış belgeleri en iyileştir… + Compare files… Dosyaları karşılaştır… + Detect form fields Form alanlarını algıla + Merge data files into spreadsheet… Veri dosyalarını elektronik tabloda birleştir… + Flatten form fields Form alanlarını düzleştir + Prepare a form Form hazırla + Tab order: by rows Sekme sırası: satırlara göre + Tab order: by columns Sekme sırası: sütunlara göre + Tab order: by document structure Sekme sırası: belge yapısına göre + Add text Metin ekle + Edit text & images Metin ve resimleri düzenle + Add or edit links Bağlantı ekle veya düzenle + Create links from URLs URL'lerden bağlantı oluştur + Remove all links Tüm bağlantıları kaldır + Add Bates numbering… Bates numaralandırması ekle… + Add image… Resim ekle… + Redact text and images Metin ve resimleri karart + Redact pages… Sayfaları karart… + Find text and redact… Metin bul ve karart… + Redaction properties… Karartma özellikleri… + Apply redactions… Karartmaları uygula… + Clear redaction marks Karartma işaretlerini temizle + Remove hidden information… Gizli bilgileri kaldır… + Sanitize document… Belgeyi temizle… + Field properties… Alan özellikleri… + Add a text field Metin alanı ekle + Add a checkbox Onay kutusu ekle + Add a radio button Radyo düğmesi ekle + Add a drop-down list Açılır liste ekle + Add a list box Liste kutusu ekle + Add a button Düğme ekle + Add an image field Resim alanı ekle + Add a date field Tarih alanı ekle + Add a digital signature field Dijital imza alanı ekle + Digitally sign Dijital olarak imzala + Certify (visible signature) Sertifikala (görünür imza) + Certify (invisible signature) Sertifikala (görünmez imza) + Validate all signatures Tüm imzaları doğrula + Signatures panel İmzalar paneli + Fill & Sign: add text Doldur ve İmzala: metin ekle + Fill & Sign: checkmark Doldur ve İmzala: onay işareti + Fill & Sign: cross Doldur ve İmzala: çarpı + Fill & Sign: dot Doldur ve İmzala: nokta + Fill & Sign: line Doldur ve İmzala: çizgi + Fill & Sign: date Doldur ve İmzala: tarih + Fill & Sign: sign Doldur ve İmzala: imzala + Fill & Sign: initials Doldur ve İmzala: paraf + Export to image… Görsele dışa aktar… + Reduce file size… Dosya boyutunu küçült… + Optimize PDF… PDF'i eniyile… + Export to text… Metne dışa aktar… + Export to Word… Word'e dışa aktar… + Export to HTML… HTML'e dışa aktar… + Export to RTF… RTF'ye dışa aktar… + JavaScript console… JavaScript konsolu… + Document JavaScripts… Belge JavaScript'leri… + Recognize text… Metni tanı… + Recognize text in multiple files… Birden çok dosyada metni tanı… + Check for accessibility… Erişilebilirliği denetle… + Open accessibility report Erişilebilirlik raporunu aç + Change reading options… Okuma seçeneklerini değiştir… + Add alternate text… Alternatif metin ekle… + Export all images… Tüm görselleri dışa aktar… + Add header & footer… Üstbilgi ve altbilgi ekle… + Update header & footer… Üstbilgi ve altbilgiyi güncelle… + Remove header & footer Üstbilgi ve altbilgiyi kaldır + Add watermark… Filigran ekle… + Update watermark… Filigranı güncelle… + Remove watermark Filigranı kaldır + Add background… Arka plan ekle… + Update background… Arka planı güncelle… + Remove background Arka planı kaldır + Protect using password… Parolayla koru… + Remove security Güvenliği kaldır + Security properties… Güvenlik özellikleri… + Organize pages Sayfaları düzenle + New bookmark Yeni yer imi + New bookmarks from structure Yapıdan yeni yer imleri + Rotate pages clockwise Sayfaları saat yönünde döndür + Rotate pages counterclockwise Sayfaları saat yönünün tersine döndür + Delete pages Sayfaları sil + Insert blank page Boş sayfa ekle + Rotate pages… Sayfaları döndür… + Duplicate pages Sayfaları çoğalt + Crop pages Sayfaları kırp + Set page boxes… Sayfa kutularını ayarla… + Insert pages from file… Dosyadan sayfa ekle… + Replace pages… Sayfaları değiştir… + Extract pages… Sayfaları ayıkla… + Split document… Belgeyi böl… + Number pages… Sayfaları numaralandır… + Keyboard shortcuts Klavye kısayolları + Join the ArtCraft Discord ArtCraft Discord'una katıl + PdfCraft web page PdfCraft web sayfası + PdfCraft on GitHub GitHub'da PdfCraft + ArtCraft website ArtCraft web sitesi + Check for updates… Güncellemeleri denetle… + About PdfCraft PdfCraft hakkında + Export a PDF PDF'i dışa aktar + Export to Şuraya aktar + Microsoft Word (.docx) Microsoft Word (.docx) + Spreadsheet (.xlsx) Elektronik Tablo (.xlsx) + Presentation (.pptx) Sunum (.pptx) + Image (PNG) Resim (PNG) + Export all images Tüm resimleri dışa aktar + HTML web page HTML web sayfası + Rich Text Format (.rtf) Zengin Metin Biçimi (.rtf) + Text (plain) Metin (düz) + PostScript / EPS PostScript / EPS + Fill & Sign Doldur ve İmzala + Fill Doldur + Checkmark Onay işareti + Cross Çarpı + Dot Nokta + Line Çizgi + Date Tarih + Signature İmza + Initials Paraf + Edit a PDF PDF'i düzenle + Modify page Sayfayı değiştir + Rotate pages Sayfaları döndür + Insert pages Sayfalar ekle + Extract pages Sayfaları ayıkla + Add content İçerik ekle + Text Metin + Image Resim + Header and footer Üstbilgi ve altbilgi + Watermark Filigran + Link Bağlantı + Bates numbering Bates numaralandırması + Background Arka plan + Attach file Dosya ekle + Create a PDF PDF oluştur + Create from Şuradan oluştur + Single file Tek dosya + Multiple files Birden çok dosya + Images Resimler + Clipboard Pano + Blank page Boş sayfa + Combine files Dosyaları birleştir + Combine Birleştir + Add files to combine Birleştirilecek dosyaları ekle + Page options Sayfa seçenekleri + Page grid Sayfa ızgarası + Rotate Döndür + Delete Sil + Extract Ayıkla + Insert Ekle + Replace Değiştir + Split Böl + More Daha fazla + Set page boxes Sayfa kutularını ayarla + Page labels Sayfa etiketleri + Page transitions Sayfa geçişleri + Add comments Yorum ekle + Markup İşaretleme + Sticky note Not + Underline Altı çizili + Strikethrough Üstü çizili + Squiggly underline Dalgalı alt çizgi + Text box Metin kutusu + Drawing Çizim + Rectangle Dikdörtgen + Oval Elips + Arrow Ok + Cloud Bulut + Stamp Damga + Review Gözden geçirme + Comment list Yorum listesi + Scan & OCR Tara ve OCR + Recognize text Metni tanı + In this file Bu dosyada + In multiple files Birden çok dosyada + Enhance scanned file Taranmış dosyayı iyileştir + Correct recognized text Tanınan metni düzelt + Protect a PDF PDF'i koru + Protect Koru + Protect with password Parola ile koru + Remove hidden information Gizli bilgileri kaldır + Advanced options Gelişmiş seçenekler + Encrypt with certificate Sertifika ile şifrele + Security properties Güvenlik özellikleri + Redact a PDF PDF'i karart + Redact Karart + Redact pages Sayfaları karart + Find text and redact Metin bul ve karart + Set properties Özellikleri ayarla + Sanitize document Belgeyi temizle + Compress a PDF PDF'i sıkıştır + Optimize En iyileştir + Reduce file size Dosya boyutunu küçült + Advanced optimization Gelişmiş iyileştirme + Audit space usage Alan kullanımını denetle + Field list Alan listesi + Field properties Alan özellikleri + Tab order by rows Satırlara göre sekme sırası + Tab order by columns Sütunlara göre sekme sırası + Import data Verileri içe aktar + Export data Verileri dışa aktar + Add form components Form bileşenleri ekle + Text field Metin alanı + Checkbox Onay kutusu + Radio button Radyo düğmesi + Drop-down list Açılır liste + List box Liste kutusu + Button Düğme + Image field Resim alanı + Date field Tarih alanı + Digital signature Dijital imza + Stamps Damgalar + Stamp palette Damga paleti + Use a certificate Sertifika kullan + Certificates Sertifikalar + Timestamp Zaman damgası + Use print production Baskı üretimini kullan + Print production Baskı üretimi + Output preview Çıktı önizleme + Preflight Preflight + Convert colors Renkleri dönüştür + Add printer marks Yazıcı işaretleri ekle + Fix hairlines Kılcal çizgileri düzelt + Measure objects Nesneleri ölç + Measure Ölç + Measuring tool Ölçüm aracı + Geospatial location Coğrafi konum + Compare files Dosyaları karşılaştır + Select files to compare Karşılaştırılacak dosyaları seç + Use guided actions Rehberli eylemleri kullan + Actions list Eylem listesi + Action Wizard Eylem Sihirbazı + Make accessible Erişilebilir yap + Prepare for distribution Dağıtım için hazırla + Optimize scanned documents Taranmış belgeleri en iyileştir + Archive documents Belgeleri arşivle + Prepare for accessibility Erişilebilirlik için hazırla + Accessibility Erişilebilirlik + Automatically tag PDF PDF'i otomatik etiketle + Change reading options Okuma seçeneklerini değiştir + Check for accessibility Erişilebilirliği denetle + Add alternate text Alternatif metin ekle + Fix reading order Okuma sırasını düzelt + Apply PDF standards PDF standartlarını uygula + Standards Standartlar + Save as PDF/A PDF/A olarak kaydet + Save as PDF/X PDF/X olarak kaydet + Save as PDF/UA PDF/UA olarak kaydet + Add search index Arama dizini ekle + Index Dizin + Manage embedded index Gömülü dizini yönet + Use JavaScript JavaScript kullan + JavaScript JavaScript + Console Konsol + Document JavaScripts Belge JavaScript'leri + Document actions Belge eylemleri + AI assistant Yapay zeka asistanı + Bring your own model (off by default) Kendi modelini getir (varsayılan olarak kapalı) + Summarize Özetle + Ask about this document Bu belge hakkında soru sor + Translate Çevir + pages sayfa + Planned Planlandı + Planned for milestone Kilometre taşı için planlandı + open to see what it will include neler içereceğini görmek için açın + Optional: needs an AI provider you configure İsteğe bağlı: yapılandıracağınız bir yapay zeka sağlayıcısı gerektirir + Coming in milestone Kilometre taşında gelecek + Items marked Ready work today. Hazır olarak işaretlenen öğeler bugün çalışmaktadır. + Preview Önizleme + Edit fields Alanları düzenle + Try the form as people filling it in will see it Formu dolduran kişilerin göreceği şekilde deneyin + Redact all Tümünü karart + Clear all Tümünü temizle + Rotate page Sayfayı döndür + Delete page Sayfayı sil + Move page Sayfayı taşı + Move pages Sayfaları taşı + Duplicate page Sayfayı çoğalt + Replace page Sayfayı değiştir + Replace pages Sayfaları değiştir + Crop page Sayfayı kırp + Cancel İptal + Don't save Kaydetme + Apply Uygula + Run Çalıştır + Clear Temizle + Sanitize Temizle + Mark all Tümünü işaretle + Save changes to “{name}” before closing? Kapatmadan önce “{name}” üzerindeki değişiklikler kaydedilsin mi? + Your changes will be lost if you don't save them. Kaydetmezseniz değişiklikleriniz kaybolur. + Password required Parola gerekli + “{name}” is protected. Enter a password to open it. “{name}” korumalı. Açmak için bir parola girin. + Password Parola + Incorrect password. Try again. Yanlış parola. Tekrar deneyin. + Recognize Text Metni Tanı + Password protection will be applied when you save Parola koruması kaydettiğinizde uygulanacaktır + Saved the print-ready PDF to {path} Baskıya hazır PDF {path} konumuna kaydedildi + Could not save: {e} Kaydedilemedi: {e} + Earlier in tab order Sekme sırasında daha önce + Later in tab order Sekme sırasında daha sonra + From: Başlangıç: + To: Bitiş: + High Resolution Yüksek Çözünürlük + Print Yazdır + Verify to see what the document needs. Belgenin neye ihtiyacı olduğunu görmek için doğrulayın. + Set Page Boxes Sayfa Kutularını Ayarla + Signature panel İmza paneli + {month} {day}, {y} {day} {month} {y} + The document's security settings don't allow filling in form fields Belgenin güvenlik ayarları form alanlarının doldurulmasına izin vermiyor + {name} is read-only {name} salt okunur + {name} has no action {name} üzerinde eylem yok + Previous month Önceki ay + Next month Sonraki ay + January Ocak + February Şubat + March Mart + April Nisan + May Mayıs + June Haziran + July Temmuz + August Ağustos + September Eylül + October Ekim + November Kasım + December Aralık + Done Bitti + This certificate is in your list of trusted certificates. Bu sertifika güvenilen sertifikalar listenizde yer alıyor. + This certificate is trusted through a certificate above it in the chain. Bu sertifikaya, zincirdeki üst sertifika aracılığıyla güveniliyor. + This certificate is not trusted. Signatures made with it show an unknown identity. Bu sertifikaya güvenilmiyor. Bununla yapılan imzalar bilinmeyen bir kimlik gösterir. + Add to Trusted Certificates Güvenilen Sertifikalara Ekle + Export… Dışa aktar… + PDF Optimizer PDF En İyileştirici + Discard Objects Nesneleri At + Discard User Data Kullanıcı Verilerini At + Clean Up Temizle + Color Images Renkli Resimler + Grayscale Images Gri Tonlamalı Resimler + Bicubic downsampling to Şu değere bikübik alt örnekleme: + for images above bundan büyük resimler için: + Compression Sıkıştırma + ZIP ZIP + Retain existing Mevcut olanı koru + Minimum En Düşük + Low Düşük + High Yüksek + Maximum En Yüksek + Each image is measured where pages draw it; an image is replaced only if the result is smaller. Her resim sayfalarda çizildiği yerde ölçülür; bir resim yalnızca sonuç daha küçükse değiştirilir. + Discard all links, actions and JavaScript Tüm bağlantıları, eylemleri ve JavaScript'i at + Discard alternate images Alternatif resimleri at + Discard embedded page thumbnails Gömülü sayfa küçük resimlerini at + Discard document tags Belge etiketlerini at + Discard embedded print settings Gömülü baskı ayarlarını at + Discard bookmarks Yer işaretlerini at + Discard hidden layer content Gizli katman içeriğini at + Discard all comments, forms and multimedia Tüm yorumları, formları ve multimedyayı at + Discard document information and metadata Belge bilgilerini ve meta verilerini at + Discard all object data (file attachments) Tüm nesne verilerini (dosya ekleri) at + Discard private data of other applications Diğer uygulamaların özel verilerini at + Discard hidden text Gizli metni at + Use Flate to encode streams that are not encoded Kodlanmamış akışları kodlamak için Flate kullan + Remove invalid links and bookmarks Geçersiz bağlantıları ve yer işaretlerini kaldır + Remove unreferenced named destinations Referans verilmeyen adlandırılmış hedefleri kaldır + Compress document structure (object streams) Belge yapısını sıkıştır (nesne akışları) + Remove unused objects and merge identical ones Kullanılmayan nesneleri kaldır ve aynı olanları birleştir + Audit space usage… Alan kullanımını denetle… + Space Audit Alan Denetimi + Bytes Bayt + Content Streams İçerik Akışları + X Object Forms XObject Formları + Structure Info Yapı Bilgisi + Thumbnails Küçük Resimler + Named Destinations Adlandırılmış Hedefler + Color Spaces Renk Uzayları + Shadings Gölgelendirmeler + Patterns (graphics objects) Desenler (grafik nesneleri) + Extended Graphics States Genişletilmiş Grafik Durumları + Embedded Files Gömülü Dosyalar + Document Overhead Belge Ek Yükü + Total Toplam + Text Field Properties Metin Alanı Özellikleri + Check Box Properties Onay Kutusu Özellikleri + Radio Button Properties Radyo Düğmesi Özellikleri + Dropdown Properties Açılır Liste Özellikleri + List Box Properties Liste Kutusu Özellikleri + Button Properties Düğme Özellikleri + Digital Signature Properties Dijital İmza Özellikleri + Position Konum + Options Seçenekler + Validate Doğrula + Calculate Hesapla + Actions Eylemler + This field is locked. Unlock it to change its properties. Bu alan kilitli. Özelliklerini değiştirmek için kilidini açın. + Tooltip: İpucu: + Common Properties Ortak Özellikler + Read Only Salt Okunur + Required Gerekli + Font Size: Yazı Tipi Boyutu: + Auto Otomatik + Text Color: Metin Rengi: + Borders and Colors Kenarlıklar ve Renkler + Border Color: Kenarlık Rengi: + Fill Color: Dolgu Rengi: + No color Renk yok + Beveled Eğimli + Left: Sol: + Bottom: Alt: + Width: Genişlik: + Height: Yükseklik: + Rotation: Döndürme: + Points from the page's bottom-left corner. Sayfanın sol alt köşesinden itibaren nokta cinsinden. + Alignment: Hizalama: + Default Value: Varsayılan Değer: + Multi-line Çok satırlı + Scroll long text Uzun metni kaydır + Allow Rich Text Formatting Zengin Metin Biçimlendirmesine İzin Ver + Limit of Sınır: + characters karakter + Field is used for file selection Alan dosya seçimi için kullanılır + Check spelling Yazım denetimi yap + Comb of Tarak alanı: + Export Value: {on} Dışa Aktarma Değeri: {on} + Check box is checked by default Onay kutusu varsayılan olarak işaretlidir + Button is checked by default Düğme varsayılan olarak seçilidir + Buttons with the same name and value are selected in unison Aynı ada ve değere sahip düğmeler birlikte seçilir + Item: Öğe: + Item List Öğe Listesi + Up Yukarı + Delete {o} {o} sil + Add the choices people pick from. Kullanıcıların seçeceği seçenekleri ekleyin. + Default: Varsayılan: + (none) (yok) + Sort items Öğeleri sırala + Allow user to enter custom text Kullanıcının özel metin girmesine izin ver + Multiple selection Çoklu seçim + Commit selected value immediately Seçilen değeri hemen uygula + Number Sayı + Percentage Yüzde + Time Saat + Special Özel + Select format category: Biçim kategorisini seçin: + The value is shown as typed. Değer yazıldığı gibi gösterilir. + Decimal places: Ondalık basamak: + Separator style: Ayırıcı stili: + Currency symbol: Para birimi simgesi: + Before the number Sayının önünde + Negative number style: Negatif sayı stili: + Format: Biçim: + Custom: Özel: + Zip Code Posta Kodu + Zip Code + 4 Posta Kodu + 4 + Phone Number Telefon Numarası + Social Security Number Sosyal Güvenlik Numarası + Arbitrary Mask İsteğe Bağlı Maske + Mask: Maske: + 9 digit, A letter, O letter or digit, X any character 9 rakam, A harf, O harf veya rakam, X herhangi bir karakter + Example: {sample} Örnek: {sample} + Field value is not validated Alan değeri doğrulanmaz + Field value is in range: Alan değeri aralıkta: + Add an Action Eylem Ekle + Select Trigger: Tetikleyici Seçin: + Mouse Up Fare Düğmesini Bırakma + Mouse Down Fare Düğmesine Basma + Mouse Enter Fareyle Üzerine Gelme + Mouse Exit Farenin Alandan Ayrılması + On Focus Odaklanıldığında + On Blur Odak Kaybolduğunda + Select Action: Eylem Seçin: + Open a web link Web bağlantısı aç + Reset a form Formu sıfırla + Execute a menu item Menü öğesini çalıştır + Show a field Alanı göster + Hide a field Alanı gizle + Submit a form Formu gönder + Fields to reset (comma-separated; empty: all) Sıfırlanacak alanlar (virgülle ayrılmış; boş: tümü) + Print, NextPage, PrevPage, FirstPage or LastPage Print, NextPage, PrevPage, FirstPage veya LastPage + Field names (comma-separated) Alan adları (virgülle ayrılmış) + URL to submit to Gönderilecek URL + Script: Komut Dosyası: + Value: Değer: + No actions Eylem yok + Value is not calculated Değer hesaplanmaz + Value is the Değer şudur: + sum (+) toplam (+) + product (x) çarpım (x) + average ortalama + minimum en küçük + maximum en büyük + of the following fields: aşağıdaki alanların: + Simplified field notation: Basitleştirilmiş alan gösterimi: + Field names with + - * / and parentheses; put \\ before spaces in names. Adlarda + - * / ve parantez içeren alan adları; adlardaki boşluklardan önce \\ koyun. + Format text Metni biçimlendir + Bold Kalın + Italic İtalik + Align left Sola hizala + Centre Ortala + Align right Sağa hizala + Justify İki yana yasla + Standard fonts; text outside Windows-1252 isn't supported yet. Standart yazı tipleri; Windows-1252 dışındaki metinler henüz desteklenmiyor. + Edit image Resmi düzenle + Rotate counterclockwise Saat yönünün tersine döndür + Rotate clockwise Saat yönünde döndür + Flip horizontal Yatay çevir + Flip vertical Dikey çevir + Replace image Resmi değiştir + Crop (% trimmed from each side) Kırp (her kenardan kırpılan %) + Click on the page to add text; each click starts a new box, and ✓ finishes. Drag items to move them, drag a corner to resize, double-click text to edit it. Metin eklemek için sayfaya tıklayın; her tıklama yeni bir kutu başlatır ve ✓ tamamlar. Taşımak için öğeleri sürükleyin, yeniden boyutlandırmak için bir köşeden sürükleyin, düzenlemek için metne çift tıklayın. + Choose an image Bir resim seçin + Export to Image Resim Olarak Dışa Aktar + Export to Text Metin Olarak Dışa Aktar + Export All Images Tüm Resimleri Dışa Aktar + Resolution Çözünürlük + Format Biçim + Quality Kalite + One {f} file per page, named after the document. Belgenin adını taşıyan sayfa başına bir {f} dosyası. + Exclude images smaller than Şundan küçük resimleri hariç tut: + No limit Sınır yok + Each image once, named after the document and page. JPEG images are saved unchanged; others as PNG. Her resim belge ve sayfaya göre adlandırılarak bir kez. JPEG resimler değiştirilmeden, diğerleri PNG olarak kaydedilir. + Plain text in reading order; pages are separated by form feeds. Okuma sırasına göre düz metin; sayfalar sayfa sonlarıyla ayrılır. + Create {what} {what} oluştur + initials paraf + signature imza + Create Custom Stamp Özel Damga Oluştur + From {file} {file} dosyasından + Category: Kategori: + e.g. My stamps ör. Damgalarım + Name: Ad: + Custom Özel + Delete stamp Damgayı sil + Create custom stamp… Özel damga oluştur… + From a PDF page or an image Bir PDF sayfasından veya resimden + Line spacing Satır aralığı + Character spacing Karakter aralığı + Horizontal scale Yatay ölçek + Rotate Clockwise Saat Yönünde Döndür + Rotate Counterclockwise Saat Yönünün Tersine Döndür + Flip Horizontal Yatay Çevir + Flip Vertical Dikey Çevir + Replace Image… Resmi Değiştir… + Save Image As… Resmi Farklı Kaydet… + Check for updates Güncellemeleri denetle + No check has run yet. Henüz denetim yapılmadı. + Checking for a newer version… Daha yeni bir sürüm denetleniyor… + PdfCraft {v} is available. PdfCraft {v} kullanılabilir. + You have version {c}. Download the new version from its release page. {c} sürümüne sahipsiniz. Yeni sürümü sürüm sayfasından indirin. + PdfCraft {c} is up to date. PdfCraft {c} güncel. + Couldn't check for updates: {e} Güncellemeler denetlenemedi: {e} + You have version {c}. All releases are listed at {page}. {c} sürümüne sahipsiniz. Tüm sürümler {page} adresinde listelenmiştir. + Asks GitHub for the latest release. Nothing is downloaded or installed automatically. En son sürüm için GitHub'ı sorgular. Hiçbir şey otomatik olarak indirilmez veya kurulmaz. + Download İndir + Later Daha sonra + none yok + Declared conformance: {shown} Bildirilen uyumluluk: {shown} + Output intent: {list} Çıktı amacı: {list} + Conformance level: Uyumluluk düzeyi: + No problems found: the document conforms to {level}. Sorun bulunamadı: Belge {level} standardına uygundur. + (page {p}) (sayfa {p}) + · not fixable here · burada düzeltilemez + Verify Doğrula + Box Kutu + Units Birimler + Set to zero Sıfırla + Current page ({n}) Geçerli sayfa ({n}) + Subset Alt küme + Update Güncelle + Add Ekle + {verb} Header and Footer Üstbilgi ve Altbilgi: {verb} + {verb} Watermark Filigran: {verb} + {verb} Background Arka Plan: {verb} + Margins (in) Kenar boşlukları (inç) + Left Header Text Sol Üstbilgi Metni + Center Header Text Orta Üstbilgi Metni + Right Header Text Sağ Üstbilgi Metni + Left Footer Text Sol Altbilgi Metni + Center Footer Text Orta Altbilgi Metni + Right Footer Text Sağ Altbilgi Metni + Insert Page Number Sayfa Numarası Ekle + Insert Date Tarih Ekle + Start page number Başlangıç sayfa numarası + Source Kaynak + Fit to page Sayfaya sığdır + Rotation Döndürme + Appear behind page Sayfanın arkasında göster + Appear on top of page Sayfanın üstünde göster + From colour Renginden + Page Range Options Sayfa Aralığı Seçenekleri + Preview: page {p} of {n} Önizleme: Sayfa {p} / {n} + Applies to 1 page 1 sayfaya uygulanır + Applies to {n} pages {n} sayfaya uygulanır + Replaces the existing one on those pages. Bu sayfalardaki mevcut olanın yerini alır. + PDF or image PDF veya resim + No file chosen Dosya seçilmedi + Page number Sayfa numarası + Scale relative to target page Hedef sayfaya göre ölçekle + Link Properties Bağlantı Özellikleri + Create Link Bağlantı Oluştur + Link Appearance Bağlantı Görünümü + Link Type: Bağlantı Türü: + Visible Rectangle Görünür Dikdörtgen + Invisible Rectangle Görünmez Dikdörtgen + Highlight Style: Vurgu Stili: + Invert Tersine Çevir + Outline Ana Hat + Inset İçe Göçük + Line Thickness: Çizgi Kalınlığı: + Thin İnce + Thick Kalın + Line Style: Çizgi Stili: + Solid Düz + Dashed Kesikli + Color: Renk: + Link Action Bağlantı Eylemi + Open a web page Web sayfası aç + Go to a page view Sayfa görünümüne git + Type the web address. Web adresini yazın. + This document has no signatures. Bu belgede imza yok. + The Keychain's digital IDs couldn't be listed: {e} Anahtar Zinciri'nin dijital kimlikleri listelenemedi: {e} + Signed and saved to {path} İmzalandı ve {path} konumuna kaydedildi + Couldn't open revision {n}: {e} {n} düzeltmesi açılamadı: {e} + Couldn't open the signed version: {e} İmzalanan sürüm açılamadı: {e} + Sign with a Digital ID Dijital Kimlik ile İmzala + Choose the digital ID that you want to use for signing: İmzalamak için kullanmak istediğiniz dijital kimliği seçin: + Keychain · Anahtar Zinciri · + Windows store · Windows depolama alanı · + Issued by: Veren: + , Expires: , Son kullanma tarihi: + No digital IDs yet. Henüz dijital kimlik yok. + Configure New Digital ID Yeni Dijital Kimlik Yapılandır + Continue Devam Et + Configure a Digital ID for Signing İmzalama için Dijital Kimlik Yapılandır + Use a Digital ID from a file Dosyadan Dijital Kimlik Kullan + Create a new Digital ID (self-signed, saved to a password-protected file) Yeni bir Dijital Kimlik oluştur (kendinden imzalı, parola korumalı bir dosyaya kaydedilir) + Name Ad + Organizational Unit Kurumsal Birim + Organization Name Kurum Adı + Email Address E-posta Adresi + Country/Region Ülke/Bölge + Key Algorithm Anahtar Algoritması + Confirm Password Parolayı Onayla + Valid for 5 years, for digital signatures. Self-signed IDs are usually not trusted by others. Dijital imzalar için 5 yıl geçerlidir. Kendinden imzalı kimliklere genellikle başkaları tarafından güvenilmez. + File (.p12, .pfx) Dosya (.p12, .pfx) + Digital ID Dijital Kimlik + Browse… Gözat… + {verb} as "{name}" "{name}" olarak {verb} + Certify Sertifikala + The signature will be invisible. İmza görünmez olacak. + Distinguished name Ayırt edici ad + Labels Etiketler + Permitted actions after certifying Sertifikalandırma sonrası izin verilen eylemler + No changes allowed Değişikliğe izin verilmez + Annotations, form fill-in, and digital signatures Açıklamalar, form doldurma ve dijital imzalar + Form fill-in and digital signatures Form doldurma ve dijital imzalar + The key is in the macOS Keychain, which may ask to allow PdfCraft to use it. Anahtar macOS Anahtar Zinciri'ndedir ve PdfCraft'ın kullanmasına izin vermenizi isteyebilir. + The key is in the Windows certificate store, which may ask to allow PdfCraft to use it. Anahtar Windows sertifika deposundadır ve PdfCraft'ın kullanmasına izin vermenizi isteyebilir. + Digital ID password Dijital kimlik parolası + Back Geri + Validate all Tümünü doğrula + Sign this field… Bu alanı imzala… + Signature is valid: İmza geçerli: + Signature validity is UNKNOWN: İmza geçerliliği BİLİNMİYOR: + Signature is INVALID: İmza GEÇERSİZ: + Signature Details İmza Ayrıntıları + Reason Neden + Signing time İmzalama zamanı + Algorithm Algoritma + Signer İmzalayan + Issued by Veren + Valid Geçerli + Field Alan + Field: {field} on page {p} Sayfa {p} üzerindeki alan: {field} + Add to trusted certificates Güvenilen sertifikalara ekle + View signed version İmzalanmış sürümü görüntüle + Show certificate… Sertifikayı göster… + Export certificate… Sertifikayı dışa aktar… + Certificate Viewer Sertifika Görüntüleyici + This dialog shows the details of a certificate and its chain. Bu iletişim kutusu bir sertifikanın ve zincirinin ayrıntılarını gösterir. + Trust Güven + Issued to Verilen kişi/kurum + Valid from Geçerlilik başlangıcı + Valid to Geçerlilik bitişi + Intended usage Kullanım amacı + Any Herhangi biri + Version Sürüm + Serial number Seri numarası + Issuer Veren + Validity starts Geçerlilik başlangıcı + Validity ends Geçerlilik bitişi + Public key Ortak anahtar + Key usage Anahtar kullanımı + Not present Mevcut değil + Self-signed Kendinden imzalı + Edit Action Eylemi Düzenle + New Action Yeni Eylem + Action name: Eylem adı: + Description: Açıklama: + Steps Adımlar + Add the steps this action runs, in order. Bu eylemin sırasıyla çalıştıracağı adımları ekleyin. + Add Step Adım Ekle + An action with this name already exists. Bu adda bir eylem zaten var. + New Action… Yeni Eylem… + Edit… Düzenle… + Add watermark Filigran ekle + Add header Üstbilgi ekle + Add footer Altbilgi ekle + Set document title Belge başlığını ayarla + Run a JavaScript Bir JavaScript çalıştır + Prepare for Distribution Dağıtım için Hazırla + Remove hidden information and flatten comments and form fields, so recipients see only the pages. Gizli bilgileri kaldırın ve yorumları ile form alanlarını düzleştirin, böylece alıcılar yalnızca sayfaları görür. + Optimize Scanned Documents Taranmış Belgeleri En İyileştir + Make scans searchable, then make them smaller. Taramaları aranabilir yapın, ardından boyutlarını küçültün. + Add Page Numbers Sayfa Numaraları Ekle + Number every page in the footer. Altbilgide her sayfayı numaralandırın. + Mark as Confidential Gizli Olarak İşaretle + A diagonal CONFIDENTIAL watermark on every page. Her sayfada köşegen GİZLİ filigranı. + Sanitize Documents Belgeleri Temizle + Remove hidden information, metadata, scripts and embedded content, and rewrite the files. Gizli bilgileri, meta verileri, komut dosyalarını ve gömülü içeriği kaldırın ve dosyaları yeniden yazın. + Type text Metin yazın + Text to insert Eklenecek metin + Inserted text Eklenen metin + Replacement text Değiştirilen metin + Edit text… Metni düzenle… + Add a sticky note here Buraya bir not ekle + {kind} Properties {kind} Özellikleri + Appearance Görünüm + General Genel + Review History İnceleme Geçmişi + This comment's appearance can't be changed yet. Bu yorumun görünümü henüz değiştirilemiyor. + Icon Simge + Thickness Kalınlık + Line ending Çizgi sonu + Open arrow Açık ok + Closed arrow Kapalı ok + Butt Düz uç + Reverse open arrow Ters açık ok + Reverse closed arrow Ters kapalı ok + Slash Eğik çizgi + Modified Değiştirildi + No status has been set. Herhangi bir durum ayarlanmadı. + Compare Files Dosyaları Karşılaştır + Old file: Eski dosya: + Open the older version first Önce eski sürümü açın + New file: Yeni dosya: + Text is compared word by word; the differences are shown on the new file. Metin sözcük sözcük karşılaştırılır; farklılıklar yeni dosyada gösterilir. + Compare this file with an older version: Compare files… in All tools. Bu dosyayı daha eski bir sürümle karşılaştırın: Tüm araçlar bölümündeki Dosyaları karşılaştır… aracını kullanın. + No differences in the text. Metinde fark yok. + {n} {kind} {n} {kind} + Inserted Eklendi + Deleted Silindi + Replaced Değiştirildi + Visual Görsel + Mark as comments Yorum olarak işaretle + Report… Rapor… + {kind} · page {p} {kind} · sayfa {p} + Set Alternate Text Alternatif Metni Ayarla + This document has no tagged figures. Bu belgede etiketli şekil yok. + on page {p} sayfa {p} üzerinde + Figure {i} of {n}{page} Şekil {i} / {n}{page} + No preview Önizleme yok + Alternate text Alternatif metin + Describe the figure Şekli tanımlayın + Decorative figure Dekoratif şekil + Previous figure Önceki şekil + Next figure Sonraki şekil + Save & Close Kaydet ve Kapat + Accessibility Checker Options Erişilebilirlik Denetleyicisi Seçenekleri + Report Options Rapor Seçenekleri + Create accessibility report Erişilebilirlik raporu oluştur + Page Range Sayfa Aralığı + All pages in document Belgedeki tüm sayfalar + Checking Options ({on} of 32 in all categories) Denetim Seçenekleri (tüm kategorilerde {on} / 32) + Show this dialog when the Checker starts Denetleyici başladığında bu iletişim kutusunu göster + Select All Tümünü Seç + Clear All Tümünü Temizle + Start Checking Denetimi Başlat + This document hasn't been checked yet. Bu belge henüz denetlenmedi. + No issues found Sorun bulunamadı + 1 issue 1 sorun + {n} issues {n} sorun + {label} (1 issue) {label} (1 sorun) + {label} ({n} issues) {label} ({n} sorun) + Check again Tekrar denetle + Report Rapor + Show the accessibility report Erişilebilirlik raporunu göster + Collapse Daralt + Expand Genişlet + {rule} - {status} {rule} - {status} + Passed Geçti + Failed Başarısız + Needs manual check Manuel denetim gerekiyor + Skipped Atlandı + Fix Düzelt + Skip Rule Kuralı Atla + Explain Açıkla + Check Again Tekrar Denetle + Show Report Raporu Göster + Options… Seçenekler… + Go to the page Sayfaya git + Page Content Sayfa İçeriği + Forms Formlar + Alternate Text Alternatif Metin + Tables Tablolar + Lists Listeler + Headings Başlıklar + Accessibility permission flag Erişilebilirlik izin bayrağı + Accessibility permission flag is set Erişilebilirlik izin bayrağı ayarlandı + Security settings must allow assistive technology to read the content ("Enable text access for screen reader devices"). Change the document's security to allow it. Güvenlik ayarları yardımcı teknolojilerin içeriği okumasına izin vermelidir ("Ekran okuyucu aygıtları için metin erişimini etkinleştir"). İzin vermek için belgenin güvenliğini değiştirin. + Image-only PDF Yalnızca resim içeren PDF + Document is not image-only PDF Belge yalnızca resim içeren PDF değil + Pages that are only pictures of text can't be read aloud or searched. Recognize text (OCR) to add a text layer. Yalnızca metin resimlerinden oluşan sayfalar sesli okunamıyor veya aranamıyor. Metin katmanı eklemek için metni tanıyın (OCR). + Document is tagged PDF Belge etiketli PDF + Tags give the document a logical structure that assistive technology follows. Tag the document (Automatically tag PDF). Etiketler belgeye yardımcı teknolojilerin takip ettiği mantıksal bir yapı kazandırır. Belgeyi etiketleyin (PDF'i otomatik etiketle). + Logical Reading Order Mantıksal Okuma Sırası + Document structure provides a logical reading order Belge yapısı mantıksal bir okuma sırası sağlar + Check that the tags read in the order a sighted reader would. Use the Reading Order tool or the Tags panel to correct it. Etiketlerin gören bir okuyucunun okuyacağı sırada okunduğunu denetleyin. Düzeltmek için Okuma Sırası aracını veya Etiketler panelini kullanın. + Primary language Birincil dil + Text language is specified Metin dili belirtildi + Screen readers choose their voice from the document's language. Set it in Document Properties > Advanced > Language. Ekran okuyucular seslerini belgenin diline göre seçer. Bunu Belge Özellikleri > Gelişmiş > Dil bölümünden ayarlayın. + Document title is showing in title bar Belge başlığı başlık çubuğunda gösteriliyor + The window should show a meaningful title rather than the file name. Set a title and Initial View > Show: Document Title. Pencere, dosya adı yerine anlamlı bir başlık göstermelidir. Bir başlık ayarlayın ve İlk Görünüm > Göster: Belge Başlığı'nı seçin. + Bookmarks are present in large documents Büyük belgelerde yer işaretleri mevcut + Documents of more than 20 pages need bookmarks for navigation. Add bookmarks for the main sections. 20 sayfadan fazla olan belgelerde gezinme için yer işaretleri gerekir. Ana bölümler için yer işaretleri ekleyin. + Color contrast Renk kontrastı + Document has appropriate color contrast Belge uygun renk kontrastına sahip + Text must contrast enough with its background for people with low vision. Check the colours by eye or with a contrast tool. Az gören kişiler için metnin arka planıyla yeterince kontrast oluşturması gerekir. Renkleri gözle veya bir kontrast aracıyla denetleyin. + Tagged content Etiketli içerik + All page content is tagged Tüm sayfa içeriği etiketli + Everything on a page must be tagged or marked as an artifact (decoration, headers, footers). Tag the untagged content. Bir sayfadaki her şey etiketlenmeli veya yapay öğe (dekorasyon, üstbilgi, altbilgi) olarak işaretlenmelidir. Etiketsiz içeriği etiketleyin. + Tagged annotations Etiketli açıklamalar + All annotations are tagged Tüm açıklamalar etiketli + Comments, links and fields must be in the tag tree to be announced. Tag the annotations. Seslendirilebilmesi için yorumların, bağlantıların ve alanların etiket ağacında olması gerekir. Açıklamaları etiketleyin. + Tab order Sekme sırası + Tab order is consistent with structure order Sekme sırası yapı sırasıyla tutarlı + Pages with links, comments or fields should tab in structure order. The fix sets each page's tab order to Use Document Structure. Bağlantı, yorum veya alan içeren sayfalar yapı sırasına göre sekmelenmelidir. Düzeltme, her sayfanın sekme sırasını Belge Yapısını Kullan olarak ayarlar. + Character encoding Karakter kodlaması + Reliable character encoding is provided Güvenilir karakter kodlaması sağlandı + Fonts must map their characters to Unicode so text can be read and copied. Fix the fonts or recreate the PDF. Metnin okunabilmesi ve kopyalanabilmesi için yazı tiplerinin karakterlerini Unicode ile eşlemesi gerekir. Yazı tiplerini düzeltin veya PDF'i yeniden oluşturun. + Tagged multimedia Etiketli multimedya + All multimedia objects are tagged Tüm multimedya nesneleri etiketli + Video, sound and rich media must be tagged to be announced. Tag the multimedia annotations. Seslendirilebilmesi için video, ses ve zengin medyanın etiketlenmesi gerekir. Multimedya açıklamalarını etiketleyin. + Screen flicker Ekran titremesi + Page will not cause screen flicker Sayfa ekran titremesine neden olmaz + Animation that flashes can cause seizures. Check scripts and media for flicker. Yanıp sönen animasyon nöbetlere neden olabilir. Titreme olup olmadığını görmek için komut dosyalarını ve medyayı denetleyin. + Scripts Komut Dosyaları + No inaccessible scripts Erişilemeyen komut dosyası yok + Timed responses Zamanlanmış yanıtlar + Page does not require timed responses Sayfa zamanlanmış yanıt gerektirmiyor + People may need more time to respond. Check that nothing has a time limit. Kullanıcıların yanıt vermek için daha fazla zamana ihtiyacı olabilir. Hiçbir şeyin zaman sınırı olmadığını denetleyin. + Navigation links Gezinme bağlantıları + Navigation links are not repetitive Gezinme bağlantıları yinelenmiyor + Repeated navigation links should be skippable. Check links that appear on every page. Yinelenen gezinme bağlantıları atlanabilir olmalıdır. Her sayfada görünen bağlantıları denetleyin. + Tagged form fields Etiketli form alanları + All form fields are tagged Tüm form alanları etiketli + Form fields must be in the tag tree (Form tags) to be announced. Tag the form fields. Seslendirilebilmesi için form alanlarının etiket ağacında (Form etiketleri) olması gerekir. Form alanlarını etiketleyin. + Field descriptions Alan açıklamaları + All form fields have description Tüm form alanlarının açıklaması var + Each field needs a tooltip that says what to enter. Add one in Field Properties > General > Tooltip. Her alanın ne girileceğini belirten bir ipucuna ihtiyacı vardır. Alan Özellikleri > Genel > İpucu bölümünden bir tane ekleyin. + Figures alternate text Şekil alternatif metni + Figures require alternate text Şekiller alternatif metin gerektirir + Pictures need alternate text that describes them, or must be marked decorative. Add alternate text to each figure. Resimlerin bunları tanımlayan bir alternatif metne ihtiyacı vardır veya dekoratif olarak işaretlenmelidir. Her şekle alternatif metin ekleyin. + Nested alternate text İç içe alternatif metin + Alternate text that will never be read Hiçbir zaman okunmayacak alternatif metin + Alternate text inside an element that already has alternate text is never read. Remove the inner alternate text. Zaten alternatif metni olan bir öğenin içindeki alternatif metin hiçbir zaman okunmaz. İçteki alternatif metni kaldırın. + Associated with content İçerikle ilişkili + Alternate text must be associated with some content Alternatif metin bir içerikle ilişkili olmalıdır + Alternate text on an element without page content is never reached. Remove it or attach the element to content. Sayfa içeriği olmayan bir öğedeki alternatif metne hiçbir zaman ulaşılamaz. Kaldırın veya öğeyi içeriğe ekleyin. + Hides annotation Açıklamayı gizler + Alternate text should not hide annotation Alternatif metin açıklamayı gizlememelidir + Alternate text on an element that contains a link or field hides that annotation from assistive technology. Move the alternate text. Bağlantı veya alan içeren bir öğedeki alternatif metin, bu açıklamayı yardımcı teknolojiden gizler. Alternatif metni taşıyın. + Other elements alternate text Diğer öğelerin alternatif metni + Other elements that require alternate text Alternatif metin gerektiren diğer öğeler + Formulas and similar non-text elements need alternate text. Add alternate text to them. Formüller ve benzeri metin dışı öğeler alternatif metne ihtiyaç duyar. Bunlara alternatif metin ekleyin. + Rows Satırlar + TR must be a child of Table, THead, TBody, or TFoot TR; Table, THead, TBody veya TFoot öğesinin alt öğesi olmalıdır + Table cells belong in a row. Fix the table's tags. Tablo hücreleri bir satıra aittir. Tablonun etiketlerini düzeltin. + Headers Üstbilgiler + Tables should have headers Tabloların üstbilgileri olmalıdır + Header cells (TH) tell listeners what each column or row holds. Mark the header cells. Üstbilgi hücreleri (TH) dinleyicilere her sütunun veya satırın ne içerdiğini bildirir. Üstbilgi hücrelerini işaretleyin. + Regularity Düzenlilik + Tables must contain the same number of columns in each row and rows in each column Tablolar her satırda aynı sayıda sütun ve her sütunda aynı sayıda satır içermelidir + Irregular tables confuse cell navigation. Give every row the same number of cells (counting spans). Düzensiz tablolar hücre gezintisini zorlaştırır. Her satıra aynı sayıda hücre verin (birleştirmeleri sayarak). + Summary Özet + Tables must have a summary Tabloların bir özeti olmalıdır + A summary describes the table's purpose and layout. Add a Summary attribute to each table. Özet, tablonun amacını ve düzenini açıklar. Her tabloya bir Summary özniteliği ekleyin. + List items Liste öğeleri + LI must be a child of L LI, L öğesinin alt öğesi olmalıdır + List items belong in a list. Fix the list's tags. Liste öğeleri bir listeye aittir. Listenin etiketlerini düzeltin. + Lbl and LBody Lbl ve LBody + Lbl and LBody must be children of LI Lbl ve LBody, LI öğesinin alt öğeleri olmalıdır + List labels and bodies belong in a list item. Fix the list's tags. Liste etiketleri ve gövdeleri bir liste öğesine aittir. Listenin etiketlerini düzeltin. + Appropriate nesting Uygun iç içe yerleştirme + Heading levels shouldn't skip (an H1 followed by an H3). Retag the headings. Başlık düzeyleri atlanmamalıdır (örneğin H1 ardından H3). Başlıkları yeniden etiketleyin. + TH and TD TH ve TD + TH and TD must be children of TR TH ve TD, TR öğesinin alt öğeleri olmalıdır + Mark Page Range Sayfa Aralığını İşaretle + Pages from Sayfa aralığı: + Single word or phrase Tek sözcük veya kelime öbeği + Text to find Aranacak metin + Patterns Kalıplar + Phone Numbers Telefon Numaraları + Email Addresses E-posta Adresleri + Credit Cards Kredi Kartları + Social Security Numbers Sosyal Güvenlik Numaraları + Dates Tarihler + Text in images isn't found; recognise text (OCR) first. Resimlerdeki metin bulunamaz; önce metni tanıyın (OCR). + No matches. Eşleşme yok. + {n} match(es) marked for redaction. Karartma için {n} eşleşme işaretlendi. + Multiple words or phrases Birden çok sözcük veya ifade + One word or phrase per line Her satıra bir sözcük veya ifade + Import list… Listeyi içe aktar… + {n} word(s) or phrase(s) {n} sözcük veya ifade + {name} is too large for a word list (at most 1 MB). {name} sözcük listesi için çok büyük (en fazla 1 MB). + Redaction Tool Properties Karartma Aracı Özellikleri + Redacted area fill colour: Karartılan alan dolgu rengi: + Use overlay text Üst metin kullan + Custom text: Özel metin: + Redaction code: Karartma kodu: + Font: Yazı tipi: + Font size: Yazı tipi boyutu: + Auto-size text to fit redaction region Metni karartma bölgesine sığacak şekilde otomatik boyutlandır + Font colour: Yazı tipi rengi: + Repeat overlay text Üst metni yinele + Text alignment: Metin hizalama: + Center Ortala + Apply redactions Karartmaları uygula + You are about to apply 1 redaction mark. Text, images and drawings under the marks, and comments and form fields that overlap them, are removed permanently. 1 karartma işaretini uygulamak üzeresiniz. İşaretlerin altındaki metin, resim ve çizimler ile bunlarla örtüşen yorumlar ve form alanları kalıcı olarak kaldırılacaktır. + You are about to apply {n} redaction marks. Text, images and drawings under the marks, and comments and form fields that overlap them, are removed permanently. {n} karartma işaretini uygulamak üzeresiniz. İşaretlerin altındaki metin, resim ve çizimler ile bunlarla örtüşen yorumlar ve form alanları kalıcı olarak kaldırılacaktır. + Save the document afterwards: saving rewrites the whole file so no trace of the removed content stays in it. Ardından belgeyi kaydedin: Kaydetme tüm dosyayı yeniden yazar, böylece kaldırılan içeriğin hiçbir izi kalmaz. + Select the items to remove from this document. Bu belgeden kaldırılacak öğeleri seçin. + Metadata Meta veriler + File attachments Dosya ekleri + Comments and markups Yorumlar ve işaretlemeler + Hidden text Gizli metin + Hidden layers Gizli katmanlar + Links, actions and JavaScripts Bağlantılar, eylemler ve JavaScript'ler + Private data of other applications Diğer uygulamaların özel verileri + None found Hiçbir şey bulunamadı + Form fields are flattened: their values stay visible. Saving rewrites the whole file. Form alanları düzleştirilir: Değerleri görünür kalır. Kaydetme tüm dosyayı yeniden yazar. + Remove Kaldır + Sanitizing removes hidden information from the document: metadata, file attachments, comments, form fields (flattened), hidden text and layers, bookmarks, links, actions and scripts, and private application data. Temizleme, belgedeki gizli bilgileri kaldırır: meta veriler, dosya ekleri, yorumlar, form alanları (düzleştirilmiş), gizli metinler ve katmanlar, yer işaretleri, bağlantılar, eylemler ve komut dosyaları ile özel uygulama verileri. + Save the document afterwards; saving rewrites the whole file so nothing removed stays in it. Ardından belgeyi kaydedin; kaydetme tüm dosyayı yeniden yazar, böylece kaldırılan hiçbir şey kalmaz. + Printer: Yazıcı: + Save as PDF PDF olarak kaydet + {name} (default) {name} (varsayılan) + Copies: Kopya: + Collate Harmanla + Print in grayscale Gri tonlamalı yazdır + Two-sided: Çift taraflı: + Off Kapalı + Flip on long edge Uzun kenardan çevir + Flip on short edge Kısa kenardan çevir + Paper: Kağıt: + Pages to Print Yazdırılacak Sayfalar + More options: Diğer seçenekler: + All pages in range Aralıktaki tüm sayfalar + Odd pages only Yalnızca tek sayfalar + Even pages only Yalnızca çift sayfalar + Reverse pages Sayfaları ters sırala + Page Sizing & Handling Sayfa Boyutlandırma ve İşleme + Size Boyut + Poster Poster + Multiple Birden çok + Booklet Kitapçık + Fit Sığdır + Shrink oversized pages Büyük sayfaları küçült + Custom scale: Özel ölçek: + Tile scale: Döşeme ölçeği: + Overlap: Örtüşme: + Cut marks Kesim işaretleri + Pages per sheet: Yaprak başına sayfa: + Page order: Sayfa sırası: + Horizontal Yatay + Horizontal reversed Yatay ters + Vertical Dikey + Vertical reversed Dikey ters + Print page border Sayfa kenarlığını yazdır + Auto-rotate pages Sayfaları otomatik döndür + Booklet subset: Kitapçık alt kümesi: + Both sides Her iki taraf + Front side only Yalnızca ön taraf + Back side only Yalnızca arka taraf + Binding: Ciltleme: + Left Sol + Right Sağ + Orientation: Yönlendirme: + Auto portrait/landscape Otomatik dikey/yatay + Portrait Dikey + Landscape Yatay + Comments & Forms Yorumlar ve Formlar + Document Belge + Document and markups Belge ve işaretlemeler + Document and stamps Belge ve damgalar + Form fields only Yalnızca form alanları + Previous sheet Önceki yaprak + Next sheet Sonraki yaprak + No sheets Yaprak yok + Sheet {s} of {n} Yaprak {s} / {n} + Removed 1 hidden item. Save to remove them from the file. 1 gizli öğe kaldırıldı. Dosyadan kaldırmak için kaydedin. + Removed {n} hidden items. Save to remove them from the file. {n} gizli öğe kaldırıldı. Dosyadan kaldırmak için kaydedin. + Document sanitized. Save to finish: saving rewrites the whole file. Belge temizlendi. Bitirmek için kaydedin: kaydetme tüm dosyayı yeniden yazar. + Applied 1 redaction mark. Save to remove the content from the file. 1 karartma işareti uygulandı. İçeriği dosyadan kaldırmak için kaydedin. + Applied {n} redaction marks. Save to remove the content from the file. {n} karartma işareti uygulandı. İçeriği dosyadan kaldırmak için kaydedin. + JavaScript Console JavaScript Konsolu + JavaScript is turned off (Preferences ▸ JavaScript). JavaScript kapalı (Tercihler > JavaScript). + Output appears here. Çıktı burada görünür. + Script Name: Komut Dosyası Adı: + This document has no document-level scripts. Bu belgede belge düzeyinde komut dosyası yok. + No form fields were detected Hiçbir form alanı algılanmadı + Detected 1 form field 1 form alanı algılandı + Detected {n} form fields {n} form alanı algılandı + {field}: {e} {field}: {e} + Current page Geçerli sayfa + Selected pages Seçilen sayfalar + Document language Belge dili + Output Çıktı + Searchable Image (Exact) Aranabilir Resim (Tam) + Adds invisible text over each word; the page image is not changed Her sözcüğün üzerine görünmez metin ekler; sayfa resmi değiştirilmez + Downsample to Şu değere alt örnekle: + Text recognition isn't installed: its model files are missing. Reinstall PdfCraft, or set PDFCRAFT_MODELS to the folder that holds them. Metin tanıma yüklü değil: model dosyaları eksik. PdfCraft'ı yeniden yükleyin veya PDFCRAFT_MODELS ortam değişkenini bunları içeren klasöre ayarlayın. + Settings Ayarlar + Optional İsteğe bağlı + Planned · {m} Planlandı · {m} + Planned for milestone {m} — open to see what it will include Kilometre taşı {m} için planlandı — neler içereceğini görmek için açın + Coming in milestone {m}. Items marked Ready work today. Kilometre taşı {m} ile gelecek. Hazır olarak işaretlenen öğeler bugün çalışmaktadır. + 1 mark 1 işaret + {n} marks {n} işaret + Dynamic Dinamik + Sign Here Burayı İmzalayın + Standard Business Standart İş + Choose a stamp, then click on the page to place it. Bir damga seçin, ardından yerleştirmek için sayfaya tıklayın. + Note Not + Polyline Çoklu Çizgi + Attachment Ek + Callout Açıklama Balonu + Typewriter Daktilo + Checked İşaretli + Unchecked İşaretsiz + Marked İşaretlendi + Locked Kilitli + Search comments Yorumları ara + Add a comment Yorum ekle + No comments yet. Henüz yorum yok. + Pick a tool in the quick bar, or type above. Hızlı çubuktan bir araç seçin veya yukarıya yazın. + No comments match. Eşleşen yorum yok. + Unknown author Bilinmeyen yazar + No colour Renk yok + Remove checkmark Onay işaretini kaldır + Mark with checkmark Onay işaretiyle işaretle + Reply Yanıtla + Add a reply Yanıt ekle + Post Gönder + Page {label} Sayfa {label} + {kind} — click to show it on the page {kind} — sayfada göstermek için tıklayın + Set status Durumu ayarla + Copy text Metni kopyala + Properties… Özellikler… + Make Current Properties Default Geçerli Özellikleri Varsayılan Yap + More options Diğer seçenekler + Sort by Sırala + Author Yazar + Type Tür + Colour Renk + Show all comments Tüm yorumları göster + Create PDF comment summary… PDF yorum özeti oluştur… + Export all to data file… Tümünü veri dosyasına aktar… + Filter comments Yorumları filtrele + Status Durum + Accepted Kabul edildi + Rejected Reddedildi + Cancelled İptal edildi + Completed Tamamlandı + Show all Tümünü göster + Yellow Sarı + Orange Turuncu + Red Kırmızı + Pink Pembe + Purple Mor + Blue Mavi + Light blue Açık mavi + Green Yeşil + Gray Gri + Black Siyah + Bookmark options Yer işareti seçenekleri + Expand all bookmarks Tüm yer işaretlerini genişlet + Expand top-level bookmarks Üst düzey yer işaretlerini genişlet + Collapse all bookmarks Tüm yer işaretlerini daralt + New bookmark ({key}) Yeni yer işareti ({key}) + This document has no bookmarks. Bu belgede yer işareti yok. + This document has no layers. Bu belgede katman yok. + This document has no attachments. Bu belgede ek yok. + Hide layer Katmanı gizle + Show layer Katmanı göster + on page {label} sayfa {label} üzerinde + Save attachment… Eki kaydet… + Open in a new tab Yeni sekmede aç + This document has no pages. Bu belgede sayfa yok. + Find text Metin bul + No matches Eşleşme yok + Searching… {s}/{p} Aranıyor… {s}/{p} + {c} of {n}{more} {c} / {n}{more} + Previous ({key}) Önceki ({key}) + Next ({key}) Sonraki ({key}) + Find options Bulma seçenekleri + Whole words only Yalnızca tam sözcükler + Case-sensitive Büyük/küçük harfe duyarlı + Close (Esc) Kapat (Esc) + Align Hizala + Top Üst + Bottom Alt + Center horizontally Yatay ortala + Center vertically Dikey ortala + Distribute Dağıt + Horizontally Yatay olarak + Vertically Dikey olarak + Set Fields to Same Size Alanları Aynı Boyuta Ayarla + Height Yükseklik + Width Genişlik + Both Her İkisi + Duplicate… Çoğalt… + All pages Tüm sayfalar + Odd pages Tek sayfalar + Even pages Çift sayfalar + Landscape pages Yatay sayfalar + Portrait pages Dikey sayfalar + Move earlier Öne taşı + Move later Arkaya taşı + Back to the document Belgeye geri dön + Protect Using Password Parola Kullanarak Koru + Requires user to enter a password for: Kullanıcının şu işlem için parola girmesini gerektirir: + Viewing Görüntüleme + Editing Düzenleme + Type Password Parola Yazın + Re-type Password Parolayı Yeniden Yazın + Strength: {s} Güç: {s} + Strong Güçlü + Medium Orta + Weak Zayıf + Advanced Options {chevron} Gelişmiş Seçenekler {chevron} + Compatibility Uyumluluk + Acrobat X and later (256-bit AES) Acrobat X ve sonrası (256-bit AES) + Acrobat 7.0 and later (128-bit AES) Acrobat 7.0 ve sonrası (128-bit AES) + Acrobat 5.0 and later (128-bit RC4) Acrobat 5.0 ve sonrası (128-bit RC4) + Acrobat 3.0 and later (40-bit RC4) Acrobat 3.0 ve sonrası (40-bit RC4) + Encrypt Şifrele + All document contents Tüm belge içerikleri + All document contents except metadata Meta veriler dışındaki tüm belge içerikleri + Printing allowed Baskıya izin verildi + Low Resolution (150 dpi) Düşük Çözünürlük (150 dpi) + Changes allowed Değişikliklere izin verildi + Inserting, deleting, and rotating pages Sayfa ekleme, silme ve döndürme + Filling in form fields and signing existing signature fields Form alanlarını doldurma ve var olan imza alanlarını imzalama + Commenting, filling in form fields, and signing existing signature fields Yorum yapma, form alanlarını doldurma ve var olan imza alanlarını imzalama + Any except extracting pages Sayfa ayıklama hariç tümü + Enable copying of text, images, and other content Metin, resim ve diğer içeriklerin kopyalanmasını etkinleştir + Enable text access for screen reader devices for the visually impaired Görme engellilere yönelik ekran okuyucu cihazlar için metin erişimini etkinleştir + Type a password. Bir parola yazın. + The passwords don't match. Parolalar eşleşmiyor. + This compatibility level supports only plain ASCII passwords. Bu uyumluluk düzeyi yalnızca düz ASCII parolaları destekler. + Anyone opening the document will need this password. It's applied when you save. Belgeyi açan herkesin bu parolaya ihtiyacı olacaktır. Kaydettiğinizde uygulanır. + The document opens without a password; this password is needed to change the restrictions. It's applied when you save. Belge parola olmadan açılır; kısıtlamaları değiştirmek için bu parola gerekir. Kaydettiğinizde uygulanır. + Recover unsaved documents? Kaydedilmemiş belgeler kurtarılsın mı? + PdfCraft didn't shut down normally. These documents had changes that were autosaved: PdfCraft normal şekilde kapanmadı. Bu belgelerde otomatik olarak kaydedilen değişiklikler vardı: + just now az önce + {n} min ago {n} dk önce + {n} h ago {n} sa önce + · password-protected · parola korumalı + Recover Kurtar + Discard Yoksay + Version {v} Sürüm {v} + A clean-room, open-source PDF application written in Rust. MIT OR Apache-2.0. Rust ile yazılmış, temiz oda tekniğiyle geliştirilmiş açık kaynaklı bir PDF uygulaması. MIT VEYA Apache-2.0. + Part of Şunun parçası: + Summarize Options Özetleme Seçenekleri + Choose a layout: Bir düzen seçin: + Comments only Yalnızca yorumlar + Sort comments by: Yorumları şuna göre sırala: + Create PDF Comment Summary PDF Yorum Özeti Oluştur + Revert to the previously saved version of “{name}”? Changes since then can't be undone afterwards. “{name}” belgesinin daha önce kaydedilmiş sürümüne geri dönülsün mü? O andan sonraki değişiklikler geri alınamaz. + Number pages Sayfaları numaralandır + Style Stil + None (prefix only) Yok (yalnızca önek) + Prefix Önek + Start Başlangıç + Labels: {preview}. Later pages keep their labels. Etiketler: {preview}. Sonraki sayfalar etiketlerini korur. + Even and Odd Pages Çift ve Tek Sayfalar + Even Pages Only Yalnızca Çift Sayfalar + Odd Pages Only Yalnızca Tek Sayfalar + Landscape and Portrait Pages Yatay ve Dikey Sayfalar + Landscape Pages Yatay Sayfalar + Portrait Pages Dikey Sayfalar + Duplicate Field Alanı Çoğalt + Duplicate "{name}" onto: "{name}" şuraya çoğaltılsın: + 1 page selected. 1 sayfa seçildi. + {n} pages selected. {n} sayfa seçildi. + Delete pages after extracting Ayıkladıktan sonra sayfaları sil + Extract pages as separate files Sayfaları ayrı dosyalar olarak ayıkla + Rotate Pages Sayfaları Döndür + Direction: Yön: + Clockwise 90 degrees Saat yönünde 90 derece + Counterclockwise 90 degrees Saat yönünün tersine 90 derece + 180 degrees 180 derece + Pages: Sayfalar: + All Tümü + Selection Seçim + From Başlangıç + Rotate: Döndür: + Repairs Onarımlar + Repair log Onarım günlüğü + Split document Belgeyi böl + Number of pages Sayfa sayısı + Pages per file Dosya başına sayfa + File size Dosya boyutu + At most En fazla + Top-level bookmarks ({n}) Üst düzey yer işaretleri ({n}) + Before each selected page (select pages in Organize) Seçilen her sayfadan önce (Düzenle bölümünde sayfaları seçin) + Creates 1 file from {n} pages. {n} sayfadan 1 dosya oluşturur. + Creates {f} files from {n} pages. {n} sayfadan {f} dosya oluşturur. + Each file holds as many of the {n} pages as fit. Her dosya, {n} sayfadan sığdığı kadarını içerir. + Replace Pages Sayfaları Değiştir + Original: replace pages Orijinal: değiştirilecek sayfalar + to bitiş + to {n} şuraya kadar: {n} + Replacement: pages of {name} Yedek: {name} sayfaları + of {name} {name} belgesinden + Only the page content changes: links, comments, form fields and bookmarks on the original pages stay. Yalnızca sayfa içeriği değişir: Orijinal sayfalardaki bağlantılar, yorumlar, form alanları ve yer işaretleri korunur. + The replacement file doesn't have that many pages. Yedek dosyada o kadar sayfa yok. + Tagged PDF Etiketli PDF + Document Properties Belge Özellikleri + Description Açıklama + Initial View İlk Görünüm + Security Güvenlik + Fonts Yazı Tipleri + Advanced Gelişmiş + Title Başlık + Subject Konu + Keywords Anahtar Kelimeler + Application Uygulama + PDF producer PDF üreticisi + Layout and Magnification Düzen ve Büyütme + Navigation tab Gezinme sekmesi + Page Only Yalnızca Sayfa + Bookmarks Panel and Page Yer İşaretleri Paneli ve Sayfa + Pages Panel and Page Sayfalar Paneli ve Sayfa + Attachments Panel and Page Ekler Paneli ve Sayfa + Layers Panel and Page Katmanlar Paneli ve Sayfa + Page layout Sayfa düzeni + Default Varsayılan + Single Page Tek Sayfa + Single Page Continuous Tek Sayfa Sürekli + Two-Up (Facing) İki Sayfa (Karşılıklı) + Two-Up Continuous (Facing) İki Sayfa Sürekli (Karşılıklı) + Two-Up (Cover Page) İki Sayfa (Kapak Sayfası) + Two-Up Continuous (Cover Page) İki Sayfa Sürekli (Kapak Sayfası) + Magnification Yakınlaştırma + Actual Size Gerçek Boyut + Fit Page Sayfaya Sığdır + Fit Width Genişliğe Sığdır + Fit Height Yüksekliğe Sığdır + Fit Visible Görünene Sığdır + Open to page Şu sayfada aç: + of {pages} / {pages} + Window Options Pencere Seçenekleri + Resize window to initial page Pencereyi başlangıç sayfasına göre yeniden boyutlandır + Center window on screen Pencereyi ekranda ortala + Open in Full Screen mode Tam Ekran modunda aç + Show: Göster: + File Name Dosya Adı + Document Title Belge Başlığı + User Interface Options Kullanıcı Arayüzü Seçenekleri + Hide menu bar Menü çubuğunu gizle + Hide toolbars Araç çubuklarını gizle + Hide window controls Pencere kontrollerini gizle + Security method Güvenlik yöntemi + No security Güvenlik yok + Restrictions Kısıtlamalar + None — everything is allowed Yok — her şeye izin verilir + Password security Parola güvenliği + Encryption Şifreleme + Opened with Şununla açıldı: + — (protection is applied when you save) — (koruma kaydettiğinizde uygulanır) + Owner password (no restrictions apply) İzin parolası (hiçbir kısıtlama uygulanmaz) + User password Kullanıcı parolası + Change settings… Ayarları değiştir… + Printing Yazdırma + Not allowed İzin verilmiyor + Allowed İzin verildi + High resolution Yüksek çözünürlük + Low resolution Düşük çözünürlük + Changing the document Belgeyi değiştirme + Document assembly Belge birleştirme + Content copying İçerik kopyalama + Content copying for accessibility Erişilebilirlik için içerik kopyalama + Commenting Yorum yapma + Filling of form fields Form alanlarını doldurma + No fonts are referenced by the pages. Sayfalarda hiçbir yazı tipine başvurulmuyor. + PDF version PDF sürümü + Location Konum + Page size Sayfa boyutu + Yes Evet + No Hayır + Revisions Düzeltmeler + View revision {n} {n}. düzeltmeyi görüntüle + Open the file as it was saved then Dosyayı o zamanki kaydedildiği haliyle aç + Binding Ciltleme + Left Edge Sol Kenar + Right Edge Sağ Kenar + Language Dil + Saved {place}: {before} → {after} ({pct}% smaller){detail} {place} kaydedildi: {before} → {after} (%{pct} daha küçük){detail} + Couldn't save the image: {e} Resim kaydedilemedi: {e} + Adding images arrives on the web with file pickers for images Resim ekleme, web'de resim dosyası seçicileriyle birlikte sunulacaktır + Replacing images on page {p} arrives on the web with image pickers ({i}) Sayfa {p} üzerindeki resimlerin değiştirilmesi web'de resim seçicilerle ({i}) sunulacaktır + {name}: choosing images arrives on the web with file pickers for images {name}: resim seçimi web'de resim dosyası seçicileriyle birlikte sunulacaktır + Replacing images arrives on the web with image file pickers Resimleri değiştirme web'de resim dosyası seçicileriyle birlikte sunulacaktır + Couldn't create a PDF: {e} PDF oluşturulamadı: {e} + The clipboard has no image or text Panoda resim veya metin yok + On the web, open or drop an image to convert it Web'de dönüştürmek için bir resim açın veya sürükleyip bırakın + Couldn't optimize the file: {e} Dosya en iyileştirilemedi: {e} + Couldn't write {path}: {e} {path} yazılamadı: {e} + The form asks to be submitted to {u}; PdfCraft doesn't send form data. Save the document to keep your entries. Form {u} adresine gönderilmek isteniyor; PdfCraft form verilerini göndermez. Girdilerinizi korumak için belgeyi kaydedin. + {s} actions run in the JavaScript engine (M6) {s} eylemleri JavaScript motorunda çalışır (M6) + {name}: the {other} action isn't supported yet {name}: {other} eylemi henüz desteklenmiyor + {name} submits the form to {url}; PdfCraft doesn't send form data. Save the document to keep your entries. {name} formu {url} adresine gönderiyor; PdfCraft form verilerini göndermez. Girdilerinizi korumak için belgeyi kaydedin. + Export stopped: {e} Dışa aktarma durduruldu: {e} + Exported 1 image 1 resim dışa aktarıldı + Exported {n} images {n} resim dışa aktarıldı + ({s} not exported: {first}) ({s} dışa aktarılmadı: {first}) + Exported the text of 1 page 1 sayfanın metni dışa aktarıldı + Exported the text of {n} pages {n} sayfanın metni dışa aktarıldı + {msg} to {dir} {msg} -> {dir} + Exporting… {d} of {t} Dışa aktarılıyor… {d} / {t} + Choose a folder for the exported files Dışa aktarılan dosyalar için bir klasör seçin + New {tool} comments will look like this one Yeni {tool} yorumları bundan sonra buna benzeyecek + This document can't be checked: it couldn't be read for editing Bu belge denetlenemiyor: Düzenleme için okunamadı + The checker found no problems in this document. Denetleyici bu belgede herhangi bir sorun bulamadı. + The checker found 1 problem which may prevent the document from being fully accessible. Denetleyici, belgenin tam olarak erişilebilir olmasını engelleyebilecek 1 sorun buldu. + The checker found {n} problems which may prevent the document from being fully accessible. Denetleyici, belgenin tam olarak erişilebilir olmasını engelleyebilecek {n} sorun buldu. + The text is the same Metin aynı + 1 difference found 1 fark bulundu + {n} differences found {n} fark bulundu + Added 1 comment 1 yorum eklendi + Added {n} comments {n} yorum eklendi + Choose a folder for the results Sonuçlar için bir klasör seçin + Files for {name} {name} için dosyalar + An action is already running Zaten çalışan bir eylem var + Running action… file {d} of {t} Eylem çalıştırılıyor… Dosya {d} / {t} + No unlinked web addresses were found in the text Metinde bağlantısız web adresi bulunamadı + Created 1 link 1 bağlantı oluşturuldu + Created {n} links {n} bağlantı oluşturuldu + Text recognition is already running Metin tanıma zaten çalışıyor + Optimizing… removing user data En iyileştiriliyor… kullanıcı verileri kaldırılıyor + Optimizing… image {d} of {t} En iyileştiriliyor… Resim {d} / {t} + Optimizing… cleaning up En iyileştiriliyor… temizleniyor + Optimizing… merging identical objects En iyileştiriliyor… özdeş nesneler birleştiriliyor + Optimizing… writing the copy En iyileştiriliyor… kopya yazılıyor + Cancelling… İptal ediliyor… + Optimization is already running En iyileştirme zaten çalışıyor + Optimization cancelled En iyileştirme iptal edildi + Couldn't optimize the file Dosya en iyileştirilemedi + Recognizing text… file {d} of {t} Metin tanınıyor… Dosya {d} / {t} + Recognizing text… page {d} of {t} Metin tanınıyor… Sayfa {d} / {t} + Recognized 1 word 1 sözcük tanındı + Recognized {n} words {n} sözcük tanındı + {words} on {pages} {pages} üzerinde {words} + ; 1 page already had text ; 1 sayfada zaten metin vardı + ; {n} pages already had text ; {n} sayfada zaten metin vardı + Recognized {w} words in 1 file 1 dosyada {w} sözcük tanındı + Recognized {w} words in {n} files {n} dosyada {w} sözcük tanındı + ; failed: {list} ; başarısız oldu: {list} + 1 file done 1 dosya tamamlandı + {n} files done {n} dosya tamamlandı + The selected area has been copied ({w} × {h} pixels) Seçilen alan kopyalandı ({w} × {h} piksel) + Couldn't take the snapshot: {e} Anlık görüntü alınamadı: {e} + Couldn't recover {name}: {e} {name} kurtarılamadı: {e} + The document now conforms to {level}; save it to keep the changes Belge artık {level} standardına uygundur; değişiklikleri korumak için kaydedin + Fixed what could be fixed; 1 problem remains Düzeltilebilecek olanlar düzeltildi; 1 sorun kaldı + Fixed what could be fixed; {n} problems remain Düzeltilebilecek olanlar düzeltildi; {n} sorun kaldı + Custom stamps arrive on the web with file pickers for images Özel damgalar web'de resim dosyası seçicileriyle birlikte sunulacaktır + {file} is too large for a stamp (at most {n} MB) {file} damga için çok büyük (en fazla {n} MB) + Sent to {printer} {printer} yazıcısına gönderildi + Sent to {printer}: {msg} {printer} yazıcısına gönderildi: {msg} + Choose a folder for the searchable files Aranabilir dosyalar için bir klasör seçin + Files are combined in this order. Leave Pages empty to take every page. Dosyalar bu sırada birleştirilir. Tüm sayfaları almak için Sayfalar alanını boş bırakın. + Pages of {name} to combine, e.g. 1-3, 6 {name} belgesinin birleştirilecek sayfaları, ör. 1-3, 6 + Remove {name} {name} kaldır + Add files… Dosya ekle… + Add at least two files En az iki dosya ekleyin + Couldn't add {name}: {e} {name} eklenemedi: {e} + Couldn't combine files: {e} Dosyalar birleştirilemedi: {e} + Export comments Yorumları dışa aktar + Export form data Form verilerini dışa aktar + Comment and form data Yorum ve form verileri + Export Dışa aktar + Select data files to merge Birleştirilecek veri dosyalarını seçin + Form data and PDF forms Form verileri ve PDF formları + Save the spreadsheet Elektronik tabloyu kaydet + Save the accessibility report Erişilebilirlik raporunu kaydet + Save image Resmi kaydet + Export certificate Sertifikayı dışa aktar + Choose a folder for the extracted pages Ayıklanan sayfalar için bir klasör seçin + Choose a folder for the split files Bölünen dosyalar için bir klasör seçin + Couldn't use {name}: {e} {name} kullanılamadı: {e} + Couldn't extract pages: {e} Sayfalar ayıklanamadı: {e} + Couldn't write {name}: {e} {name} yazılamadı: {e} + Wrote 1 file to {dir} 1 dosya {dir} konumuna yazıldı + Wrote {n} files to {dir} {n} dosya {dir} konumuna yazıldı + No pages match those choices Bu seçimlerle eşleşen sayfa yok + Couldn't split: {e} Bölünemedi: {e} + Couldn't summarize comments: {e} Yorumlar özetlenemedi: {e} + Couldn't open the result: {e} Sonuç açılamadı: {e} + Importing data arrives on the web with file pickers for data files Veri içe aktarma web'de veri dosyası seçicileriyle birlikte sunulacaktır + Exported to {path} {path} konumuna aktarıldı + Merging data files needs the desktop app Veri dosyalarını birleştirmek masaüstü uygulamasını gerektirir + Merged 1 file into {path} 1 dosya {path} içine birleştirildi + Merged {n} files into {path} {n} dosya {path} içine birleştirildi + Combined {n} files {n} dosya birleştirildi + Extracted 1 page 1 sayfa ayıklandı + Extracted {n} pages {n} sayfa ayıklandı + Created a comment summary Bir yorum özeti oluşturuldu + Couldn't open {name}: {e} {name} açılamadı: {e} + Couldn't read {name}: {e} {name} okunamadı: {e} + Portable mode is off: {folder} can't be written ({error}). Settings are kept in your user folder instead. Taşınabilir mod kapalı: {folder} konumuna yazılamıyor ({error}). Ayarlar bunun yerine kullanıcı klasörünüzde tutulur. + PdfCraft is drawing without a graphics processor, so it may be slow. Updating the graphics driver may help. PdfCraft grafik işlemcisi olmadan çizim yapıyor, bu nedenle yavaş olabilir. Grafik sürücüsünü güncellemek yardımcı olabilir. + Couldn't save: {e} Kaydedilemedi: {e} + Downloading attachments on the web arrives with M3.10 ({n} bytes ready) Web'de ek indirme M3.10 ile sunulacaktır ({n} bayt hazır) + `{command}` {when} `{command}` {when} + ships in milestone {m} {m} kilometre taşında çıkacak + needs an AI provider (off by default) bir yapay zeka sağlayıcısı gerektirir (varsayılan olarak kapalı) + is available kullanılabilir + is not available yet henüz kullanılamıyor + {label} failed: {e} {label} başarısız oldu: {e} + Undid {label} {label} geri alındı + Redid {label} {label} yinelendi + A document needs at least one page: copy instead Bir belgenin en az bir sayfaya ihtiyacı vardır: bunun yerine kopyalayın + Cut {what} {what} kesildi + Copied {what} {what} kopyalandı + 1 page 1 sayfa + {n} pages {n} sayfa + Copy or cut pages first Önce sayfaları kopyalayın veya kesin + Couldn't save {name}: {e} {name} kaydedilemedi: {e} + Saved {name} {name} kaydedildi + Saved, but reopening failed: {e} Kaydedildi, ancak yeniden açma başarısız oldu: {e} + Downloaded {name} {name} indirildi + Couldn't download {name}: {e} {name} indirilemedi: {e} + Reverted to the last saved version Son kaydedilen sürüme geri dönüldü + That didn't work: an internal error stopped it ({m}). Your documents are unchanged. Bu işlem yapılamadı: dahili bir hata durdurdu ({m}). Belgeleriniz değişmedi. + Nothing to undo Geri alınacak bir şey yok + Nothing to redo Yinelenecek bir şey yok + This document has no form fields you can fill in Bu belgede doldurabileceğiniz hiçbir form alanı yok + This document has no comments to flatten Bu belgede düzleştirilecek yorum yok + This document has no form fields to flatten Bu belgede düzleştirilecek form alanı yok + There are no redaction marks (mark text, areas or pages first) Karartma işareti yok (önce metni, alanları veya sayfaları işaretleyin) + This document has no {kind} to change Bu belgede değiştirilecek {kind} yok + header or footer üstbilgi veya altbilgi + watermark filigran + background arka plan + This document isn't password-protected Bu belge parola korumalı değil + Only the document's owner can change its security (open it with the permissions password) Yalnızca belge sahibi güvenliğini değiştirebilir (izinler parolasıyla açın) + The document's security settings don't allow this change Belgenin güvenlik ayarları bu değişikliğe izin vermiyor + Open a document first Önce bir belge açın + Security will be removed when you save Kaydettiğinizde güvenlik kaldırılacaktır + Click text or an image to edit it Düzenlemek için metne veya resme tıklayın + Drag a rectangle to create a link; double-click a link to edit it Bağlantı oluşturmak için bir dikdörtgen sürükleyin; düzenlemek için bir bağlantıya çift tıklayın + There are no redaction marks to apply Uygulanacak karartma işareti yok + Drag to draw the area where the signature should appear. İmzanın görünmesi gereken alanı çizmek için sürükleyin. + Couldn't fit the visible content: {e} Görünür içerik sığdırılamadı: {e} + Drag a rectangle around the area to copy Kopyalanacak alanın etrafına bir dikdörtgen sürükleyin + Click on the page to add a {tool}, or drag to set its size Bir {tool} eklemek için sayfaya tıklayın veya boyutunu ayarlamak için sürükleyin + Drag a rectangle on a page to crop it; double-click a page for Set Page Boxes Kırpmak için bir sayfada dikdörtgen sürükleyin; Sayfa Kutularını Ayarla için bir sayfaya çift tıklayın + Keep tool selected: on Aracı seçili tut: açık + Keep tool selected Aracı seçili tut + Opacity Opaklık + Line thickness Çizgi kalınlığı + What word or phrase would you like to search for? Hangi sözcüğü veya kelime öbeğini aramak istersiniz? + Searching… {s} of {p} pages Aranıyor… {s} / {p} sayfa + (searching… {s} of {p} pages) (aranıyor… {s} / {p} sayfa) + 1 instance 1 örnek + {n} instances {n} örnek + Go to this result Bu sonuca git + Highlight Vurgula + Draw Çiz + Polygon Çokgen + Connected lines Bağlantılı çizgiler + Eraser Silgi + Sign İmzala + Certified by {by} {by} tarafından onaylandı + At least one signature is invalid. En az bir imza geçersiz. + Signed and all signatures are valid. İmzalandı ve tüm imzalar geçerli. + At least one signature has problems. En az bir imzada sorun var. + This document is secured. Some changes are restricted by its security settings. Bu belge güvenlidir. Bazı değişiklikler güvenlik ayarlarıyla kısıtlanmıştır. + This is a dynamic XFA form, which PdfCraft can't display yet. What you see is the file's placeholder page. Bu, PdfCraft'ın henüz görüntüleyemediği dinamik bir XFA formudur. Gördüğünüz şey dosyanın yer tutucu sayfasıdır. + This form also contains XFA data, which PdfCraft doesn't read yet. You can fill its fields, but Acrobat may show the XFA values instead. Bu form aynı zamanda PdfCraft'ın henüz okumadığı XFA verilerini de içerir. Alanlarını doldurabilirsiniz ancak Acrobat bunun yerine XFA değerlerini gösterebilir. + This document contains {n} interactive form fields. Bu belge {n} etkileşimli form alanı içeriyor. + This file was damaged and has been repaired. Saving keeps the repaired version. Bu dosya hasarlıydı ve onarıldı. Kaydetmek onarılan sürümü korur. + Dismiss Kapat + Hide field highlights Alan vurgularını gizle + Highlight fields Alanları vurgula + Security settings Güvenlik ayarları + Details Ayrıntılar + Select (V) Seç (V) + Hand (H) El (H) + Delete pages (Delete) Sayfaları sil (Delete) + Insert a blank page after the selection Seçimden sonra boş bir sayfa ekle + Insert pages from a file… Dosyadan sayfa ekle… + Extract pages to a new document Sayfaları yeni bir belgeye ayıkla + Split into files… Dosyalara böl… + Select pages Sayfaları seç + Cut Kes + Copy Kopyala + Paste after Sonrasına yapıştır + Rename Yeniden Adlandır + Set to current page ({n}) Geçerli sayfaya ayarla ({n}) + Move up Yukarı taşı + Move down Aşağı taşı + Indent Girintiyi artır + Outdent Girintiyi azalt + This document has no form fields. Bu belgede form alanı yok. + Tab order: move a field with its arrows. Sekme sırası: Bir alanı oklarıyla taşıyın. + Unplaced Yerleştirilmemiş + Has JavaScript actions (run in M6) JavaScript eylemleri var (M6 içinde çalışır) + Page Sayfa + None Yok + Delete selected fields Seçili alanları sil + Diagnostic details Tanılama ayrıntıları + The operation failed. İşlem başarısız oldu. + no such document böyle bir belge yok + nothing to undo geri alınacak bir şey yok + nothing to redo yinelenecek bir şey yok + this document is signed: rewriting it would invalidate its signatures (save it incrementally instead) bu belge imzalanmış: yeniden yazılması imzalarını geçersiz kılar (bunun yerine aşamalı olarak kaydedin) + the document is protected by a password belge bir parola ile korunuyor + the password is incorrect parola yanlış + the document has no page tree belgenin sayfa ağacı yok + a document must keep at least one page bir belge en az bir sayfayı korumalıdır + a bookmark cannot be moved inside itself bir yer işareti kendi içine taşınamaz + the title must not be empty başlık boş olmamalıdır + the document has no form fields belgede form alanı yok + there are no redaction marks to apply uygulanacak karartma işareti yok + there are no pages to print yazdırılacak sayfa yok + the document has no tags belgenin etiketi yok + {0}: {1} {0}: {1} + the text recognition models are not installed (reinstall PdfCraft, or set PDFCRAFT_MODELS to the folder that holds them) metin tanıma modelleri yüklü değil (PdfCraft'ı yeniden yükleyin veya PDFCRAFT_MODELS değişkenini bunları içeren klasöre ayarlayın) + the image is empty resim boş + this file isn't XFDF, FDF, XML or text form data bu dosya XFDF, FDF, XML veya metin form verisi değil + nothing in the file matches this document dosyadaki hiçbir şey bu belgeyle eşleşmiyor + This page couldn't be displayed.\n{e} Bu sayfa görüntülenemedi.\n{e} + Go to page {p} Sayfa {p} sayfasına git + {s} action {s} eylemi + Page {p} of {n} Sayfa {p} / {n} + 1 page selected 1 sayfa seçildi + {n} pages selected {n} sayfa seçildi + {name} — unsaved changes {name} — kaydedilmemiş değişiklikler + {kind} field {kind} alanı + Select a file for the stamp Damga için bir dosya seçin + JavaScript, e.g. getField("total").value JavaScript, ör. getField("total").value + Font: Helvetica Yazı Tipi: Helvetica + e.g. en-US ör. tr-TR + Rendering: hayro (bootstrap) · UI: egui · Icons: Lucide (ISC) · Fonts: Inter, JetBrains Mono, Dancing Script (OFL) Oluşturma: hayro (bootstrap) · Arayüz: egui · Simgeler: Lucide (ISC) · Yazı Tipleri: Inter, JetBrains Mono, Dancing Script (OFL) + CheckBox OnayKutusu + Radio Radyo + PushButton BasmaDüğmesi + Combo AçılırKutu + List Liste + Unknown Bilinmeyen + The document has no XMP metadata Belgede XMP meta verisi yok + The title in the metadata and the document information differ Meta verilerdeki başlık ile belge bilgilerindeki başlık farklı + The document is encrypted Belge şifreli + Device colour is used, but there is no PDF/A output intent Cihaz rengi kullanılıyor ancak PDF/A çıktı amacı yok + DeviceCMYK is used: it needs a CMYK output intent (not added automatically) DeviceCMYK kullanılıyor: Bir CMYK çıktı amacı gerekiyor (otomatik olarak eklenmez) + The document has additional actions (/AA) Belgede ek eylemler var (/AA) + The document has document-level JavaScript Belgede belge düzeyinde JavaScript var + The form asks viewers to make appearances (NeedAppearances) Form, görüntüleyicilerden görünüm oluşturmasını istiyor (NeedAppearances) + A form field has additional actions Bir form alanının ek eylemleri var + A stream uses LZW compression Bir akış LZW sıkıştırması kullanıyor + A stream refers to an external file Bir akış harici bir dosyaya başvuruyor + An image asks to be interpolated Bir resim enterpolasyon istiyor + An image has alternates or OPI information Bir resmin alternatifleri veya OPI bilgileri var + PostScript XObjects aren't allowed PostScript XObject'lerine izin verilmez + Embedded files must be PDF/A files themselves (use PDF/A-3 for others) Gömülü dosyaların kendileri PDF/A dosyası olmalıdır (diğerleri için PDF/A-3 kullanın) + An embedded file has no AFRelationship Gömülü bir dosyanın AFRelationship ilişkisi yok + Select Icon Simge Seç + Choose images Resimleri seçin + {dpi} pixels/inch {dpi} piksel/inç + Type your {what}. {what} bilginizi yazın. + Draw your {what} below. {what} bilginizi aşağıya çizin. + Your name Adınız + Images and text (converted to PDF) Resimler ve metin (PDF'e dönüştürüldü) + Images and text Resimler ve metin + Check Box Style: Onay Kutusu Stili: + Button Style: Düğme Stili: + Check Onay + Circle Daire + Diamond Elmas + Square Kare + Star Yıldız + {month} {y} {month} {y} + Change {key} {key} değiştir + Insert pages from {name} {name} belgesinden sayfalar ekle + Fill in {name} {name} doldur + Set the image of {name} {name} resmini ayarla + Edit script of {name} {name} komut dosyasını düzenle + Import {name} {name} içe aktar + Add bookmark Yer işareti ekle + Rename bookmark Yer işaretini yeniden adlandır + Delete bookmark Yer işaretini sil + Move bookmark Yer işaretini taşı + Set bookmark destination Yer işareti hedefini belirle + Add stamp Damga ekle + Delete comment Yorumu sil + Edit comment Yorumu düzenle + Erase Sil + Lock comment Yorumu kilitle + Unlock comment Yorumun kilidini aç + Move comment Yorumu taşı + Resize comment Yorumu yeniden boyutlandır + Change comment properties Yorum özelliklerini değiştir + Add field Alan ekle + Change field properties Alan özelliklerini değiştir + Delete field Alanı sil + Duplicate field Alanı çoğalt + Set tab order Sekme sırasını ayarla + Change initial view İlk görünümü değiştir + Set alternate text Alternatif metni ayarla + Mark figure as decorative Şekli dekoratif olarak işaretle + Run JavaScript JavaScript çalıştır + Delete document JavaScript Belge JavaScript'ini sil + Edit document JavaScript Belge JavaScript'ini düzenle + Edit text Metni düzenle + Move image Resmi taşı + Rotate image Resmi döndür + Flip image Resmi çevir + Delete image Resmi sil + Add header & footer Üstbilgi ve altbilgi ekle + Update header & footer Üstbilgi ve altbilgiyi güncelle + Update watermark Filigranı güncelle + Add background Arka plan ekle + Update background Arka planı güncelle + Add image Resim ekle + Edit content İçeriği düzenle + Delete content İçeriği sil + Remove redaction marks Karartma işaretlerini kaldır + Add link Bağlantı ekle + Change link properties Bağlantı özelliklerini değiştir + Delete link Bağlantıyı sil + Create link Bağlantı oluştur + Flatten Düzleştir + Add sticky note Not ekle + Add highlight Vurgu ekle + Add underline Alt çizgi ekle + Add strikethrough Üstü çizili ekle + Add squiggly underline Dalgalı alt çizgi ekle + Add rectangle Dikdörtgen ekle + Add oval Elips ekle + Add arrow Ok ekle + Add line Çizgi ekle + Add drawing Çizim ekle + Add text box Metin kutusu ekle + Add checkmark Onay işareti ekle + Add cross Çarpı ekle + Add dot Nokta ekle + Add signature İmza ekle + Add redaction mark Karartma işareti ekle + Add cloud Bulut ekle + Add polygon Çokgen ekle + Add connected lines Bağlantılı çizgiler ekle + Add callout Açıklama balonu ekle + Add inserted text Eklenen metin ekle + Add file attachment Dosya eki ekle + Set status Accepted Kabul Edildi durumunu ayarla + Set status Rejected Reddedildi durumunu ayarla + Set status Cancelled İptal Edildi durumunu ayarla + Set status Completed Tamamlandı durumunu ayarla + Set status None Durumu Yok olarak ayarla + Set status Unmarked Durumu İşaretsiz olarak ayarla + Save as PDF/A-1b PDF/A-1b olarak kaydet + Save as PDF/A-2b PDF/A-2b olarak kaydet + Save as PDF/A-3b PDF/A-3b olarak kaydet + Fill & Sign: change signature Doldur ve İmzala: imzayı değiştir + Fill & Sign: change initials Doldur ve İmzala: parafı değiştir + Fill & Sign: remove saved signature Doldur ve İmzala: kayıtlı imzayı kaldır + Fill & Sign: remove saved initials Doldur ve İmzala: kayıtlı parafı kaldır + Change signature İmzayı değiştir + Change initials Parafı değiştir + Measure distance Mesafeyi ölç + Measure perimeter Çevreyi ölç + Measure area Alanı ölç + Set measurement scale Ölçüm ölçeğini ayarla + Measurement information Ölçüm bilgisi + Measurement snapping Ölçüm yakalama + Export measurements as CSV Ölçümleri CSV olarak dışa aktar + Distance Mesafe + Perimeter Çevre + Area Alan + Drawing scale Çizim ölçeği + Snapping Yakalama + Export measurements Ölçümleri dışa aktar + Open a PDF to measure it. Ölçmek için bir PDF açın. + Click vertices. Enter or double-click finishes a perimeter or area. Click the first point to close an area. Köşelere tıklayın. Enter veya çift tıklama çevreyi veya alanı tamamlar. Bir alanı kapatmak için ilk noktaya tıklayın. + Shift constrains to 45 degrees. Backspace removes a vertex. Escape cancels. Shift 45 dereceye sabitler. Geri alma tuşu bir köşeyi kaldırır. Escape iptal eder. + Angle Açı + Click the first point to begin. Başlamak için ilk noktaya tıklayın. + Snap to drawing Çizime yakala + Endpoints Uç noktalar + Midpoints Orta noktalar + Intersections Kesişimler + Paths Yollar + Sensitivity Duyarlılık + Unit Birim + Decimals Ondalık basamaklar + Calibrate from two points İki noktadan kalibre et + Entire page Tüm sayfa + Viewport rectangle in page points (x, y, width, height) Sayfa noktalarında görünüm penceresi dikdörtgeni (x, y, genişlik, yükseklik) + Viewport Görünüm penceresi + Apply scale Ölçeği uygula + Saved measurements Kayıtlı ölçümler + Saved measurements. Ölçümler kaydedildi. + Endpoint Uç nokta + Midpoint Orta nokta + Intersection Kesişim + Path Yol + This drawing exceeds the snapping limit; only the first paths are used. Bu çizim yakalama sınırını aşıyor; yalnızca ilk yollar kullanılır. + Some page content couldn't be read; snapping uses the rest. Bazı sayfa içerikleri okunamadı; yakalama geri kalanını kullanır. + Unsupported measurements Desteklenmeyen ölçümler + Label Etiket + {n} measurements use an unsupported format and are left out. {n} ölçüm desteklenmeyen bir biçim kullanıyor ve dahil edilmedi. + This area has too many overlapping paths; intersection snapping is limited. Bu alanda çok fazla örtüşen yol var; kesişim yakalama sınırlandırıldı. + This measurement needs at least {n} points. Bu ölçüm en az {n} nokta gerektirir. + Change {what} {what} değiştir + Cut and stack Kes ve istifle + Default workspace mode Varsayılan çalışma alanı modu + Documents and view Belgeler ve görünüm + Open email app E-posta uygulamasını aç + Open link Bağlantıyı aç + Open this web page? Bu web sayfası açılsın mı? + Scripts run in a sandbox without file or network access. A script that asks to open a web page needs your permission first. With JavaScript off, Acrobat's standard format, validate and calculate functions still work. Komut dosyaları dosya veya ağ erişimi olmadan yalıtılmış alanda (sandbox) çalışır. Bir web sayfası açmak isteyen komut dosyası önce izninizi gerektirir. JavaScript kapalıyken bile Acrobat'ın standart biçimlendirme, doğrulama ve hesaplama işlevleri çalışmaya devam eder. + Sign yourself Kendiniz imzalayın + Used when opening PDFs. An explicit launch or control mode takes precedence for the session. PDF'leri açarken kullanılır. Açık bir başlatma veya denetim modu oturum için önceliklidir. + Write this email? Bu e-posta yazılsın mı? + {who} in this document wants to open this address in your browser: Bu belgedeki {who}, tarayıcınızda şu adresi açmak istiyor: + {who} in this document wants to open your email app with this address: Bu belgedeki {who}, e-posta uygulamanızı şu adresle açmak istiyor: + This web address mixes letters from different alphabets, a common way to imitate another site's name. Bu web adresi, başka bir sitenin adını taklit etmenin yaygın bir yolu olan farklı alfabelerden harfleri karıştırıyor. + This web address uses letters from another alphabet, which can look like familiar ones. Bu web adresi, tanıdık harflere benzeyebilen başka bir alfabeden harfler kullanıyor. + Create PDF from images Resimlerden PDF oluştur + Selected image files: {count} Seçilen resim dosyaları: {count} + Use image resolution Resim çözünürlüğünü kullan + Use each image's embedded DPI; use 72 DPI when it is absent. Her resmin gömülü DPI değerini kullan; yoksa 72 DPI kullan. + Use 72 DPI (one point per pixel) 72 DPI kullan (piksel başına bir nokta) + Use custom DPI Özel DPI kullan + DPI sets the printed page size without resampling the image. DPI, resmi yeniden örneklemeden yazdırılan sayfa boyutunu ayarlar. + Create Oluştur + Dynamic XFA form laid out from its template: {pages}, {fields}. Şablonundan oluşturulmuş dinamik XFA formu: {pages}, {fields}. + XFA form: its fields and its XFA data are kept in step, so other viewers show what you fill in. XFA formu: Diğer görüntüleyicilerin doldurduklarınızı göstermesi için alanları ve XFA verileri eşitlenir. +@plural {n} page|{n} pages {n} sayfa +@plural {n} field|{n} fields {n} alan + Switch to two-page view first to show the cover page Kapak sayfasını göstermek için önce iki sayfalı görünüme geçin + Page display: {layout} Sayfa görünümü: {layout} + Fit to width scrolling Genişliğe sığdırarak kaydırma + Fit one full page Tam bir sayfaya sığdır + Default page display Varsayılan sayfa görünümü + Default zoom Varsayılan yakınlaştırma + Used when a PDF doesn't ask for a layout or zoom. Continuous scrolling never snaps between pages. Bir PDF belirli bir düzen veya yakınlaştırma istemediğinde kullanılır. Sürekli kaydırma sayfalar arasında atlama yapmaz. + Show Göster + Sort Sırala + Reverse the order Sırayı tersine çevir + Grab bag Karışık + Table Tablo + Username Kullanıcı adı + Display name Görünen ad + Real name Gerçek ad + Name (A–Z) Ad (A–Z) + Merged PRs Birleştirilen PR'lar + PRs PR'lar + Commits İşlemeler + Lines added Eklenen satırlar + +LOC +LOC + Lines deleted Silinen satırlar + −LOC −LOC + Line delta Satır farkı + ΔLOC ΔLOC + Binary assets added Eklenen ikili varlıklar + +Bin +Bin + Binary assets removed Kaldırılan ikili varlıklar + −Bin −Bin + First commit İlk işleme + First İlk + Last commit Son işleme + Last Son + No contributor data was built into this copy. Bu kopyaya hiçbir katkıda bulunan verisi dahil edilmedi. + No model credits were built into this copy. Bu kopyaya hiçbir model kredisi dahil edilmedi. + {n} contributors · {c} commits {n} katkıda bulunan · {c} işleme + Company Şirket + Model Model + % of all commits Tüm işlemelerin %'si + Lines +/− Satırlar +/− + @{login}: {prs} PRs, {commits} commits, +{added} / −{deleted} lines (Δ {delta}), +{bin_added} / −{bin_deleted} binary assets, {first} – {last} @{login}: {prs} PR, {commits} işleme, +{added} / −{deleted} satır (Δ {delta}), +{bin_added} / −{bin_deleted} ikili varlık, {first} – {last} + Set layer visibility Katman görünürlüğünü ayarla + Created a PDF from {n} files {n} dosyadan bir PDF oluşturuldu + PDF, images and text PDF, resimler ve metin + Insert a file before page {label} Sayfa {label} öncesine bir dosya ekle + Insert a file at the end Sona bir dosya ekle + Save pages Sayfaları kaydet + Save these pages as one PDF Bu sayfaları tek bir PDF olarak kaydet + No changes to save Kaydedilecek değişiklik yok + Created a PDF from {n} files; left out: {names} {n} dosyadan bir PDF oluşturuldu; hariç tutulanlar: {names} + Created a PDF from the first {n} files İlk {n} dosyadan bir PDF oluşturuldu + None of those files can be made into a PDF; use PDFs, images or .txt files Bu dosyaların hiçbiri PDF'e dönüştürülemez; PDF'ler, resimler veya .txt dosyaları kullanın + 1 file 1 dosya + 1 file needs attention 1 dosya ilgi gerektiriyor + ? of {n} ? / {n} + Add folder Klasör ekle + Add folder and subfolders Klasör ve alt klasörleri ekle + Add folder and subfolders… Klasör ve alt klasörleri ekle… + Add folder… Klasör ekle… + Add open documents Açık belgeleri ekle + Add the PDFs to combine Birleştirilecek PDF'leri ekleyin + Added more than once Birden fazla kez eklendi + All open documents Tüm açık belgeler + Can't be read: {e} Okunamıyor: {e} + Check the pages of {name}: {why} {name} sayfalarını denetleyin: {why} + Click to sort, drag to move the column. Drag its edge to resize it. Sıralamak için tıklayın, sütunu taşımak için sürükleyin. Yeniden boyutlandırmak için kenarını sürükleyin. + Drag to reorder Yeniden sıralamak için sürükleyin + Drop PDFs or folders here, or choose them. PDF'leri veya klasörleri buraya bırakın ya da seçin. + Enter the password that opens it Açan parolayı girin + Enter the permissions password to allow combining Birleştirmeye izin vermek için izinler parolasını girin + Every PDF in the folder Klasördeki her PDF + Every PDF in the folder and the folders inside it Klasördeki ve içindeki klasörlerdeki her PDF + File name Dosya adı + Files are combined from top to bottom. Leave Pages empty to take every page. Dosyalar yukarıdan aşağıya doğru birleştirilir. Tüm sayfaları almak için Sayfalar alanını boş bırakın. + Hide password Parolayı gizle + Includes unsaved changes Kaydedilmemiş değişiklikleri içerir + Its security settings don't allow copying pages Güvenlik ayarları sayfaların kopyalanmasına izin vermiyor + More ways to add files Dosya eklemenin diğer yolları + No documents are open Hiçbir belge açık değil + No pages selected Hiçbir sayfa seçilmedi + Opened, but its security settings don't allow combining. Enter the permissions password. Açıldı, ancak güvenlik ayarları birleştirmeye izin vermiyor. İzinler parolasını girin. + Password-protected Parola korumalı + Protected: the combined file won't keep its security settings Korumalı: Birleştirilen dosya güvenlik ayarlarını korumayacaktır + Remove {n} files {n} dosyayı kaldır + Reset columns Sütunları sıfırla + Select a file first Önce bir dosya seçin + Select a password-protected file first Önce parola korumalı bir dosya seçin + Show password Parolayı göster + The password is only used to read the file; it isn't saved. Parola yalnızca dosyayı okumak için kullanılır; kaydedilmez. + The same password is tried on each file. Her dosyada aynı parola denenir. + Unlock Kilidi aç + Unlock {name} {name} kilidini aç + Unlock {n} files {n} dosyanın kilidini aç + Unlocked: the combined file won't be password-protected Kilit açıldı: Birleştirilen dosya parola korumalı olmayacak + Unlock… Kilidi aç… + Warnings Uyarılar + Wrong password Yanlış parola + sorted ascending artan sırada + sorted descending azalan sırada + yesterday dün + {k} of {n} {k} / {n} + {name} can't be combined: {why} {name} birleştirilemiyor: {why} + {n} days ago {n} gün önce + {n} files {n} dosya + {n} files need attention {n} dosya ilgilenilmesi gerekiyor + {n} selected {n} seçildi + Choose a PNG or JPEG image (up to 4 MiB). Transparency is preserved. Bir PNG veya JPEG görseli seçin (en fazla 4 MiB). Saydamlık korunur. + Couldn't import the signature image: {e} İmza görseli içe aktarılamadı: {e} + Open Recent Son kullanılanları aç + Pin folder to Home… Klasörü Başlangıç'a sabitle… + Folders Klasörler + Pin a folder… Klasör sabitle… + Pin a folder to Home Giriş'e bir klasör sabitle + Pin a folder to list its newest PDFs here, such as where your scanner or a synced cloud drive saves them. En yeni PDF'lerini burada listelemek için bir klasörü sabitleyin; örneğin tarayıcınızın veya eşitlenmiş bir bulut sürücüsünün bunları kaydettiği yer. + Unpin Sabitlemeyi kaldır + Unpin {folder} {folder} sabitlemesini kaldır + Looking for PDFs… PDF'ler aranıyor… + This folder isn't available right now. It may have moved, or its drive or cloud sync is offline. Bu klasör şu anda kullanılamıyor. Taşınmış olabilir veya sürücüsü ya da bulut eşitlemesi çevrimdışı olabilir. + No PDFs in this folder yet Bu klasörde henüz PDF yok + Show less Daha az göster + Show {n} more {n} tane daha göster + The newest {n} of {total} PDFs En yeni {n} / {total} PDF + Pinned folders need the desktop app Sabitlenmiş klasörler masaüstü uygulamasını gerektirir + Choose a folder on this computer to pin Bu bilgisayarda sabitlemek için bir klasör seçin + {folder} is already pinned to Home {folder} zaten Giriş'e sabitlenmiş + You can pin up to {n} folders. Unpin one first. En fazla {n} klasör sabitleyebilirsiniz. Önce birinin sabitlemesini kaldırın. + Pinned {folder} to Home {folder} Giriş'e sabitlendi + No recent files Son kullanılan dosya yok + Go to page… Sayfaya git… + About Hakkında + Contributors Katkıda Bulunanlar + Models Modeller + Only continue if you trust this document. An address can carry information from the document, such as what you typed into its form. Yalnızca bu belgeye güveniyorsanız devam edin. Bir adres, formuna yazdıklarınız gibi belgeden bilgi taşıyabilir. + Print single-sided. Keep the sheets in order, cut at the marks, then stack the piles left to right, top to bottom. Tek taraflı yazdırın. Sayfaları sırayla tutun, işaretlerden kesin, ardından desteleri soldan sağa, yukarıdan aşağıya istifleyin. + Unlocked {n} of {m} files {n} / {m} dosyanın kilidi açıldı + No PDFs found in {folder} {folder} içinde PDF bulunamadı + Added the first {n} PDFs in {folder} {folder} içindeki ilk {n} PDF eklendi +comment menu Edit Düzenle + Undo {action} {action} geri al + Redo {action} {action} yinele +signature pad Type Yaz +certificate Subject Konu +action wizard Start Başlat + Reopen the files that were open when PdfCraft last closed PdfCraft en son kapatıldığında açık olan dosyaları yeniden aç + Add {what} {what} ekle + Use {what} {what} kullan + Place saved {what} Kayıtlı {what} yerleştir + Remove saved {what} Kayıtlı {what} kaldır + A link Bir bağlantı + A button Bir düğme + A script Bir komut dosyası + {who} in this document tried to open an address PdfCraft won't open: {e}. Bu belgedeki {who}, PdfCraft'ın açamayacağı bir adresi açmaya çalıştı: {e}. + {who} in this document tried to open an address PdfCraft won't open: {e}. Only web (http, https) and email (mailto) links open from documents. Bu belgedeki {who}, PdfCraft'ın açamayacağı bir adresi açmaya çalıştı: {e}. Belgelerden yalnızca web (http, https) ve e-posta (mailto) bağlantıları açılır. + Another file dialog is still open. Finish with it first. Başka bir dosya iletişim kutusu hala açık. Önce onu tamamlayın. + Couldn't show the file picker. Please try again. Dosya seçici gösterilemedi. Lütfen tekrar deneyin. + The document changed while you were choosing a file, so nothing was changed. Bir dosya seçerken belge değişti, bu yüzden hiçbir şey değiştirilmedi. + The document was closed before it could be saved. Belge kaydedilmeden önce kapatıldı. + The pages were extracted but not deleted, because the document changed while you were choosing a folder. Sayfalar ayıklandı ancak silinmedi, çünkü bir klasör seçerken belge değişti. + That didn't work: an internal error stopped it ({m}). Bu işlem yapılamadı: dahili bir hata durdurdu ({m}). + The Windows store's digital IDs couldn't be listed: {e} Windows deposunun dijital kimlikleri listelenemedi: {e} + Align fields Alanları hizala + Distribute fields Alanları dağıt + Match field sizes Alan boyutlarını eşleştir + Mark for redaction Karartma için işaretle + Mark pages for redaction Sayfaları karartma için işaretle + Change document properties Belge özelliklerini değiştir + Mark differences Farklılıkları işaretle + Run form script Form komut dosyasını çalıştır + Scripted content must be usable with a keyboard and assistive technology. Check the document's scripts. Komut dosyasıyla oluşturulan içerik klavye ve yardımcı teknolojiyle kullanılabilir olmalıdır. Belgenin komut dosyalarını denetleyin. + Table rows belong directly in a table or its head, body or foot. Fix the table's tags. Tablo satırları doğrudan bir tabloya veya onun üst, gövde ya da alt kısmına aittir. Tablonun etiketlerini düzeltin. + Basic constraints Temel kısıtlamalar + SHA-1 digest SHA-1 özeti + SHA-256 digest SHA-256 özeti + Squiggly Dalgalı + Home / End Home / End + H / Space (hold) H / Boşluk (basılı tutun) + Clear Recent Files Son kullanılan dosyaları temizle + Remove from Recent Son kullanılanlardan kaldır + Remove {name} from Recent {name} öğesini son kullanılanlardan kaldır + The standard fonts can't draw “{c}” ({code}). Only Western European characters can be added as text for now. Standart yazı tipleri “{c}” ({code}) karakterini çizemez. Şimdilik metin olarak yalnızca Batı Avrupa karakterleri eklenebilir. + Larger pages Daha büyük sayfalar + Smaller pages Daha küçük sayfalar + Reset page size Sayfa boyutunu sıfırla + Date format Tarih biçimi + Fill & Sign dates Doldur ve İmzala tarihleri + Pattern Kalıp + Today: {date} Bugün: {date} + yyyy or yy year · m or mm month · mmm or mmmm month name · d or dd day · ddd or dddd weekday. H, h, M, s and t are time letters and can't be used on their own: put {escape} before a letter to show it as is. yyyy veya yy yıl · m veya mm ay · mmm veya mmmm ay adı · d veya dd gün · ddd veya dddd haftanın günü. H, h, M, s ve t zaman harfleridir ve tek başlarına kullanılamazlar: harfi olduğu gibi göstermek için önüne {escape} koyun. + Same as interface ({language}) Arayüz ile aynı ({language}) + {chars} can't be written into the PDF yet: Fill & Sign text is Western European only. Pick a numeric format such as dd.mm.yyyy. {chars} henüz PDF'e yazılamıyor: Doldur ve İmzala metni yalnızca Batı Avrupa karakterlerini destekler. dd.mm.yyyy gibi sayısal bir biçim seçin. + Opens the printer driver's preferences. They are saved for every print from this computer; the copies, two-sided, grayscale and paper chosen here still apply. Yazıcı sürücüsünün tercihlerini açar. Bu bilgisayardan yapılan her yazdırma için kaydedilirler; burada seçilen kopya, çift taraflı, gri tonlama ve kağıt geçerli olmaya devam eder. + The printer driver's own settings for this print: paper tray, paper type, finishing and more. Bu baskı için yazıcı sürücüsünün kendi ayarları: kağıt tepsisi, kağıt türü, sonlandırma ve daha fazlası. + Couldn't open the printer's preferences: {e} Yazıcı tercihleri açılamadı: {e} + Printer properties Yazıcı özellikleri + Reset to printer defaults Yazıcı varsayılanlarına sıfırla + This printer offers no settings of its own. Bu yazıcı kendi başına hiçbir ayar sunmuyor. + Scroll tabs left Sekmeleri sola kaydır + Scroll tabs right Sekmeleri sağa kaydır + Shared Field Properties Paylaşılan Alan Özellikleri + {n} fields selected {n} alan seçildi + Mixed Karışık + Custom font Özel yazı tipi + Only checked properties will change. Mixed means the selected fields have different values. Yalnızca işaretli özellikler değişir. Karışık, seçilen alanların farklı değerlere sahip olduğu anlamına gelir. + Shift- or Ctrl/Command-click to select several fields. Birden çok alan seçmek için Shift veya Ctrl/Komut tuşuna basarak tıklayın. + Font size 0 uses automatic sizing. Yazı tipi boyutu 0 olduğunda otomatik boyutlandırma kullanılır. + Character limit: Karakter sınırı: + Character limit 0 means no limit. A comb of characters needs a limit. Karakter sınırı 0, sınır olmadığı anlamına gelir. Taraklı karakterler için sınır gereklidir. + To change locked fields, also set Locked to No. Kilitli alanları değiştirmek için Kilitli ayarını da Hayır olarak belirleyin. + Comb of characters Karakter tarağı + The document changed while you were editing field properties. Alan özelliklerini düzenlerken belge değişti. + The document is no longer open. Belge artık açık değil. + The field properties dialog is no longer open. Alan özellikleri iletişim kutusu artık açık değil. + The field properties could not be changed. Alan özellikleri değiştirilemedi. + The selected fields could not be found. Select them again. Seçilen alanlar bulunamadı. Bunları tekrar seçin. + The field {name} no longer exists. {name} alanı artık mevcut değil. + A selected field has no widget on any page, so the fields can't be edited together. Seçilen bir alanın hiçbir sayfada pencere öğesi yok, bu nedenle alanlar birlikte düzenlenemez. + ({n} at {dpi} dpi, the largest size they can be drawn at) ({dpi} dpi'de {n}, çizilebilecekleri en büyük boyut) + +# Keyboard key names +key Ctrl Ctrl +key Shift Shift +key Delete Delete +key Esc Esc +key Home Home +key End End +key Space Boşluk + +# Printing and multi-object selection + Looking for printers… Yazıcılar aranıyor… + Sending to {printer}… {printer} yazıcısına gönderiliyor… + Column select Sütun seçimi +@plural {n} object selected|{n} objects selected {n} nesne seçildi + Drag the selection to move it. Esc clears the selection. Taşımak için seçimi sürükleyin. Esc seçimi temizler. + Shift-click to select several objects. Drag empty space to select an area. Birden çok nesne seçmek için Shift tuşuna basarak tıklayın. Bir alanı seçmek için boş alanı sürükleyin. + Select at most 1000 objects at a time. Bir seferde en fazla 1000 nesne seçin. + Finish or discard the text being edited before selecting several objects. Birden fazla nesne seçmeden önce düzenlenen metni tamamlayın veya iptal edin. + Move object Nesneyi taşı + Move objects Nesneleri taşı + Vertical text cannot be moved with this selection. Dikey metin bu seçimle taşınamaz. + Text used as a clipping path cannot be moved with this selection. Kırpma yolu olarak kullanılan metin bu seçimle taşınamaz. + These objects could not be moved. Select them again and try a smaller move. Bu nesneler taşınamadı. Bunları tekrar seçin ve daha küçük bir taşıma deneyin. + This page contains objects that cannot be selected together. Bu sayfa birlikte seçilemeyen nesneler içeriyor. + Fill colour Dolgu rengi + No fill Dolgu yok + Fill colour: none Dolgu rengi: yok + Grid view Izgara görünümü + List view Liste görünümü + Check the pages Sayfaları denetle + Switch to List view to change the pages it takes. Alacağı sayfaları değiştirmek için Liste görünümüne geçin. + Auto ({percent}%) Otomatik (%{percent}) + Auto follows the system's text size. Pages always render at the display's full resolution. Otomatik, sistemin metin boyutunu izler. Sayfalar her zaman ekranın tam çözünürlüğünde işlenir. + Files are combined in the order shown. Drag them to change it. Dosyalar gösterilen sırada birleştirilir. Değiştirmek için sürükleyin. + Preview unavailable: {e} Önizleme kullanılamıyor: {e} + Pages: {k} of {n} Sayfalar: {k} / {n} + Some bookmarks are not shown here: the panel lists a limited number, nested a limited depth. Bazı yer imleri burada gösterilmiyor: panel sınırlı sayıda ve sınırlı derinlikte listeler. + Expand all Tümünü genişlet + Collapse all Tümünü daralt + Remove page Sayfayı kaldır + Remove {n} pages {n} sayfayı kaldır + +# Shared panel docking + Reset panel layout Panel düzenini sıfırla + Float panel Paneli yüzdür + Move to group Gruba taşı + Panels Paneller + Resize panels Panelleri yeniden boyutlandır + Resize panel window Panel penceresini yeniden boyutlandır + Tools Araçlar + Inspector Denetçi + Interface size Arayüz boyutu + +# Added for labels that landed alongside this catalog (2026-10-11). + Appearance mode Görünüm modu + Light Açık + Dark Koyu + Active Etkin + Dark theme Koyu tema + Appearance settings… Görünüm ayarları… + Save ({key}) Kaydet ({key}) + Select page {label} {label} sayfasını seç + Rotate page {label} counterclockwise {label} sayfasını saat yönünün tersine döndür + Rotate page {label} clockwise {label} sayfasını saat yönünde döndür + Delete page {label} {label} sayfasını sil + Offset from centre Merkezden uzaklık + Graphic Grafik + Signer's name İmzalayanın adı + Fill & Sign signature Doldur ve İmzala imzası + Choose image… Resim seç… + This file is already as small as it can be made ({size}). No copy was saved. Bu dosya zaten olabildiğince küçük ({size}). Kopya kaydedilmedi. + Saved {place}: {before} → {after} (not smaller){detail} {place} kaydedildi: {before} → {after} (küçülmedi){detail} + Enter permissions password… İzin parolasını gir… + Permissions password İzin parolası + “{name}” has security restrictions. Enter its permissions password to lift them. “{name}” güvenlik kısıtlamalarına sahip. Kaldırmak için izin parolasını girin. + Restrictions lifted Kısıtlamalar kaldırıldı + Couldn't unlock {name}: {e} {name} kilidi açılamadı: {e} + This document has no restrictions to lift Bu belgede kaldırılacak kısıtlama yok + The document's security settings don't allow copying text and images Belgenin güvenlik ayarları metin ve resimlerin kopyalanmasına izin vermiyor + Add the files to combine Birleştirilecek dosyaları ekleyin + Leave out files that can't be read? Okunamayan dosyalar dışarıda bırakılsın mı? + These files can't be read, so they won't be in the combined PDF: Bu dosyalar okunamıyor, bu yüzden birleştirilmiş PDF'de yer almayacak: + Combine the rest Kalanları birleştir + and {n} more ve {n} tane daha + Drop PDFs, images, text files or folders here, or choose them. PDF'leri, resimleri, metin dosyalarını veya klasörleri buraya bırakın ya da seçin. + Every PDF, image and text file in the folder Klasördeki tüm PDF, resim ve metin dosyaları + Every PDF, image and text file in the folder and the folders inside it Klasördeki ve alt klasörlerindeki tüm PDF, resim ve metin dosyaları + No PDFs, images or text files found in {folder} {folder} içinde PDF, resim veya metin dosyası bulunamadı + Added the first {n} files in {folder} {folder} içindeki ilk {n} dosya eklendi + Margins Kenar boşlukları + For page sizes and margins. Sayfa boyutları ve kenar boşlukları için. + PDF page: {size} PDF sayfası: {size} + Match page at 100% Sayfayı %100'de eşleştir + PDF scale: {scale}% PDF ölçeği: %{scale} + Printer paper and scaling may differ. Check driver settings. Yazıcı kâğıdı ve ölçekleme farklı olabilir. Sürücü ayarlarını kontrol edin. + Drawing scale changes. Use Actual size for full-scale plans. Çizim ölçeği değişir. Tam ölçekli planlar için Gerçek boyutu kullanın. + Reload documents when their files change on disk Diskteki dosyaları değiştiğinde belgeleri yeniden yükle + This file changed on disk. Reload it to see the new version; your unsaved changes will be lost. Bu dosya diskte değişti. Yeni sürümü görmek için yeniden yükleyin; kaydedilmemiş değişiklikleriniz kaybolacak. + Reload Yeniden yükle + Couldn't reload {name}: {e} {name} yeniden yüklenemedi: {e} + Mouse wheel scroll speed Fare tekerleği kaydırma hızı +scroll speed Slow Yavaş +scroll speed Normal Orta +scroll speed Fast Hızlı + Enhance camera image Kamera görüntüsünü iyileştir + Drag the corners onto the document's edges, then choose Enhance page. Köşeleri belgenin kenarlarına sürükleyin, ardından Sayfayı iyileştir'i seçin. + Enhance page Sayfayı iyileştir + Done adding text Metin ekleme bitti + Discard this text (Esc) Bu metni at (Esc) + Clean-up Temizleme + Original Orijinal + Greyscale Gri tonlamalı + Black and white Siyah beyaz diff --git a/crates/ui-egui/tests/fonts.rs b/crates/ui-egui/tests/fonts.rs index dcf91594..c9006911 100644 --- a/crates/ui-egui/tests/fonts.rs +++ b/crates/ui-egui/tests/fonts.rs @@ -238,6 +238,7 @@ fn primary_ui_fonts_cover_latin_and_cyrillic_catalogs() { ("hu", include_str!("../src/i18n/hu.tsv")), ("uk", include_str!("../src/i18n/uk.tsv")), ("it", include_str!("../src/i18n/it.tsv")), + ("tr", include_str!("../src/i18n/tr.tsv")), ("sl", include_str!("../src/i18n/sl.tsv")), ] { let mut text = String::from("áčďéěíňóřšťúůýžÁČĎÉĚÍŇÓŘŠŤÚŮÝŽãõçâêôàÃÕÇÂÊÔÀñÑüÜäÄöÖőűß„“¿¡«»…ґҐєЄіІїЇәғқңөұүһӘҒҚҢӨҰҮҺ"); diff --git a/docs/localization.md b/docs/localization.md index b19eda5c..5519e5bb 100644 --- a/docs/localization.md +++ b/docs/localization.md @@ -1,9 +1,11 @@ # Interface language +Choose **Menu > Edit > Preferences…** (Command-comma on macOS, Ctrl-comma elsewhere) **> Interface language** and select **Auto**, **English**, **日本語**, **简体中文**, **繁體中文**, **Русский**, **Български**, **Čeština**, **Português (Brasil)**, **Deutsch**, **Español**, **Français**, **తెలుగు**, **Magyar**, **Українська**, **Italiano**, **العربية** or **Türkçe**. The change applies immediately and persists between launches. Command ids, document contents and file names are unchanged. Choose **Menu > Edit > Preferences…** (Command-comma on macOS, Ctrl-comma elsewhere) **> Interface language** and select **Auto**, **English**, **日本語**, **简体中文**, **繁體中文**, **Русский**, **Български**, **Čeština**, **Português (Brasil)**, **Deutsch**, **Español**, **Français**, **తెలుగు**, **Magyar**, **Українська**, **Italiano**, **العربية** or **Slovenščina**. The change applies immediately and persists between launches. Command ids, document contents and file names are unchanged. Displayed keyboard shortcuts use the same active interface language. For example, German shows `Strg+K` and `Strg+Umschalt+S` in the toolbar, menus, command palette, tooltips and Keyboard shortcuts dialog; macOS keeps its modifier symbols (`⌘`, `⇧`, `⌃`). Key names have their own `key` catalog context, so the Delete key can read `Entf` while the command remains `Löschen`. A missing key-name entry falls back to the English key name. Actual bindings and machine-facing command ids remain stable. +**Auto** (the default) follows the system language: `LC_ALL`, `LC_MESSAGES` or `LANG`, then the preferred-languages list on macOS. Any Portuguese locale (`pt_BR`, `pt_PT`) uses the Brazilian catalog, any French locale (`fr`, `fr_FR`, `fr_CA`, `fr_BE`) the French one, any German locale (`de`, `de_DE`, `de_AT`, `de_CH`) the German one, and Chinese locales for Taiwan, Hong Kong and Macau (`zh_TW`, `zh_HK`, `zh_MO`, `zh-Hant`) the Traditional Chinese one; other Chinese locales (`zh`, `zh_CN`, `zh_SG`, `zh-Hans`) the Simplified Chinese one. Telugu locales (`te`, `te_IN`) use the Telugu catalog, Bulgarian locales (`bg`, `bg_BG`) the Bulgarian one, and Hungarian locales (`hu`, `hu_HU`) the Hungarian one, and Ukrainian locales (`uk`, `uk_UA`) the Ukrainian one, and Italian locales (`it`, `it_IT`, `it_CH`) the Italian one, and Arabic locales (`ar`, `ar_MA`, `ar_EG`, `ar_SA`, …) the Arabic one, and Turkish locales (`tr`, `tr_TR`) the Turkish one. A system language without a catalog shows English. On Windows, **Auto** uses the Windows display languages, in preference order, when no supported locale is selected by those environment variables. Neutral `C`/`POSIX` locale values (including `C.UTF-8`) inherited from a launcher do not override the Windows display language; explicitly named supported locales such as `fr_FR.UTF-8` still do. The query asks Windows directly (`GetUserPreferredUILanguages`, through the `sys-locale` crate) without starting a process, and is cached for the process. If detection fails or the display language has no catalog, PdfCraft shows English. **Auto** (the default) follows the system language: `LC_ALL`, `LC_MESSAGES` or `LANG`, then the preferred-languages list on macOS. Any Portuguese locale (`pt_BR`, `pt_PT`) uses the Brazilian catalog, any French locale (`fr`, `fr_FR`, `fr_CA`, `fr_BE`) the French one, any German locale (`de`, `de_DE`, `de_AT`, `de_CH`) the German one, and Chinese locales for Taiwan, Hong Kong and Macau (`zh_TW`, `zh_HK`, `zh_MO`, `zh-Hant`) the Traditional Chinese one; other Chinese locales (`zh`, `zh_CN`, `zh_SG`, `zh-Hans`) the Simplified Chinese one. Telugu locales (`te`, `te_IN`) use the Telugu catalog, Bulgarian locales (`bg`, `bg_BG`) the Bulgarian one, and Hungarian locales (`hu`, `hu_HU`) the Hungarian one, and Ukrainian locales (`uk`, `uk_UA`) the Ukrainian one, and Italian locales (`it`, `it_IT`, `it_CH`) the Italian one, and Arabic locales (`ar`, `ar_MA`, `ar_EG`, `ar_SA`, …) the Arabic one, and Slovenian locales (`sl`, `sl_SI`) the Slovenian one. A system language without a catalog shows English. On Windows, **Auto** uses the Windows display languages, in preference order, when no supported locale is selected by those environment variables. Neutral `C`/`POSIX` locale values (including `C.UTF-8`) inherited from a launcher do not override the Windows display language; explicitly named supported locales such as `fr_FR.UTF-8` still do. The query asks Windows directly (`GetUserPreferredUILanguages`, through the `sys-locale` crate) without starting a process, and is cached for the process. If detection fails or the display language has no catalog, PdfCraft shows English. The control channel exposes the setting through `ui.set`: @@ -12,14 +14,16 @@ The control channel exposes the setting through `ui.set`: {"method":"ui.set","params":{"key":"language","value":"ja"}} ``` -The value is `auto` or a language code (`en`, `ja`, `zh-hans`, `zh-hant`, `ru`, `bg`, `cs`, `pt-br`, `de`, `es`, `fr`, `te`, `hu`, `uk`, `it`, `ar`), in any case. `ui.state` reports `language` as written in the preferences (`auto`, `en`, `ja`, `zh-hans`, `zh-hant`, `ru`, `bg`, `cs`, `pt-br`, `de`, `es`, `fr`, `te`, `hu`, `uk`, `it` or `ar`). Unknown values return an error without changing the current setting. Preferences saved before this setting existed follow the system language. +The value is `auto` or a language code (`en`, `ja`, `zh-hans`, `zh-hant`, `ru`, `bg`, `cs`, `pt-br`, `de`, `es`, `fr`, `te`, `hu`, `uk`, `it`, `ar`, `tr`), in any case. `ui.state` reports `language` as written in the preferences (`auto`, `en`, `ja`, `zh-hans`, `zh-hant`, `ru`, `bg`, `cs`, `pt-br`, `de`, `es`, `fr`, `te`, `hu`, `uk`, `it`, `ar` or `tr`). Unknown values return an error without changing the current setting. Preferences saved before this setting existed follow the system language. +Dialogs, panels, menus, notices and history labels go through the catalog (`tl!`). Japanese, Traditional Chinese, Simplified Chinese, Russian, Bulgarian, German, Spanish, French, Telugu, Hungarian, Ukrainian, Italian, Brazilian Portuguese and Arabic translate them (about 2,000–2,140 entries each); Czech and Turkish cover the menus so far. Untranslated labels use English. Error details that come from the engine or the operating system are shown as they are, inside a translated frame ("操作「…」に失敗しました: …"). The command palette matches the translated label, the English label and the command id. Vertical Japanese PDF rendering is an existing viewer feature; this change does not add vertical text editing. Per-language coverage, script support and what each language still needs are in [localization-parity.md](localization-parity.md). Dialogs, panels, menus, notices and history labels go through the catalog (`tl!`). Japanese, Traditional Chinese, Simplified Chinese, Russian, Bulgarian, German, Spanish, French, Telugu, Hungarian, Ukrainian, Italian, Brazilian Portuguese, Arabic and Slovenian translate them (about 2,000–2,190 entries each); Czech covers the menus so far. Untranslated labels use English. Error details that come from the engine or the operating system are shown as they are, inside a translated frame ("操作「…」に失敗しました: …"). The command palette matches the translated label, the English label and the command id. Vertical Japanese PDF rendering is an existing viewer feature; this change does not add vertical text editing. Per-language coverage, script support and what each language still needs are in [localization-parity.md](localization-parity.md). Whole sentences that the layers below word — what the accessibility checker found, what the PDF/A verifier reports, what a signature's details say, what is wrong with the pages to print, what an edit is called in the history — are translated too, because those layers hand the sentence over apart from the document's own words. See [Sentences the engine words](#sentences-the-engine-words). The Ukrainian catalog has 2,047 entries, written from the meaning of the English labels using Ukrainian computing vocabulary. Commands use infinitives; fixed-count messages use count-neutral wording. Plural-aware messages use the three [CLDR Ukrainian integer forms](https://www.unicode.org/cldr/charts/48/supplemental/language_plural_rules.html#uk): `1 сторінка`, `2 сторінки`, `5 сторінок`; 11–14 take the third form, while 21 and 22 return to the first and second. Calendar dates use genitive month names (`9 жовтня 2026`), while headings use standalone names (`Жовтень 2026`). Home recommendation cards grow to fit translated text; the toolbar search hint truncates before the shortcut, with the full label available on hover. +Japanese interface text uses BIZ UDPGothic from [craft-fonts](https://github.com/storytold/craft-fonts), an optional build input that every release includes (`CRAFT_FONTS_DIR`; see the README). A build made without it has no Japanese face, so Japanese labels show replacement boxes. Chinese is drawn with the same craft-fonts CJK faces (in Simplified Chinese the `Hans` faces come first, so a line never mixes faces with different baselines), so a character none of them has shows a replacement box (and every Chinese character does in a build without craft-fonts). Russian, Bulgarian, Czech, Brazilian Portuguese, German, Spanish, French, Hungarian, Italian and Turkish need only the bundled Latin faces (tested in `tests/fonts.rs`). Hungarian uses the same one/other plural rule as English: 1 is singular, and every other count — including 0 — is the other form. After a numeral the noun stays singular (`1 oldal`, `2 oldal`). Telugu is drawn with a craft-fonts `Telu` face (Noto Sans Telugu, kept in the web build too), whose conjuncts egui shapes; a build without one shows Telugu labels as replacement boxes, and the release-pinned craft-fonts input has no Telugu face yet. Japanese interface text uses BIZ UDPGothic from [craft-fonts](https://github.com/storytold/craft-fonts), an optional build input that every release includes (`CRAFT_FONTS_DIR`; see the README). A build made without it has no Japanese face, so Japanese labels show replacement boxes. Chinese is drawn with the same craft-fonts CJK faces (in Simplified Chinese the `Hans` faces come first, so a line never mixes faces with different baselines), so a character none of them has shows a replacement box (and every Chinese character does in a build without craft-fonts). Russian, Bulgarian, Czech, Brazilian Portuguese, German, Spanish, French, Hungarian, Italian and Slovenian need only the bundled Latin faces (tested in `tests/fonts.rs`). Slovenian has four plural forms: one (1, 101, 201…), two (2, 102…), few (3, 4, 103…) and other (everything else, including 0, 5–20 and 21+), so `@plural` entries list four forms (`1 stran`, `2 strani`, `3 strani`, `5 strani`). Hungarian uses the same one/other plural rule as English: 1 is singular, and every other count — including 0 — is the other form. After a numeral the noun stays singular (`1 oldal`, `2 oldal`). Telugu is drawn with a craft-fonts `Telu` face (Noto Sans Telugu, kept in the web build too), whose conjuncts egui shapes; a build without one shows Telugu labels as replacement boxes, and the release-pinned craft-fonts input has no Telugu face yet. Japanese interface text uses BIZ UDPGothic from [craft-fonts](https://github.com/storytold/craft-fonts), an optional build input that every release includes (`CRAFT_FONTS_DIR`; see the README). A build made without it has no Japanese face, so Japanese labels show replacement boxes. Chinese is drawn with the same craft-fonts CJK faces (in Simplified Chinese the `Hans` faces come first, so a line never mixes faces with different baselines), so a character none of them has shows a replacement box (and every Chinese character does in a build without craft-fonts). Until craft-fonts has an allowed `Hans` face, the desktop app draws a Chinese interface with a Chinese face already installed on the machine (Microsoft YaHei or JhengHei on Windows, PingFang, Hiragino Sans GB or STHeiti on macOS, WenQuanYi or Droid Sans Fallback on Linux; never Noto CJK or Source Han), placed before the Japanese faces so a line keeps one face (#826). The web build has no installed fonts to fall back on. Russian, Bulgarian, Czech, Brazilian Portuguese, German, Spanish, French, Hungarian and Italian need only the bundled Latin faces (tested in `tests/fonts.rs`). Hungarian uses the same one/other plural rule as English: 1 is singular, and every other count — including 0 — is the other form. After a numeral the noun stays singular (`1 oldal`, `2 oldal`). Telugu is drawn with a craft-fonts `Telu` face (Noto Sans Telugu, kept in the web build too), whose conjuncts egui shapes; a build without one shows Telugu labels as replacement boxes, and the release-pinned craft-fonts input has no Telugu face yet. From c65f32a0782c7bf28fe53c9389f760c75fb8eecd Mon Sep 17 00:00:00 2001 From: Maksim Popov <51298369+MaksPV@users.noreply.github.com> Date: Mon, 12 Oct 2026 00:28:30 +0300 Subject: [PATCH 5/9] Content edits on heavy documents: narrow refresh, cached appearances, per-page invalidation (#1122) * Content edits on heavy documents: narrow refresh, cached appearances, per-page invalidation Every content edit (drag, resize, retype) on a large document ran a full synchronous refresh on the UI thread: a whole-file re-inspection plus a redraw of every missing annotation appearance, then dropped all page rasters. On a 72 MB / 202-page CAD file with 27k appearance-less annotations each commit cost ~1.5 s before anything re-rendered. Now ~0.3 s. - New Scope::Content for edits that touch only page content, resources and added content/marks (move/resize/retype/images/added items/marks): the page tree, annotations, fields, links, outline and fonts are unchanged, so refresh_content rebuilds working bytes, display bytes and renderer and validates with a light check_pages (parse plus page count, keeping the roll-back guarantee) instead of a full inspect. added/marks are re-read only when the edit can change them; undo/redo re-read both. - AppearanceCache in pdfcraft-annot: built display appearances are reused while no annotation edit runs (content edits leave every annotation dictionary alone); cleared on comment, structural and full refreshes. set_appearance is split into build/apply halves so a hit skips the draw. - check_pages in pdfcraft-render: light post-edit validation. - Viewer: single-page content edits invalidate only their page (page_changed); other pages keep tiles, text and thumbnails. - XFA dataset sync now also runs for content edits. Co-Authored-By: Muse Spark * Fonts added by a content edit show in Properties and doc_info (read again on demand); form edits clear cached appearances too * M14: DATE_LANGUAGES is a static (21 entries trip clippy's large_const_arrays); rustfmt the Greek entry Co-Authored-By: Claude Opus 5.5 * Preferences scroll in a short window; tests scroll to controls below the fold (#487's roomier rows); the highlighter test waits for its mark under load Co-Authored-By: Claude Opus 5.5 * Select tool: a multi-click selects only on the frame egui reports it, so a cleared selection (or the highlighter's marked word) isn't selected again The highlighter test now clicks at a human pace, with frames in between, which is what exposed it. Co-Authored-By: Claude Opus 5.5 --------- Co-authored-by: Muse Spark Co-authored-by: Brandon Thomas Co-authored-by: Claude Opus 5.5 --- crates/annot/src/lib.rs | 92 ++++++++++++- crates/annot/src/tests.rs | 23 ++++ crates/automation/src/lib.rs | 6 +- crates/engine/src/dates.rs | 32 ++++- crates/engine/src/lib.rs | 132 +++++++++++++++++-- crates/engine/src/tests.rs | 100 ++++++++++++++ crates/render/src/inspect.rs | 26 ++++ crates/render/src/lib.rs | 2 +- crates/ui-egui/src/canvas.rs | 5 +- crates/ui-egui/src/dialogs.rs | 8 +- crates/ui-egui/src/editing.rs | 38 +++++- crates/ui-egui/src/i18n/mod.rs | 4 +- crates/ui-egui/tests/date_format.rs | 3 + crates/ui-egui/tests/javascript.rs | 3 + crates/ui-egui/tests/ui.rs | 2 + crates/ui-egui/tests/ui_group/bulk_fields.rs | 3 + crates/ui-egui/tests/ui_group/identity.rs | 4 +- crates/ui-egui/tests/ui_group/text_select.rs | 20 +++ crates/ui-egui/tests/view.rs | 2 + 19 files changed, 477 insertions(+), 28 deletions(-) diff --git a/crates/annot/src/lib.rs b/crates/annot/src/lib.rs index b59f7271..00e8776b 100644 --- a/crates/annot/src/lib.rs +++ b/crates/annot/src/lib.rs @@ -14,7 +14,7 @@ #![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)] -use pdfcraft_cos::{Dict, Document, ObjRef, Object, PdfString}; +use pdfcraft_cos::{Dict, Document, ObjRef, Object, PdfString, Stream}; pub mod appearance; pub mod links; @@ -1255,7 +1255,14 @@ pub fn orient_image_stamp(doc: &mut Document, page: usize, index: usize, rotatio pub fn set_appearance(doc: &mut Document, r: ObjRef) -> Result<(), AnnotError> { let d = annot_dict(doc, r); let subtype = String::from_utf8_lossy(d.name(b"Subtype").unwrap_or_default()).into_owned(); - let Some(mut stream) = appearance::build(&d) else { return Err(AnnotError::Unsupported(subtype)) }; + let Some(stream) = build_appearance(doc, &d) else { return Err(AnnotError::Unsupported(subtype)) }; + apply_appearance(doc, r, &d, stream) +} + +/// Draw the appearance for an annotation dictionary: the expensive half of [`set_appearance`]. +/// Pure from the dictionary and the objects it points at, so [`AppearanceCache`] can reuse it. +fn build_appearance(doc: &Document, d: &Dict) -> Option { + let mut stream = appearance::build(d)?; // Image stamp restyling must retain the placement's page-axis correction. if matches!(d.get(b"PCPictureImage"), Some(Object::Bool(true))) { let matrix = d.get(b"AP").and_then(|ap| { @@ -1268,6 +1275,11 @@ pub fn set_appearance(doc: &mut Document, r: ObjRef) -> Result<(), AnnotError> { stream.dict.set(b"Matrix".to_vec(), matrix); } } + Some(stream) +} + +/// Store a built appearance on the annotation: the cheap half of [`set_appearance`]. +fn apply_appearance(doc: &mut Document, r: ObjRef, d: &Dict, stream: Stream) -> Result<(), AnnotError> { let ap = doc.add(Object::Stream(stream)); // A copy (a shared /AP is left alone) that keeps unknown entries; the old down and rollover // appearances would show the previous look on press or hover, so they go with the old /N. @@ -1282,12 +1294,75 @@ pub fn set_appearance(doc: &mut Document, r: ObjRef) -> Result<(), AnnotError> { Ok(()) } +/// Display appearances built for annotations without any, reused while the annotation +/// dictionaries they were drawn from are unchanged. +/// +/// A content edit (moving a paragraph, retyping text, …) leaves every annotation dictionary +/// alone, but the display copy is rebuilt from the edited document on every refresh: without +/// this cache each refresh redrew every appearance (27k on one CAD file, ~0.7 s per edit +/// commit). Entries are keyed by annotation reference; object numbers are never reused within +/// a document lineage, so an entry stays valid until an annotation edit runs. Callers clear +/// the cache on any refresh that may change annotations (comments, structural and full +/// refreshes) and keep it for content-only ones. +#[derive(Clone, Debug, Default)] +pub struct AppearanceCache { + entries: std::collections::HashMap, + bytes: usize, + hits: u64, + builds: u64, +} + +impl AppearanceCache { + /// Cached appearance data restarts past this instead of growing without bound. + const MAX_BYTES: usize = 64 << 20; + + /// Forget every entry (an annotation edit may have changed the dictionaries). + pub fn clear(&mut self) { + *self = Self::default(); + } + + fn get(&mut self, r: ObjRef) -> Option { + let stream = self.entries.get(&r)?.clone(); + self.hits += 1; + Some(stream) + } + + fn insert(&mut self, r: ObjRef, stream: &Stream) { + self.builds += 1; + if self.entries.len() >= 1_000_000 { + self.clear(); + } + if self.bytes.saturating_add(stream.raw.len()) <= Self::MAX_BYTES { + self.bytes += stream.raw.len(); + self.entries.insert(r, stream.clone()); + } + // Else over budget: keep serving the entries already cached instead of growing. + } + + /// Cache hits so far (reused appearances); for tests. + pub fn hits(&self) -> u64 { + self.hits + } + + /// Appearances built so far; for tests. + pub fn builds(&self) -> u64 { + self.builds + } +} + /// A copy of `doc` in which every annotation without a normal appearance (`/AP /N`) has the one /// [`appearance::build`] draws from its dictionary, for displaying the document (`None` when no /// annotation needs one). Many files carry comments without appearances (FreeText, Ink, notes, /// stamps, …) that viewers draw from the dictionary. `doc` is not changed: saving writes the file /// as it was. Links, form fields and pop-ups keep their own handling. pub fn with_missing_appearances(doc: &Document) -> Option { + with_missing_appearances_cached(doc, &mut AppearanceCache::default()) +} + +/// A copy of `doc` with [`AppearanceCache`] reuse across refreshes: annotations whose +/// dictionary is unchanged since the cached build keep their stored appearance instead of +/// redrawing it. See [`AppearanceCache`] for when entries stay valid. +pub fn with_missing_appearances_cached(doc: &Document, cache: &mut AppearanceCache) -> Option { let mut copy: Option = None; for page in page_refs(doc).ok()? { for r in annots(doc, page).iter().filter_map(Object::as_ref) { @@ -1296,11 +1371,18 @@ pub fn with_missing_appearances(doc: &Document) -> Option { continue; } let has_normal = d.get(b"AP").is_some_and(|ap| doc.resolve(ap).as_dict().is_some_and(|ap| ap.contains(b"N"))); - if has_normal || appearance::build(&d).is_none() { + if has_normal { continue; } - // Fails only when `r` isn't a dictionary, which `build` above has just ruled out. - set_appearance(copy.get_or_insert_with(|| doc.clone()), r).ok(); + if let Some(stream) = cache.get(r) { + // Fails only when `r` isn't a dictionary, which the cached build ruled out. + apply_appearance(copy.get_or_insert_with(|| doc.clone()), r, &d, stream).ok(); + continue; + } + let Some(stream) = build_appearance(doc, &d) else { continue }; + cache.insert(r, &stream); + // Fails only when `r` isn't a dictionary, which the build just ruled out. + apply_appearance(copy.get_or_insert_with(|| doc.clone()), r, &d, stream).ok(); } } copy diff --git a/crates/annot/src/tests.rs b/crates/annot/src/tests.rs index d0e29e06..589a6abb 100644 --- a/crates/annot/src/tests.rs +++ b/crates/annot/src/tests.rs @@ -1158,3 +1158,26 @@ fn drawings_resize_by_scaling_their_strokes() { // Too small to hold the stroke margin. assert!(matches!(set_rect(&mut doc, 0, i, [100.0, 400.0, 101.0, 401.0], &meta("")), Err(AnnotError::Invalid(_)))); } + +/// Content edits rebuild the display copy on every commit but leave annotations alone: the +/// cached appearance is drawn once and reused after that, byte-identical. +#[test] +fn cached_appearances_are_reused_while_annotations_are_unchanged() { + let mut doc = fixture(); + let idx = add_annotation(&mut doc, &new(0, Shape::Note { at: [100.0, 700.0], icon: NoteIcon::Comment }), &meta("cache")).expect("adds"); + // Files carry comments without appearances; strip the created one to simulate that. + let page = page_refs(&doc).unwrap()[0]; + let r = annots(&doc, page)[idx].as_ref().expect("indirect"); + doc.update_dict(r, |d| { + d.remove(b"AP"); + }) + .expect("strips"); + let mut cache = AppearanceCache::default(); + let first = with_missing_appearances_cached(&doc, &mut cache).expect("builds"); + assert_eq!((cache.builds(), cache.hits()), (1, 0), "only the note needs an appearance"); + let second = with_missing_appearances_cached(&doc, &mut cache).expect("reuses"); + assert_eq!((cache.builds(), cache.hits()), (1, 1), "nothing is redrawn"); + let [a, b] = [&first, &second].map(|copy| ap_content(copy, &list(copy, 0)[idx])); + assert!(!a.is_empty()); + assert_eq!(a, b, "the reused appearance is byte-identical"); +} diff --git a/crates/automation/src/lib.rs b/crates/automation/src/lib.rs index c2e43a3a..71602908 100644 --- a/crates/automation/src/lib.rs +++ b/crates/automation/src/lib.rs @@ -165,7 +165,11 @@ impl Automation { let out = match name { "doc_open" => self.doc_open(&a)?, "doc_list" => json!({ "documents": self.session.docs().iter().map(summary).collect::>() }), - "doc_info" => info(self.doc(&a)?), + "doc_info" => { + let id = self.doc(&a)?.id; + self.session.refresh_fonts(id); + info(self.doc(&a)?) + } "doc_close" => self.doc_close(&a)?, "doc_save" => self.doc_save(&a)?, "doc_set_info" => { diff --git a/crates/engine/src/dates.rs b/crates/engine/src/dates.rs index 195202a1..38577e17 100644 --- a/crates/engine/src/dates.rs +++ b/crates/engine/src/dates.rs @@ -56,7 +56,7 @@ const AR_DAYS: [&str; 7] = ["الأحد", "الاثنين", "الثلاثاء", /// The languages month and weekday names can be written in (Preferences ▸ Date format ▸ Language): /// the interface languages, in the same order and with the same names. -pub const DATE_LANGUAGES: [DateLanguage; 21] = [ +pub static DATE_LANGUAGES: [DateLanguage; 21] = [ DateLanguage { code: "en", name: "English", @@ -297,8 +297,34 @@ pub const DATE_LANGUAGES: [DateLanguage; 21] = [ DateLanguage { code: "el", name: "Ελληνικά", - months: ["Ιανουάριος", "Φεβρουάριος", "Μάρτιος", "Απρίλιος", "Μάιος", "Ιούνιος", "Ιούλιος", "Αύγουστος", "Σεπτέμβριος", "Οκτώβριος", "Νοέμβριος", "Δεκέμβριος"], - months_with_day: Some(["Ιανουαρίου", "Φεβρουαρίου", "Μαρτίου", "Απριλίου", "Μαΐου", "Ιουνίου", "Ιουλίου", "Αυγούστου", "Σεπτεμβρίου", "Οκτωβρίου", "Νοεμβρίου", "Δεκεμβρίου"]), + months: [ + "Ιανουάριος", + "Φεβρουάριος", + "Μάρτιος", + "Απρίλιος", + "Μάιος", + "Ιούνιος", + "Ιούλιος", + "Αύγουστος", + "Σεπτέμβριος", + "Οκτώβριος", + "Νοέμβριος", + "Δεκέμβριος", + ], + months_with_day: Some([ + "Ιανουαρίου", + "Φεβρουαρίου", + "Μαρτίου", + "Απριλίου", + "Μαΐου", + "Ιουνίου", + "Ιουλίου", + "Αυγούστου", + "Σεπτεμβρίου", + "Οκτωβρίου", + "Νοεμβρίου", + "Δεκεμβρίου", + ]), months_short: ["Ιαν", "Φεβ", "Μαρ", "Απρ", "Μαΐ", "Ιουν", "Ιουλ", "Αυγ", "Σεπ", "Οκτ", "Νοε", "Δεκ"], days: ["Κυριακή", "Δευτέρα", "Τρίτη", "Τετάρτη", "Πέμπτη", "Παρασκευή", "Σάββατο"], days_short: ["Κυρ", "Δευ", "Τρί", "Τετ", "Πέμ", "Παρ", "Σάβ"], diff --git a/crates/engine/src/lib.rs b/crates/engine/src/lib.rs index cb602e13..a0ac6ed3 100644 --- a/crates/engine/src/lib.rs +++ b/crates/engine/src/lib.rs @@ -108,8 +108,8 @@ pub fn typed_signature_graphic(text: &str) -> Option { pub use pdfcraft_annot::appearance as annot_text; pub use pdfcraft_annot::links::{Highlight as LinkHighlight, LinkAction, LinkItem, LinkStyle}; pub use pdfcraft_annot::{ - AttachIcon, FillMark, LineEnding, Markup, NOTE_SIZE, NewAnnotation, NoteIcon, OverlayFont, OverlayLook, Props as CommentProps, ReviewState, Rgb, - Shape, StampGroup, StampKind, Style, rect_quad, + AppearanceCache, AttachIcon, FillMark, LineEnding, Markup, NOTE_SIZE, NewAnnotation, NoteIcon, OverlayFont, OverlayLook, Props as CommentProps, + ReviewState, Rgb, Shape, StampGroup, StampKind, Style, rect_quad, }; pub use pdfcraft_forms::detect; pub use pdfcraft_optimize as optimize; @@ -166,6 +166,10 @@ enum Scope { Comments, /// Only form field values (and their widget appearances). Form, + /// Only page content streams, resources and added content/marks: the page tree, + /// annotations, form fields, links, outline and fonts are unchanged, so the full + /// re-inspection is skipped (seconds on very large files). + Content, } /// Edits during which field scripts run. @@ -199,6 +203,21 @@ fn scope_of(edit: &Edit) -> Scope { | Edit::FillAnnotation { .. } | Edit::SetAnnotationInfo { .. } => Scope::Comments, Edit::SetFieldValue { .. } | Edit::ResetForm { .. } | Edit::SetFieldImage { .. } | Edit::ApplyScriptChanges { .. } => Scope::Form, + // Page content, resources and added content/marks only: the page tree, annotations, + // fields, links and document metadata are untouched (see `refresh_content`). + Edit::MoveObjects { .. } + | Edit::EditTextLine { .. } + | Edit::EditTextBlock { .. } + | Edit::EditPageImage { .. } + | Edit::AddText { .. } + | Edit::AddImage { .. } + | Edit::UpdateContent { .. } + | Edit::DeleteContent { .. } + | Edit::ReplaceImage { .. } + | Edit::AddHeaderFooter { .. } + | Edit::AddWatermark { .. } + | Edit::AddBackground { .. } + | Edit::RemoveMarks { .. } => Scope::Content, Edit::Batch { edits, .. } => { let mut scopes = edits.iter().map(scope_of); let first = scopes.next().unwrap_or(Scope::Full); @@ -218,6 +237,10 @@ struct Editor { undo: Vec, redo: Vec, keys: Keys, + /// Display appearances built for annotations without any, reused while no annotation edit + /// runs (content edits leave every annotation dictionary alone). Cleared on any refresh + /// that may change annotations; see [`AppearanceCache`]. + appearances: AppearanceCache, } pub struct Document { @@ -262,6 +285,9 @@ pub struct Document { pub xfa: Option, /// The parsed template of a laid-out XFA form, for its scripts. xfa_template: Option>, + /// A content-only refresh keeps `info` from before the edit; an edit may have added or + /// embedded fonts, so the font list is read again on demand ([`Session::refresh_fonts`]). + fonts_stale: bool, /// XFA forms: what was approximated, rewritten or could not be written to the XFA data /// (also in `info.warnings`, kept there when the document is re-read). pub xfa_warnings: Vec, @@ -592,8 +618,10 @@ fn is_placeholder_title(title: &str) -> bool { /// What is displayed: the working file, plus (in memory only, never saved) the appearances /// PdfCraft draws for comments that have none ([`pdfcraft_annot::with_missing_appearances`]). /// The working file is unchanged, and so is everything that reads it (saving, signatures, …). -fn display_bytes(editor: Option<&Editor>, bytes: &Arc>) -> Arc> { - let Some(display) = editor.and_then(|e| pdfcraft_annot::with_missing_appearances(&e.cos)) else { return bytes.clone() }; +fn display_bytes(editor: Option<&mut Editor>, bytes: &Arc>) -> Arc> { + let Some(display) = editor.and_then(|e| pdfcraft_annot::with_missing_appearances_cached(&e.cos, &mut e.appearances)) else { + return bytes.clone(); + }; // Writing fails only as saving the document would; the page then shows as it always did. write_incremental(&display, &SaveOptions::default()).map(Arc::new).unwrap_or_else(|_| bytes.clone()) } @@ -2657,12 +2685,12 @@ impl Session { let (mut editor, read_only_reason) = match cos { Ok(Ok(cos)) => { let keys = Keys { render: render_password.clone(), reopen: password.map(str::to_owned) }; - (Some(Editor { cos, undo: Vec::new(), redo: Vec::new(), keys }), None) + (Some(Editor { cos, undo: Vec::new(), redo: Vec::new(), keys, appearances: AppearanceCache::default() }), None) } Ok(Err(e)) => (None, Some(e.to_string())), Err(_) => (None, Some("the document structure could not be read for editing".into())), }; - let display = display_bytes(editor.as_ref(), &bytes); + let display = display_bytes(editor.as_mut(), &bytes); let renderer = RenderPool::new(display.clone(), render_threads(), config.clone()); let (form, adopted) = editor .as_ref() @@ -2720,6 +2748,7 @@ impl Session { js_output: Default::default(), xfa, xfa_template: None, + fonts_stale: false, xfa_warnings: Vec::new(), }); Ok(id) @@ -2764,7 +2793,7 @@ impl Session { // every calculation (and a reset recalculates). They read the data, so it is brought up // to date first. if let Some(tpl) = cx.xfa.clone() { - if is_xfa && matches!(scope_of(&edit), Scope::Form | Scope::Full) { + if is_xfa && matches!(scope_of(&edit), Scope::Form | Scope::Full | Scope::Content) { let datasets = &mut cx.xfa_datasets; let notes = guard(|| xfa_sync_datasets(&mut next, Some(&tpl), datasets)) .unwrap_or_else(|m| Err(format!("writing the XFA data failed unexpectedly ({m})"))) @@ -2797,7 +2826,7 @@ impl Session { } // XFA forms keep their values in the datasets packet too, for Adobe's viewers. let mut xfa_notes = Vec::new(); - if is_xfa && matches!(scope_of(&edit), Scope::Form | Scope::Full) { + if is_xfa && matches!(scope_of(&edit), Scope::Form | Scope::Full | Scope::Content) { let (tpl, datasets) = (cx.xfa.as_deref(), &mut cx.xfa_datasets); xfa_notes = guard(|| xfa_sync_datasets(&mut next, tpl, datasets)) .unwrap_or_else(|m| Err(format!("writing the XFA data failed unexpectedly ({m})"))) @@ -2819,7 +2848,8 @@ impl Session { } editor.redo.clear(); Self::adopt_keys(doc); - let refreshed = guard(|| Self::refresh_scoped(doc, scope)).unwrap_or_else(|m| Err(EditError::Reopen(m))); + let refreshed = guard(|| if scope == Scope::Content { Self::refresh_content(doc, Some(&edit)) } else { Self::refresh_scoped(doc, scope) }) + .unwrap_or_else(|m| Err(EditError::Reopen(m))); if let Err(e) = refreshed { // Roll back: the edit produced something we cannot display. if let Some(ed) = doc.editor.as_mut() @@ -2892,7 +2922,15 @@ impl Session { if scope == Scope::Full || doc.info.encrypted != doc.editor.as_ref().is_some_and(|e| e.cos.output_handler().is_some()) { return Self::refresh(doc); } - let Some(editor) = doc.editor.as_ref() else { return Ok(()) }; + if scope == Scope::Content { + return Self::refresh_content(doc, None); + } + let Some(editor) = doc.editor.as_mut() else { return Ok(()) }; + // Comment edits, and form edits (a script may change annotation properties), may change + // annotation dictionaries, invalidating cached appearances. + if matches!(scope, Scope::Comments | Scope::Form) { + editor.appearances.clear(); + } let bytes = if editor.cos.is_modified() { Arc::new(write_incremental(&editor.cos, &SaveOptions::default()).map_err(|e| EditError::Write(e.to_string()))?) } else { @@ -2943,6 +2981,8 @@ impl Session { } // Handled above; kept as a full refresh so the match stays total. Scope::Full => return Self::refresh(doc), + // Handled above (content-only refresh); the arm keeps the match total. + Scope::Content => return Self::refresh_content(doc, None), } doc.info.file_size = bytes.len(); doc.form = form; @@ -2955,9 +2995,68 @@ impl Session { Ok(()) } + /// Which derived lists a content-only edit can change: added content (`None`, from + /// undo/redo, re-reads both: always correct, rarely paid). + fn content_refresh_lists(edit: Option<&Edit>) -> (bool, bool) { + let touches_added = edit.is_none_or(|e| { + matches!( + e, + Edit::AddText { .. } | Edit::AddImage { .. } | Edit::UpdateContent { .. } | Edit::DeleteContent { .. } | Edit::ReplaceImage { .. } + ) || matches!(e, Edit::MoveObjects { objects, .. } if objects.iter().any(|o| o.kind == ObjectKind::Added)) + }); + let touches_marks = edit.is_none_or(|e| { + matches!(e, Edit::AddHeaderFooter { .. } | Edit::AddWatermark { .. } | Edit::AddBackground { .. } | Edit::RemoveMarks { .. }) + }); + (touches_added, touches_marks) + } + + /// Rebuild working bytes, display bytes and renderer after a content-only edit: page + /// content streams, resources and added content/marks. The page tree, annotations, form + /// fields, links, outline and fonts are unchanged, so the full re-inspection (seconds on + /// very large files) is skipped. `edit` selects which derived lists are re-read (`None`, + /// from undo/redo, re-reads both: always correct, rarely paid). The cached display + /// appearances are kept: content edits leave every annotation dictionary alone. + fn refresh_content(doc: &mut Document, edit: Option<&Edit>) -> Result<(), EditError> { + let Some(editor) = doc.editor.as_mut() else { return Ok(()) }; + // A script may have laid an XFA form out again (rows added, subforms shown). + if doc.xfa.is_some() + && let Some(report) = pdfcraft_xfa::existing_layout(&editor.cos) + { + doc.xfa = Some(report); + } + let bytes = if editor.cos.is_modified() { + Arc::new(write_incremental(&editor.cos, &SaveOptions::default()).map_err(|e| EditError::Write(e.to_string()))?) + } else { + editor.cos.bytes().clone() + }; + // The edit must still leave a file the viewer can open; anything else rolls back above. + pdfcraft_render::check_pages(bytes.clone(), doc.password.as_deref(), doc.info.pages.len()).map_err(|e| EditError::Reopen(e.to_string()))?; + let (touches_added, touches_marks) = Self::content_refresh_lists(edit); + if touches_added { + doc.added = pdfcraft_edit::list_added(&editor.cos); + } + if touches_marks { + doc.marks = pdfcraft_edit::marks_present(&editor.cos); + } + doc.info.file_size = bytes.len(); + if !doc.signatures.is_empty() { + doc.signatures = signatures_of(&editor.cos, &bytes, &doc.trust, &doc.sig_cache); + } + doc.display = display_bytes(Some(editor), &bytes); + doc.renderer = RenderPool::new(doc.display.clone(), render_threads(), doc.config.clone()); + doc.bytes = bytes; + doc.fonts_stale = true; + Ok(()) + } + /// Rebuild working bytes, inspection and renderer from the current edit state. fn refresh(doc: &mut Document) -> Result<(), EditError> { - let Some(editor) = doc.editor.as_ref() else { return Ok(()) }; + // A full refresh may follow structural or annotation changes: cached appearances + // may be stale, so they are rebuilt below. + if let Some(editor) = doc.editor.as_mut() { + editor.appearances.clear(); + } + let Some(editor) = doc.editor.as_mut() else { return Ok(()) }; // A script may have laid an XFA form out again (rows added, subforms shown). if doc.xfa.is_some() && let Some(report) = pdfcraft_xfa::existing_layout(&editor.cos) @@ -2979,6 +3078,7 @@ impl Session { } note_warnings(&mut info.warnings, &doc.xfa_warnings); doc.info = info; + doc.fonts_stale = false; let mut form = pdfcraft_forms::fields(&editor.cos); xfa::mark_script_buttons(&editor.cos, &mut form); doc.form = Arc::new(form); @@ -3655,6 +3755,16 @@ impl Session { self.docs.iter().find(|d| d.id == id) } + /// Read document `id`'s font list again if content edits since the last full inspection may + /// have changed it (Document Properties ▸ Fonts, `doc_info`). A no-op otherwise. + pub fn refresh_fonts(&mut self, id: DocId) { + let Some(doc) = self.docs.iter_mut().find(|d| d.id == id && d.fonts_stale) else { return }; + if let Ok(info) = pdfcraft_render::inspect(doc.bytes.clone(), doc.password.as_deref()) { + doc.info.fonts = info.fonts; + } + doc.fonts_stale = false; + } + pub fn docs(&self) -> &[Document] { &self.docs } diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs index 68cdc0c8..838f9af2 100644 --- a/crates/engine/src/tests.rs +++ b/crates/engine/src/tests.rs @@ -3361,3 +3361,103 @@ fn enhance_camera_page_refuses_bad_corners_and_pages() { assert!(matches!(s.enhance_camera_page(DocId(999), 0, ok), Err(EditError::NoDocument))); assert_eq!(s.get(id).unwrap().can_undo(), None, "nothing changed"); } + +/// A content-only edit must not disturb the inspection results: pages, annotations, fields, +/// links and marks are unchanged, only the working bytes, the added-content list and the +/// display copy move. +#[test] +fn content_edit_keeps_pages_annotations_fields_and_links() { + let (mut session, id) = session_with(2); + let before = session.get(id).unwrap(); + let pages = format!("{:?}", before.info.pages); + let annotations = format!("{:?}", before.info.annotations); + let links = format!("{:?}", before.links); + let form = before.form.clone(); + let marks = before.marks.clone(); + let bytes = before.bytes.clone(); + let text = AddedText { + rect: [20.0, 20.0, 200.0, 60.0], + text: "hello".into(), + family: FontFamily::Helvetica, + bold: false, + italic: false, + size: 12.0, + color: [0.0, 0.0, 0.0], + align: TextAlign::Left, + }; + session.apply(id, Edit::AddText { page: 1, text }).unwrap(); + let after = session.get(id).unwrap(); + assert_eq!(format!("{:?}", after.info.pages), pages, "page geometry is untouched"); + assert_eq!(format!("{:?}", after.info.annotations), annotations); + assert_eq!(format!("{:?}", after.links), links); + assert_eq!(after.form, form, "no field was touched"); + assert_eq!(after.marks, marks, "no mark was touched"); + assert!(after.added.iter().any(|a| a.page == 1), "the added text is listed"); + assert_ne!(after.bytes, bytes, "the working file carries the edit"); + assert_ne!(after.display, bytes, "the display copy carries the edit too"); + // The display bytes still open and render the edited page. + let mut renderer = pdfcraft_render::PageRenderer::new(after.display.clone(), after.config.clone()); + let rendered = renderer.render(pdfcraft_render::RenderRequest { page: 1, scale: 1.0, ..Default::default() }); + assert!(rendered.error.is_none(), "{:?}", rendered.error); + // Undo goes through the same narrow refresh without the edit at hand. + session.undo(id).unwrap(); + let undone = session.get(id).unwrap(); + assert!(undone.added.is_empty(), "undo drops the added text"); + assert_eq!(format!("{:?}", undone.info.pages), pages); +} + +/// `refresh_content` re-reads only the derived lists the edit can change (undo/redo, with no +/// edit at hand, re-read both). +#[test] +fn content_refresh_lists_match_the_storages_an_edit_touches() { + let target = |kind| ObjectTarget { kind, index: 0 }; + let added = [ + Edit::AddText { + page: 0, + text: AddedText { + rect: [0.0, 0.0, 10.0, 10.0], + text: String::new(), + family: FontFamily::Helvetica, + bold: false, + italic: false, + size: 12.0, + color: [0.0, 0.0, 0.0], + align: TextAlign::Left, + }, + }, + Edit::MoveObjects { page: 0, objects: vec![target(crate::ObjectKind::Added)], offset: [1.0, 0.0] }, + ]; + for edit in &added { + assert_eq!(Session::content_refresh_lists(Some(edit)), (true, false), "{}", edit.label()); + } + let plain = [ + Edit::MoveObjects { page: 0, objects: vec![target(crate::ObjectKind::Text)], offset: [1.0, 0.0] }, + Edit::EditTextBlock { page: 0, block: 0, text: String::new(), style: pdfcraft_edit::BlockStyle::default() }, + ]; + for edit in &plain { + assert_eq!(Session::content_refresh_lists(Some(edit)), (false, false), "{}", edit.label()); + } + let marks = [Edit::AddWatermark { pages: vec![0], settings: Watermark::default(), replace: false, file: None }]; + for edit in &marks { + assert_eq!(Session::content_refresh_lists(Some(edit)), (false, true), "{}", edit.label()); + } + assert_eq!(Session::content_refresh_lists(None), (true, true), "undo/redo re-read both"); +} + +/// A content edit can add a font (Add text in Times); the narrow refresh leaves the font list +/// as it was, and `refresh_fonts` reads it again before Properties ▸ Fonts or `doc_info` show it. +#[test] +fn fonts_added_by_a_content_edit_show_after_refresh_fonts() { + let (mut session, id) = session_with(1); + let has_times = |s: &Session| s.get(id).unwrap().info.fonts.iter().any(|f| f.name.contains("Times")); + assert!(!has_times(&session)); + let text = AddedText { text: "serif".into(), rect: [20.0, 20.0, 200.0, 60.0], family: FontFamily::Times, ..Default::default() }; + session.apply(id, Edit::AddText { page: 0, text }).unwrap(); + assert!(!has_times(&session), "the narrow refresh doesn't re-inspect"); + session.refresh_fonts(id); + assert!(has_times(&session), "{:?}", session.get(id).unwrap().info.fonts); + // Nothing changed since: a second call is a no-op. + let fonts = format!("{:?}", session.get(id).unwrap().info.fonts); + session.refresh_fonts(id); + assert_eq!(format!("{:?}", session.get(id).unwrap().info.fonts), fonts); +} diff --git a/crates/render/src/inspect.rs b/crates/render/src/inspect.rs index d22d63c5..438932f9 100644 --- a/crates/render/src/inspect.rs +++ b/crates/render/src/inspect.rs @@ -346,6 +346,32 @@ pub struct FontInfo { pub encoding: Option, } +/// Check that `bytes` still open as the same document: the parser reads them and the page +/// count matches. Content-only edits skip the full [`inspect`] (seconds on very large files) +/// but must still catch an edit that produced something the viewer cannot display, so the +/// caller can roll it back. Never panics: parser crashes are caught and reported. +pub fn check_pages(bytes: Arc>, password: Option<&str>, expected: usize) -> Result<(), OpenError> { + let pdf = catch_unwind(AssertUnwindSafe(|| Pdf::new_with_password(bytes, password.unwrap_or("")))) + .map_err(|p| OpenError::Invalid(format!("the parser crashed: {}", crate::raster::panic_message(&p))))? + .map_err(|e| match e { + LoadPdfError::Decryption(DecryptionError::PasswordProtected) => { + if password.is_some() { + OpenError::WrongPassword + } else { + OpenError::NeedsPassword + } + } + LoadPdfError::Decryption(other) => OpenError::Unsupported(format!("encryption not supported: {other:?}")), + LoadPdfError::Invalid => OpenError::Invalid("no readable page tree or cross-reference data was found".into()), + })?; + let pages = catch_unwind(AssertUnwindSafe(|| pdf.pages().len())) + .map_err(|p| OpenError::Invalid(format!("the page tree could not be read: {}", crate::raster::panic_message(&p))))?; + if pages != expected { + return Err(OpenError::Invalid(format!("the edit changed the page count from {expected} to {pages}"))); + } + Ok(()) +} + /// Inspect a document. Fails only if the renderer itself cannot open the file (or needs a /// password). Never panics: parser crashes are caught and reported. pub fn inspect(bytes: Arc>, password: Option<&str>) -> Result { diff --git a/crates/render/src/lib.rs b/crates/render/src/lib.rs index 23d40360..832582d6 100644 --- a/crates/render/src/lib.rs +++ b/crates/render/src/lib.rs @@ -16,7 +16,7 @@ pub mod text; pub use inspect::{ Annotation, Attachment, AttachmentSource, DestView, DocInfo, Field, FieldKind, FontInfo, Layer, LayerOp, Link, LinkTarget, OutlineItem, PageInfo, - Xfa, attachment_data, inspect, pretty_date, + Xfa, attachment_data, check_pages, inspect, pretty_date, }; pub use pixels::Pixels; pub use raster::{ diff --git a/crates/ui-egui/src/canvas.rs b/crates/ui-egui/src/canvas.rs index c26b108d..c5b939bf 100644 --- a/crates/ui-egui/src/canvas.rs +++ b/crates/ui-egui/src/canvas.rs @@ -2684,7 +2684,10 @@ pub fn document_area(app: &mut PdfCraftApp, index: usize, ui: &mut egui::Ui) { view.clicks = release_clicks; } let clicks = if view.clicks > 2 && tool != QuickTool::Select { 1 } else { view.clicks }; - if clicks >= 2 && over_text { + // Only on the frame egui reports the multi-click: `view.clicks` outlives it, and + // acting on every frame re-selected the word as soon as a selection was cleared + // (a highlight made by the double-click left its word selected). + if release_clicks > 0 && clicks >= 2 && over_text { if view.selection.is_none() { let span = text.nearest(vx, vy).and_then(|a| match clicks { 2 => text.word_at(a), diff --git a/crates/ui-egui/src/dialogs.rs b/crates/ui-egui/src/dialogs.rs index 812b6c21..bab565fd 100644 --- a/crates/ui-egui/src/dialogs.rs +++ b/crates/ui-egui/src/dialogs.rs @@ -25,6 +25,10 @@ fn dialogs(app: &mut PdfCraftApp, ctx: &egui::Context) { app.view_draft = None; return; }; + // Properties ▸ Fonts shows the fonts as they are now, after any content edits. + if let (Dialog::Properties(PropsTab::Fonts), Some((_, id))) = (dialog, app.active_ids()) { + app.session.refresh_fonts(id); + } // Seed the editable Description fields from the document when the dialog opens. if let (Dialog::Properties(_), Some((_, id))) = (dialog, app.active_ids()) && app.props_draft.as_ref().is_none_or(|(d, _)| *d != id) @@ -897,7 +901,9 @@ fn dialogs(app: &mut PdfCraftApp, ctx: &egui::Context) { return; } Dialog::Preferences => { - close = crate::js_ui::preferences_body(ui, app, &t); + // The settings outgrow a short window: scroll them rather than run off the screen. + let max = (ui.ctx().content_rect().height() - 120.0).max(200.0); + close = egui::ScrollArea::vertical().max_height(max).show(ui, |ui| crate::js_ui::preferences_body(ui, app, &t)).inner; return; } Dialog::RecognizeText => { diff --git a/crates/ui-egui/src/editing.rs b/crates/ui-egui/src/editing.rs index 81fb1f2a..bd57ea4c 100644 --- a/crates/ui-egui/src/editing.rs +++ b/crates/ui-egui/src/editing.rs @@ -43,8 +43,8 @@ impl PdfCraftApp { let info = &doc.info; let view = &mut self.views[i]; view.signature_drag.committed(&edit, doc.edit_generation()); - if let Some(page) = comment_page(&edit) { - // Comment edits change one page: keep every other raster. + if let Some(page) = comment_page(&edit).or_else(|| content_page(&edit)) { + // Comment and content edits change one page: keep every other raster. view.page_changed(page); } else if let Some(form_before) = form_before && doc.info.pages.len() == page_count_before @@ -833,7 +833,23 @@ impl PdfCraftApp { } } -/// The page a comment edit changes (`None` for other edits). +/// The single page a content edit changes (`None` for other edits). Like comment edits, +/// these leave every other page's bytes, geometry and text alone, so their rasters stay valid. +fn content_page(edit: &Edit) -> Option { + match edit { + Edit::MoveObjects { page, .. } + | Edit::EditTextLine { page, .. } + | Edit::EditTextBlock { page, .. } + | Edit::EditPageImage { page, .. } + | Edit::AddText { page, .. } + | Edit::AddImage { page, .. } + | Edit::UpdateContent { page, .. } + | Edit::DeleteContent { page, .. } + | Edit::ReplaceImage { page, .. } => Some(*page), + _ => None, + } +} + fn comment_page(edit: &Edit) -> Option { match edit { Edit::AddAnnotation(a) => Some(a.page), @@ -1366,3 +1382,19 @@ mod revert_draft_tests { assert_eq!(app.toast.as_ref().unwrap().0, pdfcraft_engine::EditError::NoDocument.to_string()); } } + +#[cfg(test)] +mod content_page_tests { + use super::content_page; + use pdfcraft_engine::{Edit, ObjectKind, ObjectTarget}; + + /// Single-page content edits keep every other page's rasters; anything else still + /// invalidates the whole document. + #[test] + fn single_page_content_edits_map_to_their_page() { + let moved = Edit::MoveObjects { page: 3, objects: vec![ObjectTarget { kind: ObjectKind::Text, index: 0 }], offset: [1.0, 0.0] }; + assert_eq!(content_page(&moved), Some(3)); + assert_eq!(content_page(&Edit::RotatePages { pages: vec![0], degrees: 90 }), None); + assert_eq!(content_page(&Edit::SetInfo { key: "Title".into(), value: "x".into() }), None); + } +} diff --git a/crates/ui-egui/src/i18n/mod.rs b/crates/ui-egui/src/i18n/mod.rs index 4c399246..ba6be0f8 100644 --- a/crates/ui-egui/src/i18n/mod.rs +++ b/crates/ui-egui/src/i18n/mod.rs @@ -1434,7 +1434,9 @@ mod tests { assert!(errors.is_empty(), "Turkish catalog errors: {errors:?}"); for e in &entries { assert!( - e.source != e.translation || KEEP_AS_IS.contains(&e.source.as_str()) || (e.context == "key" && KEY_CAP_NAMES.contains(&e.source.as_str())), + e.source != e.translation + || KEEP_AS_IS.contains(&e.source.as_str()) + || (e.context == "key" && KEY_CAP_NAMES.contains(&e.source.as_str())), "{:?} is untranslated", e.source ); diff --git a/crates/ui-egui/tests/date_format.rs b/crates/ui-egui/tests/date_format.rs index 0fbe9760..0606e021 100644 --- a/crates/ui-egui/tests/date_format.rs +++ b/crates/ui-egui/tests/date_format.rs @@ -19,6 +19,9 @@ fn shows(h: &Harness<'static, PdfCraftApp>, text: &str) -> bool { } fn retype(h: &mut Harness<'static, PdfCraftApp>, text: &str) { + // The preferences scroll; bring the field into view before clicking it. + h.get_by_role_and_label(Role::TextInput, "Pattern").scroll_to_me(); + h.run_steps(4); h.get_by_role_and_label(Role::TextInput, "Pattern").click(); h.run_steps(1); h.key_press_modifiers(Modifiers::COMMAND, Key::A); diff --git a/crates/ui-egui/tests/javascript.rs b/crates/ui-egui/tests/javascript.rs index a5c5a6af..2d525104 100644 --- a/crates/ui-egui/tests/javascript.rs +++ b/crates/ui-egui/tests/javascript.rs @@ -94,6 +94,9 @@ fn document_scripts_and_preferences() { assert_eq!(h.state().session.get(id).unwrap().document_scripts()[0].0, "init"); h.state_mut().dialog = Some(Dialog::Preferences); h.run_steps(2); + // Below the fold in a 900-point window: the preferences scroll to it. + h.get_by_label("Enable Acrobat JavaScript").scroll_to_me(); + h.run_steps(4); h.get_by_label("Enable Acrobat JavaScript").click(); h.run_steps(2); assert!(!h.state().session.javascript()); diff --git a/crates/ui-egui/tests/ui.rs b/crates/ui-egui/tests/ui.rs index ded8c0ca..b4c62bd3 100644 --- a/crates/ui-egui/tests/ui.rs +++ b/crates/ui-egui/tests/ui.rs @@ -800,6 +800,8 @@ fn preferences_selects_default_workspace_for_next_open() { use pdfcraft_ui_egui::Mode; let mut h = harness(|app| app.set_option("dialog", "preferences").unwrap()); h.get_by_label("Default workspace mode"); + h.get_by_role_and_label(Role::RadioButton, "Read").scroll_to_me(); + h.run_steps(4); h.get_by_role_and_label(Role::RadioButton, "Read").click(); h.run_steps(2); assert_eq!(h.state().default_mode, Mode::Read); diff --git a/crates/ui-egui/tests/ui_group/bulk_fields.rs b/crates/ui-egui/tests/ui_group/bulk_fields.rs index b2c75c26..1ddf07b1 100644 --- a/crates/ui-egui/tests/ui_group/bulk_fields.rs +++ b/crates/ui-egui/tests/ui_group/bulk_fields.rs @@ -89,6 +89,9 @@ fn shared_properties_preserve_mixed_values_and_undo_the_whole_change() { h.state_mut().bulk_field_props.as_mut().unwrap().required.value = true; h.get_by_label("Appearance").click(); h.run_steps(4); + // Below the fold of the dialog's scroll area with the roomier rows (#487). + h.get_by_label("Line Thickness:").scroll_to_me(); + h.run_steps(4); h.get_by_label("Line Thickness:").click(); h.run_steps(4); h.state_mut().bulk_field_props.as_mut().unwrap().width.value = 4.0; diff --git a/crates/ui-egui/tests/ui_group/identity.rs b/crates/ui-egui/tests/ui_group/identity.rs index 9a4e3c38..52581fe6 100644 --- a/crates/ui-egui/tests/ui_group/identity.rs +++ b/crates/ui-egui/tests/ui_group/identity.rs @@ -29,7 +29,9 @@ fn the_identity_name_signs_new_comments_and_is_remembered() { }); h.run_steps(4); h.get_by_label_contains("Name on new comments"); - // Replace the name in the field. + // Replace the name in the field (the preferences scroll to it in a 900-point window). + h.get_by_role_and_label(Role::TextInput, "Name on new comments").scroll_to_me(); + h.run_steps(4); h.get_by_role_and_label(Role::TextInput, "Name on new comments").click(); h.run_steps(1); h.key_press_modifiers(Modifiers::COMMAND, Key::A); diff --git a/crates/ui-egui/tests/ui_group/text_select.rs b/crates/ui-egui/tests/ui_group/text_select.rs index fc8d19a8..d6f970de 100644 --- a/crates/ui-egui/tests/ui_group/text_select.rs +++ b/crates/ui-egui/tests/ui_group/text_select.rs @@ -77,8 +77,28 @@ fn quick_clicks_with_the_highlighter_mark_only_the_word() { let (mut h, at) = harness(|app| assert!(app.execute("comment.highlight"))); for _ in 0..4 { click(&mut h, at); + // The double-click's highlight re-renders the page; give its text layer up to 8 frames + // to come back (still well inside egui's 0.3 s multi-click window at 1/60 s a frame), so + // a loaded machine doesn't make the later clicks land on a page with no text yet. + for _ in 0..8 { + if !h.state().render_pending() { + break; + } + std::thread::sleep(std::time::Duration::from_millis(20)); + h.run_steps(1); + } } h.run_steps(3); + // Under load the mark can land a few frames later; wait for it (egui's clock is fixed per + // frame, so waiting can't turn the clicks into a different run). + for _ in 0..200 { + let app = h.state(); + if app.session.get(app.views[0].id).is_some_and(|d| !d.info.annotations.is_empty()) && !app.render_pending() { + break; + } + std::thread::sleep(std::time::Duration::from_millis(5)); + h.run_steps(1); + } let app = h.state(); let doc = app.session.get(app.views[0].id).unwrap(); let marks: Vec<(&str, usize)> = doc.info.annotations.iter().map(|a| (a.subtype.as_str(), a.quads.len())).collect(); diff --git a/crates/ui-egui/tests/view.rs b/crates/ui-egui/tests/view.rs index e1963d33..e6890f9b 100644 --- a/crates/ui-egui/tests/view.rs +++ b/crates/ui-egui/tests/view.rs @@ -376,6 +376,8 @@ fn preferences_set_the_default_zoom() { let mut h = harness(&[]); assert!(h.state_mut().execute("app.preferences")); h.run_steps(3); + h.get_by_label("Zoom to page level").scroll_to_me(); + h.run_steps(4); h.get_by_label("Zoom to page level").click(); h.run_steps(2); assert!(h.state_mut().execute("create.blank")); From e609722b36decde5a09022b84977d3e0c56429ed Mon Sep 17 00:00:00 2001 From: tzagim <2285958+tzagim@users.noreply.github.com> Date: Mon, 12 Oct 2026 00:41:06 +0300 Subject: [PATCH 6/9] M7: Hebrew in Edit text (reading order, writing order, Hebrew glyphs) (#521) * Hebrew fix * Tests: the faux-redaction audit gets its own work folder (it shared "audit" with the space audit, so the two raced) * Arabic golden test: compare bytes only in a build without an Arabic face (CI's Linux job builds with craft-fonts) --------- Co-authored-by: Brandon Thomas --- ROADMAP.md | 1 + crates/automation/tests/automation.rs | 2 +- crates/edit/Cargo.toml | 3 +- crates/edit/src/added/arabic_tests.rs | 7 +- crates/edit/src/bidi.rs | 364 ++++++++++++++++++++++++++ crates/edit/src/lib.rs | 1 + crates/edit/src/tests.rs | 142 ++++++++++ crates/edit/src/text.rs | 153 ++++++++--- crates/fonts/README.md | 6 +- crates/fonts/build.rs | 6 +- crates/fonts/src/craft.rs | 36 +++ crates/fonts/src/lib.rs | 4 +- crates/ui-egui/src/theme.rs | 12 +- docs/localization.md | 2 + parity/acrobat-features.toml | 8 +- 15 files changed, 694 insertions(+), 53 deletions(-) create mode 100644 crates/edit/src/bidi.rs diff --git a/ROADMAP.md b/ROADMAP.md index 45f7ec00..bfe8ac0d 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -209,6 +209,7 @@ Newest first: the date, what moved, and the new overall estimate. - **2026-10-09 (M5.7, #434 partial):** Natural-size image stamps placed at a click or through `stamp_custom` remain upright on rotated pages, with a displayed-space-sized rectangle and a counterrotated standard appearance matrix. Move, resize, restyle and undo/redo preserve that orientation. Native and external Poppler save/reopen rendering cover all four quarter-turns; UI tests verify the displayed bounds. Explicit rectangle placement, image signatures, PDF-page stamps and image-file orientation metadata remain outside this repair. #434 stays open; milestone table and effort estimates remain unchanged. - **2026-10-09 (M1, raw-stream decoding limits):** COS checks unfiltered decoded data against the caller and per-stream limits before copying it, including empty filter arrays and streams whose Crypt filter was applied on load. Strict decoding uses the same raw-data guard. Tiny synthetic regressions cover exceeded, exact and zero-byte limits while retaining the filtered decoder behavior. Milestone and overall effort estimates unchanged (about 30–35%). +- **2026-10-09 (community, M7, Hebrew in Edit text):** Editing existing Hebrew text showed it reversed (`בדיוק` read back letter-reversed) and wrote replacement boxes on save. Content streams draw right-to-left lines in visual order; Edit text now reads lines and paragraphs in typing order and writes them back in drawing order (`pdfcraft-edit::bidi`, UAX #9 through `unicode-bidi`, with numbers, Latin and mirrored brackets; where visual order allows several readings, the one that draws back exactly and pairs its brackets wins). Right-to-left paragraphs are right-aligned unless an alignment is chosen, and a single line keeps its right edge as it grows. Letters the page's font lacks are drawn with a craft-fonts `Hebr` face (Noto Sans Hebrew, storytold/craft-fonts PR) as Type 3 glyphs with ToUnicode, so the result is searchable; the interface uses that face too, and the web build keeps it. Without it, a clear error and no change. `edit.rtl-cjk-editing` moves to partial. Not yet: niqqud in replacement text (refused, not misdrawn), Arabic in Edit text (shaping; Add text has it since #403), typing right to left in the box (#437), lines drawn word by word from the right; the release craft-fonts pin must move to a revision with the Hebrew face. Overall estimate unchanged (about 30–35%). - **2026-10-09 (M2, bounded page labels):** Inspector number trees have shared work, depth, and cycle limits; prefixes are bounded before decoding and complete custom labels have per-label and aggregate UTF-8 byte caps. A refused tree leaves physical page numbers intact and records one warning, with custom labels applied only after every range succeeds. Pure COS Roman and repeated-letter formatters are shared with Organize; label queries and edits return errors for unsupported output before changing the stored tree, while ordinary labels and prefix-only ranges remain supported. Synthetic tests cover safe just-over-limit cases, large first numbers, aliases, cycles, wide/deep trees, UTF-16/UTF-8 boundaries, aggregate output, and unchanged refused edits. Milestone and overall effort estimates unchanged (about 30–35%). - **2026-10-09 (M4, page grid zoom):** The Organize page grid zooms from 50% to 300%: toolbar buttons with a percentage that resets, trackpad pinch or Ctrl/⌘ + wheel about the page under the pointer, and ⌘+ / ⌘− / ⌘0. Pages drawn larger than their thumbnails are rendered at the size drawn while in view, so text stays sharp in documents of any length. Not yet: a slider, remembering the size, a size control for the Pages panel. Overall estimate unchanged (about 30–35%). - **2026-10-09 (M7, grouped artwork):** Edit text/images selects page-drawn Form XObjects as whole figures, including nested raster/vector/text content. Their BBox and optional Matrix determine the selection; move/resize/rotate/flip/delete rewrite only the selected page placement, preserving shared Forms and other placements. Engine, automation and UI core regressions cover geometry, rendered movement, save/reopen, undo and selection/drag/Delete. Bitmap replacement/extraction remain raster-only; inline images and individual elements inside a Form are not editable through this tool. Overall estimate unchanged (about 30–35%). diff --git a/crates/automation/tests/automation.rs b/crates/automation/tests/automation.rs index be1c7134..1834d187 100644 --- a/crates/automation/tests/automation.rs +++ b/crates/automation/tests/automation.rs @@ -2399,7 +2399,7 @@ fn faux_redacted_fixture() -> Vec { #[test] fn auditing_faux_redactions_through_tools() { - let dir = workdir("audit"); + let dir = workdir("faux-redaction-audit"); std::fs::write(dir.join("redacted.pdf"), faux_redacted_fixture()).unwrap(); let mut a = auto(&dir); let doc = ok(&mut a, "doc_open", json!({ "path": "redacted.pdf" }))["doc"].as_u64().unwrap(); diff --git a/crates/edit/Cargo.toml b/crates/edit/Cargo.toml index c929bfb4..0c773767 100644 --- a/crates/edit/Cargo.toml +++ b/crates/edit/Cargo.toml @@ -15,7 +15,8 @@ pdfcraft-content = { workspace = true } pdfcraft-fonts = { workspace = true } pdfcraft-model = { workspace = true } thiserror = { workspace = true } -# Display order of right-to-left lines in added text; already in the tree via the interface. +# Display order of right-to-left lines in added text, and reading and writing order in Edit text; +# already in the tree via the interface. unicode-bidi = "0.3" [dev-dependencies] diff --git a/crates/edit/src/added/arabic_tests.rs b/crates/edit/src/added/arabic_tests.rs index 5feb62bb..68743dc7 100644 --- a/crates/edit/src/added/arabic_tests.rs +++ b/crates/edit/src/added/arabic_tests.rs @@ -134,7 +134,12 @@ fn an_item_below_the_old_limit_is_drawn_exactly_as_before() { let mut bytes = out.clone(); pdfcraft_cos::serialize(&Object::Dict(fonts.clone()), &mut bytes); bytes.extend(snapshot(&doc)); - assert_eq!(fnv1a(&bytes), SMALL_ITEM_FINGERPRINT, "output changed; the golden is the code before the sharding change"); + // The golden was recorded without craft-fonts: with an Arabic face built in, the font's + // FontName and FontFamily carry that face's name instead of "Arabic", so only the build + // without it (CI's Windows job) can compare bytes. + if pdfcraft_fonts::document_arabic_font().is_none() { + assert_eq!(fnv1a(&bytes), SMALL_ITEM_FINGERPRINT, "output changed; the golden is the code before the sharding change"); + } } #[test] diff --git a/crates/edit/src/bidi.rs b/crates/edit/src/bidi.rs new file mode 100644 index 00000000..2c66a6ca --- /dev/null +++ b/crates/edit/src/bidi.rs @@ -0,0 +1,364 @@ +//! Right-to-left text in Edit text (Hebrew, Arabic). +//! +//! A content stream draws glyphs left to right, so a producer writes a right-to-left line in +//! *visual* order: `בדיוק` is drawn, and read back from the stream, as `קוידב`. The +//! editor works on *logical* (typing) order instead: [`to_logical`] turns a line read from the +//! page into the order a person types it, and [`to_visual`] turns a line about to be written back +//! into the order it must be drawn in (UAX #9 through `unicode-bidi`). +//! +//! Visual order doesn't always determine one logical order (`abc 12 םולש` is both `abc שלום 12` +//! and `abc 12 שלום`), so [`to_logical`] tries a few readings and keeps the first that draws back +//! exactly as the page did. Lines without right-to-left characters are returned unchanged. +//! +//! Combining marks (Hebrew points, Arabic harakat) stay after their base character in both +//! orders. Brackets drawn in a right-to-left run are mirrored, as UAX #9 L4 mirrors them on +//! display. + +use unicode_bidi::{BidiInfo, Level}; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum Class { + Rtl, + Ltr, + Number, + Neutral, +} + +/// Letters, marks and punctuation of the right-to-left scripts' blocks (digits excepted). +pub(crate) fn is_rtl(c: char) -> bool { + !is_digit(c) && matches!(u32::from(c), 0x0590..=0x08FF | 0xFB1D..=0xFDFF | 0xFE70..=0xFEFC | 0x1_0800..=0x1_0FFF | 0x1_E800..=0x1_EFFF) +} + +/// The Hebrew block and the Hebrew presentation forms. +pub(crate) fn is_hebrew(c: char) -> bool { + matches!(u32::from(c), 0x0590..=0x05FF | 0xFB1D..=0xFB4F) +} + +/// Hebrew points and cantillation marks (niqqud, te'amim): combining, drawn on their letter. +pub(crate) fn is_hebrew_mark(c: char) -> bool { + matches!(u32::from(c), 0x0591..=0x05BD | 0x05BF | 0x05C1..=0x05C2 | 0x05C4..=0x05C5 | 0x05C7 | 0xFB1E) +} + +fn is_digit(c: char) -> bool { + c.is_ascii_digit() || matches!(c, '\u{0660}'..='\u{0669}' | '\u{06F0}'..='\u{06F9}') +} + +/// Combining marks that ride on the character before them. +fn is_mark(c: char) -> bool { + is_hebrew_mark(c) + || matches!( + u32::from(c), + 0x0300..=0x036F | 0x0610..=0x061A | 0x064B..=0x065F | 0x0670 | 0x06D6..=0x06DC | 0x06DF..=0x06E4 | 0x06E7..=0x06E8 | 0x06EA..=0x06ED | 0xFE20..=0xFE2F + ) +} + +/// Direction marks, embeddings, overrides and isolates: they steer the reordering and are not drawn. +fn is_bidi_control(c: char) -> bool { + matches!(c, '\u{061C}' | '\u{200E}' | '\u{200F}' | '\u{202A}'..='\u{202E}' | '\u{2066}'..='\u{2069}') +} + +fn mirrored(c: char) -> char { + match c { + '(' => ')', + ')' => '(', + '[' => ']', + ']' => '[', + '{' => '}', + '}' => '{', + '<' => '>', + '>' => '<', + '«' => '»', + '»' => '«', + other => other, + } +} + +fn class(c: char) -> Class { + if is_digit(c) { + Class::Number + } else if is_rtl(c) { + Class::Rtl + } else if c.is_alphabetic() { + Class::Ltr + } else { + Class::Neutral + } +} + +/// A base character with the combining marks after it. +#[derive(Clone, Debug)] +struct Cluster { + text: String, + class: Class, +} + +impl Cluster { + /// The cluster with its base mirrored (marks stay as they are). + fn mirror(&self) -> Cluster { + let mut chars = self.text.chars(); + let mut text: String = chars.next().map(mirrored).into_iter().collect(); + text.extend(chars); + Cluster { text, class: self.class } + } +} + +fn clusters(s: &str) -> Vec { + let mut out: Vec = Vec::new(); + for c in s.chars() { + match out.last_mut() { + Some(last) if is_mark(c) => last.text.push(c), + _ => out.push(Cluster { text: c.to_string(), class: class(c) }), + } + } + out +} + +/// Whether the line reads right to left as a whole: more right-to-left letters than +/// left-to-right ones. Counting (not the first strong character) gives the same answer for a +/// line in either order, so reading a line and writing it back agree on its direction. +pub(crate) fn rtl_base(text: &str) -> bool { + let (mut r, mut l) = (0usize, 0usize); + for c in text.chars() { + match class(c) { + Class::Rtl if !is_mark(c) => r = r.saturating_add(1), + Class::Ltr if !is_mark(c) => l = l.saturating_add(1), + _ => {} + } + } + r > l +} + +/// Maximal ranges that start and end with a `member` and hold only members and `bridge`s. +fn runs(cl: &[Cluster], member: impl Fn(&Cluster) -> bool, bridge: impl Fn(&[Cluster], usize) -> bool) -> Vec> { + let mut out = Vec::new(); + let mut i = 0; + while i < cl.len() { + if !cl.get(i).is_some_and(&member) { + i += 1; + continue; + } + let mut end = i; + let mut j = i + 1; + while let Some(c) = cl.get(j) { + if member(c) { + end = j; + } else if !bridge(cl, j) { + break; + } + j += 1; + } + out.push(i..end + 1); + i = end + 1; + } + out +} + +/// One separator between two digits (`1,000`, `12:30`, `3.5`, `1/2`, `5-6`) keeps a number whole. +fn number_separator(cl: &[Cluster], j: usize) -> bool { + let digit = |k: Option| k.and_then(|k| cl.get(k)).is_some_and(|c| c.class == Class::Number); + cl.get(j).is_some_and(|c| matches!(c.text.as_str(), "." | "," | ":" | "/" | "-" | "+")) && digit(j.checked_sub(1)) && digit(Some(j + 1)) +} + +fn reverse_mirrored(cl: &mut [Cluster]) { + cl.reverse(); + for c in cl.iter_mut() { + *c = c.mirror(); + } +} + +/// One reading of `visual` in logical order. `wide` lets runs of the other direction swallow the +/// spaces and numbers between their words; narrow keeps numbers and words apart. +fn reading(visual: &[Cluster], rtl: bool, wide: bool) -> String { + let mut cl = visual.to_vec(); + let number = |c: &Cluster| c.class == Class::Number; + if rtl { + // Everything reads right to left, except left-to-right words and numbers. + reverse_mirrored(&mut cl); + let ltr = |c: &Cluster| matches!(c.class, Class::Ltr | Class::Number); + let bridge = + |cl: &[Cluster], j: usize| cl.get(j).is_some_and(|c| c.class == Class::Neutral) && (wide || !neutral_between_word_and_number(cl, j)); + for r in runs(&cl, ltr, bridge) { + if let Some(run) = cl.get_mut(r) { + reverse_mirrored(run); + } + } + } else { + // Left to right, except right-to-left stretches; numbers inside those read left to right. + let rtl_member = |c: &Cluster| c.class == Class::Rtl; + let bridge = |cl: &[Cluster], j: usize| cl.get(j).is_some_and(|c| c.class == Class::Neutral || (wide && c.class == Class::Number)); + for r in runs(&cl, rtl_member, bridge) { + if let Some(run) = cl.get_mut(r) { + reverse_mirrored(run); + for n in runs(run, number, number_separator) { + if let Some(num) = run.get_mut(n) { + reverse_mirrored(num); + } + } + } + } + } + cl.iter().map(|c| c.text.as_str()).collect() +} + +/// Whether the neutral at `j` sits between a left-to-right word on one side and a number on the +/// other (UAX #9 resolves such a space to the line's direction, splitting the run). +fn neutral_between_word_and_number(cl: &[Cluster], j: usize) -> bool { + let strong = |mut k: usize, step: isize| -> Option { + loop { + k = k.checked_add_signed(step)?; + match cl.get(k)?.class { + Class::Neutral => continue, + other => return Some(other), + } + } + }; + matches!((strong(j, -1), strong(j, 1)), (Some(Class::Ltr), Some(Class::Number)) | (Some(Class::Number), Some(Class::Ltr))) +} + +/// A line read from a content stream (drawn left to right), in logical order. +pub fn to_logical(visual: &str) -> String { + if !visual.chars().any(is_rtl) { + return visual.to_owned(); + } + let cl = clusters(visual); + let rtl = rtl_base(visual); + let readings = [reading(&cl, rtl, true), reading(&cl, rtl, false)]; + // Readings that draw back exactly as the page did; of those, one whose brackets pair up + // (`12 (PDF)`, not `PDF) 12)`), else the first. + let exact: Vec<&String> = readings.iter().filter(|r| to_visual(r) == visual).collect(); + let chosen = exact.iter().find(|r| balanced(r)).or(exact.first()).map_or_else(|| readings[0].clone(), |r| (*r).clone()); + // Some producers (LibreOffice) map a bracket drawn in a right-to-left run to the character it + // stands for, not the shape drawn, so mirroring it again turns it inside out: `)טיוטה(`. + let swapped: String = chosen.chars().map(mirrored).collect(); + if !balanced(&chosen) && balanced(&swapped) { swapped } else { chosen } +} + +/// Whether every closing bracket closes an open one. +fn balanced(text: &str) -> bool { + let mut open: Vec = Vec::new(); + for c in text.chars() { + match c { + '(' | '[' | '{' => open.push(c), + ')' | ']' | '}' if open.pop() != Some(mirrored(c)) => return false, + _ => {} + } + } + open.is_empty() +} + +/// A line in logical order, in the order it must be drawn (left to right), without the bidi +/// controls fonts can't draw. +pub fn to_visual(logical: &str) -> String { + let text: String = logical.chars().filter(|c| !is_bidi_control(*c)).collect(); + if !text.chars().any(is_rtl) { + return text; + } + let level = if rtl_base(&text) { Level::rtl() } else { Level::ltr() }; + let info = BidiInfo::new(&text, Some(level)); + let mut out = String::with_capacity(text.len()); + for para in &info.paragraphs { + let (levels, runs) = info.visual_runs(para, para.range.clone()); + for run in runs { + let rtl = levels.get(run.start).is_some_and(|l| l.is_rtl()); + // Runs lie on character boundaries; a missing slice is skipped, never a panic. + let Some(piece) = text.get(run) else { continue }; + if rtl { + for c in clusters(piece).iter().rev() { + out.push_str(&c.mirror().text); + } + } else { + out.push_str(piece); + } + } + } + out +} + +#[cfg(test)] +mod tests { + use super::*; + + /// What a producer writes into the content stream for `logical`. + fn drawn(logical: &str) -> String { + to_visual(logical) + } + + #[test] + fn the_reported_word_reads_back_in_typing_order() { + // The report: Edit text showed "בדיוק" (exactly) reversed. + assert_eq!(drawn("בדיוק"), "קוידב"); + assert_eq!(to_logical("קוידב"), "בדיוק"); + } + + #[test] + fn hebrew_sentences_numbers_latin_and_brackets_round_trip() { + for logical in [ + "שלום עולם", + "עמוד 12 מתוך 40", + "מחיר: 1,250.50 ₪", + "השעה 12:30", + "קובץ PDF חדש", + "שלום (עולם)", + "ראו [1] ו-[2]", + "גרסה 2.0 של PdfCraft נוסחה", + "hello שלום עולם world", + "abc שלום def", + "שלום abc def עולם", + "Report: דוח שנתי 2026", + "שלום עולם, עמוד 12 (PDF)", + "פרק 3 (מבוא)", + "שָׁלוֹם עוֹלָם", + "مرحبا بالعالم", + ] { + let visual = drawn(logical); + assert_ne!(visual, logical, "{logical:?} should be reordered"); + assert_eq!(to_visual(&to_logical(&visual)), visual, "{logical:?} drawn as {visual:?}"); + assert_eq!(to_logical(&visual), logical, "{logical:?} drawn as {visual:?}"); + } + } + + #[test] + fn ambiguous_lines_still_draw_back_the_same() { + // "abc 12 םולש" is both "abc שלום 12" and "abc 12 שלום"; either must draw the same. + for logical in ["abc שלום 12", "שלום 12 abc", "a (ב) c", "דוח Report 2026", "(1) שלום"] { + let visual = drawn(logical); + assert_eq!(to_visual(&to_logical(&visual)), visual, "{logical:?}"); + } + } + + #[test] + fn brackets_mapped_to_their_logical_character_pair_up() { + // LibreOffice draws "(טיוטה)" with ToUnicode giving the logical brackets: `)הטויט(`. + assert_eq!(to_logical(")הטויט("), "(טיוטה)"); + // Brackets mapped to the shape drawn read the same. + assert_eq!(to_logical("(הטויט)"), "(טיוטה)"); + } + + #[test] + fn left_to_right_text_is_untouched() { + for s in ["", "Hello, world (1)", "日本語のテキスト", "Ünïcödé 12:30"] { + assert_eq!(to_logical(s), s); + assert_eq!(to_visual(s), s); + } + } + + #[test] + fn points_stay_on_their_letter() { + // Each point follows its letter in both orders. + let visual = drawn("שָׁלוֹם"); + let first = visual.chars().next(); + assert_eq!(first, Some('ם')); + assert!(visual.contains("לוֹ") || visual.contains("וֹל"), "{visual:?}"); + assert_eq!(to_logical(&visual), "שָׁלוֹם"); + } + + #[test] + fn controls_are_not_drawn_and_hostile_input_does_not_panic() { + assert_eq!(to_visual("\u{200F}שלום\u{202B}"), "םולש"); + for s in ["\u{05B8}", "\u{05B8}\u{05B8}א", "(((", "א\u{0300}", "1-", "-1א", "א\n\tב", "\u{202E}abc"] { + let _ = to_visual(&to_logical(s)); + } + let long = "שלום 12 abc (x) ".repeat(2_000); + assert_eq!(to_visual(&to_logical(&to_visual(&long))), to_visual(&long)); + } +} diff --git a/crates/edit/src/lib.rs b/crates/edit/src/lib.rs index 0e97c63e..2ddb348f 100644 --- a/crates/edit/src/lib.rs +++ b/crates/edit/src/lib.rs @@ -629,6 +629,7 @@ mod flatten; pub use flatten::{flatten, flatten_fill_sign}; pub mod images; pub use images::{ImageChange, PageImage, change_image, page_images, reading_images, rect_to_rect, turn_about_centre}; +pub mod bidi; pub mod text; pub use text::{BlockStyle, LineEdit, TextBlock, TextLine, reading_blocks, replace_block, replace_line, rewrite_block, text_blocks, text_lines}; pub mod added; diff --git a/crates/edit/src/tests.rs b/crates/edit/src/tests.rs index 720b6fcd..4a68edf6 100644 --- a/crates/edit/src/tests.rs +++ b/crates/edit/src/tests.rs @@ -1779,6 +1779,148 @@ fn the_graphics_state_carries_over_between_content_streams() { assert!(close(l.rect[0], 100.0) && close(l.size, 10.0), "{l:?}"); } +/// One page whose font draws a few Hebrew letters (ToUnicode): A ב, B ד, C י, D ו, E ק, F ש, +/// G ל, H ם. Content streams draw Hebrew in visual order, so "בדיוק" is `(EDCBA)`. +fn hebrew_page(content: &str) -> Document { + let cmap = "/CIDInit /ProcSet findresource begin 12 dict begin begincmap /CMapType 2 def \ + 1 begincodespacerange <00> endcodespacerange 9 beginbfchar \ + <20> <0020> <41> <05D1> <42> <05D3> <43> <05D9> <44> <05D5> <45> <05E7> <46> <05E9> <47> <05DC> <48> <05DD> \ + endbfchar endcmap CMapName currentdict /CMap defineresource pop end end"; + // Widths for codes 32..=72: a space, then the eight letters at 65..=72. + let widths: Vec<&str> = (32..=72) + .map(|c| { + if c == 32 { + "250" + } else if c >= 65 { + "500" + } else { + "0" + } + }) + .collect(); + let objs: Vec = vec![ + "<< /Type /Catalog /Pages 2 0 R >>".into(), + "<< /Type /Pages /Kids [3 0 R] /Count 1 >>".into(), + "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 600 800] /Contents 4 0 R /Resources << /Font << /F1 5 0 R >> >> >>".into(), + format!("<< /Length {} >>\nstream\n{content}\nendstream", content.len()), + format!( + "<< /Type /Font /Subtype /Type1 /BaseFont /ABCDEF+FakeHebrew /FirstChar 32 /LastChar 72 /Widths [{}] /ToUnicode 6 0 R >>", + widths.join(" ") + ), + format!("<< /Length {} >>\nstream\n{cmap}\nendstream", cmap.len()), + ]; + let mut out = b"%PDF-1.7\n".to_vec(); + let mut offs = Vec::new(); + for (i, o) in objs.iter().enumerate() { + offs.push(out.len()); + out.extend_from_slice(format!("{} 0 obj\n{o}\nendobj\n", i + 1).as_bytes()); + } + let x = out.len(); + out.extend_from_slice(format!("xref\n0 {}\n0000000000 65535 f \n", objs.len() + 1).as_bytes()); + for o in offs { + out.extend_from_slice(format!("{o:010} 00000 n \n").as_bytes()); + } + out.extend_from_slice(format!("trailer\n<< /Size {} /Root 1 0 R >>\nstartxref\n{x}\n%%EOF\n", objs.len() + 1).as_bytes()); + Document::open(Arc::new(out)).unwrap() +} + +/// The report: Edit text showed "בדיוק" (exactly) reversed, because the line was read in the +/// order the page draws it. +#[test] +fn hebrew_lines_read_in_typing_order() { + let doc = hebrew_page("BT /F1 12 Tf 300 700 Td (EDCBA) Tj ET BT /F1 12 Tf 300 600 Td (EDCBA HDGF) Tj ET"); + let lines: Vec = text::text_lines(&doc, 0).unwrap().into_iter().map(|l| l.text).collect(); + assert_eq!(lines, ["בדיוק", "שלום בדיוק"]); + let blocks: Vec = text::text_blocks(&doc, 0).unwrap().into_iter().map(|b| b.text).collect(); + assert_eq!(blocks, ["בדיוק", "שלום בדיוק"]); +} + +/// Text the page's own font can show is written back in drawing order, right-aligned to where +/// the line ended, and reads back as typed. +#[test] +fn hebrew_edits_are_drawn_right_to_left_and_keep_their_right_edge() { + let mut doc = hebrew_page("BT /F1 12 Tf 300 700 Td (EDCBA) Tj ET"); + let before = text::text_lines(&doc, 0).unwrap()[0].rect; + // One line: the bytes are the visual order of the typed text. + assert_eq!(text::replace_line(&mut doc, 0, 0, "שלום בדיוק").unwrap().substituted, None); + let content = String::from_utf8_lossy(&page_content_bytes(&doc, 0)).into_owned(); + assert!(content.contains("(EDCBA HDGF)"), "{content}"); + assert_eq!(text::text_lines(&reopen(&doc), 0).unwrap()[0].text, "שלום בדיוק"); + + // A paragraph: shorter text stays right-aligned at the old right edge. + let mut doc = hebrew_page("BT /F1 12 Tf 300 700 Td (EDCBA) Tj ET"); + assert_eq!(text::replace_block(&mut doc, 0, 0, "דוב").unwrap().substituted, None); + let content = String::from_utf8_lossy(&page_content_bytes(&doc, 0)).into_owned(); + assert!(content.contains("(ADB)"), "{content}"); + let doc = reopen(&doc); + let line = text::text_lines(&doc, 0).unwrap().swap_remove(0); + assert_eq!(line.text, "דוב"); + assert!((line.rect[2] - before[2]).abs() < 0.5, "right edge {} → {}", before[2], line.rect[2]); + assert!(line.rect[0] > before[0] + 1.0, "shorter text starts further right: {:?} vs {before:?}", line.rect); + + // Longer text grows to the left, not off the right edge. + let mut doc = hebrew_page("BT /F1 12 Tf 300 700 Td (EDCBA) Tj ET"); + text::replace_block(&mut doc, 0, 0, "שלום בדיוק שלום").unwrap(); + let doc = reopen(&doc); + let line = text::text_lines(&doc, 0).unwrap().swap_remove(0); + assert_eq!(line.text, "שלום בדיוק שלום"); + assert!((line.rect[2] - before[2]).abs() < 0.5, "right edge {} → {}", before[2], line.rect[2]); + assert!(line.rect[0] < before[0] - 1.0, "longer text grows left: {:?} vs {before:?}", line.rect); + + // An alignment asked for still wins. + let mut doc = hebrew_page("BT /F1 12 Tf 300 700 Td (EDCBA) Tj ET"); + let left = text::BlockStyle { align: Some(Align::Left), ..Default::default() }; + text::rewrite_block(&mut doc, 0, 0, Some("דוב"), &left).unwrap(); + let line = text::text_lines(&reopen(&doc), 0).unwrap().swap_remove(0); + assert!((line.rect[0] - before[0]).abs() < 0.5, "left-aligned: {:?} vs {before:?}", line.rect); +} + +/// Letters the page's font lacks are drawn with the Hebrew face from craft-fonts; a build +/// without it says so and changes nothing. Hebrew points are refused, not misdrawn. +#[test] +fn hebrew_letters_the_page_font_lacks_use_the_hebrew_face() { + let mut doc = hebrew_page("BT /F1 12 Tf 300 700 Td (EDCBA) Tj ET"); + let before = page_content_bytes(&doc, 0); + let pointed = text::replace_line(&mut doc, 0, 0, "שָׁלוֹם עוֹלָם"); + let Err(EditError::Invalid(msg)) = pointed else { panic!("expected a clear error, got {pointed:?}") }; + assert!(msg.contains("niqqud"), "{msg}"); + assert_eq!(page_content_bytes(&doc, 0), before); + + // ע and ה aren't in the page's font. + let typed = "שלום עולם, עמוד 12 (PDF)"; + let line = text::replace_line(&mut doc, 0, 0, typed); + if pdfcraft_fonts::document_hebrew_font(false).is_none() { + let Err(EditError::Invalid(msg)) = line else { panic!("expected a clear error, got {line:?}") }; + assert!(msg.contains("Hebrew fallback font") && msg.contains("CRAFT_FONTS_DIR"), "{msg}"); + assert_eq!(page_content_bytes(&doc, 0), before); + eprintln!("built without a craft-fonts Hebrew face: the Type 3 path is checked with CRAFT_FONTS_DIR"); + return; + } + assert_eq!(line.unwrap().substituted.as_deref(), Some("Noto Sans Hebrew Type3")); + let content = String::from_utf8_lossy(&page_content_bytes(&doc, 0)).into_owned(); + assert!(content.contains("/PCHe"), "{content}"); + let reopened = reopen(&doc); + assert_eq!(text::text_lines(&reopened, 0).unwrap()[0].text, typed); + // Every glyph has an outline, so nothing is drawn as a box. + let p = pdfcraft_model::pages(&reopened).swap_remove(0); + let res = reopened.resolve(p.dict.get(b"Resources").unwrap()); + let fonts = reopened.resolve(res.as_dict().unwrap().get(b"Font").unwrap()); + let font = reopened.resolve(fonts.as_dict().unwrap().get(b"PCHe").unwrap()); + let procs = reopened.resolve(font.as_dict().unwrap().get(b"CharProcs").unwrap()); + let procs = procs.as_dict().unwrap(); + assert_eq!(procs.iter().count(), typed.chars().collect::>().len()); + for (_, glyph) in procs.iter() { + let pdfcraft_cos::Object::Stream(s) = &*reopened.resolve(glyph) else { panic!("glyph stream") }; + let path = String::from_utf8(s.decoded().unwrap()).unwrap(); + assert!(path.starts_with(|c: char| c.is_ascii_digit()) && path.contains(" d1"), "{path}"); + } + + // A whole paragraph too, right-aligned. + let mut doc = hebrew_page("BT /F1 12 Tf 300 700 Td (EDCBA) Tj ET"); + assert_eq!(text::replace_block(&mut doc, 0, 0, "עברית מלאה").unwrap().substituted.as_deref(), Some("Noto Sans Hebrew Type3")); + assert_eq!(text::text_blocks(&reopen(&doc), 0).unwrap()[0].text, "עברית מלאה"); +} + /// #125: page-content tools refuse text the standard fonts can't draw instead of writing `?`, /// and leave the document untouched. #[test] diff --git a/crates/edit/src/text.rs b/crates/edit/src/text.rs index e84ab1ad..3a0d4a10 100644 --- a/crates/edit/src/text.rs +++ b/crates/edit/src/text.rs @@ -38,7 +38,7 @@ pub struct TextLine { /// Whether new text in this font can only be shown by substituting another font (no /// Unicode mapping for the line, so nothing could be reused). pub decodable: bool, - /// The Japanese fallback face the font subsets, when the line is in one (see [`same_font`]). + /// The Japanese or Hebrew fallback face the font subsets, when the line is in one (see [`same_font`]). face: Option, /// The content stream piece the line starts in, and its operators (indices into the page's /// streams parsed joined). @@ -152,7 +152,7 @@ struct Ts { /// The operators that set the current fill colour (`g`, `rg`, `k`, or `cs` + `sc`/`scn`). fill: Vec, font: Option<(Vec, Rc)>, - /// The Japanese fallback face the font subsets, when the font is one (see [`is_japanese_fallback`]). + /// The Japanese or Hebrew fallback face the font subsets, when the font is one (see [`is_type3_fallback`]). face: Option, size: f64, char_spacing: f64, @@ -358,7 +358,7 @@ fn interpret( let (m, face) = cache.entry(name.to_vec()).or_insert_with(|| { let dict = fonts_res.get(name).and_then(|f| doc.resolve(f).as_dict().cloned()); let metrics = dict.as_ref().map_or_else(Metrics::fallback, |d| Metrics::from_dict(doc, d)); - let face = dict.filter(is_japanese_fallback).map(|_| metrics.base_font.clone()); + let face = dict.filter(is_type3_fallback).map(|_| metrics.base_font.clone()); (Rc::new(metrics), face) }); ts.font = Some((name.to_vec(), m.clone())); @@ -555,6 +555,10 @@ pub fn text_lines(doc: &Document, page: usize) -> Result, EditErro } last = Some((si, s.bt, s.baseline, s.end_x, s.size.max(1.0))); } + // Content streams draw right-to-left lines in visual order; the editor works in typing order. + for l in &mut lines { + l.text = crate::bidi::to_logical(&l.text); + } // Lines of only spaces aren't editable text. lines.retain(|l| !l.text.trim().is_empty()); Ok(lines) @@ -741,14 +745,17 @@ fn needs_type3(text: &str) -> bool { /// The `/Name` of the Type 3 fonts generated for Japanese text. const JAPANESE_FONT_NAME: &str = "PCJapanese"; +/// The `/Name` of the Type 3 fonts generated for Hebrew text. +const HEBREW_FONT_NAME: &str = "PCHebrew"; -/// Whether `font` is one of those generated fonts: a replacement longer than one of them is split -/// across several, which read back as one line (see [`same_font`]). -fn is_japanese_fallback(font: &Dict) -> bool { - font.name(b"Subtype") == Some(b"Type3") && font.name(b"Name") == Some(JAPANESE_FONT_NAME.as_bytes()) +/// Whether `font` is one of those generated fonts (Japanese or Hebrew): a replacement longer than +/// one of them is split across several, which read back as one line (see [`same_font`]). +fn is_type3_fallback(font: &Dict) -> bool { + font.name(b"Subtype") == Some(b"Type3") + && font.name(b"Name").is_some_and(|n| n == JAPANESE_FONT_NAME.as_bytes() || n == HEBREW_FONT_NAME.as_bytes()) } -/// Whether a run shown in `font` (a resource name, and the Japanese fallback face it subsets, if +/// Whether a run shown in `font` (a resource name, and the Japanese or Hebrew fallback face it subsets, if /// any) continues `line`: the same font resource, or another subset of the same face. fn same_font(line: &TextLine, font: &[u8], face: Option<&str>) -> bool { line.font.as_bytes() == font || (line.face.is_some() && line.face.as_deref() == face) @@ -777,7 +784,7 @@ const MAX_TYPE3_GLYPHS: usize = 240; struct Type3Fallback { /// The family its glyphs come from ("Shippori Mincho"). family: &'static str, - /// The fonts' resource names in order; the first is `PCJp`. + /// The fonts' resource names in order; the first is `PCJp` (`PCHe` for Hebrew text). names: Vec, /// Every unique character with its advance (em), in order of first appearance. widths: Vec<(char, f64)>, @@ -800,8 +807,8 @@ fn pdf_num(v: f64) -> String { fn type3_path(face: &CraftFont, ch: char) -> Result<(Vec, f64), EditError> { let glyph = japanese_glyph_from(face, ch).map_err(|e| match e { GlyphError::NoFont => no_japanese_font(), - GlyphError::Missing => EditError::Invalid(format!("no fallback font has a glyph for U+{:04X}", ch as u32)), - GlyphError::TooComplex => EditError::Invalid(format!("Japanese fallback glyph U+{:04X} is too complex", ch as u32)), + GlyphError::Missing => EditError::Invalid(format!("the {} fallback font has no glyph for U+{:04X}", face.family, ch as u32)), + GlyphError::TooComplex => EditError::Invalid(format!("the {} fallback glyph U+{:04X} is too complex", face.family, ch as u32)), })?; let (dx, width) = proportional(ch, &glyph); let scale = 1000.0; @@ -865,6 +872,32 @@ fn fallback_face<'a>(faces: &[&'a CraftFont], chars: &[char], has_glyph: impl Fn faces.iter().copied().find(|face| chars.iter().all(|ch| has_glyph(face, *ch))).or_else(|| faces.first().copied()) } +/// This build has no Hebrew face to draw replacement text with. +fn no_hebrew_font() -> EditError { + EditError::Invalid( + "this text needs PdfCraft's Hebrew fallback font (Noto Sans Hebrew), which this build doesn't include \ + (to build it in, set CRAFT_FONTS_DIR to a craft-fonts checkout that has it)" + .into(), + ) +} + +/// The faces a Type 3 replacement may draw `text` with, best first. Text with Hebrew takes the +/// Hebrew face first (its Latin and digits too, so a line keeps one look), then the Japanese +/// faces for anything it lacks; other text takes the Japanese faces, as before. +fn type3_faces(text: &str, family: crate::added::Family, bold: bool) -> Result<(Vec<&'static CraftFont>, bool), EditError> { + let japanese = pdfcraft_fonts::document_japanese_fonts_for_style(family == crate::added::Family::Times, bold); + if !text.chars().any(crate::bidi::is_hebrew) { + return Ok((japanese, false)); + } + // Points are drawn on their letter with the font's positioning, which a one-glyph-per- + // character Type 3 font can't do; refuse rather than print them beside the letter. + if text.chars().any(crate::bidi::is_hebrew_mark) { + return Err(EditError::Invalid("Hebrew points (niqqud) can't be written into a PDF yet; type the text without them".into())); + } + let hebrew = pdfcraft_fonts::document_hebrew_font(bold).ok_or_else(no_hebrew_font)?; + Ok((std::iter::once(hebrew).chain(japanese).collect(), true)) +} + fn type3_font( doc: &mut Document, fonts_res: &mut Dict, @@ -873,7 +906,7 @@ fn type3_font( bold: bool, max_glyphs: usize, ) -> Result { - let faces = pdfcraft_fonts::document_japanese_fonts_for_style(family == crate::added::Family::Times, bold); + let (faces, hebrew) = type3_faces(text, family, bold)?; // Each unique character once, in order of first appearance. let mut chars = Vec::new(); let mut seen = std::collections::HashSet::new(); @@ -886,16 +919,20 @@ fn type3_font( return Err(EditError::Invalid("replacement text is empty".into())); } // The faces differ in coverage (e.g. of Cyrillic), so use the best face that has every - // character. When none has them all, the best face reports the character it lacks. - let face = fallback_face(&faces, &chars, |face, ch| japanese_glyph_from(face, ch).is_ok()).ok_or_else(no_japanese_font)?; + // character. When none has them all, the best face reports the character it lacks. Hebrew + // text keeps the Hebrew face and takes each character it lacks from the next face that has it. + let face = if hebrew { faces.first().copied() } else { fallback_face(&faces, &chars, |face, ch| japanese_glyph_from(face, ch).is_ok()) } + .ok_or_else(no_japanese_font)?; + let others: &[&CraftFont] = if hebrew { faces.get(1..).unwrap_or_default() } else { &[] }; // Every outline is read before the document is touched, so a character that fails leaves it // as it was. Each font takes the next `per_font` characters; codes restart at 1 in each. let per_font = max_glyphs.clamp(1, MAX_TYPE3_GLYPHS); let mut glyphs = Vec::with_capacity(chars.len()); for (i, ch) in chars.into_iter().enumerate() { - let (path, width) = type3_path(face, ch)?; + // Hebrew text: the first face with the glyph, else the Hebrew face's error. + let (path, width) = others.iter().fold(type3_path(face, ch), |found, f| found.or_else(|e| type3_path(f, ch).map_err(|_| e)))?; let code = u8::try_from((i % per_font).saturating_add(1)) - .map_err(|_| EditError::Invalid("Japanese replacement has too many unique characters".into()))?; + .map_err(|_| EditError::Invalid("replacement text has too many unique characters".into()))?; glyphs.push(Type3Glyph { code, ch, path, width }); } let mut codes = HashMap::with_capacity(glyphs.len()); @@ -911,14 +948,15 @@ fn type3_font( codes.insert(glyph.ch, (names.len(), glyph.code)); widths.push((glyph.ch, glyph.width)); } - names.push(add_type3_font(doc, fonts_res, face, group)); + names.push(add_type3_font(doc, fonts_res, face, hebrew, group)); } Ok(Type3Fallback { family: face.family, names, widths, codes }) } /// Adds one generated font whose codes 1 to `glyphs.len()` show `glyphs`, and returns its resource -/// name (`PCJp`, then `PCJp1`, ...). Nothing here can fail: the outlines were read already. -fn add_type3_font(doc: &mut Document, fonts_res: &mut Dict, face: &CraftFont, glyphs: Vec) -> String { +/// name (`PCJp`, then `PCJp1`, ...; `PCHe`, ... for Hebrew text). Nothing here can fail: the +/// outlines were read already. +fn add_type3_font(doc: &mut Document, fonts_res: &mut Dict, face: &CraftFont, hebrew: bool, glyphs: Vec) -> String { let last_char = glyphs.len(); let mut charprocs = Dict::new(); let mut widths = Vec::with_capacity(last_char); @@ -947,7 +985,7 @@ fn add_type3_font(doc: &mut Document, fonts_res: &mut Dict, face: &CraftFont, gl let mut font = Dict::new(); font.set(b"Type".to_vec(), Object::name("Font")); font.set(b"Subtype".to_vec(), Object::name("Type3")); - font.set(b"Name".to_vec(), Object::name(JAPANESE_FONT_NAME)); + font.set(b"Name".to_vec(), Object::name(if hebrew { HEBREW_FONT_NAME } else { JAPANESE_FONT_NAME })); let mut descriptor = Dict::new(); descriptor.set(b"Type".to_vec(), Object::name("FontDescriptor")); descriptor.set(b"FontName".to_vec(), Object::name(&format!("{}-{}", face.family, face.style).replace(' ', ""))); @@ -967,11 +1005,12 @@ fn add_type3_font(doc: &mut Document, fonts_res: &mut Dict, face: &CraftFont, gl font.set(b"Encoding".to_vec(), Object::Dict(encoding)); font.set(b"CharProcs".to_vec(), Object::Dict(charprocs)); font.set(b"ToUnicode".to_vec(), Object::Ref(cmap_ref)); - let mut name = String::from("PCJp"); + let prefix = if hebrew { "PCHe" } else { "PCJp" }; + let mut name = String::from(prefix); let mut suffix = 0usize; while fonts_res.contains(name.as_bytes()) { suffix = suffix.saturating_add(1); - name = format!("PCJp{suffix}"); + name = format!("{prefix}{suffix}"); } fonts_res.set(name.as_bytes().to_vec(), Object::Dict(font)); name @@ -1010,7 +1049,9 @@ pub(crate) fn replace_line_with_limit(doc: &mut Document, page: usize, line: usi let first = target.ops.first().copied().filter(|i| *i < ops.len()).ok_or_else(|| EditError::Invalid("the page's content changed".into()))?; // The line's own font, when it can show every character. let font = fonts_res.get(target.font.as_bytes()).and_then(|f| doc.resolve(f).as_dict().cloned()).map(|d| Metrics::from_dict(doc, &d)); - let reused = font.as_ref().and_then(|m| m.encode(&text)); + // Typed in logical order; drawn left to right (right-to-left runs reversed). + let shown = crate::bidi::to_visual(&text); + let reused = font.as_ref().and_then(|m| m.encode(&shown)); let mut substituted = None; let mut replacement: Vec = Vec::new(); // ' and " also move to the next line; keep that. @@ -1028,8 +1069,8 @@ pub(crate) fn replace_line_with_limit(doc: &mut Document, page: usize, line: usi None => { if needs_type3(&text) { let fallback = type3_font(doc, &mut fonts_res, &text, source_family(&target.base_font), target.bold, max_glyphs)?; - let runs = type3_encode(&fallback, &text) - .ok_or_else(|| EditError::Invalid(format!("\"{text}\" can't be shown by the Japanese fallback")))?; + let runs = type3_encode(&fallback, &shown) + .ok_or_else(|| EditError::Invalid(format!("\"{text}\" can't be shown by the {} fallback", fallback.family)))?; let size = font_size_before(&ops, first).unwrap_or(target.size); for (name, bytes) in runs { replacement.push(Op::new("Tf", vec![Object::name(&name), pdfcraft_content::num(size)])); @@ -1038,10 +1079,10 @@ pub(crate) fn replace_line_with_limit(doc: &mut Document, page: usize, line: usi replacement.push(Op::new("Tf", vec![Object::name(&target.font), pdfcraft_content::num(size)])); substituted = Some(format!("{} Type3", fallback.family)); } else { - let win = pdfcraft_fonts::win_ansi(&text); + let win = pdfcraft_fonts::win_ansi(&shown); // WinAnsi turns what it can't show into '?'; refuse rather than print the wrong thing. let back: String = win.iter().map(|b| char::from_u32(u32::from(*b)).unwrap_or('?')).collect(); - if text.chars().zip(back.chars()).any(|(a, b)| b == '?' && a != '?') { + if shown.chars().zip(back.chars()).any(|(a, b)| b == '?' && a != '?') { return Err(EditError::Invalid(format!("\"{text}\" has characters neither {} nor Helvetica can show", target.base_font))); } // The size in text space: the current Tf's size. @@ -1292,15 +1333,26 @@ pub(crate) fn rewrite_block_with_limit( if !(dx.is_finite() && dy.is_finite()) { return Err(EditError::Invalid("the paragraph can't be moved that far".into())); } - // A paragraph keeps its width (or takes the one asked for); a single line grows to the right, - // up to the page's margin. + // Right-to-left text (more Hebrew or Arabic letters than Latin) is right-aligned unless an + // alignment is asked for, and a single right-to-left line keeps its right edge as it grows. + let rtl = crate::bidi::rtl_base(&text); + let anchored_right = rtl && style.width.is_none() && members.len() == 1; + // A paragraph keeps its width (or takes the one asked for); a single line grows to the right + // (to the left when right-to-left), up to the page's margin. let width = match style.width { // Capped at the largest page PDF allows (14 400 pt). Some(w) if w.is_finite() => w.clamp(size * k, 14_400.0), Some(_) => return Err(EditError::Invalid("the paragraph's width must be a number".into())), + None if anchored_right => (b.rect[2] - b.rect[0]).max(size * k).max(b.rect[2] + dx - (p.crop(doc)[0] + 36.0)), None if members.len() == 1 => (b.rect[2] - b.rect[0]).max(size * k).max(p.crop(doc)[2] - 36.0 - (b.rect[0] + dx)), None => (b.rect[2] - b.rect[0]).max(size * k), }; + if !width.is_finite() { + return Err(EditError::Invalid("the paragraph's width must be a number".into())); + } + let align = style.align.or(rtl.then_some(crate::added::Align::Right)); + // Where a right-aligned line ends, from the paragraph's left edge (user space). + let right_edge = if anchored_right { (b.rect[2] - b.rect[0]).max(0.0) } else { width }; // The move and the width are checked here, before the fallback fonts are written: a refused // paragraph leaves the document as it was. The move is in page space, so it is taken back through // the CTM's linear part into the space the paragraph is drawn in. @@ -1397,32 +1449,38 @@ pub(crate) fn rewrite_block_with_limit( // Alignment: each line's offset from the left edge, in text space. let offset = |line: &str| -> f64 { let free = (width - advance(line)).max(0.0) / k; - match style.align { + match align { Some(crate::added::Align::Center) => free / 2.0, + // A right-to-left line anchored at its right edge may start left of the old box. + Some(crate::added::Align::Right) if anchored_right => (right_edge - advance(line)) / k, Some(crate::added::Align::Right) => free, + // A justified right-to-left paragraph ends its last line at the right edge. + Some(crate::added::Align::Justify) if rtl => free, _ => 0.0, } }; // Justify: word spacing (single-byte code 32 only) so each line but the last fills the width. let single_byte = !reuse || metrics.as_ref().is_some_and(|m| !m.composite); - let justify = style.align == Some(crate::added::Align::Justify) && single_byte; + let justify = align == Some(crate::added::Align::Justify) && single_byte; let mut x = 0.0; let mut tw_set = 0.0; let mut underlines: Vec<(f64, f64, f64)> = Vec::new(); // (x0, x1, y) in text space for (i, line) in wrapped.iter().enumerate() { - let dx = offset(line); + let spaces = line.matches(' ').count(); + let tw = + if justify && i + 1 < wrapped.len() && spaces > 0 { (width - advance(line)).max(0.0) / k / o_state.scale / spaces as f64 } else { 0.0 }; + // A stretched line fills the width from the left edge. + let dx = if tw > 0.0 { 0.0 } else { offset(line) }; if i > 0 || dx != 0.0 { block_ops.push(Op::new("Td", vec![n(dx - x), n(if i > 0 { -lead } else { 0.0 })])); } x = dx; - let spaces = line.matches(' ').count(); - let tw = - if justify && i + 1 < wrapped.len() && spaces > 0 { (width - advance(line)).max(0.0) / k / o_state.scale / spaces as f64 } else { 0.0 }; if tw != tw_set { block_ops.push(Op::new("Tw", vec![n(tw)])); tw_set = tw; } - for (font, bytes) in encode(line).ok_or_else(|| EditError::Invalid(format!("\"{line}\" can't be shown")))? { + // Each wrapped line in typing order; drawn left to right (right-to-left runs reversed). + for (font, bytes) in encode(&crate::bidi::to_visual(line)).ok_or_else(|| EditError::Invalid(format!("\"{line}\" can't be shown")))? { if font != shown_font { block_ops.push(Op::new("Tf", vec![Object::name(&font), n(size)])); shown_font = font; @@ -1695,4 +1753,27 @@ mod tests { assert_eq!(fallback_face(&faces, &['a', 'ф'], |_, ch| ch.is_ascii()).map(|f| f.family), Some("Mincho")); assert!(fallback_face(&[], &['a'], has).is_none()); } + + #[test] + fn hebrew_and_japanese_fallback_shards_are_recognized_as_ours() { + static HEBR: &[&str] = &["Hebr"]; + let face = CraftFont { family: "FakeHebrew", style: "Regular", scripts: HEBR, bytes: b"" }; + let glyph = |code, ch| Type3Glyph { code, ch, path: b"0 0 0 0 0 0 d1\nf\n".to_vec(), width: 0.5 }; + let mut doc = Document::new_empty(); + let mut fonts = Dict::new(); + // A replacement split across two Hebrew fonts, then one Japanese font. + let he = add_type3_font(&mut doc, &mut fonts, &face, true, vec![glyph(1, 'א')]); + let he1 = add_type3_font(&mut doc, &mut fonts, &face, true, vec![glyph(1, 'ב')]); + let jp = add_type3_font(&mut doc, &mut fonts, &face, false, vec![glyph(1, 'あ')]); + assert_eq!([he.as_str(), he1.as_str(), jp.as_str()], ["PCHe", "PCHe1", "PCJp"]); + for name in [&he, &he1, &jp] { + let font = fonts.get(name.as_bytes()).and_then(Object::as_dict).cloned().unwrap_or_default(); + assert!(is_type3_fallback(&font), "{name} isn't recognized as a generated fallback font"); + } + // Someone else's Type 3 font is not ours. + let mut other = Dict::new(); + other.set(b"Subtype".to_vec(), Object::name("Type3")); + other.set(b"Name".to_vec(), Object::name("PCHebrewX")); + assert!(!is_type3_fallback(&other)); + } } diff --git a/crates/fonts/README.md b/crates/fonts/README.md index aa717449..44b617dd 100644 --- a/crates/fonts/README.md +++ b/crates/fonts/README.md @@ -25,6 +25,10 @@ set). Without that variable `CRAFT_FONTS` is empty and everything below copes: titles), in manifest order. - `ui_telugu_fonts`: the `Telu` faces for Telugu interface text (the Telugu catalog, file names, document titles), in manifest order. +- `ui_hebrew_fonts` / `document_hebrew_font(bold)`: the `Hebr` faces (Noto Sans Hebrew) for + Hebrew interface text, and the face whose outlines become the Type 3 fallback for Hebrew text + written by Edit text (Bold when asked for and present, else Regular). `None` without a + craft-fonts input that has one; the editor then reports a clear error. - `document_japanese_font` / `japanese_glyph`: the face (Shippori Mincho, then BIZ UDMincho) whose outlines become the Type 3 fallback font for Japanese text written into PDFs. Without it, `japanese_glyph` returns `GlyphError::NoFont` and the editor reports a clear error. @@ -50,7 +54,7 @@ The descriptor does not change glyph programs or replace the Type 3 `FontBBox` m only Gothic Regular, so it cannot preserve bold. No new font assets or changes to that input are needed. -wasm32 builds embed only BIZ UDPGothic Regular and any `Arab` or `Telu` face, to keep the web build small: the web build +wasm32 builds embed only BIZ UDPGothic Regular and any `Arab`, `Telu` or `Hebr` face, to keep the web build small: the web build currently has no Chinese face, so Chinese there still shows the replacement glyph. Font files are never committed here (`AGENTS.md` §1.4; team members: [craftrules `standards/fonts.md`](https://github.com/storytold/craftrules/blob/main/standards/fonts.md), internal). diff --git a/crates/fonts/build.rs b/crates/fonts/build.rs index 4b98f7d8..e452f9ac 100644 --- a/crates/fonts/build.rs +++ b/crates/fonts/build.rs @@ -1,7 +1,7 @@ //! Embeds the fonts of the optional craft-fonts build input (`CRAFT_FONTS_DIR`) as `CRAFT_FONTS`. //! The recipe is craft-fonts' `docs/integration.md`; unset, `CRAFT_FONTS` is empty. It only reads //! the local checkout (no network). On wasm32 it embeds only the UI faces (BIZ UDPGothic Regular -//! and any `Arab` or `Telu` face), to keep the web build within hosting limits (Cloudflare Pages: +//! and any `Arab`, `Telu` or `Hebr` face), to keep the web build within hosting limits (Cloudflare Pages: //! 25 MiB per file). use std::fmt::Write as _; @@ -53,7 +53,9 @@ fn craft_fonts(dir: &std::path::Path) -> Result { let arabic = scripts.split(',').any(|s| s.trim() == "Arab"); // Telugu faces too, for the Telugu interface. let telugu = scripts.split(',').any(|s| s.trim() == "Telu"); - let web_face = (*family == "BIZ UDPGothic" && *style == "Regular") || arabic || telugu; + // And the Hebrew ones (small), for Hebrew text and Edit text on the web. + let hebrew = scripts.split(',').any(|s| s.trim() == "Hebr"); + let web_face = (*family == "BIZ UDPGothic" && *style == "Regular") || arabic || telugu || hebrew; if wasm && !web_face { continue; } diff --git a/crates/fonts/src/craft.rs b/crates/fonts/src/craft.rs index 163f1dae..d8ab775e 100644 --- a/crates/fonts/src/craft.rs +++ b/crates/fonts/src/craft.rs @@ -66,6 +66,25 @@ pub fn ui_telugu_fonts() -> Vec<&'static CraftFont> { CRAFT_FONTS.iter().filter(|f| f.covers("Telu")).collect() } +/// The `Hebr` craft-fonts faces for Hebrew interface text (file names, document titles, text +/// being edited), in manifest order. Empty when built without craft-fonts or when it has no +/// Hebrew face. +pub fn ui_hebrew_fonts() -> Vec<&'static CraftFont> { + CRAFT_FONTS.iter().filter(|f| f.covers("Hebr")).collect() +} + +/// The face for Hebrew text written into PDFs: the `Hebr` face of the asked weight, else its +/// Regular, else any `Hebr` face. `None` without craft-fonts or without a Hebrew face. +pub fn document_hebrew_font(bold: bool) -> Option<&'static CraftFont> { + hebrew_face(CRAFT_FONTS, bold) +} + +fn hebrew_face(faces: &[CraftFont], bold: bool) -> Option<&CraftFont> { + let hebr = || faces.iter().filter(|f| f.covers("Hebr")); + let style = if bold { "Bold" } else { "Regular" }; + hebr().find(|f| f.style == style).or_else(|| hebr().find(|f| f.style == "Regular")).or_else(|| hebr().next()) +} + /// Interface CJK faces in fallback order for the UI language: Simplified Chinese first when /// `prefer_hans`, otherwise Japanese first (the historical default). /// @@ -251,6 +270,23 @@ mod tests { assert!(!BARRED_FAMILIES.iter().any(|barred| "Noto Sans Arabic".starts_with(barred))); } + #[test] + fn hebrew_document_faces_follow_the_weight() { + static HEBR_LATN: &[&str] = &["Hebr", "Latn"]; + let faces = [ + CraftFont { family: "BIZ UDPGothic", style: "Bold", scripts: &["Jpan"], bytes: b"not-hebrew" }, + CraftFont { family: "FakeHebrew", style: "Regular", scripts: HEBR_LATN, bytes: b"regular" }, + CraftFont { family: "FakeHebrew", style: "Bold", scripts: HEBR_LATN, bytes: b"bold" }, + ]; + assert_eq!(hebrew_face(&faces, false).map(|f| f.bytes), Some(&b"regular"[..])); + assert_eq!(hebrew_face(&faces, true).map(|f| f.bytes), Some(&b"bold"[..])); + // No bold face: Regular, never a face of another script. + assert_eq!(hebrew_face(&faces[..2], true).map(|f| f.bytes), Some(&b"regular"[..])); + assert!(hebrew_face(&faces[..1], false).is_none()); + assert_eq!(ui_hebrew_fonts().len(), CRAFT_FONTS.iter().filter(|f| f.covers("Hebr")).count()); + assert_eq!(document_hebrew_font(false).is_some(), CRAFT_FONTS.iter().any(|f| f.covers("Hebr"))); + } + #[test] fn craft_fonts_are_optional_and_consistent() { // Holds with or without CRAFT_FONTS_DIR. diff --git a/crates/fonts/src/lib.rs b/crates/fonts/src/lib.rs index 75ebe6d3..672e0a6c 100644 --- a/crates/fonts/src/lib.rs +++ b/crates/fonts/src/lib.rs @@ -12,8 +12,8 @@ pub mod pdf; mod script; pub use arabic::{ShapedCluster, arabic_glyph, arabic_has, shape_arabic}; pub use craft::{ - CRAFT_FONTS, CraftFont, SHIPPORI_MINCHO, document_arabic_font, document_japanese_font, document_japanese_font_for_style, - document_japanese_fonts_for_style, ui_arabic_fonts, ui_chinese_fonts, ui_cjk_fonts, ui_japanese_fonts, ui_telugu_fonts, + CRAFT_FONTS, CraftFont, SHIPPORI_MINCHO, document_arabic_font, document_hebrew_font, document_japanese_font, document_japanese_font_for_style, + document_japanese_fonts_for_style, ui_arabic_fonts, ui_chinese_fonts, ui_cjk_fonts, ui_hebrew_fonts, ui_japanese_fonts, ui_telugu_fonts, }; pub use script::{GlyphError, GlyphOutline, INTER_REGULAR, MAX_SIGNATURE_CHARS, ScriptOutline, japanese_glyph, japanese_glyph_from, script_outline}; diff --git a/crates/ui-egui/src/theme.rs b/crates/ui-egui/src/theme.rs index 7d62a6b0..976854fc 100644 --- a/crates/ui-egui/src/theme.rs +++ b/crates/ui-egui/src/theme.rs @@ -204,8 +204,9 @@ pub fn installed_font_definitions(cjk: CjkPreference) -> FontDefinitions { } /// The interface fonts: Inter (and JetBrains Mono for code) first, then egui's defaults, then -/// the CJK, Arabic and Telugu faces of the optional craft-fonts build input as the last fallback -/// in every family. Without craft-fonts there is no Japanese, Chinese, Arabic or Telugu face here. +/// the CJK, Arabic, Telugu and Hebrew faces of the optional craft-fonts build input as the last +/// fallback in every family. Without craft-fonts there is no Japanese, Chinese, Arabic, Telugu or +/// Hebrew face here. pub fn font_definitions() -> FontDefinitions { font_definitions_for(false) } @@ -237,9 +238,10 @@ pub fn font_definitions_for(prefer_hans: bool) -> FontDefinitions { fonts.families.entry(family).or_default().push(name.clone()); } } - // Arabic-script faces (file names, document titles) and Telugu faces (the Telugu interface, - // file names) after the CJK ones; the ranges don't overlap. - for face in pdfcraft_fonts::ui_arabic_fonts().into_iter().chain(pdfcraft_fonts::ui_telugu_fonts()) { + // Arabic-script faces (file names, document titles), Telugu faces (the Telugu interface, + // file names) and Hebrew faces (file names, text being edited) after the CJK ones; the + // ranges don't overlap. + for face in pdfcraft_fonts::ui_arabic_fonts().into_iter().chain(pdfcraft_fonts::ui_telugu_fonts()).chain(pdfcraft_fonts::ui_hebrew_fonts()) { let name = face.name(); if !fonts.font_data.contains_key(&name) { add(&mut fonts, &name, face.bytes); diff --git a/docs/localization.md b/docs/localization.md index 5519e5bb..745ab538 100644 --- a/docs/localization.md +++ b/docs/localization.md @@ -23,6 +23,8 @@ Whole sentences that the layers below word — what the accessibility checker fo The Ukrainian catalog has 2,047 entries, written from the meaning of the English labels using Ukrainian computing vocabulary. Commands use infinitives; fixed-count messages use count-neutral wording. Plural-aware messages use the three [CLDR Ukrainian integer forms](https://www.unicode.org/cldr/charts/48/supplemental/language_plural_rules.html#uk): `1 сторінка`, `2 сторінки`, `5 сторінок`; 11–14 take the third form, while 21 and 22 return to the first and second. Calendar dates use genitive month names (`9 жовтня 2026`), while headings use standalone names (`Жовтень 2026`). Home recommendation cards grow to fit translated text; the toolbar search hint truncates before the shortcut, with the full label available on hover. +Edit text reads Hebrew lines in typing order and writes them back in drawing order (`crates/edit/src/bidi.rs`); letters the page's own font lacks are drawn with the craft-fonts `Hebr` face (Noto Sans Hebrew) as Type 3 glyphs, and a build without one says so. Hebrew points (niqqud) can't be written yet. The craft-fonts revision pinned for CI and releases has no `Hebr` face yet, so until one is added releases report that error instead of drawing the missing letters. + Japanese interface text uses BIZ UDPGothic from [craft-fonts](https://github.com/storytold/craft-fonts), an optional build input that every release includes (`CRAFT_FONTS_DIR`; see the README). A build made without it has no Japanese face, so Japanese labels show replacement boxes. Chinese is drawn with the same craft-fonts CJK faces (in Simplified Chinese the `Hans` faces come first, so a line never mixes faces with different baselines), so a character none of them has shows a replacement box (and every Chinese character does in a build without craft-fonts). Russian, Bulgarian, Czech, Brazilian Portuguese, German, Spanish, French, Hungarian, Italian and Turkish need only the bundled Latin faces (tested in `tests/fonts.rs`). Hungarian uses the same one/other plural rule as English: 1 is singular, and every other count — including 0 — is the other form. After a numeral the noun stays singular (`1 oldal`, `2 oldal`). Telugu is drawn with a craft-fonts `Telu` face (Noto Sans Telugu, kept in the web build too), whose conjuncts egui shapes; a build without one shows Telugu labels as replacement boxes, and the release-pinned craft-fonts input has no Telugu face yet. Japanese interface text uses BIZ UDPGothic from [craft-fonts](https://github.com/storytold/craft-fonts), an optional build input that every release includes (`CRAFT_FONTS_DIR`; see the README). A build made without it has no Japanese face, so Japanese labels show replacement boxes. Chinese is drawn with the same craft-fonts CJK faces (in Simplified Chinese the `Hans` faces come first, so a line never mixes faces with different baselines), so a character none of them has shows a replacement box (and every Chinese character does in a build without craft-fonts). Russian, Bulgarian, Czech, Brazilian Portuguese, German, Spanish, French, Hungarian, Italian and Slovenian need only the bundled Latin faces (tested in `tests/fonts.rs`). Slovenian has four plural forms: one (1, 101, 201…), two (2, 102…), few (3, 4, 103…) and other (everything else, including 0, 5–20 and 21+), so `@plural` entries list four forms (`1 stran`, `2 strani`, `3 strani`, `5 strani`). Hungarian uses the same one/other plural rule as English: 1 is singular, and every other count — including 0 — is the other form. After a numeral the noun stays singular (`1 oldal`, `2 oldal`). Telugu is drawn with a craft-fonts `Telu` face (Noto Sans Telugu, kept in the web build too), whose conjuncts egui shapes; a build without one shows Telugu labels as replacement boxes, and the release-pinned craft-fonts input has no Telugu face yet. Japanese interface text uses BIZ UDPGothic from [craft-fonts](https://github.com/storytold/craft-fonts), an optional build input that every release includes (`CRAFT_FONTS_DIR`; see the README). A build made without it has no Japanese face, so Japanese labels show replacement boxes. Chinese is drawn with the same craft-fonts CJK faces (in Simplified Chinese the `Hans` faces come first, so a line never mixes faces with different baselines), so a character none of them has shows a replacement box (and every Chinese character does in a build without craft-fonts). Until craft-fonts has an allowed `Hans` face, the desktop app draws a Chinese interface with a Chinese face already installed on the machine (Microsoft YaHei or JhengHei on Windows, PingFang, Hiragino Sans GB or STHeiti on macOS, WenQuanYi or Droid Sans Fallback on Linux; never Noto CJK or Source Han), placed before the Japanese faces so a line keeps one face (#826). The web build has no installed fonts to fall back on. Russian, Bulgarian, Czech, Brazilian Portuguese, German, Spanish, French, Hungarian and Italian need only the bundled Latin faces (tested in `tests/fonts.rs`). Hungarian uses the same one/other plural rule as English: 1 is singular, and every other count — including 0 — is the other form. After a numeral the noun stays singular (`1 oldal`, `2 oldal`). Telugu is drawn with a craft-fonts `Telu` face (Noto Sans Telugu, kept in the web build too), whose conjuncts egui shapes; a build without one shows Telugu labels as replacement boxes, and the release-pinned craft-fonts input has no Telugu face yet. diff --git a/parity/acrobat-features.toml b/parity/acrobat-features.toml index 090ba012..bb073ca2 100644 --- a/parity/acrobat-features.toml +++ b/parity/acrobat-features.toml @@ -1995,11 +1995,11 @@ title = "Edit right-to-left and CJK text" area = "C" tier = "P1" milestone = "M7" -status = "planned" +status = "partial" commands = [] -tools = [] -evidence = [] -notes = "" +tools = ["text_lines", "text_paragraphs", "text_edit"] +evidence = ["crates/edit/src/tests.rs::hebrew_lines_read_in_typing_order", "crates/edit/src/tests.rs::hebrew_edits_are_drawn_right_to_left_and_keep_their_right_edge", "crates/edit/src/tests.rs::hebrew_letters_the_page_font_lacks_use_the_hebrew_face", "crates/edit/src/bidi.rs::hebrew_sentences_numbers_latin_and_brackets_round_trip"] +notes = "Hebrew: lines and paragraphs read in typing order and are written back in drawing order, right-aligned by default; letters the page's font lacks use the craft-fonts Noto Sans Hebrew face as Type 3 glyphs. Japanese uses its Type 3 fallback. Missing: Hebrew points (niqqud) in replacement text, writing Arabic (shaping), right-to-left typing in the edit box (misc.rtl-ui), lines a producer draws word by word from the right, vertical CJK." [[feature]] id = "edit.rotated-text-editing" From d415749556badcc990705e13f49c700f925ada45 Mon Sep 17 00:00:00 2001 From: Maksim Popov <51298369+MaksPV@users.noreply.github.com> Date: Mon, 12 Oct 2026 00:42:48 +0300 Subject: [PATCH 7/9] Heavy pages: cull off-tile images, cache decodes, preview backdrops, calmer queues (#1117) * Heavy pages: cull off-tile images, cache decodes, preview backdrops, calmer queues A large CAD sheet (page 24 of the benchmark file: ~5050 pt wide, 2104 images, 9 MB of content) cost a full page interpret per tile: 4.3 s for a 1024 px tile at 1000%, 10.5 s for the whole page, and a storm of renders on every pan frame. Tiles now cost 0.6 s, the sheet is covered by a handful of large tiles, and gestures no longer queue work faster than workers finish it. - Vendored hayro patch (10): skip images whose stream-size box misses the canvas before decoding them, so a tile no longer decodes, resamples and rasterizes every image on its page. Pixel-identical; the skip regression test now covers images too. - Vendored hayro-interpret patch (23): decoded image pixels shared across renders through the interpreter cache, keyed by stream and decode target size within a 64 MiB budget. - Preview renders (hayro patch 11, RenderRequest::preview through to the device): backdrops, thumbnails, grid cells and signature-drag backgrounds skip images under 16 device px. Tiles, whole pages, print and export always draw everything. Previews may differ by those pixels; finals don't. - Viewer: zoom/pan settle holds apply only to pages whose raster proved slow (slow_pages, 500 ms), so fast pages render with no added latency; tile sides grow with zoom and page size (giant sheets need ~8 tiles, not ~28), queued center-first and keyed by exact coordinates; Edit-text outlines capped. - vendor/README.md records the new patches with their tests. Co-Authored-By: Muse Spark * Decoded-image cache: key every decode input, survive a poisoned lock The key was the dictionary's bytes and the target size, so two images with byte-identical dictionaries (uncompressed, one size) shared pixels, and an image drawn under a transfer function reused pixels decoded without it. DecodedImageKey now holds the dictionary's place in the document (its data follows it), its bytes and object, mask/stencil/raster output, interpolation, the target size and preview or final quality; images under a transfer function and inline images (named colour spaces, dictionaries in content streams) are not cached, and an image over the budget is never kept. A poisoned lock decodes uncached instead of panicking. Soft masks: the per-renderer cache can't carry a preview mask into a final render (documented). Regression tests for identical dictionaries, preview then final through one cache, and transfer functions. --------- Co-authored-by: Muse Spark Co-authored-by: Brandon Thomas --- apps/pdfcraft-cli/src/main.rs | 8 +- crates/automation/src/conventions.rs | 2 +- crates/automation/src/lib.rs | 11 +- crates/engine/tests/form_rotation.rs | 2 +- crates/render/src/raster.rs | 263 +++++++++++++++----- crates/render/src/raster/work_queue.rs | 5 +- crates/ui-egui/src/a11y_ui.rs | 9 +- crates/ui-egui/src/canvas.rs | 242 +++++++++++++++--- crates/ui-egui/src/edit_text_ui.rs | 52 ++-- crates/ui-egui/src/signature_drag.rs | 2 +- crates/ui-egui/src/zoom_snap.rs | 5 +- crates/ui-egui/tests/view.rs | 9 +- vendor/README.md | 6 +- vendor/hayro-interpret/src/cache.rs | 120 ++++++++- vendor/hayro-interpret/src/context.rs | 6 +- vendor/hayro-interpret/src/interpret/mod.rs | 14 ++ vendor/hayro-interpret/src/x_object.rs | 88 ++++++- vendor/hayro/src/lib.rs | 2 +- vendor/hayro/src/renderer.rs | 84 +++++-- 19 files changed, 778 insertions(+), 152 deletions(-) diff --git a/apps/pdfcraft-cli/src/main.rs b/apps/pdfcraft-cli/src/main.rs index f8861143..351cc67c 100644 --- a/apps/pdfcraft-cli/src/main.rs +++ b/apps/pdfcraft-cli/src/main.rs @@ -340,7 +340,7 @@ fn text(args: &[String]) -> Result<(), CliError> { }; let mut failed: Vec = Vec::new(); for (n, p) in pages.iter().enumerate() { - let out = r.render(RenderRequest { page: *p, kind: RequestKind::Text, tile: None, scale: 1.0, tag: 0 }); + let out = r.render(RenderRequest { page: *p, kind: RequestKind::Text, tile: None, scale: 1.0, tag: 0, preview: false }); if let Some(e) = out.error { let _ = writeln!(std::io::stderr().lock(), "page {}: {e}", p + 1); failed.push(p + 1); @@ -503,7 +503,7 @@ fn render(args: &[String]) -> Result<(), CliError> { read(path)?, RenderConfig { password: flag(args, "--password").map(Arc::from), reject_oversize: true, ..Default::default() }, ); - let p = r.render(RenderRequest { page: page_index, kind: RequestKind::Pixels, tile: None, scale: dpi / 72.0, tag: 0 }); + let p = r.render(RenderRequest { page: page_index, kind: RequestKind::Pixels, tile: None, scale: dpi / 72.0, tag: 0, preview: false }); if let Some(e) = p.error { return Err(e.into()); } @@ -541,8 +541,8 @@ fn check_one(args: &[String]) -> Result<(), CliError> { let mut r = PageRenderer::new(bytes, RenderConfig::default()); let mut failed = Vec::new(); for i in 0..info.pages.len().min(500) { - let p = r.render(RenderRequest { page: i, kind: RequestKind::Pixels, tile: None, scale: dpi / 72.0, tag: 0 }); - if let Some(e) = r.render(RenderRequest { page: i, kind: RequestKind::Text, tile: None, scale: 1.0, tag: 0 }).error { + let p = r.render(RenderRequest { page: i, kind: RequestKind::Pixels, tile: None, scale: dpi / 72.0, tag: 0, preview: false }); + if let Some(e) = r.render(RenderRequest { page: i, kind: RequestKind::Text, tile: None, scale: 1.0, tag: 0, preview: false }).error { failed.push(format!("p{} text: {e}", i + 1)); } if let Some(e) = p.error { diff --git a/crates/automation/src/conventions.rs b/crates/automation/src/conventions.rs index 62153082..af8cf671 100644 --- a/crates/automation/src/conventions.rs +++ b/crates/automation/src/conventions.rs @@ -114,7 +114,7 @@ impl Automation { return Err(failed("page has invalid dimensions")); } let scale = (max as f32 / side).min(1.0); - let out = self.renderer(id)?.render(RenderRequest { page, kind: RequestKind::Pixels, tile: None, scale, tag: 0 }); + let out = self.renderer(id)?.render(RenderRequest { page, kind: RequestKind::Pixels, tile: None, scale, tag: 0, preview: false }); if let Some(error) = out.error { return Err(failed(error)); } diff --git a/crates/automation/src/lib.rs b/crates/automation/src/lib.rs index 71602908..ab5d9ee1 100644 --- a/crates/automation/src/lib.rs +++ b/crates/automation/src/lib.rs @@ -1544,7 +1544,14 @@ impl Automation { if !(1.0..=MAX_DPI).contains(&dpi) { return Err(ToolError::InvalidArgs(format!("dpi must be between 1 and {MAX_DPI}"))); } - let out = self.renderer(id)?.render(RenderRequest { page, kind: RequestKind::Pixels, tile: None, scale: (dpi / 72.0) as f32, tag: 0 }); + let out = self.renderer(id)?.render(RenderRequest { + page, + kind: RequestKind::Pixels, + tile: None, + scale: (dpi / 72.0) as f32, + tag: 0, + preview: false, + }); if let Some(e) = out.error { return Err(failed(format!("page {} could not be rendered: {e}", page + 1))); } @@ -1994,7 +2001,7 @@ fn encode_png(width: u32, height: u32, premultiplied: &[u8]) -> Result> /// Extract the text of `pages` (0-based) with one renderer per worker thread. fn extract_parallel(bytes: &Arc>, password: Option>, pages: &[usize]) -> Vec<(usize, std::result::Result)> { let extract = |r: &mut PageRenderer, p: usize| { - let out = r.render(RenderRequest { page: p, kind: RequestKind::Text, tile: None, scale: 1.0, tag: 0 }); + let out = r.render(RenderRequest { page: p, kind: RequestKind::Text, tile: None, scale: 1.0, tag: 0, preview: false }); match out.error { Some(e) => Err(e), None => Ok(out.text.map(|t| (*t).clone()).unwrap_or_default()), diff --git a/crates/engine/tests/form_rotation.rs b/crates/engine/tests/form_rotation.rs index a125af7e..67d560bb 100644 --- a/crates/engine/tests/form_rotation.rs +++ b/crates/engine/tests/form_rotation.rs @@ -27,7 +27,7 @@ fn one_page() -> Document { fn ink_span(doc: &Document) -> (u32, u32) { let bytes = write_incremental(doc, &SaveOptions::default()).unwrap(); let mut r = PageRenderer::new(Arc::new(bytes), RenderConfig::default()); - let p = r.render(RenderRequest { page: 0, kind: RequestKind::Pixels, tile: None, scale: 2.0, tag: 0 }); + let p = r.render(RenderRequest { page: 0, kind: RequestKind::Pixels, tile: None, scale: 2.0, tag: 0, preview: false }); assert!(p.error.is_none(), "{:?}", p.error); let (mut x0, mut y0, mut x1, mut y1) = (u32::MAX, u32::MAX, 0, 0); let mut ink = false; diff --git a/crates/render/src/raster.rs b/crates/render/src/raster.rs index f05a8112..abb42086 100644 --- a/crates/render/src/raster.rs +++ b/crates/render/src/raster.rs @@ -156,6 +156,10 @@ pub struct RenderRequest { pub scale: f32, /// Caller-defined tag, echoed back (used for zoom generations / thumbnail vs page). pub tag: u64, + /// Low-detail render for previews (page backdrops, thumbnails): images covering only a + /// few device pixels are skipped. Never set it for a final render (tiles, whole pages, + /// print, export): previews may differ by those pixels. + pub preview: bool, } /// What a render that panicked or overran the watchdog is remembered by: its page and kind. Not @@ -331,7 +335,10 @@ fn render_page<'a>( // Rendered straight into a buffer of whole pixels, which the GUI takes over as is. let (w, h) = render_size(page, &rs); let mut pixels = Pixels::zeroed(w.into(), h.into()).ok_or_else(|| format!("page {} is too large to render", req.page + 1))?; - render_into(page, cache, settings, &rs, pixels.bytes_mut()); + // Previews render at low detail (tiny images are skipped); finals always draw all. + let mut preview_settings = settings.clone(); + preview_settings.preview = req.preview; + render_into(page, cache, &preview_settings, &rs, pixels.bytes_mut()); Ok((w.into(), h.into(), pixels, None)) })); match result { @@ -536,6 +543,8 @@ fn lock(m: &Mutex) -> std::sync::MutexGuard<'_, T> { #[cfg(not(target_arch = "wasm32"))] fn same_request(a: RenderRequest, b: RenderRequest) -> bool { + // `preview` is excluded on purpose: it names the same work for cancellation, only the + // detail level of the result differs. a.page == b.page && a.kind == b.kind && a.tile == b.tile && a.scale.to_bits() == b.scale.to_bits() && a.tag == b.tag } @@ -1889,7 +1898,7 @@ mod tests { ) .into_bytes(); let mut renderer = PageRenderer::new(Arc::new(bytes), RenderConfig::default()); - let page = renderer.render(RenderRequest { page: 0, kind: RequestKind::Pixels, tile: None, scale: 1.0, tag: 0 }); + let page = renderer.render(RenderRequest { page: 0, kind: RequestKind::Pixels, tile: None, scale: 1.0, tag: 0, ..Default::default() }); assert!(page.error.is_none(), "{:?}", page.error); assert_eq!((page.width, page.height), (20, 20)); let (pixels, remainder) = page.rgba.as_chunks::<4>(); @@ -1907,7 +1916,7 @@ mod tests { pool.set_stuck_after(std::time::Duration::ZERO); let gate = render_gate(0, false); *lock(&pool.shared.render_gates) = vec![gate.clone()]; - let req = |page| RenderRequest { page, kind: RequestKind::Pixels, tile: None, scale: 0.5, tag: 0 }; + let req = |page| RenderRequest { page, kind: RequestKind::Pixels, tile: None, scale: 0.5, tag: 0, ..Default::default() }; pool.set_queue(vec![req(0)]); gate.entered.wait(); let first = pool.try_recv().expect("the watchdog answers while the render is held"); @@ -1934,7 +1943,7 @@ mod tests { let pool = RenderPool::new(Arc::new(ONE_PAGE_TWICE.to_vec()), 2, RenderConfig::default()); let gate = render_gate(0, true); *lock(&pool.shared.render_gates) = vec![gate.clone()]; - let req = |page| RenderRequest { page, kind: RequestKind::Pixels, tile: None, scale: 0.5, tag: 0 }; + let req = |page| RenderRequest { page, kind: RequestKind::Pixels, tile: None, scale: 0.5, tag: 0, ..Default::default() }; pool.set_queue(vec![req(0)]); gate.entered.wait(); // What the canvas sends next: the page it is still waiting for, then another one. @@ -2056,8 +2065,9 @@ mod tests { } pdf += "trailer << /Root 1 0 R >>\n%%EOF"; let pool = RenderPool::new(Arc::new(pdf.into_bytes()), 3, RenderConfig::default()); - let wanted: Vec = - (0..pages).map(|page| RenderRequest { page, kind: RequestKind::Pixels, tile: None, scale: 2.0, tag: 2000 }).collect(); + let wanted: Vec = (0..pages) + .map(|page| RenderRequest { page, kind: RequestKind::Pixels, tile: None, scale: 2.0, tag: 2000, ..Default::default() }) + .collect(); // The canvas's loop: take what arrived, then queue every page still missing whenever // that list changes (requests already rendering included). let mut got = vec![0; pages]; @@ -2460,7 +2470,7 @@ trailer << /Root 1 0 R >> #[test] fn renders_a_blue_rectangle() { let pool = RenderPool::new(Arc::new(ONE_PAGE.to_vec()), 1, RenderConfig::default()); - pool.set_queue(vec![RenderRequest { page: 0, kind: RequestKind::Pixels, tile: None, scale: 1.0, tag: 7 }]); + pool.set_queue(vec![RenderRequest { page: 0, kind: RequestKind::Pixels, tile: None, scale: 1.0, tag: 7, ..Default::default() }]); let page = loop { if let Some(p) = pool.try_recv() { break p; @@ -2517,7 +2527,7 @@ trailer << /Root 1 0 R >> #[test] fn pixels_are_a_plain_renders_bytes_in_a_buffer_a_gui_can_take_over() { let mut r = PageRenderer::new(Arc::new(ONE_PAGE.to_vec()), RenderConfig::default()); - let out = r.render(RenderRequest { page: 0, kind: RequestKind::Pixels, tile: None, scale: 2.0, tag: 0 }); + let out = r.render(RenderRequest { page: 0, kind: RequestKind::Pixels, tile: None, scale: 2.0, tag: 0, ..Default::default() }); assert!(out.error.is_none(), "{:?}", out.error); // The same bytes as hayro's own render to a pixmap, with the same settings. let pdf = Pdf::new(Arc::new(ONE_PAGE.to_vec())).expect("parses"); @@ -2551,10 +2561,10 @@ trailer << /Root 1 0 R >> #[test] fn missing_page_and_garbage_input_fail_gracefully() { let mut r = PageRenderer::new(Arc::new(ONE_PAGE.to_vec()), RenderConfig::default()); - assert!(r.render(RenderRequest { page: 9, kind: RequestKind::Pixels, tile: None, scale: 1.0, tag: 0 }).error.is_some()); + assert!(r.render(RenderRequest { page: 9, kind: RequestKind::Pixels, tile: None, scale: 1.0, tag: 0, ..Default::default() }).error.is_some()); let mut g = PageRenderer::new(Arc::new(b"not a pdf at all".to_vec()), RenderConfig::default()); assert_eq!(g.page_count(), 0); - assert!(g.render(RenderRequest { page: 0, kind: RequestKind::Pixels, tile: None, scale: 1.0, tag: 0 }).error.is_some()); + assert!(g.render(RenderRequest { page: 0, kind: RequestKind::Pixels, tile: None, scale: 1.0, tag: 0, ..Default::default() }).error.is_some()); } #[test] @@ -2598,7 +2608,7 @@ endstream endobj trailer << /Root 1 0 R >> %%EOF"; let mut r = PageRenderer::new(Arc::new(pdf.to_vec()), RenderConfig::default()); - let req = RenderRequest { page: 0, kind: RequestKind::Pixels, tile: None, scale: 1.0, tag: 0 }; + let req = RenderRequest { page: 0, kind: RequestKind::Pixels, tile: None, scale: 1.0, tag: 0, ..Default::default() }; let p = r.render(req); assert!(p.error.is_none(), "{:?}", p.error); assert_eq!((p.width, p.height), (596, 842)); @@ -2641,7 +2651,7 @@ endstream endobj trailer << /Root 1 0 R >> %%EOF"; let mut r = PageRenderer::new(Arc::new(pdf.to_vec()), RenderConfig::default()); - let p = r.render(RenderRequest { page: 0, kind: RequestKind::Pixels, tile: None, scale: 1.0, tag: 0 }); + let p = r.render(RenderRequest { page: 0, kind: RequestKind::Pixels, tile: None, scale: 1.0, tag: 0, ..Default::default() }); assert!(p.error.is_none(), "{:?}", p.error); let px = |x: u32, y: u32| p.rgba[((y * p.width + x) * 4) as usize..][..4].to_vec(); assert_eq!(px(20, 80), vec![255, 0, 0, 255], "checkbox must show its /Yes state"); @@ -2676,7 +2686,7 @@ endobj trailer << /Root 1 0 R >> %%EOF"; let mut r = PageRenderer::new(Arc::new(pdf.to_vec()), RenderConfig::default()); - let p = r.render(RenderRequest { page: 0, kind: RequestKind::Pixels, tile: None, scale: 1.0, tag: 0 }); + let p = r.render(RenderRequest { page: 0, kind: RequestKind::Pixels, tile: None, scale: 1.0, tag: 0, ..Default::default() }); assert!(p.error.is_none(), "{:?}", p.error); let px = |x: u32, y: u32| p.rgba[((y * p.width + x) * 4) as usize..][..4].to_vec(); assert_eq!(px(20, 80), vec![255, 0, 0, 255], "checkbox must show the /Yes appearance, not the decoy stream"); @@ -2756,7 +2766,7 @@ trailer << /Root 1 0 R >> }; let render = |bytes: Vec| { let mut r = PageRenderer::new(Arc::new(bytes), RenderConfig::default()); - r.render(RenderRequest { page: 0, kind: RequestKind::Pixels, tile: None, scale: 1.0, tag: 0 }) + r.render(RenderRequest { page: 0, kind: RequestKind::Pixels, tile: None, scale: 1.0, tag: 0, ..Default::default() }) }; for (case, form) in [ ("zero-width /BBox", "/BBox [0 0 0 20]"), @@ -2808,7 +2818,7 @@ trailer << /Root 1 0 R >> content.len() ); let mut r = PageRenderer::new(Arc::new(pdf.into_bytes()), RenderConfig::default()); - let p = r.render(RenderRequest { page: 0, kind: RequestKind::Pixels, tile: None, scale: 1.0, tag: 0 }); + let p = r.render(RenderRequest { page: 0, kind: RequestKind::Pixels, tile: None, scale: 1.0, tag: 0, ..Default::default() }); assert!(p.error.is_none(), "{form}: {:?}", p.error); // The bounding box of everything drawn, in device pixels. let (mut x0, mut y0, mut x1, mut y1) = (u32::MAX, u32::MAX, 0, 0); @@ -2861,7 +2871,7 @@ trailer << /Root 1 0 R >> content.len() ); let mut r = PageRenderer::new(Arc::new(pdf.into_bytes()), config); - let p = r.render(RenderRequest { page: 0, kind: RequestKind::Pixels, tile: None, scale: 1.0, tag: 0 }); + let p = r.render(RenderRequest { page: 0, kind: RequestKind::Pixels, tile: None, scale: 1.0, tag: 0, ..Default::default() }); assert!(p.error.is_none(), "{annot}: {:?}", p.error); p }; @@ -2932,7 +2942,7 @@ trailer << /Root 1 0 R >> %%EOF"; let render = |hide: bool| { let mut r = PageRenderer::new(Arc::new(pdf.to_vec()), RenderConfig { hide_comments: hide, ..RenderConfig::default() }); - r.render(RenderRequest { page: 0, kind: RequestKind::Pixels, tile: None, scale: 1.0, tag: 0 }) + r.render(RenderRequest { page: 0, kind: RequestKind::Pixels, tile: None, scale: 1.0, tag: 0, ..Default::default() }) }; let px = |p: &RenderedPage, x: u32, y: u32| p.rgba[((y * p.width + x) * 4) as usize..][..4].to_vec(); let shown = render(false); @@ -2948,8 +2958,8 @@ trailer << /Root 1 0 R >> assert!(pool.is_inline()); assert!(pool.try_recv().is_none()); pool.set_queue(vec![ - RenderRequest { page: 0, kind: RequestKind::Pixels, tile: None, scale: 1.0, tag: 1 }, - RenderRequest { page: 0, kind: RequestKind::Pixels, tile: None, scale: 0.5, tag: 2 }, + RenderRequest { page: 0, kind: RequestKind::Pixels, tile: None, scale: 1.0, tag: 1, ..Default::default() }, + RenderRequest { page: 0, kind: RequestKind::Pixels, tile: None, scale: 0.5, tag: 2, ..Default::default() }, ]); assert_eq!(pool.try_recv().map(|p| p.request.tag), Some(1)); assert_eq!(pool.try_recv().map(|p| (p.request.tag, p.width)), Some((2, 50))); @@ -2998,7 +3008,7 @@ trailer << /Root 1 0 R >> pool.set_stuck_after(std::time::Duration::ZERO); let gate = render_gate(0, false); *lock(&pool.shared.render_gates) = vec![gate.clone()]; - pool.set_queue(vec![RenderRequest { page: 0, kind: RequestKind::Pixels, tile: None, scale: 0.5, tag: 0 }]); + pool.set_queue(vec![RenderRequest { page: 0, kind: RequestKind::Pixels, tile: None, scale: 0.5, tag: 0, ..Default::default() }]); gate.entered.wait(); let first = pool.try_recv().expect("the watchdog answers for the held render"); assert!(first.error.as_deref().is_some_and(|e| e.contains("took longer")), "{:?}", first.error); @@ -3023,7 +3033,7 @@ endstream endobj trailer << /Root 1 0 R >> %%EOF"; let mut r = PageRenderer::new(Arc::new(pdf.to_vec()), RenderConfig::default()); - let out = r.render(RenderRequest { page: 0, kind: RequestKind::Text, tile: None, scale: 1.0, tag: 0 }); + let out = r.render(RenderRequest { page: 0, kind: RequestKind::Text, tile: None, scale: 1.0, tag: 0, ..Default::default() }); assert!(out.error.is_none(), "{:?}", out.error); let t = out.text.expect("text"); assert_eq!(t.plain_text(), "Hello World\nSecond"); @@ -3047,7 +3057,7 @@ trailer << /Root 1 0 R >> %%EOF"; let px = |cfg: RenderConfig| { let mut r = PageRenderer::new(Arc::new(pdf.to_vec()), cfg); - let p = r.render(RenderRequest { page: 0, kind: RequestKind::Pixels, tile: None, scale: 0.5, tag: 0 }); + let p = r.render(RenderRequest { page: 0, kind: RequestKind::Pixels, tile: None, scale: 0.5, tag: 0, ..Default::default() }); p.rgba[..4].to_vec() }; assert_eq!(px(RenderConfig::default()), vec![255, 0, 0, 255], "layer is on by default"); @@ -3079,7 +3089,7 @@ trailer << /Root 1 0 R >> }; let render = |doc: String| { let mut r = PageRenderer::new(Arc::new(doc.into_bytes()), RenderConfig::default()); - r.render(RenderRequest { page: 0, kind: RequestKind::Pixels, tile: None, scale: 1.0, tag: 0 }) + r.render(RenderRequest { page: 0, kind: RequestKind::Pixels, tile: None, scale: 1.0, tag: 0, ..Default::default() }) }; let looped = render(pdf("/Pattern cs /P1 scn 0 0 10 10 re f")); assert_eq!((looped.width, looped.height), (40, 40)); @@ -3158,7 +3168,7 @@ trailer << /Root 1 0 R >> }; let render = |doc: String| { let mut r = PageRenderer::new(Arc::new(doc.into_bytes()), RenderConfig::default()); - r.render(RenderRequest { page: 0, kind: RequestKind::Pixels, tile: None, scale: 1.0, tag: 0 }) + r.render(RenderRequest { page: 0, kind: RequestKind::Pixels, tile: None, scale: 1.0, tag: 0, ..Default::default() }) }; let looped = render(pdf("1 0 d0 BT /F1 1 Tf (A) Tj ET 0 0 1 1 re f")); assert_eq!((looped.width, looped.height), (40, 40)); @@ -3197,7 +3207,7 @@ trailer << /Root 1 0 R >> content.len() ); let mut r = PageRenderer::new(Arc::new(pdf.into_bytes()), RenderConfig::default()); - let page = r.render(RenderRequest { page: 0, kind: RequestKind::Pixels, tile: None, scale: 1.0, tag: 0 }); + let page = r.render(RenderRequest { page: 0, kind: RequestKind::Pixels, tile: None, scale: 1.0, tag: 0, ..Default::default() }); assert!(page.error.is_none(), "{:?}", page.error); assert_eq!(&page.rgba[((38 * 40 + 1) * 4)..][..4], &[255, 0, 0, 255]); } @@ -3289,7 +3299,7 @@ trailer << /Root 1 0 R >> fn jpx_32_bit_image_does_not_prevent_other_page_content_from_rendering() { for bpc in [8, 32] { let mut renderer = PageRenderer::new(Arc::new(jpx_midpoint_pdf(bpc, 1)), RenderConfig::default()); - let page = renderer.render(RenderRequest { page: 0, kind: RequestKind::Pixels, tile: None, scale: 1.0, tag: 0 }); + let page = renderer.render(RenderRequest { page: 0, kind: RequestKind::Pixels, tile: None, scale: 1.0, tag: 0, ..Default::default() }); assert!(page.error.is_none(), "{bpc} bits: {:?}", page.error); assert_eq!(&page.rgba[((38 * 40 + 1) * 4)..][..4], &[255, 0, 0, 255], "other page content draws"); if bpc == 8 { @@ -3327,7 +3337,7 @@ trailer << /Root 1 0 R >> let (tx, rx) = std::sync::mpsc::channel(); std::thread::spawn(move || { let mut r = PageRenderer::new(Arc::new(pdf.into_bytes()), RenderConfig::default()); - let _ = tx.send(r.render(RenderRequest { page: 0, kind: RequestKind::Pixels, tile: None, scale: 1.0, tag: 0 })); + let _ = tx.send(r.render(RenderRequest { page: 0, kind: RequestKind::Pixels, tile: None, scale: 1.0, tag: 0, ..Default::default() })); }); let page = rx.recv_timeout(std::time::Duration::from_secs(20)).expect("an absurd image must not stall the renderer"); assert!(page.error.is_none(), "{:?}", page.error); @@ -3366,7 +3376,7 @@ trailer << /Root 1 0 R >> let (tx, rx) = std::sync::mpsc::channel(); std::thread::spawn(move || { let mut r = PageRenderer::new(Arc::new(pdf.into_bytes()), RenderConfig::default()); - let _ = tx.send(r.render(RenderRequest { page: 0, kind: RequestKind::Pixels, tile: None, scale: 1.0, tag: 0 })); + let _ = tx.send(r.render(RenderRequest { page: 0, kind: RequestKind::Pixels, tile: None, scale: 1.0, tag: 0, ..Default::default() })); }); let page = rx.recv_timeout(std::time::Duration::from_secs(20)).expect("must not stall the renderer"); assert!(page.error.is_none(), "{:?}", page.error); @@ -3404,7 +3414,7 @@ trailer << /Root 1 0 R >> let (tx, rx) = std::sync::mpsc::channel(); std::thread::spawn(move || { let mut r = PageRenderer::new(Arc::new(pdf.into_bytes()), RenderConfig::default()); - let _ = tx.send(r.render(RenderRequest { page: 0, kind: RequestKind::Pixels, tile: None, scale: 1.0, tag: 0 })); + let _ = tx.send(r.render(RenderRequest { page: 0, kind: RequestKind::Pixels, tile: None, scale: 1.0, tag: 0, ..Default::default() })); }); let page = rx.recv_timeout(std::time::Duration::from_secs(20)).expect("must not stall the renderer"); assert!(page.error.is_none(), "{:?}", page.error); @@ -3509,7 +3519,7 @@ trailer << /Root 1 0 R >> let (tx, rx) = std::sync::mpsc::channel(); std::thread::spawn(move || { let mut r = PageRenderer::new(Arc::new(pdf.as_bytes().to_vec()), RenderConfig::default()); - let _ = tx.send(r.render(RenderRequest { page: 0, kind: RequestKind::Pixels, tile: None, scale: 1.0, tag: 0 })); + let _ = tx.send(r.render(RenderRequest { page: 0, kind: RequestKind::Pixels, tile: None, scale: 1.0, tag: 0, ..Default::default() })); }); let page = rx.recv_timeout(std::time::Duration::from_secs(20)).expect("a self-multiplying Type 3 glyph must not stall the renderer"); assert!(page.error.is_none(), "{:?}", page.error); @@ -3799,7 +3809,7 @@ trailer << /Root 1 0 R >> let (tx, rx) = std::sync::mpsc::channel(); std::thread::spawn(move || { let mut r = PageRenderer::new(Arc::new(pdf), RenderConfig::default()); - let _ = tx.send(r.render(RenderRequest { page: 0, kind: RequestKind::Pixels, tile: None, scale: 1.0, tag: 0 })); + let _ = tx.send(r.render(RenderRequest { page: 0, kind: RequestKind::Pixels, tile: None, scale: 1.0, tag: 0, ..Default::default() })); }); let page = rx.recv_timeout(std::time::Duration::from_secs(20)).expect("a huge JBIG2 region must not stall the renderer"); assert!(page.error.is_none(), "{:?}", page.error); @@ -3860,7 +3870,7 @@ trailer << /Root 1 0 R >> let (tx, rx) = std::sync::mpsc::channel(); std::thread::spawn(move || { let mut r = PageRenderer::new(Arc::new(pdf), RenderConfig::default()); - let _ = tx.send(r.render(RenderRequest { page: 0, kind: RequestKind::Pixels, tile: None, scale: 1.0, tag: 0 })); + let _ = tx.send(r.render(RenderRequest { page: 0, kind: RequestKind::Pixels, tile: None, scale: 1.0, tag: 0, ..Default::default() })); }); let page = rx .recv_timeout(std::time::Duration::from_secs(20)) @@ -3902,7 +3912,7 @@ trailer << /Root 1 0 R >> let (tx, rx) = std::sync::mpsc::channel(); std::thread::spawn(move || { let mut r = PageRenderer::new(Arc::new(pdf), RenderConfig::default()); - let _ = tx.send(r.render(RenderRequest { page: 0, kind: RequestKind::Pixels, tile: None, scale: 1.0, tag: 0 })); + let _ = tx.send(r.render(RenderRequest { page: 0, kind: RequestKind::Pixels, tile: None, scale: 1.0, tag: 0, preview: false })); }); let page = rx.recv_timeout(std::time::Duration::from_secs(120)).expect("a large page must render"); assert!(page.error.is_none(), "{:?}", page.error); @@ -3921,7 +3931,7 @@ trailer << /Root 1 0 R >> let (tx, rx) = std::sync::mpsc::channel(); std::thread::spawn(move || { let mut r = PageRenderer::new(Arc::new(pdf), RenderConfig::default()); - let _ = tx.send(r.render(RenderRequest { page: 0, kind: RequestKind::Pixels, tile: None, scale: 1.0, tag: 0 })); + let _ = tx.send(r.render(RenderRequest { page: 0, kind: RequestKind::Pixels, tile: None, scale: 1.0, tag: 0, preview: false })); }); let page = rx.recv_timeout(std::time::Duration::from_secs(60)).expect("an inline image must not stall the renderer"); assert!(page.error.is_none(), "{:?}", page.error); @@ -3953,7 +3963,7 @@ trailer << /Root 1 0 R >> let (tx, rx) = std::sync::mpsc::channel(); std::thread::spawn(move || { let mut r = PageRenderer::new(Arc::new(pdf), RenderConfig::default()); - let _ = tx.send(r.render(RenderRequest { page: 0, kind: RequestKind::Pixels, tile: None, scale: 1.0, tag: 0 })); + let _ = tx.send(r.render(RenderRequest { page: 0, kind: RequestKind::Pixels, tile: None, scale: 1.0, tag: 0, preview: false })); }); let page = rx.recv_timeout(std::time::Duration::from_secs(60)).expect("a content bomb must not stall the renderer"); assert!(page.error.is_none(), "{:?}", page.error); @@ -4217,7 +4227,7 @@ trailer << /Root 1 0 R >> let (tx, rx) = std::sync::mpsc::channel(); std::thread::spawn(move || { let mut r = PageRenderer::new(Arc::new(pdf), RenderConfig::default()); - let _ = tx.send(r.render(RenderRequest { page: 0, kind: RequestKind::Pixels, tile: None, scale: 1.0, tag: 0 })); + let _ = tx.send(r.render(RenderRequest { page: 0, kind: RequestKind::Pixels, tile: None, scale: 1.0, tag: 0, ..Default::default() })); }); let page = rx.recv_timeout(std::time::Duration::from_secs(60)).expect("a self-painting form must not stall the renderer"); assert!(page.error.is_none(), "{:?}", page.error); @@ -4249,7 +4259,7 @@ trailer << /Root 1 0 R >> pdf.extend_from_slice(&form); pdf.extend_from_slice(b"\nendstream endobj\ntrailer << /Root 1 0 R >>\n%%EOF\n"); let mut r = PageRenderer::new(Arc::new(pdf), RenderConfig::default()); - let page = r.render(RenderRequest { page: 0, kind: RequestKind::Pixels, tile: None, scale: 1.0, tag: 0 }); + let page = r.render(RenderRequest { page: 0, kind: RequestKind::Pixels, tile: None, scale: 1.0, tag: 0, ..Default::default() }); assert!(page.error.is_none(), "{:?}", page.error); // The last paint is at user (30, 30)–(34, 34): device row 40 - 32 = 8. assert_eq!(&page.rgba[((8 * 40 + 32) * 4)..][..4], &[255, 0, 0, 255], "the 300th paint is drawn"); @@ -4277,7 +4287,7 @@ trailer << /Root 1 0 R >> let (tx, rx) = std::sync::mpsc::channel(); std::thread::spawn(move || { let mut r = PageRenderer::new(Arc::new(pdf), RenderConfig::default()); - let _ = tx.send(r.render(RenderRequest { page: 0, kind: RequestKind::Pixels, tile: None, scale: 1.0, tag: 0 })); + let _ = tx.send(r.render(RenderRequest { page: 0, kind: RequestKind::Pixels, tile: None, scale: 1.0, tag: 0, ..Default::default() })); }); let page = rx.recv_timeout(std::time::Duration::from_secs(60)).expect("a sampled-function bomb must not stall the renderer"); assert!(page.error.is_none(), "{:?}", page.error); @@ -4302,7 +4312,7 @@ trailer << /Root 1 0 R >> let (tx, rx) = std::sync::mpsc::channel(); std::thread::spawn(move || { let mut r = PageRenderer::new(Arc::new(pdf), RenderConfig::default()); - let _ = tx.send(r.render(RenderRequest { page: 0, kind: RequestKind::Pixels, tile: None, scale: 1.0, tag: 0 })); + let _ = tx.send(r.render(RenderRequest { page: 0, kind: RequestKind::Pixels, tile: None, scale: 1.0, tag: 0, ..Default::default() })); }); let page = rx.recv_timeout(std::time::Duration::from_secs(60)).expect("a mesh shading must not stall the renderer"); assert!(page.error.is_none(), "{:?}", page.error); @@ -4370,7 +4380,7 @@ trailer << /Root 1 0 R >> ", ); let mut r = PageRenderer::new(Arc::new(pdf), RenderConfig::default()); - let page = r.render(RenderRequest { page: 0, kind: RequestKind::Pixels, tile: None, scale: 1.0, tag: 0 }); + let page = r.render(RenderRequest { page: 0, kind: RequestKind::Pixels, tile: None, scale: 1.0, tag: 0, ..Default::default() }); assert!(page.error.is_none(), "{:?}", page.error); let middle = &page.rgba[((28 * 40 + 12) * 4)..][..4]; assert!(middle != [255, 255, 255, 255] && middle[3] == 255, "a small patch is shaded: {middle:?}"); @@ -4419,7 +4429,7 @@ trailer << /Root 1 0 R >> let (tx, rx) = std::sync::mpsc::channel(); std::thread::spawn(move || { let mut r = PageRenderer::new(Arc::new(pdf), RenderConfig::default()); - let _ = tx.send(r.render(RenderRequest { page: 0, kind: RequestKind::Pixels, tile: None, scale: 25.0, tag: 0 })); + let _ = tx.send(r.render(RenderRequest { page: 0, kind: RequestKind::Pixels, tile: None, scale: 25.0, tag: 0, ..Default::default() })); }); let page = rx.recv_timeout(std::time::Duration::from_secs(60)).expect("overlapping mesh triangles must not stall the renderer"); assert!(page.error.is_none(), "{:?}", page.error); @@ -4447,7 +4457,7 @@ trailer << /Root 1 0 R >> let (tx, rx) = std::sync::mpsc::channel(); std::thread::spawn(move || { let mut r = PageRenderer::new(Arc::new(pdf), RenderConfig::default()); - let _ = tx.send(r.render(RenderRequest { page: 0, kind: RequestKind::Pixels, tile: None, scale: 1.0, tag: 0 })); + let _ = tx.send(r.render(RenderRequest { page: 0, kind: RequestKind::Pixels, tile: None, scale: 1.0, tag: 0, ..Default::default() })); }); let page = rx.recv_timeout(std::time::Duration::from_secs(60)).expect("a mesh shading must not stall the renderer"); assert!(page.error.is_none(), "{:?}", page.error); @@ -4494,7 +4504,7 @@ trailer << /Root 1 0 R >> let (tx, rx) = std::sync::mpsc::channel(); std::thread::spawn(move || { let mut r = PageRenderer::new(Arc::new(pdf), RenderConfig::default()); - let _ = tx.send(r.render(RenderRequest { page: 0, kind: RequestKind::Pixels, tile: None, scale: 1.0, tag: 0 })); + let _ = tx.send(r.render(RenderRequest { page: 0, kind: RequestKind::Pixels, tile: None, scale: 1.0, tag: 0, ..Default::default() })); }); let page = rx.recv_timeout(std::time::Duration::from_secs(20)).expect("a Type 1 font must not stall the renderer"); assert!(page.error.is_none(), "{:?}", page.error); @@ -4521,7 +4531,7 @@ trailer << /Root 1 0 R >> let (tx, rx) = std::sync::mpsc::channel(); std::thread::spawn(move || { let mut r = PageRenderer::new(Arc::new(pdf.into_bytes()), RenderConfig::default()); - let _ = tx.send(r.render(RenderRequest { page: 0, kind: RequestKind::Pixels, tile: None, scale: 1.0, tag: 0 })); + let _ = tx.send(r.render(RenderRequest { page: 0, kind: RequestKind::Pixels, tile: None, scale: 1.0, tag: 0, ..Default::default() })); }); let page = rx.recv_timeout(std::time::Duration::from_secs(20)).expect("an absurd image must not hang the renderer"); assert!(page.error.is_none(), "{:?}", page.error); @@ -4723,8 +4733,8 @@ trailer << /Root 1 0 R >> let (tx, rx) = std::sync::mpsc::channel(); std::thread::spawn(move || { let mut r = PageRenderer::new(Arc::new(pdf.as_bytes().to_vec()), RenderConfig::default()); - let px = r.render(RenderRequest { page: 0, kind: RequestKind::Pixels, tile: None, scale: 1.0, tag: 0 }); - let text = r.render(RenderRequest { page: 0, kind: RequestKind::Text, tile: None, scale: 1.0, tag: 0 }); + let px = r.render(RenderRequest { page: 0, kind: RequestKind::Pixels, tile: None, scale: 1.0, tag: 0, ..Default::default() }); + let text = r.render(RenderRequest { page: 0, kind: RequestKind::Text, tile: None, scale: 1.0, tag: 0, ..Default::default() }); let _ = tx.send((px.error, text.error)); }); let (px, text) = rx.recv_timeout(std::time::Duration::from_secs(20)).expect("rendering a huge /W range must not hang"); @@ -4764,7 +4774,7 @@ trailer << /Root 1 0 R >> fn malformed_simple_font_widths_keep_later_widths_and_the_font() { let render = |widths: &str| { let mut r = PageRenderer::new(Arc::new(type3_boxes(widths)), RenderConfig::default()); - let p = r.render(RenderRequest { page: 0, kind: RequestKind::Pixels, tile: None, scale: 1.0, tag: 0 }); + let p = r.render(RenderRequest { page: 0, kind: RequestKind::Pixels, tile: None, scale: 1.0, tag: 0, ..Default::default() }); assert!(p.error.is_none(), "{widths}: {:?}", p.error); p }; @@ -4795,8 +4805,8 @@ trailer << /Root 1 0 R >> let (tx, rx) = std::sync::mpsc::channel(); std::thread::spawn(move || { let mut r = PageRenderer::new(Arc::new(type3_boxes("/FirstChar 4294967295 /LastChar 4294967295 /Widths [600]")), RenderConfig::default()); - let px = r.render(RenderRequest { page: 0, kind: RequestKind::Pixels, tile: None, scale: 1.0, tag: 0 }); - let text = r.render(RenderRequest { page: 0, kind: RequestKind::Text, tile: None, scale: 1.0, tag: 0 }); + let px = r.render(RenderRequest { page: 0, kind: RequestKind::Pixels, tile: None, scale: 1.0, tag: 0, ..Default::default() }); + let text = r.render(RenderRequest { page: 0, kind: RequestKind::Text, tile: None, scale: 1.0, tag: 0, ..Default::default() }); let _ = tx.send((px, text.error)); }); let (px, text) = rx.recv_timeout(std::time::Duration::from_secs(20)).expect("a huge /FirstChar must not exhaust memory"); @@ -4831,7 +4841,7 @@ trailer << /Root 1 0 R >> }; let render = |bytes: Vec, scale: f32| { let mut r = PageRenderer::new(Arc::new(bytes), RenderConfig::default()); - let p = r.render(RenderRequest { page: 0, kind: RequestKind::Pixels, tile: None, scale, tag: 0 }); + let p = r.render(RenderRequest { page: 0, kind: RequestKind::Pixels, tile: None, scale, tag: 0, ..Default::default() }); assert!(p.error.is_none(), "{:?}", p.error); p }; @@ -4877,9 +4887,9 @@ trailer << /Root 1 0 R >> #[test] fn tiles_match_full_render() { let mut r = PageRenderer::new(Arc::new(ONE_PAGE.to_vec()), RenderConfig::default()); - let full = r.render(RenderRequest { page: 0, kind: RequestKind::Pixels, tile: None, scale: 2.0, tag: 0 }); + let full = r.render(RenderRequest { page: 0, kind: RequestKind::Pixels, tile: None, scale: 2.0, tag: 0, ..Default::default() }); let tile = Tile { x: 30, y: 40, w: 50, h: 30 }; - let part = r.render(RenderRequest { page: 0, kind: RequestKind::Pixels, tile: Some(tile), scale: 2.0, tag: 0 }); + let part = r.render(RenderRequest { page: 0, kind: RequestKind::Pixels, tile: Some(tile), scale: 2.0, tag: 0, ..Default::default() }); assert_eq!((part.width, part.height), (50, 30)); for y in 0..30u32 { for x in 0..50u32 { @@ -4943,6 +4953,9 @@ trailer << /Root 1 0 R >> /// what it was without skipping, where paint reaches past a path into the next tile: round /// and square caps, a sharp miter, a rotated and a stretched matrix, a hairline, dashes, a /// curve, text, and a page shown rotated. + /// + /// Images are skipped the same way (vendored patch (10)): an image on a tile, one crossing + /// tile lines, and one fully off the page. #[test] fn skipping_paths_off_the_canvas_changes_no_pixel() { // Tiles of 80 device pixels at scale 2 meet every 40 pt; each mark crosses such a line @@ -4957,20 +4970,29 @@ q 0.7071 0.7071 -0.7071 0.7071 160 20 cm 15 w 1 J 0 0 m 20 0 l S Q 0 1 0 RG 3 w 10 200 m 10 290 70 290 70 200 c S BT /F1 30 Tf 70 205 Td (Hg) Tj ET q 4 0 0 0.25 0 0 cm 12 w 1 J 2 500 m 8 500 l S Q -300 300 m 320 320 l S"; +300 300 m 320 320 l S +q 10 0 0 10 5 5 cm /Im0 Do Q +q 30 0 0 30 100 100 cm /Im0 Do Q +q 5 0 0 5 500 500 cm /Im0 Do Q"; + // A 4x4 RGB image of plain `A` bytes (no `endstream` inside, so the raw stream parses). + let pixels = "A".repeat(4 * 4 * 3); for rotate in [0, 90] { let pdf = format!( "%PDF-1.4 1 0 obj << /Type /Catalog /Pages 2 0 R >> endobj 2 0 obj << /Type /Pages /Kids [3 0 R] /Count 1 >> endobj -3 0 obj << /Type /Page /Parent 2 0 R /MediaBox [0 0 240 240] /Rotate {rotate} /Contents 4 0 R /Resources << /Font << /F1 5 0 R >> >> >> endobj +3 0 obj << /Type /Page /Parent 2 0 R /MediaBox [0 0 240 240] /Rotate {rotate} /Contents 4 0 R /Resources << /Font << /F1 5 0 R >> /XObject << /Im0 6 0 R >> >> >> endobj 4 0 obj << /Length {} >> stream {content} endstream endobj 5 0 obj << /Type /Font /Subtype /Type1 /BaseFont /Helvetica >> endobj +6 0 obj << /Type /XObject /Subtype /Image /Width 4 /Height 4 /ColorSpace /DeviceRGB /BitsPerComponent 8 /Length {} >> stream +{pixels} +endstream endobj trailer << /Root 1 0 R >> %%EOF", - content.len() + content.len(), + pixels.len() ); let mut r = PageRenderer::new(Arc::new(pdf.into_bytes()), RenderConfig::default()); let mut both = |tile| { @@ -4991,6 +5013,125 @@ trailer << /Root 1 0 R >> } } + /// Previews skip images that would cover only a few device pixels (vendored patch (11)): + /// a tiny draw is missing from the preview but present in the final, while a large draw + /// is in both. Finals never set the flag, so they draw everything. + #[test] + fn preview_renders_skip_tiny_images_but_finals_draw_everything() { + let content = "q 50 0 0 50 100 100 cm /Im0 Do Q +q 0.5 0 0 0.5 10 10 cm /Im0 Do Q"; + let pixels = "A".repeat(4 * 4 * 3); + let pdf = format!( + "%PDF-1.4 +1 0 obj << /Type /Catalog /Pages 2 0 R >> endobj +2 0 obj << /Type /Pages /Kids [3 0 R] /Count 1 >> endobj +3 0 obj << /Type /Page /Parent 2 0 R /MediaBox [0 0 240 240] /Contents 4 0 R /Resources << /XObject << /Im0 5 0 R >> >> >> endobj +4 0 obj << /Length {} >> stream +{content} +endstream endobj +5 0 obj << /Type /XObject /Subtype /Image /Width 4 /Height 4 /ColorSpace /DeviceRGB /BitsPerComponent 8 /Length {} >> stream +{pixels} +endstream endobj +trailer << /Root 1 0 R >> +%%EOF", + content.len(), + pixels.len() + ); + let mut r = PageRenderer::new(Arc::new(pdf.into_bytes()), RenderConfig::default()); + let mut render = |preview| r.render(RenderRequest { page: 0, kind: RequestKind::Pixels, tile: None, scale: 1.0, tag: 0, preview }); + let first = render(false); + let second = render(false); + assert!(first.error.is_none(), "{:?}", first.error); + assert_eq!(first.rgba, second.rgba, "finals are deterministic"); + let gray = first.rgba.as_chunks::<4>().0.iter().filter(|px| px[0] == 0x41 && px[1] == 0x41 && px[2] == 0x41).count(); + assert!(gray > 1000, "the large image draws {gray} pixels"); + let preview = render(true); + assert!(preview.error.is_none(), "{:?}", preview.error); + assert_ne!(preview.rgba, first.rgba, "the tiny image is skipped only in the preview"); + } + + /// A page whose stream objects 5, 6, … are `objects` (dictionary, data), drawn by `content` + /// with `resources`, on a 130 × 70 pt page. + fn image_cache_pdf(content: &str, resources: &str, objects: &[(&str, &[u8])]) -> Vec { + let mut pdf = format!( + "%PDF-1.4 +1 0 obj << /Type /Catalog /Pages 2 0 R >> endobj +2 0 obj << /Type /Pages /Kids [3 0 R] /Count 1 >> endobj +3 0 obj << /Type /Page /Parent 2 0 R /MediaBox [0 0 130 70] /Contents 4 0 R /Resources {resources} >> endobj +4 0 obj << /Length {} >> stream +{content} +endstream endobj +", + content.len() + ) + .into_bytes(); + for (i, (dict, data)) in objects.iter().enumerate() { + pdf.extend(format!("{} 0 obj {dict} stream\n", i + 5).bytes()); + pdf.extend_from_slice(data); + pdf.extend_from_slice(b"\nendstream endobj\n"); + } + pdf.extend_from_slice(b"trailer << /Root 1 0 R >>\n%%EOF"); + pdf + } + + /// The RGB of the pixel at (`x`, `y`) (from the top left) of a `width`-pixel-wide RGBA raster. + fn rgb_at(rgba: &[u8], width: usize, x: usize, y: usize) -> [u8; 3] { + let i = (y * width + x) * 4; + [rgba[i], rgba[i + 1], rgba[i + 2]] + } + + const ONE_PIXEL_RGB: &str = "<< /Type /XObject /Subtype /Image /Width 1 /Height 1 /ColorSpace /DeviceRGB /BitsPerComponent 8 /Length 3 >>"; + + /// Two images whose dictionaries are byte for byte the same (uncompressed, one size) but + /// whose pixels differ each draw their own: the decoded-image cache tells them apart by the + /// stream, not by the dictionary's bytes alone, in one render and across renders sharing a + /// cache, and previews never hand their entries to a final render. + #[test] + fn decoded_images_with_identical_dictionaries_keep_their_own_pixels() { + let pdf = image_cache_pdf( + // B over A at the same place and size (so one decode target): B must show. A alone + // on the right, at a size of its own. + "q 50 0 0 50 10 10 cm /A Do Q q 50 0 0 50 10 10 cm /B Do Q q 40 0 0 40 80 15 cm /A Do Q", + "<< /XObject << /A 5 0 R /B 6 0 R >> >>", + &[(ONE_PIXEL_RGB, b"\xff\x00\x00"), (ONE_PIXEL_RGB, b"\x00\x00\xff")], + ); + let req = |preview| RenderRequest { page: 0, kind: RequestKind::Pixels, tile: None, scale: 1.0, tag: 0, preview }; + let mut r = PageRenderer::new(Arc::new(pdf.clone()), RenderConfig::default()); + let out = r.render(req(false)); + assert!(out.error.is_none(), "{:?}", out.error); + let w = out.width as usize; + assert_eq!(rgb_at(&out.rgba, w, 35, 35), [0, 0, 255], "the second image drew the first one's pixels"); + assert_eq!(rgb_at(&out.rgba, w, 100, 35), [255, 0, 0]); + // One cache across a preview and then a final render, as a render worker keeps it. + let pdf = parse(&Arc::new(pdf), None).expect("parses"); + let cache = RenderCache::new(); + let settings = InterpreterSettings::default(); + let preview = render_page(&pdf, &cache, &settings, false, req(true)).expect("preview renders"); + let fin = render_page(&pdf, &cache, &settings, false, req(false)).expect("final renders"); + let fresh = render_page(&pdf, &RenderCache::new(), &settings, false, req(false)).expect("final renders"); + assert_eq!(rgb_at(&preview.2, w, 35, 35), [0, 0, 255]); + assert_eq!(&fin.2[..], &fresh.2[..], "a final render after a preview matches one with a fresh cache"); + } + + /// An image drawn under a transfer function (`/TR`) is decoded with it, even when the same + /// image was drawn without one just before (and the other way round). + #[test] + fn decoded_images_under_a_transfer_function_are_not_shared() { + let pdf = image_cache_pdf( + // The image, then the same image at the same place under the inverting function + // (one decode target), then the image alone on the right. + "q 50 0 0 50 10 10 cm /A Do Q q /Inv gs 50 0 0 50 10 10 cm /A Do Q q 40 0 0 40 80 15 cm /A Do Q", + "<< /XObject << /A 5 0 R >> /ExtGState << /Inv << /TR 6 0 R >> >> >>", + &[(ONE_PIXEL_RGB, b"\xff\x00\x00"), ("<< /FunctionType 4 /Domain [0 1] /Range [0 1] /Length 14 >>", b"{ 1 exch sub }")], + ); + let mut r = PageRenderer::new(Arc::new(pdf), RenderConfig::default()); + let out = r.render(RenderRequest { page: 0, kind: RequestKind::Pixels, tile: None, scale: 1.0, tag: 0, preview: false }); + assert!(out.error.is_none(), "{:?}", out.error); + let w = out.width as usize; + assert_eq!(rgb_at(&out.rgba, w, 35, 35), [0, 255, 255], "the transfer function was not applied to a cached image"); + assert_eq!(rgb_at(&out.rgba, w, 100, 35), [255, 0, 0]); + } + /// A Japanese CID font that isn't embedded (Adobe-Japan1, as `HeiseiMin-W3` with /// `UniJIS-UCS2-H` in #260's test file) drew nothing: hayro's substitutes for fonts that /// aren't embedded are Latin-only. With craft-fonts (the build input release builds embed), @@ -5018,7 +5159,7 @@ trailer << /Root 1 0 R >> }; for base_font in ["HeiseiMin-W3", "HeiseiKakuGo-W5"] { let mut r = PageRenderer::new(Arc::new(pdf(base_font).into_bytes()), RenderConfig::default()); - let p = r.render(RenderRequest { page: 0, kind: RequestKind::Pixels, tile: None, scale: 1.0, tag: 0 }); + let p = r.render(RenderRequest { page: 0, kind: RequestKind::Pixels, tile: None, scale: 1.0, tag: 0, ..Default::default() }); assert!(p.error.is_none(), "{base_font}: {:?}", p.error); let inked = p.rgba.as_chunks::<4>().0.iter().filter(|c| c[0] < 128).count(); if pdfcraft_fonts::document_japanese_font().is_none() { @@ -5082,7 +5223,7 @@ trailer << /Root 1 0 R >> content.len() ); let mut r = PageRenderer::new(Arc::new(pdf.into_bytes()), RenderConfig::default()); - let p = r.render(RenderRequest { page: 0, kind: RequestKind::Pixels, tile: None, scale: 1.0, tag: 0 }); + let p = r.render(RenderRequest { page: 0, kind: RequestKind::Pixels, tile: None, scale: 1.0, tag: 0, ..Default::default() }); assert!(p.error.is_none(), "{base_font} <{code}>: {:?}", p.error); p.rgba }; @@ -5156,7 +5297,7 @@ trailer << /Root 1 0 R >> cmap.len() ); let mut r = PageRenderer::new(Arc::new(pdf.into_bytes()), RenderConfig::default()); - let p = r.render(RenderRequest { page: 0, kind: RequestKind::Pixels, tile: None, scale: 1.0, tag: 0 }); + let p = r.render(RenderRequest { page: 0, kind: RequestKind::Pixels, tile: None, scale: 1.0, tag: 0, ..Default::default() }); assert!(p.error.is_none(), "{base_font} <{code}>: {:?}", p.error); p.rgba }; @@ -5242,7 +5383,7 @@ endstream endobj }; let render = |bytes: Vec| { let mut r = PageRenderer::new(Arc::new(bytes), RenderConfig::default()); - let p = r.render(RenderRequest { page: 0, kind: RequestKind::Pixels, tile: None, scale: 1.0, tag: 0 }); + let p = r.render(RenderRequest { page: 0, kind: RequestKind::Pixels, tile: None, scale: 1.0, tag: 0, preview: false }); assert!(p.error.is_none(), "{:?}", p.error); p }; @@ -5344,7 +5485,7 @@ endstream endobj trailer << /Root 1 0 R >> %%EOF"; let mut r = PageRenderer::new(Arc::new(pdf.to_vec()), RenderConfig::default()); - let p = r.render(RenderRequest { page: 0, kind: RequestKind::Pixels, tile: None, scale: 1.0, tag: 0 }); + let p = r.render(RenderRequest { page: 0, kind: RequestKind::Pixels, tile: None, scale: 1.0, tag: 0, ..Default::default() }); let px = |x: u32, y: u32| p.rgba[((y * p.width + x) * 4) as usize..][..4].to_vec(); assert_eq!(px(25, 50), vec![255, 0, 0, 255], "inside the mask"); assert_eq!(px(75, 50), vec![255, 255, 255, 255], "outside the mask"); @@ -5371,7 +5512,7 @@ trailer << /Root 1 0 R >> content.len() ); let mut r = PageRenderer::new(Arc::new(pdf.into_bytes()), RenderConfig::default()); - let p = r.render(RenderRequest { page: 0, kind: RequestKind::Pixels, tile: None, scale: 1.0, tag: 0 }); + let p = r.render(RenderRequest { page: 0, kind: RequestKind::Pixels, tile: None, scale: 1.0, tag: 0, ..Default::default() }); assert!(p.error.is_none(), "{content}: {:?}", p.error); p }; diff --git a/crates/render/src/raster/work_queue.rs b/crates/render/src/raster/work_queue.rs index eaa0cbab..eb1d05b4 100644 --- a/crates/render/src/raster/work_queue.rs +++ b/crates/render/src/raster/work_queue.rs @@ -19,17 +19,18 @@ struct Key { tile: Option, scale: u32, tag: u64, + preview: bool, } impl From for Key { fn from(r: RenderRequest) -> Self { - Self { page: r.page, kind: r.kind, tile: r.tile, scale: r.scale.to_bits(), tag: r.tag } + Self { page: r.page, kind: r.kind, tile: r.tile, scale: r.scale.to_bits(), tag: r.tag, preview: r.preview } } } impl Key { fn request(self) -> RenderRequest { - RenderRequest { page: self.page, kind: self.kind, tile: self.tile, scale: f32::from_bits(self.scale), tag: self.tag } + RenderRequest { page: self.page, kind: self.kind, tile: self.tile, scale: f32::from_bits(self.scale), tag: self.tag, preview: self.preview } } } diff --git a/crates/ui-egui/src/a11y_ui.rs b/crates/ui-egui/src/a11y_ui.rs index da88e8e7..57a7ce86 100644 --- a/crates/ui-egui/src/a11y_ui.rs +++ b/crates/ui-egui/src/a11y_ui.rs @@ -445,7 +445,14 @@ fn figure_picture(ctx: &egui::Context, doc: &pdfcraft_engine::Document, f: &pdfc }; let config = pdfcraft_render::RenderConfig { password: doc.password.as_deref().map(std::sync::Arc::from), ..Default::default() }; let mut r = pdfcraft_render::PageRenderer::new(doc.bytes.clone(), config); - let out = r.render(pdfcraft_render::RenderRequest { page, kind: pdfcraft_render::RequestKind::Pixels, tile: Some(tile), scale, tag: 0 }); + let out = r.render(pdfcraft_render::RenderRequest { + page, + kind: pdfcraft_render::RequestKind::Pixels, + tile: Some(tile), + scale, + tag: 0, + preview: false, + }); if out.error.is_some() || out.width == 0 { return None; } diff --git a/crates/ui-egui/src/canvas.rs b/crates/ui-egui/src/canvas.rs index c5b939bf..3306b6d3 100644 --- a/crates/ui-egui/src/canvas.rs +++ b/crates/ui-egui/src/canvas.rs @@ -29,6 +29,8 @@ const GRID_TAG: u64 = 1 << 61; const STALE_TAG: u64 = u64::MAX; /// Pages whose raster would exceed this many device pixels on a side are drawn in tiles. const TILE_THRESHOLD: f32 = 4096.0; +/// Base device-pixel side of a page tile. The actual side grows with the zoom +/// ([`tile_side`]), so a zoomed viewport is covered by a few tiles instead of many. const TILE: u32 = 1024; /// Longest side of the backdrop drawn under tiles, and of a live page preview (signature drag). pub(crate) const BASE_SIDE: f32 = 2048.0; @@ -450,6 +452,23 @@ pub struct DocView { /// Sharp renders of the pages the grid shows larger than a thumbnail: only those in view, /// at the size drawn (device pixels wide). grid_pages: HashMap, + /// Zoom-settle debounce for heavy pages: the last requested device scale, and the egui + /// time until which sharp whole-page/tile re-renders are held back. While the zoom is + /// still moving, every frame would otherwise cancel the in-flight full-page interpret + /// (seconds on image-heavy CAD plots) and start it over, so a sharp raster never + /// arrives. The stale raster stays on screen stretched meanwhile. + last_scale: f32, + sharp_hold_until: f64, + /// Pan-settle debounce for tiled pages: the scroll offset last frame, and the egui time + /// until which sharp tile re-renders are held back. Each tile costs a full page + /// interpret, so a drag would otherwise queue a storm of them (one per frame) while the + /// in-flight ones cannot be cancelled mid-operator. Older tiles and the backdrop stay + /// stretched meanwhile; fresh tiles go out once the pan settles. + last_scroll: Vec2, + pan_hold_until: f64, + /// Pages whose raster proved slow (see `SLOW_MILLIS`): only these get the zoom/pan + /// settle holds. Fast pages render straight through, with no added latency. + slow_pages: HashSet, } /// The page grid's zoom range and the step of its buttons and keys. @@ -618,6 +637,11 @@ impl DocView { grid_zoom_request: None, grid_anchor: None, grid_pages: HashMap::new(), + last_scale: 0.0, + sharp_hold_until: 0.0, + last_scroll: Vec2::ZERO, + pan_hold_until: 0.0, + slow_pages: HashSet::new(), comments: Default::default(), measure: Default::default(), forms: Default::default(), @@ -764,6 +788,7 @@ impl DocView { self.texts.clear(); self.text_failed.clear(); self.errors.clear(); + self.slow_pages.clear(); self.waiting_since.clear(); self.last_queue.clear(); self.selection = None; @@ -797,6 +822,7 @@ impl DocView { self.stale_tiles.insert(key); } } + self.slow_pages.remove(&page); self.texts.remove(&page); self.text_failed.remove(&page); self.errors.remove(&page); @@ -1316,7 +1342,15 @@ impl DocView { .filter_map(|(page, _)| { let p = info.pages.get(page)?; let scale = thumbnail_scale(p.width, p.height, density); - Some(RenderRequest { page, kind: RequestKind::Pixels, tile: None, scale, tag: THUMB_TAG | u64::from(scale.to_bits()) }) + Some(RenderRequest { + page, + kind: RequestKind::Pixels, + tile: None, + scale, + tag: THUMB_TAG | u64::from(scale.to_bits()), + // Thumbnails are previews: tiny images are skipped for them. + preview: true, + }) }) .collect() } @@ -1349,7 +1383,9 @@ impl DocView { // decide how much prefetch fits beside it. let visible = self.frame_visible.contains(&req.page); if let Some(tile) = req.tile { - let key = (req.page, req.tag, tile.x / TILE, tile.y / TILE); + // Keyed by exact device-pixel origin: the tile side varies with the zoom + // (`tile_side`), so grid indices would need the scale to decode. + let key = (req.page, req.tag, tile.x, tile.y); let bytes = rgba_bytes(tile.w as usize, tile.h as usize); if (!visible && tile_bytes.saturating_add(bytes) > TILE_BYTES) || !tiles.insert(key) { continue; @@ -1510,6 +1546,11 @@ impl DocView { self.errors.insert(r.request.page, e); return 0; } + // Remember heavy pages: only these get the zoom/pan settle holds, so fast pages + // never pay added latency. Stale entries die with the tiles on edits. + if r.millis >= SLOW_MILLIS { + self.slow_pages.insert(r.request.page); + } let page = r.request.page; let bytes = rgba_bytes(r.width as usize, r.height as usize); if bytes != r.rgba.len() || !self.admit_texture(r.request, bytes) { @@ -1518,7 +1559,7 @@ impl DocView { // The length was checked above, so the renderer's buffer becomes the texture data as is. let img = texture_image([r.width as usize, r.height as usize], r.rgba); if let Some(t) = r.request.tile { - let key = (page, r.request.tag, t.x / TILE, t.y / TILE); + let key = (page, r.request.tag, t.x, t.y); let tex = ctx.load_texture(format!("tile-{:?}-{page}-{}-{}", self.id, t.x, t.y), img, TextureOptions::LINEAR); self.tiles.insert(key, tex); self.stale_tiles.remove(&key); @@ -1559,7 +1600,7 @@ impl DocView { return true; } if let Some(t) = req.tile { - let key = (req.page, req.tag, t.x / TILE, t.y / TILE); + let key = (req.page, req.tag, t.x, t.y); self.tiles.remove(&key); let retained = self.tiles.values().fold(0usize, |n, tex| n.saturating_add(texture_bytes(tex))); visible || bytes <= TILE_BYTES.saturating_sub(retained) @@ -1670,7 +1711,7 @@ impl DocView { let tag = scale_tag(scale) | PRINT_TAG; let fresh = self.print_pages.get(&page).is_some_and(|(have, _)| *have == tag); if !fresh { - self.frame_queue.push(RenderRequest { page, kind: RequestKind::Pixels, tile: None, scale, tag }); + self.frame_queue.push(RenderRequest { page, kind: RequestKind::Pixels, tile: None, scale, tag, preview: false }); } } } @@ -1686,9 +1727,28 @@ fn tag_scale(tag: u64) -> f32 { (tag as f64 / 65536.0) as f32 } +/// Device-pixel side of a page tile at `scale` for a page `page_max_px` device pixels on +/// its long side. Each tile costs a full page interpret, so tiles grow with the zoom and +/// with the page: a zoomed viewport, or a giant sheet shown whole, is covered by a few +/// large tiles instead of dozens of small ones. Bounded by the render backend (tile sides +/// above 4096 are clamped there) and kept granular at ordinary zooms. +fn tile_side(scale: f32, page_max_px: u32) -> u32 { + if !scale.is_finite() || scale <= 0.0 { + return TILE; + } + let by_zoom = (TILE as f32 * (scale / 6.0)).clamp(TILE as f32, 4096.0) as u32; + // At most four tiles along the long side, so a giant sheet needs a handful of + // interprets instead of dozens. + by_zoom.max(page_max_px.div_ceil(4).min(4096)) +} + /// Tiles of earlier zooms kept for a page while its current tiles are rendered. const OLD_TILES: usize = 64; +/// A page whose raster took at least this long is heavy: zoom/pan holds apply to it, +/// so gestures stay on stretched surfaces instead of queueing full-page interprets. +const SLOW_MILLIS: u32 = 500; + /// `r` moved so its corner lies on a whole physical pixel, so that a raster drawn from there /// maps texel for texel onto the screen. fn snap_to_pixels(r: Rect, ppp: f32) -> Rect { @@ -2159,6 +2219,18 @@ pub fn document_area(app: &mut PdfCraftApp, index: usize, ui: &mut egui::Ui) { let ppp = ui.ctx().pixels_per_point(); let scale = view.render_scale(ppp); let tag = scale_tag(scale); + // Hold sharp re-renders while the zoom is still moving: a new scale every frame would + // cancel the in-flight full-page interpret and restart it, so on a heavy page (a CAD + // plot with ~1000 images renders for seconds) the sharp raster never arrives and the + // view looks stuck on a soft image. The stale raster stays stretched meanwhile; the + // sharp request goes out once the zoom settles. + let now_scale = ui.input(|i| i.time); + if (scale - view.last_scale).abs() > f32::EPSILON { + view.last_scale = scale; + view.sharp_hold_until = now_scale + 0.35; + ui.ctx().request_repaint_after(std::time::Duration::from_millis(380)); + } + let sharp_held = now_scale < view.sharp_hold_until; view.current_scale_tag = tag; let hand = app.quick_tool == QuickTool::Hand; let tool = app.quick_tool; @@ -2368,6 +2440,11 @@ pub fn document_area(app: &mut PdfCraftApp, index: usize, ui: &mut egui::Ui) { // (found by mapping the visible screen corners back into the page). Sides round // up as whole-page rasters do, so the last partial row and column get tiles. let (dw, dh) = (device_pixels(pw_pt, scale), device_pixels(ph_pt, scale)); + // Adaptive tile side for this zoom and page, within the GPU texture limit: + // a zoomed viewport or a giant sheet is covered by a few large tiles + // instead of many small ones, and each tile costs one full page interpret. + // Never zero (u32 division below). + let ts = tile_side(scale, dw.max(dh)).min(max_side as u32).max(64); let vis = r.intersect(visible); let corners = [vis.left_top(), vis.right_top(), vis.right_bottom(), vis.left_bottom()].map(|c| xf.screen_to_norm(c)); let (u0, u1) = corners.iter().fold((1.0f32, 0.0f32), |(a, b), c| (a.min(c.0), b.max(c.0))); @@ -2380,34 +2457,46 @@ pub fn document_area(app: &mut PdfCraftApp, index: usize, ui: &mut egui::Ui) { .tiles .iter() .filter(|((p, t, _, _), _)| *p == i && *t != tag) - .map(|((_, t, tx, ty), tex)| (tag_scale(*t), *tx, *ty, tex)) + .map(|((_, t, ox, oy), tex)| (tag_scale(*t), *ox, *oy, tex)) .collect(); older.sort_by(|a, b| (b.0 / scale).ln().abs().total_cmp(&(a.0 / scale).ln().abs())); - for (s, tx, ty, tex) in older { + for (s, ox, oy, tex) in older { let (ow, oh) = (device_pixels(pw_pt, s) as f32, device_pixels(ph_pt, s) as f32); let [w, h] = tex.size(); - let (x, y) = ((tx * TILE) as f32, (ty * TILE) as f32); + let (x, y) = (ox as f32, oy as f32); xf.paint_image(painter, tex.id(), x / ow, y / oh, (x + w as f32) / ow, (y + h as f32) / oh); } - for ty in vy0 / TILE..=(vy1.saturating_sub(1)) / TILE { - for tx in vx0 / TILE..=(vx1.saturating_sub(1)) / TILE { - let (x, y) = (tx * TILE, ty * TILE); - let (w, h) = (TILE.min(dw.saturating_sub(x)), TILE.min(dh.saturating_sub(y))); - if w == 0 || h == 0 { - continue; - } - wanted.push((i, scale, tag, Some(Tile { x, y, w, h }))); - if let Some(tex) = view.tiles.get(&(i, tag, tx, ty)) { - let (fw, fh) = (dw as f32, dh as f32); - xf.texel_aligned([dw as usize, dh as usize], ppp).paint_image( - painter, - tex.id(), - x as f32 / fw, - y as f32 / fh, - (x + w) as f32 / fw, - (y + h) as f32 / fh, - ); - } + // Visible tiles, nearest the viewport centre first: each tile costs a full + // page interpret, so the workers sharpen what is looked at before the edges. + let (vcx, vcy) = ((vx0 + vx1) as f32 / 2.0, (vy0 + vy1) as f32 / 2.0); + let mut coords: Vec<(u32, u32)> = Vec::new(); + for ty in vy0 / ts..=(vy1.saturating_sub(1)) / ts { + for tx in vx0 / ts..=(vx1.saturating_sub(1)) / ts { + coords.push((tx, ty)); + } + } + coords.sort_by(|a, b| { + let da = ((a.0 * ts + ts / 2) as f32 - vcx).abs() + ((a.1 * ts + ts / 2) as f32 - vcy).abs(); + let db = ((b.0 * ts + ts / 2) as f32 - vcx).abs() + ((b.1 * ts + ts / 2) as f32 - vcy).abs(); + da.total_cmp(&db) + }); + for (tx, ty) in coords { + let (x, y) = (tx * ts, ty * ts); + let (w, h) = (ts.min(dw.saturating_sub(x)), ts.min(dh.saturating_sub(y))); + if w == 0 || h == 0 { + continue; + } + wanted.push((i, scale, tag, Some(Tile { x, y, w, h }))); + if let Some(tex) = view.tiles.get(&(i, tag, x, y)) { + let (fw, fh) = (dw as f32, dh as f32); + xf.texel_aligned([dw as usize, dh as usize], ppp).paint_image( + painter, + tex.id(), + x as f32 / fw, + y as f32 / fh, + (x + w) as f32 / fw, + (y + h) as f32 / fh, + ); } } } @@ -2904,6 +2993,19 @@ pub fn document_area(app: &mut PdfCraftApp, index: usize, ui: &mut egui::Ui) { view.auto_scroll.paint(ui, avail); // Collect all main-page demand, including cached surfaces, for byte admission later. + // Pan-settle debounce for tiled pages: each tile costs a full page interpret, so holding + // fresh tiles back while the scroll offset is still moving keeps a drag on the stretched + // backdrop/older tiles instead of queueing a storm of interprets (one per frame) that + // the workers cannot cancel mid-operator. Fresh tiles go out once the pan settles; + // pages with nothing on screen yet still queue. + let scroll_offset = out.state.offset; + let now_settle = ui.input(|i| i.time); + if (scroll_offset - view.last_scroll).length() > 2.0 { + view.pan_hold_until = now_settle + 0.25; + ui.ctx().request_repaint_after(std::time::Duration::from_millis(280)); + } + view.last_scroll = scroll_offset; + let pan_held = now_settle < view.pan_hold_until; let (mut wanted, visible_now) = out.inner; let cur = view.current; wanted.sort_by_key(|w| (!visible_now.contains(&w.0), w.0.abs_diff(cur), w.3.is_some())); @@ -2919,8 +3021,51 @@ pub fn document_area(app: &mut PdfCraftApp, index: usize, ui: &mut egui::Ui) { view.tiles.remove(&k); } } - let mut queue: Vec = - wanted.iter().map(|&(page, scale, tag, tile)| RenderRequest { page, kind: RequestKind::Pixels, tile, scale, tag }).collect(); + let mut queue: Vec = wanted + .iter() + .map(|&(page, wanted_scale, wanted_tag, tile)| RenderRequest { + page, + kind: RequestKind::Pixels, + tile, + scale: wanted_scale, + tag: wanted_tag, + // The low-res backdrop under tiles is a preview: tiny images are skipped for it. + // It is the whole-page request whose scale is not the current one; sharp tiles + // and whole-page finals always draw everything. + preview: tile.is_none() && (wanted_scale != scale || wanted_tag != tag), + }) + .collect(); + // While the zoom or pan is settling, hold back sharp re-renders of pages that already + // show something (a stale whole-page raster or older tiles, drawn stretched): each new + // scale would cancel the running full-page interpret and restart it from scratch, and + // each pan frame would queue a storm of full-page tile interprets. + // Pages with nothing on screen yet still queue, so panning to a fresh page draws it. + if sharp_held || pan_held { + let main_tag = tag; + queue.retain(|req| { + // Fast pages render straight through: the hold only protects heavy pages + // whose full-page interpret costs seconds. + if !view.slow_pages.contains(&req.page) { + return true; + } + if req.tile.is_some() { + let has_current_tile = view.tiles.keys().any(|(p, t, _, _)| *p == req.page && *t == req.tag); + if has_current_tile { + return false; + } + let has_any_surface = view.pages.contains_key(&req.page) || view.tiles.keys().any(|(p, _, _, _)| *p == req.page); + !has_any_surface + } else if req.tag != main_tag { + // The low-res backdrop under tiles is zoom-independent: let it through, + // it is the stretched surface the hold relies on. + true + } else { + // The stale raster stays stretched until the zoom or pan settles; a page with + // nothing on screen yet still renders. + !view.pages.contains_key(&req.page) + } + }); + } // Text layers: visible pages for selection, every page while a search is active. let need_text = |p: &usize| !view.texts.contains_key(p) && !view.text_failed.contains(p); let mut text_pages: Vec = if hand { Vec::new() } else { visible_now.iter().copied().filter(need_text).collect() }; @@ -2929,7 +3074,14 @@ pub fn document_area(app: &mut PdfCraftApp, index: usize, ui: &mut egui::Ui) { let rest: Vec = (0..n).map(|k| (cur + k) % n).filter(need_text).filter(|p| !text_pages.contains(p)).collect(); text_pages.extend(rest); } - queue.extend(text_pages.into_iter().map(|page| RenderRequest { page, kind: RequestKind::Text, tile: None, scale: 1.0, tag: TEXT_TAG })); + queue.extend(text_pages.into_iter().map(|page| RenderRequest { + page, + kind: RequestKind::Text, + tile: None, + scale: 1.0, + tag: TEXT_TAG, + preview: false, + })); view.frame_queue = queue; view.frame_visible = visible_now.iter().copied().collect(); @@ -3861,7 +4013,15 @@ fn organize_grid(view: &mut DocView, info: &DocInfo, editable: bool, dirty: bool if !thumb_ok && ui.is_rect_visible(c) { in_view.push((i, want)); if !sharp.is_some_and(|(w, _)| sharp_enough(*w, want)) && !view.errors.contains_key(&i) { - sharper.push(RenderRequest { page: i, kind: RequestKind::Pixels, tile: None, scale: want / p.width.max(1.0), tag: GRID_TAG }); + sharper.push(RenderRequest { + page: i, + kind: RequestKind::Pixels, + tile: None, + scale: want / p.width.max(1.0), + tag: GRID_TAG, + // Grid cells are previews: tiny images are skipped for them. + preview: true, + }); } } if let Some(tex) = sharp.map(|(_, t)| t).or(thumb) { @@ -4078,7 +4238,7 @@ endstream endobj trailer << /Root 1 0 R >> %%EOF"; let mut r = pdfcraft_render::PageRenderer::new(Arc::new(pdf.to_vec()), Default::default()); - let out = r.render(RenderRequest { page: 0, kind: RequestKind::Pixels, tile: None, scale: 1.5, tag: 0 }); + let out = r.render(RenderRequest { page: 0, kind: RequestKind::Pixels, tile: None, scale: 1.5, tag: 0, preview: false }); assert!(out.error.is_none(), "{:?}", out.error); let size = [out.width as usize, out.height as usize]; let copied = egui::ColorImage::from_rgba_premultiplied(size, &out.rgba); @@ -4088,6 +4248,24 @@ trailer << /Root 1 0 R >> assert_eq!(img.pixels.as_ptr().cast::(), at, "the renderer's own buffer"); } + #[test] + fn tiles_stay_granular_up_close_and_cover_giant_sheets_in_a_handful() { + // Ordinary zooms keep 1024 px tiles; degenerate scales fall back to it, never zero. + assert_eq!(tile_side(1.0, 2000), TILE); + assert_eq!(tile_side(4.0, 2000), TILE); + assert_eq!(tile_side(f32::NAN, 2000), TILE); + assert_eq!(tile_side(f32::INFINITY, 2000), TILE); + assert_eq!(tile_side(0.0, 2000), TILE); + // Zoomed viewports need a few large tiles instead of many small ones. + assert_eq!(tile_side(12.0, 2000), 2048); + assert_eq!(tile_side(20.0, 2000), 3413); + assert_eq!(tile_side(100.0, 2000), 4096); + // A giant sheet at fit width (6740 px long side) is covered four tiles a side. + assert_eq!(tile_side(96.0 / 72.0, 6740), 1685); + // The backend clamps tile sides at 4096. + assert_eq!(tile_side(20.0, 100_000), 4096); + } + #[test] fn grid_zoom_is_clamped_and_sharp_renders_tolerate_a_pinch() { assert!(sharp_enough(900, 876.0) && sharp_enough(800, 876.0) && sharp_enough(1300, 876.0)); diff --git a/crates/ui-egui/src/edit_text_ui.rs b/crates/ui-egui/src/edit_text_ui.rs index 9cf38e40..6dbe70b8 100644 --- a/crates/ui-egui/src/edit_text_ui.rs +++ b/crates/ui-egui/src/edit_text_ui.rs @@ -12,6 +12,12 @@ use crate::canvas::{DocView, PageXform}; const ACCENT: Color32 = Color32::from_rgb(0x14, 0x73, 0xE6); +/// How many faint Edit-text outlines are drawn per page per frame. CAD plots can draw +/// ~1000 tiny images on one page; stroking all of them every frame (plus a hover query +/// per box) stalls the UI. Past this, only the selected and hovered boxes are outlined: +/// hit-testing still covers every image, so nothing becomes unselectable. +const MAX_OUTLINES: usize = 250; + /// A paragraph being edited. #[derive(Clone, Debug, PartialEq)] pub struct LineEditor { @@ -283,25 +289,33 @@ pub(crate) fn image_input( ) -> bool { let boxes: Vec = images.iter().map(|im| xf.user_rect(info, page, im.rect.map(|v| v as f32))).collect(); let painter = ui.painter(); - for b in &boxes { - painter.rect_stroke(*b, CornerRadius::ZERO, Stroke::new(0.75, ACCENT.gamma_multiply(0.35)), egui::StrokeKind::Outside); - } - let pointer = ui.input(|i| i.pointer.hover_pos()); + let hover_pos = ui.input(|i| i.pointer.hover_pos()); let selected = view.image_selection.as_ref().filter(|s| s.page == page).map(|s| s.index).filter(|i| *i < boxes.len()); - for (i, b) in boxes.iter().enumerate() { - let selected_box = selected == Some(i); - let hovered = ui.input(|inp| inp.pointer.hover_pos()).is_some_and(|p| b.contains(p)); - let stroke = if selected_box { - Stroke::new(1.5, ACCENT) - } else if hovered { - Stroke::new(1.5, ACCENT.gamma_multiply(0.7)) - } else { - Stroke::new(0.75, ACCENT.gamma_multiply(0.35)) - }; - if selected_box { - painter.rect_filled(*b, CornerRadius::ZERO, ACCENT.gamma_multiply(0.04)); + // Few images: faint outlines for all of them, as before. Many: only the selected and + // hovered boxes, so a ~1000-image CAD page stays interactive in Edit text & images. + let faint_all = boxes.len() <= MAX_OUTLINES; + if faint_all { + for b in &boxes { + painter.rect_stroke(*b, CornerRadius::ZERO, Stroke::new(0.75, ACCENT.gamma_multiply(0.35)), egui::StrokeKind::Outside); + } + } + let hovered_box = hover_pos.and_then(|p| boxes.iter().rposition(|b| b.contains(p))); + if !faint_all { + if let Some(i) = selected { + painter.rect_stroke(boxes[i], CornerRadius::ZERO, Stroke::new(0.75, ACCENT.gamma_multiply(0.35)), egui::StrokeKind::Outside); + } + if let Some(h) = hovered_box.filter(|h| Some(*h) != selected) { + painter.rect_stroke(boxes[h], CornerRadius::ZERO, Stroke::new(0.75, ACCENT.gamma_multiply(0.35)), egui::StrokeKind::Outside); + } + } + for i in selected.into_iter().chain(hovered_box).collect::>() { + let b = boxes[i]; + if selected == Some(i) { + painter.rect_filled(b, CornerRadius::ZERO, ACCENT.gamma_multiply(0.04)); + painter.rect_stroke(b, CornerRadius::ZERO, Stroke::new(1.5, ACCENT), egui::StrokeKind::Outside); + } else { + painter.rect_stroke(b, CornerRadius::ZERO, Stroke::new(1.5, ACCENT.gamma_multiply(0.7)), egui::StrokeKind::Outside); } - painter.rect_stroke(*b, CornerRadius::ZERO, stroke, egui::StrokeKind::Outside); } // The selected image: frame, corner handles, dragging. if let Some(i) = selected { @@ -330,7 +344,7 @@ pub(crate) fn image_input( } } if let Some((start, opposite)) = view.image_selection.as_ref().and_then(|s| s.drag) - && let Some(p) = pointer + && let Some(p) = hover_pos { let preview = match opposite { // Resize from the opposite corner, keeping the aspect ratio. @@ -362,7 +376,7 @@ pub(crate) fn image_input( return true; } } - let Some(p) = pointer.filter(|p| xf.rect.contains(*p)) else { return false }; + let Some(p) = hover_pos.filter(|p| xf.rect.contains(*p)) else { return false }; let Some(hit) = boxes.iter().rposition(|b| b.contains(p)) else { return false }; if selected != Some(hit) { painter.rect_stroke(boxes[hit], CornerRadius::ZERO, Stroke::new(1.5, ACCENT.gamma_multiply(0.7)), egui::StrokeKind::Outside); diff --git a/crates/ui-egui/src/signature_drag.rs b/crates/ui-egui/src/signature_drag.rs index fb683f1f..e61b3c9c 100644 --- a/crates/ui-egui/src/signature_drag.rs +++ b/crates/ui-egui/src/signature_drag.rs @@ -66,7 +66,7 @@ impl SignatureDrag { let scale = scale.min(crate::canvas::BASE_SIDE / p.width.max(p.height).max(1.0)); let tag = (scale * 1000.0) as u64; if layers.tag != Some(tag) { - layers.renderer.set_queue(vec![RenderRequest { page, scale, tag, ..Default::default() }]); + layers.renderer.set_queue(vec![RenderRequest { page, scale, tag, preview: true, ..Default::default() }]); layers.tag = Some(tag); } if let Some(out) = layers.renderer.try_recv() diff --git a/crates/ui-egui/src/zoom_snap.rs b/crates/ui-egui/src/zoom_snap.rs index 76e6729c..eb2cc867 100644 --- a/crates/ui-egui/src/zoom_snap.rs +++ b/crates/ui-egui/src/zoom_snap.rs @@ -89,7 +89,8 @@ impl PdfCraftApp { let mut r = pdfcraft_render::PageRenderer::new(doc.bytes.clone(), config); // About 800 px along the longer side is plenty to find the margins. let size = doc.info.pages.get(page).map_or(792.0, |p| p.width.max(p.height)); - let out = r.render(RenderRequest { page, kind: RequestKind::Pixels, tile: None, scale: (800.0 / size).clamp(0.1, 4.0), tag: 0 }); + let out = + r.render(RenderRequest { page, kind: RequestKind::Pixels, tile: None, scale: (800.0 / size).clamp(0.1, 4.0), tag: 0, preview: false }); if let Some(e) = out.error { return Err(e); } @@ -132,7 +133,7 @@ impl PdfCraftApp { } let config = RenderConfig { password: doc.password.as_deref().map(std::sync::Arc::from), ..RenderConfig::default() }; let mut r = pdfcraft_render::PageRenderer::new(doc.bytes.clone(), config); - let out = r.render(RenderRequest { page, kind: RequestKind::Pixels, tile: Some(tile), scale, tag: 0 }); + let out = r.render(RenderRequest { page, kind: RequestKind::Pixels, tile: Some(tile), scale, tag: 0, preview: false }); if let Some(e) = out.error { return Err(e); } diff --git a/crates/ui-egui/tests/view.rs b/crates/ui-egui/tests/view.rs index e6890f9b..d3d16e06 100644 --- a/crates/ui-egui/tests/view.rs +++ b/crates/ui-egui/tests/view.rs @@ -874,7 +874,14 @@ fn compare_page_with_raster(h: &mut Harness<'static, PdfCraftApp>, pdf: &[u8], z let img = h.render().expect("renders"); let scale = zoom * 96.0 / 72.0 * ppp; let mut renderer = pdfcraft_render::PageRenderer::new(std::sync::Arc::new(pdf.to_vec()), pdfcraft_render::RenderConfig::default()); - let page = renderer.render(pdfcraft_render::RenderRequest { page: 0, kind: pdfcraft_render::RequestKind::Pixels, tile: None, scale, tag: 0 }); + let page = renderer.render(pdfcraft_render::RenderRequest { + page: 0, + kind: pdfcraft_render::RequestKind::Pixels, + tile: None, + scale, + tag: 0, + preview: false, + }); assert!(page.error.is_none(), "{:?}", page.error); // Compare the lined area (page x 40..270 pt, y 60..330 pt from the top), screen against raster. let (ox, oy) = ((r.min.x * ppp).round() as i64, (r.min.y * ppp).round() as i64); diff --git a/vendor/README.md b/vendor/README.md index 7c9e3634..e2c3aae8 100644 --- a/vendor/README.md +++ b/vendor/README.md @@ -7,10 +7,10 @@ the fix. Vendoring copyleft code is never allowed (plan/adr/0001). | Crate | Version | Licence | Patches | Test | |---|---|---|---|---| -| hayro-interpret | 0.7.0 | Apache-2.0 OR MIT | (1) select `/AP /N` appearance states via `/AS` (checkboxes, radios, stateful icons) and honour the NoView flag; (2) Type3 Unicode fallback to `/Encoding` glyph names, then printable ASCII; (3) `Type3Glyph::advance_width`; (4) `InterpreterSettings::ocg_overrides` for viewer layer toggles; (5) soft masks whose group lacks `/CS` were dropped (Chrome gradient text); (6) paint nesting cap `MAX_PAINT_NESTING` for tiling patterns and Type 3 glyphs (self-referencing content overflowed the stack); (7) CID width ranges in `/W` and `/W2` clamped to `MAX_CID` (a `0 4294967295 w` range hung); (8) `InterpreterSettings::hide_comments` skips markup annotations but keeps widgets and links (View ▸ Hide all comments); (9) `image_size_ok`: images and masks over 2^28 pixels are not decoded (a fuzzed `/W 4294967295` stencil mask allocated 4 GiB); (10) `MAX_NESTED_PAINTS`: nested paints (forms, Type 3 glyphs and tiling patterns inside one another) share a per-page budget of 50,000 (a glyph showing eight glyphs of its own font, sixteen deep, hung); (11) depends on skrifa 0.47 instead of 0.42 (upstream read-fonts fixed a Type 1 tokenizer that recursed through runs of numbers: a fuzzed font hung or overflowed the stack), with the one API change (`Charset::glyph_id` returns `Option`); (12) annotation appearances whose `/BBox` or `/Matrix` collapses to a line or a point are skipped (mapping them onto `/Rect` divided by zero and handed the device infinite or NaN matrices); (13) a Highlight annotation without a usable normal appearance is drawn from its `/QuadPoints`, `/C` and `/CA` with Multiply blending, as pdfcraft-annot draws highlights (it was not drawn at all); (14) simple-font `/Widths` (`read_widths`): entries only for codes 0–255 (one entry per code below `/FirstChar` was allocated: `4294967295` asked for 34 GB), an entry that isn't a number leaves its code missing instead of ending the array, and a `/LastChar` below `/FirstChar` gives no widths instead of discarding the font for a standard one; (15) the appearance box is mapped onto `/Rect` corner to corner: the translation now accounts for the scale (an appearance whose box didn't start at the origin and differed in size from `/Rect` was drawn shifted); (16) a clip set by `W`/`W*` takes effect after whichever path-painting operator ends the path (`S`, `f`, `B`, … as well as `n`; ISO 32000-2 §8.5.4): it was applied only on `n`, so `re W* S` clipped nothing and the clip stayed pending for a later, unrelated `n`; (17) `InterpreterSettings::cancelled`: once set, interpretation stops before the next content operator (a render pool sets it for every worker when dropped, after an edit or on closing, and for one worker when the watchdog gives up on its render, so renders whose answers nobody can receive stop instead of running to the end); (18) `MAX_PAGE_CONTENT`: the content a page holds at once (its contents, and the forms, tiling patterns and Type 3 glyph procedures being painted) stays within 512 MiB (`decode_content`; once it runs out, `InterpreterWarning::ContentTruncated` is reported (once per page by pdfcraft-render) so a truncated page is not a clean render; each level gives its bytes back when it finishes): forms nest fifty deep and each level kept its content while the next ran, so a form painting itself around a stream inflating to 256 MiB would have held over 12 GB; and Type 0 sampled functions take at most `MAX_SAMPLE_ENTRIES` (2^21) table points and `MAX_SAMPLE_VALUES` (2^23) samples, read no further than `/Size` asks (256 MiB of 1-bit samples asked for over 8 GB); (19) mesh shadings (types 4–7) are sampled only inside the area being drawn (`ShadingPattern::encode_within`, which the vendored hayro renderer calls with that area plus a pixel), within 8 visits per pixel of it plus 4 per triangle (`TriangleSampler`), and patches are triangulated one at a time: points far off the page were sampled over their whole bounding box, up to 65536 × 65536 entries (a fuzzed 147-byte patch stream; a synthetic page passed 6 GB in six seconds), ten thousand overlapping triangles ran for minutes, and every patch became 722 triangles before any limit; meshes hold at most `MAX_MESH_PATCHES` (2^16) patches and `MAX_MESH_TRIANGLES` (2^20) triangles, a lattice needs two to 2^19 vertices per row (`/VerticesPerRow 0` pushed empty rows for ever; one huge row read the whole stream), and a page samples at most `MAX_PAGE_MESH_TRIANGLES` (2^25) mesh triangles in all (a mesh painted a thousand times was triangulated a thousand times); each cut logs a warning; (20) PDF function construction is limited to 64 levels and 10,000 total nodes with cycle detection; calculator parsing and evaluation share procedure depth/work budgets, integer division/bitshift/rotation/index are panic-free, and fixed-size stack errors propagate; (21) a Type 0 font drawn with a substitute that isn't CID-keyed (it isn't embedded, or its program doesn't load) draws `.notdef` for a CID of an Adobe character collection (Japan1, GB1, CNS1, Korea1) that it can't find by Unicode: hayro took the CID for a glyph index of the substitute, which numbers its glyphs its own way, so the character was drawn as an unrelated one (𠮷 as "Q" in BIZ UDPGothic); (22) a Type 0 font drawn with a substitute finds a character given with a variation selector (`/ToUnicode`, or Adobe-Japan1-UCS2, which maps the JIS X 0208 forms of the kanji JIS X 0213:2004 redrew that way: CID 1247 is 噂 U+E0100) through the substitute's variation sequences, else as the character alone: such a CID found no glyph and was drawn as an unrelated one; (23) backport of hayro 0.8's `gradient` module: `EncodedShadingPattern::as_svg_gradient` turns an axial or radial shading into stops of an sRGB gradient (within 0.01 per channel, a stop at every stitching-function bound via `Function::stitching_bounds`) when a native gradient draws it exactly (extended past the box drawn in; a radial one with no background colour) | (1) `raster::tests::widget_appearance_states`; (2, 3) `xtask text-oracle` on corpus/pdfjs issue918; (4) `raster::tests::layer_override_hides_content`; (5) `raster::tests::soft_mask_without_group_colour_space_is_applied`; (6) `raster::tests::self_referencing_tiling_pattern_terminates`, `raster::tests::self_referencing_type3_glyph_terminates`; (7) `raster::tests::huge_cid_width_ranges_terminate`; (8) `raster::tests::hiding_comments_keeps_fields`; (9) `raster::tests::absurd_mask_and_fax_sizes_are_refused_before_decoding`; (10) `raster::tests::type3_glyphs_that_fan_out_into_themselves_finish`; (11) `raster::tests::type1_font_programs_with_long_runs_of_numbers_load_quickly`; (12) `raster::tests::degenerate_annotation_appearances_are_skipped`; (13) `raster::tests::highlights_without_an_appearance_are_drawn`; (14) `raster::tests::malformed_simple_font_widths_keep_later_widths_and_the_font`, `raster::tests::huge_simple_font_first_char_terminates`; (15) `raster::tests::annotation_appearance_box_maps_onto_rect`; (16) `raster::tests::clipping_path_applies_after_any_painting_operator`; (17) `raster::tests::dropping_the_pool_cancels_its_renders`, `raster::tests::the_watchdog_stops_the_render_it_gives_up_on`; (18) `raster::tests::page_contents_and_nested_forms_share_one_budget`, `raster::tests::content_painted_again_and_again_keeps_drawing`, `raster::tests::sampled_functions_with_billions_of_samples_are_refused`; (19) `raster::tests::mesh_shadings_far_off_the_page_render`, `raster::tests::many_overlapping_mesh_triangles_finish`, `raster::tests::mesh_shadings_with_many_patches_or_no_rows_finish`; (20) `raster::tests::function_limits_calculator_arithmetic_and_depth`, `raster::tests::function_limits_public_construction_work_and_depth`, `raster::tests::function_limits_stitching_cycles_are_refused_and_shared_children_work`, `raster::tests::function_limits_invalid_calculator_keeps_the_page_renderable`; (21) `raster::tests::cids_a_substitute_lacks_are_not_drawn_as_other_glyphs` |; (22) `raster::tests::variation_sequences_draw_their_kanji_in_a_substitute`; (23) `raster::tests::axial_and_radial_shadings_draw_as_native_gradients` +| hayro-interpret | 0.7.0 | Apache-2.0 OR MIT | (1) select `/AP /N` appearance states via `/AS` (checkboxes, radios, stateful icons) and honour the NoView flag; (2) Type3 Unicode fallback to `/Encoding` glyph names, then printable ASCII; (3) `Type3Glyph::advance_width`; (4) `InterpreterSettings::ocg_overrides` for viewer layer toggles; (5) soft masks whose group lacks `/CS` were dropped (Chrome gradient text); (6) paint nesting cap `MAX_PAINT_NESTING` for tiling patterns and Type 3 glyphs (self-referencing content overflowed the stack); (7) CID width ranges in `/W` and `/W2` clamped to `MAX_CID` (a `0 4294967295 w` range hung); (8) `InterpreterSettings::hide_comments` skips markup annotations but keeps widgets and links (View ▸ Hide all comments); (9) `image_size_ok`: images and masks over 2^28 pixels are not decoded (a fuzzed `/W 4294967295` stencil mask allocated 4 GiB); (10) `MAX_NESTED_PAINTS`: nested paints (forms, Type 3 glyphs and tiling patterns inside one another) share a per-page budget of 50,000 (a glyph showing eight glyphs of its own font, sixteen deep, hung); (11) depends on skrifa 0.47 instead of 0.42 (upstream read-fonts fixed a Type 1 tokenizer that recursed through runs of numbers: a fuzzed font hung or overflowed the stack), with the one API change (`Charset::glyph_id` returns `Option`); (12) annotation appearances whose `/BBox` or `/Matrix` collapses to a line or a point are skipped (mapping them onto `/Rect` divided by zero and handed the device infinite or NaN matrices); (13) a Highlight annotation without a usable normal appearance is drawn from its `/QuadPoints`, `/C` and `/CA` with Multiply blending, as pdfcraft-annot draws highlights (it was not drawn at all); (14) simple-font `/Widths` (`read_widths`): entries only for codes 0–255 (one entry per code below `/FirstChar` was allocated: `4294967295` asked for 34 GB), an entry that isn't a number leaves its code missing instead of ending the array, and a `/LastChar` below `/FirstChar` gives no widths instead of discarding the font for a standard one; (15) the appearance box is mapped onto `/Rect` corner to corner: the translation now accounts for the scale (an appearance whose box didn't start at the origin and differed in size from `/Rect` was drawn shifted); (16) a clip set by `W`/`W*` takes effect after whichever path-painting operator ends the path (`S`, `f`, `B`, … as well as `n`; ISO 32000-2 §8.5.4): it was applied only on `n`, so `re W* S` clipped nothing and the clip stayed pending for a later, unrelated `n`; (17) `InterpreterSettings::cancelled`: once set, interpretation stops before the next content operator (a render pool sets it for every worker when dropped, after an edit or on closing, and for one worker when the watchdog gives up on its render, so renders whose answers nobody can receive stop instead of running to the end); (18) `MAX_PAGE_CONTENT`: the content a page holds at once (its contents, and the forms, tiling patterns and Type 3 glyph procedures being painted) stays within 512 MiB (`decode_content`; once it runs out, `InterpreterWarning::ContentTruncated` is reported (once per page by pdfcraft-render) so a truncated page is not a clean render; each level gives its bytes back when it finishes): forms nest fifty deep and each level kept its content while the next ran, so a form painting itself around a stream inflating to 256 MiB would have held over 12 GB; and Type 0 sampled functions take at most `MAX_SAMPLE_ENTRIES` (2^21) table points and `MAX_SAMPLE_VALUES` (2^23) samples, read no further than `/Size` asks (256 MiB of 1-bit samples asked for over 8 GB); (19) mesh shadings (types 4–7) are sampled only inside the area being drawn (`ShadingPattern::encode_within`, which the vendored hayro renderer calls with that area plus a pixel), within 8 visits per pixel of it plus 4 per triangle (`TriangleSampler`), and patches are triangulated one at a time: points far off the page were sampled over their whole bounding box, up to 65536 × 65536 entries (a fuzzed 147-byte patch stream; a synthetic page passed 6 GB in six seconds), ten thousand overlapping triangles ran for minutes, and every patch became 722 triangles before any limit; meshes hold at most `MAX_MESH_PATCHES` (2^16) patches and `MAX_MESH_TRIANGLES` (2^20) triangles, a lattice needs two to 2^19 vertices per row (`/VerticesPerRow 0` pushed empty rows for ever; one huge row read the whole stream), and a page samples at most `MAX_PAGE_MESH_TRIANGLES` (2^25) mesh triangles in all (a mesh painted a thousand times was triangulated a thousand times); each cut logs a warning; (20) PDF function construction is limited to 64 levels and 10,000 total nodes with cycle detection; calculator parsing and evaluation share procedure depth/work budgets, integer division/bitshift/rotation/index are panic-free, and fixed-size stack errors propagate; (21) a Type 0 font drawn with a substitute that isn't CID-keyed (it isn't embedded, or its program doesn't load) draws `.notdef` for a CID of an Adobe character collection (Japan1, GB1, CNS1, Korea1) that it can't find by Unicode: hayro took the CID for a glyph index of the substitute, which numbers its glyphs its own way, so the character was drawn as an unrelated one (𠮷 as "Q" in BIZ UDPGothic); (22) a Type 0 font drawn with a substitute finds a character given with a variation selector (`/ToUnicode`, or Adobe-Japan1-UCS2, which maps the JIS X 0208 forms of the kanji JIS X 0213:2004 redrew that way: CID 1247 is 噂 U+E0100) through the substitute's variation sequences, else as the character alone: such a CID found no glyph and was drawn as an unrelated one; (23) backport of hayro 0.8's `gradient` module: `EncodedShadingPattern::as_svg_gradient` turns an axial or radial shading into stops of an sRGB gradient (within 0.01 per channel, a stop at every stitching-function bound via `Function::stitching_bounds`) when a native gradient draws it exactly (extended past the box drawn in; a radial one with no background colour); (24) `DecodedImageCache`: decoded image pixels shared across renders through the interpreter cache within a 64 MiB budget (an image-heavy page decoded its thousands of images again per tile and per scale), keyed by every decode input (`DecodedImageKey`: the stream's place and dictionary bytes, its object, mask/stencil/raster output, interpolation, decode target size, preview or final); images under a transfer function and inline images are not cached, an image over the budget is never kept, and a poisoned lock decodes uncached | (1) `raster::tests::widget_appearance_states`; (2, 3) `xtask text-oracle` on corpus/pdfjs issue918; (4) `raster::tests::layer_override_hides_content`; (5) `raster::tests::soft_mask_without_group_colour_space_is_applied`; (6) `raster::tests::self_referencing_tiling_pattern_terminates`, `raster::tests::self_referencing_type3_glyph_terminates`; (7) `raster::tests::huge_cid_width_ranges_terminate`; (8) `raster::tests::hiding_comments_keeps_fields`; (9) `raster::tests::absurd_mask_and_fax_sizes_are_refused_before_decoding`; (10) `raster::tests::type3_glyphs_that_fan_out_into_themselves_finish`; (11) `raster::tests::type1_font_programs_with_long_runs_of_numbers_load_quickly`; (12) `raster::tests::degenerate_annotation_appearances_are_skipped`; (13) `raster::tests::highlights_without_an_appearance_are_drawn`; (14) `raster::tests::malformed_simple_font_widths_keep_later_widths_and_the_font`, `raster::tests::huge_simple_font_first_char_terminates`; (15) `raster::tests::annotation_appearance_box_maps_onto_rect`; (16) `raster::tests::clipping_path_applies_after_any_painting_operator`; (17) `raster::tests::dropping_the_pool_cancels_its_renders`, `raster::tests::the_watchdog_stops_the_render_it_gives_up_on`; (18) `raster::tests::page_contents_and_nested_forms_share_one_budget`, `raster::tests::content_painted_again_and_again_keeps_drawing`, `raster::tests::sampled_functions_with_billions_of_samples_are_refused`; (19) `raster::tests::mesh_shadings_far_off_the_page_render`, `raster::tests::many_overlapping_mesh_triangles_finish`, `raster::tests::mesh_shadings_with_many_patches_or_no_rows_finish`; (20) `raster::tests::function_limits_calculator_arithmetic_and_depth`, `raster::tests::function_limits_public_construction_work_and_depth`, `raster::tests::function_limits_stitching_cycles_are_refused_and_shared_children_work`, `raster::tests::function_limits_invalid_calculator_keeps_the_page_renderable`; (21) `raster::tests::cids_a_substitute_lacks_are_not_drawn_as_other_glyphs` |; (22) `raster::tests::variation_sequences_draw_their_kanji_in_a_substitute`; (23) `raster::tests::axial_and_radial_shadings_draw_as_native_gradients`; (24) `raster::tests::tiles_match_full_render`, `raster::tests::skipping_paths_off_the_canvas_changes_no_pixel` (both decode through the cache), `raster::tests::decoded_images_with_identical_dictionaries_keep_their_own_pixels`, `raster::tests::decoded_images_under_a_transfer_function_are_not_shared` -| hayro | 0.7.1 | Apache-2.0 OR MIT | (1) `RenderSettings::{x_offset, y_offset}` to render one tile of a large page; (2) images over `MAX_IMAGE_PIXELS` (2^28) are skipped (a fuzzed `/W 4294967295` hung in resampling); (3) `tiling_cell_scale` keeps a tiling pattern's cell pixmap within 3000 px a side whatever its `/XStep`/`/YStep` (a fuzzed step far beyond `/BBox` allocated 2 GB); (4) stroke widths clamped to 4 × (canvas width + height) device pixels (a fuzzed `/LW 9223372036854775807` allocated 10 GB in stroke expansion); (5) `usable_dash_pattern`: a stroke whose dash pattern could cut it into more than `MAX_DASHES_PER_STROKE` (250,000) pieces, counting kurbo's restart of the pattern at every subpath and `ClosePath`, is drawn solid (a fuzzed `/D [[11] 0]` on a line from x = 92234775807 allocated over 5 GB, `[0.000001] 0 d` on a 20 pt line over 10 GB); (6) a dash pattern with a negative or non-finite entry, or a period that isn't positive, is drawn solid (`[-1 -1] 0 d` looped for ever in kurbo); (7) `render_size` and `render_into`: render into a buffer the caller allocates (PdfCraft renders into pixel-aligned memory that the GUI takes over as texture data); `render` is `render_into` on a new pixmap; (8) checked image source/target/RGBA, alpha-premultiplication scratch and intermediate sizes before resampling, finite transforms and exact source/mask buffers; sources wider than u16 may shrink before backend limits, with a 2^20 source-axis cap and conservative 128 MiB filter-planning estimate; an over-limit or failed resize keeps the original image resolution and geometry when the source fits the backend, otherwise skips it, with a logged warning; staging, resampling output/scratch, RGBA conversion and padding buffers use fallible reserves; (9) `may_paint_canvas`: a fill or stroke whose control-point box, grown by how far its paint reaches past the path (half the stroke width times the larger of the miter limit and √2) plus 0.5 user units and 2 pixels, misses the canvas is not drawn, so a tile no longer strokes and fills every path of its page (item 5 of #623); `set_skip_offscreen_paths` turns this off for the regression test; (10) backport of hayro 0.8: shadings that `as_svg_gradient` can represent are painted as a `vello_cpu` linear or two-point radial gradient instead of a texture sampled pixel by pixel through the shading function (InDesign fades are luminosity soft masks of one full-page axial `sh`: 40 on a report page took 15 s at 144 dpi, now under 1 s); without upstream's half-pixel shift, which suits vello_cpu 0.3 but moves 0.0.8's gradients off pixel centres | (1) `raster::tests::tiles_match_full_render`; (2) `raster::tests::absurd_image_dimensions_are_skipped`; (3) `raster::tests::tiling_cells_stay_small_whatever_the_step`; (4) `raster::tests::absurd_predictor_columns_and_line_widths_render`; (5) `raster::tests::dash_patterns_that_cut_a_stroke_into_billions_of_pieces_render`; (6) `raster::tests::invalid_dash_patterns_terminate_and_are_drawn_solid`; (7) `raster::tests::pixels_are_a_plain_renders_bytes_in_a_buffer_a_gui_can_take_over`; (8) `raster::tests::image_resampling_rejects_anisotropic_target_growth`, `raster::tests::image_resampling_bounds_intermediate_before_planning`, `raster::tests::image_resampling_rejects_invalid_sources_and_scales`, `raster::tests::image_resampling_keeps_valid_sizes_and_exact_limits`, `raster::tests::resampling_fallback_preserves_original_geometry_and_pixels`, `raster::tests::image_resampling_wide_sources_shrink_before_backend_side_limits`, `raster::tests::image_resampling_wide_transparent_sources_still_shrink`, `raster::tests::image_resampling_padded_backend_dimensions_stay_checked`, `raster::tests::image_resampling_mismatched_alpha_mask_keeps_pixels_and_placement`; (9) `raster::tests::skipping_paths_off_the_canvas_changes_no_pixel`; (10) `raster::tests::axial_and_radial_shadings_draw_as_native_gradients` | -| hayro | 0.7.1 | Apache-2.0 OR MIT | (1) `RenderSettings::{x_offset, y_offset}` to render one tile of a large page; (2) images over `MAX_IMAGE_PIXELS` (2^28) are skipped (a fuzzed `/W 4294967295` hung in resampling); (3) `tiling_cell_scale` keeps a tiling pattern's cell pixmap within 3000 px a side whatever its `/XStep`/`/YStep` (a fuzzed step far beyond `/BBox` allocated 2 GB); (4) stroke widths clamped to 4 × (canvas width + height) device pixels (a fuzzed `/LW 9223372036854775807` allocated 10 GB in stroke expansion); (5) `usable_dash_pattern`: a stroke whose dash pattern could cut it into more than `MAX_DASHES_PER_STROKE` (250,000) pieces, counting kurbo's restart of the pattern at every subpath and `ClosePath`, is drawn solid (a fuzzed `/D [[11] 0]` on a line from x = 92234775807 allocated over 5 GB, `[0.000001] 0 d` on a 20 pt line over 10 GB); (6) a dash pattern with a negative or non-finite entry, or a period that isn't positive, is drawn solid (`[-1 -1] 0 d` looped for ever in kurbo); (7) `render_size` and `render_into`: render into a buffer the caller allocates (PdfCraft renders into pixel-aligned memory that the GUI takes over as texture data); `render` is `render_into` on a new pixmap; (8) checked image source/target/RGBA, alpha-premultiplication scratch and intermediate sizes before resampling, finite transforms and exact source/mask buffers; sources wider than u16 may shrink before backend limits, with a 2^20 source-axis cap and conservative 128 MiB filter-planning estimate; an over-limit or failed resize keeps the original image resolution and geometry when the source fits the backend, otherwise skips it, with a logged warning; staging, resampling output/scratch, RGBA conversion and padding buffers use fallible reserves; (9) `may_paint_canvas`: a fill or stroke whose control-point box, grown by how far its paint reaches past the path (half the stroke width times the larger of the miter limit and √2) plus 0.5 user units and 2 pixels, misses the canvas is not drawn, so a tile no longer strokes and fills every path of its page (item 5 of #623); `set_skip_offscreen_paths` turns this off for the regression test; (10) `/Interpolate` governs the pre-resize too (#624): a minified image that does not request interpolation is pre-resized with nearest-neighbour, planned without the Catmull-Rom filter limits (`image_resampling_size` takes the flag); a Type 3 glyph image is sampled bicubically only when interpolated; a minified image with a matching soft mask is nearest-sampled straight into premultiplied RGBA | (1) `raster::tests::tiles_match_full_render`; (2) `raster::tests::absurd_image_dimensions_are_skipped`; (3) `raster::tests::tiling_cells_stay_small_whatever_the_step`; (4) `raster::tests::absurd_predictor_columns_and_line_widths_render`; (5) `raster::tests::dash_patterns_that_cut_a_stroke_into_billions_of_pieces_render`; (6) `raster::tests::invalid_dash_patterns_terminate_and_are_drawn_solid`; (7) `raster::tests::pixels_are_a_plain_renders_bytes_in_a_buffer_a_gui_can_take_over`; (8) `raster::tests::image_resampling_rejects_anisotropic_target_growth`, `raster::tests::image_resampling_bounds_intermediate_before_planning`, `raster::tests::image_resampling_rejects_invalid_sources_and_scales`, `raster::tests::image_resampling_keeps_valid_sizes_and_exact_limits`, `raster::tests::resampling_fallback_preserves_original_geometry_and_pixels`, `raster::tests::image_resampling_wide_sources_shrink_before_backend_side_limits`, `raster::tests::image_resampling_wide_transparent_sources_still_shrink`, `raster::tests::image_resampling_padded_backend_dimensions_stay_checked`, `raster::tests::image_resampling_mismatched_alpha_mask_keeps_pixels_and_placement`; (9) `raster::tests::skipping_paths_off_the_canvas_changes_no_pixel`; (10) `raster::tests::non_interpolated_images_are_not_smoothed_when_minified`, `raster::tests::non_interpolated_gray_images_are_not_smoothed_when_minified`, `raster::tests::non_integer_minification_samples_the_same_columns`, `raster::tests::non_integer_minification_samples_colour_and_mask_from_the_same_columns`, `raster::tests::non_interpolated_alpha_images_keep_their_colours_and_mask_when_minified`, `raster::tests::non_interpolated_type3_glyph_images_are_not_smoothed_when_minified`, `raster::tests::image_resampling_nearest_accepts_wide_sources_the_filter_refuses`, `raster::tests::wide_non_interpolated_sources_draw_after_shrinking` | +| hayro | 0.7.1 | Apache-2.0 OR MIT | (1) `RenderSettings::{x_offset, y_offset}` to render one tile of a large page; (2) images over `MAX_IMAGE_PIXELS` (2^28) are skipped (a fuzzed `/W 4294967295` hung in resampling); (3) `tiling_cell_scale` keeps a tiling pattern's cell pixmap within 3000 px a side whatever its `/XStep`/`/YStep` (a fuzzed step far beyond `/BBox` allocated 2 GB); (4) stroke widths clamped to 4 × (canvas width + height) device pixels (a fuzzed `/LW 9223372036854775807` allocated 10 GB in stroke expansion); (5) `usable_dash_pattern`: a stroke whose dash pattern could cut it into more than `MAX_DASHES_PER_STROKE` (250,000) pieces, counting kurbo's restart of the pattern at every subpath and `ClosePath`, is drawn solid (a fuzzed `/D [[11] 0]` on a line from x = 92234775807 allocated over 5 GB, `[0.000001] 0 d` on a 20 pt line over 10 GB); (6) a dash pattern with a negative or non-finite entry, or a period that isn't positive, is drawn solid (`[-1 -1] 0 d` looped for ever in kurbo); (7) `render_size` and `render_into`: render into a buffer the caller allocates (PdfCraft renders into pixel-aligned memory that the GUI takes over as texture data); `render` is `render_into` on a new pixmap; (8) checked image source/target/RGBA, alpha-premultiplication scratch and intermediate sizes before resampling, finite transforms and exact source/mask buffers; sources wider than u16 may shrink before backend limits, with a 2^20 source-axis cap and conservative 128 MiB filter-planning estimate; an over-limit or failed resize keeps the original image resolution and geometry when the source fits the backend, otherwise skips it, with a logged warning; staging, resampling output/scratch, RGBA conversion and padding buffers use fallible reserves; (9) `may_paint_canvas`: a fill or stroke whose control-point box, grown by how far its paint reaches past the path (half the stroke width times the larger of the miter limit and √2) plus 0.5 user units and 2 pixels, misses the canvas is not drawn, so a tile no longer strokes and fills every path of its page (item 5 of #623); `set_skip_offscreen_paths` turns this off for the regression test; (10) backport of hayro 0.8: shadings that `as_svg_gradient` can represent are painted as a `vello_cpu` linear or two-point radial gradient instead of a texture sampled pixel by pixel through the shading function (InDesign fades are luminosity soft masks of one full-page axial `sh`: 40 on a report page took 15 s at 144 dpi, now under 1 s); without upstream's half-pixel shift, which suits vello_cpu 0.3 but moves 0.0.8's gradients off pixel centres; (11) `may_paint_rect`: an image whose stream-size box, mapped to the device, misses the canvas is skipped before decoding (decoded data can only shrink against that box), so a tile of an image-heavy page no longer decodes, resamples and rasterizes every image on it; (12) preview renders (`InterpreterSettings::preview`: backdrops, thumbnails, grid cells, signature-drag backgrounds) skip images under `PREVIEW_MIN_IMAGE_AREA` device pixels, while final renders always draw them | (1) `raster::tests::tiles_match_full_render`; (2) `raster::tests::absurd_image_dimensions_are_skipped`; (3) `raster::tests::tiling_cells_stay_small_whatever_the_step`; (4) `raster::tests::absurd_predictor_columns_and_line_widths_render`; (5) `raster::tests::dash_patterns_that_cut_a_stroke_into_billions_of_pieces_render`; (6) `raster::tests::invalid_dash_patterns_terminate_and_are_drawn_solid`; (7) `raster::tests::pixels_are_a_plain_renders_bytes_in_a_buffer_a_gui_can_take_over`; (8) `raster::tests::image_resampling_rejects_anisotropic_target_growth`, `raster::tests::image_resampling_bounds_intermediate_before_planning`, `raster::tests::image_resampling_rejects_invalid_sources_and_scales`, `raster::tests::image_resampling_keeps_valid_sizes_and_exact_limits`, `raster::tests::resampling_fallback_preserves_original_geometry_and_pixels`, `raster::tests::image_resampling_wide_sources_shrink_before_backend_side_limits`, `raster::tests::image_resampling_wide_transparent_sources_still_shrink`, `raster::tests::image_resampling_padded_backend_dimensions_stay_checked`, `raster::tests::image_resampling_mismatched_alpha_mask_keeps_pixels_and_placement`; (9) `raster::tests::skipping_paths_off_the_canvas_changes_no_pixel`; (10) `raster::tests::axial_and_radial_shadings_draw_as_native_gradients`; (11) `raster::tests::skipping_paths_off_the_canvas_changes_no_pixel`; (12) `raster::tests::preview_renders_skip_tiny_images_but_finals_draw_everything` | +| hayro | 0.7.1 | Apache-2.0 OR MIT | (1) `RenderSettings::{x_offset, y_offset}` to render one tile of a large page; (2) images over `MAX_IMAGE_PIXELS` (2^28) are skipped (a fuzzed `/W 4294967295` hung in resampling); (3) `tiling_cell_scale` keeps a tiling pattern's cell pixmap within 3000 px a side whatever its `/XStep`/`/YStep` (a fuzzed step far beyond `/BBox` allocated 2 GB); (4) stroke widths clamped to 4 × (canvas width + height) device pixels (a fuzzed `/LW 9223372036854775807` allocated 10 GB in stroke expansion); (5) `usable_dash_pattern`: a stroke whose dash pattern could cut it into more than `MAX_DASHES_PER_STROKE` (250,000) pieces, counting kurbo's restart of the pattern at every subpath and `ClosePath`, is drawn solid (a fuzzed `/D [[11] 0]` on a line from x = 92234775807 allocated over 5 GB, `[0.000001] 0 d` on a 20 pt line over 10 GB); (6) a dash pattern with a negative or non-finite entry, or a period that isn't positive, is drawn solid (`[-1 -1] 0 d` looped for ever in kurbo); (7) `render_size` and `render_into`: render into a buffer the caller allocates (PdfCraft renders into pixel-aligned memory that the GUI takes over as texture data); `render` is `render_into` on a new pixmap; (8) checked image source/target/RGBA, alpha-premultiplication scratch and intermediate sizes before resampling, finite transforms and exact source/mask buffers; sources wider than u16 may shrink before backend limits, with a 2^20 source-axis cap and conservative 128 MiB filter-planning estimate; an over-limit or failed resize keeps the original image resolution and geometry when the source fits the backend, otherwise skips it, with a logged warning; staging, resampling output/scratch, RGBA conversion and padding buffers use fallible reserves; (9) `may_paint_canvas`: a fill or stroke whose control-point box, grown by how far its paint reaches past the path (half the stroke width times the larger of the miter limit and √2) plus 0.5 user units and 2 pixels, misses the canvas is not drawn, so a tile no longer strokes and fills every path of its page (item 5 of #623); `set_skip_offscreen_paths` turns this off for the regression test; (10) `/Interpolate` governs the pre-resize too (#624): a minified image that does not request interpolation is pre-resized with nearest-neighbour, planned without the Catmull-Rom filter limits (`image_resampling_size` takes the flag); a Type 3 glyph image is sampled bicubically only when interpolated; a minified image with a matching soft mask is nearest-sampled straight into premultiplied RGBA; (11) `may_paint_rect`: an image whose stream-size box, mapped to the device, misses the canvas is skipped before decoding (decoded data can only shrink against that box), so a tile of an image-heavy page no longer decodes, resamples and rasterizes every image on it; (12) preview renders (`InterpreterSettings::preview`: backdrops, thumbnails, grid cells, signature-drag backgrounds) skip images under `PREVIEW_MIN_IMAGE_AREA` device pixels, while final renders always draw them | (1) `raster::tests::tiles_match_full_render`; (2) `raster::tests::absurd_image_dimensions_are_skipped`; (3) `raster::tests::tiling_cells_stay_small_whatever_the_step`; (4) `raster::tests::absurd_predictor_columns_and_line_widths_render`; (5) `raster::tests::dash_patterns_that_cut_a_stroke_into_billions_of_pieces_render`; (6) `raster::tests::invalid_dash_patterns_terminate_and_are_drawn_solid`; (7) `raster::tests::pixels_are_a_plain_renders_bytes_in_a_buffer_a_gui_can_take_over`; (8) `raster::tests::image_resampling_rejects_anisotropic_target_growth`, `raster::tests::image_resampling_bounds_intermediate_before_planning`, `raster::tests::image_resampling_rejects_invalid_sources_and_scales`, `raster::tests::image_resampling_keeps_valid_sizes_and_exact_limits`, `raster::tests::resampling_fallback_preserves_original_geometry_and_pixels`, `raster::tests::image_resampling_wide_sources_shrink_before_backend_side_limits`, `raster::tests::image_resampling_wide_transparent_sources_still_shrink`, `raster::tests::image_resampling_padded_backend_dimensions_stay_checked`, `raster::tests::image_resampling_mismatched_alpha_mask_keeps_pixels_and_placement`; (9) `raster::tests::skipping_paths_off_the_canvas_changes_no_pixel`; (10) `raster::tests::non_interpolated_images_are_not_smoothed_when_minified`, `raster::tests::non_interpolated_gray_images_are_not_smoothed_when_minified`, `raster::tests::non_integer_minification_samples_the_same_columns`, `raster::tests::non_integer_minification_samples_colour_and_mask_from_the_same_columns`, `raster::tests::non_interpolated_alpha_images_keep_their_colours_and_mask_when_minified`, `raster::tests::non_interpolated_type3_glyph_images_are_not_smoothed_when_minified`, `raster::tests::image_resampling_nearest_accepts_wide_sources_the_filter_refuses`, `raster::tests::wide_non_interpolated_sources_draw_after_shrinking`; (11) `raster::tests::skipping_paths_off_the_canvas_changes_no_pixel`; (12) `raster::tests::preview_renders_skip_tiny_images_but_finals_draw_everything` | | hayro-jbig2 | 0.3.0 | Apache-2.0 OR MIT | (1) at most 2^28 pixels per bitmap, page bitmap, halftone grid and grayscale image (each side could be 65535: 4.3 billion pixels decoded one by one); (2) at most 2^26 symbol pixels per symbol dictionary (a fuzzed dictionary decoded for minutes); (3) a symbol dictionary's height classes are capped at one per new symbol and its export runs at `2 × symbols + 1`, each plus `EXTRA_EMPTY_STEPS` (1024): an empty height class or a zero-length export run doesn't advance, and past the end of its data the arithmetic decoder can decode them for ever (one changed byte hung the renderer) | (1) `raster::tests::jbig2_regions_of_billions_of_pixels_are_refused`; (2) the nightly `cargo xtask fuzz` finding, which now finishes in about a second; (3) `raster::tests::jbig2_symbol_dictionaries_that_never_advance_terminate` | | lopdf | 0.45.0 | MIT | (1) PNG predictor rows longer than the data are refused before their buffers are allocated (a fuzzed `/Columns 4294967295` allocated two 4 GiB rows), and empty frames return without row buffers; (2) predictor row widths use saturating arithmetic or reject overflow; (3) sub-byte TIFF Predictor 2 scratch is bounded by the samples present in the input instead of the declared `/Colors`, aligned with [lopdf#589](https://github.com/J-F-Liu/lopdf/pull/589) | (1) `inspect::tests::png_predictor_rows_longer_than_the_data_are_refused_before_allocating`, `inspect::tests::empty_png_predictor_frames_do_not_allocate_rows`; (2) `inspect::tests::tiff_subbyte_predictor_row_width_overflow_is_refused`; (3) `inspect::tests::tiff_subbyte_predictor_scratch_is_bounded_by_available_samples`, `inspect::tests::tiff_subbyte_predictors_keep_components_rows_and_padding` | | hayro-syntax | 0.7.2 | Apache-2.0 OR MIT | (1) Page-tree cycle guard in `resolve_pages` (visited set + depth cap): a `/Kids` loop among compressed objects overflowed the stack and aborted the process; (2) `ccitt_size_ok`: CCITT images wider than 2^20 columns or over 2^28 pixels are not decoded (a fuzzed `/Columns 4294967295` allocated 4 GiB); (3) Flate/LZW predictor row widths are saturating `usize`, a zero width is refused and a row longer than the data is not allocated (a fuzzed `/Columns 9223372036854775807` wrapped to a 2^61-byte allocation and aborted); (4) the inline-image end-of-data check (`EI`) doesn't nest inside its own look-ahead and examines at most 256 candidates and steps (50 images with "EI" in their data ran for over ten minutes); (5) a stream whose `/Length` is wrong is recovered only when `stream` is the next token — scanning ahead to a later object's `stream` made an indirect appearance-state dictionary (Quartz checkboxes: `/AP /N` → `<< /Yes … /Off … >>`) render as a non-form, so the widget was skipped; (6) JPX rescaling uses an exact integer sample range for 1–32-bit components (32 shifted past u32, and floating-point maxima at 25–31 bits wrapped to zero), and refuses zero-sized rows; (7) `decode_limit`: Flate (through flate2 and the fallback inflater), LZW and RunLength never write past `MAX_DECODED_STREAM` (256 MiB), or, for the last filter of an image of known size, past twice its pixel bytes (a predictor tag byte per byte), at most 1 GiB; `Stream::decoded_within` for budgeted callers; a page's `/Contents` array shares one `MAX_DECODED_STREAM` (a 2 MB page whose content stream or 8 × 8 image inflated to 2 GB took the renderer past 3 GB in under a second; naming the stream twenty times in `/Contents`, 5 GB); (8) high-precision numbers (jsPDF MediaBox coordinates with over 19 digits) parse without overflowing the u64 mantissa: fractional digits past it are dropped, integer digits past it scale the value (a real, kept finite); (9) large dictionary indexes are built on first use and shared across repeated reads within each PDF (#307); the validated-byte cache is bounded, reader contexts remain separate, repairs and inline/content dictionaries bypass sharing, and small dictionaries stay eager; (10) decoded object-stream slots publish once under contention, cold loading rejects recursive dependencies, and xref repair serializes without holding recursive read guards; this permits one parser per native render pool with private worker caches; (11) `sync.rs`: a poisoned lock is recovered instead of unwrapped (a worker panic no longer cascades into every reader of the same PDF), and xref repair takes a blocking write lock instead of asserting a non-blocking one; (12) `SegmentList` (`util.rs`) returns `None` for an out-of-range index instead of panicking; (13) a page's `/UserUnit` scales `render_dimensions` and `initial_transform`, so the page and everything on it are sized in points, as Acrobat shows them; it is read as Acrobat reads it (the page's own entry, not inherited; a number from 1 up, at most 75,000; anything else counts as 1), and a page without it keeps exactly the transform it had; (14) revision 2–4 passwords are tried as given and then in PDFDocEncoding (`legacy_passwords` in `crypto/mod.rs`, which resolves the upstream `TODO: Convert to PDFDocEncoding`): passwords arrive as UTF-8 text, so a password with characters outside ASCII (`Çok güzel`, `ılık€`) written in PDFDocEncoding, as ISO 32000-2 §7.6.4.3.2 asks and qpdf and Acrobat write it, was always wrong; (15) page content is read a piece at a time (`page::ContentReader`, `Page::content_reader`): plain streams and Flate streams without a predictor are decoded as they are read, with a budget of 64 times their compressed size and never less than `MAX_DECODED_STREAM` (a stream that expands past it is cut and reported); other streams are decoded whole, as before, and share `MAX_DECODED_STREAM`. The content tokenizer reads a window at a time (`UntypedIter::new_window`, `TypedIter::needs_more`, `TypedIter::committed`): an instruction that runs past the window is not decided until more data follows, which `Reader` records by flagging reads and `at_end` past the end of the data, including through the clones the parser makes. The Flate decoder keeps its own 32 KiB history, so it pauses at any output size. `ContentReader::new` reuses raw/decrypted stream bytes for budget accounting and piece creation, and `decoded_raw_within` applies the existing Whole-stream filters and limits to those bytes without decrypting twice. | (1) `cos/tests/writer.rs::page_tree_loops_in_object_streams_do_not_crash_readers`; (2) `raster::tests::absurd_mask_and_fax_sizes_are_refused_before_decoding`; (3) `raster::tests::absurd_predictor_columns_and_line_widths_render`; (4) `raster::tests::inline_images_with_ei_in_their_data_parse_in_linear_time`; (5) `raster::tests::indirect_appearance_state_dict_is_not_another_stream`; (6) `raster::tests::jpx_stream_bit_depth_boundaries_do_not_panic`, `raster::tests::jpx_32_bit_image_does_not_prevent_other_page_content_from_rendering`, `raster::tests::jpx_gray_rgb_and_alpha_keep_eight_bit_samples`, `raster::tests::jpx_streams_reject_out_of_range_bit_depths`; (7) `raster::tests::decompression_bombs_stop_at_the_stream_limit`, `raster::tests::page_contents_and_nested_forms_share_one_budget`; (8) `inspect::tests::jspdf_high_precision_mediabox_does_not_overflow_dimensions`, `object::number::tests::high_precision_float_does_not_overflow_mantissa` (hayro-syntax); (9) `object::dict::lazy_index_tests`, `raster::tests::large_resource_indexes_preserve_pixels_and_text`; (10) `data::tests`, `xref::tests::concurrent_cold_object_stream_reads_all_succeed`, `raster::tests::shared_parser_is_lazy_and_initializes_once_under_contention`; (11) `data::tests::panicking_initializer_wakes_waiters_and_prevents_retries`, `xref::tests::concurrent_cold_object_stream_reads_all_succeed`; (12) `util::tests::segment_list_rejects_out_of_range_indices`; (13) `raster::tests::user_unit_scales_the_page_and_everything_on_it`; (14) `crypto::pdfcraft_password_tests`, `pdfcraft-engine` `tests::legacy_passwords_with_turkish_characters_open`; (15) `content::tests::windows_keep_every_instruction_at_every_split`, `content::tests::windows_keep_every_instruction_in_pieces_of_any_size`, `content::tests::windows_match_the_slice_on_random_content`, `content::tests::windows_drop_blanks_and_keep_a_comment_that_may_go_on`, `page::content_reader_tests::content_past_the_cap_is_read_to_the_end`, `page::content_reader_tests::a_stream_that_expands_past_the_budget_is_cut_there`, `page::content_reader_tests::a_small_expansion_under_the_cap_is_not_cut`, `raster::tests::page_content_past_the_old_cap_renders`, `raster::tests::content_reader_returns_the_same_bytes_in_any_pieces`, `raster::tests::content_bombs_stop_at_the_budget_and_say_so`, `page::content_reader_tests::plain_and_whole_pieces_keep_their_bytes`, `page::content_reader_tests::repeated_content_reference_counts_compressed_bytes_once`, `page::content_reader_tests::whole_cap_still_counts_later_unique_streams` diff --git a/vendor/hayro-interpret/src/cache.rs b/vendor/hayro-interpret/src/cache.rs index 01bbf46c..2dda2e8a 100644 --- a/vendor/hayro-interpret/src/cache.rs +++ b/vendor/hayro-interpret/src/cache.rs @@ -9,7 +9,6 @@ use std::sync::{Arc, Mutex}; type CacheMap = HashMap>>; #[derive(Clone)] pub(crate) struct Cache(Arc>); - impl Default for Cache { fn default() -> Self { Self::new() @@ -50,6 +49,125 @@ impl Cache { } } +/// PdfCraft patch: a byte-budgeted cache of decoded images, keyed by everything a decode reads +/// ([`DecodedImageKey`]). Image-heavy pages decode thousands of images; without this every tile +/// and every scale decodes them all again. Entries hold owned pixels, so a hit never changes +/// them; when an insert would pass the budget the whole map is dropped (a fresh epoch), which +/// only costs re-decoding, and an image larger than the whole budget is never kept. The budget +/// caps memory per interpreter cache. A poisoned lock decodes without the cache. +#[derive(Clone)] +pub(crate) struct DecodedImageCache { + inner: Arc>, + /// Whether the render this handle serves is a preview (`InterpreterSettings::preview`). + /// Part of every key, so a preview's entries are never handed to a final render. + pub(crate) preview: bool, +} + +/// PdfCraft patch: what a decoded image depends on. Two images share an entry only when all of +/// it is equal: +/// - where the stream's dictionary lies in the document's data (`dict_at`, `dict_len`): a +/// stream's data follows its dictionary, so this tells apart streams whose dictionaries are +/// byte for byte the same (two uncompressed images of one size), which `dict_hash` alone +/// would not; the data stays put for as long as the document (and so this cache) lives; +/// - the dictionary's bytes (`dict_hash`: `/Decode`, `/ColorSpace`, `/SMask`, `/Mask`, +/// `/BitsPerComponent`, filters, …) and its object (`obj`: the decryption key depends on it); +/// - whether it is decoded as a mask, a stencil mask, raster or mask output, interpolation, +/// the decode target size, and preview or final quality. +/// +/// A transfer function (graphics state `/TR`) also changes the pixels and has no cheap key, so +/// an image drawn under one is not cached; neither is an inline image, whose dictionary lives in +/// a content stream and whose colour space can be a name resolved through the resources. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub(crate) struct DecodedImageKey { + pub(crate) mask_output: bool, + pub(crate) dict_at: usize, + pub(crate) dict_len: usize, + pub(crate) dict_hash: u128, + pub(crate) obj: Option<(i32, i32)>, + pub(crate) is_mask: bool, + pub(crate) is_stencil_mask: bool, + pub(crate) interpolate: bool, + pub(crate) target: Option<(u32, u32)>, + pub(crate) preview: bool, +} + +#[derive(Default)] +struct DecodedImageCacheInner { + map: HashMap>>, + bytes: usize, +} + +impl DecodedImageCache { + pub(crate) fn new() -> Self { + Self { + inner: Arc::new(Mutex::new(DecodedImageCacheInner::default())), + preview: false, + } + } + + /// The same cache, for a preview (`true`) or a final render. + pub(crate) fn for_render(&self, preview: bool) -> Self { + Self { + inner: self.inner.clone(), + preview, + } + } + + /// The decoded image for `key`, from the cache or from `f`. `None` as the key decodes + /// without the cache. + pub(crate) fn get_or_insert_with( + &self, + key: Option, + budget: usize, + size_of: impl FnOnce(&T) -> usize, + f: impl FnOnce() -> Option, + ) -> Option { + let Some(key) = key else { return f() }; + // Fast path: a hit clones the pixels without decoding. + let hit = match self.inner.lock() { + Ok(locked) => locked + .map + .get(&key) + .and_then(|val| val.as_ref()) + .and_then(|val| val.downcast_ref::().cloned()), + // A thread panicked while holding the lock: decode without the cache. + Err(_) => return f(), + }; + if let Some(hit) = hit { + return Some(hit); + } + // Miss: decode without holding the lock (decoding must never deadlock the cache). + let val = f(); + let Ok(mut locked) = self.inner.lock() else { + return val; + }; + // Another thread may have decoded it meanwhile: keep the first copy. + if let Some(existing) = locked.map.get(&key) + && let Some(existing) = existing.as_ref().and_then(|val| val.downcast_ref::()) + { + return Some(existing.clone()); + } + let size = match &val { + Some(v) => size_of(v), + None => 0, + }; + if size > budget { + return val; + } + if locked.bytes.saturating_add(size) > budget { + locked.map.clear(); + locked.bytes = 0; + } + locked.bytes = locked.bytes.saturating_add(size); + locked.map.insert( + key, + val.clone() + .map(|val| Box::new(val) as Box), + ); + val + } +} + /// A trait for objects that can generate a unique cache key. pub trait CacheKey { /// Returns the cache key for this object. diff --git a/vendor/hayro-interpret/src/context.rs b/vendor/hayro-interpret/src/context.rs index abf38e31..3e83229c 100644 --- a/vendor/hayro-interpret/src/context.rs +++ b/vendor/hayro-interpret/src/context.rs @@ -1,4 +1,4 @@ -use crate::cache::{Cache, CacheKey}; +use crate::cache::{Cache, CacheKey, DecodedImageCache}; use crate::color::ColorSpace; use crate::convert::convert_transform; use crate::font::{Font, StandardFont}; @@ -120,6 +120,9 @@ pub(crate) fn take_nested_paint(depth: u32) -> bool { pub struct InterpreterCache<'a> { pub(crate) font_cache: Rc>>>>, pub(crate) object_cache: Cache, + /// PdfCraft patch: decoded images, shared by every render through this cache so a tile + /// no longer decodes its page's images again. + pub(crate) decoded_cache: DecodedImageCache, } impl<'a> Default for InterpreterCache<'a> { @@ -134,6 +137,7 @@ impl<'a> InterpreterCache<'a> { Self { font_cache: Rc::new(RefCell::new(HashMap::new())), object_cache: Cache::new(), + decoded_cache: DecodedImageCache::new(), } } } diff --git a/vendor/hayro-interpret/src/interpret/mod.rs b/vendor/hayro-interpret/src/interpret/mod.rs index 5ede4d44..5db7e8b6 100644 --- a/vendor/hayro-interpret/src/interpret/mod.rs +++ b/vendor/hayro-interpret/src/interpret/mod.rs @@ -115,6 +115,11 @@ pub struct InterpreterSettings { /// operator. What was drawn is then incomplete, so only set it when the output will be /// thrown away (a viewer's render whose result nobody can receive any more). pub cancelled: Option>, + /// PdfCraft patch: a low-detail render for previews (page backdrops, thumbnails). Images + /// that would cover only a few device pixels are skipped: redrawing them at full detail + /// costs a decode and a resample each for pixels nobody can see. Never set it for a final + /// render (tiles, whole pages, print, export). + pub preview: bool, } impl Default for InterpreterSettings { @@ -136,6 +141,7 @@ impl Default for InterpreterSettings { hide_comments: false, ocg_overrides: Arc::new(Vec::new()), cancelled: None, + preview: false, } } } @@ -758,12 +764,18 @@ fn interpret_window<'a>( TypedInstruction::ShapeGlyph(_) => {} TypedInstruction::XObject(x) => { let cache = context.interpreter_cache.object_cache.clone(); + // PdfCraft patch: decoded images are cached per quality (preview or final). + let decoded_cache = context + .interpreter_cache + .decoded_cache + .for_render(context.settings.preview); let transfer_function = context.get().graphics_state.transfer_function.clone(); if let Some(x_object) = resources.get_x_object(x.0).and_then(|s| { XObject::new( &s, &context.settings.warning_sink, &cache, + Some(&decoded_cache), transfer_function.clone(), ) }) { @@ -774,11 +786,13 @@ fn interpret_window<'a>( let warning_sink = context.settings.warning_sink.clone(); let transfer_function = context.get().graphics_state.transfer_function.clone(); let cache = context.interpreter_cache.object_cache.clone(); + // PdfCraft patch: inline images are not cached (see `DecodedImageKey`). if let Some(x_object) = ImageXObject::new( i.0, |name| context.get_color_space(resources, name), &warning_sink, &cache, + None, false, transfer_function, ) { diff --git a/vendor/hayro-interpret/src/x_object.rs b/vendor/hayro-interpret/src/x_object.rs index 86b6fa13..68d97d55 100644 --- a/vendor/hayro-interpret/src/x_object.rs +++ b/vendor/hayro-interpret/src/x_object.rs @@ -1,4 +1,4 @@ -use crate::cache::Cache; +use crate::cache::{Cache, DecodedImageCache, DecodedImageKey}; use crate::color::{ColorComponents, ColorSpace, ToRgb}; use crate::context::Context; use crate::device::Device; @@ -34,6 +34,7 @@ impl<'a> XObject<'a> { stream: &Stream<'a>, warning_sink: &WarningSinkFn, cache: &Cache, + decoded_cache: Option<&DecodedImageCache>, transfer_function: Option, ) -> Option { let dict = stream.dict(); @@ -43,6 +44,7 @@ impl<'a> XObject<'a> { |_| None, warning_sink, cache, + decoded_cache, false, transfer_function, )?)), @@ -270,6 +272,9 @@ pub(crate) struct ImageXObject<'a> { height: u32, color_space: Option, cache: Cache, + /// PdfCraft patch: decoded pixels shared across renders (see `decoded_raster`); `None` + /// for an image that is never cached (an inline image). + decoded_cache: Option, interpolate: bool, is_mask: bool, is_stencil_mask: bool, @@ -284,6 +289,7 @@ impl<'a> ImageXObject<'a> { resolve_cs: impl FnOnce(&Name<'_>) -> Option, warning_sink: &WarningSinkFn, cache: &Cache, + decoded_cache: Option<&DecodedImageCache>, mut is_mask: bool, transfer_function: Option, ) -> Option { @@ -329,6 +335,7 @@ impl<'a> ImageXObject<'a> { Some(Self { width, cache: cache.clone(), + decoded_cache: decoded_cache.cloned(), height, color_space: image_cs, warning_sink: warning_sink.clone(), @@ -345,7 +352,16 @@ impl<'a> ImageXObject<'a> { return None; } - decode_mask(self, target_dimension) + // PdfCraft patch: decode once per `DecodedImageKey`; tiles and scales share it. + let Some(cache) = &self.decoded_cache else { + return decode_mask(self, target_dimension); + }; + cache.get_or_insert_with( + self.decode_key(cache, true, target_dimension), + MAX_DECODED_IMAGE_BYTES, + decoded_mask_size, + || decode_mask(self, target_dimension), + ) } pub(crate) fn decoded_raster( @@ -356,7 +372,44 @@ impl<'a> ImageXObject<'a> { return None; } - decode_raster(self, target_dimension) + // PdfCraft patch: decode once per `DecodedImageKey`; tiles and scales share it. + let Some(cache) = &self.decoded_cache else { + return decode_raster(self, target_dimension); + }; + cache.get_or_insert_with( + self.decode_key(cache, false, target_dimension), + MAX_DECODED_IMAGE_BYTES, + decoded_raster_size, + || decode_raster(self, target_dimension), + ) + } + + /// PdfCraft patch: the cache key of decoding this image (as a mask when `mask_output`) at + /// `target_dimension`: every input of the decode (see [`DecodedImageKey`]). `None` when one + /// of them can't be keyed cheaply (a transfer function): such a decode isn't cached. + pub(crate) fn decode_key( + &self, + cache: &DecodedImageCache, + mask_output: bool, + target_dimension: Option<(u32, u32)>, + ) -> Option { + if self.transfer_function.is_some() { + return None; + } + let dict = self.stream.dict(); + let bytes = dict.data(); + Some(DecodedImageKey { + mask_output, + dict_at: bytes.as_ptr() as usize, + dict_len: bytes.len(), + dict_hash: self.stream.cache_key(), + obj: dict.obj_id().map(|id| (id.obj_number, id.gen_number)), + is_mask: self.is_mask, + is_stencil_mask: self.is_stencil_mask, + interpolate: self.interpolate, + target: target_dimension, + preview: cache.preview, + }) } pub(crate) fn width(&self) -> u32 { @@ -378,10 +431,35 @@ impl<'a> ImageXObject<'a> { } } +/// PdfCraft patch: budget for [`DecodedImageCache`] entries per interpreter cache: decoded +/// image pixels shared across tiles and scales. Past it the cache starts a fresh epoch. +const MAX_DECODED_IMAGE_BYTES: usize = 64 << 20; + +/// PdfCraft patch: pixel bytes held by a decoded mask. +fn decoded_mask_size(mask: &DecodedMask) -> usize { + mask.luma.width as usize * mask.luma.height as usize +} + +/// PdfCraft patch: pixel bytes held by a decoded raster image and its mask. +fn decoded_raster_size(raster: &DecodedRaster) -> usize { + let image = match &raster.image { + ImageData::Rgb(d) => d.width as usize * d.height as usize * 3, + ImageData::Luma(d) => d.width as usize * d.height as usize, + }; + let alpha = raster + .alpha + .as_ref() + .map(|a| a.width as usize * a.height as usize) + .unwrap_or(0); + image.saturating_add(alpha) +} + +#[derive(Clone)] pub(crate) struct DecodedMask { pub(crate) luma: LumaData, } +#[derive(Clone)] pub(crate) struct DecodedRaster { pub(crate) image: ImageData, pub(crate) alpha: Option, @@ -754,7 +832,7 @@ fn resolve_alpha( .get::>(SMASK) .or_else(|| dict.get::>(MASK)) { - let obj = ImageXObject::new(&s_mask, |_| None, &obj.warning_sink, &obj.cache, true, None)?; + let obj = ImageXObject::new(&s_mask, |_| None, &obj.warning_sink, &obj.cache, obj.decoded_cache.as_ref(), true, None)?; decode_mask(&obj, target_dimension).map(|decoded| decoded.luma) } else if let Some(color_key_mask) = dict.get::>(MASK) { @@ -814,7 +892,7 @@ fn resolve_matte( let mut matte_rgb = [0_u8; 3]; color_space.convert_f32(&matte, &mut matte_rgb, false); - let mask_obj = ImageXObject::new(&s_mask, |_| None, &obj.warning_sink, &obj.cache, true, None)?; + let mask_obj = ImageXObject::new(&s_mask, |_| None, &obj.warning_sink, &obj.cache, obj.decoded_cache.as_ref(), true, None)?; let alpha = decode_mask(&mask_obj, target_dimension)?.luma; Some((alpha, matte_rgb)) diff --git a/vendor/hayro/src/lib.rs b/vendor/hayro/src/lib.rs index 908f7550..f87d74e9 100644 --- a/vendor/hayro/src/lib.rs +++ b/vendor/hayro/src/lib.rs @@ -194,7 +194,7 @@ pub fn render_into<'a>( render_mode: RenderMode::OptimizeSpeed, }; - let mut device = Renderer::new(pix_width, pix_height, vc_settings, cache); + let mut device = Renderer::new(pix_width, pix_height, vc_settings, cache, interpreter_settings.preview); device.ctx.set_paint(render_settings.bg_color); device diff --git a/vendor/hayro/src/renderer.rs b/vendor/hayro/src/renderer.rs index 56e4fc63..b4ee1b68 100644 --- a/vendor/hayro/src/renderer.rs +++ b/vendor/hayro/src/renderer.rs @@ -310,6 +310,10 @@ fn control_polygon_length(path: &BezPath) -> (f64, usize) { /// Fills are flattened into chords, which stay inside the outline's bounds. const DRAW_SLACK: f64 = 0.5; +/// PdfCraft patch: previews skip images below this target area in device pixels: redrawing +/// them at full detail costs a decode and a resample each for pixels nobody can see. +const PREVIEW_MIN_IMAGE_AREA: u32 = 16; + thread_local! { /// PdfCraft patch: whether [`may_paint_canvas`] may skip paths on this thread. Turned off /// only by PdfCraft's regression test, which renders each tile with and without skipping. @@ -322,6 +326,23 @@ pub fn set_skip_offscreen_paths(skip: bool) { SKIP_OFFSCREEN_PATHS.with(|s| s.set(skip)); } +/// PdfCraft patch: whether a device-space box can paint a pixel of a `width` × `height` +/// canvas. A box whose bounds miss the canvas is skipped without changing a pixel. +/// Bounds that aren't finite are drawn as before. +fn paints_canvas(device: Rect, width: u16, height: u16) -> bool { + if !SKIP_OFFSCREEN_PATHS.with(std::cell::Cell::get) { + return true; + } + let finite = [device.x0, device.y0, device.x1, device.y1] + .iter() + .all(|v| v.is_finite()); + !finite + || (device.x0 < f64::from(width) + && device.x1 > 0.0 + && device.y0 < f64::from(height) + && device.y1 > 0.0) +} + /// PdfCraft patch: whether a path drawn with `transform` can paint a pixel of a `width` × /// `height` canvas. `reach` is how far its paint extends beyond the path, in user space: 0 for a /// fill; for a stroke, half its width times the larger of its miter limit (kurbo draws a miter @@ -338,9 +359,6 @@ fn may_paint_canvas( width: u16, height: u16, ) -> bool { - if !SKIP_OFFSCREEN_PATHS.with(std::cell::Cell::get) { - return true; - } let mut points = Rect::new( f64::INFINITY, f64::INFINITY, @@ -367,24 +385,34 @@ fn may_paint_canvas( let device = transform .transform_rect_bbox(points.inflate(grow, grow)) .inflate(2.0, 2.0); - let finite = [device.x0, device.y0, device.x1, device.y1] - .iter() - .all(|v| v.is_finite()); - !finite - || (device.x0 < f64::from(width) - && device.x1 > 0.0 - && device.y0 < f64::from(height) - && device.y1 > 0.0) + paints_canvas(device, width, height) +} + +/// PdfCraft patch: whether a `rect` drawn with `transform` can paint a pixel of a `width` × +/// `height` canvas. Used for images: decoded data can only shrink against the stream-size box +/// (subsampled `scale_factors`), so the box over-approximates what is drawn and skipping it +/// never changes a pixel, while a tile of an image-heavy page no longer decodes, resamples and +/// rasterizes every image on the page. +fn may_paint_rect(rect: Rect, transform: Affine, width: u16, height: u16) -> bool { + let device = transform + .transform_rect_bbox(rect.inflate(DRAW_SLACK, DRAW_SLACK)) + .inflate(2.0, 2.0); + paints_canvas(device, width, height) } pub(crate) struct Renderer { pub(crate) ctx: RenderContext, pub(crate) inside_pattern: bool, + /// Soft masks drawn during this render. PdfCraft note: it belongs to one `Renderer` (one + /// render, every nested renderer starting empty) and so to one `preview` setting: a mask + /// drawn for a preview, which may leave out tiny images, never reaches a final render. pub(crate) soft_mask_cache: HashMap, pub(crate) outline_cache: Rc>>>, pub(crate) cur_mask: Option, pub(crate) in_type3_glyph: bool, pub(crate) scaler: Scaler, + /// PdfCraft patch: low-detail preview render (see `InterpreterSettings::preview`). + pub(crate) preview: bool, // TODO: Remove this once vello_cpu bug with non-transparent images is fixed image_transparency_stack: Vec, } @@ -402,6 +430,7 @@ impl Renderer { height: u16, settings: RenderSettings, cache: &RenderCache<'_>, + preview: bool, ) -> Self { Self { ctx: RenderContext::new_with(width, height, settings), @@ -411,6 +440,7 @@ impl Renderer { cur_mask: None, in_type3_glyph: false, scaler: Scaler::new(ResamplingFunction::CatmullRom), + preview, image_transparency_stack: Vec::new(), } } @@ -505,6 +535,7 @@ impl Renderer { cur_mask: None, in_type3_glyph: false, scaler: self.scaler, + preview: self.preview, image_transparency_stack: Vec::new(), }; let mut mask_pix = Pixmap::new(self.ctx.width(), self.ctx.height()); @@ -1044,6 +1075,7 @@ impl Renderer { outline_cache: self.outline_cache.clone(), in_type3_glyph: false, scaler: self.scaler, + preview: self.preview, image_transparency_stack: Vec::new(), }; let mut initial_transform = Affine::scale_non_uniform(xs as f64, ys as f64) @@ -1218,6 +1250,16 @@ impl Renderer { impl<'a> Device<'a> for Renderer { fn draw_image(&mut self, image: hayro_interpret::Image<'a, '_>, mut transform: Affine) { + // PdfCraft patch: see `may_paint_rect`. Checked before decoding: off-canvas images + // cost nothing, so a tile no longer decodes every image on its page. + if !may_paint_rect( + Rect::new(0.0, 0.0, f64::from(image.width()), f64::from(image.height())), + transform, + self.ctx.width(), + self.ctx.height(), + ) { + return; + } self.ctx.set_paint_transform(Affine::IDENTITY); self.ctx.set_aliasing_threshold(Some(1)); @@ -1230,6 +1272,18 @@ impl<'a> Device<'a> for Renderer { .length() .ceil() as u32; + // PdfCraft patch: previews skip images that would cover only a few device pixels. + // Final renders always draw them. (Measured as axis lengths: the `target_*` above + // map a point, so they include the translation and only suit the decode hint.) + if self.preview { + let c = transform.as_coeffs(); + let tiny_w = (f64::from(image.width()) * (c[0] * c[0] + c[1] * c[1]).sqrt()).ceil() as u32; + let tiny_h = (f64::from(image.height()) * (c[2] * c[2] + c[3] * c[3]).sqrt()).ceil() as u32; + if tiny_w.saturating_mul(tiny_h) < PREVIEW_MIN_IMAGE_AREA { + return; + } + } + match image { hayro_interpret::Image::Stencil(s) => { s.with_stencil( @@ -1315,6 +1369,7 @@ impl<'a> Device<'a> for Renderer { cur_mask: None, in_type3_glyph: false, scaler: self.scaler, + preview: self.preview, image_transparency_stack: Vec::new(), }; let mut sub_pix = Pixmap::new(width, height); @@ -1399,7 +1454,7 @@ impl<'a> Device<'a> for Renderer { self.soft_mask_cache .entry(m.cache_key()) - .or_insert_with(|| draw_soft_mask(&m, settings, width, height)) + .or_insert_with(|| draw_soft_mask(&m, settings, width, height, self.preview)) .clone() }), None, @@ -1425,7 +1480,7 @@ impl<'a> Device<'a> for Renderer { self.soft_mask_cache .entry(m.cache_key()) - .or_insert_with(|| draw_soft_mask(&m, settings, width, height)) + .or_insert_with(|| draw_soft_mask(&m, settings, width, height, self.preview)) .clone() }); if let Some(mask) = self.cur_mask.clone() { @@ -1585,7 +1640,7 @@ fn render_shading_texture( ) } -fn draw_soft_mask(mask: &SoftMask<'_>, settings: RenderSettings, width: u16, height: u16) -> Mask { +fn draw_soft_mask(mask: &SoftMask<'_>, settings: RenderSettings, width: u16, height: u16, preview: bool) -> Mask { let mut renderer = Renderer { ctx: RenderContext::new_with(width, height, derive_settings(&settings)), inside_pattern: false, @@ -1594,6 +1649,7 @@ fn draw_soft_mask(mask: &SoftMask<'_>, settings: RenderSettings, width: u16, hei outline_cache: Rc::new(std::cell::RefCell::new(HashMap::new())), in_type3_glyph: false, scaler: Scaler::new(ResamplingFunction::CatmullRom), + preview, image_transparency_stack: Vec::new(), }; From b91d38c0738927ed195ed7b3646a34fbdfb68ba5 Mon Sep 17 00:00:00 2001 From: Zeyu Li Date: Mon, 12 Oct 2026 05:53:55 +0800 Subject: [PATCH 8/9] M14: text the fonts can't draw is kept and reported, never written as ? silently (#1112) * M14: text the fonts can't draw is kept and reported, never written as ? silently Form values and captions, text boxes, callouts, Fill & Sign text, measurement labels and stamp by-lines keep text the standard fonts can't draw, draw ? for it, mark the annotation (PCUndrawn) and report the edit (interface notice, tools' warnings); forms set /NeedAppearances (not in PDF/A). Flattening, redaction overlay text, typed and visible signatures and create-from-text refuse instead. Reports escape such characters as [U+XXXX]. * Catalogs: the two new can't-draw messages in Czech, Slovene, Kazakh, Turkish and Greek; refresh hashes --------- Co-authored-by: Brandon Thomas --- ATTRIBUTION.toml | 38 ++++---- ROADMAP.md | 2 + crates/annot/README.md | 9 +- crates/annot/src/appearance.rs | 22 ++++- crates/annot/src/lib.rs | 63 +++++++++++--- crates/annot/src/tests.rs | 87 ++++++++++++++++++ crates/automation/src/comments.rs | 8 ++ crates/automation/src/lib.rs | 20 +++-- crates/automation/src/signing.rs | 8 ++ crates/automation/src/tools.rs | 6 +- crates/automation/tests/automation.rs | 53 +++++++++++ crates/create/README.md | 6 +- crates/create/src/lib.rs | 44 +++++++++- crates/create/src/tests.rs | 26 ++++++ crates/edit/src/flatten.rs | 39 ++++++++- crates/edit/src/lib.rs | 6 -- crates/edit/src/tests.rs | 52 +++++++++++ crates/engine/src/compare.rs | 2 +- crates/engine/src/lib.rs | 121 ++++++++++++++++++++++++-- crates/engine/src/tests.rs | 28 ++++++ crates/engine/src/xfa.rs | 1 + crates/fonts/README.md | 8 +- crates/fonts/src/lib.rs | 87 +++++++++++++++++- crates/fonts/src/script.rs | 25 +++++- crates/forms/README.md | 10 ++- crates/forms/src/appearance.rs | 46 ++++++++-- crates/forms/src/author.rs | 31 +++++-- crates/forms/src/lib.rs | 63 +++++++++++++- crates/forms/src/tests.rs | 109 ++++++++++++++++++++++- crates/ocr/src/lib.rs | 8 ++ crates/redact/src/lib.rs | 11 +++ crates/redact/src/tests.rs | 33 +++++++ crates/sign/src/pdf.rs | 26 +++++- crates/sign/tests/pdf.rs | 23 +++++ crates/ui-egui/src/content_ui.rs | 13 +++ crates/ui-egui/src/editing.rs | 28 ++++++ crates/ui-egui/src/i18n/ar.tsv | 2 + crates/ui-egui/src/i18n/bg.tsv | 2 + crates/ui-egui/src/i18n/cs.tsv | 2 + crates/ui-egui/src/i18n/de.tsv | 2 + crates/ui-egui/src/i18n/el.tsv | 2 + crates/ui-egui/src/i18n/es.tsv | 2 + crates/ui-egui/src/i18n/fr.tsv | 2 + crates/ui-egui/src/i18n/hu.tsv | 2 + crates/ui-egui/src/i18n/it.tsv | 2 + crates/ui-egui/src/i18n/ja.tsv | 2 + crates/ui-egui/src/i18n/kk.tsv | 2 + crates/ui-egui/src/i18n/pl.tsv | 2 + crates/ui-egui/src/i18n/pt-br.tsv | 2 + crates/ui-egui/src/i18n/ru.tsv | 2 + crates/ui-egui/src/i18n/sl.tsv | 2 + crates/ui-egui/src/i18n/tr.tsv | 2 + crates/ui-egui/src/i18n/uk.tsv | 2 + crates/ui-egui/src/i18n/zh-hans.tsv | 2 + crates/ui-egui/src/i18n/zh-hant.tsv | 2 + crates/xfa/src/pdf.rs | 12 +++ crates/xfa/src/tests.rs | 19 ++++ 57 files changed, 1149 insertions(+), 82 deletions(-) diff --git a/ATTRIBUTION.toml b/ATTRIBUTION.toml index 80866b35..c8c9b2a3 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 = "23a9402d3ebbcff69aed133706e6e428a804ae06a7dba4923892cca5c98e92d1" +sha256 = "bf6e968d781894c6641df2b52224a2b273b33ab4110b05d5b92ea67d80753d19" # 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 = "1863437c886142b2bccce3f44a538ae80d0ceea81074b84dfdfe8d9a4a4227ab" +sha256 = "eebd627422ee48d52e35655c41e8a587e4d86c38fcb56d5465d8446d5971dabf" [[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 = "6c8604d5cd71412c6a673b75001c7ef629727839eca1449147c7d6ec4bf0be99" +sha256 = "2f11d8535473dfdefc0aea011d3079eaada0286a58982a7084e20bda8477de54" [[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 = "5ae2e315a9b99b4c0dfaf7be019c4d430ace17b4fbbfea5a2779b3e972846a21" +sha256 = "7d3f5d26df0596f96f5ff0e7cccabbdf1f0c2278062cfa0a1046b364b04c7ba7" [[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 = "8b9e22fa25339e4aedb0542b8bf6f68dfd97bc3634105199e1951475e30eed53" +sha256 = "d1a7742f43f01253d40f339c525ad0e2c15c18be1a60d5cc4c8a7bebbebcf3de" [[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 = "6d81592677f2b4eb3b97ba5810de9752da969545d91897e27033f37310696ffc" +sha256 = "819ab9c8426bf3d1fc2c7021f4b5a19d41f8b8cc65301da625ecb12a2297fc8c" [[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 = "fb9c91a98c6975bc24249e8ae750fd46b9245ac5d2d7381f1e21319b9adbcaac" +sha256 = "258518681eb774c7b90335f1d2089bf1bb37831084cf8121f3bbea75931e3f6b" [[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 = "f36aefae1a1ba6d576a4647fbfdd65b9b5086bfb266ea260e513244adc7af05a" +sha256 = "404766adf3b492801346390914884c1b75d97e2b4e9e3dcd9239fc8062e7332c" [[asset]] path = "crates/ui-egui/src/i18n/te.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 = "14884c08a550600d310a794ceb0b2bb864987a074178ee49b657578b66625bef" +sha256 = "b5ad0282d7a1ddb4e818c7f8a76c2182bd3c9343b8a555b0dae7b78b831b594a" [[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 = "be960bdf9b6eeac2d95db91325076ee79c9b2efdcc4e459c7400e7ddb05fab3a" +sha256 = "71dfed7bedeff0755c7377b7244ccfe57d9958cc803202c52b04da58bce9b1dd" [[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 = "bada9b6f0b3775e4dab1213d9759070f7551d8a6142de10159268c88a04d3f0c" +sha256 = "5d0bfe97ef2ef2d1577c538793fc3267483f5c9fe9de4df97924d04cf18f7292" [[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 = "efb75d8575df9e1dd4610602f20545b3a14a3a0814060efc27eb7cab591ccccb" +sha256 = "b69b54cbeee8c9a876db5ed7d49e056e794742bd0a001a2c6383d464e24c31e4" [[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 = "ca305b188abc8d9c261901dde10f7cd8518520983fdf88d2358374b352ad3d5a" +sha256 = "db5ba8dc27fb272e2a76e017c5bb38bad200bb7299b9cce40d94d837704507fb" [[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 = "77afe7d7fa215c99e8e627a758178d79c51e08e33564e00b97b8840aeb91a9dd" +sha256 = "3c94bc6bb9950d8172c3f43c32d09496a382eea32717fcee3855d338881f35fa" [[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 = "f004b06f4e7eb8f4a99e22e1abfd97c95795b2215080e4008574522e54eda74c" +sha256 = "10ff47a04951e99f902efeb9832d12b78f5f632d54503843283adfd2ad9e8186" [[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 = "cd9ecaf661533cbd3b055cfd3994602f6739c83061d1e225ba43ee6f229068cf" +sha256 = "983c0cc2efa98d2d3f0449f45c7f7ebdd39c93578b77a07631ee5613b0989a98" [[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 = "a8a66390b2d80b4708c4f854318b923eef7f0f5932439fb5ef25f2651980f24e" +sha256 = "439ad5e6ae9c3e2484b1a5f9f11868ff33ddb150308d32d8c32691f2817c15a6" [[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 = "5688538845a80c154feabd096badbf097485dea8a7402fede4ef7d71466e9d6a" +sha256 = "b4e90e5929a95a8c85cf88099003cb772e45591759e192f89026cd883a8149c7" [[asset]] path = "assets/icons/accessibility.svg" @@ -3339,4 +3339,4 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Slovene UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "8590f5acce1da70d6a3ce58de073bf4f86815651ff74a59625980c6c52e051b7" +sha256 = "be33bd6c0f2952eafb04d22cbc71f4e43594bceecc0acaca9cfd2b602100c29c" diff --git a/ROADMAP.md b/ROADMAP.md index bfe8ac0d..923639fc 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -161,6 +161,8 @@ Newest first: the date, what moved, and the new overall estimate. - **2026-10-11 (M14, #307, Plan 2):** Forms now return terminal fields and the page-widget adoption count from one enumeration, and engine open reuses that result while retaining XFA script-button marking. The 12,000-page/no-form PDF (1,442,052 bytes) and 2,500-orphan-widget PDF (673,432 bytes) produce byte-identical `doc_open`/`doc_info` outputs against upstream `33c14e07cfd742a82bf03baa6b103c01008fa68c`, including the orphan repair note. After one warm-up per binary and three interleaved base/candidate rounds, median open times were 0.16/0.15 s and 0.08/0.07 s, with median peak RSS 62.69/59.98 MiB and 33.28/32.20 MiB. Workspace tests, Clippy, formatting, focused forms/engine/organize tests, and the WASM check pass. A behavior-preserving local binding in `organize` also lets the workspace Clippy gate pass. No estimate change. - **2026-10-11 (M14, Traditional Chinese semantics):** Corrected accessibility pass/fail labels, the inverted script-accessibility rule, Taiwan row/column terminology and form choice/read-only labels; added the missing table-row explanation. Catalog regressions cover the rule metadata and preserve field names. Milestone and effort estimates unchanged. + +- **2026-10-11 (M14, no silent `?`):** Text PdfCraft's fonts can't draw (Chinese, Greek, Czech's ř in Helvetica) is never written into a PDF as `?` without a word. Form values and button captions, text boxes, callouts, Fill & Sign text, measurement labels and dynamic stamps' by-lines are kept as typed, as Acrobat keeps them: the appearance draws `?` there, the annotation is marked (`PCUndrawn`), and the edit says so (the interface shows a translated notice; tools return `warnings`). A form also sets `/NeedAppearances`, so other viewers redraw the field from its value (not in a PDF/A document, which forbids it); a restyle doesn't replace a comment another program drew with those characters. Where `?` would be permanent, the edit is refused with the character and what to do instead: flattening what PdfCraft drew with `?`, redaction overlay text (also checked before anything is redacted), typed signatures through `fill_sign_add`, visible signature details, and new documents from text (with the line). Reports PdfCraft writes from documents (comment summaries, compare reports) write such characters as `[U+XXXX]` and say so; a dynamic XFA form's own text is laid out and reported. One end-to-end test drives every path through the tools. Not yet: drawing those characters (embedding a font that has them). Overall estimate unchanged. - **2026-10-10 (M14, interface size):** Preferences ▸ Interface size scales the whole interface (50–250 %, setting `ui_scale`, control option `ui-scale`); 50 % brings an interface the system draws at 200 % back to 100 % (#456). Auto, the default, follows the desktop's text scaling: on Linux and the BSDs the desktop app reads GNOME's `text-scaling-factor` once at launch, so with Large Text (1.25) on a 1× display the interface no longer draws 13 px labels that show their pixels next to the system's text. The size is egui's zoom factor, so `pixels_per_point` grows with it and page rasters stay one texel per screen pixel; egui's own ⌘+/⌘− interface zoom is turned off so those keys stay page zoom. Still missing: KDE's font DPI and Windows' "Make text bigger" aren't read (both scale only text there), and egui's text is still greyscale-antialiased without the system's gamma, so small text looks rougher than GTK's at 100 %. - **2026-10-10 (M14, #496):** Shortcut displays share a UI formatter across toolbar search, registry menus, command palette, Help and tooltips. Modifier and key names use a dedicated catalog context in every bundled translation; missing entries keep the English key name rather than a command translation. German shows Strg/Umschalt/Entf, macOS retains its symbols, and runtime language changes refresh the display. Regression coverage checks localized layout widths, English/German switching, platform conventions and unchanged command execution. Milestone and effort estimates unchanged. diff --git a/crates/annot/README.md b/crates/annot/README.md index 4d0fa7c9..67285f8d 100644 --- a/crates/annot/README.md +++ b/crates/annot/README.md @@ -10,7 +10,14 @@ Comments (annotations), ISO 32000-2 §12.5. Layer L3, depends on `pdfcraft-cos`, - **Appearance streams** (`appearance::build`) generated from the annotation dictionary, so new and restyled comments look the same in every viewer. Highlights multiply (`/BM /Multiply`). Note icons are PdfCraft's own drawings. Text boxes use standard Helvetica (WinAnsi), with - line breaking by approximate Helvetica proportions (no font program is bundled). + line breaking by approximate Helvetica proportions (no font program is bundled). Text it can't + draw (in a text box, callout, Fill & Sign text, measurement label or dynamic stamp's by-line; + `appearance::undrawable_text`) is kept as typed and drawn as `?`, and the comment is marked with + the first such character (`pdfcraft_fonts::UNDRAWN_MARK`) so the engine reports the edit and + flattening refuses to make it permanent. Two refusals (`AnnotError::Undrawable`): a redaction + mark's overlay text, which becomes page content when applied; and a restyle or resize of a + comment another program drew with a font that has those characters, whose appearance is kept + rather than redrawn with `?` (new text for it is taken). - **Edits:** reply (`/IRT`), set review status (a `/State` reply, as Acrobat's "Set status" does), change text, move, resize, restyle, delete (with pop-up and replies). diff --git a/crates/annot/src/appearance.rs b/crates/annot/src/appearance.rs index fbd77b3e..8aeaa1b3 100644 --- a/crates/annot/src/appearance.rs +++ b/crates/annot/src/appearance.rs @@ -11,8 +11,8 @@ //! Helvetica's proportions by character class (no font program or metrics file is bundled). use pdfcraft_cos::{Dict, Object, PdfString, Stream}; +use pdfcraft_fonts::{first_undrawable, literal, win_ansi}; pub use pdfcraft_fonts::{helvetica_width as text_width, wrap}; -use pdfcraft_fonts::{literal, win_ansi}; use crate::{NOTE_SIZE, Rgb, n}; @@ -264,7 +264,27 @@ pub(crate) fn picture_turn(d: &Dict) -> i64 { d.get(b"PCPictureRotate").and_then(Object::as_int).unwrap_or(0).rem_euclid(360) / 90 * 90 } +/// The first text [`build`] would draw in the standard fonts (Helvetica, WinAnsiEncoding) that +/// they can't, which would come out as `?`: what it is (for the message) and the character. The +/// text of a text box, callout or Fill & Sign text, a measurement's label, a dynamic stamp's +/// "By …" line, and a redaction mark's overlay text (drawn when it is applied). `None` when all of +/// it can be drawn. +pub fn undrawable_text(d: &Dict) -> Option<(&'static str, char)> { + let text = |key: &[u8]| d.get(key).and_then(Object::as_string).map(PdfString::to_text); + let (what, text) = match d.name(b"Subtype")? { + b"FreeText" => ("the comment's text", text(b"Contents")?), + b"Line" | b"PolyLine" | b"Polygon" => ("the measurement label", text(b"PCMeasureValue")?), + b"Stamp" if matches!(d.get(b"PCStamp"), Some(Object::Bool(true))) => ("the stamp's \"By\" line", text(b"PCByLine")?), + // Drawn in Helvetica when the redaction is applied: refused when it is marked. + b"Redact" => ("the redaction's overlay text", text(b"OverlayText")?), + _ => return None, + }; + Some((what, first_undrawable(&text)?)) +} + /// Draw the normal appearance of an annotation, or `None` if this subtype/variant isn't supported. +/// Text the standard fonts can't draw comes out as `?`: [`undrawable_text`] says which, and +/// [`crate::set_appearance`] marks the annotation with it (`pdfcraft_fonts::UNDRAWN_MARK`). pub fn build(d: &Dict) -> Option { // Imported measurement appearances may include leaders, captions and formatting that // this builder cannot reproduce. Preserve them rather than silently losing detail. diff --git a/crates/annot/src/lib.rs b/crates/annot/src/lib.rs index 00e8776b..642969c5 100644 --- a/crates/annot/src/lib.rs +++ b/crates/annot/src/lib.rs @@ -34,6 +34,11 @@ pub enum AnnotError { Invalid(String), #[error("{0} comments can't be restyled yet (their appearance can't be regenerated)")] Unsupported(String), + /// Text the standard fonts can't draw where `?` would be permanent or replace a good drawing: + /// a redaction's overlay text, or an edit that would redraw a comment another program drew + /// with a font that has those characters. The message names the character. + #[error("{0}")] + Undrawable(String), #[error("{0}")] Cos(#[from] pdfcraft_cos::CosError), } @@ -1202,8 +1207,16 @@ pub fn add_annotation(doc: &mut Document, new: &NewAnnotation, meta: &Meta) -> R // Survives a later subject edit, so flatten-on-save still finds the mark. d.set(b"PCFillSign".to_vec(), Object::Bool(true)); } + // A redaction's overlay text becomes page content when it is applied, where `?` would be there + // for good: refused before anything is added. A comment's own text is stored as typed and + // drawn with `?` for what the standard fonts can't draw (marked, see `draw_appearance`). + if d.name(b"Subtype") == Some(b"Redact") + && let Some((what, c)) = appearance::undrawable_text(&d) + { + return Err(AnnotError::Undrawable(pdfcraft_fonts::undrawable_message(what, c))); + } let r = doc.add(Object::Dict(d.clone())); - set_appearance(doc, r)?; + draw_appearance(doc, r)?; let mut list = annots(doc, page); let index = list.len(); list.push(Object::Ref(r)); @@ -1250,9 +1263,33 @@ pub fn orient_image_stamp(doc: &mut Document, page: usize, index: usize, rotatio Ok(()) } -/// (Re)generate `/AP /N` for the annotation `r` from its dictionary. -/// Regenerate an annotation's normal appearance from its dictionary. +/// (Re)generate `/AP /N` for the annotation `r` from its dictionary, for an edit that keeps its +/// text (a restyle, a new size or colour). +/// +/// A comment whose text the standard fonts can't draw, with an appearance another program drew +/// (with a font that has those characters), keeps that appearance: redrawing it would put `?` in +/// their place, so the edit is refused (`AnnotError::Undrawable`). One PdfCraft drew with `?` +/// (marked `pdfcraft_fonts::UNDRAWN_MARK`) is redrawn. pub fn set_appearance(doc: &mut Document, r: ObjRef) -> Result<(), AnnotError> { + let d = annot_dict(doc, r); + let drawn_elsewhere = + d.get(b"AP").is_some_and(|ap| doc.resolve(ap).as_dict().is_some_and(|ap| ap.contains(b"N"))) && pdfcraft_fonts::undrawn_mark(&d).is_none(); + if drawn_elsewhere && let Some((what, c)) = appearance::undrawable_text(&d) { + return Err(AnnotError::Undrawable(format!( + "PdfCraft can't redraw this comment: the standard fonts can't draw \"{}\" (U+{:04X}) in {what}, which would then show as ? (the comment was left as it is)", + c.escape_debug(), + u32::from(c) + ))); + } + draw_appearance(doc, r) +} + +/// Draw `r`'s normal appearance from its dictionary, whatever its text: characters the standard +/// fonts can't draw come out as `?`, and the annotation is marked (`pdfcraft_fonts::UNDRAWN_MARK`, +/// cleared when all of it can be drawn) so the edit is reported and flattening won't make that +/// `?` permanent. For new text (a new comment, changed contents), as Acrobat keeps text its font +/// can't show, and for display-only copies. +fn draw_appearance(doc: &mut Document, r: ObjRef) -> Result<(), AnnotError> { let d = annot_dict(doc, r); let subtype = String::from_utf8_lossy(d.name(b"Subtype").unwrap_or_default()).into_owned(); let Some(stream) = build_appearance(doc, &d) else { return Err(AnnotError::Unsupported(subtype)) }; @@ -1280,6 +1317,8 @@ fn build_appearance(doc: &Document, d: &Dict) -> Option { /// Store a built appearance on the annotation: the cheap half of [`set_appearance`]. fn apply_appearance(doc: &mut Document, r: ObjRef, d: &Dict, stream: Stream) -> Result<(), AnnotError> { + // Characters the standard fonts can't draw came out as `?`: mark the annotation with the first. + let missing = appearance::undrawable_text(d).map(|(_, c)| c); let ap = doc.add(Object::Stream(stream)); // A copy (a shared /AP is left alone) that keeps unknown entries; the old down and rollover // appearances would show the previous look on press or hover, so they go with the old /N. @@ -1290,6 +1329,7 @@ fn apply_appearance(doc: &mut Document, r: ObjRef, d: &Dict, stream: Stream) -> doc.update_dict(r, |d| { d.set(b"AP".to_vec(), Object::Dict(apd)); d.remove(b"AS"); + pdfcraft_fonts::set_undrawn_mark(d, missing); })?; Ok(()) } @@ -1489,10 +1529,12 @@ pub fn set_contents(doc: &mut Document, page: usize, index: usize, text: &str, m })?; if free_text { // The box grows and shrinks with the text instead of clipping it. A locked box keeps its - // text editable (as in Acrobat), so the re-fit ignores the lock. + // text editable (as in Acrobat), so the re-fit ignores the lock. New text is drawn even + // where the standard fonts can't draw some of it (marked and reported, see + // `draw_appearance`). match fitted_box(doc, r, text) { - Some(rect) => apply_text_box(doc, r, rect, meta)?, - None => set_appearance(doc, r)?, + Some(rect) => apply_text_box(doc, r, rect, meta, true)?, + None => draw_appearance(doc, r)?, } } Ok(()) @@ -1700,7 +1742,7 @@ pub fn set_rect(doc: &mut Document, page: usize, index: usize, rect: [f64; 4], m if !matches!(subtype.as_str(), "Square" | "Circle" | "FreeText") { return Err(AnnotError::Invalid(format!("{subtype} comments can't be resized"))); } - apply_text_box(doc, r, rect, meta) + apply_text_box(doc, r, rect, meta, false) } /// Scale a drawing's strokes into `rect`. The margin between the strokes and the old `/Rect` (half @@ -1750,8 +1792,9 @@ fn resize_ink(doc: &mut Document, r: ObjRef, rect: [f64; 4], meta: &Meta) -> Res } /// Store `rect` as a FreeText annotation's text box (or a square's/oval's rectangle): a callout's -/// leader line re-attaches and `/Rect` grows to hold it, and the appearance is redrawn. -fn apply_text_box(doc: &mut Document, r: ObjRef, rect: [f64; 4], meta: &Meta) -> Result<(), AnnotError> { +/// leader line re-attaches and `/Rect` grows to hold it, and the appearance is redrawn +/// (`new_text`: for changed contents, see [`set_appearance`] and `draw_appearance`). +fn apply_text_box(doc: &mut Document, r: ObjRef, rect: [f64; 4], meta: &Meta, new_text: bool) -> Result<(), AnnotError> { let rect = normalize(rect); if !finite(&rect) || rect[2] - rect[0] < 1.0 || rect[3] - rect[1] < 1.0 { return Err(AnnotError::Invalid("invalid rectangle (too small)".into())); @@ -1782,7 +1825,7 @@ fn apply_text_box(doc: &mut Document, r: ObjRef, rect: [f64; 4], meta: &Meta) -> } touch(d, meta); })?; - set_appearance(doc, r) + if new_text { draw_appearance(doc, r) } else { set_appearance(doc, r) } } fn border_width_of(d: &Dict) -> f64 { diff --git a/crates/annot/src/tests.rs b/crates/annot/src/tests.rs index 589a6abb..c00ed2be 100644 --- a/crates/annot/src/tests.rs +++ b/crates/annot/src/tests.rs @@ -1159,6 +1159,93 @@ fn drawings_resize_by_scaling_their_strokes() { assert!(matches!(set_rect(&mut doc, 0, i, [100.0, 400.0, 101.0, 401.0], &meta("")), Err(AnnotError::Invalid(_)))); } +/// Comment text the standard fonts can't draw is kept, as Acrobat keeps it: a text box, Fill & Sign +/// text or a dynamic stamp's by-line is added with its text as typed, its appearance draws `?` for +/// those characters, and the comment is marked with the first one (cleared once all of it can be +/// drawn). A comment another program drew with a font that has those characters keeps that +/// appearance: a restyle is refused rather than replace it with `?`, while new text is taken. +#[test] +fn comment_text_the_standard_fonts_cannot_draw_is_kept_and_marked() { + let mut doc = fixture(); + let mark = |doc: &Document, i: usize| pdfcraft_fonts::undrawn_mark(&list(doc, 0)[i]); + let mut boxed = new(0, Shape::TextBox { rect: [100.0, 600.0, 300.0, 650.0], font_size: 12.0 }); + boxed.contents = "签名 Ada".into(); + let t = add_annotation(&mut doc, &boxed, &meta("t")).unwrap(); + assert_eq!(text(&list(&doc, 0)[t], b"Contents"), "签名 Ada"); + assert!(ap_content(&doc, &list(&doc, 0)[t]).contains("(?? Ada)"), "{}", ap_content(&doc, &list(&doc, 0)[t])); + assert_eq!(mark(&doc, t), Some('签')); + let typed = Shape::Typewriter { rect: [100.0, 700.0, 200.0, 716.0], font_size: 11.0 }; + let f = add_annotation( + &mut doc, + &NewAnnotation { page: 0, style: Style::default_for(&typed), shape: typed, contents: "张三".into(), author: "Ada".into() }, + &meta("x"), + ) + .unwrap(); + assert_eq!(mark(&doc, f), Some('张')); + let stamp = + Shape::Stamp { rect: [100.0, 100.0, 260.0, 142.0], stamp: StampKind::DynApproved, by: Some("By 张三 at 2:14 pm, Oct 02, 2026".into()) }; + let s = add_annotation( + &mut doc, + &NewAnnotation { page: 0, style: Style::default_for(&stamp), shape: stamp, contents: String::new(), author: "张三".into() }, + &meta("s"), + ) + .unwrap(); + assert_eq!(mark(&doc, s), Some('张')); + // Our own `?` appearance is redrawn on a restyle; Western European text clears the mark. + set_style(&mut doc, 0, t, Some([1.0, 0.0, 0.0]), None, None, None, &meta("")).unwrap(); + assert_eq!(mark(&doc, t), Some('签')); + set_contents(&mut doc, 0, t, "Zürich —\nGenève", &meta("")).unwrap(); + assert_eq!(mark(&doc, t), None); + set_contents(&mut doc, 0, t, "签名", &meta("")).unwrap(); + assert_eq!((text(&list(&doc, 0)[t], b"Contents").as_str(), mark(&doc, t)), ("签名", Some('签'))); + // A text box another program drew (with a font that has 欢): a restyle keeps that appearance. + let mut foreign = Dict::new(); + foreign.set(b"Type".to_vec(), Object::name("Annot")); + foreign.set(b"Subtype".to_vec(), Object::name("FreeText")); + foreign.set(b"Rect".to_vec(), Object::Array([100.0, 400.0, 300.0, 430.0].iter().map(|v| Object::Real(*v)).collect())); + foreign.set(b"DA".to_vec(), PdfString::literal(b"0 g /Helv 12 Tf".to_vec())); + foreign.set(b"Contents".to_vec(), PdfString::text("欢迎")); + let mut drawn = Dict::new(); + drawn.set(b"Type".to_vec(), Object::name("XObject")); + drawn.set(b"Subtype".to_vec(), Object::name("Form")); + drawn.set(b"BBox".to_vec(), Object::Array([0.0, 0.0, 200.0, 30.0].iter().map(|v| Object::Real(*v)).collect())); + let drawn = doc.add(Object::Stream(pdfcraft_cos::Stream::from_raw(drawn, b"BT /F1 12 Tf (\x01\x02) Tj ET".to_vec()))); + let mut ap = Dict::new(); + ap.set(b"N".to_vec(), Object::Ref(drawn)); + foreign.set(b"AP".to_vec(), Object::Dict(ap)); + let r = doc.add(Object::Dict(foreign)); + let page = page_ref(&doc, 0).unwrap(); + let mut on_page = annots(&doc, page); + on_page.push(Object::Ref(r)); + set_annots(&mut doc, page, on_page).unwrap(); + let k = list(&doc, 0).len() - 1; + let err = set_style(&mut doc, 0, k, Some([1.0, 0.0, 0.0]), None, None, None, &meta("")).unwrap_err(); + assert!(matches!(&err, AnnotError::Undrawable(m) if m.contains("can't redraw") && m.contains("\"欢\" (U+6B22)")), "{err:?}"); + assert_eq!(list(&doc, 0)[k].get(b"AP").and_then(|a| a.as_dict()).and_then(|a| a.reference(b"N")), Some(drawn), "its appearance is kept"); + // New text from the user is taken (and marked), as for any comment. + set_contents(&mut doc, 0, k, "欢迎光临", &meta("")).unwrap(); + assert_eq!(mark(&doc, k), Some('欢')); + // One without an appearance is shown in the display copy only; drawing it marks it. + let mut bare = Dict::new(); + bare.set(b"Type".to_vec(), Object::name("Annot")); + bare.set(b"Subtype".to_vec(), Object::name("FreeText")); + bare.set(b"Rect".to_vec(), Object::Array([100.0, 300.0, 300.0, 330.0].iter().map(|v| Object::Real(*v)).collect())); + bare.set(b"DA".to_vec(), PdfString::literal(b"0 g /Helv 12 Tf".to_vec())); + bare.set(b"Contents".to_vec(), PdfString::text("欢迎")); + let b = doc.add(Object::Dict(bare)); + let mut on_page = annots(&doc, page); + on_page.push(Object::Ref(b)); + set_annots(&mut doc, page, on_page).unwrap(); + let shown = with_missing_appearances(&doc).expect("a display copy"); + assert!(shown.get(b).as_dict().unwrap().contains(b"AP"), "displayed anyway"); + assert!(!doc.get(b).as_dict().unwrap().contains(b"AP"), "the document itself is unchanged"); + set_appearance(&mut doc, b).unwrap(); + assert_eq!(doc.get(b).as_dict().and_then(pdfcraft_fonts::undrawn_mark), Some('欢')); + // Kept through a save. + let doc = reopen(&doc); + assert_eq!(text(&list(&doc, 0)[t], b"Contents"), "签名"); +} + /// Content edits rebuild the display copy on every commit but leave annotations alone: the /// cached appearance is drawn once and reused after that, byte-identical. #[test] diff --git a/crates/automation/src/comments.rs b/crates/automation/src/comments.rs index 4aac6ac5..29c11a1c 100644 --- a/crates/automation/src/comments.rs +++ b/crates/automation/src/comments.rs @@ -501,6 +501,14 @@ impl Automation { // A typed signature or initials in the script font, left edge at `at`, upright as displayed. kind @ ("signature" | "initials") => { let t = a.str("text")?; + // Nobody sees a preview here: refuse what the script font would leave out. + if let Some(c) = pdfcraft_engine::script_missing(t) { + return Err(ToolError::InvalidArgs(format!( + "the signature script font can't draw \"{}\" (U+{:04X}); type the {kind} in Latin, Cyrillic or Greek letters, or pass `path` to a PNG or JPEG image of it instead of `text`", + c.escape_debug(), + u32::from(c) + ))); + } let h = if kind == "initials" { 24.0 } else { 32.0 }; let shape = pdfcraft_engine::typed_signature_shape(at, t, h, i64::from(info.rotation)) .ok_or_else(|| ToolError::InvalidArgs("text has nothing to draw".into()))?; diff --git a/crates/automation/src/lib.rs b/crates/automation/src/lib.rs index ab5d9ee1..1d76b40c 100644 --- a/crates/automation/src/lib.rs +++ b/crates/automation/src/lib.rs @@ -969,7 +969,13 @@ impl Automation { fn apply(&mut self, a: &Args, edit: Edit) -> Result { let id = self.doc(a)?.id; self.session.apply(id, edit).map_err(failed)?; - Ok(summary(self.doc(a)?)) + let doc = self.doc(a)?; + let mut out = summary(doc); + // Text the edit stored as typed but drew with `?` (its font can't draw it): said, not silent. + if !doc.edit_notices.is_empty() { + out["warnings"] = json!(doc.edit_notices.iter().map(ToString::to_string).collect::>()); + } + Ok(out) } fn marks(&mut self, tool: &str, a: &Args) -> Result { @@ -1136,11 +1142,13 @@ impl Automation { } (None, None) => return Err(ToolError::InvalidArgs("text needs `text` or `path`".into())), }; - // The page font is Helvetica/WinAnsi: refuse what it would silently turn into `?`. - if let Some(c) = pdfcraft_engine::first_undrawable_plain(&text) { - return Err(ToolError::InvalidArgs(pdfcraft_engine::undrawable_message(c))); - } - (format!("{title}.pdf"), self.session.create_from_text(&title, &text).map_err(failed)?) + // The page font is Helvetica/WinAnsi: text it would turn into `?` is refused (with + // its line) by `pdfcraft_create::from_text`, as for every other caller. + let bytes = self.session.create_from_text(&title, &text).map_err(|e| match e { + pdfcraft_engine::EditError::Create(pdfcraft_engine::CreateError::Invalid(m)) => ToolError::InvalidArgs(m), + e => failed(e), + })?; + (format!("{title}.pdf"), bytes) } other => return Err(ToolError::InvalidArgs(format!("unknown source {other:?}"))), }; diff --git a/crates/automation/src/signing.rs b/crates/automation/src/signing.rs index 7701f612..2d0281fc 100644 --- a/crates/automation/src/signing.rs +++ b/crates/automation/src/signing.rs @@ -214,6 +214,14 @@ impl Automation { } if let Some(text) = o.get("text") { let text = text.as_str().ok_or_else(|| bad("graphic.text must be a string"))?; + // Nobody sees a preview here: refuse what the script font and its fallback would leave out. + if let Some(c) = pdfcraft_engine::script_missing(text) { + return Err(bad(format!( + "the signature script font can't draw \"{}\" (U+{:04X}) in graphic.text; type it in Latin, Cyrillic or Greek letters, or pass graphic.image instead", + c.escape_debug(), + u32::from(c) + ))); + } return pdfcraft_engine::typed_signature_graphic(text).ok_or_else(|| { bad(format!( "graphic.text {text:?} has nothing the script font can draw (at most {} characters)", diff --git a/crates/automation/src/tools.rs b/crates/automation/src/tools.rs index 7e5e0267..c712ab38 100644 --- a/crates/automation/src/tools.rs +++ b/crates/automation/src/tools.rs @@ -303,7 +303,7 @@ pub fn tools() -> Vec { t( "form_fill", "Fill in form fields", - "Set several fields at once (one undo step). values maps field names (from form_fields) to: a string for text fields, combo boxes and radio groups (an option), true/false for check boxes, an array of strings for multi-select lists. Appearances are regenerated so every viewer shows the values.", + "Set several fields at once (one undo step). values maps field names (from form_fields) to: a string for text fields, combo boxes and radio groups (an option), true/false for check boxes, an array of strings for multi-select lists. Appearances are regenerated so every viewer shows the values. A value whose characters the field's font can't draw (Chinese in a Helvetica field) is still saved; `warnings` in the result names the field and character: PdfCraft draws `?` there and asks other viewers to redraw the field from its value.", ) .with(schema(json!({ "doc": doc(), "values": { "type": "object", "description": "Field name → value." } }), &["doc", "values"])), t("form_reset", "Clear form", "Reset fields to their default values: all of them, or only those listed. Undoable.") @@ -600,7 +600,7 @@ pub fn tools() -> Vec { "Add a comment as Acrobat's commenting tools do. Geometry is in points with the origin at the top-left of the displayed page, y down (as in page_render images at 72 dpi and text_find rects). \ note: `at` [x, y] (icon top-left). stamp: `at` [x, y] (its centre) and `stamp`: approved, completed, confidential, draft, final, for comment, for public release, information only, not approved, not for public release, preliminary results, void, accepted, initial here, rejected, sign here, witness; dynamic: true for the dynamic approved/confidential/received/reviewed/revised stamps with a By … at … line. highlight/underline/strikeout/squiggly: `find` (text on the page to mark; every match with all: true) or `quads`. \ rectangle/oval/textbox: `rect` [x0, y0, x1, y1]. line/arrow: `from`, `to`. Optional `endings` (two names: start, end) for a line, arrow or polyline, or one name for a callout: None, Square, Circle, Diamond, OpenArrow, ClosedArrow, Butt, ROpenArrow, RClosedArrow, Slash. An arrow without endings is None then OpenArrow. ink: `strokes` [[[x, y], …], …]. \ - polygon/cloud/polyline (connected lines): `points` [[x, y], …]. callout: `rect` (its text box), `to` (the point the arrow touches), optional `knee`. caret (inserted text): `at`, the insertion point on the baseline. replace (Replace Text): `find` or `quads` like highlight, `contents` the replacement. attachment: `path` (the file), `at`, optional `icon` (PushPin, Paperclip, Graph, Tag). Undoable.", + polygon/cloud/polyline (connected lines): `points` [[x, y], …]. callout: `rect` (its text box), `to` (the point the arrow touches), optional `knee`. caret (inserted text): `at`, the insertion point on the baseline. replace (Replace Text): `find` or `quads` like highlight, `contents` the replacement. attachment: `path` (the file), `at`, optional `icon` (PushPin, Paperclip, Graph, Tag). Undoable. Text a text box, callout or stamp draws in the standard fonts that they can't draw (Chinese, Greek) is saved as typed and drawn as `?`; `warnings` in the result says so.", ) .with(schema( json!({ @@ -995,7 +995,7 @@ pub fn tools() -> Vec { t( "fill_sign_add", "Fill & Sign: type text or place a mark", - "Fill in a form that has no fields, as Acrobat's Fill & Sign does: type text (`text`, 10 pt), place a check, cross, dot or line, today's date (in Preferences ▸ Date format, see fill_sign_date_format, or `format`), or a signature or initials (either `text` drawn in a script font, or `path` to a local PNG/JPEG image up to 4 MiB and 32 megapixels, scaled down to 4 megapixels, preserving transparency). `at` [x, y] is in points from the top-left of the page: text's top-left, mark's centre, signature's left edge centred vertically. Image signatures fit within 150 pt wide and 32 pt tall (24 pt for initials). Creates movable, undoable annotations.", + "Fill in a form that has no fields, as Acrobat's Fill & Sign does: type text (`text`, 10 pt), place a check, cross, dot or line, today's date (in Preferences ▸ Date format, see fill_sign_date_format, or `format`), or a signature or initials (either `text` drawn in a script font, or `path` to a local PNG/JPEG image up to 4 MiB and 32 megapixels, scaled down to 4 megapixels, preserving transparency). `at` [x, y] is in points from the top-left of the page: text's top-left, mark's centre, signature's left edge centred vertically. Image signatures fit within 150 pt wide and 32 pt tall (24 pt for initials). Creates movable, undoable annotations. Typed text the standard fonts can't draw is saved and drawn as `?` (`warnings` in the result says so); a typed signature neither the script font nor its fallback can draw is refused (pass `path` instead).", ) .cmd("sign.fill.text") .with(schema( diff --git a/crates/automation/tests/automation.rs b/crates/automation/tests/automation.rs index 1834d187..593350ce 100644 --- a/crates/automation/tests/automation.rs +++ b/crates/automation/tests/automation.rs @@ -2921,6 +2921,58 @@ fn comment_checkmarks_locks_hiding_and_summaries_through_tools() { assert!(matches!(a.call("comments_summarize", &json!({ "doc": doc, "sort": "colour" })), Err(ToolError::InvalidArgs(_)))); } +/// Text the standard fonts can't draw is never written as `?` silently, through the tools. A form +/// value, a button caption, a text box and Fill & Sign text are kept as typed (as Acrobat keeps +/// them) and the result's `warnings` names the character; a form also asks other viewers to +/// redraw it. Where the `?` would be permanent the tool refuses: a typed signature, a redaction's +/// overlay text, a new document from text, and flattening what PdfCraft drew with `?`. A comment +/// summary that quotes such text writes it as [U+XXXX]. +#[test] +fn text_the_standard_fonts_cannot_draw_is_never_silent_through_tools() { + let dir = workdir("undrawable"); + let mut a = auto(&dir); + let doc = ok(&mut a, "doc_open", json!({ "path": "a.pdf" }))["doc"].as_u64().unwrap(); + let refused = |a: &mut Automation, tool: &str, args: Value, ch: &str| match a.call(tool, &args) { + Err(ToolError::Failed(e) | ToolError::InvalidArgs(e)) => assert!(e.contains(ch), "{tool}: {e}"), + other => panic!("{tool}: expected a refusal, got {other:?}"), + }; + let warned = |out: &Value, parts: &[&str]| { + let w = out["warnings"].as_array().unwrap_or_else(|| panic!("no warnings: {out}")); + assert!(w.iter().filter_map(Value::as_str).any(|m| parts.iter().all(|p| m.contains(p))), "{parts:?} not in {w:?}"); + }; + // Forms: a value is kept and the form asks other viewers to redraw it; a button caption too. + ok(&mut a, "form_add_field", json!({ "doc": doc, "page": 1, "type": "text", "rect": [20, 20, 180, 42], "name": "who" })); + let out = ok(&mut a, "form_fill", json!({ "doc": doc, "values": { "who": "Ada" } })); + assert!(out.get("warnings").is_none(), "{out}"); + let out = ok(&mut a, "form_fill", json!({ "doc": doc, "values": { "who": "张三" } })); + warned(&out, &["form field \"who\"", "\"张\" (U+5F20)", "other PDF viewers redraw the field"]); + let fields = ok(&mut a, "form_fields", json!({ "doc": doc })); + assert_eq!(fields["fields"][0]["value"], "张三", "{fields}"); + let out = ok(&mut a, "form_add_field", json!({ "doc": doc, "page": 1, "type": "button", "rect": [20, 60, 120, 82], "caption": "提交" })); + warned(&out, &["\"提\" (U+63D0)"]); + // Comments and Fill & Sign text: kept, and said. + let out = ok(&mut a, "comment_add", json!({ "doc": doc, "page": 1, "type": "textbox", "rect": [10, 200, 190, 240], "contents": "欢迎" })); + warned(&out, &["a comment on page 1", "\"欢\" (U+6B22)", "every viewer"]); + let out = ok(&mut a, "fill_sign_add", json!({ "doc": doc, "page": 1, "type": "text", "at": [20, 300], "text": "签名" })); + warned(&out, &["U+7B7E"]); + // A sticky note draws an icon, not its text: nothing to say. + let out = ok(&mut a, "comment_add", json!({ "doc": doc, "page": 1, "type": "note", "at": [40, 40], "contents": "签名 OK", "author": "Ada" })); + assert!(out.get("warnings").is_none(), "{out}"); + // Permanent `?`: refused. + refused(&mut a, "fill_sign_add", json!({ "doc": doc, "page": 1, "type": "signature", "at": [20, 340], "text": "张三" }), "pass `path`"); + // Cyrillic comes from the script font's Inter fallback (#963), so it isn't refused. + ok(&mut a, "fill_sign_add", json!({ "doc": doc, "page": 1, "type": "signature", "at": [20, 380], "text": "Саша" })); + refused(&mut a, "redact_mark", json!({ "doc": doc, "page": 1, "rect": [10, 400, 190, 420], "overlay": "机密" }), "\"机\" (U+673A)"); + refused(&mut a, "doc_flatten", json!({ "doc": doc }), "nothing was flattened"); + refused(&mut a, "doc_create", json!({ "from": "text", "text": "Notes\n第二行" }), "(line 2)"); + // A summary quotes the note: escaped, not refused, not `?`. + ok(&mut a, "comments_summarize", json!({ "doc": doc, "out": "summary.pdf" })); + let summary = ok(&mut a, "doc_open", json!({ "path": "summary.pdf" }))["doc"].as_u64().unwrap(); + let text = page_text(&mut a, summary).join("\n"); + assert!(text.contains("[U+7B7E][U+540D] OK") && text.contains("[U+XXXX]"), "{text}"); + assert!(!text.contains("?? OK"), "{text}"); +} + #[test] fn line_endings_through_the_tool() { let dir = workdir("endings"); @@ -3061,6 +3113,7 @@ fn signing_with_an_image_typed_or_drawn_graphic_through_tools() { (json!("logo.png"), "not an object"), (json!({ "image": "broken.png" }), "not an image"), (json!({ "text": " " }), "nothing to draw"), + (json!({ "text": "张三" }), "a name the script font and its fallback can't draw"), (json!({ "strokes": [[[0, 0], ["x", 1]]] }), "malformed strokes"), (json!({ "strokes": [] }), "empty strokes"), ] { diff --git a/crates/create/README.md b/crates/create/README.md index 7cd68277..88c6da2e 100644 --- a/crates/create/README.md +++ b/crates/create/README.md @@ -16,6 +16,10 @@ Layer L4. Create a PDF (Acrobat's Create a PDF tool), execution plan M10.2: resolution (1–1200 dpi), without resampling or recompressing image pixels. Use 72 dpi for one point per pixel, or `ImageResolution::Embedded` for the same behavior as `from_images`. The largest page side is still capped at 14,400 points; -- `from_text(text, …)`: plain text set in Helvetica, wrapped and paginated. +- `from_text(text, …)`: plain text set in Helvetica, wrapped and paginated. Text Helvetica + can't draw is refused, naming the line and the character, never set as `?`. +- `from_text_escaped(text, …)`: the same for reports PdfCraft generates from documents + (comment summaries, compare reports): such characters are written as `[U+XXXX]` and the first + line says so (`ESCAPED_NOTE`). Every function returns a `pdfcraft_cos::Document`; the caller writes it. diff --git a/crates/create/src/lib.rs b/crates/create/src/lib.rs index aa6e8f6f..4d21ea5e 100644 --- a/crates/create/src/lib.rs +++ b/crates/create/src/lib.rs @@ -618,8 +618,50 @@ fn turkish_code_page(bytes: &[u8]) -> bool { && !bytes.iter().any(|b| matches!(b, 0xC1 | 0xCD | 0xD3 | 0xDA | 0xE1 | 0xED | 0xF3 | 0xFA)) } -/// Plain text set in Helvetica on pages of `page` size with 1-inch margins. +/// Plain text set in Helvetica on pages of `page` size with 1-inch margins. Text Helvetica can't +/// draw (outside Western European) is refused with [`CreateError::Invalid`] naming the line and +/// the character; text PdfCraft generates goes through [`from_text_escaped`] instead. pub fn from_text(title: &str, text: &str, page: (f64, f64), font_size: f64) -> Result { + // Helvetica in WinAnsiEncoding: text it can't draw is refused with where it is, not set as `?`. + if let Some((line, c)) = first_undrawable_line(text) { + return Err(CreateError::Invalid(pdfcraft_fonts::undrawable_message(&format!("the text (line {line})"), c))); + } + set_text(title, text, page, font_size) +} + +/// The line (1-based) and character of the first one Helvetica can't draw. Line breaks, tabs and +/// form feeds (new pages) are layout, not drawn. +fn first_undrawable_line(text: &str) -> Option<(usize, char)> { + text.split('\n').enumerate().find_map(|(i, l)| pdfcraft_fonts::first_undrawable(&l.replace('\u{c}', "")).map(|c| (i + 1, c))) +} + +/// What a PDF made from text PdfCraft generated says, in its first line, about characters it +/// wrote as `[U+XXXX]`. +pub const ESCAPED_NOTE: &str = "Characters Helvetica can't draw are written as [U+XXXX] code points."; + +/// [`from_text`] for text PdfCraft generates from documents (a comment summary, a compare +/// report), which may quote any language. Each character Helvetica can't draw is written as +/// `[U+XXXX]` instead of being refused or set as `?`, so nothing is lost, and the first line says +/// so ([`ESCAPED_NOTE`]). Returns the document and how many characters were written that way. +pub fn from_text_escaped(title: &str, text: &str, page: (f64, f64), font_size: f64) -> Result<(Document, usize), CreateError> { + let mut escaped = 0usize; + let mut out = String::with_capacity(text.len()); + for c in text.chars() { + if matches!(c, '\n' | '\r' | '\u{c}') || pdfcraft_fonts::first_undrawable(c.encode_utf8(&mut [0; 4])).is_none() { + out.push(c); + } else { + escaped = escaped.saturating_add(1); + out.push_str(&format!("[U+{:04X}]", u32::from(c))); + } + } + if escaped > 0 { + out = format!("{ESCAPED_NOTE}\n\n{out}"); + } + // The title is a text string in /Info, which holds any character. + Ok((set_text(title, &out, page, font_size)?, escaped)) +} + +fn set_text(title: &str, text: &str, page: (f64, f64), font_size: f64) -> Result { let size = if font_size.is_finite() && font_size > 0.0 { font_size.clamp(4.0, 72.0) } else { 11.0 }; let (w, h) = page; let margin = 72.0; diff --git a/crates/create/src/tests.rs b/crates/create/src/tests.rs index 79732121..88ac114a 100644 --- a/crates/create/src/tests.rs +++ b/crates/create/src/tests.rs @@ -124,6 +124,32 @@ fn text_is_wrapped_and_paginated() { assert!(content(p.last().unwrap()).contains("(After a form feed) Tj"), "a form feed starts a page"); } +/// Text Helvetica can't draw is refused with its line and character, never set as `?`; text +/// PdfCraft generates writes such characters as [U+XXXX] and says so. +#[test] +fn text_helvetica_cannot_draw_is_refused_or_escaped() { + let err = from_text("notes", "Café —\tok\r\n\u{c}第二页 notes", LETTER, 11.0).unwrap_err(); + let CreateError::Invalid(msg) = &err else { panic!("{err:?}") }; + assert!(msg.contains("(line 2)") && msg.contains("\"第\" (U+7B2C)") && msg.contains("Western European"), "{msg}"); + // Western European text, tabs, CRLF and form feeds included, still converts. + from_text("notes", "Café —\tok\r\n\u{c}Seite zwei: Grüße", LETTER, 11.0).unwrap(); + let (doc, escaped) = from_text_escaped("Summary of Comments on 报告.pdf", "Comment: 签名 ok\nPage 2", LETTER, 11.0).unwrap(); + assert_eq!(escaped, 2); + let doc = reopen(&doc); + let p = pages(&doc); + let c = doc.resolve(p[0].get(b"Contents").unwrap()); + let Object::Stream(s) = &*c else { panic!() }; + let shown = String::from_utf8_lossy(&s.decoded().unwrap()).into_owned(); + assert!(shown.contains("(Comment: [U+7B7E][U+540D] ok) Tj") && shown.contains(&format!("({ESCAPED_NOTE}) Tj")), "{shown}"); + assert!(!shown.contains('?'), "{shown}"); + // Nothing to escape: no note. + let (doc, escaped) = from_text_escaped("r", "Plain report", LETTER, 11.0).unwrap(); + assert_eq!(escaped, 0); + let p = pages(&doc); + let Object::Stream(s) = &*doc.resolve(p[0].get(b"Contents").unwrap()) else { panic!() }; + assert!(!String::from_utf8_lossy(&s.decoded().unwrap()).contains("U+XXXX")); +} + fn image_of(doc: &Document, page: usize) -> Dict { let p = &pages(doc)[page]; let res = doc.resolve(p.get(b"Resources").unwrap()).as_dict().cloned().unwrap(); diff --git a/crates/edit/src/flatten.rs b/crates/edit/src/flatten.rs index 13469666..92cc56d3 100644 --- a/crates/edit/src/flatten.rs +++ b/crates/edit/src/flatten.rs @@ -97,9 +97,45 @@ pub fn flatten_fill_sign(doc: &mut Document, pages: &[usize]) -> Result Option<(usize, bool, char)> { + for &i in pages { + let list = all.get(i)?.dict.get(b"Annots").map(|a| doc.resolve(a)).and_then(|a| a.as_array().cloned()).unwrap_or_default(); + for entry in &list { + let Some(d) = doc.resolve(entry).as_dict().cloned() else { continue }; + let flags = d.get(b"F").and_then(|f| doc.resolve(f).as_int()).unwrap_or(0); + if !selected(doc, &d, which) || flags & (HIDDEN | NO_VIEW) != 0 { + continue; + } + let widget = d.name(b"Subtype") == Some(b"Widget"); + let drawn = pdfcraft_fonts::undrawn_mark(&d); + let would_draw = || (!widget && appearance(doc, &d).is_none()).then(|| pdfcraft_annot::appearance::undrawable_text(&d)).flatten(); + if let Some(c) = drawn.or_else(|| would_draw().map(|(_, c)| c)) { + return Some((i, widget, c)); + } + } + } + None +} + fn flatten_which(doc: &mut Document, pages: &[usize], which: Which) -> Result { let all = page_list(doc); check(pages, all.len())?; + // `?` in an appearance can still be fixed (a field's value, a comment's text are stored as + // typed, and other viewers redraw form fields); flattened, it would be the page for good. + if let Some((i, widget, c)) = undrawn_on(doc, &all, pages, which) { + return Err(EditError::Invalid(format!( + "can't flatten {} on page {}: it shows ? in place of \"{}\" (U+{:04X}), which its font can't draw, and flattening would make that permanent (nothing was flattened); change or remove that text first", + if widget { "a form field" } else { "a comment" }, + i + 1, + c.escape_debug(), + u32::from(c) + ))); + } let mut drawn = 0; let mut removed_widgets: HashSet = HashSet::new(); for &i in pages { @@ -132,7 +168,8 @@ fn flatten_which(doc: &mut Document, pages: &[usize], which: Which) -> Result String { format!("the standard fonts can't draw \"{c}\" (U+{:04X}); only Western European characters can be added as text for now", u32::from(c)) } -/// The first character of plain `text` that Helvetica/WinAnsi can't draw (line and page breaks -/// and tabs are layout, not glyphs). -pub fn first_undrawable_plain(text: &str) -> Option { - text.chars().find(|c| !matches!(c, '\r' | '\n' | '\t' | '\u{c}') && !pdfcraft_fonts::win_ansi_encodable(*c)) -} - fn text_op(x: f64, y: f64, text: &str) -> Vec { let mut v = format!("1 0 0 1 {} {} Tm ", n(x), n(y)).into_bytes(); v.extend(literal(&win_ansi(text))); diff --git a/crates/edit/src/tests.rs b/crates/edit/src/tests.rs index 4a68edf6..0c26de0e 100644 --- a/crates/edit/src/tests.rs +++ b/crates/edit/src/tests.rs @@ -234,6 +234,58 @@ fn flattening_keeps_a_note_whose_appearance_is_missing() { assert!(flat.contains("q 1 0 0 1 10 10 cm /PCFl0 Do Q"), "{flat}"); } +/// Flattening never makes a `?` permanent: it refuses, changing nothing, a text box without an +/// appearance whose text Helvetica can't draw (another program's), and a comment or form field +/// whose appearance PdfCraft drew with `?` (marked `PCUndrawn`). A comment another program drew +/// with a font that has those characters (an appearance, no mark) flattens as before. +#[test] +fn flattening_refuses_what_would_bake_in_question_marks() { + let page = stream("", " "); + let refused = |annot: &str, comments: bool, fields: bool, what: &str| { + let mut doc = build(&[ + "<< /Type /Catalog /Pages 2 0 R >>", + "<< /Type /Pages /Kids [3 0 R] /Count 1 >>", + "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 200 200] /Resources << >> /Contents 4 0 R /Annots [5 0 R] >>", + &page, + annot, + "<< /Type /XObject /Subtype /Form /BBox [0 0 140 30] /Length 0 >>\nstream\n\nendstream", + ]); + let before = write_incremental(&doc, &SaveOptions::default()).unwrap(); + let err = flatten(&mut doc, &[0], comments, fields).unwrap_err().to_string(); + assert!(err.contains(what) && err.contains("page 1") && err.contains("\"欢\" (U+6B22)") && err.contains("nothing was flattened"), "{err}"); + assert_eq!(write_incremental(&doc, &SaveOptions::default()).unwrap(), before, "a refused flatten changes nothing"); + assert!(pdfcraft_model::pages(&doc)[0].dict.contains(b"Annots"), "the annotation is still there"); + }; + refused( + "<< /Type /Annot /Subtype /FreeText /Rect [10 10 150 40] /DA (0 g /Helv 12 Tf) /Contents /F 4 >>", + true, + false, + "a comment", + ); + refused( + "<< /Type /Annot /Subtype /FreeText /Rect [10 10 150 40] /DA (0 g /Helv 12 Tf) /Contents /F 4 /AP << /N 6 0 R >> /PCUndrawn >>", + true, + false, + "a comment", + ); + refused( + "<< /Type /Annot /Subtype /Widget /FT /Tx /T (name) /V /Rect [10 10 150 40] /F 4 /AP << /N 6 0 R >> /PCUndrawn >>", + false, + true, + "a form field", + ); + // Drawn by another program, with a font that has 欢: no mark, so it flattens. + let mut doc = build(&[ + "<< /Type /Catalog /Pages 2 0 R >>", + "<< /Type /Pages /Kids [3 0 R] /Count 1 >>", + "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 200 200] /Resources << >> /Contents 4 0 R /Annots [5 0 R] >>", + &page, + "<< /Type /Annot /Subtype /FreeText /Rect [10 10 150 40] /DA (0 g /Helv 12 Tf) /Contents /F 4 /AP << /N 6 0 R >> >>", + "<< /Type /XObject /Subtype /Form /BBox [0 0 140 30] /Length 0 >>\nstream\n\nendstream", + ]); + assert_eq!(flatten(&mut doc, &[0], true, false).unwrap(), 1); +} + /// Display space is in points, so on a `/UserUnit 2` page an item's box maps to half as many /// user-space units. Items record the `/UserUnit` they were written for; one without it was /// written when display space was in user-space units, and reads back scaled to points. diff --git a/crates/engine/src/compare.rs b/crates/engine/src/compare.rs index c11fc261..04dc83cd 100644 --- a/crates/engine/src/compare.rs +++ b/crates/engine/src/compare.rs @@ -85,7 +85,7 @@ impl Session { pub fn compare_report(&self, old: DocId, new: DocId) -> Result>, EditError> { let c = self.compare(old, new)?; let name = |id| self.get(id).map(|d| d.name.clone()).unwrap_or_default(); - self.create_from_text("Compare Report", &pdfcraft_compare::report(&c, &name(old), &name(new))) + self.create_report("Compare Report", &pdfcraft_compare::report(&c, &name(old), &name(new))) } /// Mark the differences in `new` as comments: highlights over replaced and inserted text diff --git a/crates/engine/src/lib.rs b/crates/engine/src/lib.rs index a0ac6ed3..ffc22681 100644 --- a/crates/engine/src/lib.rs +++ b/crates/engine/src/lib.rs @@ -38,7 +38,7 @@ pub use pdfcraft_organize::LabelStyle; pub use pdfcraft_organize::view::{InitialView, Layout as InitialLayout, Magnification, Navigation}; pub use pdfcraft_cos::Algorithm; -pub use pdfcraft_create::{CONVERTIBLE, ImageResolution, SourceKind, decode_text, source_kind}; +pub use pdfcraft_create::{CONVERTIBLE, CreateError, ImageResolution, SourceKind, decode_text, source_kind}; pub use pdfcraft_edit::{ Added, AddedImage, AddedText, Align as TextAlign, Background, Content as AddedContent, Family as FontFamily, HeaderFooter, MarkKind, Watermark, }; @@ -49,7 +49,7 @@ pub use pdfcraft_forms::{ }; pub use pdfcraft_a11y as a11y; -pub use pdfcraft_edit::{BlockStyle, PageImage, TextBlock, TextLine, first_undrawable, first_undrawable_plain, undrawable_message}; +pub use pdfcraft_edit::{BlockStyle, PageImage, TextBlock, TextLine, first_undrawable}; pub use pdfcraft_measure as measure; pub use pdfcraft_xfa::Report as XfaLayout; @@ -72,7 +72,7 @@ pub enum ImageEdit { }, Delete, } -pub use pdfcraft_fonts::{MAX_SIGNATURE_CHARS, ScriptOutline, script_outline}; +pub use pdfcraft_fonts::{MAX_SIGNATURE_CHARS, ScriptOutline, script_missing, script_outline}; /// 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. @@ -291,6 +291,81 @@ pub struct Document { /// XFA forms: what was approximated, rewritten or could not be written to the XFA data /// (also in `info.warnings`, kept there when the document is re-read). pub xfa_warnings: Vec, + /// What the last successful [`Session::apply`] wrote that its fonts can't draw: kept as typed, + /// drawn with `?` in its place (see [`TextNotice`]). The interface shows it; tools return it. + pub edit_notices: Vec, +} + +/// Text an edit stored as typed but drew with `?` in place of `ch`, because the font it is drawn +/// with (a form field's, or the standard fonts for comments) can't draw that character. Acrobat +/// keeps such text too, drawing it with fonts PdfCraft can't embed yet. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct TextNotice { + /// The form field's full name, or `None` for a comment. + pub field: Option, + /// The page (0-based) of the comment or of the field's widget. + pub page: usize, + pub ch: char, + /// Other PDF viewers redraw it from the stored text (a form field, with `/NeedAppearances` + /// set); otherwise the `?` shows in every viewer. + pub redrawn_elsewhere: bool, +} + +impl std::fmt::Display for TextNotice { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let ch = format!("\"{}\" (U+{:04X})", self.ch.escape_debug(), u32::from(self.ch)); + let page = self.page + 1; + match (&self.field, self.redrawn_elsewhere) { + (Some(name), true) => write!( + f, + "form field {name:?} (page {page}) holds {ch}, which its font can't draw: the value is saved, PdfCraft shows ? in its place, and other PDF viewers redraw the field from the value" + ), + (Some(name), false) => write!( + f, + "form field {name:?} (page {page}) holds {ch}, which its font can't draw: the value is saved, but this PDF/A document can't ask other viewers to redraw it, so ? shows in its place everywhere" + ), + (None, _) => write!( + f, + "a comment on page {page} has {ch}, which the standard fonts can't draw: its text is saved, but its appearance shows ? in its place in every viewer" + ), + } + } +} + +/// What `next` drew with `?` that `prev` didn't (a new or redrawn appearance carrying +/// `pdfcraft_fonts::UNDRAWN_MARK`), one notice per field or comment, in page order. +fn undrawn_notices(prev: &pdfcraft_cos::Document, next: &pdfcraft_cos::Document) -> Vec { + let Ok(pages) = pdfcraft_annot::page_refs(next) else { return Vec::new() }; + let redrawn_elsewhere = pdfcraft_forms::need_appearances(next); + let mut names: Option> = None; + let mut out: Vec = Vec::new(); + for (page, p) in pages.iter().enumerate() { + let annots = next.get(*p).as_dict().and_then(|d| d.get(b"Annots").cloned()).map(|a| next.resolve(&a)); + for r in annots.as_deref().and_then(pdfcraft_cos::Object::as_array).into_iter().flatten().filter_map(pdfcraft_cos::Object::as_ref) { + let now = next.get(r); + let Some(d) = now.as_dict() else { continue }; + let Some(ch) = pdfcraft_fonts::undrawn_mark(d) else { continue }; + // Only what this edit drew: a new annotation, or a new appearance. + let was = prev.get(r); + if was.as_dict().is_some_and(|w| w.get(b"AP") == d.get(b"AP") && pdfcraft_fonts::undrawn_mark(w) == Some(ch)) { + continue; + } + let field = (d.name(b"Subtype") == Some(b"Widget")).then(|| { + let names = names.get_or_insert_with(|| { + pdfcraft_forms::fields(next) + .into_iter() + .flat_map(|f| f.widgets.iter().map(|w| (w.obj, f.name.clone())).collect::>()) + .collect() + }); + names.get(&r).cloned().unwrap_or_default() + }); + if field.is_some() && out.iter().any(|n| n.field == field) { + continue; + } + out.push(TextNotice { redrawn_elsewhere: redrawn_elsewhere && field.is_some(), field, page, ch }); + } + } + out } impl Document { @@ -2352,8 +2427,10 @@ pub struct Session { /// Lay a dynamic XFA form out (pages and fields) and give its widgets appearances. fn xfa_layout(doc: &mut pdfcraft_cos::Document) -> Result { - let report = pdfcraft_xfa::render_into(doc).map_err(|e| e.to_string())?; + let mut report = pdfcraft_xfa::render_into(doc).map_err(|e| e.to_string())?; xfa_redraw(doc)?; + // The form's own values: one its field's font can't draw is drawn with `?` and reported. + report.warnings.extend(undrawn_field_warnings(doc)); Ok(report) } @@ -2369,6 +2446,17 @@ pub(crate) fn xfa_redraw(doc: &mut pdfcraft_cos::Document) -> Result<(), String> Ok(()) } +/// A warning for each field whose appearance PdfCraft drew with `?` for a character its font +/// can't draw (laying out an XFA form's own values). +fn undrawn_field_warnings(doc: &pdfcraft_cos::Document) -> Vec { + pdfcraft_forms::undrawn_fields(doc) + .into_iter() + .map(|(name, c)| { + format!("form field {name:?} holds \"{}\" (U+{:04X}), which its font can't draw: it shows ? in its place", c.escape_debug(), u32::from(c)) + }) + .collect() +} + /// The form's fields as the XFA data layer wants them. fn xfa_field_data(doc: &pdfcraft_cos::Document) -> Vec { use pdfcraft_forms::FieldKind as K; @@ -2750,6 +2838,7 @@ impl Session { xfa_template: None, fonts_stale: false, xfa_warnings: Vec::new(), + edit_notices: Vec::new(), }); Ok(id) } @@ -2764,6 +2853,7 @@ impl Session { let today = self.today(); let js_off = self.js_off; let doc = self.doc_mut(id)?; + doc.edit_notices.clear(); let name = doc.name.clone(); let is_xfa = doc.info.xfa.is_some(); let mut cx = EditCtx::new(now, doc.generation ^ (id.0 << 48)); @@ -2838,6 +2928,13 @@ impl Session { if signed && rewrites(&next) && !rewrites(&editor.cos) { return Err(EditError::SignedRewrite(edit.label().to_string())); } + // What this edit drew with `?` (a value or comment text its font can't draw, stored as + // typed): reported, never silent (AGENTS.md §4). + let notices = if matches!(scope_of(&edit), Scope::Form | Scope::Comments | Scope::Full) { + guard(|| undrawn_notices(&editor.cos, &next)).unwrap_or_default() + } else { + Vec::new() + }; let previous = std::mem::replace(&mut editor.cos, next); let keys = keys_after(&edit).unwrap_or_else(|| editor.keys.clone()); let previous_keys = std::mem::replace(&mut editor.keys, keys); @@ -2864,6 +2961,7 @@ impl Session { } doc.dirty = true; doc.generation += 1; + doc.edit_notices = notices; note_warnings(&mut doc.xfa_warnings, &xfa_notes); let notes = doc.xfa_warnings.clone(); note_warnings(&mut doc.info.warnings, ¬es); @@ -2880,6 +2978,7 @@ impl Session { pub fn undo(&mut self, id: DocId) -> Result { let doc = self.doc_mut(id)?; + doc.edit_notices.clear(); let editor = doc.editor.as_mut().ok_or(EditError::NothingToUndo)?; let (label, prev, keys, scope) = editor.undo.pop().ok_or(EditError::NothingToUndo)?; let current = std::mem::replace(&mut editor.cos, prev); @@ -2894,6 +2993,7 @@ impl Session { pub fn redo(&mut self, id: DocId) -> Result { let doc = self.doc_mut(id)?; + doc.edit_notices.clear(); let editor = doc.editor.as_mut().ok_or(EditError::NothingToRedo)?; let (label, next, keys, scope) = editor.redo.pop().ok_or(EditError::NothingToRedo)?; let current = std::mem::replace(&mut editor.cos, next); @@ -3236,11 +3336,20 @@ impl Session { self.write_new(&pdfcraft_create::from_images_with_resolution(images, resolution)?) } - /// A new document from plain text (US Letter, 11 pt Helvetica). + /// A new document from plain text (US Letter, 11 pt Helvetica). Text Helvetica can't draw is + /// refused, naming the line and the character (never set as `?`). pub fn create_from_text(&self, title: &str, text: &str) -> Result>, EditError> { self.write_new(&pdfcraft_create::from_text(title, text, pdfcraft_create::LETTER, 11.0)?) } + /// [`Self::create_from_text`] for a report PdfCraft writes from documents (comment summaries, + /// compare reports), which can quote any language: characters Helvetica can't draw are written + /// as `[U+XXXX]`, and the report's first line says so, rather than the report being refused. + fn create_report(&self, title: &str, text: &str) -> Result>, EditError> { + let (doc, _escaped) = pdfcraft_create::from_text_escaped(title, text, pdfcraft_create::LETTER, 11.0)?; + self.write_new(&doc) + } + /// Convert a file Create understands (an image or plain text) to PDF bytes; a PDF is checked /// (it must open and allow copying pages) and returned as it is. pub fn convert_to_pdf(&self, name: &str, bytes: &Arc>) -> Result<(SourceKind, Arc>), EditError> { @@ -3657,7 +3766,7 @@ impl Session { pub fn summarize_comments(&self, id: DocId, sort: SummarySort) -> Result>, EditError> { let doc = self.get(id).ok_or(EditError::NoDocument)?; let text = comment_summary(&doc.name, &doc.info.annotations, sort); - self.create_from_text(&format!("Summary of Comments on {}", doc.name), &text) + self.create_report(&format!("Summary of Comments on {}", doc.name), &text) } /// The certificates trusted for signing. diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs index 838f9af2..6b99cec4 100644 --- a/crates/engine/src/tests.rs +++ b/crates/engine/src/tests.rs @@ -1170,6 +1170,34 @@ fn form_fixture() -> Vec { out } +/// A value its field's font can't draw is kept, and the edit that drew it reports it in +/// `edit_notices` (the interface shows it, tools return it): not a later unrelated edit, not a +/// failed one, and not a value that can be drawn. The form (an inline AcroForm here) asks other +/// viewers to redraw it. +#[test] +fn a_value_its_font_cannot_draw_is_reported_by_the_edit_that_drew_it() { + let mut s = Session::new().with_clock(|| 1_700_000_000); + let id = s.open("form.pdf", None, Arc::new(form_fixture()), None).unwrap(); + let fill = |s: &mut Session, name: &str, v: FieldValue| s.apply(id, Edit::SetFieldValue { name: name.into(), value: v }); + fill(&mut s, "name", FieldValue::Text("张三".into())).unwrap(); + let d = s.get(id).unwrap(); + assert_eq!(d.form[0].value, ["张三"]); + let n = &d.edit_notices; + assert_eq!(n.len(), 1, "{n:?}"); + assert_eq!((n[0].field.as_deref(), n[0].page, n[0].ch, n[0].redrawn_elsewhere), (Some("name"), 0, '张', true)); + assert!(n[0].to_string().contains("\"张\" (U+5F20)") && n[0].to_string().contains("other PDF viewers redraw"), "{}", n[0]); + fill(&mut s, "ok", FieldValue::Check(true)).unwrap(); + assert!(s.get(id).unwrap().edit_notices.is_empty(), "an unrelated edit reports nothing"); + fill(&mut s, "name", FieldValue::Text("李四".into())).unwrap(); + assert_eq!(s.get(id).unwrap().edit_notices.len(), 1, "a new value is reported again"); + assert!(fill(&mut s, "missing", FieldValue::Text("x".into())).is_err()); + assert!(s.get(id).unwrap().edit_notices.is_empty(), "no stale notice after a failed edit"); + fill(&mut s, "name", FieldValue::Text("Ada".into())).unwrap(); + assert!(s.get(id).unwrap().edit_notices.is_empty()); + let saved = pdfcraft_cos::Document::open(s.save_bytes(id).unwrap()).unwrap(); + assert!(pdfcraft_forms::need_appearances(&saved)); +} + #[test] fn filling_a_form_shows_saves_and_undoes() { let mut s = Session::new().with_clock(|| 1_700_000_000); diff --git a/crates/engine/src/xfa.rs b/crates/engine/src/xfa.rs index b0f4c0b3..bca3bc36 100644 --- a/crates/engine/src/xfa.rs +++ b/crates/engine/src/xfa.rs @@ -453,6 +453,7 @@ impl<'a, 'b> Runner<'a, 'b> { let report = pdfcraft_xfa::rerender(self.doc, self.tpl).map_err(|e| e.to_string())?; self.console(report.warnings); crate::xfa_redraw(self.doc)?; + self.console(crate::undrawn_field_warnings(self.doc)); self.by_som = None; self.fields = None; self.cached = None; diff --git a/crates/fonts/README.md b/crates/fonts/README.md index 44b617dd..4f0dda79 100644 --- a/crates/fonts/README.md +++ b/crates/fonts/README.md @@ -9,7 +9,13 @@ Today it holds what generated appearances need without a font program: enough for line breaking and alignment, not for typesetting. - `wrap`: greedy line breaking with that measure (paragraphs on newlines, long words split). - `win_ansi`: Unicode → WinAnsiEncoding bytes (`?` for characters it can't represent), and - `literal` to write bytes as a PDF literal string. + `literal` to write bytes as a PDF literal string. Nothing writes that `?` silently. Callers + check `first_undrawable` (or `UnicodeCMap::first_unencodable`, for a character beyond U+FFFF in + UCS-2), then either refuse with `undrawable_message` (one wording that names the character and + what can be written instead) where the `?` would be permanent page content, or, for a form + value or a comment's text, which is stored as typed, draw the `?` and mark the annotation with + `set_undrawn_mark` (`UNDRAWN_MARK`, read back by `undrawn_mark`) so the edit is reported and + flattening refuses. `script_missing` names what the typed-signature script font and its Inter fallback both lack. It also holds the fonts of the optional [craft-fonts](https://github.com/storytold/craft-fonts) build input. `build.rs` embeds them as `CRAFT_FONTS` when the build sets diff --git a/crates/fonts/src/lib.rs b/crates/fonts/src/lib.rs index 672e0a6c..2cc41ba3 100644 --- a/crates/fonts/src/lib.rs +++ b/crates/fonts/src/lib.rs @@ -15,7 +15,9 @@ pub use craft::{ CRAFT_FONTS, CraftFont, SHIPPORI_MINCHO, document_arabic_font, document_hebrew_font, document_japanese_font, document_japanese_font_for_style, document_japanese_fonts_for_style, ui_arabic_fonts, ui_chinese_fonts, ui_cjk_fonts, ui_hebrew_fonts, ui_japanese_fonts, ui_telugu_fonts, }; -pub use script::{GlyphError, GlyphOutline, INTER_REGULAR, MAX_SIGNATURE_CHARS, ScriptOutline, japanese_glyph, japanese_glyph_from, script_outline}; +pub use script::{ + GlyphError, GlyphOutline, INTER_REGULAR, MAX_SIGNATURE_CHARS, ScriptOutline, japanese_glyph, japanese_glyph_from, script_missing, script_outline, +}; /// Approximate advance of `s` in Helvetica (or Arial) at `size` points. pub fn helvetica_width(s: &str, size: f64) -> f64 { @@ -151,7 +153,17 @@ impl UnicodeCMap { }) } - /// Encode `s`; tabs become spaces, and characters UCS-2 can't hold become `?`. + /// The first character of `s` this encoding can't hold, which [`Self::encode`] would write as + /// `?`: a character beyond U+FFFF in UCS-2 (`𠀋`). UTF-16, UTF-8 and UTF-32 hold every one. + pub fn first_unencodable(self, s: &str) -> Option { + match self { + Self::Ucs2 => s.chars().find(|c| u16::try_from(u32::from(*c)).is_err()), + Self::Utf16 | Self::Utf8 | Self::Utf32 => None, + } + } + + /// Encode `s`; tabs become spaces, and characters UCS-2 can't hold become `?` (check them + /// first with [`Self::first_unencodable`]: PdfCraft refuses such text rather than write `?`). pub fn encode(self, s: &str) -> Vec { let mut out = Vec::with_capacity(s.len().saturating_mul(2)); let mut units = [0u16; 2]; @@ -186,6 +198,47 @@ pub fn first_non_win_ansi(s: &str) -> Option { s.chars().find(|c| !win_ansi_encodable(*c)) } +/// The first character of `s` the standard fonts (Helvetica and the rest of the standard 14, in +/// WinAnsiEncoding) can't draw, so that a generated appearance would show it as `?`. Line breaks +/// don't count: appearances lay lines out themselves. PdfCraft refuses such text with +/// [`undrawable_message`] instead of writing `?` (AGENTS.md §4: never lose work silently). +pub fn first_undrawable(s: &str) -> Option { + s.chars().find(|c| !matches!(c, '\n' | '\r') && !win_ansi_encodable(*c)) +} + +/// The error for text the standard fonts can't draw ([`first_undrawable`]): which character, in +/// what (`"the comment"`, `"the reason"`), and what can be written instead. One wording for every +/// path that writes text in the standard fonts, so agents and users can act on it the same way. +pub fn undrawable_message(what: &str, c: char) -> String { + format!( + "the standard fonts can't draw \"{}\" (U+{:04X}) in {what}; only Western European characters can be written there for now", + c.escape_debug(), + u32::from(c) + ) +} + +/// The private key on an annotation (a comment or a form field's widget) whose appearance PdfCraft +/// drew with `?` in place of characters its fonts can't draw: a text string holding the first +/// such character. The text itself (a field's `/V`, a comment's `/Contents`) is stored as typed; +/// the mark lets flattening refuse to make that `?` permanent, lets an edit report it, and tells +/// such an appearance apart from one another program drew with a font that has those characters. +pub const UNDRAWN_MARK: &[u8] = b"PCUndrawn"; + +/// The character [`UNDRAWN_MARK`] records on `d`, if PdfCraft drew its appearance with `?`. +pub fn undrawn_mark(d: &pdfcraft_cos::Dict) -> Option { + d.get(UNDRAWN_MARK).and_then(pdfcraft_cos::Object::as_string).and_then(|s| s.to_text().chars().next()) +} + +/// Record (`Some`) or clear (`None`) [`UNDRAWN_MARK`] on `d`, for the appearance just drawn. +pub fn set_undrawn_mark(d: &mut pdfcraft_cos::Dict, c: Option) { + match c { + Some(c) => d.set(UNDRAWN_MARK.to_vec(), pdfcraft_cos::PdfString::text(&c.to_string())), + None => { + d.remove(UNDRAWN_MARK); + } + } +} + /// Bytes as a PDF literal string, `(` … `)`, with delimiters escaped. pub fn literal(bytes: &[u8]) -> Vec { let mut out = Vec::with_capacity(bytes.len() + 2); @@ -216,6 +269,30 @@ mod tests { } assert_eq!(first_non_win_ansi("Café — 5€"), None); assert_eq!(first_non_win_ansi("Dvořák"), Some('ř')); + // Line breaks are laid out, not drawn; other controls would be written as `?`. + assert_eq!(first_undrawable("Café\r\n— 5€\tok"), None); + assert_eq!(first_undrawable("签名 Ada"), Some('签')); + assert_eq!(first_undrawable("a\u{7}b"), Some('\u{7}')); + assert_eq!(first_undrawable("𠀋"), Some('𠀋')); + let m = undrawable_message("the comment", '签'); + assert_eq!(m, "the standard fonts can't draw \"签\" (U+7B7E) in the comment; only Western European characters can be written there for now"); + assert!(undrawable_message("the reason", '\u{7}').contains("\"\\u{7}\" (U+0007)")); + assert!(undrawable_message("x", '𠀋').contains("(U+2000B)")); + } + + #[test] + fn the_undrawn_mark_records_one_character_and_clears() { + let mut d = pdfcraft_cos::Dict::new(); + assert_eq!(undrawn_mark(&d), None); + set_undrawn_mark(&mut d, Some('𠀋')); + assert_eq!(undrawn_mark(&d), Some('𠀋'), "beyond U+FFFF, as a UTF-16 text string"); + set_undrawn_mark(&mut d, Some('签')); + assert_eq!(undrawn_mark(&d), Some('签')); + set_undrawn_mark(&mut d, None); + assert!(!d.contains(UNDRAWN_MARK)); + // Another value under the key is not a character: no mark. + d.set(UNDRAWN_MARK.to_vec(), pdfcraft_cos::Object::Bool(true)); + assert_eq!(undrawn_mark(&d), None); } #[test] @@ -261,6 +338,12 @@ mod tests { assert_eq!(UnicodeCMap::Utf16.encode("令A"), [0x4e, 0xe4, 0x00, 0x41]); assert_eq!(UnicodeCMap::Utf16.encode("𠀋"), [0xd8, 0x40, 0xdc, 0x0b]); assert_eq!(UnicodeCMap::Ucs2.encode("𠀋\t"), [0x00, b'?', 0x00, b' ']); + // That `?` is never written silently: callers check first. + assert_eq!(UnicodeCMap::Ucs2.first_unencodable("令和𠀋"), Some('𠀋')); + assert_eq!(UnicodeCMap::Ucs2.first_unencodable("令和"), None); + for cmap in [UnicodeCMap::Utf16, UnicodeCMap::Utf8, UnicodeCMap::Utf32] { + assert_eq!(cmap.first_unencodable("令和𠀋"), None, "{cmap:?}"); + } assert_eq!(UnicodeCMap::Utf8.encode("令"), "令".as_bytes()); assert_eq!(UnicodeCMap::Utf32.encode("令"), [0, 0, 0x4e, 0xe4]); assert_eq!(cjk_width("令和", 10.0), 20.0); diff --git a/crates/fonts/src/script.rs b/crates/fonts/src/script.rs index 5fee1009..70fc6ce0 100644 --- a/crates/fonts/src/script.rs +++ b/crates/fonts/src/script.rs @@ -188,9 +188,21 @@ pub(crate) fn bounded_outline(font: &FontRef, gid: GlyphId, width: f64) -> Resul Ok(GlyphOutline { contours: pen.contours, width, bbox }) } +/// The first character of `text` (other than white space) that neither the script font of typed +/// signatures nor its Inter fallback has a glyph for: [`script_outline`] would leave it out. +/// `None` when they can draw all of them. +pub fn script_missing(text: &str) -> Option { + let font = FontRef::new(FONT).ok()?; + let charmap = font.charmap(); + let fallback = FontRef::new(INTER_REGULAR).ok(); + let fallback_map = fallback.as_ref().map(|f| f.charmap()); + text.chars().find(|c| !c.is_whitespace() && charmap.map(*c).is_none() && fallback_map.as_ref().and_then(|m| m.map(*c)).is_none()) +} + /// The outlines of `text` in the script font. Characters it lacks (Cyrillic, Greek, …) are drawn /// from Inter, slanted and scaled to the script's capital height; characters neither font has are -/// skipped. Over-limit input returns an empty outline instead of a silently truncated signature. +/// skipped (check [`script_missing`] first where nobody sees a preview). Over-limit input returns +/// an empty outline instead of a silently truncated signature. pub fn script_outline(text: &str) -> ScriptOutline { if text.chars().take(MAX_SIGNATURE_CHARS + 1).count() > MAX_SIGNATURE_CHARS { return ScriptOutline::default(); @@ -281,6 +293,17 @@ mod tests { assert!(mixed.width > latin_only.width + 0.5, "{} vs {}", mixed.width, latin_only.width); } + #[test] + fn missing_script_glyphs_are_named() { + assert_eq!(super::script_missing("Ada Lovelace-King"), None); + assert_eq!(super::script_missing("Zoë Ñúñez"), None); + assert_eq!(super::script_missing("Ada 张三"), Some('张')); + // Cyrillic and Greek come from the Inter fallback (#963), so they aren't missing. + assert_eq!(super::script_missing("Саша Дермановић Νίκος"), None); + assert_eq!(super::script_missing("Саша 张"), Some('张')); + assert_eq!(super::script_missing(" \t"), None); + } + #[test] fn a_name_becomes_outlines() { let o = super::script_outline("Ada L."); diff --git a/crates/forms/README.md b/crates/forms/README.md index 9af549ad..3280cc24 100644 --- a/crates/forms/README.md +++ b/crates/forms/README.md @@ -33,7 +33,15 @@ delete_field(&mut doc, "email")?; field tree; the form's `/DA` and `/Q` are the last fallback. - Text and choice fields get new appearance streams (`/Tx BMC … EMC`): auto-size (`0 Tf`), comb, multiline wrapping, password masking, quadding, `/MK` background and border, list-box selection. - The `/DA` font is used when `/DR` has it as a simple font; otherwise Helvetica (WinAnsi). + The `/DA` font is used when `/DR` has it as a simple font or a composite font with a predefined + Unicode CMap; otherwise Helvetica (WinAnsi). A value or button caption the font can't hold + (Chinese in a Helvetica field, a character beyond U+FFFF in a UCS-2 CMap) is kept, as Acrobat + keeps it: `/V` is stored as typed, the appearance draws `?` for those characters, the widget is + marked with the first one (`pdfcraft_fonts::UNDRAWN_MARK`, cleared once all of it can be drawn) + and the form sets `/NeedAppearances`, so other viewers draw the field from its value with fonts + that have them. Not in a document that declares PDF/A, which forbids that flag (`declares_pdfa`). + `undrawn_fields` lists the marked fields; the engine reports each edit that draws one, and + flattening refuses to make that `?` permanent. - Check boxes and radio buttons switch `/V` and each widget's `/AS`. Widgets with no appearances get PdfCraft's own (a drawn check mark or dot; no symbol font needed). - Values are validated (options, `MaxLen`, single vs multi-select, NoToggleToOff, read-only) and a diff --git a/crates/forms/src/appearance.rs b/crates/forms/src/appearance.rs index 8478cc26..dc5ffcef 100644 --- a/crates/forms/src/appearance.rs +++ b/crates/forms/src/appearance.rs @@ -8,9 +8,13 @@ //! The `/DA` font is used when the form's `/DR` defines it as a simple font that can encode the //! displayed text (using its own encoding and Differences), or as a composite font with a //! predefined Unicode CMap such as `UniJIS-UTF16-H`, as Japanese forms use. Otherwise (unsupported -//! characters, other composite fonts, missing resources) Helvetica is used. Widths use the -//! approximate metrics of `pdfcraft-fonts` (Helvetica, or one em per full-width character in -//! composite fonts). +//! characters, other composite fonts, missing resources) Helvetica is used. Text the chosen font +//! can't hold (outside WinAnsi in Helvetica, or beyond U+FFFF in a UCS-2 CMap) is drawn as `?`, as +//! Acrobat keeps a value its field font can't show: the value itself is stored as typed, +//! [`field_appearance_drawn`] returns the first such character, and the caller marks the widget +//! (`pdfcraft_fonts::UNDRAWN_MARK`) and asks other viewers to redraw the field +//! (`/NeedAppearances`). Widths use the approximate metrics of `pdfcraft-fonts` (Helvetica, or one +//! em per full-width character in composite fonts). //! //! `/MK /R` (0, 90, 180, 270, counterclockwise) lays the content out in the upright box and sets //! the appearance `/Matrix` so that box lands on the widget rectangle. Any other value is 0, and @@ -139,6 +143,19 @@ impl FontText { } } + /// The first character of `s` the encoding can't hold, which [`Self::encode`] writes as `?` + /// (a reused simple font is only chosen when it holds them all): reported by + /// [`field_appearance_drawn`], so that `?` is never silent (AGENTS.md §4). + fn first_unencodable(&self, s: &str) -> Option { + match self { + Self::WinAnsi => pdfcraft_fonts::first_undrawable(s), + Self::Simple(metrics) => { + s.chars().map(|c| if c == '\t' { ' ' } else { c }).find(|c| metrics.encode(c.encode_utf8(&mut [0; 4])).is_none()) + } + Self::Unicode(cmap) => cmap.first_unencodable(s), + } + } + fn width(&self, s: &str, size: f64) -> f64 { match self { Self::WinAnsi | Self::Simple(_) => helvetica_width(s, size), @@ -268,7 +285,8 @@ fn frame(doc: &Document, wd: &Dict, w: f64, h: f64) -> (String, f64) { (c, 0.0) } -/// The appearance of a text or choice field's widget showing `values`. +/// The appearance of a text or choice field's widget showing `values`. Characters its font can't +/// hold come out as `?`; [`field_appearance_drawn`] says which. pub fn field_appearance(doc: &Document, f: &Field, w: &Widget, values: &[String]) -> Stream { field_appearance_as(doc, f, w, values, true) } @@ -276,6 +294,13 @@ pub fn field_appearance(doc: &Document, f: &Field, w: &Widget, values: &[String] /// [`field_appearance`], where `format` false means `values` are already what to show (a /// custom Format script ran). pub fn field_appearance_as(doc: &Document, f: &Field, w: &Widget, values: &[String], format: bool) -> Stream { + field_appearance_drawn(doc, f, w, values, format).0 +} + +/// [`field_appearance_as`] with the first character it drew as `?` because the field's font can't +/// hold it: the form's font is a simple (WinAnsi) font, Helvetica stands in for one PdfCraft can't +/// write with (an embedded subset, `Identity-H`), or a UCS-2 CMap meets a character beyond U+FFFF. +pub fn field_appearance_drawn(doc: &Document, f: &Field, w: &Widget, values: &[String], format: bool) -> (Stream, Option) { // The Format event: what is shown, not what is stored. let formatted: Vec; let values = if format @@ -319,9 +344,16 @@ pub fn field_appearance_as(doc: &Document, f: &Field, w: &Widget, values: &[Stri let inner_w = (width - 2.0 * pad).max(1.0); let q = wd.get(b"Q").and_then(|o| doc.resolve(o).as_int()).unwrap_or(f.quadding); let mut body: Vec = Vec::new(); - let show = |body: &mut Vec, x: f64, y: f64, text: &str| { + // The first character drawn that the font can't hold (drawn as `?`), for the caller to report. + let mut unencodable: Option = None; + let mut show = |body: &mut Vec, x: f64, y: f64, text: &str| { + // A line break in a single-line value is a space, as in viewers (multiline text is split). + let text: String = text.chars().map(|c| if matches!(c, '\n' | '\r') { ' ' } else { c }).collect(); + if unencodable.is_none() { + unencodable = enc.first_unencodable(&text); + } body.extend(format!("1 0 0 1 {} {} Tm ", n(x), n(y)).bytes()); - body.extend(literal(&enc.encode(text))); + body.extend(literal(&enc.encode(&text))); body.extend_from_slice(b" Tj\n"); }; let x_for = |text: &str, size: f64| -> f64 { @@ -419,7 +451,7 @@ pub fn field_appearance_as(doc: &Document, f: &Field, w: &Widget, values: &[Stri d.set(b"Subtype".to_vec(), Object::name("Form")); set_form_box(&mut d, place); d.set(b"Resources".to_vec(), Object::Dict(res)); - Stream::flate(d, &content) + (Stream::flate(d, &content), unencodable) } /// On/Off appearances for a check box or radio button that has none (a check mark or a dot, diff --git a/crates/forms/src/author.rs b/crates/forms/src/author.rs index e75beeb3..458bf3f0 100644 --- a/crates/forms/src/author.rs +++ b/crates/forms/src/author.rs @@ -669,13 +669,19 @@ pub fn add_field(doc: &mut Document, page: usize, rect: [f64; 4], kind: &NewFiel Ok(field_name) } -/// Regenerate every widget appearance of a field from its current value and settings. +/// Regenerate every widget appearance of a field from its current value and settings. A value or +/// button caption its font can't hold is drawn with `?` for those characters, the widget is marked +/// (`pdfcraft_fonts::UNDRAWN_MARK`) and other viewers are asked to redraw the form (see +/// `ask_viewers_to_redraw`), as when a value is set. pub fn redraw_field(doc: &mut Document, name: &str) -> Result<(), FormError> { let f = fields(doc).into_iter().find(|f| f.name == name).ok_or_else(|| FormError::NoSuchField(name.into()))?; + let mut undrawn = false; for w in &f.widgets { + let mut missing = None; let ap = match f.kind { FieldKind::Text | FieldKind::Combo | FieldKind::List => { - let s = appearance::field_appearance(doc, &f, w, &f.value); + let (s, m) = appearance::field_appearance_drawn(doc, &f, w, &f.value, true); + missing = m; let r = doc.add(Object::Stream(s)); let mut d = Dict::new(); d.set(b"N".to_vec(), Object::Ref(r)); @@ -686,7 +692,8 @@ pub fn redraw_field(doc: &mut Document, name: &str) -> Result<(), FormError> { appearance::check_box_states(doc, w, f.kind, &on) } FieldKind::PushButton => { - let s = button_appearance(doc, w); + let (s, m) = button_appearance(doc, w); + missing = m; let r = doc.add(Object::Stream(s)); let mut d = Dict::new(); d.set(b"N".to_vec(), Object::Ref(r)); @@ -700,8 +707,15 @@ pub fn redraw_field(doc: &mut Document, name: &str) -> Result<(), FormError> { d } }; + undrawn |= missing.is_some(); let existing = crate::merged_appearance(doc, w.obj, &ap); - doc.update_dict(w.obj, |d| d.set(b"AP".to_vec(), Object::Dict(existing)))?; + doc.update_dict(w.obj, |d| { + d.set(b"AP".to_vec(), Object::Dict(existing)); + pdfcraft_fonts::set_undrawn_mark(d, missing); + })?; + } + if undrawn { + crate::ask_viewers_to_redraw(doc)?; } Ok(()) } @@ -737,8 +751,9 @@ fn form_stream(place: crate::appearance::Placement, content: Vec, resources: } /// A push button: background, border, its icon (`/MK /I`, scaled to fit and centred) and its -/// centred caption (unless the layout is icon only, `/TP 1`). -fn button_appearance(doc: &Document, w: &Widget) -> Stream { +/// centred caption (unless the layout is icon only, `/TP 1`). The caption is drawn in Helvetica, +/// with the first character it can't draw (drawn as `?`), for the caller to mark and report. +fn button_appearance(doc: &Document, w: &Widget) -> (Stream, Option) { let (mut c, place) = frame_only(doc, w); let (width, height) = (place.layout_w, place.layout_h); let wobj = doc.get(w.obj); @@ -762,7 +777,9 @@ fn button_appearance(doc: &Document, w: &Widget) -> Stream { xobjects.set(b"Icon".to_vec(), Object::Ref(icon)); } } + let mut missing = None; if !icon_only && !caption.is_empty() { + missing = pdfcraft_fonts::first_undrawable(&caption); let size = ((height - 4.0) * 0.6).clamp(4.0, 14.0); let tw = helvetica_width(&caption, size); content.extend(format!("BT /Helv {size:.2} Tf 0 g 1 0 0 1 {:.3} {:.3} Tm ", (width - tw) / 2.0, (height - size * 0.7) / 2.0).bytes()); @@ -781,7 +798,7 @@ fn button_appearance(doc: &Document, w: &Widget) -> Stream { if !xobjects.is_empty() { res.set(b"XObject".to_vec(), Object::Dict(xobjects)); } - form_stream(place, content, res) + (form_stream(place, content, res), missing) } /// Give a push button (an image field) the picture `image` (an image XObject of `px` pixels): diff --git a/crates/forms/src/lib.rs b/crates/forms/src/lib.rs index 5d889559..b3de8ca6 100644 --- a/crates/forms/src/lib.rs +++ b/crates/forms/src/lib.rs @@ -1096,11 +1096,13 @@ fn redraw(doc: &mut Document, f: &Field, values: &[String], scripts: &mut dyn Sc [one] if matches!(f.kind, FieldKind::Text | FieldKind::Combo) => scripting::formatted(doc, f, one, scripts), _ => None, }; + let mut undrawn = false; for w in &f.widgets { - let stream = match &shown { - Some(s) => appearance::field_appearance_as(doc, f, w, std::slice::from_ref(s), false), - None => appearance::field_appearance(doc, f, w, values), + let (stream, missing) = match &shown { + Some(s) => appearance::field_appearance_drawn(doc, f, w, std::slice::from_ref(s), false), + None => appearance::field_appearance_drawn(doc, f, w, values, true), }; + undrawn |= missing.is_some(); let ap = doc.add(Object::Stream(stream)); let mut fresh = Dict::new(); fresh.set(b"N".to_vec(), Object::Ref(ap)); @@ -1108,11 +1110,66 @@ fn redraw(doc: &mut Document, f: &Field, values: &[String], scripts: &mut dyn Sc doc.update_dict(w.obj, |d| { d.set(b"AP".to_vec(), Object::Dict(apd)); d.remove(b"AS"); + pdfcraft_fonts::set_undrawn_mark(d, missing); })?; } + if undrawn { + ask_viewers_to_redraw(doc)?; + } Ok(()) } +/// A value its widget's font can't show is stored as typed and drawn with `?` (as Acrobat keeps +/// such a value): `/NeedAppearances` then asks other viewers to draw the fields from their values +/// with fonts that have those characters (the interactive form dictionary, §12.7.3). Not in a +/// document that declares PDF/A, which forbids it: there the widget's mark +/// (`pdfcraft_fonts::UNDRAWN_MARK`) is all, and the edit's report says the `?` shows everywhere. +pub(crate) fn ask_viewers_to_redraw(doc: &mut Document) -> Result<(), FormError> { + if declares_pdfa(doc) || need_appearances(doc) { + return Ok(()); + } + let Some(root) = doc.root() else { return Ok(()) }; + let entry = doc.get(root).as_dict().and_then(|c| c.get(b"AcroForm").cloned()); + match entry { + Some(Object::Ref(r)) => doc.update_dict(r, |d| d.set(b"NeedAppearances".to_vec(), Object::Bool(true)))?, + Some(Object::Dict(mut af)) => { + af.set(b"NeedAppearances".to_vec(), Object::Bool(true)); + doc.update_dict(root, |c| c.set(b"AcroForm".to_vec(), Object::Dict(af)))?; + } + _ => {} + } + Ok(()) +} + +/// Whether the form asks viewers to draw its fields' appearances themselves (`/NeedAppearances`). +pub fn need_appearances(doc: &Document) -> bool { + acroform(doc).is_some_and(|af| matches!(af.get(b"NeedAppearances").map(|o| doc.resolve(o)).as_deref(), Some(Object::Bool(true)))) +} + +/// Whether the document's XMP metadata declares PDF/A conformance (`pdfaid:part`). +pub fn declares_pdfa(doc: &Document) -> bool { + let Some(root) = doc.root() else { return false }; + let Some(m) = doc.get(root).as_dict().and_then(|c| c.get(b"Metadata").cloned()) else { return false }; + let m = doc.resolve(&m); + match &*m { + Object::Stream(s) => s.decoded().is_ok_and(|b| b.windows(11).any(|w| w == b"pdfaid:part")), + _ => false, + } +} + +/// Every field with a widget whose appearance PdfCraft drew with `?` for a character its font +/// can't hold (`pdfcraft_fonts::UNDRAWN_MARK`): the field's name and that character, once per +/// field. +pub fn undrawn_fields(doc: &Document) -> Vec<(String, char)> { + fields(doc) + .into_iter() + .filter_map(|f| { + let c = f.widgets.iter().find_map(|w| doc.get(w.obj).as_dict().and_then(pdfcraft_fonts::undrawn_mark))?; + Some((f.name, c)) + }) + .collect() +} + /// Acrobat's Clear form: every field (or the named ones) back to its default value. pub fn reset(doc: &mut Document, names: Option<&[String]>) -> Result { let all = fields(doc); diff --git a/crates/forms/src/tests.rs b/crates/forms/src/tests.rs index aa98dd7c..45e85013 100644 --- a/crates/forms/src/tests.rs +++ b/crates/forms/src/tests.rs @@ -369,6 +369,8 @@ fn japanese_choices_use_the_unicode_cid_font() { assert_eq!(field(&fields(&doc), "era").options.get(2), Some(&("令".to_string(), "令".to_string()))); set_value(&mut doc, "era", &FieldValue::Text("令".into())).unwrap(); set_value(&mut doc, "address", &FieldValue::Text("日本語のテキスト入力欄です".into())).unwrap(); + // An Identity-H subset can't be addressed by Unicode: Helvetica stands in, and can't draw 令. + // The value is kept, drawn as `??`, marked, and other viewers are asked to redraw the form. set_value(&mut doc, "subset", &FieldValue::Text("令和".into())).unwrap(); let doc = reopen(&doc); let all = fields(&doc); @@ -392,10 +394,115 @@ fn japanese_choices_use_the_unicode_cid_font() { let addr_text = String::from_utf8_lossy(&addr); assert!(addr_text.matches(" Tj").count() >= 4, "{addr_text}"); assert!(addr.windows(4).any(|x| x == utf16("日本").as_slice())); - // An Identity-H subset can't be addressed by Unicode: Helvetica, as before. let (sub, sub_fonts) = raw("subset"); assert!(String::from_utf8_lossy(&sub).contains("/Helv "), "{}", String::from_utf8_lossy(&sub)); + assert!(sub.windows(4).any(|x| x == b"(??)"), "{}", String::from_utf8_lossy(&sub)); assert_eq!(sub_fonts, ["Helv"]); + assert_eq!(field(&all, "subset").value, ["令和"]); + // Only that widget is marked: the CID fonts drew theirs. + assert_eq!(undrawn_fields(&doc), [("subset".to_string(), '令')]); + assert!(need_appearances(&doc)); +} + +/// Text a field's font can't hold is kept, as Acrobat keeps it: the value is stored as typed, the +/// appearance draws `?` for those characters, the widget is marked with the first one, and the +/// form asks other viewers to redraw it (`/NeedAppearances`). Drawable text clears the mark. +#[test] +fn text_a_field_font_cannot_hold_is_kept_marked_and_redrawn_elsewhere() { + let objs: Vec = vec![ + "<< /Type /Catalog /Pages 2 0 R /AcroForm 4 0 R >>".into(), + "<< /Type /Pages /Kids [3 0 R] /Count 1 /MediaBox [0 0 600 800] >>".into(), + "<< /Type /Page /Parent 2 0 R /Annots [7 0 R 8 0 R 9 0 R] >>".into(), + "<< /Fields [7 0 R 8 0 R 9 0 R] /DA (/Helv 0 Tf 0 g) /DR << /Font << /Helv 5 0 R /CN 6 0 R >> >> >>".into(), + "<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica /Encoding /WinAnsiEncoding >>".into(), + "<< /Type /Font /Subtype /Type0 /BaseFont /STSong-Light /Encoding /UniGB-UCS2-H /DescendantFonts [<< /Type /Font /Subtype /CIDFontType0 /BaseFont /STSong-Light /CIDSystemInfo << /Registry (Adobe) /Ordering (GB1) /Supplement 4 >> >>] >>".into(), + "<< /Type /Annot /Subtype /Widget /FT /Tx /T (name) /DA (/Helv 10 Tf 0 g) /V (Ada) /Rect [50 700 200 715] /P 3 0 R >>".into(), + "<< /Type /Annot /Subtype /Widget /FT /Tx /T (cn) /DA (/CN 10 Tf 0 g) /Rect [50 600 200 615] /P 3 0 R >>".into(), + "<< /Type /Annot /Subtype /Widget /FT /Ch /T (city) /Ff 131072 /DA (/Helv 10 Tf 0 g) /Opt [(Zurich) ] /Rect [50 500 200 515] /P 3 0 R >>".into(), + ]; + let mut doc = document(&objs); + let shown = |doc: &Document, name: &str| { + let all = fields(doc); + let w = &field(&all, name).widgets[0]; + let n = doc.get(w.obj).as_dict().unwrap().get(b"AP").unwrap().as_dict().unwrap().reference(b"N").unwrap(); + let Object::Stream(s) = &*doc.get(n) else { panic!() }; + let mark = doc.get(w.obj).as_dict().and_then(pdfcraft_fonts::undrawn_mark); + (String::from_utf8_lossy(&s.decoded().unwrap()).into_owned(), mark) + }; + // A WinAnsi field: the Chinese value is kept and marked; Helvetica draws `?` for it. + assert!(!need_appearances(&doc)); + set_value(&mut doc, "name", &FieldValue::Text("签名 Ada".into())).unwrap(); + assert_eq!(field(&fields(&doc), "name").value, ["签名 Ada"]); + let (ap, mark) = shown(&doc, "name"); + assert!(ap.contains("(?? Ada) Tj"), "{ap}"); + assert_eq!(mark, Some('签')); + assert!(need_appearances(&doc), "other viewers are asked to redraw the form"); + // A choice whose label Helvetica can't draw, the same way. + set_value(&mut doc, "city", &FieldValue::Text("北N京".into())).unwrap(); + assert_eq!(shown(&doc, "city").1, Some('北')); + // A UCS-2 CMap holds 签, so nothing is marked, but not a character beyond U+FFFF. + set_value(&mut doc, "cn", &FieldValue::Text("签名".into())).unwrap(); + assert_eq!(shown(&doc, "cn").1, None); + set_value(&mut doc, "cn", &FieldValue::Text("𠀋".into())).unwrap(); + assert_eq!(field(&fields(&doc), "cn").value, ["𠀋"]); + assert_eq!(shown(&doc, "cn").1, Some('𠀋')); + let mut undrawn = undrawn_fields(&doc); + undrawn.sort(); + assert_eq!(undrawn, [("city".to_string(), '北'), ("cn".to_string(), '𠀋'), ("name".to_string(), '签')]); + // Drawable text clears the mark; a line break in a single-line value is a space, not `?`. + set_value(&mut doc, "name", &FieldValue::Text("Ada\nLovelace".into())).unwrap(); + let (ap, mark) = shown(&doc, "name"); + assert!(ap.contains("(Ada Lovelace) Tj") && !ap.contains('?'), "{ap}"); + assert_eq!(mark, None); + // Kept through a save. + let doc = reopen(&doc); + assert_eq!(field(&fields(&doc), "city").value, ["北N京"]); + assert!(need_appearances(&doc)); +} + +/// A document that declares PDF/A can't ask other viewers to redraw its fields (PDF/A forbids +/// `/NeedAppearances`): the value is still kept and its widget marked, and the flag stays off. +#[test] +fn a_pdfa_form_keeps_the_value_without_asking_viewers_to_redraw() { + let xmp = r#""#; + let objs: Vec = vec![ + "<< /Type /Catalog /Pages 2 0 R /AcroForm 4 0 R /Metadata 7 0 R >>".into(), + "<< /Type /Pages /Kids [3 0 R] /Count 1 /MediaBox [0 0 600 800] >>".into(), + "<< /Type /Page /Parent 2 0 R /Annots [6 0 R] >>".into(), + "<< /Fields [6 0 R] /DA (/Helv 0 Tf 0 g) /DR << /Font << /Helv 5 0 R >> >> >>".into(), + "<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica /Encoding /WinAnsiEncoding >>".into(), + "<< /Type /Annot /Subtype /Widget /FT /Tx /T (name) /DA (/Helv 10 Tf 0 g) /Rect [50 700 200 715] /P 3 0 R >>".into(), + format!("<< /Type /Metadata /Subtype /XML /Length {} >>\nstream\n{xmp}\nendstream", xmp.len() + 1), + ]; + let mut doc = document(&objs); + assert!(declares_pdfa(&doc)); + set_value(&mut doc, "name", &FieldValue::Text("张三".into())).unwrap(); + assert_eq!(field(&fields(&doc), "name").value, ["张三"]); + assert_eq!(undrawn_fields(&doc), [("name".to_string(), '张')]); + assert!(!need_appearances(&doc), "PDF/A forbids /NeedAppearances"); +} + +/// A button caption Helvetica can't draw is kept and marked like a value; redrawing a value another +/// program stored (a dynamic form's layout, a property change) draws and marks it rather than +/// failing, and an edit to an unrelated field never fails because of it. +#[test] +fn captions_and_redrawn_values_are_kept_and_marked() { + let mut doc = fixture(); + let go = add_field(&mut doc, 0, [50.0, 50.0, 150.0, 80.0], &NewField::Button { caption: "提交".into() }, Some("go2")).unwrap(); + assert_eq!(undrawn_fields(&doc), [(go.clone(), '提')]); + assert!(need_appearances(&doc)); + // A value stored by another program that Helvetica can't draw. + let name = field(&fields(&doc), "name").clone(); + doc.update_dict(name.obj, |d| d.set(b"V".to_vec(), PdfString::text("张三"))).unwrap(); + redraw_field(&mut doc, "name").unwrap(); + let mut undrawn = undrawn_fields(&doc); + undrawn.sort(); + assert_eq!(undrawn, [(go, '提'), ("name".to_string(), '张')]); + set_value(&mut doc, "notes", &FieldValue::Text("Unrelated".into())).unwrap(); + // Drawable again: the mark goes. + doc.update_dict(name.obj, |d| d.set(b"V".to_vec(), PdfString::text("Ada"))).unwrap(); + redraw_field(&mut doc, "name").unwrap(); + assert!(!undrawn_fields(&doc).iter().any(|(n, _)| n == "name")); } #[test] diff --git a/crates/ocr/src/lib.rs b/crates/ocr/src/lib.rs index a34386c1..d6e2f00c 100644 --- a/crates/ocr/src/lib.rs +++ b/crates/ocr/src/lib.rs @@ -234,6 +234,14 @@ mod tests { assert_eq!(w.up, [0.0, 20.0]); } + /// The text layer is Helvetica in WinAnsiEncoding: every character the model can recognise + /// must be one it can hold, or recognised text would be saved as `?`. A model with a wider + /// alphabet (Chinese, for example) needs a text layer in another font first. + #[test] + fn the_model_alphabet_fits_the_text_layer_font() { + assert_eq!(pdfcraft_fonts::first_undrawable(ALPHABET), None); + } + #[test] fn text_layer_is_invisible_text_fitted_to_each_box() { let w = PlacedWord { text: "Hi".into(), origin: [10.0, 20.0], across: [30.0, 0.0], up: [0.0, 9.3] }; diff --git a/crates/redact/src/lib.rs b/crates/redact/src/lib.rs index 4ceef50b..e9310165 100644 --- a/crates/redact/src/lib.rs +++ b/crates/redact/src/lib.rs @@ -37,6 +37,9 @@ pub enum RedactError { Unreadable(usize), #[error("redaction could not be verified: {0} item(s) were still found under the marks, so nothing was changed")] Residue(usize), + /// A mark's overlay text has a character Helvetica can't draw; nothing was redacted. + #[error("{0}")] + Undrawable(String), #[error("page {0} has vertical-writing text under a redaction mark, which PdfCraft cannot remove yet, so nothing was changed")] VerticalText(usize), #[error(transparent)] @@ -276,6 +279,14 @@ pub fn apply(doc: &mut Document, pages: Option<&[usize]>) -> Result = chosen.iter().map(|m| m.page).collect(); by_page.sort_unstable(); diff --git a/crates/redact/src/tests.rs b/crates/redact/src/tests.rs index ef725674..bd1fa5a1 100644 --- a/crates/redact/src/tests.rs +++ b/crates/redact/src/tests.rs @@ -560,6 +560,39 @@ fn overlay_text_takes_its_font_size_colour_alignment_and_repeats() { assert!(fonts.contains(b"PCCour") && !fonts.contains(b"PCTimes")); } +/// Overlay text the standard fonts can't draw: refused when it is marked, and for a mark another +/// program made, refused when it is applied, before anything is redacted, rather than burnt into +/// the page as `?`. +#[test] +fn overlay_text_the_standard_fonts_cannot_draw_is_refused() { + let mut doc = one_page(b"BT /F1 12 Tf 20 250 Td (Secret salary figures) Tj ET", "", Vec::new()); + let look = pdfcraft_annot::OverlayLook::default(); + let mark = |overlay: &str| Shape::Redact { quads: vec![rect_quad([10.0, 200.0, 290.0, 270.0])], overlay: overlay.into(), look }; + let add = |doc: &mut Document, shape: Shape| { + let style = Style::default_for(&shape); + add_annotation(doc, &NewAnnotation { page: 0, shape, style, contents: String::new(), author: "T".into() }, &Meta::default()) + }; + let err = add(&mut doc, mark("机密")).unwrap_err().to_string(); + assert!(err.contains("\"机\" (U+673A)") && err.contains("overlay text"), "{err}"); + assert!(marks(&doc).is_empty()); + // A mark whose overlay text was written by another program. + add(&mut doc, mark("REDACTED")).unwrap(); + let obj = marks(&doc)[0].obj; + doc.update_dict(obj, |d| d.set(b"OverlayText".to_vec(), pdfcraft_cos::PdfString::text("机密"))).unwrap(); + let before = content(&doc, 0); + let err = apply(&mut doc, None).unwrap_err(); + assert!( + matches!(&err, RedactError::Undrawable(m) if m.contains("page 1") && m.contains("U+673A") && m.contains("nothing was redacted")), + "{err:?}" + ); + assert_eq!(content(&doc, 0), before, "nothing was redacted"); + assert_eq!(marks(&doc).len(), 1, "the mark is still there"); + // Cleared, it applies. + doc.update_dict(obj, |d| d.set(b"OverlayText".to_vec(), pdfcraft_cos::PdfString::text(""))).unwrap(); + apply(&mut doc, None).unwrap(); + assert!(!content(&reopen(&doc), 0).contains("Secret")); +} + #[test] fn redaction_codes_join_in_set_order_and_reject_strangers() { use crate::codes::{CODE_SETS, CodeSet}; diff --git a/crates/sign/src/pdf.rs b/crates/sign/src/pdf.rs index 4ba11e9d..9627dbdf 100644 --- a/crates/sign/src/pdf.rs +++ b/crates/sign/src/pdf.rs @@ -1909,11 +1909,31 @@ pub fn display_date(pdf: &str) -> String { /// The visible signature: the signer's name large on the left, or the graphic in its place, and /// the details on the right (Acrobat's standard layout), in Helvetica. A graphic with no details -/// beside it fills the box. +/// beside it fills the box. An error, before anything is drawn, when a detail it writes in +/// 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. fn appearance(doc: &mut Document, rect: [f64; 4], name: &str, cert: &Certificate, opts: &SignOptions) -> Result { - use pdfcraft_fonts::{helvetica_width, literal, win_ansi, wrap}; + 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)); let a = &opts.appearance; + let dn = cert.subject.display(); + if w > 1.0 && h > 1.0 { + let shown = [ + (a.name, "the signer's name", Some(name)), + (a.distinguished_name, "the certificate's subject", Some(dn.as_str())), + (a.reason, "the reason", opts.reason.as_deref()), + (a.location, "the location", opts.location.as_deref()), + ]; + for (on, what, text) in shown { + if on && let Some(c) = text.and_then(first_undrawable) { + return Err(SignError::Unsupported(format!( + "{}; sign without a visible appearance, or leave {what} out of it", + undrawable_message(&format!("{what} 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() }; // The labels are the caller's words, so a signature signed in another language reads in it. @@ -1923,7 +1943,7 @@ fn appearance(doc: &mut Document, rect: [f64; 4], name: &str, cert: &Certificate lines.push(name.to_string()); } if a.distinguished_name { - lines.push(label(&wording.distinguished_name, &cert.subject.display())); + lines.push(label(&wording.distinguished_name, &dn)); } if a.reason && let Some(r) = opts.reason.as_deref().filter(|r| !r.is_empty()) diff --git a/crates/sign/tests/pdf.rs b/crates/sign/tests/pdf.rs index bb0d3b48..54d76af6 100644 --- a/crates/sign/tests/pdf.rs +++ b/crates/sign/tests/pdf.rs @@ -1738,6 +1738,29 @@ fn encrypted_documents_take_a_document_timestamp() { assert!(matches!(pdfcraft_sign::timestamp_document(&print_only, &tsa, "D:20261006120000Z"), Err(SignError::Pdf(_)))); } +/// A visible signature draws its details in Helvetica: a reason or location it can't draw is +/// refused with the character and what to do instead, rather than drawn as `?`. An +/// invisible signature stores the same reason as text and signs. +#[test] +fn visible_appearance_text_helvetica_cannot_draw_is_refused() { + let id = pkcs12::open(&data("ec-p256.p12"), "test").unwrap(); + let shown = pdfcraft_sign::Appearance { reason: true, location: true, ..pdfcraft_sign::Appearance::default() }; + let chinese = SignOptions { reason: Some("同意此文件".into()), appearance: shown.clone(), ..opts() }; + let err = pdfcraft_sign::sign(&open(&fixture()), &id, &chinese).unwrap_err().to_string(); + assert!(err.contains("\"同\" (U+540C)") && err.contains("the reason") && err.contains("sign without a visible appearance"), "{err}"); + let located = SignOptions { location: Some("北京".into()), appearance: shown, ..opts() }; + assert!(pdfcraft_sign::sign(&open(&fixture()), &id, &located).unwrap_err().to_string().contains("the location")); + // Left out of the appearance, or signed invisibly: it signs, and the reason is kept as text. + let hidden = SignOptions { reason: Some("同意此文件".into()), ..opts() }; + assert!(!hidden.appearance.reason, "the default appearance doesn't show the reason"); + pdfcraft_sign::sign(&open(&fixture()), &id, &hidden).unwrap(); + let invisible = SignOptions { rect: None, ..chinese }; + let signed = pdfcraft_sign::sign(&open(&fixture()), &id, &invisible).unwrap(); + let doc = open(&signed); + let s = signatures(&doc, &signed, &TrustStore::default()).into_iter().find(|s| s.signed).unwrap(); + assert_eq!(s.reason.as_deref(), Some("同意此文件")); +} + /// 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/content_ui.rs b/crates/ui-egui/src/content_ui.rs index fdabe8b3..49c1c9e1 100644 --- a/crates/ui-egui/src/content_ui.rs +++ b/crates/ui-egui/src/content_ui.rs @@ -42,6 +42,19 @@ pub(crate) fn undrawable_message(c: char) -> String { ) } +/// The warning after an edit that saved text its font can't draw: PdfCraft drew `?` for it (a form +/// field, which other viewers redraw from its value, or a comment, where the `?` shows everywhere). +pub(crate) fn undrawn_message(n: &pdfcraft_engine::TextNotice) -> String { + let template = if n.redrawn_elsewhere { + tl!( + "This field's font can't draw “{c}” ({code}), so PdfCraft shows ? in its place. The value is saved, and other PDF viewers redraw the field from it." + ) + } else { + tl!("The fonts PdfCraft writes with can't draw “{c}” ({code}), so it shows as ? in every PDF viewer. The text is saved.") + }; + crate::i18n::fmt(template, &[("c", &n.ch.to_string()), ("code", &format!("U+{:04X}", u32::from(n.ch)))]) +} + const SELECT_BLUE: Color32 = Color32::from_rgb(0x14, 0x73, 0xE6); /// The text being typed or retyped in place. diff --git a/crates/ui-egui/src/editing.rs b/crates/ui-egui/src/editing.rs index bd57ea4c..41126bd0 100644 --- a/crates/ui-egui/src/editing.rs +++ b/crates/ui-egui/src/editing.rs @@ -138,6 +138,11 @@ impl PdfCraftApp { if std::mem::take(&mut view.comments.tool_done) && !self.comment_prefs.pinned { self.quick_tool = crate::QuickTool::Select; } + // Text saved as typed but drawn with `?` (its font can't draw it): said, not silent. + // Before the scripts' output, so an alert they raise still shows. + if let Some(n) = self.session.get(id).and_then(|d| d.edit_notices.first()).cloned() { + self.notify(crate::content_ui::undrawn_message(&n)); + } let out = self.session.take_js_output(id); self.handle_js(id, out); true @@ -1310,6 +1315,29 @@ mod revert_draft_tests { } } + /// A value the field's font can't draw is kept, and the user is told why it shows `?`, in the + /// interface language; a drawable value says nothing. + #[test] + fn a_value_its_font_cannot_draw_is_kept_and_explained() { + let mut app = app(); + let fill = + |app: &mut PdfCraftApp, text: &str| app.apply_edit(Edit::SetFieldValue { name: "name".into(), value: FieldValue::Text(text.into()) }); + assert!(fill(&mut app, "张三")); + let toast = app.toast.as_ref().map(|t| t.0.clone()).expect("a notice"); + assert!(toast.contains("“张” (U+5F20)") && toast.contains("other PDF viewers redraw the field"), "{toast}"); + assert_eq!(app.session.get(app.views[0].id).unwrap().form.iter().find(|f| f.name == "name").unwrap().value, ["张三"]); + // The shell sets the drawing language each frame; here, directly (per thread). + let previous = crate::i18n::current(); + crate::i18n::set_current(crate::i18n::Lang::from_code("zh-hans").expect("registered")); + assert!(fill(&mut app, "李四")); + crate::i18n::set_current(previous); + let toast = app.toast.as_ref().map(|t| t.0.clone()).expect("a notice"); + assert!(toast.contains("此字段的字体无法显示“李”(U+674E)"), "{toast}"); + app.toast = None; + assert!(fill(&mut app, "Ada")); + assert!(app.toast.is_none(), "{:?}", app.toast); + } + #[test] fn unsaved_flag_follows_dirty_state() { use std::sync::atomic::Ordering; diff --git a/crates/ui-egui/src/i18n/ar.tsv b/crates/ui-egui/src/i18n/ar.tsv index f6904d3c..097afcc6 100644 --- a/crates/ui-egui/src/i18n/ar.tsv +++ b/crates/ui-egui/src/i18n/ar.tsv @@ -2152,6 +2152,8 @@ calendar date December ديسمبر Reverse closed arrow سهم مغلق معكوس Slash شرطة مائلة The standard fonts can't draw “{c}” ({code}). Only Western European characters can be added as text for now. الخطوط القياسية لا تستطيع رسم «{c}» ({code}). حاليًا يمكن إضافة محارف أوروبا الغربية فقط كنص. + This field's font can't draw “{c}” ({code}), so PdfCraft shows ? in its place. The value is saved, and other PDF viewers redraw the field from it. خط هذا الحقل لا يستطيع رسم «{c}» ({code})، لذا يعرض PdfCraft علامة ? مكانه. القيمة محفوظة، وتعيد برامج عرض PDF الأخرى رسم الحقل منها. + The fonts PdfCraft writes with can't draw “{c}” ({code}), so it shows as ? in every PDF viewer. The text is saved. الخطوط التي يكتب بها PdfCraft لا تستطيع رسم «{c}» ({code})، لذا يظهر بشكل ? في كل برامج عرض PDF. النص محفوظ. Larger pages صفحات أكبر Smaller pages صفحات أصغر Reset page size إعادة ضبط حجم الصفحة diff --git a/crates/ui-egui/src/i18n/bg.tsv b/crates/ui-egui/src/i18n/bg.tsv index 3da0853c..ecfab7a9 100644 --- a/crates/ui-egui/src/i18n/bg.tsv +++ b/crates/ui-egui/src/i18n/bg.tsv @@ -2094,6 +2094,8 @@ action wizard Start Старт Remove from Recent Премахни от последно отваряните Remove {name} from Recent Премахни {name} от последно отваряните The standard fonts can't draw “{c}” ({code}). Only Western European characters can be added as text for now. Стандартните шрифтове не могат да покажат „{c}“ ({code}). Засега като текст могат да се добавят само западноевропейски символи. + This field's font can't draw “{c}” ({code}), so PdfCraft shows ? in its place. The value is saved, and other PDF viewers redraw the field from it. Шрифтът на това поле не може да покаже „{c}“ ({code}), затова PdfCraft показва ? на негово място. Стойността е запазена и другите програми за PDF преначертават полето по нея. + The fonts PdfCraft writes with can't draw “{c}” ({code}), so it shows as ? in every PDF viewer. The text is saved. Шрифтовете, с които PdfCraft пише, не могат да покажат „{c}“ ({code}), затова във всяка програма за PDF то се показва като ?. Текстът е запазен. Larger pages По-големи страници Smaller pages По-малки страници Reset page size Възстанови размера на страниците diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv index c579e549..04110e5b 100644 --- a/crates/ui-egui/src/i18n/cs.tsv +++ b/crates/ui-egui/src/i18n/cs.tsv @@ -2324,3 +2324,5 @@ scroll speed Fast Rychlá Original Originál Greyscale Stupně šedi Black and white Černobílé + This field's font can't draw “{c}” ({code}), so PdfCraft shows ? in its place. The value is saved, and other PDF viewers redraw the field from it. Písmo tohoto pole neumí zobrazit „{c}“ ({code}), proto PdfCraft místo něj ukazuje ?. Hodnota se uloží a jiné prohlížeče PDF pole podle ní překreslí. + The fonts PdfCraft writes with can't draw “{c}” ({code}), so it shows as ? in every PDF viewer. The text is saved. Písma, kterými PdfCraft píše, neumí zobrazit „{c}“ ({code}), proto se ve všech prohlížečích PDF zobrazí jako ?. Text se uloží. diff --git a/crates/ui-egui/src/i18n/de.tsv b/crates/ui-egui/src/i18n/de.tsv index ccf07c12..b498a892 100644 --- a/crates/ui-egui/src/i18n/de.tsv +++ b/crates/ui-egui/src/i18n/de.tsv @@ -2136,6 +2136,8 @@ action wizard Start Starten Remove from Recent Aus „Zuletzt geöffnet“ entfernen Remove {name} from Recent {name} aus „Zuletzt geöffnet“ entfernen The standard fonts can't draw “{c}” ({code}). Only Western European characters can be added as text for now. Die Standardschriften können „{c}“ ({code}) nicht darstellen. Derzeit lassen sich nur westeuropäische Zeichen als Text hinzufügen. + This field's font can't draw “{c}” ({code}), so PdfCraft shows ? in its place. The value is saved, and other PDF viewers redraw the field from it. Die Schrift dieses Felds kann „{c}“ ({code}) nicht darstellen, daher zeigt PdfCraft an dieser Stelle ? an. Der Wert ist gespeichert, und andere PDF-Viewer zeichnen das Feld anhand des Werts neu. + The fonts PdfCraft writes with can't draw “{c}” ({code}), so it shows as ? in every PDF viewer. The text is saved. Die Schriften, mit denen PdfCraft schreibt, können „{c}“ ({code}) nicht darstellen, daher erscheint es in jedem PDF-Viewer als ?. Der Text ist gespeichert. Larger pages Größere Seiten Smaller pages Kleinere Seiten Reset page size Seitengröße zurücksetzen diff --git a/crates/ui-egui/src/i18n/el.tsv b/crates/ui-egui/src/i18n/el.tsv index 3c6c2ab4..26fd3792 100644 --- a/crates/ui-egui/src/i18n/el.tsv +++ b/crates/ui-egui/src/i18n/el.tsv @@ -2266,3 +2266,5 @@ scroll speed Fast Γρήγορη Original Πρωτότυπο Greyscale Κλίμακα του γκρι Black and white Ασπρόμαυρο + This field's font can't draw “{c}” ({code}), so PdfCraft shows ? in its place. The value is saved, and other PDF viewers redraw the field from it. Η γραμματοσειρά αυτού του πεδίου δεν μπορεί να σχεδιάσει το «{c}» ({code}), γι' αυτό το PdfCraft δείχνει ? στη θέση του. Η τιμή αποθηκεύεται και άλλα προγράμματα προβολής PDF επανασχεδιάζουν το πεδίο από αυτήν. + The fonts PdfCraft writes with can't draw “{c}” ({code}), so it shows as ? in every PDF viewer. The text is saved. Οι γραμματοσειρές με τις οποίες γράφει το PdfCraft δεν μπορούν να σχεδιάσουν το «{c}» ({code}), γι' αυτό εμφανίζεται ως ? σε κάθε πρόγραμμα προβολής PDF. Το κείμενο αποθηκεύεται. diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv index 62a6e601..b751dbea 100644 --- a/crates/ui-egui/src/i18n/es.tsv +++ b/crates/ui-egui/src/i18n/es.tsv @@ -2049,6 +2049,8 @@ Go to page… Ir a la página… For page sizes and margins. Para tamaños de página y márgenes. The standard fonts can't draw “{c}” ({code}). Only Western European characters can be added as text for now. Las fuentes estándar no pueden mostrar «{c}» ({code}). Por ahora solo se pueden añadir como texto caracteres de Europa occidental. + This field's font can't draw “{c}” ({code}), so PdfCraft shows ? in its place. The value is saved, and other PDF viewers redraw the field from it. La fuente de este campo no puede mostrar «{c}» ({code}), así que PdfCraft muestra ? en su lugar. El valor se guarda y los demás visores de PDF vuelven a dibujar el campo a partir de él. + The fonts PdfCraft writes with can't draw “{c}” ({code}), so it shows as ? in every PDF viewer. The text is saved. Las fuentes con las que escribe PdfCraft no pueden mostrar «{c}» ({code}), así que aparece como ? en todos los visores de PDF. El texto se guarda. Open arrow Flecha abierta Closed arrow Flecha cerrada Butt Extremo recto diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv index 4e6bd6e4..60e0752d 100644 --- a/crates/ui-egui/src/i18n/fr.tsv +++ b/crates/ui-egui/src/i18n/fr.tsv @@ -2090,6 +2090,8 @@ Go to page… Aller à la page… For page sizes and margins. Pour les tailles de page et les marges. The standard fonts can't draw “{c}” ({code}). Only Western European characters can be added as text for now. Les polices standard ne peuvent pas afficher « {c} » ({code}). Pour l'instant, seuls les caractères d'Europe occidentale peuvent être ajoutés comme texte. + This field's font can't draw “{c}” ({code}), so PdfCraft shows ? in its place. The value is saved, and other PDF viewers redraw the field from it. La police de ce champ ne peut pas afficher « {c} » ({code}) : PdfCraft affiche ? à sa place. La valeur est enregistrée, et les autres lecteurs PDF redessinent le champ à partir de celle-ci. + The fonts PdfCraft writes with can't draw “{c}” ({code}), so it shows as ? in every PDF viewer. The text is saved. Les polices qu'utilise PdfCraft pour écrire ne peuvent pas afficher « {c} » ({code}) : il apparaît comme ? dans tous les lecteurs PDF. Le texte est enregistré. Larger pages Pages plus grandes Smaller pages Pages plus petites Reset page size Rétablir la taille des pages diff --git a/crates/ui-egui/src/i18n/hu.tsv b/crates/ui-egui/src/i18n/hu.tsv index 5ff1e26d..60dec82d 100644 --- a/crates/ui-egui/src/i18n/hu.tsv +++ b/crates/ui-egui/src/i18n/hu.tsv @@ -2128,6 +2128,8 @@ action wizard Start Indítás Reverse closed arrow Fordított zárt nyíl Slash Perjel The standard fonts can't draw “{c}” ({code}). Only Western European characters can be added as text for now. A szabványos betűtípusok nem tudják megjeleníteni ezt: „{c}” ({code}). Egyelőre csak nyugat-európai karakterek adhatók hozzá szövegként. + This field's font can't draw “{c}” ({code}), so PdfCraft shows ? in its place. The value is saved, and other PDF viewers redraw the field from it. A mező betűtípusa nem tudja megjeleníteni ezt: „{c}” ({code}), ezért a PdfCraft ? jelet mutat a helyén. Az érték mentve van, és a többi PDF-megjelenítő ebből rajzolja újra a mezőt. + The fonts PdfCraft writes with can't draw “{c}” ({code}), so it shows as ? in every PDF viewer. The text is saved. A PdfCraft által íráshoz használt betűtípusok nem tudják megjeleníteni ezt: „{c}” ({code}), ezért minden PDF-megjelenítőben ? látszik a helyén. A szöveg mentve van. Larger pages Nagyobb oldalak Smaller pages Kisebb oldalak Reset page size Oldalméret visszaállítása diff --git a/crates/ui-egui/src/i18n/it.tsv b/crates/ui-egui/src/i18n/it.tsv index 48fe778c..26a1e0a3 100644 --- a/crates/ui-egui/src/i18n/it.tsv +++ b/crates/ui-egui/src/i18n/it.tsv @@ -2144,6 +2144,8 @@ The document was closed before it could be saved. Il documento è stato chiuso prima che potesse essere salvato. The pages were extracted but not deleted, because the document changed while you were choosing a folder. Le pagine sono state estratte ma non eliminate, perché il documento è cambiato mentre sceglievi una cartella. The printer driver's own settings for this print: paper tray, paper type, finishing and more. Le impostazioni proprie del driver della stampante per questa stampa: vassoio, tipo di carta, finitura e altro. + This field's font can't draw “{c}” ({code}), so PdfCraft shows ? in its place. The value is saved, and other PDF viewers redraw the field from it. Il font di questo campo non può disegnare «{c}» ({code}), quindi PdfCraft mostra ? al suo posto. Il valore è salvato e gli altri visualizzatori PDF ridisegnano il campo a partire da esso. + The fonts PdfCraft writes with can't draw “{c}” ({code}), so it shows as ? in every PDF viewer. The text is saved. I font con cui PdfCraft scrive non possono disegnare «{c}» ({code}), quindi appare come ? in ogni visualizzatore PDF. Il testo è salvato. The standard fonts can't draw “{c}” ({code}). Only Western European characters can be added as text for now. I caratteri standard non possono disegnare “{c}” ({code}). Per ora si possono aggiungere come testo solo caratteri dell'Europa occidentale. This printer offers no settings of its own. Questa stampante non offre impostazioni proprie. Today: {date} Oggi: {date} diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv index 94d10925..2bbe998e 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -2020,6 +2020,8 @@ For page sizes and margins. ページサイズと余白に使います。 Reopen the files that were open when PdfCraft last closed 前回 PdfCraft を閉じたときに開いていたファイルを再度開く The standard fonts can't draw “{c}” ({code}). Only Western European characters can be added as text for now. 標準フォントでは「{c}」({code})を表示できません。現在、テキストとして追加できるのは西ヨーロッパの文字のみです。 + This field's font can't draw “{c}” ({code}), so PdfCraft shows ? in its place. The value is saved, and other PDF viewers redraw the field from it. このフィールドのフォントでは「{c}」({code})を表示できないため、PdfCraft では ? と表示されます。値は保存され、他の PDF ビューアーではこの値からフィールドが再描画されます。 + The fonts PdfCraft writes with can't draw “{c}” ({code}), so it shows as ? in every PDF viewer. The text is saved. PdfCraft が書き込みに使うフォントでは「{c}」({code})を表示できないため、どの PDF ビューアーでも ? と表示されます。テキストは保存されています。 Open arrow 開いた矢印 Closed arrow 閉じた矢印 Butt 平ら diff --git a/crates/ui-egui/src/i18n/kk.tsv b/crates/ui-egui/src/i18n/kk.tsv index 1cd217d0..e6abf105 100644 --- a/crates/ui-egui/src/i18n/kk.tsv +++ b/crates/ui-egui/src/i18n/kk.tsv @@ -2248,3 +2248,5 @@ scroll speed Fast Жылдам Original Түпнұсқа Greyscale Сұр реңктер Black and white Ақ-қара + This field's font can't draw “{c}” ({code}), so PdfCraft shows ? in its place. The value is saved, and other PDF viewers redraw the field from it. Бұл өрістің қаріпі «{c}» ({code}) таңбасын сала алмайды, сондықтан PdfCraft оның орнына ? көрсетеді. Мән сақталады, ал басқа PDF қарау құралдары өрісті соған қарап қайта салады. + The fonts PdfCraft writes with can't draw “{c}” ({code}), so it shows as ? in every PDF viewer. The text is saved. PdfCraft жазатын қаріптер «{c}» ({code}) таңбасын сала алмайды, сондықтан ол барлық PDF қарау құралдарында ? болып көрінеді. Мәтін сақталады. diff --git a/crates/ui-egui/src/i18n/pl.tsv b/crates/ui-egui/src/i18n/pl.tsv index 093f931d..2ebeae6f 100644 --- a/crates/ui-egui/src/i18n/pl.tsv +++ b/crates/ui-egui/src/i18n/pl.tsv @@ -2002,6 +2002,8 @@ Go to page… Przejdź do strony… Reopen the files that were open when PdfCraft last closed Otwórz ponownie pliki otwarte przy ostatnim zamknięciu PdfCraft The standard fonts can't draw “{c}” ({code}). Only Western European characters can be added as text for now. Standardowe czcionki nie obsługują znaku „{c}” ({code}). Na razie można dodawać tylko tekst ze znakami zachodnioeuropejskimi. + This field's font can't draw “{c}” ({code}), so PdfCraft shows ? in its place. The value is saved, and other PDF viewers redraw the field from it. Czcionka tego pola nie obsługuje znaku „{c}” ({code}), więc PdfCraft wyświetla w jego miejscu ?. Wartość została zapisana, a inne przeglądarki PDF odrysują pole na jej podstawie. + The fonts PdfCraft writes with can't draw “{c}” ({code}), so it shows as ? in every PDF viewer. The text is saved. Czcionki, którymi pisze PdfCraft, nie obsługują znaku „{c}” ({code}), więc w każdej przeglądarce PDF w jego miejscu widać ?. Tekst został zapisany. Open arrow Otwarta strzałka Closed arrow Zamknięta strzałka Butt Poprzeczna kreska diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv index 88b53958..9c17dd23 100644 --- a/crates/ui-egui/src/i18n/pt-br.tsv +++ b/crates/ui-egui/src/i18n/pt-br.tsv @@ -2032,6 +2032,8 @@ This web address uses letters from another alphabet, which can look like familiar ones. Este endereço da web usa letras de outro alfabeto, que podem parecer familiares. Only continue if you trust this document. An address can carry information from the document, such as what you typed into its form. Continue apenas se você confiar neste documento. Um endereço pode carregar informações do documento, como o que você digitou no formulário dele. The standard fonts can't draw “{c}” ({code}). Only Western European characters can be added as text for now. As fontes padrão não conseguem desenhar “{c}” ({code}). Por enquanto, apenas caracteres da Europa Ocidental podem ser adicionados como texto. + This field's font can't draw “{c}” ({code}), so PdfCraft shows ? in its place. The value is saved, and other PDF viewers redraw the field from it. A fonte deste campo não consegue desenhar “{c}” ({code}), então o PdfCraft mostra ? no lugar. O valor foi salvo, e outros leitores de PDF redesenham o campo a partir dele. + The fonts PdfCraft writes with can't draw “{c}” ({code}), so it shows as ? in every PDF viewer. The text is saved. As fontes que o PdfCraft usa para escrever não conseguem desenhar “{c}” ({code}), então ele aparece como ? em qualquer leitor de PDF. O texto foi salvo. Line ending Fim de linha Show Mostrar Sort Classificar diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv index 947b74b0..8d424256 100644 --- a/crates/ui-egui/src/i18n/ru.tsv +++ b/crates/ui-egui/src/i18n/ru.tsv @@ -2050,6 +2050,8 @@ Go to page… Перейти на страницу… For page sizes and margins. Для размеров страниц и полей. The standard fonts can't draw “{c}” ({code}). Only Western European characters can be added as text for now. Стандартные шрифты не могут отобразить «{c}» ({code}). Пока в виде текста можно добавлять только западноевропейские символы. + This field's font can't draw “{c}” ({code}), so PdfCraft shows ? in its place. The value is saved, and other PDF viewers redraw the field from it. Шрифт этого поля не может отобразить «{c}» ({code}), поэтому PdfCraft показывает на его месте ?. Значение сохранено, и другие программы просмотра PDF перерисуют поле по нему. + The fonts PdfCraft writes with can't draw “{c}” ({code}), so it shows as ? in every PDF viewer. The text is saved. Шрифты, которыми пишет PdfCraft, не могут отобразить «{c}» ({code}), поэтому во всех программах просмотра PDF на его месте будет ?. Текст сохранён. Larger pages Крупнее страницы Smaller pages Мельче страницы Reset page size Сбросить размер страниц diff --git a/crates/ui-egui/src/i18n/sl.tsv b/crates/ui-egui/src/i18n/sl.tsv index c6805c34..c03f604a 100644 --- a/crates/ui-egui/src/i18n/sl.tsv +++ b/crates/ui-egui/src/i18n/sl.tsv @@ -2255,3 +2255,5 @@ scroll speed Fast Hitro Original Izvirnik Greyscale Sivine Black and white Črno-belo + This field's font can't draw “{c}” ({code}), so PdfCraft shows ? in its place. The value is saved, and other PDF viewers redraw the field from it. Pisava tega polja ne more prikazati »{c}« ({code}), zato PdfCraft namesto njega prikaže ?. Vrednost se shrani, drugi pregledovalniki PDF pa polje znova narišejo iz nje. + The fonts PdfCraft writes with can't draw “{c}” ({code}), so it shows as ? in every PDF viewer. The text is saved. Pisave, s katerimi piše PdfCraft, ne morejo prikazati »{c}« ({code}), zato se v vseh pregledovalnikih PDF prikaže kot ?. Besedilo se shrani. diff --git a/crates/ui-egui/src/i18n/tr.tsv b/crates/ui-egui/src/i18n/tr.tsv index f4c6213a..f429a63c 100644 --- a/crates/ui-egui/src/i18n/tr.tsv +++ b/crates/ui-egui/src/i18n/tr.tsv @@ -2246,3 +2246,5 @@ scroll speed Fast Hızlı Original Orijinal Greyscale Gri tonlamalı Black and white Siyah beyaz + This field's font can't draw “{c}” ({code}), so PdfCraft shows ? in its place. The value is saved, and other PDF viewers redraw the field from it. Bu alanın yazı tipi “{c}” ({code}) karakterini çizemiyor, bu yüzden PdfCraft yerine ? gösteriyor. Değer kaydedilir ve diğer PDF görüntüleyiciler alanı ondan yeniden çizer. + The fonts PdfCraft writes with can't draw “{c}” ({code}), so it shows as ? in every PDF viewer. The text is saved. PdfCraft'ın yazdığı yazı tipleri “{c}” ({code}) karakterini çizemiyor, bu yüzden her PDF görüntüleyicide ? olarak görünür. Metin kaydedilir. diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv index 03160ba3..ad874cef 100644 --- a/crates/ui-egui/src/i18n/uk.tsv +++ b/crates/ui-egui/src/i18n/uk.tsv @@ -2152,6 +2152,8 @@ calendar date December грудня Reverse closed arrow Зворотна закрита стрілка Slash Коса риска The standard fonts can't draw “{c}” ({code}). Only Western European characters can be added as text for now. Стандартні шрифти не можуть відобразити «{c}» ({code}). Поки що як текст можна додавати лише західноєвропейські символи. + This field's font can't draw “{c}” ({code}), so PdfCraft shows ? in its place. The value is saved, and other PDF viewers redraw the field from it. Шрифт цього поля не може відобразити «{c}» ({code}), тому PdfCraft показує на його місці ?. Значення збережено, і інші програми перегляду PDF перемалюють поле за ним. + The fonts PdfCraft writes with can't draw “{c}” ({code}), so it shows as ? in every PDF viewer. The text is saved. Шрифти, якими пише PdfCraft, не можуть відобразити «{c}» ({code}), тому в усіх програмах перегляду PDF на його місці буде ?. Текст збережено. Larger pages Більші сторінки Smaller pages Менші сторінки Reset page size Скинути розмір сторінок diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv index d649c3b5..16a1d7ef 100644 --- a/crates/ui-egui/src/i18n/zh-hans.tsv +++ b/crates/ui-egui/src/i18n/zh-hans.tsv @@ -2101,6 +2101,8 @@ Go to page… 转到页面… For page sizes and margins. 用于页面尺寸和边距。 The standard fonts can't draw “{c}” ({code}). Only Western European characters can be added as text for now. 标准字体无法显示“{c}”({code})。目前只能添加西欧字符作为文本。 + This field's font can't draw “{c}” ({code}), so PdfCraft shows ? in its place. The value is saved, and other PDF viewers redraw the field from it. 此字段的字体无法显示“{c}”({code}),PdfCraft 会在该处显示 ?。值已保存,其他 PDF 阅读器会根据该值重新绘制此字段。 + The fonts PdfCraft writes with can't draw “{c}” ({code}), so it shows as ? in every PDF viewer. The text is saved. PdfCraft 写入时使用的字体无法显示“{c}”({code}),因此它在所有 PDF 阅读器中都会显示为 ?。文本已保存。 Larger pages 放大页面 Smaller pages 缩小页面 Reset page size 重置页面大小 diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv index 0e1d0bd5..c689159a 100644 --- a/crates/ui-egui/src/i18n/zh-hant.tsv +++ b/crates/ui-egui/src/i18n/zh-hant.tsv @@ -2011,6 +2011,8 @@ For page sizes and margins. 用於頁面尺寸和邊距。 Reopen the files that were open when PdfCraft last closed 重新開啟上次關閉 PdfCraft 時開啟的檔案 The standard fonts can't draw “{c}” ({code}). Only Western European characters can be added as text for now. 標準字型無法顯示「{c}」({code})。目前只能新增西歐字元作為文字。 + This field's font can't draw “{c}” ({code}), so PdfCraft shows ? in its place. The value is saved, and other PDF viewers redraw the field from it. 此欄位的字型無法顯示「{c}」({code}),PdfCraft 會在該處顯示 ?。值已儲存,其他 PDF 閱讀器會依據該值重新繪製此欄位。 + The fonts PdfCraft writes with can't draw “{c}” ({code}), so it shows as ? in every PDF viewer. The text is saved. PdfCraft 寫入時使用的字型無法顯示「{c}」({code}),因此它在所有 PDF 閱讀器中都會顯示為 ?。文字已儲存。 Open arrow 開口箭頭 Closed arrow 閉合箭頭 Butt 平頭 diff --git a/crates/xfa/src/pdf.rs b/crates/xfa/src/pdf.rs index 44896d40..e3413112 100644 --- a/crates/xfa/src/pdf.rs +++ b/crates/xfa/src/pdf.rs @@ -584,6 +584,18 @@ impl Emitter<'_> { ); } Item::Text(s) => { + // The form's own text, set in the standard fonts: what they can't draw is + // shown as `?` and reported once (the layout goes on), never silently. + if let Some(c) = pdfcraft_fonts::first_undrawable(&s.text) { + const PREFIX: &str = "some of the form's text has characters the standard fonts can't draw"; + if !self.warnings.iter().any(|w| w.starts_with(PREFIX)) { + self.warnings.push(format!( + "{PREFIX} (the first is \"{}\" (U+{:04X})); they are shown as ? until PdfCraft can embed fonts for them", + c.escape_debug(), + u32::from(c) + )); + } + } let name = s.face.resource_name(); fonts.insert(name, s.face.family.base_font(s.face.bold, s.face.italic)); content.extend( diff --git a/crates/xfa/src/tests.rs b/crates/xfa/src/tests.rs index 24260ec0..5f891991 100644 --- a/crates/xfa/src/tests.rs +++ b/crates/xfa/src/tests.rs @@ -191,6 +191,25 @@ fn rendering_into_the_pdf_replaces_the_placeholder_page_and_adds_fields() { assert!(!is_dynamic(&doc)); } +/// A dynamic form's own text that the standard fonts can't draw is laid out anyway and reported +/// once, never written as `?` silently. +#[test] +fn form_text_the_standard_fonts_cannot_draw_is_reported() { + let xdp = r##" +"##; + let mut doc = Document::open(Arc::new(shell(xdp))).unwrap(); + let report = render_into(&mut doc).unwrap(); + let hits: Vec<&String> = report.warnings.iter().filter(|w| w.contains("standard fonts can't draw")).collect(); + assert_eq!(hits.len(), 1, "reported once: {:?}", report.warnings); + assert!(hits[0].contains("\"申\" (U+7533)"), "{hits:?}"); + // Latin text alone reports nothing. + let latin = xdp.replace("申请表", "Application").replace("签名", "Signature"); + let mut doc = Document::open(Arc::new(shell(&latin))).unwrap(); + assert!(!render_into(&mut doc).unwrap().warnings.iter().any(|w| w.contains("standard fonts"))); +} + #[test] fn documents_without_xfa_are_not_touched() { let mut doc = Document::new_empty(); From 8ee3673bd434c42daf162b04c725948af75ed130 Mon Sep 17 00:00:00 2001 From: Tanapat Eiam-arj <59855164+PuemMTH@users.noreply.github.com> Date: Mon, 12 Oct 2026 05:07:36 +0700 Subject: [PATCH 9/9] M7: support shaped Thai text in Add Text and UI fallback fonts (#767) * M7: support shaped Thai text in Add Text and UI fallback fonts Share the bounded shaping and Type 3 writer with Thai, preserving vowel and tone clusters and original Unicode through save/reopen. Keep the Thai face optional and update UI notices and their translations. Constraint: Thai font assets remain in the companion craft-fonts repository. Confidence: high Scope-risk: moderate Co-authored-by: Codex * M7: show Thai Add Text before and after the fix Document the rejected input on the base commit and the saved Thai text on the fixed build using genuine UI captures. Constraint: Synthetic content and licensed fonts only; system font fallback disabled. Co-authored-by: Codex * M7: demonstrate Thai stress cases in the comparison screenshot Show nine lines of stacked marks, both Sara Am encodings, Thai digits and mixed Latin text. Verify all 198 Unicode code points survive saving and extraction with both PdfCraft and Poppler. Co-authored-by: Codex * CI and releases: craft-fonts pin to 3d9ab3a (adds Noto Sans Thai) ci.yml, freebsd.yml and the craft-fonts-ref release.yml passes to craft-common-actions move from 8dcdacd to storytold/craft-fonts 3d9ab3a, the revision with Noto Sans Thai (#18), so Thai Add Text is built in and tested. * ATTRIBUTION: drop the screenshot entry again (the image isn't committed) --------- Co-authored-by: Codex Co-authored-by: Brandon Thomas --- .github/workflows/ci.yml | 2 +- .github/workflows/freebsd.yml | 2 +- .github/workflows/release.yml | 2 +- ATTRIBUTION.toml | 40 ++--- Cargo.lock | 1 + NOTICE | 7 +- ROADMAP.md | 1 + crates/automation/Cargo.toml | 1 + crates/automation/tests/automation.rs | 24 +++ crates/edit/src/added.rs | 184 +++++++++++++++------ crates/edit/src/added/arabic_tests.rs | 37 +++-- crates/edit/src/tests.rs | 31 ++++ crates/fonts/README.md | 6 +- crates/fonts/build.rs | 5 +- crates/fonts/src/craft.rs | 9 + crates/fonts/src/lib.rs | 7 +- crates/fonts/src/{arabic.rs => shaping.rs} | 115 +++++++++++-- crates/ui-egui/src/content_ui.rs | 4 +- crates/ui-egui/src/i18n/ar.tsv | 3 +- crates/ui-egui/src/i18n/bg.tsv | 3 +- crates/ui-egui/src/i18n/cs.tsv | 4 +- crates/ui-egui/src/i18n/de.tsv | 3 +- crates/ui-egui/src/i18n/el.tsv | 4 +- crates/ui-egui/src/i18n/es.tsv | 3 +- crates/ui-egui/src/i18n/fr.tsv | 3 +- crates/ui-egui/src/i18n/hu.tsv | 3 +- crates/ui-egui/src/i18n/it.tsv | 3 +- crates/ui-egui/src/i18n/ja.tsv | 3 +- crates/ui-egui/src/i18n/kk.tsv | 4 +- crates/ui-egui/src/i18n/pl.tsv | 3 +- crates/ui-egui/src/i18n/pt-br.tsv | 3 +- crates/ui-egui/src/i18n/ru.tsv | 3 +- crates/ui-egui/src/i18n/sl.tsv | 4 +- crates/ui-egui/src/i18n/te.tsv | 2 +- crates/ui-egui/src/i18n/tr.tsv | 4 +- crates/ui-egui/src/i18n/uk.tsv | 3 +- crates/ui-egui/src/i18n/zh-hans.tsv | 3 +- crates/ui-egui/src/i18n/zh-hant.tsv | 3 +- crates/ui-egui/src/theme.rs | 15 +- crates/ui-egui/tests/content.rs | 20 +++ parity/acrobat-features.toml | 4 +- 41 files changed, 437 insertions(+), 144 deletions(-) rename crates/fonts/src/{arabic.rs => shaping.rs} (61%) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 48bb2549..44f3445f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,7 +36,7 @@ jobs: uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: repository: storytold/craft-fonts - ref: 8dcdacd5153e64560d109541a47d806f26f048c0 + ref: 3d9ab3a132ed0eff457e424226a435a901da30d3 path: craft-fonts - name: Install Linux system dependencies (egui/winit/rfd) diff --git a/.github/workflows/freebsd.yml b/.github/workflows/freebsd.yml index 37e9fbde..fba20b1c 100644 --- a/.github/workflows/freebsd.yml +++ b/.github/workflows/freebsd.yml @@ -26,4 +26,4 @@ jobs: # Intentionally tracked at @main (not SHA-pinned) so org-wide CI improvements, security fixes, and build optimizations propagate without PR churn across all craftapp repos. uses: storytold/craft-common-actions/.github/workflows/freebsd.yml@main with: - craft-fonts-ref: 8dcdacd5153e64560d109541a47d806f26f048c0 + craft-fonts-ref: 3d9ab3a132ed0eff457e424226a435a901da30d3 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b5929d06..5f227be8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -24,7 +24,7 @@ jobs: with: app-name: pdfcraft app-title: PdfCraft - craft-fonts-ref: 8dcdacd5153e64560d109541a47d806f26f048c0 + craft-fonts-ref: 3d9ab3a132ed0eff457e424226a435a901da30d3 version: ${{ inputs.version }} secrets: APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} diff --git a/ATTRIBUTION.toml b/ATTRIBUTION.toml index c8c9b2a3..002b91c9 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 = "bf6e968d781894c6641df2b52224a2b273b33ab4110b05d5b92ea67d80753d19" +sha256 = "7ed6a7c482dab19fe1b54a7704902610e79e9bc60f19115ce8af7b1ebca504ff" # 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 = "eebd627422ee48d52e35655c41e8a587e4d86c38fcb56d5465d8446d5971dabf" +sha256 = "e5141febe07d89c4c4c7b2d2f87abf71ad1ab8dc4a7880d03805db6f0022118b" [[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 = "2f11d8535473dfdefc0aea011d3079eaada0286a58982a7084e20bda8477de54" +sha256 = "6dff4fded1106659f7c0b1c7e2c671c02e59313ab58875fabf591a9356098767" [[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 = "7d3f5d26df0596f96f5ff0e7cccabbdf1f0c2278062cfa0a1046b364b04c7ba7" +sha256 = "1978cfd55b08fe9905e00ddd16a10d3d0f5371143e2f00a58a77affcc1d874c3" [[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 = "d1a7742f43f01253d40f339c525ad0e2c15c18be1a60d5cc4c8a7bebbebcf3de" +sha256 = "928435778bd8c703083087d401851ab01dc038db3ff568e572b6cf646f805b03" [[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 = "819ab9c8426bf3d1fc2c7021f4b5a19d41f8b8cc65301da625ecb12a2297fc8c" +sha256 = "4f95f2dc524177a5a6261f2f6df6def2cd061596bddac20b264cf6051978b3e3" [[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 = "258518681eb774c7b90335f1d2089bf1bb37831084cf8121f3bbea75931e3f6b" +sha256 = "7c0df34f12030b1dc266413839dbb507bc890cc8847f6b2eac324ed54ff3fc19" [[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 = "404766adf3b492801346390914884c1b75d97e2b4e9e3dcd9239fc8062e7332c" +sha256 = "77f4ec8eb9cba7e2f2aad0883a91a62ca871140135c586f32c725f66774a0632" [[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 = "656940b456a6c2401f8bd3107a71cedcae5e52cd446e86e806285b6322d8ed8e" +sha256 = "4e4d4e4e2f9d4d4be91ed2767a07640b626d83733093d12bff99d0a3986aa7d7" [[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 = "b5ad0282d7a1ddb4e818c7f8a76c2182bd3c9343b8a555b0dae7b78b831b594a" +sha256 = "3d1af7e703706bf4c814062871b47308b70f358356c3b17782dde3eaaa66abb7" [[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 = "71dfed7bedeff0755c7377b7244ccfe57d9958cc803202c52b04da58bce9b1dd" +sha256 = "d03048fe760c7daf08c0d8ed8470633c0eaca8f68c41d1c8ce889813c7f6fab1" [[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 = "5d0bfe97ef2ef2d1577c538793fc3267483f5c9fe9de4df97924d04cf18f7292" +sha256 = "69e07b82ae409de7544f538fedc8e56cdb9f4092be7f45cc7366bddbc03ce93f" [[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 = "b69b54cbeee8c9a876db5ed7d49e056e794742bd0a001a2c6383d464e24c31e4" +sha256 = "0a71d58a88ea532358d3b133eba3aae217ad4bcbb763b6235b137615ce226d2b" [[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 = "db5ba8dc27fb272e2a76e017c5bb38bad200bb7299b9cce40d94d837704507fb" +sha256 = "1e5172befbe995c800cd69e9d6abc02c92544b62d8e6dac0f4a0c26cd0b1efbb" [[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 = "3c94bc6bb9950d8172c3f43c32d09496a382eea32717fcee3855d338881f35fa" +sha256 = "dd76d58abda6153e5c0b9fcc761d652cf2ed96522fab18c9ce87e9cec594043a" [[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 = "10ff47a04951e99f902efeb9832d12b78f5f632d54503843283adfd2ad9e8186" +sha256 = "6c1ca7f6d53454c69b3e362a2a09c5c28d04b615c53936c639e3b43c880552d3" [[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 = "983c0cc2efa98d2d3f0449f45c7f7ebdd39c93578b77a07631ee5613b0989a98" +sha256 = "9151c1049289e318409965c5ad061448db9f35a7e8f14a754ba83616e3d87930" [[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 = "439ad5e6ae9c3e2484b1a5f9f11868ff33ddb150308d32d8c32691f2817c15a6" +sha256 = "096be489498c16dbf85fcd6d2d0d96590736a8b0bbe3b3a5b557e2627c89f9c1" [[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 = "b4e90e5929a95a8c85cf88099003cb772e45591759e192f89026cd883a8149c7" +sha256 = "e72215bc4bf21f598bf82a6ac55551bdef47d62cceedea239d89c0febd9cb1b7" [[asset]] path = "assets/icons/accessibility.svg" @@ -3339,4 +3339,4 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Slovene UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "be33bd6c0f2952eafb04d22cbc71f4e43594bceecc0acaca9cfd2b602100c29c" +sha256 = "7cc8bf51e042d07a397dbebf0747b496879e721a6644f1af197cfb3cbcbe7904" diff --git a/Cargo.lock b/Cargo.lock index 47121b7e..4fd9e89a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3611,6 +3611,7 @@ dependencies = [ "image", "pdfcraft-cos", "pdfcraft-engine", + "pdfcraft-fonts", "pdfcraft-model", "pdfcraft-organize", "pdfcraft-platform", diff --git a/NOTICE b/NOTICE index 72184779..1d46735c 100644 --- a/NOTICE +++ b/NOTICE @@ -20,12 +20,15 @@ list (author, source, license) is in ATTRIBUTION.md. - Inter typeface (assets/fonts/Inter-*.ttf) — SIL Open Font License 1.1. See assets/fonts/OFL-Inter.txt. - JetBrains Mono typeface (assets/fonts/JetBrainsMono-*.ttf) — SIL Open Font License 1.1. See assets/fonts/OFL-JetBrainsMono.txt. - Dancing Script typeface (assets/fonts/DancingScript.ttf), Copyright 2016 The Dancing Script Project Authors — SIL Open Font License 1.1 (Reserved Font Name "Dancing Script"). See assets/fonts/OFL-DancingScript.txt. -- Japanese fonts from craft-fonts (https://github.com/storytold/craft-fonts), an optional build input +- Fonts from craft-fonts (https://github.com/storytold/craft-fonts), an optional build input (CRAFT_FONTS_DIR) that is not part of this repository. Builds made with it, which includes every official release, embed the fonts listed in https://github.com/storytold/craft-fonts/blob/main/ATTRIBUTION.md (BIZ UDPGothic, Shippori Mincho, BIZ UDMincho; the web build only BIZ UDPGothic Regular) under the SIL Open Font License 1.1. Those - packages carry each font's licence as OFL-.txt. + packages carry each font's licence as OFL-.txt. Builds made with the craft-fonts revision + pinned in .github/workflows/ also include Noto Sans Thai (in the web build too), Copyright 2022 + The Noto Project Authors (https://github.com/notofonts/thai), under SIL OFL 1.1; its licence is + fonts/noto-sans-thai/OFL.txt in the craft-fonts build input. - hayro (rendering, via crates.io) — Apache-2.0 OR MIT. Its `embed-fonts` feature bundles the Foxit standard-14 Type 1 fonts under BSD-3-Clause ("Copyright 2014 PDFium Authors"), and hayro-cmap bundles Adobe CMap resources under BSD-3-Clause. diff --git a/ROADMAP.md b/ROADMAP.md index 923639fc..11eaf342 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -139,6 +139,7 @@ Ranked; detail and the full milestone table in [docs/roadmap.md](docs/roadmap.md Newest first: the date, what moved, and the new overall estimate. +- **2026-10-10 (community, M7, Thai Add Text):** Optional craft-fonts Thai face (Noto Sans Thai) for the UI and shaped Add Text through the engine, CLI and MCP. Stacked marks and Sara Am are kept through Type 3 clusters with ToUnicode; regular style, and wrapping at cluster boundaries (no dictionary word segmentation). The craft-fonts pin for CI and releases moves to the revision with the Thai face. Forms and Fill & Sign are outside this change. Estimates unchanged. - **2026-10-11 (#995, #1077, permissions password after opening):** A PDF with only a permissions password opens without asking, restricted, and nothing let the user enter that password afterwards: the notice and the hint pointed at reopening with it. Now the security notice and Properties ▸ Security offer Unlock…, and Change settings / Remove security on a restricted document ask for the permissions password first and then carry on (as Acrobat does). The open password is refused there. `Session::unlock` upgrades the open document in place (owner and user passwords unlock the same file key, so nothing is re-read), unlocks the undo and redo states and reopens saves as owner; it is not an edit and doesn't mark the document unsaved. New command `protect.unlock`; the new strings are in all 15 full catalogs. Tests cover crypt, cos, engine (undo, redo, save, separate open password) and the UI. Still missing: an automation tool for it. Estimates unchanged. - **2026-10-11 (M14, #307 Plan 3):** Engine open and full refresh use one page/content traversal to populate marks and added content. The independent public scans remain available; the combined scanner borrows direct objects and holds referenced objects through `Arc` instead of cloning scan-only `/Contents` arrays or `/PCAdded` dictionaries. Differential tests cover an indirect `/PCAdded` dictionary as well as ordering, malformed/direct/indirect/repeated entries and `/UserUnit`. Exact `bbbe600`→final release CLI measurements used 10 alternating pairs: wall-time medians were 183.645→174.440 ms on 10,000 marker-free pages (10/10 wins), 33.587→32.752 ms on 1,500 mixed pages (8/10), and 24.331→20.133 ms on the 100,000-entry array stress fixture (10/10). The stress fixture's maximum-RSS median fell 28,655,616→19,038,208 B; peak-footprint medians were 14,747,136→14,673,408 B. From P0, the follow-up stress run was 20.886→20.201 ms (10/10), with RSS 23,953,408→19,087,360 B and peak footprint 14,706,144→14,697,952 B. `doc_open` stdout hashes matched every pair. The ancillary `organize` binding preserves the prior `prune_legacy` update condition. Estimates unchanged. - **2026-10-11 (M4, Organize overview):** Opening Organize pages from All tools, Home, the tool palette or `ui.set tool=organize` opens the thumbnail grid directly. Checkboxes toggle individual pages without modifier keys; page controls rotate either way or delete one page, while the toolbar acts on the selection. Portrait and landscape previews retain their aspect ratio. UI and control regressions cover entry, selection, single-page versus batch actions, undo, permissions and retaining the last page. Milestone and effort estimates unchanged. diff --git a/crates/automation/Cargo.toml b/crates/automation/Cargo.toml index 3f8d63e2..f9227c07 100644 --- a/crates/automation/Cargo.toml +++ b/crates/automation/Cargo.toml @@ -24,6 +24,7 @@ default = ["mcp"] mcp = ["dep:base64"] [dev-dependencies] +pdfcraft-fonts = { workspace = true } pdfcraft-cos = { workspace = true } pdfcraft-model = { workspace = true } pdfcraft-xfa = { workspace = true } diff --git a/crates/automation/tests/automation.rs b/crates/automation/tests/automation.rs index 593350ce..5f8a928c 100644 --- a/crates/automation/tests/automation.rs +++ b/crates/automation/tests/automation.rs @@ -4107,6 +4107,30 @@ fn command_batch_refuses_more_than_a_thousand_steps() { assert!(err.to_string().contains("at most 1000 steps"), "{err}"); } +#[test] +fn thai_text_through_tools_saves_reopens_extracts_and_renders() { + let dir = workdir("thai-content"); + let mut a = auto(&dir); + let doc = ok(&mut a, "doc_open", json!({"path": "a.pdf"}))["doc"].as_u64().unwrap(); + let text = "สวัสดี น้ำ ผู้ใช้ ปู่ ๑๒๓ Hello 123"; + let args = json!({"doc": doc, "page": 1, "text": text, "at": [10, 20], "size": 9}); + if pdfcraft_fonts::document_thai_font().is_none() { + assert!(a.call("page_add_text", &args).unwrap_err().to_string().contains("Thai")); + assert_eq!(ok(&mut a, "content_list", json!({"doc": doc}))["count"], 0); + return; + } + ok(&mut a, "page_add_text", args); + assert!(page_text(&mut a, doc)[0].contains(text), "{:?}", page_text(&mut a, doc)); + ok(&mut a, "doc_save", json!({"doc": doc, "path": "thai.pdf"})); + let reopened = ok(&mut a, "doc_open", json!({"path": "thai.pdf"}))["doc"].as_u64().unwrap(); + assert_eq!(ok(&mut a, "content_list", json!({"doc": reopened}))["items"][0]["text"], text); + assert!(page_text(&mut a, reopened)[0].contains(text)); + let rendered = a.call("page_render", &json!({"doc": reopened, "page": 1, "dpi": 144})).unwrap(); + assert!(rendered.iter().any(|c| matches!(c, Content::Png { data, width, height } if !data.is_empty() && *width > 0 && *height > 0))); + ok(&mut a, "content_update", json!({"doc": reopened, "page": 1, "index": 1, "text": "แก้ไข น้ำ"})); + assert!(page_text(&mut a, reopened)[0].contains("แก้ไข น้ำ")); +} + #[test] fn mixed_objects_move_through_tools_with_generation_and_one_undo() { let dir = workdir("mixed-object-move"); diff --git a/crates/edit/src/added.rs b/crates/edit/src/added.rs index 575affb4..67b2059b 100644 --- a/crates/edit/src/added.rs +++ b/crates/edit/src/added.rs @@ -11,7 +11,9 @@ use std::collections::{HashMap, HashSet}; use pdfcraft_cos::{Dict, Document, ObjRef, Object, PdfString, Stream}; -use pdfcraft_fonts::{GlyphError, GlyphOutline, ShapedCluster, arabic_glyph, helvetica_width, literal, shape_arabic, win_ansi}; +use pdfcraft_fonts::{ + GlyphError, GlyphOutline, ShapedCluster, ShapedScript, helvetica_width, literal, shape_arabic, shape_thai, shaped_glyph, win_ansi, +}; use unicode_bidi::{Level, ParagraphBidiInfo}; use crate::{EditError, MarkKind, check, contents, n, page_list, place_tagged}; @@ -225,17 +227,26 @@ fn wrapped(t: &AddedText) -> Vec<(String, bool)> { let mut out = Vec::new(); for para in t.text.split('\n') { let arabic = para.chars().any(is_arabic); + let thai = para.chars().any(is_thai); + if thai + && !arabic + && let Ok(lines) = thai_lines(t, para, width) + { + out.extend(lines.into_iter().map(|line| (line, false))); + continue; + } let rtl = arabic && unicode_bidi::get_base_direction(para) == unicode_bidi::Direction::Rtl; // Arabic words are shaped one by one (joining stops at spaces), so a line's width is the // sum of its words' and shaping stays linear in the paragraph's length. The space is the // paragraph's; a space between Latin words is drawn in the item's font, a few hundredths // of an em apart. - let measure = |s: &str| if arabic { arabic_width(t, s, rtl) } else { t.family.width(s, t.size, t.bold) }; + let measure = |s: &str| if arabic || thai { shaped_width(t, s, rtl) } else { t.family.width(s, t.size, t.bold) }; let space = measure(" "); let (mut line, mut line_w) = (String::new(), 0.0); for word in para.split(' ') { let candidate = if line.is_empty() { word.to_string() } else { format!("{line} {word}") }; - let candidate_w = if arabic { if line.is_empty() { measure(word) } else { line_w + space + measure(word) } } else { measure(&candidate) }; + let candidate_w = + if arabic || thai { if line.is_empty() { measure(word) } else { line_w + space + measure(word) } } else { measure(&candidate) }; if !line.is_empty() && candidate_w > width { out.push((std::mem::take(&mut line), rtl)); line = word.to_string(); @@ -250,6 +261,39 @@ fn wrapped(t: &AddedText) -> Vec<(String, bool)> { out } +/// Thai wrapping at shaped cluster boundaries never detaches a tone/vowel mark from its base. +/// This is glyph-safe wrapping, not dictionary-based Thai word segmentation. +fn thai_lines(t: &AddedText, para: &str, width: f64) -> Result, EditError> { + let (pieces, _) = shaped_layout(t, para, false)?; + let mut atoms = Vec::new(); + for piece in pieces { + match piece { + Piece::Shaped(clusters) => atoms.extend(clusters.into_iter().map(|c| (c.text, c.advance * t.size))), + Piece::Latin(s) => atoms.extend(s.chars().map(|c| { + let text = c.to_string(); + let w = t.family.width(&text, t.size, t.bold); + (text, w) + })), + } + } + let mut lines = Vec::new(); + let (mut line, mut used) = (String::new(), 0.0); + for (text, advance) in atoms { + if !line.is_empty() && used + advance > width { + lines.push(std::mem::take(&mut line)); + used = 0.0; + } + line.push_str(&text); + used += advance; + } + lines.push(line); + Ok(lines) +} + +fn is_thai(c: char) -> bool { + matches!(u32::from(c), 0x0E00..=0x0E7F) +} + /// Arabic letters, marks, digits and punctuation: an item with any of them is shaped with the /// craft-fonts Arabic face. fn is_arabic(c: char) -> bool { @@ -260,6 +304,13 @@ fn is_arabic(c: char) -> bool { /// with the craft-fonts Arabic face (#125), or Arabic that the face can't draw (or that has no /// face to draw it). Page-content tools refuse such text; the Add-text editor keeps it open. pub fn first_undrawable(t: &AddedText) -> Option { + if t.text.chars().any(is_thai) { + if let Some(c) = t.text.chars().find(|c| is_thai(*c) && !pdfcraft_fonts::thai_has(*c)) { + return Some(c); + } + let rest = AddedText { text: t.text.chars().filter(|c| !is_thai(*c)).collect(), ..t.clone() }; + return first_undrawable(&rest); + } // Arabic that nothing can draw: there's no face, or the face lacks it. if let Some(c) = t.text.chars().find(|c| shaped_arabic(*c) && !pdfcraft_fonts::arabic_has(*c)) { return Some(c); @@ -269,12 +320,12 @@ pub fn first_undrawable(t: &AddedText) -> Option { // that is also how `draw` redraws an existing item, so it's checked that way. return t.text.split('\n').find_map(pdfcraft_fonts::first_non_win_ansi); } - // What `arabic_layout` leaves to the standard font, exactly as `draw_arabic` splits it. + // What `shaped_layout` leaves to the standard font, exactly as `draw_shaped` splits it. wrapped(t).into_iter().find_map(|(line, rtl)| { - let (pieces, _) = arabic_layout(t, &line, rtl).ok()?; + let (pieces, _) = shaped_layout(t, &line, rtl).ok()?; pieces.iter().find_map(|p| match p { Piece::Latin(s) => pdfcraft_fonts::first_non_win_ansi(s), - Piece::Arabic(_) => None, + Piece::Shaped(_) => None, }) }) } @@ -305,16 +356,16 @@ fn mirrored(c: char) -> char { } } -/// A piece of a line with Arabic text, in drawing order. +/// A piece of a line with shaped text, in drawing order. enum Piece { /// Shown in the item's own standard font. Latin(String), - Arabic(Vec), + Shaped(Vec), } -/// A line of an Arabic item in drawing order (the Unicode bidirectional algorithm, UAX #9), and +/// A line of a shaped-text item in drawing order (the Unicode bidirectional algorithm, UAX #9), and /// its width at the item's size. `rtl` is its paragraph's direction. -fn arabic_layout(t: &AddedText, line: &str, rtl: bool) -> Result<(Vec, f64), EditError> { +fn shaped_layout(t: &AddedText, line: &str, rtl: bool) -> Result<(Vec, f64), EditError> { // unicode-bidi indexes the first level of a line, so an empty one would panic. if line.is_empty() { return Ok((Vec::new(), 0.0)); @@ -329,20 +380,30 @@ fn arabic_layout(t: &AddedText, line: &str, rtl: bool) -> Result<(Vec, f6 let Some(text) = line.get(run) else { continue }; // Arabic letters take the Arabic face; so do spaces and punctuation between them, which the // shaper then mirrors. Latin text (and what the face lacks) keeps the item's font. - let mut segments: Vec<(bool, String)> = Vec::new(); + let mut segments: Vec<(Option, String)> = Vec::new(); for c in text.chars().filter(|c| !is_bidi_control(*c)) { - let arabic = is_arabic(c) || (run_rtl && !c.is_alphanumeric() && pdfcraft_fonts::arabic_has(c)); + let script = if is_thai(c) { + Some(ShapedScript::Thai) + } else if is_arabic(c) || (run_rtl && !c.is_alphanumeric() && pdfcraft_fonts::arabic_has(c)) { + Some(ShapedScript::Arabic) + } else { + None + }; match segments.last_mut() { - Some((kind, s)) if *kind == arabic => s.push(c), - _ => segments.push((arabic, c.to_string())), + Some((kind, s)) if *kind == script => s.push(c), + _ => segments.push((script, c.to_string())), } } if run_rtl { segments.reverse(); } - for (arabic, s) in segments { - pieces.push(if arabic { - Piece::Arabic(shape_arabic(&s, run_rtl).map_err(|e| arabic_error(e, &s))?) + for (script, s) in segments { + pieces.push(if let Some(script) = script { + let shaped = match script { + ShapedScript::Arabic => shape_arabic(&s, run_rtl).map_err(|e| arabic_error(e, &s))?, + ShapedScript::Thai => shape_thai(&s).map_err(|e| thai_error(e, &s))?, + }; + Piece::Shaped(shaped) } else if run_rtl { Piece::Latin(s.chars().rev().map(mirrored).collect()) } else { @@ -354,16 +415,16 @@ fn arabic_layout(t: &AddedText, line: &str, rtl: bool) -> Result<(Vec, f6 .iter() .map(|p| match p { Piece::Latin(s) => t.family.width(s, t.size, t.bold), - Piece::Arabic(clusters) => clusters.iter().map(|c| c.advance).sum::() * t.size, + Piece::Shaped(clusters) => clusters.iter().map(|c| c.advance).sum::() * t.size, }) .sum(); Ok((pieces, width)) } -/// The width of `s` in an Arabic item; the item's font's estimate if it can't be shaped (drawing +/// The width of `s` in a shaped-text item; the item's font's estimate if it can't be shaped (drawing /// then reports why). -fn arabic_width(t: &AddedText, s: &str, rtl: bool) -> f64 { - arabic_layout(t, s, rtl).map(|(_, w)| w).unwrap_or_else(|_| t.family.width(s, t.size, t.bold)) +fn shaped_width(t: &AddedText, s: &str, rtl: bool) -> f64 { + shaped_layout(t, s, rtl).map(|(_, w)| w).unwrap_or_else(|_| t.family.width(s, t.size, t.bold)) } fn arabic_error(e: GlyphError, text: &str) -> EditError { @@ -376,6 +437,16 @@ fn arabic_error(e: GlyphError, text: &str) -> EditError { }) } +fn thai_error(e: GlyphError, text: &str) -> EditError { + EditError::Invalid(match e { + GlyphError::NoFont => { + "Thai text needs a Thai font in this build; rebuild with CRAFT_FONTS_DIR pointing to craft-fonts with a Thai face".into() + } + GlyphError::Missing => format!("the Thai font can't show \"{text}\""), + GlyphError::TooComplex => "the Thai glyphs are too complex; use a shorter text box".into(), + }) +} + /// The box a text item occupies (height from its lines). pub fn text_rect(t: &AddedText) -> [f64; 4] { let r = norm(t.rect); @@ -391,31 +462,31 @@ fn font_name(base: &str) -> String { /// per 240 distinct glyphs it needs. const GLYPHS_PER_FONT: usize = 240; -/// A line of an Arabic item after layout: its text, whether its paragraph runs right to left, its -/// pieces in drawing order and its width at the item's size. +/// A line of a shaped-text item after layout: its text, whether its paragraph runs right to left, +/// its pieces in drawing order and its width at the item's size. type LaidLine = (String, bool, Vec, f64); -/// Draw a text item that has Arabic in it. Each line is laid out in display order; Arabic glyphs +/// Draw a text item that has Arabic or Thai in it. Each line is laid out in display order; Arabic glyphs /// come from Type 3 fonts drawn from the craft-fonts face's outlines (no font file is embedded), /// with a ToUnicode map for copy and search. Other text uses the item's standard font. -fn draw_arabic(doc: &mut Document, t: &AddedText, taken: &Dict, fonts: &mut Dict, out: &mut Vec) -> Result<(), EditError> { +fn draw_shaped(doc: &mut Document, t: &AddedText, taken: &Dict, fonts: &mut Dict, out: &mut Vec) -> Result<(), EditError> { let laid = wrapped(t) .into_iter() - .map(|(line, rtl)| arabic_layout(t, &line, rtl).map(|(pieces, w)| (line, rtl, pieces, w))) + .map(|(line, rtl)| shaped_layout(t, &line, rtl).map(|(pieces, w)| (line, rtl, pieces, w))) .collect::, _>>()?; - draw_laid_arabic(doc, t, &laid, taken, fonts, out, arabic_glyph) + draw_laid_shaped(doc, t, &laid, taken, fonts, out, shaped_glyph) } -/// [`draw_arabic`] for lines that are already laid out. `outline` is the source of glyph outlines -/// (the craft-fonts face, or a stand-in in tests). -fn draw_laid_arabic( +/// [`draw_shaped`] for lines that are already laid out. `outline` is the source of glyph outlines +/// for a script's face (the craft-fonts faces, or a stand-in in tests). +fn draw_laid_shaped( doc: &mut Document, t: &AddedText, laid: &[LaidLine], taken: &Dict, fonts: &mut Dict, out: &mut Vec, - outline: impl Fn(u32) -> Result, + outline: impl Fn(ShapedScript, u32) -> Result, ) -> Result<(), EditError> { let r = text_rect(t); let latin = font_name(t.family.base_font(t.bold, t.italic)); @@ -424,7 +495,7 @@ fn draw_laid_arabic( let mut keys: Vec<&ShapedCluster> = Vec::new(); let mut index: HashMap = HashMap::new(); for c in laid.iter().flat_map(|(_, _, pieces, _)| pieces).flat_map(|p| match p { - Piece::Arabic(clusters) => clusters.as_slice(), + Piece::Shaped(clusters) => clusters.as_slice(), Piece::Latin(_) => &[], }) { index.entry(cluster_key(c)).or_insert_with(|| { @@ -432,12 +503,16 @@ fn draw_laid_arabic( keys.len() - 1 }); } + // Every outline is read before anything is written, so a failure leaves the document as it was. let mut outlines = HashMap::new(); for c in &keys { for (id, _) in &c.glyphs { - if !outlines.contains_key(id) { - outlines.insert(*id, outline(*id).map_err(|e| arabic_error(e, &c.text))?); + if let std::collections::hash_map::Entry::Vacant(entry) = outlines.entry((c.script, *id)) { + entry.insert(outline(c.script, *id).map_err(|e| match c.script { + ShapedScript::Arabic => arabic_error(e, &c.text), + ShapedScript::Thai => thai_error(e, &c.text), + })?); } } } @@ -445,7 +520,7 @@ fn draw_laid_arabic( let mut taken_names: HashSet> = taken.iter().chain(fonts.iter()).map(|(name, _)| name.clone()).collect(); let mut names = Vec::new(); for chunk in keys.chunks(GLYPHS_PER_FONT) { - let font = type3_arabic(doc, chunk, &outlines); + let font = type3_shaped(doc, chunk, &outlines); // A name no font on the page has, so items never replace each other's fonts (object // numbers alone don't do: a full save renumbers objects but keeps resource names). let mut name = format!("PCAr{}", font.num); @@ -483,7 +558,7 @@ fn draw_laid_arabic( current = Some(latin.as_str()); pen += t.family.width(s, t.size, t.bold) + tw * s.matches(' ').count() as f64; } - Piece::Arabic(clusters) => { + Piece::Shaped(clusters) => { for c in clusters { let Some(&k) = index.get(&cluster_key(c)) else { continue }; let Some(name) = names.get(k / GLYPHS_PER_FONT) else { continue }; @@ -508,14 +583,14 @@ fn draw_laid_arabic( /// What makes two clusters the same Type 3 glyph: text, glyphs, offsets and advance (bit for bit; /// equal clusters come from the same font units). -type ClusterKey<'a> = (&'a str, Vec<(u32, u64, u64)>, u64); +type ClusterKey<'a> = (ShapedScript, &'a str, Vec<(u32, u64, u64)>, u64); fn cluster_key(c: &ShapedCluster) -> ClusterKey<'_> { - (c.text.as_str(), c.glyphs.iter().map(|(id, [x, y])| (*id, x.to_bits(), y.to_bits())).collect(), c.advance.to_bits()) + (c.script, c.text.as_str(), c.glyphs.iter().map(|(id, [x, y])| (*id, x.to_bits(), y.to_bits())).collect(), c.advance.to_bits()) } -/// A Type 3 font with `clusters` as codes 1…, each drawn from the Arabic face's glyph `outlines`. -fn type3_arabic(doc: &mut Document, clusters: &[&ShapedCluster], outlines: &HashMap) -> ObjRef { +/// A Type 3 font with `clusters` as codes 1…, each drawn from its script face's glyph `outlines`. +fn type3_shaped(doc: &mut Document, clusters: &[&ShapedCluster], outlines: &HashMap<(ShapedScript, u32), GlyphOutline>) -> ObjRef { let mut charprocs = Dict::new(); let mut differences = vec![Object::Int(1)]; let mut widths = Vec::with_capacity(clusters.len()); @@ -527,7 +602,7 @@ fn type3_arabic(doc: &mut Document, clusters: &[&ShapedCluster], outlines: &Hash let mut paths = Vec::new(); let mut ink: Option<[f64; 4]> = None; for (id, [dx, dy]) in &c.glyphs { - let Some(g) = outlines.get(id) else { continue }; + let Some(g) = outlines.get(&(c.script, *id)) else { continue }; for contour in &g.contours { for (j, p) in contour.iter().enumerate() { paths.extend(format!("{} {} {}\n", n((p[0] + dx) * 1000.0), n((p[1] + dy) * 1000.0), if j == 0 { "m" } else { "l" }).bytes()); @@ -564,7 +639,18 @@ fn type3_arabic(doc: &mut Document, clusters: &[&ShapedCluster], outlines: &Hash } cmap.push_str("endcmap\nCMapName currentdict /CMap defineresource pop\nend\nend\n"); let cmap = doc.add(Object::Stream(Stream::flate(Dict::new(), cmap.as_bytes()))); - let family = pdfcraft_fonts::document_arabic_font().map_or("Arabic", |f| f.family); + // The face's family when every glyph comes from one script; without the face, the script's + // name (as Arabic items were always named). + let family = match clusters.first().map(|c| c.script).filter(|s| clusters.iter().all(|c| c.script == *s)) { + Some(script) => script.font().map_or( + match script { + ShapedScript::Arabic => "Arabic", + ShapedScript::Thai => "Thai", + }, + |f| f.family, + ), + None => "PdfCraft Shaped Text", + }; let mut descriptor = Dict::new(); descriptor.set(b"Type".to_vec(), Object::name("FontDescriptor")); descriptor.set(b"FontName".to_vec(), Object::name(&family.replace(' ', ""))); @@ -611,8 +697,8 @@ fn draw(doc: &mut Document, c: &Content, view: [f64; 6], taken: &Dict, existing: } let mut fonts = Dict::new(); fonts.set(name.clone().into_bytes(), Object::Dict(font)); - if t.text.chars().any(is_arabic) && (pdfcraft_fonts::document_arabic_font().is_some() || !existing) { - draw_arabic(doc, t, taken, &mut fonts, &mut out)?; + if t.text.chars().any(is_thai) || (t.text.chars().any(is_arabic) && (pdfcraft_fonts::document_arabic_font().is_some() || !existing)) { + draw_shaped(doc, t, taken, &mut fonts, &mut out)?; res.set(b"Font".to_vec(), Object::Dict(fonts)); out.extend_from_slice(b"Q\n"); return Ok((out, res)); @@ -840,11 +926,17 @@ fn validate(c: &Content) -> Result<(), EditError> { if (r[2] - r[0]).abs() < 1.0 { return Err(EditError::Invalid("the text box is too narrow".into())); } + if t.text.chars().any(is_thai) && pdfcraft_fonts::document_thai_font().is_none() { + return Err(thai_error(GlyphError::NoFont, &t.text)); + } // Without the Arabic face, `draw` decides for the Arabic letters themselves (#403): a new // item is refused with what's missing, and one that already holds Arabic stays movable. // Everything else in it is drawn in the standard font, so it still has to fit. let undrawable = if t.text.chars().any(shaped_arabic) && pdfcraft_fonts::document_arabic_font().is_none() { - t.text.split('\n').find_map(|line| line.chars().find(|c| !shaped_arabic(*c) && !pdfcraft_fonts::win_ansi_encodable(*c))) + t.text.split('\n').find_map(|line| { + line.chars() + .find(|c| !shaped_arabic(*c) && !(is_thai(*c) && pdfcraft_fonts::thai_has(*c)) && !pdfcraft_fonts::win_ansi_encodable(*c)) + }) } else { first_undrawable(t) }; @@ -963,7 +1055,7 @@ pub fn delete_content(doc: &mut Document, page: usize, index: usize) -> Result<( Ok(()) } -/// The Type 3 fonts an Arabic item's stream shows text with. Each item has its own (`draw_arabic` +/// The Type 3 fonts an Arabic item's stream shows text with. Each item has its own (`draw_shaped` /// names them so), so they go when the item is replaced or deleted. fn own_fonts(doc: &Document, obj: ObjRef) -> Vec> { let Object::Stream(s) = &*doc.get(obj) else { return Vec::new() }; diff --git a/crates/edit/src/added/arabic_tests.rs b/crates/edit/src/added/arabic_tests.rs index 68743dc7..d2e818e1 100644 --- a/crates/edit/src/added/arabic_tests.rs +++ b/crates/edit/src/added/arabic_tests.rs @@ -12,14 +12,14 @@ const SMALL_ITEM_FINGERPRINT: u64 = 14_135_892_979_695_233_112; /// A cluster that shows one character with the single glyph `id`, at `advance` em. fn cluster(id: u32, advance: f64) -> ShapedCluster { - ShapedCluster { glyphs: vec![(id, [0.0, 0.0])], advance, text: "ب".into() } + ShapedCluster { script: ShapedScript::Arabic, glyphs: vec![(id, [0.0, 0.0])], advance, text: "ب".into() } } /// One run of `clusters` as a line of an item at `item()`'s size. fn line(clusters: Vec, rtl: bool) -> LaidLine { let text = clusters.iter().map(|c| c.text.as_str()).collect(); let width = clusters.iter().map(|c| c.advance).sum::() * item().size; - (text, rtl, vec![Piece::Arabic(clusters)], width) + (text, rtl, vec![Piece::Shaped(clusters)], width) } /// A glyph outline: a box on the baseline, `advance` em wide. @@ -85,7 +85,7 @@ fn an_item_past_the_old_glyph_limit_gets_a_font_per_240_glyphs() { let laid = vec![line((0..n).map(|i| cluster(i as u32 + 1, 0.5)).collect(), false)]; let mut doc = Document::new_empty(); let (mut fonts, mut out) = (Dict::new(), Vec::new()); - let drawn = draw_laid_arabic(&mut doc, &item(), &laid, &Dict::new(), &mut fonts, &mut out, |_| Ok(boxed(0.5))); + let drawn = draw_laid_shaped(&mut doc, &item(), &laid, &Dict::new(), &mut fonts, &mut out, |_, _| Ok(boxed(0.5))); assert!(drawn.is_ok(), "{drawn:?}"); let made = fonts_of(&doc, &fonts); let mut expected = vec![GLYPHS_PER_FONT; 16]; @@ -108,7 +108,7 @@ fn an_outline_missing_from_a_later_font_leaves_the_document_unchanged() { let before = snapshot(&doc); let missing = n as u32; let (mut fonts, mut out) = (Dict::new(), Vec::new()); - let drawn = draw_laid_arabic(&mut doc, &item(), &laid, &Dict::new(), &mut fonts, &mut out, |id| { + let drawn = draw_laid_shaped(&mut doc, &item(), &laid, &Dict::new(), &mut fonts, &mut out, |_, id| { if id == missing { Err(GlyphError::Missing) } else { Ok(boxed(0.5)) } }); assert!(matches!(&drawn, Err(EditError::Invalid(msg)) if msg.contains("can't show")), "{drawn:?}"); @@ -120,15 +120,15 @@ fn an_outline_missing_from_a_later_font_leaves_the_document_unchanged() { fn an_item_below_the_old_limit_is_drawn_exactly_as_before() { // A left-to-right line with a Latin run, a space and a two-glyph cluster, then a right-to-left // line. Glyphs repeat across the two lines and share one font. - let space = ShapedCluster { glyphs: vec![(6, [0.0, 0.0])], advance: 0.25, text: " ".into() }; - let ligature = ShapedCluster { glyphs: vec![(4, [0.0, 0.0]), (5, [-0.1, 0.2])], advance: 0.6, text: "لا".into() }; + let space = ShapedCluster { script: ShapedScript::Arabic, glyphs: vec![(6, [0.0, 0.0])], advance: 0.25, text: " ".into() }; + let ligature = ShapedCluster { script: ShapedScript::Arabic, glyphs: vec![(4, [0.0, 0.0]), (5, [-0.1, 0.2])], advance: 0.6, text: "لا".into() }; let mixed: LaidLine = - ("Hi ب".into(), false, vec![Piece::Latin("Hi ".into()), Piece::Arabic(vec![cluster(1, 0.5), cluster(2, 0.5), space, ligature])], 4.0); + ("Hi ب".into(), false, vec![Piece::Latin("Hi ".into()), Piece::Shaped(vec![cluster(1, 0.5), cluster(2, 0.5), space, ligature])], 4.0); let rtl = line(vec![cluster(2, 0.5), cluster(9, 0.75)], true); let laid = vec![mixed, rtl]; let mut doc = Document::new_empty(); let (mut fonts, mut out) = (Dict::new(), Vec::new()); - let drawn = draw_laid_arabic(&mut doc, &item(), &laid, &Dict::new(), &mut fonts, &mut out, |id| Ok(boxed(0.5 + f64::from(id) * 0.01))); + let drawn = draw_laid_shaped(&mut doc, &item(), &laid, &Dict::new(), &mut fonts, &mut out, |_, id| Ok(boxed(0.5 + f64::from(id) * 0.01))); assert!(drawn.is_ok(), "{drawn:?}"); assert_eq!(fonts.len(), 1); let mut bytes = out.clone(); @@ -150,7 +150,7 @@ fn long_text_with_few_distinct_glyphs_needs_one_font() { let reads = Cell::new(0usize); let mut doc = Document::new_empty(); let (mut fonts, mut out) = (Dict::new(), Vec::new()); - let drawn = draw_laid_arabic(&mut doc, &item(), &laid, &Dict::new(), &mut fonts, &mut out, |_| { + let drawn = draw_laid_shaped(&mut doc, &item(), &laid, &Dict::new(), &mut fonts, &mut out, |_, _| { reads.set(reads.get() + 1); Ok(boxed(0.5)) }); @@ -164,13 +164,18 @@ fn long_text_with_few_distinct_glyphs_needs_one_font() { fn distinct_clusters_past_the_old_limit_share_their_glyph_outlines() { // 3,841 different clusters (their advances differ) built from four glyphs: four outlines are read. let clusters: Vec = (0..3_841u32) - .map(|i| ShapedCluster { glyphs: vec![(1 + i % 4, [0.0, 0.0])], advance: 0.5 + f64::from(i) * 1e-6, text: "ب".into() }) + .map(|i| ShapedCluster { + script: ShapedScript::Arabic, + glyphs: vec![(1 + i % 4, [0.0, 0.0])], + advance: 0.5 + f64::from(i) * 1e-6, + text: "ب".into(), + }) .collect(); let laid = vec![line(clusters, false)]; let reads = Cell::new(0usize); let mut doc = Document::new_empty(); let (mut fonts, mut out) = (Dict::new(), Vec::new()); - let drawn = draw_laid_arabic(&mut doc, &item(), &laid, &Dict::new(), &mut fonts, &mut out, |_| { + let drawn = draw_laid_shaped(&mut doc, &item(), &laid, &Dict::new(), &mut fonts, &mut out, |_, _| { reads.set(reads.get() + 1); Ok(boxed(0.5)) }); @@ -183,18 +188,18 @@ fn distinct_clusters_past_the_old_limit_share_their_glyph_outlines() { #[test] fn hostile_clusters_draw_empty_or_fail_without_changing_the_document() { // A cluster with no glyphs and no text draws nothing, but it is still one glyph of a font. - let empty = ShapedCluster { glyphs: Vec::new(), advance: 0.0, text: String::new() }; + let empty = ShapedCluster { script: ShapedScript::Arabic, glyphs: Vec::new(), advance: 0.0, text: String::new() }; let laid = vec![line(vec![empty, cluster(1, 0.5)], false)]; let mut doc = Document::new_empty(); let (mut fonts, mut out) = (Dict::new(), Vec::new()); - let drawn = draw_laid_arabic(&mut doc, &item(), &laid, &Dict::new(), &mut fonts, &mut out, |_| Ok(boxed(0.5))); + let drawn = draw_laid_shaped(&mut doc, &item(), &laid, &Dict::new(), &mut fonts, &mut out, |_, _| Ok(boxed(0.5))); assert!(drawn.is_ok(), "{drawn:?}"); assert_eq!(glyphs_shown(&out).len(), 2); // A glyph id the face doesn't have fails the item before anything is written. let before = snapshot(&doc); let laid = vec![line(vec![cluster(1, 0.5), cluster(u32::MAX, 0.5)], false)]; let (mut fonts, mut out) = (Dict::new(), Vec::new()); - let drawn = draw_laid_arabic(&mut doc, &item(), &laid, &Dict::new(), &mut fonts, &mut out, |id| { + let drawn = draw_laid_shaped(&mut doc, &item(), &laid, &Dict::new(), &mut fonts, &mut out, |_, id| { if id == u32::MAX { Err(GlyphError::Missing) } else { Ok(boxed(0.5)) } }); assert!(drawn.is_err()); @@ -207,7 +212,7 @@ fn a_font_name_the_page_already_uses_is_not_reused() { let laid = vec![line(vec![cluster(1, 0.5)], false)]; let mut doc = Document::new_empty(); let (mut fonts, mut out) = (Dict::new(), Vec::new()); - let drawn = draw_laid_arabic(&mut doc, &item(), &laid, &Dict::new(), &mut fonts, &mut out, |_| Ok(boxed(0.5))); + let drawn = draw_laid_shaped(&mut doc, &item(), &laid, &Dict::new(), &mut fonts, &mut out, |_, _| Ok(boxed(0.5))); assert!(drawn.is_ok(), "{drawn:?}"); let first = fonts_of(&doc, &fonts)[0].0.clone(); // The same item on a page whose resources already use that name. @@ -215,7 +220,7 @@ fn a_font_name_the_page_already_uses_is_not_reused() { taken.set(first.clone(), Object::Null); let mut doc = Document::new_empty(); let (mut fonts, mut out) = (Dict::new(), Vec::new()); - let drawn = draw_laid_arabic(&mut doc, &item(), &laid, &taken, &mut fonts, &mut out, |_| Ok(boxed(0.5))); + let drawn = draw_laid_shaped(&mut doc, &item(), &laid, &taken, &mut fonts, &mut out, |_, _| Ok(boxed(0.5))); assert!(drawn.is_ok(), "{drawn:?}"); let second = fonts_of(&doc, &fonts)[0].0.clone(); assert_ne!(second, first); diff --git a/crates/edit/src/tests.rs b/crates/edit/src/tests.rs index 0c26de0e..1c79f881 100644 --- a/crates/edit/src/tests.rs +++ b/crates/edit/src/tests.rs @@ -2245,6 +2245,37 @@ fn hostile_form_xobjects_are_read_once() { assert!(images::reading_images(&doc, 0).unwrap().is_empty()); } +#[test] +fn added_thai_text_wraps_clusters_and_survives_reopen() { + let mut doc = fixture(); + let t = AddedText { + rect: [20.0, 100.0, 250.0, 300.0], text: "สวัสดี น้ำ ผู้ใช้ ปู่ ๑๒๓ Hello 123".into(), size: 16.0, ..AddedText::default() + }; + if pdfcraft_fonts::document_thai_font().is_none() { + assert_eq!(first_undrawable(&t), Some('ส')); + let before = list_added(&doc); + let err = add_content(&mut doc, 0, &Content::Text(t)).unwrap_err().to_string(); + assert!(err.contains("Thai") && err.contains("CRAFT_FONTS_DIR"), "{err}"); + assert_eq!(list_added(&doc), before); + return; + } + assert_eq!(first_undrawable(&t), None); + add_content(&mut doc, 0, &Content::Text(t.clone())).unwrap(); + let reopened = reopen(&doc); + assert_eq!(list_added(&reopened)[0].content, Content::Text(AddedText { rect: super::added::text_rect(&t), ..t.clone() })); + let narrow = AddedText { rect: [0.0, 0.0, 16.0, 200.0], text: "กิ่กิ่กิ่".into(), ..t.clone() }; + assert_eq!(super::added::lines(&narrow), ["กิ่", "กิ่", "กิ่"]); + update_content(&mut doc, 0, 0, &Content::Text(AddedText { text: "แก้ไข น้ำ".into(), ..t.clone() })).unwrap(); + assert!(!own(&doc, 0).is_empty()); + let bad = Content::Text(AddedText { text: "ไทย 日".into(), ..t }); + let before = list_added(&doc); + assert!(update_content(&mut doc, 0, 0, &bad).is_err()); + assert_eq!(list_added(&doc), before); + delete_content(&mut doc, 0, 0).unwrap(); + assert!(list_added(&doc).is_empty()); + assert!(arabic_font_names(&doc, 0).is_empty()); +} + #[test] fn group_moves_preserve_glyph_bytes_and_unselected_text_state() { for content in [ diff --git a/crates/fonts/README.md b/crates/fonts/README.md index 4f0dda79..8b40ed5d 100644 --- a/crates/fonts/README.md +++ b/crates/fonts/README.md @@ -50,6 +50,10 @@ set). Without that variable `CRAFT_FONTS` is empty and everything below copes: - `document_japanese_fonts_for_style`: every `Jpan` face in that order of preference, so the editor can use the first one that has all of the replacement's glyphs (the faces differ in coverage, e.g. of Cyrillic). +- `ui_thai_fonts` / `document_thai_font` / `shape_thai`: the `Thai` faces for the interface + and shaped Thai Add Text content. Harfrust positions stacked vowel/tone marks and decomposes + Sara Am; cluster ToUnicode entries preserve the original text. The shared Type 3 writer + embeds outlines, so reopening does not depend on installed fonts. - `SHIPPORI_MINCHO`: Shippori Mincho's bytes, or `None`. Generated Japanese Type 3 fonts record the fallback face's family and style in an indirect @@ -60,7 +64,7 @@ The descriptor does not change glyph programs or replace the Type 3 `FontBBox` m only Gothic Regular, so it cannot preserve bold. No new font assets or changes to that input are needed. -wasm32 builds embed only BIZ UDPGothic Regular and any `Arab`, `Telu` or `Hebr` face, to keep the web build small: the web build +wasm32 builds embed only BIZ UDPGothic Regular and any `Arab`, `Telu`, `Hebr` or `Thai` face, to keep the web build small: the web build currently has no Chinese face, so Chinese there still shows the replacement glyph. Font files are never committed here (`AGENTS.md` §1.4; team members: [craftrules `standards/fonts.md`](https://github.com/storytold/craftrules/blob/main/standards/fonts.md), internal). diff --git a/crates/fonts/build.rs b/crates/fonts/build.rs index e452f9ac..9be57030 100644 --- a/crates/fonts/build.rs +++ b/crates/fonts/build.rs @@ -1,7 +1,7 @@ //! Embeds the fonts of the optional craft-fonts build input (`CRAFT_FONTS_DIR`) as `CRAFT_FONTS`. //! The recipe is craft-fonts' `docs/integration.md`; unset, `CRAFT_FONTS` is empty. It only reads //! the local checkout (no network). On wasm32 it embeds only the UI faces (BIZ UDPGothic Regular -//! and any `Arab`, `Telu` or `Hebr` face), to keep the web build within hosting limits (Cloudflare Pages: +//! and any `Arab`, `Telu`, `Hebr` or `Thai` face), to keep the web build within hosting limits (Cloudflare Pages: //! 25 MiB per file). use std::fmt::Write as _; @@ -55,7 +55,8 @@ fn craft_fonts(dir: &std::path::Path) -> Result { let telugu = scripts.split(',').any(|s| s.trim() == "Telu"); // And the Hebrew ones (small), for Hebrew text and Edit text on the web. let hebrew = scripts.split(',').any(|s| s.trim() == "Hebr"); - let web_face = (*family == "BIZ UDPGothic" && *style == "Regular") || arabic || telugu || hebrew; + let web_face = + (*family == "BIZ UDPGothic" && *style == "Regular") || arabic || telugu || hebrew || scripts.split(',').any(|s| s.trim() == "Thai"); if wasm && !web_face { continue; } diff --git a/crates/fonts/src/craft.rs b/crates/fonts/src/craft.rs index d8ab775e..099179cf 100644 --- a/crates/fonts/src/craft.rs +++ b/crates/fonts/src/craft.rs @@ -66,6 +66,15 @@ pub fn ui_telugu_fonts() -> Vec<&'static CraftFont> { CRAFT_FONTS.iter().filter(|f| f.covers("Telu")).collect() } +/// Thai faces shared by the interface and PDF text writer. +pub fn ui_thai_fonts() -> Vec<&'static CraftFont> { + CRAFT_FONTS.iter().filter(|f| f.covers("Thai")).collect() +} + +pub fn document_thai_font() -> Option<&'static CraftFont> { + ui_thai_fonts().into_iter().next() +} + /// The `Hebr` craft-fonts faces for Hebrew interface text (file names, document titles, text /// being edited), in manifest order. Empty when built without craft-fonts or when it has no /// Hebrew face. diff --git a/crates/fonts/src/lib.rs b/crates/fonts/src/lib.rs index 2cc41ba3..9390c988 100644 --- a/crates/fonts/src/lib.rs +++ b/crates/fonts/src/lib.rs @@ -5,19 +5,20 @@ #![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)] -mod arabic; mod craft; mod encodings; pub mod pdf; mod script; -pub use arabic::{ShapedCluster, arabic_glyph, arabic_has, shape_arabic}; +mod shaping; pub use craft::{ CRAFT_FONTS, CraftFont, SHIPPORI_MINCHO, document_arabic_font, document_hebrew_font, document_japanese_font, document_japanese_font_for_style, - document_japanese_fonts_for_style, ui_arabic_fonts, ui_chinese_fonts, ui_cjk_fonts, ui_hebrew_fonts, ui_japanese_fonts, ui_telugu_fonts, + document_japanese_fonts_for_style, document_thai_font, ui_arabic_fonts, ui_chinese_fonts, ui_cjk_fonts, ui_hebrew_fonts, ui_japanese_fonts, + ui_telugu_fonts, ui_thai_fonts, }; pub use script::{ GlyphError, GlyphOutline, INTER_REGULAR, MAX_SIGNATURE_CHARS, ScriptOutline, japanese_glyph, japanese_glyph_from, script_missing, script_outline, }; +pub use shaping::{ShapedCluster, ShapedScript, arabic_glyph, arabic_has, shape_arabic, shape_thai, shaped_glyph, thai_has}; /// Approximate advance of `s` in Helvetica (or Arial) at `size` points. pub fn helvetica_width(s: &str, size: f64) -> f64 { diff --git a/crates/fonts/src/arabic.rs b/crates/fonts/src/shaping.rs similarity index 61% rename from crates/fonts/src/arabic.rs rename to crates/fonts/src/shaping.rs index 6a4a53a6..75b6321c 100644 --- a/crates/fonts/src/arabic.rs +++ b/crates/fonts/src/shaping.rs @@ -1,5 +1,4 @@ -//! Arabic text written into PDFs: shaping (joining forms, lam-alef ligatures, mark placement) with -//! the craft-fonts `Arab` face, and that face's glyph outlines. +//! Arabic and Thai text shaping with the matching craft-fonts face and bounded glyph outlines. use std::sync::OnceLock; @@ -10,10 +9,12 @@ use skrifa::{FontRef, GlyphId, MetadataProvider}; use crate::{GlyphError, GlyphOutline}; -/// One cluster of shaped Arabic text: the glyphs that show one character (a letter's body and +/// One cluster of shaped text: the glyphs that show one character (a letter's body and /// dots, say), or several characters for a ligature. Em units. #[derive(Clone, Debug, PartialEq)] pub struct ShapedCluster { + /// Face that owns the glyph ids. + pub script: ShapedScript, /// Glyph ids, each with its offset from the cluster's pen position (marks sit above or below). pub glyphs: Vec<(u32, [f64; 2])>, pub advance: f64, @@ -21,34 +22,76 @@ pub struct ShapedCluster { pub text: String, } -fn face() -> Option<&'static (FontRef<'static>, ShaperData)> { +/// The face owning a shaped glyph id. IDs from different faces must never share a cache key. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum ShapedScript { + Arabic, + Thai, +} + +impl ShapedScript { + pub fn font(self) -> Option<&'static crate::CraftFont> { + match self { + Self::Arabic => crate::document_arabic_font(), + Self::Thai => crate::document_thai_font(), + } + } +} + +fn face(script: ShapedScript) -> Option<&'static (FontRef<'static>, ShaperData)> { static FACE: OnceLock, ShaperData)>> = OnceLock::new(); - FACE.get_or_init(|| { - let font = FontRef::new(crate::document_arabic_font()?.bytes).ok()?; - let data = ShaperData::new(&font); - Some((font, data)) - }) - .as_ref() + static THAI: OnceLock, ShaperData)>> = OnceLock::new(); + let cache = match script { + ShapedScript::Arabic => &FACE, + ShapedScript::Thai => &THAI, + }; + cache + .get_or_init(|| { + let font = FontRef::new(script.font()?.bytes).ok()?; + let data = ShaperData::new(&font); + Some((font, data)) + }) + .as_ref() } /// Whether the Arabic document face has a glyph for `c` (false without the face). pub fn arabic_has(c: char) -> bool { - face().is_some_and(|(font, _)| font.charmap().map(c).is_some()) + face(ShapedScript::Arabic).is_some_and(|(font, _)| font.charmap().map(c).is_some()) } /// Shape one directional run of `text` with the Arabic document face. The clusters come in /// drawing order, left to right. [`GlyphError::NoFont`] without the face; /// [`GlyphError::Missing`] when it lacks a character. pub fn shape_arabic(text: &str, rtl: bool) -> Result, GlyphError> { - let (font, data) = face().ok_or(GlyphError::NoFont)?; + shape_script(text, rtl, ShapedScript::Arabic) +} + +pub fn thai_has(c: char) -> bool { + face(ShapedScript::Thai).is_some_and(|(font, _)| font.charmap().map(c).is_some()) +} + +/// Shape Thai with grapheme clusters so Sara Am decomposition and stacked marks keep their +/// original Unicode text exactly once, including when wrapped or copied from the saved PDF. +pub fn shape_thai(text: &str) -> Result, GlyphError> { + shape_script(text, false, ShapedScript::Thai) +} + +fn shape_script(text: &str, rtl: bool, kind: ShapedScript) -> Result, GlyphError> { + let (font, data) = face(kind).ok_or(GlyphError::NoFont)?; let shaper = data.shaper(font).build(); let scale = 1.0 / f64::from(shaper.units_per_em().max(1)); let mut buffer = UnicodeBuffer::new(); buffer.push_str(text); buffer.set_direction(if rtl { Direction::RightToLeft } else { Direction::LeftToRight }); - buffer.set_script(script::ARABIC); - // One cluster per character, so marks keep their own text for copy and search. - buffer.set_cluster_level(BufferClusterLevel::Characters); + buffer.set_script(match kind { + ShapedScript::Arabic => script::ARABIC, + ShapedScript::Thai => script::THAI, + }); + // Preserve the historical Arabic per-character mapping; Thai marks stay with their base. + buffer.set_cluster_level(match kind { + ShapedScript::Arabic => BufferClusterLevel::Characters, + ShapedScript::Thai => BufferClusterLevel::MonotoneGraphemes, + }); let shaped = shaper.shape(buffer, ShapeOptions::new()); // Clusters are byte offsets into `text`; a cluster shows the characters up to the next one. let mut starts: Vec = shaped.glyph_infos().iter().map(|g| g.cluster as usize).collect(); @@ -66,7 +109,7 @@ pub fn shape_arabic(text: &str, rtl: bool) -> Result, GlyphEr _ => { let end = starts.get(starts.partition_point(|s| *s <= start)).copied().unwrap_or(text.len()); let text = text.get(start..end).unwrap_or_default().to_string(); - out.push((start, ShapedCluster { glyphs: Vec::new(), advance: 0.0, text })); + out.push((start, ShapedCluster { script: kind, glyphs: Vec::new(), advance: 0.0, text })); // Just pushed. let Some((_, c)) = out.last_mut() else { continue }; c @@ -82,7 +125,12 @@ pub fn shape_arabic(text: &str, rtl: bool) -> Result, GlyphEr /// Glyph `id` of the Arabic document face, bounded like [`crate::japanese_glyph`]. Glyphs without /// outlines (spaces) come back empty. pub fn arabic_glyph(id: u32) -> Result { - let (font, _) = face().ok_or(GlyphError::NoFont)?; + shaped_glyph(ShapedScript::Arabic, id) +} + +/// A bounded outline from the same face that produced the shaped glyph id. +pub fn shaped_glyph(script: ShapedScript, id: u32) -> Result { + let (font, _) = face(script).ok_or(GlyphError::NoFont)?; if font.maxp().map_or(true, |m| id >= u32::from(m.num_glyphs())) { return Err(GlyphError::Missing); } @@ -169,3 +217,36 @@ mod tests { assert!(shape_arabic("", true).unwrap().is_empty()); } } + +#[cfg(test)] +mod thai_tests { + use super::*; + + #[test] + fn thai_clusters_preserve_sara_am_and_stacked_marks() { + if crate::document_thai_font().is_none() { + assert_eq!(shape_thai("น้ำ"), Err(GlyphError::NoFont)); + assert!(!thai_has('ก')); + return; + } + for text in ["น้ำ", "กิ่", "ผู้ใช้", "ปู่", "เก้า", "สวัสดีภาษาไทย", "๑๒๓", ""] + { + let clusters = shape_thai(text).unwrap(); + assert_eq!(clusters.iter().map(|c| c.text.as_str()).collect::(), text); + for c in clusters { + assert_eq!(c.script, ShapedScript::Thai); + assert!(c.advance.is_finite() && c.advance >= 0.0); + for (id, pos) in c.glyphs { + assert!(pos.iter().all(|v| v.is_finite())); + shaped_glyph(c.script, id).unwrap(); + } + } + } + let stacked = shape_thai("กิ่").unwrap(); + assert_eq!(stacked.len(), 1, "base, vowel and tone must wrap together"); + assert!(stacked[0].glyphs.len() >= 3); + assert!(stacked[0].glyphs.iter().any(|(_, p)| p[0] != 0.0 || p[1] != 0.0)); + assert_eq!(shape_thai("日"), Err(GlyphError::Missing)); + assert_eq!(shaped_glyph(ShapedScript::Thai, u32::MAX).map(|_| ()), Err(GlyphError::Missing)); + } +} diff --git a/crates/ui-egui/src/content_ui.rs b/crates/ui-egui/src/content_ui.rs index 49c1c9e1..fa781e18 100644 --- a/crates/ui-egui/src/content_ui.rs +++ b/crates/ui-egui/src/content_ui.rs @@ -37,7 +37,7 @@ pub(crate) type UndrawableMemo = std::cell::RefCell String { crate::i18n::fmt( - tl!("The standard fonts can't draw “{c}” ({code}). Only Western European characters can be added as text for now."), + tl!("The PDF text fonts can't draw “{c}” ({code}). Choose supported text."), &[("c", &c.to_string()), ("code", &format!("U+{:04X}", u32::from(c)))], ) } @@ -466,7 +466,7 @@ pub(crate) fn format_panel(ui: &mut egui::Ui, t: &Tokens, style: &AddedText) -> if let Some(picked) = crate::comments::swatch_grid(ui, Some(c)) { s.color = picked; } - ui.label(egui::RichText::new(tl!("Standard fonts; text outside Windows-1252 isn't supported yet.")).small().color(t.text_faint)); + ui.label(egui::RichText::new(tl!("Thai and Arabic use regular fallback fonts when available.")).small().color(t.text_faint)); (s != *style).then_some(s) } diff --git a/crates/ui-egui/src/i18n/ar.tsv b/crates/ui-egui/src/i18n/ar.tsv index 097afcc6..c2b21d8d 100644 --- a/crates/ui-egui/src/i18n/ar.tsv +++ b/crates/ui-egui/src/i18n/ar.tsv @@ -705,7 +705,7 @@ Centre توسيط Align right محاذاة لليمين Justify ضبط - Standard fonts; text outside Windows-1252 isn't supported yet. خطوط قياسية؛ النص خارج Windows-1252 غير مدعوم بعد. + Thai and Arabic use regular fallback fonts when available. تستخدم التايلاندية والعربية خطوطًا بديلة بالنمط العادي عند توفرها. Edit image تحرير الصورة Rotate counterclockwise تدوير عكس عقارب الساعة Rotate clockwise تدوير مع عقارب الساعة @@ -2151,6 +2151,7 @@ calendar date December ديسمبر Reverse open arrow سهم مفتوح معكوس Reverse closed arrow سهم مغلق معكوس Slash شرطة مائلة + The PDF text fonts can't draw “{c}” ({code}). Choose supported text. لا تستطيع خطوط نص PDF عرض «{c}» ({code}). اختر نصًا مدعومًا. The standard fonts can't draw “{c}” ({code}). Only Western European characters can be added as text for now. الخطوط القياسية لا تستطيع رسم «{c}» ({code}). حاليًا يمكن إضافة محارف أوروبا الغربية فقط كنص. This field's font can't draw “{c}” ({code}), so PdfCraft shows ? in its place. The value is saved, and other PDF viewers redraw the field from it. خط هذا الحقل لا يستطيع رسم «{c}» ({code})، لذا يعرض PdfCraft علامة ? مكانه. القيمة محفوظة، وتعيد برامج عرض PDF الأخرى رسم الحقل منها. The fonts PdfCraft writes with can't draw “{c}” ({code}), so it shows as ? in every PDF viewer. The text is saved. الخطوط التي يكتب بها PdfCraft لا تستطيع رسم «{c}» ({code})، لذا يظهر بشكل ? في كل برامج عرض PDF. النص محفوظ. diff --git a/crates/ui-egui/src/i18n/bg.tsv b/crates/ui-egui/src/i18n/bg.tsv index ecfab7a9..91c77d44 100644 --- a/crates/ui-egui/src/i18n/bg.tsv +++ b/crates/ui-egui/src/i18n/bg.tsv @@ -385,7 +385,7 @@ Centre Центрирай Align right Подравни вдясно Justify Двустранно подравняване - Standard fonts; text outside Windows-1252 isn't supported yet. Стандартни шрифтове; текст извън Windows-1252 все още не се поддържа. + Thai and Arabic use regular fallback fonts when available. Тайландският и арабският използват резервни шрифтове с нормален стил, когато са налични. Edit image Редактирай изображение Rotate counterclockwise Завърти обратно на часовниковата стрелка Rotate clockwise Завърти по часовниковата стрелка @@ -2093,6 +2093,7 @@ action wizard Start Старт Clear Recent Files Изчисти последно отваряните файлове Remove from Recent Премахни от последно отваряните Remove {name} from Recent Премахни {name} от последно отваряните + The PDF text fonts can't draw “{c}” ({code}). Choose supported text. Шрифтовете за PDF текст не могат да изобразят „{c}“ ({code}). Изберете поддържан текст. The standard fonts can't draw “{c}” ({code}). Only Western European characters can be added as text for now. Стандартните шрифтове не могат да покажат „{c}“ ({code}). Засега като текст могат да се добавят само западноевропейски символи. This field's font can't draw “{c}” ({code}), so PdfCraft shows ? in its place. The value is saved, and other PDF viewers redraw the field from it. Шрифтът на това поле не може да покаже „{c}“ ({code}), затова PdfCraft показва ? на негово място. Стойността е запазена и другите програми за PDF преначертават полето по нея. The fonts PdfCraft writes with can't draw “{c}” ({code}), so it shows as ? in every PDF viewer. The text is saved. Шрифтовете, с които PdfCraft пише, не могат да покажат „{c}“ ({code}), затова във всяка програма за PDF то се показва като ?. Текстът е запазен. diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv index 04110e5b..604f4f45 100644 --- a/crates/ui-egui/src/i18n/cs.tsv +++ b/crates/ui-egui/src/i18n/cs.tsv @@ -782,7 +782,7 @@ key Space Mezerník Centre Na střed Align right Zarovnat doprava Justify Do bloku - Standard fonts; text outside Windows-1252 isn't supported yet. Standardní písma; text mimo Windows-1252 zatím není podporován. + Thai and Arabic use regular fallback fonts when available. Thajština a arabština používají běžná náhradní písma, pokud jsou k dispozici. Edit image Upravit obrázek Rotate counterclockwise Otočit proti směru hodinových ručiček Rotate clockwise Otočit po směru hodinových ručiček @@ -2178,7 +2178,7 @@ calendar date December prosince Home / End Home / End H / Space (hold) H / mezerník (podržet) Line ending Zakončení čáry - The standard fonts can't draw “{c}” ({code}). Only Western European characters can be added as text for now. Standardní písma neumí vykreslit „{c}“ ({code}). Jako text lze zatím přidávat jen západoevropské znaky. + The PDF text fonts can't draw “{c}” ({code}). Choose supported text. Písma pro text v PDF neumí vykreslit „{c}“ ({code}). Zvolte podporovaný text. Larger pages Větší stránky Smaller pages Menší stránky Reset page size Obnovit velikost stránek diff --git a/crates/ui-egui/src/i18n/de.tsv b/crates/ui-egui/src/i18n/de.tsv index b498a892..4f6e8757 100644 --- a/crates/ui-egui/src/i18n/de.tsv +++ b/crates/ui-egui/src/i18n/de.tsv @@ -678,7 +678,7 @@ Centre Zentriert Align right Rechtsbündig Justify Blocksatz - Standard fonts; text outside Windows-1252 isn't supported yet. Standardschriften; Text außerhalb von Windows-1252 wird noch nicht unterstützt. + Thai and Arabic use regular fallback fonts when available. Thai und Arabisch verwenden, falls verfügbar, Ersatzschriften im normalen Schriftschnitt. Edit image Bild bearbeiten Rotate counterclockwise Gegen den Uhrzeigersinn drehen Rotate clockwise Im Uhrzeigersinn drehen @@ -2135,6 +2135,7 @@ action wizard Start Starten Clear Recent Files Liste „Zuletzt geöffnet“ leeren Remove from Recent Aus „Zuletzt geöffnet“ entfernen Remove {name} from Recent {name} aus „Zuletzt geöffnet“ entfernen + The PDF text fonts can't draw “{c}” ({code}). Choose supported text. Die PDF-Textschriften können „{c}“ ({code}) nicht darstellen. Wählen Sie unterstützten Text. The standard fonts can't draw “{c}” ({code}). Only Western European characters can be added as text for now. Die Standardschriften können „{c}“ ({code}) nicht darstellen. Derzeit lassen sich nur westeuropäische Zeichen als Text hinzufügen. This field's font can't draw “{c}” ({code}), so PdfCraft shows ? in its place. The value is saved, and other PDF viewers redraw the field from it. Die Schrift dieses Felds kann „{c}“ ({code}) nicht darstellen, daher zeigt PdfCraft an dieser Stelle ? an. Der Wert ist gespeichert, und andere PDF-Viewer zeichnen das Feld anhand des Werts neu. The fonts PdfCraft writes with can't draw “{c}” ({code}), so it shows as ? in every PDF viewer. The text is saved. Die Schriften, mit denen PdfCraft schreibt, können „{c}“ ({code}) nicht darstellen, daher erscheint es in jedem PDF-Viewer als ?. Der Text ist gespeichert. diff --git a/crates/ui-egui/src/i18n/el.tsv b/crates/ui-egui/src/i18n/el.tsv index 26fd3792..336e0c87 100644 --- a/crates/ui-egui/src/i18n/el.tsv +++ b/crates/ui-egui/src/i18n/el.tsv @@ -1437,7 +1437,7 @@ Couldn't import the signature image: {e} Δεν ήταν δυνατή η εισαγωγή της εικόνας υπογραφής: {e} Go to page… Μετάβαση στη σελίδα… Reopen the files that were open when PdfCraft last closed Εκ νέου άνοιγμα των αρχείων που ήταν ανοιχτά κατά τον τελευταίο τερματισμό του PdfCraft - The standard fonts can't draw “{c}” ({code}). Only Western European characters can be added as text for now. Οι τυπικές γραμματοσειρές δεν μπορούν να εμφανίσουν τον χαρακτήρα «{c}» ({code}). Προς το παρόν μπορούν να προστεθούν ως κείμενο μόνο δυτικοευρωπαϊκοί χαρακτήρες. + The PDF text fonts can't draw “{c}” ({code}). Choose supported text. Οι γραμματοσειρές κειμένου PDF δεν μπορούν να εμφανίσουν τον χαρακτήρα «{c}» ({code}). Επιλέξτε κείμενο που υποστηρίζεται. Open arrow Ανοιχτή αιχμή βέλους Closed arrow Κλειστή αιχμή βέλους Butt Επίπεδο άκρο @@ -1984,7 +1984,7 @@ Field names with + - * / and parentheses; put \\ before spaces in names. Ονόματα πεδίων με + - * / και παρενθέσεις· προσθέστε \\ πριν από τα κενά των ονομάτων. Format text Μορφοποίηση κειμένου Centre Κεντράρισμα - Standard fonts; text outside Windows-1252 isn't supported yet. Τυπικές γραμματοσειρές· κείμενο εκτός της κωδικοποίησης Windows-1252 δεν υποστηρίζεται ακόμη. + Thai and Arabic use regular fallback fonts when available. Τα ταϊλανδικά και τα αραβικά χρησιμοποιούν κανονικές εναλλακτικές γραμματοσειρές, όταν είναι διαθέσιμες. Crop (% trimmed from each side) Περικοπή (% αφαίρεσης από κάθε πλευρά) Click on the page to add text; each click starts a new box, and ✓ finishes. Drag items to move them, drag a corner to resize, double-click text to edit it. Κάντε κλικ στη σελίδα για να προσθέσετε κείμενο· κάθε κλικ δημιουργεί νέο πλαίσιο και το ✓ ολοκληρώνει την εισαγωγή. Σύρετε τα στοιχεία για μετακίνηση, μια γωνία για αλλαγή μεγέθους ή κάντε διπλό κλικ στο κείμενο για επεξεργασία. Choose an image Επιλογή εικόνας diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv index b751dbea..d046a2aa 100644 --- a/crates/ui-egui/src/i18n/es.tsv +++ b/crates/ui-egui/src/i18n/es.tsv @@ -699,7 +699,7 @@ Centre Centrar Align right Alinear a la derecha Justify Justificar - Standard fonts; text outside Windows-1252 isn't supported yet. Fuentes estándar; el texto fuera de Windows-1252 aún no es compatible. + Thai and Arabic use regular fallback fonts when available. El tailandés y el árabe usan fuentes alternativas de estilo normal cuando están disponibles. Edit image Editar imagen Rotate counterclockwise Girar a la izquierda Rotate clockwise Girar a la derecha @@ -2048,6 +2048,7 @@ Couldn't import the signature image: {e} No se pudo importar la imagen de la firma: {e} Go to page… Ir a la página… For page sizes and margins. Para tamaños de página y márgenes. + The PDF text fonts can't draw “{c}” ({code}). Choose supported text. Las fuentes de texto PDF no pueden mostrar «{c}» ({code}). Elija texto compatible. The standard fonts can't draw “{c}” ({code}). Only Western European characters can be added as text for now. Las fuentes estándar no pueden mostrar «{c}» ({code}). Por ahora solo se pueden añadir como texto caracteres de Europa occidental. This field's font can't draw “{c}” ({code}), so PdfCraft shows ? in its place. The value is saved, and other PDF viewers redraw the field from it. La fuente de este campo no puede mostrar «{c}» ({code}), así que PdfCraft muestra ? en su lugar. El valor se guarda y los demás visores de PDF vuelven a dibujar el campo a partir de él. The fonts PdfCraft writes with can't draw “{c}” ({code}), so it shows as ? in every PDF viewer. The text is saved. Las fuentes con las que escribe PdfCraft no pueden mostrar «{c}» ({code}), así que aparece como ? en todos los visores de PDF. El texto se guarda. diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv index 60e0752d..66521af6 100644 --- a/crates/ui-egui/src/i18n/fr.tsv +++ b/crates/ui-egui/src/i18n/fr.tsv @@ -678,7 +678,7 @@ Centre Centrer Align right Aligner à droite Justify Justifier - Standard fonts; text outside Windows-1252 isn't supported yet. Polices standard ; le texte hors Windows-1252 n'est pas encore pris en charge. + Thai and Arabic use regular fallback fonts when available. Le thaï et l’arabe utilisent des polices de remplacement de style normal lorsqu’elles sont disponibles. Edit image Modifier l'image Rotate counterclockwise Pivoter vers la gauche Rotate clockwise Pivoter vers la droite @@ -2089,6 +2089,7 @@ No recent files Aucun fichier récent Go to page… Aller à la page… For page sizes and margins. Pour les tailles de page et les marges. + The PDF text fonts can't draw “{c}” ({code}). Choose supported text. Les polices de texte PDF ne peuvent pas afficher « {c} » ({code}). Choisissez un texte pris en charge. The standard fonts can't draw “{c}” ({code}). Only Western European characters can be added as text for now. Les polices standard ne peuvent pas afficher « {c} » ({code}). Pour l'instant, seuls les caractères d'Europe occidentale peuvent être ajoutés comme texte. This field's font can't draw “{c}” ({code}), so PdfCraft shows ? in its place. The value is saved, and other PDF viewers redraw the field from it. La police de ce champ ne peut pas afficher « {c} » ({code}) : PdfCraft affiche ? à sa place. La valeur est enregistrée, et les autres lecteurs PDF redessinent le champ à partir de celle-ci. The fonts PdfCraft writes with can't draw “{c}” ({code}), so it shows as ? in every PDF viewer. The text is saved. Les polices qu'utilise PdfCraft pour écrire ne peuvent pas afficher « {c} » ({code}) : il apparaît comme ? dans tous les lecteurs PDF. Le texte est enregistré. diff --git a/crates/ui-egui/src/i18n/hu.tsv b/crates/ui-egui/src/i18n/hu.tsv index 60dec82d..9b187e62 100644 --- a/crates/ui-egui/src/i18n/hu.tsv +++ b/crates/ui-egui/src/i18n/hu.tsv @@ -675,7 +675,7 @@ Centre Középre Align right Jobbra igazítás Justify Sorkizárás - Standard fonts; text outside Windows-1252 isn't supported yet. Szabványos betűtípusok; a Windows-1252-n kívüli szöveg még nem támogatott. + Thai and Arabic use regular fallback fonts when available. A thai és az arab szöveg normál stílusú helyettesítő betűtípusokat használ, ha rendelkezésre állnak. Edit image Kép szerkesztése Rotate counterclockwise Forgatás az óramutató járásával ellentétesen Rotate clockwise Forgatás az óramutató járásával megegyezően @@ -2127,6 +2127,7 @@ action wizard Start Indítás Reverse open arrow Fordított nyitott nyíl Reverse closed arrow Fordított zárt nyíl Slash Perjel + The PDF text fonts can't draw “{c}” ({code}). Choose supported text. A PDF szövegének betűtípusai nem tudják megjeleníteni ezt: „{c}” ({code}). Válasszon támogatott szöveget. The standard fonts can't draw “{c}” ({code}). Only Western European characters can be added as text for now. A szabványos betűtípusok nem tudják megjeleníteni ezt: „{c}” ({code}). Egyelőre csak nyugat-európai karakterek adhatók hozzá szövegként. This field's font can't draw “{c}” ({code}), so PdfCraft shows ? in its place. The value is saved, and other PDF viewers redraw the field from it. A mező betűtípusa nem tudja megjeleníteni ezt: „{c}” ({code}), ezért a PdfCraft ? jelet mutat a helyén. Az érték mentve van, és a többi PDF-megjelenítő ebből rajzolja újra a mezőt. The fonts PdfCraft writes with can't draw “{c}” ({code}), so it shows as ? in every PDF viewer. The text is saved. A PdfCraft által íráshoz használt betűtípusok nem tudják megjeleníteni ezt: „{c}” ({code}), ezért minden PDF-megjelenítőben ? látszik a helyén. A szöveg mentve van. diff --git a/crates/ui-egui/src/i18n/it.tsv b/crates/ui-egui/src/i18n/it.tsv index 26a1e0a3..4c2cbb78 100644 --- a/crates/ui-egui/src/i18n/it.tsv +++ b/crates/ui-egui/src/i18n/it.tsv @@ -701,7 +701,7 @@ Centre Centra Align right Allinea a destra Justify Giustifica - Standard fonts; text outside Windows-1252 isn't supported yet. Caratteri standard; il testo al di fuori di Windows-1252 non è ancora supportato. + Thai and Arabic use regular fallback fonts when available. Il thailandese e l’arabo usano caratteri sostitutivi in stile normale, quando disponibili. Edit image Modifica immagine Rotate counterclockwise Ruota in senso antiorario Rotate clockwise Ruota in senso orario @@ -2144,6 +2144,7 @@ The document was closed before it could be saved. Il documento è stato chiuso prima che potesse essere salvato. The pages were extracted but not deleted, because the document changed while you were choosing a folder. Le pagine sono state estratte ma non eliminate, perché il documento è cambiato mentre sceglievi una cartella. The printer driver's own settings for this print: paper tray, paper type, finishing and more. Le impostazioni proprie del driver della stampante per questa stampa: vassoio, tipo di carta, finitura e altro. + The PDF text fonts can't draw “{c}” ({code}). Choose supported text. I caratteri del testo PDF non possono visualizzare «{c}» ({code}). Scegliere un testo supportato. This field's font can't draw “{c}” ({code}), so PdfCraft shows ? in its place. The value is saved, and other PDF viewers redraw the field from it. Il font di questo campo non può disegnare «{c}» ({code}), quindi PdfCraft mostra ? al suo posto. Il valore è salvato e gli altri visualizzatori PDF ridisegnano il campo a partire da esso. The fonts PdfCraft writes with can't draw “{c}” ({code}), so it shows as ? in every PDF viewer. The text is saved. I font con cui PdfCraft scrive non possono disegnare «{c}» ({code}), quindi appare come ? in ogni visualizzatore PDF. Il testo è salvato. The standard fonts can't draw “{c}” ({code}). Only Western European characters can be added as text for now. I caratteri standard non possono disegnare “{c}” ({code}). Per ora si possono aggiungere come testo solo caratteri dell'Europa occidentale. diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv index 2bbe998e..b191a704 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -705,7 +705,7 @@ Centre 中央揃え Align right 右揃え Justify 両端揃え - Standard fonts; text outside Windows-1252 isn't supported yet. 標準フォントを使用します。Windows-1252 に含まれない文字にはまだ対応していません。 + Thai and Arabic use regular fallback fonts when available. タイ語とアラビア語には、利用可能な場合、標準スタイルの代替フォントを使用します。 Edit image 画像を編集 Rotate counterclockwise 反時計回りに回転 Rotate clockwise 時計回りに回転 @@ -2019,6 +2019,7 @@ Go to page… ページへ移動… For page sizes and margins. ページサイズと余白に使います。 Reopen the files that were open when PdfCraft last closed 前回 PdfCraft を閉じたときに開いていたファイルを再度開く + The PDF text fonts can't draw “{c}” ({code}). Choose supported text. PDF テキスト用フォントでは「{c}」({code})を表示できません。対応している文字を使用してください。 The standard fonts can't draw “{c}” ({code}). Only Western European characters can be added as text for now. 標準フォントでは「{c}」({code})を表示できません。現在、テキストとして追加できるのは西ヨーロッパの文字のみです。 This field's font can't draw “{c}” ({code}), so PdfCraft shows ? in its place. The value is saved, and other PDF viewers redraw the field from it. このフィールドのフォントでは「{c}」({code})を表示できないため、PdfCraft では ? と表示されます。値は保存され、他の PDF ビューアーではこの値からフィールドが再描画されます。 The fonts PdfCraft writes with can't draw “{c}” ({code}), so it shows as ? in every PDF viewer. The text is saved. PdfCraft が書き込みに使うフォントでは「{c}」({code})を表示できないため、どの PDF ビューアーでも ? と表示されます。テキストは保存されています。 diff --git a/crates/ui-egui/src/i18n/kk.tsv b/crates/ui-egui/src/i18n/kk.tsv index e6abf105..669078db 100644 --- a/crates/ui-egui/src/i18n/kk.tsv +++ b/crates/ui-egui/src/i18n/kk.tsv @@ -665,7 +665,7 @@ Centre Ортаға туралау Align right Оңға туралау Justify Екі жаққа туралау - Standard fonts; text outside Windows-1252 isn't supported yet. Стандартты қаріптер; Windows-1252-ден тыс мәтін әзірге қолдау таппайды. + Thai and Arabic use regular fallback fonts when available. Тай және араб тілдері қолжетімді болса, қалыпты қосалқы қаріптерді пайдаланады. Edit image Суретті түзету Rotate counterclockwise Сағат тілінің бағытына қарсы бұру Rotate clockwise Сағат тілінің бағытымен бұру @@ -2071,7 +2071,7 @@ calendar date December Желтоқсан H / Space (hold) H / Space (ұстап тұру) Rotation: Бұру: Line ending Сызық ұшы - The standard fonts can't draw “{c}” ({code}). Only Western European characters can be added as text for now. Стандартты қаріптер «{c}» таңбасын ({code}) сала алмайды. Әзірге мәтін ретінде тек Батыс Еуропа таңбаларын қосуға болады. + The PDF text fonts can't draw “{c}” ({code}). Choose supported text. PDF мәтін қаріптері «{c}» таңбасын ({code}) сала алмайды. Қолдау көрсетілетін мәтінді таңдаңыз. Larger pages Үлкенірек беттер Smaller pages Кішірек беттер Reset page size Бет өлшемін қалпына келтіру diff --git a/crates/ui-egui/src/i18n/pl.tsv b/crates/ui-egui/src/i18n/pl.tsv index 2ebeae6f..56e2f243 100644 --- a/crates/ui-egui/src/i18n/pl.tsv +++ b/crates/ui-egui/src/i18n/pl.tsv @@ -700,7 +700,7 @@ Centre Wyśrodkuj Align right Wyrównaj do prawej Justify Wyjustuj - Standard fonts; text outside Windows-1252 isn't supported yet. Standardowe czcionki; tekst spoza Windows-1252 nie jest jeszcze obsługiwany. + Thai and Arabic use regular fallback fonts when available. Tajski i arabski używają zwykłych czcionek zastępczych, gdy są dostępne. Edit image Edytuj obraz Rotate counterclockwise Obróć w lewo Rotate clockwise Obróć w prawo @@ -2001,6 +2001,7 @@ Couldn't import the signature image: {e} Nie można zaimportować obrazu podpisu: {e} Go to page… Przejdź do strony… Reopen the files that were open when PdfCraft last closed Otwórz ponownie pliki otwarte przy ostatnim zamknięciu PdfCraft + The PDF text fonts can't draw “{c}” ({code}). Choose supported text. Czcionki tekstu PDF nie obsługują znaku „{c}” ({code}). Wybierz obsługiwany tekst. The standard fonts can't draw “{c}” ({code}). Only Western European characters can be added as text for now. Standardowe czcionki nie obsługują znaku „{c}” ({code}). Na razie można dodawać tylko tekst ze znakami zachodnioeuropejskimi. This field's font can't draw “{c}” ({code}), so PdfCraft shows ? in its place. The value is saved, and other PDF viewers redraw the field from it. Czcionka tego pola nie obsługuje znaku „{c}” ({code}), więc PdfCraft wyświetla w jego miejscu ?. Wartość została zapisana, a inne przeglądarki PDF odrysują pole na jej podstawie. The fonts PdfCraft writes with can't draw “{c}” ({code}), so it shows as ? in every PDF viewer. The text is saved. Czcionki, którymi pisze PdfCraft, nie obsługują znaku „{c}” ({code}), więc w każdej przeglądarce PDF w jego miejscu widać ?. Tekst został zapisany. diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv index 9c17dd23..87843998 100644 --- a/crates/ui-egui/src/i18n/pt-br.tsv +++ b/crates/ui-egui/src/i18n/pt-br.tsv @@ -339,7 +339,7 @@ Centre Centralizar Align right Alinhar à direita Justify Justificar - Standard fonts; text outside Windows-1252 isn't supported yet. Fontes padrão; texto fora de Windows-1252 ainda não é suportado. + Thai and Arabic use regular fallback fonts when available. O tailandês e o árabe usam fontes alternativas de estilo normal quando disponíveis. Edit image Editar imagem Rotate counterclockwise Girar no sentido anti-horário Rotate clockwise Girar no sentido horário @@ -2031,6 +2031,7 @@ This web address mixes letters from different alphabets, a common way to imitate another site's name. Este endereço da web mistura letras de alfabetos diferentes, uma forma comum de imitar o nome de outro site. This web address uses letters from another alphabet, which can look like familiar ones. Este endereço da web usa letras de outro alfabeto, que podem parecer familiares. Only continue if you trust this document. An address can carry information from the document, such as what you typed into its form. Continue apenas se você confiar neste documento. Um endereço pode carregar informações do documento, como o que você digitou no formulário dele. + The PDF text fonts can't draw “{c}” ({code}). Choose supported text. As fontes de texto PDF não podem exibir “{c}” ({code}). Escolha um texto compatível. The standard fonts can't draw “{c}” ({code}). Only Western European characters can be added as text for now. As fontes padrão não conseguem desenhar “{c}” ({code}). Por enquanto, apenas caracteres da Europa Ocidental podem ser adicionados como texto. This field's font can't draw “{c}” ({code}), so PdfCraft shows ? in its place. The value is saved, and other PDF viewers redraw the field from it. A fonte deste campo não consegue desenhar “{c}” ({code}), então o PdfCraft mostra ? no lugar. O valor foi salvo, e outros leitores de PDF redesenham o campo a partir dele. The fonts PdfCraft writes with can't draw “{c}” ({code}), so it shows as ? in every PDF viewer. The text is saved. As fontes que o PdfCraft usa para escrever não conseguem desenhar “{c}” ({code}), então ele aparece como ? em qualquer leitor de PDF. O texto foi salvo. diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv index 8d424256..1784d8ab 100644 --- a/crates/ui-egui/src/i18n/ru.tsv +++ b/crates/ui-egui/src/i18n/ru.tsv @@ -699,7 +699,7 @@ Centre По центру Align right Выровнять вправо Justify По ширине - Standard fonts; text outside Windows-1252 isn't supported yet. Стандартные шрифты; текст вне Windows-1252 пока не поддерживается. + Thai and Arabic use regular fallback fonts when available. Для тайского и арабского текста используются доступные резервные шрифты обычного начертания. Edit image Редактировать изображение Rotate counterclockwise Повернуть против часовой стрелки Rotate clockwise Повернуть по часовой стрелке @@ -2049,6 +2049,7 @@ Created a PDF from the first {n} files PDF создан из первых файлов: {n} Go to page… Перейти на страницу… For page sizes and margins. Для размеров страниц и полей. + The PDF text fonts can't draw “{c}” ({code}). Choose supported text. Шрифты текста PDF не могут отобразить «{c}» ({code}). Выберите поддерживаемый текст. The standard fonts can't draw “{c}” ({code}). Only Western European characters can be added as text for now. Стандартные шрифты не могут отобразить «{c}» ({code}). Пока в виде текста можно добавлять только западноевропейские символы. This field's font can't draw “{c}” ({code}), so PdfCraft shows ? in its place. The value is saved, and other PDF viewers redraw the field from it. Шрифт этого поля не может отобразить «{c}» ({code}), поэтому PdfCraft показывает на его месте ?. Значение сохранено, и другие программы просмотра PDF перерисуют поле по нему. The fonts PdfCraft writes with can't draw “{c}” ({code}), so it shows as ? in every PDF viewer. The text is saved. Шрифты, которыми пишет PdfCraft, не могут отобразить «{c}» ({code}), поэтому во всех программах просмотра PDF на его месте будет ?. Текст сохранён. diff --git a/crates/ui-egui/src/i18n/sl.tsv b/crates/ui-egui/src/i18n/sl.tsv index c03f604a..da8ae8b0 100644 --- a/crates/ui-egui/src/i18n/sl.tsv +++ b/crates/ui-egui/src/i18n/sl.tsv @@ -1685,7 +1685,7 @@ Stamp palette Paleta žigov Stamps Žigi Standard Business Standardno poslovno - Standard fonts; text outside Windows-1252 isn't supported yet. Standardne pisave; besedilo zunaj nabora Windows-1252 še ni podprto. + Thai and Arabic use regular fallback fonts when available. Tajščina in arabščina uporabljata običajne nadomestne pisave, kadar so na voljo. Standards Standardi Star Zvezda Start Začetek @@ -2134,7 +2134,7 @@ signature pad Type Vtipkaj The field {name} no longer exists. Polje {name} ne obstaja več. The printer driver's own settings for this print: paper tray, paper type, finishing and more. Lastne nastavitve gonilnika tiskalnika za to tiskanje: pladenj za papir, vrsta papirja, dodelava in drugo. The selected fields could not be found. Select them again. Izbranih polj ni bilo mogoče najti. Izberite jih znova. - The standard fonts can't draw “{c}” ({code}). Only Western European characters can be added as text for now. Standardne pisave ne morejo izrisati znaka „{c}“ ({code}). Zaenkrat je kot besedilo mogoče dodati samo zahodnoevropske znake. + The PDF text fonts can't draw “{c}” ({code}). Choose supported text. Pisave za besedilo PDF ne morejo izrisati znaka „{c}“ ({code}). Izberite podprto besedilo. This printer offers no settings of its own. Ta tiskalnik nima lastnih nastavitev. To change locked fields, also set Locked to No. Za spremembo zaklenjenih polj nastavite tudi Zaklenjeno na Ne. Today: {date} Danes: {date} diff --git a/crates/ui-egui/src/i18n/te.tsv b/crates/ui-egui/src/i18n/te.tsv index 001fe1b5..0f4d67aa 100644 --- a/crates/ui-egui/src/i18n/te.tsv +++ b/crates/ui-egui/src/i18n/te.tsv @@ -682,7 +682,7 @@ comment menu Edit సవరించు Centre మధ్యకు అమర్చు Align right కుడికి అమర్చు Justify రెండు వైపులా అమర్చు - Standard fonts; text outside Windows-1252 isn't supported yet. ప్రామాణిక ఫాంట్‌లు; Windows-1252 పరిధికి బయటి టెక్స్ట్‌కు ఇంకా మద్దతు లేదు. + Thai and Arabic use regular fallback fonts when available. థాయ్ మరియు అరబిక్ కోసం అందుబాటులో ఉన్నప్పుడు సాధారణ శైలి ప్రత్యామ్నాయ ఫాంట్‌లను ఉపయోగిస్తారు. Edit image చిత్ర సవరణ Rotate counterclockwise అపసవ్యదిశలో తిప్పు Rotate clockwise సవ్యదిశలో తిప్పు diff --git a/crates/ui-egui/src/i18n/tr.tsv b/crates/ui-egui/src/i18n/tr.tsv index f429a63c..510f4993 100644 --- a/crates/ui-egui/src/i18n/tr.tsv +++ b/crates/ui-egui/src/i18n/tr.tsv @@ -663,7 +663,7 @@ Centre Ortala Align right Sağa hizala Justify İki yana yasla - Standard fonts; text outside Windows-1252 isn't supported yet. Standart yazı tipleri; Windows-1252 dışındaki metinler henüz desteklenmiyor. + Thai and Arabic use regular fallback fonts when available. Tayca ve Arapça, mevcut olduğunda normal yedek yazı tiplerini kullanır. Edit image Resmi düzenle Rotate counterclockwise Saat yönünün tersine döndür Rotate clockwise Saat yönünde döndür @@ -2094,7 +2094,7 @@ action wizard Start Başlat Clear Recent Files Son kullanılan dosyaları temizle Remove from Recent Son kullanılanlardan kaldır Remove {name} from Recent {name} öğesini son kullanılanlardan kaldır - The standard fonts can't draw “{c}” ({code}). Only Western European characters can be added as text for now. Standart yazı tipleri “{c}” ({code}) karakterini çizemez. Şimdilik metin olarak yalnızca Batı Avrupa karakterleri eklenebilir. + The PDF text fonts can't draw “{c}” ({code}). Choose supported text. PDF metin yazı tipleri “{c}” ({code}) karakterini çizemez. Desteklenen bir metin seçin. Larger pages Daha büyük sayfalar Smaller pages Daha küçük sayfalar Reset page size Sayfa boyutunu sıfırla diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv index ad874cef..69ac8f85 100644 --- a/crates/ui-egui/src/i18n/uk.tsv +++ b/crates/ui-egui/src/i18n/uk.tsv @@ -703,7 +703,7 @@ Centre По центру Align right Вирівняти праворуч Justify За шириною - Standard fonts; text outside Windows-1252 isn't supported yet. Стандартні шрифти; текст поза Windows-1252 ще не підтримується. + Thai and Arabic use regular fallback fonts when available. Для тайського й арабського тексту використовуються доступні резервні шрифти звичайного накреслення. Edit image Редагувати зображення Rotate counterclockwise Повернути проти годинникової стрілки Rotate clockwise Повернути за годинниковою стрілкою @@ -2151,6 +2151,7 @@ calendar date December грудня Reverse open arrow Зворотна відкрита стрілка Reverse closed arrow Зворотна закрита стрілка Slash Коса риска + The PDF text fonts can't draw “{c}” ({code}). Choose supported text. Шрифти тексту PDF не можуть відобразити «{c}» ({code}). Виберіть підтримуваний текст. The standard fonts can't draw “{c}” ({code}). Only Western European characters can be added as text for now. Стандартні шрифти не можуть відобразити «{c}» ({code}). Поки що як текст можна додавати лише західноєвропейські символи. This field's font can't draw “{c}” ({code}), so PdfCraft shows ? in its place. The value is saved, and other PDF viewers redraw the field from it. Шрифт цього поля не може відобразити «{c}» ({code}), тому PdfCraft показує на його місці ?. Значення збережено, і інші програми перегляду PDF перемалюють поле за ним. The fonts PdfCraft writes with can't draw “{c}” ({code}), so it shows as ? in every PDF viewer. The text is saved. Шрифти, якими пише PdfCraft, не можуть відобразити «{c}» ({code}), тому в усіх програмах перегляду PDF на його місці буде ?. Текст збережено. diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv index 16a1d7ef..4c6ddf8c 100644 --- a/crates/ui-egui/src/i18n/zh-hans.tsv +++ b/crates/ui-egui/src/i18n/zh-hans.tsv @@ -391,7 +391,7 @@ Centre 居中 Align right 右对齐 Justify 两端对齐 - Standard fonts; text outside Windows-1252 isn't supported yet. 标准字体;尚不支持 Windows-1252 之外的文本。 + Thai and Arabic use regular fallback fonts when available. 泰语和阿拉伯语使用可用的常规样式备用字体。 Edit image 编辑图像 Rotate counterclockwise 逆时针旋转 Rotate clockwise 顺时针旋转 @@ -2100,6 +2100,7 @@ Couldn't import the signature image: {e} 无法导入签名图像:{e} Go to page… 转到页面… For page sizes and margins. 用于页面尺寸和边距。 + The PDF text fonts can't draw “{c}” ({code}). Choose supported text. PDF 文本字体无法显示“{c}”({code})。请选择受支持的文本。 The standard fonts can't draw “{c}” ({code}). Only Western European characters can be added as text for now. 标准字体无法显示“{c}”({code})。目前只能添加西欧字符作为文本。 This field's font can't draw “{c}” ({code}), so PdfCraft shows ? in its place. The value is saved, and other PDF viewers redraw the field from it. 此字段的字体无法显示“{c}”({code}),PdfCraft 会在该处显示 ?。值已保存,其他 PDF 阅读器会根据该值重新绘制此字段。 The fonts PdfCraft writes with can't draw “{c}” ({code}), so it shows as ? in every PDF viewer. The text is saved. PdfCraft 写入时使用的字体无法显示“{c}”({code}),因此它在所有 PDF 阅读器中都会显示为 ?。文本已保存。 diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv index c689159a..5722dc79 100644 --- a/crates/ui-egui/src/i18n/zh-hant.tsv +++ b/crates/ui-egui/src/i18n/zh-hant.tsv @@ -347,7 +347,7 @@ Centre 居中 Align right 右對齊 Justify 兩端對齊 - Standard fonts; text outside Windows-1252 isn't supported yet. 標準字型;尚不支援 Windows-1252 之外的文字。 + Thai and Arabic use regular fallback fonts when available. 泰語和阿拉伯語使用可用的一般樣式備用字型。 Edit image 編輯圖片 Rotate counterclockwise 逆時針旋轉 Rotate clockwise 順時針旋轉 @@ -2010,6 +2010,7 @@ Go to page… 轉到頁面… For page sizes and margins. 用於頁面尺寸和邊距。 Reopen the files that were open when PdfCraft last closed 重新開啟上次關閉 PdfCraft 時開啟的檔案 + The PDF text fonts can't draw “{c}” ({code}). Choose supported text. PDF 文字字型無法顯示「{c}」({code})。請選擇支援的文字。 The standard fonts can't draw “{c}” ({code}). Only Western European characters can be added as text for now. 標準字型無法顯示「{c}」({code})。目前只能新增西歐字元作為文字。 This field's font can't draw “{c}” ({code}), so PdfCraft shows ? in its place. The value is saved, and other PDF viewers redraw the field from it. 此欄位的字型無法顯示「{c}」({code}),PdfCraft 會在該處顯示 ?。值已儲存,其他 PDF 閱讀器會依據該值重新繪製此欄位。 The fonts PdfCraft writes with can't draw “{c}” ({code}), so it shows as ? in every PDF viewer. The text is saved. PdfCraft 寫入時使用的字型無法顯示「{c}」({code}),因此它在所有 PDF 閱讀器中都會顯示為 ?。文字已儲存。 diff --git a/crates/ui-egui/src/theme.rs b/crates/ui-egui/src/theme.rs index 976854fc..078b5b6c 100644 --- a/crates/ui-egui/src/theme.rs +++ b/crates/ui-egui/src/theme.rs @@ -204,9 +204,9 @@ pub fn installed_font_definitions(cjk: CjkPreference) -> FontDefinitions { } /// The interface fonts: Inter (and JetBrains Mono for code) first, then egui's defaults, then -/// the CJK, Arabic, Telugu and Hebrew faces of the optional craft-fonts build input as the last -/// fallback in every family. Without craft-fonts there is no Japanese, Chinese, Arabic, Telugu or -/// Hebrew face here. +/// the CJK, Arabic, Telugu, Hebrew and Thai faces of the optional craft-fonts build input as the +/// last fallback in every family. Without craft-fonts there is no Japanese, Chinese, Arabic, +/// Telugu, Hebrew or Thai face here. pub fn font_definitions() -> FontDefinitions { font_definitions_for(false) } @@ -239,9 +239,14 @@ pub fn font_definitions_for(prefer_hans: bool) -> FontDefinitions { } } // Arabic-script faces (file names, document titles), Telugu faces (the Telugu interface, - // file names) and Hebrew faces (file names, text being edited) after the CJK ones; the + // file names), Hebrew faces (file names, text being edited) and Thai faces after the CJK ones; the // ranges don't overlap. - for face in pdfcraft_fonts::ui_arabic_fonts().into_iter().chain(pdfcraft_fonts::ui_telugu_fonts()).chain(pdfcraft_fonts::ui_hebrew_fonts()) { + for face in pdfcraft_fonts::ui_arabic_fonts() + .into_iter() + .chain(pdfcraft_fonts::ui_telugu_fonts()) + .chain(pdfcraft_fonts::ui_hebrew_fonts()) + .chain(pdfcraft_fonts::ui_thai_fonts()) + { let name = face.name(); if !fonts.font_data.contains_key(&name) { add(&mut fonts, &name, face.bytes); diff --git a/crates/ui-egui/tests/content.rs b/crates/ui-egui/tests/content.rs index fb91af5f..2435d776 100644 --- a/crates/ui-egui/tests/content.rs +++ b/crates/ui-egui/tests/content.rs @@ -211,3 +211,23 @@ fn a_kept_draft_goes_with_its_deleted_page() { h.run_steps(2); assert!(h.state().views[0].content.draft.is_none()); } + +#[test] +fn typing_thai_text_keeps_marks_and_commits_to_page() { + if pdfcraft_fonts::document_thai_font().is_none() { + return; + } + let fonts = pdfcraft_ui_egui::theme::font_definitions(); + let face = pdfcraft_fonts::document_thai_font().unwrap(); + assert!(fonts.families[&egui::FontFamily::Proportional].contains(&face.name())); + let mut h = harness(); + assert!(h.state_mut().execute("edit.text")); + h.run_steps(2); + let p = at(&h, 40.0, 100.0); + click(&mut h, p); + h.event(egui::Event::Text("น้ำ กิ่ ผู้ใช้ Hello".into())); + h.run_steps(3); + let p = at(&h, 40.0, 200.0); + click(&mut h, p); + assert_eq!(texts(&h), ["น้ำ กิ่ ผู้ใช้ Hello"]); +} diff --git a/parity/acrobat-features.toml b/parity/acrobat-features.toml index bb073ca2..50b16cd5 100644 --- a/parity/acrobat-features.toml +++ b/parity/acrobat-features.toml @@ -1974,8 +1974,8 @@ milestone = "M7" status = "shipped" commands = ["edit.text"] tools = ["page_add_text", "content_update"] -evidence = ["crates/ui-egui/tests/content.rs::typing_moving_styling_and_deleting_added_content", "crates/ui-egui/tests/content.rs::clicking_elsewhere_keeps_the_text_and_starts_a_new_box", "crates/automation/tests/automation.rs::adding_content_through_tools", "crates/edit/src/tests.rs::added_text_and_images_are_page_content_that_stays_editable", "crates/engine/src/tests.rs::added_text_and_images_render_and_stay_editable", "crates/edit/src/tests.rs::added_arabic_text_is_shaped_in_display_order_and_stays_searchable"] -notes = "page content, editable later (retype, move, restyle); standard fonts, Windows-1252 text. Arabic text is shaped with the craft-fonts Arab face (Type 3 outlines with ToUnicode) and laid out in bidirectional display order; it is drawn regular whatever the bold/italic setting, Hebrew and other right-to-left scripts are not covered, and typing in the box is still left to right (misc.rtl-ui). Each click on the page keeps the box being typed and starts a new one; Done and Discard beside the box" +evidence = ["crates/ui-egui/tests/content.rs::typing_moving_styling_and_deleting_added_content", "crates/ui-egui/tests/content.rs::clicking_elsewhere_keeps_the_text_and_starts_a_new_box", "crates/automation/tests/automation.rs::adding_content_through_tools", "crates/edit/src/tests.rs::added_text_and_images_are_page_content_that_stays_editable", "crates/engine/src/tests.rs::added_text_and_images_render_and_stay_editable", "crates/edit/src/tests.rs::added_arabic_text_is_shaped_in_display_order_and_stays_searchable", "crates/automation/tests/automation.rs::thai_text_through_tools_saves_reopens_extracts_and_renders", "crates/ui-egui/tests/content.rs::typing_thai_text_keeps_marks_and_commits_to_page"] +notes = "page content, editable later (retype, move, restyle); standard fonts, Windows-1252 text. Arabic text is shaped with the craft-fonts Arab face (Type 3 outlines with ToUnicode) and laid out in bidirectional display order; it is drawn regular whatever the bold/italic setting, Hebrew and other right-to-left scripts are not covered, and typing in the box is still left to right (misc.rtl-ui). Thai Add Text uses the craft-fonts Thai face, with shaped clusters and ToUnicode; Thai is regular style, wrapping preserves clusters but is not dictionary-based, and forms/Fill and Sign are not covered by this change. Each click on the page keeps the box being typed and starts a new one; Done and Discard beside the box" [[feature]] id = "edit.find-replace"