mirror of
https://github.com/typst/typst
synced 2025-05-15 01:25:28 +08:00
179 lines
5.9 KiB
Rust
179 lines
5.9 KiB
Rust
use tiny_skia as sk;
|
|
use typst::layout::{Abs, Axes, Point, Ratio, Size};
|
|
use typst::visualize::{
|
|
DashPattern, FillRule, FixedStroke, Geometry, LineCap, LineJoin, Path, PathItem,
|
|
Shape,
|
|
};
|
|
|
|
use crate::{paint, AbsExt, State};
|
|
|
|
/// Render a geometrical shape into the canvas.
|
|
pub fn render_shape(canvas: &mut sk::Pixmap, state: State, shape: &Shape) -> Option<()> {
|
|
let ts = state.transform;
|
|
let path = match shape.geometry {
|
|
Geometry::Line(target) => {
|
|
let mut builder = sk::PathBuilder::new();
|
|
builder.line_to(target.x.to_f32(), target.y.to_f32());
|
|
builder.finish()?
|
|
}
|
|
Geometry::Rect(size) => {
|
|
let w = size.x.to_f32();
|
|
let h = size.y.to_f32();
|
|
let rect = if w < 0.0 || h < 0.0 {
|
|
// Skia doesn't normally allow for negative dimensions, but
|
|
// Typst supports them, so we apply a transform if needed
|
|
// Because this operation is expensive according to tiny-skia's
|
|
// docs, we prefer to not apply it if not needed
|
|
let transform = sk::Transform::from_scale(w.signum(), h.signum());
|
|
let rect = sk::Rect::from_xywh(0.0, 0.0, w.abs(), h.abs())?;
|
|
rect.transform(transform)?
|
|
} else {
|
|
sk::Rect::from_xywh(0.0, 0.0, w, h)?
|
|
};
|
|
|
|
sk::PathBuilder::from_rect(rect)
|
|
}
|
|
Geometry::Path(ref path) => convert_path(path)?,
|
|
};
|
|
|
|
if let Some(fill) = &shape.fill {
|
|
let mut pixmap = None;
|
|
let mut paint: sk::Paint = paint::to_sk_paint(
|
|
fill,
|
|
state,
|
|
shape.geometry.bbox_size(),
|
|
false,
|
|
None,
|
|
&mut pixmap,
|
|
None,
|
|
);
|
|
|
|
if matches!(shape.geometry, Geometry::Rect(_)) {
|
|
paint.anti_alias = false;
|
|
}
|
|
|
|
let rule = match shape.fill_rule {
|
|
FillRule::NonZero => sk::FillRule::Winding,
|
|
FillRule::EvenOdd => sk::FillRule::EvenOdd,
|
|
};
|
|
canvas.fill_path(&path, &paint, rule, ts, state.mask);
|
|
}
|
|
|
|
if let Some(FixedStroke { paint, thickness, cap, join, dash, miter_limit }) =
|
|
&shape.stroke
|
|
{
|
|
let width = thickness.to_f32();
|
|
|
|
// Don't draw zero-pt stroke.
|
|
if width > 0.0 {
|
|
let dash = dash.as_ref().and_then(to_sk_dash_pattern);
|
|
|
|
let bbox = shape.geometry.bbox_size();
|
|
let offset_bbox = (!matches!(shape.geometry, Geometry::Line(..)))
|
|
.then(|| offset_bounding_box(bbox, *thickness))
|
|
.unwrap_or(bbox);
|
|
|
|
let fill_transform =
|
|
(!matches!(shape.geometry, Geometry::Line(..))).then(|| {
|
|
sk::Transform::from_translate(
|
|
-thickness.to_f32(),
|
|
-thickness.to_f32(),
|
|
)
|
|
});
|
|
|
|
let gradient_map =
|
|
(!matches!(shape.geometry, Geometry::Line(..))).then(|| {
|
|
(
|
|
Point::new(
|
|
-*thickness * state.pixel_per_pt as f64,
|
|
-*thickness * state.pixel_per_pt as f64,
|
|
),
|
|
Axes::new(
|
|
Ratio::new(offset_bbox.x / bbox.x),
|
|
Ratio::new(offset_bbox.y / bbox.y),
|
|
),
|
|
)
|
|
});
|
|
|
|
let mut pixmap = None;
|
|
let paint = paint::to_sk_paint(
|
|
paint,
|
|
state,
|
|
offset_bbox,
|
|
false,
|
|
fill_transform,
|
|
&mut pixmap,
|
|
gradient_map,
|
|
);
|
|
let stroke = sk::Stroke {
|
|
width,
|
|
line_cap: to_sk_line_cap(*cap),
|
|
line_join: to_sk_line_join(*join),
|
|
dash,
|
|
miter_limit: miter_limit.get() as f32,
|
|
};
|
|
canvas.stroke_path(&path, &paint, &stroke, ts, state.mask);
|
|
}
|
|
}
|
|
|
|
Some(())
|
|
}
|
|
|
|
/// Convert a Typst path into a tiny-skia path.
|
|
pub fn convert_path(path: &Path) -> Option<sk::Path> {
|
|
let mut builder = sk::PathBuilder::new();
|
|
for elem in &path.0 {
|
|
match elem {
|
|
PathItem::MoveTo(p) => {
|
|
builder.move_to(p.x.to_f32(), p.y.to_f32());
|
|
}
|
|
PathItem::LineTo(p) => {
|
|
builder.line_to(p.x.to_f32(), p.y.to_f32());
|
|
}
|
|
PathItem::CubicTo(p1, p2, p3) => {
|
|
builder.cubic_to(
|
|
p1.x.to_f32(),
|
|
p1.y.to_f32(),
|
|
p2.x.to_f32(),
|
|
p2.y.to_f32(),
|
|
p3.x.to_f32(),
|
|
p3.y.to_f32(),
|
|
);
|
|
}
|
|
PathItem::ClosePath => {
|
|
builder.close();
|
|
}
|
|
};
|
|
}
|
|
builder.finish()
|
|
}
|
|
|
|
fn offset_bounding_box(bbox: Size, stroke_width: Abs) -> Size {
|
|
Size::new(bbox.x + stroke_width * 2.0, bbox.y + stroke_width * 2.0)
|
|
}
|
|
|
|
pub fn to_sk_line_cap(cap: LineCap) -> sk::LineCap {
|
|
match cap {
|
|
LineCap::Butt => sk::LineCap::Butt,
|
|
LineCap::Round => sk::LineCap::Round,
|
|
LineCap::Square => sk::LineCap::Square,
|
|
}
|
|
}
|
|
|
|
pub fn to_sk_line_join(join: LineJoin) -> sk::LineJoin {
|
|
match join {
|
|
LineJoin::Miter => sk::LineJoin::Miter,
|
|
LineJoin::Round => sk::LineJoin::Round,
|
|
LineJoin::Bevel => sk::LineJoin::Bevel,
|
|
}
|
|
}
|
|
|
|
pub fn to_sk_dash_pattern(pattern: &DashPattern<Abs, Abs>) -> Option<sk::StrokeDash> {
|
|
// tiny-skia only allows dash patterns with an even number of elements,
|
|
// while pdf allows any number.
|
|
let pattern_len = pattern.array.len();
|
|
let len = if pattern_len % 2 == 1 { 2 * pattern_len } else { pattern_len };
|
|
let dash_array = pattern.array.iter().map(|l| l.to_f32()).cycle().take(len).collect();
|
|
sk::StrokeDash::new(dash_array, pattern.phase.to_f32())
|
|
}
|