typst/crates/typst-pdf/src/pattern.rs
2023-12-19 10:36:18 +01:00

161 lines
5.1 KiB
Rust

use ecow::eco_format;
use pdf_writer::types::{ColorSpaceOperand, PaintType, TilingType};
use pdf_writer::{Filter, Finish, Name, Rect};
use typst::layout::{Abs, Ratio, 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::{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 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().x.to_pt() as _,
pattern.size().y.to_pt() as _,
))
.x_step((pattern.size().x + pattern.spacing().x).to_pt() as _)
.y_step((pattern.size().y + pattern.spacing().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.pre_concat(Transform::scale(Ratio::one(), -Ratio::one())),
))
.filter(Filter::FlateDecode);
}
}
/// A pattern and its transform.
#[derive(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.wait().clone(),
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,
on_text: bool,
transforms: Transforms,
) {
ctx.reset_stroke_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_stroke_color_space(ColorSpaceOperand::Pattern);
ctx.content.set_stroke_pattern(None, name);
ctx.resources
.insert(PageResource::new(ResourceKind::Pattern, id), index);
}
}