mirror of
https://github.com/typst/typst
synced 2025-05-15 01:25:28 +08:00
197 lines
6.4 KiB
Rust
197 lines
6.4 KiB
Rust
use ecow::EcoString;
|
|
use ttf_parser::OutlineBuilder;
|
|
use typst::layout::{Abs, Ratio, Size, Transform};
|
|
use typst::visualize::{
|
|
FixedStroke, Geometry, LineCap, LineJoin, Paint, Path, PathItem, RelativeTo, Shape,
|
|
};
|
|
|
|
use crate::paint::ColorEncode;
|
|
use crate::{SVGRenderer, State, SvgPathBuilder};
|
|
|
|
impl SVGRenderer {
|
|
/// Render a shape element.
|
|
pub(super) fn render_shape(&mut self, state: State, shape: &Shape) {
|
|
self.xml.start_element("path");
|
|
self.xml.write_attribute("class", "typst-shape");
|
|
|
|
if let Some(paint) = &shape.fill {
|
|
self.write_fill(
|
|
paint,
|
|
shape.fill_rule,
|
|
self.shape_fill_size(state, paint, shape),
|
|
self.shape_paint_transform(state, paint, shape),
|
|
);
|
|
} else {
|
|
self.xml.write_attribute("fill", "none");
|
|
}
|
|
|
|
if let Some(stroke) = &shape.stroke {
|
|
self.write_stroke(
|
|
stroke,
|
|
self.shape_fill_size(state, &stroke.paint, shape),
|
|
self.shape_paint_transform(state, &stroke.paint, shape),
|
|
);
|
|
}
|
|
|
|
let path = convert_geometry_to_path(&shape.geometry);
|
|
self.xml.write_attribute("d", &path);
|
|
self.xml.end_element();
|
|
}
|
|
|
|
/// Calculate the transform of the shape's fill or stroke.
|
|
fn shape_paint_transform(
|
|
&self,
|
|
state: State,
|
|
paint: &Paint,
|
|
shape: &Shape,
|
|
) -> Transform {
|
|
let mut shape_size = shape.geometry.bbox_size();
|
|
// Edge cases for strokes.
|
|
if shape_size.x.to_pt() == 0.0 {
|
|
shape_size.x = Abs::pt(1.0);
|
|
}
|
|
|
|
if shape_size.y.to_pt() == 0.0 {
|
|
shape_size.y = Abs::pt(1.0);
|
|
}
|
|
|
|
if let Paint::Gradient(gradient) = paint {
|
|
match gradient.unwrap_relative(false) {
|
|
RelativeTo::Self_ => Transform::scale(
|
|
Ratio::new(shape_size.x.to_pt()),
|
|
Ratio::new(shape_size.y.to_pt()),
|
|
),
|
|
RelativeTo::Parent => Transform::scale(
|
|
Ratio::new(state.size.x.to_pt()),
|
|
Ratio::new(state.size.y.to_pt()),
|
|
)
|
|
.post_concat(state.transform.invert().unwrap()),
|
|
}
|
|
} else if let Paint::Pattern(pattern) = paint {
|
|
match pattern.unwrap_relative(false) {
|
|
RelativeTo::Self_ => Transform::identity(),
|
|
RelativeTo::Parent => state.transform.invert().unwrap(),
|
|
}
|
|
} else {
|
|
Transform::identity()
|
|
}
|
|
}
|
|
|
|
/// Calculate the size of the shape's fill.
|
|
fn shape_fill_size(&self, state: State, paint: &Paint, shape: &Shape) -> Size {
|
|
let mut shape_size = shape.geometry.bbox_size();
|
|
// Edge cases for strokes.
|
|
if shape_size.x.to_pt() == 0.0 {
|
|
shape_size.x = Abs::pt(1.0);
|
|
}
|
|
|
|
if shape_size.y.to_pt() == 0.0 {
|
|
shape_size.y = Abs::pt(1.0);
|
|
}
|
|
|
|
if let Paint::Gradient(gradient) = paint {
|
|
match gradient.unwrap_relative(false) {
|
|
RelativeTo::Self_ => shape_size,
|
|
RelativeTo::Parent => state.size,
|
|
}
|
|
} else {
|
|
shape_size
|
|
}
|
|
}
|
|
|
|
/// Write a stroke attribute.
|
|
pub(super) fn write_stroke(
|
|
&mut self,
|
|
stroke: &FixedStroke,
|
|
size: Size,
|
|
fill_transform: Transform,
|
|
) {
|
|
match &stroke.paint {
|
|
Paint::Solid(color) => self.xml.write_attribute("stroke", &color.encode()),
|
|
Paint::Gradient(gradient) => {
|
|
let id = self.push_gradient(gradient, size, fill_transform);
|
|
self.xml.write_attribute_fmt("stroke", format_args!("url(#{id})"));
|
|
}
|
|
Paint::Pattern(pattern) => {
|
|
let id = self.push_pattern(pattern, size, fill_transform);
|
|
self.xml.write_attribute_fmt("stroke", format_args!("url(#{id})"));
|
|
}
|
|
}
|
|
|
|
self.xml.write_attribute("stroke-width", &stroke.thickness.to_pt());
|
|
self.xml.write_attribute(
|
|
"stroke-linecap",
|
|
match stroke.cap {
|
|
LineCap::Butt => "butt",
|
|
LineCap::Round => "round",
|
|
LineCap::Square => "square",
|
|
},
|
|
);
|
|
self.xml.write_attribute(
|
|
"stroke-linejoin",
|
|
match stroke.join {
|
|
LineJoin::Miter => "miter",
|
|
LineJoin::Round => "round",
|
|
LineJoin::Bevel => "bevel",
|
|
},
|
|
);
|
|
self.xml
|
|
.write_attribute("stroke-miterlimit", &stroke.miter_limit.get());
|
|
if let Some(pattern) = &stroke.dash {
|
|
self.xml.write_attribute("stroke-dashoffset", &pattern.phase.to_pt());
|
|
self.xml.write_attribute(
|
|
"stroke-dasharray",
|
|
&pattern
|
|
.array
|
|
.iter()
|
|
.map(|dash| dash.to_pt().to_string())
|
|
.collect::<Vec<_>>()
|
|
.join(" "),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Convert a geometry to an SVG path.
|
|
#[comemo::memoize]
|
|
fn convert_geometry_to_path(geometry: &Geometry) -> EcoString {
|
|
let mut builder = SvgPathBuilder::default();
|
|
match geometry {
|
|
Geometry::Line(t) => {
|
|
builder.move_to(0.0, 0.0);
|
|
builder.line_to(t.x.to_pt() as f32, t.y.to_pt() as f32);
|
|
}
|
|
Geometry::Rect(rect) => {
|
|
let x = rect.x.to_pt() as f32;
|
|
let y = rect.y.to_pt() as f32;
|
|
builder.rect(x, y);
|
|
}
|
|
Geometry::Path(p) => return convert_path(p),
|
|
};
|
|
builder.0
|
|
}
|
|
|
|
pub fn convert_path(path: &Path) -> EcoString {
|
|
let mut builder = SvgPathBuilder::default();
|
|
for item in &path.0 {
|
|
match item {
|
|
PathItem::MoveTo(m) => {
|
|
builder.move_to(m.x.to_pt() as f32, m.y.to_pt() as f32)
|
|
}
|
|
PathItem::LineTo(l) => {
|
|
builder.line_to(l.x.to_pt() as f32, l.y.to_pt() as f32)
|
|
}
|
|
PathItem::CubicTo(c1, c2, t) => builder.curve_to(
|
|
c1.x.to_pt() as f32,
|
|
c1.y.to_pt() as f32,
|
|
c2.x.to_pt() as f32,
|
|
c2.y.to_pt() as f32,
|
|
t.x.to_pt() as f32,
|
|
t.y.to_pt() as f32,
|
|
),
|
|
PathItem::ClosePath => builder.close(),
|
|
}
|
|
}
|
|
builder.0
|
|
}
|