From 6094516a63e757f745846b1bbf03b79c69245234 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gabriel=20Ara=C3=BAjo?= Date: Fri, 30 May 2025 23:31:45 -0300 Subject: [PATCH] Add bleed support to page layout --- crates/typst-layout/src/pages/finalize.rs | 20 ++++++----- crates/typst-layout/src/pages/run.rs | 8 +++++ crates/typst-library/src/layout/page.rs | 42 ++++++++++++++++++++++ crates/typst-pdf/src/convert.rs | 20 +++++++++-- tests/ref/page-bleed-content-bleeding.png | Bin 0 -> 93 bytes tests/ref/page-bleed.png | Bin 0 -> 111 bytes tests/suite/layout/page.typ | 20 +++++++++++ 7 files changed, 100 insertions(+), 10 deletions(-) create mode 100644 tests/ref/page-bleed-content-bleeding.png create mode 100644 tests/ref/page-bleed.png diff --git a/crates/typst-layout/src/pages/finalize.rs b/crates/typst-layout/src/pages/finalize.rs index b16d95699..3b570832a 100644 --- a/crates/typst-layout/src/pages/finalize.rs +++ b/crates/typst-layout/src/pages/finalize.rs @@ -15,6 +15,7 @@ pub fn finalize( LayoutedPage { inner, mut margin, + bleed, binding, two_sided, header, @@ -34,33 +35,36 @@ pub fn finalize( } // Create a frame for the full page. - let mut frame = Frame::hard(inner.size() + margin.sum_by_axis()); + let mut frame = + Frame::hard(inner.size() + margin.sum_by_axis() + bleed.sum_by_axis()); // Add tags. for tag in tags.drain(..) { frame.push(Point::zero(), FrameItem::Tag(tag)); } + let bleed_start = Point::new(bleed.left, bleed.top); + // Add the "before" marginals. The order in which we push things here is // important as it affects the relative ordering of introspectable elements // and thus how counters resolve. if let Some(background) = background { - frame.push_frame(Point::zero(), background); + frame.push_frame(bleed_start, background); } if let Some(header) = header { - frame.push_frame(Point::with_x(margin.left), header); + frame.push_frame(bleed_start + Point::with_x(margin.left), header); } // Add the inner contents. - frame.push_frame(Point::new(margin.left, margin.top), inner); + frame.push_frame(bleed_start + Point::new(margin.left, margin.top), inner); // Add the "after" marginals. if let Some(footer) = footer { - let y = frame.height() - footer.height(); - frame.push_frame(Point::new(margin.left, y), footer); + let y = frame.height() - footer.height() - bleed.bottom; + frame.push_frame(Point::new(margin.left + bleed.left, y), footer); } if let Some(foreground) = foreground { - frame.push_frame(Point::zero(), foreground); + frame.push_frame(bleed_start + Point::zero(), foreground); } // Apply counter updates from within the page to the manual page counter. @@ -70,5 +74,5 @@ pub fn finalize( let number = counter.logical(); counter.step(); - Ok(Page { frame, fill, numbering, supplement, number }) + Ok(Page { frame, bleed, fill, numbering, supplement, number }) } diff --git a/crates/typst-layout/src/pages/run.rs b/crates/typst-layout/src/pages/run.rs index 6d2d29da5..0f02501e6 100644 --- a/crates/typst-layout/src/pages/run.rs +++ b/crates/typst-layout/src/pages/run.rs @@ -28,6 +28,7 @@ use crate::flow::{layout_flow, FlowMode}; pub struct LayoutedPage { pub inner: Frame, pub margin: Sides, + pub bleed: Sides, pub binding: Binding, pub two_sided: bool, pub header: Option, @@ -123,6 +124,12 @@ fn layout_page_run_impl( .resolve(styles) .relative_to(size); + let bleed = PageElem::bleed_in(styles) + .sides + .map(|side| side.and_then(Smart::custom).unwrap_or(Rel::zero())) + .resolve(styles) + .relative_to(size); + let fill = PageElem::fill_in(styles); let foreground = PageElem::foreground_in(styles); let background = PageElem::background_in(styles); @@ -215,6 +222,7 @@ fn layout_page_run_impl( background: layout_marginal(background, full_size, mid)?, foreground: layout_marginal(foreground, full_size, mid)?, margin, + bleed, binding, two_sided, }); diff --git a/crates/typst-library/src/layout/page.rs b/crates/typst-library/src/layout/page.rs index 98afbd06f..674611543 100644 --- a/crates/typst-library/src/layout/page.rs +++ b/crates/typst-library/src/layout/page.rs @@ -148,6 +148,46 @@ pub struct PageElem { #[ghost] pub margin: Margin, + /// The page's bleed margin. + /// + /// The bleed is a part of the content that extends beyond the edge of the + /// final trimmed page. It ensures that there are no unprinted edges in the + /// final product, even if there's a slight misalignment during trimming. + /// + /// - `{auto}`: The bleed is set to 0mm on each side. + /// - A single length: The same bleed on all sides. + /// - A dictionary: With a dictionary, the bleed can be set individually. + /// The dictionary can contain the following keys in order of precedence: + /// - `top`: The top bleed. + /// - `right`: The right bleed. + /// - `bottom`: The bottom bleed. + /// - `left`: The left bleed. + /// - `inside`: The bleed at the inner side of the page (where the + /// [binding]($page.binding) is). + /// - `outside`: The bleed at the outer side of the page (opposite to the + /// [binding]($page.binding)). + /// - `x`: The horizontal bleeds. + /// - `y`: The vertical bleeds. + /// - `rest`: The bleeds on all sides except those for which the + /// dictionary explicitly sets a size. + /// + /// The values for `left` and `right` are mutually exclusive with + /// the values for `inside` and `outside`. + /// + /// On PDF output, if bleed is different of zero, it sets a TrimBox and a + /// BleedBox for the page. + /// + /// ```example + /// #set page( + /// width: 3cm, + /// height: 4cm, + /// bleed: 5mm, + /// background: rect(width: 100%, height: 100%, fill: aqua), + /// ) + /// ``` + #[ghost] + pub bleed: Margin, + /// On which side the pages will be bound. /// /// - `{auto}`: Equivalent to `left` if the [text direction]($text.dir) @@ -467,6 +507,8 @@ pub struct PagedDocument { pub struct Page { /// The frame that defines the page. pub frame: Frame, + /// The bleed amount to be added on each side of the page. + pub bleed: Sides, /// How the page is filled. /// /// - When `None`, the background is transparent. diff --git a/crates/typst-pdf/src/convert.rs b/crates/typst-pdf/src/convert.rs index 645d56f11..66d3fb74e 100644 --- a/crates/typst-pdf/src/convert.rs +++ b/crates/typst-pdf/src/convert.rs @@ -7,7 +7,7 @@ use krilla::configure::{Configuration, ValidationError, Validator}; use krilla::destination::{NamedDestination, XyzDestination}; use krilla::embed::EmbedError; use krilla::error::KrillaError; -use krilla::geom::PathBuilder; +use krilla::geom::{PathBuilder, Rect}; use krilla::page::{PageLabel, PageSettings}; use krilla::surface::Surface; use krilla::{Document, SerializeSettings}; @@ -16,7 +16,7 @@ use typst_library::diag::{bail, error, SourceDiagnostic, SourceResult}; use typst_library::foundations::{NativeElement, Repr}; use typst_library::introspection::Location; use typst_library::layout::{ - Abs, Frame, FrameItem, GroupItem, PagedDocument, Size, Transform, + Abs, Frame, FrameItem, GroupItem, PagedDocument, Sides, Size, Transform, }; use typst_library::model::HeadingElem; use typst_library::text::{Font, Lang}; @@ -81,6 +81,22 @@ fn convert_pages(gc: &mut GlobalContext, document: &mut Document) -> SourceResul typst_page.frame.height().to_f32(), ); + if typst_page.bleed != Sides::splat(Abs::zero()) { + settings = settings + .with_bleed_box(Rect::from_xywh( + 0.0, + 0.0, + typst_page.frame.width().to_f32(), + typst_page.frame.height().to_f32(), + )) + .with_trim_box(Rect::from_ltrb( + typst_page.bleed.left.to_f32(), + typst_page.bleed.top.to_f32(), + (typst_page.frame.width() - typst_page.bleed.right).to_f32(), + (typst_page.frame.height() - typst_page.bleed.bottom).to_f32(), + )); + } + if let Some(label) = typst_page .numbering .as_ref() diff --git a/tests/ref/page-bleed-content-bleeding.png b/tests/ref/page-bleed-content-bleeding.png new file mode 100644 index 0000000000000000000000000000000000000000..930c4f718a59179fb410ebc0581ae3b2634762e5 GIT binary patch literal 93 zcmeAS@N?(olHy`uVBq!ia0vp^6(GzABp3wcx$A+Hil>WXNX4z>AN^bsY_k6un3)Xv pKe_*0{3r0AM*S3jnfeLW^BE@XwG01a6;lAx=jrO_vd$@?2>=9j8wLOX literal 0 HcmV?d00001 diff --git a/tests/ref/page-bleed.png b/tests/ref/page-bleed.png new file mode 100644 index 0000000000000000000000000000000000000000..c2f69a8dad11680787ae6c62c9a7e4490c3cc95a GIT binary patch literal 111 zcmeAS@N?(olHy`uVBq!ia0vp^6(Gz6BpAZ>7p@0VW}YsNAr-gYUfamYpdi3<@b!L? z*;meZ^n7`kXg2rHjx*T@f9Uo*OVmGpV{mMT?Cs*1Q*~N;Pg#G(JYBwP+Iee`NuI8L JF6*2UngG9XDlGs2 literal 0 HcmV?d00001 diff --git a/tests/suite/layout/page.typ b/tests/suite/layout/page.typ index 4df9f9cac..315777f80 100644 --- a/tests/suite/layout/page.typ +++ b/tests/suite/layout/page.typ @@ -348,6 +348,26 @@ A A ] +--- page-bleed --- +#set page( + bleed: 20pt, + margin: 20pt, + height: 80pt, + width: 80pt, + background: rect(width: 100%, height: 100%, fill: gray), +) +#rect(width: 100%, height: 100%, fill: black) + +--- page-bleed-content-bleeding --- +#set page( + bleed: 20pt, + margin: 20pt, + height: 80pt, + width: 80pt, +) +#set align(center + horizon) +#rect(width: 100pt, height: 100pt, fill: black) + --- issue-2631-page-header-ordering --- #set text(6pt) #show heading: set text(6pt, weight: "regular")