diff --git a/crates/typst-layout/src/pages/finalize.rs b/crates/typst-layout/src/pages/finalize.rs index b16d95699..8e99fda13 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 content_origin = 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(content_origin, background); } if let Some(header) = header { - frame.push_frame(Point::with_x(margin.left), header); + frame.push_frame(content_origin + Point::with_x(margin.left), header); } // Add the inner contents. - frame.push_frame(Point::new(margin.left, margin.top), inner); + frame.push_frame(content_origin + 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(content_origin + 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..e7b018e75 100644 --- a/crates/typst-library/src/layout/page.rs +++ b/crates/typst-library/src/layout/page.rs @@ -148,6 +148,47 @@ pub struct PageElem { #[ghost] pub margin: Margin, + /// The page's bleed margin. + /// + /// The bleed is the area of content that extends beyond the final trimmed + /// size of the page. It ensures that no unprinted edges appear in the final + /// product, even if minor trimming misalignments occur. + /// + /// Accepted values: + /// + /// - `{auto}`: Sets the bleed to `0mm` on all sides. + /// - A single length: Applies the same bleed to all sides. + /// - A dictionary: Allows setting bleed values individually. The dictionary + /// may include the following keys, listed in order of precedence: + /// - `top`: Bleed at the top of the page. + /// - `right`: Bleed at the right side. + /// - `bottom`: Bleed at the bottom. + /// - `left`: Bleed at the left side. + /// - `inside`: Bleed on the inner side of the page (next to + /// [binding]($page.binding)). + /// - `outside`: Bleed on the outer side of the page (opposite the + /// [binding]($page.binding)). + /// - `x`: Horizontal bleed (applies to both left/right or inside/outside). + /// - `y`: Vertical bleed (applies to both top and bottom). + /// - `rest`: Default bleed for any sides not explicitly set. + /// + /// Note: The keys `left` and `right` are mutually exclusive with `inside` and + /// `outside`. + /// + /// On PDF output, if the bleed is non-zero, a `TrimBox` and a `BleedBox` are + /// defined 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 +508,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")