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 <noreply@anthropic.com>
This commit is contained in:
Osvaldo DuranandClaude Opus 5.5 committed 2026-11-01 18:53:50 -05:00
1 parent 0cba87ae6f
commit 8c6c5a261b
61 files changed
+1316 -19

No files matched your search

+1 -1
View File
@@ -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)?;
+22
View File
@@ -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<bool> { 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 }
+16 -2
View File
@@ -1038,13 +1038,27 @@ pub fn tools() -> Vec<ToolDef> {
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 (`<name>_page_<n>.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(
+29
View File
@@ -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");
+2
View File
@@ -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
};
+2
View File
@@ -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,
+3
View File
@@ -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",
}
}
}
+340
View File
@@ -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<f64>,
/// 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 <tint>]`: 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<f64> = 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<Vec<usize>, 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<f64> = 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)])
}
+46
View File
@@ -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<f64> = 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.
@@ -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:?}");
}
@@ -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<Vec<f64>> {
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]));
}
+108
View File
@@ -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<Vec<f64>> {
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));
}
+1 -1
View File
@@ -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")),
],
}],
+2
View File
@@ -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"),
+12 -3
View File
@@ -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<InitialView>),
/// 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<usize>,
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 {
+2
View File
@@ -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"),
+4
View File
@@ -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 })?;
+1
View File
@@ -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.
+2 -1
View File
@@ -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,
+8 -1
View File
@@ -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)
}
+1
View File
@@ -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(),
}
}
+1
View File
@@ -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)),
+4 -1
View File
@@ -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 });
+11
View File
@@ -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()
{
+7 -1
View File
@@ -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<char> {
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 },
}
}
@@ -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");
}
}
}
+15
View File
@@ -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 النماذج والتوقيعات
Can't render this file because it contains an unexpected character in line 6 and column 47.
+15
View File
@@ -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 Формуляри и подписи
Can't render this file because it contains an unexpected character in line 8 and column 16.
+15
View File
@@ -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
Can't render this file because it contains an unexpected character in line 7 and column 3.
+15
View File
@@ -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
Can't render this file because it contains an unexpected character in line 6 and column 47.
+15
View File
@@ -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
Can't render this file because it contains an unexpected character in line 6 and column 51.
+15
View File
@@ -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 Φόρμες και υπογραφές
Can't render this file because it contains an unexpected character in line 490 and column 9.
+15
View File
@@ -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
Can't render this file because it contains an unexpected character in line 6 and column 60.
+15
View File
@@ -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
Can't render this file because it contains an unexpected character in line 5 and column 47.
+15
View File
@@ -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
Can't render this file because it contains an unexpected character in line 6 and column 39.
+15
View File
@@ -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 आज
Can't render this file because it contains an unexpected character in line 875 and column 12.
+15
View File
@@ -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
Can't render this file because it contains an unexpected character in line 7 and column 44.
+15
View File
@@ -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
Can't render this file because it contains an unexpected character in line 485 and column 12.
+15
View File
@@ -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
Can't render this file because it contains an unexpected character in line 6 and column 47.
+15
View File
@@ -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 フォームと署名
Can't render this file because it contains an unexpected character in line 6 and column 47.
+15
View File
@@ -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 Пішіндер және қолтаңбалар
Can't render this file because it contains an unexpected character in line 885 and column 9.
+15
View File
@@ -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 오늘
Can't render this file because it contains an unexpected character in line 875 and column 12.
+15
View File
@@ -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
Can't render this file because it contains an unexpected character in line 408 and column 9.
+15
View File
@@ -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
Can't render this file because it contains an unexpected character in line 6 and column 47.
+15
View File
@@ -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
Can't render this file because it contains an unexpected character in line 9 and column 16.
+15
View File
@@ -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
Can't render this file because it contains an unexpected character in line 3 and column 110.
+15
View File
@@ -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 Формы и подписи
Can't render this file because it contains an unexpected character in line 9 and column 16.
+15
View File
@@ -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
Can't render this file because it contains an unexpected character in line 9 and column 16.
+15
View File
@@ -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
Can't render this file because it contains an unexpected character in line 9 and column 16.
+15
View File
@@ -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 ఫారమ్‌లు & సంతకాలు
Can't render this file because it contains an unexpected character in line 13 and column 16.
+15
View File
@@ -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
Can't render this file because it contains an unexpected character in line 884 and column 9.
+15
View File
@@ -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 Форми й підписи
Can't render this file because it contains an unexpected character in line 6 and column 47.
+15
View File
@@ -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
Can't render this file because it contains an unexpected character in line 495 and column 12.
+15
View File
@@ -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 表单和签名
Can't render this file because it contains an unexpected character in line 9 and column 16.
+15
View File
@@ -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 表單與簽名
Can't render this file because it contains an unexpected character in line 9 and column 16.
+2
View File
@@ -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).
+2
View File
@@ -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.
+4
View File
@@ -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();
+26
View File
@@ -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]
+3 -3
View File
@@ -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
+5 -5
View File
@@ -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"