2024-05-07 09:55:38 +00:00

282 lines
8.9 KiB
Rust

use std::sync::Arc;
use pixglyph::Bitmap;
use tiny_skia as sk;
use ttf_parser::{GlyphId, OutlineBuilder};
use typst::layout::{Abs, Axes, Point, Size};
use typst::text::color::{frame_for_glyph, is_color_glyph};
use typst::text::{Font, TextItem};
use typst::visualize::{FixedStroke, Paint};
use crate::paint::{self, GradientSampler, PaintSampler, PatternSampler};
use crate::{shape, AbsExt, State};
/// Render a text run into the canvas.
pub fn render_text(canvas: &mut sk::Pixmap, state: State, text: &TextItem) {
let mut x = 0.0;
for glyph in &text.glyphs {
let id = GlyphId(glyph.id);
let offset = x + glyph.x_offset.at(text.size).to_f32();
if is_color_glyph(&text.font, glyph) {
let upem = text.font.units_per_em();
let text_scale = Abs::raw(text.size.to_raw() / upem);
let state = state
.pre_translate(Point::new(Abs::raw(offset as _), -text.size))
.pre_scale(Axes::new(text_scale, text_scale));
let glyph_frame = frame_for_glyph(&text.font, glyph.id);
crate::render_frame(canvas, state, &glyph_frame);
} else {
let state =
state.pre_translate(Point::new(Abs::raw(offset as _), Abs::raw(0.0)));
render_outline_glyph(canvas, state, text, id);
}
x += glyph.x_advance.at(text.size).to_f32();
}
}
/// Render an outline glyph into the canvas. This is the "normal" case.
fn render_outline_glyph(
canvas: &mut sk::Pixmap,
state: State,
text: &TextItem,
id: GlyphId,
) -> Option<()> {
let ts = &state.transform;
let ppem = text.size.to_f32() * ts.sy;
// Render a glyph directly as a path. This only happens when the fast glyph
// rasterization can't be used due to very large text size or weird
// scale/skewing transforms.
if ppem > 100.0
|| ts.kx != 0.0
|| ts.ky != 0.0
|| ts.sx != ts.sy
|| text.stroke.is_some()
{
let path = {
let mut builder = WrappedPathBuilder(sk::PathBuilder::new());
text.font.ttf().outline_glyph(id, &mut builder)?;
builder.0.finish()?
};
let scale = text.size.to_f32() / text.font.units_per_em() as f32;
let mut pixmap = None;
let rule = sk::FillRule::default();
// Flip vertically because font design coordinate
// system is Y-up.
let ts = ts.pre_scale(scale, -scale);
let state_ts = state.pre_concat(sk::Transform::from_scale(scale, -scale));
let paint = paint::to_sk_paint(
&text.fill,
state_ts,
Size::zero(),
true,
None,
&mut pixmap,
None,
);
canvas.fill_path(&path, &paint, rule, ts, state.mask);
if let Some(FixedStroke { paint, thickness, cap, join, dash, miter_limit }) =
&text.stroke
{
if thickness.to_f32() > 0.0 {
let dash = dash.as_ref().and_then(shape::to_sk_dash_pattern);
let paint = paint::to_sk_paint(
paint,
state_ts,
Size::zero(),
true,
None,
&mut pixmap,
None,
);
let stroke = sk::Stroke {
width: thickness.to_f32() / scale, // When we scale the path, we need to scale the stroke width, too.
line_cap: shape::to_sk_line_cap(*cap),
line_join: shape::to_sk_line_join(*join),
dash,
miter_limit: miter_limit.get() as f32,
};
canvas.stroke_path(&path, &paint, &stroke, ts, state.mask);
}
}
return Some(());
}
// Rasterize the glyph with `pixglyph`.
#[comemo::memoize]
fn rasterize(
font: &Font,
id: GlyphId,
x: u32,
y: u32,
size: u32,
) -> Option<Arc<Bitmap>> {
let glyph = pixglyph::Glyph::load(font.ttf(), id)?;
Some(Arc::new(glyph.rasterize(
f32::from_bits(x),
f32::from_bits(y),
f32::from_bits(size),
)))
}
// Try to retrieve a prepared glyph or prepare it from scratch if it
// doesn't exist, yet.
let bitmap =
rasterize(&text.font, id, ts.tx.to_bits(), ts.ty.to_bits(), ppem.to_bits())?;
match &text.fill {
Paint::Gradient(gradient) => {
let sampler = GradientSampler::new(gradient, &state, Size::zero(), true);
write_bitmap(canvas, &bitmap, &state, sampler)?;
}
Paint::Solid(color) => {
write_bitmap(
canvas,
&bitmap,
&state,
paint::to_sk_color_u8(*color).premultiply(),
)?;
}
Paint::Pattern(pattern) => {
let pixmap = paint::render_pattern_frame(&state, pattern);
let sampler = PatternSampler::new(pattern, &pixmap, &state, true);
write_bitmap(canvas, &bitmap, &state, sampler)?;
}
}
Some(())
}
fn write_bitmap<S: PaintSampler>(
canvas: &mut sk::Pixmap,
bitmap: &Bitmap,
state: &State,
sampler: S,
) -> Option<()> {
// If we have a clip mask we first render to a pixmap that we then blend
// with our canvas
if state.mask.is_some() {
let mw = bitmap.width;
let mh = bitmap.height;
// Pad the pixmap with 1 pixel in each dimension so that we do
// not get any problem with floating point errors along their border
let mut pixmap = sk::Pixmap::new(mw + 2, mh + 2)?;
for x in 0..mw {
for y in 0..mh {
let alpha = bitmap.coverage[(y * mw + x) as usize];
let color = sampler.sample((x, y));
pixmap.pixels_mut()[((y + 1) * (mw + 2) + (x + 1)) as usize] =
sk::ColorU8::from_rgba(
color.red(),
color.green(),
color.blue(),
alpha,
)
.premultiply();
}
}
let left = bitmap.left;
let top = bitmap.top;
canvas.draw_pixmap(
left - 1,
top - 1,
pixmap.as_ref(),
&sk::PixmapPaint::default(),
sk::Transform::identity(),
state.mask,
);
} else {
let cw = canvas.width() as i32;
let ch = canvas.height() as i32;
let mw = bitmap.width as i32;
let mh = bitmap.height as i32;
// Determine the pixel bounding box that we actually need to draw.
let left = bitmap.left;
let right = left + mw;
let top = bitmap.top;
let bottom = top + mh;
// Blend the glyph bitmap with the existing pixels on the canvas.
let pixels = bytemuck::cast_slice_mut::<u8, u32>(canvas.data_mut());
for x in left.clamp(0, cw)..right.clamp(0, cw) {
for y in top.clamp(0, ch)..bottom.clamp(0, ch) {
let ai = ((y - top) * mw + (x - left)) as usize;
let cov = bitmap.coverage[ai];
if cov == 0 {
continue;
}
let color = sampler.sample((x as _, y as _));
let color = bytemuck::cast(color);
let pi = (y * cw + x) as usize;
// Fast path if color is opaque.
if cov == u8::MAX && color & 0xFF == 0xFF {
pixels[pi] = color;
continue;
}
let applied = alpha_mul(color, cov as u32);
pixels[pi] = blend_src_over(applied, pixels[pi]);
}
}
}
Some(())
}
/// Allows to build tiny-skia paths from glyph outlines.
struct WrappedPathBuilder(sk::PathBuilder);
impl OutlineBuilder for WrappedPathBuilder {
fn move_to(&mut self, x: f32, y: f32) {
self.0.move_to(x, y);
}
fn line_to(&mut self, x: f32, y: f32) {
self.0.line_to(x, y);
}
fn quad_to(&mut self, x1: f32, y1: f32, x: f32, y: f32) {
self.0.quad_to(x1, y1, x, y);
}
fn curve_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, x: f32, y: f32) {
self.0.cubic_to(x1, y1, x2, y2, x, y);
}
fn close(&mut self) {
self.0.close();
}
}
// Alpha multiplication and blending are ported from:
// https://skia.googlesource.com/skia/+/refs/heads/main/include/core/SkColorPriv.h
/// Blends two premulitplied, packed 32-bit RGBA colors. Alpha channel must be
/// in the 8 high bits.
fn blend_src_over(src: u32, dst: u32) -> u32 {
src + alpha_mul(dst, 256 - (src >> 24))
}
/// Alpha multiply a color.
fn alpha_mul(color: u32, scale: u32) -> u32 {
let mask = 0xff00ff;
let rb = ((color & mask) * scale) >> 8;
let ag = ((color >> 8) & mask) * scale;
(rb & mask) | (ag & !mask)
}