mirror of
https://github.com/typst/typst
synced 2025-05-14 04:56:26 +08:00
Gradient Part 6 - Pattern fills (#2740)
This commit is contained in:
parent
3d2f1d2d6c
commit
1756718bab
@ -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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -7,10 +7,12 @@ mod gradient;
|
||||
mod image;
|
||||
mod outline;
|
||||
mod page;
|
||||
mod pattern;
|
||||
|
||||
use std::cmp::Eq;
|
||||
use std::collections::{BTreeMap, HashMap};
|
||||
use std::hash::Hash;
|
||||
use std::sync::Arc;
|
||||
|
||||
use base64::Engine;
|
||||
use ecow::{eco_format, EcoString};
|
||||
@ -18,7 +20,7 @@ use pdf_writer::types::Direction;
|
||||
use pdf_writer::{Finish, Name, Pdf, Ref, TextStr};
|
||||
use typst::foundations::Datetime;
|
||||
use typst::introspection::Introspector;
|
||||
use typst::layout::{Abs, Dir, Em};
|
||||
use typst::layout::{Abs, Dir, Em, Transform};
|
||||
use typst::model::Document;
|
||||
use typst::text::{Font, Lang};
|
||||
use typst::util::Deferred;
|
||||
@ -30,6 +32,7 @@ use crate::extg::ExtGState;
|
||||
use crate::gradient::PdfGradient;
|
||||
use crate::image::EncodedImage;
|
||||
use crate::page::Page;
|
||||
use crate::pattern::PdfPattern;
|
||||
|
||||
/// Export a document into a PDF file.
|
||||
///
|
||||
@ -57,6 +60,7 @@ pub fn pdf(
|
||||
image::write_images(&mut ctx);
|
||||
gradient::write_gradients(&mut ctx);
|
||||
extg::write_external_graphics_states(&mut ctx);
|
||||
pattern::write_patterns(&mut ctx);
|
||||
page::write_page_tree(&mut ctx);
|
||||
write_catalog(&mut ctx, ident, timestamp);
|
||||
ctx.pdf.finish()
|
||||
@ -97,6 +101,8 @@ struct PdfContext<'a> {
|
||||
image_refs: Vec<Ref>,
|
||||
/// The IDs of written gradients.
|
||||
gradient_refs: Vec<Ref>,
|
||||
/// The IDs of written patterns.
|
||||
pattern_refs: Vec<Ref>,
|
||||
/// The IDs of written external graphics states.
|
||||
ext_gs_refs: Vec<Ref>,
|
||||
/// Handles color space writing.
|
||||
@ -110,6 +116,8 @@ struct PdfContext<'a> {
|
||||
image_deferred_map: HashMap<usize, Deferred<EncodedImage>>,
|
||||
/// Deduplicates gradients used across the document.
|
||||
gradient_map: Remapper<PdfGradient>,
|
||||
/// Deduplicates patterns used across the document.
|
||||
pattern_map: Remapper<PdfPattern>,
|
||||
/// Deduplicates external graphics states used across the document.
|
||||
extg_map: Remapper<ExtGState>,
|
||||
}
|
||||
@ -131,12 +139,14 @@ impl<'a> PdfContext<'a> {
|
||||
font_refs: vec![],
|
||||
image_refs: vec![],
|
||||
gradient_refs: vec![],
|
||||
pattern_refs: vec![],
|
||||
ext_gs_refs: vec![],
|
||||
colors: ColorSpaces::default(),
|
||||
font_map: Remapper::new(),
|
||||
image_map: Remapper::new(),
|
||||
image_deferred_map: HashMap::default(),
|
||||
gradient_map: Remapper::new(),
|
||||
pattern_map: Remapper::new(),
|
||||
extg_map: Remapper::new(),
|
||||
}
|
||||
}
|
||||
@ -263,6 +273,12 @@ fn deflate(data: &[u8]) -> Vec<u8> {
|
||||
miniz_oxide::deflate::compress_to_vec_zlib(data, COMPRESSION_LEVEL)
|
||||
}
|
||||
|
||||
/// Memoized version of [`deflate`] specialized for a page's content stream.
|
||||
#[comemo::memoize]
|
||||
fn deflate_memoized(content: &[u8]) -> Arc<Vec<u8>> {
|
||||
Arc::new(deflate(content))
|
||||
}
|
||||
|
||||
/// Create a base64-encoded hash of the value.
|
||||
fn hash_base64<T: Hash>(value: &T) -> String {
|
||||
base64::engine::general_purpose::STANDARD
|
||||
@ -341,10 +357,6 @@ where
|
||||
})
|
||||
}
|
||||
|
||||
fn map(&self, item: &T) -> usize {
|
||||
self.to_pdf[item]
|
||||
}
|
||||
|
||||
fn pdf_indices<'a>(
|
||||
&'a self,
|
||||
refs: &'a [Ref],
|
||||
@ -380,3 +392,15 @@ impl EmExt for Em {
|
||||
1000.0 * self.get() as f32
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert to an array of floats.
|
||||
fn transform_to_array(ts: Transform) -> [f32; 6] {
|
||||
[
|
||||
ts.sx.get() as f32,
|
||||
ts.ky.get() as f32,
|
||||
ts.kx.get() as f32,
|
||||
ts.sy.get() as f32,
|
||||
ts.tx.to_f32(),
|
||||
ts.ty.to_f32(),
|
||||
]
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
use std::collections::HashMap;
|
||||
use std::num::NonZeroUsize;
|
||||
use std::sync::Arc;
|
||||
|
||||
use ecow::eco_format;
|
||||
use ecow::{eco_format, EcoString};
|
||||
use pdf_writer::types::{
|
||||
ActionType, AnnotationType, ColorSpaceOperand, LineCapStyle, LineJoinStyle,
|
||||
NumberingStyle,
|
||||
@ -23,21 +23,22 @@ use typst::visualize::{
|
||||
use crate::color::PaintEncode;
|
||||
use crate::extg::ExtGState;
|
||||
use crate::image::deferred_image;
|
||||
use crate::{deflate, AbsExt, EmExt, PdfContext};
|
||||
use crate::{deflate_memoized, AbsExt, EmExt, PdfContext};
|
||||
|
||||
/// Construct page objects.
|
||||
#[tracing::instrument(skip_all)]
|
||||
pub(crate) fn construct_pages(ctx: &mut PdfContext, frames: &[Frame]) {
|
||||
for frame in frames {
|
||||
construct_page(ctx, frame);
|
||||
let (page_ref, page) = construct_page(ctx, frame);
|
||||
ctx.page_refs.push(page_ref);
|
||||
ctx.pages.push(page);
|
||||
}
|
||||
}
|
||||
|
||||
/// Construct a page object.
|
||||
#[tracing::instrument(skip_all)]
|
||||
pub(crate) fn construct_page(ctx: &mut PdfContext, frame: &Frame) {
|
||||
pub(crate) fn construct_page(ctx: &mut PdfContext, frame: &Frame) -> (Ref, Page) {
|
||||
let page_ref = ctx.alloc.bump();
|
||||
ctx.page_refs.push(page_ref);
|
||||
|
||||
let mut ctx = PageContext {
|
||||
parent: ctx,
|
||||
@ -49,6 +50,7 @@ pub(crate) fn construct_page(ctx: &mut PdfContext, frame: &Frame) {
|
||||
saves: vec![],
|
||||
bottom: 0.0,
|
||||
links: vec![],
|
||||
resources: HashMap::default(),
|
||||
};
|
||||
|
||||
let size = frame.size();
|
||||
@ -74,9 +76,10 @@ pub(crate) fn construct_page(ctx: &mut PdfContext, frame: &Frame) {
|
||||
uses_opacities: ctx.uses_opacities,
|
||||
links: ctx.links,
|
||||
label: ctx.label,
|
||||
resources: ctx.resources,
|
||||
};
|
||||
|
||||
ctx.parent.pages.push(page);
|
||||
(page_ref, page)
|
||||
}
|
||||
|
||||
/// Write the page tree.
|
||||
@ -117,6 +120,11 @@ pub(crate) fn write_page_tree(ctx: &mut PdfContext) {
|
||||
patterns.pair(Name(name.as_bytes()), gradient_ref);
|
||||
}
|
||||
|
||||
for (pattern_ref, p) in ctx.pattern_map.pdf_indices(&ctx.pattern_refs) {
|
||||
let name = eco_format!("P{}", p);
|
||||
patterns.pair(Name(name.as_bytes()), pattern_ref);
|
||||
}
|
||||
|
||||
patterns.finish();
|
||||
|
||||
let mut ext_gs_states = resources.ext_g_states();
|
||||
@ -190,7 +198,7 @@ fn write_page(ctx: &mut PdfContext, i: usize) {
|
||||
annotations.finish();
|
||||
page_writer.finish();
|
||||
|
||||
let data = deflate_content(&page.content);
|
||||
let data = deflate_memoized(&page.content);
|
||||
ctx.pdf.stream(content_id, &data).filter(Filter::FlateDecode);
|
||||
}
|
||||
|
||||
@ -243,12 +251,6 @@ pub(crate) fn write_page_labels(ctx: &mut PdfContext) -> Vec<(NonZeroUsize, Ref)
|
||||
result
|
||||
}
|
||||
|
||||
/// Memoized version of [`deflate`] specialized for a page's content stream.
|
||||
#[comemo::memoize]
|
||||
fn deflate_content(content: &[u8]) -> Arc<Vec<u8>> {
|
||||
Arc::new(deflate(content))
|
||||
}
|
||||
|
||||
/// Data for an exported page.
|
||||
pub struct Page {
|
||||
/// The indirect object id of the page.
|
||||
@ -263,6 +265,63 @@ pub struct Page {
|
||||
pub links: Vec<(Destination, Rect)>,
|
||||
/// The page's PDF label.
|
||||
pub label: Option<PdfPageLabel>,
|
||||
/// The page's used resources
|
||||
pub resources: HashMap<PageResource, usize>,
|
||||
}
|
||||
|
||||
/// Represents a resource being used in a PDF page by its name.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct PageResource {
|
||||
kind: ResourceKind,
|
||||
name: EcoString,
|
||||
}
|
||||
|
||||
impl PageResource {
|
||||
pub fn new(kind: ResourceKind, name: EcoString) -> Self {
|
||||
Self { kind, name }
|
||||
}
|
||||
}
|
||||
|
||||
/// A kind of resource being used in a PDF page.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub enum ResourceKind {
|
||||
XObject,
|
||||
Font,
|
||||
Gradient,
|
||||
Pattern,
|
||||
ExtGState,
|
||||
}
|
||||
|
||||
impl PageResource {
|
||||
/// Returns the name of the resource.
|
||||
pub fn name(&self) -> Name<'_> {
|
||||
Name(self.name.as_bytes())
|
||||
}
|
||||
|
||||
/// Returns whether the resource is an XObject.
|
||||
pub fn is_x_object(&self) -> bool {
|
||||
matches!(self.kind, ResourceKind::XObject)
|
||||
}
|
||||
|
||||
/// Returns whether the resource is a font.
|
||||
pub fn is_font(&self) -> bool {
|
||||
matches!(self.kind, ResourceKind::Font)
|
||||
}
|
||||
|
||||
/// Returns whether the resource is a gradient.
|
||||
pub fn is_gradient(&self) -> bool {
|
||||
matches!(self.kind, ResourceKind::Gradient)
|
||||
}
|
||||
|
||||
/// Returns whether the resource is a pattern.
|
||||
pub fn is_pattern(&self) -> bool {
|
||||
matches!(self.kind, ResourceKind::Pattern)
|
||||
}
|
||||
|
||||
/// Returns whether the resource is an external graphics state.
|
||||
pub fn is_ext_g_state(&self) -> bool {
|
||||
matches!(self.kind, ResourceKind::ExtGState)
|
||||
}
|
||||
}
|
||||
|
||||
/// An exporter for the contents of a single PDF page.
|
||||
@ -276,6 +335,8 @@ pub struct PageContext<'a, 'b> {
|
||||
bottom: f32,
|
||||
uses_opacities: bool,
|
||||
links: Vec<(Destination, Rect)>,
|
||||
/// Keep track of the resources being used in the page.
|
||||
pub resources: HashMap<PageResource, usize>,
|
||||
}
|
||||
|
||||
/// A simulated graphics state used to deduplicate graphics state changes and
|
||||
@ -350,9 +411,11 @@ impl PageContext<'_, '_> {
|
||||
fn set_external_graphics_state(&mut self, graphics_state: &ExtGState) {
|
||||
let current_state = self.state.external_graphics_state.as_ref();
|
||||
if current_state != Some(graphics_state) {
|
||||
self.parent.extg_map.insert(*graphics_state);
|
||||
let name = eco_format!("Gs{}", self.parent.extg_map.map(graphics_state));
|
||||
let index = self.parent.extg_map.insert(*graphics_state);
|
||||
let name = eco_format!("Gs{index}");
|
||||
self.content.set_parameters(Name(name.as_bytes()));
|
||||
self.resources
|
||||
.insert(PageResource::new(ResourceKind::ExtGState, name), index);
|
||||
|
||||
if graphics_state.uses_opacities() {
|
||||
self.uses_opacities = true;
|
||||
@ -365,7 +428,7 @@ impl PageContext<'_, '_> {
|
||||
.map(|stroke| {
|
||||
let color = match &stroke.paint {
|
||||
Paint::Solid(color) => *color,
|
||||
Paint::Gradient(_) => return 255,
|
||||
Paint::Gradient(_) | Paint::Pattern(_) => return 255,
|
||||
};
|
||||
|
||||
color.alpha().map_or(255, |v| (v * 255.0).round() as u8)
|
||||
@ -375,7 +438,7 @@ impl PageContext<'_, '_> {
|
||||
.map(|paint| {
|
||||
let color = match paint {
|
||||
Paint::Solid(color) => *color,
|
||||
Paint::Gradient(_) => return 255,
|
||||
Paint::Gradient(_) | Paint::Pattern(_) => return 255,
|
||||
};
|
||||
|
||||
color.alpha().map_or(255, |v| (v * 255.0).round() as u8)
|
||||
@ -407,9 +470,11 @@ impl PageContext<'_, '_> {
|
||||
|
||||
fn set_font(&mut self, font: &Font, size: Abs) {
|
||||
if self.state.font.as_ref().map(|(f, s)| (f, *s)) != Some((font, size)) {
|
||||
self.parent.font_map.insert(font.clone());
|
||||
let name = eco_format!("F{}", self.parent.font_map.map(font));
|
||||
let index = self.parent.font_map.insert(font.clone());
|
||||
let name = eco_format!("F{index}");
|
||||
self.content.set_font(Name(name.as_bytes()), size.to_f32());
|
||||
self.resources
|
||||
.insert(PageResource::new(ResourceKind::Font, name), index);
|
||||
self.state.font = Some((font.clone(), size));
|
||||
}
|
||||
}
|
||||
@ -681,13 +746,13 @@ fn write_path(ctx: &mut PageContext, x: f32, y: f32, path: &Path) {
|
||||
|
||||
/// Encode a vector or raster image into the content stream.
|
||||
fn write_image(ctx: &mut PageContext, x: f32, y: f32, image: &Image, size: Size) {
|
||||
let idx = ctx.parent.image_map.insert(image.clone());
|
||||
let index = ctx.parent.image_map.insert(image.clone());
|
||||
ctx.parent
|
||||
.image_deferred_map
|
||||
.entry(idx)
|
||||
.entry(index)
|
||||
.or_insert_with(|| deferred_image(image.clone()));
|
||||
|
||||
let name = eco_format!("Im{idx}");
|
||||
let name = eco_format!("Im{index}");
|
||||
let w = size.x.to_f32();
|
||||
let h = size.y.to_f32();
|
||||
ctx.content.save_state();
|
||||
@ -707,6 +772,8 @@ fn write_image(ctx: &mut PageContext, x: f32, y: f32, image: &Image, size: Size)
|
||||
ctx.content.x_object(Name(name.as_bytes()));
|
||||
}
|
||||
|
||||
ctx.resources
|
||||
.insert(PageResource::new(ResourceKind::XObject, name.clone()), index);
|
||||
ctx.content.restore_state();
|
||||
}
|
||||
|
||||
|
154
crates/typst-pdf/src/pattern.rs
Normal file
154
crates/typst-pdf/src/pattern.rs
Normal 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);
|
||||
}
|
||||
}
|
@ -15,8 +15,8 @@ use typst::layout::{
|
||||
};
|
||||
use typst::text::{Font, TextItem};
|
||||
use typst::visualize::{
|
||||
Color, FixedStroke, Geometry, Gradient, GradientRelative, Image, ImageKind, LineCap,
|
||||
LineJoin, Paint, Path, PathItem, RasterFormat, Shape,
|
||||
Color, FixedStroke, Geometry, Gradient, Image, ImageKind, LineCap, LineJoin, Paint,
|
||||
Path, PathItem, Pattern, RasterFormat, RelativeTo, Shape,
|
||||
};
|
||||
use usvg::{NodeExt, TreeParsing};
|
||||
|
||||
@ -433,7 +433,17 @@ fn render_outline_glyph(
|
||||
write_bitmap(canvas, &bitmap, &state, sampler)?;
|
||||
}
|
||||
Paint::Solid(color) => {
|
||||
write_bitmap(canvas, &bitmap, &state, *color)?;
|
||||
write_bitmap(
|
||||
canvas,
|
||||
&bitmap,
|
||||
&state,
|
||||
to_sk_color_u8_without_alpha(*color).premultiply(),
|
||||
)?;
|
||||
}
|
||||
Paint::Pattern(pattern) => {
|
||||
let pixmap = render_pattern_frame(&state, pattern);
|
||||
let sampler = PatternSampler::new(pattern, &pixmap, &state, true);
|
||||
write_bitmap(canvas, &bitmap, &state, sampler)?;
|
||||
}
|
||||
}
|
||||
|
||||
@ -458,7 +468,7 @@ fn write_bitmap<S: PaintSampler>(
|
||||
for x in 0..mw {
|
||||
for y in 0..mh {
|
||||
let alpha = bitmap.coverage[(y * mw + x) as usize];
|
||||
let color = to_sk_color_u8_without_alpha(sampler.sample((x, y)));
|
||||
let color = sampler.sample((x, y));
|
||||
pixmap.pixels_mut()[((y + 1) * (mw + 2) + (x + 1)) as usize] =
|
||||
sk::ColorU8::from_rgba(
|
||||
color.red(),
|
||||
@ -504,8 +514,7 @@ fn write_bitmap<S: PaintSampler>(
|
||||
}
|
||||
|
||||
let color = sampler.sample((x as _, y as _));
|
||||
let color =
|
||||
bytemuck::cast(to_sk_color_u8_without_alpha(color).premultiply());
|
||||
let color = bytemuck::cast(color);
|
||||
let pi = (y * cw + x) as usize;
|
||||
if cov == 255 {
|
||||
pixels[pi] = color;
|
||||
@ -746,11 +755,22 @@ fn scaled_texture(image: &Image, w: u32, h: u32) -> Option<Arc<sk::Pixmap>> {
|
||||
/// abstraction over solid colors and gradients.
|
||||
trait PaintSampler: Copy {
|
||||
/// Sample the color at the `pos` in the pixmap.
|
||||
fn sample(self, pos: (u32, u32)) -> Color;
|
||||
fn sample(self, pos: (u32, u32)) -> sk::PremultipliedColorU8;
|
||||
|
||||
/// Write the sampler to a pixmap.
|
||||
fn write_to_pixmap(self, canvas: &mut sk::Pixmap) {
|
||||
let width = canvas.width();
|
||||
for x in 0..canvas.width() {
|
||||
for y in 0..canvas.height() {
|
||||
let color = self.sample((x, y));
|
||||
canvas.pixels_mut()[(y * width + x) as usize] = color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PaintSampler for Color {
|
||||
fn sample(self, _: (u32, u32)) -> Color {
|
||||
impl PaintSampler for sk::PremultipliedColorU8 {
|
||||
fn sample(self, _: (u32, u32)) -> sk::PremultipliedColorU8 {
|
||||
self
|
||||
}
|
||||
}
|
||||
@ -775,13 +795,13 @@ impl<'a> GradientSampler<'a> {
|
||||
) -> Self {
|
||||
let relative = gradient.unwrap_relative(on_text);
|
||||
let container_size = match relative {
|
||||
GradientRelative::Self_ => item_size,
|
||||
GradientRelative::Parent => state.size,
|
||||
RelativeTo::Self_ => item_size,
|
||||
RelativeTo::Parent => state.size,
|
||||
};
|
||||
|
||||
let fill_transform = match relative {
|
||||
GradientRelative::Self_ => sk::Transform::identity(),
|
||||
GradientRelative::Parent => state.container_transform.invert().unwrap(),
|
||||
RelativeTo::Self_ => sk::Transform::identity(),
|
||||
RelativeTo::Parent => state.container_transform.invert().unwrap(),
|
||||
};
|
||||
|
||||
Self {
|
||||
@ -794,16 +814,69 @@ impl<'a> GradientSampler<'a> {
|
||||
|
||||
impl PaintSampler for GradientSampler<'_> {
|
||||
/// Samples a single point in a glyph.
|
||||
fn sample(self, (x, y): (u32, u32)) -> Color {
|
||||
fn sample(self, (x, y): (u32, u32)) -> sk::PremultipliedColorU8 {
|
||||
// Compute the point in the gradient's coordinate space.
|
||||
let mut point = sk::Point { x: x as f32, y: y as f32 };
|
||||
self.transform_to_parent.map_point(&mut point);
|
||||
|
||||
// Sample the gradient
|
||||
self.gradient.sample_at(
|
||||
to_sk_color_u8_without_alpha(self.gradient.sample_at(
|
||||
(point.x, point.y),
|
||||
(self.container_size.x.to_f32(), self.container_size.y.to_f32()),
|
||||
)
|
||||
))
|
||||
.premultiply()
|
||||
}
|
||||
}
|
||||
|
||||
/// State used when sampling patterns for text.
|
||||
///
|
||||
/// It caches the inverse transform to the parent, so that we can
|
||||
/// reuse it instead of recomputing it for each pixel.
|
||||
#[derive(Clone, Copy)]
|
||||
struct PatternSampler<'a> {
|
||||
size: Size,
|
||||
transform_to_parent: sk::Transform,
|
||||
pixmap: &'a sk::Pixmap,
|
||||
pixel_per_pt: f32,
|
||||
}
|
||||
|
||||
impl<'a> PatternSampler<'a> {
|
||||
fn new(
|
||||
pattern: &'a Pattern,
|
||||
pixmap: &'a sk::Pixmap,
|
||||
state: &State,
|
||||
on_text: bool,
|
||||
) -> Self {
|
||||
let relative = pattern.unwrap_relative(on_text);
|
||||
let fill_transform = match relative {
|
||||
RelativeTo::Self_ => sk::Transform::identity(),
|
||||
RelativeTo::Parent => state.container_transform.invert().unwrap(),
|
||||
};
|
||||
|
||||
Self {
|
||||
pixmap,
|
||||
size: (pattern.size_abs() + pattern.spacing_abs())
|
||||
* state.pixel_per_pt as f64,
|
||||
transform_to_parent: fill_transform,
|
||||
pixel_per_pt: state.pixel_per_pt,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PaintSampler for PatternSampler<'_> {
|
||||
/// Samples a single point in a glyph.
|
||||
fn sample(self, (x, y): (u32, u32)) -> sk::PremultipliedColorU8 {
|
||||
// Compute the point in the pattern's coordinate space.
|
||||
let mut point = sk::Point { x: x as f32, y: y as f32 };
|
||||
self.transform_to_parent.map_point(&mut point);
|
||||
|
||||
let x =
|
||||
(point.x * self.pixel_per_pt).rem_euclid(self.size.x.to_f32()).floor() as u32;
|
||||
let y =
|
||||
(point.y * self.pixel_per_pt).rem_euclid(self.size.y.to_f32()).floor() as u32;
|
||||
|
||||
// Sample the pattern
|
||||
self.pixmap.pixel(x, y).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
@ -859,13 +932,13 @@ fn to_sk_paint<'a>(
|
||||
Paint::Gradient(gradient) => {
|
||||
let relative = gradient.unwrap_relative(on_text);
|
||||
let container_size = match relative {
|
||||
GradientRelative::Self_ => item_size,
|
||||
GradientRelative::Parent => state.size,
|
||||
RelativeTo::Self_ => item_size,
|
||||
RelativeTo::Parent => state.size,
|
||||
};
|
||||
|
||||
let fill_transform = match relative {
|
||||
GradientRelative::Self_ => fill_transform.unwrap_or_default(),
|
||||
GradientRelative::Parent => state
|
||||
RelativeTo::Self_ => fill_transform.unwrap_or_default(),
|
||||
RelativeTo::Parent => state
|
||||
.container_transform
|
||||
.post_concat(state.transform.invert().unwrap()),
|
||||
};
|
||||
@ -892,11 +965,49 @@ fn to_sk_paint<'a>(
|
||||
|
||||
sk_paint.anti_alias = gradient.anti_alias();
|
||||
}
|
||||
Paint::Pattern(pattern) => {
|
||||
let relative = pattern.unwrap_relative(on_text);
|
||||
|
||||
let fill_transform = match relative {
|
||||
RelativeTo::Self_ => fill_transform.unwrap_or_default(),
|
||||
RelativeTo::Parent => state
|
||||
.container_transform
|
||||
.post_concat(state.transform.invert().unwrap()),
|
||||
};
|
||||
|
||||
let canvas = render_pattern_frame(&state, pattern);
|
||||
*pixmap = Some(Arc::new(canvas));
|
||||
|
||||
// Create the shader
|
||||
sk_paint.shader = sk::Pattern::new(
|
||||
pixmap.as_ref().unwrap().as_ref().as_ref(),
|
||||
sk::SpreadMode::Repeat,
|
||||
sk::FilterQuality::Nearest,
|
||||
1.0,
|
||||
fill_transform
|
||||
.pre_scale(1.0 / state.pixel_per_pt, 1.0 / state.pixel_per_pt),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
sk_paint
|
||||
}
|
||||
|
||||
fn render_pattern_frame(state: &State, pattern: &Pattern) -> sk::Pixmap {
|
||||
let size = pattern.size_abs() + pattern.spacing_abs();
|
||||
let mut canvas = sk::Pixmap::new(
|
||||
(size.x.to_f32() * state.pixel_per_pt).round() as u32,
|
||||
(size.y.to_f32() * state.pixel_per_pt).round() as u32,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Render the pattern into a new canvas.
|
||||
let ts = sk::Transform::from_scale(state.pixel_per_pt, state.pixel_per_pt);
|
||||
let temp_state = State::new(pattern.size_abs(), ts, state.pixel_per_pt);
|
||||
render_frame(&mut canvas, temp_state, pattern.frame());
|
||||
canvas
|
||||
}
|
||||
|
||||
fn to_sk_color(color: Color) -> sk::Color {
|
||||
let [r, g, b, a] = color.to_rgb().to_vec4_u8();
|
||||
sk::Color::from_rgba8(r, g, b, a)
|
||||
|
@ -14,9 +14,8 @@ use typst::layout::{
|
||||
use typst::text::{Font, TextItem};
|
||||
use typst::util::hash128;
|
||||
use typst::visualize::{
|
||||
Color, FixedStroke, Geometry, Gradient, GradientRelative, Image, ImageFormat,
|
||||
LineCap, LineJoin, Paint, Path, PathItem, RasterFormat, RatioOrAngle, Shape,
|
||||
VectorFormat,
|
||||
Color, FixedStroke, Geometry, Gradient, Image, ImageFormat, LineCap, LineJoin, Paint,
|
||||
Path, PathItem, Pattern, RasterFormat, RatioOrAngle, RelativeTo, Shape, VectorFormat,
|
||||
};
|
||||
use xmlwriter::XmlWriter;
|
||||
|
||||
@ -77,6 +76,12 @@ struct SVGRenderer {
|
||||
/// different transforms. Therefore this allows us to reuse the same gradient
|
||||
/// multiple times.
|
||||
gradient_refs: Deduplicator<GradientRef>,
|
||||
/// Deduplicated patterns with transform matrices. They use a reference
|
||||
/// (`href`) to a "source" pattern instead of being defined inline.
|
||||
/// This saves a lot of space since patterns are often reused but with
|
||||
/// different transforms. Therefore this allows us to reuse the same gradient
|
||||
/// multiple times.
|
||||
pattern_refs: Deduplicator<PatternRef>,
|
||||
/// These are the actual gradients being written in the SVG file.
|
||||
/// These gradients are deduplicated because they do not contain the transform
|
||||
/// matrix, allowing them to be reused across multiple invocations.
|
||||
@ -84,6 +89,12 @@ struct SVGRenderer {
|
||||
/// The `Ratio` is the aspect ratio of the gradient, this is used to correct
|
||||
/// the angle of the gradient.
|
||||
gradients: Deduplicator<(Gradient, Ratio)>,
|
||||
/// These are the actual patterns being written in the SVG file.
|
||||
/// These patterns are deduplicated because they do not contain the transform
|
||||
/// matrix, allowing them to be reused across multiple invocations.
|
||||
///
|
||||
/// The `String` is the rendered pattern frame.
|
||||
patterns: Deduplicator<Pattern>,
|
||||
/// These are the gradients that compose a conic gradient.
|
||||
conic_subgradients: Deduplicator<SVGSubGradient>,
|
||||
}
|
||||
@ -141,6 +152,20 @@ struct GradientRef {
|
||||
transform: Transform,
|
||||
}
|
||||
|
||||
/// A reference to a deduplicated pattern, with a transform matrix.
|
||||
///
|
||||
/// Allows patterns to be reused across multiple invocations,
|
||||
/// simply by changing the transform matrix.
|
||||
#[derive(Hash)]
|
||||
struct PatternRef {
|
||||
/// The ID of the deduplicated gradient
|
||||
id: Id,
|
||||
/// The transform matrix to apply to the pattern.
|
||||
transform: Transform,
|
||||
/// The ratio of the size of the cell to the size of the filled area.
|
||||
ratio: Axes<Ratio>,
|
||||
}
|
||||
|
||||
/// A subgradient for conic gradients.
|
||||
#[derive(Hash)]
|
||||
struct SVGSubGradient {
|
||||
@ -199,6 +224,8 @@ impl SVGRenderer {
|
||||
gradient_refs: Deduplicator::new('g'),
|
||||
gradients: Deduplicator::new('f'),
|
||||
conic_subgradients: Deduplicator::new('s'),
|
||||
pattern_refs: Deduplicator::new('p'),
|
||||
patterns: Deduplicator::new('t'),
|
||||
}
|
||||
}
|
||||
|
||||
@ -219,6 +246,20 @@ impl SVGRenderer {
|
||||
self.xml.write_attribute("xmlns:h5", "http://www.w3.org/1999/xhtml");
|
||||
}
|
||||
|
||||
/// Render a frame to a string.
|
||||
fn render_pattern_frame(
|
||||
&mut self,
|
||||
state: State,
|
||||
ts: Transform,
|
||||
frame: &Frame,
|
||||
) -> String {
|
||||
let mut xml = XmlWriter::new(xmlwriter::Options::default());
|
||||
std::mem::swap(&mut self.xml, &mut xml);
|
||||
self.render_frame(state, ts, frame);
|
||||
std::mem::swap(&mut self.xml, &mut xml);
|
||||
xml.end_document()
|
||||
}
|
||||
|
||||
/// Render a frame with the given transform.
|
||||
fn render_frame(&mut self, state: State, ts: Transform, frame: &Frame) {
|
||||
self.xml.start_element("g");
|
||||
@ -286,37 +327,27 @@ impl SVGRenderer {
|
||||
/// of them works, we will skip the text.
|
||||
fn render_text(&mut self, state: State, text: &TextItem) {
|
||||
let scale: f64 = text.size.to_pt() / text.font.units_per_em();
|
||||
let inv_scale: f64 = text.font.units_per_em() / text.size.to_pt();
|
||||
|
||||
self.xml.start_element("g");
|
||||
self.xml.write_attribute("class", "typst-text");
|
||||
self.xml.write_attribute_fmt(
|
||||
"transform",
|
||||
format_args!("scale({} {})", scale, -scale),
|
||||
);
|
||||
self.xml.write_attribute("transform", "scale(1, -1)");
|
||||
|
||||
let mut x: f64 = 0.0;
|
||||
for glyph in &text.glyphs {
|
||||
let id = GlyphId(glyph.id);
|
||||
let offset = x + glyph.x_offset.at(text.size).to_pt();
|
||||
|
||||
self.render_svg_glyph(text, id, offset, inv_scale)
|
||||
.or_else(|| self.render_bitmap_glyph(text, id, offset, inv_scale))
|
||||
self.render_svg_glyph(text, id, offset, scale)
|
||||
.or_else(|| self.render_bitmap_glyph(text, id, offset))
|
||||
.or_else(|| {
|
||||
self.render_outline_glyph(
|
||||
state
|
||||
.pre_concat(Transform::scale(
|
||||
Ratio::new(scale),
|
||||
Ratio::new(-scale),
|
||||
))
|
||||
.pre_translate(Point::new(
|
||||
Abs::pt(offset / scale),
|
||||
Abs::zero(),
|
||||
)),
|
||||
.pre_concat(Transform::scale(Ratio::one(), -Ratio::one()))
|
||||
.pre_translate(Point::new(Abs::pt(offset), Abs::zero())),
|
||||
text,
|
||||
id,
|
||||
offset,
|
||||
inv_scale,
|
||||
scale,
|
||||
)
|
||||
});
|
||||
|
||||
@ -332,7 +363,7 @@ impl SVGRenderer {
|
||||
text: &TextItem,
|
||||
id: GlyphId,
|
||||
x_offset: f64,
|
||||
inv_scale: f64,
|
||||
scale: f64,
|
||||
) -> Option<()> {
|
||||
let data_url = convert_svg_glyph_to_base64_url(&text.font, id)?;
|
||||
let upem = Abs::raw(text.font.units_per_em());
|
||||
@ -344,13 +375,12 @@ impl SVGRenderer {
|
||||
width: upem.to_pt(),
|
||||
height: upem.to_pt(),
|
||||
ts: Transform::translate(Abs::zero(), Abs::pt(-origin_ascender))
|
||||
.post_concat(Transform::scale(Ratio::new(1.0), Ratio::new(-1.0))),
|
||||
.post_concat(Transform::scale(Ratio::new(scale), Ratio::new(-scale))),
|
||||
});
|
||||
|
||||
self.xml.start_element("use");
|
||||
self.xml.write_attribute_fmt("xlink:href", format_args!("#{id}"));
|
||||
self.xml
|
||||
.write_attribute_fmt("x", format_args!("{}", x_offset * inv_scale));
|
||||
self.xml.write_attribute("x", &x_offset);
|
||||
self.xml.end_element();
|
||||
|
||||
Some(())
|
||||
@ -362,7 +392,6 @@ impl SVGRenderer {
|
||||
text: &TextItem,
|
||||
id: GlyphId,
|
||||
x_offset: f64,
|
||||
inv_scale: f64,
|
||||
) -> Option<()> {
|
||||
let (image, bitmap_x_offset, bitmap_y_offset) =
|
||||
convert_bitmap_glyph_to_image(&text.font, id)?;
|
||||
@ -390,11 +419,7 @@ impl SVGRenderer {
|
||||
self.xml.write_attribute("x", &(x_offset / scale_factor));
|
||||
self.xml.write_attribute_fmt(
|
||||
"transform",
|
||||
format_args!(
|
||||
"scale({} -{})",
|
||||
inv_scale * scale_factor,
|
||||
inv_scale * scale_factor,
|
||||
),
|
||||
format_args!("scale({scale_factor} -{scale_factor})",),
|
||||
);
|
||||
self.xml.end_element();
|
||||
|
||||
@ -408,19 +433,23 @@ impl SVGRenderer {
|
||||
text: &TextItem,
|
||||
glyph_id: GlyphId,
|
||||
x_offset: f64,
|
||||
inv_scale: f64,
|
||||
scale: f64,
|
||||
) -> Option<()> {
|
||||
let path = convert_outline_glyph_to_path(&text.font, glyph_id)?;
|
||||
let hash = hash128(&(&text.font, glyph_id));
|
||||
let scale = Ratio::new(scale);
|
||||
let path = convert_outline_glyph_to_path(&text.font, glyph_id, scale)?;
|
||||
let hash = hash128(&(&text.font, glyph_id, scale));
|
||||
let id = self.glyphs.insert_with(hash, || RenderedGlyph::Path(path));
|
||||
|
||||
let glyph_size = text.font.ttf().glyph_bounding_box(glyph_id)?;
|
||||
let width = glyph_size.width() as f64 * scale.get();
|
||||
let height = glyph_size.height() as f64 * scale.get();
|
||||
|
||||
self.xml.start_element("use");
|
||||
self.xml.write_attribute_fmt("xlink:href", format_args!("#{id}"));
|
||||
self.xml
|
||||
.write_attribute_fmt("x", format_args!("{}", x_offset * inv_scale));
|
||||
self.xml.write_attribute_fmt("x", format_args!("{}", x_offset));
|
||||
self.write_fill(
|
||||
&text.fill,
|
||||
state.size,
|
||||
Size::new(Abs::pt(width), Abs::pt(height)),
|
||||
self.text_paint_transform(state, &text.fill),
|
||||
);
|
||||
self.xml.end_element();
|
||||
@ -429,17 +458,20 @@ impl SVGRenderer {
|
||||
}
|
||||
|
||||
fn text_paint_transform(&self, state: State, paint: &Paint) -> Transform {
|
||||
let Paint::Gradient(gradient) = paint else {
|
||||
return Transform::identity();
|
||||
};
|
||||
|
||||
match gradient.unwrap_relative(true) {
|
||||
GradientRelative::Self_ => Transform::scale(Ratio::one(), Ratio::one()),
|
||||
GradientRelative::Parent => Transform::scale(
|
||||
Ratio::new(state.size.x.to_pt()),
|
||||
Ratio::new(state.size.y.to_pt()),
|
||||
)
|
||||
.post_concat(state.transform.invert().unwrap()),
|
||||
match paint {
|
||||
Paint::Solid(_) => Transform::identity(),
|
||||
Paint::Gradient(gradient) => match gradient.unwrap_relative(true) {
|
||||
RelativeTo::Self_ => Transform::identity(),
|
||||
RelativeTo::Parent => Transform::scale(
|
||||
Ratio::new(state.size.x.to_pt()),
|
||||
Ratio::new(state.size.y.to_pt()),
|
||||
)
|
||||
.post_concat(state.transform.invert().unwrap()),
|
||||
},
|
||||
Paint::Pattern(pattern) => match pattern.unwrap_relative(true) {
|
||||
RelativeTo::Self_ => Transform::identity(),
|
||||
RelativeTo::Parent => state.transform.invert().unwrap(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@ -490,16 +522,21 @@ impl SVGRenderer {
|
||||
|
||||
if let Paint::Gradient(gradient) = paint {
|
||||
match gradient.unwrap_relative(false) {
|
||||
GradientRelative::Self_ => Transform::scale(
|
||||
RelativeTo::Self_ => Transform::scale(
|
||||
Ratio::new(shape_size.x.to_pt()),
|
||||
Ratio::new(shape_size.y.to_pt()),
|
||||
),
|
||||
GradientRelative::Parent => Transform::scale(
|
||||
RelativeTo::Parent => Transform::scale(
|
||||
Ratio::new(state.size.x.to_pt()),
|
||||
Ratio::new(state.size.y.to_pt()),
|
||||
)
|
||||
.post_concat(state.transform.invert().unwrap()),
|
||||
}
|
||||
} else if let Paint::Pattern(pattern) = paint {
|
||||
match pattern.unwrap_relative(false) {
|
||||
RelativeTo::Self_ => Transform::identity(),
|
||||
RelativeTo::Parent => state.transform.invert().unwrap(),
|
||||
}
|
||||
} else {
|
||||
Transform::identity()
|
||||
}
|
||||
@ -519,8 +556,8 @@ impl SVGRenderer {
|
||||
|
||||
if let Paint::Gradient(gradient) = paint {
|
||||
match gradient.unwrap_relative(false) {
|
||||
GradientRelative::Self_ => shape_size,
|
||||
GradientRelative::Parent => state.size,
|
||||
RelativeTo::Self_ => shape_size,
|
||||
RelativeTo::Parent => state.size,
|
||||
}
|
||||
} else {
|
||||
shape_size
|
||||
@ -535,6 +572,10 @@ impl SVGRenderer {
|
||||
let id = self.push_gradient(gradient, size, ts);
|
||||
self.xml.write_attribute_fmt("fill", format_args!("url(#{id})"));
|
||||
}
|
||||
Paint::Pattern(pattern) => {
|
||||
let id = self.push_pattern(pattern, size, ts);
|
||||
self.xml.write_attribute_fmt("fill", format_args!("url(#{id})"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -564,6 +605,29 @@ impl SVGRenderer {
|
||||
})
|
||||
}
|
||||
|
||||
fn push_pattern(&mut self, pattern: &Pattern, size: Size, ts: Transform) -> Id {
|
||||
let pattern_size = pattern.size_abs() + pattern.spacing_abs();
|
||||
// Unfortunately due to a limitation of `xmlwriter`, we need to
|
||||
// render the frame twice: once to allocate all of the resources
|
||||
// that it needs and once to actually render it.
|
||||
self.render_pattern_frame(
|
||||
State::new(pattern_size, Transform::identity()),
|
||||
Transform::identity(),
|
||||
pattern.frame(),
|
||||
);
|
||||
|
||||
let pattern_id = self.patterns.insert_with(hash128(pattern), || pattern.clone());
|
||||
self.pattern_refs
|
||||
.insert_with(hash128(&(pattern_id, ts)), || PatternRef {
|
||||
id: pattern_id,
|
||||
transform: ts,
|
||||
ratio: Axes::new(
|
||||
Ratio::new(pattern_size.x.to_pt() / size.x.to_pt()),
|
||||
Ratio::new(pattern_size.y.to_pt() / size.y.to_pt()),
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
/// Write a stroke attribute.
|
||||
fn write_stroke(
|
||||
&mut self,
|
||||
@ -577,6 +641,10 @@ impl SVGRenderer {
|
||||
let id = self.push_gradient(gradient, size, fill_transform);
|
||||
self.xml.write_attribute_fmt("stroke", format_args!("url(#{id})"));
|
||||
}
|
||||
Paint::Pattern(pattern) => {
|
||||
let id = self.push_pattern(pattern, size, fill_transform);
|
||||
self.xml.write_attribute_fmt("stroke", format_args!("url(#{id})"));
|
||||
}
|
||||
}
|
||||
|
||||
self.xml.write_attribute("stroke-width", &stroke.thickness.to_pt());
|
||||
@ -630,6 +698,8 @@ impl SVGRenderer {
|
||||
self.write_gradients();
|
||||
self.write_gradient_refs();
|
||||
self.write_subgradients();
|
||||
self.write_patterns();
|
||||
self.write_pattern_refs();
|
||||
self.xml.end_document()
|
||||
}
|
||||
|
||||
@ -948,12 +1018,78 @@ impl SVGRenderer {
|
||||
|
||||
self.xml.end_element();
|
||||
}
|
||||
|
||||
/// Write the raw gradients (without transform) to the SVG file.
|
||||
fn write_patterns(&mut self) {
|
||||
if self.patterns.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
self.xml.start_element("defs");
|
||||
self.xml.write_attribute("id", "patterns");
|
||||
|
||||
for (id, pattern) in
|
||||
self.patterns.iter().map(|(i, p)| (i, p.clone())).collect::<Vec<_>>()
|
||||
{
|
||||
let size = pattern.size_abs() + pattern.spacing_abs();
|
||||
self.xml.start_element("pattern");
|
||||
self.xml.write_attribute("id", &id);
|
||||
self.xml.write_attribute("width", &size.x.to_pt());
|
||||
self.xml.write_attribute("height", &size.y.to_pt());
|
||||
self.xml.write_attribute("patternUnits", "userSpaceOnUse");
|
||||
self.xml.write_attribute_fmt(
|
||||
"viewBox",
|
||||
format_args!("0 0 {:.3} {:.3}", size.x.to_pt(), size.y.to_pt()),
|
||||
);
|
||||
|
||||
// Render the frame.
|
||||
let state = State::new(size, Transform::identity());
|
||||
let ts = Transform::identity();
|
||||
self.render_frame(state, ts, pattern.frame());
|
||||
|
||||
self.xml.end_element();
|
||||
}
|
||||
|
||||
self.xml.end_element()
|
||||
}
|
||||
|
||||
/// Writes the references to the deduplicated patterns for each usage site.
|
||||
fn write_pattern_refs(&mut self) {
|
||||
if self.pattern_refs.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
self.xml.start_element("defs");
|
||||
self.xml.write_attribute("id", "pattern-refs");
|
||||
for (id, pattern_ref) in self.pattern_refs.iter() {
|
||||
self.xml.start_element("pattern");
|
||||
self.xml
|
||||
.write_attribute("patternTransform", &SvgMatrix(pattern_ref.transform));
|
||||
|
||||
self.xml.write_attribute("id", &id);
|
||||
|
||||
// Writing the href attribute to the "reference" pattern.
|
||||
self.xml
|
||||
.write_attribute_fmt("href", format_args!("#{}", pattern_ref.id));
|
||||
|
||||
// Also writing the xlink:href attribute for compatibility.
|
||||
self.xml
|
||||
.write_attribute_fmt("xlink:href", format_args!("#{}", pattern_ref.id));
|
||||
self.xml.end_element();
|
||||
}
|
||||
|
||||
self.xml.end_element();
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert an outline glyph to an SVG path.
|
||||
#[comemo::memoize]
|
||||
fn convert_outline_glyph_to_path(font: &Font, id: GlyphId) -> Option<EcoString> {
|
||||
let mut builder = SvgPathBuilder::default();
|
||||
fn convert_outline_glyph_to_path(
|
||||
font: &Font,
|
||||
id: GlyphId,
|
||||
scale: Ratio,
|
||||
) -> Option<EcoString> {
|
||||
let mut builder = SvgPathBuilder::with_scale(scale);
|
||||
font.ttf().outline_glyph(id, &mut builder)?;
|
||||
Some(builder.0)
|
||||
}
|
||||
@ -1170,10 +1306,17 @@ impl Display for SvgMatrix {
|
||||
}
|
||||
|
||||
/// A builder for SVG path.
|
||||
#[derive(Default)]
|
||||
struct SvgPathBuilder(pub EcoString);
|
||||
struct SvgPathBuilder(pub EcoString, pub Ratio);
|
||||
|
||||
impl SvgPathBuilder {
|
||||
fn with_scale(scale: Ratio) -> Self {
|
||||
Self(EcoString::new(), scale)
|
||||
}
|
||||
|
||||
fn scale(&self) -> f32 {
|
||||
self.1.get() as f32
|
||||
}
|
||||
|
||||
/// Create a rectangle path. The rectangle is created with the top-left
|
||||
/// corner at (0, 0). The width and height are the size of the rectangle.
|
||||
fn rect(&mut self, width: f32, height: f32) {
|
||||
@ -1193,34 +1336,63 @@ impl SvgPathBuilder {
|
||||
sweep_flag: u32,
|
||||
pos: (f32, f32),
|
||||
) {
|
||||
let scale = self.scale();
|
||||
write!(
|
||||
&mut self.0,
|
||||
"A {rx} {ry} {x_axis_rot} {large_arc_flag} {sweep_flag} {x} {y} ",
|
||||
rx = radius.0,
|
||||
ry = radius.1,
|
||||
x = pos.0,
|
||||
y = pos.1,
|
||||
rx = radius.0 * scale,
|
||||
ry = radius.1 * scale,
|
||||
x = pos.0 * scale,
|
||||
y = pos.1 * scale,
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for SvgPathBuilder {
|
||||
fn default() -> Self {
|
||||
Self(Default::default(), Ratio::one())
|
||||
}
|
||||
}
|
||||
|
||||
/// A builder for SVG path. This is used to build the path for a glyph.
|
||||
impl ttf_parser::OutlineBuilder for SvgPathBuilder {
|
||||
fn move_to(&mut self, x: f32, y: f32) {
|
||||
write!(&mut self.0, "M {} {} ", x, y).unwrap();
|
||||
let scale = self.scale();
|
||||
write!(&mut self.0, "M {} {} ", x * scale, y * scale).unwrap();
|
||||
}
|
||||
|
||||
fn line_to(&mut self, x: f32, y: f32) {
|
||||
write!(&mut self.0, "L {} {} ", x, y).unwrap();
|
||||
let scale = self.scale();
|
||||
write!(&mut self.0, "L {} {} ", x * scale, y * scale).unwrap();
|
||||
}
|
||||
|
||||
fn quad_to(&mut self, x1: f32, y1: f32, x: f32, y: f32) {
|
||||
write!(&mut self.0, "Q {} {} {} {} ", x1, y1, x, y).unwrap();
|
||||
let scale = self.scale();
|
||||
write!(
|
||||
&mut self.0,
|
||||
"Q {} {} {} {} ",
|
||||
x1 * scale,
|
||||
y1 * scale,
|
||||
x * scale,
|
||||
y * scale
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
fn curve_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, x: f32, y: f32) {
|
||||
write!(&mut self.0, "C {} {} {} {} {} {} ", x1, y1, x2, y2, x, y).unwrap();
|
||||
let scale = self.scale();
|
||||
write!(
|
||||
&mut self.0,
|
||||
"C {} {} {} {} {} {} ",
|
||||
x1 * scale,
|
||||
y1 * scale,
|
||||
x2 * scale,
|
||||
y2 * scale,
|
||||
x * scale,
|
||||
y * scale
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
fn close(&mut self) {
|
||||
|
@ -234,6 +234,15 @@ pub fn add(lhs: Value, rhs: Value) -> StrResult<Value> {
|
||||
}
|
||||
.into_value(),
|
||||
|
||||
(Pattern(pattern), Length(thickness)) | (Length(thickness), Pattern(pattern)) => {
|
||||
Stroke {
|
||||
paint: Smart::Custom(pattern.into()),
|
||||
thickness: Smart::Custom(thickness),
|
||||
..Stroke::default()
|
||||
}
|
||||
.into_value()
|
||||
}
|
||||
|
||||
(Duration(a), Duration(b)) => Duration(a + b),
|
||||
(Datetime(a), Duration(b)) => Datetime(a + b),
|
||||
(Duration(a), Datetime(b)) => Datetime(b + a),
|
||||
|
@ -21,7 +21,7 @@ use crate::layout::{Abs, Angle, Em, Fr, Length, Ratio, Rel};
|
||||
use crate::symbols::Symbol;
|
||||
use crate::syntax::{ast, Span};
|
||||
use crate::text::{RawElem, TextElem};
|
||||
use crate::visualize::{Color, Gradient};
|
||||
use crate::visualize::{Color, Gradient, Pattern};
|
||||
|
||||
/// A computational value.
|
||||
#[derive(Default, Clone)]
|
||||
@ -51,6 +51,8 @@ pub enum Value {
|
||||
Color(Color),
|
||||
/// A gradient value: `gradient.linear(...)`.
|
||||
Gradient(Gradient),
|
||||
/// A pattern fill: `pattern(...)`.
|
||||
Pattern(Pattern),
|
||||
/// A symbol: `arrow.l`.
|
||||
Symbol(Symbol),
|
||||
/// A version.
|
||||
@ -127,6 +129,7 @@ impl Value {
|
||||
Self::Fraction(_) => Type::of::<Fr>(),
|
||||
Self::Color(_) => Type::of::<Color>(),
|
||||
Self::Gradient(_) => Type::of::<Gradient>(),
|
||||
Self::Pattern(_) => Type::of::<Pattern>(),
|
||||
Self::Symbol(_) => Type::of::<Symbol>(),
|
||||
Self::Version(_) => Type::of::<Version>(),
|
||||
Self::Str(_) => Type::of::<Str>(),
|
||||
@ -238,6 +241,7 @@ impl Debug for Value {
|
||||
Self::Fraction(v) => Debug::fmt(v, f),
|
||||
Self::Color(v) => Debug::fmt(v, f),
|
||||
Self::Gradient(v) => Debug::fmt(v, f),
|
||||
Self::Pattern(v) => Debug::fmt(v, f),
|
||||
Self::Symbol(v) => Debug::fmt(v, f),
|
||||
Self::Version(v) => Debug::fmt(v, f),
|
||||
Self::Str(v) => Debug::fmt(v, f),
|
||||
@ -274,6 +278,7 @@ impl Repr for Value {
|
||||
Self::Fraction(v) => v.repr(),
|
||||
Self::Color(v) => v.repr(),
|
||||
Self::Gradient(v) => v.repr(),
|
||||
Self::Pattern(v) => v.repr(),
|
||||
Self::Symbol(v) => v.repr(),
|
||||
Self::Version(v) => v.repr(),
|
||||
Self::Str(v) => v.repr(),
|
||||
@ -323,6 +328,7 @@ impl Hash for Value {
|
||||
Self::Fraction(v) => v.hash(state),
|
||||
Self::Color(v) => v.hash(state),
|
||||
Self::Gradient(v) => v.hash(state),
|
||||
Self::Pattern(v) => v.hash(state),
|
||||
Self::Symbol(v) => v.hash(state),
|
||||
Self::Version(v) => v.hash(state),
|
||||
Self::Str(v) => v.hash(state),
|
||||
@ -635,6 +641,7 @@ primitive! { Rel<Length>: "relative length",
|
||||
primitive! { Fr: "fraction", Fraction }
|
||||
primitive! { Color: "color", Color }
|
||||
primitive! { Gradient: "gradient", Gradient }
|
||||
primitive! { Pattern: "pattern", Pattern }
|
||||
primitive! { Symbol: "symbol", Symbol }
|
||||
primitive! { Version: "version", Version }
|
||||
primitive! {
|
||||
|
@ -306,6 +306,18 @@ cast! {
|
||||
},
|
||||
}
|
||||
|
||||
cast! {
|
||||
Axes<Length>,
|
||||
self => array![self.x, self.y].into_value(),
|
||||
array: Array => {
|
||||
let mut iter = array.into_iter();
|
||||
match (iter.next(), iter.next(), iter.next()) {
|
||||
(Some(a), Some(b), None) => Axes::new(a.cast()?, b.cast()?),
|
||||
_ => bail!("length array must contain exactly two entries"),
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
impl<T: Resolve> Resolve for Axes<T> {
|
||||
type Output = Axes<T::Output>;
|
||||
|
||||
|
@ -43,7 +43,7 @@ use crate::foundations::{
|
||||
use crate::layout::{Abs, Axis, Dir, Length, Rel};
|
||||
use crate::model::ParElem;
|
||||
use crate::syntax::Spanned;
|
||||
use crate::visualize::{Color, GradientRelative, Paint};
|
||||
use crate::visualize::{Color, Paint, RelativeTo};
|
||||
|
||||
/// Text styling.
|
||||
///
|
||||
@ -226,16 +226,14 @@ pub struct TextElem {
|
||||
#[parse({
|
||||
let paint: Option<Spanned<Paint>> = args.named_or_find("fill")?;
|
||||
if let Some(paint) = &paint {
|
||||
if let Paint::Gradient(gradient) = &paint.v {
|
||||
if gradient.relative() == Smart::Custom(GradientRelative::Self_) {
|
||||
bail!(
|
||||
error!(
|
||||
paint.span,
|
||||
"gradients on text must be relative to the parent"
|
||||
)
|
||||
.with_hint("make sure to set `relative: auto` on your text fill")
|
||||
);
|
||||
}
|
||||
if paint.v.relative() == Smart::Custom(RelativeTo::Self_) {
|
||||
bail!(
|
||||
error!(
|
||||
paint.span,
|
||||
"gradients and patterns on text must be relative to the parent"
|
||||
)
|
||||
.with_hint("make sure to set `relative: auto` on your text fill")
|
||||
);
|
||||
}
|
||||
}
|
||||
paint.map(|paint| paint.v)
|
||||
|
@ -208,7 +208,7 @@ impl Gradient {
|
||||
/// element.
|
||||
#[named]
|
||||
#[default(Smart::Auto)]
|
||||
relative: Smart<GradientRelative>,
|
||||
relative: Smart<RelativeTo>,
|
||||
/// The direction of the gradient.
|
||||
#[external]
|
||||
#[default(Dir::LTR)]
|
||||
@ -295,7 +295,7 @@ impl Gradient {
|
||||
/// box, column, grid, or stack that contains the element.
|
||||
#[named]
|
||||
#[default(Smart::Auto)]
|
||||
relative: Smart<GradientRelative>,
|
||||
relative: Smart<RelativeTo>,
|
||||
/// The center of the end circle of the gradient.
|
||||
///
|
||||
/// A value of `{(50%, 50%)}` means that the end circle is
|
||||
@ -409,7 +409,7 @@ impl Gradient {
|
||||
/// box, column, grid, or stack that contains the element.
|
||||
#[named]
|
||||
#[default(Smart::Auto)]
|
||||
relative: Smart<GradientRelative>,
|
||||
relative: Smart<RelativeTo>,
|
||||
/// The center of the last circle of the gradient.
|
||||
///
|
||||
/// A value of `{(50%, 50%)}` means that the end circle is
|
||||
@ -665,7 +665,7 @@ impl Gradient {
|
||||
|
||||
/// Returns the relative placement of this gradient.
|
||||
#[func]
|
||||
pub fn relative(&self) -> Smart<GradientRelative> {
|
||||
pub fn relative(&self) -> Smart<RelativeTo> {
|
||||
match self {
|
||||
Self::Linear(linear) => linear.relative,
|
||||
Self::Radial(radial) => radial.relative,
|
||||
@ -718,7 +718,7 @@ impl Gradient {
|
||||
|
||||
impl Gradient {
|
||||
/// Clones this gradient, but with a different relative placement.
|
||||
pub fn with_relative(mut self, relative: GradientRelative) -> Self {
|
||||
pub fn with_relative(mut self, relative: RelativeTo) -> Self {
|
||||
match &mut self {
|
||||
Self::Linear(linear) => {
|
||||
Arc::make_mut(linear).relative = Smart::Custom(relative);
|
||||
@ -815,12 +815,12 @@ impl Gradient {
|
||||
|
||||
/// Returns the relative placement of this gradient, handling
|
||||
/// the special case of `auto`.
|
||||
pub fn unwrap_relative(&self, on_text: bool) -> GradientRelative {
|
||||
pub fn unwrap_relative(&self, on_text: bool) -> RelativeTo {
|
||||
self.relative().unwrap_or_else(|| {
|
||||
if on_text {
|
||||
GradientRelative::Parent
|
||||
RelativeTo::Parent
|
||||
} else {
|
||||
GradientRelative::Self_
|
||||
RelativeTo::Self_
|
||||
}
|
||||
})
|
||||
}
|
||||
@ -870,7 +870,7 @@ pub struct LinearGradient {
|
||||
/// The color space in which to interpolate the gradient.
|
||||
pub space: ColorSpace,
|
||||
/// The relative placement of the gradient.
|
||||
pub relative: Smart<GradientRelative>,
|
||||
pub relative: Smart<RelativeTo>,
|
||||
/// Whether to anti-alias the gradient (used for sharp gradients).
|
||||
pub anti_alias: bool,
|
||||
}
|
||||
@ -938,7 +938,7 @@ pub struct RadialGradient {
|
||||
/// The color space in which to interpolate the gradient.
|
||||
pub space: ColorSpace,
|
||||
/// The relative placement of the gradient.
|
||||
pub relative: Smart<GradientRelative>,
|
||||
pub relative: Smart<RelativeTo>,
|
||||
/// Whether to anti-alias the gradient (used for sharp gradients).
|
||||
pub anti_alias: bool,
|
||||
}
|
||||
@ -1016,7 +1016,7 @@ pub struct ConicGradient {
|
||||
/// The color space in which to interpolate the gradient.
|
||||
pub space: ColorSpace,
|
||||
/// The relative placement of the gradient.
|
||||
pub relative: Smart<GradientRelative>,
|
||||
pub relative: Smart<RelativeTo>,
|
||||
/// Whether to anti-alias the gradient (used for sharp gradients).
|
||||
pub anti_alias: bool,
|
||||
}
|
||||
@ -1070,7 +1070,7 @@ impl Repr for ConicGradient {
|
||||
|
||||
/// What is the gradient relative to.
|
||||
#[derive(Cast, Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum GradientRelative {
|
||||
pub enum RelativeTo {
|
||||
/// The gradient is relative to itself (its own bounding box).
|
||||
Self_,
|
||||
/// The gradient is relative to its parent (the parent's bounding box).
|
||||
|
@ -6,6 +6,7 @@ mod image;
|
||||
mod line;
|
||||
mod paint;
|
||||
mod path;
|
||||
mod pattern;
|
||||
mod polygon;
|
||||
mod shape;
|
||||
mod stroke;
|
||||
@ -16,6 +17,7 @@ pub use self::image::*;
|
||||
pub use self::line::*;
|
||||
pub use self::paint::*;
|
||||
pub use self::path::*;
|
||||
pub use self::pattern::*;
|
||||
pub use self::polygon::*;
|
||||
pub use self::shape::*;
|
||||
pub use self::stroke::*;
|
||||
@ -35,6 +37,7 @@ pub(super) fn define(global: &mut Scope) {
|
||||
global.category(VISUALIZE);
|
||||
global.define_type::<Color>();
|
||||
global.define_type::<Gradient>();
|
||||
global.define_type::<Pattern>();
|
||||
global.define_type::<Stroke>();
|
||||
global.define_elem::<ImageElem>();
|
||||
global.define_elem::<LineElem>();
|
||||
|
@ -2,8 +2,8 @@ use std::fmt::{self, Debug, Formatter};
|
||||
|
||||
use ecow::EcoString;
|
||||
|
||||
use crate::foundations::{cast, Repr};
|
||||
use crate::visualize::{Color, Gradient, GradientRelative};
|
||||
use crate::foundations::{cast, Repr, Smart};
|
||||
use crate::visualize::{Color, Gradient, Pattern, RelativeTo};
|
||||
|
||||
/// How a fill or stroke should be painted.
|
||||
#[derive(Clone, Eq, PartialEq, Hash)]
|
||||
@ -12,6 +12,8 @@ pub enum Paint {
|
||||
Solid(Color),
|
||||
/// A gradient.
|
||||
Gradient(Gradient),
|
||||
/// A pattern.
|
||||
Pattern(Pattern),
|
||||
}
|
||||
|
||||
impl Paint {
|
||||
@ -19,19 +21,31 @@ impl Paint {
|
||||
pub fn unwrap_solid(&self) -> Color {
|
||||
match self {
|
||||
Self::Solid(color) => *color,
|
||||
Self::Gradient(_) => panic!("expected solid color"),
|
||||
Self::Gradient(_) | Self::Pattern(_) => panic!("expected solid color"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets the relative coordinate system for this paint.
|
||||
pub fn relative(&self) -> Smart<RelativeTo> {
|
||||
match self {
|
||||
Self::Solid(_) => Smart::Auto,
|
||||
Self::Gradient(gradient) => gradient.relative(),
|
||||
Self::Pattern(pattern) => pattern.relative(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Turns this paint into a paint for a text decoration.
|
||||
///
|
||||
/// If this paint is a gradient, it will be converted to a gradient with
|
||||
/// relative set to [`GradientRelative::Parent`].
|
||||
/// relative set to [`RelativeTo::Parent`].
|
||||
pub fn as_decoration(&self) -> Self {
|
||||
match self {
|
||||
Self::Solid(color) => Self::Solid(*color),
|
||||
Self::Gradient(gradient) => {
|
||||
Self::Gradient(gradient.clone().with_relative(GradientRelative::Parent))
|
||||
Self::Gradient(gradient.clone().with_relative(RelativeTo::Parent))
|
||||
}
|
||||
Self::Pattern(pattern) => {
|
||||
Self::Pattern(pattern.clone().with_relative(RelativeTo::Parent))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -42,15 +56,23 @@ impl Debug for Paint {
|
||||
match self {
|
||||
Self::Solid(v) => v.fmt(f),
|
||||
Self::Gradient(v) => v.fmt(f),
|
||||
Self::Pattern(v) => v.fmt(f),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Pattern> for Paint {
|
||||
fn from(pattern: Pattern) -> Self {
|
||||
Self::Pattern(pattern)
|
||||
}
|
||||
}
|
||||
|
||||
impl Repr for Paint {
|
||||
fn repr(&self) -> EcoString {
|
||||
match self {
|
||||
Self::Solid(color) => color.repr(),
|
||||
Self::Gradient(gradient) => gradient.repr(),
|
||||
Self::Pattern(pattern) => pattern.repr(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -72,7 +94,9 @@ cast! {
|
||||
self => match self {
|
||||
Self::Solid(color) => color.into_value(),
|
||||
Self::Gradient(gradient) => gradient.into_value(),
|
||||
Self::Pattern(pattern) => pattern.into_value(),
|
||||
},
|
||||
color: Color => Self::Solid(color),
|
||||
gradient: Gradient => Self::Gradient(gradient),
|
||||
pattern: Pattern => Self::Pattern(pattern),
|
||||
}
|
||||
|
288
crates/typst/src/visualize/pattern.rs
Normal file
288
crates/typst/src/visualize/pattern.rs
Normal 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
|
||||
}
|
||||
}
|
@ -7,7 +7,7 @@ use crate::foundations::{
|
||||
};
|
||||
use crate::layout::{Abs, Length};
|
||||
use crate::util::{Numeric, Scalar};
|
||||
use crate::visualize::{Color, Gradient, Paint};
|
||||
use crate::visualize::{Color, Gradient, Paint, Pattern};
|
||||
|
||||
/// Defines how to draw a line.
|
||||
///
|
||||
@ -381,6 +381,10 @@ cast! {
|
||||
paint: Smart::Custom(gradient.into()),
|
||||
..Default::default()
|
||||
},
|
||||
pattern: Pattern => Self {
|
||||
paint: Smart::Custom(pattern.into()),
|
||||
..Default::default()
|
||||
},
|
||||
mut dict: Dict => {
|
||||
// Get a value by key, accepting either Auto or something convertible to type T.
|
||||
fn take<T: FromValue>(dict: &mut Dict, key: &str) -> StrResult<Smart<T>> {
|
||||
|
BIN
tests/ref/visualize/pattern-relative.png
Normal file
BIN
tests/ref/visualize/pattern-relative.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.5 KiB |
BIN
tests/ref/visualize/pattern-small.png
Normal file
BIN
tests/ref/visualize/pattern-small.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 106 B |
BIN
tests/ref/visualize/pattern-spacing.png
Normal file
BIN
tests/ref/visualize/pattern-spacing.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 307 B |
BIN
tests/ref/visualize/pattern-stroke.png
Normal file
BIN
tests/ref/visualize/pattern-stroke.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 352 B |
BIN
tests/ref/visualize/pattern-text.png
Normal file
BIN
tests/ref/visualize/pattern-text.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 29 KiB |
@ -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")
|
||||
|
@ -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"))
|
||||
|
||||
---
|
||||
|
23
tests/typ/visualize/pattern-relative.typ
Normal file
23
tests/typ/visualize/pattern-relative.typ
Normal 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)
|
14
tests/typ/visualize/pattern-small.typ
Normal file
14
tests/typ/visualize/pattern-small.typ
Normal 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))
|
||||
)
|
31
tests/typ/visualize/pattern-spacing.typ
Normal file
31
tests/typ/visualize/pattern-spacing.typ
Normal 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)
|
13
tests/typ/visualize/pattern-stroke.typ
Normal file
13
tests/typ/visualize/pattern-stroke.typ
Normal 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))
|
||||
)
|
||||
)
|
||||
)
|
28
tests/typ/visualize/pattern-text.typ
Normal file
28
tests/typ/visualize/pattern-text.typ
Normal 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)
|
||||
]))
|
@ -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))
|
||||
|
||||
---
|
||||
|
Loading…
x
Reference in New Issue
Block a user