Gradient Part 6 - Pattern fills (#2740)

This commit is contained in:
Sébastien d'Herbais de Thun 2023-11-24 15:46:20 +01:00 committed by GitHub
parent 3d2f1d2d6c
commit 1756718bab
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 1145 additions and 168 deletions

View File

@ -294,6 +294,7 @@ impl PaintEncode for Paint {
match self { match self {
Self::Solid(c) => c.set_as_fill(ctx, on_text, transforms), Self::Solid(c) => c.set_as_fill(ctx, on_text, transforms),
Self::Gradient(gradient) => gradient.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 { match self {
Self::Solid(c) => c.set_as_stroke(ctx, transforms), Self::Solid(c) => c.set_as_stroke(ctx, transforms),
Self::Gradient(gradient) => gradient.set_as_stroke(ctx, transforms), Self::Gradient(gradient) => gradient.set_as_stroke(ctx, transforms),
Self::Pattern(pattern) => pattern.set_as_stroke(ctx, transforms),
} }
} }
} }

View File

@ -1,19 +1,19 @@
use std::f32::consts::{PI, TAU}; use std::f32::consts::{PI, TAU};
use std::sync::Arc; use std::sync::Arc;
use ecow::{eco_format, EcoString}; use ecow::eco_format;
use pdf_writer::types::{ColorSpaceOperand, FunctionShadingType}; use pdf_writer::types::{ColorSpaceOperand, FunctionShadingType};
use pdf_writer::writers::StreamShadingType; use pdf_writer::writers::StreamShadingType;
use pdf_writer::{Filter, Finish, Name, Ref}; use pdf_writer::{Filter, Finish, Name, Ref};
use typst::layout::{Abs, Angle, Point, Quadrant, Ratio, Transform}; use typst::layout::{Abs, Angle, Point, Quadrant, Ratio, Transform};
use typst::util::Numeric; use typst::util::Numeric;
use typst::visualize::{ use typst::visualize::{
Color, ColorSpace, ConicGradient, Gradient, GradientRelative, WeightedColor, Color, ColorSpace, ConicGradient, Gradient, RelativeTo, WeightedColor,
}; };
use crate::color::{ColorSpaceExt, PaintEncode, QuantizedColor}; use crate::color::{ColorSpaceExt, PaintEncode, QuantizedColor};
use crate::page::{PageContext, Transforms}; use crate::page::{PageContext, PageResource, ResourceKind, Transforms};
use crate::{deflate, AbsExt, PdfContext}; use crate::{deflate, transform_to_array, AbsExt, PdfContext};
/// A unique-transform-aspect-ratio combination that will be encoded into the /// A unique-transform-aspect-ratio combination that will be encoded into the
/// PDF. /// PDF.
@ -268,21 +268,27 @@ impl PaintEncode for Gradient {
fn set_as_fill(&self, ctx: &mut PageContext, on_text: bool, transforms: Transforms) { fn set_as_fill(&self, ctx: &mut PageContext, on_text: bool, transforms: Transforms) {
ctx.reset_fill_color_space(); 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()); let name = Name(id.as_bytes());
ctx.content.set_fill_color_space(ColorSpaceOperand::Pattern); ctx.content.set_fill_color_space(ColorSpaceOperand::Pattern);
ctx.content.set_fill_pattern(None, name); 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) { fn set_as_stroke(&self, ctx: &mut PageContext, transforms: Transforms) {
ctx.reset_stroke_color_space(); 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()); let name = Name(id.as_bytes());
ctx.content.set_stroke_color_space(ColorSpaceOperand::Pattern); ctx.content.set_stroke_color_space(ColorSpaceOperand::Pattern);
ctx.content.set_stroke_pattern(None, name); 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, gradient: &Gradient,
on_text: bool, on_text: bool,
mut transforms: Transforms, mut transforms: Transforms,
) -> EcoString { ) -> usize {
// Edge cases for strokes. // Edge cases for strokes.
if transforms.size.x.is_zero() { if transforms.size.x.is_zero() {
transforms.size.x = Abs::pt(1.0); transforms.size.x = Abs::pt(1.0);
@ -302,8 +308,8 @@ fn register_gradient(
transforms.size.y = Abs::pt(1.0); transforms.size.y = Abs::pt(1.0);
} }
let size = match gradient.unwrap_relative(on_text) { let size = match gradient.unwrap_relative(on_text) {
GradientRelative::Self_ => transforms.size, RelativeTo::Self_ => transforms.size,
GradientRelative::Parent => transforms.container_size, RelativeTo::Parent => transforms.container_size,
}; };
let (offset_x, offset_y) = match gradient { let (offset_x, offset_y) = match gradient {
@ -317,8 +323,8 @@ fn register_gradient(
let rotation = gradient.angle().unwrap_or_else(Angle::zero); let rotation = gradient.angle().unwrap_or_else(Angle::zero);
let transform = match gradient.unwrap_relative(on_text) { let transform = match gradient.unwrap_relative(on_text) {
GradientRelative::Self_ => transforms.transform, RelativeTo::Self_ => transforms.transform,
GradientRelative::Parent => transforms.container_transform, RelativeTo::Parent => transforms.container_transform,
}; };
let scale_offset = match gradient { let scale_offset = match gradient {
@ -341,20 +347,7 @@ fn register_gradient(
angle: Gradient::correct_aspect_ratio(rotation, size.aspect_ratio()), angle: Gradient::correct_aspect_ratio(rotation, size.aspect_ratio()),
}; };
let index = ctx.parent.gradient_map.insert(pdf_gradient); 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(),
]
} }
/// Writes a single Coons Patch as defined in the PDF specification /// Writes a single Coons Patch as defined in the PDF specification

View File

@ -7,10 +7,12 @@ mod gradient;
mod image; mod image;
mod outline; mod outline;
mod page; mod page;
mod pattern;
use std::cmp::Eq; use std::cmp::Eq;
use std::collections::{BTreeMap, HashMap}; use std::collections::{BTreeMap, HashMap};
use std::hash::Hash; use std::hash::Hash;
use std::sync::Arc;
use base64::Engine; use base64::Engine;
use ecow::{eco_format, EcoString}; use ecow::{eco_format, EcoString};
@ -18,7 +20,7 @@ use pdf_writer::types::Direction;
use pdf_writer::{Finish, Name, Pdf, Ref, TextStr}; use pdf_writer::{Finish, Name, Pdf, Ref, TextStr};
use typst::foundations::Datetime; use typst::foundations::Datetime;
use typst::introspection::Introspector; use typst::introspection::Introspector;
use typst::layout::{Abs, Dir, Em}; use typst::layout::{Abs, Dir, Em, Transform};
use typst::model::Document; use typst::model::Document;
use typst::text::{Font, Lang}; use typst::text::{Font, Lang};
use typst::util::Deferred; use typst::util::Deferred;
@ -30,6 +32,7 @@ use crate::extg::ExtGState;
use crate::gradient::PdfGradient; use crate::gradient::PdfGradient;
use crate::image::EncodedImage; use crate::image::EncodedImage;
use crate::page::Page; use crate::page::Page;
use crate::pattern::PdfPattern;
/// Export a document into a PDF file. /// Export a document into a PDF file.
/// ///
@ -57,6 +60,7 @@ pub fn pdf(
image::write_images(&mut ctx); image::write_images(&mut ctx);
gradient::write_gradients(&mut ctx); gradient::write_gradients(&mut ctx);
extg::write_external_graphics_states(&mut ctx); extg::write_external_graphics_states(&mut ctx);
pattern::write_patterns(&mut ctx);
page::write_page_tree(&mut ctx); page::write_page_tree(&mut ctx);
write_catalog(&mut ctx, ident, timestamp); write_catalog(&mut ctx, ident, timestamp);
ctx.pdf.finish() ctx.pdf.finish()
@ -97,6 +101,8 @@ struct PdfContext<'a> {
image_refs: Vec<Ref>, image_refs: Vec<Ref>,
/// The IDs of written gradients. /// The IDs of written gradients.
gradient_refs: Vec<Ref>, gradient_refs: Vec<Ref>,
/// The IDs of written patterns.
pattern_refs: Vec<Ref>,
/// The IDs of written external graphics states. /// The IDs of written external graphics states.
ext_gs_refs: Vec<Ref>, ext_gs_refs: Vec<Ref>,
/// Handles color space writing. /// Handles color space writing.
@ -110,6 +116,8 @@ struct PdfContext<'a> {
image_deferred_map: HashMap<usize, Deferred<EncodedImage>>, image_deferred_map: HashMap<usize, Deferred<EncodedImage>>,
/// Deduplicates gradients used across the document. /// Deduplicates gradients used across the document.
gradient_map: Remapper<PdfGradient>, gradient_map: Remapper<PdfGradient>,
/// Deduplicates patterns used across the document.
pattern_map: Remapper<PdfPattern>,
/// Deduplicates external graphics states used across the document. /// Deduplicates external graphics states used across the document.
extg_map: Remapper<ExtGState>, extg_map: Remapper<ExtGState>,
} }
@ -131,12 +139,14 @@ impl<'a> PdfContext<'a> {
font_refs: vec![], font_refs: vec![],
image_refs: vec![], image_refs: vec![],
gradient_refs: vec![], gradient_refs: vec![],
pattern_refs: vec![],
ext_gs_refs: vec![], ext_gs_refs: vec![],
colors: ColorSpaces::default(), colors: ColorSpaces::default(),
font_map: Remapper::new(), font_map: Remapper::new(),
image_map: Remapper::new(), image_map: Remapper::new(),
image_deferred_map: HashMap::default(), image_deferred_map: HashMap::default(),
gradient_map: Remapper::new(), gradient_map: Remapper::new(),
pattern_map: Remapper::new(),
extg_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) 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. /// Create a base64-encoded hash of the value.
fn hash_base64<T: Hash>(value: &T) -> String { fn hash_base64<T: Hash>(value: &T) -> String {
base64::engine::general_purpose::STANDARD base64::engine::general_purpose::STANDARD
@ -341,10 +357,6 @@ where
}) })
} }
fn map(&self, item: &T) -> usize {
self.to_pdf[item]
}
fn pdf_indices<'a>( fn pdf_indices<'a>(
&'a self, &'a self,
refs: &'a [Ref], refs: &'a [Ref],
@ -380,3 +392,15 @@ impl EmExt for Em {
1000.0 * self.get() as f32 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(),
]
}

