diff --git a/crates/typst-cli/src/compile.rs b/crates/typst-cli/src/compile.rs index d15264255..cc85d9209 100644 --- a/crates/typst-cli/src/compile.rs +++ b/crates/typst-cli/src/compile.rs @@ -10,10 +10,9 @@ use parking_lot::RwLock; use rayon::iter::{IntoParallelRefIterator, ParallelIterator}; use typst::diag::{bail, Severity, SourceDiagnostic, StrResult, Warned}; use typst::foundations::{Datetime, Smart}; -use typst::layout::{Frame, PageRanges}; +use typst::layout::{Frame, Page, PageRanges}; use typst::model::Document; use typst::syntax::{FileId, Source, Span}; -use typst::visualize::Color; use typst::WorldExt; use crate::args::{ @@ -269,7 +268,7 @@ fn export_image( Output::Stdout => Output::Stdout, }; - export_image_page(command, &page.frame, &output, fmt)?; + export_image_page(command, page, &output, fmt)?; Ok(()) }) .collect::, EcoString>>()?; @@ -309,13 +308,13 @@ mod output_template { /// Export single image. fn export_image_page( command: &CompileCommand, - frame: &Frame, + page: &Page, output: &Output, fmt: ImageExportFormat, ) -> StrResult<()> { match fmt { ImageExportFormat::Png => { - let pixmap = typst_render::render(frame, command.ppi / 72.0, Color::WHITE); + let pixmap = typst_render::render(page, command.ppi / 72.0); let buf = pixmap .encode_png() .map_err(|err| eco_format!("failed to encode PNG file ({err})"))?; @@ -324,7 +323,7 @@ fn export_image_page( .map_err(|err| eco_format!("failed to write PNG file ({err})"))?; } ImageExportFormat::Svg => { - let svg = typst_svg::svg(frame); + let svg = typst_svg::svg(page); output .write(svg.as_bytes()) .map_err(|err| eco_format!("failed to write SVG file ({err})"))?; diff --git a/crates/typst-pdf/src/color_font.rs b/crates/typst-pdf/src/color_font.rs index 201915b19..4889d9151 100644 --- a/crates/typst-pdf/src/color_font.rs +++ b/crates/typst-pdf/src/color_font.rs @@ -243,7 +243,7 @@ impl ColorFontMap<()> { let width = font.advance(gid).unwrap_or(Em::new(0.0)).get() * font.units_per_em(); let instructions = - content::build(&mut self.resources, &frame, Some(width as f32)); + content::build(&mut self.resources, &frame, None, Some(width as f32)); color_font.glyphs.push(ColorGlyph { gid, instructions }); color_font.glyph_indices.insert(gid, index); diff --git a/crates/typst-pdf/src/content.rs b/crates/typst-pdf/src/content.rs index da9e4ed44..d9830e439 100644 --- a/crates/typst-pdf/src/content.rs +++ b/crates/typst-pdf/src/content.rs @@ -36,6 +36,7 @@ use crate::{deflate_deferred, AbsExt, EmExt}; pub fn build( resources: &mut Resources<()>, frame: &Frame, + fill: Option, color_glyph_width: Option, ) -> Encoded { let size = frame.size(); @@ -53,6 +54,11 @@ pub fn build( .post_concat(Transform::translate(Abs::zero(), size.y)), ); + if let Some(fill) = fill { + let shape = Geometry::Rect(frame.size()).filled(fill); + write_shape(&mut ctx, Point::zero(), &shape); + } + // Encode the frame into the content stream. write_frame(&mut ctx, frame); diff --git a/crates/typst-pdf/src/page.rs b/crates/typst-pdf/src/page.rs index 2983f504f..b07490cc0 100644 --- a/crates/typst-pdf/src/page.rs +++ b/crates/typst-pdf/src/page.rs @@ -8,7 +8,7 @@ use pdf_writer::{ }; use typst::foundations::Label; use typst::introspection::Location; -use typst::layout::{Abs, Frame}; +use typst::layout::{Abs, Page}; use typst::model::{Destination, Numbering}; use typst::text::Case; @@ -33,7 +33,7 @@ pub fn traverse_pages( pages.push(None); skipped_pages += 1; } else { - let mut encoded = construct_page(&mut resources, &page.frame); + let mut encoded = construct_page(&mut resources, page); encoded.label = page .numbering .as_ref() @@ -60,9 +60,8 @@ pub fn traverse_pages( /// Construct a page object. #[typst_macros::time(name = "construct page")] -fn construct_page(out: &mut Resources<()>, frame: &Frame) -> EncodedPage { - let content = content::build(out, frame, None); - +fn construct_page(out: &mut Resources<()>, page: &Page) -> EncodedPage { + let content = content::build(out, &page.frame, page.fill_or_transparent(), None); EncodedPage { content, label: None } } diff --git a/crates/typst-pdf/src/pattern.rs b/crates/typst-pdf/src/pattern.rs index e06c04f87..d4f5a6e08 100644 --- a/crates/typst-pdf/src/pattern.rs +++ b/crates/typst-pdf/src/pattern.rs @@ -103,7 +103,7 @@ fn register_pattern( }; // Render the body. - let content = content::build(&mut patterns.resources, pattern.frame(), None); + let content = content::build(&mut patterns.resources, pattern.frame(), None, None); let pdf_pattern = PdfPattern { transform, diff --git a/crates/typst-render/src/lib.rs b/crates/typst-render/src/lib.rs index 305dcd1fc..d5eeacce8 100644 --- a/crates/typst-render/src/lib.rs +++ b/crates/typst-render/src/lib.rs @@ -7,45 +7,49 @@ mod text; use tiny_skia as sk; use typst::layout::{ - Abs, Axes, Frame, FrameItem, FrameKind, GroupItem, Point, Size, Transform, + Abs, Axes, Frame, FrameItem, FrameKind, GroupItem, Page, Point, Size, Transform, }; use typst::model::Document; -use typst::visualize::Color; +use typst::visualize::{Color, Geometry, Paint}; -/// Export a frame into a raster image. +/// Export a page into a raster image. /// -/// This renders the frame at the given number of pixels per point and returns +/// This renders the page at the given number of pixels per point and returns /// the resulting `tiny-skia` pixel buffer. #[typst_macros::time(name = "render")] -pub fn render(frame: &Frame, pixel_per_pt: f32, fill: Color) -> sk::Pixmap { - let size = frame.size(); +pub fn render(page: &Page, pixel_per_pt: f32) -> sk::Pixmap { + let size = page.frame.size(); let pxw = (pixel_per_pt * size.x.to_f32()).round().max(1.0) as u32; let pxh = (pixel_per_pt * size.y.to_f32()).round().max(1.0) as u32; - let mut canvas = sk::Pixmap::new(pxw, pxh).unwrap(); - canvas.fill(paint::to_sk_color(fill)); - let ts = sk::Transform::from_scale(pixel_per_pt, pixel_per_pt); - render_frame(&mut canvas, State::new(size, ts, pixel_per_pt), frame); + let state = State::new(size, ts, pixel_per_pt); + + let mut canvas = sk::Pixmap::new(pxw, pxh).unwrap(); + + if let Some(fill) = page.fill_or_white() { + if let Paint::Solid(color) = fill { + canvas.fill(paint::to_sk_color(color)); + } else { + let rect = Geometry::Rect(page.frame.size()).filled(fill); + shape::render_shape(&mut canvas, state, &rect); + } + } + + render_frame(&mut canvas, state, &page.frame); canvas } /// Export a document with potentially multiple pages into a single raster image. -/// -/// The gap will be added between the individual frames. pub fn render_merged( document: &Document, pixel_per_pt: f32, - frame_fill: Color, gap: Abs, - gap_fill: Color, + fill: Option, ) -> sk::Pixmap { - let pixmaps: Vec<_> = document - .pages - .iter() - .map(|page| render(&page.frame, pixel_per_pt, frame_fill)) - .collect(); + let pixmaps: Vec<_> = + document.pages.iter().map(|page| render(page, pixel_per_pt)).collect(); let gap = (pixel_per_pt * gap.to_f32()).round() as u32; let pxw = pixmaps.iter().map(sk::Pixmap::width).max().unwrap_or_default(); @@ -53,7 +57,9 @@ pub fn render_merged( + gap * pixmaps.len().saturating_sub(1) as u32; let mut canvas = sk::Pixmap::new(pxw, pxh).unwrap(); - canvas.fill(paint::to_sk_color(gap_fill)); + if let Some(fill) = fill { + canvas.fill(paint::to_sk_color(fill)); + } let mut y = 0; for pixmap in pixmaps { diff --git a/crates/typst-svg/src/lib.rs b/crates/typst-svg/src/lib.rs index 01ed3faee..145e23f86 100644 --- a/crates/typst-svg/src/lib.rs +++ b/crates/typst-svg/src/lib.rs @@ -11,11 +11,11 @@ use std::fmt::{self, Display, Formatter, Write}; use ecow::EcoString; use ttf_parser::OutlineBuilder; use typst::layout::{ - Abs, Frame, FrameItem, FrameKind, GroupItem, Point, Ratio, Size, Transform, + Abs, Frame, FrameItem, FrameKind, GroupItem, Page, Point, Ratio, Size, Transform, }; use typst::model::Document; use typst::utils::hash128; -use typst::visualize::{Gradient, Pattern}; +use typst::visualize::{Geometry, Gradient, Pattern}; use xmlwriter::XmlWriter; use crate::paint::{GradientRef, PatternRef, SVGSubGradient}; @@ -23,12 +23,12 @@ use crate::text::RenderedGlyph; /// Export a frame into a SVG file. #[typst_macros::time(name = "svg")] -pub fn svg(frame: &Frame) -> String { +pub fn svg(page: &Page) -> String { let mut renderer = SVGRenderer::new(); - renderer.write_header(frame.size()); + renderer.write_header(page.frame.size()); - let state = State::new(frame.size(), Transform::identity()); - renderer.render_frame(state, Transform::identity(), frame); + let state = State::new(page.frame.size(), Transform::identity()); + renderer.render_page(state, Transform::identity(), page); renderer.finalize() } @@ -57,7 +57,7 @@ pub fn svg_merged(document: &Document, padding: Abs) -> String { for page in &document.pages { let ts = Transform::translate(x, y); let state = State::new(page.frame.size(), Transform::identity()); - renderer.render_frame(state, ts, &page.frame); + renderer.render_page(state, ts, page); y += page.frame.height() + padding; } @@ -176,6 +176,16 @@ impl SVGRenderer { self.xml.write_attribute("xmlns:h5", "http://www.w3.org/1999/xhtml"); } + /// Render a page with the given transform. + fn render_page(&mut self, state: State, ts: Transform, page: &Page) { + if let Some(fill) = page.fill_or_white() { + let shape = Geometry::Rect(page.frame.size()).filled(fill); + self.render_shape(state, &shape); + } + + self.render_frame(state, ts, &page.frame); + } + /// Render a frame with the given transform. fn render_frame(&mut self, state: State, ts: Transform, frame: &Frame) { self.xml.start_element("g"); diff --git a/crates/typst/src/layout/page.rs b/crates/typst/src/layout/page.rs index cf8989175..ca2a0ce91 100644 --- a/crates/typst/src/layout/page.rs +++ b/crates/typst/src/layout/page.rs @@ -24,7 +24,7 @@ use crate::layout::{ use crate::model::Numbering; use crate::text::TextElem; use crate::utils::{NonZeroExt, Numeric, Scalar}; -use crate::visualize::Paint; +use crate::visualize::{Color, Paint}; /// Layouts its child onto one or multiple pages. /// @@ -178,12 +178,20 @@ pub struct PageElem { #[default(NonZeroUsize::ONE)] pub columns: NonZeroUsize, - /// The page's background color. + /// The page's background fill. /// - /// This instructs the printer to color the complete page with the given - /// color. If you are considering larger production runs, it may be more - /// environmentally friendly and cost-effective to source pre-dyed pages and - /// not set this property. + /// Setting this to something non-transparent instructs the printer to color + /// the complete page. If you are considering larger production runs, it may + /// be more environmentally friendly and cost-effective to source pre-dyed + /// pages and not set this property. + /// + /// When set to `{none}`, the background becomes transparent. Note that PDF + /// pages will still appear with a (usually white) background in viewers, + /// but they are conceptually transparent. (If you print them, no color is + /// used for the background.) + /// + /// The default of `{auto}` results in `{none}` for PDF output, and + /// `{white}` for PNG and SVG. /// /// ```example /// #set page(fill: rgb("444352")) @@ -191,7 +199,7 @@ pub struct PageElem { /// *Dark mode enabled.* /// ``` #[borrowed] - pub fill: Option, + pub fill: Smart>, /// How to [number]($numbering) the pages. /// @@ -555,13 +563,10 @@ impl PageLayout<'_> { } } - if let Some(fill) = fill { - frame.fill(fill.clone()); - } - page_counter.visit(engine, &frame)?; pages.push(Page { frame, + fill: fill.clone(), numbering: numbering.clone(), number: page_counter.logical(), }); @@ -578,6 +583,15 @@ impl PageLayout<'_> { pub struct Page { /// The frame that defines the page. pub frame: Frame, + /// How the page is filled. + /// + /// - When `None`, the background is transparent. + /// - When `Auto`, the background is transparent for PDF and white + /// for raster and SVG targets. + /// + /// Exporters should access the resolved value of this property through + /// `fill_or_transparent()` or `fill_or_white()`. + pub fill: Smart>, /// The page's numbering. pub numbering: Option, /// The logical page number (controlled by `counter(page)` and may thus not @@ -585,6 +599,22 @@ pub struct Page { pub number: usize, } +impl Page { + /// Get the configured background or `None` if it is `Auto`. + /// + /// This is used in PDF export. + pub fn fill_or_transparent(&self) -> Option { + self.fill.clone().unwrap_or(None) + } + + /// Get the configured background or white if it is `Auto`. + /// + /// This is used in raster and SVG export. + pub fn fill_or_white(&self) -> Option { + self.fill.clone().unwrap_or_else(|| Some(Color::WHITE.into())) + } +} + /// Specification of the page's margins. #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] pub struct Margin { diff --git a/docs/src/main.rs b/docs/src/main.rs index f4414b109..3f53dfc84 100644 --- a/docs/src/main.rs +++ b/docs/src/main.rs @@ -3,7 +3,6 @@ use std::path::{Path, PathBuf}; use clap::Parser; use typst::model::Document; -use typst::visualize::Color; use typst_docs::{provide, Html, Resolver}; use typst_render::render; @@ -35,8 +34,8 @@ impl<'a> Resolver for CliResolver<'a> { ); } - let frame = &document.pages.first().expect("page 0").frame; - let pixmap = render(frame, 2.0, Color::WHITE); + let page = document.pages.first().expect("page 0"); + let pixmap = render(page, 2.0); let filename = format!("{hash:x}.png"); let path = self.assets_dir.join(&filename); fs::create_dir_all(path.parent().expect("parent")).expect("create dir"); diff --git a/tests/fuzz/src/compile.rs b/tests/fuzz/src/compile.rs index c9536150f..fa9397814 100644 --- a/tests/fuzz/src/compile.rs +++ b/tests/fuzz/src/compile.rs @@ -6,7 +6,6 @@ use typst::foundations::{Bytes, Datetime}; use typst::syntax::{FileId, Source}; use typst::text::{Font, FontBook}; use typst::utils::LazyHash; -use typst::visualize::Color; use typst::{Library, World}; struct FuzzWorld { @@ -68,7 +67,7 @@ fuzz_target!(|text: &str| { let world = FuzzWorld::new(text); if let Ok(document) = typst::compile(&world).output { if let Some(page) = document.pages.first() { - std::hint::black_box(typst_render::render(&page.frame, 1.0, Color::WHITE)); + std::hint::black_box(typst_render::render(page, 1.0)); } } comemo::evict(10); diff --git a/tests/ref/page-fill-none.png b/tests/ref/page-fill-none.png new file mode 100644 index 000000000..d225135f4 Binary files /dev/null and b/tests/ref/page-fill-none.png differ diff --git a/tests/ref/pattern-relative-self.png b/tests/ref/pattern-relative-self.png index 284080811..617e7dd6f 100644 Binary files a/tests/ref/pattern-relative-self.png and b/tests/ref/pattern-relative-self.png differ diff --git a/tests/src/run.rs b/tests/src/run.rs index 3db03ba43..9681ae4cc 100644 --- a/tests/src/run.rs +++ b/tests/src/run.rs @@ -359,13 +359,8 @@ fn render(document: &Document, pixel_per_pt: f32) -> sk::Pixmap { } let gap = Abs::pt(1.0); - let mut pixmap = typst_render::render_merged( - document, - pixel_per_pt, - Color::WHITE, - gap, - Color::BLACK, - ); + let mut pixmap = + typst_render::render_merged(document, pixel_per_pt, gap, Some(Color::BLACK)); let gap = (pixel_per_pt * gap.to_pt() as f32).round(); diff --git a/tests/suite/layout/page.typ b/tests/suite/layout/page.typ index f833af59b..0e1f77295 100644 --- a/tests/suite/layout/page.typ +++ b/tests/suite/layout/page.typ @@ -66,7 +66,13 @@ // Test page fill. #set page(width: 80pt, height: 40pt, fill: eastern) #text(15pt, font: "Roboto", fill: white, smallcaps[Typst]) -#page(width: 40pt, fill: none, margin: (top: 10pt, rest: auto))[Hi] +#page(width: 40pt, fill: auto, margin: (top: 10pt, rest: auto))[Hi] + +--- page-fill-none --- +// Test disabling page fill. +// The PNG is filled with black anyway due to the test runner. +#set page(fill: none) +#rect(fill: green) --- page-margin-uniform --- // Set all margins at once. diff --git a/tests/suite/visualize/pattern.typ b/tests/suite/visualize/pattern.typ index 08051ed20..b0c92efaf 100644 --- a/tests/suite/visualize/pattern.typ +++ b/tests/suite/visualize/pattern.typ @@ -21,24 +21,30 @@ --- pattern-relative-self --- // Test with relative set to `"self"` #let pat(..args) = pattern(size: (30pt, 30pt), ..args)[ + #set line(stroke: green) #place(top + left, line(start: (0%, 0%), end: (100%, 100%), stroke: 1pt)) #place(top + left, line(start: (0%, 100%), end: (100%, 0%), stroke: 1pt)) ] #set page(fill: pat(), width: 100pt, height: 100pt) - -#rect(fill: pat(relative: "self"), width: 100%, height: 100%, stroke: 1pt) +#rect( + width: 100%, + height: 100%, + fill: pat(relative: "self"), + stroke: 1pt + green, +) --- pattern-relative-parent --- // Test with relative set to `"parent"` -#let pat(..args) = pattern(size: (30pt, 30pt), ..args)[ +#let pat(fill, ..args) = pattern(size: (30pt, 30pt), ..args)[ + #rect(width: 100%, height: 100%, fill: fill, stroke: none) #place(top + left, line(start: (0%, 0%), end: (100%, 100%), stroke: 1pt)) #place(top + left, line(start: (0%, 100%), end: (100%, 0%), stroke: 1pt)) ] -#set page(fill: pat(), width: 100pt, height: 100pt) +#set page(fill: pat(white), width: 100pt, height: 100pt) -#rect(fill: pat(relative: "parent"), width: 100%, height: 100%, stroke: 1pt) +#rect(fill: pat(none, relative: "parent"), width: 100%, height: 100%, stroke: 1pt) --- pattern-small --- // Tests small patterns for pixel accuracy.