diff --git a/crates/typst-pdf/src/color.rs b/crates/typst-pdf/src/color.rs index 17c4686af..d7781b352 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 5e7e5f3d9..b12ac53fa 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 c753315c6..005b5a9e0 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, /// The IDs of written gradients. gradient_refs: Vec, + /// The IDs of written patterns. + pattern_refs: Vec, /// The IDs of written external graphics states. ext_gs_refs: Vec, /// Handles color space writing. @@ -110,6 +116,8 @@ struct PdfContext<'a> { image_deferred_map: HashMap>, /// Deduplicates gradients used across the document. gradient_map: Remapper, + /// Deduplicates patterns used across the document. + pattern_map: Remapper, /// Deduplicates external graphics states used across the document. extg_map: Remapper, } @@ -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 { 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> { + Arc::new(deflate(content)) +} + /// Create a base64-encoded hash of the value. fn hash_base64(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 545380dac..1bbef7af6 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> { - 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, + /// The page's used resources + pub resources: HashMap, +} + +/// 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, } /// 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 000000000..7b9e27192 --- /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, + /// 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 251f647a1..5c1b8482e 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( 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( } 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> { /// 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 7d3a773d8..205cff148 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, + /// 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, /// 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, /// These are the gradients that compose a conic gradient. conic_subgradients: Deduplicator, } @@ -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, +} + /// 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::>() + { + 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 { - let mut builder = SvgPathBuilder::default(); +fn convert_outline_glyph_to_path( + font: &Font, + id: GlyphId, + scale: Ratio, +) -> Option { + 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 8f8e128a7..cb830614e 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 { } .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 b3141a16c..a1660e85d 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::(), Self::Color(_) => Type::of::(), Self::Gradient(_) => Type::of::(), + Self::Pattern(_) => Type::of::(), Self::Symbol(_) => Type::of::(), Self::Version(_) => Type::of::(), Self::Str(_) => Type::of::(), @@ -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: "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 585e66988..e5c47edd6 100644 --- a/crates/typst/src/layout/axes.rs +++ b/crates/typst/src/layout/axes.rs @@ -306,6 +306,18 @@ cast! { }, } +cast! { + Axes, + 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 Resolve for Axes { type Output = Axes; diff --git a/crates/typst/src/text/mod.rs b/crates/typst/src/text/mod.rs index b2a5a840d..45de35f9a 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> = 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 bdd26c0d4..4e804d9ac 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, + relative: Smart, /// 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, + relative: Smart, /// 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, + relative: Smart, /// 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 { + pub fn relative(&self) -> Smart { 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, + pub relative: Smart, /// 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, + pub relative: Smart, /// 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, + pub relative: Smart, /// 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 e733e5a41..744e4e85e 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::(); global.define_type::(); + global.define_type::(); global.define_type::(); global.define_elem::(); global.define_elem::(); diff --git a/crates/typst/src/visualize/paint.rs b/crates/typst/src/visualize/paint.rs index ca5d0d40a..cd1006aa3 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 { + 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 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 000000000..55d685006 --- /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); + +/// Internal representation of [`Pattern`]. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +struct PatternRepr { + /// The body of the pattern + body: Prehashed, + /// The pattern's rendered content. + frame: Prehashed, + /// The pattern's tile size. + size: Size, + /// The pattern's tile spacing. + spacing: Size, + /// The pattern's relative transform. + relative: Smart, +} + +#[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>>, + /// The spacing between cells of the pattern. + #[named] + #[default(Spanned::new(Axes::splat(Length::zero()), Span::detached()))] + spacing: Spanned>, + /// 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, + /// The content of each cell of the pattern. + body: Content, + ) -> SourceResult { + 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 { + 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 { + self.0.spacing.map(|l| Length { abs: l, em: Em::zero() }) + } + + /// Returns the relative placement of the pattern. + #[func] + pub fn relative(&self) -> Smart { + 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 3a90c3b95..cc93cee7a 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(dict: &mut Dict, key: &str) -> StrResult> { diff --git a/tests/ref/visualize/pattern-relative.png b/tests/ref/visualize/pattern-relative.png new file mode 100644 index 000000000..7958bf7f6 Binary files /dev/null and b/tests/ref/visualize/pattern-relative.png differ diff --git a/tests/ref/visualize/pattern-small.png b/tests/ref/visualize/pattern-small.png new file mode 100644 index 000000000..6af592dd8 Binary files /dev/null and b/tests/ref/visualize/pattern-small.png differ diff --git a/tests/ref/visualize/pattern-spacing.png b/tests/ref/visualize/pattern-spacing.png new file mode 100644 index 000000000..4c95a3b02 Binary files /dev/null and b/tests/ref/visualize/pattern-spacing.png differ diff --git a/tests/ref/visualize/pattern-stroke.png b/tests/ref/visualize/pattern-stroke.png new file mode 100644 index 000000000..d71f1c920 Binary files /dev/null and b/tests/ref/visualize/pattern-stroke.png differ diff --git a/tests/ref/visualize/pattern-text.png b/tests/ref/visualize/pattern-text.png new file mode 100644 index 000000000..2ecf2fdab Binary files /dev/null and b/tests/ref/visualize/pattern-text.png differ diff --git a/tests/typ/layout/table.typ b/tests/typ/layout/table.typ index 5ddc8503f..529f27201 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 a233ba6b9..671172e10 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 000000000..78517e1e1 --- /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 000000000..888cfee1c --- /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 000000000..f8f5f9fdf --- /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 000000000..3cc43a704 --- /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 000000000..a9fbfb37b --- /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 6447b55d4..ea0e66b0e 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)) ---