//! Rendering into raster images. use std::io::Read; use std::sync::Arc; use image::imageops::FilterType; use image::{GenericImageView, Rgba}; use pixglyph::Bitmap; use resvg::tiny_skia::IntRect; use tiny_skia as sk; use ttf_parser::{GlyphId, OutlineBuilder}; use typst::introspection::Meta; use typst::layout::{ Abs, Axes, Frame, FrameItem, FrameKind, GroupItem, Point, Ratio, Size, Transform, }; use typst::model::Document; use typst::text::{Font, TextItem}; use typst::visualize::{ Color, DashPattern, FixedStroke, Geometry, Gradient, Image, ImageKind, LineCap, LineJoin, Paint, Path, PathItem, Pattern, RasterFormat, RelativeTo, Shape, }; use usvg::TreeParsing; /// Export a frame into a raster image. /// /// This renders the frame at the given number of pixels per point and returns /// the resulting `tiny-skia` pixel buffer. #[typst_macros::time(name = "render")] pub fn render(frame: &Frame, pixel_per_pt: f32, fill: Color) -> sk::Pixmap { let size = frame.size(); let pxw = (pixel_per_pt * size.x.to_f32()).round().max(1.0) as u32; let pxh = (pixel_per_pt * size.y.to_f32()).round().max(1.0) as u32; let mut canvas = sk::Pixmap::new(pxw, pxh).unwrap(); canvas.fill(to_sk_color(fill)); let ts = sk::Transform::from_scale(pixel_per_pt, pixel_per_pt); render_frame(&mut canvas, State::new(size, ts, pixel_per_pt), frame); canvas } /// Export a document with potentially multiple pages into a single raster image. /// /// The padding will be added around and between the individual frames. pub fn render_merged( document: &Document, pixel_per_pt: f32, frame_fill: Color, padding: Abs, padding_fill: Color, ) -> sk::Pixmap { let pixmaps: Vec<_> = document .pages .iter() .map(|page| render(&page.frame, pixel_per_pt, frame_fill)) .collect(); let padding = (pixel_per_pt * padding.to_f32()).round() as u32; let pxw = 2 * padding + pixmaps.iter().map(sk::Pixmap::width).max().unwrap_or_default(); let pxh = padding + pixmaps.iter().map(|pixmap| pixmap.height() + padding).sum::(); let mut canvas = sk::Pixmap::new(pxw, pxh).unwrap(); canvas.fill(to_sk_color(padding_fill)); let [x, mut y] = [padding; 2]; for pixmap in pixmaps { canvas.draw_pixmap( x as i32, y as i32, pixmap.as_ref(), &sk::PixmapPaint::default(), sk::Transform::identity(), None, ); y += pixmap.height() + padding; } canvas } /// Additional metadata carried through the rendering process. #[derive(Clone, Copy, Default)] struct State<'a> { /// The transform of the current item. transform: sk::Transform, /// The transform of the first hard frame in the hierarchy. container_transform: sk::Transform, /// The mask of the current item. mask: Option<&'a sk::Mask>, /// The pixel per point ratio. pixel_per_pt: f32, /// The size of the first hard frame in the hierarchy. size: Size, } impl<'a> State<'a> { fn new(size: Size, transform: sk::Transform, pixel_per_pt: f32) -> Self { Self { size, transform, container_transform: transform, pixel_per_pt, ..Default::default() } } /// Pre translate the current item's transform. fn pre_translate(self, pos: Point) -> Self { Self { transform: self.transform.pre_translate(pos.x.to_f32(), pos.y.to_f32()), ..self } } /// Pre concat the current item's transform. fn pre_concat(self, transform: sk::Transform) -> Self { Self { transform: self.transform.pre_concat(transform), ..self } } /// Sets the current mask. fn with_mask(self, mask: Option<&sk::Mask>) -> State<'_> { // Ensure that we're using the parent's mask if we don't have one. if mask.is_some() { State { mask, ..self } } else { State { mask: None, ..self } } } /// Sets the size of the first hard frame in the hierarchy. fn with_size(self, size: Size) -> Self { Self { size, ..self } } /// Pre concat the container's transform. fn pre_concat_container(self, transform: sk::Transform) -> Self { Self { container_transform: self.container_transform.pre_concat(transform), ..self } } } /// Render a frame into the canvas. fn render_frame(canvas: &mut sk::Pixmap, state: State, frame: &Frame) { for (pos, item) in frame.items() { match item { FrameItem::Group(group) => { render_group(canvas, state, *pos, group); } FrameItem::Text(text) => { render_text(canvas, state.pre_translate(*pos), text); } FrameItem::Shape(shape, _) => { render_shape(canvas, state.pre_translate(*pos), shape); } FrameItem::Image(image, size, _) => { render_image(canvas, state.pre_translate(*pos), image, *size); } FrameItem::Meta(meta, _) => match meta { Meta::Link(_) => {} Meta::Elem(_) => {} Meta::Hide => {} }, } } } /// Render a group frame with optional transform and clipping into the canvas. fn render_group(canvas: &mut sk::Pixmap, state: State, pos: Point, group: &GroupItem) { let sk_transform = to_sk_transform(&group.transform); let state = match group.frame.kind() { FrameKind::Soft => state.pre_translate(pos).pre_concat(sk_transform), FrameKind::Hard => state .pre_translate(pos) .pre_concat(sk_transform) .pre_concat_container( state .transform .post_concat(state.container_transform.invert().unwrap()), ) .pre_concat_container(to_sk_transform(&Transform::translate(pos.x, pos.y))) .pre_concat_container(sk_transform) .with_size(group.frame.size()), }; let mut mask = state.mask; let storage; if let Some(clip_path) = group.clip_path.as_ref() { if let Some(path) = convert_path(clip_path).and_then(|path| path.transform(state.transform)) { if let Some(mask) = mask { let mut mask = mask.clone(); mask.intersect_path( &path, sk::FillRule::default(), false, sk::Transform::default(), ); storage = mask; } else { let pxw = canvas.width(); let pxh = canvas.height(); let Some(mut mask) = sk::Mask::new(pxw, pxh) else { // Fails if clipping rect is empty. In that case we just // clip everything by returning. return; }; mask.fill_path( &path, sk::FillRule::default(), false, sk::Transform::default(), ); storage = mask; }; mask = Some(&storage); } } render_frame(canvas, state.with_mask(mask), &group.frame); } /// Render a text run into the canvas. 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(); let state = state.pre_translate(Point::new(Abs::raw(offset as _), Abs::raw(0.0))); render_svg_glyph(canvas, state, text, id) .or_else(|| render_bitmap_glyph(canvas, state, text, id)) .or_else(|| render_outline_glyph(canvas, state, text, id)); x += glyph.x_advance.at(text.size).to_f32(); } } /// Render an SVG glyph into the canvas. fn render_svg_glyph( canvas: &mut sk::Pixmap, state: State, text: &TextItem, id: GlyphId, ) -> Option<()> { let ts = &state.transform; let mut data = text.font.ttf().glyph_svg_image(id)?.data; // Decompress SVGZ. let mut decoded = vec![]; if data.starts_with(&[0x1f, 0x8b]) { let mut decoder = flate2::read::GzDecoder::new(data); decoder.read_to_end(&mut decoded).ok()?; data = &decoded; } // Parse XML. let xml = std::str::from_utf8(data).ok()?; let document = roxmltree::Document::parse(xml).ok()?; let root = document.root_element(); // Parse SVG. let opts = usvg::Options::default(); let mut tree = usvg::Tree::from_xmltree(&document, &opts).ok()?; tree.calculate_bounding_boxes(); let view_box = tree.view_box.rect; // If there's no viewbox defined, use the em square for our scale // transformation ... let upem = text.font.units_per_em() as f32; let (mut width, mut height) = (upem, upem); // ... but if there's a viewbox or width, use that. if root.has_attribute("viewBox") || root.has_attribute("width") { width = view_box.width(); } // Same as for width. if root.has_attribute("viewBox") || root.has_attribute("height") { height = view_box.height(); } let size = text.size.to_f32(); let ts = ts.pre_scale(size / width, size / height); // Compute the space we need to draw our glyph. // See https://github.com/RazrFalcon/resvg/issues/602 for why // using the svg size is problematic here. let mut bbox = usvg::BBox::default(); if let Some(tree_bbox) = tree.root.bounding_box { bbox = bbox.expand(tree_bbox); } // Compute the bbox after the transform is applied. // We add a nice 5px border along the bounding box to // be on the safe size. We also compute the intersection // with the canvas rectangle let bbox = bbox.transform(ts)?.to_rect()?.round_out()?; let bbox = IntRect::from_xywh( bbox.left() - 5, bbox.y() - 5, bbox.width() + 10, bbox.height() + 10, )?; let mut pixmap = sk::Pixmap::new(bbox.width(), bbox.height())?; // We offset our transform so that the pixmap starts at the edge of the bbox. let ts = ts.post_translate(-bbox.left() as f32, -bbox.top() as f32); resvg::render(&tree, ts, &mut pixmap.as_mut()); canvas.draw_pixmap( bbox.left(), bbox.top(), pixmap.as_ref(), &sk::PixmapPaint::default(), sk::Transform::identity(), state.mask, ); Some(()) } /// Render a bitmap glyph into the canvas. fn render_bitmap_glyph( canvas: &mut sk::Pixmap, state: State, text: &TextItem, id: GlyphId, ) -> Option<()> { let ts = state.transform; let size = text.size.to_f32(); let ppem = size * ts.sy; let raster = text.font.ttf().glyph_raster_image(id, ppem as u16)?; if raster.format != ttf_parser::RasterImageFormat::PNG { return None; } let image = Image::new(raster.data.into(), RasterFormat::Png.into(), None).ok()?; // FIXME: Vertical alignment isn't quite right for Apple Color Emoji, // and maybe also for Noto Color Emoji. And: Is the size calculation // correct? let h = text.size; let w = (image.width() / image.height()) * h; let dx = (raster.x as f32) / (image.width() as f32) * size; let dy = (raster.y as f32) / (image.height() as f32) * size; render_image( canvas, state.pre_translate(Point::new(Abs::raw(dx as _), Abs::raw((-size - dy) as _))), &image, Size::new(w, h), ) } /// 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 = 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(to_sk_dash_pattern); let 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: 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); } } 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, to_sk_color_u8(*color).premultiply())?; } Paint::Pattern(pattern) => { let pixmap = 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(()) } /// Render a geometrical shape into the canvas. 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 = 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 = 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 = sk::FillRule::default(); 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 = 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. fn convert_path(path: &Path) -> Option { 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() } /// Render a raster or SVG image into the canvas. fn render_image( canvas: &mut sk::Pixmap, state: State, image: &Image, size: Size, ) -> Option<()> { let ts = state.transform; let view_width = size.x.to_f32(); let view_height = size.y.to_f32(); // For better-looking output, resize `image` to its final size before // painting it to `canvas`. For the math, see: // https://github.com/typst/typst/issues/1404#issuecomment-1598374652 let theta = f32::atan2(-ts.kx, ts.sx); // To avoid division by 0, choose the one of { sin, cos } that is // further from 0. let prefer_sin = theta.sin().abs() > std::f32::consts::FRAC_1_SQRT_2; let scale_x = f32::abs(if prefer_sin { ts.kx / theta.sin() } else { ts.sx / theta.cos() }); let aspect = (image.width() as f32) / (image.height() as f32); let w = (scale_x * view_width.max(aspect * view_height)).ceil() as u32; let h = ((w as f32) / aspect).ceil() as u32; let pixmap = scaled_texture(image, w, h)?; let paint_scale_x = view_width / pixmap.width() as f32; let paint_scale_y = view_height / pixmap.height() as f32; let paint = sk::Paint { shader: sk::Pattern::new( (*pixmap).as_ref(), sk::SpreadMode::Pad, sk::FilterQuality::Nearest, 1.0, sk::Transform::from_scale(paint_scale_x, paint_scale_y), ), ..Default::default() }; let rect = sk::Rect::from_xywh(0.0, 0.0, view_width, view_height)?; canvas.fill_rect(rect, &paint, ts, state.mask); Some(()) } /// Prepare a texture for an image at a scaled size. #[comemo::memoize] fn scaled_texture(image: &Image, w: u32, h: u32) -> Option> { let mut pixmap = sk::Pixmap::new(w, h)?; match image.kind() { ImageKind::Raster(raster) => { let downscale = w < raster.width(); let filter = if downscale { FilterType::Lanczos3 } else { FilterType::CatmullRom }; let buf = raster.dynamic().resize(w, h, filter); for ((_, _, src), dest) in buf.pixels().zip(pixmap.pixels_mut()) { let Rgba([r, g, b, a]) = src; *dest = sk::ColorU8::from_rgba(r, g, b, a).premultiply(); } } // Safety: We do not keep any references to tree nodes beyond the scope // of `with`. ImageKind::Svg(svg) => unsafe { svg.with(|tree| { let ts = tiny_skia::Transform::from_scale( w as f32 / tree.size.width(), h as f32 / tree.size.height(), ); resvg::render(tree, ts, &mut pixmap.as_mut()) }); }, } Some(Arc::new(pixmap)) } /// Trait for sampling of a paint, used as a generic /// abstraction over solid colors and gradients. trait PaintSampler: Copy { /// Sample the color at the `pos` in the pixmap. fn sample(self, pos: (u32, u32)) -> sk::PremultipliedColorU8; } impl PaintSampler for sk::PremultipliedColorU8 { fn sample(self, _: (u32, u32)) -> sk::PremultipliedColorU8 { self } } /// State used when sampling colors for text. /// /// It caches the inverse transform to the parent, so that we can /// reuse it instead of recomputing it for each pixel. #[derive(Clone, Copy)] struct GradientSampler<'a> { gradient: &'a Gradient, container_size: Size, transform_to_parent: sk::Transform, } impl<'a> GradientSampler<'a> { fn new( gradient: &'a Gradient, state: &State, item_size: Size, on_text: bool, ) -> Self { let relative = gradient.unwrap_relative(on_text); let container_size = match relative { RelativeTo::Self_ => item_size, RelativeTo::Parent => state.size, }; let fill_transform = match relative { RelativeTo::Self_ => sk::Transform::identity(), RelativeTo::Parent => state.container_transform.invert().unwrap(), }; Self { gradient, container_size, transform_to_parent: fill_transform, } } } impl PaintSampler for GradientSampler<'_> { /// Samples a single point in a glyph. fn sample(self, (x, y): (u32, u32)) -> sk::PremultipliedColorU8 { // Compute the point in the gradient's coordinate space. let mut point = sk::Point { x: x as f32, y: y as f32 }; self.transform_to_parent.map_point(&mut point); // Sample the gradient to_sk_color_u8(self.gradient.sample_at( (point.x, point.y), (self.container_size.x.to_f32(), self.container_size.y.to_f32()), )) .premultiply() } } /// State used when sampling patterns for text. /// /// It caches the inverse transform to the parent, so that we can /// reuse it instead of recomputing it for each pixel. #[derive(Clone, Copy)] struct PatternSampler<'a> { size: Size, transform_to_parent: sk::Transform, pixmap: &'a sk::Pixmap, pixel_per_pt: f32, } impl<'a> PatternSampler<'a> { fn new( pattern: &'a Pattern, pixmap: &'a sk::Pixmap, state: &State, on_text: bool, ) -> Self { let relative = pattern.unwrap_relative(on_text); let fill_transform = match relative { RelativeTo::Self_ => sk::Transform::identity(), RelativeTo::Parent => state.container_transform.invert().unwrap(), }; Self { pixmap, size: (pattern.size() + pattern.spacing()) * state.pixel_per_pt as f64, transform_to_parent: fill_transform, pixel_per_pt: state.pixel_per_pt, } } } impl PaintSampler for PatternSampler<'_> { /// Samples a single point in a glyph. fn sample(self, (x, y): (u32, u32)) -> sk::PremultipliedColorU8 { // Compute the point in the pattern's coordinate space. let mut point = sk::Point { x: x as f32, y: y as f32 }; self.transform_to_parent.map_point(&mut point); let x = (point.x * self.pixel_per_pt).rem_euclid(self.size.x.to_f32()).floor() as u32; let y = (point.y * self.pixel_per_pt).rem_euclid(self.size.y.to_f32()).floor() as u32; // Sample the pattern self.pixmap.pixel(x, y).unwrap() } } /// Transforms a [`Paint`] into a [`sk::Paint`]. /// Applying the necessary transform, if the paint is a gradient. /// /// `gradient_map` is used to scale and move the gradient being sampled, /// this is used to line up the stroke and the fill of a shape. fn to_sk_paint<'a>( paint: &Paint, state: State, item_size: Size, on_text: bool, fill_transform: Option, pixmap: &'a mut Option>, gradient_map: Option<(Point, Axes)>, ) -> sk::Paint<'a> { /// Actual sampling of the gradient, cached for performance. #[comemo::memoize] fn cached( gradient: &Gradient, width: u32, height: u32, gradient_map: Option<(Point, Axes)>, ) -> Arc { let (offset, scale) = gradient_map.unwrap_or_else(|| (Point::zero(), Axes::splat(Ratio::one()))); let mut pixmap = sk::Pixmap::new(width.max(1), height.max(1)).unwrap(); for x in 0..width { for y in 0..height { let color = gradient.sample_at( ( (x as f32 + offset.x.to_f32()) * scale.x.get() as f32, (y as f32 + offset.y.to_f32()) * scale.y.get() as f32, ), (width as f32, height as f32), ); pixmap.pixels_mut()[(y * width + x) as usize] = to_sk_color(color).premultiply().to_color_u8(); } } Arc::new(pixmap) } let mut sk_paint: sk::Paint<'_> = sk::Paint::default(); match paint { Paint::Solid(color) => { sk_paint.set_color(to_sk_color(*color)); sk_paint.anti_alias = true; } Paint::Gradient(gradient) => { let relative = gradient.unwrap_relative(on_text); let container_size = match relative { RelativeTo::Self_ => item_size, RelativeTo::Parent => state.size, }; let fill_transform = match relative { RelativeTo::Self_ => fill_transform.unwrap_or_default(), RelativeTo::Parent => state .container_transform .post_concat(state.transform.invert().unwrap()), }; let width = (container_size.x.to_f32() * state.pixel_per_pt).ceil() as u32; let height = (container_size.y.to_f32() * state.pixel_per_pt).ceil() as u32; *pixmap = Some(cached( gradient, width.max(state.pixel_per_pt.ceil() as u32), height.max(state.pixel_per_pt.ceil() as u32), gradient_map, )); // We can use FilterQuality::Nearest here because we're // rendering to a pixmap that is already at native resolution. sk_paint.shader = sk::Pattern::new( pixmap.as_ref().unwrap().as_ref().as_ref(), sk::SpreadMode::Pad, sk::FilterQuality::Nearest, 1.0, fill_transform .pre_scale(1.0 / state.pixel_per_pt, 1.0 / state.pixel_per_pt), ); sk_paint.anti_alias = gradient.anti_alias(); } Paint::Pattern(pattern) => { let relative = pattern.unwrap_relative(on_text); let fill_transform = match relative { RelativeTo::Self_ => fill_transform.unwrap_or_default(), RelativeTo::Parent => state .container_transform .post_concat(state.transform.invert().unwrap()), }; let canvas = render_pattern_frame(&state, pattern); *pixmap = Some(Arc::new(canvas)); // Create the shader sk_paint.shader = sk::Pattern::new( pixmap.as_ref().unwrap().as_ref().as_ref(), sk::SpreadMode::Repeat, sk::FilterQuality::Nearest, 1.0, fill_transform .pre_scale(1.0 / state.pixel_per_pt, 1.0 / state.pixel_per_pt), ); } } sk_paint } fn render_pattern_frame(state: &State, pattern: &Pattern) -> sk::Pixmap { let size = pattern.size() + pattern.spacing(); let mut canvas = sk::Pixmap::new( (size.x.to_f32() * state.pixel_per_pt).round() as u32, (size.y.to_f32() * state.pixel_per_pt).round() as u32, ) .unwrap(); // Render the pattern into a new canvas. let ts = sk::Transform::from_scale(state.pixel_per_pt, state.pixel_per_pt); let temp_state = State::new(pattern.size(), ts, state.pixel_per_pt); render_frame(&mut canvas, temp_state, pattern.frame()); canvas } fn to_sk_color(color: Color) -> sk::Color { let [r, g, b, a] = color.to_rgb().to_vec4(); sk::Color::from_rgba(r, g, b, a) .expect("components must always be in the range [0..=1]") } fn to_sk_color_u8(color: Color) -> sk::ColorU8 { let [r, g, b, a] = color.to_rgb().to_vec4_u8(); sk::ColorU8::from_rgba(r, g, b, a) } 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, } } 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, } } fn to_sk_transform(transform: &Transform) -> sk::Transform { let Transform { sx, ky, kx, sy, tx, ty } = *transform; sk::Transform::from_row( sx.get() as _, ky.get() as _, kx.get() as _, sy.get() as _, tx.to_f32(), ty.to_f32(), ) } fn to_sk_dash_pattern(pattern: &DashPattern) -> Option { // 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()) } /// 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(); } } /// Additional methods for [`Length`]. trait AbsExt { /// Convert to a number of points as f32. fn to_f32(self) -> f32; } impl AbsExt for Abs { fn to_f32(self) -> f32 { self.to_pt() as f32 } } // 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) } fn offset_bounding_box(bbox: Size, stroke_width: Abs) -> Size { Size::new(bbox.x + stroke_width * 2.0, bbox.y + stroke_width * 2.0) }