From 8c6c5a261b6a22ce04a569038ca349618dbe2c9e Mon Sep 17 00:00:00 2001 From: Osvaldo Duran <338149330+regendorf-ux@users.noreply.github.com> Date: Sun, 1 Nov 2026 18:53:50 -0500 Subject: [PATCH 1/2] Print production: Add printer marks (dialog, edit, tool) Trim marks at the trim box's corners, dashed bleed marks at the bleed box, registration targets, a colour bar and a page-information line, drawn outside the trim box as a removable page mark (MarkKind::PrinterMarks); the page is widened to make room when asked and the trim box stays, so the printed page cuts the same. Edit::AddPrinterMarks (scope Full, undoable) and Remove printer marks through Edit::RemoveMarks; Dialog::PrinterMarks reachable from the catalog, the palette and --view printer-marks; tools doc_printer_marks and doc_remove_marks kind printer_marks; rows in every language; parity print.printer-marks shipped. Carries the review fixes: - ATTRIBUTION.toml: the language files' checksums after the new rows - Printer marks: drop the unreachable AddPrinterMarks pattern in scope_of - Tests: printer marks read the document's marks through upstream's marks() (#1434) - Printer marks: make room inside the crop box and keep the trim box on rerun - Format with rustfmt - Printer marks: keep accented labels and make room for the bleed marks - Printer marks test: the page is letter size, so the bleed marks (33 pt beyond the trim box) fit - i18n(ui): carry this PR's new wordings in the catalogues that must cover them - Parity: evidence paths follow the tests moved into tests/it - Fix the build after merging upstream/main - fix(printer-marks): refuse numbers out of range, print on every plate Co-Authored-By: Claude Opus 5.5 --- crates/automation/src/dispatch.rs | 2 +- crates/automation/src/documents.rs | 22 ++ crates/automation/src/tools.rs | 18 +- crates/automation/tests/it/automation.rs | 29 ++ crates/edit/src/added/store.rs | 2 + crates/edit/src/lib.rs | 2 + crates/edit/src/marks.rs | 3 + crates/edit/src/printer_marks.rs | 340 ++++++++++++++++++ crates/edit/src/tests.rs | 46 +++ .../tests/printer_marks_accented_label.rs | 12 + .../edit/tests/printer_marks_bleed_reach.rs | 50 +++ crates/edit/tests/printer_marks_boxes.rs | 108 ++++++ crates/engine/src/catalog.rs | 2 +- crates/engine/src/commands.rs | 2 + crates/engine/src/edit.rs | 15 +- crates/engine/src/edit/labels.rs | 2 + crates/engine/src/edit/run.rs | 4 + crates/engine/src/history.rs | 1 + crates/engine/src/lib.rs | 3 +- crates/engine/src/session/editing.rs | 9 +- crates/ui-egui/src/app.rs | 1 + crates/ui-egui/src/app/control.rs | 1 + crates/ui-egui/src/commands.rs | 5 +- crates/ui-egui/src/dialogs.rs | 11 + crates/ui-egui/src/dialogs/marks_ui.rs | 8 +- .../ui-egui/src/dialogs/printer_marks_ui.rs | 152 ++++++++ crates/ui-egui/src/i18n/ar.tsv | 15 + crates/ui-egui/src/i18n/bg.tsv | 15 + crates/ui-egui/src/i18n/ca.tsv | 15 + crates/ui-egui/src/i18n/cs.tsv | 15 + crates/ui-egui/src/i18n/de.tsv | 15 + crates/ui-egui/src/i18n/el.tsv | 15 + crates/ui-egui/src/i18n/es.tsv | 15 + crates/ui-egui/src/i18n/fi.tsv | 15 + crates/ui-egui/src/i18n/fr.tsv | 15 + crates/ui-egui/src/i18n/hi.tsv | 15 + crates/ui-egui/src/i18n/hu.tsv | 15 + crates/ui-egui/src/i18n/id.tsv | 15 + crates/ui-egui/src/i18n/it.tsv | 15 + crates/ui-egui/src/i18n/ja.tsv | 15 + crates/ui-egui/src/i18n/kk.tsv | 15 + crates/ui-egui/src/i18n/ko.tsv | 15 + crates/ui-egui/src/i18n/lt.tsv | 15 + crates/ui-egui/src/i18n/pl.tsv | 15 + crates/ui-egui/src/i18n/pt-br.tsv | 15 + crates/ui-egui/src/i18n/ro.tsv | 15 + crates/ui-egui/src/i18n/ru.tsv | 15 + crates/ui-egui/src/i18n/sl.tsv | 15 + crates/ui-egui/src/i18n/sq.tsv | 15 + crates/ui-egui/src/i18n/te.tsv | 15 + crates/ui-egui/src/i18n/tr.tsv | 15 + crates/ui-egui/src/i18n/uk.tsv | 15 + crates/ui-egui/src/i18n/vi.tsv | 15 + crates/ui-egui/src/i18n/zh-hans.tsv | 15 + crates/ui-egui/src/i18n/zh-hant.tsv | 15 + crates/ui-egui/src/lib.rs | 2 + crates/ui-egui/src/state.rs | 2 + crates/ui-egui/tests/it/commands.rs | 4 + crates/ui-egui/tests/it/editing.rs | 26 ++ docs/gaps.md | 6 +- parity/acrobat-features.toml | 10 +- 61 files changed, 1316 insertions(+), 19 deletions(-) create mode 100644 crates/edit/src/printer_marks.rs create mode 100644 crates/edit/tests/printer_marks_accented_label.rs create mode 100644 crates/edit/tests/printer_marks_bleed_reach.rs create mode 100644 crates/edit/tests/printer_marks_boxes.rs create mode 100644 crates/ui-egui/src/dialogs/printer_marks_ui.rs diff --git a/crates/automation/src/dispatch.rs b/crates/automation/src/dispatch.rs index 8bc23ff280..8a7903251d 100644 --- a/crates/automation/src/dispatch.rs +++ b/crates/automation/src/dispatch.rs @@ -689,7 +689,7 @@ impl Automation { summary(self.session.get(new).ok_or_else(|| failed("the document vanished"))?) } "doc_export_images" | "doc_export_text" | "doc_export_all_images" => self.export(name, &a)?, - "doc_header_footer" | "doc_watermark" | "doc_background" | "doc_remove_marks" => self.marks(name, &a)?, + "doc_header_footer" | "doc_watermark" | "doc_background" | "doc_printer_marks" | "doc_remove_marks" => self.marks(name, &a)?, "doc_backdrops" => self.backdrops(&a)?, "doc_unprotect" => { let mut out = self.apply(&a, Edit::RemoveProtection)?; diff --git a/crates/automation/src/documents.rs b/crates/automation/src/documents.rs index 859c92da48..e0d2d0376a 100644 --- a/crates/automation/src/documents.rs +++ b/crates/automation/src/documents.rs @@ -546,11 +546,33 @@ impl Automation { replace, file: self.mark_file(a)?, }, + "doc_printer_marks" => { + use pdfcraft_engine::PrinterMarks; + let flag = |k: &str, default: bool| -> Result { Ok(a.opt_bool(k)?.unwrap_or(default)) }; + let (y, m, d) = self.session.today(); + let settings = PrinterMarks { + trim: flag("trim", true)?, + bleed: flag("bleed", true)?, + registration: flag("registration", true)?, + color_bars: flag("color_bars", true)?, + page_info: flag("page_info", true)?, + line_weight: a.opt_num("line_weight")?.unwrap_or(0.25), + make_room: match a.opt_num("make_room")? { + Some(r) if r < 0.0 => None, + Some(r) => Some(r), + None => Some(36.0), + }, + label: a.opt_str("label")?.map(str::to_string).unwrap_or_else(|| self.doc(a).map(|d| d.name.clone()).unwrap_or_default()), + date: a.opt_str("date")?.map(str::to_string).unwrap_or_else(|| format!("{y:04}-{m:02}-{d:02}")), + }; + Edit::AddPrinterMarks { pages, settings, replace: a.opt_bool("replace")?.unwrap_or(true) } + } _ => { let kind = match a.str("kind")? { "header_footer" => MarkKind::HeaderFooter, "watermark" => MarkKind::Watermark, "background" => MarkKind::Background, + "printer_marks" => MarkKind::PrinterMarks, other => return Err(ToolError::InvalidArgs(format!("unknown kind {other:?}"))), }; Edit::RemoveMarks { kind } diff --git a/crates/automation/src/tools.rs b/crates/automation/src/tools.rs index 866138899d..d33ccf5aa9 100644 --- a/crates/automation/src/tools.rs +++ b/crates/automation/src/tools.rs @@ -1038,13 +1038,27 @@ pub fn tools() -> Vec { json!({ "doc": doc(), "pages": pages("to fill (default: all)"), "color": { "type": "string" }, "file": { "type": "string" }, "file_page": { "type": "integer", "minimum": 1 }, "scale": { "type": "number", "exclusiveMinimum": 0, "maximum": 1 }, "opacity": { "type": "number", "minimum": 0, "maximum": 1 }, "replace": { "type": "boolean" }, "replace_backdrop": { "type": "boolean", "description": "Replace a white backdrop the page paints itself, which would hide the background. Default true." } }), &["doc"], )), + t("doc_printer_marks", "Add printer marks", "Print production: trim marks, bleed marks (dashed, at the bleed box), registration targets, colour bars and a page-information line (label, page n of N, date) drawn outside the trim box as a removable page mark, in registration colour (Separation All, so on every plate) except the process-ink colour bars; the page is widened by make_room points a side when the marks would fall outside it (the trim box stays). replace (default true) replaces marks added before. Undoable; doc_remove_marks kind printer_marks takes them off.") + .cmd("prepress.marks") + .with(schema( + json!({ + "doc": doc(), "pages": pages("the pages to mark (default all)"), + "trim": { "type": "boolean" }, "bleed": { "type": "boolean" }, "registration": { "type": "boolean" }, "color_bars": { "type": "boolean" }, "page_info": { "type": "boolean" }, + "line_weight": { "type": "number", "minimum": 0.1, "maximum": 4, "description": "Points (Acrobat offers 0.25, 0.5 and 1)." }, + "make_room": { "type": "number", "maximum": 288, "description": "Points to widen the page by on each side when needed (default 36); negative leaves the page as it is." }, + "label": { "type": "string", "description": "The page-information line's label (default: the file's name)." }, + "date": { "type": "string", "description": "The page-information line's date (default: today)." }, + "replace": { "type": "boolean" }, + }), + &["doc"], + )), t("doc_backdrops", "List page backdrops", "Pages that paint their own backdrop, which hides a background added behind the content: `fill` (an opaque white fill doc_background can replace) or `image` (a page-sized picture, e.g. a scan, that still hides it). Pages are 1-based; pages without one are left out.") .ro() .cmd("edit.background") .with(schema(json!({ "doc": doc(), "pages": pages("to check (default: all)") }), &["doc"])), - t("doc_remove_marks", "Remove header & footer, watermark or background", "Remove every header and footer, watermark or background PdfCraft (or a compatible tool) added. Undoable.") + t("doc_remove_marks", "Remove header & footer, watermark, background or printer marks", "Remove every header and footer, watermark, background or printer marks PdfCraft (or a compatible tool) added. Undoable.") .destructive() - .with(schema(json!({ "doc": doc(), "kind": { "type": "string", "enum": ["header_footer", "watermark", "background"] } }), &["doc", "kind"])), + .with(schema(json!({ "doc": doc(), "kind": { "type": "string", "enum": ["header_footer", "watermark", "background", "printer_marks"] } }), &["doc", "kind"])), t("doc_export_images", "Export pages as images", "Write pages as PNG, JPEG or TIFF files (`_page_.png|jpg|tif`) into a folder, at a resolution (default 150 dpi). JPEG and TIFF are flattened onto white paper. A page too large for the renderer at that resolution is drawn at the most it allows, listed in `lower_dpi` ({page, dpi}). Includes unsaved edits.") .cmd("export.image") .with(schema( diff --git a/crates/automation/tests/it/automation.rs b/crates/automation/tests/it/automation.rs index 0d73dbb900..d17584a99f 100644 --- a/crates/automation/tests/it/automation.rs +++ b/crates/automation/tests/it/automation.rs @@ -9457,6 +9457,35 @@ fn a_root_less_write_output_is_atomic_too() { let _ = std::fs::remove_dir_all(&dir); } +/// Printer marks through the tools: added around the trim box (the page widened to make room), +/// removable by kind. +#[test] +fn printer_marks_through_tools() { + let dir = workdir("printer-marks"); + let mut a = auto(&dir); + let doc = ok(&mut a, "doc_open", json!({ "path": "a.pdf" }))["doc"].as_u64().unwrap(); + let width_before = ok(&mut a, "doc_info", json!({ "doc": doc }))["pages"][0]["width"].as_f64().unwrap_or(0.0); + let r = ok(&mut a, "doc_printer_marks", json!({ "doc": doc, "pages": [1], "color_bars": false, "line_weight": 0.5 })); + assert_eq!(r["undo"], "Add printer marks"); + let info = ok(&mut a, "doc_info", json!({ "doc": doc })); + let width_after = info["pages"][0]["width"].as_f64().unwrap_or(0.0); + assert!(width_after > width_before + 70.0 || width_before == 0.0, "widened to make room: {width_before} → {width_after}; {info}"); + let r = ok(&mut a, "doc_remove_marks", json!({ "doc": doc, "kind": "printer_marks" })); + assert_eq!(r["undo"], "Remove printer marks"); + for bad in [json!({ "line_weight": -1 }), json!({ "line_weight": 0 }), json!({ "line_weight": 10000 }), json!({ "make_room": 1000000 })] { + let mut args = bad.clone(); + args["doc"] = json!(doc); + assert!(matches!(a.call("doc_printer_marks", &args), Err(ToolError::Failed(_) | ToolError::InvalidArgs(_))), "{bad}"); + } + assert!(matches!( + a.call( + "doc_printer_marks", + &json!({ "doc": doc, "trim": false, "bleed": false, "registration": false, "color_bars": false, "page_info": false }) + ), + Err(ToolError::Failed(_) | ToolError::InvalidArgs(_)) + )); +} + #[test] fn attaching_and_deleting_files_through_the_tools() { let dir = workdir("attach"); diff --git a/crates/edit/src/added/store.rs b/crates/edit/src/added/store.rs index 7dae55596f..84cd137c4e 100644 --- a/crates/edit/src/added/store.rs +++ b/crates/edit/src/added/store.rs @@ -86,6 +86,8 @@ pub fn scan_markers(doc: &Document) -> MarkerScan { Some(MarkKind::Watermark) } else if tag == Some(&b"Background"[..]) { Some(MarkKind::Background) + } else if tag == Some(&b"PrinterMarks"[..]) { + Some(MarkKind::PrinterMarks) } else { None }; diff --git a/crates/edit/src/lib.rs b/crates/edit/src/lib.rs index aa11617c14..91778488da 100644 --- a/crates/edit/src/lib.rs +++ b/crates/edit/src/lib.rs @@ -54,6 +54,8 @@ pub use text::{ BlockKey, BlockStyle, LineEdit, MAX_FORM_DEPTH, MAX_FORM_VISITS, RangeStyle, ReadingBlocks, TextBlock, TextLine, TextSession, block_overlapping, clear_block, glyph_text_lines, reading_blocks, reading_blocks_checked, replace_block, replace_line, rewrite_block, text_blocks, text_lines, }; +pub mod printer_marks; +pub use printer_marks::{PrinterMarks, add_printer_marks}; pub mod added; pub use added::{ Added, AddedImage, AddedText, Align, Content, Family, MarkerScan, add_content, delete_content, first_undrawable, isolate_content, list_added, diff --git a/crates/edit/src/marks.rs b/crates/edit/src/marks.rs index d936955184..2e667c7cc6 100644 --- a/crates/edit/src/marks.rs +++ b/crates/edit/src/marks.rs @@ -6,6 +6,8 @@ pub enum MarkKind { HeaderFooter, Watermark, Background, + /// Printer marks: trim, bleed, registration, colour bars, page information. + PrinterMarks, } impl MarkKind { @@ -14,6 +16,7 @@ impl MarkKind { MarkKind::HeaderFooter => "HeaderFooter", MarkKind::Watermark => "Watermark", MarkKind::Background => "Background", + MarkKind::PrinterMarks => "PrinterMarks", } } } diff --git a/crates/edit/src/printer_marks.rs b/crates/edit/src/printer_marks.rs new file mode 100644 index 0000000000..a247f65d30 --- /dev/null +++ b/crates/edit/src/printer_marks.rs @@ -0,0 +1,340 @@ +//! Printer marks (Acrobat's Print production ▸ Add Printer Marks): trim and +//! bleed marks, registration targets, colour bars and page information drawn outside the trim +//! box, as a page mark like a header, so they can be removed or replaced. The page is widened +//! when asked and the marks would otherwise fall outside it; the trim box stays where it is. +//! +//! Lines, targets and the information line are in registration colour, the `/All` Separation +//! (ISO 32000-2, 8.6.6.4), so they print on every plate, spot plates too; viewers show its +//! four-ink alternate. The colour bars are process inks and stay DeviceCMYK. + +use pdfcraft_cos::{Dict, Document, Object}; +use pdfcraft_fonts::{literal, win_ansi}; + +use crate::marks::{MarkKind, n}; +use crate::place::{END, add_resources, begin, check, page_list, place}; +use crate::{EditError, remove_marks}; + +/// Which marks, and how heavy. +#[derive(Clone, Debug, PartialEq)] +pub struct PrinterMarks { + pub trim: bool, + pub bleed: bool, + pub registration: bool, + pub color_bars: bool, + pub page_info: bool, + /// Line weight in points (Acrobat: 0.25, 0.5, 1). + pub line_weight: f64, + /// Widen the page by this many points on each side (at least what the marks need) when + /// they would fall outside it; `None` leaves the page and clips the marks. + pub make_room: Option, + /// What the page information line says before the page number (the file's name). + pub label: String, + /// The date on the page information line. + pub date: String, +} + +impl Default for PrinterMarks { + fn default() -> Self { + Self { + trim: true, + bleed: true, + registration: true, + color_bars: true, + page_info: true, + line_weight: 0.25, + make_room: Some(36.0), + label: String::new(), + date: String::new(), + } + } +} + +/// How far a mark reaches beyond the trim box: a gap, then the mark itself. +const GAP: f64 = 6.0; +const LENGTH: f64 = 18.0; +/// Registration targets sit this far beyond the trim box, centred on each side. +const TARGET_OFFSET: f64 = 16.0; +const TARGET_RADIUS: f64 = 4.0; +const BAR: f64 = 10.0; +const INFO_SIZE: f64 = 7.0; +/// The colour space resource holding registration colour. +const REGISTRATION: &str = "PCReg"; +/// The largest page side PDF viewers take (Annex C of ISO 32000-1 lists it as Acrobat's limit). +const MAX_PAGE_SIDE: f64 = 14_400.0; + +/// `[/Separation /All /DeviceCMYK ]`: full tint is all four process inks. +fn registration() -> Object { + let reals = |v: &[f64]| Object::Array(v.iter().map(|x| Object::Real(*x)).collect()); + let mut tint = Dict::new(); + tint.set(b"FunctionType".to_vec(), Object::Int(2)); + tint.set(b"Domain".to_vec(), reals(&[0.0, 1.0])); + tint.set(b"C0".to_vec(), reals(&[0.0; 4])); + tint.set(b"C1".to_vec(), reals(&[1.0; 4])); + tint.set(b"N".to_vec(), Object::Int(1)); + Object::Array(vec![Object::name("Separation"), Object::name("All"), Object::name("DeviceCMYK"), Object::Dict(tint)]) +} + +impl PrinterMarks { + /// The line weights a run accepts, in points. + pub const WEIGHTS: std::ops::RangeInclusive = 0.1..=4.0; + /// The most a run widens a page by on each side at the caller's asking, in points. + pub const MAX_ROOM: f64 = 288.0; + + fn any(&self) -> bool { + self.trim || self.bleed || self.registration || self.color_bars || self.page_info + } + + /// How far beyond the trim box the chosen marks reach. + pub fn reach(&self) -> f64 { + let mut r: f64 = 0.0; + if self.trim || self.bleed { + r = r.max(GAP + LENGTH); + } + if self.registration { + r = r.max(TARGET_OFFSET + TARGET_RADIUS + 2.0); + } + if self.color_bars || self.page_info { + r = r.max(GAP + BAR + 6.0); + } + r + } + + /// The marks' content for a page whose trim box is `trim` and bleed box `bleed` (user + /// space), `page` of `count` (1-based). + pub fn content(&self, trim: [f64; 4], bleed: [f64; 4], page: usize, count: usize) -> String { + let w = self.line_weight.clamp(*Self::WEIGHTS.start(), *Self::WEIGHTS.end()); + // Registration colour: every plate. + let mut c = format!("/{REGISTRATION} CS 1 SCN /{REGISTRATION} cs 1 scn {} w 0 J\n", n(w)); + let [x0, y0, x1, y1] = trim; + let line = |c: &mut String, ax: f64, ay: f64, bx: f64, by: f64| { + c.push_str(&format!("{} {} m {} {} l S\n", n(ax), n(ay), n(bx), n(by))); + }; + // Crop (trim) marks: at each corner, outside the box, along both edges. + let corner_marks = |c: &mut String, b: [f64; 4]| { + let [bx0, by0, bx1, by1] = b; + for (x, dir) in [(bx0, -1.0), (bx1, 1.0)] { + for y in [by0, by1] { + // Vertical mark beyond the top or bottom edge. + let (ya, yb) = if y == by0 { (y - GAP, y - GAP - LENGTH) } else { (y + GAP, y + GAP + LENGTH) }; + line(c, x, ya, x, yb); + // Horizontal mark beyond the left or right edge. + line(c, x + dir * GAP, y, x + dir * (GAP + LENGTH), y); + } + } + }; + if self.trim { + corner_marks(&mut c, trim); + } + if self.bleed && bleed != trim { + c.push_str(&format!("[{} {}] 0 d\n", n(w * 4.0), n(w * 4.0))); + corner_marks(&mut c, bleed); + c.push_str("[] 0 d\n"); + } + if self.registration { + let r = TARGET_RADIUS; + let k = 0.5523 * r; + let target = |c: &mut String, cx: f64, cy: f64| { + c.push_str(&format!( + "{} {} m {} {} {} {} {} {} c {} {} {} {} {} {} c {} {} {} {} {} {} c {} {} {} {} {} {} c S\n", + n(cx + r), + n(cy), + n(cx + r), + n(cy + k), + n(cx + k), + n(cy + r), + n(cx), + n(cy + r), + n(cx - k), + n(cy + r), + n(cx - r), + n(cy + k), + n(cx - r), + n(cy), + n(cx - r), + n(cy - k), + n(cx - k), + n(cy - r), + n(cx), + n(cy - r), + n(cx + k), + n(cy - r), + n(cx + r), + n(cy - k), + n(cx + r), + n(cy) + )); + line(c, cx - r * 1.6, cy, cx + r * 1.6, cy); + line(c, cx, cy - r * 1.6, cx, cy + r * 1.6); + }; + let (mx, my) = ((x0 + x1) / 2.0, (y0 + y1) / 2.0); + target(&mut c, mx, y1 + TARGET_OFFSET); + target(&mut c, mx, y0 - TARGET_OFFSET); + target(&mut c, x0 - TARGET_OFFSET, my); + target(&mut c, x1 + TARGET_OFFSET, my); + } + if self.color_bars { + // Above the top edge, from the left: C, M, Y, K at 100%, then 75, 50, 25% K. + let y = y1 + GAP + 2.0; + let x = x0 + LENGTH + GAP + 12.0; + let swatches: [(f64, f64, f64, f64); 7] = [ + (1.0, 0.0, 0.0, 0.0), + (0.0, 1.0, 0.0, 0.0), + (0.0, 0.0, 1.0, 0.0), + (0.0, 0.0, 0.0, 1.0), + (0.0, 0.0, 0.0, 0.75), + (0.0, 0.0, 0.0, 0.5), + (0.0, 0.0, 0.0, 0.25), + ]; + for (i, (cy, m, yl, k)) in swatches.iter().enumerate() { + c.push_str(&format!("{} {} {} {} k {} {} {} {} re f\n", n(*cy), n(*m), n(*yl), n(*k), n(x + i as f64 * BAR), n(y), n(BAR), n(BAR))); + } + c.push_str(&format!("/{REGISTRATION} cs 1 scn\n")); + } + if self.page_info { + let mut info = String::new(); + if !self.label.trim().is_empty() { + info.push_str(self.label.trim()); + info.push_str(" "); + } + info.push_str(&format!("{page} of {count}")); + if !self.date.trim().is_empty() { + info.push_str(" "); + info.push_str(self.date.trim()); + } + let info: String = info.chars().filter(|ch| pdfcraft_fonts::win_ansi_encodable(*ch)).collect(); + c.push_str(&format!("BT /PCHelv {} Tf 1 0 0 1 {} {} Tm ", n(INFO_SIZE), n(x0 + LENGTH + GAP + 12.0), n(y0 - GAP - BAR))); + c.push_str(&ascii_literal(&literal(&win_ansi(&info)))); + c.push_str(" Tj ET\n"); + } + c + } +} + +/// A PDF literal string made of WinAnsi bytes, as ASCII text: bytes from 0x80 up are written as +/// octal escapes, so they reach the content stream unchanged instead of being replaced when the +/// bytes are turned into a `String`. +fn ascii_literal(bytes: &[u8]) -> String { + let mut out = String::with_capacity(bytes.len()); + for &b in bytes { + if b < 0x80 { + out.push(char::from(b)); + } else { + out.push_str(&format!("\\{b:03o}")); + } + } + out +} + +/// How far the chosen marks reach beyond the trim box on each side (left, bottom, right, top): +/// `reach`, or more where the bleed marks, which start at the bleed box, go further out. +fn side_reach(marks: &PrinterMarks, trim: [f64; 4], bleed: [f64; 4]) -> [f64; 4] { + let base = marks.reach(); + if !(marks.bleed && bleed != trim) { + return [base; 4]; + } + let beyond = |bleed_edge: f64, trim_edge: f64| base.max((trim_edge - bleed_edge).abs() + GAP + LENGTH); + [beyond(bleed[0], trim[0]), beyond(bleed[1], trim[1]), beyond(bleed[2], trim[2]), beyond(bleed[3], trim[3])] +} + +/// Add printer marks to `pages` (0-based). With `replace`, marks already there are removed +/// first. Returns the pages that were widened (media or crop box) to make room. +pub fn add_printer_marks(doc: &mut Document, pages: &[usize], marks: &PrinterMarks, replace: bool) -> Result, EditError> { + let all = page_list(doc); + check(pages, all.len())?; + if !marks.any() { + return Err(EditError::Invalid("choose at least one kind of mark".into())); + } + if !PrinterMarks::WEIGHTS.contains(&marks.line_weight) { + let (low, high) = (PrinterMarks::WEIGHTS.start(), PrinterMarks::WEIGHTS.end()); + return Err(EditError::Invalid(format!("the line weight must be from {low} to {high} points"))); + } + if marks.make_room.is_some_and(|room| !(0.0..=PrinterMarks::MAX_ROOM).contains(&room)) { + return Err(EditError::Invalid(format!("the room to make must be from 0 to {} points", PrinterMarks::MAX_ROOM))); + } + if replace { + remove_marks(doc, pages, MarkKind::PrinterMarks)?; + } + let count = all.len(); + let mut widened = Vec::new(); + for &i in pages { + let [media, crop, bleed, trim, art] = pdfcraft_organize_boxes(doc, i)?; + let side = side_reach(marks, trim, bleed); + let needed = [trim[0] - side[0], trim[1] - side[1], trim[2] + side[2], trim[3] + side[3]]; + let within = |b: [f64; 4]| b[0] <= needed[0] && b[1] <= needed[1] && b[2] >= needed[2] && b[3] >= needed[3]; + // The crop box is what shows, so the marks need room inside it, not only inside the media box. + if !within(crop) + && let Some(room) = marks.make_room + { + let new_media = if within(media) { + None + } else { + let room = |s: f64| room.max(s); + Some([ + media[0].min(trim[0] - room(side[0])), + media[1].min(trim[1] - room(side[1])), + media[2].max(trim[2] + room(side[2])), + media[3].max(trim[3] + room(side[3])), + ]) + }; + if new_media.is_some_and(|m| m[2] - m[0] > MAX_PAGE_SIDE || m[3] - m[1] > MAX_PAGE_SIDE) { + return Err(EditError::Invalid(format!("page {} would be wider than the {MAX_PAGE_SIDE} points a PDF page can be", i + 1))); + } + // Without a wider media box, only the crop box grows, to the marks and no further than the media box. + let new_crop = match new_media { + Some(m) => m, + None => [ + crop[0].min(needed[0]).max(media[0]), + crop[1].min(needed[1]).max(media[1]), + crop[2].max(needed[2]).min(media[2]), + crop[3].max(needed[3]).min(media[3]), + ], + }; + let rect = |r: [f64; 4]| Object::Array(r.iter().map(|v| Object::Real(*v)).collect()); + let page = page_list(doc)[i].clone(); + doc.update_dict(page.obj, |d| { + if let Some(m) = new_media { + d.set(b"MediaBox".to_vec(), rect(m)); + } + // The crop box shows the marks too. The trim, bleed and art boxes default to the + // crop box and are clipped to the media box, so each is written out as it was in + // effect, or it would move with the wider boxes and a second run would widen the + // page again. + d.set(b"CropBox".to_vec(), rect(new_crop)); + for (key, value) in [(&b"BleedBox"[..], bleed), (&b"TrimBox"[..], trim), (&b"ArtBox"[..], art)] { + d.set(key.to_vec(), rect(value)); + } + })?; + widened.push(i); + } + let page = page_list(doc)[i].clone(); + let content = marks.content(trim, bleed, i + 1, count); + let body = format!("{}{}{}", begin(MarkKind::PrinterMarks, "PrinterMarks", [1.0, 0.0, 0.0, 1.0, 0.0, 0.0]), content, END); + add_resources(doc, &page, None, Some(body.as_bytes()))?; + let page = page_list(doc)[i].clone(); + // add_resources gave the page resources of its own. + let mut res = page.dict.get(b"Resources").map(|r| doc.resolve(r)).and_then(|r| r.as_dict().cloned()).unwrap_or_default(); + let mut spaces = res.get(b"ColorSpace").map(|c| doc.resolve(c)).and_then(|c| c.as_dict().cloned()).unwrap_or_default(); + spaces.set(REGISTRATION.as_bytes().to_vec(), registration()); + res.set(b"ColorSpace".to_vec(), Object::Dict(spaces)); + doc.update_dict(page.obj, |d| d.set(b"Resources".to_vec(), Object::Dict(res)))?; + let page = page_list(doc)[i].clone(); + place(doc, &page, MarkKind::PrinterMarks, body.into_bytes(), false)?; + } + Ok(widened) +} + +/// A page's boxes: media, crop, bleed, trim, art (each inherited or falling back as the +/// standard says), without depending on the organize crate. +fn pdfcraft_organize_boxes(doc: &Document, page: usize) -> Result<[[f64; 4]; 5], EditError> { + let p = page_list(doc).get(page).cloned().ok_or_else(|| EditError::Invalid(format!("page {} does not exist", page + 1)))?; + let rect = |k: &[u8]| -> Option<[f64; 4]> { + let a = p.dict.get(k).map(|o| doc.resolve(o))?; + let v: Vec = a.as_array()?.iter().filter_map(|x| doc.resolve(x).as_f64()).collect(); + (v.len() == 4 && v.iter().all(|x| x.is_finite())).then(|| [v[0].min(v[2]), v[1].min(v[3]), v[0].max(v[2]), v[1].max(v[3])]) + }; + let media = rect(b"MediaBox").unwrap_or([0.0, 0.0, 612.0, 792.0]); + let clip = |r: [f64; 4]| [r[0].max(media[0]), r[1].max(media[1]), r[2].min(media[2]), r[3].min(media[3])]; + let crop = rect(b"CropBox").map(clip).unwrap_or(media); + let (bleed, trim, art) = (rect(b"BleedBox").map(clip), rect(b"TrimBox").map(clip), rect(b"ArtBox").map(clip)); + Ok([media, crop, bleed.unwrap_or(crop), trim.unwrap_or(crop), art.unwrap_or(crop)]) +} diff --git a/crates/edit/src/tests.rs b/crates/edit/src/tests.rs index f7740d60cd..7ab812f082 100644 --- a/crates/edit/src/tests.rs +++ b/crates/edit/src/tests.rs @@ -7816,6 +7816,52 @@ fn removing_marks_strips_wrappers_that_closed_open_state() { assert_eq!(streams(&doc, 0), ["q 2 0 0 2 0 0 cm"]); } +/// Printer marks: drawn outside the trim box as a removable page mark; the page +/// is widened to make room when asked and the trim box stays; running again replaces them. +#[test] +fn printer_marks_are_drawn_outside_the_trim_box_and_removable() { + use crate::printer_marks::PrinterMarks; + let mut doc = fixture(); + let page_content_text = |doc: &Document, page: usize| String::from_utf8_lossy(&page_content_bytes(doc, page)).into_owned(); + let page = page_list(&doc)[0].clone(); + // A trim box inside a letter-size page, 36 pt in: room for the marks, bleed marks included + // (the bleed box is 9 pt out, so they reach 33 pt beyond the trim box), without widening. + doc.update_dict(page.obj, |d| { + d.set(b"MediaBox".to_vec(), Object::Array([0.0, 0.0, 612.0, 792.0].iter().map(|v| Object::Real(*v)).collect())); + d.set(b"TrimBox".to_vec(), Object::Array([36.0, 36.0, 576.0, 756.0].iter().map(|v| Object::Real(*v)).collect())); + d.set(b"BleedBox".to_vec(), Object::Array([27.0, 27.0, 585.0, 765.0].iter().map(|v| Object::Real(*v)).collect())); + }) + .unwrap(); + let marks = PrinterMarks { label: "Quote 1042.pdf".into(), date: "2026-10-13".into(), ..PrinterMarks::default() }; + let widened = add_printer_marks(&mut doc, &[0, 1], &marks, false).unwrap(); + assert_eq!(widened, vec![1], "page 2 has no trim box inside its media box, so it was widened"); + assert_eq!(marks_present(&doc), vec![MarkKind::PrinterMarks]); + let text = page_content_text(&doc, 0); + assert!(text.contains("/PCMark /PrinterMarks"), "{text}"); + // A trim mark at the lower-left corner: vertical below the box, horizontal to its left. + assert!(text.contains("36 30 m 36 12 l S") && text.contains("30 36 m 12 36 l S"), "{text}"); + assert!(text.contains("/PCReg CS 1 SCN /PCReg cs 1 scn"), "registration colour: {text}"); + assert!(!text.contains("1 1 1 1 K"), "not four process inks alone: {text}"); + assert!(text.contains("[1 1] 0 d") && text.contains("27 21 m 27 3 l S"), "dashed bleed marks: {text}"); + assert!(text.contains("1 0 0 0 k") && text.contains("0 0 0 0.25 k"), "colour bars: {text}"); + assert!(text.contains("(Quote 1042.pdf 1 of 3 2026-10-13) Tj"), "page information: {text}"); + // Page 2: the media box grew by 36 pt a side around its (whole-page) trim box. + let p2 = page_list(&doc)[1].clone(); + let media: Vec = p2.dict.get(b"MediaBox").and_then(|m| m.as_array()).unwrap().iter().filter_map(|v| v.as_f64()).collect(); + assert_eq!(media, vec![-36.0, -36.0, 636.0, 836.0]); + // Again with replace: still one set of marks per page; without replace, two. + add_printer_marks(&mut doc, &[0], &marks, true).unwrap(); + assert_eq!(page_content_text(&doc, 0).matches("/PCMark /PrinterMarks").count(), 1); + add_printer_marks(&mut doc, &[0], &marks, false).unwrap(); + assert_eq!(page_content_text(&doc, 0).matches("/PCMark /PrinterMarks").count(), 2); + assert_eq!(remove_marks(&mut doc, &[0, 1], MarkKind::PrinterMarks).unwrap(), 3); + assert!(marks_present(&doc).is_empty()); + // Nothing chosen, or a bad weight, is refused. + let none = PrinterMarks { trim: false, bleed: false, registration: false, color_bars: false, page_info: false, ..PrinterMarks::default() }; + assert!(add_printer_marks(&mut doc, &[0], &none, false).is_err()); + assert!(add_printer_marks(&mut doc, &[0], &PrinterMarks { line_weight: 0.0, ..PrinterMarks::default() }, false).is_err()); +} + /// #3087: flattening comments leaves alone what isn't a comment: links, and the non-markup /// multimedia and print-production annotations (3D, Screen, Movie, RichMedia, PrinterMark, /// TrapNet, Watermark), which it used to bake into the page and delete with their data. diff --git a/crates/edit/tests/printer_marks_accented_label.rs b/crates/edit/tests/printer_marks_accented_label.rs new file mode 100644 index 0000000000..ccb8eb5861 --- /dev/null +++ b/crates/edit/tests/printer_marks_accented_label.rs @@ -0,0 +1,12 @@ +//! The page information line keeps accented letters: WinAnsi bytes from 0x80 up must reach the +//! content stream as escapes, not be replaced with U+FFFD. + +use pdfcraft_edit::PrinterMarks; + +#[test] +fn accented_label_is_written_as_winansi_escapes() { + let marks = PrinterMarks { label: "Café.pdf".into(), ..PrinterMarks::default() }; + let content = marks.content([0.0, 0.0, 612.0, 792.0], [0.0, 0.0, 612.0, 792.0], 1, 1); + assert!(!content.contains('\u{FFFD}'), "no replacement character in the stream: {content:?}"); + assert!(content.contains("(Caf\\351.pdf 1 of 1)"), "é is the WinAnsi byte 0xE9 written as \\351: {content:?}"); +} diff --git a/crates/edit/tests/printer_marks_bleed_reach.rs b/crates/edit/tests/printer_marks_bleed_reach.rs new file mode 100644 index 0000000000..cb7590768c --- /dev/null +++ b/crates/edit/tests/printer_marks_bleed_reach.rs @@ -0,0 +1,50 @@ +//! Bleed marks start at the bleed box, so the room made for the marks counts the bleed too. + +use pdfcraft_cos::{Dict, Document, Object}; +use pdfcraft_edit::{PrinterMarks, add_printer_marks}; + +fn rect(r: [f64; 4]) -> Object { + Object::Array(r.iter().map(|v| Object::Real(*v)).collect()) +} + +fn page_with(boxes: &[(&[u8], [f64; 4])]) -> Document { + let mut doc = Document::new_empty(); + let pages = doc.get(doc.root().unwrap()).as_dict().unwrap().reference(b"Pages").unwrap(); + let mut page = Dict::new(); + page.set(b"Type".to_vec(), Object::name("Page")); + page.set(b"Parent".to_vec(), Object::Ref(pages)); + for (key, r) in boxes { + page.set(key.to_vec(), rect(*r)); + } + let page = doc.add(page); + doc.update_dict(pages, |d| { + d.set(b"Kids".to_vec(), Object::Array(vec![Object::Ref(page)])); + d.set(b"Count".to_vec(), Object::Int(1)); + }) + .unwrap(); + doc +} + +fn page_box(doc: &Document, key: &[u8]) -> Option> { + let page = pdfcraft_model::pages(doc).first().cloned()?; + let a = page.dict.get(key).map(|o| doc.resolve(o))?; + Some(a.as_array()?.iter().filter_map(|v| doc.resolve(v).as_f64()).collect()) +} + +/// A 9 pt bleed: the bleed marks run out to 9 + 6 + 18 = 33 pt from the trim box, further than the +/// usual 24, so the crop box grows by that much. +#[test] +fn crop_box_grows_to_hold_the_bleed_marks() { + let mut doc = page_with(&[ + (b"MediaBox", [0.0, 0.0, 1000.0, 1000.0]), + (b"CropBox", [200.0, 200.0, 800.0, 800.0]), + (b"TrimBox", [200.0, 200.0, 800.0, 800.0]), + (b"BleedBox", [191.0, 191.0, 809.0, 809.0]), + ]); + let marks = PrinterMarks::default(); + assert_eq!(marks.reach(), 24.0); + let widened = add_printer_marks(&mut doc, &[0], &marks, false).unwrap(); + assert_eq!(widened, vec![0]); + assert_eq!(page_box(&doc, b"CropBox"), Some(vec![167.0, 167.0, 833.0, 833.0])); + assert_eq!(page_box(&doc, b"TrimBox"), Some(vec![200.0, 200.0, 800.0, 800.0])); +} diff --git a/crates/edit/tests/printer_marks_boxes.rs b/crates/edit/tests/printer_marks_boxes.rs new file mode 100644 index 0000000000..d236c48197 --- /dev/null +++ b/crates/edit/tests/printer_marks_boxes.rs @@ -0,0 +1,108 @@ +//! Printer marks and the page boxes: the marks must show inside the crop box, and running +//! again must not move the trim box or grow the page. + +use pdfcraft_cos::{Dict, Document, Object}; +use pdfcraft_edit::{PrinterMarks, add_printer_marks}; + +fn rect(r: [f64; 4]) -> Object { + Object::Array(r.iter().map(|v| Object::Real(*v)).collect()) +} + +/// One empty page with these boxes set on it. +fn page_with(boxes: &[(&[u8], [f64; 4])]) -> Document { + let mut doc = Document::new_empty(); + let pages = doc.get(doc.root().unwrap()).as_dict().unwrap().reference(b"Pages").unwrap(); + let mut page = Dict::new(); + page.set(b"Type".to_vec(), Object::name("Page")); + page.set(b"Parent".to_vec(), Object::Ref(pages)); + for (key, r) in boxes { + page.set(key.to_vec(), rect(*r)); + } + let page = doc.add(page); + doc.update_dict(pages, |d| { + d.set(b"Kids".to_vec(), Object::Array(vec![Object::Ref(page)])); + d.set(b"Count".to_vec(), Object::Int(1)); + }) + .unwrap(); + doc +} + +fn page_box(doc: &Document, key: &[u8]) -> Option> { + let page = pdfcraft_model::pages(doc).first().cloned()?; + let a = page.dict.get(key).map(|o| doc.resolve(o))?; + Some(a.as_array()?.iter().filter_map(|v| doc.resolve(v).as_f64()).collect()) +} + +/// A crop box cut to the trim inside a large media box: the marks fit the media box but +/// not the crop box, so the crop box grows to show them and the trim box stays. +#[test] +fn printer_marks_widen_crop_inside_media() { + let mut doc = page_with(&[(b"MediaBox", [0.0, 0.0, 1000.0, 1000.0]), (b"CropBox", [200.0, 200.0, 800.0, 800.0])]); + let marks = PrinterMarks::default(); + let reach = marks.reach(); + let widened = add_printer_marks(&mut doc, &[0], &marks, false).unwrap(); + assert_eq!(widened, vec![0]); + assert_eq!(page_box(&doc, b"MediaBox"), Some(vec![0.0, 0.0, 1000.0, 1000.0]), "the media box already had room"); + let crop = page_box(&doc, b"CropBox").unwrap(); + assert_eq!(crop, vec![200.0 - reach, 200.0 - reach, 800.0 + reach, 800.0 + reach]); + assert_eq!(page_box(&doc, b"TrimBox"), Some(vec![200.0, 200.0, 800.0, 800.0])); +} + +/// No trim box given: it defaults to the crop box, which widening moves. Running again with +/// replace must keep the trim box and the page size from the first run. +#[test] +fn printer_marks_rerun_keeps_trim() { + let mut doc = page_with(&[(b"MediaBox", [0.0, 0.0, 612.0, 792.0])]); + let marks = PrinterMarks::default(); + add_printer_marks(&mut doc, &[0], &marks, false).unwrap(); + let media = page_box(&doc, b"MediaBox"); + let crop = page_box(&doc, b"CropBox"); + assert_eq!(page_box(&doc, b"TrimBox"), Some(vec![0.0, 0.0, 612.0, 792.0])); + assert_eq!(page_box(&doc, b"BleedBox"), Some(vec![0.0, 0.0, 612.0, 792.0])); + let widened = add_printer_marks(&mut doc, &[0], &marks, true).unwrap(); + assert!(widened.is_empty(), "the page already has room"); + assert_eq!(page_box(&doc, b"MediaBox"), media, "the page did not grow again"); + assert_eq!(page_box(&doc, b"CropBox"), crop); + assert_eq!(page_box(&doc, b"TrimBox"), Some(vec![0.0, 0.0, 612.0, 792.0])); +} + +/// A trim box past the media box is clipped to it. Widening the media box must not let it +/// grow to what was written. +#[test] +fn printer_marks_keep_the_trim_box_in_effect() { + let mut doc = page_with(&[(b"MediaBox", [0.0, 0.0, 100.0, 100.0]), (b"TrimBox", [-20.0, -20.0, 120.0, 120.0])]); + add_printer_marks(&mut doc, &[0], &PrinterMarks::default(), false).unwrap(); + assert_eq!(page_box(&doc, b"TrimBox"), Some(vec![0.0, 0.0, 100.0, 100.0])); +} + +/// Line weights and room out of range are refused, not clamped, and nothing past the largest +/// page a PDF can have is written. +#[test] +fn printer_marks_refuse_numbers_out_of_range() { + let mut doc = page_with(&[(b"MediaBox", [0.0, 0.0, 612.0, 792.0])]); + for line_weight in [-1.0, 0.0, 0.05, 5.0, 10_000.0, f64::NAN, f64::INFINITY] { + assert!(add_printer_marks(&mut doc, &[0], &PrinterMarks { line_weight, ..PrinterMarks::default() }, false).is_err(), "{line_weight}"); + } + for room in [-1.0, 289.0, 1e9, f64::NAN, f64::INFINITY] { + assert!(add_printer_marks(&mut doc, &[0], &PrinterMarks { make_room: Some(room), ..PrinterMarks::default() }, false).is_err(), "{room}"); + } + let mut big = page_with(&[(b"MediaBox", [0.0, 0.0, 14_400.0, 1000.0])]); + assert!(add_printer_marks(&mut big, &[0], &PrinterMarks::default(), false).is_err()); + assert!(add_printer_marks(&mut big, &[0], &PrinterMarks { make_room: None, ..PrinterMarks::default() }, false).is_ok(), "left as it is"); +} + +/// Registration colour is the /All Separation, in the page's resources beside what it had. +#[test] +fn printer_marks_use_the_all_separation() { + let mut doc = page_with(&[(b"MediaBox", [0.0, 0.0, 612.0, 792.0])]); + add_printer_marks(&mut doc, &[0], &PrinterMarks::default(), false).unwrap(); + let page = pdfcraft_model::pages(&doc).swap_remove(0); + let res = doc.resolve(page.dict.get(b"Resources").unwrap()); + let res = res.as_dict().unwrap(); + assert!(res.get(b"Font").is_some()); + let spaces = doc.resolve(res.get(b"ColorSpace").unwrap()); + let cs = doc.resolve(spaces.as_dict().unwrap().get(b"PCReg").unwrap()); + let cs = cs.as_array().unwrap(); + assert_eq!(cs[..3], [Object::name("Separation"), Object::name("All"), Object::name("DeviceCMYK")]); + assert!(cs[3].as_dict().unwrap().int(b"FunctionType") == Some(2)); +} diff --git a/crates/engine/src/catalog.rs b/crates/engine/src/catalog.rs index e479232bbb..e96cc918af 100644 --- a/crates/engine/src/catalog.rs +++ b/crates/engine/src/catalog.rs @@ -411,7 +411,7 @@ pub static TOOL_GROUPS: &[ToolGroup] = &[ item("Preflight", "file-check", "preflight.run", Planned("M11")), item("Convert colors", "palette", "prepress.convert_colors", Planned("M11")), item("Set page boxes", "square-dashed-mouse-pointer", "page.boxes", Ready), - item("Add printer marks", "ruler", "prepress.marks", Planned("M11")), + item("Add printer marks", "ruler", "prepress.marks", Ready), item("Fix hairlines", "minus", "prepress.hairlines", Planned("M11")), ], }], diff --git a/crates/engine/src/commands.rs b/crates/engine/src/commands.rs index 0647726452..054c618ea9 100644 --- a/crates/engine/src/commands.rs +++ b/crates/engine/src/commands.rs @@ -397,6 +397,8 @@ pub const COMMANDS: &[CommandSpec] = &[ c("page.extract", "Extract pages…", PAGES, None, Assembly, "file-output"), c("page.split", "Split document…", PAGES, None, Assembly, "scissors"), c("page.number", "Number pages…", PAGES, None, Assembly, "hash"), + c("prepress.marks", "Add printer marks…", None, None, Modification, "ruler"), + c("prepress.marks.remove", "Remove printer marks", None, None, Marks(crate::MarkKind::PrinterMarks), "ruler"), c("page.properties", "Page properties…", PAGES, None, Modification, "info"), c("help.shortcuts", "Keyboard shortcuts", HELP, None, Nothing, "circle-help"), c("help.discord", "Join the ArtCraft Discord", HELP, None, Nothing, "messages-square"), diff --git a/crates/engine/src/edit.rs b/crates/engine/src/edit.rs index 8f43689aa9..77bb0271e1 100644 --- a/crates/engine/src/edit.rs +++ b/crates/engine/src/edit.rs @@ -4,9 +4,10 @@ use std::sync::Arc; use crate::{ AddedContent, AddedText, Background, BoxSpec, EditError, FieldProps, FieldValue, FillMark, HeaderFooter, Hidden, ImageEdit, InitialView, Label, - LineEnding, LinkAction, LinkStyle, MarkFile, MarkKind, Markup, NewAnnotation, NewField, NoteIcon, ObjectTarget, PageBox, Protection, ReviewState, - Rgb, Shape, Standard, Style, TabOrder, TransitionSpec, Watermark, a11y, copy_image, decode_data_file, embed_image, field_table, fit_picture, - image_content_type, is_xfa_data_file, js, mark_source, measure, open_source, strip_xfa, unrestricted, xfa, xfa_after_data_change, + LineEnding, LinkAction, LinkStyle, MarkFile, MarkKind, Markup, NewAnnotation, NewField, NoteIcon, ObjectTarget, PageBox, PrinterMarks, + Protection, ReviewState, Rgb, Shape, Standard, Style, TabOrder, TransitionSpec, Watermark, a11y, copy_image, decode_data_file, embed_image, + field_table, fit_picture, image_content_type, is_xfa_data_file, js, mark_source, measure, open_source, strip_xfa, unrestricted, xfa, + xfa_after_data_change, }; mod labels; @@ -305,6 +306,14 @@ pub enum Edit { }, /// Document Properties ▸ Initial View (and the language and binding). SetInitialView(Box), + /// Print production ▸ Add printer marks: trim and bleed marks, registration targets, colour + /// bars and page information outside the trim box, as a page mark; the page may be widened + /// to make room (`settings.make_room`), so the page geometry is read again. + AddPrinterMarks { + pages: Vec, + settings: PrinterMarks, + replace: bool, + }, /// Edit ▸ Attach a file: a document-level attachment (`/Names /EmbeddedFiles`) under /// `name`, made unique; `mime` is the embedded file's `/Subtype` when known. AddAttachment { diff --git a/crates/engine/src/edit/labels.rs b/crates/engine/src/edit/labels.rs index 5343804ef3..b8d79f9a4c 100644 --- a/crates/engine/src/edit/labels.rs +++ b/crates/engine/src/edit/labels.rs @@ -284,6 +284,8 @@ impl Edit { Edit::RemoveMarks { kind: MarkKind::HeaderFooter } => Label::of("Remove header & footer"), Edit::RemoveMarks { kind: MarkKind::Watermark } => Label::of("Remove watermark"), Edit::RemoveMarks { kind: MarkKind::Background } => Label::of("Remove background"), + Edit::RemoveMarks { kind: MarkKind::PrinterMarks } => Label::of("Remove printer marks"), + Edit::AddPrinterMarks { .. } => Label::of("Add printer marks"), Edit::AddText { .. } => Label::of("Add text"), Edit::AddImage { .. } => Label::of("Add image"), Edit::UpdateContent { .. } => Label::of("Edit content"), diff --git a/crates/engine/src/edit/run.rs b/crates/engine/src/edit/run.rs index 5ed70aed7a..72861fe748 100644 --- a/crates/engine/src/edit/run.rs +++ b/crates/engine/src/edit/run.rs @@ -96,6 +96,7 @@ pub(crate) fn check_permission(edit: &Edit, p: &pdfcraft_cos::Permissions) -> Re } Edit::SetInfo { .. } | Edit::AddHeaderFooter { .. } + | Edit::AddPrinterMarks { .. } | Edit::AddWatermark { .. } | Edit::AddBackground { .. } | Edit::RemoveMarks { .. } @@ -704,6 +705,9 @@ pub(crate) fn run_edit(doc: &mut pdfcraft_cos::Document, edit: &Edit, cx: &mut E Edit::MarkTagArtifact { object } => { a11y::mark_tag_artifact(doc, *object).map_err(|e| EditError::Accessibility(e.to_string()))?; } + Edit::AddPrinterMarks { pages, settings, replace } => { + pdfcraft_edit::add_printer_marks(doc, pages, settings, *replace)?; + } Edit::AddHeaderFooter { pages, settings, replace } => { let date = cx.today; pdfcraft_edit::add_header_footer(doc, pages, settings, *replace, &pdfcraft_edit::Context { date })?; diff --git a/crates/engine/src/history.rs b/crates/engine/src/history.rs index fafa65b4c2..75af5d8fcb 100644 --- a/crates/engine/src/history.rs +++ b/crates/engine/src/history.rs @@ -47,6 +47,7 @@ pub(crate) fn uses_scripts(edit: &Edit) -> bool { pub(crate) fn scope_of(edit: &Edit) -> Scope { match edit { Edit::SetInfo { .. } => Scope::Metadata, + Edit::AddPrinterMarks { .. } => Scope::Full, // The Attachments list is read from the whole document. Edit::AddAttachment { .. } | Edit::DeleteAttachment { .. } => Scope::Full, // A file attachment also changes the Attachments list. diff --git a/crates/engine/src/lib.rs b/crates/engine/src/lib.rs index fe139164be..9e64e0c523 100644 --- a/crates/engine/src/lib.rs +++ b/crates/engine/src/lib.rs @@ -86,7 +86,8 @@ pub use pdfcraft_edit::added::{ }; pub use pdfcraft_edit::paragraph_align; pub use pdfcraft_edit::{ - Added, AddedImage, AddedText, Align as TextAlign, Background, Content as AddedContent, Family as FontFamily, HeaderFooter, MarkKind, Watermark, + Added, AddedImage, AddedText, Align as TextAlign, Background, Content as AddedContent, Family as FontFamily, HeaderFooter, MarkKind, + PrinterMarks, Watermark, }; pub use pdfcraft_forms::{ BorderStyle, CalculatedField, CalculationIssue, CalculationOrder, CheckStyle, Field as FormField, FieldAction, FieldChange, FieldFont, diff --git a/crates/engine/src/session/editing.rs b/crates/engine/src/session/editing.rs index be7fd5aca5..48b7f00088 100644 --- a/crates/engine/src/session/editing.rs +++ b/crates/engine/src/session/editing.rs @@ -417,7 +417,14 @@ impl Session { ) || matches!(e, Edit::MoveObjects { objects, .. } if objects.iter().any(|o| o.kind == ObjectKind::Added)) }); let touches_marks = edit.is_none_or(|e| { - matches!(e, Edit::AddHeaderFooter { .. } | Edit::AddWatermark { .. } | Edit::AddBackground { .. } | Edit::RemoveMarks { .. }) + matches!( + e, + Edit::AddHeaderFooter { .. } + | Edit::AddWatermark { .. } + | Edit::AddBackground { .. } + | Edit::AddPrinterMarks { .. } + | Edit::RemoveMarks { .. } + ) }); (touches_added, touches_marks) } diff --git a/crates/ui-egui/src/app.rs b/crates/ui-egui/src/app.rs index 43d0066e63..49dbc865ee 100644 --- a/crates/ui-egui/src/app.rs +++ b/crates/ui-egui/src/app.rs @@ -295,6 +295,7 @@ impl PdfCraftApp { replace_draft: None, number_draft: NumberDraft { from: 1, to: 1, style: pdfcraft_engine::LabelStyle::Decimal, prefix: String::new(), start: 1 }, word_count_draft: word_count_ui::Draft::default(), + printer_marks_draft: Default::default(), } } diff --git a/crates/ui-egui/src/app/control.rs b/crates/ui-egui/src/app/control.rs index e4078efd13..c7dd79e1b0 100644 --- a/crates/ui-egui/src/app/control.rs +++ b/crates/ui-egui/src/app/control.rs @@ -110,6 +110,7 @@ impl PdfCraftApp { "header-footer" => Some(Dialog::Marks(pdfcraft_engine::MarkKind::HeaderFooter)), "watermark" => Some(Dialog::Marks(pdfcraft_engine::MarkKind::Watermark)), "background" => Some(Dialog::Marks(pdfcraft_engine::MarkKind::Background)), + "printer-marks" => Some(Dialog::PrinterMarks), "export-image" => Some(Dialog::Export(export_ui::ExportKind::Image)), "word-count" => Some(Dialog::WordCount), "export-text" => Some(Dialog::Export(export_ui::ExportKind::Text)), diff --git a/crates/ui-egui/src/commands.rs b/crates/ui-egui/src/commands.rs index f072b7febe..cebd86e3a2 100644 --- a/crates/ui-egui/src/commands.rs +++ b/crates/ui-egui/src/commands.rs @@ -132,6 +132,7 @@ impl PdfCraftApp { pdfcraft_engine::MarkKind::HeaderFooter => tl!("header or footer"), pdfcraft_engine::MarkKind::Watermark => tl!("watermark"), pdfcraft_engine::MarkKind::Background => tl!("background"), + pdfcraft_engine::MarkKind::PrinterMarks => tl!("printer marks"), }; crate::i18n::fmt(tl!("This document has no {kind} to change"), &[("kind", kind)]) } @@ -510,11 +511,13 @@ impl PdfCraftApp { self.marks_draft.preview = None; self.dialog = Some(Dialog::Marks(pdfcraft_engine::MarkKind::HeaderFooter)); } - "edit.header_footer.remove" | "edit.watermark.remove" | "edit.background.remove" => { + "prepress.marks" => self.dialog = Some(Dialog::PrinterMarks), + "edit.header_footer.remove" | "edit.watermark.remove" | "edit.background.remove" | "prepress.marks.remove" => { use pdfcraft_engine::MarkKind as K; let kind = match id { "edit.header_footer.remove" => K::HeaderFooter, "edit.watermark.remove" => K::Watermark, + "prepress.marks.remove" => K::PrinterMarks, _ => K::Background, }; self.apply_edit(Edit::RemoveMarks { kind }); diff --git a/crates/ui-egui/src/dialogs.rs b/crates/ui-egui/src/dialogs.rs index 42bed83657..592ebcc524 100644 --- a/crates/ui-egui/src/dialogs.rs +++ b/crates/ui-egui/src/dialogs.rs @@ -14,6 +14,7 @@ pub(super) mod credits; pub(super) mod marks_ui; pub(super) mod pageboxes; pub(super) mod palette; +pub(super) mod printer_marks_ui; mod prompts; pub(super) mod protect; pub(super) mod standards_ui; @@ -39,6 +40,7 @@ struct Pending { link_command: Option<&'static str>, protect_now: bool, boxes_now: bool, + printer_marks_now: bool, marks_now: bool, export_now: bool, props_now: bool, @@ -423,6 +425,12 @@ fn dialog_body(ui: &mut egui::Ui, app: &mut PdfCraftApp, dialog: Dialog, req: &m req.close = req.apply || cancel; return false; } + Dialog::PrinterMarks => { + let (go, cancel) = crate::printer_marks_ui::body(ui, app, &t); + req.printer_marks_now = go; + req.close = go || cancel; + return false; + } Dialog::BookmarksFromTextStyles => { req.close = crate::bookmark_styles_ui::body(ui, app, &t); return false; @@ -1922,6 +1930,9 @@ fn finish_edits(app: &mut PdfCraftApp, dialog: Dialog, req: &mut Pending) { app.protect_draft = Default::default(); app.notify_tr("Password protection will be applied when you save"); } + if req.printer_marks_now { + app.add_printer_marks(); + } if req.apply_number && let Some(edit) = req.number_now.take() { diff --git a/crates/ui-egui/src/dialogs/marks_ui.rs b/crates/ui-egui/src/dialogs/marks_ui.rs index 085c581119..45e0002fa3 100644 --- a/crates/ui-egui/src/dialogs/marks_ui.rs +++ b/crates/ui-egui/src/dialogs/marks_ui.rs @@ -469,6 +469,8 @@ pub(crate) fn body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens, kind: M MarkKind::HeaderFooter => crate::i18n::fmt(tl!("{verb} Header and Footer"), &[("verb", verb)]), MarkKind::Watermark => crate::i18n::fmt(tl!("{verb} Watermark"), &[("verb", verb)]), MarkKind::Background => crate::i18n::fmt(tl!("{verb} Background"), &[("verb", verb)]), + // Printer marks have a dialog of their own (printer_marks_ui); this one is never opened for them. + MarkKind::PrinterMarks => tl!("Add printer marks").to_string(), }; ui.label(egui::RichText::new(title).font(theme::semibold(18.0))); ui.add_space(8.0); @@ -603,6 +605,7 @@ pub(crate) fn body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens, kind: M }); }); } + MarkKind::PrinterMarks => {} MarkKind::Background => { ui.horizontal(|ui| { widgets::radio_value(ui, &mut d.use_file, false, tl!("From colour")); @@ -714,6 +717,7 @@ pub(crate) fn body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens, kind: M } // A colour is the page's tint. MarkKind::Background => {} + MarkKind::PrinterMarks => {} }); ui.vertical(|ui| { ui.horizontal(|ui| { @@ -764,6 +768,7 @@ pub(crate) fn body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens, kind: M } } MarkKind::Background => !d.use_file || d.file.is_some(), + MarkKind::PrinterMarks => true, } && !d.range.pages(count).is_empty(); if ui.add_enabled_ui(ready, |ui| widgets::pill_button(ui, tl!("OK"), true)).inner.clicked() { @@ -799,7 +804,7 @@ fn undrawable(kind: MarkKind, d: &MarksDraft) -> Option { match kind { MarkKind::HeaderFooter => d.hf.first_undrawable(), MarkKind::Watermark if !d.use_file => d.wm.first_undrawable(), - MarkKind::Watermark | MarkKind::Background => None, + MarkKind::Watermark | MarkKind::Background | MarkKind::PrinterMarks => None, } } @@ -878,6 +883,7 @@ pub fn edit(d: &MarksDraft, kind: MarkKind, count: usize) -> Edit { Edit::AddWatermark { pages, settings: wm, replace: d.replace, file: mark_file(d) } } MarkKind::Background => Edit::AddBackground { pages, settings: d.bg.clone(), replace: d.replace, file: mark_file(d) }, + MarkKind::PrinterMarks => Edit::AddPrinterMarks { pages, settings: pdfcraft_engine::PrinterMarks::default(), replace: d.replace }, } } diff --git a/crates/ui-egui/src/dialogs/printer_marks_ui.rs b/crates/ui-egui/src/dialogs/printer_marks_ui.rs new file mode 100644 index 0000000000..5331cfa3a5 --- /dev/null +++ b/crates/ui-egui/src/dialogs/printer_marks_ui.rs @@ -0,0 +1,152 @@ +//! Print production ▸ Add printer marks (Acrobat's Add Printer Marks): which +//! marks, the line weight, whether to widen the page to make room, and the pages. The marks are +//! a removable page mark (Remove printer marks takes them off; adding again replaces them). + +use pdfcraft_engine::{Edit, PrinterMarks}; + +use crate::pageboxes::Range; +use crate::theme::{self, Tokens}; +use crate::{PdfCraftApp, widgets}; + +#[derive(Clone, Debug, PartialEq)] +pub struct PrinterMarksDraft { + pub settings: PrinterMarks, + pub range: Range, + /// Widen the page when the marks would fall outside it (`settings.make_room`). + pub make_room: bool, + pub room: f64, +} + +impl Default for PrinterMarksDraft { + fn default() -> Self { + Self { settings: PrinterMarks::default(), range: Range::All, make_room: true, room: 36.0 } + } +} + +impl PrinterMarksDraft { + fn all(&self) -> bool { + let s = &self.settings; + s.trim && s.bleed && s.registration && s.color_bars && s.page_info + } + + fn set_all(&mut self, on: bool) { + let s = &mut self.settings; + s.trim = on; + s.bleed = on; + s.registration = on; + s.color_bars = on; + s.page_info = on; + } + + fn any(&self) -> bool { + let s = &self.settings; + s.trim || s.bleed || s.registration || s.color_bars || s.page_info + } + + /// The edit for `count` pages with `current` on screen, labelled with the file's name and + /// today's date. + pub fn edit(&self, current: usize, count: usize, label: &str, date: &str) -> Edit { + let pages = match self.range { + Range::All => (0..count).collect(), + Range::Current => vec![current.min(count.saturating_sub(1))], + Range::Pages(a, b) => (a.max(1) - 1..b.min(count)).collect(), + }; + let settings = PrinterMarks { + make_room: self.make_room.then_some(self.room.max(0.0)), + label: label.to_string(), + date: date.to_string(), + ..self.settings.clone() + }; + Edit::AddPrinterMarks { pages, settings, replace: true } + } +} + +/// The dialog: (add, cancel). +pub(crate) fn body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens) -> (bool, bool) { + let Some((i, id)) = app.active_ids() else { return (false, true) }; + let count = app.session.get(id).map(|d| d.info.pages.len()).unwrap_or(0); + let current = app.views[i].current; + let d = &mut app.printer_marks_draft; + ui.label(egui::RichText::new(tl!("Add printer marks")).font(theme::semibold(18.0))); + ui.add_space(8.0); + widgets::section_title(ui, tl!("Marks")); + let mut all = d.all(); + if ui.add(widgets::checkbox(&mut all, tl!("All marks"))).changed() { + d.set_all(all); + } + ui.horizontal(|ui| { + ui.add(widgets::checkbox(&mut d.settings.trim, tl!("Trim marks"))); + ui.add(widgets::checkbox(&mut d.settings.bleed, tl!("Bleed marks"))); + ui.add(widgets::checkbox(&mut d.settings.registration, tl!("Registration marks"))); + }); + ui.horizontal(|ui| { + ui.add(widgets::checkbox(&mut d.settings.color_bars, tl!("Colour bars"))); + ui.add(widgets::checkbox(&mut d.settings.page_info, tl!("Page information"))); + }); + ui.horizontal(|ui| { + let weight_label = ui.label(tl!("Line weight")); + let weights = [(0.25, "0.25 pt"), (0.5, "0.50 pt"), (1.0, "1.00 pt")]; + let shown = weights.iter().find(|(w, _)| (*w - d.settings.line_weight).abs() < 1e-6).map_or("0.25 pt", |(_, l)| l); + let combo = craft_ui::ComboBox::from_id_salt("marks-weight").selected_text(shown).width(100.0).show_ui(ui, |ui| { + for (w, l) in weights { + ui.selectable_value(&mut d.settings.line_weight, w, l); + } + }); + combo.response.labelled_by(weight_label.id); + }); + widgets::section_title(ui, tl!("Page")); + ui.horizontal(|ui| { + ui.add(widgets::checkbox(&mut d.make_room, tl!("Widen the page to make room"))); + ui.add_enabled_ui(d.make_room, |ui| { + let room_label = ui.label(tl!("Room on each side")); + ui.add(egui::DragValue::new(&mut d.room).range(0.0..=PrinterMarks::MAX_ROOM).suffix(" pt")).labelled_by(room_label.id); + }); + }); + ui.label(egui::RichText::new(tl!("The trim box is kept where it is, so the printed page cuts the same.")).small().color(t.text_muted)); + widgets::section_title(ui, tl!("Pages")); + ui.horizontal(|ui| { + widgets::radio_value(ui, &mut d.range, Range::All, tl!("All")); + widgets::radio_value(ui, &mut d.range, Range::Current, crate::i18n::fmt(tl!("Current page ({n})"), &[("n", &(current + 1).to_string())])); + let (mut a, mut b) = match d.range { + Range::Pages(a, b) => (a, b), + _ => (1, count.max(1)), + }; + let on = matches!(d.range, Range::Pages(..)); + if ui.add(widgets::radio(on, tl!("From"))).clicked() { + d.range = Range::Pages(a, b); + } + let ra = ui.add(egui::DragValue::new(&mut a).range(1..=count.max(1))); + ui.label(tl!("to")); + let rb = ui.add(egui::DragValue::new(&mut b).range(1..=count.max(1))); + if ra.changed() || rb.changed() { + d.range = Range::Pages(a.min(b), b.max(a)); + } + }); + ui.add_space(12.0); + let (mut go, mut cancel) = (false, false); + let ready = d.any() && count > 0; + ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { + if ui.add_enabled_ui(ready, |ui| widgets::pill_button(ui, tl!("Add"), true)).inner.clicked() { + go = true; + } + if widgets::pill_button(ui, tl!("Cancel"), false).clicked() { + cancel = true; + } + }); + (go, cancel) +} + +impl PdfCraftApp { + /// Add: the marks on the chosen pages, labelled with the file's name and today's date. + pub(crate) fn add_printer_marks(&mut self) { + let Some((i, id)) = self.active_ids() else { return }; + let Some(doc) = self.session.get(id) else { return }; + let (count, label) = (doc.info.pages.len(), doc.name.clone()); + let (y, m, d) = self.session.today(); + let date = format!("{y:04}-{m:02}-{d:02}"); + let edit = self.printer_marks_draft.edit(self.views[i].current, count, &label, &date); + if self.apply_edit(edit) { + self.notify_tr("Printer marks added"); + } + } +} diff --git a/crates/ui-egui/src/i18n/ar.tsv b/crates/ui-egui/src/i18n/ar.tsv index 1b08929307..9d703752b0 100644 --- a/crates/ui-egui/src/i18n/ar.tsv +++ b/crates/ui-egui/src/i18n/ar.tsv @@ -2815,6 +2815,21 @@ certificate Version الإصدار Edit this PDF تحرير هذا الملف PDF Export this PDF تصدير هذا الملف PDF Find text or tools البحث عن نص أو أدوات + Add printer marks… إضافة علامات الطباعة… + Remove printer marks إزالة علامات الطباعة + Marks العلامات + All marks كل العلامات + Trim marks علامات القص + Bleed marks علامات النزيف + Registration marks علامات التسجيل + Colour bars أشرطة الألوان + Page information معلومات الصفحة + Line weight سُمك الخط + Widen the page to make room توسيع الصفحة لإفساح المجال + Room on each side المساحة على كل جانب + The trim box is kept where it is, so the printed page cuts the same. يبقى مربع القص في مكانه، فتُقص الصفحة المطبوعة كما هي. + Printer marks added تمت إضافة علامات الطباعة + printer marks علامات الطباعة Create & Edit الإنشاء والتحرير Review & Compare المراجعة والمقارنة Forms & Signatures النماذج والتوقيعات diff --git a/crates/ui-egui/src/i18n/bg.tsv b/crates/ui-egui/src/i18n/bg.tsv index e269febf18..76edd04473 100644 --- a/crates/ui-egui/src/i18n/bg.tsv +++ b/crates/ui-egui/src/i18n/bg.tsv @@ -2502,6 +2502,21 @@ animation effects Off Изключени Edit this PDF Редактирай този PDF Export this PDF Експортирай този PDF Find text or tools Търсене на текст или инструменти + Add printer marks… Добавяне на печатарски знаци… + Remove printer marks Премахване на печатарските знаци + Marks Знаци + All marks Всички знаци + Trim marks Знаци за обрязване + Bleed marks Знаци за наддаване + Registration marks Знаци за регистрация + Colour bars Цветни ленти + Page information Информация за страницата + Line weight Дебелина на линията + Widen the page to make room Разширяване на страницата, за да има място + Room on each side Място от всяка страна + The trim box is kept where it is, so the printed page cuts the same. Полето за обрязване остава на мястото си, така че отпечатаната страница се реже по същия начин. + Printer marks added Печатарските знаци са добавени + printer marks печатарски знаци Create & Edit Създаване и редактиране Review & Compare Преглед и сравнение Forms & Signatures Формуляри и подписи diff --git a/crates/ui-egui/src/i18n/ca.tsv b/crates/ui-egui/src/i18n/ca.tsv index 7ff0f83fee..97564b02ff 100644 --- a/crates/ui-egui/src/i18n/ca.tsv +++ b/crates/ui-egui/src/i18n/ca.tsv @@ -2846,6 +2846,21 @@ theme Appearance Aspecte Unpin tool Desenganxa l'eina Arrange image Organitza la imatge Crop Escapça + Add printer marks… Afegeix marques d'impressora… + All marks Totes les marques + Bleed marks Marques de sang + Colour bars Barres de color + Line weight Gruix de línia + Marks Marques + Page information Informació de la pàgina + Printer marks added S'han afegit les marques d'impressora + Registration marks Marques de registre + Remove printer marks Elimina les marques d'impressora + Room on each side Espai a cada costat + The trim box is kept where it is, so the printed page cuts the same. El quadre de retall es manté on és, de manera que la pàgina impresa es talla igual. + Trim marks Marques de retall + Widen the page to make room Amplia la pàgina per fer-hi lloc + printer marks marques d'impressora # Date picker: Today button (#4274) (AI-assisted, not separately native-reviewed) Today Avui diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv index 22f6e3e056..5659fbb18d 100644 --- a/crates/ui-egui/src/i18n/cs.tsv +++ b/crates/ui-egui/src/i18n/cs.tsv @@ -2613,6 +2613,21 @@ animation effects Off Vypnuto Edit this PDF Upravit toto PDF Export this PDF Exportovat toto PDF Find text or tools Hledat text nebo nástroje + Add printer marks… Přidat tiskové značky… + Remove printer marks Odebrat tiskové značky + Marks Značky + All marks Všechny značky + Trim marks Ořezové značky + Bleed marks Značky spadávky + Registration marks Soutiskové značky + Colour bars Barevné pruhy + Page information Informace o stránce + Line weight Tloušťka čáry + Widen the page to make room Rozšířit stránku, aby bylo místo + Room on each side Místo na každé straně + The trim box is kept where it is, so the printed page cuts the same. Ořezový rámeček zůstává na místě, takže se vytištěná stránka ořízne stejně. + Printer marks added Tiskové značky přidány + printer marks tiskové značky Create & Edit Vytváření a úpravy Review & Compare Revize a porovnání Forms & Signatures Formuláře a podpisy diff --git a/crates/ui-egui/src/i18n/de.tsv b/crates/ui-egui/src/i18n/de.tsv index 2c9ce7ae36..508958c5da 100644 --- a/crates/ui-egui/src/i18n/de.tsv +++ b/crates/ui-egui/src/i18n/de.tsv @@ -2540,6 +2540,21 @@ animation effects Off Aus Edit this PDF Dieses PDF bearbeiten Export this PDF Dieses PDF exportieren Find text or tools Text oder Werkzeuge suchen + Add printer marks… Druckmarken hinzufügen… + Remove printer marks Druckmarken entfernen + Marks Marken + All marks Alle Marken + Trim marks Schnittmarken + Bleed marks Beschnittmarken + Registration marks Passermarken + Colour bars Farbbalken + Page information Seiteninformationen + Line weight Linienstärke + Widen the page to make room Seite verbreitern, um Platz zu schaffen + Room on each side Platz auf jeder Seite + The trim box is kept where it is, so the printed page cuts the same. Der Endformatrahmen bleibt, wo er ist, sodass die gedruckte Seite gleich beschnitten wird. + Printer marks added Druckmarken hinzugefügt + printer marks Druckmarken Create & Edit Erstellen & Bearbeiten Review & Compare Prüfen & Vergleichen Forms & Signatures Formulare & Signaturen diff --git a/crates/ui-egui/src/i18n/el.tsv b/crates/ui-egui/src/i18n/el.tsv index d83bf4aef7..9aaefad43e 100644 --- a/crates/ui-egui/src/i18n/el.tsv +++ b/crates/ui-egui/src/i18n/el.tsv @@ -2546,6 +2546,21 @@ animation effects Off Ανενεργά Edit this PDF Επεξεργασία αυτού του PDF Export this PDF Εξαγωγή αυτού του PDF Find text or tools Εύρεση κειμένου ή εργαλείων + Add printer marks… Προσθήκη σημαδιών εκτύπωσης… + Remove printer marks Αφαίρεση σημαδιών εκτύπωσης + Marks Σημάδια + All marks Όλα τα σημάδια + Trim marks Σημάδια ξακρίσματος + Bleed marks Σημάδια ξεχειλώματος + Registration marks Σημάδια σύμπτωσης + Colour bars Χρωματικές ράβδοι + Page information Πληροφορίες σελίδας + Line weight Πάχος γραμμής + Widen the page to make room Διεύρυνση της σελίδας για χώρο + Room on each side Χώρος σε κάθε πλευρά + The trim box is kept where it is, so the printed page cuts the same. Το πλαίσιο ξακρίσματος μένει όπου είναι, ώστε η τυπωμένη σελίδα να κόβεται ίδια. + Printer marks added Προστέθηκαν σημάδια εκτύπωσης + printer marks σημάδια εκτύπωσης Create & Edit Δημιουργία και επεξεργασία Review & Compare Αναθεώρηση και σύγκριση Forms & Signatures Φόρμες και υπογραφές diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv index 131d2dacaf..d84bbbc05f 100644 --- a/crates/ui-egui/src/i18n/es.tsv +++ b/crates/ui-egui/src/i18n/es.tsv @@ -2709,6 +2709,21 @@ animation effects Off Desactivados Edit this PDF Editar este PDF Export this PDF Exportar este PDF Find text or tools Buscar texto o herramientas + Add printer marks… Añadir marcas de impresión… + Remove printer marks Quitar marcas de impresión + Marks Marcas + All marks Todas las marcas + Trim marks Marcas de recorte + Bleed marks Marcas de sangrado + Registration marks Marcas de registro + Colour bars Barras de color + Page information Información de página + Line weight Grosor de línea + Widen the page to make room Ensanchar la página para hacer sitio + Room on each side Espacio a cada lado + The trim box is kept where it is, so the printed page cuts the same. El cuadro de recorte se mantiene donde está, así que la página impresa se corta igual. + Printer marks added Marcas de impresión añadidas + printer marks marcas de impresión Attach a file… Adjuntar un archivo… Delete attachment Eliminar adjunto Attaching files is not available on the web yet Adjuntar archivos aún no está disponible en la web diff --git a/crates/ui-egui/src/i18n/fi.tsv b/crates/ui-egui/src/i18n/fi.tsv index 8c1a9baab9..842af58ebd 100644 --- a/crates/ui-egui/src/i18n/fi.tsv +++ b/crates/ui-egui/src/i18n/fi.tsv @@ -2856,6 +2856,21 @@ theme Appearance Teema Unpin tool Irrota työkalu Arrange image Järjestä kuva Crop Rajaa + Add printer marks… Lisää painomerkit… + All marks Kaikki merkit + Bleed marks Leikkuuvaramerkit + Colour bars Värikaistat + Line weight Viivan paksuus + Marks Merkit + Page information Sivun tiedot + Printer marks added Painomerkit lisätty + Registration marks Rekisterimerkit + Remove printer marks Poista painomerkit + Room on each side Tilaa joka sivulla + The trim box is kept where it is, so the printed page cuts the same. Leikkuulaatikko pidetään paikallaan, joten painettu sivu leikataan samoin. + Trim marks Leikkuumerkit + Widen the page to make room Levennä sivua tilan tekemiseksi + printer marks painomerkit # Date picker: Today button (#4274) (AI-assisted, not separately native-reviewed) Today Tänään diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv index 1c22cf9d81..2afcd3ff80 100644 --- a/crates/ui-egui/src/i18n/fr.tsv +++ b/crates/ui-egui/src/i18n/fr.tsv @@ -2539,6 +2539,21 @@ animation effects Off Désactivés Edit this PDF Modifier ce PDF Export this PDF Exporter ce PDF Find text or tools Rechercher du texte ou des outils + Add printer marks… Ajouter des repères d'impression… + Remove printer marks Supprimer les repères d'impression + Marks Repères + All marks Tous les repères + Trim marks Traits de coupe + Bleed marks Repères de fond perdu + Registration marks Repères de montage + Colour bars Gammes de couleurs + Page information Informations sur la page + Line weight Épaisseur de trait + Widen the page to make room Élargir la page pour faire de la place + Room on each side Marge de chaque côté + The trim box is kept where it is, so the printed page cuts the same. La zone de rognage reste en place : la page imprimée se coupe de la même façon. + Printer marks added Repères d'impression ajoutés + printer marks repères d'impression Create & Edit Créer et modifier Review & Compare Réviser et comparer Forms & Signatures Formulaires et signatures diff --git a/crates/ui-egui/src/i18n/hi.tsv b/crates/ui-egui/src/i18n/hi.tsv index 52c5450bb3..3e32357a6c 100644 --- a/crates/ui-egui/src/i18n/hi.tsv +++ b/crates/ui-egui/src/i18n/hi.tsv @@ -2816,6 +2816,21 @@ theme Appearance स्वरूप Remove crop क्रॉप हटाएँ Send backward पीछे भेजें Send to back सबसे पीछे भेजें + Add printer marks… प्रिंटर मार्क जोड़ें… + All marks सभी मार्क + Bleed marks ब्लीड मार्क + Colour bars कलर बार + Line weight रेखा की मोटाई + Marks मार्क + Page information पृष्ठ की जानकारी + Printer marks added प्रिंटर मार्क जोड़े गए + Registration marks रजिस्ट्रेशन मार्क + Remove printer marks प्रिंटर मार्क हटाएँ + Room on each side हर ओर जगह + The trim box is kept where it is, so the printed page cuts the same. ट्रिम बॉक्स वहीं रखा जाता है जहाँ है, इसलिए प्रिंट किया गया पृष्ठ उसी तरह कटता है। + Trim marks ट्रिम मार्क + Widen the page to make room जगह बनाने के लिए पृष्ठ चौड़ा करें + printer marks प्रिंटर मार्क # Date picker: Today button (#4274) (AI-assisted, not separately native-reviewed) Today आज diff --git a/crates/ui-egui/src/i18n/hu.tsv b/crates/ui-egui/src/i18n/hu.tsv index 47d9c24b40..a66a051492 100644 --- a/crates/ui-egui/src/i18n/hu.tsv +++ b/crates/ui-egui/src/i18n/hu.tsv @@ -2539,6 +2539,21 @@ animation effects Off Ki Edit this PDF Ezen PDF szerkesztése Export this PDF Ezen PDF exportálása Find text or tools Szöveg vagy eszközök keresése + Add printer marks… Nyomdai jelek hozzáadása… + Remove printer marks Nyomdai jelek eltávolítása + Marks Jelek + All marks Minden jel + Trim marks Vágójelek + Bleed marks Kifutójelek + Registration marks Illesztőjelek + Colour bars Színcsíkok + Page information Oldalinformáció + Line weight Vonalvastagság + Widen the page to make room Az oldal szélesítése, hogy legyen hely + Room on each side Hely mindkét oldalon + The trim box is kept where it is, so the printed page cuts the same. A vágókeret a helyén marad, így a nyomtatott oldal ugyanúgy vágható. + Printer marks added Nyomdai jelek hozzáadva + printer marks nyomdai jelek Create & Edit Létrehozás és szerkesztés Review & Compare Áttekintés és összehasonlítás Forms & Signatures Űrlapok és aláírások diff --git a/crates/ui-egui/src/i18n/id.tsv b/crates/ui-egui/src/i18n/id.tsv index 89152f62a4..8520086970 100644 --- a/crates/ui-egui/src/i18n/id.tsv +++ b/crates/ui-egui/src/i18n/id.tsv @@ -2817,6 +2817,21 @@ theme Appearance Tampilan Remove crop Hapus pangkasan Send backward Mundur Selangkah Send to back Kirim ke Belakang + Add printer marks… Tambahkan tanda printer… + All marks Semua tanda + Bleed marks Tanda bleed + Colour bars Bilah warna + Line weight Ketebalan garis + Marks Tanda + Page information Informasi halaman + Printer marks added Tanda printer ditambahkan + Registration marks Tanda registrasi + Remove printer marks Hapus tanda printer + Room on each side Ruang di setiap sisi + The trim box is kept where it is, so the printed page cuts the same. Kotak pangkas tetap di tempatnya, sehingga halaman cetak terpotong sama persis. + Trim marks Tanda pangkas + Widen the page to make room Lebarkan halaman untuk memberi ruang + printer marks tanda printer # Date picker: Today button (#4274) (AI-assisted, not separately native-reviewed) Today Hari ini diff --git a/crates/ui-egui/src/i18n/it.tsv b/crates/ui-egui/src/i18n/it.tsv index 0cae8f3e98..95ef56c3cc 100644 --- a/crates/ui-egui/src/i18n/it.tsv +++ b/crates/ui-egui/src/i18n/it.tsv @@ -2751,6 +2751,21 @@ certificate SHA-256 digest Impronta SHA-256 Edit this PDF Modifica questo PDF Export this PDF Esporta questo PDF Find text or tools Trova testo o strumenti + Add printer marks… Aggiungi segni di stampa… + Remove printer marks Rimuovi segni di stampa + Marks Segni + All marks Tutti i segni + Trim marks Segni di rifilo + Bleed marks Segni di abbondanza + Registration marks Segni di registro + Colour bars Barre colore + Page information Informazioni pagina + Line weight Spessore linea + Widen the page to make room Allarga la pagina per fare spazio + Room on each side Spazio su ogni lato + The trim box is kept where it is, so the printed page cuts the same. Il riquadro di rifilo resta dov'è, così la pagina stampata si taglia allo stesso modo. + Printer marks added Segni di stampa aggiunti + printer marks segni di stampa Create & Edit Crea e modifica Review & Compare Revisione e confronto Forms & Signatures Moduli e firme diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv index abc616b817..bf5c1f0961 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -2539,6 +2539,21 @@ animation effects Off オフ Edit this PDF この PDF を編集 Export this PDF この PDF を書き出す Find text or tools テキストまたはツールを検索 + Add printer marks… プリンターマークを追加… + Remove printer marks プリンターマークを削除 + Marks マーク + All marks すべてのマーク + Trim marks トンボ(仕上がり) + Bleed marks 裁ち落としマーク + Registration marks レジストレーションマーク + Colour bars カラーバー + Page information ページ情報 + Line weight 線の太さ + Widen the page to make room 余白を作るためにページを広げる + Room on each side 各辺の余白 + The trim box is kept where it is, so the printed page cuts the same. 仕上がりボックスはそのままなので、印刷したページは同じ位置で裁断されます。 + Printer marks added プリンターマークを追加しました + printer marks プリンターマーク Create & Edit 作成と編集 Review & Compare レビューと比較 Forms & Signatures フォームと署名 diff --git a/crates/ui-egui/src/i18n/kk.tsv b/crates/ui-egui/src/i18n/kk.tsv index fc6a2d783a..1928e156e5 100644 --- a/crates/ui-egui/src/i18n/kk.tsv +++ b/crates/ui-egui/src/i18n/kk.tsv @@ -2537,6 +2537,21 @@ animation effects Off Өшірулі Edit this PDF Осы PDF файлын түзету Export this PDF Осы PDF файлын экспорттау Find text or tools Мәтінді немесе құралдарды табу + Add printer marks… Басып шығару белгілерін қосу… + Remove printer marks Басып шығару белгілерін алып тастау + Marks Белгілер + All marks Барлық белгілер + Trim marks Кесу белгілері + Bleed marks Шығыңқы жиек белгілері + Registration marks Тіркеу белгілері + Colour bars Түс жолақтары + Page information Бет туралы ақпарат + Line weight Сызық қалыңдығы + Widen the page to make room Орын босату үшін бетті кеңейту + Room on each side Әр жақтағы орын + The trim box is kept where it is, so the printed page cuts the same. Кесу жақтауы орнында қалады, сондықтан басылған бет сол күйінде кесіледі. + Printer marks added Басып шығару белгілері қосылды + printer marks басып шығару белгілері Create & Edit Жасау және өңдеу Review & Compare Шолу және салыстыру Forms & Signatures Пішіндер және қолтаңбалар diff --git a/crates/ui-egui/src/i18n/ko.tsv b/crates/ui-egui/src/i18n/ko.tsv index 20315b5fcf..5d71bb59fb 100644 --- a/crates/ui-egui/src/i18n/ko.tsv +++ b/crates/ui-egui/src/i18n/ko.tsv @@ -2816,6 +2816,21 @@ theme Appearance 모양 Remove crop 자르기 제거 Send backward 뒤로 보내기 Send to back 맨 뒤로 보내기 + Add printer marks… 인쇄용 표시 추가… + All marks 모든 표시 + Bleed marks 재단 여유 표시 + Colour bars 색상 막대 + Line weight 선 두께 + Marks 표시 + Page information 페이지 정보 + Printer marks added 인쇄용 표시를 추가했습니다 + Registration marks 등록 표시 + Remove printer marks 인쇄용 표시 제거 + Room on each side 각 면의 여백 + The trim box is kept where it is, so the printed page cuts the same. 재단 상자는 그대로 유지되므로 인쇄된 페이지도 동일하게 재단됩니다. + Trim marks 재단 표시 + Widen the page to make room 페이지를 넓혀 공간 확보 + printer marks 인쇄용 표시 # Date picker: Today button (#4274) (AI-assisted, not separately native-reviewed) Today 오늘 diff --git a/crates/ui-egui/src/i18n/lt.tsv b/crates/ui-egui/src/i18n/lt.tsv index fd7f431154..351869c7d9 100644 --- a/crates/ui-egui/src/i18n/lt.tsv +++ b/crates/ui-egui/src/i18n/lt.tsv @@ -2809,6 +2809,21 @@ theme Appearance Išvaizda Edit this PDF Redaguoti šį PDF Export this PDF Eksportuoti šį PDF Find text or tools Rasti tekstą arba įrankius + Add printer marks… Pridėti spaudos žymes… + Remove printer marks Pašalinti spaudos žymes + Marks Žymės + All marks Visos žymės + Trim marks Apkirpimo žymės + Bleed marks Užlaidos žymės + Registration marks Sutapdinimo žymės + Colour bars Spalvų juostos + Page information Puslapio informacija + Line weight Linijos storis + Widen the page to make room Praplėsti puslapį, kad būtų vietos + Room on each side Vieta kiekvienoje pusėje + The trim box is kept where it is, so the printed page cuts the same. Apkirpimo rėmelis lieka vietoje, todėl atspausdintas puslapis kerpamas taip pat. + Printer marks added Spaudos žymės pridėtos + printer marks spaudos žymės Attach a file… Pridėti failą… Delete attachment Ištrinti priedą Attaching files is not available on the web yet Failų pridėjimas žiniatinklyje dar negalimas diff --git a/crates/ui-egui/src/i18n/pl.tsv b/crates/ui-egui/src/i18n/pl.tsv index 208dd1c61e..98ac33dc2c 100644 --- a/crates/ui-egui/src/i18n/pl.tsv +++ b/crates/ui-egui/src/i18n/pl.tsv @@ -2515,6 +2515,21 @@ animation effects Off Wyłączone Edit this PDF Edytuj ten plik PDF Export this PDF Eksportuj ten plik PDF Find text or tools Znajdź tekst lub narzędzia + Add printer marks… Dodaj znaczniki drukarskie… + Remove printer marks Usuń znaczniki drukarskie + Marks Znaczniki + All marks Wszystkie znaczniki + Trim marks Znaczniki cięcia + Bleed marks Znaczniki spadu + Registration marks Pasery + Colour bars Paski kolorów + Page information Informacje o stronie + Line weight Grubość linii + Widen the page to make room Poszerz stronę, aby zrobić miejsce + Room on each side Miejsce z każdej strony + The trim box is kept where it is, so the printed page cuts the same. Ramka cięcia zostaje na miejscu, więc wydrukowana strona tnie się tak samo. + Printer marks added Dodano znaczniki drukarskie + printer marks znaczniki drukarskie Create & Edit Tworzenie i edycja Review & Compare Recenzja i porównanie Forms & Signatures Formularze i podpisy diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv index a172a03879..807acddbd4 100644 --- a/crates/ui-egui/src/i18n/pt-br.tsv +++ b/crates/ui-egui/src/i18n/pt-br.tsv @@ -2552,6 +2552,21 @@ animation effects Off Desativados Edit this PDF Editar este PDF Export this PDF Exportar este PDF Find text or tools Localizar texto ou ferramentas + Add printer marks… Adicionar marcas de impressão… + Remove printer marks Remover marcas de impressão + Marks Marcas + All marks Todas as marcas + Trim marks Marcas de corte + Bleed marks Marcas de sangria + Registration marks Marcas de registro + Colour bars Barras de cores + Page information Informações da página + Line weight Espessura da linha + Widen the page to make room Alargar a página para abrir espaço + Room on each side Espaço de cada lado + The trim box is kept where it is, so the printed page cuts the same. A caixa de corte fica onde está, então a página impressa é cortada do mesmo jeito. + Printer marks added Marcas de impressão adicionadas + printer marks marcas de impressão Create & Edit Criar e editar Review & Compare Revisar e comparar Forms & Signatures Formulários e assinaturas diff --git a/crates/ui-egui/src/i18n/ro.tsv b/crates/ui-egui/src/i18n/ro.tsv index 0d0fbf16b4..c6fe080ba6 100644 --- a/crates/ui-egui/src/i18n/ro.tsv +++ b/crates/ui-egui/src/i18n/ro.tsv @@ -2846,6 +2846,21 @@ theme Appearance Aspect Unpin tool Anulează fixarea instrumentului Arrange image Aranjează imaginea Crop Decupare + Add printer marks… Adăugare semne de imprimare… + All marks Toate semnele + Bleed marks Semne de tăiere cu bleed + Colour bars Bare de culoare + Line weight Grosimea liniei + Marks Semne + Page information Informații despre pagină + Printer marks added Semne de imprimare adăugate + Registration marks Semne de registru + Remove printer marks Eliminare semne de imprimare + Room on each side Loc pe fiecare parte + The trim box is kept where it is, so the printed page cuts the same. Caseta de tăiere rămâne unde este, astfel încât pagina imprimată se taie la fel. + Trim marks Semne de tăiere + Widen the page to make room Lărgiți pagina pentru a face loc + printer marks semne de imprimare # Date picker: Today button (#4274) (AI-assisted, not separately native-reviewed) Today Astăzi diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv index b45d73ecab..3d0550eac7 100644 --- a/crates/ui-egui/src/i18n/ru.tsv +++ b/crates/ui-egui/src/i18n/ru.tsv @@ -2452,6 +2452,21 @@ animation effects Off Выкл Edit this PDF Редактировать этот PDF Export this PDF Экспортировать этот PDF Find text or tools Найти текст или инструменты + Add printer marks… Добавить типографские метки… + Remove printer marks Удалить типографские метки + Marks Метки + All marks Все метки + Trim marks Метки обреза + Bleed marks Метки выпуска под обрез + Registration marks Приводные метки + Colour bars Цветовые шкалы + Page information Сведения о странице + Line weight Толщина линии + Widen the page to make room Расширить страницу, чтобы освободить место + Room on each side Место с каждой стороны + The trim box is kept where it is, so the printed page cuts the same. Рамка обреза остаётся на месте, поэтому напечатанная страница режется так же. + Printer marks added Типографские метки добавлены + printer marks типографские метки Create & Edit Создание и редактирование Review & Compare Рецензирование и сравнение Forms & Signatures Формы и подписи diff --git a/crates/ui-egui/src/i18n/sl.tsv b/crates/ui-egui/src/i18n/sl.tsv index c95f6d6bd6..04ae1dab45 100644 --- a/crates/ui-egui/src/i18n/sl.tsv +++ b/crates/ui-egui/src/i18n/sl.tsv @@ -2544,6 +2544,21 @@ animation effects Off Izklopljeni Edit this PDF Uredi ta PDF Export this PDF Izvozi ta PDF Find text or tools Poišči besedilo ali orodja + Add printer marks… Dodaj tiskarske oznake… + Remove printer marks Odstrani tiskarske oznake + Marks Oznake + All marks Vse oznake + Trim marks Oznake za obrez + Bleed marks Oznake za dodatek za obrez + Registration marks Oznake za poravnavo + Colour bars Barvne lestvice + Page information Podatki o strani + Line weight Debelina črte + Widen the page to make room Razširi stran, da bo prostor + Room on each side Prostor na vsaki strani + The trim box is kept where it is, so the printed page cuts the same. Okvir obreza ostane, kjer je, zato se natisnjena stran reže enako. + Printer marks added Tiskarske oznake so dodane + printer marks tiskarske oznake Create & Edit Ustvarjanje in urejanje Review & Compare Pregled in primerjava Forms & Signatures Obrazci in podpisi diff --git a/crates/ui-egui/src/i18n/sq.tsv b/crates/ui-egui/src/i18n/sq.tsv index eb9eed3a0e..4c108c6e25 100644 --- a/crates/ui-egui/src/i18n/sq.tsv +++ b/crates/ui-egui/src/i18n/sq.tsv @@ -2571,6 +2571,21 @@ animation effects Off Joaktiv Remove crop Hiq prerjen Send backward Kalo prapa Send to back Dërgo mbrapa + Add printer marks… Shto shenja shtypjeje… + All marks Të gjitha shenjat + Bleed marks Shenjat e bleed-it + Colour bars Shiritat e ngjyrave + Line weight Trashësia e vijës + Marks Shenjat + Page information Informacioni i faqes + Printer marks added U shtuan shenjat e shtypjes + Registration marks Shenjat e regjistrimit + Remove printer marks Hiq shenjat e shtypjes + Room on each side Hapësira në secilën anë + The trim box is kept where it is, so the printed page cuts the same. Kutia e prerjes (trim) mbetet aty ku është, kështu që faqja e printuar pritet njësoj. + Trim marks Shenjat e prerjes + Widen the page to make room Zgjero faqen për të krijuar hapësirë + printer marks shenja shtypjeje # Date picker: Today button (#4274) (AI-assisted, not separately native-reviewed) Today Sot diff --git a/crates/ui-egui/src/i18n/te.tsv b/crates/ui-egui/src/i18n/te.tsv index 9b81ffb8b9..d35e611940 100644 --- a/crates/ui-egui/src/i18n/te.tsv +++ b/crates/ui-egui/src/i18n/te.tsv @@ -2448,6 +2448,21 @@ animation effects Off ఆఫ్ Edit this PDF ఈ PDFను సవరించు Export this PDF ఈ PDFను ఎగుమти చేయి Find text or tools వచనం లేదా సాధనాలను కనుగొనండి + Add printer marks… ప్రింటర్ గుర్తులను జోడించు… + Remove printer marks ప్రింటర్ గుర్తులను తొలగించు + Marks గుర్తులు + All marks అన్ని గుర్తులు + Trim marks ట్రిమ్ గుర్తులు + Bleed marks బ్లీడ్ గుర్తులు + Registration marks రిజిస్ట్రేషన్ గుర్తులు + Colour bars రంగు పట్టీలు + Page information పేజీ సమాచారం + Line weight గీత మందం + Widen the page to make room స్థలం కోసం పేజీని వెడల్పు చేయండి + Room on each side ప్రతి వైపు స్థలం + The trim box is kept where it is, so the printed page cuts the same. ట్రిమ్ బాక్స్ ఉన్నచోటే ఉంటుంది, కాబట్టి ముద్రించిన పేజీ అలాగే కత్తిరించబడుతుంది. + Printer marks added ప్రింటర్ గుర్తులు జోడించబడ్డాయి + printer marks ప్రింటర్ గుర్తులు Create & Edit సృష్టించు & సవరించు Review & Compare సమీక్షించు & పోల్చు Forms & Signatures ఫారమ్‌లు & సంతకాలు diff --git a/crates/ui-egui/src/i18n/tr.tsv b/crates/ui-egui/src/i18n/tr.tsv index 6921a6c33e..0e4b879281 100644 --- a/crates/ui-egui/src/i18n/tr.tsv +++ b/crates/ui-egui/src/i18n/tr.tsv @@ -2535,6 +2535,21 @@ animation effects Off Kapalı Edit this PDF Bu PDF'i düzenle Export this PDF Bu PDF'i dışa aktar Find text or tools Metin veya araç bul + Add printer marks… Yazıcı işaretleri ekle… + Remove printer marks Yazıcı işaretlerini kaldır + Marks İşaretler + All marks Tüm işaretler + Trim marks Kesim işaretleri + Bleed marks Taşma payı işaretleri + Registration marks Hizalama işaretleri + Colour bars Renk çubukları + Page information Sayfa bilgisi + Line weight Çizgi kalınlığı + Widen the page to make room Yer açmak için sayfayı genişlet + Room on each side Her kenarda boşluk + The trim box is kept where it is, so the printed page cuts the same. Kesim kutusu yerinde kalır, böylece basılan sayfa aynı şekilde kesilir. + Printer marks added Yazıcı işaretleri eklendi + printer marks yazıcı işaretleri Create & Edit Oluştur ve Düzenle Review & Compare İncele ve Karşılaştır Forms & Signatures Formlar ve İmzalar diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv index a824769600..b16b4ef468 100644 --- a/crates/ui-egui/src/i18n/uk.tsv +++ b/crates/ui-egui/src/i18n/uk.tsv @@ -2561,6 +2561,21 @@ animation effects Off Вимкнено Edit this PDF Редактировать цей PDF Export this PDF Експортувати цей PDF Find text or tools Знайти текст або інструменти + Add printer marks… Додати друкарські позначки… + Remove printer marks Вилучити друкарські позначки + Marks Позначки + All marks Усі позначки + Trim marks Позначки обрізання + Bleed marks Позначки випуску під обріз + Registration marks Приводні позначки + Colour bars Кольорові шкали + Page information Відомості про сторінку + Line weight Товщина лінії + Widen the page to make room Розширити сторінку, щоб звільнити місце + Room on each side Місце з кожного боку + The trim box is kept where it is, so the printed page cuts the same. Рамка обрізання лишається на місці, тож надрукована сторінка ріжеться так само. + Printer marks added Друкарські позначки додано + printer marks друкарські позначки Create & Edit Створення й редагування Review & Compare Рецензування й порівняння Forms & Signatures Форми й підписи diff --git a/crates/ui-egui/src/i18n/vi.tsv b/crates/ui-egui/src/i18n/vi.tsv index e79a8aacc4..2bc7605e4d 100644 --- a/crates/ui-egui/src/i18n/vi.tsv +++ b/crates/ui-egui/src/i18n/vi.tsv @@ -2819,6 +2819,21 @@ key Space Space Edit this PDF Chỉnh sửa PDF này Export this PDF Xuất PDF này Find text or tools Tìm văn bản hoặc công cụ + Add printer marks… Thêm dấu máy in… + Remove printer marks Gỡ dấu máy in + Marks Dấu + All marks Tất cả các dấu + Trim marks Dấu xén + Bleed marks Dấu tràn lề + Registration marks Dấu chồng màu + Colour bars Thanh màu + Page information Thông tin trang + Line weight Độ dày nét + Widen the page to make room Mở rộng trang để có chỗ + Room on each side Khoảng trống mỗi bên + The trim box is kept where it is, so the printed page cuts the same. Hộp xén được giữ nguyên vị trí, nên trang in được xén như cũ. + Printer marks added Đã thêm dấu máy in + printer marks dấu máy in Attach a file… Đính kèm tệp… Delete attachment Xóa tệp đính kèm Attaching files is not available on the web yet Chưa thể đính kèm tệp trên web diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv index 96e9c3bb75..69aeb500bc 100644 --- a/crates/ui-egui/src/i18n/zh-hans.tsv +++ b/crates/ui-egui/src/i18n/zh-hans.tsv @@ -2503,6 +2503,21 @@ animation effects Off 关 Edit this PDF 编辑此 PDF Export this PDF 导出此 PDF Find text or tools 查找文本或工具 + Add printer marks… 添加打印标记… + Remove printer marks 移除打印标记 + Marks 标记 + All marks 所有标记 + Trim marks 裁切标记 + Bleed marks 出血标记 + Registration marks 套准标记 + Colour bars 色条 + Page information 页面信息 + Line weight 线条粗细 + Widen the page to make room 加宽页面以留出空间 + Room on each side 每侧空间 + The trim box is kept where it is, so the printed page cuts the same. 裁切框保持不变,打印页面的裁切位置相同。 + Printer marks added 已添加打印标记 + printer marks 打印标记 Create & Edit 创建和编辑 Review & Compare 审阅和比较 Forms & Signatures 表单和签名 diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv index a1b933b830..03957ab42d 100644 --- a/crates/ui-egui/src/i18n/zh-hant.tsv +++ b/crates/ui-egui/src/i18n/zh-hant.tsv @@ -2489,6 +2489,21 @@ animation effects Off 關 Edit this PDF 編輯此 PDF Export this PDF 匯出此 PDF Find text or tools 尋找文字或工具 + Add printer marks… 新增印刷標記… + Remove printer marks 移除印刷標記 + Marks 標記 + All marks 所有標記 + Trim marks 裁切標記 + Bleed marks 出血標記 + Registration marks 對位標記 + Colour bars 色條 + Page information 頁面資訊 + Line weight 線條粗細 + Widen the page to make room 加寬頁面以留出空間 + Room on each side 每側空間 + The trim box is kept where it is, so the printed page cuts the same. 裁切框保持不變,列印頁面的裁切位置相同。 + Printer marks added 已新增印刷標記 + printer marks 印刷標記 Create & Edit 建立與編輯 Review & Compare 審閱與比較 Forms & Signatures 表單與簽名 diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index fe0138f27f..21d4d80c0f 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -105,6 +105,7 @@ use dialogs::credits; use dialogs::marks_ui; use dialogs::pageboxes; use dialogs::palette; +use dialogs::printer_marks_ui; use dialogs::protect; use dialogs::standards_ui; use dialogs::word_count_ui; @@ -609,6 +610,7 @@ pub struct PdfCraftApp { /// Number pages dialog settings (1-based pages). pub number_draft: NumberDraft, pub(crate) word_count_draft: word_count_ui::Draft, + pub printer_marks_draft: printer_marks_ui::PrinterMarksDraft, /// Protect Using Password dialog state. pub protect_draft: protect::ProtectDraft, /// New bookmarks from text styles dialog state (#1693). diff --git a/crates/ui-egui/src/state.rs b/crates/ui-egui/src/state.rs index e3a9c540a6..2f9be25d28 100644 --- a/crates/ui-egui/src/state.rs +++ b/crates/ui-egui/src/state.rs @@ -109,6 +109,8 @@ pub enum Dialog { Split, /// Pages ▸ Number pages… (page labels). NumberPages, + /// Print production ▸ Add printer marks. + PrinterMarks, /// Organize pages ▸ Page properties (the tab order of the selected pages). PageProps, /// Protect Using Password. diff --git a/crates/ui-egui/tests/it/commands.rs b/crates/ui-egui/tests/it/commands.rs index 1e3c36216f..2e59144ff3 100644 --- a/crates/ui-egui/tests/it/commands.rs +++ b/crates/ui-egui/tests/it/commands.rs @@ -100,6 +100,10 @@ fn every_registered_command_is_implemented() { if spec.id == "edit.redo" { app.undo(); } + if spec.id == "prepress.marks.remove" { + use pdfcraft_engine::{Edit, PrinterMarks}; + app.apply_edit(Edit::AddPrinterMarks { pages: vec![0], settings: PrinterMarks::default(), replace: false }); + } if spec.id.starts_with("edit.") && (spec.id.ends_with(".update") || spec.id.ends_with(".remove")) { use pdfcraft_engine::{Background, Edit, HeaderFooter, Watermark}; let mut hf = HeaderFooter::default(); diff --git a/crates/ui-egui/tests/it/editing.rs b/crates/ui-egui/tests/it/editing.rs index 7b245a5a9e..1e677caafa 100644 --- a/crates/ui-egui/tests/it/editing.rs +++ b/crates/ui-egui/tests/it/editing.rs @@ -8034,6 +8034,32 @@ fn test_cropped_image_selection_and_bounding_box() { assert!(h.state().views[0].picked_image.is_none(), "clicking cropped-away region must not hit-test or select the image"); } +/// Print production ▸ Add printer marks: the dialog adds removable marks around +/// the trim box, widening the page to make room; Remove printer marks takes them off. +#[test] +fn printer_marks_are_added_from_the_dialog_and_removed() { + let mut h = harness(2, |_| {}); + let id = h.state().views[0].id; + let before = h.state().session.get(id).unwrap().info.pages[0].width; + assert!(h.state_mut().execute("prepress.marks")); + h.run_steps(2); + assert_eq!(h.state().dialog, Some(Dialog::PrinterMarks)); + h.get_by_label("All marks"); + h.get_by_label("Add").click(); + h.run_steps(3); + let doc = h.state().session.get(id).unwrap(); + assert_eq!(doc.can_undo(), Some("Add printer marks")); + assert!(doc.marks().contains(&pdfcraft_engine::MarkKind::PrinterMarks)); + // No trim box: the whole page is the trim box, so the page grew 36 pt a side for the marks. + assert!((doc.info.pages[0].width - (before + 72.0)).abs() < 0.5, "{} → {}", before, doc.info.pages[0].width); + assert_eq!(h.state().toast.clone().unwrap().0, "Printer marks added"); + assert!(h.state_mut().execute("prepress.marks.remove")); + h.run_steps(3); + let doc = h.state().session.get(id).unwrap(); + assert!(!doc.marks().contains(&pdfcraft_engine::MarkKind::PrinterMarks)); + assert_eq!(doc.can_undo(), Some("Remove printer marks")); +} + /// The Read mode command (not the mode bar) applies a paragraph still open in the editor, as /// putting the Edit text tool down does; left open, it would stay drawn in Read mode with no tool. #[test] diff --git a/docs/gaps.md b/docs/gaps.md index 4f35e651d1..50e149225d 100644 --- a/docs/gaps.md +++ b/docs/gaps.md @@ -358,9 +358,9 @@ update the numbers. ### 18. Print production tools absent - **Missing:** Output Preview, separations, total area coverage, overprint preview, ink manager, - colour conversion, printer marks, fix hairlines, flattener preview, colour management settings; - print as image; PostScript output. -- **Evidence:** 18 print-production features planned (L area). + colour conversion, fix hairlines, flattener preview, colour management settings; + print as image; PostScript output. Printer marks are in (Print production ▸ Add printer marks…). +- **Evidence:** 17 print-production features planned (L area). - **Estimate:** 25–45 h (part of the L area's 30–55). **Doc:** [target-app-parity.md](target-app-parity.md). ### 19. Preferences mostly missing diff --git a/parity/acrobat-features.toml b/parity/acrobat-features.toml index cabfc7823a..14c89acb5f 100644 --- a/parity/acrobat-features.toml +++ b/parity/acrobat-features.toml @@ -8442,11 +8442,11 @@ title = "Add printer marks (crop, bleed, registration, colour bars)" area = "L" tier = "P1" milestone = "M11" -status = "planned" -commands = [] -tools = [] -evidence = [] -notes = "" +status = "shipped" +commands = ["prepress.marks", "prepress.marks.remove"] +tools = ["doc_printer_marks", "doc_remove_marks"] +evidence = ["crates/edit/src/tests.rs::printer_marks_are_drawn_outside_the_trim_box_and_removable", "crates/ui-egui/tests/it/editing.rs::printer_marks_are_added_from_the_dialog_and_removed", "crates/automation/tests/it/automation.rs::printer_marks_through_tools"] +notes = "Print production ▸ Add printer marks…: trim marks at the trim box's corners, dashed bleed marks at the bleed box, registration targets centred on each side, a C/M/Y/K + 75/50/25 % K colour bar and a page-information line (file name, page n of N, date), at 0.25/0.5/1 pt, outside the trim box as a removable page mark (Remove printer marks; adding again replaces), in registration colour (the /All Separation) except the process-ink colour bar; the page is widened by 36 pt a side when the marks would fall outside it, the trim box kept; all, current or a range of pages; tool doc_printer_marks and doc_remove_marks kind printer_marks. Still missing against Acrobat: the Japanese-style and Eastern-style mark variants, and the choice of marks per page box (always the trim box)." [[feature]] id = "print.fix-hairlines" From cbb2fc80afd9d26b860598f7b3c21fd993243b18 Mon Sep 17 00:00:00 2001 From: Osvaldo Duran Melendez Date: Mon, 2 Nov 2026 19:55:56 -0500 Subject: [PATCH 2/2] Follow upstream: the mark kinds the new replacement tests match on Co-Authored-By: Claude Sonnet 5.5 --- crates/edit/src/mark_transaction_tests.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/edit/src/mark_transaction_tests.rs b/crates/edit/src/mark_transaction_tests.rs index 2623da88ba..22530c8836 100644 --- a/crates/edit/src/mark_transaction_tests.rs +++ b/crates/edit/src/mark_transaction_tests.rs @@ -121,6 +121,7 @@ fn apply_replacement(doc: &mut Document, kind: MarkKind, source: MarkSource, rep MarkKind::Background => { add_background(doc, SELECTED, &Background { color: [0.1, 0.3, 0.7], replace_backdrop: true, ..Default::default() }, replace) } + MarkKind::PrinterMarks => Ok(()), } } @@ -172,6 +173,7 @@ fn replacements_commit_all_selected_pages_and_preserve_other_marks_and_shared_re MarkKind::HeaderFooter => "(New header)".to_string(), MarkKind::Watermark => format!("/PCPic{} Do", source.xobject.num), MarkKind::Background => "0.1 0.3 0.7 rg".to_string(), + MarkKind::PrinterMarks => String::new(), }; apply_replacement(&mut doc, kind, source, true).unwrap(); let doc = reopen(&doc);