summaryrefslogtreecommitdiff
path: root/crates
diff options
context:
space:
mode:
authorLaurenz <laurmaedje@gmail.com>2024-07-20 14:51:24 +0200
committerGitHub <noreply@github.com>2024-07-20 12:51:24 +0000
commit0c37a2c2334afb6947f265e00bded0fe75be6434 (patch)
treec6dd66554f1f9cadc1f43f386639c05f3aa6bf3e /crates
parent3aa18beacf84e8e982a1cb28170d281769c06dd0 (diff)
Support transparent page fill (#4586)
Co-authored-by: Martin Haug <mhaug@live.de>
Diffstat (limited to 'crates')
-rw-r--r--crates/typst-cli/src/compile.rs11
-rw-r--r--crates/typst-pdf/src/color_font.rs2
-rw-r--r--crates/typst-pdf/src/content.rs6
-rw-r--r--crates/typst-pdf/src/page.rs9
-rw-r--r--crates/typst-pdf/src/pattern.rs2
-rw-r--r--crates/typst-render/src/lib.rs44
-rw-r--r--crates/typst-svg/src/lib.rs24
-rw-r--r--crates/typst/src/layout/page.rs52
8 files changed, 100 insertions, 50 deletions
diff --git a/crates/typst-cli/src/compile.rs b/crates/typst-cli/src/compile.rs
index d1526425..cc85d920 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::<Result<Vec<()>, 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 201915b1..4889d915 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 da9e4ed4..d9830e43 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<Paint>,
color_glyph_width: Option<f32>,
) -> 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 2983f504..b07490cc 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 e06c04f8..d4f5a6e0 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 305dcd1f..d5eeacce 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 ts = sk::Transform::from_scale(pixel_per_pt, pixel_per_pt);
+ let state = State::new(size, ts, pixel_per_pt);
+
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);
+ 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<Color>,
) -> 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 01ed3fae..145e23f8 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 cf898917..ca2a0ce9 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<Paint>,
+ pub fill: Smart<Option<Paint>>,
/// 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<Option<Paint>>,
/// The page's numbering.
pub numbering: Option<Numbering>,
/// 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<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.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub struct Margin {