diff --git a/crates/doc/src/marks.rs b/crates/doc/src/marks.rs index 7a7a2104..ac1500eb 100644 --- a/crates/doc/src/marks.rs +++ b/crates/doc/src/marks.rs @@ -120,6 +120,11 @@ impl TrimMarks { } } +/// `r` grown by `[top, bottom, left, right]` (a bleed, or how far marks reach). +pub fn outset(r: Rect, [top, bottom, left, right]: [f64; 4]) -> Rect { + Rect::new(r.x0 - left, r.y0 - top, r.x1 + right, r.y1 + bottom) +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/doc/src/setup.rs b/crates/doc/src/setup.rs index ee92f745..5a893438 100644 --- a/crates/doc/src/setup.rs +++ b/crates/doc/src/setup.rs @@ -257,8 +257,7 @@ impl Default for DocSetup { impl DocSetup { /// `artboard` grown by the bleed. pub fn bleed_rect(&self, artboard: Rect) -> Rect { - let [top, bottom, left, right] = self.bleed; - Rect::new(artboard.x0 - left, artboard.y0 - top, artboard.x1 + right, artboard.y1 + bottom) + crate::marks::outset(artboard, self.bleed) } pub fn has_bleed(&self) -> bool { self.bleed.iter().any(|b| *b > 0.0) diff --git a/crates/engine/src/cmd/fileio/pdf.rs b/crates/engine/src/cmd/fileio/pdf.rs index abd5fcc6..b0814790 100644 --- a/crates/engine/src/cmd/fileio/pdf.rs +++ b/crates/engine/src/cmd/fileio/pdf.rs @@ -40,10 +40,21 @@ pub(super) const OPTIONS: &[FormatOption] = &[ }, FormatOption { name: "compression", ty: "object", default: "null", description: "{color, gray, mono, compressText} (see document.exportPdf)" }, FormatOption { name: "marks", ty: "object", default: "null", description: "printer's marks (see document.exportPdf)" }, - FormatOption { name: "bleed", ty: "object", default: "null", description: "{useDocument, top, bottom, left, right} in points" }, + FormatOption { + name: "bleed", + ty: "object", + default: "null", + description: "{useDocument, top, bottom, left, right} in points: the page grows by the bleed (BleedBox) around its artboard (TrimBox)", + }, FormatOption { name: "output", ty: "object", default: "null", description: "colour conversion and output intent (see document.exportPdf)" }, FormatOption { name: "advanced", ty: "object", default: "null", description: "{fontSubsetPercent, outlineText, overprint}" }, FormatOption { name: "security", ty: "object", default: "null", description: "passwords (not supported yet) and permissions" }, + FormatOption { + name: "includeNonPrinting", + ty: "boolean", + default: "false", + description: "keep the layers whose Print option is off (left out otherwise, unless createLayers)", + }, ]; pub fn specs() -> Vec { @@ -53,7 +64,7 @@ pub fn specs() -> Vec { "Export PDF", [], None, - "{path?, preset?: \"VectorCraft Default\" (built-in or saved: pdf.preset.list), artboard? | artboards?: [i…] | range?: \"1-3, 5\" (1-based; default all, one page each), standard?: none|pdfA2b|pdfX1a|pdfX3|pdfX4, compatibility?: 1.4|1.5|1.6|1.7 (default)|2.0, preserveEditing? (on in VectorCraft Default: the native document as an embedded file, which document.open restores; choosing a standard turns it off, PDF/A refuses it), thumbnails?, fastWebView?, viewAfterSaving? (the app opens the written file), createLayers?, compression?: {color?, gray?: {downsample: none|average|subsample|bicubic, ppi: 300, abovePpi: 450, compression: none|zip|jpeg|jpeg2000|auto, quality: minimum|low|medium|high|maximum}, mono?: {downsample, ppi: 1200, abovePpi: 1800, compression: none|ccittG3|ccittG4|zip|runLength} (black-and-white images), compressText?: true} (images above abovePpi are resampled to ppi; auto keeps JPEGs JPEG, the others lossless; JPEG needs opaque images; none, jpeg2000, CCITT and runLength are written as ZIP with a warning), marks?: {trim, registration, colorBars, pageInfo, kind: roman|japanese, weight: 0.25, offset: 6}, bleed?: {useDocument, top, bottom, left, right} (pt), output?: {conversion: none|destination|preserveNumbers, destination, profiles: none|all|destination|taggedSource, outputIntent, outputCondition, outputConditionId, registry, trapped}, advanced?: {fontSubsetPercent: 100, outlineText: true, overprint: preserve|discard}, security?: {openPassword, permissionsPassword, printing: none|low|high, changes: none|pages|forms|comments|any, copy, screenReader, plaintextMetadata}} → {path, bytes, warnings}; no path → {dataBase64, bytes, warnings}. The options apply over the preset (null keeps its value); options accepted but not applied yet come back as warnings; PDF/X, passwords and PDF/A-2b at 2.0 are refused. document.export {format: pdf} takes the same options", + "{path?, preset?: \"VectorCraft Default\" (built-in or saved: pdf.preset.list), artboard? | artboards?: [i…] | range?: \"1-3, 5\" (1-based; default all, one page each), standard?: none|pdfA2b|pdfX1a|pdfX3|pdfX4, compatibility?: 1.4|1.5|1.6|1.7 (default)|2.0, preserveEditing? (on in VectorCraft Default: the native document as an embedded file, which document.open restores; choosing a standard turns it off, PDF/A refuses it), thumbnails?, fastWebView?, viewAfterSaving? (the app opens the written file), createLayers?, includeNonPrinting? (keep layers whose Print option is off; left out by default unless createLayers), compression?: {color?, gray?: {downsample: none|average|subsample|bicubic, ppi: 300, abovePpi: 450, compression: none|zip|jpeg|jpeg2000|auto, quality: minimum|low|medium|high|maximum}, mono?: {downsample, ppi: 1200, abovePpi: 1800, compression: none|ccittG3|ccittG4|zip|runLength} (black-and-white images), compressText?: true} (images above abovePpi are resampled to ppi; auto keeps JPEGs JPEG, the others lossless; JPEG needs opaque images; none, jpeg2000, CCITT and runLength are written as ZIP with a warning), marks?: {trim, registration, colorBars, pageInfo, kind: roman|japanese, weight: 0.25, offset: 6}, bleed?: {useDocument (the document's bleed, document.setup), top, bottom, left, right} (pt; art in the bleed is kept). Each page: TrimBox = artboard, BleedBox = MediaBox = artboard + bleed, output?: {conversion: none|destination|preserveNumbers, destination, profiles: none|all|destination|taggedSource, outputIntent, outputCondition, outputConditionId, registry, trapped}, advanced?: {fontSubsetPercent: 100, outlineText: true, overprint: preserve|discard}, security?: {openPassword, permissionsPassword, printing: none|low|high, changes: none|pages|forms|comments|any, copy, screenReader, plaintextMetadata}} → {path, bytes, warnings}; no path → {dataBase64, bytes, warnings}. The options apply over the preset (null keeps its value); options accepted but not applied yet come back as warnings; PDF/X, passwords and PDF/A-2b at 2.0 are refused. document.export {format: pdf} takes the same options", has_doc, export_pdf ), diff --git a/crates/engine/src/lib.rs b/crates/engine/src/lib.rs index 291d37cc..12247d64 100644 --- a/crates/engine/src/lib.rs +++ b/crates/engine/src/lib.rs @@ -1094,6 +1094,8 @@ mod tests_pattern; #[cfg(test)] mod tests_pdffidelity; #[cfg(test)] +mod tests_pdfmarks; +#[cfg(test)] mod tests_pdfpresets; #[cfg(test)] mod tests_pdfraster; diff --git a/crates/engine/src/tests_pdfmarks.rs b/crates/engine/src/tests_pdfmarks.rs new file mode 100644 index 00000000..2fb23159 --- /dev/null +++ b/crates/engine/src/tests_pdfmarks.rs @@ -0,0 +1,59 @@ +//! `document.exportPdf` Marks and Bleeds: the document's bleed or custom values set the page +//! boxes, non-printing layers are left out unless asked. + +use serde_json::{Value, json}; +use vectorcraft_pdf::CropTo; + +use super::*; + +fn session() -> Session { + let mut s = Session::new(); + s.execute("file.new", &json!({"width": 100, "height": 80, "bleed": 9})).unwrap(); + s.execute("shape.rectangle", &json!({"x": 10, "y": 10, "width": 40, "height": 30})).unwrap(); + s +} + +/// The PDF `document.exportPdf {p}` writes, and its warnings. +fn export(s: &mut Session, p: Value) -> (Vec, Vec) { + let v = s.execute("document.exportPdf", &p).unwrap_or_else(|e| panic!("{p}: {e}")); + let bytes = vectorcraft_format::base64_decode(v["dataBase64"].as_str().unwrap()).unwrap(); + (bytes, serde_json::from_value(v["warnings"].clone()).unwrap()) +} + +fn boxes(bytes: &[u8]) -> (kurbo::Rect, kurbo::Rect, kurbo::Rect) { + let page = &vectorcraft_pdf::info(bytes, None).unwrap().pages[0]; + let get = |c: CropTo| page.boxes.iter().find(|(b, _)| *b == c).unwrap().1; + (get(CropTo::Media), get(CropTo::Bleed), get(CropTo::Trim)) +} + +#[test] +fn the_document_bleed_or_custom_values_grow_each_page() { + let mut s = session(); + let (bytes, warnings) = export(&mut s, json!({"bleed": {"useDocument": true}})); + assert!(warnings.is_empty(), "{warnings:?}"); + let (media, bleed, trim) = boxes(&bytes); + assert_eq!((media.width(), media.height()), (118.0, 98.0), "MediaBox = artboard + the document's 9 pt bleed"); + assert_eq!((bleed, trim), (media, kurbo::Rect::new(9.0, 9.0, 109.0, 89.0))); + // Press Quality uses the document's bleed; the default preset and custom values don't. + assert_eq!(boxes(&export(&mut s, json!({"preset": "Press Quality"})).0).0.width(), 118.0); + assert_eq!(boxes(&export(&mut s, json!({})).0).0.width(), 100.0); + assert_eq!(boxes(&export(&mut s, json!({"bleed": {"left": 4.5}})).0).0.width(), 104.5); +} + +#[test] +fn non_printing_layers_are_left_out_unless_included() { + let mut s = session(); + let layer = s.execute("layer.new", &json!({"name": "Notes"})).unwrap()["id"].as_u64().unwrap(); + s.execute("layer.setProps", &json!({"id": layer, "printable": false})).unwrap(); + s.execute("shape.ellipse", &json!({"x": 60, "y": 40, "width": 20, "height": 20})).unwrap(); + let paths = |s: &mut Session, p| { + let doc = vectorcraft_pdf::import(&export(s, p).0).unwrap(); + let mut n = 0; + doc.walk(|x| n += usize::from(x.path_data().is_some())); + n + }; + let without = paths(&mut s, json!({})); + assert_eq!(paths(&mut s, json!({"includeNonPrinting": true})), without + 1, "the ellipse on the non-printing layer"); + s.execute("layer.setProps", &json!({"id": layer, "printable": true})).unwrap(); + assert_eq!(paths(&mut s, json!({})), without + 1); +} diff --git a/crates/pdf/src/export.rs b/crates/pdf/src/export.rs index 8f1b4e04..394cef20 100644 --- a/crates/pdf/src/export.rs +++ b/crates/pdf/src/export.rs @@ -19,6 +19,7 @@ use vectorcraft_effects::stroke::{self, WrittenShape}; use vectorcraft_geom::{Affine, BezPath, FillRule, Rect}; use crate::lab_spot::{find, rfind}; +use crate::marks::PageBoxes; use crate::{Compatibility, ExportReport, PdfError, PdfOptions, Standard}; /// Export `doc` as PDF bytes: one page per artboard (or the artboards chosen in `opts`). @@ -104,6 +105,7 @@ pub fn export_with_report(doc: &Document, opts: &PdfOptions) -> Result Result { interpolate: bool, /// How images are resampled and compressed. compression: &'a crate::CompressionSettings, + /// Layers whose Print option is off are written too. + non_printing: bool, } /// Most pixels along a side of a freeform gradient's image (its colour field is smooth). @@ -600,7 +607,9 @@ impl Exporter<'_> { if !force && !n.visible { return; } - if let NodeKind::Layer { template: true, .. } = n.kind { + if let NodeKind::Layer { template, printable, .. } = n.kind + && (template || !(printable || self.non_printing)) + { return; } match n.visual_bounds() { diff --git a/crates/pdf/src/lib.rs b/crates/pdf/src/lib.rs index 36237249..93cd675f 100644 --- a/crates/pdf/src/lib.rs +++ b/crates/pdf/src/lib.rs @@ -6,7 +6,10 @@ //! as their brush art), opacity and blend modes (transparency groups), //! clip groups, linear/radial gradients (shadings), pattern fills and strokes as their tiles and //! freeform gradients as images (clipped to what they paint), embedded images (resampled and -//! compressed as the Compression settings say) and text as outlined glyph paths. Hidden objects, guides and template layers are skipped. [`PdfSettings`] is the Save PDF +//! compressed as the Compression settings say) and text as outlined glyph paths. Hidden objects, +//! guides and template layers are skipped, and so are non-printing layers unless asked for. +//! Each page is its artboard (the trim box) grown by the bleed (the bleed box and media box). +//! [`PdfSettings`] is the Save PDF //! dialog's model (standard, compatibility, General, Compression, Marks and Bleeds, Output, //! Advanced, Security); options the writer doesn't apply yet come back as warnings. With Preserve //! Editing the native document rides along as an embedded file ([`editing()`]). @@ -35,6 +38,7 @@ mod import_scan; mod import_shading; mod import_text; mod lab_spot; +mod marks; mod pages; mod presets; mod settings; @@ -197,6 +201,8 @@ mod tests_import_layers; #[cfg(test)] mod tests_import_options; #[cfg(test)] +mod tests_marks; +#[cfg(test)] mod tests_presets; #[cfg(test)] mod tests_settings; diff --git a/crates/pdf/src/marks.rs b/crates/pdf/src/marks.rs new file mode 100644 index 00000000..b3222ffe --- /dev/null +++ b/crates/pdf/src/marks.rs @@ -0,0 +1,35 @@ +//! Marks and Bleeds: the boxes of each exported page. + +use vectorcraft_doc::Document; +use vectorcraft_doc::marks::outset; +use vectorcraft_doc::setup::MAX_BLEED; +use vectorcraft_geom::Rect; + +use crate::PdfSettings; + +impl PdfSettings { + /// The bleed of `doc`'s pages, `[top, bottom, left, right]` in points: the document's (Document + /// Setup; one read from a file is kept to 0–[`MAX_BLEED`]) with Use Document Bleed, else the + /// settings' own. + pub fn bleed_of(&self, doc: &Document) -> [f64; 4] { + if self.bleed.use_document { doc.setup.bleed.map(|b| if b.is_finite() { b.clamp(0.0, MAX_BLEED) } else { 0.0 }) } else { self.bleed.values() } + } +} + +/// The boxes of one page, in document coordinates. +#[derive(Clone, Copy, Debug, PartialEq)] +pub(crate) struct PageBoxes { + /// The artboard: the finished page. + pub trim: Rect, + /// The trim box grown by the bleed: the art it holds is kept. + pub bleed: Rect, + /// The sheet: the bleed box. + pub media: Rect, +} + +impl PageBoxes { + pub fn new(trim: Rect, bleed: [f64; 4]) -> Self { + let r = outset(trim, bleed); + Self { trim, bleed: r, media: r } + } +} diff --git a/crates/pdf/src/settings.rs b/crates/pdf/src/settings.rs index b472646b..ef50721a 100644 --- a/crates/pdf/src/settings.rs +++ b/crates/pdf/src/settings.rs @@ -209,6 +209,9 @@ pub struct PdfSettings { pub view_after_saving: bool, /// Write top-level layers as PDF layers (optional content). pub create_layers: bool, + /// Keep the layers whose Print option is off (they are left out otherwise, unless + /// `create_layers` is on). + pub include_non_printing: bool, pub compression: CompressionSettings, pub marks: MarkSettings, pub bleed: BleedSettings, @@ -310,7 +313,8 @@ pub struct BleedSettings { } impl BleedSettings { - fn values(&self) -> [f64; 4] { + /// `[top, bottom, left, right]`. + pub(crate) fn values(&self) -> [f64; 4] { [self.top, self.bottom, self.left, self.right] } } @@ -440,7 +444,6 @@ impl PdfSettings { (self.fast_web_view, "fast web view (a linearised file) is not written yet"), (self.create_layers, "PDF layers are not written yet: every layer is plain page content"), (self.marks.any(), "printer's marks are not drawn yet"), - (!self.bleed.use_document && self.bleed.values().iter().any(|v| *v > 0.0), "bleed is not added yet: each page is its artboard"), (o.conversion != ColorConversion::None, "colour conversion is not applied yet: colours are written as they are"), (o.profiles != ProfileInclusion::None, "ICC profiles are not embedded yet"), ( diff --git a/crates/pdf/src/tests_marks.rs b/crates/pdf/src/tests_marks.rs new file mode 100644 index 00000000..0ebcd524 --- /dev/null +++ b/crates/pdf/src/tests_marks.rs @@ -0,0 +1,108 @@ +//! Marks and Bleeds: page boxes, the art kept in the bleed and non-printing layers. + +use std::sync::Arc; + +use serde_json::json; +use vectorcraft_color::{Color, Paint}; +use vectorcraft_doc::{Appearance, Document, LayerColor, Node, NodeKind}; +use vectorcraft_geom::{Rect, shapes}; + +use crate::*; + +/// A 200 × 100 document with a red square reaching 5 pt left of its artboard. +fn doc() -> Document { + let mut d = Document::new(200.0, 100.0); + let layer = d.default_layer().unwrap(); + let id = d.alloc_id(); + let red = Appearance::basic(Paint::solid(Color::rgb(1.0, 0.0, 0.0)), Paint::None, 0.0); + d.insert(Some(layer), 0, Node::path(id, shapes::rectangle(Rect::new(-5.0, 10.0, 40.0, 50.0)), red)).unwrap(); + d +} + +fn settings(v: serde_json::Value) -> PdfSettings { + serde_json::from_value(v).unwrap() +} + +/// The PDF of `d` with settings `v`; the bleed raises no warning. +fn pdf(d: &Document, v: serde_json::Value) -> Vec { + let opts = PdfOptions { settings: settings(v), created: Some(1_791_200_000), ..PdfOptions::uncompressed() }; + let r = export_with_report(d, &opts).unwrap(); + assert!(r.warnings.iter().all(|w| !w.contains("bleed")), "{:?}", r.warnings); + r.bytes +} + +/// Page 1's box `which` (PDF space: y up). +fn page_box(bytes: &[u8], which: CropTo) -> Rect { + info(bytes, None).unwrap().pages[0].boxes.iter().find(|(c, _)| *c == which).unwrap().1 +} + +/// The bounds of every path of `d` painted with a paint `pick` accepts. +fn painted(d: &Document, pick: impl Fn(&Paint) -> bool) -> Vec { + let mut out = vec![]; + d.walk(|n| { + let paints = n.appearance.fill().map(|f| &f.paint).into_iter().chain(n.appearance.stroke().map(|s| &s.paint)); + if paints.into_iter().any(&pick) + && let Some(b) = n.path_data().and_then(|p| p.bounds()) + { + out.push(b); + } + }); + out +} + +fn rgb(p: &Paint, rgb: [u8; 3]) -> bool { + p.color().is_some_and(|c| c.to_rgba8(1.0)[..3] == rgb) +} + +#[test] +fn bleed_grows_the_media_box_around_the_trim_box() { + let d = doc(); + // No bleed and no marks: the page is the artboard. + let plain = pdf(&d, json!({})); + assert_eq!(page_box(&plain, CropTo::Media), Rect::new(0.0, 0.0, 200.0, 100.0)); + assert_eq!(page_box(&plain, CropTo::Trim), Rect::new(0.0, 0.0, 200.0, 100.0)); + // A 9 pt bleed, custom or the document's. + let mut nine = d.clone(); + nine.setup.bleed = [9.0; 4]; + for v in [json!({"bleed": {"top": 9, "bottom": 9, "left": 9, "right": 9}}), json!({"bleed": {"useDocument": true, "top": 1}})] { + let bytes = pdf(&nine, v.clone()); + let media = page_box(&bytes, CropTo::Media); + assert_eq!((media.width(), media.height()), (218.0, 118.0), "{v}: MediaBox = artboard + 2 × 9 pt"); + assert_eq!(page_box(&bytes, CropTo::Trim), Rect::new(9.0, 9.0, 209.0, 109.0), "{v}: TrimBox = artboard"); + assert_eq!(page_box(&bytes, CropTo::Bleed), media, "{v}: BleedBox = artboard + bleed"); + } + // A document bleed read from a file is kept in range. + nine.setup.bleed = [f64::NAN, -3.0, 1e9, 2.0]; + let b = pdf(&nine, json!({"bleed": {"useDocument": true}})); + assert_eq!(page_box(&b, CropTo::Media), Rect::new(0.0, 0.0, 274.0, 100.0)); + // Uneven bleed (PDF space is y up: the bottom bleed is below the trim box). + let bytes = pdf(&d, json!({"bleed": {"top": 4, "bottom": 2, "left": 3, "right": 1}})); + assert_eq!(page_box(&bytes, CropTo::Media), Rect::new(0.0, 0.0, 204.0, 106.0)); + assert_eq!(page_box(&bytes, CropTo::Trim), Rect::new(3.0, 2.0, 203.0, 102.0)); +} + +#[test] +fn art_in_the_bleed_is_kept() { + let d = doc(); + let back = import(&pdf(&d, json!({"bleed": {"left": 9, "top": 9, "bottom": 9, "right": 9}}))).unwrap(); + let red = painted(&back, |p| rgb(p, [255, 0, 0])); + assert_eq!(red.len(), 1); + assert!((red[0].x0 - 4.0).abs() < 0.01, "the square reaches 5 pt into the 9 pt bleed: {red:?}"); +} + +#[test] +fn non_printing_layers_are_left_out_unless_asked_for() { + let mut d = doc(); + let mut layer = Node::layer(d.alloc_id(), "Notes", LayerColor::Preset(1)); + let blue = Appearance::basic(Paint::solid(Color::rgb(0.0, 0.0, 1.0)), Paint::None, 0.0); + let note = Node::path(d.alloc_id(), shapes::rectangle(Rect::new(100.0, 20.0, 150.0, 60.0)), blue); + if let NodeKind::Layer { children, printable, .. } = &mut layer.kind { + *printable = false; + children.push(Arc::new(note)); + } + d.layers.push(Arc::new(layer)); + let blues = |v| painted(&import(&pdf(&d, v)).unwrap(), |p| rgb(p, [0, 0, 255])).len(); + assert_eq!(blues(json!({})), 0, "a non-printing layer is absent"); + assert_eq!(blues(json!({"includeNonPrinting": true})), 1); + assert_eq!(blues(json!({"createLayers": true})), 1, "PDF layers keep every layer"); +} diff --git a/crates/pdf/src/tests_settings.rs b/crates/pdf/src/tests_settings.rs index cf4deca6..0cc1ba40 100644 --- a/crates/pdf/src/tests_settings.rs +++ b/crates/pdf/src/tests_settings.rs @@ -75,7 +75,6 @@ fn options_not_applied_yet_come_back_as_warnings() { (json!({"fastWebView": true}), "fast web view"), (json!({"createLayers": true}), "layers"), (json!({"marks": {"registration": true}}), "marks"), - (json!({"bleed": {"top": 9}}), "bleed"), (json!({"output": {"conversion": "destination"}}), "conversion"), (json!({"output": {"profiles": "all"}}), "ICC"), (json!({"output": {"trapped": true}}), "trapped"), @@ -85,10 +84,12 @@ fn options_not_applied_yet_come_back_as_warnings() { let w = settings(v.clone()).warnings(); assert!(w.len() == 1 && w[0].contains(word), "{v}: {w:?}"); } - // A document bleed, the default view/overprint choices and applied options (image compression + // Bleed, the default view/overprint choices and applied options (image compression // too: a codec the writer lacks is reported when an image needs it) warn about nothing. for v in [ json!({"bleed": {"useDocument": true, "top": 9}}), + json!({"bleed": {"top": 9}}), + json!({"includeNonPrinting": true}), json!({"viewAfterSaving": true}), json!({"compression": {"compressText": false}}), json!({"compression": {"color": {"compression": "jpeg"}, "mono": {"compression": "ccittG4"}}}), diff --git a/crates/ui-egui/src/dialogs/save_pdf.rs b/crates/ui-egui/src/dialogs/save_pdf.rs index b052daac..e64adf32 100644 --- a/crates/ui-egui/src/dialogs/save_pdf.rs +++ b/crates/ui-egui/src/dialogs/save_pdf.rs @@ -409,6 +409,7 @@ fn general(ui: &mut egui::Ui, d: &mut Dialog, editor: bool) { flag(ui, d, "fastWebView", "Optimize for fast web view", true); flag(ui, d, "viewAfterSaving", "View PDF after saving", true); flag(ui, d, "createLayers", "Create PDF layers from top-level layers", true); + flag(ui, d, "includeNonPrinting", "Include non-printing layers", true); if editor { heading(ui, "Description"); if let Some(text) = widgets::text_field(ui, "pdf-preset-description", Some(&d.str("description")), 460.0, 3) { @@ -501,12 +502,18 @@ fn marks_and_bleeds(app: &VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) { row(ui, "Offset:", |ui| length(ui, d, "marks.offset", unit, true)); heading(ui, "Bleeds"); flag(ui, d, "bleed.useDocument", "Use document bleed settings", true); - let custom = get(d, "bleed.useDocument").as_bool() != Some(true); + // With the document's bleed the fields show it (Document Setup's `[top, bottom, left, right]`). + let document = (get(d, "bleed.useDocument").as_bool() == Some(true)).then(|| app.session.active().map_or([0.0; 4], |s| s.doc.setup.bleed)); egui::Grid::new("pdf-bleed").num_columns(4).spacing([10.0, 6.0]).show(ui, |ui| { - for pair in [[("bleed.top", "Top:"), ("bleed.bottom", "Bottom:")], [("bleed.left", "Left:"), ("bleed.right", "Right:")]] { - for (p, label) in pair { + for pair in [[(0, "bleed.top", "Top:"), (1, "bleed.bottom", "Bottom:")], [(2, "bleed.left", "Left:"), (3, "bleed.right", "Right:")]] { + for (i, p, label) in pair { ui.label(label); - length(ui, d, p, unit, custom); + match document { + Some(b) => { + ui.add_enabled_ui(false, |ui| widgets::num_field(ui, p, b.get(i).copied(), unit, 80.0)); + } + None => length(ui, d, p, unit, true), + } } ui.end_row(); } @@ -796,6 +803,34 @@ mod tests { assert!(app.run("ui.savePdfDialog", json!({"compatibility": "1.0"})).is_err(), "bad options are refused"); } + #[test] + fn bleed_and_non_printing_layers_reach_the_pdf() { + let (mut app, written, _) = app(); + app.run("document.setup", json!({"bleed": 9})).unwrap(); + let notes = app.run("layer.new", json!({})).unwrap()["id"].clone(); + app.run("layer.setProps", json!({"id": notes, "printable": false})).unwrap(); + app.run("shape.ellipse", json!({"x": 60, "y": 40, "width": 20, "height": 20})).unwrap(); + app.run("ui.savePdfDialog", json!({"path": "/tmp/marks.pdf"})).unwrap(); + set_field(&mut app, "__section", json!("Marks and Bleeds")); + set_field(&mut app, "bleed.useDocument", json!(true)); + frame(&mut app); + set_field(&mut app, "__section", json!("General")); + set_field(&mut app, "includeNonPrinting", json!(true)); + frame(&mut app); + let r = super::super::confirm(&mut app).unwrap(); + assert_eq!(r["warnings"], json!([]), "the bleed is applied"); + let bytes = &written.borrow()[0].1; + let page = &vectorcraft_pdf::info(bytes, None).unwrap().pages[0]; + let trim = page.boxes.iter().find(|(b, _)| *b == vectorcraft_pdf::CropTo::Trim).unwrap().1; + assert_eq!((trim.width(), trim.x0), (120.0, 9.0), "the trim box is the artboard, inside the bleed"); + let mut ellipse = false; + vectorcraft_pdf::import(bytes).unwrap().walk(|n| { + let b = n.path_data().and_then(|p| p.bounds()).unwrap_or_default(); + ellipse |= (b.width() - 20.0).abs() < 0.1 && (b.height() - 20.0).abs() < 0.1; + }); + assert!(ellipse, "the non-printing layer is included"); + } + #[test] fn option_labels_read_well() { assert_eq!(option_label("compression.color.abovePpi"), "Compression › Color › Above Ppi");