summaryrefslogtreecommitdiff
path: root/crates/typst-render/src/text.rs
diff options
context:
space:
mode:
authorLuizAugustoPapa <112978478+LuizAugustoPapa@users.noreply.github.com>2024-05-07 06:55:38 -0300
committerGitHub <noreply@github.com>2024-05-07 09:55:38 +0000
commitc9e91d4cf14b4e2d900988216a3f7a763b2855aa (patch)
treed0d61f4a1b5e0a6516ea90f8edba2c0239f65f0b /crates/typst-render/src/text.rs
parent2f390c53173707671387292b18cc40e4c3f12a60 (diff)
Refactor typst-render (#4068)
Diffstat (limited to 'crates/typst-render/src/text.rs')
-rw-r--r--crates/typst-render/src/text.rs281
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)
+}