Support transparent page fill (#4586)

Co-authored-by: Martin Haug <mhaug@live.de>
This commit is contained in:
Laurenz 2024-07-20 14:51:24 +02:00 committed by GitHub
parent 3aa18beacf
commit 0c37a2c233
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 124 additions and 69 deletions

View File

@ -10,10 +10,9 @@ use parking_lot::RwLock;
use rayon::iter::{IntoParallelRefIterator, ParallelIterator}; use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
use typst::diag::{bail, Severity, SourceDiagnostic, StrResult, Warned}; use typst::diag::{bail, Severity, SourceDiagnostic, StrResult, Warned};
use typst::foundations::{Datetime, Smart}; use typst::foundations::{Datetime, Smart};
use typst::layout::{Frame, PageRanges}; use typst::layout::{Frame, Page, PageRanges};
use typst::model::Document; use typst::model::Document;
use typst::syntax::{FileId, Source, Span}; use typst::syntax::{FileId, Source, Span};
use typst::visualize::Color;
use typst::WorldExt; use typst::WorldExt;
use crate::args::{ use crate::args::{
@ -269,7 +268,7 @@ fn export_image(
Output::Stdout => Output::Stdout, Output::Stdout => Output::Stdout,
}; };
export_image_page(command, &page.frame, &output, fmt)?; export_image_page(command, page, &output, fmt)?;
Ok(()) Ok(())
}) })
.collect::<Result<Vec<()>, EcoString>>()?; .collect::<Result<Vec<()>, EcoString>>()?;
@ -309,13 +308,13 @@ mod output_template {
/// Export single image. /// Export single image.
fn export_image_page( fn export_image_page(
command: &CompileCommand, command: &CompileCommand,
frame: &Frame, page: &Page,
output: &Output, output: &Output,
fmt: ImageExportFormat, fmt: ImageExportFormat,
) -> StrResult<()> { ) -> StrResult<()> {
match fmt { match fmt {
ImageExportFormat::Png => { 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 let buf = pixmap
.encode_png() .encode_png()
.map_err(|err| eco_format!("failed to encode PNG file ({err})"))?; .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})"))?; .map_err(|err| eco_format!("failed to write PNG file ({err})"))?;
} }
ImageExportFormat::Svg => { ImageExportFormat::Svg => {
let svg = typst_svg::svg(frame); let svg = typst_svg::svg(page);
output output
.write(svg.as_bytes()) .write(svg.as_bytes())
.map_err(|err| eco_format!("failed to write SVG file ({err})"))?; .map_err(|err| eco_format!("failed to write SVG file ({err})"))?;

View File

@ -243,7 +243,7 @@ impl ColorFontMap<()> {
let width = let width =
font.advance(gid).unwrap_or(Em::new(0.0)).get() * font.units_per_em(); font.advance(gid).unwrap_or(Em::new(0.0)).get() * font.units_per_em();
let instructions = 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.glyphs.push(ColorGlyph { gid, instructions });
color_font.glyph_indices.insert(gid, index); color_font.glyph_indices.insert(gid, index);

View File

@ -36,6 +36,7 @@ use crate::{deflate_deferred, AbsExt, EmExt};
pub fn build( pub fn build(
resources: &mut Resources<()>, resources: &mut Resources<()>,
frame: &Frame, frame: &Frame,
fill: Option<Paint>,
color_glyph_width: Option<f32>, color_glyph_width: Option<f32>,
) -> Encoded { ) -> Encoded {
let size = frame.size(); let size = frame.size();
@ -53,6 +54,11 @@ pub fn build(
.post_concat(Transform::translate(Abs::zero(), size.y)), .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. // Encode the frame into the content stream.
write_frame(&mut ctx, frame); write_frame(&mut ctx, frame);

View File

@ -8,7 +8,7 @@ use pdf_writer::{
}; };
use typst::foundations::Label; use typst::foundations::Label;
use typst::introspection::Location; use typst::introspection::Location;
use typst::layout::{Abs, Frame}; use typst::layout::{Abs, Page};
use typst::model::{Destination, Numbering}; use typst::model::{Destination, Numbering};
use typst::text::Case; use typst::text::Case;
@ -33,7 +33,7 @@ pub fn traverse_pages(
pages.push(None); pages.push(None);
skipped_pages += 1; skipped_pages += 1;
} else { } else {
let mut encoded = construct_page(&mut resources, &page.frame); let mut encoded = construct_page(&mut resources, page);
encoded.label = page encoded.label = page
.numbering .numbering
.as_ref() .as_ref()
@ -60,9 +60,8 @@ pub fn traverse_pages(
/// Construct a page object. /// Construct a page object.
#[typst_macros::time(name = "construct page")] #[typst_macros::time(name = "construct page")]
fn construct_page(out: &mut Resources<()>, frame: &Frame) -> EncodedPage { fn construct_page(out: &mut Resources<()>, page: &Page) -> EncodedPage {
let content = content::build(out, frame, None); let content = content::build(out, &page.frame, page.fill_or_transparent(), None);
EncodedPage { content, label: None } EncodedPage { content, label: None }
} }

View File

@ -103,7 +103,7 @@ fn register_pattern(
}; };
// Render the body. // 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 { let pdf_pattern = PdfPattern {
transform, transform,

View File

@ -7,45 +7,49 @@ mod text;
use tiny_skia as sk; use tiny_skia as sk;
use typst::layout::{ 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::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. /// the resulting `tiny-skia` pixel buffer.
#[typst_macros::time(name = "render")] #[typst_macros::time(name = "render")]
pub fn render(frame: &Frame, pixel_per_pt: f32, fill: Color) -> sk::Pixmap { pub fn render(page: &Page, pixel_per_pt: f32) -> sk::Pixmap {
let size = frame.size(); let size = page.frame.size();
let pxw = (pixel_per_pt * size.x.to_f32()).round().max(1.0) as u32; 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 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); 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 canvas
} }
/// Export a document with potentially multiple pages into a single raster image. /// 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( pub fn render_merged(
document: &Document, document: &Document,
pixel_per_pt: f32, pixel_per_pt: f32,
frame_fill: Color,
gap: Abs, gap: Abs,
gap_fill: Color, fill: Option<Color>,
) -> sk::Pixmap { ) -> sk::Pixmap {
let pixmaps: Vec<_> = document let pixmaps: Vec<_> =
.pages document.pages.iter().map(|page| render(page, pixel_per_pt)).collect();
.iter()
.map(|page| render(&page.frame, pixel_per_pt, frame_fill))
.collect();
let gap = (pixel_per_pt * gap.to_f32()).round() as u32; let gap = (pixel_per_pt * gap.to_f32()).round() as u32;
let pxw = pixmaps.iter().map(sk::Pixmap::width).max().unwrap_or_default(); 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; + gap * pixmaps.len().saturating_sub(1) as u32;
let mut canvas = sk::Pixmap::new(pxw, pxh).unwrap(); 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; let mut y = 0;
for pixmap in pixmaps { for pixmap in pixmaps {

View File

@ -11,11 +11,11 @@ use std::fmt::{self, Display, Formatter, Write};
use ecow::EcoString; use ecow::EcoString;
use ttf_parser::OutlineBuilder; use ttf_parser::OutlineBuilder;
use typst::layout::{ 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::model::Document;
use typst::utils::hash128; use typst::utils::hash128;
use typst::visualize::{Gradient, Pattern}; use typst::visualize::{Geometry, Gradient, Pattern};
use xmlwriter::XmlWriter; use xmlwriter::XmlWriter;
use crate::paint::{GradientRef, PatternRef, SVGSubGradient}; use crate::paint::{GradientRef, PatternRef, SVGSubGradient};
@ -23,12 +23,12 @@ use crate::text::RenderedGlyph;
/// Export a frame into a SVG file. /// Export a frame into a SVG file.
#[typst_macros::time(name = "svg")] #[typst_macros::time(name = "svg")]
pub fn svg(frame: &Frame) -> String { pub fn svg(page: &Page) -> String {
let mut renderer = SVGRenderer::new(); let mut renderer = SVGRenderer::new();
renderer.write_header(frame.size()); renderer.write_header(page.frame.size());
let state = State::new(frame.size(), Transform::identity()); let state = State::new(page.frame.size(), Transform::identity());
renderer.render_frame(state, Transform::identity(), frame); renderer.render_page(state, Transform::identity(), page);
renderer.finalize() renderer.finalize()
} }
@ -57,7 +57,7 @@ pub fn svg_merged(document: &Document, padding: Abs) -> String {
for page in &document.pages { for page in &document.pages {
let ts = Transform::translate(x, y); let ts = Transform::translate(x, y);
let state = State::new(page.frame.size(), Transform::identity()); 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; y += page.frame.height() + padding;
} }
@ -176,6 +176,16 @@ impl SVGRenderer {
self.xml.write_attribute("xmlns:h5", "http://www.w3.org/1999/xhtml"); 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. /// Render a frame with the given transform.
fn render_frame(&mut self, state: State, ts: Transform, frame: &Frame) { fn render_frame(&mut self, state: State, ts: Transform, frame: &Frame) {
self.xml.start_element("g"); self.xml.start_element("g");

View File

@ -24,7 +24,7 @@ use crate::layout::{
use crate::model::Numbering; use crate::model::Numbering;
use crate::text::TextElem; use crate::text::TextElem;
use crate::utils::{NonZeroExt, Numeric, Scalar}; use crate::utils::{NonZeroExt, Numeric, Scalar};
use crate::visualize::Paint; use crate::visualize::{Color, Paint};
/// Layouts its child onto one or multiple pages. /// Layouts its child onto one or multiple pages.
/// ///
@ -178,12 +178,20 @@ pub struct PageElem {
#[default(NonZeroUsize::ONE)] #[default(NonZeroUsize::ONE)]
pub columns: NonZeroUsize, 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 /// Setting this to something non-transparent instructs the printer to color
/// color. If you are considering larger production runs, it may be more /// the complete page. If you are considering larger production runs, it may
/// environmentally friendly and cost-effective to source pre-dyed pages and /// be more environmentally friendly and cost-effective to source pre-dyed
/// not set this property. /// 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 /// ```example
/// #set page(fill: rgb("444352")) /// #set page(fill: rgb("444352"))
@ -191,7 +199,7 @@ pub struct PageElem {
/// *Dark mode enabled.* /// *Dark mode enabled.*
/// ``` /// ```
#[borrowed] #[borrowed]
pub fill: Option<Paint>, pub fill: Smart<Option<Paint>>,
/// How to [number]($numbering) the pages. /// 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)?; page_counter.visit(engine, &frame)?;
pages.push(Page { pages.push(Page {
frame, frame,
fill: fill.clone(),
numbering: numbering.clone(), numbering: numbering.clone(),
number: page_counter.logical(), number: page_counter.logical(),
}); });
@ -578,6 +583,15 @@ impl PageLayout<'_> {
pub struct Page { pub struct Page {
/// The frame that defines the page. /// The frame that defines the page.
pub frame: Frame, 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<Option<Paint>>,
/// The page's numbering. /// The page's numbering.
pub numbering: Option<Numbering>, pub numbering: Option<Numbering>,
/// The logical page number (controlled by `counter(page)` and may thus not /// The logical page number (controlled by `counter(page)` and may thus not
@ -585,6 +599,22 @@ pub struct Page {
pub number: usize, 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<Paint> {
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<Paint> {
self.fill.clone().unwrap_or_else(|| Some(Color::WHITE.into()))
}
}
/// Specification of the page's margins. /// Specification of the page's margins.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub struct Margin { pub struct Margin {

View File

@ -3,7 +3,6 @@ use std::path::{Path, PathBuf};
use clap::Parser; use clap::Parser;
use typst::model::Document; use typst::model::Document;
use typst::visualize::Color;
use typst_docs::{provide, Html, Resolver}; use typst_docs::{provide, Html, Resolver};
use typst_render::render; use typst_render::render;
@ -35,8 +34,8 @@ impl<'a> Resolver for CliResolver<'a> {
); );
} }
let frame = &document.pages.first().expect("page 0").frame; let page = document.pages.first().expect("page 0");
let pixmap = render(frame, 2.0, Color::WHITE); let pixmap = render(page, 2.0);
let filename = format!("{hash:x}.png"); let filename = format!("{hash:x}.png");
let path = self.assets_dir.join(&filename); let path = self.assets_dir.join(&filename);
fs::create_dir_all(path.parent().expect("parent")).expect("create dir"); fs::create_dir_all(path.parent().expect("parent")).expect("create dir");

View File

@ -6,7 +6,6 @@ use typst::foundations::{Bytes, Datetime};
use typst::syntax::{FileId, Source}; use typst::syntax::{FileId, Source};
use typst::text::{Font, FontBook}; use typst::text::{Font, FontBook};
use typst::utils::LazyHash; use typst::utils::LazyHash;
use typst::visualize::Color;
use typst::{Library, World}; use typst::{Library, World};
struct FuzzWorld { struct FuzzWorld {
@ -68,7 +67,7 @@ fuzz_target!(|text: &str| {
let world = FuzzWorld::new(text); let world = FuzzWorld::new(text);
if let Ok(document) = typst::compile(&world).output { if let Ok(document) = typst::compile(&world).output {
if let Some(page) = document.pages.first() { 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); comemo::evict(10);

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 423 B

After

Width:  |  Height:  |  Size: 474 B

View File

@ -359,13 +359,8 @@ fn render(document: &Document, pixel_per_pt: f32) -> sk::Pixmap {
} }
let gap = Abs::pt(1.0); let gap = Abs::pt(1.0);
let mut pixmap = typst_render::render_merged( let mut pixmap =
document, typst_render::render_merged(document, pixel_per_pt, gap, Some(Color::BLACK));
pixel_per_pt,
Color::WHITE,
gap,
Color::BLACK,
);
let gap = (pixel_per_pt * gap.to_pt() as f32).round(); let gap = (pixel_per_pt * gap.to_pt() as f32).round();

View File

@ -66,7 +66,13 @@
// Test page fill. // Test page fill.
#set page(width: 80pt, height: 40pt, fill: eastern) #set page(width: 80pt, height: 40pt, fill: eastern)
#text(15pt, font: "Roboto", fill: white, smallcaps[Typst]) #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 --- --- page-margin-uniform ---
// Set all margins at once. // Set all margins at once.

View File

@ -21,24 +21,30 @@
--- pattern-relative-self --- --- pattern-relative-self ---
// Test with relative set to `"self"` // Test with relative set to `"self"`
#let pat(..args) = pattern(size: (30pt, 30pt), ..args)[ #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%, 0%), end: (100%, 100%), stroke: 1pt))
#place(top + left, line(start: (0%, 100%), end: (100%, 0%), 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(), width: 100pt, height: 100pt)
#rect(
#rect(fill: pat(relative: "self"), width: 100%, height: 100%, stroke: 1pt) width: 100%,
height: 100%,
fill: pat(relative: "self"),
stroke: 1pt + green,
)
--- pattern-relative-parent --- --- pattern-relative-parent ---
// Test with relative set to `"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%, 0%), end: (100%, 100%), stroke: 1pt))
#place(top + left, line(start: (0%, 100%), end: (100%, 0%), 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 --- --- pattern-small ---
// Tests small patterns for pixel accuracy. // Tests small patterns for pixel accuracy.