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> { 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( 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::(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) }