summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSébastien d'Herbais de Thun <sebastien.d.herbais@gmail.com>2023-11-24 15:46:20 +0100
committerGitHub <noreply@github.com>2023-11-24 15:46:20 +0100
commit1756718bab3055597723a9b433419ff07e6b7f02 (patch)
treea06a7a381c994d762c298ec66903db0718877806
parent3d2f1d2d6cc34fa64c56abd335dd14ea4c932a6c (diff)
Gradient Part 6 - Pattern fills (#2740)
-rw-r--r--crates/typst-pdf/src/color.rs2
-rw-r--r--crates/typst-pdf/src/gradient.rs43
-rw-r--r--crates/typst-pdf/src/lib.rs34
-rw-r--r--crates/typst-pdf/src/page.rs113
-rw-r--r--crates/typst-pdf/src/pattern.rs154
-rw-r--r--crates/typst-render/src/lib.rs151
-rw-r--r--crates/typst-svg/src/lib.rs296
-rw-r--r--crates/typst/src/eval/ops.rs9
-rw-r--r--crates/typst/src/foundations/value.rs9
-rw-r--r--crates/typst/src/layout/axes.rs12
-rw-r--r--crates/typst/src/text/mod.rs20
-rw-r--r--crates/typst/src/visualize/gradient.rs24
-rw-r--r--crates/typst/src/visualize/mod.rs3
-rw-r--r--crates/typst/src/visualize/paint.rs34
-rw-r--r--crates/typst/src/visualize/pattern.rs288
-rw-r--r--crates/typst/src/visualize/stroke.rs6
-rw-r--r--tests/ref/visualize/pattern-relative.pngbin0 -> 1548 bytes
-rw-r--r--tests/ref/visualize/pattern-small.pngbin0 -> 106 bytes
-rw-r--r--tests/ref/visualize/pattern-spacing.pngbin0 -> 307 bytes
-rw-r--r--tests/ref/visualize/pattern-stroke.pngbin0 -> 352 bytes
-rw-r--r--tests/ref/visualize/pattern-text.pngbin0 -> 29319 bytes
-rw-r--r--tests/typ/layout/table.typ2
-rw-r--r--tests/typ/visualize/gradient-text.typ2
-rw-r--r--tests/typ/visualize/pattern-relative.typ23
-rw-r--r--tests/typ/visualize/pattern-small.typ14
-rw-r--r--tests/typ/visualize/pattern-spacing.typ31
-rw-r--r--tests/typ/visualize/pattern-stroke.typ13
-rw-r--r--tests/typ/visualize/pattern-text.typ28
-rw-r--r--tests/typ/visualize/shape-rect.typ2
29 files changed, 1145 insertions, 168 deletions
diff --git a/crates/typst-pdf/src/color.rs b/crates/typst-pdf/src/color.rs
index 17c4686a..d7781b35 100644
--- a/crates/typst-pdf/src/color.rs
+++ b/crates/typst-pdf/src/color.rs
@@ -294,6 +294,7 @@ impl PaintEncode for Paint {
match self {
Self::Solid(c) => c.set_as_fill(ctx, on_text, transforms),
Self::Gradient(gradient) => gradient.set_as_fill(ctx, on_text, transforms),
+ Self::Pattern(pattern) => pattern.set_as_fill(ctx, on_text, transforms),
}
}
@@ -301,6 +302,7 @@ impl PaintEncode for Paint {
match self {
Self::Solid(c) => c.set_as_stroke(ctx, transforms),
Self::Gradient(gradient) => gradient.set_as_stroke(ctx, transforms),
+ Self::Pattern(pattern) => pattern.set_as_stroke(ctx, transforms),
}
}
}
diff --git a/crates/typst-pdf/src/gradient.rs b/crates/typst-pdf/src/gradient.rs
index 5e7e5f3d..b12ac53f 100644
--- a/crates/typst-pdf/src/gradient.rs
+++ b/crates/typst-pdf/src/gradient.rs
@@ -1,19 +1,19 @@
use std::f32::consts::{PI, TAU};
use std::sync::Arc;
-use ecow::{eco_format, EcoString};
+use ecow::eco_format;
use pdf_writer::types::{ColorSpaceOperand, FunctionShadingType};
use pdf_writer::writers::StreamShadingType;
use pdf_writer::{Filter, Finish, Name, Ref};
use typst::layout::{Abs, Angle, Point, Quadrant, Ratio, Transform};
use typst::util::Numeric;
use typst::visualize::{
- Color, ColorSpace, ConicGradient, Gradient, GradientRelative, WeightedColor,
+ Color, ColorSpace, ConicGradient, Gradient, RelativeTo, WeightedColor,
};
use crate::color::{ColorSpaceExt, PaintEncode, QuantizedColor};
-use crate::page::{PageContext, Transforms};
-use crate::{deflate, AbsExt, PdfContext};
+use crate::page::{PageContext, PageResource, ResourceKind, Transforms};
+use crate::{deflate, transform_to_array, AbsExt, PdfContext};
/// A unique-transform-aspect-ratio combination that will be encoded into the
/// PDF.
@@ -268,21 +268,27 @@ impl PaintEncode for Gradient {
fn set_as_fill(&self, ctx: &mut PageContext, on_text: bool, transforms: Transforms) {
ctx.reset_fill_color_space();
- let id = register_gradient(ctx, self, on_text, transforms);
+ let index = register_gradient(ctx, self, on_text, transforms);
+ let id = eco_format!("Gr{index}");
let name = Name(id.as_bytes());
ctx.content.set_fill_color_space(ColorSpaceOperand::Pattern);
ctx.content.set_fill_pattern(None, name);
+ ctx.resources
+ .insert(PageResource::new(ResourceKind::Gradient, id), index);
}
fn set_as_stroke(&self, ctx: &mut PageContext, transforms: Transforms) {
ctx.reset_stroke_color_space();
- let id = register_gradient(ctx, self, false, transforms);
+ let index = register_gradient(ctx, self, false, transforms);
+ let id = eco_format!("Gr{index}");
let name = Name(id.as_bytes());
ctx.content.set_stroke_color_space(ColorSpaceOperand::Pattern);
ctx.content.set_stroke_pattern(None, name);
+ ctx.resources
+ .insert(PageResource::new(ResourceKind::Gradient, id), index);
}
}
@@ -292,7 +298,7 @@ fn register_gradient(
gradient: &Gradient,
on_text: bool,
mut transforms: Transforms,
-) -> EcoString {
+) -> usize {
// Edge cases for strokes.
if transforms.size.x.is_zero() {
transforms.size.x = Abs::pt(1.0);
@@ -302,8 +308,8 @@ fn register_gradient(
transforms.size.y = Abs::pt(1.0);
}
let size = match gradient.unwrap_relative(on_text) {
- GradientRelative::Self_ => transforms.size,
- GradientRelative::Parent => transforms.container_size,
+ RelativeTo::Self_ => transforms.size,
+ RelativeTo::Parent => transforms.container_size,
};
let (offset_x, offset_y) = match gradient {
@@ -317,8 +323,8 @@ fn register_gradient(
let rotation = gradient.angle().unwrap_or_else(Angle::zero);
let transform = match gradient.unwrap_relative(on_text) {
- GradientRelative::Self_ => transforms.transform,
- GradientRelative::Parent => transforms.container_transform,
+ RelativeTo::Self_ => transforms.transform,
+ RelativeTo::Parent => transforms.container_transform,
};
let scale_offset = match gradient {
@@ -341,20 +347,7 @@ fn register_gradient(
angle: Gradient::correct_aspect_ratio(rotation, size.aspect_ratio()),
};
- let index = ctx.parent.gradient_map.insert(pdf_gradient);
- eco_format!("Gr{}", index)
-}
-
-/// Convert to an array of floats.
-fn transform_to_array(ts: Transform) -> [f32; 6] {
- [
- ts.sx.get() as f32,
- ts.ky.get() as f32,
- ts.kx.get() as f32,
- ts.sy.get() as f32,
- ts.tx.to_f32(),
- ts.ty.to_f32(),
- ]
+ ctx.parent.gradient_map.insert(pdf_gradient)
}
/// Writes a single Coons Patch as defined in the PDF specification
diff --git a/crates/typst-pdf/src/lib.rs b/crates/typst-pdf/src/lib.rs
index c753315c..005b5a9e 100644
--- a/crates/typst-pdf/src/lib.rs
+++ b/crates/typst-pdf/src/lib.rs
@@ -7,10 +7,12 @@ mod gradient;
mod image;
mod outline;
mod page;
+mod pattern;
use std::cmp::Eq;
use std::collections::{BTreeMap, HashMap};
use std::hash::Hash;
+use std::sync::Arc;
use base64::Engine;
use ecow::{eco_format, EcoString};
@@ -18,7 +20,7 @@ use pdf_writer::types::Direction;
use pdf_writer::{Finish, Name, Pdf, Ref, TextStr};
use typst::foundations::Datetime;
use typst::introspection::Introspector;
-use typst::layout::{Abs, Dir, Em};
+use typst::layout::{Abs, Dir, Em, Transform};
use typst::model::Document;
use typst::text::{Font, Lang};
use typst::util::Deferred;
@@ -30,6 +32,7 @@ use crate::extg::ExtGState;
use crate::gradient::PdfGradient;
use crate::image::EncodedImage;
use crate::page::Page;
+use crate::pattern::PdfPattern;
/// Export a document into a PDF file.
///
@@ -57,6 +60,7 @@ pub fn pdf(
image::write_images(&mut ctx);
gradient::write_gradients(&mut ctx);
extg::write_external_graphics_states(&mut ctx);
+ pattern::write_patterns(&mut ctx);
page::write_page_tree(&mut ctx);
write_catalog(&mut ctx, ident, timestamp);
ctx.pdf.finish()
@@ -97,6 +101,8 @@ struct PdfContext<'a> {
image_refs: Vec<Ref>,
/// The IDs of written gradients.
gradient_refs: Vec<Ref>,
+ /// The IDs of written patterns.
+ pattern_refs: Vec<Ref>,
/// The IDs of written external graphics states.
ext_gs_refs: Vec<Ref>,
/// Handles color space writing.
@@ -110,6 +116,8 @@ struct PdfContext<'a> {
image_deferred_map: HashMap<usize, Deferred<EncodedImage>>,
/// Deduplicates gradients used across the document.
gradient_map: Remapper<PdfGradient>,
+ /// Deduplicates patterns used across the document.
+ pattern_map: Remapper<PdfPattern>,
/// Deduplicates external graphics states used across the document.
extg_map: Remapper<ExtGState>,
}
@@ -131,12 +139,14 @@ impl<'a> PdfContext<'a> {
font_refs: vec![],
image_refs: vec![],
gradient_refs: vec![],
+ pattern_refs: vec![],
ext_gs_refs: vec![],
colors: ColorSpaces::default(),
font_map: Remapper::new(),
image_map: Remapper::new(),
image_deferred_map: HashMap::default(),
gradient_map: Remapper::new(),
+ pattern_map: Remapper::new(),
extg_map: Remapper::new(),
}
}
@@ -263,6 +273,12 @@ fn deflate(data: &[u8]) -> Vec<u8> {
miniz_oxide::deflate::compress_to_vec_zlib(data, COMPRESSION_LEVEL)
}
+/// Memoized version of [`deflate`] specialized for a page's content stream.
+#[comemo::memoize]
+fn deflate_memoized(content: &[u8]) -> Arc<Vec<u8>> {
+ Arc::new(deflate(content))
+}
+
/// Create a base64-encoded hash of the value.
fn hash_base64<T: Hash>(value: &T) -> String {
base64::engine::general_purpose::STANDARD
@@ -341,10 +357,6 @@ where
})
}
- fn map(&self, item: &T) -> usize {
- self.to_pdf[item]
- }
-
fn pdf_indices<'a>(
&'a self,
refs: &'a [Ref],
@@ -380,3 +392,15 @@ impl EmExt for Em {
1000.0 * self.get() as f32
}
}
+
+/// Convert to an array of floats.
+fn transform_to_array(ts: Transform) -> [f32; 6] {
+ [
+ ts.sx.get() as f32,
+ ts.ky.get() as f32,
+ ts.kx.get() as f32,
+ ts.sy.get() as f32,
+ ts.tx.to_f32(),
+ ts.ty.to_f32(),
+ ]
+}
diff --git a/crates/typst-pdf/src/page.rs b/crates/typst-pdf/src/page.rs
index 545380da..1bbef7af 100644
--- a/crates/typst-pdf/src/page.rs
+++ b/crates/typst-pdf/src/page.rs
@@ -1,7 +1,7 @@
+use std::collections::HashMap;
use std::num::NonZeroUsize;
-use std::sync::Arc;
-use ecow::eco_format;
+use ecow::{eco_format, EcoString};
use pdf_writer::types::{
ActionType, AnnotationType, ColorSpaceOperand, LineCapStyle, LineJoinStyle,
NumberingStyle,
@@ -23,21 +23,22 @@ use typst::visualize::{
use crate::color::PaintEncode;
use crate::extg::ExtGState;
use crate::image::deferred_image;
-use crate::{deflate, AbsExt, EmExt, PdfContext};
+use crate::{deflate_memoized, AbsExt, EmExt, PdfContext};
/// Construct page objects.
#[tracing::instrument(skip_all)]
pub(crate) fn construct_pages(ctx: &mut PdfContext, frames: &[Frame]) {
for frame in frames {
- construct_page(ctx, frame);
+ let (page_ref, page) = construct_page(ctx, frame);
+ ctx.page_refs.push(page_ref);
+ ctx.pages.push(page);
}
}
/// Construct a page object.
#[tracing::instrument(skip_all)]
-pub(crate) fn construct_page(ctx: &mut PdfContext, frame: &Frame) {
+pub(crate) fn construct_page(ctx: &mut PdfContext, frame: &Frame) -> (Ref, Page) {
let page_ref = ctx.alloc.bump();
- ctx.page_refs.push(page_ref);
let mut ctx = PageContext {
parent: ctx,
@@ -49,6 +50,7 @@ pub(crate) fn construct_page(ctx: &mut PdfContext, frame: &Frame) {
saves: vec![],
bottom: 0.0,
links: vec![],
+ resources: HashMap::default(),
};
let size = frame.size();
@@ -74,9 +76,10 @@ pub(crate) fn construct_page(ctx: &mut PdfContext, frame: &Frame) {
uses_opacities: ctx.uses_opacities,
links: ctx.links,
label: ctx.label,
+ resources: ctx.resources,
};
- ctx.parent.pages.push(page);
+ (page_ref, page)
}
/// Write the page tree.
@@ -117,6 +120,11 @@ pub(crate) fn write_page_tree(ctx: &mut PdfContext) {
patterns.pair(Name(name.as_bytes()), gradient_ref);
}
+ for (pattern_ref, p) in ctx.pattern_map.pdf_indices(&ctx.pattern_refs) {
+ let name = eco_format!("P{}", p);
+ patterns.pair(Name(name.as_bytes()), pattern_ref);
+ }
+
patterns.finish();
let mut ext_gs_states = resources.ext_g_states();
@@ -190,7 +198,7 @@ fn write_page(ctx: &mut PdfContext, i: usize) {
annotations.finish();
page_writer.finish();
- let data = deflate_content(&page.content);
+ let data = deflate_memoized(&page.content);
ctx.pdf.stream(content_id, &data).filter(Filter::FlateDecode);
}
@@ -243,12 +251,6 @@ pub(crate) fn write_page_labels(ctx: &mut PdfContext) -> Vec<(NonZeroUsize, Ref)
result
}
-/// Memoized version of [`deflate`] specialized for a page's content stream.
-#[comemo::memoize]
-fn deflate_content(content: &[u8]) -> Arc<Vec<u8>> {
- Arc::new(deflate(content))
-}
-
/// Data for an exported page.
pub struct Page {
/// The indirect object id of the page.
@@ -263,6 +265,63 @@ pub struct Page {
pub links: Vec<(Destination, Rect)>,
/// The page's PDF label.
pub label: Option<PdfPageLabel>,
+ /// The page's used resources
+ pub resources: HashMap<PageResource, usize>,
+}
+
+/// Represents a resource being used in a PDF page by its name.
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
+pub struct PageResource {
+ kind: ResourceKind,
+ name: EcoString,
+}
+
+impl PageResource {
+ pub fn new(kind: ResourceKind, name: EcoString) -> Self {
+ Self { kind, name }
+ }
+}
+
+/// A kind of resource being used in a PDF page.
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
+pub enum ResourceKind {
+ XObject,
+ Font,
+ Gradient,
+ Pattern,
+ ExtGState,
+}
+
+impl PageResource {
+ /// Returns the name of the resource.
+ pub fn name(&self) -> Name<'_> {
+ Name(self.name.as_bytes())
+ }
+
+ /// Returns whether the resource is an XObject.
+ pub fn is_x_object(&self) -> bool {
+ matches!(self.kind, ResourceKind::XObject)
+ }
+
+ /// Returns whether the resource is a font.
+ pub fn is_font(&self) -> bool {
+ matches!(self.kind, ResourceKind::Font)
+ }
+
+ /// Returns whether the resource is a gradient.
+ pub fn is_gradient(&self) -> bool {
+ matches!(self.kind, ResourceKind::Gradient)
+ }
+
+ /// Returns whether the resource is a pattern.
+ pub fn is_pattern(&self) -> bool {
+ matches!(self.kind, ResourceKind::Pattern)
+ }
+
+ /// Returns whether the resource is an external graphics state.
+ pub fn is_ext_g_state(&self) -> bool {
+ matches!(self.kind, ResourceKind::ExtGState)
+ }
}
/// An exporter for the contents of a single PDF page.
@@ -276,6 +335,8 @@ pub struct PageContext<'a, 'b> {
bottom: f32,
uses_opacities: bool,
links: Vec<(Destination, Rect)>,
+ /// Keep track of the resources being used in the page.
+ pub resources: HashMap<PageResource, usize>,
}
/// A simulated graphics state used to deduplicate graphics state changes and
@@ -350,9 +411,11 @@ impl PageContext<'_, '_> {
fn set_external_graphics_state(&mut self, graphics_state: &ExtGState) {
let current_state = self.state.external_graphics_state.as_ref();
if current_state != Some(graphics_state) {
- self.parent.extg_map.insert(*graphics_state);
- let name = eco_format!("Gs{}", self.parent.extg_map.map(graphics_state));
+ let index = self.parent.extg_map.insert(*graphics_state);
+ let name = eco_format!("Gs{index}");
self.content.set_parameters(Name(name.as_bytes()));
+ self.resources
+ .insert(PageResource::new(ResourceKind::ExtGState, name), index);
if graphics_state.uses_opacities() {
self.uses_opacities = true;
@@ -365,7 +428,7 @@ impl PageContext<'_, '_> {
.map(|stroke| {
let color = match &stroke.paint {
Paint::Solid(color) => *color,
- Paint::Gradient(_) => return 255,
+ Paint::Gradient(_) | Paint::Pattern(_) => return 255,
};
color.alpha().map_or(255, |v| (v * 255.0).round() as u8)
@@ -375,7 +438,7 @@ impl PageContext<'_, '_> {
.map(|paint| {
let color = match paint {
Paint::Solid(color) => *color,
- Paint::Gradient(_) => return 255,
+ Paint::Gradient(_) | Paint::Pattern(_) => return 255,
};
color.alpha().map_or(255, |v| (v * 255.0).round() as u8)
@@ -407,9 +470,11 @@ impl PageContext<'_, '_> {
fn set_font(&mut self, font: &Font, size: Abs) {
if self.state.font.as_ref().map(|(f, s)| (f, *s)) != Some((font, size)) {
- self.parent.font_map.insert(font.clone());
- let name = eco_format!("F{}", self.parent.font_map.map(font));
+ let index = self.parent.font_map.insert(font.clone());
+ let name = eco_format!("F{index}");
self.content.set_font(Name(name.as_bytes()), size.to_f32());
+ self.resources
+ .insert(PageResource::new(ResourceKind::Font, name), index);
self.state.font = Some((font.clone(), size));
}
}
@@ -681,13 +746,13 @@ fn write_path(ctx: &mut PageContext, x: f32, y: f32, path: &Path) {
/// Encode a vector or raster image into the content stream.
fn write_image(ctx: &mut PageContext, x: f32, y: f32, image: &Image, size: Size) {
- let idx = ctx.parent.image_map.insert(image.clone());
+ let index = ctx.parent.image_map.insert(image.clone());
ctx.parent
.image_deferred_map
- .entry(idx)
+ .entry(index)
.or_insert_with(|| deferred_image(image.clone()));
- let name = eco_format!("Im{idx}");
+ let name = eco_format!("Im{index}");
let w = size.x.to_f32();
let h = size.y.to_f32();
ctx.content.save_state();
@@ -707,6 +772,8 @@ fn write_image(ctx: &mut PageContext, x: f32, y: f32, image: &Image, size: Size)
ctx.content.x_object(Name(name.as_bytes()));
}
+ ctx.resources
+ .insert(PageResource::new(ResourceKind::XObject, name.clone()), index);
ctx.content.restore_state();
}
diff --git a/crates/typst-pdf/src/pattern.rs b/crates/typst-pdf/src/pattern.rs
new file mode 100644
index 00000000..7b9e2719
--- /dev/null
+++ b/crates/typst-pdf/src/pattern.rs
@@ -0,0 +1,154 @@
+use ecow::eco_format;
+use pdf_writer::types::{ColorSpaceOperand, PaintType, TilingType};
+use pdf_writer::{Filter, Finish, Name, Rect};
+use typst::layout::{Abs, Transform};
+use typst::util::Numeric;
+use typst::visualize::{Pattern, RelativeTo};
+
+use crate::color::PaintEncode;
+use crate::page::{construct_page, PageContext, PageResource, ResourceKind, Transforms};
+use crate::{deflate_memoized, transform_to_array, PdfContext};
+
+/// Writes the actual patterns (tiling patterns) to the PDF.
+/// This is performed once after writing all pages.
+pub(crate) fn write_patterns(ctx: &mut PdfContext) {
+ for PdfPattern { transform, pattern, content, resources } in ctx.pattern_map.items() {
+ let tiling = ctx.alloc.bump();
+ ctx.pattern_refs.push(tiling);
+
+ let content = deflate_memoized(content);
+ let mut tiling_pattern = ctx.pdf.tiling_pattern(tiling, &content);
+ tiling_pattern
+ .tiling_type(TilingType::ConstantSpacing)
+ .paint_type(PaintType::Colored)
+ .bbox(Rect::new(
+ 0.0,
+ 0.0,
+ pattern.size_abs().x.to_pt() as _,
+ pattern.size_abs().y.to_pt() as _,
+ ))
+ .x_step((pattern.size_abs().x + pattern.spacing_abs().x).to_pt() as _)
+ .y_step((pattern.size_abs().y + pattern.spacing_abs().y).to_pt() as _);
+
+ let mut resources_map = tiling_pattern.resources();
+
+ resources_map.x_objects().pairs(
+ resources
+ .iter()
+ .filter(|(res, _)| res.is_x_object())
+ .map(|(res, ref_)| (res.name(), ctx.image_refs[*ref_])),
+ );
+
+ resources_map.fonts().pairs(
+ resources
+ .iter()
+ .filter(|(res, _)| res.is_font())
+ .map(|(res, ref_)| (res.name(), ctx.font_refs[*ref_])),
+ );
+
+ ctx.colors
+ .write_color_spaces(resources_map.color_spaces(), &mut ctx.alloc);
+
+ resources_map
+ .patterns()
+ .pairs(
+ resources
+ .iter()
+ .filter(|(res, _)| res.is_pattern())
+ .map(|(res, ref_)| (res.name(), ctx.pattern_refs[*ref_])),
+ )
+ .pairs(
+ resources
+ .iter()
+ .filter(|(res, _)| res.is_gradient())
+ .map(|(res, ref_)| (res.name(), ctx.gradient_refs[*ref_])),
+ );
+
+ resources_map.ext_g_states().pairs(
+ resources
+ .iter()
+ .filter(|(res, _)| res.is_ext_g_state())
+ .map(|(res, ref_)| (res.name(), ctx.ext_gs_refs[*ref_])),
+ );
+
+ resources_map.finish();
+ tiling_pattern
+ .matrix(transform_to_array(*transform))
+ .filter(Filter::FlateDecode);
+ }
+}
+
+/// A pattern and its transform.
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
+pub struct PdfPattern {
+ /// The transform to apply to the gradient.
+ pub transform: Transform,
+ /// The pattern to paint.
+ pub pattern: Pattern,
+ /// The rendered pattern.
+ pub content: Vec<u8>,
+ /// The resources used by the pattern.
+ pub resources: Vec<(PageResource, usize)>,
+}
+
+/// Registers a pattern with the PDF.
+fn register_pattern(
+ ctx: &mut PageContext,
+ pattern: &Pattern,
+ on_text: bool,
+ mut transforms: Transforms,
+) -> usize {
+ // Edge cases for strokes.
+ if transforms.size.x.is_zero() {
+ transforms.size.x = Abs::pt(1.0);
+ }
+
+ if transforms.size.y.is_zero() {
+ transforms.size.y = Abs::pt(1.0);
+ }
+
+ let transform = match pattern.unwrap_relative(on_text) {
+ RelativeTo::Self_ => transforms.transform,
+ RelativeTo::Parent => transforms.container_transform,
+ };
+
+ // Render the body.
+ let (_, content) = construct_page(ctx.parent, pattern.frame());
+
+ let pdf_pattern = PdfPattern {
+ transform,
+ pattern: pattern.clone(),
+ content: content.content,
+ resources: content.resources.into_iter().collect(),
+ };
+
+ ctx.parent.pattern_map.insert(pdf_pattern)
+}
+
+impl PaintEncode for Pattern {
+ fn set_as_fill(&self, ctx: &mut PageContext, on_text: bool, transforms: Transforms) {
+ ctx.reset_fill_color_space();
+
+ let index = register_pattern(ctx, self, on_text, transforms);
+ let id = eco_format!("P{index}");
+ let name = Name(id.as_bytes());
+
+ ctx.content.set_fill_color_space(ColorSpaceOperand::Pattern);
+ ctx.content.set_fill_pattern(None, name);
+ ctx.resources
+ .insert(PageResource::new(ResourceKind::Pattern, id), index);
+ }
+
+ fn set_as_stroke(&self, ctx: &mut PageContext, transforms: Transforms) {
+ ctx.reset_stroke_color_space();
+
+ let index = register_pattern(ctx, self, false, transforms);
+ let id = eco_format!("P{index}");
+ let name = Name(id.as_bytes());
+
+ ctx.content.set_stroke_color_space(ColorSpaceOperand::Pattern);
+ ctx.content.set_stroke_pattern(None, name);
+ ctx.resources
+ .insert(PageResource::new(ResourceKind::Pattern, id), index);
+ }
+}
diff --git a/crates/typst-render/src/lib.rs b/crates/typst-render/src/lib.rs
index 251f647a..5c1b8482 100644
--- a/crates/typst-render/src/lib.rs
+++ b/crates/typst-render/src/lib.rs
@@ -15,8 +15,8 @@ use typst::layout::{
};
use typst::text::{Font, TextItem};
use typst::visualize::{
- Color, FixedStroke, Geometry, Gradient, GradientRelative, Image, ImageKind, LineCap,
- LineJoin, Paint, Path, PathItem, RasterFormat, Shape,
+ Color, FixedStroke, Geometry, Gradient, Image, ImageKind, LineCap, LineJoin, Paint,
+ Path, PathItem, Pattern, RasterFormat, RelativeTo, Shape,
};
use usvg::{NodeExt, TreeParsing};
@@ -433,7 +433,17 @@ fn render_outline_glyph(
write_bitmap(canvas, &bitmap, &state, sampler)?;
}
Paint::Solid(color) => {
- write_bitmap(canvas, &bitmap, &state, *color)?;
+ write_bitmap(
+ canvas,
+ &bitmap,
+ &state,
+ to_sk_color_u8_without_alpha(*color).premultiply(),
+ )?;
+ }
+ Paint::Pattern(pattern) => {
+ let pixmap = render_pattern_frame(&state, pattern);
+ let sampler = PatternSampler::new(pattern, &pixmap, &state, true);
+ write_bitmap(canvas, &bitmap, &state, sampler)?;
}
}
@@ -458,7 +468,7 @@ fn write_bitmap<S: PaintSampler>(
for x in 0..mw {
for y in 0..mh {
let alpha = bitmap.coverage[(y * mw + x) as usize];
- let color = to_sk_color_u8_without_alpha(sampler.sample((x, y)));
+ let color = sampler.sample((x, y));
pixmap.pixels_mut()[((y + 1) * (mw + 2) + (x + 1)) as usize] =
sk::ColorU8::from_rgba(
color.red(),
@@ -504,8 +514,7 @@ fn write_bitmap<S: PaintSampler>(
}
let color = sampler.sample((x as _, y as _));
- let color =
- bytemuck::cast(to_sk_color_u8_without_alpha(color).premultiply());
+ let color = bytemuck::cast(color);
let pi = (y * cw + x) as usize;
if cov == 255 {
pixels[pi] = color;
@@ -746,11 +755,22 @@ fn scaled_texture(image: &Image, w: u32, h: u32) -> Option<Arc<sk::Pixmap>> {
/// abstraction over solid colors and gradients.
trait PaintSampler: Copy {
/// Sample the color at the `pos` in the pixmap.
- fn sample(self, pos: (u32, u32)) -> Color;
+ fn sample(self, pos: (u32, u32)) -> sk::PremultipliedColorU8;
+
+ /// Write the sampler to a pixmap.
+ fn write_to_pixmap(self, canvas: &mut sk::Pixmap) {
+ let width = canvas.width();
+ for x in 0..canvas.width() {
+ for y in 0..canvas.height() {
+ let color = self.sample((x, y));
+ canvas.pixels_mut()[(y * width + x) as usize] = color;
+ }
+ }
+ }
}
-impl PaintSampler for Color {
- fn sample(self, _: (u32, u32)) -> Color {
+impl PaintSampler for sk::PremultipliedColorU8 {
+ fn sample(self, _: (u32, u32)) -> sk::PremultipliedColorU8 {
self
}
}
@@ -775,13 +795,13 @@ impl<'a> GradientSampler<'a> {
) -> Self {
let relative = gradient.unwrap_relative(on_text);
let container_size = match relative {
- GradientRelative::Self_ => item_size,
- GradientRelative::Parent => state.size,
+ RelativeTo::Self_ => item_size,
+ RelativeTo::Parent => state.size,
};
let fill_transform = match relative {
- GradientRelative::Self_ => sk::Transform::identity(),
- GradientRelative::Parent => state.container_transform.invert().unwrap(),
+ RelativeTo::Self_ => sk::Transform::identity(),
+ RelativeTo::Parent => state.container_transform.invert().unwrap(),
};
Self {
@@ -794,16 +814,69 @@ impl<'a> GradientSampler<'a> {
impl PaintSampler for GradientSampler<'_> {
/// Samples a single point in a glyph.
- fn sample(self, (x, y): (u32, u32)) -> Color {
+ fn sample(self, (x, y): (u32, u32)) -> sk::PremultipliedColorU8 {
// Compute the point in the gradient's coordinate space.
let mut point = sk::Point { x: x as f32, y: y as f32 };
self.transform_to_parent.map_point(&mut point);
// Sample the gradient
- self.gradient.sample_at(
+ to_sk_color_u8_without_alpha(self.gradient.sample_at(
(point.x, point.y),
(self.container_size.x.to_f32(), self.container_size.y.to_f32()),
- )
+ ))
+ .premultiply()
+ }
+}
+
+/// State used when sampling patterns for text.
+///
+/// It caches the inverse transform to the parent, so that we can
+/// reuse it instead of recomputing it for each pixel.
+#[derive(Clone, Copy)]
+struct PatternSampler<'a> {
+ size: Size,
+ transform_to_parent: sk::Transform,
+ pixmap: &'a sk::Pixmap,
+ pixel_per_pt: f32,
+}
+
+impl<'a> PatternSampler<'a> {
+ fn new(
+ pattern: &'a Pattern,
+ pixmap: &'a sk::Pixmap,
+ state: &State,
+ on_text: bool,
+ ) -> Self {
+ let relative = pattern.unwrap_relative(on_text);
+ let fill_transform = match relative {
+ RelativeTo::Self_ => sk::Transform::identity(),
+ RelativeTo::Parent => state.container_transform.invert().unwrap(),
+ };
+
+ Self {
+ pixmap,
+ size: (pattern.size_abs() + pattern.spacing_abs())
+ * state.pixel_per_pt as f64,
+ transform_to_parent: fill_transform,
+ pixel_per_pt: state.pixel_per_pt,
+ }
+ }
+}
+
+impl PaintSampler for PatternSampler<'_> {
+ /// Samples a single point in a glyph.
+ fn sample(self, (x, y): (u32, u32)) -> sk::PremultipliedColorU8 {
+ // Compute the point in the pattern's coordinate space.
+ let mut point = sk::Point { x: x as f32, y: y as f32 };
+ self.transform_to_parent.map_point(&mut point);
+
+ let x =
+ (point.x * self.pixel_per_pt).rem_euclid(self.size.x.to_f32()).floor() as u32;
+ let y =
+ (point.y * self.pixel_per_pt).rem_euclid(self.size.y.to_f32()).floor() as u32;
+
+ // Sample the pattern
+ self.pixmap.pixel(x, y).unwrap()
}
}
@@ -859,13 +932,13 @@ fn to_sk_paint<'a>(
Paint::Gradient(gradient) => {
let relative = gradient.unwrap_relative(on_text);
let container_size = match relative {
- GradientRelative::Self_ => item_size,
- GradientRelative::Parent => state.size,
+ RelativeTo::Self_ => item_size,
+ RelativeTo::Parent => state.size,
};
let fill_transform = match relative {
- GradientRelative::Self_ => fill_transform.unwrap_or_default(),
- GradientRelative::Parent => state
+ RelativeTo::Self_ => fill_transform.unwrap_or_default(),
+ RelativeTo::Parent => state
.container_transform
.post_concat(state.transform.invert().unwrap()),
};
@@ -892,11 +965,49 @@ fn to_sk_paint<'a>(
sk_paint.anti_alias = gradient.anti_alias();
}
+ Paint::Pattern(pattern) => {
+ let relative = pattern.unwrap_relative(on_text);
+
+ let fill_transform = match relative {
+ RelativeTo::Self_ => fill_transform.unwrap_or_default(),
+ RelativeTo::Parent => state
+ .container_transform
+ .post_concat(state.transform.invert().unwrap()),
+ };
+
+ let canvas = render_pattern_frame(&state, pattern);
+ *pixmap = Some(Arc::new(canvas));
+
+ // Create the shader
+ sk_paint.shader = sk::Pattern::new(
+ pixmap.as_ref().unwrap().as_ref().as_ref(),
+ sk::SpreadMode::Repeat,
+ sk::FilterQuality::Nearest,
+ 1.0,
+ fill_transform
+ .pre_scale(1.0 / state.pixel_per_pt, 1.0 / state.pixel_per_pt),
+ );
+ }
}
sk_paint
}
+fn render_pattern_frame(state: &State, pattern: &Pattern) -> sk::Pixmap {
+ let size = pattern.size_abs() + pattern.spacing_abs();
+ let mut canvas = sk::Pixmap::new(
+ (size.x.to_f32() * state.pixel_per_pt).round() as u32,
+ (size.y.to_f32() * state.pixel_per_pt).round() as u32,
+ )
+ .unwrap();
+
+ // Render the pattern into a new canvas.
+ let ts = sk::Transform::from_scale(state.pixel_per_pt, state.pixel_per_pt);
+ let temp_state = State::new(pattern.size_abs(), ts, state.pixel_per_pt);
+ render_frame(&mut canvas, temp_state, pattern.frame());
+ canvas
+}
+
fn to_sk_color(color: Color) -> sk::Color {
let [r, g, b, a] = color.to_rgb().to_vec4_u8();
sk::Color::from_rgba8(r, g, b, a)
diff --git a/crates/typst-svg/src/lib.rs b/crates/typst-svg/src/lib.rs
index 7d3a773d..205cff14 100644
--- a/crates/typst-svg/src/lib.rs
+++ b/crates/typst-svg/src/lib.rs
@@ -14,9 +14,8 @@ use typst::layout::{
use typst::text::{Font, TextItem};
use typst::util::hash128;
use typst::visualize::{
- Color, FixedStroke, Geometry, Gradient, GradientRelative, Image, ImageFormat,
- LineCap, LineJoin, Paint, Path, PathItem, RasterFormat, RatioOrAngle, Shape,
- VectorFormat,
+ Color, FixedStroke, Geometry, Gradient, Image, ImageFormat, LineCap, LineJoin, Paint,
+ Path, PathItem, Pattern, RasterFormat, RatioOrAngle, RelativeTo, Shape, VectorFormat,
};
use xmlwriter::XmlWriter;
@@ -77,6 +76,12 @@ struct SVGRenderer {
/// different transforms. Therefore this allows us to reuse the same gradient
/// multiple times.
gradient_refs: Deduplicator<GradientRef>,
+ /// Deduplicated patterns with transform matrices. They use a reference
+ /// (`href`) to a "source" pattern instead of being defined inline.
+ /// This saves a lot of space since patterns are often reused but with
+ /// different transforms. Therefore this allows us to reuse the same gradient
+ /// multiple times.
+ pattern_refs: Deduplicator<PatternRef>,
/// These are the actual gradients being written in the SVG file.
/// These gradients are deduplicated because they do not contain the transform
/// matrix, allowing them to be reused across multiple invocations.
@@ -84,6 +89,12 @@ struct SVGRenderer {
/// The `Ratio` is the aspect ratio of the gradient, this is used to correct
/// the angle of the gradient.
gradients: Deduplicator<(Gradient, Ratio)>,
+ /// These are the actual patterns being written in the SVG file.
+ /// These patterns are deduplicated because they do not contain the transform
+ /// matrix, allowing them to be reused across multiple invocations.
+ ///
+ /// The `String` is the rendered pattern frame.
+ patterns: Deduplicator<Pattern>,
/// These are the gradients that compose a conic gradient.
conic_subgradients: Deduplicator<SVGSubGradient>,
}
@@ -141,6 +152,20 @@ struct GradientRef {
transform: Transform,
}
+/// A reference to a deduplicated pattern, with a transform matrix.
+///
+/// Allows patterns to be reused across multiple invocations,
+/// simply by changing the transform matrix.
+#[derive(Hash)]
+struct PatternRef {
+ /// The ID of the deduplicated gradient
+ id: Id,
+ /// The transform matrix to apply to the pattern.
+ transform: Transform,
+ /// The ratio of the size of the cell to the size of the filled area.
+ ratio: Axes<Ratio>,
+}
+
/// A subgradient for conic gradients.
#[derive(Hash)]
struct SVGSubGradient {
@@ -199,6 +224,8 @@ impl SVGRenderer {
gradient_refs: Deduplicator::new('g'),
gradients: Deduplicator::new('f'),
conic_subgradients: Deduplicator::new('s'),
+ pattern_refs: Deduplicator::new('p'),
+ patterns: Deduplicator::new('t'),
}
}
@@ -219,6 +246,20 @@ impl SVGRenderer {
self.xml.write_attribute("xmlns:h5", "http://www.w3.org/1999/xhtml");
}
+ /// Render a frame to a string.
+ fn render_pattern_frame(
+ &mut self,
+ state: State,
+ ts: Transform,
+ frame: &Frame,
+ ) -> String {
+ let mut xml = XmlWriter::new(xmlwriter::Options::default());
+ std::mem::swap(&mut self.xml, &mut xml);
+ self.render_frame(state, ts, frame);
+ std::mem::swap(&mut self.xml, &mut xml);
+ xml.end_document()
+ }
+
/// Render a frame with the given transform.
fn render_frame(&mut self, state: State, ts: Transform, frame: &Frame) {
self.xml.start_element("g");
@@ -286,37 +327,27 @@ impl SVGRenderer {
/// of them works, we will skip the text.
fn render_text(&mut self, state: State, text: &TextItem) {
let scale: f64 = text.size.to_pt() / text.font.units_per_em();
- let inv_scale: f64 = text.font.units_per_em() / text.size.to_pt();
self.xml.start_element("g");
self.xml.write_attribute("class", "typst-text");
- self.xml.write_attribute_fmt(
- "transform",
- format_args!("scale({} {})", scale, -scale),
- );
+ self.xml.write_attribute("transform", "scale(1, -1)");
let mut x: f64 = 0.0;
for glyph in &text.glyphs {
let id = GlyphId(glyph.id);
let offset = x + glyph.x_offset.at(text.size).to_pt();
- self.render_svg_glyph(text, id, offset, inv_scale)
- .or_else(|| self.render_bitmap_glyph(text, id, offset, inv_scale))
+ self.render_svg_glyph(text, id, offset, scale)
+ .or_else(|| self.render_bitmap_glyph(text, id, offset))
.or_else(|| {
self.render_outline_glyph(
state
- .pre_concat(Transform::scale(
- Ratio::new(scale),
- Ratio::new(-scale),
- ))
- .pre_translate(Point::new(
- Abs::pt(offset / scale),
- Abs::zero(),
- )),
+ .pre_concat(Transform::scale(Ratio::one(), -Ratio::one()))
+ .pre_translate(Point::new(Abs::pt(offset), Abs::zero())),
text,
id,
offset,
- inv_scale,
+ scale,
)
});
@@ -332,7 +363,7 @@ impl SVGRenderer {
text: &TextItem,
id: GlyphId,
x_offset: f64,
- inv_scale: f64,
+ scale: f64,
) -> Option<()> {
let data_url = convert_svg_glyph_to_base64_url(&text.font, id)?;
let upem = Abs::raw(text.font.units_per_em());
@@ -344,13 +375,12 @@ impl SVGRenderer {
width: upem.to_pt(),
height: upem.to_pt(),
ts: Transform::translate(Abs::zero(), Abs::pt(-origin_ascender))
- .post_concat(Transform::scale(Ratio::new(1.0), Ratio::new(-1.0))),
+ .post_concat(Transform::scale(Ratio::new(scale), Ratio::new(-scale))),
});
self.xml.start_element("use");
self.xml.write_attribute_fmt("xlink:href", format_args!("#{id}"));
- self.xml
- .write_attribute_fmt("x", format_args!("{}", x_offset * inv_scale));
+ self.xml.write_attribute("x", &x_offset);
self.xml.end_element();
Some(())
@@ -362,7 +392,6 @@ impl SVGRenderer {
text: &TextItem,
id: GlyphId,
x_offset: f64,
- inv_scale: f64,
) -> Option<()> {
let (image, bitmap_x_offset, bitmap_y_offset) =
convert_bitmap_glyph_to_image(&text.font, id)?;
@@ -390,11 +419,7 @@ impl SVGRenderer {
self.xml.write_attribute("x", &(x_offset / scale_factor));
self.xml.write_attribute_fmt(
"transform",
- format_args!(
- "scale({} -{})",
- inv_scale * scale_factor,
- inv_scale * scale_factor,
- ),
+ format_args!("scale({scale_factor} -{scale_factor})",),
);
self.xml.end_element();
@@ -408,19 +433,23 @@ impl SVGRenderer {
text: &TextItem,
glyph_id: GlyphId,
x_offset: f64,
- inv_scale: f64,
+ scale: f64,
) -> Option<()> {
- let path = convert_outline_glyph_to_path(&text.font, glyph_id)?;
- let hash = hash128(&(&text.font, glyph_id));
+ let scale = Ratio::new(scale);
+ let path = convert_outline_glyph_to_path(&text.font, glyph_id, scale)?;
+ let hash = hash128(&(&text.font, glyph_id, scale));
let id = self.glyphs.insert_with(hash, || RenderedGlyph::Path(path));
+ let glyph_size = text.font.ttf().glyph_bounding_box(glyph_id)?;
+ let width = glyph_size.width() as f64 * scale.get();
+ let height = glyph_size.height() as f64 * scale.get();
+
self.xml.start_element("use");
self.xml.write_attribute_fmt("xlink:href", format_args!("#{id}"));
- self.xml
- .write_attribute_fmt("x", format_args!("{}", x_offset * inv_scale));
+ self.xml.write_attribute_fmt("x", format_args!("{}", x_offset));
self.write_fill(
&text.fill,
- state.size,
+ Size::new(Abs::pt(width), Abs::pt(height)),
self.text_paint_transform(state, &text.fill),
);
self.xml.end_element();
@@ -429,17 +458,20 @@ impl SVGRenderer {
}
fn text_paint_transform(&self, state: State, paint: &Paint) -> Transform {
- let Paint::Gradient(gradient) = paint else {
- return Transform::identity();
- };
-
- match gradient.unwrap_relative(true) {
- GradientRelative::Self_ => Transform::scale(Ratio::one(), Ratio::one()),
- GradientRelative::Parent => Transform::scale(
- Ratio::new(state.size.x.to_pt()),
- Ratio::new(state.size.y.to_pt()),
- )
- .post_concat(state.transform.invert().unwrap()),
+ match paint {
+ Paint::Solid(_) => Transform::identity(),
+ Paint::Gradient(gradient) => match gradient.unwrap_relative(true) {
+ RelativeTo::Self_ => Transform::identity(),
+ RelativeTo::Parent => Transform::scale(
+ Ratio::new(state.size.x.to_pt()),
+ Ratio::new(state.size.y.to_pt()),
+ )
+ .post_concat(state.transform.invert().unwrap()),
+ },
+ Paint::Pattern(pattern) => match pattern.unwrap_relative(true) {
+ RelativeTo::Self_ => Transform::identity(),
+ RelativeTo::Parent => state.transform.invert().unwrap(),
+ },
}
}
@@ -490,16 +522,21 @@ impl SVGRenderer {
if let Paint::Gradient(gradient) = paint {
match gradient.unwrap_relative(false) {
- GradientRelative::Self_ => Transform::scale(
+ RelativeTo::Self_ => Transform::scale(
Ratio::new(shape_size.x.to_pt()),
Ratio::new(shape_size.y.to_pt()),
),
- GradientRelative::Parent => Transform::scale(
+ RelativeTo::Parent => Transform::scale(
Ratio::new(state.size.x.to_pt()),
Ratio::new(state.size.y.to_pt()),
)
.post_concat(state.transform.invert().unwrap()),
}
+ } else if let Paint::Pattern(pattern) = paint {
+ match pattern.unwrap_relative(false) {
+ RelativeTo::Self_ => Transform::identity(),
+ RelativeTo::Parent => state.transform.invert().unwrap(),
+ }
} else {
Transform::identity()
}
@@ -519,8 +556,8 @@ impl SVGRenderer {
if let Paint::Gradient(gradient) = paint {
match gradient.unwrap_relative(false) {
- GradientRelative::Self_ => shape_size,
- GradientRelative::Parent => state.size,
+ RelativeTo::Self_ => shape_size,
+ RelativeTo::Parent => state.size,
}
} else {
shape_size
@@ -535,6 +572,10 @@ impl SVGRenderer {
let id = self.push_gradient(gradient, size, ts);
self.xml.write_attribute_fmt("fill", format_args!("url(#{id})"));
}
+ Paint::Pattern(pattern) => {
+ let id = self.push_pattern(pattern, size, ts);
+ self.xml.write_attribute_fmt("fill", format_args!("url(#{id})"));
+ }
}
}
@@ -564,6 +605,29 @@ impl SVGRenderer {
})
}
+ fn push_pattern(&mut self, pattern: &Pattern, size: Size, ts: Transform) -> Id {
+ let pattern_size = pattern.size_abs() + pattern.spacing_abs();
+ // Unfortunately due to a limitation of `xmlwriter`, we need to
+ // render the frame twice: once to allocate all of the resources
+ // that it needs and once to actually render it.
+ self.render_pattern_frame(
+ State::new(pattern_size, Transform::identity()),
+ Transform::identity(),
+ pattern.frame(),
+ );
+
+ let pattern_id = self.patterns.insert_with(hash128(pattern), || pattern.clone());
+ self.pattern_refs
+ .insert_with(hash128(&(pattern_id, ts)), || PatternRef {
+ id: pattern_id,
+ transform: ts,
+ ratio: Axes::new(
+ Ratio::new(pattern_size.x.to_pt() / size.x.to_pt()),
+ Ratio::new(pattern_size.y.to_pt() / size.y.to_pt()),
+ ),
+ })
+ }
+
/// Write a stroke attribute.
fn write_stroke(
&mut self,
@@ -577,6 +641,10 @@ impl SVGRenderer {
let id = self.push_gradient(gradient, size, fill_transform);
self.xml.write_attribute_fmt("stroke", format_args!("url(#{id})"));
}
+ Paint::Pattern(pattern) => {
+ let id = self.push_pattern(pattern, size, fill_transform);
+ self.xml.write_attribute_fmt("stroke", format_args!("url(#{id})"));
+ }
}
self.xml.write_attribute("stroke-width", &stroke.thickness.to_pt());
@@ -630,6 +698,8 @@ impl SVGRenderer {
self.write_gradients();
self.write_gradient_refs();
self.write_subgradients();
+ self.write_patterns();
+ self.write_pattern_refs();
self.xml.end_document()
}
@@ -948,12 +1018,78 @@ impl SVGRenderer {
self.xml.end_element();
}
+
+ /// Write the raw gradients (without transform) to the SVG file.
+ fn write_patterns(&mut self) {
+ if self.patterns.is_empty() {
+ return;
+ }
+
+ self.xml.start_element("defs");
+ self.xml.write_attribute("id", "patterns");
+
+ for (id, pattern) in
+ self.patterns.iter().map(|(i, p)| (i, p.clone())).collect::<Vec<_>>()
+ {
+ let size = pattern.size_abs() + pattern.spacing_abs();
+ self.xml.start_element("pattern");
+ self.xml.write_attribute("id", &id);
+ self.xml.write_attribute("width", &size.x.to_pt());
+ self.xml.write_attribute("height", &size.y.to_pt());
+ self.xml.write_attribute("patternUnits", "userSpaceOnUse");
+ self.xml.write_attribute_fmt(
+ "viewBox",
+ format_args!("0 0 {:.3} {:.3}", size.x.to_pt(), size.y.to_pt()),
+ );
+
+ // Render the frame.
+ let state = State::new(size, Transform::identity());
+ let ts = Transform::identity();
+ self.render_frame(state, ts, pattern.frame());
+
+ self.xml.end_element();
+ }
+
+ self.xml.end_element()
+ }
+
+ /// Writes the references to the deduplicated patterns for each usage site.
+ fn write_pattern_refs(&mut self) {
+ if self.pattern_refs.is_empty() {
+ return;
+ }
+
+ self.xml.start_element("defs");
+ self.xml.write_attribute("id", "pattern-refs");
+ for (id, pattern_ref) in self.pattern_refs.iter() {
+ self.xml.start_element("pattern");
+ self.xml
+ .write_attribute("patternTransform", &SvgMatrix(pattern_ref.transform));
+
+ self.xml.write_attribute("id", &id);
+
+ // Writing the href attribute to the "reference" pattern.
+ self.xml
+ .write_attribute_fmt("href", format_args!("#{}", pattern_ref.id));
+
+ // Also writing the xlink:href attribute for compatibility.
+ self.xml
+ .write_attribute_fmt("xlink:href", format_args!("#{}", pattern_ref.id));
+ self.xml.end_element();
+ }
+
+ self.xml.end_element();
+ }
}
/// Convert an outline glyph to an SVG path.
#[comemo::memoize]
-fn convert_outline_glyph_to_path(font: &Font, id: GlyphId) -> Option<EcoString> {
- let mut builder = SvgPathBuilder::default();
+fn convert_outline_glyph_to_path(
+ font: &Font,
+ id: GlyphId,
+ scale: Ratio,
+) -> Option<EcoString> {
+ let mut builder = SvgPathBuilder::with_scale(scale);
font.ttf().outline_glyph(id, &mut builder)?;
Some(builder.0)
}
@@ -1170,10 +1306,17 @@ impl Display for SvgMatrix {
}
/// A builder for SVG path.
-#[derive(Default)]
-struct SvgPathBuilder(pub EcoString);
+struct SvgPathBuilder(pub EcoString, pub Ratio);
impl SvgPathBuilder {
+ fn with_scale(scale: Ratio) -> Self {
+ Self(EcoString::new(), scale)
+ }
+
+ fn scale(&self) -> f32 {
+ self.1.get() as f32
+ }
+
/// Create a rectangle path. The rectangle is created with the top-left
/// corner at (0, 0). The width and height are the size of the rectangle.
fn rect(&mut self, width: f32, height: f32) {
@@ -1193,34 +1336,63 @@ impl SvgPathBuilder {
sweep_flag: u32,
pos: (f32, f32),
) {
+ let scale = self.scale();
write!(
&mut self.0,
"A {rx} {ry} {x_axis_rot} {large_arc_flag} {sweep_flag} {x} {y} ",
- rx = radius.0,
- ry = radius.1,
- x = pos.0,
- y = pos.1,
+ rx = radius.0 * scale,
+ ry = radius.1 * scale,
+ x = pos.0 * scale,
+ y = pos.1 * scale,
)
.unwrap();
}
}
+impl Default for SvgPathBuilder {
+ fn default() -> Self {
+ Self(Default::default(), Ratio::one())
+ }
+}
+
/// A builder for SVG path. This is used to build the path for a glyph.
impl ttf_parser::OutlineBuilder for SvgPathBuilder {
fn move_to(&mut self, x: f32, y: f32) {
- write!(&mut self.0, "M {} {} ", x, y).unwrap();
+ let scale = self.scale();
+ write!(&mut self.0, "M {} {} ", x * scale, y * scale).unwrap();
}
fn line_to(&mut self, x: f32, y: f32) {
- write!(&mut self.0, "L {} {} ", x, y).unwrap();
+ let scale = self.scale();
+ write!(&mut self.0, "L {} {} ", x * scale, y * scale).unwrap();
}
fn quad_to(&mut self, x1: f32, y1: f32, x: f32, y: f32) {
- write!(&mut self.0, "Q {} {} {} {} ", x1, y1, x, y).unwrap();
+ let scale = self.scale();
+ write!(
+ &mut self.0,
+ "Q {} {} {} {} ",
+ x1 * scale,
+ y1 * scale,
+ x * scale,
+ y * scale
+ )
+ .unwrap();
}
fn curve_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, x: f32, y: f32) {
- write!(&mut self.0, "C {} {} {} {} {} {} ", x1, y1, x2, y2, x, y).unwrap();
+ let scale = self.scale();
+ write!(
+ &mut self.0,
+ "C {} {} {} {} {} {} ",
+ x1 * scale,
+ y1 * scale,
+ x2 * scale,
+ y2 * scale,
+ x * scale,
+ y * scale
+ )
+ .unwrap();
}
fn close(&mut self) {
diff --git a/crates/typst/src/eval/ops.rs b/crates/typst/src/eval/ops.rs
index 8f8e128a..cb830614 100644
--- a/crates/typst/src/eval/ops.rs
+++ b/crates/typst/src/eval/ops.rs
@@ -234,6 +234,15 @@ pub fn add(lhs: Value, rhs: Value) -> StrResult<Value> {
}
.into_value(),
+ (Pattern(pattern), Length(thickness)) | (Length(thickness), Pattern(pattern)) => {
+ Stroke {
+ paint: Smart::Custom(pattern.into()),
+ thickness: Smart::Custom(thickness),
+ ..Stroke::default()
+ }
+ .into_value()
+ }
+
(Duration(a), Duration(b)) => Duration(a + b),
(Datetime(a), Duration(b)) => Datetime(a + b),
(Duration(a), Datetime(b)) => Datetime(b + a),
diff --git a/crates/typst/src/foundations/value.rs b/crates/typst/src/foundations/value.rs
index b3141a16..a1660e85 100644
--- a/crates/typst/src/foundations/value.rs
+++ b/crates/typst/src/foundations/value.rs
@@ -21,7 +21,7 @@ use crate::layout::{Abs, Angle, Em, Fr, Length, Ratio, Rel};
use crate::symbols::Symbol;
use crate::syntax::{ast, Span};
use crate::text::{RawElem, TextElem};
-use crate::visualize::{Color, Gradient};
+use crate::visualize::{Color, Gradient, Pattern};
/// A computational value.
#[derive(Default, Clone)]
@@ -51,6 +51,8 @@ pub enum Value {
Color(Color),
/// A gradient value: `gradient.linear(...)`.
Gradient(Gradient),
+ /// A pattern fill: `pattern(...)`.
+ Pattern(Pattern),
/// A symbol: `arrow.l`.
Symbol(Symbol),
/// A version.
@@ -127,6 +129,7 @@ impl Value {
Self::Fraction(_) => Type::of::<Fr>(),
Self::Color(_) => Type::of::<Color>(),
Self::Gradient(_) => Type::of::<Gradient>(),
+ Self::Pattern(_) => Type::of::<Pattern>(),
Self::Symbol(_) => Type::of::<Symbol>(),
Self::Version(_) => Type::of::<Version>(),
Self::Str(_) => Type::of::<Str>(),
@@ -238,6 +241,7 @@ impl Debug for Value {
Self::Fraction(v) => Debug::fmt(v, f),
Self::Color(v) => Debug::fmt(v, f),
Self::Gradient(v) => Debug::fmt(v, f),
+ Self::Pattern(v) => Debug::fmt(v, f),
Self::Symbol(v) => Debug::fmt(v, f),
Self::Version(v) => Debug::fmt(v, f),
Self::Str(v) => Debug::fmt(v, f),
@@ -274,6 +278,7 @@ impl Repr for Value {
Self::Fraction(v) => v.repr(),
Self::Color(v) => v.repr(),
Self::Gradient(v) => v.repr(),
+ Self::Pattern(v) => v.repr(),
Self::Symbol(v) => v.repr(),
Self::Version(v) => v.repr(),
Self::Str(v) => v.repr(),
@@ -323,6 +328,7 @@ impl Hash for Value {
Self::Fraction(v) => v.hash(state),
Self::Color(v) => v.hash(state),
Self::Gradient(v) => v.hash(state),
+ Self::Pattern(v) => v.hash(state),
Self::Symbol(v) => v.hash(state),
Self::Version(v) => v.hash(state),
Self::Str(v) => v.hash(state),
@@ -635,6 +641,7 @@ primitive! { Rel<Length>: "relative length",
primitive! { Fr: "fraction", Fraction }
primitive! { Color: "color", Color }
primitive! { Gradient: "gradient", Gradient }
+primitive! { Pattern: "pattern", Pattern }
primitive! { Symbol: "symbol", Symbol }
primitive! { Version: "version", Version }
primitive! {
diff --git a/crates/typst/src/layout/axes.rs b/crates/typst/src/layout/axes.rs
index 585e6698..e5c47edd 100644
--- a/crates/typst/src/layout/axes.rs
+++ b/crates/typst/src/layout/axes.rs
@@ -306,6 +306,18 @@ cast! {
},
}
+cast! {
+ Axes<Length>,
+ self => array![self.x, self.y].into_value(),
+ array: Array => {
+ let mut iter = array.into_iter();
+ match (iter.next(), iter.next(), iter.next()) {
+ (Some(a), Some(b), None) => Axes::new(a.cast()?, b.cast()?),
+ _ => bail!("length array must contain exactly two entries"),
+ }
+ },
+}
+
impl<T: Resolve> Resolve for Axes<T> {
type Output = Axes<T::Output>;
diff --git a/crates/typst/src/text/mod.rs b/crates/typst/src/text/mod.rs
index b2a5a840..45de35f9 100644
--- a/crates/typst/src/text/mod.rs
+++ b/crates/typst/src/text/mod.rs
@@ -43,7 +43,7 @@ use crate::foundations::{
use crate::layout::{Abs, Axis, Dir, Length, Rel};
use crate::model::ParElem;
use crate::syntax::Spanned;
-use crate::visualize::{Color, GradientRelative, Paint};
+use crate::visualize::{Color, Paint, RelativeTo};
/// Text styling.
///
@@ -226,16 +226,14 @@ pub struct TextElem {
#[parse({
let paint: Option<Spanned<Paint>> = args.named_or_find("fill")?;
if let Some(paint) = &paint {
- if let Paint::Gradient(gradient) = &paint.v {
- if gradient.relative() == Smart::Custom(GradientRelative::Self_) {
- bail!(
- error!(
- paint.span,
- "gradients on text must be relative to the parent"
- )
- .with_hint("make sure to set `relative: auto` on your text fill")
- );
- }
+ if paint.v.relative() == Smart::Custom(RelativeTo::Self_) {
+ bail!(
+ error!(
+ paint.span,
+ "gradients and patterns on text must be relative to the parent"
+ )
+ .with_hint("make sure to set `relative: auto` on your text fill")
+ );
}
}
paint.map(|paint| paint.v)
diff --git a/crates/typst/src/visualize/gradient.rs b/crates/typst/src/visualize/gradient.rs
index bdd26c0d..4e804d9a 100644
--- a/crates/typst/src/visualize/gradient.rs
+++ b/crates/typst/src/visualize/gradient.rs
@@ -208,7 +208,7 @@ impl Gradient {
/// element.
#[named]
#[default(Smart::Auto)]
- relative: Smart<GradientRelative>,
+ relative: Smart<RelativeTo>,
/// The direction of the gradient.
#[external]
#[default(Dir::LTR)]
@@ -295,7 +295,7 @@ impl Gradient {
/// box, column, grid, or stack that contains the element.
#[named]
#[default(Smart::Auto)]
- relative: Smart<GradientRelative>,
+ relative: Smart<RelativeTo>,
/// The center of the end circle of the gradient.
///
/// A value of `{(50%, 50%)}` means that the end circle is
@@ -409,7 +409,7 @@ impl Gradient {
/// box, column, grid, or stack that contains the element.
#[named]
#[default(Smart::Auto)]
- relative: Smart<GradientRelative>,
+ relative: Smart<RelativeTo>,
/// The center of the last circle of the gradient.
///
/// A value of `{(50%, 50%)}` means that the end circle is
@@ -665,7 +665,7 @@ impl Gradient {
/// Returns the relative placement of this gradient.
#[func]
- pub fn relative(&self) -> Smart<GradientRelative> {
+ pub fn relative(&self) -> Smart<RelativeTo> {
match self {
Self::Linear(linear) => linear.relative,
Self::Radial(radial) => radial.relative,
@@ -718,7 +718,7 @@ impl Gradient {
impl Gradient {
/// Clones this gradient, but with a different relative placement.
- pub fn with_relative(mut self, relative: GradientRelative) -> Self {
+ pub fn with_relative(mut self, relative: RelativeTo) -> Self {
match &mut self {
Self::Linear(linear) => {
Arc::make_mut(linear).relative = Smart::Custom(relative);
@@ -815,12 +815,12 @@ impl Gradient {
/// Returns the relative placement of this gradient, handling
/// the special case of `auto`.
- pub fn unwrap_relative(&self, on_text: bool) -> GradientRelative {
+ pub fn unwrap_relative(&self, on_text: bool) -> RelativeTo {
self.relative().unwrap_or_else(|| {
if on_text {
- GradientRelative::Parent
+ RelativeTo::Parent
} else {
- GradientRelative::Self_
+ RelativeTo::Self_
}
})
}
@@ -870,7 +870,7 @@ pub struct LinearGradient {
/// The color space in which to interpolate the gradient.
pub space: ColorSpace,
/// The relative placement of the gradient.
- pub relative: Smart<GradientRelative>,
+ pub relative: Smart<RelativeTo>,
/// Whether to anti-alias the gradient (used for sharp gradients).
pub anti_alias: bool,
}
@@ -938,7 +938,7 @@ pub struct RadialGradient {
/// The color space in which to interpolate the gradient.
pub space: ColorSpace,
/// The relative placement of the gradient.
- pub relative: Smart<GradientRelative>,
+ pub relative: Smart<RelativeTo>,
/// Whether to anti-alias the gradient (used for sharp gradients).
pub anti_alias: bool,
}
@@ -1016,7 +1016,7 @@ pub struct ConicGradient {
/// The color space in which to interpolate the gradient.
pub space: ColorSpace,
/// The relative placement of the gradient.
- pub relative: Smart<GradientRelative>,
+ pub relative: Smart<RelativeTo>,
/// Whether to anti-alias the gradient (used for sharp gradients).
pub anti_alias: bool,
}
@@ -1070,7 +1070,7 @@ impl Repr for ConicGradient {
/// What is the gradient relative to.
#[derive(Cast, Debug, Clone, Copy, PartialEq, Eq, Hash)]
-pub enum GradientRelative {
+pub enum RelativeTo {
/// The gradient is relative to itself (its own bounding box).
Self_,
/// The gradient is relative to its parent (the parent's bounding box).
diff --git a/crates/typst/src/visualize/mod.rs b/crates/typst/src/visualize/mod.rs
index e733e5a4..744e4e85 100644
--- a/crates/typst/src/visualize/mod.rs
+++ b/crates/typst/src/visualize/mod.rs
@@ -6,6 +6,7 @@ mod image;
mod line;
mod paint;
mod path;
+mod pattern;
mod polygon;
mod shape;
mod stroke;
@@ -16,6 +17,7 @@ pub use self::image::*;
pub use self::line::*;
pub use self::paint::*;
pub use self::path::*;
+pub use self::pattern::*;
pub use self::polygon::*;
pub use self::shape::*;
pub use self::stroke::*;
@@ -35,6 +37,7 @@ pub(super) fn define(global: &mut Scope) {
global.category(VISUALIZE);
global.define_type::<Color>();
global.define_type::<Gradient>();
+ global.define_type::<Pattern>();
global.define_type::<Stroke>();
global.define_elem::<ImageElem>();
global.define_elem::<LineElem>();
diff --git a/crates/typst/src/visualize/paint.rs b/crates/typst/src/visualize/paint.rs
index ca5d0d40..cd1006aa 100644
--- a/crates/typst/src/visualize/paint.rs
+++ b/crates/typst/src/visualize/paint.rs
@@ -2,8 +2,8 @@ use std::fmt::{self, Debug, Formatter};
use ecow::EcoString;
-use crate::foundations::{cast, Repr};
-use crate::visualize::{Color, Gradient, GradientRelative};
+use crate::foundations::{cast, Repr, Smart};
+use crate::visualize::{Color, Gradient, Pattern, RelativeTo};
/// How a fill or stroke should be painted.
#[derive(Clone, Eq, PartialEq, Hash)]
@@ -12,6 +12,8 @@ pub enum Paint {
Solid(Color),
/// A gradient.
Gradient(Gradient),
+ /// A pattern.
+ Pattern(Pattern),
}
impl Paint {
@@ -19,19 +21,31 @@ impl Paint {
pub fn unwrap_solid(&self) -> Color {
match self {
Self::Solid(color) => *color,
- Self::Gradient(_) => panic!("expected solid color"),
+ Self::Gradient(_) | Self::Pattern(_) => panic!("expected solid color"),
+ }
+ }
+
+ /// Gets the relative coordinate system for this paint.
+ pub fn relative(&self) -> Smart<RelativeTo> {
+ match self {
+ Self::Solid(_) => Smart::Auto,
+ Self::Gradient(gradient) => gradient.relative(),
+ Self::Pattern(pattern) => pattern.relative(),
}
}
/// Turns this paint into a paint for a text decoration.
///
/// If this paint is a gradient, it will be converted to a gradient with
- /// relative set to [`GradientRelative::Parent`].
+ /// relative set to [`RelativeTo::Parent`].
pub fn as_decoration(&self) -> Self {
match self {
Self::Solid(color) => Self::Solid(*color),
Self::Gradient(gradient) => {
- Self::Gradient(gradient.clone().with_relative(GradientRelative::Parent))
+ Self::Gradient(gradient.clone().with_relative(RelativeTo::Parent))
+ }
+ Self::Pattern(pattern) => {
+ Self::Pattern(pattern.clone().with_relative(RelativeTo::Parent))
}
}
}
@@ -42,15 +56,23 @@ impl Debug for Paint {
match self {
Self::Solid(v) => v.fmt(f),
Self::Gradient(v) => v.fmt(f),
+ Self::Pattern(v) => v.fmt(f),
}
}
}
+impl From<Pattern> for Paint {
+ fn from(pattern: Pattern) -> Self {
+ Self::Pattern(pattern)
+ }
+}
+
impl Repr for Paint {
fn repr(&self) -> EcoString {
match self {
Self::Solid(color) => color.repr(),
Self::Gradient(gradient) => gradient.repr(),
+ Self::Pattern(pattern) => pattern.repr(),
}
}
}
@@ -72,7 +94,9 @@ cast! {
self => match self {
Self::Solid(color) => color.into_value(),
Self::Gradient(gradient) => gradient.into_value(),
+ Self::Pattern(pattern) => pattern.into_value(),
},
color: Color => Self::Solid(color),
gradient: Gradient => Self::Gradient(gradient),
+ pattern: Pattern => Self::Pattern(pattern),
}
diff --git a/crates/typst/src/visualize/pattern.rs b/crates/typst/src/visualize/pattern.rs
new file mode 100644
index 00000000..55d68500
--- /dev/null
+++ b/crates/typst/src/visualize/pattern.rs
@@ -0,0 +1,288 @@
+use std::hash::Hash;
+use std::sync::Arc;
+
+use comemo::Prehashed;
+use ecow::{eco_format, EcoString};
+
+use crate::diag::{bail, error, SourceResult};
+use crate::eval::Vm;
+use crate::foundations::{func, scope, ty, Content, Repr, Smart, StyleChain};
+use crate::layout::{Abs, Axes, Em, Frame, Layout, Length, Regions, Size};
+use crate::syntax::{Span, Spanned};
+use crate::util::Numeric;
+use crate::visualize::RelativeTo;
+use crate::World;
+
+/// A repeating pattern fill.
+///
+/// Typst supports the most common pattern type of tiled patterns, where a
+/// pattern is repeated in a grid-like fashion. The pattern is defined by a
+/// body and a tile size. The tile size is the size of each cell of the pattern.
+/// The body is the content of each cell of the pattern. The pattern is
+/// repeated in a grid-like fashion covering the entire area of the element
+/// being filled. You can also specify a spacing between the cells of the
+/// pattern, which is defined by a horizontal and vertical spacing. The spacing
+/// is the distance between the edges of adjacent cells of the pattern. The default
+/// spacing is zero.
+///
+/// # Examples
+///
+/// ```example
+/// #let pat = pattern(size: (30pt, 30pt))[
+/// #place(top + left, line(start: (0%, 0%), end: (100%, 100%), stroke: 1pt))
+/// #place(top + left, line(start: (0%, 100%), end: (100%, 0%), stroke: 1pt))
+/// ]
+///
+/// #rect(fill: pat, width: 100%, height: 100%, stroke: 1pt)
+/// ```
+///
+/// Patterns are also supported on text, but only when setting the
+/// [relativeness]($pattern.relative) to either `{auto}` (the default value) or
+/// `{"parent"}`. To create word-by-word or glyph-by-glyph patterns, you can
+/// wrap the words or characters of your text in [boxes]($box) manually or
+/// through a [show rule]($styling/#show-rules).
+///
+/// ```example
+/// #let pat = pattern(
+/// size: (30pt, 30pt),
+/// relative: "parent",
+/// square(size: 30pt, fill: gradient.conic(..color.map.rainbow))
+/// )
+///
+/// #set text(fill: pat)
+/// #lorem(10)
+/// ```
+///
+/// You can also space the elements further or closer apart using the
+/// [`spacing`]($pattern.spacing) feature of the pattern. If the spacing
+/// is lower than the size of the pattern, the pattern will overlap.
+/// If it is higher, the pattern will have gaps of the same color as the
+/// background of the pattern.
+///
+/// ```example
+/// #let pat = pattern(
+/// size: (30pt, 30pt),
+/// spacing: (10pt, 10pt),
+/// relative: "parent",
+/// square(size: 30pt, fill: gradient.conic(..color.map.rainbow))
+/// )
+///
+/// #rect(width: 100%, height: 100%, fill: pat)
+/// ```
+///
+/// # Relativeness
+/// The location of the starting point of the pattern is dependant on the
+/// dimensions of a container. This container can either be the shape they
+/// are painted on, or the closest surrounding container. This is controlled by
+/// the `relative` argument of a pattern constructor. By default, patterns are
+/// relative to the shape they are painted on, unless the pattern is applied on
+/// text, in which case they are relative to the closest ancestor container.
+///
+/// Typst determines the ancestor container as follows:
+/// - For shapes that are placed at the root/top level of the document, the
+/// closest ancestor is the page itself.
+/// - For other shapes, the ancestor is the innermost [`block`]($block) or
+/// [`box`]($box) that contains the shape. This includes the boxes and blocks
+/// that are implicitly created by show rules and elements. For example, a
+/// [`rotate`]($rotate) will not affect the parent of a gradient, but a
+/// [`grid`]($grid) will.
+#[ty(scope)]
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
+pub struct Pattern(Arc<PatternRepr>);
+
+/// Internal representation of [`Pattern`].
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
+struct PatternRepr {
+ /// The body of the pattern
+ body: Prehashed<Content>,
+ /// The pattern's rendered content.
+ frame: Prehashed<Frame>,
+ /// The pattern's tile size.
+ size: Size,
+ /// The pattern's tile spacing.
+ spacing: Size,
+ /// The pattern's relative transform.
+ relative: Smart<RelativeTo>,
+}
+
+#[scope]
+impl Pattern {
+ /// Construct a new pattern.
+ ///
+ /// ```example
+ /// #let pat = pattern(
+ /// size: (20pt, 20pt),
+ /// relative: "parent",
+ /// place(dx: 5pt, dy: 5pt, rotate(45deg, square(size: 5pt, fill: black)))
+ /// )
+ ///
+ /// #rect(width: 100%, height: 100%, fill: pat)
+ /// ```
+ #[func(constructor)]
+ pub fn construct(
+ vm: &mut Vm,
+ /// The bounding box of each cell of the pattern.
+ #[named]
+ #[default(Spanned::new(Smart::Auto, Span::detached()))]
+ size: Spanned<Smart<Axes<Length>>>,
+ /// The spacing between cells of the pattern.
+ #[named]
+ #[default(Spanned::new(Axes::splat(Length::zero()), Span::detached()))]
+ spacing: Spanned<Axes<Length>>,
+ /// The [relative placement](#relativeness) of the pattern.
+ ///
+ /// For an element placed at the root/top level of the document, the
+ /// parent is the page itself. For other elements, the parent is the
+ /// innermost block, box, column, grid, or stack that contains the
+ /// element.
+ #[named]
+ #[default(Smart::Auto)]
+ relative: Smart<RelativeTo>,
+ /// The content of each cell of the pattern.
+ body: Content,
+ ) -> SourceResult<Pattern> {
+ let span = size.span;
+ if let Smart::Custom(size) = size.v {
+ // Ensure that sizes are absolute.
+ if !size.x.em.is_zero() || !size.y.em.is_zero() {
+ bail!(span, "pattern tile size must be absolute");
+ }
+
+ // Ensure that sizes are non-zero and finite.
+ if size.x.is_zero()
+ || size.y.is_zero()
+ || !size.x.is_finite()
+ || !size.y.is_finite()
+ {
+ bail!(span, "pattern tile size must be non-zero and non-infinite");
+ }
+ }
+
+ // Ensure that spacing is absolute.
+ if !spacing.v.x.em.is_zero() || !spacing.v.y.em.is_zero() {
+ bail!(spacing.span, "pattern tile spacing must be absolute");
+ }
+
+ // Ensure that spacing is finite.
+ if !spacing.v.x.is_finite() || !spacing.v.y.is_finite() {
+ bail!(spacing.span, "pattern tile spacing must be finite");
+ }
+
+ // The size of the frame
+ let size = size.v.map(|l| l.map(|a| a.abs));
+ let region = size.unwrap_or_else(|| Axes::splat(Abs::inf()));
+
+ // Layout the pattern.
+ let world = vm.vt.world;
+ let library = world.library();
+ let styles = StyleChain::new(&library.styles);
+ let pod = Regions::one(region, Axes::splat(false));
+ let mut frame = body.layout(&mut vm.vt, styles, pod)?.into_frame();
+
+ // Check that the frame is non-zero.
+ if size.is_auto() && frame.size().is_zero() {
+ bail!(error!(span, "pattern tile size must be non-zero")
+ .with_hint("try setting the size manually"));
+ }
+
+ // Set the size of the frame if the size is enforced.
+ if let Smart::Custom(size) = size {
+ frame.set_size(size);
+ }
+
+ Ok(Self(Arc::new(PatternRepr {
+ size: frame.size(),
+ body: Prehashed::new(body),
+ frame: Prehashed::new(frame),
+ spacing: spacing.v.map(|l| l.abs),
+ relative,
+ })))
+ }
+
+ /// Returns the content of an individual tile of the pattern.
+ #[func]
+ pub fn body(&self) -> Content {
+ self.0.body.clone().into_inner()
+ }
+
+ /// Returns the size of an individual tile of the pattern.
+ #[func]
+ pub fn size(&self) -> Axes<Length> {
+ self.0.size.map(|l| Length { abs: l, em: Em::zero() })
+ }
+
+ /// Returns the spacing between tiles of the pattern.
+ #[func]
+ pub fn spacing(&self) -> Axes<Length> {
+ self.0.spacing.map(|l| Length { abs: l, em: Em::zero() })
+ }
+
+ /// Returns the relative placement of the pattern.
+ #[func]
+ pub fn relative(&self) -> Smart<RelativeTo> {
+ self.0.relative
+ }
+}
+
+impl Pattern {
+ /// Set the relative placement of the pattern.
+ pub fn with_relative(mut self, relative: RelativeTo) -> Self {
+ if let Some(this) = Arc::get_mut(&mut self.0) {
+ this.relative = Smart::Custom(relative);
+ } else {
+ self.0 = Arc::new(PatternRepr {
+ relative: Smart::Custom(relative),
+ ..self.0.as_ref().clone()
+ });
+ }
+
+ self
+ }
+
+ /// Returns the relative placement of the pattern.
+ pub fn unwrap_relative(&self, on_text: bool) -> RelativeTo {
+ self.0.relative.unwrap_or_else(|| {
+ if on_text {
+ RelativeTo::Parent
+ } else {
+ RelativeTo::Self_
+ }
+ })
+ }
+
+ /// Return the size of the pattern in absolute units.
+ pub fn size_abs(&self) -> Size {
+ self.0.size
+ }
+
+ /// Return the spacing of the pattern in absolute units.
+ pub fn spacing_abs(&self) -> Size {
+ self.0.spacing
+ }
+
+ /// Return the frame of the pattern.
+ pub fn frame(&self) -> &Frame {
+ &self.0.frame
+ }
+}
+
+impl Repr for Pattern {
+ fn repr(&self) -> EcoString {
+ let mut out =
+ eco_format!("pattern(({}, {})", self.0.size.x.repr(), self.0.size.y.repr());
+
+ if self.spacing() != Axes::splat(Length::zero()) {
+ out.push_str(", spacing: (");
+ out.push_str(&self.0.spacing.x.repr());
+ out.push_str(", ");
+ out.push_str(&self.0.spacing.y.repr());
+ out.push(')');
+ }
+
+ out.push_str(", ");
+ out.push_str(&self.0.body.repr());
+ out.push(')');
+
+ out
+ }
+}
diff --git a/crates/typst/src/visualize/stroke.rs b/crates/typst/src/visualize/stroke.rs
index 3a90c3b9..cc93cee7 100644
--- a/crates/typst/src/visualize/stroke.rs
+++ b/crates/typst/src/visualize/stroke.rs
@@ -7,7 +7,7 @@ use crate::foundations::{
};
use crate::layout::{Abs, Length};
use crate::util::{Numeric, Scalar};
-use crate::visualize::{Color, Gradient, Paint};
+use crate::visualize::{Color, Gradient, Paint, Pattern};
/// Defines how to draw a line.
///
@@ -381,6 +381,10 @@ cast! {
paint: Smart::Custom(gradient.into()),
..Default::default()
},
+ pattern: Pattern => Self {
+ paint: Smart::Custom(pattern.into()),
+ ..Default::default()
+ },
mut dict: Dict => {
// Get a value by key, accepting either Auto or something convertible to type T.
fn take<T: FromValue>(dict: &mut Dict, key: &str) -> StrResult<Smart<T>> {
diff --git a/tests/ref/visualize/pattern-relative.png b/tests/ref/visualize/pattern-relative.png
new file mode 100644
index 00000000..7958bf7f
--- /dev/null
+++ b/tests/ref/visualize/pattern-relative.png
Binary files differ
diff --git a/tests/ref/visualize/pattern-small.png b/tests/ref/visualize/pattern-small.png
new file mode 100644
index 00000000..6af592dd
--- /dev/null
+++ b/tests/ref/visualize/pattern-small.png
Binary files differ
diff --git a/tests/ref/visualize/pattern-spacing.png b/tests/ref/visualize/pattern-spacing.png
new file mode 100644
index 00000000..4c95a3b0
--- /dev/null
+++ b/tests/ref/visualize/pattern-spacing.png
Binary files differ
diff --git a/tests/ref/visualize/pattern-stroke.png b/tests/ref/visualize/pattern-stroke.png
new file mode 100644
index 00000000..d71f1c92
--- /dev/null
+++ b/tests/ref/visualize/pattern-stroke.png
Binary files differ
diff --git a/tests/ref/visualize/pattern-text.png b/tests/ref/visualize/pattern-text.png
new file mode 100644
index 00000000..2ecf2fda
--- /dev/null
+++ b/tests/ref/visualize/pattern-text.png
Binary files differ
diff --git a/tests/typ/layout/table.typ b/tests/typ/layout/table.typ
index 5ddc8503..529f2720 100644
--- a/tests/typ/layout/table.typ
+++ b/tests/typ/layout/table.typ
@@ -66,5 +66,5 @@
#table()
---
-// Error: 14-19 expected color, gradient, none, array, or function, found string
+// Error: 14-19 expected color, gradient, pattern, none, array, or function, found string
#table(fill: "hey")
diff --git a/tests/typ/visualize/gradient-text.typ b/tests/typ/visualize/gradient-text.typ
index a233ba6b..671172e1 100644
--- a/tests/typ/visualize/gradient-text.typ
+++ b/tests/typ/visualize/gradient-text.typ
@@ -9,7 +9,7 @@
// Make sure they don't work when `relative: "self"`.
// Hint: 17-61 make sure to set `relative: auto` on your text fill
-// Error: 17-61 gradients on text must be relative to the parent
+// Error: 17-61 gradients and patterns on text must be relative to the parent
#set text(fill: gradient.linear(red, blue, relative: "self"))
---
diff --git a/tests/typ/visualize/pattern-relative.typ b/tests/typ/visualize/pattern-relative.typ
new file mode 100644
index 00000000..78517e1e
--- /dev/null
+++ b/tests/typ/visualize/pattern-relative.typ
@@ -0,0 +1,23 @@
+// Test pattern with different `relative`.
+
+---
+// Test with relative set to `"self"`
+#let pat(..args) = pattern(size: (30pt, 30pt), ..args)[
+ #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)
+
+---
+// Test with relative set to `"parent"`
+#let pat(..args) = pattern(size: (30pt, 30pt), ..args)[
+ #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: "parent"), width: 100%, height: 100%, stroke: 1pt)
diff --git a/tests/typ/visualize/pattern-small.typ b/tests/typ/visualize/pattern-small.typ
new file mode 100644
index 00000000..888cfee1
--- /dev/null
+++ b/tests/typ/visualize/pattern-small.typ
@@ -0,0 +1,14 @@
+// Tests small patterns for pixel accuracy.
+
+---
+#box(
+ width: 8pt,
+ height: 1pt,
+ fill: pattern(size: (1pt, 1pt), square(size: 1pt, fill: black))
+)
+#v(-1em)
+#box(
+ width: 8pt,
+ height: 1pt,
+ fill: pattern(size: (2pt, 1pt), square(size: 1pt, fill: black))
+)
diff --git a/tests/typ/visualize/pattern-spacing.typ b/tests/typ/visualize/pattern-spacing.typ
new file mode 100644
index 00000000..f8f5f9fd
--- /dev/null
+++ b/tests/typ/visualize/pattern-spacing.typ
@@ -0,0 +1,31 @@
+// Test pattern with different `spacing`.
+
+---
+// Test with spacing set to `(-10pt, -10pt)`
+#let pat(..args) = pattern(size: (30pt, 30pt), ..args)[
+ #square(width: 100%, height: 100%, stroke: 1pt, fill: blue)
+]
+
+#set page(width: 100pt, height: 100pt)
+
+#rect(fill: pat(spacing: (-10pt, -10pt)), width: 100%, height: 100%, stroke: 1pt)
+
+---
+// Test with spacing set to `(0pt, 0pt)`
+#let pat(..args) = pattern(size: (30pt, 30pt), ..args)[
+ #square(width: 100%, height: 100%, stroke: 1pt, fill: blue)
+]
+
+#set page(width: 100pt, height: 100pt)
+
+#rect(fill: pat(spacing: (0pt, 0pt)), width: 100%, height: 100%, stroke: 1pt)
+
+---
+// Test with spacing set to `(10pt, 10pt)`
+#let pat(..args) = pattern(size: (30pt, 30pt), ..args)[
+ #square(width: 100%, height: 100%, stroke: 1pt, fill: blue)
+]
+
+#set page(width: 100pt, height: 100pt)
+
+#rect(fill: pat(spacing: (10pt, 10pt,)), width: 100%, height: 100%, stroke: 1pt)
diff --git a/tests/typ/visualize/pattern-stroke.typ b/tests/typ/visualize/pattern-stroke.typ
new file mode 100644
index 00000000..3cc43a70
--- /dev/null
+++ b/tests/typ/visualize/pattern-stroke.typ
@@ -0,0 +1,13 @@
+// Test pattern on strokes
+
+---
+#align(
+ center + top,
+ square(
+ size: 50pt,
+ stroke: 5pt + pattern(
+ size: (5pt, 5pt),
+ align(horizon + center, circle(fill: blue, radius: 2.5pt))
+ )
+ )
+)
diff --git a/tests/typ/visualize/pattern-text.typ b/tests/typ/visualize/pattern-text.typ
new file mode 100644
index 00000000..a9fbfb37
--- /dev/null
+++ b/tests/typ/visualize/pattern-text.typ
@@ -0,0 +1,28 @@
+// Test a pattern on some text
+
+---
+// You shouldn't be able to see the text, if you can then
+// that means that the transform matrices are not being
+// applied to the text correctly.
+#let pat = pattern(
+ size: (30pt, 30pt),
+ relative: "parent",
+ square(size: 30pt, fill: gradient.conic(..color.map.rainbow))
+);
+
+#set page(
+ width: 140pt,
+ height: 140pt,
+ fill: pat
+)
+
+#rotate(45deg, scale(x: 50%, y: 70%, rect(
+ width: 100%,
+ height: 100%,
+ stroke: 1pt,
+)[
+ #lorem(10)
+
+ #set text(fill: pat)
+ #lorem(10)
+]))
diff --git a/tests/typ/visualize/shape-rect.typ b/tests/typ/visualize/shape-rect.typ
index 6447b55d..ea0e66b0 100644
--- a/tests/typ/visualize/shape-rect.typ
+++ b/tests/typ/visualize/shape-rect.typ
@@ -51,7 +51,7 @@
#rect(radius: (left: 10pt, cake: 5pt))
---
-// Error: 15-21 expected length, color, gradient, dictionary, stroke, none, or auto, found array
+// Error: 15-21 expected length, color, gradient, pattern, dictionary, stroke, none, or auto, found array
#rect(stroke: (1, 2))
---