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 000000000..930c4f718 Binary files /dev/null and b/tests/ref/page-bleed-content-bleeding.png differ diff --git a/tests/ref/page-bleed.png b/tests/ref/page-bleed.png new file mode 100644 index 000000000..c2f69a8da Binary files /dev/null and b/tests/ref/page-bleed.png differ 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")