diff options
| author | LuizAugustoPapa <112978478+LuizAugustoPapa@users.noreply.github.com> | 2024-05-07 06:55:38 -0300 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2024-05-07 09:55:38 +0000 |
| commit | c9e91d4cf14b4e2d900988216a3f7a763b2855aa (patch) | |
| tree | d0d61f4a1b5e0a6516ea90f8edba2c0239f65f0b /crates/typst-render/src/text.rs | |
| parent | 2f390c53173707671387292b18cc40e4c3f12a60 (diff) | |
Refactor typst-render (#4068)
Diffstat (limited to 'crates/typst-render/src/text.rs')
| -rw-r--r-- | crates/typst-render/src/text.rs | 281 |
1 files changed, 281 insertions, 0 deletions
diff --git a/crates/typst-render/src/text.rs b/crates/typst-render/src/text.rs new file mode 100644 index 00000000..c4e83340 --- /dev/null +++ b/crates/typst-render/src/text.rs @@ -0,0 +1,281 @@ +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) +} |
