diff --git a/ATTRIBUTION.md b/ATTRIBUTION.md index 87c919596..412fc978f 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 (287) +## In this repository (288) | Asset | Title | Author | Licence | Source | Used for | |---|---|---|---|---|---| @@ -288,6 +288,7 @@ Every asset PdfCraft includes, bundles or uses to build its published material, | `crates/ui-egui/src/i18n/vi.tsv` | Vietnamese interface translation catalog | PdfCraft contributors | MIT OR Apache-2.0 | Contributor-original: clean-room translations of PdfCraft's English UI labels; PhotoCraft and VectorCraft Vietnamese catalogs used only as translation memory when English source and semantic context matched; original catalog reviewed by a native Vietnamese-speaking contributor; 2026-10-15 coverage additions are AI-assisted and not separately native-reviewed; 2026-10-18 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed | Vietnamese UI strings, compiled in by crates/ui-egui/src/i18n/mod.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; 2026-10-18 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed | Slovene UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs | | `vendor/vello_common/assets/probe.png` | vello_common probe reference image (rendered from probe.rgba) | Linebender vello contributors | MIT OR Apache-2.0 | vello_common 0.0.8 (crates.io), vendored in vendor/vello_common; regenerated by its vello_sparse_tests regenerate_probe_reference binary | Reference picture of the vendored vello_common probe module, for people only; the build reads probe.rgba, not this file | +| `crates/ui-egui/src/i18n/nb.tsv` | Norwegian Bokmål 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 | Norwegian Bokmål UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs | | `assets/icons/superscript.svg` | Lucide icon "superscript" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/superscript.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs | | `assets/icons/subscript.svg` | Lucide icon "subscript" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/subscript.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs | | `assets/icons/pilcrow.svg` | Lucide icon "pilcrow" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/pilcrow.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs | diff --git a/ATTRIBUTION.toml b/ATTRIBUTION.toml index 491b84bb9..9c4332887 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 = "b99b70882188433ee4b8d6b4fba91ae47865be297edf09aa667e03d928416443" +sha256 = "1902f1c31698e6adfdaf65b35b1fe1628f5cd2bd0182dd3b99bcf5e21720f394" # 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 = "160da8dc60e9aa16f623140cae82f814afa0ee99b7261ce6c40cffc2816f90d4" +sha256 = "2e23c41fcd5b9118cfde03b3a45aa2f988f7eeba650677b6f1f6664e543c7a59" [[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 = "8fbe740d2c6ed07199c04f7f759a55a113dbd0f83759ef6940ab9317d3a0a6f0" +sha256 = "105703f2fbc07c0663308ebd6852199de12a5851a37507d7e51e477eb5e57218" [[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 = "7441f8f1e10615e635672521bcccdab673ba9ffe12b7d5f6a08ba2fa466b1ba5" +sha256 = "7358300715a54e72a477c5888dad53f426ddf76da141a6202bec74594eb5ff0a" [[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 = "5f8e017c45a6c6952dfbf37154d288c5190c43d040bcf1697c1d3db3bda33574" +sha256 = "5d55ab0f9be7b69481505b5bfe142fb274bcfffc85590d971f1a6630f3ec5aeb" [[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 = "71c3b6dd722f28552d32cb07fa06f850eb0d00aab7b5d4d3b33def60f9867481" +sha256 = "ffcf42859cfe38ae13c6fe4219cbe6225a20b4572c202db9e82ee38bfb433799" [[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 = "79143de65c7c8beaaef45a91a61a4343d61da8c2deb1fc130e498626cb3d49c1" +sha256 = "3f31e5c9abdb0208d48d8c68aa81437d9d1e10aeb615dedf811fefa539d93279" [[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 = "c499def3452aec052c48379275cc8182733df09c5e5443c628a7f76550c17d70" +sha256 = "815a3a672421cc3590fb1f267755316659cdb2046e30bbb3ad8b31e96f08b20a" [[asset]] path = "crates/ui-egui/src/i18n/lo.tsv" @@ -118,7 +118,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 = "75b94788f9befbd963d1f13029bc105034662539978090faab6c2544f086c70f" +sha256 = "9cd63425705fb9a3aa029ff3a5110d0877a34f9038f1d64a2102ccd6c3cb35a6" [[asset]] path = "crates/ui-egui/src/i18n/it.tsv" @@ -129,7 +129,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 = "64b5ffb88228d2f115b57082987af9da04326f5a604388a96091781365da29b0" +sha256 = "76f138293fc12ea3584568a81f6f90a1cedfff6256e78b4f60cb21b1b66bbba4" [[asset]] path = "crates/ui-egui/src/i18n/pl.tsv" @@ -140,7 +140,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 = "649eabf0eb4c83b02bc1fb4eea76211aaca97a43aea454b6f273391604682c75" +sha256 = "7bca90240f873fcec687bea84e9ea5835f0903827501d0e73ec26b98b82e8a52" [[asset]] path = "crates/ui-egui/src/i18n/ru.tsv" @@ -151,7 +151,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 = "e530520d956162432e36b5719f04b9db744fab0696577a49a58eff567458ae23" +sha256 = "3d539a28afcf8c2e36b7087ac23334b50bac7971e03da20a62b6e7e400763a87" [[asset]] path = "crates/ui-egui/src/i18n/hu.tsv" @@ -162,7 +162,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 = "c08126de1267fc211a5e1c79229ccf2ad4ba41ca570a9fb94d96ace66fd22551" +sha256 = "71b7e8180fc759aebad4660953ab8b55383def0572c1ace2288cd8bfeb1b82dc" [[asset]] path = "crates/ui-egui/src/i18n/bg.tsv" @@ -173,7 +173,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 = "0d6ea88cc9569f6e164ddda6b699c7f34c59b605d107f0ae46df8d37a87704d6" +sha256 = "e689fb24a8acfa7e85f0f60644f7e2da1225595e6cd6a94109fbe86082926244" [[asset]] path = "crates/ui-egui/src/i18n/kk.tsv" @@ -184,7 +184,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 = "f150d7358183fa08e32c26e7dd2032433c5dd5fec78f0c21a2bb11646c59bb51" +sha256 = "c601ea117ec2a3cdba606fdc9a62220fcffbd0c20c1133bacb653a979cf046a5" [[asset]] path = "crates/ui-egui/src/i18n/ja.tsv" @@ -195,7 +195,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 = "172cd342ccc517fedc3ad5e023b893a86da7d31d640f4bb512c7949cde8f2f13" +sha256 = "006867b891ae2f995c7a711e55b6401db29d20ab493889c6284dc3671f9faa2d" [[asset]] path = "crates/ui-egui/src/i18n/uk.tsv" @@ -206,7 +206,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 = "1d936e9ee4ff23b9012d1d0bb455d72810e6920af373dca5fde3c189eea64831" +sha256 = "9e5b18a09c209525cf08ef53a242ed457cfa346295eef7facde482a75cf72f52" [[asset]] path = "crates/ui-egui/src/i18n/ar.tsv" @@ -217,7 +217,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 = "aa213c7c0611cf9c490c86da72732f9350ba36cfa1f18edc9103410970c65ab9" +sha256 = "2306dbbe6a73671fd20df116adb159a7f66c85fbbd1bae5ecef5f6e882d6b7f1" [[asset]] path = "crates/ui-egui/src/i18n/ckb.tsv" @@ -239,7 +239,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 = "9c1c64e6005c62ea0fd755a4a75aa19785b23846b9bce0a07e4910700dafea80" +sha256 = "463a2d28bd5c5ac8df31a5b5c68400d7a61ce51237a58d9337078c973e6f9e3d" [[asset]] path = "crates/ui-egui/src/i18n/lt.tsv" @@ -250,7 +250,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Lithuanian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "78bd6b73007276e72b41527d81392453703313ec219dec61b54df4b1313fd9db" +sha256 = "dea5076ce9c19f054812b678241931ad5e7f85dee08acf1c1e12c7ecaaa5d0f5" [[asset]] path = "crates/ui-egui/src/i18n/hi.tsv" @@ -3426,7 +3426,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Vietnamese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "ed5381b6b9cea94839aea5c033a42d192b22f8fbef9280c70491b54ada6fdee2" +sha256 = "1c73f9d51b599923d704bf00c8e56b23696de16141b7a2c20aae6f60f88927a5" [[asset]] path = "crates/ui-egui/src/i18n/sl.tsv" @@ -3437,7 +3437,7 @@ 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 = "fe2e389cb68e06a78a09ed3489eb1aacd9826935f6c63ad39c1aa91d11748f39" +sha256 = "a3b8351f0da80c2d394221dbe5d62e794bb019c74c2adfc5d933d999f7d3236f" [[asset]] @@ -3451,6 +3451,17 @@ kind = "image" usage = "Reference picture of the vendored vello_common probe module, for people only; the build reads probe.rgba, not this file" sha256 = "78c59a0cfd2c829909d77006f94e34dac1ae5d1f90e7f10f72e84de541fef697" +[[asset]] +path = "crates/ui-egui/src/i18n/nb.tsv" +title = "Norwegian Bokmål 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 = "Norwegian Bokmål UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" +sha256 = "ded29f83905d714188a0c1208a478f0da928556a309743824113d749b78ff0fa" + [[asset]] path = "assets/icons/superscript.svg" title = "Lucide icon \"superscript\"" diff --git a/ROADMAP.md b/ROADMAP.md index dc860dc32..bc057209c 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -212,6 +212,7 @@ Ranked; detail and the full milestone table in [docs/roadmap.md](docs/roadmap.md - **2026-10-20 (M2, #3601):** A missing spacing glyph in an OpenType substitute uses its ordinary-space glyph. Synthetic Japanese CID renderer tests check blank spaces, `/W` and `/DW` placement, and unchanged Unicode extraction. Font selection and embedded fonts are unchanged. Estimates unchanged. - **2026-10-20 (M12, #3673):** Character encoding accepts Identity-H/V fonts with Adobe CJK collections and recognizes predefined non-Identity CMaps independently. Unknown CMaps and foreign registries remain refused; synthetic checker and read-only automation regressions passed after failing before the fix. Per-glyph mapping validation and embedded-font extraction remain separate. Estimates unchanged. - **2026-10-19 (M6, renames keep formulas):** Renaming a field renames it in every calculation that reads it, in the same undo step: `AFSimple_Calculate` field lists, simplified field notation (the comment and the JavaScript after it) and `getField` calls naming it in one string literal; the rest of each script is kept as written. A sum used to drop the renamed field, notation read it as 0 and a script kept a stale value. Field Properties says how many formulas changed and `form_set_props` lists them as `formulas_updated`. Names built at run time and other events' scripts are not rewritten. Estimates unchanged. +- **2026-10-19 (M6, text field table):** Prepare a form ▸ Text field table drags out an area and adds rows and columns of named text fields as one undoable edit, laid out on the page as displayed, with a live preview, per-column name patterns and widths, and refusal before anything is added when a cell would be too small or a name taken; `form_add_field_table` does the same headlessly. `form.multiple-copies` is partial: copying an existing field into a grid is still missing. Estimates unchanged. - **2026-10-16 (M7, #2516):** A paragraph number from `text_paragraphs` keeps naming the paragraph it listed after edits regroup the page: `text_edit`, `text_edit_layout` and `page_render`'s `hide_text_block` find it again by its text and box, and refuse one that was merged into a neighbour (the edited paragraph included) instead of rewriting the paragraph that took its number (`stale_paragraphs` lists the numbers gone). The inline editor's switch-paragraph click holds the same key. No stable identity across save and reopen yet (#2508). - **2026-10-19 (M7, #3591):** A background added to a page that already has one is painted over the old one instead of going under it and staying hidden. Behind the content, backgrounds come first, in the order added, and then behind-content watermarks, so a watermark added after a background no longer disappears. Synthetic regressions in `crates/edit` check the order. Estimates unchanged. @@ -284,6 +285,7 @@ Ranked; detail and the full milestone table in [docs/roadmap.md](docs/roadmap.md - **2026-10-15 (M14, #2383 candidate):** Device-limit requests now clamp unsupported maxima and minimum alignments before enabling the adapter's full 2D texture extent. This addresses the reported V3D request of 16 shader variables on an adapter supporting 15. Synthetic regressions fail before the clamp and pass afterward, along with the existing HiDPI case and Windows GPU-required application tests. Raspberry Pi startup remains unverified; milestone and overall estimates are unchanged. - **2026-10-12 (M6):** XFDF field import and data-file extraction join all text nodes in a value, preserving text after XML comments and processing instructions. Synthetic regressions cover Unicode, CDATA, empty values and PDF save/reopen. Estimates unchanged. - **2026-10-12 (M7, picture right-click menu):** The Edit text & images menu of a picture gains Cut Image, Redact Image (a redaction mark over the picture, applied from Redact a PDF), Create Link… and Recognize Text… (for that page), in all 21 catalogs. The menu was drawn only while the pointer was over the picture, so its items outside the picture (most of a logo's menu, and Delete below a page) could not be clicked; an open menu now stays drawn. Still missing from Acrobat's menu: Arrange (bring to front, send backward), Remove crop, Draw freehand and Edit image using… (opening an external editor). +- **2026-10-15 (M14, Norwegian Bokmål localization):** Added Norwegian Bokmål (`nb`) as an interface and date language: a catalog covering every key the other catalogs have, Norwegian key names, month and weekday names for Fill & Sign dates, and `no`/`nn` locales resolving to it until a Nynorsk catalog exists. Written from the meaning of the English labels; terminology reviewed by a native-speaking contributor, not yet every line. Estimates unchanged. - **2026-10-12 (M2, inline-image boundaries):** Unfiltered inline images with known sample layouts keep pixel bytes that look like the `EI` terminator when content is rewritten. Regression tests cover row padding, colour-space and parameter aliases, masks and malformed sizes; classic and packed saved PDFs retain the pixels in independent readers and renderers. Filtered and resource-defined layouts retain the existing tolerant heuristic. Estimates unchanged. - **2026-10-12 (M3, drawing markups):** While a comment is being drawn, the pointer in a band along any edge of the view (or just past it) scrolls the view up, down, left or right, so points beyond what is shown can be reached. A polygon, cloud or run of connected lines takes back its last point with Backspace, Delete or Undo (the first point's removal cancels it). Edge scrolling has unit tests of its maths only; it has not been exercised end to end in the app by an automated test. - **2026-10-15 (M2, large object streams):** An object stream's index of `objnum offset` pairs is read once per stream instead of once per object fetched from it (vendored hayro-syntax patch 22), so reading every object of a stream of N objects no longer takes N² steps. A Firefox page whose 65,542 objects share one stream (pdf.js corpus `bug1978317.pdf`) drew in 37 s and extracted its text in 40 s; it now takes 0.5 s and 1.9 s, and leaves the robustness baseline's known timeouts. 40,000 pages in one stream open in 0.2 s instead of 54 s. The regression fails before the fix. Estimates unchanged. diff --git a/crates/automation/src/forms.rs b/crates/automation/src/forms.rs index aba207a70..26e7b9d06 100644 --- a/crates/automation/src/forms.rs +++ b/crates/automation/src/forms.rs @@ -1,5 +1,5 @@ //! Form tools: list fields, fill them (several at once, one undo step), clear the form, and -//! prepare a form (add, change and delete fields). +//! prepare a form (add fields one at a time or as a table, change and delete them). use pdfcraft_engine::form_scripts::{CalcOp, Calculate, Format, Validate}; use pdfcraft_engine::{CalculationIssue, Edit, FieldProps, FieldValue, FormField, FormFieldKind, NewField, field_flags}; @@ -339,6 +339,67 @@ impl Automation { Ok(out) } + /// A table of text fields: `rect` (points from the top-left of the displayed page) divided + /// into rows and columns, one field per cell, as one undo step. + pub(crate) fn form_add_field_table(&mut self, a: &Args) -> Result { + use pdfcraft_engine::field_table::{self, FieldTable, MAX_COLUMNS, TableColumn, TableError}; + let page = self.page(a)?; + let rect = a + .get("rect") + .and_then(Value::as_array) + .and_then(|r| r.iter().map(Value::as_f64).collect::>>()) + .and_then(|r| <[f64; 4]>::try_from(r).ok()) + .ok_or_else(|| bad("rect must be 4 numbers"))?; + let names = a.get("names").map(|_| a.strs("names")).transpose()?; + let widths = match a.get("widths") { + None => None, + Some(v) => Some( + v.as_array() + .and_then(|w| w.iter().map(Value::as_f64).collect::>>()) + .ok_or_else(|| bad("widths must be an array of numbers"))?, + ), + }; + // A negative count is no count: the table refuses it with its own words. + let counts = [a.opt_int("columns")?.map(|n| usize::try_from(n).unwrap_or(0)), names.as_ref().map(Vec::len), widths.as_ref().map(Vec::len)]; + let mut given = counts.into_iter().flatten(); + let columns = given.next().unwrap_or(1); + if given.any(|n| n != columns) { + return Err(bad("columns, names and widths must agree on the number of columns")); + } + // Before anything is sized by it. + if !(1..=MAX_COLUMNS).contains(&columns) { + return Err(bad(TableError::Columns.to_string())); + } + let (edit, fields) = { + let doc = self.doc(a)?; + let base = field_table::free_base(doc.form.iter().map(|f| f.name.as_str())); + let mut table = FieldTable::new(rect, &base); + table.rows = usize::try_from(a.int("rows")?).unwrap_or(0); + table.columns = (0..columns) + .map(|c| TableColumn { + pattern: names.as_ref().and_then(|n| n.get(c)).map_or_else(|| field_table::column_pattern(&base, c + 1), |p| (*p).to_owned()), + width: widths.as_ref().and_then(|w| w.get(c)).copied().unwrap_or(1.0), + }) + .collect(); + table.row_gap = a.opt_num("row_gap")?.unwrap_or(0.0); + table.column_gap = a.opt_num("column_gap")?.unwrap_or(0.0); + table.first_row = a.opt_int("first_row")?.map_or(1, |n| usize::try_from(n).unwrap_or(usize::MAX)); + let info = doc.info.pages.get(page).ok_or_else(|| bad(format!("page {} does not exist", page + 1)))?; + let edit = field_table::edit(&table, page, info, &doc.form).map_err(|e| bad(e.to_string()))?; + // The names the batch adds, in the order it adds them. + let fields: Vec = match &edit { + Edit::Batch { edits, .. } => { + edits.iter().filter_map(|e| if let Edit::AddField { name, .. } = e { name.clone() } else { None }).collect() + } + _ => Vec::new(), + }; + (edit, fields) + }; + let mut out = self.apply(a, edit)?; + out["fields"] = json!(fields); + Ok(out) + } + pub(crate) fn form_set_props(&mut self, a: &Args) -> Result { if a.get("widget").is_some() && a.get("caption").is_none() { return Err(bad("widget requires caption")); diff --git a/crates/automation/src/lib.rs b/crates/automation/src/lib.rs index e87505c9f..c90240875 100644 --- a/crates/automation/src/lib.rs +++ b/crates/automation/src/lib.rs @@ -771,6 +771,7 @@ impl Automation { } "form_reset" => self.form_reset(&a)?, "form_add_field" => self.form_add_field(&a)?, + "form_add_field_table" => self.form_add_field_table(&a)?, "form_set_props" => self.form_set_props(&a)?, "form_delete_field" => self.form_delete_field(&a)?, "form_tab_order" => self.form_tab_order(&a)?, diff --git a/crates/automation/src/tools.rs b/crates/automation/src/tools.rs index 4eca7a060..9ce6b12d4 100644 --- a/crates/automation/src/tools.rs +++ b/crates/automation/src/tools.rs @@ -368,6 +368,27 @@ pub fn tools() -> Vec { }), &["doc", "page", "type", "rect"], )), + t( + "form_add_field_table", + "Prepare form: add a table of text fields", + "Divide rect into rows and columns of text fields, one per cell, as one undo step. rect in points from the top-left of the displayed page [x0, y0, x1, y1]; rows 1-100 from the top; columns 1-50, or as many as names or widths list. Each column names its cells from its pattern in names: an N on its own after _ - / or : (price_N), # or {row} is the row number, {col} the column number, {seq} counts every cell along the rows, starting at first_row; with none of them the row number goes at the end. Rows count from first_row (default 1). Without names the columns are Table1_N, Table1_c2_N, … using the first free TableK. widths are relative (default equal); row_gap and column_gap are points between cells (default 0). Nothing is added if a cell would be under 4 points or a name would contain a period, repeat, or already exist; the error says which. Returns the fields' names. Undoable.", + ) + .cmd("form.add.text_table") + .with(schema( + json!({ + "doc": doc(), + "page": { "type": "integer", "minimum": 1 }, + "rect": { "type": "array", "items": { "type": "number" }, "minItems": 4, "maxItems": 4 }, + "rows": { "type": "integer", "minimum": 1, "maximum": 100 }, + "columns": { "type": "integer", "minimum": 1, "maximum": 50 }, + "names": { "type": "array", "items": { "type": "string" }, "description": "One name pattern per column." }, + "widths": { "type": "array", "items": { "type": "number", "exclusiveMinimum": 0 }, "description": "Relative column widths, one per column." }, + "row_gap": { "type": "number", "minimum": 0 }, + "column_gap": { "type": "number", "minimum": 0 }, + "first_row": { "type": "integer", "minimum": 0, "maximum": 99999 }, + }), + &["doc", "page", "rect", "rows"], + )), t("stamp_custom", "Add a custom stamp", "Stamp a picture: a PDF page (file_page, default 1) or an image file, centred at at [x, y] (points from the top-left of the displayed page) at its natural size, at most 200 pt. name labels it (default: the file name). Undoable.") .cmd("comment.stamp") .with(schema( diff --git a/crates/automation/tests/automation.rs b/crates/automation/tests/automation.rs index b271c21fc..bf6169f42 100644 --- a/crates/automation/tests/automation.rs +++ b/crates/automation/tests/automation.rs @@ -3731,6 +3731,65 @@ fn rotating_a_field_through_tools() { assert_eq!((rot, back), (0, before)); } +/// A table of text fields through the tool: laid out from the top of the page as displayed, named +/// column by column, one undo step, and refused whole, with the reason, when any of it can't go in. +#[test] +fn adding_a_field_table_through_tools() { + let dir = workdir("field-table"); + let mut a = auto(&dir); + let doc = ok(&mut a, "doc_open", json!({ "path": "a.pdf" }))["doc"].as_u64().unwrap(); + let table = + json!({ "doc": doc, "page": 1, "rect": [20, 20, 220, 120], "rows": 2, "names": ["desc_N", "price_N"], "widths": [3, 1], "row_gap": 10 }); + assert_eq!(ok(&mut a, "form_add_field_table", table.clone())["fields"], json!(["desc_1", "price_1", "desc_2", "price_2"])); + let rects = |a: &mut Automation| { + let fields = ok(a, "form_fields", json!({ "doc": doc }))["fields"].as_array().unwrap().clone(); + fields.iter().map(|f| (f["name"].as_str().unwrap().to_owned(), f["rect"].clone())).collect::>() + }; + // 150 and 50 points wide, (100 - 10) / 2 = 45 high, the first row at the top. + let near = |v: &Value, want: [f64; 4]| v.as_array().unwrap().iter().zip(want).all(|(g, w)| (g.as_f64().unwrap() - w).abs() < 0.01); + let got = rects(&mut a); + for (name, want) in [ + ("desc_1", [20.0, 20.0, 170.0, 65.0]), + ("price_1", [170.0, 20.0, 220.0, 65.0]), + ("desc_2", [20.0, 75.0, 170.0, 120.0]), + ("price_2", [170.0, 75.0, 220.0, 120.0]), + ] { + let rect = got.iter().find(|(n, _)| n == name).map(|(_, r)| r).unwrap(); + assert!(near(rect, want), "{name}: {rect} != {want:?}"); + } + // Without names, each table takes the first free base. + let plain = json!({ "doc": doc, "page": 2, "rect": [20, 20, 220, 120], "rows": 2 }); + assert_eq!(ok(&mut a, "form_add_field_table", plain.clone())["fields"], json!(["Table1_1", "Table1_2"])); + assert_eq!(ok(&mut a, "form_add_field_table", plain)["fields"], json!(["Table2_1", "Table2_2"])); + let count = |a: &mut Automation| ok(a, "form_fields", json!({ "doc": doc }))["fields"].as_array().unwrap().len(); + assert_eq!(count(&mut a), 8); + // Refused whole, saying why: nothing is added. + let refused = |a: &mut Automation, args: Value| match a.call("form_add_field_table", &args) { + Err(ToolError::InvalidArgs(m)) => m, + other => panic!("refused, not {other:?}"), + }; + assert_eq!(refused(&mut a, table), "a field named \"desc_1\" already exists"); + let small = json!({ "doc": doc, "page": 1, "rect": [20, 200, 220, 300], "rows": 30 }); + assert!(refused(&mut a, small).starts_with("the rows are too short")); + let dotted = json!({ "doc": doc, "page": 1, "rect": [20, 200, 220, 300], "rows": 2, "names": ["a.{n}"] }); + assert_eq!(refused(&mut a, dotted), "field names can't contain a period: \"a.1\""); + let disagree = json!({ "doc": doc, "page": 1, "rect": [20, 200, 220, 300], "rows": 2, "columns": 3, "names": ["a_N", "b_N"] }); + assert!(refused(&mut a, disagree).contains("must agree")); + let wide = json!({ "doc": doc, "page": 1, "rect": [20, 200, 220, 300], "rows": 2, "columns": 51 }); + assert_eq!(refused(&mut a, wide), "a table has 1 to 50 columns"); + let none = json!({ "doc": doc, "page": 1, "rect": [20, 200, 220, 300], "rows": -1 }); + assert_eq!(refused(&mut a, none), "a table has 1 to 100 rows"); + let huge = json!({ "doc": doc, "page": 1, "rect": [20, 200, 1e39, 300], "rows": 2 }); + assert!(refused(&mut a, huge).starts_with("the area, the spacing and the column widths must be numbers")); + assert_eq!(count(&mut a), 8); + // The table tool's command runs it too. + let by_command = json!({ "id": "form.add.text_table", "params": { "doc": doc, "page": 3, "rect": [20, 20, 220, 120], "rows": 2 } }); + assert_eq!(ok(&mut a, "command_run", by_command)["fields"], json!(["Table3_1", "Table3_2"])); + assert_eq!(count(&mut a), 10); + assert_eq!(ok(&mut a, "edit_undo", json!({ "doc": doc }))["undone"], "Add text field table"); + assert_eq!(count(&mut a), 8, "one undo takes back the whole last table"); +} + #[test] fn redact_mark_respects_single_page_and_rejects_conflicting_selectors() { let dir = workdir("redact-single-page"); diff --git a/crates/engine/src/catalog.rs b/crates/engine/src/catalog.rs index 551cbdc67..c19e1e42c 100644 --- a/crates/engine/src/catalog.rs +++ b/crates/engine/src/catalog.rs @@ -357,6 +357,7 @@ pub static TOOL_GROUPS: &[ToolGroup] = &[ title: "Add form components", items: &[ item("Text field", "text-cursor-input", "form.add.text", Ready), + item("Text field table", "grid-3x3", "form.add.text_table", Ready), item("Checkbox", "check-circle-2", "form.add.checkbox", Ready), item("Radio button", "circle", "form.add.radio", Ready), item("Drop-down list", "chevron-down", "form.add.combo", Ready), diff --git a/crates/engine/src/commands.rs b/crates/engine/src/commands.rs index 1a3ede01e..32cf5b443 100644 --- a/crates/engine/src/commands.rs +++ b/crates/engine/src/commands.rs @@ -296,6 +296,7 @@ pub const COMMANDS: &[CommandSpec] = &[ c("redact.sanitize", "Sanitize document…", None, None, Modification, "sparkles"), c("form.field.properties", "Field properties…", None, None, HasFields, "settings-2"), c("form.add.text", "Add a text field", None, None, Modification, "text-cursor-input"), + c("form.add.text_table", "Add text field table", None, None, Modification, "grid-3x3"), c("form.add.checkbox", "Add a checkbox", None, None, Modification, "check-circle-2"), c("form.add.radio", "Add a radio button", None, None, Modification, "circle"), c("form.add.combo", "Add a drop-down list", None, None, Modification, "chevron-down"), diff --git a/crates/engine/src/dates.rs b/crates/engine/src/dates.rs index e46a1ef6f..606ca53c6 100644 --- a/crates/engine/src/dates.rs +++ b/crates/engine/src/dates.rs @@ -63,7 +63,7 @@ const CKB_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 static DATE_LANGUAGES: [DateLanguage; 33] = [ +pub static DATE_LANGUAGES: [DateLanguage; 34] = [ DateLanguage { code: "en", name: "English", @@ -555,6 +555,15 @@ pub static DATE_LANGUAGES: [DateLanguage; 33] = [ days: ["일요일", "월요일", "화요일", "수요일", "목요일", "금요일", "토요일"], days_short: ["일", "월", "화", "수", "목", "금", "토"], }, + DateLanguage { + code: "nb", + name: "Norsk bokmål", + months: ["januar", "februar", "mars", "april", "mai", "juni", "juli", "august", "september", "oktober", "november", "desember"], + months_with_day: None, + months_short: ["jan.", "feb.", "mar.", "apr.", "mai", "jun.", "jul.", "aug.", "sep.", "okt.", "nov.", "des."], + days: ["søndag", "mandag", "tirsdag", "onsdag", "torsdag", "fredag", "lørdag"], + days_short: ["søn.", "man.", "tir.", "ons.", "tor.", "fre.", "lør."], + }, ]; /// The date language for `code` (any case), or `None` when there's none. @@ -862,6 +871,8 @@ mod tests { assert_eq!(in_lang("ddd", "zh-hant"), "週六"); assert_eq!(in_lang("dddd, d mmmm yyyy", "sq"), "e shtunë, 7 mars 2026"); assert_eq!(in_lang("ddd d mmm", "sq"), "sht 7 mar"); + assert_eq!(in_lang("dddd d. mmmm yyyy", "nb"), "lørdag 7. mars 2026"); + assert_eq!(in_lang("ddd d. mmm", "nb"), "lør. 7. mar."); assert_eq!(date_language("PT-BR").map(|l| l.code), Some("pt-br")); assert!(date_language("xx").is_none()); for l in &DATE_LANGUAGES { @@ -983,6 +994,7 @@ mod tests { assert_eq!(unwritable("8. října 2026"), "ř"); assert_eq!(unwritable("2026年10月8日"), "年月日"); assert_eq!(unwritable("sábado, 7 de março – 2026?"), "", "Western European, dashes and ? are fine"); + assert_eq!(unwritable("lørdag 7. mars, søn. 8. mai (æøå ÆØÅ)"), "", "Norwegian letters are Western European"); let mut s = Session::new().with_clock(|| 1_700_000_000); s.set_date_format("d. mmmm yyyy").unwrap(); assert_eq!(s.today_text_for_pdf(None, Some("es")).as_deref(), Ok("14. noviembre 2023")); diff --git a/crates/engine/src/field_table.rs b/crates/engine/src/field_table.rs new file mode 100644 index 000000000..f3fd96551 --- /dev/null +++ b/crates/engine/src/field_table.rs @@ -0,0 +1,131 @@ +//! Prepare a form ▸ Text field table: one undoable edit adding a [`FieldTable`]'s cells as text +//! fields. The table is laid out and named in `pdfcraft_forms::table`; this maps its cells from +//! view space onto the page and refuses, before anything is added, what `add_field` would refuse. + +pub use pdfcraft_forms::table::*; + +use pdfcraft_forms::MIN_FIELD_SIDE; +use pdfcraft_render::PageInfo; + +use crate::{Edit, FormField, NewField}; + +/// The edit's name in the history ("Undo Add text field table"), one of [`crate::BATCH_LABELS`]. +pub const LABEL: &str = "Add text field table"; + +/// The edit adding `table`'s cells to `page` (0-based, shown as `info`) of a document whose +/// fields are `form`. Refused, with nothing added, when a cell would be too small on the page or +/// its name can't be used. +pub fn edit(table: &FieldTable, page: usize, info: &PageInfo, form: &[FormField]) -> Result { + let cells = table.cells_for(form.iter().map(|f| f.name.as_str()))?; + // A page turned a quarter runs down the screen along user space's x. + let sideways = matches!(info.rotation, 90 | 270); + let mut edits = Vec::with_capacity(cells.len()); + for cell in cells { + let [x0, y0, x1, y1] = cell.rect; + let rect = info.view_rect_to_user([x0 as f32, y0 as f32, x1 as f32, y1 as f32]).map(f64::from); + // A corner past `f32`'s range comes back infinite. + if !rect.iter().all(|v| v.is_finite()) { + return Err(TableError::Numbers); + } + // `add_field`'s own test, after the page's scale (`/UserUnit`) and rounding to `f32`. + let (across, down) = if sideways { (rect[3] - rect[1], rect[2] - rect[0]) } else { (rect[2] - rect[0], rect[3] - rect[1]) }; + if down < MIN_FIELD_SIDE { + return Err(TableError::RowsTooShort); + } + if across < MIN_FIELD_SIDE { + return Err(TableError::ColumnTooNarrow); + } + edits.push(Edit::AddField { page, rect, kind: NewField::Text { multiline: false }, name: Some(cell.name) }); + } + Ok(Edit::Batch { label: LABEL.to_owned(), edits }) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::Session; + + fn page(width: f32, height: f32, crop: [f32; 4], rotation: u16) -> PageInfo { + PageInfo { width, height, label: "1".into(), crop, rotation } + } + + fn grid(rect: [f64; 4], rows: usize, patterns: &[&str]) -> FieldTable { + let mut t = FieldTable::new(rect, "unused"); + t.rows = rows; + t.columns = patterns.iter().map(|p| TableColumn { pattern: (*p).to_owned(), width: 1.0 }).collect(); + t + } + + #[test] + fn a_table_is_added_and_undone_as_one_step() { + let mut s = Session::new(); + let bytes = s.create_blank(300.0, 400.0, 1).unwrap(); + let id = s.open("t.pdf", None, bytes, None).unwrap(); + // 200 × 100 points from (50, 40) down the page as displayed: two rows of two. + let table = grid([50.0, 40.0, 250.0, 140.0], 2, &["desc_N", "price_N"]); + let add = |s: &Session| { + let doc = s.get(id).unwrap(); + edit(&table, 0, &doc.info.pages[0], &doc.form) + }; + s.apply(id, add(&s).unwrap()).unwrap(); + let doc = s.get(id).unwrap(); + let rect = |name: &str| doc.form.iter().find(|f| f.name == name).map(|f| f.widgets[0].rect); + // User space has y up: the first row is the higher one. + assert_eq!(rect("desc_1"), Some([50.0, 310.0, 150.0, 360.0])); + assert_eq!(rect("price_1"), Some([150.0, 310.0, 250.0, 360.0])); + assert_eq!(rect("desc_2"), Some([50.0, 260.0, 150.0, 310.0])); + assert_eq!(rect("price_2"), Some([150.0, 260.0, 250.0, 310.0])); + // Laid out again over the same names, it is refused before anything is added. + assert_eq!(add(&s).err(), Some(TableError::Taken("desc_1".into()))); + assert_eq!(s.undo(id).unwrap().to_string(), LABEL); + assert!(s.get(id).unwrap().form.is_empty(), "one undo takes the whole table back"); + } + + #[test] + fn rows_run_down_a_turned_page_as_it_is_shown() { + let mut s = Session::new(); + let bytes = s.create_blank(300.0, 400.0, 1).unwrap(); + let id = s.open("t.pdf", None, bytes, None).unwrap(); + s.apply(id, Edit::RotatePages { pages: vec![0], degrees: 90 }).unwrap(); + let doc = s.get(id).unwrap(); + let info = doc.info.pages[0].clone(); + assert_eq!((info.rotation, info.width, info.height), (90, 400.0, 300.0)); + let table = grid([40.0, 30.0, 240.0, 130.0], 2, &["a_N", "b_N"]); + s.apply(id, edit(&table, 0, &info, &doc.form).unwrap()).unwrap(); + // Each field, taken back to the page as shown, is where its cell was laid out. + let doc = s.get(id).unwrap(); + for cell in table.cells().unwrap() { + let r = doc.form.iter().find(|f| f.name == cell.name).unwrap().widgets[0].rect; + let (a, b) = (info.user_to_view(r[0] as f32, r[1] as f32), info.user_to_view(r[2] as f32, r[3] as f32)); + let shown = [a[0].min(b[0]), a[1].min(b[1]), a[0].max(b[0]), a[1].max(b[1])].map(f64::from); + assert!(shown.iter().zip(cell.rect).all(|(s, c)| (s - c).abs() < 1e-3), "{}: {shown:?} vs {:?}", cell.name, cell.rect); + } + } + + #[test] + fn a_cell_too_small_on_the_page_is_refused_along_the_right_side() { + // Half a point of user space for each point shown across, a whole one down: a quarter + // turn puts "across" on user space's y. + let info = page(200.0, 100.0, [0.0, 0.0, 100.0, 100.0], 90); + let narrow = grid([0.0, 0.0, 12.0, 12.0], 2, &["a_N", "b_N"]); + assert!(narrow.cells().is_ok(), "6 × 6 shown"); + assert_eq!(edit(&narrow, 0, &info, &[]).err(), Some(TableError::ColumnTooNarrow)); + let unturned = page(100.0, 200.0, [0.0, 0.0, 100.0, 100.0], 0); + assert_eq!(edit(&narrow, 0, &unturned, &[]).err(), Some(TableError::RowsTooShort)); + let roomy = page(100.0, 100.0, [0.0, 0.0, 100.0, 100.0], 90); + assert!(edit(&narrow, 0, &roomy, &[]).is_ok()); + } + + /// A corner finite as an `f64` but past `f32`'s range lays out, then comes back from the page's + /// `f32` mapping infinite: one such corner leaves a side infinite, not NaN, and the cell used to + /// pass, for `add_field` to refuse it once the edit was applied. + #[test] + fn a_table_past_the_pages_number_range_is_refused_before_anything_is_added() { + let huge = grid([0.0, 0.0, 1e39, 100.0], 2, &["a_N"]); + assert!(huge.cells().is_ok(), "finite as laid out"); + for rotation in [0, 90] { + let info = page(300.0, 400.0, [0.0, 0.0, 300.0, 400.0], rotation); + assert_eq!(edit(&huge, 0, &info, &[]).err(), Some(TableError::Numbers), "/Rotate {rotation}"); + } + } +} diff --git a/crates/engine/src/lib.rs b/crates/engine/src/lib.rs index c0fe65de7..e20cbc596 100644 --- a/crates/engine/src/lib.rs +++ b/crates/engine/src/lib.rs @@ -24,6 +24,7 @@ pub mod compare; mod copy_image; pub mod dates; pub mod export; +pub mod field_table; pub mod js; pub mod links; pub mod ocr; @@ -2023,7 +2024,8 @@ pub const EDIT_LABELS: &[&str] = &[ /// The names of the batches built below L7, which an interface shows in the same places as /// [`EDIT_LABELS`] and so translates the same way. A batch built in the interface itself arrives /// already worded in the interface's language. -pub const BATCH_LABELS: &[&str] = &["Set document title", "Mark differences", "Detect form fields", "Run JavaScript", "Recognize text"]; +pub const BATCH_LABELS: &[&str] = + &["Set document title", "Mark differences", "Detect form fields", "Run JavaScript", "Recognize text", field_table::LABEL]; impl Edit { /// Label for the Edit menu and history ("Undo Rotate pages"). diff --git a/crates/forms/README.md b/crates/forms/README.md index 992244fdf..ac48d6c58 100644 --- a/crates/forms/README.md +++ b/crates/forms/README.md @@ -25,6 +25,14 @@ set_props(&mut doc, "email", &FieldProps { ..Default::default() })?; delete_field(&mut doc, "email")?; + +// A table of text fields (view space, y down), laid out and named before any of it is added: +// refused whole for a cell under MIN_FIELD_SIDE or a name with a period, repeated or taken. +let names = all.iter().map(|f| f.name.as_str()); +let base = table::free_base(names.clone()); // "Table1", or the next one free +let mut grid = table::FieldTable::new([40.0, 240.0, 240.0, 360.0], &base); +grid.set_columns(2, &base); +let cells: Vec = grid.cells_for(names)?; // "Table1_1", "Table1_c2_1", … ``` ## Behaviour diff --git a/crates/forms/src/author.rs b/crates/forms/src/author.rs index 5bcf9f37e..f5178a687 100644 --- a/crates/forms/src/author.rs +++ b/crates/forms/src/author.rs @@ -10,6 +10,9 @@ use pdfcraft_cos::{Dict, Document, ObjRef, Object, PdfString, Stream}; use crate::{Field, FieldKind, FormError, Widget, appearance, fields, flags, page_refs}; +/// The smallest width and height of a field's widget, in points: a smaller one is refused. +pub const MIN_FIELD_SIDE: f64 = 4.0; + /// The kind of field to add. #[derive(Clone, Debug, PartialEq)] pub enum NewField { @@ -514,7 +517,7 @@ pub fn add_field(doc: &mut Document, page: usize, rect: [f64; 4], kind: &NewFiel let pages = page_refs(doc); let page_ref = *pages.get(page).ok_or_else(|| FormError::Invalid(format!("page {} does not exist", page + 1)))?; let rect = [rect[0].min(rect[2]), rect[1].min(rect[3]), rect[0].max(rect[2]), rect[1].max(rect[3])]; - if !rect.iter().all(|v| v.is_finite()) || rect[2] - rect[0] < 4.0 || rect[3] - rect[1] < 4.0 { + if !rect.iter().all(|v| v.is_finite()) || rect[2] - rect[0] < MIN_FIELD_SIDE || rect[3] - rect[1] < MIN_FIELD_SIDE { return invalid("the field is too small"); } let existing = fields(doc); @@ -855,7 +858,7 @@ fn swapped_around_center(r: [f64; 4]) -> Option<[f64; 4]> { let (cx, cy) = ((x0 + x1) / 2.0, (y0 + y1) / 2.0); let (nw, nh) = (y1 - y0, x1 - x0); let out = [cx - nw / 2.0, cy - nh / 2.0, cx + nw / 2.0, cy + nh / 2.0]; - (out.iter().all(|v| v.is_finite()) && out[2] - out[0] >= 4.0 && out[3] - out[1] >= 4.0).then_some(out) + (out.iter().all(|v| v.is_finite()) && out[2] - out[0] >= MIN_FIELD_SIDE && out[3] - out[1] >= MIN_FIELD_SIDE).then_some(out) } /// Change a field's properties (General and Options tabs) and redraw it. @@ -1098,7 +1101,7 @@ pub fn set_props(doc: &mut Document, name: &str, props: &FieldProps) -> Result, + /// Points left empty between two rows. + pub row_gap: f64, + /// Points left empty between two columns. + pub column_gap: f64, + /// The number the first row's names count from. + pub first_row: usize, +} + +/// One cell of a [`FieldTable`]: where its field goes (view space, `[x0, y0, x1, y1]` with y down) +/// and what it is called. +#[derive(Clone, Debug, PartialEq)] +pub struct Cell { + pub rect: [f64; 4], + pub name: String, +} + +impl FieldTable { + /// Four rows in one column filling `rect`, named after `base` (see [`column_pattern`]). + pub fn new(rect: [f64; 4], base: &str) -> Self { + FieldTable { + rect, + rows: 4, + columns: vec![TableColumn { pattern: column_pattern(base, 1), width: 1.0 }], + row_gap: 0.0, + column_gap: 0.0, + first_row: 1, + } + } + + /// Make the table `n` columns wide, `n` held to 1 to [`MAX_COLUMNS`]. Columns past `n` are + /// dropped; a new one is named after `base` and is as wide as the others are on average, so + /// the widths already set keep their proportions. + pub fn set_columns(&mut self, n: usize, base: &str) { + let n = n.clamp(1, MAX_COLUMNS); + self.columns.truncate(n); + let sum: f64 = self.columns.iter().map(|c| c.width).sum(); + let average = sum / self.columns.len().max(1) as f64; + let width = if average.is_finite() && average > 0.0 { average } else { 1.0 }; + while self.columns.len() < n { + let pattern = column_pattern(base, self.columns.len() + 1); + self.columns.push(TableColumn { pattern, width }); + } + } + + /// Where each cell goes and what it is called, row by row. Refuses a table out of bounds or + /// with a cell under [`MIN_FIELD_SIDE`]; the names are not checked (see [`Self::cells_for`]). + pub fn cells(&self) -> Result, TableError> { + let (rows, cols) = (self.rows, self.columns.len()); + if !(1..=MAX_ROWS).contains(&rows) { + return Err(TableError::Rows); + } + if !(1..=MAX_COLUMNS).contains(&cols) { + return Err(TableError::Columns); + } + if self.first_row > MAX_FIRST_ROW { + return Err(TableError::FirstRow); + } + let gaps = [self.row_gap, self.column_gap]; + if !self.rect.iter().all(|v| v.is_finite()) + || !gaps.iter().all(|g| g.is_finite() && *g >= 0.0) + || !self.columns.iter().all(|c| c.width.is_finite() && c.width > 0.0) + { + return Err(TableError::Numbers); + } + let r = self.rect; + let (x0, y0, x1, y1) = (r[0].min(r[2]), r[1].min(r[3]), r[0].max(r[2]), r[1].max(r[3])); + // `rows` and `cols` are at most 100 and 50 from here on, so the counts below can't overflow + // and are exact as floats. + let row_height = (y1 - y0 - self.row_gap * (rows - 1) as f64) / rows as f64; + let room = x1 - x0 - self.column_gap * (cols - 1) as f64; + let total: f64 = self.columns.iter().map(|c| c.width).sum(); + // Corners far enough apart overflow to infinity. + if !(row_height.is_finite() && room.is_finite() && total.is_finite()) { + return Err(TableError::Numbers); + } + // Every number from here on is finite: the corners, gaps and widths are, and their sums were + // checked above. + if row_height < MIN_FIELD_SIDE { + return Err(TableError::RowsTooShort); + } + let mut spans = Vec::with_capacity(cols); + // The widths of the columns so far: each edge is worked out from the start, not from the + // last edge, so rounding doesn't add up across the table. + let mut before = 0.0; + for (c, column) in self.columns.iter().enumerate() { + let left = x0 + room * before / total + c as f64 * self.column_gap; + before += column.width; + // The last column ends at the table's edge, not where rounding puts it. + let right = if c + 1 == cols { x1 } else { x0 + room * before / total + c as f64 * self.column_gap }; + if right - left < MIN_FIELD_SIDE { + return Err(TableError::ColumnTooNarrow); + } + spans.push((left, right)); + } + let mut cells = Vec::with_capacity(rows * cols); + for row in 0..rows { + let top = y0 + row as f64 * (row_height + self.row_gap); + let bottom = if row + 1 == rows { y1 } else { top + row_height }; + if bottom - top < MIN_FIELD_SIDE { + return Err(TableError::RowsTooShort); + } + for (c, (&(left, right), column)) in spans.iter().zip(&self.columns).enumerate() { + // At most MAX_FIRST_ROW + MAX_ROWS * MAX_COLUMNS. + let seq = self.first_row + row * cols + c; + let name = cell_name(&column.pattern, self.first_row + row, c + 1, seq); + cells.push(Cell { rect: [left, top, right, bottom], name }); + } + } + Ok(cells) + } + + /// [`Self::cells`] for a form whose fields are called `existing` (full names), refusing a + /// name with a period, a name two cells share and a name the form already has, as a field + /// or as the parent of one. + pub fn cells_for<'a>(&self, existing: impl IntoIterator) -> Result, TableError> { + let cells = self.cells()?; + // "a.b" also takes "a": a new field of that name would sit beside the parent of "a.b". + let mut taken = HashSet::new(); + for name in existing { + taken.insert(name); + taken.extend(name.match_indices('.').filter_map(|(at, _)| name.get(..at))); + } + let mut seen = HashSet::with_capacity(cells.len()); + for cell in &cells { + let name = cell.name.as_str(); + if name.contains('.') { + return Err(TableError::Period(cell.name.clone())); + } + if !seen.insert(name) { + return Err(TableError::Twice(cell.name.clone())); + } + if taken.contains(name) { + return Err(TableError::Taken(cell.name.clone())); + } + } + Ok(cells) + } +} + +/// The pattern a new table's column `column` (counted from 1) starts with: `{base}_N` for the +/// first, `{base}_c2_N`, `{base}_c3_N`, … after it. +pub fn column_pattern(base: &str, column: usize) -> String { + if column <= 1 { format!("{base}_N") } else { format!("{base}_c{column}_N") } +} + +/// The first of "Table1", "Table2", … that no field in `existing` (full names) starts with, so a +/// new table's names are free. One pass over the names, however many the form has. +pub fn free_base<'a>(existing: impl IntoIterator) -> String { + // The K of every "TableK" a name starts with: "Table1", "Table1_3" and "Table1.x" take 1, and + // "Table12_1" takes 12, not 1. + let taken: HashSet = existing + .into_iter() + .filter_map(|name| { + let rest = name.strip_prefix("Table")?; + let digits = rest.get(..rest.find(|c: char| !c.is_ascii_digit()).unwrap_or(rest.len()))?; + digits.parse().ok() + }) + .collect(); + // One of the first `taken.len() + 1` is always free. + let k = (1..=taken.len() as u64 + 1).find(|k| !taken.contains(k)).unwrap_or(1); + format!("Table{k}") +} + +/// What may stand on either side of an `N` that is the row number. +const SEPARATORS: [char; 4] = ['_', '-', '/', ':']; + +/// The name `pattern` gives the cell in `row` and `column` (`column` counted from 1); `seq` is the +/// cell's number counting along the rows. +/// +/// - `{row}`, `{r}`, `{n}` and `#`: the row number; `[row]`, `[r]` and `[n]`: the row number in +/// brackets (`item[N]` → `item[1]`); +/// - `{col}` and `{c}`, `[col]` and `[c]`: the column number; +/// - `{seq}` and `[seq]`: the cell number, counting along the rows from the table's first row +/// number, so a table starting at row 1 numbers its cells 1, 2, 3, …; +/// - with no row or cell number, an `N` or `n` standing alone after `_`, `-`, `/` or `:` +/// (`price_N`, `line-n-total`) is the row number, so the N in `Name` or `NET_N` stays; +/// - with no row or cell number still, the row number goes at the end. +/// +/// Braces and brackets match ignoring case. Anything else is kept as written, a period included, +/// though no field name may contain one: [`FieldTable::cells_for`] refuses those. +pub fn cell_name(pattern: &str, row: usize, column: usize, seq: usize) -> String { + let row_number = row.to_string(); + let mut out = String::with_capacity(pattern.len() + 8); + // Whether the name has a row or cell number yet. + let mut numbered = false; + let mut rest = pattern.trim(); + while let Some(at) = rest.find(['{', '[', '#']) { + // `at` is where an ASCII character starts, so both halves are whole characters. + let (before, from) = rest.split_at(at); + out.push_str(before); + if let Some(after) = from.strip_prefix('#') { + out.push_str(&row_number); + numbered = true; + rest = after; + continue; + } + let (open, close) = if from.starts_with('[') { ('[', ']') } else { ('{', '}') }; + let inside = from.get(1..).unwrap_or_default(); + let token = inside.split_once(close).and_then(|(word, after)| { + let is = |name: &str| word.eq_ignore_ascii_case(name); + if is("row") || is("r") || is("n") { + Some((row, true, after)) + } else if is("seq") { + Some((seq, true, after)) + } else if is("col") || is("c") { + Some((column, false, after)) + } else { + None + } + }); + match token { + Some((n, counts, after)) => { + if open == '[' { + out.push('['); + } + out.push_str(&n.to_string()); + if open == '[' { + out.push(']'); + } + numbered |= counts; + rest = after; + } + None => { + out.push(open); + rest = inside; + } + } + } + out.push_str(rest); + if numbered { + return out; + } + if out == "N" || out == "n" { + return row_number; + } + let chars: Vec = out.chars().collect(); + let mut named = String::with_capacity(out.len() + row_number.len()); + for (i, &ch) in chars.iter().enumerate() { + let after_separator = i.checked_sub(1).and_then(|p| chars.get(p)).is_some_and(|c| SEPARATORS.contains(c)); + let before_separator = chars.get(i + 1).is_none_or(|c| SEPARATORS.contains(c)); + if matches!(ch, 'N' | 'n') && after_separator && before_separator { + named.push_str(&row_number); + numbered = true; + } else { + named.push(ch); + } + } + if !numbered { + named.push_str(&row_number); + } + named +} + +/// Why a table can't be added. Each is a mistake a person can make, with the numbers and names +/// apart from the words, so an interface can say it in its own language with [`TableError::said`]. +#[derive(Clone, Debug, PartialEq)] +pub enum TableError { + /// Fewer than one row, or more than [`MAX_ROWS`]. + Rows, + /// Fewer than one column, or more than [`MAX_COLUMNS`]. + Columns, + /// A first row number past [`MAX_FIRST_ROW`]. + FirstRow, + /// A corner, gap or width that is not a usable number. + Numbers, + /// The rows come out under [`MIN_FIELD_SIDE`] high. + RowsTooShort, + /// A column comes out under [`MIN_FIELD_SIDE`] wide. + ColumnTooNarrow, + /// A cell's name has a period, which would make it a child of another field. + Period(String), + /// Two cells would have this name. + Twice(String), + /// The form already has a field of this name. + Taken(String), +} + +const ROWS: &str = "a table has 1 to {max} rows"; +const COLUMNS: &str = "a table has 1 to {max} columns"; +const FIRST_ROW: &str = "the first row number can be at most {max}"; +const NUMBERS: &str = "the area, the spacing and the column widths must be numbers, the spacing 0 or more and each width more than 0"; +const ROWS_TOO_SHORT: &str = "the rows are too short: make the area taller, or use fewer rows or less row spacing"; +const COLUMN_TOO_NARROW: &str = + "a column is too narrow: make the area wider, give the column more of it, or use fewer columns or less column spacing"; +const PERIOD: &str = "field names can't contain a period: {name}"; +const TWICE: &str = "two cells would both be named {name}"; +const TAKEN: &str = "a field named {name} already exists"; + +/// Every sentence a [`TableError`] says, in English with its holes still in it, for an interface +/// to translate; `wordings_are_said` keeps the list in step with the errors. +pub const WORDINGS: &[&str] = &[ROWS, COLUMNS, FIRST_ROW, NUMBERS, ROWS_TOO_SHORT, COLUMN_TOO_NARROW, PERIOD, TWICE, TAKEN]; + +/// How an interface says one of our wordings in its own language, holes and all: given the English +/// in [`WORDINGS`], the same wording in that language. [`in_english`] keeps the English. +pub type Words<'a> = &'a dyn Fn(&str) -> String; + +/// The [`Words`] that translate nothing: the wording as it is written here. +pub fn in_english(wording: &str) -> String { + wording.to_owned() +} + +impl TableError { + /// This error as one sentence, said in the language `words` translates into. The wording is + /// translated and the numbers and names go in afterwards, in a single pass, so a name with + /// braces in it is never read as a hole. With [`in_english`] this is the error's `Display`. + pub fn said(&self, words: Words) -> String { + let limit = |wording: &str, max: usize| say(&words(wording), &[("{max}", max.to_string())]); + // A name goes in quoted, as `add_field` quotes it. + let named = |wording: &str, name: &str| say(&words(wording), &[("{name}", format!("{name:?}"))]); + match self { + TableError::Rows => limit(ROWS, MAX_ROWS), + TableError::Columns => limit(COLUMNS, MAX_COLUMNS), + TableError::FirstRow => limit(FIRST_ROW, MAX_FIRST_ROW), + TableError::Numbers => words(NUMBERS), + TableError::RowsTooShort => words(ROWS_TOO_SHORT), + TableError::ColumnTooNarrow => words(COLUMN_TOO_NARROW), + TableError::Period(name) => named(PERIOD, name), + TableError::Twice(name) => named(TWICE, name), + TableError::Taken(name) => named(TAKEN, name), + } + } +} + +impl fmt::Display for TableError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(&self.said(&in_english)) + } +} + +impl std::error::Error for TableError {} + +/// A wording with its holes filled. A hole is a name in braces, such as `{max}`; what is not in +/// `values` is left as it is written. +fn say(wording: &str, values: &[(&str, String)]) -> String { + let mut out = String::with_capacity(wording.len() + 8); + let mut rest = wording; + while let Some(open) = rest.find('{') { + let (before, from_brace) = rest.split_at(open); + out.push_str(before); + // `find` counts from the brace and `}` is one byte, so this is a char boundary. + let Some(close) = from_brace.find('}') else { + out.push_str(from_brace); + return out; + }; + let Some(hole) = from_brace.get(..=close) else { return out + from_brace }; + match values.iter().find(|(name, _)| *name == hole) { + Some((_, value)) => out.push_str(value), + None => out.push_str(hole), + } + rest = from_brace.get(close + 1..).unwrap_or_default(); + } + out.push_str(rest); + out +} + +#[cfg(test)] +mod tests { + use proptest::prelude::*; + + use super::*; + + fn table(rect: [f64; 4], rows: usize, patterns: &[&str]) -> FieldTable { + FieldTable { + rect, + rows, + columns: patterns.iter().map(|p| TableColumn { pattern: (*p).to_owned(), width: 1.0 }).collect(), + row_gap: 0.0, + column_gap: 0.0, + first_row: 1, + } + } + + #[test] + fn a_row_number_goes_where_the_pattern_says() { + assert_eq!(cell_name("fieldCategory_N", 1, 1, 1), "fieldCategory_1"); + assert_eq!(cell_name("fieldCategory_N", 10, 1, 10), "fieldCategory_10"); + assert_eq!(cell_name("row_{n}", 3, 1, 3), "row_3"); + assert_eq!(cell_name("item-#", 5, 1, 5), "item-5"); + assert_eq!(cell_name("cell[N]", 2, 1, 2), "cell[2]"); + assert_eq!(cell_name("N", 4, 1, 4), "4"); + assert_eq!(cell_name("line-n-total", 6, 1, 6), "line-6-total"); + assert_eq!(cell_name("a:N/b", 7, 1, 7), "a:7/b"); + } + + #[test] + fn rows_columns_and_cells_have_their_own_tokens() { + assert_eq!(cell_name("item_{row}_{col}", 2, 3, 5), "item_2_3"); + assert_eq!(cell_name("cell_R{row}C{col}", 1, 4, 4), "cell_R1C4"); + assert_eq!(cell_name("grid[{r},{c}]", 3, 2, 8), "grid[3,2]"); + assert_eq!(cell_name("table_r{r}_c{c}", 4, 5, 20), "table_r4_c5"); + assert_eq!(cell_name("cell{seq}", 2, 2, 7), "cell7"); + assert_eq!(cell_name("cell[seq]", 2, 2, 7), "cell[7]"); + // Braces and brackets match ignoring case. + assert_eq!(cell_name("x{ROW}y{Col}z[SEQ]", 2, 3, 9), "x2y3z[9]"); + // A column number alone leaves the row to be found or added. + assert_eq!(cell_name("price{c}_N", 2, 3, 9), "price3_2"); + assert_eq!(cell_name("col{c}", 1, 2, 2), "col21"); + } + + #[test] + fn without_a_row_token_the_row_number_goes_at_the_end() { + assert_eq!(cell_name("", 1, 1, 1), "1"); + assert_eq!(cell_name(" ", 3, 1, 3), "3"); + assert_eq!(cell_name("test_", 1, 1, 1), "test_1"); + assert_eq!(cell_name("Phone_Number", 4, 1, 4), "Phone_Number4"); + // What isn't a token is kept as written. + assert_eq!(cell_name("{x}_N", 1, 1, 1), "{x}_1"); + assert_eq!(cell_name("{row", 1, 1, 1), "{row1"); + assert_eq!(cell_name("a]b[", 2, 1, 2), "a]b[2"); + // A period is kept, for `cells_for` to refuse. + assert_eq!(cell_name("test.", 1, 1, 1), "test.1"); + } + + #[test] + fn an_n_inside_a_word_is_not_the_row_number() { + assert_eq!(cell_name("Name_N", 1, 1, 1), "Name_1"); + assert_eq!(cell_name("ItemNumber_N", 2, 1, 2), "ItemNumber_2"); + assert_eq!(cell_name("NET_AMOUNT_N", 3, 1, 3), "NET_AMOUNT_3"); + assert_eq!(cell_name("CustomerName_{row}", 5, 1, 5), "CustomerName_5"); + assert_eq!(cell_name("NOTE_#", 6, 1, 6), "NOTE_6"); + assert_eq!(cell_name("BankName[N]", 7, 1, 7), "BankName[7]"); + assert_eq!(cell_name("PLAN_N", 8, 1, 8), "PLAN_8"); + assert_eq!(cell_name("line_n", 9, 1, 9), "line_9"); + assert_eq!(cell_name("Prix_é_N", 1, 1, 1), "Prix_é_1"); + assert_eq!(cell_name("名前_N", 2, 1, 2), "名前_2"); + } + + #[test] + fn a_grid_fills_its_area_with_gaps_between_the_cells() { + let mut t = table([100.0, 100.0, 300.0, 300.0], 2, &["cell_{r}_{c}", "cell_{r}_{c}"]); + t.row_gap = 10.0; + t.column_gap = 20.0; + // Width (200 - 20) / 2 = 90, height (200 - 10) / 2 = 95, rows from the top down. + let cells = t.cells().unwrap(); + let got: Vec<_> = cells.iter().map(|c| (c.rect, c.name.as_str())).collect(); + assert_eq!( + got, + [ + ([100.0, 100.0, 190.0, 195.0], "cell_1_1"), + ([210.0, 100.0, 300.0, 195.0], "cell_1_2"), + ([100.0, 205.0, 190.0, 300.0], "cell_2_1"), + ([210.0, 205.0, 300.0, 300.0], "cell_2_2"), + ] + ); + // Corners in either order are the same area. + t.rect = [300.0, 300.0, 100.0, 100.0]; + assert_eq!(t.cells().unwrap(), cells); + } + + #[test] + fn columns_share_the_width_by_their_widths() { + let mut t = table([100.0, 100.0, 300.0, 300.0], 2, &["desc_N", "price_N"]); + t.columns[0].width = 3.0; + let cells = t.cells().unwrap(); + assert_eq!(cells[0].rect, [100.0, 100.0, 250.0, 200.0]); + assert_eq!(cells[1].rect, [250.0, 100.0, 300.0, 200.0]); + assert_eq!((cells[2].name.as_str(), cells[3].name.as_str()), ("desc_2", "price_2")); + // However the shares round, the last column ends at the table's edge. + let mut t = table([0.0, 0.0, 100.0, 50.0], 1, &["a", "b", "c"]); + t.column_gap = 1.0 / 3.0; + assert_eq!(t.cells().unwrap()[2].rect[2], 100.0); + } + + #[test] + fn rows_count_from_the_first_row_and_cells_along_the_rows() { + let mut t = table([0.0, 0.0, 100.0, 100.0], 2, &["a_{seq}_{r}", "b_{seq}_{r}"]); + t.first_row = 10; + let names: Vec<_> = t.cells().unwrap().into_iter().map(|c| c.name).collect(); + assert_eq!(names, ["a_10_10", "b_11_10", "a_12_11", "b_13_11"]); + } + + #[test] + fn a_table_out_of_bounds_is_refused() { + let ok = table([0.0, 0.0, 200.0, 200.0], 4, &["a_N"]); + assert!(ok.cells().is_ok()); + let with = |f: &dyn Fn(&mut FieldTable)| { + let mut t = ok.clone(); + f(&mut t); + t.cells() + }; + assert_eq!(with(&|t| t.rows = 0), Err(TableError::Rows)); + assert_eq!(with(&|t| t.rows = MAX_ROWS + 1), Err(TableError::Rows)); + assert_eq!(with(&|t| t.columns.clear()), Err(TableError::Columns)); + // 200 points hold 50 columns of exactly 4, and 199 don't. + assert_eq!(with(&|t| t.set_columns(MAX_COLUMNS, "x")).map(|c| c.len()), Ok(4 * MAX_COLUMNS)); + assert_eq!( + with(&|t| { + t.rect[2] = 199.0; + t.set_columns(MAX_COLUMNS, "x"); + }), + Err(TableError::ColumnTooNarrow) + ); + assert_eq!(with(&|t| t.columns = vec![t.columns[0].clone(); MAX_COLUMNS + 1]), Err(TableError::Columns)); + assert_eq!(with(&|t| t.first_row = MAX_FIRST_ROW + 1), Err(TableError::FirstRow)); + assert_eq!(with(&|t| t.rect[0] = f64::NAN), Err(TableError::Numbers)); + assert_eq!(with(&|t| t.rect = [-f64::MAX, 0.0, f64::MAX, 200.0]), Err(TableError::Numbers)); + assert_eq!(with(&|t| t.row_gap = -1.0), Err(TableError::Numbers)); + assert_eq!(with(&|t| t.column_gap = f64::INFINITY), Err(TableError::Numbers)); + assert_eq!(with(&|t| t.columns[0].width = 0.0), Err(TableError::Numbers)); + // 200 points hold 50 rows of exactly 4, not 51. + assert_eq!(with(&|t| t.rows = 50).map(|c| c.len()), Ok(50)); + assert_eq!(with(&|t| t.rows = 51), Err(TableError::RowsTooShort)); + assert_eq!(with(&|t| t.row_gap = 70.0), Err(TableError::RowsTooShort)); + assert_eq!(with(&|t| t.rect = [0.0, 0.0, 3.0, 200.0]), Err(TableError::ColumnTooNarrow)); + // One column squeezed under 4 points by the others' widths. + assert_eq!( + with(&|t| t.columns = vec![TableColumn { pattern: "a_N".into(), width: 1.0 }, TableColumn { pattern: "b_N".into(), width: 99.0 }]), + Err(TableError::ColumnTooNarrow) + ); + } + + #[test] + fn names_a_form_would_refuse_are_refused_before_anything_is_added() { + let t = table([0.0, 0.0, 200.0, 200.0], 2, &["a_N", "b_N"]); + assert!(t.cells_for(["c_1", "a_10"]).is_ok()); + assert_eq!(t.cells_for(["x", "b_2"]), Err(TableError::Taken("b_2".into()))); + // The parent of an existing field is taken too. + assert_eq!(t.cells_for(["a_1.street"]), Err(TableError::Taken("a_1".into()))); + let same = table([0.0, 0.0, 200.0, 200.0], 2, &["item_N", "item_N"]); + assert_eq!(same.cells_for([]), Err(TableError::Twice("item_1".into()))); + let dotted = table([0.0, 0.0, 200.0, 200.0], 2, &["row.{n}"]); + assert_eq!(dotted.cells_for([]), Err(TableError::Period("row.1".into()))); + } + + #[test] + fn a_new_table_takes_the_first_free_base() { + assert_eq!(free_base([]), "Table1"); + assert_eq!(free_base(["Table1_1", "Table1_c2_4"]), "Table2"); + assert_eq!(free_base(["Table1", "Table2.x", "Table3_N"]), "Table4"); + // "Table12_1" belongs to Table12, not Table1. + assert_eq!(free_base(["Table12_1", "Tables"]), "Table1"); + // As fast for a form with thousands of fields: one pass, not one per candidate. + let many: Vec = (1..=20_000).map(|k| format!("Table{k}_1")).collect(); + assert_eq!(free_base(many.iter().map(String::as_str)), "Table20001"); + let t = FieldTable::new([0.0, 0.0, 200.0, 200.0], &free_base(["Table1_1"])); + let names: Vec<_> = t.cells_for(["Table1_1"]).unwrap().into_iter().map(|c| c.name).collect(); + assert_eq!(names, ["Table2_1", "Table2_2", "Table2_3", "Table2_4"]); + } + + #[test] + fn added_columns_keep_the_widths_already_set() { + let mut t = FieldTable::new([0.0, 0.0, 400.0, 100.0], "T"); + t.columns[0].width = 2.0; + t.set_columns(3, "T"); + let got: Vec<_> = t.columns.iter().map(|c| (c.pattern.as_str(), c.width)).collect(); + assert_eq!(got, [("T_N", 2.0), ("T_c2_N", 2.0), ("T_c3_N", 2.0)]); + t.columns[2].width = 4.0; + t.set_columns(4, "T"); + assert_eq!(t.columns[3].width, 8.0 / 3.0); + t.set_columns(2, "T"); + assert_eq!(t.columns.len(), 2); + // Held to the bounds. + t.set_columns(0, "T"); + assert_eq!(t.columns.len(), 1); + t.set_columns(usize::MAX, "T"); + assert_eq!(t.columns.len(), MAX_COLUMNS); + } + + #[test] + fn wordings_are_said() { + let mistakes = [ + TableError::Rows, + TableError::Columns, + TableError::FirstRow, + TableError::Numbers, + TableError::RowsTooShort, + TableError::ColumnTooNarrow, + TableError::Period("a.1".into()), + TableError::Twice("a_1".into()), + TableError::Taken("a_1".into()), + ]; + assert_eq!(WORDINGS.len(), mistakes.len(), "one wording per mistake"); + // Each mistake says one of the wordings: translated to a marker, only the values remain. + for e in &mistakes { + let marked = e.said(&|w| format!("<{}>", WORDINGS.iter().position(|x| *x == w).unwrap_or(usize::MAX))); + assert!(!marked.contains(&usize::MAX.to_string()), "{e:?} says a wording that isn't listed"); + assert_eq!(e.said(&in_english), e.to_string()); + } + assert_eq!(TableError::Rows.to_string(), "a table has 1 to 100 rows"); + assert_eq!(TableError::Taken("a_1".into()).to_string(), "a field named \"a_1\" already exists"); + // A name goes in as it stands, braces and all, and is never read as a hole. + let other = |w: &str| if w == TAKEN { "{name} existe ya".to_owned() } else { w.to_owned() }; + assert_eq!(TableError::Taken("{max}".into()).said(&other), "\"{max}\" existe ya"); + } + + proptest! { + /// Any pattern names every cell, and never panics. + #[test] + fn any_pattern_gives_a_name(pattern in "(\\PC|[{}\\[\\]#Nn_:/.-]){0,24}", row in 0usize..200_000, column in 1usize..60, seq in 0usize..300_000) { + let name = cell_name(&pattern, row, column, seq); + prop_assert!(!name.is_empty()); + prop_assert_eq!(name.trim(), name.as_str()); + } + + /// Any table either lays out cells of at least the smallest side inside its area, or says + /// why not. + #[test] + fn any_table_lays_out_or_says_why( + rect in prop::array::uniform4(-2000.0f64..2000.0), + rows in 0usize..120, + widths in prop::collection::vec(0.01f64..100.0, 0..60), + row_gap in -1.0f64..50.0, + column_gap in -1.0f64..50.0, + ) { + let t = FieldTable { + rect, + rows, + columns: widths.iter().map(|w| TableColumn { pattern: "a{seq}".into(), width: *w }).collect(), + row_gap, + column_gap, + first_row: 1, + }; + if let Ok(cells) = t.cells_for([]) { + prop_assert_eq!(cells.len(), rows * widths.len()); + let (x0, y0, x1, y1) = (rect[0].min(rect[2]), rect[1].min(rect[3]), rect[0].max(rect[2]), rect[1].max(rect[3])); + for c in &cells { + let [l, t, r, b] = c.rect; + prop_assert!(r - l >= MIN_FIELD_SIDE && b - t >= MIN_FIELD_SIDE, "{:?}", c); + prop_assert!(l >= x0 - 1e-6 && r <= x1 + 1e-6 && t >= y0 - 1e-6 && b <= y1 + 1e-6, "{:?}", c); + } + } + } + } +} diff --git a/crates/ui-egui/src/canvas.rs b/crates/ui-egui/src/canvas.rs index 9cda984a0..f8df86fb3 100644 --- a/crates/ui-egui/src/canvas.rs +++ b/crates/ui-egui/src/canvas.rs @@ -3071,6 +3071,9 @@ pub fn document_area(app: &mut PdfCraftApp, index: usize, ui: &mut egui::Ui) { let mut open_props: Option<(usize, usize)> = None; let mut field_props = false; let mut field_placed = false; + let mut open_table: Option<(usize, [f64; 4])> = None; + // The table being set up in its window, shown on its own document's page. + let table_preview = app.field_table_draft.as_mut().filter(|d| d.doc == view.id).and_then(|d| Some(d.preview(info.pages.get(d.page)?, &form))); let can_modify = doc.allows_modification(); if preparing { crate::prepare::after_refresh(view, &form); @@ -3417,9 +3420,12 @@ pub fn document_area(app: &mut PdfCraftApp, index: usize, ui: &mut egui::Ui) { QuickTool::Field(f) => Some(f), _ => None, }; - let o = crate::prepare::page_input(ui, &resp, &xf, i, info, &form, field_tool, can_modify, view); + // While a table is being set up, nothing else is drawn or moved. + let allowed = can_modify && table_preview.is_none() && open_table.is_none(); + let o = crate::prepare::page_input(ui, &resp, &xf, i, info, &form, field_tool, allowed, view); field_props |= o.properties; field_placed |= o.placed; + open_table = open_table.or(o.table); (o.consumed || field_tool.is_some(), false) } else { use crate::forms_ui::FieldPointer; @@ -3734,6 +3740,9 @@ pub fn document_area(app: &mut PdfCraftApp, index: usize, ui: &mut egui::Ui) { } if preparing { crate::prepare::paint_page(ui, painter, &xf, i, info, &form, view); + if let Some(preview) = &table_preview { + crate::field_table_ui::paint_preview(painter, &xf, i, preview); + } if calc_order { let selected = view.prepare.selected.as_ref().map(|(name, _)| name.as_str()); if let Some(name) = crate::calc_order_ui::paint_page(ui, painter, &xf, i, info, &form, &view.calc_order, generation, selected) { @@ -4375,6 +4384,14 @@ pub fn document_area(app: &mut PdfCraftApp, index: usize, ui: &mut egui::Ui) { app.boxes_draft.seeded = None; app.dialog = Some(crate::Dialog::PageBoxes); } + if let Some((page, rect)) = open_table { + app.field_table_draft = Some(crate::field_table_ui::FieldTableDraft::new(app.views[index].id, page, rect, &form, generation)); + app.dialog = Some(crate::Dialog::FieldTable); + // The window lasts while the form is prepared. Drawn with the tool picked but its panel + // closed, the tool goes back to Select above, so the panel opens to keep it prepared. + app.left = crate::LeftPanel::Tool("form"); + app.reveal_tools(); + } if let Some(n) = form_notice { match n { crate::forms_ui::FormNotice::Security => app.notify_tr("The document's security settings don't allow filling in form fields"), diff --git a/crates/ui-egui/src/dialogs.rs b/crates/ui-egui/src/dialogs.rs index b6d1619d9..0337a8cbf 100644 --- a/crates/ui-egui/src/dialogs.rs +++ b/crates/ui-egui/src/dialogs.rs @@ -25,6 +25,14 @@ fn dialogs(app: &mut PdfCraftApp, ctx: &egui::Context) { if app.dialog != Some(Dialog::WordCount) { app.word_count_draft.cancel(); } + if app.dialog != Some(Dialog::FieldTable) { + app.field_table_draft = None; + } + // A movable window, not this modal: the page under it shows the table as it changes. + if app.dialog == Some(Dialog::FieldTable) { + crate::field_table_ui::show(app, ctx); + return; + } let Some(dialog) = app.dialog else { app.props_draft = None; app.view_draft = None; @@ -719,6 +727,9 @@ fn dialogs(app: &mut PdfCraftApp, ctx: &egui::Context) { }); return; } + // Never reached: `dialogs` shows this movable window above and returns before the modal. + // The arm keeps the match exhaustive. + Dialog::FieldTable => close = true, Dialog::Optimize => { let (ok, cancel) = crate::optimize_ui::body(ui, &mut app.optimize_draft, &t); optimize_now = ok; diff --git a/crates/ui-egui/src/field_table_ui.rs b/crates/ui-egui/src/field_table_ui.rs new file mode 100644 index 000000000..9314dc927 --- /dev/null +++ b/crates/ui-egui/src/field_table_ui.rs @@ -0,0 +1,512 @@ +//! Prepare a form ▸ Text field table: a movable window that divides the area drawn on a page into +//! rows and columns of text fields, while the page shows the table as it changes. The table is +//! the engine's [`FieldTable`]; this is its window. +//! +//! The window is not modal, so that the page stays in view, but it belongs to one page of one +//! document as it was when the window opened: it closes when that document stops being the one +//! prepared or changes in any way (a page rotated, moved or deleted, an edit undone), and adds the +//! table there. + +use std::ops::RangeInclusive; +use std::sync::Arc; + +use egui::{Align, Color32, CornerRadius, Layout, Rect, RichText, Sense, Stroke, pos2, vec2}; +use pdfcraft_engine::field_table::{self, Cell, FieldTable, MAX_COLUMNS, MAX_FIRST_ROW, MAX_ROWS, TableError}; +use pdfcraft_engine::{DocId, Edit, FormField}; +use pdfcraft_render::PageInfo; + +use crate::canvas::PageXform; +use crate::{Dialog, PdfCraftApp, i18n, theme, widgets}; + +const SELECT_BLUE: Color32 = Color32::from_rgb(0x14, 0x73, 0xE6); +const ERROR_RED: Color32 = Color32::from_rgb(0xD1, 0x3B, 0x3B); +/// A divider can't be dragged to leave a column less than this share of the table, or less than a +/// quarter of what the two columns it divides have between them when that is smaller. +const MIN_SHARE: f64 = 0.04; + +/// The table being set up in the window, and where it goes. +#[derive(Clone, Debug)] +pub struct FieldTableDraft { + /// The document the table goes in. + pub doc: DocId, + /// The page it goes on, 0-based. + pub page: usize, + pub table: FieldTable, + /// What added columns are named after: the first free "TableK" when the window opened. + base: String, + /// The document's edit generation when the window opened: any edit since closes it. + generation: u64, + /// The table as last laid out, kept until the table changes. + laid: Option>, + /// A field in the window had the keyboard last frame, so Escape leaves the field. + typing: bool, +} + +/// Two drafts are the same table in the same place; what is kept for the next frame isn't part of it. +impl PartialEq for FieldTableDraft { + fn eq(&self, other: &Self) -> bool { + (self.doc, self.page, &self.table, &self.base, self.generation) == (other.doc, other.page, &other.table, &other.base, other.generation) + } +} + +/// A table laid out and checked once, for every frame it stays the same: the cells the page and the +/// window show, and the edit Add fields applies. Up to 100 × 50 cells, so not every frame. +#[derive(Debug)] +pub(crate) struct Laid { + table: FieldTable, + cells: Result, TableError>, + edit: Result, +} + +/// What the page shows of the table being set up: its cells, or why it can't be laid out. +pub(crate) struct Preview { + page: usize, + laid: Arc, +} + +/// How the window was left this frame. +enum Choice { + Add, + Cancel, +} + +impl FieldTableDraft { + /// A table filling `rect` (view space) on `page` of a document at edit `generation`, named after + /// the first base `form` leaves free. + pub(crate) fn new(doc: DocId, page: usize, rect: [f64; 4], form: &[FormField], generation: u64) -> Self { + let base = field_table::free_base(form.iter().map(|f| f.name.as_str())); + FieldTableDraft { doc, page, table: FieldTable::new(rect, &base), base, generation, laid: None, typing: false } + } + + /// The table laid out on its page (`info`, in a form whose fields are `form`), worked out again + /// only when the table has changed since. The form can't change under it: an edit closes the + /// window. + pub(crate) fn laid(&mut self, info: &PageInfo, form: &[FormField]) -> Arc { + if let Some(laid) = self.laid.as_ref().filter(|l| l.table == self.table) { + return Arc::clone(laid); + } + let laid = + Arc::new(Laid { table: self.table.clone(), cells: self.table.cells(), edit: field_table::edit(&self.table, self.page, info, form) }); + self.laid = Some(Arc::clone(&laid)); + laid + } + + /// What the page shows of this table this frame. + pub(crate) fn preview(&mut self, info: &PageInfo, form: &[FormField]) -> Preview { + Preview { page: self.page, laid: self.laid(info, form) } + } + + /// Each column's share of the table's width, together 1. + fn shares(&self) -> Vec { + let total: f64 = self.table.columns.iter().map(|c| c.width).sum(); + let n = self.table.columns.len().max(1) as f64; + self.table.columns.iter().map(|c| if total > 0.0 && total.is_finite() { c.width / total } else { 1.0 / n }).collect() + } + + /// Move the divider after column `i` (0-based) by `delta`, a share of the table's width. The + /// two columns it divides keep [`MIN_SHARE`] each, or a quarter of what they have between them + /// when that is less, so narrow columns can still be resized; the other columns keep theirs. + fn move_divider(&mut self, i: usize, delta: f64) { + let total: f64 = self.table.columns.iter().map(|c| c.width).sum(); + let (Some(a), Some(b)) = (self.table.columns.get(i).map(|c| c.width), self.table.columns.get(i + 1).map(|c| c.width)) else { return }; + if !(total > 0.0 && total.is_finite() && delta.is_finite()) { + return; + } + let pair = (a + b) / total; + // At most half the pair, so the bounds never cross; `max` then `min` all the same, never + // `clamp`, which panics if they do. + let least = MIN_SHARE.min(pair / 4.0); + let left = (a / total + delta).max(least).min(pair - least); + if let Some(c) = self.table.columns.get_mut(i) { + c.width = left * total; + } + if let Some(c) = self.table.columns.get_mut(i + 1) { + c.width = (pair - left) * total; + } + } + + /// Give every column the same width: their widths are shares, so the same number does it. + fn distribute_evenly(&mut self) { + for c in &mut self.table.columns { + c.width = 1.0; + } + } +} + +/// Show the window while [`Dialog::FieldTable`] is open, and add the table when asked to. +pub(crate) fn show(app: &mut PdfCraftApp, ctx: &egui::Context) { + // The window belongs to one page of one document, while that document is being prepared. + let active = app.active_ids().map(|(_, id)| id); + let preparing = app.is_preparing(); + let belongs = app.field_table_draft.as_ref().is_some_and(|d| { + active == Some(d.doc) + && preparing + && app + .session + .get(d.doc) + .is_some_and(|doc| doc.edit_generation() == d.generation && d.page < doc.info.pages.len() && doc.allows_modification()) + }); + if !belongs { + close(app); + return; + } + // Both there, as `belongs` says; the fields are borrowed apart, so the window can change the + // table while it reads the document. + let Some(draft) = app.field_table_draft.as_mut() else { return }; + let Some((info, form)) = app.session.get(draft.doc).and_then(|doc| Some((doc.info.pages.get(draft.page)?, &doc.form))) else { return }; + let t = theme::Tokens::get(ctx); + let mut open = true; + let mut choice = None; + let mut laid = None; + egui::Window::new(tl!("Text Field Table")) + .id(egui::Id::new("field_table_window")) + .open(&mut open) + .collapsible(false) + .resizable(false) + .order(egui::Order::Foreground) + .min_width(440.0) + .max_width(440.0) + .default_pos(egui::pos2(120.0, 100.0)) + .show(ctx, |ui| { + widgets::begin_dialog(ui); + let (c, l) = body(ui, draft, info, form, &t); + choice = c; + laid = Some(l); + }); + // Escape closes it, as it closes a modal dialog, unless a field had the keyboard: then Escape + // only leaves the field, and what was set up stays. Enter is left to the field being typed in. + let escape = !draft.typing && ctx.input_mut(|i| i.consume_key(egui::Modifiers::NONE, egui::Key::Escape)); + draft.typing = ctx.egui_wants_keyboard_input(); + match choice { + _ if !open || escape => close(app), + Some(Choice::Cancel) => close(app), + // Added, the window closes; refused, it stays as it is, with the notice saying why. + Some(Choice::Add) => { + if let Some(Ok(edit)) = laid.as_deref().map(|l| l.edit.clone()) + && app.apply_edit(edit) + { + close(app); + } + } + None => {} + } +} + +/// Drop the table being set up, and the dialog with it if it is still the open one. +fn close(app: &mut PdfCraftApp) { + app.field_table_draft = None; + if app.dialog == Some(Dialog::FieldTable) { + app.dialog = None; + } +} + +/// The window's controls, the names the table will have, and why it can't be added if it can't. +/// Returns what was chosen and the table as it now stands, laid out and checked. +fn body(ui: &mut egui::Ui, d: &mut FieldTableDraft, info: &PageInfo, form: &[FormField], t: &theme::Tokens) -> (Option, Arc) { + let columns = d.table.columns.len(); + egui::Grid::new("field_table_grid").num_columns(2).spacing([14.0, 8.0]).min_col_width(120.0).show(ui, |ui| { + ui.label(tl!("Rows")); + stepper(ui, &mut d.table.rows, 1..=MAX_ROWS, 1.0, ""); + ui.end_row(); + ui.label(tl!("Columns")); + let mut n = columns; + if stepper(ui, &mut n, 1..=MAX_COLUMNS, 1.0, "") { + d.table.set_columns(n, &d.base); + } + ui.end_row(); + ui.label(tl!("Row spacing")); + stepper(ui, &mut d.table.row_gap, 0.0..=100.0, 1.0, " pt"); + ui.end_row(); + ui.label(tl!("Column spacing")); + ui.add_enabled_ui(columns > 1, |ui| stepper(ui, &mut d.table.column_gap, 0.0..=100.0, 1.0, " pt")); + ui.end_row(); + ui.label(tl!("First row number")); + ui.add(egui::DragValue::new(&mut d.table.first_row).range(0..=MAX_FIRST_ROW)); + ui.end_row(); + }); + + ui.add_space(8.0); + ui.separator(); + ui.add_space(6.0); + ui.horizontal(|ui| { + ui.label(RichText::new(tl!("Column widths")).strong()); + ui.with_layout(Layout::right_to_left(Align::Center), |ui| { + ui.add_enabled_ui(d.table.columns.len() > 1, |ui| { + if widgets::pill_button(ui, tl!("Distribute evenly"), false).clicked() { + d.distribute_evenly(); + } + }); + }); + }); + ui.add_space(4.0); + column_bar(ui, d, t); + + ui.add_space(8.0); + ui.label(RichText::new(tl!("Field names")).strong()); + // How the names are worked out. Its tokens are the same in every language. + let hint = tl!("N or {row} is the row number, {col} the column number, {seq} the cell number. With none of them, the row number goes last."); + ui.add(egui::Label::new(RichText::new(hint).color(t.text_muted).size(11.0)).wrap()); + ui.add_space(4.0); + // The widths shown don't depend on the names being typed below. + let layout = d.laid(info, form); + let shares = d.shares(); + let scrolls = d.table.columns.len() > 3; + let mut column_fields = |ui: &mut egui::Ui| { + for (c, column) in d.table.columns.iter_mut().enumerate() { + let width = layout.cells.as_ref().ok().and_then(|cells| cells.get(c)).map(|cell| cell.rect[2] - cell.rect[0]); + let share = shares.get(c).copied().unwrap_or_default(); + ui.horizontal(|ui| { + ui.label(RichText::new(column_label(c)).strong()); + ui.with_layout(Layout::right_to_left(Align::Center), |ui| { + let size = width.map_or_else(|| "—".to_owned(), |w| format!("{w:.1} pt · {:.0}%", share * 100.0)); + ui.label(RichText::new(size).color(t.text_muted).size(11.0)); + }); + }); + let full = ui.available_width(); + ui.add(widgets::line(&mut column.pattern).desired_width(full).id_salt(("field table column", c))); + ui.add_space(4.0); + } + }; + if scrolls { + egui::ScrollArea::vertical().max_height(180.0).show(ui, column_fields); + } else { + column_fields(ui); + } + + ui.add_space(8.0); + // The names, and the edit Add fields applies, from the table as it now stands. + let laid = d.laid(info, form); + if let Ok(cells) = &laid.cells { + let names: Vec<&str> = cells.iter().map(|c| c.name.as_str()).collect(); + let shown = match names.as_slice() { + [] => String::new(), + [one] => (*one).to_owned(), + [first, second] => format!("{first}, {second}"), + [first, second, .., last] => format!("{first}, {second} … {last}"), + }; + ui.horizontal(|ui| { + ui.label(RichText::new(i18n::trn(i18n::current(), names.len() as u64, "{n} field", "{n} fields")).color(t.text_muted)); + ui.add(egui::Label::new(RichText::new(shown).font(theme::medium(12.0))).truncate()); + }); + } + if let Err(e) = &laid.edit { + ui.add(egui::Label::new(RichText::new(e.said(&i18n::words)).color(ERROR_RED).size(11.0)).wrap()); + } + + ui.add_space(14.0); + let mut choice = None; + ui.with_layout(Layout::right_to_left(Align::Center), |ui| { + if ui.add_enabled_ui(laid.edit.is_ok(), |ui| widgets::pill_button(ui, tl!("Add fields"), true)).inner.clicked() { + choice = Some(Choice::Add); + } + if widgets::pill_button(ui, tl!("Cancel"), false).clicked() { + choice = Some(Choice::Cancel); + } + }); + (choice, laid) +} + +/// "Column 2" for the column at 0-based `index`. +fn column_label(index: usize) -> String { + i18n::fmt(tl!("Column {n}"), &[("n", &index.saturating_add(1).to_string())]) +} + +/// A number with minus and plus buttons either side, held to `range`, moving by `step`. Whether it +/// changed. +fn stepper(ui: &mut egui::Ui, value: &mut N, range: RangeInclusive, step: f64, suffix: &str) -> bool { + let before = *value; + let (low, high) = (range.start().to_f64(), range.end().to_f64()); + ui.horizontal(|ui| { + if ui.button("-").clicked() { + *value = N::from_f64((value.to_f64() - step).max(low)); + } + ui.add(egui::DragValue::new(value).range(range).speed(step / 2.0).suffix(suffix)); + if ui.button("+").clicked() { + *value = N::from_f64((value.to_f64() + step).min(high)); + } + }); + *value != before +} + +/// The columns as a bar with a divider between each two, dragged (or moved with the arrow keys once +/// focused) to share the width differently. +fn column_bar(ui: &mut egui::Ui, d: &mut FieldTableDraft, t: &theme::Tokens) { + let (bar, _) = ui.allocate_exact_size(vec2(ui.available_width().max(100.0), 28.0), Sense::hover()); + let edges = |shares: &[f64]| -> Vec { + let mut x = bar.left(); + shares + .iter() + .map(|s| { + x += *s as f32 * bar.width(); + x + }) + .collect() + }; + let n = d.table.columns.len(); + // Wide enough to grab, never reaching past the middle of a column. + let reach = (bar.width() / n.max(1) as f32 / 2.0).clamp(2.0, 8.0); + let mut active = None; + let ends = edges(&d.shares()); + for (i, &x) in ends.iter().enumerate().take(n.saturating_sub(1)) { + let hit = Rect::from_min_max(pos2(x - reach, bar.top()), pos2(x + reach, bar.bottom())); + let resp = ui.interact(hit, ui.id().with(("field table divider", i)), Sense::click_and_drag()); + let share = d.shares().get(i).copied().unwrap_or_default(); + resp.widget_info(|| egui::WidgetInfo::slider(true, (share * 100.0).round(), column_label(i))); + let mut delta = f64::from(resp.drag_delta().x) / f64::from(bar.width()); + if resp.has_focus() { + ui.input_mut(|input| { + if input.consume_key(egui::Modifiers::NONE, egui::Key::ArrowLeft) { + delta -= 0.01; + } + if input.consume_key(egui::Modifiers::NONE, egui::Key::ArrowRight) { + delta += 0.01; + } + }); + } + if delta != 0.0 { + d.move_divider(i, delta); + } + if resp.hovered() || resp.dragged() { + ui.ctx().set_cursor_icon(egui::CursorIcon::ResizeHorizontal); + } + if resp.hovered() || resp.dragged() || resp.has_focus() { + active = Some(i); + } + } + + let painter = ui.painter_at(bar); + let shares = d.shares(); + let mut left = bar.left(); + for (c, (&right, share)) in edges(&shares).iter().zip(&shares).enumerate() { + // The last column ends at the bar's edge, whatever rounding says. + let right = if c + 1 == n { bar.right() } else { right.min(bar.right()) }; + let column = Rect::from_min_max(pos2(left, bar.top()), pos2(right, bar.bottom())); + painter.rect_filled(column, CornerRadius::ZERO, SELECT_BLUE.gamma_multiply(if c % 2 == 0 { 0.11 } else { 0.055 })); + if column.width() > 32.0 { + painter.text(column.center(), egui::Align2::CENTER_CENTER, format!("{:.0}%", share * 100.0), theme::medium(11.0), t.text); + } + left = right; + } + painter.rect_stroke(bar, CornerRadius::same(3), Stroke::new(1.0, t.border), egui::StrokeKind::Inside); + for (i, &x) in edges(&shares).iter().enumerate().take(n.saturating_sub(1)) { + let stroke = if active == Some(i) { Stroke::new(3.0, SELECT_BLUE) } else { Stroke::new(1.5, t.border) }; + painter.line_segment([pos2(x, bar.top() + 1.0), pos2(x, bar.bottom() - 1.0)], stroke); + } +} + +/// The table being set up, on its page: each cell with its name, or the table's outline in red +/// with the reason it can't be laid out. Only the cells in view are drawn, and only those with room +/// for it get their name: a table can have 5,000 cells. +pub(crate) fn paint_preview(painter: &egui::Painter, xf: &PageXform, page: usize, preview: &Preview) { + if preview.page != page { + return; + } + let zoom = (xf.rect.width() / xf.pw.max(1.0)).clamp(0.5, 3.0); + let font = theme::regular((9.0 * zoom).clamp(7.0, 13.0)); + let on_screen = |r: [f64; 4]| xf.view_rect([r[0] as f32, r[1] as f32, r[2] as f32, r[3] as f32]); + let outline = on_screen(preview.laid.table.rect); + let visible = painter.clip_rect(); + // A tag of white `text` in the middle of `area`, kept inside it. + let tag = |area: Rect, text: String, fill: Color32| { + let galley = painter.layout_no_wrap(text, font.clone(), Color32::WHITE); + let tag = Rect::from_center_size(area.center(), galley.size() + vec2(6.0, 2.0)); + let inside = painter.with_clip_rect(painter.clip_rect().intersect(area)); + inside.rect_filled(tag, CornerRadius::same(1), fill); + inside.galley(tag.min + vec2(3.0, 1.0), galley, Color32::WHITE); + }; + match &preview.laid.cells { + Ok(cells) => { + painter.rect_stroke(outline, CornerRadius::ZERO, Stroke::new(1.0, SELECT_BLUE.gamma_multiply(0.3)), egui::StrokeKind::Outside); + for cell in cells { + let r = on_screen(cell.rect); + if !visible.intersects(r) { + continue; + } + painter.rect_filled(r, CornerRadius::ZERO, crate::prepare::FIELD_FILL); + painter.rect_stroke(r, CornerRadius::ZERO, Stroke::new(1.5, SELECT_BLUE), egui::StrokeKind::Inside); + if r.height() >= font.size + 2.0 && r.width() >= 24.0 { + tag(r, cell.name.clone(), SELECT_BLUE); + } + } + } + Err(e) => { + painter.rect_stroke(outline, CornerRadius::ZERO, Stroke::new(1.5, ERROR_RED), egui::StrokeKind::Inside); + tag(outline, e.said(&i18n::words), ERROR_RED); + } + } +} + +#[cfg(test)] +mod tests { + use pdfcraft_engine::field_table::TableColumn; + + use super::*; + + fn draft(columns: usize) -> FieldTableDraft { + let mut d = FieldTableDraft::new(DocId(1), 0, [0.0, 0.0, 400.0, 200.0], &[], 0); + d.table.set_columns(columns, &d.base); + d + } + + /// Thirty even columns leave each pair less than twice the least share a divider keeps: the + /// two bounds crossed, and `f64::clamp` panicked on the first drag. + #[test] + fn dragging_a_divider_between_narrow_columns_never_panics() { + for columns in [2, 25, 26, 30, MAX_COLUMNS] { + for delta in [-1.0, -0.01, 0.0, 0.01, 1.0, f64::NAN, f64::INFINITY] { + let mut d = draft(columns); + let before: f64 = d.table.columns.iter().map(|c| c.width).sum(); + d.move_divider(0, delta); + d.move_divider(columns - 2, delta); + let after: f64 = d.table.columns.iter().map(|c| c.width).sum(); + assert!((after - before).abs() < 1e-9, "{columns} columns, {delta}: the total width stays"); + assert!(d.table.columns.iter().all(|c| c.width > 0.0), "{columns} columns, {delta}: no column vanishes"); + } + // And the divider still moves. + let mut d = draft(columns); + d.move_divider(0, -0.01); + assert!(d.table.columns[0].width < d.table.columns[1].width, "{columns} columns: {:?}", d.shares()); + } + } + + #[test] + fn a_divider_moves_only_the_two_columns_it_divides() { + let mut d = draft(3); + d.move_divider(0, 0.1); + let shares = d.shares(); + assert!((shares[0] - (1.0 / 3.0 + 0.1)).abs() < 1e-9 && (shares[1] - (1.0 / 3.0 - 0.1)).abs() < 1e-9, "{shares:?}"); + assert!((shares[2] - 1.0 / 3.0).abs() < 1e-9); + // Dragged past its neighbour, a column stops at the least share; past the end, nothing. + d.move_divider(0, 1.0); + assert!((d.shares()[1] - MIN_SHARE).abs() < 1e-9); + let before = d.clone(); + d.move_divider(2, 0.5); + assert_eq!(d, before); + d.distribute_evenly(); + assert!(d.table.columns.iter().all(|c| c.width == 1.0)); + } + + /// The window and the page share one layout of the table, worked out again only when the table + /// changes: at 100 × 50 cells, laying it out several times a frame was 25,000 names a frame. + #[test] + fn the_layout_is_worked_out_again_only_when_the_table_changes() { + let info = PageInfo { width: 400.0, height: 300.0, label: "1".into(), crop: [0.0, 0.0, 400.0, 300.0], rotation: 0 }; + let mut d = draft(2); + let first = d.laid(&info, &[]); + assert!(Arc::ptr_eq(&first, &d.laid(&info, &[])), "the same table, the same layout"); + assert!(Arc::ptr_eq(&first, &d.preview(&info, &[]).laid), "the page shares it"); + d.table.rows = 5; + let second = d.laid(&info, &[]); + assert!(!Arc::ptr_eq(&first, &second), "a changed table is laid out again"); + assert_eq!(second.cells.as_ref().map(Vec::len), Ok(10)); + assert!(second.edit.is_ok()); + } + + #[test] + fn added_columns_are_named_after_the_tables_base() { + let mut d = FieldTableDraft::new(DocId(1), 0, [0.0, 0.0, 400.0, 200.0], &[], 0); + d.table.set_columns(2, &d.base); + let patterns: Vec<_> = d.table.columns.iter().map(|c: &TableColumn| c.pattern.as_str()).collect(); + assert_eq!(patterns, ["Table1_N", "Table1_c2_N"]); + } +} diff --git a/crates/ui-egui/src/i18n/ar.tsv b/crates/ui-egui/src/i18n/ar.tsv index f9b499ab9..765bbba55 100644 --- a/crates/ui-egui/src/i18n/ar.tsv +++ b/crates/ui-egui/src/i18n/ar.tsv @@ -2771,3 +2771,23 @@ certificate Version الإصدار Larger thumbnails تكبير الصور المصغرة # Lines, connected lines and polygons reshaped by their ends and corners (M5). Reshape comment إعادة تشكيل التعليق + +# Prepare a form ▸ Text field table + Add text field table إضافة جدول حقول نصية + Text field table جدول حقول نصية + Text Field Table جدول حقول نصية + Columns الأعمدة + Row spacing المسافة بين الصفوف + Column spacing المسافة بين الأعمدة + First row number رقم الصف الأول + Column widths عروض الأعمدة + Distribute evenly توزيع بالتساوي + Field names أسماء الحقول + N or {row} is the row number, {col} the column number, {seq} the cell number. With none of them, the row number goes last. N أو {row} هو رقم الصف، و{col} رقم العمود، و{seq} رقم الخلية. إذا لم يوجد أي منها، يُضاف رقم الصف في النهاية. + Column {n} العمود {n} + Add fields إضافة الحقول + the rows are too short: make the area taller, or use fewer rows or less row spacing الصفوف قصيرة جدًا: اجعل المنطقة أطول، أو استخدم صفوفًا أقل أو مسافة أقل بين الصفوف + a column is too narrow: make the area wider, give the column more of it, or use fewer columns or less column spacing أحد الأعمدة ضيق جدًا: اجعل المنطقة أعرض، أو امنح العمود عرضًا أكبر، أو استخدم أعمدة أقل أو مسافة أقل بين الأعمدة + field names can't contain a period: {name} لا يمكن أن تحتوي أسماء الحقول على نقطة: {name} + two cells would both be named {name} ستحمل خليتان الاسم نفسه {name} + a field named {name} already exists يوجد حقل باسم {name} بالفعل diff --git a/crates/ui-egui/src/i18n/bg.tsv b/crates/ui-egui/src/i18n/bg.tsv index cc6cc5017..9bb377a93 100644 --- a/crates/ui-egui/src/i18n/bg.tsv +++ b/crates/ui-egui/src/i18n/bg.tsv @@ -2458,3 +2458,23 @@ animation effects Off Изключени Larger thumbnails По-големи миниатюри # Lines, connected lines and polygons reshaped by their ends and corners (M5). Reshape comment Промяна на формата на коментара + +# Prepare a form ▸ Text field table + Add text field table Добави таблица от текстови полета + Text field table Таблица от текстови полета + Text Field Table Таблица от текстови полета + Columns Колони + Row spacing Разстояние между редовете + Column spacing Разстояние между колоните + First row number Номер на първия ред + Column widths Ширини на колоните + Distribute evenly Разпредели равномерно + Field names Имена на полетата + N or {row} is the row number, {col} the column number, {seq} the cell number. With none of them, the row number goes last. N или {row} е номерът на реда, {col} – на колоната, {seq} – на клетката. Без тях номерът на реда се добавя накрая. + Column {n} Колона {n} + Add fields Добави полета + the rows are too short: make the area taller, or use fewer rows or less row spacing редовете са твърде ниски: направете областта по-висока или използвайте по-малко редове или по-малко разстояние между тях + a column is too narrow: make the area wider, give the column more of it, or use fewer columns or less column spacing колона е твърде тясна: направете областта по-широка, дайте ѝ повече място или използвайте по-малко колони или по-малко разстояние между тях + field names can't contain a period: {name} имената на полетата не може да съдържат точка: {name} + two cells would both be named {name} две клетки биха имали едно и също име {name} + a field named {name} already exists вече съществува поле с име {name} diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv index e70d27db4..4e736bbf4 100644 --- a/crates/ui-egui/src/i18n/cs.tsv +++ b/crates/ui-egui/src/i18n/cs.tsv @@ -2800,3 +2800,23 @@ certificate Version Verze The signature's signed attributes have no message digest, so the signature does not cover the document. Podepsané atributy podpisu neobsahují otisk zprávy, takže se podpis na dokument nevztahuje. The signature's signed attributes name a content type other than data, so it is not a signature of the document. Podepsané atributy podpisu uvádějí jiný typ obsahu než data, takže nejde o podpis dokumentu. Remove {standard} identification Odebrat identifikaci {standard} + +# Prepare a form ▸ Text field table + Add text field table Přidat tabulku textových polí + Text field table Tabulka textových polí + Text Field Table Tabulka textových polí + Columns Sloupce + Row spacing Mezera mezi řádky + Column spacing Mezera mezi sloupci + First row number Číslo prvního řádku + Column widths Šířky sloupců + Distribute evenly Rozložit rovnoměrně + Field names Názvy polí + N or {row} is the row number, {col} the column number, {seq} the cell number. With none of them, the row number goes last. N nebo {row} je číslo řádku, {col} číslo sloupce a {seq} číslo buňky. Bez nich se číslo řádku přidá na konec. + Column {n} Sloupec {n} + Add fields Přidat pole + the rows are too short: make the area taller, or use fewer rows or less row spacing řádky jsou příliš nízké: zvětšete výšku oblasti nebo použijte méně řádků či menší mezeru mezi řádky + a column is too narrow: make the area wider, give the column more of it, or use fewer columns or less column spacing sloupec je příliš úzký: rozšiřte oblast, dejte sloupci více místa nebo použijte méně sloupců či menší mezeru mezi sloupci + field names can't contain a period: {name} názvy polí nesmějí obsahovat tečku: {name} + two cells would both be named {name} dvě buňky by se jmenovaly {name} + a field named {name} already exists pole s názvem {name} už existuje diff --git a/crates/ui-egui/src/i18n/de.tsv b/crates/ui-egui/src/i18n/de.tsv index 02f010e24..76366d452 100644 --- a/crates/ui-egui/src/i18n/de.tsv +++ b/crates/ui-egui/src/i18n/de.tsv @@ -2496,3 +2496,23 @@ animation effects Off Aus Larger thumbnails Größere Miniaturen # Lines, connected lines and polygons reshaped by their ends and corners (M5). Reshape comment Kommentar umformen + +# Prepare a form ▸ Text field table + Add text field table Textfeldtabelle hinzufügen + Text field table Textfeldtabelle + Text Field Table Textfeldtabelle + Columns Spalten + Row spacing Zeilenabstand + Column spacing Spaltenabstand + First row number Erste Zeilennummer + Column widths Spaltenbreiten + Distribute evenly Gleichmäßig verteilen + Field names Feldnamen + N or {row} is the row number, {col} the column number, {seq} the cell number. With none of them, the row number goes last. N oder {row} ist die Zeilennummer, {col} die Spaltennummer, {seq} die Zellennummer. Ohne eines davon steht die Zeilennummer am Ende. + Column {n} Spalte {n} + Add fields Felder hinzufügen + the rows are too short: make the area taller, or use fewer rows or less row spacing die Zeilen sind zu niedrig: Mach den Bereich höher oder verwende weniger Zeilen oder weniger Zeilenabstand + a column is too narrow: make the area wider, give the column more of it, or use fewer columns or less column spacing eine Spalte ist zu schmal: Mach den Bereich breiter, gib der Spalte mehr Breite oder verwende weniger Spalten oder weniger Spaltenabstand + field names can't contain a period: {name} Feldnamen dürfen keinen Punkt enthalten: {name} + two cells would both be named {name} zwei Zellen hätten beide den Namen {name} + a field named {name} already exists ein Feld namens {name} ist bereits vorhanden diff --git a/crates/ui-egui/src/i18n/el.tsv b/crates/ui-egui/src/i18n/el.tsv index e73436930..18464664a 100644 --- a/crates/ui-egui/src/i18n/el.tsv +++ b/crates/ui-egui/src/i18n/el.tsv @@ -2501,3 +2501,23 @@ animation effects Off Ανενεργά Larger thumbnails Μεγαλύτερες μικρογραφίες # Lines, connected lines and polygons reshaped by their ends and corners (M5). Reshape comment Αλλαγή σχήματος σχολίου + +# Prepare a form ▸ Text field table + Add text field table Προσθήκη πίνακα πεδίων κειμένου + Text field table Πίνακας πεδίων κειμένου + Text Field Table Πίνακας πεδίων κειμένου + Columns Στήλες + Row spacing Απόσταση γραμμών + Column spacing Απόσταση στηλών + First row number Αριθμός πρώτης γραμμής + Column widths Πλάτη στηλών + Distribute evenly Ομοιόμορφη κατανομή + Field names Ονόματα πεδίων + N or {row} is the row number, {col} the column number, {seq} the cell number. With none of them, the row number goes last. Το N ή το {row} είναι ο αριθμός γραμμής, το {col} ο αριθμός στήλης και το {seq} ο αριθμός κελιού. Χωρίς κανένα από αυτά, ο αριθμός γραμμής μπαίνει στο τέλος. + Column {n} Στήλη {n} + Add fields Προσθήκη πεδίων + the rows are too short: make the area taller, or use fewer rows or less row spacing οι γραμμές είναι πολύ χαμηλές: κάντε την περιοχή ψηλότερη ή χρησιμοποιήστε λιγότερες γραμμές ή μικρότερη απόσταση γραμμών + a column is too narrow: make the area wider, give the column more of it, or use fewer columns or less column spacing μια στήλη είναι πολύ στενή: κάντε την περιοχή φαρδύτερη, δώστε στη στήλη περισσότερο πλάτος ή χρησιμοποιήστε λιγότερες στήλες ή μικρότερη απόσταση στηλών + field names can't contain a period: {name} τα ονόματα πεδίων δεν μπορούν να περιέχουν τελεία: {name} + two cells would both be named {name} δύο κελιά θα είχαν το ίδιο όνομα {name} + a field named {name} already exists υπάρχει ήδη πεδίο με όνομα {name} diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv index 4e0e08e5e..db00c69dd 100644 --- a/crates/ui-egui/src/i18n/es.tsv +++ b/crates/ui-egui/src/i18n/es.tsv @@ -2812,3 +2812,27 @@ theme Appearance Aspecto Zoom in / out (also pinch or ⌘-scroll) Acercar / alejar (también con pellizco o ⌘ + desplazamiento) Rotate view clockwise (⇧⌘+) Girar la vista a la derecha (⇧⌘+) Save (M4) Guardar (M4) + +# Prepare a form ▸ Text field table + Add text field table Añadir tabla de campos de texto + Text field table Tabla de campos de texto + Text Field Table Tabla de campos de texto + Columns Columnas + Row spacing Espacio entre filas + Column spacing Espacio entre columnas + First row number Número de la primera fila + Column widths Anchos de columna + Distribute evenly Repartir por igual + Field names Nombres de los campos + N or {row} is the row number, {col} the column number, {seq} the cell number. With none of them, the row number goes last. N o {row} es el número de fila, {col} el de columna y {seq} el de celda. Si no hay ninguno, el número de fila va al final. + Column {n} Columna {n} + Add fields Añadir campos + a table has 1 to {max} rows una tabla tiene de 1 a {max} filas + a table has 1 to {max} columns una tabla tiene de 1 a {max} columnas + the first row number can be at most {max} el número de la primera fila puede ser como mucho {max} + the area, the spacing and the column widths must be numbers, the spacing 0 or more and each width more than 0 el área, el espaciado y los anchos de columna deben ser números, el espaciado 0 o más y cada ancho mayor que 0 + the rows are too short: make the area taller, or use fewer rows or less row spacing las filas son demasiado bajas: haz el área más alta o usa menos filas o menos espacio entre filas + a column is too narrow: make the area wider, give the column more of it, or use fewer columns or less column spacing una columna es demasiado estrecha: haz el área más ancha, dale más anchura a la columna o usa menos columnas o menos espacio entre columnas + field names can't contain a period: {name} los nombres de campo no pueden contener un punto: {name} + two cells would both be named {name} dos celdas se llamarían {name} + a field named {name} already exists ya existe un campo llamado {name} diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv index 346cc57dc..ec6e59e56 100644 --- a/crates/ui-egui/src/i18n/fr.tsv +++ b/crates/ui-egui/src/i18n/fr.tsv @@ -2761,3 +2761,23 @@ certificate Valid to Valide jusqu'au certificate Validity ends Fin de validité certificate Validity starts Début de validité certificate Version Version + +# Prepare a form ▸ Text field table + Add text field table Ajouter un tableau de champs de texte + Text field table Tableau de champs de texte + Text Field Table Tableau de champs de texte + Columns Colonnes + Row spacing Espacement des lignes + Column spacing Espacement des colonnes + First row number Numéro de la première ligne + Column widths Largeurs des colonnes + Distribute evenly Répartir uniformément + Field names Noms des champs + N or {row} is the row number, {col} the column number, {seq} the cell number. With none of them, the row number goes last. N ou {row} est le numéro de ligne, {col} le numéro de colonne, {seq} le numéro de cellule. Sans aucun d'eux, le numéro de ligne est ajouté à la fin. + Column {n} Colonne {n} + Add fields Ajouter les champs + the rows are too short: make the area taller, or use fewer rows or less row spacing les lignes sont trop basses : agrandissez la zone en hauteur, ou utilisez moins de lignes ou un espacement des lignes plus petit + a column is too narrow: make the area wider, give the column more of it, or use fewer columns or less column spacing une colonne est trop étroite : élargissez la zone, donnez-lui plus de largeur, ou utilisez moins de colonnes ou un espacement des colonnes plus petit + field names can't contain a period: {name} les noms de champ ne peuvent pas contenir de point : {name} + two cells would both be named {name} deux cellules porteraient le nom {name} + a field named {name} already exists un champ nommé {name} existe déjà diff --git a/crates/ui-egui/src/i18n/hu.tsv b/crates/ui-egui/src/i18n/hu.tsv index 0d72ae297..f1723629c 100644 --- a/crates/ui-egui/src/i18n/hu.tsv +++ b/crates/ui-egui/src/i18n/hu.tsv @@ -2494,3 +2494,23 @@ animation effects Off Ki Larger thumbnails Nagyobb bélyegképek # Lines, connected lines and polygons reshaped by their ends and corners (M5). Reshape comment Megjegyzés alakjának módosítása + +# Prepare a form ▸ Text field table + Add text field table Szövegmező-táblázat hozzáadása + Text field table Szövegmező-táblázat + Text Field Table Szövegmező-táblázat + Columns Oszlopok + Row spacing Sorköz + Column spacing Oszlopköz + First row number Első sor száma + Column widths Oszlopszélességek + Distribute evenly Egyenletes elosztás + Field names Mezőnevek + N or {row} is the row number, {col} the column number, {seq} the cell number. With none of them, the row number goes last. Az N vagy a {row} a sor száma, a {col} az oszlopé, a {seq} a cellaé. Ha egyik sincs, a sor száma a végére kerül. + Column {n} {n}. oszlop + Add fields Mezők hozzáadása + the rows are too short: make the area taller, or use fewer rows or less row spacing a sorok túl alacsonyak: növeld a terület magasságát, vagy használj kevesebb sort vagy kisebb sorközt + a column is too narrow: make the area wider, give the column more of it, or use fewer columns or less column spacing egy oszlop túl keskeny: szélesítsd a területet, adj több helyet az oszlopnak, vagy használj kevesebb oszlopot vagy kisebb oszlopközt + field names can't contain a period: {name} a mezőnevek nem tartalmazhatnak pontot: {name} + two cells would both be named {name} két cella neve is {name} lenne + a field named {name} already exists már van {name} nevű mező diff --git a/crates/ui-egui/src/i18n/it.tsv b/crates/ui-egui/src/i18n/it.tsv index 25da10e6b..0dc6bb087 100644 --- a/crates/ui-egui/src/i18n/it.tsv +++ b/crates/ui-egui/src/i18n/it.tsv @@ -2723,3 +2723,27 @@ certificate SHA-256 digest Impronta SHA-256 System Sistema Zoom in / out (also pinch or ⌘-scroll) Ingrandisci / riduci (anche con il pizzico o ⌘+scorrimento) of di + +# Prepare a form ▸ Text field table + Add text field table Aggiungi tabella di campi di testo + Text field table Tabella di campi di testo + Text Field Table Tabella di campi di testo + Columns Colonne + Row spacing Spaziatura tra le righe + Column spacing Spaziatura tra le colonne + First row number Numero della prima riga + Column widths Larghezze delle colonne + Distribute evenly Distribuisci uniformemente + Field names Nomi dei campi + N or {row} is the row number, {col} the column number, {seq} the cell number. With none of them, the row number goes last. N o {row} è il numero di riga, {col} quello di colonna, {seq} quello di cella. Senza nessuno di essi, il numero di riga va alla fine. + Column {n} Colonna {n} + Add fields Aggiungi campi + a table has 1 to {max} rows una tabella ha da 1 a {max} righe + a table has 1 to {max} columns una tabella ha da 1 a {max} colonne + the first row number can be at most {max} il numero della prima riga può essere al massimo {max} + the area, the spacing and the column widths must be numbers, the spacing 0 or more and each width more than 0 l'area, la spaziatura e le larghezze delle colonne devono essere numeri, la spaziatura 0 o più e ogni larghezza maggiore di 0 + the rows are too short: make the area taller, or use fewer rows or less row spacing le righe sono troppo basse: rendi l'area più alta oppure usa meno righe o meno spaziatura tra le righe + a column is too narrow: make the area wider, give the column more of it, or use fewer columns or less column spacing una colonna è troppo stretta: rendi l'area più larga, dai più spazio alla colonna oppure usa meno colonne o meno spaziatura tra le colonne + field names can't contain a period: {name} i nomi dei campi non possono contenere un punto: {name} + two cells would both be named {name} due celle si chiamerebbero entrambe {name} + a field named {name} already exists esiste già un campo chiamato {name} diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv index acc0f9d67..963e8d364 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -2495,3 +2495,23 @@ animation effects Off オフ Larger thumbnails サムネイルを大きく # Lines, connected lines and polygons reshaped by their ends and corners (M5). Reshape comment コメントの形状を変更 + +# Prepare a form ▸ Text field table + Add text field table テキストフィールドの表を追加 + Text field table テキストフィールドの表 + Text Field Table テキストフィールドの表 + Columns 列 + Row spacing 行の間隔 + Column spacing 列の間隔 + First row number 最初の行番号 + Column widths 列の幅 + Distribute evenly 均等に配分 + Field names フィールド名 + N or {row} is the row number, {col} the column number, {seq} the cell number. With none of them, the row number goes last. N または {row} は行番号、{col} は列番号、{seq} はセル番号です。どれもない場合は、行番号が末尾に付きます。 + Column {n} 列 {n} + Add fields フィールドを追加 + the rows are too short: make the area taller, or use fewer rows or less row spacing 行の高さが足りません: 範囲を高くするか、行数または行の間隔を減らしてください + a column is too narrow: make the area wider, give the column more of it, or use fewer columns or less column spacing 列の幅が足りません: 範囲を広げるか、その列の幅を増やすか、列数または列の間隔を減らしてください + field names can't contain a period: {name} フィールド名にピリオドは使えません: {name} + two cells would both be named {name} 2 つのセルが同じ名前 {name} になります + a field named {name} already exists {name} という名前のフィールドは既にあります diff --git a/crates/ui-egui/src/i18n/kk.tsv b/crates/ui-egui/src/i18n/kk.tsv index 07861a23e..311626f34 100644 --- a/crates/ui-egui/src/i18n/kk.tsv +++ b/crates/ui-egui/src/i18n/kk.tsv @@ -2493,3 +2493,23 @@ animation effects Off Өшірулі Larger thumbnails Үлкенірек нобайлар # Lines, connected lines and polygons reshaped by their ends and corners (M5). Reshape comment Пікір пішінін өзгерту + +# Prepare a form ▸ Text field table + Add text field table Мәтін өрістерінің кестесін қосу + Text field table Мәтін өрістерінің кестесі + Text Field Table Мәтін өрістерінің кестесі + Columns Бағандар + Row spacing Жолдар аралығы + Column spacing Бағандар аралығы + First row number Бірінші жол нөмірі + Column widths Баған ендері + Distribute evenly Біркелкі бөлу + Field names Өріс атаулары + N or {row} is the row number, {col} the column number, {seq} the cell number. With none of them, the row number goes last. N немесе {row} — жол нөмірі, {col} — баған нөмірі, {seq} — ұяшық нөмірі. Олардың ешқайсысы болмаса, жол нөмірі соңына қосылады. + Column {n} {n}-баған + Add fields Өрістерді қосу + the rows are too short: make the area taller, or use fewer rows or less row spacing жолдар тым аласа: аймақты биіктетіңіз немесе жолдар санын не жолдар аралығын азайтыңыз + a column is too narrow: make the area wider, give the column more of it, or use fewer columns or less column spacing баған тым тар: аймақты кеңейтіңіз, бағанға көбірек ен беріңіз немесе бағандар санын не бағандар аралығын азайтыңыз + field names can't contain a period: {name} өріс атауларында нүкте болмауы керек: {name} + two cells would both be named {name} екі ұяшықтың да атауы {name} болар еді + a field named {name} already exists {name} атты өріс бар diff --git a/crates/ui-egui/src/i18n/lt.tsv b/crates/ui-egui/src/i18n/lt.tsv index 2962d83d5..0e542211f 100644 --- a/crates/ui-egui/src/i18n/lt.tsv +++ b/crates/ui-egui/src/i18n/lt.tsv @@ -2750,3 +2750,27 @@ theme Appearance Išvaizda Larger thumbnails Didesnės miniatiūros # Lines, connected lines and polygons reshaped by their ends and corners (M5). Reshape comment Keisti komentaro formą + +# Prepare a form ▸ Text field table + Add text field table Pridėti teksto laukų lentelę + Text field table Teksto laukų lentelė + Text Field Table Teksto laukų lentelė + Columns Stulpeliai + Row spacing Tarpas tarp eilučių + Column spacing Tarpas tarp stulpelių + First row number Pirmosios eilutės numeris + Column widths Stulpelių pločiai + Distribute evenly Paskirstyti tolygiai + Field names Laukų pavadinimai + N or {row} is the row number, {col} the column number, {seq} the cell number. With none of them, the row number goes last. N arba {row} – eilutės numeris, {col} – stulpelio numeris, {seq} – langelio numeris. Jei nė vieno nėra, eilutės numeris pridedamas gale. + Column {n} Stulpelis {n} + Add fields Pridėti laukus + the rows are too short: make the area taller, or use fewer rows or less row spacing eilutės per žemos: padidinkite srities aukštį arba naudokite mažiau eilučių ar mažesnį tarpą tarp jų + a column is too narrow: make the area wider, give the column more of it, or use fewer columns or less column spacing stulpelis per siauras: praplatinkite sritį, skirkite stulpeliui daugiau pločio arba naudokite mažiau stulpelių ar mažesnį tarpą tarp jų + field names can't contain a period: {name} laukų pavadinimuose negali būti taško: {name} + two cells would both be named {name} du langeliai būtų pavadinti {name} + a field named {name} already exists laukas pavadinimu {name} jau yra + a table has 1 to {max} rows lentelėje gali būti nuo 1 iki {max} eilučių + a table has 1 to {max} columns lentelėje gali būti nuo 1 iki {max} stulpelių + the first row number can be at most {max} pirmosios eilutės numeris gali būti ne didesnis nei {max} + the area, the spacing and the column widths must be numbers, the spacing 0 or more and each width more than 0 sritis, tarpai ir stulpelių pločiai turi būti skaičiai, tarpai – 0 arba daugiau, o kiekvienas plotis – daugiau nei 0 diff --git a/crates/ui-egui/src/i18n/mod.rs b/crates/ui-egui/src/i18n/mod.rs index 936f73623..dff8efc3d 100644 --- a/crates/ui-egui/src/i18n/mod.rs +++ b/crates/ui-egui/src/i18n/mod.rs @@ -159,7 +159,7 @@ fn plural_sl(n: u64) -> usize { } /// The registry. English first: it is the fallback and the source language. -pub static LANGUAGES: [LangInfo; 33] = [ +pub static LANGUAGES: [LangInfo; 34] = [ 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`). @@ -250,6 +250,9 @@ pub static LANGUAGES: [LangInfo; 33] = [ // Korean; every `ko-*` locale (`ko-KR`, `ko_KR.UTF-8`) resolves here. CLDR `ko` counts have // one form ("other"), so `@plural` entries list a single form. LangInfo { code: "ko", name: "한국어", source: include_str!("ko.tsv"), plural: plural_none, rtl: false, catalog: OnceLock::new() }, + // Norwegian Bokmål; every `nb-*` locale resolves here, and so do general Norwegian (`no-*`) and + // Nynorsk (`nn-*`), which have no catalog of their own (see `candidates`). + LangInfo { code: "nb", name: "Norsk bokmål", source: include_str!("nb.tsv"), plural: plural_one_other, rtl: false, catalog: OnceLock::new() }, ]; impl LangInfo { @@ -366,6 +369,11 @@ fn candidates(tag: &str) -> Vec { // The only Portuguese catalog is Brazilian; other regions use it rather than English. out.insert(out.len().saturating_sub(1), "pt-br".to_string()); } + if matches!(primary, "no" | "nn") { + // The only Norwegian catalog is Bokmål. `no` (Norwegian in general) and Nynorsk readers use + // it rather than English; last, so a catalog of their own would win. + out.push("nb".to_string()); + } out } @@ -730,6 +738,9 @@ mod tests { assert_eq!(candidates("zh-CN"), ["zh-cn", "zh-hans", "zh"]); assert_eq!(candidates("zh-Hant-HK"), ["zh-hant-hk", "zh-hant", "zh"]); assert_eq!(candidates("lo_LA.UTF-8"), ["lo-la", "lo"]); + assert_eq!(candidates("nb_NO.UTF-8"), ["nb-no", "nb"]); + assert_eq!(candidates("no_NO.UTF-8"), ["no-no", "no", "nb"]); + assert_eq!(candidates("nn-NO"), ["nn-no", "nn", "nb"]); } #[test] @@ -1422,7 +1433,7 @@ mod tests { /// Spanish says everything the layers below word for themselves: what the accessibility /// checker finds and what its saved report calls things, what the PDF/A verifier reports, what /// a signature's details say and the changes they name, what can be wrong with the pages to - /// print, and what an edit is called in the history. + /// print or with a table of text fields, and what an edit is called in the history. /// /// These lists are the whole point of the engine handing the wording over apart from the /// document's own words: a catalog that covers them says those sentences in its own language. @@ -1430,7 +1441,7 @@ mod tests { #[test] fn spanish_covers_what_the_engine_says() { let es = Lang::from_code("es").expect("es registered"); - let lists: [(&str, &[&str]); 8] = [ + let lists: [(&str, &[&str]); 9] = [ ("accessibility finding", pdfcraft_engine::a11y::WORDINGS), ("accessibility report", pdfcraft_engine::a11y::REPORT_WORDS), ("PDF/A problem", pdfcraft_engine::pdfa::WORDINGS), @@ -1439,6 +1450,7 @@ mod tests { ("pages to print", pdfcraft_engine::print::WORDINGS), ("edit label", pdfcraft_engine::EDIT_LABELS), ("batch label", pdfcraft_engine::BATCH_LABELS), + ("text field table mistake", pdfcraft_engine::field_table::WORDINGS), ]; for (what, list) in lists { assert!(!list.is_empty(), "no {what} wordings to cover"); @@ -1454,11 +1466,12 @@ mod tests { #[test] fn spanish_keeps_the_holes_in_what_the_engine_says() { let es = Lang::from_code("es").expect("es registered"); - let lists: [&[&str]; 4] = [ + let lists: [&[&str]; 5] = [ pdfcraft_engine::a11y::WORDINGS, pdfcraft_engine::pdfa::WORDINGS, pdfcraft_engine::sign::pdf::WORDINGS, pdfcraft_engine::print::WORDINGS, + pdfcraft_engine::field_table::WORDINGS, ]; for wording in lists.into_iter().flatten() { let said = tr(es, wording); @@ -2493,6 +2506,29 @@ mod tests { assert!(restored.set_option("language", "xx").is_err()); } + #[test] + fn norwegian_bokmal_is_registered_and_persists() { + let nb = Lang::from_code("nb").expect("nb registered"); + assert_eq!(nb.name(), "Norsk bokmål"); + assert_eq!(normalize_pref("NB"), Some("nb")); + assert_eq!(normalize_pref("no"), None, "only exact codes are preferences"); + // Current systems say `nb`; older ones and some Linux setups say `no`. Nynorsk is `nn`. + for tag in ["nb", "nb-NO", "nb_NO.UTF-8", "no", "no_NO.UTF-8", "NO-no", "nn", "nn_NO.UTF-8"] { + assert_eq!(lang_from_tag(tag), Some(nb), "{tag}"); + } + assert_eq!(first_supported("(\n \"nb-NO\",\n \"en-US\"\n)\n"), Some(nb)); + assert_eq!(first_supported("nn-NO\r\nen-US\r\n"), Some(nb)); + assert_eq!((0..=3).map(|n| (nb.0.plural)(n)).collect::>(), [1, 0, 1, 1]); + assert_eq!(tr(nb, "Allow"), "Tillat"); + assert_eq!(tr_ctx(nb, "key", "Space"), "Mellomrom"); + let mut app = crate::PdfCraftApp::default(); + app.set_option("language", "NB").unwrap(); + assert_eq!(app.language, "nb"); + let mut restored = crate::PdfCraftApp::default(); + restored.restore(&app.persist()); + assert_eq!(restored.language, "nb"); + } + #[test] fn lithuanian_covers_commands_and_catalogue() { let lt = Lang::from_code("lt").expect("lt registered"); diff --git a/crates/ui-egui/src/i18n/nb.tsv b/crates/ui-egui/src/i18n/nb.tsv new file mode 100644 index 000000000..d0b542f7b --- /dev/null +++ b/crates/ui-egui/src/i18n/nb.tsv @@ -0,0 +1,2692 @@ +# Norwegian Bokmål (nb) catalog. Format: see the header of ja.tsv +# (context source translation; @id, @plural and other contexts; escapes \n \t \\). +# Plural forms (see `plural_one_other` in i18n/mod.rs): one (1) | other (0, 2, 3 ...). +# Clean-room: every entry was written from the plain meaning of the English label using ordinary +# Norwegian 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. + + Allow Tillat + Cancel Avbryt + Only allow this if you trust the source document. Tillat dette bare hvis du stoler på kildedokumentet. + Open this local PDF? Åpne denne lokale PDF-filen? + This document is trying to open a local file: Dette dokumentet prøver å åpne en lokal fil: + + Menu Meny + Edit Rediger + Pages Sider + View Vis + Help Hjelp + Zoom Zoom + Actual size Faktisk størrelse + Zoom to page level Zoom til sidenivå + Fit to width Tilpass til bredde + Display theme Visningstema + Side panels Sidepaneler + Preferences Innstillinger + Preferences… Innstillinger… + Interface language Grensesnittspråk + Interface size Grensesnittstørrelse + Auto ({percent}%) Auto ({percent}%) + Identity Identitet + Name on new comments Navn på nye kommentarer + Open… Åpne… + Open Recent Åpne nylige + Clear Recent Files Tøm nylige filer + Remove from Recent Fjern fra nylige + No recent files Ingen nylige filer + New blank PDF Ny tom PDF + Create PDF from file… Opprett PDF fra fil… + Create PDF from images… Opprett PDF fra bilder… + Create PDF from multiple files… Opprett PDF fra flere filer… + Create separate PDFs… Opprett separate PDF-er… + Create PDF from clipboard Opprett PDF fra utklippstavlen + Combine files… Slå sammen filer… + Save Lagre + Save as… Lagre som… + Close file Lukk fil + Close all Lukk alle + Revert Tilbakestill til lagret + Print… Skriv ut… + Document properties… Dokumentegenskaper… + Find… Finn… + Advanced search… Avansert søk… + Copy Kopier + Cut Klipp ut + Paste Lim inn + Delete Slett + Select all Merk alt + OK OK + Apply Bruk + Close Lukk + Back Tilbake + Next Neste + Previous Forrige + Name Navn + File name Filnavn + Location Plassering + Browse… Bla gjennom… + Choose… Velg… + Search Søk + Clear Tøm + Refresh Oppdater + Loading… Laster… + Save changes? Lagre endringer? + Don't save Ikke lagre + Page Side + Page {n} Side {n} + of av + Fit one full page Tilpass én hel side + Single page Enkeltside + Two-page view Tosidevisning + Continuous scrolling Kontinuerlig rulling + Page display Sidevisning + Default page display Standard sidevisning + Default zoom Standard zoom + Full screen mode Fullskjermmodus + Read mode Lesemodus + Comments Kommentarer + Comment Kommentar + Add comment Legg til kommentar + Delete comment Slett kommentar + Reply Svar + Highlight Uthev + Underline Understrek + Strikethrough Gjennomstreking + Text Tekst + Font Skrift + Size Størrelse + Color Farge + Opacity Gjennomsiktighet + Width Bredde + Height Høyde + Rotation Rotasjon + More Mer + Settings Innstillinger + General Generelt + Language Språk + Appearance Utseende + Theme Tema + Light Lyst + Dark Mørkt + System System + Open file Åpne fil + Save file Lagre fil + Export Eksporter + Import Importer + Export to image… Eksporter til bilde… + Export to text… Eksporter til tekst… + Export to Word… Eksporter til Word… + Export to HTML… Eksporter til HTML… + Export to RTF… Eksporter til RTF… + Reduce file size… Reduser filstørrelse… + Optimize PDF… Optimaliser PDF… + Protect using password… Beskytt med passord… + Remove security Fjern sikkerhet + Security properties… Sikkerhetsegenskaper… + Organize pages Organiser sider + Rotate pages… Roter sider… + Delete pages Slett sider + Insert blank page Sett inn tom side + Insert pages from file… Sett inn sider fra fil… + Replace pages… Erstatt sider… + Extract pages… Trekk ut sider… + Split document… Del opp dokument… + Number pages… Nummerer sider… + Bookmarks Bokmerker + New bookmark Nytt bokmerke + Forms Skjemaer + Form fields Skjemafelt + Signatures Signaturer + Sign Signer + Fill & Sign Fyll ut og signer + Accessibility Tilgjengelighet + Help… Hjelp… + Check for updates… Se etter oppdateringer… + About PdfCraft Om PdfCraft + Automatically Automatisk + Never Aldri + Always Alltid + Yes Ja + No Nei + On På + Off Av + Enabled Slått på + Disabled Slått av + Required Påkrevd + Optional Valgfritt + None Ingen + Unknown Ukjent + Error Feil + Warning Advarsel + Information Informasjon + Success Vellykket + Failed Mislyktes + File Fil + Folder Mappe + Choose a file… Velg en fil… + Choose a folder… Velg en mappe… + Open folder Åpne mappe + Create folder Opprett mappe + Remove Fjern + Rename Gi nytt navn + Duplicate Dupliser + Move Flytt + Copy to clipboard Kopier til utklippstavlen + Copied to clipboard Kopiert til utklippstavlen + Undo Angre + Redo Gjør om + Keyboard shortcuts Hurtigtaster + Find tools and commands… Finn verktøy og kommandoer… + Take a snapshot Ta et øyeblikksbilde + Organize Organiser + Comment and review Kommenter og gjennomgå + Prepare a form Lag et skjema + Protect Beskytt + Print Skriv ut + Create Opprett + Convert Konverter + Optimize Optimaliser + Measure Mål + Redact Sladd + Search results Søkeresultater + No results Ingen resultater + Page {n} of {total} Side {n} av {total} + {n} pages selected {n} sider valgt + · not fixable here · kan ikke rettes her + (page {p}) (side {p}) + (searching… {s} of {p} pages) (søker… {s} av {p} sider) + ({n} at {dpi} dpi, the largest size they can be drawn at) ({n} ved {dpi} dpi, den største størrelsen de kan tegnes i) + ({s} not exported: {first}) ({s} ikke eksportert: {first}) + on page {p} på side {p} + · password-protected · passordbeskyttet + (none) (ingen) + , Expires: , Utløper: + 1 difference found 1 forskjell funnet + 1 file 1 fil + 1 file done 1 fil ferdig + 1 file needs attention 1 fil trenger oppmerksomhet + 1 instance 1 forekomst + 1 issue 1 problem + 1 mark 1 markering + 1 page 1 side + 1 page selected 1 side valgt + 1 page selected. 1 side valgt. + 180 degrees 180 grader + 9 digit, A letter, O letter or digit, X any character 9 siffer, A bokstav, O bokstav eller siffer, X hvilket som helst tegn + ; 1 page already had text ; 1 side hadde allerede tekst + ; failed: {list} ; mislyktes: {list} + ; {n} pages already had text ; {n} sider hadde allerede tekst + ? of {n} ? av {n} + A clean-room, open-source PDF application written in Rust. MIT OR Apache-2.0. Et uavhengig utviklet PDF-program med åpen kildekode, skrevet i Rust. MIT OR Apache-2.0. + Accessibility Checker Tilgjengelighetskontroll + Accessibility Checker Options Alternativer for tilgjengelighetskontroll + Acrobat 3.0 and later (40-bit RC4) Acrobat 3.0 og nyere (40-bits RC4) + Acrobat 5.0 and later (128-bit RC4) Acrobat 5.0 og nyere (128-bits RC4) + Acrobat 7.0 and later (128-bit AES) Acrobat 7.0 og nyere (128-bits AES) + Acrobat X and later (256-bit AES) Acrobat X og nyere (256-bits AES) + Action name: Handlingsnavn: + Actions Handlinger + Active Aktiv + Actual Size Faktisk størrelse + Actual text Faktisk tekst + Add Legg til + Add Step Legg til trinn + Add a comment Legg til en kommentar + Add a reply Legg til et svar + Add a sticky note here Legg til en notatlapp her + Add an Action Legg til en handling + Add at least two files Legg til minst to filer + Add files… Legg til filer… + Add folder Legg til mappe + Add folder and subfolders Legg til mappe og undermapper + Add folder and subfolders… Legg til mappe og undermapper… + Add folder… Legg til mappe… + Add open documents Legg til åpne dokumenter + Add the choices people pick from. Legg til valgene folk kan velge mellom. + Add the files to combine Legg til filene som skal slås sammen + Add the steps this action runs, in order. Legg til trinnene denne handlingen kjører, i rekkefølge. + Add to Trusted Certificates Legg til i klarerte sertifikater + Add to trusted certificates Legg til i klarerte sertifikater + Added 1 comment La til 1 kommentar + Added more than once Lagt til mer enn én gang + Added {n} comments La til {n} kommentarer + Adds invisible text over each word; the page image is not changed Legger usynlig tekst over hvert ord; sidebildet endres ikke + Advanced Avansert + Advanced Options {chevron} Avanserte alternativer {chevron} + Align Juster + Align left Venstrejuster + Align right Høyrejuster + Alignment: Justering: + All Alle + All document contents Alt dokumentinnhold + All document contents except metadata Alt dokumentinnhold unntatt metadata + All open documents Alle åpne dokumenter + All pages Alle sider + All pages in document Alle sider i dokumentet + All pages in range Alle sider i området + Allow Rich Text Formatting Tillat rik tekstformatering + Allow user to enter custom text Tillat brukeren å skrive inn egen tekst + Also recognize pages that already have text Gjenkjenn også sider som allerede har tekst + Alternate text Alternativ tekst + An action with this name already exists. En handling med dette navnet finnes allerede. + An open-source PDF workbench — local, private, and scriptable. En PDF-arbeidsbenk med åpen kildekode – lokal, privat og skriptbar. + Angle Vinkel + Animation effects Animasjonseffekter + Annotations, form fill-in, and digital signatures Merknader, skjemautfylling og digitale signaturer + Any Alle + Any except extracting pages Alle unntatt uttrekking av sider + Anyone opening the document will need this password. It's applied when you save. Alle som åpner dokumentet, trenger dette passordet. Det tas i bruk når du lagrer. + Appear behind page Vis bak siden + Appear on top of page Vis over siden + Appearance mode Utseendemodus + Appearance settings… Utseendeinnstillinger… + Applies to 1 page Gjelder 1 side + Applies to {n} pages Gjelder {n} sider + Apply redactions Bruk sladdinger + Apply scale Bruk skala + Asks GitHub for the latest release. Nothing is downloaded or installed automatically. Spør GitHub om den nyeste utgivelsen. Ingenting lastes ned eller installeres automatisk. + At most Høyst + Attachments Vedlegg + Attachments Panel and Page Vedleggspanel og side + Audit space usage… Revider plassbruk… + Author Forfatter + Auto Auto + Auto follows the system's text size. Pages always render at the display's full resolution. Auto følger systemets tekststørrelse. Sider gjengis alltid i skjermens fulle oppløsning. + Auto portrait/landscape Automatisk stående/liggende + Auto-rotate pages Roter sider automatisk + Auto-size text to fit redaction region Tilpass tekststørrelsen til sladdeområdet + Available Tilgjengelig + Back side only Bare baksiden + Back to all tools Tilbake til alle verktøy + Back to the document Tilbake til dokumentet + Before each selected page (select pages in Organize) Før hver valgte side (velg sider i Organiser) + Before the number Før nummeret + Bicubic downsampling to Bikubisk nedsampling til + Binding Innbinding + Binding: Innbinding: + Black and white Svart-hvitt + Bold Fet + Booklet Hefte + Booklet subset: Heftedelsett: + Bookmark options Bokmerkealternativer + Bookmarks Panel and Page Bokmerkepanel og side + Border Color: Kantlinjefarge: + Borders and Colors Kantlinjer og farger + Both sides Begge sider + Bottom Bunn + Bottom: Bunn: + Box Boks + Browser's print dialog Nettleserens utskriftsdialog + Butt Flat + Button is checked by default Knappen er avkrysset som standard + Buttons with the same name and value are selected in unison Knapper med samme navn og verdi velges samtidig + Bytes Byte + Calibrate from two points Kalibrer fra to punkter + Can't be read: {e} Kan ikke leses: {e} + Cancelling… Avbryter… + Case-sensitive Skill mellom store og små bokstaver + Category: Kategori: + Center Midtstill + Center window on screen Midtstill vinduet på skjermen + Centre Midten + Certify Sertifiser + Change settings… Endre innstillinger… + Change the colour of line art as well as text Endre fargen på strektegninger i tillegg til tekst + Change {what} Endre {what} + Changes allowed Tillatte endringer + Character limit 0 means no limit. A comb of characters needs a limit. Tegngrense 0 betyr ingen grense. En tegnkam krever en grense. + Character spacing Tegnavstand + Check Again Sjekk igjen + Check again Sjekk igjen + Check box is checked by default Avkrysningsboksen er avkrysset som standard + Check for updates Se etter oppdateringer + Check spelling Stavekontroll + Check the pages Kontroller sidene + Check the pages of {name}: {why} Kontroller sidene i {name}: {why} + Checking Options ({on} of 32 in all categories) Kontrollalternativer ({on} av 32 i alle kategorier) + Checking for a newer version… Ser etter en nyere versjon… + Choose a PNG or JPEG image (up to 4 MiB). Transparency is preserved. Velg et PNG- eller JPEG-bilde (opptil 4 MiB). Gjennomsiktighet bevares. + Choose a folder for the exported files Velg en mappe for de eksporterte filene + Choose a folder for the extracted pages Velg en mappe for de uttrukne sidene + Choose a folder for the results Velg en mappe for resultatene + Choose a folder for the searchable files Velg en mappe for de søkbare filene + Choose a layout: Velg et oppsett: + Choose a stamp, then click on the page to place it. Velg et stempel, og klikk deretter på siden for å plassere det. + Choose an image Velg et bilde + Choose images Velg bilder + Choose image… Velg bilde… + Choose photo… Velg foto… + Choose the digital ID that you want to use for signing: Velg den digitale ID-en du vil signere med: + Circle Sirkel + Clean Up Rydd opp + Clean-up Opprydding + Clear All Tøm alt + Clear all Tøm alt + Clear selection / close find Fjern utvalg / lukk søk + 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. Klikk på siden for å legge til tekst; hvert klikk starter en ny boks, og ✓ fullfører. Dra elementer for å flytte dem, dra et hjørne for å endre størrelse, dobbeltklikk på tekst for å redigere den. + Click the first point to begin. Klikk på det første punktet for å begynne. + Click to sort, drag to move the column. Drag its edge to resize it. Klikk for å sortere, dra for å flytte kolonnen. Dra kanten for å endre bredden. + Click vertices. Enter or double-click finishes a perimeter or area. Click the first point to close an area. Klikk på hjørnepunktene. Enter eller dobbeltklikk fullfører en omkrets eller et areal. Klikk på det første punktet for å lukke et areal. + Clockwise 90 degrees 90 grader med klokka + Close (Esc) Lukk (Esc) + Close Tab Lukk fane + Close panel Lukk panel + Closed arrow Lukket pil + Collapse Skjul + Collapse all Skjul alle + Collapse all bookmarks Skjul alle bokmerker + Collapse tag Skjul tagg + Collate Sorter + Color: Farge: + Colour Farge + Colours are remapped while rendering; the document itself never changes. Fargene omdefineres under gjengivelsen; selve dokumentet endres aldri. + Comb of Kam på + Combine the rest Slå sammen resten + Combined {n} files Slo sammen {n} filer + Coming in milestone Kommer i milepæl + Comment and form data Kommentar- og skjemadata + Commenting, filling in form fields, and signing existing signature fields Kommentering, utfylling av skjemafelt og signering av eksisterende signaturfelt + Comments & Forms Kommentarer og skjemaer + Comments only Bare kommentarer + Commit selected value immediately Bekreft valgt verdi umiddelbart + Common Properties Felles egenskaper + Compare Files Sammenlign filer + Compare this file with an older version: Compare files… in All tools. Sammenlign denne filen med en eldre versjon: Sammenlign filer… under Alle verktøy. + Compatibility Kompatibilitet + Compress document structure (object streams) Komprimer dokumentstrukturen (objektstrømmer) + Compression Komprimering + Configure New Digital ID Sett opp ny digital ID + Remove "{name}" from this list? The file itself is not deleted. Fjerne «{name}» fra denne listen? Selve filen slettes ikke. + Confirm Password Bekreft passord + Conformance level: Samsvarsnivå: + Continue Fortsett + Copies, two-sided printing and colour are chosen in the browser's print dialog. Antall kopier, tosidig utskrift og farge velges i nettleserens utskriftsdialog. + Copies: Antall kopier: + Copy selected text Kopier merket tekst + Copy text Kopier tekst + Couldn't check for updates: {e} Kunne ikke se etter oppdateringer: {e} + Couldn't open the printer's preferences: {e} Kunne ikke åpne skriverinnstillingene: {e} + Counterclockwise 90 degrees 90 grader mot klokka + Country/Region Land/region + Create Custom Stamp Lag egendefinert stempel + Create Link Lag lenke + Create PDF Comment Summary Lag PDF-kommentarsammendrag + Create PDF comment summary… Lag PDF-kommentarsammendrag… + Create PDF from images Lag PDF fra bilder + Create a new Digital ID (self-signed, saved to a password-protected file) Lag en ny digital ID (selvsignert, lagret i en passordbeskyttet fil) + Create accessibility report Lag tilgjengelighetsrapport + Create custom stamp… Lag egendefinert stempel… + Create {what} Lag {what} + Created a PDF from {n} files Laget en PDF fra {n} filer + Created a PDF from {n} files; left out: {names} Laget en PDF fra {n} filer; utelatt: {names} + Creates 1 file from {n} pages. Lager 1 fil av {n} sider. + Creates {f} files from {n} pages. Lager {f} filer av {n} sider. + Crop (% trimmed from each side) Beskjær (% fjernet fra hver side) + Currency symbol: Valutasymbol: + Current page Gjeldende side + Current page ({n}) Gjeldende side ({n}) + Current page — type a page number or label (such as iv) and press Enter Gjeldende side – skriv inn et sidetall eller en sideetikett (som iv) og trykk Enter + Zoom — type a percentage and press Enter Zoom – skriv inn en prosentverdi og trykk Enter + Custom Egendefinert + Custom scale: Egendefinert skala: + Custom text: Egendefinert tekst: + Custom: Egendefinert: + Cut and stack Klipp og stable + Cut marks Skjæremerker + DPI sets the printed page size without resampling the image. DPI angir den utskrevne sidestørrelsen uten å resample bildet. + Dark theme Mørkt tema + Date format Datoformat + Decimal places: Desimaler: + Decimals Desimaler + Declared conformance: {shown} Oppgitt samsvar: {shown} + Decorative figure Dekorativ figur + Default Standard + Default Value: Standardverdi: + Default workspace mode Standard arbeidsområdemodus + Default: Standard: + Delete page {label} Slett side {label} + Delete pages (Delete) Slett sider (Delete) + Delete pages after extracting Slett sidene etter uttrekking + Delete selected pages (Organize) Slett valgte sider (Organiser) + Delete stamp Slett stempel + Delete {o} Slett {o} + Describe the figure Beskriv figuren + Description Beskrivelse + Description: Beskrivelse: + Details Detaljer + Diamond Rombe + Digital ID Digital ID + Digital ID password Passord for digital ID + Direction: Retning: + Discard Forkast + Discard Objects Forkast objekter + Discard User Data Forkast brukerdata + Discard alternate images Forkast alternative bilder + Discard document tags Forkast dokumenttagger + Discard embedded page thumbnails Forkast innebygde sideminiatyrer + Discard embedded print settings Forkast innebygde utskriftsinnstillinger + Discard this text (Esc) Forkast denne teksten (Esc) + Dismiss Lukk + Distinguished name Entydig navn + Distribute Fordel + Document Dokument + Document Properties Dokumentegenskaper + Document Title Dokumenttittel + Document and markups Dokument og markeringer + Document and stamps Dokument og stempler + Document language Dokumentspråk + Document properties ({key}) Dokumentegenskaper ({key}) + Documents and view Dokumenter og visning + Done Ferdig + Done adding text Ferdig med å legge til tekst + Download Last ned + Downsample to Nedsample til + Drag the selection to move it. Esc clears the selection. Dra utvalget for å flytte det. Esc fjerner utvalget. + Drag to reorder Dra for å endre rekkefølge + Draw Tegn + Draw your {what} below. Tegn {what} nedenfor. + Drawing scale changes. Use Actual size for full-scale plans. Tegningsskalaen endres. Bruk Faktisk størrelse for planer i full skala. + Drop PDFs, images, text files or folders here, or choose them. Slipp PDF-er, bilder, tekstfiler eller mapper her, eller velg dem. + Duplicate "{name}" onto: Dupliser «{name}» til: + Duplicate Field Dupliser felt + Duplicate… Dupliser… + Dynamic Dynamisk + Dynamic XFA form laid out from its template: {pages}, {fields}. Dynamisk XFA-skjema satt opp fra malen: {pages}, {fields}. + Each file holds as many of the {n} pages as fit. Hver fil inneholder så mange av de {n} sidene som får plass. + Each image is measured where pages draw it; an image is replaced only if the result is smaller. Hvert bilde måles der sidene tegner det; et bilde erstattes bare hvis resultatet blir mindre. + Each image once, named after the document and page. JPEG images are saved unchanged; others as PNG. Hvert bilde én gang, navngitt etter dokument og side. JPEG-bilder lagres uendret; andre som PNG. + Earlier in tab order Tidligere i tabulatorrekkefølgen + Edit Action Rediger handling + Edit image Rediger bilde + Edit text… Rediger tekst… + Edit… Rediger… + Email Address E-postadresse + Enable Acrobat JavaScript Slå på Acrobat JavaScript + Enable copying of text, images, and other content Tillat kopiering av tekst, bilder og annet innhold + Enable text access for screen reader devices for the visually impaired Tillat teksttilgang for skjermlesere for synshemmede + Encrypt Krypter + Endpoints Endepunkter + Enhance page Forbedre side + Enter the password that opens it Skriv inn passordet som åpner den + Enter the permissions password to allow combining Skriv inn tillatelsespassordet for å tillate sammenslåing + Entire page Hele siden + Even Pages Only Bare partallssider + Even and Odd Pages Partalls- og oddetallssider + Even pages only Bare partallssider + Every PDF, image and text file in the folder Alle PDF-, bilde- og tekstfiler i mappen + Every PDF, image and text file in the folder and the folders inside it Alle PDF-, bilde- og tekstfiler i mappen og mappene inni den + Example: {sample} Eksempel: {sample} + Exclude images smaller than Utelat bilder mindre enn + Expand Utvid + Expand all Utvid alle + Expand all bookmarks Utvid alle bokmerker + Expand tag Utvid tagg + Expand top-level bookmarks Utvid bokmerker på øverste nivå + Explain Forklar + Export All Images Eksporter alle bilder + Export Value: {on} Eksportverdi: {on} + Export all to data file… Eksporter alt til datafil… + Export certificate… Eksporter sertifikat… + Export comments Eksporter kommentarer + Export form data Eksporter skjemadata + Export stopped: {e} Eksporten stoppet: {e} + Export to Image Eksporter til bilde + Export to Text Eksporter til tekst + Exported 1 image Eksporterte 1 bilde + Exported the text of 1 page Eksporterte teksten fra 1 side + Exported the text of {n} pages Eksporterte teksten fra {n} sider + Exported {n} images Eksporterte {n} bilder + Exporting… {d} of {t} Eksporterer… {d} av {t} + Export… Eksporter… + Extract pages as separate files Trekk ut sider som separate filer + Extract pages to a new document Trekk ut sider til et nytt dokument + Extracted 1 page Trakk ut 1 side + Extracted {n} pages Trakk ut {n} sider + Field is used for file selection Feltet brukes til filvalg + Field names (comma-separated) Feltnavn (kommaseparert) + Field names with + - * / and parentheses; put \\ before spaces in names. Feltnavn med + - * / og parenteser; sett \\ foran mellomrom i navn. + Field value is in range: Feltverdien er i området: + Field value is not validated Feltverdien valideres ikke + Field: {field} on page {p} Felt: {field} på side {p} + Fields to reset (comma-separated; empty: all) Felt som skal tilbakestilles (kommaseparert; tomt: alle) + Figure {i} of {n}{page} Figur {i} av {n}{page} + File (.p12, .pfx) Fil (.p12, .pfx) + File Name Filnavn + File size Filstørrelse + Files are combined from top to bottom. Leave Pages empty to take every page. Filene slås sammen fra topp til bunn. La Sider stå tomt for å ta med alle sidene. + Files are combined in the order shown. Drag them to change it. Filene slås sammen i rekkefølgen som vises. Dra dem for å endre den. + Files for {name} Filer for {name} + Files you open in PdfCraft appear here. Drop a PDF anywhere to open it. Filer du åpner i PdfCraft vises her. Slipp en PDF hvor som helst for å åpne den. + Fill & Sign dates Fyll ut og signer-datoer + Fill & Sign signature Fyll ut og signer-signatur + Fill Color: Fyllfarge: + Fill colour Fyllfarge + Fill colour: none Fyllfarge: ingen + Filling in form fields and signing existing signature fields Utfylling av skjemafelt og signering av eksisterende signaturfelt + Filter comments Filtrer kommentarer + Find a tag Finn en tagg + Find options Søkealternativer + Find text Finn tekst + Find tools and commands Finn verktøy og kommandoer + Finding document fonts… Finner dokumentskrifter… + Finish or discard the text being edited before selecting several objects. Fullfør eller forkast teksten som redigeres før du merker flere objekter. + First / last page Første/siste side + Fit Tilpass + Fit Height Tilpass til høyde + Fit Page Tilpass til side + Fit Visible Tilpass til synlig + Fit Width Tilpass til bredde + Fit to height Tilpass til høyde + Fit to page Tilpass til side + Fix Fiks + Fixed what could be fixed; 1 problem remains Rettet det som kunne rettes; 1 problem gjenstår + Fixed what could be fixed; {n} problems remain Rettet det som kunne rettes; {n} problemer gjenstår + Flatten Fill & Sign when saving Flat ut Fyll ut og signer ved lagring + Flip Horizontal Vend horisontalt + Flip Vertical Vend vertikalt + Flip horizontal Vend horisontalt + Flip on long edge Vend langs langsiden + Flip on short edge Vend langs kortsiden + Flip vertical Vend vertikalt + Folders Mapper + Font Size: Skriftstørrelse: + Font colour: Skriftfarge: + Font size 0 uses automatic sizing. Skriftstørrelse 0 gir automatisk størrelse. + Font size: Skriftstørrelse: + Font: Skrift: + Font: Helvetica Skrift: Helvetica + Fonts Skrifter + For page sizes and margins. For sidestørrelser og marger. + Form data and PDF forms Skjemadata og PDF-skjemaer + Form fields are flattened: their values stay visible. Saving rewrites the whole file. Skjemafelt flates ut: verdiene forblir synlige. Lagring skriver hele filen på nytt. + Form fields only Bare skjemafelt + Form fill-in and digital signatures Skjemautfylling og digitale signaturer + Format Format + Format text Formater tekst + Format: Format: + From Fra + From a PDF page or an image Fra en PDF-side eller et bilde + From colour Fra farge + From {file} Fra {file} + From: Fra: + Front side only Bare forsiden + Get help, share feedback and follow development on Discord. Få hjelp, gi tilbakemelding og følg utviklingen på Discord. + Go to a page view Gå til en sidevisning + Go to page {p} Gå til side {p} + Go to the page Gå til siden + Go to this result Gå til dette treffet + Graphic Grafikk + Greyscale Gråtoner + Grid view Rutenettvisning + Hand (H) Hånd (H) + Has JavaScript actions (run in M6) Har JavaScript-handlinger (kjøres i M6) + Height: Høyde: + Hide field highlights Skjul feltuthevinger + Hide layer Skjul lag + Hide menu bar Skjul menylinjen + Hide password Skjul passord + Hide toolbars Skjul verktøylinjer + Hide window controls Skjul vinduskontroller + High Resolution Høy oppløsning + High resolution Høy oppløsning + Highlight Style: Uthevingsstil: + Highlight fields Uthev felt + Home Hjem + Horizontal Horisontal + Horizontal reversed Horisontal omvendt + Horizontal scale Horisontal skalering + Icon Ikon + Images and text Bilder og tekst + Images and text (converted to PDF) Bilder og tekst (konvertert til PDF) + Import list… Importer liste… + Includes unsaved changes Inkluderer ulagrede endringer + Indent Innrykk + Initial View Startvisning + Ink sensitivity Blekkfølsomhet + Insert Date Sett inn dato + Insert Page Number Sett inn sidetall + Insert a blank page after the selection Sett inn en tom side etter utvalget + Insert a file at the end Sett inn en fil på slutten + Insert a file before page {label} Sett inn en fil før side {label} + Insert pages from a file… Sett inn sider fra en fil… + Inserting, deleting, and rotating pages Innsetting, sletting og rotering av sider + Installed fonts… Installerte skrifter… + Intersections Skjæringspunkter + Invisible Rectangle Usynlig rektangel + Issued by: Utstedt av: + Italic Kursiv + Item List Elementliste + Item: Element: + Items marked Ready work today. Elementer merket Klar fungerer i dag. + Its security settings don't allow copying pages Sikkerhetsinnstillingene tillater ikke kopiering av sider + JavaScript Console JavaScript-konsoll + JavaScript is turned off (Preferences ▸ JavaScript). JavaScript er slått av (Innstillinger > JavaScript). + JavaScript, e.g. getField("total").value JavaScript, f.eks. getField("total").value + Join our Discord Bli med på Discord-serveren vår + Join the ArtCraft community Bli med i ArtCraft-fellesskapet + Justify Blokkjuster + Keep source fonts Behold kildeskrifter + Keep tool selected Behold verktøyet valgt + Keep tool selected: on Behold verktøyet valgt: på + Key Algorithm Nøkkelalgoritme + Keychain · Nøkkelring · + Label Etikett + Labels Etiketter + Labels: {preview}. Later pages keep their labels. Etiketter: {preview}. Senere sider beholder etikettene sine. + Landscape Liggende + Landscape Pages Liggende sider + Landscape and Portrait Pages Liggende og stående sider + Larger pages Større sider + Later Senere + Later in tab order Senere i tabulatorrekkefølgen + Layers Lag + Layers Panel and Page Lagpanel og side + Layout and Magnification Oppsett og forstørrelse + Leave out files that can't be read? Vil du utelate filer som ikke kan leses? + Left Venstre + Left Edge Venstre kant + Left out: {names} Utelatt: {names} + Left: Venstre: + Light theme Lyst tema + Limit of Grense på + Line Style: Linjestil: + Line Thickness: Linjetykkelse: + Line ending Linjeende + Line spacing Linjeavstand + Line thickness Linjetykkelse + Link Action Lenkehandling + Link Appearance Lenkeutseende + Link Properties Lenkeegenskaper + Link Type: Lenketype: + List view Listevisning + Load font… Last inn skrift… + Locked Låst + Looking for PDFs… Ser etter PDF-er… + Looking for printers… Ser etter skrivere… + Low Resolution (150 dpi) Lav oppløsning (150 dpi) + Low resolution Lav oppløsning + Magnification Forstørrelse + Make Current Properties Default Gjør gjeldende egenskaper til standard + Margins Marger + Mark Page Range Merk sideområde + Mark as comments Merk som kommentarer + Mark with checkmark Merk med hake + Marked Merket + Mask: Maske: + Match page at 100% Vis siden i 100 % + Medium Middels + Midpoints Midtpunkter + Mixed Blandet + Modified Endret + More options Flere alternativer + More options: Flere alternativer: + More ways to add files Flere måter å legge til filer på + Mouse wheel scroll speed Rullehastighet for musehjul + Move down Flytt ned + Move earlier Flytt tidligere + Move later Flytt senere + Move up Flytt opp + Multi-line Flerlinjet + Multiple Flere + Multiple selection Flervalg + Multiple words or phrases Flere ord eller fraser + Name: Navn: + Navigation tab Navigasjonsfane + Needs manual check Må sjekkes manuelt + Negative number style: Stil for negative tall: + New Action Ny handling + New Action… Ny handling… + New bookmark ({key}) Nytt bokmerke ({key}) + New file: Ny fil: + Next ({key}) Neste ({key}) + Next / previous match Neste/forrige treff + Next figure Neste figur + Next month Neste måned + Next page Neste side + Next sheet Neste ark + Next view Neste visning + No PDFs in this folder yet Ingen PDF-er i denne mappen ennå + No actions Ingen handlinger + No changes allowed Ingen endringer tillatt + No changes to save Ingen endringer å lagre + No check has run yet. Ingen kontroll er kjørt ennå. + No color Ingen farge + No colour Ingen farge + No comments match. Ingen kommentarer samsvarer. + No comments yet. Ingen kommentarer ennå. + No differences in the text. Ingen forskjeller i teksten. + No digital IDs yet. Ingen digitale ID-er ennå. + No documents are open Ingen dokumenter er åpne + No file chosen Ingen fil valgt + No fill Ingen fyll + No fonts are referenced by the pages. Ingen skrifter refereres av sidene. + No issues found Ingen problemer funnet + No limit Ingen grense + No matches Ingen treff + No matches. Ingen treff. + No matching tools Ingen samsvarende verktøy + No openly licensed document fonts found. Load a font file. Fant ingen dokumentskrifter med åpen lisens. Last inn en skriftfil. + No pages selected Ingen sider valgt + No photo chosen Ingen foto valgt + No preview Ingen forhåndsvisning + No problems found: the document conforms to {level}. Ingen problemer funnet: dokumentet samsvarer med {level}. + No security Ingen sikkerhet + No sheets Ingen ark + No signature was found in the frame. Move the frame over the signature or raise the ink sensitivity. Fant ingen signatur i rammen. Flytt rammen over signaturen eller øk blekkfølsomheten. + No status has been set. Ingen status er angitt. + None (prefix only) Ingen (bare prefiks) + None found Ingen funnet + None — everything is allowed Ingen — alt er tillatt + Not allowed Ikke tillatt + Not present Ikke til stede + Nothing to redo Ingenting å gjøre om + Nothing to undo Ingenting å angre + Number Tall + Number of pages Antall sider + Number pages Nummerer sider + Odd Pages Only Bare oddetallssider + Odd pages only Bare oddetallssider + Offset from centre Forskyvning fra midten + Old file: Gammel fil: + One word or phrase per line Ett ord eller én frase per linje + One {f} file per page, named after the document. Én {f}-fil per side, oppkalt etter dokumentet. + Only change the colour of black text or line art Endre bare fargen på svart tekst eller strektegninger + Only checked properties will change. Mixed means the selected fields have different values. Bare avkryssede egenskaper endres. Blandet betyr at de valgte feltene har ulike verdier. + Only the page content changes: links, comments, form fields and bookmarks on the original pages stay. Bare sideinnholdet endres: lenker, kommentarer, skjemafelt og bokmerker på de opprinnelige sidene beholdes. + Open Åpne + Open a PDF ({key}) Åpne en PDF ({key}) + Open a PDF to measure it. Åpne en PDF for å måle den. + Open a document first Åpne et dokument først + Open a web page Åpne en nettside + Open arrow Åpen pil + Open email app Åpne e-postprogram + Open in Full Screen mode Åpne i fullskjermmodus + Open in a new tab Åpne i ny fane + Open link Åpne lenke + Open the file as it was saved then Åpne filen slik den ble lagret da + Open the older version first Åpne den eldre versjonen først + Open this web page? Åpne denne nettsiden? + Open to page Åpne på side + Opened, but its security settings don't allow combining. Enter the permissions password. Åpnet, men sikkerhetsinnstillingene tillater ikke sammenslåing. Skriv inn tillatelsespassordet. + 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. Åpner innstillingene til skriverdriveren. De lagres for alle utskrifter fra denne datamaskinen; antall kopier, tosidig, gråtoner og papir valgt her gjelder fortsatt. + Optimizing… cleaning up Optimaliserer… rydder opp + Optimizing… image {d} of {t} Optimaliserer… bilde {d} av {t} + Optimizing… merging identical objects Optimaliserer… slår sammen identiske objekter + Optimizing… removing user data Optimaliserer… fjerner brukerdata + Optimizing… writing the copy Optimaliserer… skriver kopien + Optional: needs an AI provider you configure Valgfritt: krever en KI-leverandør du setter opp + Options… Alternativer… + Organization Name Organisasjonsnavn + Organizational Unit Organisasjonsenhet + Orientation: Retning: + Original Original + Original: replace pages Original: erstatt sider + Other colour… Annen farge… + Outdent Reduser innrykk + Output Utdata + Output appears here. Utdata vises her. + Output intent: {list} Utdataformål: {list} + Overlap: Overlapping: + Owner password (no restrictions apply) Eierpassord (ingen begrensninger gjelder) + PDF Optimizer PDF-optimalisering + PDF or image PDF eller bilde + PDF page: {size} PDF-side: {size} + PDF scale: {scale}% PDF-skala: {scale}% + PDF, images and text PDF, bilder og tekst + Page Only Bare siden + Page Range Options Alternativer for sideområde + Page Sizing & Handling Sidestørrelse og håndtering + Page background Sidebakgrunn + Page display: {layout} Sidevisning: {layout} + Page layout Sideoppsett + Page navigation Sidenavigering + Page number Sidetall + Page order: Siderekkefølge: + Page size Sidestørrelse + Page {label} Side {label} + Page {p} of {n} Side {p} av {n} + Pages Panel and Page Sider-panelet og siden + Pages from Sider fra + Pages of {name} to combine, e.g. 1-3, 6 Sider i {name} som skal slås sammen, f.eks. 1-3, 6 + Pages per file Sider per fil + Pages per sheet: Sider per ark: + Pages to Print Sider som skal skrives ut + Pages: Sider: + Pages: {k} of {n} Sider: {k} av {n} + Paper: Papir: + Part of Del av + Password Passord + Password required Passord kreves + Password security Passordsikkerhet + Password-protected Passordbeskyttet + Paste after Lim inn etter + Paths Baner + Pattern Mønster + Patterns Mønstre + Patterns (graphics objects) Mønstre (grafikkobjekter) + PdfCraft didn't shut down normally. These documents had changes that were autosaved: PdfCraft ble ikke avsluttet normalt. Disse dokumentene hadde endringer som ble autolagret: + PdfCraft works offline. No telemetry, no account, and no cloud processing unless you add a provider. PdfCraft fungerer uten nett. Ingen telemetri, ingen konto og ingen skybehandling med mindre du legger til en leverandør. + PdfCraft {c} is up to date. PdfCraft {c} er oppdatert. + PdfCraft {v} is available. PdfCraft {v} er tilgjengelig. + Percentage Prosent + Permissions password Tillatelsespassord + Permitted actions after certifying Tillatte handlinger etter sertifisering + Photo Foto + Pick a tool in the quick bar, or type above. Velg et verktøy i hurtiglinjen, eller skriv ovenfor. + Pictures on those pages are recognized too; words already in the page's text are not added again Bilder på disse sidene gjenkjennes også; ord som allerede finnes i sidens tekst legges ikke til på nytt + Pin a folder to Home Fest en mappe til Hjem + Pin a folder to list its newest PDFs here, such as where your scanner or a synced cloud drive saves them. Fest en mappe for å vise de nyeste PDF-ene i den her, for eksempel der skanneren eller en synkronisert skylagring lagrer dem. + Pin a folder… Fest en mappe… + Plain text in reading order; pages are separated by form feeds. Ren tekst i leserekkefølge; sidene skilles med sideskifttegn. + Planned Planlagt + Planned for milestone Planlagt for milepæl + Points from the page's bottom-left corner. Punkter fra sidens nedre venstre hjørne. + Portrait Stående + Portrait Pages Stående sider + Post Publiser + Poster Plakat + Prefix Prefiks + Preview Forhåndsvisning + Preview unavailable: {e} Forhåndsvisning utilgjengelig: {e} + Preview: page {p} of {n} Forhåndsvisning: side {p} av {n} + Previous ({key}) Forrige ({key}) + Previous / next page Forrige / neste side + Previous figure Forrige figur + Previous month Forrige måned + Previous page Forrige side + Previous sheet Forrige ark + Previous view Forrige visning + Print ({key}) Skriv ut ({key}) + Print in grayscale Skriv ut i gråtoner + Print page border Skriv ut sidekant + Print, NextPage, PrevPage, FirstPage or LastPage Print, NextPage, PrevPage, FirstPage eller LastPage + Printer paper and scaling may differ. Check driver settings. Skriverens papir og skalering kan avvike. Sjekk driverinnstillingene. + Printer properties Skriveregenskaper + Printer: Skriver: + Printing allowed Utskrift tillatt + Privacy Personvern + Properties… Egenskaper… + Protect Using Password Beskytt med passord + Protected: the combined file won't keep its security settings Beskyttet: den sammenslåtte filen beholder ikke sikkerhetsinnstillingene + Quality Kvalitet + Read Only Skrivebeskyttet + Reading colours Lesefarger + Ready Klar + Reason Årsak + Recent Nylige + Recent colours Nylige farger + Recognize Text Gjenkjenn tekst + Recognized 1 word Gjenkjente 1 ord + Recognized {n} words Gjenkjente {n} ord + Recognized {w} words in 1 file Gjenkjente {w} ord i 1 fil + Recognized {w} words in {n} files Gjenkjente {w} ord i {n} filer + Recognizing text… file {d} of {t} Gjenkjenner tekst… fil {d} av {t} + Recognizing text… page {d} of {t} Gjenkjenner tekst… side {d} av {t} + Recommended tools Anbefalte verktøy + Recover Gjenopprett + Recover unsaved documents? Gjenopprette ulagrede dokumenter? + Redact all Sladd alle + Redacted area fill colour: Fyllfarge for sladdet område: + Redaction Tool Properties Egenskaper for sladdeverktøy + Redaction code: Sladdekode: + Reload Last inn på nytt + Reload documents when their files change on disk Last inn dokumenter på nytt når filene endres på disk + Remove checkmark Fjern hake + Remove invalid links and bookmarks Fjern ugyldige lenker og bokmerker + Remove page Fjern side + Remove unreferenced named destinations Fjern navngitte mål uten referanser + Remove unused objects and merge identical ones Fjern ubrukte objekter og slå sammen identiske + Remove {name} Fjern {name} + Remove {name} from Recent Fjern {name} fra nylige + Remove {n} files Fjern {n} filer + Remove {n} pages Fjern {n} sider + Reopen Closed Tab Åpne lukket fane på nytt + Reopen the files that were open when PdfCraft last closed Åpne filene som var åpne da PdfCraft sist ble lukket + Repair log Reparasjonslogg + Repeat overlay text Gjenta overleggstekst + Replace Image… Erstatt bilde… + Replace Pages Erstatt sider + Replace document colours Erstatt dokumentfarger + Replace image Erstatt bilde + Replacement text Erstatningstekst + Replacement: pages of {name} Erstatning: sider fra {name} + Replaces the existing one on those pages. Erstatter den eksisterende på disse sidene. + Report Rapport + Report… Rapport… + Requires user to enter a password for: Krever at brukeren skriver inn et passord for: + Reset columns Tilbakestill kolonner + Reset page size Tilbakestill sidestørrelse + Reset to printer defaults Tilbakestill til skriverens standardverdier + Resize window to initial page Tilpass vinduet til første side + Resolution Oppløsning + Retain existing Behold eksisterende + Reveal in File Explorer Vis i Filutforsker + Reveal in Finder Vis i Finder + Reverse closed arrow Omvendt lukket pil + Reverse open arrow Omvendt åpen pil + Reverse pages Snu siderekkefølgen + Revert changes Forkast endringer + Revert to the previously saved version of “{name}”? Changes since then can't be undone afterwards. Gå tilbake til den sist lagrede versjonen av «{name}»? Endringer gjort siden da kan ikke angres etterpå. + Review History Gjennomgangshistorikk + Revisions Revisjoner + Right Høyre + Right Edge Høyre kant + Rotate Clockwise Roter med klokken + Rotate Counterclockwise Roter mot klokken + Rotate Pages Roter sider + Rotate clockwise Roter med klokken + Rotate counterclockwise Roter mot klokken + Rotate view Roter visning + Rotate view clockwise Roter visningen med klokken + Rotate view clockwise ({key}) Roter visningen med klokken ({key}) + Rotate view counterclockwise Roter visningen mot klokken + Rotate: Roter: + Rotation: Rotasjon: + Running action… file {d} of {t} Kjører handling… fil {d} av {t} + Same as interface ({language}) Samme som grensesnittet ({language}) + 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. Rensing fjerner skjult informasjon fra dokumentet: metadata, filvedlegg, kommentarer, skjemafelt (flates ut), skjult tekst og lag, bokmerker, lenker, handlinger og skript, og private programdata. + Save & Close Lagre og lukk + Save ({key}) Lagre ({key}) + Save Image As… Lagre bilde som… + Copy Image Kopier bilde + The image has been copied ({w} × {h} pixels) Bildet er kopiert ({w} × {h} piksler) + Couldn't copy the image: {e} Kunne ikke kopiere bildet: {e} + Copy Image in Original Format Kopier bilde i opprinnelig format + The image has been copied as a {ext} file Bildet er kopiert som en {ext}-fil + The page has changed; select the image again Siden er endret; velg bildet på nytt + Save as PDF Lagre som PDF + Save attachment… Lagre vedlegg… + Save changes to “{name}” before closing? Lagre endringene i «{name}» før du lukker? + Save pages Lagre sider + Save tag properties Lagre taggegenskaper + Save the accessibility report Lagre tilgjengelighetsrapporten + Save the document afterwards: saving rewrites the whole file so no trace of the removed content stays in it. Lagre dokumentet etterpå: lagring skriver hele filen på nytt, så ingen spor av det fjernede innholdet blir igjen i den. + Save the document afterwards; saving rewrites the whole file so nothing removed stays in it. Lagre dokumentet etterpå; lagring skriver hele filen på nytt, så ingenting av det fjernede blir igjen i den. + Save the spreadsheet Lagre regnearket + Save these pages as one PDF Lagre disse sidene som én PDF + Saved measurements Lagrede målinger + Saved measurements. Lagret målingene. + Scale relative to target page Skaler i forhold til målsiden + Scan Skann + Script Name: Skriptnavn: + Script: Skript: + 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. Skript kjører i en sandkasse uten fil- eller nettverkstilgang. Et skript som ber om å åpne en nettside, trenger tillatelse fra deg først. Med JavaScript av fungerer Acrobats standardfunksjoner for formatering, validering og beregning fortsatt. + Scroll long text Rull lang tekst + Scroll tabs left Rull faner til venstre + Scroll tabs right Rull faner til høyre + Search comments Søk i kommentarer + Search tools and commands… Søk etter verktøy og kommandoer… + Searchable Image (Exact) Søkbart bilde (eksakt) + Searching… {s} of {p} pages Søker… {s} av {p} sider + Searching… {s}/{p} Søker… {s}/{p} + Security Sikkerhet + Security settings Sikkerhetsinnstillinger + Select (V) Velg (V) + Select Action: Velg handling: + Select All Merk alt + Select Icon Velg ikon + Select Trigger: Velg utløser: + Select a file first Velg en fil først + Select a file for the stamp Velg en fil til stempelet + Select a password-protected file first Velg en passordbeskyttet fil først + Select a word Velg et ord + Select all pages (Organize) Merk alle sider (Organiser) + Select at most 1000 objects at a time. Velg høyst 1000 objekter om gangen. + Select data files to merge Velg datafiler som skal slås sammen + Select format category: Velg formatkategori: + Select page {label} Velg side {label} + Select pages Velg sider + Select the items to remove from this document. Velg elementene som skal fjernes fra dette dokumentet. + Selected image files: {count} Valgte bildefiler: {count} + Selected pages Valgte sider + Selection Utvalg + Sending to {printer}… Sender til {printer}… + Sensitivity Følsomhet + Sent to {printer} Sendt til {printer} + Sent to {printer}: {msg} Sendt til {printer}: {msg} + Separator style: Skilletegnstil: + Set Alternate Text Angi alternativ tekst + Set Fields to Same Size Gi feltene samme størrelse + Set Page Boxes Angi sidebokser + Set layer visibility Angi lagsynlighet + Set status Angi status + Set to current page ({n}) Sett til gjeldende side ({n}) + Set to zero Sett til null + Shared Field Properties Delte feltegenskaper + Sheet {s} of {n} Ark {s} av {n} + Shift constrains to 45 degrees. Backspace removes a vertex. Escape cancels. Shift låser til 45 grader. Tilbaketast fjerner et punkt. Esc avbryter. + Shift- or Ctrl/Command-click to select several fields. Shift- eller Ctrl/Command-klikk for å velge flere felt. + Shift-click to select several objects. Drag empty space to select an area. Shift-klikk for å velge flere objekter. Dra i tomt område for å velge et område. + Show Report Vis rapport + Show all Vis alle + Show all comments Vis alle kommentarer + Show certificate… Vis sertifikat… + Show layer Vis lag + Show less Vis mindre + Show password Vis passord + Show the accessibility report Vis tilgjengelighetsrapporten + Show this dialog when the Checker starts Vis denne dialogen når kontrollen starter + Show {n} more Vis {n} til + Show: Vis: + Shrink oversized pages Krymp for store sider + Sign Here Signer her + Sign this field… Signer dette feltet… + Sign yourself Signer selv + Signature Details Signaturdetaljer + Signature is INVALID: Signaturen er UGYLDIG: + Signature is valid: Signaturen er gyldig: + Signature panel Signaturpanel + Signature validity is UNKNOWN: Signaturens gyldighet er UKJENT: + Signer's name Underskriverens navn + Simplified field notation: Forenklet feltnotasjon: + Single Page Enkeltside + Single Page Continuous Enkeltside, sammenhengende + Single word or phrase Enkeltord eller frase + Skip Rule Hopp over regel + Skipped Hoppet over + Slash Skråstrek + Smaller pages Mindre sider + Snap to drawing Fest til tegning + Some bookmarks are not shown here: the panel lists a limited number, nested a limited depth. Noen bokmerker vises ikke her: panelet viser et begrenset antall, med begrenset dybde. + Some page content couldn't be read; snapping uses the rest. Noe sideinnhold kunne ikke leses; festing bruker resten. + Sort by Sorter etter + Sort comments by: Sorter kommentarer etter: + Sort items Sorter elementer + Source Kilde + Space Audit Plassrevisjon + Special Spesial + Split document Del opp dokument + Split into files… Del opp i filer… + Square Firkant + Standard Business Standard forretning + Start Start + Start Checking Start kontroll + Start page number Første sidetall + Status Status + Steps Trinn + Strength: {s} Styrke: {s} + Style Stil + Subject Emne + Subset Delsett + Summarize Options Alternativer for sammendrag + Summary Sammendrag + Switch to List view to change the pages it takes. Bytt til listevisning for å endre hvilke sider den tar. + Switch to two-page view first to show the cover page Bytt til tosidevisning først for å vise forsiden + Tab order: move a field with its arrows. Tabulatorrekkefølge: flytt et felt med pilene. + Tag type Taggtype + Tags Tagger + Text Color: Tekstfarge: + Text alignment: Tekstjustering: + Text in images isn't found; recognise text (OCR) first. Tekst i bilder blir ikke funnet; gjenkjenn tekst (OCR) først. + Text is compared word by word; the differences are shown on the new file. Teksten sammenlignes ord for ord; forskjellene vises på den nye filen. + Text recognition isn't installed: its model files are missing. Reinstall PdfCraft, or set PDFCRAFT_MODELS to the folder that holds them. Tekstgjenkjenning er ikke installert: modellfilene mangler. Installer PdfCraft på nytt, eller sett PDFCRAFT_MODELS til mappen som inneholder dem. + Text to find Tekst å finne + Text to insert Tekst å sette inn + Text used as a clipping path cannot be moved with this selection. Tekst brukt som klippebane kan ikke flyttes med dette utvalget. + Text, marks and signatures become part of the page. Other comments stay editable. Tekst, markeringer og signaturer blir en del av siden. Andre kommentarer kan fortsatt redigeres. + Thai and Arabic use regular fallback fonts when available. Thai og arabisk bruker vanlige reserveskrifter når de er tilgjengelige. + The PDF text fonts can't draw “{c}” ({code}). Choose supported text. PDF-tekstskriftene kan ikke tegne «{c}» ({code}). Velg støttet tekst. + The checker found 1 problem which may prevent the document from being fully accessible. Kontrollen fant 1 problem som kan hindre at dokumentet er fullt tilgjengelig. + The checker found no problems in this document. Kontrollen fant ingen problemer i dette dokumentet. + The checker found {n} problems which may prevent the document from being fully accessible. Kontrollen fant {n} problemer som kan hindre at dokumentet er fullt tilgjengelig. + The document now conforms to {level}; save it to keep the changes Dokumentet samsvarer nå med {level}; lagre det for å beholde endringene + The document opens without a password; this password is needed to change the restrictions. It's applied when you save. Dokumentet åpnes uten passord; dette passordet trengs for å endre begrensningene. Det brukes når du lagrer. + The document's security settings don't allow copying text and images Dokumentets sikkerhetsinnstillinger tillater ikke kopiering av tekst og bilder + The document's security settings don't allow this change Dokumentets sikkerhetsinnstillinger tillater ikke denne endringen + The existing action will be preserved. Den eksisterende handlingen beholdes. + The fonts PdfCraft writes with can't draw “{c}” ({code}), so it shows as ? in every PDF viewer. The text is saved. Skriftene PdfCraft skriver med kan ikke tegne «{c}» ({code}), så det vises som ? i alle PDF-visere. Teksten blir lagret. + The key is in the Windows certificate store, which may ask to allow PdfCraft to use it. Nøkkelen ligger i sertifikatlageret i Windows, som kan be om å tillate at PdfCraft bruker den. + The key is in the macOS Keychain, which may ask to allow PdfCraft to use it. Nøkkelen ligger i nøkkelringen i macOS, som kan be om å tillate at PdfCraft bruker den. + The newest {n} of {total} PDFs De nyeste {n} av {total} PDF-er + The password is only used to read the file; it isn't saved. Passordet brukes bare til å lese filen; det lagres ikke. + The printer driver's own settings for this print: paper tray, paper type, finishing and more. Skriverdriverens egne innstillinger for denne utskriften: papirskuff, papirtype, etterbehandling og mer. + The replacement file doesn't have that many pages. Erstatningsfilen har ikke så mange sider. + The same password is tried on each file. Samme passord prøves på hver fil. + The signature will be invisible. Signaturen blir usynlig. + The text is the same Teksten er lik + The value is shown as typed. Verdien vises slik den er skrevet. + There are no redaction marks (mark text, areas or pages first) Det finnes ingen sladdemarkeringer (marker tekst, områder eller sider først) + These files can't be read, so they won't be in the combined PDF: Disse filene kan ikke leses, så de blir ikke med i den sammenslåtte PDF-en: + These objects could not be moved. Select them again and try a smaller move. Disse objektene kunne ikke flyttes. Velg dem på nytt og prøv en mindre flytting. + Thick Tykk + Thickness Tykkelse + Thin Tynn + This area has too many overlapping paths; intersection snapping is limited. Dette området har for mange overlappende baner; festing til skjæringspunkter er begrenset. + This certificate is in your list of trusted certificates. Dette sertifikatet er i listen din over klarerte sertifikater. + This certificate is not trusted. Signatures made with it show an unknown identity. Dette sertifikatet er ikke klarert. Signaturer laget med det viser en ukjent identitet. + This certificate is trusted through a certificate above it in the chain. Dette sertifikatet er klarert gjennom et sertifikat over det i kjeden. + This comment's appearance can't be changed yet. Utseendet til denne kommentaren kan ikke endres ennå. + This dialog shows the details of a certificate and its chain. Denne dialogen viser detaljene for et sertifikat og kjeden dets. + This document contains {n} interactive form fields. Dette dokumentet inneholder {n} interaktive skjemafelt. + This document has no comments to flatten Dette dokumentet har ingen kommentarer å flate ut + This document has no document-level scripts. Dette dokumentet har ingen skript på dokumentnivå. + This document has no editable structure tags. Dette dokumentet har ingen redigerbare strukturtagger. + This document has no form fields to flatten Dette dokumentet har ingen skjemafelt å flate ut + This document has no form fields you can fill in Dette dokumentet har ingen skjemafelt du kan fylle ut + This document has no pages. Dette dokumentet har ingen sider. + This document has no restrictions to lift Dette dokumentet har ingen begrensninger å oppheve + This document has no signatures. Dette dokumentet har ingen signaturer. + This document has no tagged figures. Dette dokumentet har ingen taggede figurer. + This document has no {kind} to change Dette dokumentet har ingen {kind} å endre + This document hasn't been checked yet. Dette dokumentet er ikke kontrollert ennå. + This document is secured. Some changes are restricted by its security settings. Dette dokumentet er sikret. Noen endringer er begrenset av sikkerhetsinnstillingene. + This document isn't password-protected Dette dokumentet er ikke passordbeskyttet + This drawing exceeds the snapping limit; only the first paths are used. Denne tegningen overskrider festegrensen; bare de første banene brukes. + This field is locked. Unlock it to change its properties. Dette feltet er låst. Lås det opp for å endre egenskapene. + This file changed on disk. Reload it to see the new version; your unsaved changes will be lost. Denne filen er endret på disken. Last den inn på nytt for å se den nye versjonen; ulagrede endringer går tapt. + This file was damaged and has been repaired. Saving keeps the repaired version. Denne filen var skadet og er reparert. Lagring beholder den reparerte versjonen. + This folder isn't available right now. It may have moved, or its drive or cloud sync is offline. Denne mappen er ikke tilgjengelig akkurat nå. Den kan ha blitt flyttet, eller stasjonen eller skysynkroniseringen er frakoblet. + This form's scripts are turned off because one ran too long. Reopen the document to run them again. Skriptene i dette skjemaet er slått av fordi ett kjørte for lenge. Åpne dokumentet på nytt for å kjøre dem igjen. + This is a dynamic XFA form, which PdfCraft can't display yet. What you see is the file's placeholder page. Dette er et dynamisk XFA-skjema, som PdfCraft ikke kan vise ennå. Det du ser er filens plassholderside. + This measurement needs at least {n} points. Denne målingen trenger minst {n} punkter. + This page contains objects that cannot be selected together. Denne siden inneholder objekter som ikke kan velges sammen. + This page couldn't be displayed.\n{e} Denne siden kunne ikke vises.\n{e} + This printer offers no settings of its own. Denne skriveren har ingen egne innstillinger. + This printer's driver doesn't take two-sided settings from other programs, so its own setting applies. Change it in Properties… Driveren til denne skriveren tar ikke imot tosidig-innstillinger fra andre programmer, så dens egen innstilling gjelder. Endre den under Egenskaper… + This web address mixes letters from different alphabets, a common way to imitate another site's name. Denne nettadressen blander bokstaver fra ulike alfabeter, en vanlig måte å etterligne et annet nettsteds navn på. + This web address uses letters from another alphabet, which can look like familiar ones. Denne nettadressen bruker bokstaver fra et annet alfabet, som kan ligne på kjente bokstaver. + Tile scale: Flisskala: + Time Tid + Title Tittel + To change locked fields, also set Locked to No. For å endre låste felt må du også sette Låst til Nei. + To: Til: + Today: {date} I dag: {date} + Toggle fit page / fit width Veksle mellom tilpass til side og tilpass til bredde + Tool Verktøy + Tools panel side Side for verktøypanelet + Tooltip: Verktøytips: + Top Topp + Top-level bookmarks ({n}) Bokmerker på øverste nivå ({n}) + Total Totalt + Trust Stol på + Try the form as people filling it in will see it Prøv skjemaet slik de som fyller det ut vil se det + Two-Up (Cover Page) To sider (forside) + Two-Up (Facing) To sider (motstående) + Two-Up Continuous (Cover Page) To sider, sammenhengende (forside) + Two-Up Continuous (Facing) To sider, sammenhengende (motstående) + Two-sided: Tosidig: + Type Type + Type text Skriv tekst + Type the web address. Skriv inn nettadressen. + Type your {what}. Skriv inn {what}. + URL to submit to URL å sende til + Unit Enhet + Units Enheter + Unknown author Ukjent forfatter + Unlock Lås opp + Unlock {name} Lås opp {name} + Unlock {n} files Lås opp {n} filer + Unlocked: the combined file won't be password-protected Låst opp: den sammenslåtte filen blir ikke passordbeskyttet + Unlock… Lås opp… + Unpin Løsne + Unpin {folder} Løsne {folder} + Unplaced Ikke plassert + Unsupported measurements Målinger som ikke støttes + Up Opp + Update Oppdater + Use 72 DPI (one point per pixel) Bruk 72 DPI (ett punkt per piksel) + Use Flate to encode streams that are not encoded Bruk Flate til å kode strømmer som ikke er kodet + Use a Digital ID from a file Bruk en digital ID fra en fil + Use custom DPI Bruk egendefinert DPI + Use each image's embedded DPI; use 72 DPI when it is absent. Bruk hvert bildes innebygde DPI; bruk 72 DPI når den mangler. + Use image resolution Bruk bildeoppløsning + Use now Bruk nå + Use overlay text Bruk overleggstekst + Used when a PDF doesn't ask for a layout or zoom. Continuous scrolling never snaps between pages. Brukes når en PDF ikke ber om et oppsett eller en zoom. Sammenhengende rulling fester aldri mellom sider. + Used when opening PDFs. An explicit launch or control mode takes precedence for the session. Brukes når PDF-er åpnes. En eksplisitt oppstarts- eller kontrollmodus har forrang for økten. + User Interface Options Alternativer for brukergrensesnitt + User password Brukerpassord + Valid for 5 years, for digital signatures. Self-signed IDs are usually not trusted by others. Gyldig i 5 år, for digitale signaturer. Selvsignerte ID-er er vanligvis ikke klarert av andre. + Validate all Valider alle + Value is not calculated Verdien beregnes ikke + Value is the Verdien er + Value: Verdi: + Verify Verifiser + Verify to see what the document needs. Verifiser for å se hva dokumentet trenger. + Version {v} Versjon {v} + Vertical Loddrett + Vertical reversed Loddrett omvendt + Vertical text cannot be moved with this selection. Loddrett tekst kan ikke flyttes med dette utvalget. + View revision {n} Vis revisjon {n} + View signed version Vis signert versjon + You are viewing revision {rev} of “{from}”. It is a snapshot: changes here won't affect the original. Du ser på revisjon {rev} av «{from}». Dette er et øyeblikksbilde: endringer her påvirker ikke originalen. + You are viewing the signed version of “{from}” (revision {rev}). It is a snapshot: changes here won't affect the original. Du ser på den signerte versjonen av «{from}» (revisjon {rev}). Dette er et øyeblikksbilde: endringer her påvirker ikke originalen. + Viewport Visningsområde + Viewport rectangle in page points (x, y, width, height) Visningsområdets rektangel i sidepunkter (x, y, bredde, høyde) + Visible Rectangle Synlig rektangel + Visual Visuell + Warnings Advarsler + Welcome to PdfCraft Velkommen til PdfCraft + What word or phrase would you like to search for? Hvilket ord eller hvilken frase vil du søke etter? + Whole words only Bare hele ord + Width: Bredde: + Window Options Vindusalternativer + Windows store · Windows-lager · + Write this email? Skrive denne e-posten? + Wrong password Feil passord + XFA form: its fields and its XFA data are kept in step, so other viewers show what you fill in. XFA-skjema: feltene og XFA-dataene holdes i takt, så andre visere viser det du fyller inn. + 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. Du er i ferd med å bruke 1 sladdemarkering. Tekst, bilder og tegninger under markeringene, og kommentarer og skjemafelt som overlapper dem, fjernes permanent. + 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. Du er i ferd med å bruke {n} sladdemarkeringer. Tekst, bilder og tegninger under markeringene, og kommentarer og skjemafelt som overlapper dem, fjernes permanent. + You have version {c}. All releases are listed at {page}. Du har versjon {c}. Alle utgivelser er listet på {page}. + You have version {c}. Download the new version from its release page. Du har versjon {c}. Last ned den nye versjonen fra utgivelsessiden. + Your changes will be lost if you don't save them. Endringene dine går tapt hvis du ikke lagrer dem. + ZIP ZIP + Zoom in Zoom inn + Zoom in ({key}) Zoom inn ({key}) + Zoom in / out (also pinch or {key}-scroll) Zoom inn / ut (også knip eller {key}-rull) + Zoom out Zoom ut + Zoom out ({key}) Zoom ut ({key}) + already a PDF allerede en PDF + and {n} more og {n} til + background bakgrunn + characters tegn + e.g. My stamps f.eks. Mine stempler + e.g. en-US f.eks. nb-NO + for images above for bilder over + header or footer topptekst eller bunntekst + initials initialer + is available er tilgjengelig + is not available yet er ikke tilgjengelig ennå + just now nettopp + yesterday i går + needs an AI provider (off by default) trenger en AI-leverandør (av som standard) + no output folder was chosen ingen utdatamappe ble valgt + no unused output name ikke noe ubrukt utdatanavn + none ingen + of the following fields: av følgende felt: + AI assistant AI-assistent + Action Wizard Handlingsveiviser + Action Wizard… Handlingsveiviser… + Actions list Handlingsliste + Add Bates numbering… Legg til Bates-nummerering… + Add a button Legg til en knapp + Add a callout Legg til en bildeforklaring + Add a checkbox Legg til en avkrysningsboks + Add a date field Legg til et datofelt + Add a digital signature field Legg til et felt for digital signatur + Add a drop-down list Legg til en nedtrekksliste + Add a list box Legg til en listeboks + Add a radio button Legg til en alternativknapp + Add a stamp Legg til et stempel + Add a sticky note Legg til en notatlapp + Add a text box Legg til en tekstboks + Add a text field Legg til et tekstfelt + Add alternate text Legg til alternativ tekst + Add alternate text… Legg til alternativ tekst… + Add an image field Legg til et bildefelt + Add background… Legg til bakgrunn… + Add comments Legg til kommentarer + Add content Legg til innhold + Add files to combine Legg til filer som skal slås sammen + Add form components Legg til skjemakomponenter + Add header & footer… Legg til topptekst og bunntekst… + Add image… Legg til bilde… + Add or edit links Legg til eller rediger lenker + Add printer marks Legg til skrivermerker + Add search index Legg til søkeindeks + Add text Legg til tekst + Add watermark… Legg til vannmerke… + Advanced optimization Avansert optimalisering + Advanced options Avanserte alternativer + Apply PDF standards Bruk PDF-standarder + Apply redactions… Utfør sladding… + Archive documents Arkiver dokumenter + Area Areal + Arrow Pil + ArtCraft website ArtCraft-nettsted + Ask about this document Spør om dette dokumentet + Attach a file Legg ved en fil + Attach file Legg ved fil + Audit space usage Analyser plassbruk + Automatically tag PDF Tagg PDF automatisk + Background Bakgrunn + Bates numbering Bates-nummerering + Blank page Tom side + Bring your own model (off by default) Bruk din egen modell (av som standard) + Button Knapp + Certificates Sertifikater + Certify (invisible signature) Sertifiser (usynlig signatur) + Certify (visible signature) Sertifiser (synlig signatur) + Change initials Endre initialer + Change reading options Endre lesealternativer + Change reading options… Endre lesealternativer… + Change signature Endre signatur + Check for accessibility Sjekk tilgjengelighet + Check for accessibility… Sjekk tilgjengelighet… + Checkbox Avkrysningsboks + Checkmark Hake + Clear form Tøm skjema + Clear redaction marks Fjern sladdemarkeringer + Clipboard Utklippstavle + Cloud Sky + Column select Kolonnemerking + Combine Slå sammen + Combine files Slå sammen filer + Comment list Kommentarliste + Comments panel Kommentarpanel + Compare Sammenlign + Compare files Sammenlign filer + Compare files… Sammenlign filer… + Compress a PDF Komprimer en PDF + Console Konsoll + Convert colors Konverter farger + Copy pages Kopier sider + Correct recognized text Korriger gjenkjent tekst + Create a PDF Opprett en PDF + Create from Opprett fra + Create links from URLs Opprett lenker fra URL-er + Crop pages Beskjær sider + Cross Kryss + Cut pages Klipp ut sider + Dark gray Mørk grå + Date Dato + Date field Datofelt + Detect form fields Oppdag skjemafelt + Digital signature Digital signatur + Digitally sign Signer digitalt + Distance Avstand + Document JavaScripts Dokument-JavaScript + Document JavaScripts… Dokument-JavaScript… + Document actions Dokumenthandlinger + Dot Prikk + Draw a cloud Tegn en sky + Draw a line Tegn en linje + Draw a polygon Tegn en mangekant + Draw a rectangle Tegn et rektangel + Draw an arrow Tegn en pil + Draw an oval Tegn en ellipse + Draw connected lines Tegn sammenhengende linjer + Draw freehand Tegn på frihånd + Drawing Tegning + Drawing scale Tegneskala + Drop-down list Nedtrekksliste + Duplicate pages Dupliser sider + Edit a PDF Rediger en PDF + Edit text & images Rediger tekst og bilder + Encrypt with certificate Krypter med sertifikat + Enhance camera image Forbedre kamerabilde + Enhance scanned file Forbedre skannet fil + Enter permissions password… Skriv inn tillatelsespassord… + Erase drawings Visk ut tegninger + Export a PDF Eksporter en PDF + Export all comments to data file… Eksporter alle kommentarer til datafil… + Export all images Eksporter alle bilder + Export all images… Eksporter alle bilder… + Export data Eksporter data + Export form data… Eksporter skjemadata… + Export measurements Eksporter målinger + Export measurements as CSV Eksporter målinger som CSV + Export to Eksporter til + Export to PowerPoint… Eksporter til PowerPoint… + Extract Trekk ut + Extract pages Trekk ut sider + Field list Feltliste + Field properties Feltegenskaper + Field properties… Feltegenskaper… + Fields Felt + Fill Fyll + Fill & Sign: add text Fyll ut og signer: legg til tekst + Fill & Sign: change initials Fyll ut og signer: endre initialer + Fill & Sign: change signature Fyll ut og signer: endre signatur + Fill & Sign: checkmark Fyll ut og signer: hake + Fill & Sign: cross Fyll ut og signer: kryss + Fill & Sign: date Fyll ut og signer: dato + Fill & Sign: dot Fyll ut og signer: prikk + Fill & Sign: initials Fyll ut og signer: initialer + Fill & Sign: line Fyll ut og signer: linje + Fill & Sign: remove saved initials Fyll ut og signer: fjern lagrede initialer + Fill & Sign: remove saved signature Fyll ut og signer: fjern lagret signatur + Fill & Sign: sign Fyll ut og signer: signer + Find text and redact Finn tekst og sladd + Find text and redact… Finn tekst og sladd… + Fit to width scrolling Rulling tilpasset til bredde + Fit visible Tilpass synlig + Fix hairlines Reparer hårlinjer + Fix reading order Rett opp leserekkefølgen + Flatten comments Flat ut kommentarer + Flatten form fields Flat ut skjemafelt + Form fields panel Skjemafeltpanel + Geospatial location Geografisk plassering + Go to page… Gå til side… + HTML web page HTML-nettside + Header and footer Topptekst og bunntekst + Hide all comments Skjul alle kommentarer + Highlight text Uthev tekst + Image Bilde + Image (PNG) Bilde (PNG) + Image field Bildefelt + Images Bilder + Import comments… Importer kommentarer… + Import data Importer data + Import form data… Importer skjemadata… + In multiple files I flere filer + In this file I denne filen + Index Indeks + Initials Initialer + Insert Sett inn + Insert pages Sett inn sider + Insert text Sett inn tekst + JavaScript JavaScript + JavaScript console… JavaScript-konsoll… + Join the ArtCraft Discord Bli med i ArtCraft-Discorden + Light gray Lys grå + Line Linje + Link Lenke + List box Listeboks + Make accessible Gjør tilgjengelig + Manage embedded index Administrer innebygd indeks + Markup Markering + Marquee zoom Markeringszoom + Measure area Mål areal + Measure distance Mål avstand + Measure objects Mål objekter + Measure perimeter Mål omkrets + Measurement information Målingsinformasjon + Measurement snapping Målingsfesting + Merge data files into spreadsheet… Slå sammen datafiler til regneark… + Microsoft Word (.docx) Microsoft Word (.docx) + Modify page Endre side + Multiple files Flere filer + New bookmarks from structure Nye bokmerker fra struktur + Open accessibility report Åpne tilgjengelighetsrapport + Optimize scanned documents Optimaliser skannede dokumenter + Optimize scanned documents… Optimaliser skannede dokumenter… + Output preview Forhåndsvisning av utdata + Oval Ellipse + PDF/A… PDF/A… + Page grid Siderutenett + Page labels Sideetiketter + Page options Sidealternativer + Page transitions Sideoverganger + Paste pages Lim inn sider + PdfCraft on GitHub PdfCraft på GitHub + PdfCraft web page PdfCraft-nettside + Perimeter Omkrets + Pin folder to Home… Fest mappe til Hjem… + PostScript / EPS PostScript / EPS + Preflight Preflight + Prepare for accessibility Forbered for tilgjengelighet + Prepare for distribution Forbered for distribusjon + Prepare for distribution… Forbered for distribusjon… + Presentation (.pptx) Presentasjon (.pptx) + Print production Trykkproduksjon + Protect a PDF Beskytt en PDF + Protect with password Beskytt med passord + Radio button Alternativknapp + Recognize text Gjenkjenn tekst + Recognize text in multiple files… Gjenkjenn tekst i flere filer… + Recognize text… Gjenkjenn tekst… + Rectangle Rektangel + Redact a PDF Sladd en PDF + Redact pages Sladd sider + Redact pages… Sladd sider… + Redact text and images Sladd tekst og bilder + Redaction properties… Sladdingsegenskaper… + Reduce file size Reduser filstørrelse + Remove all links Fjern alle lenker + Remove background Fjern bakgrunn + Remove header & footer Fjern topptekst og bunntekst + Remove hidden information Fjern skjult informasjon + Remove hidden information… Fjern skjult informasjon… + Remove watermark Fjern vannmerke + Replace Erstatt + Replace text Erstatt tekst + Reset panel layout Tilbakestill paneloppsett + Review Gjennomgang + Rich Text Format (.rtf) Rik tekst-format (.rtf) + Rotate Roter + Rotate pages Roter sider + Rotate pages clockwise Roter sider med klokken + Rotate pages counterclockwise Roter sider mot klokken + Sanitize document Rens dokument + Sanitize document… Rens dokument… + Save as PDF/A Lagre som PDF/A + Save as PDF/UA Lagre som PDF/UA + Save as PDF/X Lagre som PDF/X + Scan & OCR Skann og OCR + Security properties Sikkerhetsegenskaper + Select files to compare Velg filer som skal sammenlignes + Separate PDFs Del opp PDF-er + Set measurement scale Angi målestokk + Set page boxes Angi sidebokser + Set page boxes… Angi sidebokser… + Set properties Angi egenskaper + Show cover page in two-page view Vis forside i tosidevisning + Signature Signatur + Signatures panel Signaturpanel + Single file Én fil + Snapping Festing + Split Del opp + Spreadsheet (.xlsx) Regneark (.xlsx) + Squiggly underline Bølgete understreking + Squiggly underline text Bølgete understreking av tekst + Stamp Stempel + Stamp palette Stempelpalett + Stamps Stempler + Standards Standarder + Sticky note Notatlapp + Strikethrough text Gjennomstreking av tekst + Summarize Oppsummer + Summarize comments Oppsummer kommentarer + Switch light / dark theme Bytt mellom lyst og mørkt tema + Tab order by columns Tabulatorrekkefølge etter kolonner + Tab order by rows Tabulatorrekkefølge etter rader + Tab order: by columns Tabulatorrekkefølge: etter kolonner + Tab order: by document structure Tabulatorrekkefølge: etter dokumentstruktur + Tab order: by rows Tabulatorrekkefølge: etter rader + Tags panel Taggpanel + Text (plain) Tekst (ren) + Text box Tekstboks + Text field Tekstfelt + Timestamp Tidsstempel + Translate Oversett + Underline text Understrek tekst + Update background… Oppdater bakgrunn… + Update header & footer… Oppdater topptekst og bunntekst… + Update watermark… Oppdater vannmerke… + Use JavaScript Bruk JavaScript + Use a certificate Bruk et sertifikat + Use guided actions Bruk veiledede handlinger + Use print production Bruk trykkproduksjon + Use system setting Bruk systeminnstilling + Validate all signatures Valider alle signaturer + Watermark Vannmerke + Move object Flytt objekt + Move objects Flytt objekter + All tools Alle verktøy + Read Les + E-Sign E-signer + Tools Verktøy + Inspector Inspektør + on page {label} på side {label} + A font isn't embedded En skrift er ikke innebygd + A {} action isn't allowed En {}-handling er ikke tillatt + A {} annotation has additional actions En {}-merknad har tilleggshandlinger + A {} annotation has no appearance stream En {}-merknad mangler utseendestrøm + A {} annotation isn't set to print, or is hidden En {}-merknad er ikke satt til å skrives ut, eller er skjult + Accessibility Report Tilgjengelighetsrapport + Add the PDFs to combine Legg til PDF-ene som skal slås sammen + Added the first {n} PDFs in {folder} La til de første {n} PDF-ene i {folder} + CRL Signing CRL-signering + Certificate Signing Sertifikatsignering + Change Author Endre forfatter + Change Keywords Endre nøkkelord + Change Subject Endre emne + Change Title Endre tittel + Choose a digital ID. Velg en digital ID. + Create links Opprett lenker + Creator Opprettet av + Cut and stack needs Two-sided: Off. Print single-sided sheets. Klipp og stable krever Tosidig: Av. Skriv ut ensidige ark. + Data Encipherment Datakryptering + Decipher Only Kun dekryptering + Detailed Report Detaljert rapport + DeviceCMYK is used, but the PDF/A output intent isn't a CMYK profile DeviceCMYK brukes, men PDF/A-utdataintensjonen er ikke en CMYK-profil + DeviceRGB is used, but the PDF/A output intent isn't an RGB profile DeviceRGB brukes, men PDF/A-utdataintensjonen er ikke en RGB-profil + Digital Signature Digital signatur + Document properties (⌘D) Dokumentegenskaper (⌘D) + Drop PDFs or folders here, or choose them. Slipp PDF-er eller mapper her, eller velg dem. + Encipher Only Kun kryptering + English Engelsk + Enter a URL Skriv inn en URL + Enter a name. Skriv inn et navn. + Enter a page number Skriv inn et sidetall + Enter the field names Skriv inn feltnavnene + Every PDF in the folder Alle PDF-er i mappen + Every PDF in the folder and the folders inside it Alle PDF-er i mappen og mappene i den + Field "{}" on page {p} has no description (tooltip) Feltet «{}» på side {p} mangler beskrivelse (verktøytips) + Filename Filnavn + Flatten Fill & Sign Flat ut Fyll ut og signer + Font (unnamed) on page {p} doesn't map its characters to Unicode Skrift (uten navn) på side {p} tilordner ikke tegnene sine til Unicode + Font {} on page {p} doesn't map its characters to Unicode Skriften {} på side {p} tilordner ikke tegnene sine til Unicode + Form field on page {p} isn't tagged Skjemafelt på side {p} er ikke tagget + H{n} follows an H{d}; headings should not skip levels H{n} følger etter en H{d}; overskrifter bør ikke hoppe over nivåer + Inserted text Innsatt tekst + Key Agreement Nøkkelavtale + Key Encipherment Nøkkelkryptering + Margins (in) Marger (tommer) + New bookmark (⌘B) Nytt bokmerke (⌘B) + Next (⌘G) Neste (⌘G) + No PDFs found in {folder} Fant ingen PDF-er i {folder} + No document language is set Ingen dokumentspråk er angitt + No page has text: the document appears to be images of pages Ingen sider har tekst: dokumentet ser ut til å være bilder av sider + Non-Repudiation Ikke-benekting + Only the document's owner can change its security (open it with the permissions password) Bare dokumentets eier kan endre sikkerheten (åpne det med tillatelsespassordet) + Open a PDF (⌘O) Åpne en PDF (⌘O) + Page {p} doesn't tab in structure order Side {p} tabulerer ikke i strukturrekkefølge + Page {p}: {n} untagged content item Side {p}: {n} utagget innholdselement + Page {p}: {n} untagged content items Side {p}: {n} utaggede innholdselementer + Page {} has additional actions Side {} har tilleggshandlinger + Panel layout could not be restored: {error} Paneloppsettet kunne ikke gjenopprettes: {error} + Panel move was not applied: {error} Panelflyttingen ble ikke utført: {error} + PdfCraft can't check this signature yet: {}. PdfCraft kan ikke kontrollere denne signaturen ennå: {}. + Previous (⇧⌘G) Forrige (⇧⌘G) + Print (⌘P) Skriv ut (⌘P) + Producer Produsent + Redo {action} Gjør om {action} + Rendering… Gjengir… + Report created Rapport opprettet + Rev. {rev}: Certified by an unknown signer Rev. {rev}: Sertifisert av en ukjent undertegner + Rev. {rev}: Certified by {name} Rev. {rev}: Sertifisert av {name} + Rev. {rev}: Signed by an unknown signer Rev. {rev}: Signert av en ukjent undertegner + Rev. {rev}: Signed by {name} Rev. {rev}: Signert av {name} + Rotate view clockwise (⇧⌘+) Roter visningen med klokken (⇧⌘+) + Rule Name Regelnavn + Save (M4) Lagre (M4) + Save as {level} Lagre som {level} + Signature is invalid Signaturen er ugyldig + Signature is valid Signaturen er gyldig + Signature validity is unknown Signaturens gyldighet er ukjent + Signing time is from the clock on the signer's computer. Signeringstidspunktet kommer fra klokken på undertegnerens datamaskin. + Still rendering — this page is unusually complex… Gjengir fortsatt – denne siden er uvanlig kompleks… + The ECDSA signature is not in canonical DER (trailing bytes, padding or a missing sign byte); it was read anyway. ECDSA-signaturen er ikke i kanonisk DER (etterfølgende byte, utfylling eller manglende fortegnsbyte); den ble lest likevel. + The ECDSA signature is the raw r and s values instead of a DER sequence; it was read anyway. ECDSA-signaturen er de rå r- og s-verdiene i stedet for en DER-sekvens; den ble lest likevel. + The RSA signature value does not have the length of the key's modulus (leading zero bytes dropped or added); it was read anyway. RSA-signaturverdien har ikke samme lengde som nøkkelens modulus (innledende nullbyte er fjernet eller lagt til); den ble lest likevel. + The RSA signature's DigestInfo has no NULL parameter (older signers write it so); it was read anyway. RSA-signaturens DigestInfo mangler NULL-parameter (eldre signeringsprogrammer skriver den slik); den ble lest likevel. + The certificate of {} does not allow this many CA certificates below it (path length), so it is not used to vouch for {}. Sertifikatet til {} tillater ikke så mange CA-sertifikater under seg (stilengde), så det brukes ikke til å gå god for {}. + The certificate of {} is not a CA certificate that may issue certificates, so it is not used to vouch for {}. Sertifikatet til {} er ikke et CA-sertifikat som kan utstede sertifikater, så det brukes ikke til å gå god for {}. + The certificate of {} was not valid at the time of signing, so it is not used to vouch for {}. Sertifikatet til {} var ikke gyldig på signeringstidspunktet, så det brukes ikke til å gå god for {}. + The checker found problems which may prevent the document from being fully accessible. Kontrollen fant problemer som kan hindre at dokumentet er fullt tilgjengelig. + The document has been altered or corrupted since the signature was applied. Dokumentet er endret eller skadet etter at signaturen ble påført. + The document has been altered or corrupted since the timestamp was applied. Dokumentet er endret eller skadet etter at tidsstempelet ble påført. + The document has been altered since this signature was applied in ways it does not permit ({}). Dokumentet er endret etter at denne signaturen ble påført, på måter den ikke tillater ({}). + The document has been altered since this timestamp was applied in ways it does not permit ({}). Dokumentet er endret etter at dette tidsstempelet ble påført, på måter det ikke tillater ({}). + The document has been modified since this signature was applied, but the changes are permitted ({}). Dokumentet er endret etter at denne signaturen ble påført, men endringene er tillatt ({}). + The document has been modified since this timestamp was applied, but the changes are permitted ({}). Dokumentet er endret etter at dette tidsstempelet ble påført, men endringene er tillatt ({}). + The document has no tags (structure tree) Dokumentet har ingen tagger (strukturtre) + The document has no title Dokumentet har ingen tittel + The document isn't marked as tagged (MarkInfo Marked) Dokumentet er ikke merket som tagget (MarkInfo Marked) + The document isn't tagged, so none of its content is Dokumentet er ikke tagget, så ingenting av innholdet er det + The document's embedded revocation information shows that the signer's certificate has not been revoked. Dokumentets innebygde tilbakekallingsinformasjon viser at undertegnerens sertifikat ikke er tilbakekalt. + The embedded timestamp token is valid and its authority is trusted; trusted time is {}. Det innebygde tidsstempeltokenet er gyldig og utstederen er klarert; klarert tid er {}. + The font {} isn't embedded Skriften {} er ikke innebygd + The metadata doesn't identify the file as {} Metadataene identifiserer ikke filen som {} + The password is incorrect. Passordet er feil. + The password must have at least 6 characters. Passordet må ha minst 6 tegn. + The passwords do not match. Passordene stemmer ikke overens. + The security settings don't allow screen readers to read the text Sikkerhetsinnstillingene tillater ikke at skjermlesere leser teksten + The signature algorithm and the SignerInfo's digest algorithm name different hashes; the signature was made with the SignerInfo's. Signaturalgoritmen og SignerInfo-ens sammendragsalgoritme navngir ulike hasher; signaturen ble laget med SignerInfo-ens. + The signature carries an unverified timestamp ({}): the timestamp authority is not in your list of trusted certificates, so the signing time claimed by the signer is used instead. Signaturen har et uverifisert tidsstempel ({}): tidsstempelutstederen er ikke i listen din over klarerte sertifikater, så signeringstidspunktet undertegneren oppgir brukes i stedet. + The signature contents are not hexadecimal. Signaturinnholdet er ikke heksadesimalt. + The signature could not be checked ({}). Signaturen kunne ikke kontrolleres ({}). + The signature could not be read ({}). Signaturen kunne ikke leses ({}). + The signature could not be verified ({}). Signaturen kunne ikke verifiseres ({}). + The signature has no valid byte range. Signaturen har ikke noe gyldig byteområde. + The signature includes an embedded timestamp. Signaturen inneholder et innebygd tidsstempel. + The signature is BER-encoded (indefinite lengths) rather than DER; it was read leniently. Signaturen er BER-kodet (ubestemte lengder) i stedet for DER; den ble lest lempelig. + The signature uses the legacy adbe.x509.rsa_sha1 format. Signaturen bruker det eldre formatet adbe.x509.rsa_sha1. + The signature value does not match the signer's certificate: the signature is corrupt. Signaturverdien stemmer ikke med undertegnerens sertifikat: signaturen er skadet. + The signature's byte range does not exclude exactly its contents. Signaturens byteområde utelater ikke nøyaktig innholdet sitt. + The signature's byte range does not match the file. Signaturens byteområde stemmer ikke med filen. + The signature's timestamp token could not be verified ({}). Signaturens tidsstempeltoken kunne ikke verifiseres ({}). + The signature's timestamp token does not cover the signature value. Signaturens tidsstempeltoken dekker ikke signaturverdien. + The signed content is a constructed (segmented) OCTET STRING, as BER allows; it was read anyway. Det signerte innholdet er en konstruert (segmentert) OCTET STRING, slik BER tillater; det ble lest likevel. + The signer's certificate does not allow digital signatures (its key usage has neither digital signature nor non-repudiation). Undertegnerens sertifikat tillater ikke digitale signaturer (nøkkelbruken har verken digital signatur eller ikke-benekting). + The signer's certificate has been revoked ({}). Undertegnerens sertifikat er tilbakekalt ({}). + The signer's certificate has critical extensions PdfCraft does not recognize ({}). Undertegnerens sertifikat har kritiske utvidelser som PdfCraft ikke kjenner igjen ({}). + The signer's certificate is not in the signature. Undertegnerens sertifikat finnes ikke i signaturen. + The signer's certificate is not issued for signing documents (its extended key usage has no document-signing purpose). Undertegnerens sertifikat er ikke utstedt for signering av dokumenter (den utvidede nøkkelbruken har ikke noe formål for dokumentsignering). + The signer's certificate was not valid at the time of signing. Undertegnerens sertifikat var ikke gyldig på signeringstidspunktet. + The signer's identity is unknown because it has not been included in your list of trusted certificates and none of its parent certificates are trusted certificates. Undertegnerens identitet er ukjent fordi den ikke er tatt med i listen din over klarerte sertifikater, og ingen av de overordnede sertifikatene er klarerte sertifikater. + The signer's identity is valid. Undertegnerens identitet er gyldig. + The timestamp authority's certificate was not valid at the time of timestamping. Tidsstempelutstederens sertifikat var ikke gyldig på tidsstemplingstidspunktet. + The timestamp contents are not hexadecimal. Tidsstempelinnholdet er ikke heksadesimalt. + The timestamp has no valid byte range. Tidsstempelet har ikke noe gyldig byteområde. + The timestamp token could not be read ({}). Tidsstempeltokenet kunne ikke leses ({}). + The timestamp token is valid and its authority is trusted. Tidsstempeltokenet er gyldig og utstederen er klarert. + The timestamp token is valid, but the timestamp authority is not in your list of trusted certificates. Tidsstempeltokenet er gyldig, men tidsstempelutstederen er ikke i listen din over klarerte sertifikater. + The timestamp's byte range does not exclude exactly its contents. Tidsstempelets byteområde utelater ikke nøyaktig innholdet sitt. + The timestamp's byte range does not match the file. Tidsstempelets byteområde stemmer ikke med filen. + The window shows the file name, not the title (Initial View > Show) Vinduet viser filnavnet, ikke tittelen (Startvisning > Vis) + There is no folder to save the digital ID in. Det finnes ingen mappe å lagre den digitale ID-en i. + This document has not been modified since this signature was applied. Dette dokumentet er ikke endret etter at denne signaturen ble påført. + This document has not been modified since this timestamp was applied. Dette dokumentet er ikke endret etter at dette tidsstempelet ble påført. + This signature uses RIPEMD-160, a legacy 160-bit hash algorithm that is no longer recommended for signatures. It still validates, but it should not be relied on. Denne signaturen bruker RIPEMD-160, en eldre 160-bits hashalgoritme som ikke lenger anbefales for signaturer. Den validerer fortsatt, men bør ikke stoles på. + This signature uses SHA-1, a weak hash algorithm: collisions in it have been demonstrated. It still validates because many documents signed years ago use it, but it should not be relied on. Denne signaturen bruker SHA-1, en svak hashalgoritme: kollisjoner i den er påvist. Den validerer fortsatt fordi mange dokumenter signert for flere år siden bruker den, men bør ikke stoles på. + Undo {action} Angre {action} + Unsigned signature field Usignert signaturfelt + Unsigned signature field: {name} Usignert signaturfelt: {name} + Zoom in (⌘+) Zoom inn (⌘+) + Zoom in / out (also pinch or ⌘-scroll) Zoom inn / ut (også knip eller ⌘-rulling) + Zoom out (⌘−) Zoom ut (⌘−) + alternate text hides an annotation inside it alternativ tekst skjuler en merknad inni seg + alternate text on an element with no page content alternativ tekst på et element uten sideinnhold + comments kommentarer + document security store dokumentets sikkerhetslager + document structure dokumentstruktur + form fill skjemautfylling + its alternate text is inside another element's and will never be read den alternative teksten ligger inni et annet elements og vil aldri bli lest + links lenker + metadata metadata + no alternate text ingen alternativ tekst + no header cells (TH) ingen overskriftsceller (TH) + no summary ikke noe sammendrag + not in a TR ikke i en TR + not in a Table, THead, TBody or TFoot ikke i en Table, THead, TBody eller TFoot + not in an L (list) ikke i en L (liste) + not in an LI (list item) ikke i en LI (listeelement) + other changes andre endringer + page content sideinnhold + page {n} is out of range (1–{count}) side {n} er utenfor området (1–{count}) + pages added or removed sider lagt til eller fjernet + rows have between {min} and {max} columns radene har mellom {min} og {max} kolonner + the first heading is an H{n}; headings should not skip levels den første overskriften er en H{n}; overskrifter bør ikke hoppe over nivåer + the signed version could not be read den signerte versjonen kunne ikke leses + {n} pages and no bookmarks {n} sider og ingen bokmerker + {page} is not a page number or label {page} er ikke et sidetall eller en sideetikett + {ty} element on page {p}: {what} {ty}-element på side {p}: {what} + {ty} element: {what} {ty}-element: {what} + {who}, certificate issued by an unknown issuer. {who}, sertifikat utstedt av en ukjent utsteder. + {who}, certificate issued by {issuer}. {who}, sertifikat utstedt av {issuer}. + {} annotation on page {p} isn't tagged {}-merknad på side {p} er ikke tagget + {} annotations aren't allowed {}-merknader er ikke tillatt + … and {n} more … og {n} til + A button En knapp + A diagonal CONFIDENTIAL watermark on every page. Et diagonalt KONFIDENSIELT-vannmerke på hver side. + A document needs at least one page: copy instead Et dokument trenger minst én side: kopier i stedet + A form field has additional actions Et skjemafelt har tilleggshandlinger + A link En lenke + A script Et skript + A selected field has no widget on any page, so the fields can't be edited together. Et valgt felt har ingen kontroll på noen side, så feltene kan ikke redigeres sammen. + A stream refers to an external file En strøm viser til en ekstern fil + A stream uses LZW compression En strøm bruker LZW-komprimering + A summary describes the table's purpose and layout. Add a Summary attribute to each table. Et sammendrag beskriver tabellens formål og oppsett. Legg til et Summary-attributt på hver tabell. + About Om + Accepted Godtatt + Accessibility permission flag Tilgjengelighetstillatelsesflagg + Accessibility permission flag is set Tilgjengelighetstillatelsesflagget er satt + Add Page Numbers Legg til sidetall + Add arrow Legg til pil + Add background Legg til bakgrunn + Add bookmark Legg til bokmerke + Add callout Legg til bildeforklaring + Add checkmark Legg til hake + Add cloud Legg til sky + Add connected lines Legg til sammenkoblede linjer + Add cross Legg til kryss + Add dot Legg til prikk + Add drawing Legg til tegning + Add field Legg til felt + Add file attachment Legg til filvedlegg + Add footer Legg til bunntekst + Add header Legg til topptekst + Add header & footer Legg til topptekst og bunntekst + Add highlight Legg til utheving + Add image Legg til bilde + Add inserted text Legg til innsatt tekst + Add line Legg til linje + Add link Legg til lenke + Add oval Legg til oval + Add polygon Legg til polygon + Add rectangle Legg til rektangel + Add redaction mark Legg til sladdemerke + Add signature Legg til signatur + Add squiggly underline Legg til bølgete understreking + Add stamp Legg til stempel + Add sticky note Legg til notatlapp + Add strikethrough Legg til gjennomstreking + Add text box Legg til tekstboks + Add underline Legg til understreking + Add watermark Legg til vannmerke + Add {what} Legg til {what} + Added the first {n} files in {folder} La til de første {n} filene i {folder} + Adding images arrives on the web with file pickers for images Å legge til bilder kommer på nettet med filvelgere for bilder + Algorithm Algoritme + Align fields Juster felt + All annotations are tagged Alle merknader er tagget + All form fields are tagged Alle skjemafelt er tagget + All form fields have description Alle skjemafelt har beskrivelse + All multimedia objects are tagged Alle multimedieobjekter er tagget + All page content is tagged Alt sideinnhold er tagget + Allowed Tillatt + Alternate Text Alternativ tekst + Alternate text inside an element that already has alternate text is never read. Remove the inner alternate text. Alternativ tekst inne i et element som allerede har alternativ tekst, blir aldri lest. Fjern den indre alternative teksten. + Alternate text must be associated with some content Alternativ tekst må være knyttet til noe innhold + Alternate text on an element that contains a link or field hides that annotation from assistive technology. Move the alternate text. Alternativ tekst på et element som inneholder en lenke eller et felt, skjuler merknaden for hjelpeteknologi. Flytt den alternative teksten. + Alternate text on an element without page content is never reached. Remove it or attach the element to content. Alternativ tekst på et element uten sideinnhold nås aldri. Fjern den, eller knytt elementet til innhold. + Alternate text should not hide annotation Alternativ tekst bør ikke skjule merknader + Alternate text that will never be read Alternativ tekst som aldri blir lest + An action is already running En handling kjører allerede + An embedded file has no AFRelationship En innebygd fil mangler AFRelationship + An image asks to be interpolated Et bilde ber om å bli interpolert + An image has alternates or OPI information Et bilde har alternativer eller OPI-informasjon + Animation that flashes can cause seizures. Check scripts and media for flicker. Animasjon som blinker, kan utløse anfall. Sjekk skript og medier for flimring. + Another file dialog is still open. Finish with it first. En annen fildialog er fortsatt åpen. Fullfør den først. + Application Program + Applied 1 redaction mark. Save to remove the content from the file. Brukte 1 sladdemerke. Lagre for å fjerne innholdet fra filen. + Applied {n} redaction marks. Save to remove the content from the file. Brukte {n} sladdemerker. Lagre for å fjerne innholdet fra filen. + Appropriate nesting Riktig nesting + Arbitrary Mask Vilkårlig maske + Associated with content Knyttet til innhold + At least one signature has problems. Minst én signatur har problemer. + At least one signature is invalid. Minst én signatur er ugyldig. + Attachment Vedlegg + Basic constraints Grunnleggende begrensninger + Beveled Skråkant + Black Svart + Black on white Svart på hvitt + Blue Blå + Bookmarks are present in large documents Bokmerker finnes i store dokumenter + Both Begge + Button Properties Knappegenskaper + Button Style: Knappestil: + Calculate Beregn + Callout Bildeforklaring + Cancelled Avbrutt + Center Footer Text Bunntekst midten + Center Header Text Topptekst midten + Center horizontally Midtstill vannrett + Center vertically Midtstill loddrett + Certificate Viewer Sertifikatvisning + Certified by {by} Sertifisert av {by} + Change comment properties Endre kommentaregenskaper + Change document properties Endre dokumentegenskaper + Change field properties Endre feltegenskaper + Change initial view Endre startvisning + Change link properties Endre lenkeegenskaper + Change tag properties Endre tag-egenskaper + Change {key} Endre {key} + Changing the document Endring av dokumentet + Character encoding Tegnkoding + Character limit: Tegngrense: + Check Hake + Check Box Properties Egenskaper for avkrysningsboks + Check Box Style: Stil for avkrysningsboks: + Check that the tags read in the order a sighted reader would. Use the Reading Order tool or the Tags panel to correct it. Kontroller at taggene leses i den rekkefølgen en seende leser ville brukt. Bruk verktøyet Leserekkefølge eller Tagger-panelet for å rette det. + CheckBox Avkrysningsboks + Checked Avkrysset + Choose a folder for the split files Velg en mappe for de oppdelte filene + Choose a folder on this computer to pin Velg en mappe på denne datamaskinen å feste + Click on the page to add a {tool}, or drag to set its size Klikk på siden for å legge til {tool}, eller dra for å angi størrelsen + Click text or an image to edit it Klikk på tekst eller et bilde for å redigere det + Color Images Fargebilder + Color Spaces Fargerom + Color contrast Fargekontrast + Comb of characters Tegnkam + Combo Kombinasjonsboks + Coming in milestone {m}. Items marked Ready work today. Kommer i milepæl {m}. Elementer merket Klar fungerer i dag. + Command Kommando + Commenting Kommentering + Comments and markups Kommentarer og markeringer + Comments, links and fields must be in the tag tree to be announced. Tag the annotations. Kommentarer, lenker og felt må være i tag-treet for å bli lest opp. Tagg merknadene. + Completed Fullført + Configure a Digital ID for Signing Sett opp en digital ID for signering + Connected lines Sammenkoblede linjer + Content Streams Innholdsstrømmer + Content copying Kopiering av innhold + Content copying for accessibility Kopiering av innhold for tilgjengelighet + Contributors Bidragsytere + Copied {what} Kopierte {what} + Copy or cut pages first Kopier eller klipp ut sider først + Could not save: {e} Kunne ikke lagre: {e} + Couldn't add {name}: {e} Kunne ikke legge til {name}: {e} + Couldn't combine files: {e} Kunne ikke slå sammen filene: {e} + Couldn't create a PDF: {e} Kunne ikke lage en PDF: {e} + Couldn't download {name}: {e} Kunne ikke laste ned {name}: {e} + Couldn't extract pages: {e} Kunne ikke trekke ut sider: {e} + Couldn't fit the visible content: {e} Kunne ikke tilpasse det synlige innholdet: {e} + Couldn't follow linked PDF: {reason} Kunne ikke følge lenket PDF: {reason} + Couldn't import the signature image: {e} Kunne ikke importere signaturbildet: {e} + Couldn't open revision {n}: {e} Kunne ikke åpne revisjon {n}: {e} + Couldn't open the result: {e} Kunne ikke åpne resultatet: {e} + Couldn't open the signed version: {e} Kunne ikke åpne den signerte versjonen: {e} + Couldn't open {name}: {e} Kunne ikke åpne {name}: {e} + Couldn't optimize the file Kunne ikke optimalisere filen + Couldn't optimize the file: {e} Kunne ikke optimalisere filen: {e} + Couldn't print from the browser ({e}); downloaded {name} to print instead Kunne ikke skrive ut fra nettleseren ({e}); lastet ned {name} for utskrift i stedet + Couldn't read {name}: {e} Kunne ikke lese {name}: {e} + Couldn't recover {name}: {e} Kunne ikke gjenopprette {name}: {e} + Couldn't reload {name}: {e} Kunne ikke laste {name} på nytt: {e} + Couldn't save the image: {e} Kunne ikke lagre bildet: {e} + Couldn't save {name}: {e} Kunne ikke lagre {name}: {e} + Couldn't save: {e} Kunne ikke lagre: {e} + Couldn't show the file picker. Please try again. Kunne ikke vise filvelgeren. Prøv igjen. + Couldn't split: {e} Kunne ikke dele opp: {e} + Couldn't summarize comments: {e} Kunne ikke oppsummere kommentarer: {e} + Couldn't take the snapshot: {e} Kunne ikke ta øyeblikksbildet: {e} + Couldn't unlock {name}: {e} Kunne ikke låse opp {name}: {e} + Couldn't use the photo: {e} Kunne ikke bruke bildet: {e} + Couldn't use {name}: {e} Kunne ikke bruke {name}: {e} + Couldn't write {name}: {e} Kunne ikke skrive {name}: {e} + the name is not a plain file name navnet er ikke et vanlig filnavn + Couldn't write {path}: {e} Kunne ikke skrive {path}: {e} + Create link Opprett lenke + Created 1 link Opprettet 1 lenke + Created a PDF from the first {n} files Opprettet en PDF fra de første {n} filene + Created a comment summary Opprettet en kommentaroppsummering + Created {n} links Opprettet {n} lenker + Credit Cards Kredittkort + Crop page Beskjær side + Custom font Egendefinert skrift + Custom stamps arrive on the web with file pickers for images Egendefinerte stempler kommer på nettet med filvelgere for bilder + Cut {what} Klipp ut {what} + Dark gray theme Mørkegrått tema + Dashed Stiplet + Dates Datoer + Delete bookmark Slett bokmerke + Delete content Slett innhold + Delete document JavaScript Slett dokument-JavaScript + Delete field Slett felt + Delete image Slett bilde + Delete link Slett lenke + Delete page Slett side + Delete selected fields Slett valgte felt + Deleted Slettet + Detected 1 form field Fant 1 skjemafelt + Detected {n} form fields Fant {n} skjemafelt + Device colour is used, but there is no PDF/A output intent Enhetsfarge brukes, men det finnes ingen PDF/A-utdataintensjon + DeviceCMYK is used: it needs a CMYK output intent (not added automatically) DeviceCMYK brukes: det krever en CMYK-utdataintensjon (legges ikke til automatisk) + Diagnostic details Diagnostiske detaljer + Digital Signature Properties Egenskaper for digital signatur + Discard all comments, forms and multimedia Forkast alle kommentarer, skjemaer og multimedia + Discard all links, actions and JavaScript Forkast alle lenker, handlinger og JavaScript + Discard all object data (file attachments) Forkast alle objektdata (filvedlegg) + Discard bookmarks Forkast bokmerker + Discard document information and metadata Forkast dokumentinformasjon og metadata + Discard hidden layer content Forkast innhold i skjulte lag + Discard hidden text Forkast skjult tekst + Discard private data of other applications Forkast private data fra andre programmer + Distribute fields Fordel felt + Document Overhead Dokumentoverhead + Document assembly Dokumentsammensetting + Document has appropriate color contrast Dokumentet har tilstrekkelig fargekontrast + Document is not image-only PDF Dokumentet er ikke en PDF med bare bilder + Document is tagged PDF Dokumentet er tagget PDF + Document sanitized. Save to finish: saving rewrites the whole file. Dokumentet er renset. Lagre for å fullføre: lagring skriver hele filen på nytt. + Document structure provides a logical reading order Dokumentstrukturen gir en logisk leserekkefølge + Document title is showing in title bar Dokumenttittelen vises i tittellinjen + Documents of more than 20 pages need bookmarks for navigation. Add bookmarks for the main sections. Dokumenter på mer enn 20 sider trenger bokmerker for navigering. Legg til bokmerker for hovedavsnittene. + Double-click Dobbeltklikk + Downloaded {name} Lastet ned {name} + Downloading attachments on the web arrives with M3.10 ({n} bytes ready) Nedlasting av vedlegg på nettet kommer med M3.10 ({n} byte klare) + Drag a rectangle around the area to copy Dra et rektangel rundt området som skal kopieres + Drag a rectangle on a page to crop it; double-click a page for Set Page Boxes Dra et rektangel på en side for å beskjære den; dobbeltklikk en side for Angi sidebokser + Drag a rectangle to create a link; double-click a link to edit it Dra et rektangel for å opprette en lenke; dobbeltklikk en lenke for å redigere den + Drag the corners onto the document's edges, then choose Enhance page. Dra hjørnene til dokumentets kanter, og velg deretter Forbedre side. + Drag to draw the area where the signature should appear. Dra for å tegne området der signaturen skal vises. + Dropdown Properties Egenskaper for nedtrekksliste + Duplicate field Dupliser felt + Duplicate page Dupliser side + Each field needs a tooltip that says what to enter. Add one in Field Properties > General > Tooltip. Hvert felt trenger et verktøytips som sier hva som skal fylles inn. Legg til ett under Feltegenskaper > Generelt > Verktøytips. + Edit comment Rediger kommentar + Edit content Rediger innhold + Edit document JavaScript Rediger dokument-JavaScript + Edit fields Rediger felt + Edit script of {name} Rediger skript for {name} + Edit text Rediger tekst + Editing Redigering + Email Addresses E-postadresser + Embedded Files Innebygde filer + Embedded files must be PDF/A files themselves (use PDF/A-3 for others) Innebygde filer må selv være PDF/A-filer (bruk PDF/A-3 for andre) + Encryption Kryptering + Endpoint Endepunkt + Erase Visk ut + Eraser Viskelær + Even pages Partallssider + Everything on a page must be tagged or marked as an artifact (decoration, headers, footers). Tag the untagged content. Alt på en side må være tagget eller merket som artefakt (dekor, topptekster, bunntekster). Tagg det utaggede innholdet. + Execute a menu item Kjør et menyelement + Export certificate Eksporter sertifikat + Exported to {path} Eksportert til {path} + Extended Graphics States Utvidede grafikktilstander + Field Felt + Field descriptions Feltbeskrivelser + Figures alternate text Alternativ tekst for figurer + Figures require alternate text Figurer krever alternativ tekst + File attachments Filvedlegg + Files are combined in this order. Leave Pages empty to take every page. Filene slås sammen i denne rekkefølgen. La Sider stå tomt for å ta med alle sidene. + Fill in {name} Fyll ut {name} + Filling of form fields Utfylling av skjemafelt + Flatten Flat ut + Flip image Vend bilde + Fonts must map their characters to Unicode so text can be read and copied. Fix the fonts or recreate the PDF. Skrifter må tilordne tegnene sine til Unicode slik at teksten kan leses og kopieres. Rett skriftene eller lag PDF-en på nytt. + Form fields must be in the tag tree (Form tags) to be announced. Tag the form fields. Skjemafelt må være i tag-treet (Form-tagger) for å bli lest opp. Tagg skjemafeltene. + Formulas and similar non-text elements need alternate text. Add alternate text to them. Formler og lignende elementer som ikke er tekst, trenger alternativ tekst. Legg til alternativ tekst på dem. + Gray Grå + Grayscale Images Gråtonebilder + Green Grønn + Green on black Grønn på svart + H / Space (hold) H / Mellomrom (hold) + Header cells (TH) tell listeners what each column or row holds. Mark the header cells. Overskriftsceller (TH) forteller lytteren hva hver kolonne eller rad inneholder. Merk overskriftscellene. + Headers Topptekster + Heading levels shouldn't skip (an H1 followed by an H3). Retag the headings. Overskriftsnivåer bør ikke hoppe over nivåer (en H1 etterfulgt av en H3). Tagg overskriftene på nytt. + Headings Overskrifter + Hidden layers Skjulte lag + Hidden text Skjult tekst + Hide a field Skjul et felt + Hides annotation Skjuler merknad + High Høy + Home / End Home / End + Horizontally Vannrett + Image-only PDF PDF med bare bilder + Import {name} Importer {name} + Importing data arrives on the web with file pickers for data files Import av data kommer på nettet med filvelgere for datafiler + Incorrect password. Try again. Feil passord. Prøv igjen. + Insert pages from {name} Sett inn sider fra {name} + Insert pages from {name} — 1 field renamed Sett inn sider fra {name} — 1 felt fikk nytt navn + Insert pages from {name} — fields renamed Sett inn sider fra {name} — felt fikk nytt navn + Inserted Satt inn + Inset Innfelt + Intended usage Tiltenkt bruk + Intersection Snitt + Invert Inverter + Irregular tables confuse cell navigation. Give every row the same number of cells (counting spans). Uregelmessige tabeller forvirrer cellenavigering. Gi hver rad like mange celler (medregnet spenn). + Issued by Utstedt av + Issued to Utstedt til + Issuer Utsteder + Key usage Nøkkelbruk + Keywords Nøkkelord + LI must be a child of L LI må være et barn av L + Landscape pages Liggende sider + Lbl and LBody Lbl og LBody + Lbl and LBody must be children of LI Lbl og LBody må være barn av LI + Left Footer Text Bunntekst venstre + Left Header Text Topptekst venstre + Light blue Lyseblå + Links, actions and JavaScripts Lenker, handlinger og JavaScript + List Liste + List Box Properties Egenskaper for listeboks + List items Listeelementer + List items belong in a list. Fix the list's tags. Listeelementer hører hjemme i en liste. Rett listens tagger. + List labels and bodies belong in a list item. Fix the list's tags. Listeetiketter og -innhold hører hjemme i et listeelement. Rett listens tagger. + Lists Lister + Load document font Last inn dokumentskrift + Local cross-PDF links require the desktop app Lokale lenker mellom PDF-er krever skrivebordsappen + Lock comment Lås kommentar + Logical Reading Order Logisk leserekkefølge + Low Lav + Make scans searchable, then make them smaller. Gjør skanninger søkbare, og gjør dem deretter mindre. + Mark all Merk alle + Mark as Confidential Merk som konfidensielt + Mark differences Marker forskjeller + Mark figure as decorative Merk figur som dekorativ + Mark for redaction Merk for sladding + Mark pages for redaction Merk sider for sladding + Match field sizes Tilpass feltstørrelser + Maximum Maksimum + Measuring tool Måleverktøy + Merged 1 file into {path} Slo sammen 1 fil til {path} + Merged {n} files into {path} Slo sammen {n} filer til {path} + Merging data files needs the desktop app Sammenslåing av datafiler krever skrivebordsappen + Metadata Metadata + Midpoint Midtpunkt + Minimum Minimum + Models Modeller + Mouse Down Museknapp ned + Mouse Enter Mus inn + Mouse Exit Mus ut + Mouse Up Museknapp opp + Move bookmark Flytt bokmerke + Move comment Flytt kommentar + Move image Flytt bilde + Move page Flytt side + Move pages Flytt sider + Named Destinations Navngitte mål + Navigation links Navigasjonslenker + Navigation links are not repetitive Navigasjonslenker er ikke repeterende + Nested alternate text Nestet alternativ tekst + New {tool} comments will look like this one Nye {tool}-kommentarer vil se ut som denne + No PDFs, images or text files found in {folder} Fant ingen PDF-er, bilder eller tekstfiler i {folder} + No form fields were detected Ingen skjemafelt ble oppdaget + No inaccessible scripts Ingen utilgjengelige skript + No links to remove Ingen lenker å fjerne + No pages match those choices Ingen sider samsvarer med de valgene + No unlinked web addresses were found in the text Fant ingen nettadresser uten lenke i teksten + None of those files can be made into a PDF; use PDFs, images or .txt files Ingen av de filene kan gjøres om til PDF; bruk PDF-er, bilder eller .txt-filer + Note Notat + Number every page in the footer. Nummerer hver side i bunnteksten. + Odd pages Oddetallssider + On Blur Ved fokustap + On Focus Ved fokus + On the web, open or drop an image to convert it På nettet: åpne eller slipp et bilde for å konvertere det + Open a web link Åpne en nettlenke + Opened with Åpnet med + Opening the browser's print dialog… Åpner nettleserens utskriftsdialog… + Optimization cancelled Optimalisering avbrutt + Optimization is already running Optimalisering kjører allerede + Optimize Scanned Documents Optimaliser skannede dokumenter + Options Alternativer + Orange Oransje + Other elements alternate text Alternativ tekst for andre elementer + Other elements that require alternate text Andre elementer som krever alternativ tekst + Outline Disposisjon + PDF producer PDF-produsent + PDF version PDF-versjon + Page Content Sideinnhold + Page Range Sideområde + Page does not require timed responses Siden krever ikke tidsbegrensede svar + Page thumbnails Miniatyrbilder av sider + Page will not cause screen flicker Siden vil ikke forårsake skjermflimring + Pages that are only pictures of text can't be read aloud or searched. Recognize text (OCR) to add a text layer. Sider som bare er bilder av tekst, kan ikke leses opp eller søkes i. Gjenkjenn tekst (OCR) for å legge til et tekstlag. + Pages with links, comments or fields should tab in structure order. The fix sets each page's tab order to Use Document Structure. Sider med lenker, kommentarer eller felt bør ha tabulatorrekkefølge etter strukturen. Reparasjonen setter hver sides tabulatorrekkefølge til Bruk dokumentstruktur. + Passed Bestått + Password protection will be applied when you save Passordbeskyttelse tas i bruk når du lagrer + Path Bane + PdfCraft is drawing without a graphics processor, so it may be slow. Updating the graphics driver may help. PdfCraft tegner uten grafikkprosessor, så det kan gå tregt. Det kan hjelpe å oppdatere grafikkdriveren. + People may need more time to respond. Check that nothing has a time limit. Noen kan trenge mer tid på å svare. Kontroller at ingenting har en tidsgrense. + Phone Number Telefonnummer + Phone Numbers Telefonnumre + Pictures need alternate text that describes them, or must be marked decorative. Add alternate text to each figure. Bilder trenger alternativ tekst som beskriver dem, eller må merkes som dekorative. Legg til alternativ tekst for hver figur. + Pink Rosa + Pinned folders need the desktop app Festede mapper krever skrivebordsappen + Pinned {folder} to Home Festet {folder} til Hjem + Place saved {what} Plasser lagret {what} + Planned for milestone {m} — open to see what it will include Planlagt for milepæl {m} – åpne for å se hva den vil inneholde + Planned · {m} Planlagt · {m} + Polygon Polygon + Polyline Polylinje + Portable mode is off: {folder} can't be written ({error}). Settings are kept in your user folder instead. Portabel modus er av: {folder} kan ikke skrives til ({error}). Innstillingene lagres i brukermappen din i stedet. + Portrait pages Stående sider + Position Posisjon + PostScript XObjects aren't allowed PostScript-XObjects er ikke tillatt + Prepare for Distribution Klargjør for distribusjon + Preparing sheet {done} of {total} for printing… Klargjør ark {done} av {total} for utskrift… + Preparing {n} sheets for printing… Klargjør {n} ark for utskrift… + Primary language Primærspråk + Printing Skriver ut + Private data of other applications Private data fra andre programmer + Public key Offentlig nøkkel + Purple Lilla + PushButton Trykknapp + Radio Alternativknapp + Radio Button Properties Egenskaper for alternativknapp + Re-type Password Gjenta passord + Red Rød + Redid {label} Gjorde om {label} + Regularity Regelmessighet + Rejected Avvist + Reliable character encoding is provided Pålitelig tegnkoding er angitt + Remove hidden information and flatten comments and form fields, so recipients see only the pages. Fjern skjult informasjon og flat ut kommentarer og skjemafelt, slik at mottakerne bare ser sidene. + Remove hidden information, metadata, scripts and embedded content, and rewrite the files. Fjern skjult informasjon, metadata, skript og innebygd innhold, og skriv filene på nytt. + Remove redaction marks Fjern sladdemerker + Remove saved {what} Fjern lagret {what} + Removed 1 hidden item. Save to remove them from the file. Fjernet 1 skjult element. Lagre for å fjerne det fra filen. + Removed 1 link Fjernet 1 lenke + Removed {n} hidden items. Save to remove them from the file. Fjernet {n} skjulte elementer. Lagre for å fjerne dem fra filen. + Removed {n} links Fjernet {n} lenker + Rename bookmark Gi bokmerke nytt navn + Rendering: hayro (bootstrap) · UI: egui · Icons: Lucide (ISC) · Fonts: Inter, JetBrains Mono, Dancing Script (OFL) Gjengivelse: hayro (oppstart) · Grensesnitt: egui · Ikoner: Lucide (ISC) · Skrifter: Inter, JetBrains Mono, Dancing Script (OFL) + Repairs Reparasjoner + Repeated navigation links should be skippable. Check links that appear on every page. Gjentatte navigasjonslenker bør kunne hoppes over. Kontroller lenker som vises på hver side. + Replace page Erstatt side + Replace pages Erstatt sider + Replaced Erstattet + Replacing images arrives on the web with image file pickers Erstatting av bilder kommer på nettet sammen med bildefilvelgere + Replacing images on page {p} arrives on the web with image pickers ({i}) Erstatting av bilder på side {p} kommer på nettet sammen med bildevelgere ({i}) + Report Options Rapportalternativer + Reset a form Tilbakestill et skjema + Resize comment Endre størrelse på kommentar + Restrictions Begrensninger + Restrictions lifted Begrensninger opphevet + Reverted to the last saved version Gikk tilbake til sist lagrede versjon + Right Footer Text Høyre bunntekst + Right Header Text Høyre topptekst + Rotate image Roter bilde + Rotate page Roter side + Rotate page {label} clockwise Roter side {label} med klokken + Rotate page {label} counterclockwise Roter side {label} mot klokken + Rows Rader + Run Kjør + Run JavaScript Kjør JavaScript + Run a JavaScript Kjør et JavaScript + Run form script Kjør skjemaskript + SHA-1 digest SHA-1-sammendrag + SHA-256 digest SHA-256-sammendrag + Sanitize Rens + Sanitize Documents Rens dokumenter + Save as PDF/A-1b Lagre som PDF/A-1b + Save as PDF/A-2b Lagre som PDF/A-2b + Save as PDF/A-3b Lagre som PDF/A-3b + Save image Lagre bilde + Save or revert changes before selecting another tag. Lagre eller forkast endringene før du velger en annen tagg. + Saved the print-ready PDF to {path} Lagret den utskriftsklare PDF-en til {path} + Saved {name} Lagret {name} + Saved {place}: {before} → {after} (not smaller){detail} Lagret {place}: {before} → {after} (ikke mindre){detail} + Saved {place}: {before} → {after} ({pct}% smaller){detail} Lagret {place}: {before} → {after} ({pct} % mindre){detail} + Saved, but reopening failed: {e} Lagret, men gjenåpning mislyktes: {e} + Screen flicker Skjermflimring + Screen readers choose their voice from the document's language. Set it in Document Properties > Advanced > Language. Skjermlesere velger stemme ut fra dokumentets språk. Angi det under Dokumentegenskaper > Avansert > Språk. + Scripted content must be usable with a keyboard and assistive technology. Check the document's scripts. Skriptet innhold må kunne brukes med tastatur og hjelpeteknologi. Kontroller dokumentets skript. + Scripts Skript + Scripts run in a sandbox without file or network access. With JavaScript off, Acrobat's standard format, validate and calculate functions still work. Skript kjører i en sandkasse uten fil- eller nettverkstilgang. Med JavaScript av fungerer Acrobats standardfunksjoner for formatering, validering og beregning fortsatt. + Security method Sikkerhetsmetode + 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. Sikkerhetsinnstillingene må tillate at hjelpeteknologi leser innholdet («Tillat teksttilgang for skjermlesere»). Endre dokumentets sikkerhet for å tillate det. + Security will be removed when you save Sikkerheten fjernes når du lagrer + Self-signed Selvsignert + Serial number Serienummer + Set alternate text Angi alternativ tekst + Set bookmark destination Angi bokmerkemål + Set document title Angi dokumenttittel + Set status Accepted Sett status Godtatt + Set status Cancelled Sett status Avbrutt + Set status Completed Sett status Fullført + Set status None Sett status Ingen + Set status Rejected Sett status Avvist + Set status Unmarked Sett status Umerket + Set tab order Angi tabulatorrekkefølge + Set the image of {name} Angi bildet for {name} + Shadings Skyggelegginger + Show a field Vis et felt + Sign with a Digital ID Signer med en digital ID + Signed and all signatures are valid. Signert, og alle signaturer er gyldige. + Signed and saved to {path} Signert og lagret til {path} + Signer Underskriver + Signing time Signeringstidspunkt + Social Security Number Personnummer + Social Security Numbers Personnumre + Solid Heltrukket + Squiggly Bølgete + Star Stjerne + Strong Sterk + Structure Info Strukturinformasjon + Submit a form Send inn et skjema + TH and TD TH og TD + TH and TD must be children of TR TH og TD må være barn av TR + TR must be a child of Table, THead, TBody, or TFoot TR må være barn av Table, THead, TBody eller TFoot + Tab order Tabulatorrekkefølge + Tab order is consistent with structure order Tabulatorrekkefølgen samsvarer med strukturrekkefølgen + Table cells belong in a row. Fix the table's tags. Tabellceller hører hjemme i en rad. Rett opp tabellens tagger. + Table rows belong directly in a table or its head, body or foot. Fix the table's tags. Tabellrader hører hjemme direkte i en tabell eller dens hode, kropp eller fot. Rett opp tabellens tagger. + Tables Tabeller + Tables must contain the same number of columns in each row and rows in each column Tabeller må ha like mange kolonner i hver rad og like mange rader i hver kolonne + Tables must have a summary Tabeller må ha et sammendrag + Tables should have headers Tabeller bør ha overskrifter + Tagged PDF Tagget PDF + Tagged annotations Taggede merknader + Tagged content Tagget innhold + Tagged form fields Taggede skjemafelt + Tagged multimedia Tagget multimedia + Tags give the document a logical structure that assistive technology follows. Tag the document (Automatically tag PDF). Tagger gir dokumentet en logisk struktur som hjelpeteknologi følger. Tagg dokumentet (Tagg PDF automatisk). + Text Field Properties Egenskaper for tekstfelt + Text language is specified Tekstspråk er angitt + Text must contrast enough with its background for people with low vision. Check the colours by eye or with a contrast tool. Tekst må ha nok kontrast mot bakgrunnen for folk med nedsatt syn. Kontroller fargene med øyet eller med et kontrastverktøy. + Text recognition is already running Tekstgjenkjenning kjører allerede + Text recognition isn't installed: its models are missing (run `cargo xtask models`, or set PDFCRAFT_MODELS). Tekstgjenkjenning er ikke installert: modellene mangler (kjør `cargo xtask models`, eller sett PDFCRAFT_MODELS). + That didn't work: an internal error stopped it ({m}). Det gikk ikke: en intern feil stoppet det ({m}). + That didn't work: an internal error stopped it ({m}). Your documents are unchanged. Det gikk ikke: en intern feil stoppet det ({m}). Dokumentene dine er uendret. + The Keychain's digital IDs couldn't be listed: {e} Kunne ikke liste opp de digitale ID-ene i nøkkelringen: {e} + The Windows store's digital IDs couldn't be listed: {e} Kunne ikke liste opp de digitale ID-ene i Windows-lageret: {e} + The clipboard has no image or text Utklippstavlen har verken bilde eller tekst + The document changed while you were choosing a file, so nothing was changed. Dokumentet ble endret mens du valgte en fil, så ingenting ble endret. + The document changed while you were editing field properties. Dokumentet ble endret mens du redigerte feltegenskaper. + The document has additional actions (/AA) Dokumentet har tilleggshandlinger (/AA) + The document has document-level JavaScript Dokumentet har JavaScript på dokumentnivå + The document has no XMP metadata Dokumentet har ingen XMP-metadata + The document is encrypted Dokumentet er kryptert + The document is no longer open. Dokumentet er ikke lenger åpent. + The document was closed before it could be saved. Dokumentet ble lukket før det kunne lagres. + The document's security settings don't allow filling in form fields Dokumentets sikkerhetsinnstillinger tillater ikke utfylling av skjemafelt + The field properties could not be changed. Feltegenskapene kunne ikke endres. + The field properties dialog is no longer open. Dialogen for feltegenskaper er ikke lenger åpen. + The form asks to be submitted to {u}; PdfCraft doesn't send form data. Save the document to keep your entries. Skjemaet ber om å bli sendt til {u}; PdfCraft sender ikke skjemadata. Lagre dokumentet for å beholde det du har fylt inn. + The form asks viewers to make appearances (NeedAppearances) Skjemaet ber visningsprogrammet lage utseender (NeedAppearances) + The linked PDF opened, but its named destination was not applied. Den lenkede PDF-en ble åpnet, men det navngitte målet ble ikke brukt. + The linked PDF opened, but its page destination was out of range. Den lenkede PDF-en ble åpnet, men sidemålet var utenfor området. + The operation failed. Operasjonen mislyktes. + The pages were extracted but not deleted, because the document changed while you were choosing a folder. Sidene ble trukket ut, men ikke slettet, fordi dokumentet ble endret mens du valgte en mappe. + The passwords don't match. Passordene er ikke like. + The selected area has been copied ({w} × {h} pixels) Det merkede området er kopiert ({w} × {h} piksler) + The selected fields could not be found. Select them again. Fant ikke de valgte feltene. Velg dem på nytt. + The standard fonts can't draw “{c}” ({code}). Only Western European characters can be added as text for now. Standardskriftene kan ikke tegne «{c}» ({code}). Foreløpig kan bare vesteuropeiske tegn legges til som tekst. + The title in the metadata and the document information differ Tittelen i metadataene og dokumentinformasjonen er forskjellige + The window should show a meaningful title rather than the file name. Set a title and Initial View > Show: Document Title. Vinduet bør vise en meningsfull tittel i stedet for filnavnet. Angi en tittel og Startvisning > Vis: Dokumenttittel. + There are no redaction marks to apply Det er ingen sladdemerker å bruke + This PDF uses the unsupported {s} action. Denne PDF-en bruker handlingen {s}, som ikke støttes. + This compatibility level supports only plain ASCII passwords. Dette kompatibilitetsnivået støtter bare rene ASCII-passord. + This document can't be checked: it couldn't be read for editing Dette dokumentet kan ikke kontrolleres: det kunne ikke leses for redigering + This document has no attachments. Dette dokumentet har ingen vedlegg. + This document has no bookmarks. Dette dokumentet har ingen bokmerker. + This document has no form fields. Dette dokumentet har ingen skjemafelt. + This document has no layers. Dette dokumentet har ingen lag. + This file is already as small as it can be made ({size}). No copy was saved. Denne filen er allerede så liten som den kan bli ({size}). Ingen kopi ble lagret. + 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. Dette skjemaet inneholder også XFA-data, som PdfCraft ikke leser ennå. Du kan fylle ut feltene, men Acrobat kan vise XFA-verdiene i stedet. + Thumbnails Miniatyrbilder + Timed responses Tidsbegrensede svar + Type Password Skriv inn passord + Type a password. Skriv inn et passord. + Typewriter Skrivemaskin + Unchecked Ikke avkrysset + Undid {label} Angret {label} + Unlock comment Lås opp kommentar + Unlocked {n} of {m} files Låste opp {n} av {m} filer + Update background Oppdater bakgrunn + Update header & footer Oppdater topptekst og bunntekst + Update watermark Oppdater vannmerke + Use {what} Bruk {what} + Valid Gyldig + Valid from Gyldig fra + Valid to Gyldig til + Validate Valider + Validity ends Gyldighet slutter + Validity starts Gyldighet starter + Vertically Loddrett + Video, sound and rich media must be tagged to be announced. Tag the multimedia annotations. Video, lyd og rike medier må være tagget for å bli lest opp. Tagg multimediemerknadene. + Viewing Visning + Weak Svak + White on black Hvitt på svart + Wrote 1 file to {dir} Skrev 1 fil til {dir} + Wrote {n} files to {dir} Skrev {n} filer til {dir} + X Object Forms Form-XObjects + Yellow Gul + Yellow on black Gult på svart + You can pin up to {n} folders. Unpin one first. Du kan feste opptil {n} mapper. Løsne én først. + Your name Navnet ditt + Zip Code Postnummer + Zip Code + 4 Postnummer + 4 + `{command}` {when} `{command}` {when} + a bookmark cannot be moved inside itself et bokmerke kan ikke flyttes inn i seg selv + a document must keep at least one page et dokument må beholde minst én side + average gjennomsnitt + maximum maksimum + minimum minimum + no such document dokumentet finnes ikke + nothing in the file matches this document ingenting i filen passer til dette dokumentet + nothing to redo ingenting å gjøre om + nothing to undo ingenting å angre + pages sider + product (x) produkt (x) + sum (+) sum (+) + the document has no form fields dokumentet har ingen skjemafelt + the document has no page tree dokumentet har ikke noe sidetre + the document has no tags dokumentet har ingen tagger + the document is protected by a password dokumentet er beskyttet med passord + the image is empty bildet er tomt + the password is incorrect passordet er feil + the text recognition models are not installed (reinstall PdfCraft, or set PDFCRAFT_MODELS to the folder that holds them) modellene for tekstgjenkjenning er ikke installert (installer PdfCraft på nytt, eller sett PDFCRAFT_MODELS til mappen som inneholder dem) + the title must not be empty tittelen kan ikke være tom + there are no pages to print det er ingen sider å skrive ut + there are no redaction marks to apply det er ingen sladdemerker å bruke + this document is signed: rewriting it would invalidate its signatures (save it incrementally instead) dette dokumentet er signert: å skrive det om ville gjøre signaturene ugyldige (lagre det inkrementelt i stedet) + this file isn't XFDF, FDF, XML or text form data denne filen er ikke XFDF, FDF, XML eller skjemadata som tekst + {0}: {1} {0}: {1} + {field}: {e} {field}: {e} + {file} is too large for a stamp (at most {n} MB) {file} er for stor for et stempel (høyst {n} MB) + {folder} is already pinned to Home {folder} er allerede festet til Hjem + {label} failed: {e} {label} mislyktes: {e} + {name} has no action {name} har ingen handling + {name} is too large for a word list (at most 1 MB). {name} er for stor for en ordliste (høyst 1 MB). + {name} submits the form to {url}; PdfCraft doesn't send form data. Save the document to keep your entries. {name} sender skjemaet til {url}; PdfCraft sender ikke skjemadata. Lagre dokumentet for å beholde det du har fylt inn. + {name}: choosing images arrives on the web with file pickers for images {name}: bildevalg kommer på nett med filvelgere for bilder + {name}: the {other} action isn't supported yet {name}: handlingen {other} støttes ikke ennå + {who} in this document tried to open an address PdfCraft won't open: {e}. {who} i dette dokumentet prøvde å åpne en adresse PdfCraft ikke åpner: {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} i dette dokumentet prøvde å åpne en adresse PdfCraft ikke åpner: {e}. Bare nettlenker (http, https) og e-postlenker (mailto) åpnes fra dokumenter. + View more Vis mer + View less Vis mindre + Only continue if you trust this document. An address can carry information from the document, such as what you typed into its form. Fortsett bare hvis du stoler på dette dokumentet. En adresse kan inneholde informasjon fra dokumentet, for eksempel det du skrev inn i skjemaet. + Print single-sided. Keep the sheets in order, cut at the marks, then stack the piles left to right, top to bottom. Skriv ut ensidig. Hold arkene i rekkefølge, klipp langs merkene, og stable bunkene fra venstre til høyre, ovenfra og ned. + Sign a sheet of white paper and take a photo of it in bright, even light (use the flash). Choose the photo or drop it here: PdfCraft finds the signature and removes the paper. Signer på et hvitt ark og ta et bilde av det i sterkt, jevnt lys (bruk blitsen). Velg bildet eller slipp det her: PdfCraft finner signaturen og fjerner papiret. + 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. Skriften i dette feltet kan ikke tegne “{c}” ({code}), så PdfCraft viser ? i stedet. Verdien lagres, og andre PDF-lesere tegner feltet på nytt ut fra den. + {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} kan ikke skrives inn i PDF-en ennå: Fyll ut og signer-tekst støtter bare vesteuropeiske tegn. Velg et numerisk format som dd.mm.yyyy. + January januar + February februar + March mars + April april + May mai + June juni + July juli + August august + September september + October oktober + November november + December desember + Certificate authority Sertifikatutsteder + End entity Sluttenhet + Float panel Frikoble panel + Move to group Flytt til gruppe + Panels Paneler + Resize panel window Endre størrelse på panelvindu + Resize panels Endre størrelse på paneler + Grab bag Diverse + No contributor data was built into this copy. Ingen bidragsyterdata ble bygd inn i denne kopien. + No model credits were built into this copy. Ingen modellkrediteringer ble bygd inn i denne kopien. + Reverse the order Snu rekkefølgen + Show Vis + Sort Sorter + Table Tabell + The field {name} no longer exists. Feltet {name} finnes ikke lenger. + {n} contributors · {c} commits {n} bidragsytere · {c} commits + Username Brukernavn + Display name Visningsnavn + Real name Egentlig navn + Name (A–Z) Navn (A–Å) + Merged PRs Sammenslåtte PR-er + PRs PR-er + Commits Commits + Lines added Linjer lagt til + +LOC +LOC + Lines deleted Linjer slettet + −LOC −LOC + Line delta Linjedifferanse + ΔLOC ΔLOC + Binary assets added Binærfiler lagt til + +Bin +Bin + Binary assets removed Binærfiler fjernet + −Bin −Bin + First commit Første commit + First Første + Last commit Siste commit + Last Siste + Company Selskap + Model Modell + Version Versjon + % of all commits % av alle commits + Lines +/− Linjer +/− + @{login}: {prs} PRs, {commits} commits, +{added} / −{deleted} lines (Δ {delta}), +{bin_added} / −{bin_deleted} binary assets, {first} – {last} @{login}: {prs} PR-er, {commits} commits, +{added} / −{deleted} linjer (Δ {delta}), +{bin_added} / −{bin_deleted} binærfiler, {first} – {last} + of {name} av {name} + of {pages} av {pages} + on page {label} på side {label} + open to see what it will include åpne for å se hva som blir med + ships in milestone {m} kommer i milepæl {m} + signature signatur + sorted ascending sortert stigende + sorted descending sortert synkende + to til + to {n} til {n} + watermark vannmerke + 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 eller yy år · m eller mm måned · mmm eller mmmm månedsnavn · d eller dd dag · ddd eller dddd ukedag. H, h, M, s og t er tidsbokstaver og kan ikke brukes alene: sett {escape} foran en bokstav for å vise den som den er. + {c} of {n}{more} {c} av {n}{more} + {dpi} pixels/inch {dpi} piksler/tomme + {kind} Properties Egenskaper for {kind} + {kind} field {kind}-felt + {kind} · page {p} {kind} · side {p} + {kind} — click to show it on the page {kind} — klikk for å vise det på siden + {k} of {n} {k} av {n} + {label} (1 issue) {label} (1 problem) + {label} ({n} issues) {label} ({n} problemer) + {month} {day}, {y} {day}. {month} {y} + {month} {y} {month} {y} + {msg} to {dir} {msg} til {dir} + {name} (default) {name} (standard) + {name} can't be combined: {why} {name} kan ikke slås sammen: {why} + {name} is read-only {name} er skrivebeskyttet + {name} — unsaved changes {name} — ulagrede endringer + {n} days ago {n} dager siden + {n} differences found {n} forskjeller funnet + {n} fields selected {n} felt valgt + {n} files {n} filer + {n} files done {n} filer ferdige + {n} files need attention {n} filer trenger oppmerksomhet + {n} h ago {n} t siden + {n} instances {n} forekomster + {n} issues {n} problemer + {n} marks {n} merker + {n} match(es) marked for redaction. {n} treff merket for sladding. + {n} measurements use an unsupported format and are left out. {n} målinger bruker et format som ikke støttes, og er utelatt. + {n} min ago {n} min siden + {n} pages {n} sider + {n} pages selected. {n} sider valgt. + {n} selected {n} valgt + {n} tags {n} tagger + {n} word(s) or phrase(s) {n} ord eller fraser + {n} {kind} {n} {kind} + {rule} - {status} {rule} - {status} + {s} action {s}-handling + {verb} Background {verb} bakgrunn + {verb} Header and Footer {verb} topptekst og bunntekst + {verb} Watermark {verb} vannmerke + {verb} as "{name}" {verb} som "{name}" + {who} in this document wants to open this address in your browser: {who} i dette dokumentet vil åpne denne adressen i nettleseren din: + {who} in this document wants to open your email app with this address: {who} i dette dokumentet vil åpne e-postprogrammet ditt med denne adressen: + {words} on {pages} {words} på {pages} + — (protection is applied when you save) — (beskyttelsen tas i bruk når du lagrer) + “{name}” has security restrictions. Enter its permissions password to lift them. “{name}” har sikkerhetsbegrensninger. Skriv inn tillatelsespassordet for å oppheve dem. + “{name}” is protected. Enter a password to open it. “{name}” er beskyttet. Skriv inn et passord for å åpne den. + Minimize Minimer + Maximize Maksimer + Restore Gjenopprett + Use system title bar Bruk systemets tittellinje + Show a basic title bar for the window buttons instead of the tab strip. GNOME doesn't draw title bars for apps, so this one doesn't follow its theme. Vis en enkel tittellinje for vindusknappene i stedet for fanelinjen. GNOME tegner ikke tittellinjer for apper, så denne følger ikke temaet. + Move tag earlier Flytt tagg tidligere + Move tag later Flytt tagg senere + Next tab Neste fane + Previous tab Forrige fane + Hide hover previews for highlights without comment text Skjul forhåndsvisning ved peking for uthevinger uten kommentartekst + Digitally signed by Digitalt signert av + DN: DN: + Reason: Årsak: + Location: Sted: + Date: Dato: + The labels are in English: the signature's font can't draw them in this language. Etikettene er på engelsk: signaturens skrift kan ikke tegne dem på dette språket. + Font size Skriftstørrelse + No printers were found. Check that the printer is installed and the print spooler is running, then open Print again. Fant ingen skrivere. Kontroller at skriveren er installert og at utskriftskøen kjører, og åpne Skriv ut på nytt. + Make Properties Default Bruk egenskapene som standard + Scroll speed (mouse wheel and arrow keys) Rullehastighet (musehjul og piltaster) + Save over the changed file? Lagre over den endrede filen? + “{name}” changed on disk after it was opened or last saved. Saving now replaces that newer version with this one. «{name}» ble endret på disken etter at den ble åpnet eller sist lagret. Lagrer du nå, erstattes den nyere versjonen med denne. + Save anyway Lagre likevel + Sending sheet {s} of {n} to {printer}… Sender ark {s} av {n} til {printer}… + What takes up the space in this file, by kind of content. Hva som tar plass i denne filen, etter innholdstype. + This file changed on disk. Reload shows the new version and drops your unsaved changes; Save replaces it with this version. Denne filen er endret på disken. Last inn på nytt viser den nye versjonen og forkaster ulagrede endringer; Lagre erstatter den med denne versjonen. + Set page transitions Angi sideoverganger + The password must have at least 8 characters. Passordet må ha minst 8 tegn. + Open in Full Screen mode? Åpne i fullskjermmodus? + This document asks to open in Full Screen mode: the page fills the screen and nothing else shows. Press Esc at any time to leave it. Dette dokumentet ber om å bli åpnet i fullskjermmodus: siden fyller hele skjermen, og ingenting annet vises. Trykk Esc når som helst for å avslutte. + Full Screen Fullskjerm + Not now Ikke nå + Find untagged content Finn innhold uten tagger + No untagged content was found. Fant ikke noe innhold uten tagger. + Found {n} untagged content problems. Fant {n} problemer med innhold uten tagger. + Tag form fields Tagg skjemafelt + Mark as artifact Merk som artefakt + Remove this leaf tag from reading order and mark its direct content as an artifact Fjern denne taggen fra leserekkefølgen og merk innholdet dens som artefakt + Mark content as artifact Merk innhold som artefakt + Word count Ordtelling + Current page only Bare gjeldende side + Exclude tagged tables Utelat taggede tabeller + Exclude tagged code blocks Utelat taggede kodeblokker + Exclude image/OCR text Utelat bilde-/OCR-tekst + Exclusions use PDF tags. Untagged tables and code remain included. Image exclusion also removes invisible text. Utelatelser bruker PDF-tagger. Tabeller og kode uten tagger telles fortsatt. Utelating av bilder fjerner også usynlig tekst. + Words Ord + Characters Tegn + Characters without spaces Tegn uten mellomrom + Pages counted Sider talt + Some pages contain no countable text. Scanned images need OCR first. Noen sider inneholder ingen tekst som kan telles. Skannede bilder trenger tekstgjenkjenning først. + Some content could not be classified safely and was kept in the count. Noe innhold kunne ikke klassifiseres sikkert og ble beholdt i tellingen. + Counting words… {n} pages read Teller ord … {n} sider lest + Word counting stopped unexpectedly. Ordtellingen stoppet uventet. + Word count… Ordtelling… + Standard Standard + Mobile Mobil + Merge image fragments Slå sammen bildefragmenter + Optimizing… merging image fragments Optimaliserer … slår sammen bildefragmenter + Side by side Side om side + New file only Bare ny fil + Old file: {name} Gammel fil: {name} + New file: {name} Ny fil: {name} + Print as image Skriv ut som bilde + Resolution: Oppløsning: + Use it when a printer skips text, shading or transparency. The print is no longer searchable text; with Print in grayscale, the pictures are grey. Bruk dette når en skriver hopper over tekst, skyggelegging eller gjennomsiktighet. Utskriften er ikke lenger søkbar tekst; med Skriv ut i gråtoner blir bildene grå. + Crop Image Beskjær bilde + Remove Crop Fjern beskjæring + Trim the picture with its handles; Enter or Done applies Beskjær bildet med håndtakene; Enter eller Ferdig bekrefter + Redaction mark appearance Utseende på sladdemarkeringer + Outline colour: Kantfarge: + Fill colour: Fyllfarge: + Shown while the pointer is over a mark. Vises mens pekeren er over en markering. + Opacity: Gjennomsiktighet: + Make properties default Bruk egenskapene som standard + Pages selected Valgte sider + Page properties Sideegenskaper + Cut Image Klipp ut bilde + Redact Image Sladd bilde + Create Link… Opprett lenke… + Recognize Text… Gjenkjenn tekst… + Turn upside down (180°) Snu opp ned (180°) + 4 x 6 in label (101.6 x 152.4 mm) 4 x 6 tommer etikett (101,6 x 152,4 mm) + Cuts each page to its printed area, turns it upright and fits it to the sheet without stretching. One label per sheet. Beskjærer hver side til det trykte området, snur den rett vei og tilpasser den til arket uten å strekke. Én etikett per ark. + Collapse all pages Skjul alle sider + Expand all pages Utvid alle sider + +# Words that need a different translation in one place (see tr_ctx). +theme Appearance Utseende +action wizard Start Start +comment menu Edit Rediger +animation effects Off Av +animation effects On På +calendar date January januar +calendar date February februar +calendar date March mars +calendar date April april +calendar date May mai +calendar date June juni +calendar date July juli +calendar date August august +calendar date September september +calendar date October oktober +calendar date November november +calendar date December desember +calendar weekday Su sø +calendar weekday Mo ma +calendar weekday Tu ti +calendar weekday We on +calendar weekday Th to +calendar weekday Fr fr +calendar weekday Sa lø +signature pad Type Skriv +scroll speed Slow Sakte +scroll speed Normal Normal +scroll speed Fast Rask +certificate Subject Emne +certificate Issued to Utstedt til +certificate Issued by Utstedt av +certificate Valid from Gyldig fra +certificate Valid to Gyldig til +certificate Intended usage Tiltenkt bruk +certificate Version Versjon +certificate Serial number Serienummer +certificate Issuer Utsteder +certificate Validity starts Gyldighet starter +certificate Validity ends Gyldighet slutter +certificate Public key Offentlig nøkkel +certificate Basic constraints Grunnleggende begrensninger +certificate Key usage Nøkkelbruk +certificate Self-signed Selvsignert +certificate SHA-1 digest SHA-1-sammendrag +certificate SHA-256 digest SHA-256-sammendrag + +# Command ids (win over the English label for that menu item). +@id file.word_count Ordtelling… + +# Plural forms: one (1) | other (0, 2, 3 ...). +@plural Delete {n} field|Delete {n} fields Slett {n} felt|Slett {n} felt +@plural {n} byte|{n} bytes {n} byte|{n} byte +@plural {n} image optimized|{n} images optimized {n} bilde optimalisert|{n} bilder optimalisert +@plural {n} item discarded|{n} items discarded {n} element forkastet|{n} elementer forkastet +@plural {n} pixel|{n} pixels {n} piksel|{n} piksler +@plural {n} page|{n} pages {n} side|{n} sider +@plural {n} file|{n} files {n} fil|{n} filer +@plural {n} field|{n} fields {n} felt|{n} felt +@plural {n} comment|{n} comments {n} kommentar|{n} kommentarer +@plural {n} result|{n} results {n} resultat|{n} resultater +@plural {n} field name collides with earlier files and will be renamed: {list}|{n} field names collide with earlier files and will be renamed: {list} {n} feltnavn kolliderer med tidligere filer og blir omdøpt: {list}|{n} feltnavn kolliderer med tidligere filer og blir omdøpt: {list} +@plural {n} object selected|{n} objects selected {n} objekt valgt|{n} objekter valgt +@plural Downloaded {n} PDF|Downloaded {n} PDFs Lastet ned {n} PDF|Lastet ned {n} PDF-er + +# Keyboard key names, distinct from command labels. +key Ctrl Ctrl +key Shift Shift +key Delete Delete +key Esc Esc +key Home Home +key End End +key Space Mellomrom diff --git a/crates/ui-egui/src/i18n/pl.tsv b/crates/ui-egui/src/i18n/pl.tsv index 2c291a03b..cd381b539 100644 --- a/crates/ui-egui/src/i18n/pl.tsv +++ b/crates/ui-egui/src/i18n/pl.tsv @@ -2589,3 +2589,23 @@ animation effects Off Wyłączone The signature's signed attributes have no message digest, so the signature does not cover the document. The signature's signed attributes have no message digest, so the signature does not cover the document. The signature's signed attributes name a content type other than data, so it is not a signature of the document. The signature's signed attributes name a content type other than data, so it is not a signature of the document. This signature uses an RSA key shorter than 2048 bits, which is no longer considered strong enough for signatures. It still validates, but it should not be relied on. This signature uses an RSA key shorter than 2048 bits, which is no longer considered strong enough for signatures. It still validates, but it should not be relied on. + +# Prepare a form ▸ Text field table + Add text field table Dodaj tabelę pól tekstowych + Text field table Tabela pól tekstowych + Text Field Table Tabela pól tekstowych + Columns Kolumny + Row spacing Odstęp między wierszami + Column spacing Odstęp między kolumnami + First row number Numer pierwszego wiersza + Column widths Szerokości kolumn + Distribute evenly Rozłóż równomiernie + Field names Nazwy pól + N or {row} is the row number, {col} the column number, {seq} the cell number. With none of them, the row number goes last. N lub {row} to numer wiersza, {col} numer kolumny, a {seq} numer komórki. Bez żadnego z nich numer wiersza trafia na koniec. + Column {n} Kolumna {n} + Add fields Dodaj pola + the rows are too short: make the area taller, or use fewer rows or less row spacing wiersze są za niskie: powiększ obszar w pionie albo użyj mniej wierszy lub mniejszego odstępu między wierszami + a column is too narrow: make the area wider, give the column more of it, or use fewer columns or less column spacing kolumna jest za wąska: poszerz obszar, daj kolumnie więcej miejsca albo użyj mniej kolumn lub mniejszego odstępu między kolumnami + field names can't contain a period: {name} nazwy pól nie mogą zawierać kropki: {name} + two cells would both be named {name} dwie komórki miałyby nazwę {name} + a field named {name} already exists pole o nazwie {name} już istnieje diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv index 0027622ec..0b9ded419 100644 --- a/crates/ui-egui/src/i18n/pt-br.tsv +++ b/crates/ui-egui/src/i18n/pt-br.tsv @@ -2772,3 +2772,23 @@ theme Appearance Aparência The signature's signed attributes have no message digest, so the signature does not cover the document. The signature's signed attributes have no message digest, so the signature does not cover the document. The signature's signed attributes name a content type other than data, so it is not a signature of the document. The signature's signed attributes name a content type other than data, so it is not a signature of the document. Remove {standard} identification Remove {standard} identification + +# Prepare a form ▸ Text field table + Add text field table Adicionar tabela de campos de texto + Text field table Tabela de campos de texto + Text Field Table Tabela de campos de texto + Columns Colunas + Row spacing Espaçamento entre linhas + Column spacing Espaçamento entre colunas + First row number Número da primeira linha + Column widths Larguras das colunas + Distribute evenly Distribuir igualmente + Field names Nomes dos campos + N or {row} is the row number, {col} the column number, {seq} the cell number. With none of them, the row number goes last. N ou {row} é o número da linha, {col} o da coluna e {seq} o da célula. Sem nenhum deles, o número da linha vai no final. + Column {n} Coluna {n} + Add fields Adicionar campos + the rows are too short: make the area taller, or use fewer rows or less row spacing as linhas estão baixas demais: deixe a área mais alta ou use menos linhas ou menos espaçamento entre linhas + a column is too narrow: make the area wider, give the column more of it, or use fewer columns or less column spacing uma coluna está estreita demais: deixe a área mais larga, dê mais largura à coluna ou use menos colunas ou menos espaçamento entre colunas + field names can't contain a period: {name} nomes de campo não podem conter ponto: {name} + two cells would both be named {name} duas células teriam o nome {name} + a field named {name} already exists já existe um campo chamado {name} diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv index e7259b0ff..0dc40fdba 100644 --- a/crates/ui-egui/src/i18n/ru.tsv +++ b/crates/ui-egui/src/i18n/ru.tsv @@ -2408,3 +2408,23 @@ animation effects Off Выкл Larger thumbnails Увеличить миниатюры # Lines, connected lines and polygons reshaped by their ends and corners (M5). Reshape comment Изменить форму комментария + +# Prepare a form ▸ Text field table + Add text field table Добавить таблицу текстовых полей + Text field table Таблица текстовых полей + Text Field Table Таблица текстовых полей + Columns Столбцы + Row spacing Интервал между строками + Column spacing Интервал между столбцами + First row number Номер первой строки + Column widths Ширина столбцов + Distribute evenly Распределить равномерно + Field names Имена полей + N or {row} is the row number, {col} the column number, {seq} the cell number. With none of them, the row number goes last. N или {row} — номер строки, {col} — номер столбца, {seq} — номер ячейки. Если их нет, номер строки добавляется в конец. + Column {n} Столбец {n} + Add fields Добавить поля + the rows are too short: make the area taller, or use fewer rows or less row spacing строки слишком низкие: увеличьте высоту области, уменьшите число строк или интервал между ними + a column is too narrow: make the area wider, give the column more of it, or use fewer columns or less column spacing столбец слишком узкий: расширьте область, отдайте столбцу больше ширины, уменьшите число столбцов или интервал между ними + field names can't contain a period: {name} имена полей не могут содержать точку: {name} + two cells would both be named {name} две ячейки получили бы одно имя {name} + a field named {name} already exists поле с именем {name} уже существует diff --git a/crates/ui-egui/src/i18n/sl.tsv b/crates/ui-egui/src/i18n/sl.tsv index 667d22f91..f5e374ae7 100644 --- a/crates/ui-egui/src/i18n/sl.tsv +++ b/crates/ui-egui/src/i18n/sl.tsv @@ -2500,3 +2500,23 @@ animation effects Off Izklopljeni Larger thumbnails Večje sličice # Lines, connected lines and polygons reshaped by their ends and corners (M5). Reshape comment Spremeni obliko komentarja + +# Prepare a form ▸ Text field table + Add text field table Dodaj tabelo besedilnih polj + Text field table Tabela besedilnih polj + Text Field Table Tabela besedilnih polj + Columns Stolpci + Row spacing Razmik med vrsticami + Column spacing Razmik med stolpci + First row number Številka prve vrstice + Column widths Širine stolpcev + Distribute evenly Enakomerno porazdeli + Field names Imena polj + N or {row} is the row number, {col} the column number, {seq} the cell number. With none of them, the row number goes last. N ali {row} je številka vrstice, {col} številka stolpca in {seq} številka celice. Brez njih se številka vrstice doda na konec. + Column {n} Stolpec {n} + Add fields Dodaj polja + the rows are too short: make the area taller, or use fewer rows or less row spacing vrstice so prenizke: povečajte višino območja ali uporabite manj vrstic ali manjši razmik med vrsticami + a column is too narrow: make the area wider, give the column more of it, or use fewer columns or less column spacing stolpec je preozek: razširite območje, stolpcu dajte več širine ali uporabite manj stolpcev ali manjši razmik med stolpci + field names can't contain a period: {name} imena polj ne smejo vsebovati pike: {name} + two cells would both be named {name} dve celici bi imeli enako ime {name} + a field named {name} already exists polje z imenom {name} že obstaja diff --git a/crates/ui-egui/src/i18n/te.tsv b/crates/ui-egui/src/i18n/te.tsv index d8ce8af4e..fa3f6cec4 100644 --- a/crates/ui-egui/src/i18n/te.tsv +++ b/crates/ui-egui/src/i18n/te.tsv @@ -2404,3 +2404,23 @@ animation effects Off ఆఫ్ Larger thumbnails పెద్ద థంబ్‌నెయిల్‌లు # Lines, connected lines and polygons reshaped by their ends and corners (M5). Reshape comment వ్యాఖ్య ఆకారాన్ని మార్చు + +# Prepare a form ▸ Text field table + Add text field table టెక్స్ట్ ఫీల్డ్ పట్టిక జోడించు + Text field table టెక్స్ట్ ఫీల్డ్ పట్టిక + Text Field Table టెక్స్ట్ ఫీల్డ్ పట్టిక + Columns నిలువు వరుసలు + Row spacing వరుసల మధ్య దూరం + Column spacing నిలువు వరుసల మధ్య దూరం + First row number మొదటి వరుస సంఖ్య + Column widths నిలువు వరుసల వెడల్పులు + Distribute evenly సమానంగా పంచు + Field names ఫీల్డ్ పేర్లు + N or {row} is the row number, {col} the column number, {seq} the cell number. With none of them, the row number goes last. N లేదా {row} వరుస సంఖ్య, {col} నిలువు వరుస సంఖ్య, {seq} గడి సంఖ్య. వీటిలో ఏదీ లేకపోతే, వరుస సంఖ్య చివర చేరుతుంది. + Column {n} నిలువు వరుస {n} + Add fields ఫీల్డ్‌లు జోడించు + the rows are too short: make the area taller, or use fewer rows or less row spacing వరుసలు చాలా తక్కువ ఎత్తులో ఉన్నాయి: ప్రాంతాన్ని ఎత్తుగా చేయండి, లేదా తక్కువ వరుసలు లేదా తక్కువ వరుస దూరం వాడండి + a column is too narrow: make the area wider, give the column more of it, or use fewer columns or less column spacing ఒక నిలువు వరుస చాలా ఇరుకుగా ఉంది: ప్రాంతాన్ని వెడల్పు చేయండి, ఆ నిలువు వరుసకు ఎక్కువ వెడల్పు ఇవ్వండి, లేదా తక్కువ నిలువు వరుసలు లేదా తక్కువ దూరం వాడండి + field names can't contain a period: {name} ఫీల్డ్ పేర్లలో చుక్క ఉండకూడదు: {name} + two cells would both be named {name} రెండు గడులకూ {name} అనే పేరు వస్తుంది + a field named {name} already exists {name} అనే ఫీల్డ్ ఇప్పటికే ఉంది diff --git a/crates/ui-egui/src/i18n/tr.tsv b/crates/ui-egui/src/i18n/tr.tsv index 240a3cde1..c292b8863 100644 --- a/crates/ui-egui/src/i18n/tr.tsv +++ b/crates/ui-egui/src/i18n/tr.tsv @@ -2491,3 +2491,23 @@ animation effects Off Kapalı Larger thumbnails Küçük resimleri büyüt # Lines, connected lines and polygons reshaped by their ends and corners (M5). Reshape comment Yorumun şeklini değiştir + +# Prepare a form ▸ Text field table + Add text field table Metin alanı tablosu ekle + Text field table Metin alanı tablosu + Text Field Table Metin alanı tablosu + Columns Sütunlar + Row spacing Satır aralığı + Column spacing Sütun aralığı + First row number İlk satır numarası + Column widths Sütun genişlikleri + Distribute evenly Eşit dağıt + Field names Alan adları + N or {row} is the row number, {col} the column number, {seq} the cell number. With none of them, the row number goes last. N veya {row} satır numarası, {col} sütun numarası, {seq} hücre numarasıdır. Hiçbiri yoksa satır numarası sona eklenir. + Column {n} Sütun {n} + Add fields Alanları ekle + the rows are too short: make the area taller, or use fewer rows or less row spacing satırlar çok kısa: alanı uzatın ya da daha az satır veya daha az satır aralığı kullanın + a column is too narrow: make the area wider, give the column more of it, or use fewer columns or less column spacing bir sütun çok dar: alanı genişletin, sütuna daha fazla yer verin ya da daha az sütun veya daha az sütun aralığı kullanın + field names can't contain a period: {name} alan adları nokta içeremez: {name} + two cells would both be named {name} iki hücrenin de adı {name} olurdu + a field named {name} already exists {name} adlı bir alan zaten var diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv index f09c69264..cc8f45765 100644 --- a/crates/ui-egui/src/i18n/uk.tsv +++ b/crates/ui-egui/src/i18n/uk.tsv @@ -2517,3 +2517,23 @@ animation effects Off Вимкнено Larger thumbnails Збільшити мініатюри # Lines, connected lines and polygons reshaped by their ends and corners (M5). Reshape comment Змінити форму коментаря + +# Prepare a form ▸ Text field table + Add text field table Додати таблицю текстових полів + Text field table Таблиця текстових полів + Text Field Table Таблиця текстових полів + Columns Стовпці + Row spacing Інтервал між рядками + Column spacing Інтервал між стовпцями + First row number Номер першого рядка + Column widths Ширина стовпців + Distribute evenly Розподілити рівномірно + Field names Імена полів + N or {row} is the row number, {col} the column number, {seq} the cell number. With none of them, the row number goes last. N або {row} — номер рядка, {col} — номер стовпця, {seq} — номер клітинки. Якщо їх немає, номер рядка додається в кінці. + Column {n} Стовпець {n} + Add fields Додати поля + the rows are too short: make the area taller, or use fewer rows or less row spacing рядки занадто низькі: збільште висоту області, зменште кількість рядків або інтервал між ними + a column is too narrow: make the area wider, give the column more of it, or use fewer columns or less column spacing стовпець занадто вузький: розширте область, дайте стовпцю більше ширини, зменште кількість стовпців або інтервал між ними + field names can't contain a period: {name} імена полів не можуть містити крапку: {name} + two cells would both be named {name} дві клітинки мали б однакове ім'я {name} + a field named {name} already exists поле з іменем {name} уже існує diff --git a/crates/ui-egui/src/i18n/vi.tsv b/crates/ui-egui/src/i18n/vi.tsv index 9b14f9e29..bb20dcc5b 100644 --- a/crates/ui-egui/src/i18n/vi.tsv +++ b/crates/ui-egui/src/i18n/vi.tsv @@ -2754,3 +2754,27 @@ key Space Space # AI-assisted additions for reading printer options off the GUI thread (#1818) (2026-10-16); not reviewed by a native speaker. Reading the printer's settings… Đang đọc cài đặt của máy in… + +# Prepare a form ▸ Text field table + Add text field table Thêm bảng trường văn bản + Text field table Bảng trường văn bản + Text Field Table Bảng trường văn bản + Columns Cột + Row spacing Khoảng cách hàng + Column spacing Khoảng cách cột + First row number Số hàng đầu tiên + Column widths Độ rộng cột + Distribute evenly Phân bố đều + Field names Tên trường + N or {row} is the row number, {col} the column number, {seq} the cell number. With none of them, the row number goes last. N hoặc {row} là số hàng, {col} là số cột, {seq} là số ô. Nếu không có cái nào, số hàng được thêm vào cuối. + Column {n} Cột {n} + Add fields Thêm các trường + the rows are too short: make the area taller, or use fewer rows or less row spacing các hàng quá thấp: hãy làm vùng cao hơn, hoặc dùng ít hàng hơn hay khoảng cách hàng nhỏ hơn + a column is too narrow: make the area wider, give the column more of it, or use fewer columns or less column spacing một cột quá hẹp: hãy làm vùng rộng hơn, cho cột thêm độ rộng, hoặc dùng ít cột hơn hay khoảng cách cột nhỏ hơn + field names can't contain a period: {name} tên trường không được chứa dấu chấm: {name} + two cells would both be named {name} hai ô sẽ cùng có tên {name} + a field named {name} already exists đã có trường tên {name} + a table has 1 to {max} rows một bảng có từ 1 đến {max} hàng + a table has 1 to {max} columns một bảng có từ 1 đến {max} cột + the first row number can be at most {max} số hàng đầu tiên tối đa là {max} + the area, the spacing and the column widths must be numbers, the spacing 0 or more and each width more than 0 vùng, khoảng cách và độ rộng cột phải là số, khoảng cách từ 0 trở lên và mỗi độ rộng lớn hơn 0 diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv index 184d81066..594f68df8 100644 --- a/crates/ui-egui/src/i18n/zh-hans.tsv +++ b/crates/ui-egui/src/i18n/zh-hans.tsv @@ -2459,3 +2459,23 @@ animation effects Off 关 Larger thumbnails 放大缩略图 # Lines, connected lines and polygons reshaped by their ends and corners (M5). Reshape comment 更改注释形状 + +# Prepare a form ▸ Text field table + Add text field table 添加文本字段表格 + Text field table 文本字段表格 + Text Field Table 文本字段表格 + Columns 列 + Row spacing 行间距 + Column spacing 列间距 + First row number 起始行号 + Column widths 列宽 + Distribute evenly 平均分布 + Field names 字段名称 + N or {row} is the row number, {col} the column number, {seq} the cell number. With none of them, the row number goes last. N 或 {row} 表示行号,{col} 表示列号,{seq} 表示单元格序号。如果都没有,行号加在末尾。 + Column {n} 第 {n} 列 + Add fields 添加字段 + the rows are too short: make the area taller, or use fewer rows or less row spacing 行太矮:请增加区域高度,或减少行数或行间距 + a column is too narrow: make the area wider, give the column more of it, or use fewer columns or less column spacing 某列太窄:请加宽区域、给该列更多宽度,或减少列数或列间距 + field names can't contain a period: {name} 字段名称不能包含句点:{name} + two cells would both be named {name} 两个单元格的名称都会是 {name} + a field named {name} already exists 已存在名为 {name} 的字段 diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv index e7f71e32b..cb578ba26 100644 --- a/crates/ui-egui/src/i18n/zh-hant.tsv +++ b/crates/ui-egui/src/i18n/zh-hant.tsv @@ -2445,3 +2445,23 @@ animation effects Off 關 Larger thumbnails 放大縮圖 # Lines, connected lines and polygons reshaped by their ends and corners (M5). Reshape comment 變更註解形狀 + +# Prepare a form ▸ Text field table + Add text field table 新增文字欄位表格 + Text field table 文字欄位表格 + Text Field Table 文字欄位表格 + Columns 欄 + Row spacing 列間距 + Column spacing 欄間距 + First row number 起始列號 + Column widths 欄寬 + Distribute evenly 平均分配 + Field names 欄位名稱 + N or {row} is the row number, {col} the column number, {seq} the cell number. With none of them, the row number goes last. N 或 {row} 代表列號,{col} 代表欄號,{seq} 代表儲存格序號。若都沒有,列號會加在結尾。 + Column {n} 第 {n} 欄 + Add fields 新增欄位 + the rows are too short: make the area taller, or use fewer rows or less row spacing 列太矮:請加高區域,或減少列數或列間距 + a column is too narrow: make the area wider, give the column more of it, or use fewer columns or less column spacing 某欄太窄:請加寬區域、給該欄更多寬度,或減少欄數或欄間距 + field names can't contain a period: {name} 欄位名稱不能包含句點:{name} + two cells would both be named {name} 兩個儲存格的名稱都會是 {name} + a field named {name} already exists 已有名為 {name} 的欄位 diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index e64b005ac..64b22ff18 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -82,6 +82,7 @@ mod bidi; pub mod bulk_fields; mod dialogs; mod edit_text_ui; +pub mod field_table_ui; pub use edit_text_ui::CropMode; mod editing; #[cfg(not(target_arch = "wasm32"))] @@ -336,6 +337,8 @@ pub enum Dialog { Optimize, /// Prepare a form ▸ right-click a field ▸ Duplicate. DuplicateField, + /// Prepare a form ▸ Text field table: a movable window rather than a modal (`field_table_ui`). + FieldTable, /// Check for accessibility ▸ Accessibility Checker Options. AccessibilityOptions, /// Scan & OCR ▸ Recognize text. @@ -751,6 +754,8 @@ pub struct PdfCraftApp { pub attach_override: Option<(String, Vec)>, /// Duplicate Field: which field and onto which pages. pub duplicate_draft: Option, + /// Text field table: the table being set up, and the page it goes on. + pub field_table_draft: Option, /// Prepare a form ▸ Preview: fill the form instead of editing its fields. pub form_preview: bool, /// Where autosaves go (`None`: autosave off, e.g. on the web and in tests). @@ -1074,6 +1079,7 @@ impl PdfCraftApp { v_pressed: false, attach_override: None, duplicate_draft: None, + field_table_draft: None, form_preview: false, window_title: String::new(), recovery: None, diff --git a/crates/ui-egui/src/prepare.rs b/crates/ui-egui/src/prepare.rs index 13767e55f..f79ebf17f 100644 --- a/crates/ui-egui/src/prepare.rs +++ b/crates/ui-egui/src/prepare.rs @@ -17,12 +17,14 @@ use crate::widgets; const SELECT_BLUE: Color32 = Color32::from_rgb(0x14, 0x73, 0xE6); /// Acrobat's field fill in Prepare Form (light blue). -const FIELD_FILL: Color32 = Color32::from_rgba_premultiplied(0x1B, 0x34, 0x54, 0x59); +pub(crate) const FIELD_FILL: Color32 = Color32::from_rgba_premultiplied(0x1B, 0x34, 0x54, 0x59); /// The Add form components tools. #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] pub enum FieldTool { Text, + /// Rows and columns of text fields in the area drawn, set up in a window first. + TextTable, CheckBox, Radio, Combo, @@ -33,8 +35,9 @@ pub enum FieldTool { Signature, } -pub const FIELD_TOOLS: [FieldTool; 9] = [ +pub const FIELD_TOOLS: [FieldTool; 10] = [ FieldTool::Text, + FieldTool::TextTable, FieldTool::CheckBox, FieldTool::Radio, FieldTool::Combo, @@ -49,6 +52,7 @@ impl FieldTool { pub fn command(self) -> &'static str { match self { FieldTool::Text => "form.add.text", + FieldTool::TextTable => "form.add.text_table", FieldTool::CheckBox => "form.add.checkbox", FieldTool::Radio => "form.add.radio", FieldTool::Combo => "form.add.combo", @@ -67,6 +71,7 @@ impl FieldTool { pub fn label(self) -> &'static str { match self { FieldTool::Text => "Text field", + FieldTool::TextTable => "Text field table", FieldTool::CheckBox => "Checkbox", FieldTool::Radio => "Radio button", FieldTool::Combo => "Drop-down list", @@ -82,7 +87,8 @@ impl FieldTool { /// join it with the next free choice. pub fn new_field(self, join: Option<&FormField>) -> NewField { match self { - FieldTool::Text => NewField::Text { multiline: false }, + // A table's cells are text fields. + FieldTool::Text | FieldTool::TextTable => NewField::Text { multiline: false }, FieldTool::CheckBox => NewField::CheckBox, FieldTool::Radio => match join.filter(|f| f.kind == FormFieldKind::Radio) { Some(g) => { @@ -105,6 +111,7 @@ impl FieldTool { /// Width and height in points of a field placed with a single click. pub fn default_size(self) -> (f64, f64) { match self { + FieldTool::TextTable => (180.0, 110.0), FieldTool::CheckBox | FieldTool::Radio => (14.0, 14.0), FieldTool::List => (144.0, 54.0), FieldTool::Button => (72.0, 22.0), @@ -178,6 +185,23 @@ fn to_user(xf: &PageXform, info: &DocInfo, page: usize, p: Pos2) -> [f64; 2] { [u[0] as f64, u[1] as f64] } +/// Where a table goes, in view space, for a screen rect drawn with the table tool: a click (a rect +/// under 4 pixels either way) puts the default size's top-left corner at the pointer, moved in from +/// the page's edges as far as it takes to stay on the page. +fn table_rect(xf: &PageXform, r: Rect) -> [f64; 4] { + let (x0, y0) = xf.screen_to_view(r.min); + if r.width() < 4.0 || r.height() < 4.0 { + let (pw, ph) = (f64::from(xf.pw), f64::from(xf.ph)); + let (dw, dh) = FieldTool::TextTable.default_size(); + // Smaller than the default only on a page smaller than it. + let (w, h) = (dw.min(pw), dh.min(ph)); + let (x, y) = (f64::from(x0).min(pw - w).max(0.0), f64::from(y0).min(ph - h).max(0.0)); + return [x, y, x + w, y + h]; + } + let (x1, y1) = xf.screen_to_view(r.max); + [x0, y0, x1, y1].map(f64::from) +} + /// A screen rect → a normalised user-space rect. fn user_rect(xf: &PageXform, info: &DocInfo, page: usize, r: Rect) -> [f64; 4] { let (a, b) = (to_user(xf, info, page, r.min), to_user(xf, info, page, r.max)); @@ -208,6 +232,8 @@ pub(crate) struct Outcome { pub properties: bool, /// A field was placed: go back to the Select tool. pub placed: bool, + /// A table of text fields was drawn: open its window for this page and area (view space). + pub table: Option<(usize, [f64; 4])>, } /// Pointer input on one page while preparing a form. @@ -223,7 +249,7 @@ pub(crate) fn page_input( allowed: bool, view: &mut DocView, ) -> Outcome { - let mut out = Outcome { consumed: false, properties: false, placed: false }; + let mut out = Outcome { consumed: false, properties: false, placed: false, table: None }; let pointer = ui.input(|i| i.pointer.hover_pos().or(i.pointer.interact_pos())); let prep = &mut view.prepare; @@ -267,6 +293,11 @@ pub(crate) fn page_input( } Grab::Draw(from) => { let Some(tool) = tool else { return out }; + if tool == FieldTool::TextTable { + out.table = Some((page, table_rect(xf, Rect::from_two_pos(from, end).intersect(xf.rect)))); + out.placed = true; + return out; + } let r = Rect::from_two_pos(from, end).intersect(xf.rect); let rect = user_rect(xf, info, page, r); let rect = if r.width() < 4.0 || r.height() < 4.0 { @@ -312,6 +343,11 @@ pub(crate) fn page_input( if resp.clicked() { // A click without a drag: finish at once. prep.grab = None; + if tool == FieldTool::TextTable { + out.table = Some((page, table_rect(xf, Rect::from_min_size(p, egui::Vec2::ZERO)))); + out.placed = true; + return out; + } let at = to_user(xf, info, page, p); let (w, h) = tool.default_size(); let join = prep.selected.as_ref().and_then(|(n, _)| form.iter().find(|f| &f.name == n)); diff --git a/crates/ui-egui/tests/fonts.rs b/crates/ui-egui/tests/fonts.rs index 6d8ed58bd..d311dfc52 100644 --- a/crates/ui-egui/tests/fonts.rs +++ b/crates/ui-egui/tests/fonts.rs @@ -372,11 +372,12 @@ fn primary_ui_fonts_cover_latin_and_cyrillic_catalogs() { ("sq", include_str!("../src/i18n/sq.tsv")), ("ca", include_str!("../src/i18n/ca.tsv")), ("ro", include_str!("../src/i18n/ro.tsv")), + ("nb", include_str!("../src/i18n/nb.tsv")), ] { // Vietnamese stacks its diacritics: the precomposed letters and the base letters they // combine with, including `đ` and the tone marks `ạ ả ấ ầ ẩ ẫ ậ ắ ằ ẳ ẵ ặ ế ề ể ễ ệ`. let mut text = String::from( - "áčďéěíňóřšťúůýžÁČĎÉĚÍŇÓŘŠŤÚŮÝŽãõçâêôàÃÕÇÂÊÔÀñÑüÜäÄöÖőűß„“¿¡«»…ґҐєЄіІїЇәғқңөұүһӘҒҚҢӨҰҮҺđĐạảấầẩẫậắằẳẵặếềểễệọỏốồổỗộớờởỡợụủứừửữựỳỵỷỹ", + "áčďéěíňóřšťúůýžÁČĎÉĚÍŇÓŘŠŤÚŮÝŽãõçâêôàÃÕÇÂÊÔÀñÑüÜäÄöÖőűßæøåÆØÅ„“¿¡«»…ґҐєЄіІїЇәғқңөұүһӘҒҚҢӨҰҮҺđĐạảấầẩẫậắằẳẵặếềểễệọỏốồổỗộớờởỡợụủứừửữựỳỵỷỹ", ); for line in catalog.lines().filter(|l| !l.starts_with('#')) { if let Some(translation) = line.split('\t').nth(2) { diff --git a/crates/ui-egui/tests/prepare.rs b/crates/ui-egui/tests/prepare.rs index 10bcd90d5..a702ff27e 100644 --- a/crates/ui-egui/tests/prepare.rs +++ b/crates/ui-egui/tests/prepare.rs @@ -2,9 +2,11 @@ //! Field Properties and deleting. use egui::Pos2; +use egui::accesskit::Role; use egui_kittest::Harness; -use egui_kittest::kittest::Queryable; -use pdfcraft_ui_egui::{PdfCraftApp, QuickTool}; +use egui_kittest::kittest::{Queryable, by}; +use pdfcraft_engine::field_table::TableColumn; +use pdfcraft_ui_egui::{Dialog, PdfCraftApp, QuickTool}; fn harness() -> Harness<'static, PdfCraftApp> { let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).build_eframe(|cc| { @@ -710,6 +712,233 @@ fn actions_tab_adds_and_removes_actions() { ); } +/// Text field table: pick the tool and drag out an area in the empty lower part of page 1, 200 by +/// 120 points from (40, 240) as displayed. The table's window opens for it. +fn draw_table(h: &mut Harness<'static, PdfCraftApp>) { + assert!(h.state_mut().execute("form.add.text_table")); + h.run_steps(2); + let (a, b) = (at(h, 40.0, 240.0), at(h, 240.0, 360.0)); + drag(h, a, b); + assert_eq!(h.state().dialog, Some(Dialog::FieldTable)); +} + +fn columns(patterns: &[(&str, f64)]) -> Vec { + patterns.iter().map(|(p, w)| TableColumn { pattern: (*p).to_owned(), width: *w }).collect() +} + +#[test] +fn a_text_field_table_is_drawn_set_up_and_added_in_one_step() { + let mut h = harness(); + let before = names(&h); + draw_table(&mut h); + assert_eq!(h.state().quick_tool, QuickTool::Select, "back to Select once the area is drawn"); + { + let s = h.state_mut(); + let id = s.views[0].id; + let d = s.field_table_draft.as_mut().expect("the table's window is open"); + assert_eq!((d.doc, d.page), (id, 0)); + let r = d.table.rect; + assert!([40.0, 240.0, 240.0, 360.0].iter().zip(r).all(|(w, g)| (w - g).abs() < 0.5), "drawn as displayed: {r:?}"); + d.table.rows = 2; + d.table.columns = columns(&[("desc_N", 3.0), ("price_N", 1.0)]); + } + h.run_steps(2); + h.get_by_label("Add fields").click(); + h.run_steps(3); + assert_eq!((h.state().dialog, h.state().field_table_draft.is_some()), (None, false), "added, the window closes"); + let added: Vec<_> = names(&h).into_iter().filter(|n| !before.contains(n)).collect(); + assert_eq!(added, ["desc_1", "price_1", "desc_2", "price_2"]); + // 150 and 50 points wide, 60 high, the first row at the top: user space has y up from 400. + let r = rect_of(&h, "desc_1"); + assert!([40.0, 100.0, 190.0, 160.0].iter().zip(r).all(|(w, g)| (w - g).abs() < 0.5), "{r:?}"); + let r = rect_of(&h, "price_2"); + assert!([190.0, 40.0, 240.0, 100.0].iter().zip(r).all(|(w, g)| (w - g).abs() < 0.5), "{r:?}"); + assert_eq!(h.state().session.get(h.state().views[0].id).unwrap().can_undo(), Some("Add text field table")); + h.state_mut().execute("edit.undo"); + h.run_steps(2); + assert_eq!(names(&h), before, "one undo takes the whole table back"); +} + +#[test] +fn a_table_whose_names_are_taken_stays_open_saying_so() { + let mut h = harness(); + draw_table(&mut h); + h.get_by_label("Add fields").click(); + h.run_steps(3); + assert!(names(&h).contains(&"Table1_4".to_owned()), "{:?}", names(&h)); + // The next table takes the next free names on its own. + draw_table(&mut h); + assert_eq!(h.state().field_table_draft.as_ref().unwrap().table.columns[0].pattern, "Table2_N"); + // Named like the first, it can't be added: the window says why and keeps what was set up. + h.state_mut().field_table_draft.as_mut().unwrap().table.columns[0].pattern = "Table1_N".into(); + h.run_steps(2); + let count = names(&h).len(); + h.get_by_label("a field named \"Table1_1\" already exists"); + h.get_by_label("Add fields").click(); + h.run_steps(3); + assert_eq!(names(&h).len(), count, "nothing added"); + assert_eq!(h.state().dialog, Some(Dialog::FieldTable), "still open"); + // Escape closes it, adding nothing. + h.key_press(egui::Key::Escape); + h.run_steps(2); + assert_eq!((h.state().dialog, h.state().field_table_draft.is_some(), names(&h).len()), (None, false, count)); +} + +/// Enter commits a number being typed. It used to add the table there and then, before the rest +/// of it was set up. +#[test] +fn enter_in_a_number_field_does_not_add_the_table() { + let mut h = harness(); + let before = names(&h).len(); + draw_table(&mut h); + let rows = h.query_all(by().role(Role::SpinButton)).next().map(|n| n.rect()).expect("the Rows field"); + h.hover_at(rows.center()); + h.drag_at(rows.center()); + h.run_steps(1); + h.drop_at(rows.center()); + h.run_steps(2); + h.key_press(egui::Key::Backspace); + h.event(egui::Event::Text("6".into())); + h.run_steps(1); + h.key_press(egui::Key::Enter); + h.run_steps(3); + assert_eq!(names(&h).len(), before, "nothing added"); + assert_eq!(h.state().field_table_draft.as_ref().map(|d| d.table.rows), Some(6), "the number is in, the window open"); +} + +/// With 26 or more columns each pair of columns has less than twice the least share a divider +/// keeps, and dragging one panicked in `f64::clamp`. +#[test] +fn dragging_a_column_divider_between_many_columns() { + let mut h = harness(); + draw_table(&mut h); + h.state_mut().field_table_draft.as_mut().unwrap().table.columns = columns(&[("c{c}_N", 1.0); 30]); + h.run_steps(2); + let (above, below) = (h.get_by_label("Distribute evenly").rect(), h.get_by_label("Field names").rect()); + let p = egui::pos2(above.center().x - 120.0, (above.bottom() + below.top()) / 2.0); + h.hover_at(p); + h.run_steps(1); + h.drag_at(p); + h.run_steps(1); + for dx in [-10.0, -20.0] { + h.hover_at(p + egui::vec2(dx, 0.0)); + h.run_steps(1); + } + h.drop_at(p + egui::vec2(-20.0, 0.0)); + h.run_steps(2); + let widths: Vec = h.state().field_table_draft.as_ref().unwrap().table.columns.iter().map(|c| c.width).collect(); + assert!(widths.iter().any(|w| (w - 1.0).abs() > 1e-9), "a divider moved: {widths:?}"); + assert!((widths.iter().sum::() - 30.0).abs() < 1e-9 && widths.iter().all(|w| *w > 0.0), "{widths:?}"); +} + +#[test] +fn the_table_window_closes_when_its_document_is_left() { + let mut h = harness(); + let blank = pdfcraft_engine::Session::new().create_blank(300.0, 400.0, 1).unwrap().to_vec(); + h.state_mut().open_bytes("other.pdf", None, blank).unwrap(); + h.state_mut().active = Some(0); + h.run_steps(4); + let before = names(&h).len(); + draw_table(&mut h); + // The other document's tab is still in reach of the pointer: choosing it closes the window. + let tab = h.get_all_by_label("other.pdf").next().map(|n| n.rect()).expect("its tab"); + h.hover_at(tab.center()); + h.run_steps(1); + h.drag_at(tab.center()); + h.run_steps(1); + h.drop_at(tab.center()); + h.run_steps(3); + assert_eq!(h.state().active, Some(1)); + assert_eq!((h.state().dialog, h.state().field_table_draft.is_some()), (None, false)); + h.state_mut().active = Some(0); + h.run_steps(2); + assert_eq!(names(&h).len(), before, "nothing added to either document"); + // So does leaving Prepare a form. + draw_table(&mut h); + h.state_mut().left_open = false; + h.run_steps(2); + assert_eq!((h.state().dialog, h.state().field_table_draft.is_some()), (None, false)); +} + +/// With its panel closed after the tool was picked, the form is still prepared and a table can be +/// drawn: its window opens Prepare a form again, which it belongs to. Drawing sends the tool back +/// to Select, so the window used to close itself at once, nothing being prepared any more. +#[test] +fn a_table_drawn_with_the_panel_closed_opens_it_again() { + let mut h = harness(); + let before = names(&h).len(); + assert!(h.state_mut().execute("form.add.text_table")); + h.run_steps(2); + h.state_mut().left_open = false; + h.run_steps(2); + assert!(h.state().is_preparing(), "the picked tool keeps the form prepared"); + let (a, b) = (at(&h, 40.0, 240.0), at(&h, 240.0, 360.0)); + drag(&mut h, a, b); + h.run_steps(3); + assert_eq!((h.state().dialog, h.state().field_table_draft.is_some()), (Some(Dialog::FieldTable), true), "the window stays open"); + assert!(h.state().left_open && h.state().is_preparing(), "Prepare a form is open again"); + h.get_by_label("Add fields").click(); + h.run_steps(3); + assert!(names(&h).len() > before, "the table is added"); +} + +/// The window belongs to the page as it was when it opened: rotated (or moved, deleted, undone) +/// under it, the page would put the area the table was set up for somewhere else. +#[test] +fn the_table_window_closes_when_the_document_changes_under_it() { + let mut h = harness(); + let before = names(&h); + draw_table(&mut h); + assert!(h.state_mut().apply_edit(pdfcraft_engine::Edit::RotatePages { pages: vec![0], degrees: 90 })); + h.run_steps(2); + assert_eq!((h.state().dialog, h.state().field_table_draft.is_some()), (None, false), "closed by the rotation"); + assert_eq!(names(&h), before, "nothing added"); +} + +/// Escape in a name field leaves the field, as it does in any field; it closed the window too, +/// and the column setup with it. +#[test] +fn escape_in_a_name_field_leaves_the_field_not_the_table() { + let mut h = harness(); + draw_table(&mut h); + // The app's own text boxes (zoom, page number) are inputs too: take the one in the window. + let window = h.get_by_label("Text Field Table").rect(); + let field = + h.query_all(by().role(Role::TextInput)).map(|n| n.rect()).find(|r| window.contains(r.center())).expect("the first column's name field"); + h.hover_at(field.center()); + h.drag_at(field.center()); + h.run_steps(1); + h.drop_at(field.center()); + h.run_steps(2); + h.event(egui::Event::Text("x".into())); + h.run_steps(1); + h.key_press(egui::Key::Escape); + h.run_steps(2); + assert_eq!(h.state().dialog, Some(Dialog::FieldTable), "still open"); + assert_eq!(h.state().field_table_draft.as_ref().map(|d| d.table.columns[0].pattern.as_str()), Some("Table1_Nx"), "what was typed stays"); + h.key_press(egui::Key::Escape); + h.run_steps(2); + assert_eq!((h.state().dialog, h.state().field_table_draft.is_some()), (None, false), "a second Escape closes it"); +} + +/// A click puts the default 180 × 110 table's corner at the pointer; near the page's edges it is +/// moved in so that all of it is on the page (here 5 pt from the right, 100 pt from the bottom). +#[test] +fn a_table_clicked_near_the_page_edge_stays_on_the_page() { + let mut h = harness(); + assert!(h.state_mut().execute("form.add.text_table")); + h.run_steps(2); + let p = at(&h, 295.0, 300.0); + h.hover_at(p); + h.run_steps(1); + h.drag_at(p); + h.run_steps(1); + h.drop_at(p); + h.run_steps(3); + let r = h.state().field_table_draft.as_ref().expect("the table's window").table.rect; + assert!([120.0, 290.0, 300.0, 400.0].iter().zip(r).all(|(w, g)| (w - g).abs() < 0.5), "on the 300 × 400 page: {r:?}"); +} + /// Prepare a form ▸ Calculation order opens as a page of its own, with the form still being /// prepared, says when nothing calculates, and Back returns to Prepare a form. #[test] diff --git a/docs/localization-parity.md b/docs/localization-parity.md index 7ca9ef337..e6754d1ce 100644 --- a/docs/localization-parity.md +++ b/docs/localization-parity.md @@ -1,6 +1,5 @@ # Localization parity -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-13 · **Change:** minor (Vietnamese 100% of current key union; native-speaker community review; twelve-language mean ≈68%) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS) > **Last reviewed:** 2026-10-21 · **Last updated:** 2026-10-21 · **Change:** minor (Brazilian Portuguese now covers the 2,674-key catalog union; twelve-language mean still rounds to ≈68%; Spanish 100% of the current key union) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS) Per-language status of PdfCraft's interface against Acrobat Pro's. How translations work and how to @@ -79,10 +78,11 @@ help/docs polish (catalog and script work are already done), not missing UI stri | Albanian | `sq` | 2,344 entries | partial; not yet reviewed by a native speaker | no | | Catalan | `ca` | 2,592 entries | partial: no translated help, engine error details in English; native-speaker review by the contributor | no | | Romanian | `ro` | 2,673 entries (every bundled key) | partial; AI-assisted, not yet reviewed line by line by a native speaker | no | +| Norwegian Bokmål | `nb` | 2,674 entries | partial; native-speaker terminology review by the contributor ([#1836](https://github.com/storytold/pdfcraft/pull/1836)), not yet line by line | yes | -**15 other languages** ship (27 catalogs plus English = **28** interface languages in Preferences). -The entry counts without percentages were measured on 2026-10-12–14; they are not percentages of the older 2,143-key snapshot above. Their presence does not imply a detailed linguistic or runtime review. -Acrobat languages PdfCraft lacks: Danish, Dutch, Norwegian, Swedish, Hebrew. Vietnamese is shipped here but not by Acrobat. +**19 other languages** ship (33 catalogs plus English = **34** interface languages in Preferences). +The entry counts without percentages were measured on 2026-10-12–14 (Norwegian Bokmål on 2026-10-18); they are not percentages of the older 2,143-key snapshot above. Their presence does not imply a detailed linguistic or runtime review. +Acrobat languages PdfCraft lacks: Danish, Dutch, Swedish, Hebrew. Vietnamese is shipped here but not by Acrobat. ## Revision history @@ -90,6 +90,7 @@ Acrobat languages PdfCraft lacks: Danish, Dutch, Norwegian, Swedish, Hebrew. Vie |---|---|---| | 2026-10-21 | minor | Brazilian Portuguese (`pt-br`) now covers all 2,674 distinct keys in the current catalog union; no help or engine/OS error-detail localization; twelve-language mean still rounds to ≈ 68% | | 2026-10-20 | minor | Spanish: the 143 keys the other catalogs had and `es.tsv` was missing; `es` now covers the whole 2,674-key union, like `lt` and `vi`. Restated Lithuanian against the current union (its 2,585 was measured on 2026-10-13) and said plainly in *How this was measured* that the union moves, so counts only compare within a date | +| 2026-10-19 | minor | Removed the 2026-10-13 status line and the 14-language summary that concurrent merges left beside the current ones; the summary keeps the Italian and Norwegian Bokmål measurement dates | | 2026-10-16 | minor | Czech: 2,674 entries after adding the keys the other catalogs had (the engine's sentences, weekday heads, certificate fields, shared buttons) and unifying "tag" as *tag* (not *značka*, which is a redaction mark) | | 2026-10-13 | minor | Vietnamese (`vi`) catalog: 2,591 entries (100% of current key union + `theme`/`Appearance`); twelve-language mean ≈ 68%; native-speaker community review; registered with `plural_none` | | 2026-10-14 | minor | Italian: 2,528 entries after adding the 201 keys the other catalogs had and the code looks up | @@ -97,5 +98,6 @@ Acrobat languages PdfCraft lacks: Danish, Dutch, Norwegian, Swedish, Hebrew. Vie | 2026-10-12 | minor | Added Albanian (`sq`): catalog, date names, Preferences entry | | 2026-10-14 | minor | Added Catalan (`ca`, 2,592 entries, measured 2026-10-14); retained existing language assessments | | 2026-10-15 | minor | Romanian (`ro`) catalog: 2,673 entries (every bundled key); three plural forms (`plural_ro`); not one of the twelve key languages, so the mean is unchanged | +| 2026-10-15 | minor | Norwegian Bokmål added as an interface and date language with a 2,674-entry catalog | | 2026-10-12 | minor | Added current counts for Kazakh, Polish, Slovenian, Greek, Turkish and Lithuanian; retained existing language assessments and the original percentage snapshot | | 2026-10-10 | major | Created. Catalog counts measured from `crates/ui-egui/src/i18n/*.tsv` against the 2,143-key union; Acrobat's localizations from its bundle's `.lproj` and Sequences folder names | diff --git a/docs/localization.md b/docs/localization.md index 66f1d6a99..586a85ba5 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**, **العربية**, **کوردیی ناوەندی**, **Polski**, **Slovenščina**, **Ελληνικά**, **Türkçe**, **Lietuvių**, **Tiếng Việt**, **Suomi**, **Shqip**, **Català** or **Română**. 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**, **العربية**, **کوردیی ناوەندی**, **Polski**, **Slovenščina**, **Ελληνικά**, **Türkçe**, **Lietuvių**, **Tiếng Việt**, **Suomi**, **Shqip**, **Català**, **Română** or **Norsk bokmål**. 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, and Slovenian locales (`sl`, `sl_SI`) the Slovenian one, Vietnamese locales (`vi`, `vi_VN`) the Vietnamese one, Albanian locales (`sq`, `sq_AL`, `sq_XK`, …) the Albanian one, Catalan locales (`ca`, `ca_ES`, `ca_AD`, `ca_ES@valencia`, …) the Catalan one, and Romanian locales (`ro`, `ro_RO`, `ro-RO`, `ro_MD`) the Romanian 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 Turkish locales (`tr`, `tr_TR`) the Turkish one, and Slovenian locales (`sl`, `sl_SI`) the Slovenian one, Vietnamese locales (`vi`, `vi_VN`) the Vietnamese one, Albanian locales (`sq`, `sq_AL`, `sq_XK`, …) the Albanian one, Catalan locales (`ca`, `ca_ES`, `ca_AD`, `ca_ES@valencia`, …) the Catalan one, and Romanian locales (`ro`, `ro_RO`, `ro-RO`, `ro_MD`) the Romanian one. Norwegian Bokmål locales (`nb`, `nb_NO`) use the Bokmål catalog, and so do general Norwegian (`no`, `no_NO`) and Nynorsk (`nn`, `nn_NO`), which have no catalog of their own. 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,9 +12,9 @@ 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`, `cs`, `pt-br`, `de`, `es`, `fr`, `ru`, `kk`, `bg`, `te`, `hu`, `uk`, `it`, `ar`, `ckb`, `pl`, `sl`, `el`, `tr`, `lt`, `vi`, `fi`, `sq`, `ca` or `ro`), in any case. `ui.state` reports `language` as written in the preferences (`auto`, `en`, `ja`, `zh-hans`, `zh-hant`, `cs`, `pt-br`, `de`, `es`, `fr`, `ru`, `kk`, `bg`, `te`, `hu`, `uk`, `it`, `ar`, `ckb`, `pl`, `sl`, `el`, `tr`, `lt`, `vi`, `fi`, `sq`, `ca` or `ro`). 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`, `cs`, `pt-br`, `de`, `es`, `fr`, `ru`, `kk`, `bg`, `te`, `hu`, `uk`, `it`, `ar`, `ckb`, `pl`, `sl`, `el`, `tr`, `lt`, `vi`, `fi`, `sq`, `ca`, `ro` or `nb`), in any case. `ui.state` reports `language` as written in the preferences (`auto`, `en`, `ja`, `zh-hans`, `zh-hant`, `cs`, `pt-br`, `de`, `es`, `fr`, `ru`, `kk`, `bg`, `te`, `hu`, `uk`, `it`, `ar`, `ckb`, `pl`, `sl`, `el`, `tr`, `lt`, `vi`, `fi`, `sq`, `ca`, `ro` or `nb`). 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!`). Most shipped catalogs (Japanese, Chinese, Russian, Bulgarian, German, Spanish, French, Telugu, Hungarian, Ukrainian, Italian, Brazilian Portuguese, Arabic, Kurdish, Slovenian, Kazakh, Polish, Greek, Turkish, Lithuanian, Vietnamese, Romanian and others) cover the current localizable surface (about 2,000–2,700 entries each), and Czech (2,674 entries) covers the same surface. 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. Document contents, filenames, CLI/MCP/control protocol strings and external help stay outside the catalog. 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!`). Most shipped catalogs (Japanese, Chinese, Russian, Bulgarian, German, Spanish, French, Telugu, Hungarian, Ukrainian, Italian, Brazilian Portuguese, Arabic, Kurdish, Slovenian, Kazakh, Polish, Greek, Turkish, Lithuanian, Vietnamese, Romanian, Norwegian Bokmål and others) cover the current localizable surface (about 2,000–2,700 entries each), and Czech (2,674 entries) covers the same surface. 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. Document contents, filenames, CLI/MCP/control protocol strings and external help stay outside the catalog. 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). @@ -30,11 +30,11 @@ The Catalan catalog has 2,592 entries, written from the meaning of the English l The Romanian catalog (`ro`, Română) has 2,673 entries and three plural forms (`plural_ro`, CLDR `ro`): `one` for 1, `few` for 0 and counts whose last two digits are 1–19 ("2 pagini", "101 pagini"), `other` for the rest, written with "de" ("20 de pagini"). Commands use the noun form common in Romanian software ("Salvare", "Deschidere…"); messages address the user formally. Redaction is "Anonimizare" ("redactare" means editing in Romanian), a watermark "Filigran". Calendar month names are lowercase inside dates (`{day} {month} {y}`). The bundled Inter faces cover ă, â, î, ș, ț (comma below). Coverage tests require every bundled key. Not yet reviewed by a second native speaker; status `partial`: no translated help, and engine/OS details stay English inside a translated frame. +The Norwegian Bokmål catalog covers every key the other bundled catalogs have, with the English one/other plural rule, Norwegian key names (`Mellomrom`; Shift keeps its name) and month and weekday names for Fill & Sign dates. It was written from the meaning of the English labels; a native-speaking contributor reviewed its terminology (key names, Opacity, Redact, Highlight, Fill & Sign), not yet every line. + 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 in Edit text. Add text (#128) draws Hebrew with the same face through the shaped path Arabic and Thai use: right to left with numbers and Latin in place, points kept on their letter, and typing order for copy and search. 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, and refuse Hebrew in Add text. -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, Turkish, Lithuanian and Albanian 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, Slovenian and Albanian 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, Italian and Albanian 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). 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, Italian, Slovenian, Turkish, Lithuanian, Norwegian, Finnish, Albanian, Catalan and Romanian 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. The Brazilian Portuguese catalog has 2,674 entries and covers the current union of catalog keys. It began with the ~1,826-entry gist André Cardoso posted in [#288](https://github.com/storytold/pdfcraft/issues/288), with labels added since and the existing wording reviewed. Portuguese, like French, treats 0 and 1 as singular (`1 página`, `2 páginas`). diff --git a/docs/roadmap.md b/docs/roadmap.md index 004421b92..f2972ab10 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -170,7 +170,7 @@ total matches the parity estimate. | M4 | Engine, history, save, organize | 100–180 | 95% | 8–15 | Done:
- command registry (menus, shortcuts and palette all use it);
- undo/redo; incremental, atomic and encrypted saves;
- autosave and crash recovery;
- organize, combine, extract, split and insert-from-file, with identical fonts and images stored once;
- CLI `edit/combine/extract/split`.
Done since: Combine files in its own tab (sortable, resizable columns; folders; early warnings; unlocking protected files), bookmark editing, page labels (Number pages), CLI `run`, Set Page Boxes, Crop tool, Duplicate pages, Ctrl+A / Cmd+A to select all pages in Organize, Replace pages, bookmarks from tagged headings. Missing: page transitions, recovery on the web M4: Insert-from-file preserves source bookmarks; integration tests retained with current main. | | M5 | Comments (all annotation types, XFDF) | 120–200 | 88% | 12–25 | Done: notes, highlight/underline/strikeout/squiggly, text boxes, ink, lines, arrows, rectangles, ovals, stamps (dynamic, Sign Here, Standard Business), with appearance streams; replies, status, checkmarks, locking, move/resize/restyle/delete; properties dialog; panel filter and sort; hide all; summaries (comments only); XFDF/FDF import and export (partial imports preserve existing reply and review-status links); flatten; Fill & Sign with adjustable text/date size; agent tools.
Done since: stamps placed on a rotated page read upright (#1398); polygons, connected lines, clouds, callouts, inserted text, colour and checkmark filters; image signature pointer previews, selection handles and live image movement/resizing with proportional corners and independent edge handles. Missing: custom stamps, replace-text proposals, summary layouts with the page | | M5 | Comments (all annotation types, XFDF) | 120–200 | 88% | 12–25 | Done: notes, highlight/underline/strikeout/squiggly, text boxes, ink, lines, arrows, rectangles, ovals, stamps (dynamic, Sign Here, Standard Business), with appearance streams; replies, status, checkmarks, locking, move/resize/restyle/delete; properties dialog; panel filter and sort; hide all; summaries (comments only); XFDF/FDF import and export (partial imports preserve existing reply and review-status links; nested FDF export uses lazy array owners and annotation positions, with retained entries budgeted before cloning under copied-graph limits and source `/Page` and `/Pages` dictionaries replaced by null at every copy depth); flatten; Fill & Sign with adjustable text/date size; agent tools.
Done since: polygons, connected lines, clouds, callouts, inserted text, colour and checkmark filters; image signature pointer previews, selection handles and live image movement/resizing with proportional corners and independent edge handles. A refused restyle or resize of a text box another program drew leaves that comment unchanged (#1327). Make Current Properties Default on an arrow updates arrows, not plain lines (#1302). Flattening keeps a visible comment it cannot draw (#1317). Missing: custom stamps, replace-text proposals, summary layouts with the page | -| M6 | Forms + JavaScript | 160–320 | 50% | 40–75 | Done: filling all field types with regenerated appearances; Prepare a form (every field type, multi-select/shared properties, move/resize/delete, Field Properties General/Appearance/Position/Options/Format/Validate/Calculate); Acrobat's AF format/keystroke/validate/calculate functions and simplified field notation run natively in Acrobat's event order; tab order; push-button actions; flatten; agent tools. Form data exchange (FDF, XFDF, XML, CSV, text) done; XFDF values retain text across XML comments and processing instructions. Plain AcroForm XML imports retain all direct text around comments and processing instructions, including CDATA and character references. CSV export and spreadsheet merging quote tabs, and import distinguishes quoted tabs from TSV delimiters without changing field identities. XML/XFDF exports preserve tabs and line breaks in names and comment metadata. Refreshed against upstream `99941daa` (2026-10-15).
Done since: JavaScript engine (boa, sandboxed) running custom keystroke/validate/calculate/format and button scripts, document JavaScripts, console. Done since: Push-button normal labels are editable per widget through Options and form_set_props, independently of field names; rendering, undo/redo and incremental/full save/reopen are covered. (#2892). Missing: Actions tab, wider object model (annotations, layers, dialogs), XFA
Done since: Multi-select choice fields write ascending `/I` option indices while preserving `/V` selection order; synthetic forms/engine regressions cover reverse selection and save/reopen. CSV export preserves carriage returns in names and values.
Done since: Form detection preserves comments across content streams and does not add fields over rectangles hidden by those comments. PR #2279 refreshed on main: field highlighting defaults on, stored choices are retained; secured-form toggle expectations updated. CI validation pending; estimates unchanged. | +| M6 | Forms + JavaScript | 160–320 | 50% | 40–75 | Done: filling all field types with regenerated appearances; Prepare a form (every field type, multi-select/shared properties, move/resize/delete, Field Properties General/Appearance/Position/Options/Format/Validate/Calculate); Acrobat's AF format/keystroke/validate/calculate functions and simplified field notation run natively in Acrobat's event order; tab order; push-button actions; flatten; agent tools. Form data exchange (FDF, XFDF, XML, CSV, text) done; XFDF values retain text across XML comments and processing instructions. Plain AcroForm XML imports retain all direct text around comments and processing instructions, including CDATA and character references. CSV export and spreadsheet merging quote tabs, and import distinguishes quoted tabs from TSV delimiters without changing field identities. XML/XFDF exports preserve tabs and line breaks in names and comment metadata. Refreshed against upstream `99941daa` (2026-10-15).
Done since: JavaScript engine (boa, sandboxed) running custom keystroke/validate/calculate/format and button scripts, document JavaScripts, console. Done since: Push-button normal labels are editable per widget through Options and form_set_props, independently of field names; rendering, undo/redo and incremental/full save/reopen are covered. (#2892). Missing: Actions tab, wider object model (annotations, layers, dialogs), XFA
Done since: Multi-select choice fields write ascending `/I` option indices while preserving `/V` selection order; synthetic forms/engine regressions cover reverse selection and save/reopen. CSV export preserves carriage returns in names and values.
Done since: Text field table: an area divided into rows and columns of named text fields, added as one undoable edit, with `form_add_field_table`; copying an existing field into a grid remains. Form detection preserves comments across content streams and does not add fields over rectangles hidden by those comments. PR #2279 refreshed on main: field highlighting defaults on, stored choices are retained; secured-form toggle expectations updated. CI validation pending; estimates unchanged. | | M6 | Forms + JavaScript | 160–320 | 50% | 40–75 | Done: filling all field types with regenerated appearances; Prepare a form (every field type, multi-select/shared properties, move/resize/delete, Field Properties General/Appearance/Position/Options/Format/Validate/Calculate); Acrobat's AF format/keystroke/validate/calculate functions and simplified field notation run natively in Acrobat's event order; tab order; push-button actions; flatten; agent tools. Form data exchange (FDF, XFDF, XML, CSV, text) done; XFDF values retain text across XML comments and processing instructions. Plain AcroForm XML imports retain all direct text around comments and processing instructions, including CDATA and character references.
Done since: JavaScript engine (boa, sandboxed) running custom keystroke/validate/calculate/format and button scripts, document JavaScripts, console. Missing: Actions tab, wider object model (annotations, layers, dialogs), XFA
Done since: Multi-select choice fields write ascending `/I` option indices while preserving `/V` selection order; synthetic forms/engine regressions cover reverse selection and save/reopen. CSV export preserves carriage returns in names and values. | | M6 | Forms + JavaScript | 160–320 | 50% | 40–75 | Done: filling all field types with regenerated appearances; Prepare a form (every field type, multi-select/shared properties, move/resize/delete, Field Properties General/Appearance/Position/Options/Format/Validate/Calculate); Acrobat's AF format/keystroke/validate/calculate functions and simplified field notation run natively in Acrobat's event order; tab order; push-button actions; flatten; agent tools. Form data exchange (FDF, XFDF, XML, CSV, text) done; XFDF values retain text across XML comments and processing instructions. XML/XFDF exports preserve tabs and line breaks in names and comment metadata. Refreshed against upstream `99941daa` (2026-10-15).
Done since: JavaScript engine (boa, sandboxed) running custom keystroke/validate/calculate/format and button scripts, document JavaScripts, console. Missing: Actions tab, wider object model (annotations, layers, dialogs), XFA
Done since: Multi-select choice fields write ascending `/I` option indices while preserving `/V` selection order; synthetic forms/engine regressions cover reverse selection and save/reopen. CSV export preserves carriage returns in names and values. | | M6 | Forms + JavaScript | 160–320 | 50% | 40–75 | Done: filling all field types with regenerated appearances; Prepare a form (every field type, multi-select/shared properties, move/resize/delete, Field Properties General/Appearance/Position/Options/Format/Validate/Calculate); Acrobat's AF format/keystroke/validate/calculate functions and simplified field notation run natively in Acrobat's event order; tab order; push-button actions; flatten; agent tools. Form data exchange (FDF, XFDF, XML, CSV, text) done; XFDF values retain text across XML comments and processing instructions. XML/XFDF exports preserve tabs and line breaks in names and comment metadata. Plain AcroForm XML imports retain all direct text around comments and processing instructions, including CDATA and character references. CSV export and spreadsheet merging quote tabs, and import distinguishes quoted tabs from TSV delimiters without changing field identities.
Done since: JavaScript engine (boa, sandboxed) running custom keystroke/validate/calculate/format and button scripts, document JavaScripts, console. Missing: Actions tab, wider object model (annotations, layers, dialogs), XFA
Done since: Multi-select choice fields write ascending `/I` option indices while preserving `/V` selection order; synthetic forms/engine regressions cover reverse selection and save/reopen. CSV export preserves carriage returns in names and values. | @@ -188,7 +188,7 @@ total matches the parity estimate. | M13 | Automation (MCP, Action Wizard, CLI) + AI providers | 60–120 | 45% | 25–45 | Done: MCP resources (document info, text, page images); headless tool table (123 tools incl. signing, optimizing, initial view, links, stamps, data exchange, comment review, forms authoring and scripts, redaction, sanitize, print, add content), opt-in MCP server over stdio, CLI `run`/`tools` (closed stdout pipes exit cleanly), UI control channel with drag. Missing: Action Wizard, AI providers
Done since: CLI run and MCP reject missing, empty, repeated and option-valued --root arguments before any tool/script execution or MCP initialization. A single-hyphen directory name remains valid; synthetic CLI regressions preserve a would-be overwritten file. `text_find` also caps the per-page match ranges it materializes at the remaining result limit plus one continuation match. CLI robustness scans visit each directory once through overlapping roots and symlink aliases. | | M12 | Accessibility, compare, measure, search, XFA | 200–380 | 30% | 90–160 | Done: new `a11y` crate with the Accessibility Checker (all 32 rules, report, Fix/Skip/Explain, options dialog and results panel, agent tools). Locatable figure, annotation and form findings highlight their page bounds. Character encoding accepts Identity-H/V with Adobe CJK collections and predefined CJK CMaps independently (#3673); per-glyph validation and embedded-font extraction remain separate.
Done since: 2D distance, perimeter and area measurements with persistent viewport calibration, snapping, live information and CSV export; a bounded text-only auto-tag proposal tool; text-only Apply writes marked content, paragraph tags and a parent tree, then opens the Tags panel for review. Missing: mixed-content and field autotag, heading/list/table/figure classification, Order/Content panels, Reading Order tool, compare, geospatial/3D measurement, search index, XFA
Done since: CLI info, text and render use dynamic XFA layout pages, with synthetic count/text/pixel and copy-permission regressions (#1645).
Reading colours: pending page backgrounds follow the configured colour (#2059). | | M13 | Automation (MCP, Action Wizard, CLI) + AI providers | 60–120 | 45% | 25–45 | Done: MCP resources (document info, text, page images); headless tool table (123 tools incl. signing, optimizing, initial view, links, stamps, data exchange, comment review, forms authoring and scripts, redaction, sanitize, print, add content), opt-in MCP server over stdio, CLI `run`/`tools` (closed stdout pipes exit cleanly), UI control channel with drag. Missing: Action Wizard, AI providers
Done since: CLI run and MCP reject missing, empty, repeated and option-valued --root arguments before any tool/script execution or MCP initialization. A single-hyphen directory name remains valid; synthetic CLI regressions preserve a would-be overwritten file. `text_find` also caps the per-page match ranges it materializes at the remaining result limit plus one continuation match. | -| M14 | 1.0 polish: performance, localization, installers | 120–250 | 15% | 80–150 | Done: PhotoCraft's translation system (`i18n/`: TSV catalogs, `tl!`, command-id and plural entries, system-language detection, strict catalog tests); every dialog, panel and notice goes through `tl!`. Catalogs of ≈ 2,000–2,140 entries for Japanese, Simplified and Traditional Chinese, Russian, Bulgarian, German, Spanish, French, Telugu, Hungarian, Ukrainian and Arabic (logical-order catalog put into display order; layout not yet mirrored); the 2,674-entry Brazilian Portuguese catalog covers the current key union; Czech covers the menus. Italian has 2,528 entries: every key of the other catalogs that the code looks up. Signed and notarized macOS builds, signed Windows installers (x64, x86, ARM64), AppImage, deb, rpm, Flatpak and FreeBSD packages. #307 (bounded large-PDF memory): shared resource indexes and parser metadata, lazy inspection, bounded caches; a 9,156-page file inspects in ≈ 1.8 s at ≈ 500 MiB instead of exceeding 10 GiB. Edits let go of the previous working file before writing the next (a 1 GiB file peaks at two copies while editing, not three; gap 13). Shortcut display uses localized modifier/key names across menus, toolbar, palette, Help and tooltips (#496). Lithuanian adds a 2,578-entry catalog and date names, reviewed by a native-speaking contributor. Validated candidate (macOS raster probe; Windows runtime unverified): the Windows policy changes only embedded JetBrains Mono; horizontal alpha-edge energy per ink improves 2.78%, 3.87% and 1.59% at 100%, 125% and 150%, and is down 0.79% at 200%. Inter is pixel-identical; layout hashes, glyph coverage and the 8 MiB atlas are unchanged. Timing and RSS ranges overlap, with no improvement claimed. Estimates unchanged. Candidate #2383 clamps GPU requests to supported limits while retaining the adapter texture extent; synthetic V3D/downlevel and Windows GPU-required tests pass, but Raspberry Pi startup is unverified. Estimates unchanged. Missing: a right-to-left layout, a Chinese UI face in releases, Hindi/Indonesian/Korean, file-backed input and global memory budgets, performance budgets, keyboard-only operation Linux/Unix UI fallback fonts also use Fontconfig directories, including Nix store paths, with bounded discovery and the existing candidate allowlist; relative includes and digit-prefixed config filtering have isolated regressions. | +| M14 | 1.0 polish: performance, localization, installers | 120–250 | 15% | 80–150 | Done: PhotoCraft's translation system (`i18n/`: TSV catalogs, `tl!`, command-id and plural entries, system-language detection, strict catalog tests); every dialog, panel and notice goes through `tl!`. Catalogs of ≈ 2,000–2,140 entries for Japanese, Simplified and Traditional Chinese, Russian, Bulgarian, German, Spanish, French, Telugu, Hungarian, Ukrainian and Arabic (logical-order catalog put into display order; layout not yet mirrored); the 2,674-entry Brazilian Portuguese catalog covers the current key union; Czech covers the menus. Italian has 2,528 entries: every key of the other catalogs that the code looks up. Signed and notarized macOS builds, signed Windows installers (x64, x86, ARM64), AppImage, deb, rpm, Flatpak and FreeBSD packages. #307 (bounded large-PDF memory): shared resource indexes and parser metadata, lazy inspection, bounded caches; a 9,156-page file inspects in ≈ 1.8 s at ≈ 500 MiB instead of exceeding 10 GiB. Edits let go of the previous working file before writing the next (a 1 GiB file peaks at two copies while editing, not three; gap 13). Shortcut display uses localized modifier/key names across menus, toolbar, palette, Help and tooltips (#496). Lithuanian adds a 2,578-entry catalog and date names, reviewed by a native-speaking contributor. Norwegian Bokmål adds a catalog and date names (terminology reviewed by a native speaker). Validated candidate (macOS raster probe; Windows runtime unverified): the Windows policy changes only embedded JetBrains Mono; horizontal alpha-edge energy per ink improves 2.78%, 3.87% and 1.59% at 100%, 125% and 150%, and is down 0.79% at 200%. Inter is pixel-identical; layout hashes, glyph coverage and the 8 MiB atlas are unchanged. Timing and RSS ranges overlap, with no improvement claimed. Estimates unchanged. Candidate #2383 clamps GPU requests to supported limits while retaining the adapter texture extent; synthetic V3D/downlevel and Windows GPU-required tests pass, but Raspberry Pi startup is unverified. Estimates unchanged. Missing: a right-to-left layout, a Chinese UI face in releases, Hindi/Indonesian/Korean, file-backed input and global memory budgets, performance budgets, keyboard-only operation Linux/Unix UI fallback fonts also use Fontconfig directories, including Nix store paths, with bounded discovery and the existing candidate allowlist; relative includes and digit-prefixed config filtering have isolated regressions. | | M14 | 1.0 polish: performance, localization, installers | 120–250 | 15% | 80–150 | Done: PhotoCraft's translation system (`i18n/`: TSV catalogs, `tl!`, command-id and plural entries, system-language detection, strict catalog tests); every dialog, panel and notice goes through `tl!`. Catalogs of ≈ 2,000–2,140 entries for Japanese, Simplified and Traditional Chinese, Russian, Bulgarian, German, Spanish, French, Telugu, Hungarian, Ukrainian and Arabic (logical-order catalog put into display order; layout not yet mirrored); the 2,674-entry Brazilian Portuguese catalog covers the current key union; Czech covers the menus. Italian has 2,528 entries: every key of the other catalogs that the code looks up. Signed and notarized macOS builds, signed Windows installers (x64, x86, ARM64), AppImage, deb, rpm, Flatpak and FreeBSD packages. #307 (bounded large-PDF memory): shared resource indexes and parser metadata, lazy inspection, bounded caches; a 9,156-page file inspects in ≈ 1.8 s at ≈ 500 MiB instead of exceeding 10 GiB. Edits let go of the previous working file before writing the next (a 1 GiB file peaks at two copies while editing, not three; gap 13). Shortcut display uses localized modifier/key names across menus, toolbar, palette, Help and tooltips (#496). Lithuanian adds a 2,578-entry catalog and date names, reviewed by a native-speaking contributor. Validated candidate (macOS raster probe; Windows runtime unverified): the Windows policy changes only embedded JetBrains Mono; horizontal alpha-edge energy per ink improves 2.78%, 3.87% and 1.59% at 100%, 125% and 150%, and is down 0.79% at 200%. Inter is pixel-identical; layout hashes, glyph coverage and the 8 MiB atlas are unchanged. Timing and RSS ranges overlap, with no improvement claimed. Estimates unchanged. Missing: a right-to-left layout, a Chinese UI face in releases, Hindi/Indonesian/Korean, file-backed input and global memory budgets, performance budgets, keyboard-only operation XDG desktops use the configuration base for preferences with non-overwriting migration and original-data preservation (#1395). | | M13 | Automation (MCP, Action Wizard, CLI) + AI providers | 60–120 | 45% | 25–45 | Done: MCP resources (document info, text, page images); headless tool table (123 tools incl. signing, optimizing, initial view, links, stamps, data exchange, comment review, forms authoring and scripts, redaction, sanitize, print, add content), opt-in MCP server over stdio, CLI `run`/`tools` (closed stdout pipes exit cleanly), UI control channel with drag. Missing: Action Wizard, AI providers | | M14 | 1.0 polish: performance, localization, installers | 120–250 | 15% | 80–150 | Done: PhotoCraft's translation system (`i18n/`: TSV catalogs, `tl!`, command-id and plural entries, system-language detection, strict catalog tests); every dialog, panel and notice goes through `tl!`. Catalogs of ≈ 2,000–2,140 entries for Japanese, Simplified and Traditional Chinese, Russian, Bulgarian, German, Spanish, French, Telugu, Hungarian, Ukrainian and Arabic (logical-order catalog put into display order; layout not yet mirrored); the 2,674-entry Brazilian Portuguese catalog covers the current key union; Czech covers the menus. Italian has 2,528 entries: every key of the other catalogs that the code looks up. Signed and notarized macOS builds, signed Windows installers (x64, x86, ARM64), AppImage, deb, rpm, Flatpak and FreeBSD packages. #307 (bounded large-PDF memory): shared resource indexes and parser metadata, lazy inspection, bounded caches; a 9,156-page file inspects in ≈ 1.8 s at ≈ 500 MiB instead of exceeding 10 GiB. Edits let go of the previous working file before writing the next (a 1 GiB file peaks at two copies while editing, not three; gap 13). Shortcut display uses localized modifier/key names across menus, toolbar, palette, Help and tooltips (#496). Traditional Chinese accessibility statuses, rule semantics and Taiwan form/table terms have catalog regressions. Long main-menu lists scroll in short windows (#3373). Missing: a right-to-left layout, a Chinese UI face in releases, Hindi/Indonesian/Korean, file-backed input and global memory budgets, performance budgets, keyboard-only operation | @@ -252,6 +252,7 @@ M2 (renderer), M7 (content editing), M10 (OCR and Office), M11 (Preflight and st | 2026-10-20 | minor | M3/#3620: overlapping comment hover follows original annotation indices independently of spatial inspection sorting; a synthetic differing-upper-edge regression covers paint order. Estimates unchanged. | | 2026-10-20 | minor | M10, #3532 partial: vector printing retains existing appearances with absent Subtype; estimates unchanged. | | 2026-10-19 | minor | M4 #3309: named-destination pruning keeps large documents usable; 60,000-name and deep-tree regressions verify targets through undo/redo and both save modes. Estimates unchanged. | +| 2026-10-19 | minor | M6: Text field table (rows and columns of text fields as one edit, `form_add_field_table`); estimates unchanged | | 2026-10-18 | minor | M5, PR #1971: nested source `/Page` and `/Pages` dictionaries export as null without traversing page content or the page tree. Synthetic saved/reopened three-page regression retains unrelated metadata and aliases. FDF import semantics, copied-graph limits and estimates unchanged. | | 2026-10-15 | minor | M5: budget retained FDF entries before cloning; discovery keeps lazy array owners and annotation positions, with alias/cache-eviction stress fixtures. Limits cover the copied graph; source parsing/page discovery retain existing COS/parser limits, with no total source-memory bound. FDF import semantics and estimates unchanged. | | 2026-10-15 | minor | M7: Edit text caret height follows its row, with a kittest regression; estimates unchanged | diff --git a/parity/acrobat-features.toml b/parity/acrobat-features.toml index 686711c16..c06f26e24 100644 --- a/parity/acrobat-features.toml +++ b/parity/acrobat-features.toml @@ -5049,11 +5049,16 @@ title = "Create multiple copies of a field in a grid" area = "F" tier = "P2" milestone = "M6" -status = "planned" -commands = [] -tools = [] -evidence = [] -notes = "" +status = "partial" +commands = ["form.add.text_table"] +tools = ["form_add_field_table"] +evidence = [ + "crates/forms/src/table.rs::a_grid_fills_its_area_with_gaps_between_the_cells", + "crates/engine/src/field_table.rs::a_table_is_added_and_undone_as_one_step", + "crates/automation/tests/automation.rs::adding_a_field_table_through_tools", + "crates/ui-egui/tests/prepare.rs::a_text_field_table_is_drawn_set_up_and_added_in_one_step", +] +notes = "Prepare a form ▸ Text field table: drag an area, then set rows, columns, spacing, column widths (draggable dividers) and a name pattern per column in a movable window, with the table previewed on the page. Added as one undoable edit, or refused whole with the reason (a cell under 4 pt, a name with a period, a repeated or existing name); form_add_field_table does the same headlessly. Missing: copying an existing field of any type, with its properties, into a grid from its context menu, which is what this entry describes; the table only creates new text fields." [[feature]] id = "form.rename-lock-fields" @@ -9960,8 +9965,8 @@ milestone = "M14" status = "partial" commands = ["app.preferences"] tools = [] -evidence = ["crates/ui-egui/src/commands.rs::localized_shortcut_labels_follow_language_and_platform", "crates/ui-egui/src/i18n/mod.rs::keyboard_key_names_have_a_separate_context_and_english_fallback", "crates/ui-egui/tests/commands.rs::localized_shortcuts_update_immediately_and_keep_command_ids_and_key_bindings", "crates/ui-egui/src/i18n/mod.rs::windows_display_language_resolves_chinese_scripts", "crates/ui-egui/src/i18n/mod.rs::neutral_locale_does_not_override_display_language", "crates/ui-egui/src/i18n/mod.rs::windows_display_language_query_returns_a_locale_tag", "crates/ui-egui/src/i18n/mod.rs::language_persists_and_invalid_input_keeps_current_language", "crates/ui-egui/src/i18n/mod.rs::bundled_catalogs_are_consistent", "crates/ui-egui/src/i18n/mod.rs::arabic_is_registered_and_persists", "crates/ui-egui/src/i18n/mod.rs::arabic_lookups_are_in_display_order", "crates/ui-egui/src/i18n/mod.rs::arabic_plurals_follow_the_six_cldr_forms", "crates/ui-egui/src/i18n/mod.rs::arabic_covers_commands_and_catalogue", "crates/ui-egui/src/i18n/mod.rs::arabic_covers_ui_literals", "crates/ui-egui/src/i18n/mod.rs::arabic_labels_have_glyphs", "crates/ui-egui/src/bidi.rs::rtl_labels_keep_placeholders_whole", "crates/ui-egui/src/i18n/mod.rs::japanese_covers_commands_and_catalogue", "crates/ui-egui/src/i18n/mod.rs::simplified_chinese_covers_commands_and_catalogue", "crates/ui-egui/src/i18n/mod.rs::simplified_chinese_covers_ui_literals", "crates/ui-egui/src/i18n/mod.rs::simplified_chinese_history_and_diagnostics_preserve_user_values", "crates/ui-egui/tests/control.rs::simplified_chinese_signature_prompts_and_errors_keep_document_state", "crates/ui-egui/tests/control.rs::language_switch_preserves_document_and_command_ids", "crates/ui-egui/tests/control.rs::japanese_controls_and_search_keep_command_ids", "crates/ui-egui/tests/control.rs::japanese_dialogs_errors_and_custom_action_names", "crates/ui-egui/src/i18n/mod.rs::french_is_registered", "crates/ui-egui/src/i18n/mod.rs::french_covers_commands_and_catalogue", "crates/ui-egui/src/i18n/mod.rs::french_covers_ui_literals", "crates/ui-egui/src/i18n/mod.rs::french_history_and_diagnostics_preserve_user_values", "crates/ui-egui/src/i18n/mod.rs::german_certificate_subject_has_a_distinct_context", "crates/ui-egui/src/widgets.rs::search_placeholder_never_overlaps_the_shortcut", "crates/ui-egui/src/home.rs::translated_card_description_leaves_room_for_the_action", "crates/ui-egui/tests/control.rs::german_preferences_and_search_keep_command_ids", "crates/ui-egui/src/i18n/mod.rs::telugu_covers_commands_and_catalogue", "crates/ui-egui/src/i18n/mod.rs::telugu_labels_have_glyphs", "crates/ui-egui/src/i18n/mod.rs::hungarian_is_registered", "crates/ui-egui/src/i18n/mod.rs::hungarian_covers_commands_and_catalogue", "crates/ui-egui/src/i18n/mod.rs::hungarian_covers_ui_literals", "crates/ui-egui/src/i18n/mod.rs::hungarian_history_and_diagnostics_preserve_user_values", "crates/ui-egui/src/i18n/mod.rs::ukrainian_is_registered_and_persists", "crates/ui-egui/src/i18n/mod.rs::ukrainian_integer_plurals_cover_teens_and_large_counts", "crates/ui-egui/src/i18n/mod.rs::ukrainian_calendar_uses_month_case_for_dates", "crates/ui-egui/src/i18n/mod.rs::ukrainian_catalog_does_not_leave_english_ui_text", "crates/ui-egui/src/i18n/mod.rs::ukrainian_covers_commands_and_catalogue", "crates/ui-egui/src/i18n/mod.rs::ukrainian_covers_ui_literals", "crates/ui-egui/src/i18n/mod.rs::ukrainian_history_and_diagnostics_preserve_user_values", "crates/ui-egui/src/i18n/mod.rs::spanish_covers_ui_literals", "crates/ui-egui/src/i18n/mod.rs::spanish_covers_what_the_engine_says", "crates/ui-egui/src/i18n/mod.rs::spanish_keeps_the_holes_in_what_the_engine_says", "crates/print/src/tests.rs::wordings_are_said"] -notes = "Shortcut modifiers and key names use a dedicated catalog context across toolbar, menus, palette, Help and tooltips, with English fallback and macOS symbols preserved. Preferences switches Auto (system language)/English/Japanese/Simplified Chinese/Traditional Chinese/Czech/Brazilian Portuguese/German/Spanish/French/Telugu/Hungarian/Ukrainian/Italian/Arabic and persists; ui.set language auto|en|ja|zh-hans|zh-hant|cs|pt-br|de|es|fr|te|hu|uk|it|ar and ui.state expose it. TSV catalogs per language (same system as PhotoCraft); every dialog, panel and notice goes through tl!. Japanese, Traditional Chinese, Simplified Chinese, German, Spanish, French and Hungarian cover commands, dialogs, panels, shortcuts and history labels; Brazilian Portuguese covers the current union of catalog keys, including signature and accessibility messages; Czech remains limited to menus. French locales (fr, fr_FR, fr_CA, fr_BE) select the French catalog, German locales (de, de_DE, de_AT, de_CH) the German one, Hungarian locales (hu, hu_HU) the Hungarian one. German search hints reserve shortcut space, Home cards bound translated text, certificate Subject has its own context, and inserted tool names keep their capitalization. The sentences the layers below word for themselves (accessibility findings and the saved report, PDF/A problems, signature details, page-range mistakes, edit labels) are handed over apart from the document's own values and go through the catalog too, through said(&i18n::words); Spanish, Italian and Brazilian Portuguese translate them, Polish the signature details and the changes after signing; strings absent from other catalogs fall back to English. OS error details, and faults rather than mistakes, stay as they are. Auto detects the Windows display language after explicitly named supported locale overrides; neutral C/POSIX launcher locales are skipped on Windows, with Chinese script/region mapping and English fallback. Arabic (ar and every ar-* locale) covers commands, dialogs and panels with the six CLDR plural forms; its labels are put into display order when the catalog loads, because egui joins Arabic letters but lays words out left to right. Missing: a right-to-left layout (Arabic labels sit in the left-to-right layout, a wrapped Arabic paragraph breaks at the wrong end, and text typed into a field is not reordered), Czech dialogs, Chinese and Telugu font delivery in releases (the pinned craft-fonts revision has an Arabic face; its Noto Sans CJK SC is never embedded, AGENTS.md §1.1), the engine's sentences in the other catalogs, a full localization audit." +evidence = ["crates/ui-egui/src/commands.rs::localized_shortcut_labels_follow_language_and_platform", "crates/ui-egui/src/i18n/mod.rs::keyboard_key_names_have_a_separate_context_and_english_fallback", "crates/ui-egui/tests/commands.rs::localized_shortcuts_update_immediately_and_keep_command_ids_and_key_bindings", "crates/ui-egui/src/i18n/mod.rs::windows_display_language_resolves_chinese_scripts", "crates/ui-egui/src/i18n/mod.rs::neutral_locale_does_not_override_display_language", "crates/ui-egui/src/i18n/mod.rs::windows_display_language_query_returns_a_locale_tag", "crates/ui-egui/src/i18n/mod.rs::language_persists_and_invalid_input_keeps_current_language", "crates/ui-egui/src/i18n/mod.rs::bundled_catalogs_are_consistent", "crates/ui-egui/src/i18n/mod.rs::arabic_is_registered_and_persists", "crates/ui-egui/src/i18n/mod.rs::arabic_lookups_are_in_display_order", "crates/ui-egui/src/i18n/mod.rs::arabic_plurals_follow_the_six_cldr_forms", "crates/ui-egui/src/i18n/mod.rs::arabic_covers_commands_and_catalogue", "crates/ui-egui/src/i18n/mod.rs::arabic_covers_ui_literals", "crates/ui-egui/src/i18n/mod.rs::arabic_labels_have_glyphs", "crates/ui-egui/src/bidi.rs::rtl_labels_keep_placeholders_whole", "crates/ui-egui/src/i18n/mod.rs::japanese_covers_commands_and_catalogue", "crates/ui-egui/src/i18n/mod.rs::simplified_chinese_covers_commands_and_catalogue", "crates/ui-egui/src/i18n/mod.rs::simplified_chinese_covers_ui_literals", "crates/ui-egui/src/i18n/mod.rs::simplified_chinese_history_and_diagnostics_preserve_user_values", "crates/ui-egui/tests/control.rs::simplified_chinese_signature_prompts_and_errors_keep_document_state", "crates/ui-egui/tests/control.rs::language_switch_preserves_document_and_command_ids", "crates/ui-egui/tests/control.rs::japanese_controls_and_search_keep_command_ids", "crates/ui-egui/tests/control.rs::japanese_dialogs_errors_and_custom_action_names", "crates/ui-egui/src/i18n/mod.rs::french_is_registered", "crates/ui-egui/src/i18n/mod.rs::french_covers_commands_and_catalogue", "crates/ui-egui/src/i18n/mod.rs::french_covers_ui_literals", "crates/ui-egui/src/i18n/mod.rs::french_history_and_diagnostics_preserve_user_values", "crates/ui-egui/src/i18n/mod.rs::german_certificate_subject_has_a_distinct_context", "crates/ui-egui/src/widgets.rs::search_placeholder_never_overlaps_the_shortcut", "crates/ui-egui/src/home.rs::translated_card_description_leaves_room_for_the_action", "crates/ui-egui/tests/control.rs::german_preferences_and_search_keep_command_ids", "crates/ui-egui/src/i18n/mod.rs::telugu_covers_commands_and_catalogue", "crates/ui-egui/src/i18n/mod.rs::telugu_labels_have_glyphs", "crates/ui-egui/src/i18n/mod.rs::hungarian_is_registered", "crates/ui-egui/src/i18n/mod.rs::hungarian_covers_commands_and_catalogue", "crates/ui-egui/src/i18n/mod.rs::hungarian_covers_ui_literals", "crates/ui-egui/src/i18n/mod.rs::hungarian_history_and_diagnostics_preserve_user_values", "crates/ui-egui/src/i18n/mod.rs::ukrainian_is_registered_and_persists", "crates/ui-egui/src/i18n/mod.rs::ukrainian_integer_plurals_cover_teens_and_large_counts", "crates/ui-egui/src/i18n/mod.rs::ukrainian_calendar_uses_month_case_for_dates", "crates/ui-egui/src/i18n/mod.rs::ukrainian_catalog_does_not_leave_english_ui_text", "crates/ui-egui/src/i18n/mod.rs::ukrainian_covers_commands_and_catalogue", "crates/ui-egui/src/i18n/mod.rs::ukrainian_covers_ui_literals", "crates/ui-egui/src/i18n/mod.rs::ukrainian_history_and_diagnostics_preserve_user_values", "crates/ui-egui/src/i18n/mod.rs::spanish_covers_ui_literals", "crates/ui-egui/src/i18n/mod.rs::spanish_covers_what_the_engine_says", "crates/ui-egui/src/i18n/mod.rs::spanish_keeps_the_holes_in_what_the_engine_says", "crates/print/src/tests.rs::wordings_are_said", "crates/ui-egui/src/i18n/mod.rs::norwegian_bokmal_is_registered_and_persists"] +notes = "Shortcut modifiers and key names use a dedicated catalog context across toolbar, menus, palette, Help and tooltips, with English fallback and macOS symbols preserved. Preferences switches Auto (system language)/English/Japanese/Simplified Chinese/Traditional Chinese/Czech/Brazilian Portuguese/German/Spanish/French/Telugu/Hungarian/Ukrainian/Italian/Arabic/Norwegian Bokmål and persists; ui.set language auto|en|ja|zh-hans|zh-hant|cs|pt-br|de|es|fr|te|hu|uk|it|ar|nb and ui.state expose it. TSV catalogs per language (same system as PhotoCraft); every dialog, panel and notice goes through tl!. Japanese, Traditional Chinese, Simplified Chinese, German, Spanish, French and Hungarian cover commands, dialogs, panels, shortcuts and history labels; Brazilian Portuguese covers the current union of catalog keys, including signature and accessibility messages; Czech remains limited to menus. French locales (fr, fr_FR, fr_CA, fr_BE) select the French catalog, German locales (de, de_DE, de_AT, de_CH) the German one, Hungarian locales (hu, hu_HU) the Hungarian one, and Norwegian locales (nb, nb_NO, and no/nn until a Nynorsk catalog exists) the Bokmål one. German search hints reserve shortcut space, Home cards bound translated text, certificate Subject has its own context, and inserted tool names keep their capitalization. The sentences the layers below word for themselves (accessibility findings and the saved report, PDF/A problems, signature details, page-range mistakes, edit labels) are handed over apart from the document's own values and go through the catalog too, through said(&i18n::words); Spanish, Italian and Brazilian Portuguese translate them, Polish the signature details and the changes after signing; strings absent from other catalogs fall back to English. OS error details, and faults rather than mistakes, stay as they are. Auto detects the Windows display language after explicitly named supported locale overrides; neutral C/POSIX launcher locales are skipped on Windows, with Chinese script/region mapping and English fallback. Arabic (ar and every ar-* locale) covers commands, dialogs and panels with the six CLDR plural forms; its labels are put into display order when the catalog loads, because egui joins Arabic letters but lays words out left to right. Missing: a right-to-left layout (Arabic labels sit in the left-to-right layout, a wrapped Arabic paragraph breaks at the wrong end, and text typed into a field is not reordered), Czech dialogs, Chinese and Telugu font delivery in releases (the pinned craft-fonts revision has an Arabic face; its Noto Sans CJK SC is never embedded, AGENTS.md §1.1), the engine's sentences in the other catalogs, a full localization audit." [[feature]] id = "misc.prefs-reading"