View File

@ -1,7 +1,7 @@
use std::collections::HashMap;
use std::num::NonZeroUsize; use std::num::NonZeroUsize;
use std::sync::Arc;
use ecow::eco_format; use ecow::{eco_format, EcoString};
use pdf_writer::types::{ use pdf_writer::types::{
ActionType, AnnotationType, ColorSpaceOperand, LineCapStyle, LineJoinStyle, ActionType, AnnotationType, ColorSpaceOperand, LineCapStyle, LineJoinStyle,
NumberingStyle, NumberingStyle,
@ -23,21 +23,22 @@ use typst::visualize::{
use crate::color::PaintEncode; use crate::color::PaintEncode;
use crate::extg::ExtGState; use crate::extg::ExtGState;
use crate::image::deferred_image; use crate::image::deferred_image;
use crate::{deflate, AbsExt, EmExt, PdfContext}; use crate::{deflate_memoized, AbsExt, EmExt, PdfContext};
/// Construct page objects. /// Construct page objects.
#[tracing::instrument(skip_all)] #[tracing::instrument(skip_all)]
pub(crate) fn construct_pages(ctx: &mut PdfContext, frames: &[Frame]) { pub(crate) fn construct_pages(ctx: &mut PdfContext, frames: &[Frame]) {
for frame in frames { 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. /// Construct a page object.
#[tracing::instrument(skip_all)] #[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(); let page_ref = ctx.alloc.bump();
ctx.page_refs.push(page_ref);
let mut ctx = PageContext { let mut ctx = PageContext {
parent: ctx, parent: ctx,
@ -49,6 +50,7 @@ pub(crate) fn construct_page(ctx: &mut PdfContext, frame: &Frame) {
saves: vec![], saves: vec![],
bottom: 0.0, bottom: 0.0,
links: vec![], links: vec![],
resources: HashMap::default(),
}; };
let size = frame.size(); let size = frame.size();
@ -74,9 +76,10 @@ pub(crate) fn construct_page(ctx: &mut PdfContext, frame: &Frame) {
uses_opacities: ctx.uses_opacities, uses_opacities: ctx.uses_opacities,
links: ctx.links, links: ctx.links,
label: ctx.label, label: ctx.label,
resources: ctx.resources,
}; };
ctx.parent.pages.push(page); (page_ref, page)
} }
/// Write the page tree. /// 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); 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(); patterns.finish();
let mut ext_gs_states = resources.ext_g_states(); let mut ext_gs_states = resources.ext_g_states();
@ -190,7 +198,7 @@ fn write_page(ctx: &mut PdfContext, i: usize) {
annotations.finish(); annotations.finish();
page_writer.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); 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 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. /// Data for an exported page.
pub struct Page { pub struct Page {
/// The indirect object id of the page. /// The indirect object id of the page.
@ -263,6 +265,63 @@ pub struct Page {
pub links: Vec<(Destination, Rect)>, pub links: Vec<(Destination, Rect)>,
/// The page's PDF label. /// The page's PDF label.
pub label: Option<PdfPageLabel>, 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. /// An exporter for the contents of a single PDF page.
@ -276,6 +335,8 @@ pub struct PageContext<'a, 'b> {
bottom: f32, bottom: f32,
uses_opacities: bool, uses_opacities: bool,
links: Vec<(Destination, Rect)>, 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 /// 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) { fn set_external_graphics_state(&mut self, graphics_state: &ExtGState) {
let current_state = self.state.external_graphics_state.as_ref(); let current_state = self.state.external_graphics_state.as_ref();
if current_state != Some(graphics_state) { if current_state != Some(graphics_state) {
self.parent.extg_map.insert(*graphics_state); let index = self.parent.extg_map.insert(*graphics_state);
let name = eco_format!("Gs{}", self.parent.extg_map.map(graphics_state)); let name = eco_format!("Gs{index}");
self.content.set_parameters(Name(name.as_bytes())); self.content.set_parameters(Name(name.as_bytes()));
self.resources
.insert(PageResource::new(ResourceKind::ExtGState, name), index);
if graphics_state.uses_opacities() { if graphics_state.uses_opacities() {
self.uses_opacities = true; self.uses_opacities = true;
@ -365,7 +428,7 @@ impl PageContext<'_, '_> {
.map(|stroke| { .map(|stroke| {
let color = match &stroke.paint { let color = match &stroke.paint {
Paint::Solid(color) => *color, 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) color.alpha().map_or(255, |v| (v * 255.0).round() as u8)
@ -375,7 +438,7 @@ impl PageContext<'_, '_> {
.map(|paint| { .map(|paint| {
let color = match paint { let color = match paint {
Paint::Solid(color) => *color, 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) 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) { fn set_font(&mut self, font: &Font, size: Abs) {
if self.state.font.as_ref().map(|(f, s)| (f, *s)) != Some((font, size)) { if self.state.font.as_ref().map(|(f, s)| (f, *s)) != Some((font, size)) {
self.parent.font_map.insert(font.clone()); let index = self.parent.font_map.insert(font.clone());
let name = eco_format!("F{}", self.parent.font_map.map(font)); let name = eco_format!("F{index}");
self.content.set_font(Name(name.as_bytes()), size.to_f32()); 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)); 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. /// Encode a vector or raster image into the content stream.
fn write_image(ctx: &mut PageContext, x: f32, y: f32, image: &Image, size: Size) { 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 ctx.parent
.image_deferred_map .image_deferred_map
.entry(idx) .entry(index)
.or_insert_with(|| deferred_image(image.clone())); .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 w = size.x.to_f32();
let h = size.y.to_f32(); let h = size.y.to_f32();
ctx.content.save_state(); 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.content.x_object(Name(name.as_bytes()));
} }
ctx.resources
.insert(PageResource::new(ResourceKind::XObject, name.clone()), index);
ctx.content.restore_state(); ctx.content.restore_state();
} }

View File

@ -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);
}
}

View File

@ -15,8 +15,8 @@ use typst::layout::{
}; };
use typst::text::{Font, TextItem}; use typst::text::{Font, TextItem};
use typst::visualize::{ use typst::visualize::{
Color, FixedStroke, Geometry, Gradient, GradientRelative, Image, ImageKind, LineCap, Color, FixedStroke, Geometry, Gradient, Image, ImageKind, LineCap, LineJoin, Paint,
LineJoin, Paint, Path, PathItem, RasterFormat, Shape, Path, PathItem, Pattern, RasterFormat, RelativeTo, Shape,
}; };
use usvg::{NodeExt, TreeParsing}; use usvg::{NodeExt, TreeParsing};
@ -433,7 +433,17 @@ fn render_outline_glyph(
write_bitmap(canvas, &bitmap, &state, sampler)?; write_bitmap(canvas, &bitmap, &state, sampler)?;
} }
Paint::Solid(color) => { 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 x in 0..mw {
for y in 0..mh { for y in 0..mh {
let alpha = bitmap.coverage[(y * mw + x) as usize]; 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] = pixmap.pixels_mut()[((y + 1) * (mw + 2) + (x + 1)) as usize] =
sk::ColorU8::from_rgba( sk::ColorU8::from_rgba(
color.red(), color.red(),
@ -504,8 +514,7 @@ fn write_bitmap<S: PaintSampler>(
} }
let color = sampler.sample((x as _, y as _)); let color = sampler.sample((x as _, y as _));
let color = let color = bytemuck::cast(color);
bytemuck::cast(to_sk_color_u8_without_alpha(color).premultiply());
let pi = (y * cw + x) as usize; let pi = (y * cw + x) as usize;
if cov == 255 { if cov == 255 {
pixels[pi] = color; 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. /// abstraction over solid colors and gradients.
trait PaintSampler: Copy { trait PaintSampler: Copy {
/// Sample the color at the `pos` in the pixmap. /// 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 { impl PaintSampler for sk::PremultipliedColorU8 {
fn sample(self, _: (u32, u32)) -> Color { fn sample(self, _: (u32, u32)) -> sk::PremultipliedColorU8 {
self self
} }
} }
@ -775,13 +795,13 @@ impl<'a> GradientSampler<'a> {
) -> Self { ) -> Self {
let relative = gradient.unwrap_relative(on_text); let relative = gradient.unwrap_relative(on_text);
let container_size = match relative { let container_size = match relative {
GradientRelative::Self_ => item_size, RelativeTo::Self_ => item_size,
GradientRelative::Parent => state.size, RelativeTo::Parent => state.size,
}; };
let fill_transform = match relative { let fill_transform = match relative {
GradientRelative::Self_ => sk::Transform::identity(), RelativeTo::Self_ => sk::Transform::identity(),
GradientRelative::Parent => state.container_transform.invert().unwrap(), RelativeTo::Parent => state.container_transform.invert().unwrap(),
}; };
Self { Self {
@ -794,16 +814,69 @@ impl<'a> GradientSampler<'a> {
impl PaintSampler for GradientSampler<'_> { impl PaintSampler for GradientSampler<'_> {
/// Samples a single point in a glyph. /// 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. // Compute the point in the gradient's coordinate space.
let mut point = sk::Point { x: x as f32, y: y as f32 }; let mut point = sk::Point { x: x as f32, y: y as f32 };
self.transform_to_parent.map_point(&mut point); self.transform_to_parent.map_point(&mut point);
// Sample the gradient // Sample the gradient
self.gradient.sample_at( to_sk_color_u8_without_alpha(self.gradient.sample_at(
(point.x, point.y), (point.x, point.y),
(self.container_size.x.to_f32(), self.container_size.y.to_f32()), (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) => { Paint::Gradient(gradient) => {
let relative = gradient.unwrap_relative(on_text); let relative = gradient.unwrap_relative(on_text);
let container_size = match relative { let container_size = match relative {
GradientRelative::Self_ => item_size, RelativeTo::Self_ => item_size,
GradientRelative::Parent => state.size, RelativeTo::Parent => state.size,
}; };
let fill_transform = match relative { let fill_transform = match relative {
GradientRelative::Self_ => fill_transform.unwrap_or_default(), RelativeTo::Self_ => fill_transform.unwrap_or_default(),
GradientRelative::Parent => state RelativeTo::Parent => state
.container_transform .container_transform
.post_concat(state.transform.invert().unwrap()), .post_concat(state.transform.invert().unwrap()),
}; };
@ -892,11 +965,49 @@ fn to_sk_paint<'a>(
sk_paint.anti_alias = gradient.anti_alias(); 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 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 { fn to_sk_color(color: Color) -> sk::Color {
let [r, g, b, a] = color.to_rgb().to_vec4_u8(); let [r, g, b, a] = color.to_rgb().to_vec4_u8();
sk::Color::from_rgba8(r, g, b, a) sk::Color::from_rgba8(r, g, b, a)

View File

@ -14,9 +14,8 @@ use typst::layout::{
use typst::text::{Font, TextItem}; use typst::text::{Font, TextItem};
use typst::util::hash128; use typst::util::hash128;
use typst::visualize::{ use typst::visualize::{
Color, FixedStroke, Geometry, Gradient, GradientRelative, Image, ImageFormat, Color, FixedStroke, Geometry, Gradient, Image, ImageFormat, LineCap, LineJoin, Paint,
LineCap, LineJoin, Paint, Path, PathItem, RasterFormat, RatioOrAngle, Shape, Path, PathItem, Pattern, RasterFormat, RatioOrAngle, RelativeTo, Shape, VectorFormat,
VectorFormat,
}; };
use xmlwriter::XmlWriter; use xmlwriter::XmlWriter;
@ -77,6 +76,12 @@ struct SVGRenderer {
/// different transforms. Therefore this allows us to reuse the same gradient /// different transforms. Therefore this allows us to reuse the same gradient
/// multiple times. /// multiple times.
gradient_refs: Deduplicator<GradientRef>, 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 are the actual gradients being written in the SVG file.
/// These gradients are deduplicated because they do not contain the transform /// These gradients are deduplicated because they do not contain the transform
/// matrix, allowing them to be reused across multiple invocations. /// 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 `Ratio` is the aspect ratio of the gradient, this is used to correct
/// the angle of the gradient. /// the angle of the gradient.
gradients: Deduplicator<(Gradient, Ratio)>, 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. /// These are the gradients that compose a conic gradient.
conic_subgradients: Deduplicator<SVGSubGradient>, conic_subgradients: Deduplicator<SVGSubGradient>,
} }
@ -141,6 +152,20 @@ struct GradientRef {
transform: Transform, 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. /// A subgradient for conic gradients.
#[derive(Hash)] #[derive(Hash)]
struct SVGSubGradient { struct SVGSubGradient {
@ -199,6 +224,8 @@ impl SVGRenderer {
gradient_refs: Deduplicator::new('g'), gradient_refs: Deduplicator::new('g'),
gradients: Deduplicator::new('f'), gradients: Deduplicator::new('f'),
conic_subgradients: Deduplicator::new('s'), 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"); 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. /// Render a frame with the given transform.
fn render_frame(&mut self, state: State, ts: Transform, frame: &Frame) { fn render_frame(&mut self, state: State, ts: Transform, frame: &Frame) {
self.xml.start_element("g"); self.xml.start_element("g");
@ -286,37 +327,27 @@ impl SVGRenderer {
/// of them works, we will skip the text. /// of them works, we will skip the text.
fn render_text(&mut self, state: State, text: &TextItem) { fn render_text(&mut self, state: State, text: &TextItem) {
let scale: f64 = text.size.to_pt() / text.font.units_per_em(); 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.start_element("g");
self.xml.write_attribute("class", "typst-text"); self.xml.write_attribute("class", "typst-text");
self.xml.write_attribute_fmt( self.xml.write_attribute("transform", "scale(1, -1)");
"transform",
format_args!("scale({} {})", scale, -scale),
);
let mut x: f64 = 0.0; let mut x: f64 = 0.0;
for glyph in &text.glyphs { for glyph in &text.glyphs {
let id = GlyphId(glyph.id); let id = GlyphId(glyph.id);
let offset = x + glyph.x_offset.at(text.size).to_pt(); let offset = x + glyph.x_offset.at(text.size).to_pt();
self.render_svg_glyph(text, id, offset, inv_scale) self.render_svg_glyph(text, id, offset, scale)
.or_else(|| self.render_bitmap_glyph(text, id, offset, inv_scale)) .or_else(|| self.render_bitmap_glyph(text, id, offset))
.or_else(|| { .or_else(|| {
self.render_outline_glyph( self.render_outline_glyph(
state state
.pre_concat(Transform::scale( .pre_concat(Transform::scale(Ratio::one(), -Ratio::one()))
Ratio::new(scale), .pre_translate(Point::new(Abs::pt(offset), Abs::zero())),
Ratio::new(-scale),
))
.pre_translate(Point::new(
Abs::pt(offset / scale),
Abs::zero(),
)),
text, text,
id, id,
offset, offset,
inv_scale, scale,
) )
}); });
@ -332,7 +363,7 @@ impl SVGRenderer {
text: &TextItem, text: &TextItem,
id: GlyphId, id: GlyphId,
x_offset: f64, x_offset: f64,
inv_scale: f64, scale: f64,
) -> Option<()> { ) -> Option<()> {
let data_url = convert_svg_glyph_to_base64_url(&text.font, id)?; let data_url = convert_svg_glyph_to_base64_url(&text.font, id)?;
let upem = Abs::raw(text.font.units_per_em()); let upem = Abs::raw(text.font.units_per_em());
@ -344,13 +375,12 @@ impl SVGRenderer {
width: upem.to_pt(), width: upem.to_pt(),
height: upem.to_pt(), height: upem.to_pt(),
ts: Transform::translate(Abs::zero(), Abs::pt(-origin_ascender)) 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.start_element("use");
self.xml.write_attribute_fmt("xlink:href", format_args!("#{id}")); self.xml.write_attribute_fmt("xlink:href", format_args!("#{id}"));
self.xml self.xml.write_attribute("x", &x_offset);
.write_attribute_fmt("x", format_args!("{}", x_offset * inv_scale));
self.xml.end_element(); self.xml.end_element();
Some(()) Some(())
@ -362,7 +392,6 @@ impl SVGRenderer {
text: &TextItem, text: &TextItem,
id: GlyphId, id: GlyphId,
x_offset: f64, x_offset: f64,
inv_scale: f64,
) -> Option<()> { ) -> Option<()> {
let (image, bitmap_x_offset, bitmap_y_offset) = let (image, bitmap_x_offset, bitmap_y_offset) =
convert_bitmap_glyph_to_image(&text.font, id)?; 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("x", &(x_offset / scale_factor));
self.xml.write_attribute_fmt( self.xml.write_attribute_fmt(
"transform", "transform",
format_args!( format_args!("scale({scale_factor} -{scale_factor})",),
"scale({} -{})",
inv_scale * scale_factor,
inv_scale * scale_factor,
),
); );
self.xml.end_element(); self.xml.end_element();
@ -408,19 +433,23 @@ impl SVGRenderer {
text: &TextItem, text: &TextItem,
glyph_id: GlyphId, glyph_id: GlyphId,
x_offset: f64, x_offset: f64,
inv_scale: f64, scale: f64,
) -> Option<()> { ) -> Option<()> {
let path = convert_outline_glyph_to_path(&text.font, glyph_id)?; let scale = Ratio::new(scale);
let hash = hash128(&(&text.font, glyph_id)); 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 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.start_element("use");
self.xml.write_attribute_fmt("xlink:href", format_args!("#{id}")); self.xml.write_attribute_fmt("xlink:href", format_args!("#{id}"));
self.xml self.xml.write_attribute_fmt("x", format_args!("{}", x_offset));
.write_attribute_fmt("x", format_args!("{}", x_offset * inv_scale));
self.write_fill( self.write_fill(
&text.fill, &text.fill,
state.size, Size::new(Abs::pt(width), Abs::pt(height)),
self.text_paint_transform(state, &text.fill), self.text_paint_transform(state, &text.fill),
); );
self.xml.end_element(); self.xml.end_element();
@ -429,17 +458,20 @@ impl SVGRenderer {
} }
fn text_paint_transform(&self, state: State, paint: &Paint) -> Transform { fn text_paint_transform(&self, state: State, paint: &Paint) -> Transform {
let Paint::Gradient(gradient) = paint else { match paint {
return Transform::identity(); Paint::Solid(_) => Transform::identity(),
}; Paint::Gradient(gradient) => match gradient.unwrap_relative(true) {
RelativeTo::Self_ => Transform::identity(),
match gradient.unwrap_relative(true) { RelativeTo::Parent => Transform::scale(
GradientRelative::Self_ => Transform::scale(Ratio::one(), Ratio::one()), Ratio::new(state.size.x.to_pt()),
GradientRelative::Parent => Transform::scale( Ratio::new(state.size.y.to_pt()),
Ratio::new(state.size.x.to_pt()), )
Ratio::new(state.size.y.to_pt()), .post_concat(state.transform.invert().unwrap()),
) },
.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 { if let Paint::Gradient(gradient) = paint {
match gradient.unwrap_relative(false) { match gradient.unwrap_relative(false) {
GradientRelative::Self_ => Transform::scale( RelativeTo::Self_ => Transform::scale(
Ratio::new(shape_size.x.to_pt()), Ratio::new(shape_size.x.to_pt()),
Ratio::new(shape_size.y.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.x.to_pt()),
Ratio::new(state.size.y.to_pt()), Ratio::new(state.size.y.to_pt()),
) )
.post_concat(state.transform.invert().unwrap()), .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 { } else {
Transform::identity() Transform::identity()
} }
@ -519,8 +556,8 @@ impl SVGRenderer {
if let Paint::Gradient(gradient) = paint { if let Paint::Gradient(gradient) = paint {
match gradient.unwrap_relative(false) { match gradient.unwrap_relative(false) {
GradientRelative::Self_ => shape_size, RelativeTo::Self_ => shape_size,
GradientRelative::Parent => state.size, RelativeTo::Parent => state.size,
} }
} else { } else {
shape_size shape_size
@ -535,6 +572,10 @@ impl SVGRenderer {
let id = self.push_gradient(gradient, size, ts); let id = self.push_gradient(gradient, size, ts);
self.xml.write_attribute_fmt("fill", format_args!("url(#{id})")); 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. /// Write a stroke attribute.
fn write_stroke( fn write_stroke(
&mut self, &mut self,
@ -577,6 +641,10 @@ impl SVGRenderer {
let id = self.push_gradient(gradient, size, fill_transform); let id = self.push_gradient(gradient, size, fill_transform);
self.xml.write_attribute_fmt("stroke", format_args!("url(#{id})")); 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()); self.xml.write_attribute("stroke-width", &stroke.thickness.to_pt());
@ -630,6 +698,8 @@ impl SVGRenderer {
self.write_gradients(); self.write_gradients();
self.write_gradient_refs(); self.write_gradient_refs();
self.write_subgradients(); self.write_subgradients();
self.write_patterns();
self.write_pattern_refs();
self.xml.end_document() self.xml.end_document()
} }
@ -948,12 +1018,78 @@ impl SVGRenderer {
self.xml.end_element(); 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. /// Convert an outline glyph to an SVG path.
#[comemo::memoize] #[comemo::memoize]
fn convert_outline_glyph_to_path(font: &Font, id: GlyphId) -> Option<EcoString> { fn convert_outline_glyph_to_path(
let mut builder = SvgPathBuilder::default(); font: &Font,
id: GlyphId,
scale: Ratio,
) -> Option<EcoString> {
let mut builder = SvgPathBuilder::with_scale(scale);
font.ttf().outline_glyph(id, &mut builder)?; font.ttf().outline_glyph(id, &mut builder)?;
Some(builder.0) Some(builder.0)
} }
@ -1170,10 +1306,17 @@ impl Display for SvgMatrix {
} }
/// A builder for SVG path. /// A builder for SVG path.
#[derive(Default)] struct SvgPathBuilder(pub EcoString, pub Ratio);
struct SvgPathBuilder(pub EcoString);
impl SvgPathBuilder { 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 /// 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. /// corner at (0, 0). The width and height are the size of the rectangle.
fn rect(&mut self, width: f32, height: f32) { fn rect(&mut self, width: f32, height: f32) {
@ -1193,34 +1336,63 @@ impl SvgPathBuilder {
sweep_flag: u32, sweep_flag: u32,
pos: (f32, f32), pos: (f32, f32),
) { ) {
let scale = self.scale();
write!( write!(
&mut self.0, &mut self.0,
"A {rx} {ry} {x_axis_rot} {large_arc_flag} {sweep_flag} {x} {y} ", "A {rx} {ry} {x_axis_rot} {large_arc_flag} {sweep_flag} {x} {y} ",
rx = radius.0, rx = radius.0 * scale,
ry = radius.1, ry = radius.1 * scale,
x = pos.0, x = pos.0 * scale,
y = pos.1, y = pos.1 * scale,
) )
.unwrap(); .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. /// A builder for SVG path. This is used to build the path for a glyph.
impl ttf_parser::OutlineBuilder for SvgPathBuilder { impl ttf_parser::OutlineBuilder for SvgPathBuilder {
fn move_to(&mut self, x: f32, y: f32) { 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) { 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) { 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) { 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) { fn close(&mut self) {

View File

@ -234,6 +234,15 @@ pub fn add(lhs: Value, rhs: Value) -> StrResult<Value> {
} }
.into_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), (Duration(a), Duration(b)) => Duration(a + b),
(Datetime(a), Duration(b)) => Datetime(a + b), (Datetime(a), Duration(b)) => Datetime(a + b),
(Duration(a), Datetime(b)) => Datetime(b + a), (Duration(a), Datetime(b)) => Datetime(b + a),

View File

@ -21,7 +21,7 @@ use crate::layout::{Abs, Angle, Em, Fr, Length, Ratio, Rel};
use crate::symbols::Symbol; use crate::symbols::Symbol;
use crate::syntax::{ast, Span}; use crate::syntax::{ast, Span};
use crate::text::{RawElem, TextElem}; use crate::text::{RawElem, TextElem};
use crate::visualize::{Color, Gradient}; use crate::visualize::{Color, Gradient, Pattern};
/// A computational value. /// A computational value.
#[derive(Default, Clone)] #[derive(Default, Clone)]
@ -51,6 +51,8 @@ pub enum Value {
Color(Color), Color(Color),
/// A gradient value: `gradient.linear(...)`. /// A gradient value: `gradient.linear(...)`.
Gradient(Gradient), Gradient(Gradient),
/// A pattern fill: `pattern(...)`.
Pattern(Pattern),
/// A symbol: `arrow.l`. /// A symbol: `arrow.l`.
Symbol(Symbol), Symbol(Symbol),
/// A version. /// A version.
@ -127,6 +129,7 @@ impl Value {
Self::Fraction(_) => Type::of::<Fr>(), Self::Fraction(_) => Type::of::<Fr>(),
Self::Color(_) => Type::of::<Color>(), Self::Color(_) => Type::of::<Color>(),
Self::Gradient(_) => Type::of::<Gradient>(), Self::Gradient(_) => Type::of::<Gradient>(),
Self::Pattern(_) => Type::of::<Pattern>(),
Self::Symbol(_) => Type::of::<Symbol>(), Self::Symbol(_) => Type::of::<Symbol>(),
Self::Version(_) => Type::of::<Version>(), Self::Version(_) => Type::of::<Version>(),
Self::Str(_) => Type::of::<Str>(), Self::Str(_) => Type::of::<Str>(),
@ -238,6 +241,7 @@ impl Debug for Value {
Self::Fraction(v) => Debug::fmt(v, f), Self::Fraction(v) => Debug::fmt(v, f),
Self::Color(v) => Debug::fmt(v, f), Self::Color(v) => Debug::fmt(v, f),
Self::Gradient(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::Symbol(v) => Debug::fmt(v, f),
Self::Version(v) => Debug::fmt(v, f), Self::Version(v) => Debug::fmt(v, f),
Self::Str(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::Fraction(v) => v.repr(),
Self::Color(v) => v.repr(), Self::Color(v) => v.repr(),
Self::Gradient(v) => v.repr(), Self::Gradient(v) => v.repr(),
Self::Pattern(v) => v.repr(),
Self::Symbol(v) => v.repr(), Self::Symbol(v) => v.repr(),
Self::Version(v) => v.repr(), Self::Version(v) => v.repr(),
Self::Str(v) => v.repr(), Self::Str(v) => v.repr(),
@ -323,6 +328,7 @@ impl Hash for Value {
Self::Fraction(v) => v.hash(state), Self::Fraction(v) => v.hash(state),
Self::Color(v) => v.hash(state), Self::Color(v) => v.hash(state),
Self::Gradient(v) => v.hash(state), Self::Gradient(v) => v.hash(state),
Self::Pattern(v) => v.hash(state),
Self::Symbol(v) => v.hash(state), Self::Symbol(v) => v.hash(state),
Self::Version(v) => v.hash(state), Self::Version(v) => v.hash(state),
Self::Str(v) => v.hash(state), Self::Str(v) => v.hash(state),
@ -635,6 +641,7 @@ primitive! { Rel<Length>: "relative length",
primitive! { Fr: "fraction", Fraction } primitive! { Fr: "fraction", Fraction }
primitive! { Color: "color", Color } primitive! { Color: "color", Color }
primitive! { Gradient: "gradient", Gradient } primitive! { Gradient: "gradient", Gradient }
primitive! { Pattern: "pattern", Pattern }
primitive! { Symbol: "symbol", Symbol } primitive! { Symbol: "symbol", Symbol }
primitive! { Version: "version", Version } primitive! { Version: "version", Version }
primitive! { primitive! {

View File

@ -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> { impl<T: Resolve> Resolve for Axes<T> {
type Output = Axes<T::Output>; type Output = Axes<T::Output>;

View File

@ -43,7 +43,7 @@ use crate::foundations::{
use crate::layout::{Abs, Axis, Dir, Length, Rel}; use crate::layout::{Abs, Axis, Dir, Length, Rel};
use crate::model::ParElem; use crate::model::ParElem;
use crate::syntax::Spanned; use crate::syntax::Spanned;
use crate::visualize::{Color, GradientRelative, Paint}; use crate::visualize::{Color, Paint, RelativeTo};
/// Text styling. /// Text styling.
/// ///
@ -226,16 +226,14 @@ pub struct TextElem {
#[parse({ #[parse({
let paint: Option<Spanned<Paint>> = args.named_or_find("fill")?; let paint: Option<Spanned<Paint>> = args.named_or_find("fill")?;
if let Some(paint) = &paint { if let Some(paint) = &paint {
if let Paint::Gradient(gradient) = &paint.v { if paint.v.relative() == Smart::Custom(RelativeTo::Self_) {
if gradient.relative() == Smart::Custom(GradientRelative::Self_) { bail!(
bail!( error!(
error!( paint.span,
paint.span, "gradients and patterns on text must be relative to the parent"
"gradients on text must be relative to the parent" )
) .with_hint("make sure to set `relative: auto` on your text fill")
.with_hint("make sure to set `relative: auto` on your text fill") );
);
}
} }
} }
paint.map(|paint| paint.v) paint.map(|paint| paint.v)

View File

@ -208,7 +208,7 @@ impl Gradient {
/// element. /// element.
#[named] #[named]
#[default(Smart::Auto)] #[default(Smart::Auto)]
relative: Smart<GradientRelative>, relative: Smart<RelativeTo>,
/// The direction of the gradient. /// The direction of the gradient.
#[external] #[external]
#[default(Dir::LTR)] #[default(Dir::LTR)]
@ -295,7 +295,7 @@ impl Gradient {
/// box, column, grid, or stack that contains the element. /// box, column, grid, or stack that contains the element.
#[named] #[named]
#[default(Smart::Auto)] #[default(Smart::Auto)]
relative: Smart<GradientRelative>, relative: Smart<RelativeTo>,
/// The center of the end circle of the gradient. /// The center of the end circle of the gradient.
/// ///
/// A value of `{(50%, 50%)}` means that the end circle is /// 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. /// box, column, grid, or stack that contains the element.
#[named] #[named]
#[default(Smart::Auto)] #[default(Smart::Auto)]
relative: Smart<GradientRelative>, relative: Smart<RelativeTo>,
/// The center of the last circle of the gradient. /// The center of the last circle of the gradient.
/// ///
/// A value of `{(50%, 50%)}` means that the end circle is /// A value of `{(50%, 50%)}` means that the end circle is
@ -665,7 +665,7 @@ impl Gradient {
/// Returns the relative placement of this gradient. /// Returns the relative placement of this gradient.
#[func] #[func]
pub fn relative(&self) -> Smart<GradientRelative> { pub fn relative(&self) -> Smart<RelativeTo> {
match self { match self {
Self::Linear(linear) => linear.relative, Self::Linear(linear) => linear.relative,
Self::Radial(radial) => radial.relative, Self::Radial(radial) => radial.relative,
@ -718,7 +718,7 @@ impl Gradient {
impl Gradient { impl Gradient {
/// Clones this gradient, but with a different relative placement. /// 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 { match &mut self {
Self::Linear(linear) => { Self::Linear(linear) => {
Arc::make_mut(linear).relative = Smart::Custom(relative); Arc::make_mut(linear).relative = Smart::Custom(relative);
@ -815,12 +815,12 @@ impl Gradient {
/// Returns the relative placement of this gradient, handling /// Returns the relative placement of this gradient, handling
/// the special case of `auto`. /// 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(|| { self.relative().unwrap_or_else(|| {
if on_text { if on_text {
GradientRelative::Parent RelativeTo::Parent
} else { } else {
GradientRelative::Self_ RelativeTo::Self_
} }
}) })
} }
@ -870,7 +870,7 @@ pub struct LinearGradient {
/// The color space in which to interpolate the gradient. /// The color space in which to interpolate the gradient.
pub space: ColorSpace, pub space: ColorSpace,
/// The relative placement of the gradient. /// The relative placement of the gradient.
pub relative: Smart<GradientRelative>, pub relative: Smart<RelativeTo>,
/// Whether to anti-alias the gradient (used for sharp gradients). /// Whether to anti-alias the gradient (used for sharp gradients).
pub anti_alias: bool, pub anti_alias: bool,
} }
@ -938,7 +938,7 @@ pub struct RadialGradient {
/// The color space in which to interpolate the gradient. /// The color space in which to interpolate the gradient.
pub space: ColorSpace, pub space: ColorSpace,
/// The relative placement of the gradient. /// The relative placement of the gradient.
pub relative: Smart<GradientRelative>, pub relative: Smart<RelativeTo>,
/// Whether to anti-alias the gradient (used for sharp gradients). /// Whether to anti-alias the gradient (used for sharp gradients).
pub anti_alias: bool, pub anti_alias: bool,
} }
@ -1016,7 +1016,7 @@ pub struct ConicGradient {
/// The color space in which to interpolate the gradient. /// The color space in which to interpolate the gradient.
pub space: ColorSpace, pub space: ColorSpace,
/// The relative placement of the gradient. /// The relative placement of the gradient.
pub relative: Smart<GradientRelative>, pub relative: Smart<RelativeTo>,
/// Whether to anti-alias the gradient (used for sharp gradients). /// Whether to anti-alias the gradient (used for sharp gradients).
pub anti_alias: bool, pub anti_alias: bool,
} }
@ -1070,7 +1070,7 @@ impl Repr for ConicGradient {
/// What is the gradient relative to. /// What is the gradient relative to.
#[derive(Cast, Debug, Clone, Copy, PartialEq, Eq, Hash)] #[derive(Cast, Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum GradientRelative { pub enum RelativeTo {
/// The gradient is relative to itself (its own bounding box). /// The gradient is relative to itself (its own bounding box).
Self_, Self_,
/// The gradient is relative to its parent (the parent's bounding box). /// The gradient is relative to its parent (the parent's bounding box).

View File

@ -6,6 +6,7 @@ mod image;
mod line; mod line;
mod paint; mod paint;
mod path; mod path;
mod pattern;
mod polygon; mod polygon;
mod shape; mod shape;
mod stroke; mod stroke;
@ -16,6 +17,7 @@ pub use self::image::*;
pub use self::line::*; pub use self::line::*;
pub use self::paint::*; pub use self::paint::*;
pub use self::path::*; pub use self::path::*;
pub use self::pattern::*;
pub use self::polygon::*; pub use self::polygon::*;
pub use self::shape::*; pub use self::shape::*;
pub use self::stroke::*; pub use self::stroke::*;
@ -35,6 +37,7 @@ pub(super) fn define(global: &mut Scope) {
global.category(VISUALIZE); global.category(VISUALIZE);
global.define_type::<Color>(); global.define_type::<Color>();
global.define_type::<Gradient>(); global.define_type::<Gradient>();
global.define_type::<Pattern>();
global.define_type::<Stroke>(); global.define_type::<Stroke>();
global.define_elem::<ImageElem>(); global.define_elem::<ImageElem>();
global.define_elem::<LineElem>(); global.define_elem::<LineElem>();

View File

@ -2,8 +2,8 @@ use std::fmt::{self, Debug, Formatter};
use ecow::EcoString; use ecow::EcoString;
use crate::foundations::{cast, Repr}; use crate::foundations::{cast, Repr, Smart};
use crate::visualize::{Color, Gradient, GradientRelative}; use crate::visualize::{Color, Gradient, Pattern, RelativeTo};
/// How a fill or stroke should be painted. /// How a fill or stroke should be painted.
#[derive(Clone, Eq, PartialEq, Hash)] #[derive(Clone, Eq, PartialEq, Hash)]
@ -12,6 +12,8 @@ pub enum Paint {
Solid(Color), Solid(Color),
/// A gradient. /// A gradient.
Gradient(Gradient), Gradient(Gradient),
/// A pattern.
Pattern(Pattern),
} }
impl Paint { impl Paint {
@ -19,19 +21,31 @@ impl Paint {
pub fn unwrap_solid(&self) -> Color { pub fn unwrap_solid(&self) -> Color {
match self { match self {
Self::Solid(color) => *color, 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. /// Turns this paint into a paint for a text decoration.
/// ///
/// If this paint is a gradient, it will be converted to a gradient with /// 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 { pub fn as_decoration(&self) -> Self {
match self { match self {
Self::Solid(color) => Self::Solid(*color), Self::Solid(color) => Self::Solid(*color),
Self::Gradient(gradient) => { 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 { match self {
Self::Solid(v) => v.fmt(f), Self::Solid(v) => v.fmt(f),
Self::Gradient(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 { impl Repr for Paint {
fn repr(&self) -> EcoString { fn repr(&self) -> EcoString {
match self { match self {
Self::Solid(color) => color.repr(), Self::Solid(color) => color.repr(),
Self::Gradient(gradient) => gradient.repr(), Self::Gradient(gradient) => gradient.repr(),
Self::Pattern(pattern) => pattern.repr(),
} }
} }
} }
@ -72,7 +94,9 @@ cast! {
self => match self { self => match self {
Self::Solid(color) => color.into_value(), Self::Solid(color) => color.into_value(),
Self::Gradient(gradient) => gradient.into_value(), Self::Gradient(gradient) => gradient.into_value(),
Self::Pattern(pattern) => pattern.into_value(),
}, },
color: Color => Self::Solid(color), color: Color => Self::Solid(color),
gradient: Gradient => Self::Gradient(gradient), gradient: Gradient => Self::Gradient(gradient),
pattern: Pattern => Self::Pattern(pattern),
} }

View File

@ -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
}
}

View File

@ -7,7 +7,7 @@ use crate::foundations::{
}; };
use crate::layout::{Abs, Length}; use crate::layout::{Abs, Length};
use crate::util::{Numeric, Scalar}; use crate::util::{Numeric, Scalar};
use crate::visualize::{Color, Gradient, Paint}; use crate::visualize::{Color, Gradient, Paint, Pattern};
/// Defines how to draw a line. /// Defines how to draw a line.
/// ///
@ -381,6 +381,10 @@ cast! {
paint: Smart::Custom(gradient.into()), paint: Smart::Custom(gradient.into()),
..Default::default() ..Default::default()
}, },
pattern: Pattern => Self {
paint: Smart::Custom(pattern.into()),
..Default::default()
},
mut dict: Dict => { mut dict: Dict => {
// Get a value by key, accepting either Auto or something convertible to type T. // 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>> { fn take<T: FromValue>(dict: &mut Dict, key: &str) -> StrResult<Smart<T>> {

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 307 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 352 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

View File

@ -66,5 +66,5 @@
#table() #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") #table(fill: "hey")

View File

@ -9,7 +9,7 @@
// Make sure they don't work when `relative: "self"`. // Make sure they don't work when `relative: "self"`.
// Hint: 17-61 make sure to set `relative: auto` on your text fill // 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")) #set text(fill: gradient.linear(red, blue, relative: "self"))
--- ---

View File

@ -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)

View File

@ -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))
)

View File

@ -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)

View File

@ -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))
)
)
)

View File

@ -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)
]))

View File

@ -51,7 +51,7 @@
#rect(radius: (left: 10pt, cake: 5pt)) #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)) #rect(stroke: (1, 2))
--- ---