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 {
|
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),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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(),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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::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)
|
||||||
|
@ -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) {
|
||||||
|
@ -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),
|
||||||
|
@ -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! {
|
||||||
|
@ -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>;
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
@ -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).
|
||||||
|
@ -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>();
|
||||||
|
@ -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),
|
||||||
}
|
}
|
||||||
|
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::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>> {
|
||||||
|
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()
|
#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")
|
||||||
|
@ -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"))
|
||||||
|
|
||||||
---
|
---
|
||||||
|
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))
|
#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))
|
||||||
|
|
||||||
---
|
---
|
||||||
|
Loading…
x
Reference in New Issue
Block a user