diff --git a/Cargo.lock b/Cargo.lock index cb6a1c93b..706cd4818 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -427,6 +427,14 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "db8bcd96cb740d03149cbad5518db9fd87126a10ab519c011893b1754134c468" +[[package]] +name = "pixglyph" +version = "0.1.0" +source = "git+https://github.com/typst/pixglyph#b63319b30eb34bcd7f3f3bb4c253a0731bae4234" +dependencies = [ + "ttf-parser", +] + [[package]] name = "png" version = "0.16.8" @@ -745,6 +753,7 @@ dependencies = [ "codespan-reporting", "dirs", "filedescriptor", + "flate2", "fxhash", "iai", "image", @@ -754,8 +763,10 @@ dependencies = [ "once_cell", "pdf-writer", "pico-args", + "pixglyph", "rand", "resvg", + "roxmltree", "rustybuzz", "same-file", "serde", diff --git a/Cargo.toml b/Cargo.toml index 3d2d77069..bfeeb060d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,7 +23,7 @@ bytemuck = "1" fxhash = "0.2" itertools = "0.10" once_cell = "1" -serde = { version = "1", features = ["derive", "rc"] } +serde = { version = "1", features = ["derive"] } # Text and font handling ttf-parser = "0.12" @@ -35,7 +35,6 @@ xi-unicode = "0.3" # Raster and vector graphics handling image = { version = "0.23", default-features = false, features = ["png", "jpeg"] } -resvg = { version = "0.20", default-features = false } usvg = { version = "0.20", default-features = false } # PDF export @@ -43,6 +42,13 @@ miniz_oxide = "0.4" pdf-writer = "0.4" svg2pdf = "0.2" +# Raster export / rendering +tiny-skia = "0.6.2" +pixglyph = { git = "https://github.com/typst/pixglyph" } +resvg = { version = "0.20", default-features = false } +roxmltree = "0.14" +flate2 = "1" + # Command line interface pico-args = { version = "0.4", optional = true } codespan-reporting = { version = "0.11", optional = true } @@ -59,8 +65,6 @@ rand = { version = "0.8", optional = true } [dev-dependencies] filedescriptor = "0.8" iai = { git = "https://github.com/reknih/iai" } -resvg = { version = "0.20", default-features = false } -tiny-skia = "0.6.2" walkdir = "2" [profile.dev] diff --git a/NOTICE b/NOTICE index 847dac8f7..d4b52a166 100644 --- a/NOTICE +++ b/NOTICE @@ -737,3 +737,37 @@ licenses. Creative Commons may be contacted at creativecommons.org. ================================================================================ + +================================================================================ +Alpha multiplication and source-over blending in `src/export/render.rs` are +ported from Skia code which can be found here: +https://skia.googlesource.com/skia/+/refs/heads/main/include/core/SkColorPriv.h + +Copyright (c) 2011 Google Inc. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + * Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +================================================================================ diff --git a/benches/oneshot.rs b/benches/oneshot.rs index 4f71b944f..8a5cec1b1 100644 --- a/benches/oneshot.rs +++ b/benches/oneshot.rs @@ -17,6 +17,19 @@ fn context() -> (Context, SourceId) { (ctx, id) } +main!( + bench_decode, + bench_scan, + bench_tokenize, + bench_parse, + bench_edit, + bench_eval, + bench_layout, + bench_highlight, + bench_byte_to_utf16, + bench_render, +); + fn bench_decode(iai: &mut Iai) { iai.run(|| { // We don't use chars().count() because that has a special @@ -86,14 +99,8 @@ fn bench_byte_to_utf16(iai: &mut Iai) { }); } -main!( - bench_decode, - bench_scan, - bench_tokenize, - bench_parse, - bench_edit, - bench_eval, - bench_layout, - bench_highlight, - bench_byte_to_utf16, -); +fn bench_render(iai: &mut Iai) { + let (mut ctx, id) = context(); + let frames = ctx.typeset(id).unwrap(); + iai.run(|| typst::export::render(&mut ctx, &frames[0], 1.0)) +} diff --git a/src/diag.rs b/src/diag.rs index f0efa500d..be431e12f 100644 --- a/src/diag.rs +++ b/src/diag.rs @@ -2,8 +2,6 @@ use std::fmt::{self, Display, Formatter}; -use serde::{Deserialize, Serialize}; - use crate::syntax::{Span, Spanned}; /// Early-return with a vec-boxed [`Error`]. @@ -24,7 +22,7 @@ pub type TypResult = Result>>; pub type StrResult = Result; /// An error in a source file. -#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] +#[derive(Debug, Clone, Eq, PartialEq)] pub struct Error { /// The erroneous location in the source code. pub span: Span, @@ -52,7 +50,7 @@ impl Error { } /// A part of an error's [trace](Error::trace). -#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize)] +#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd)] pub enum Tracepoint { /// A function call. Call(Option), diff --git a/src/export/mod.rs b/src/export/mod.rs index d3836859d..b782ae131 100644 --- a/src/export/mod.rs +++ b/src/export/mod.rs @@ -1,7 +1,8 @@ //! Exporting into external formats. mod pdf; +mod render; mod subset; pub use pdf::*; -pub use subset::*; +pub use render::*; diff --git a/src/export/pdf.rs b/src/export/pdf.rs index f0fd10f66..1477e2832 100644 --- a/src/export/pdf.rs +++ b/src/export/pdf.rs @@ -12,7 +12,7 @@ use pdf_writer::types::{ use pdf_writer::{Content, Filter, Finish, Name, PdfWriter, Rect, Ref, Str, TextStr}; use ttf_parser::{name_id, GlyphId, Tag}; -use super::subset; +use super::subset::subset; use crate::font::{find_name, FaceId, FontStore}; use crate::frame::{Element, Frame, Geometry, Group, Shape, Stroke, Text}; use crate::geom::{self, Color, Em, Length, Paint, Point, Size, Transform}; @@ -22,8 +22,8 @@ use crate::Context; /// Export a collection of frames into a PDF file. /// /// This creates one page per frame. In addition to the frames, you need to pass -/// in the context used during compilation such that things like fonts and -/// images can be included in the PDF. +/// in the context used during compilation so that fonts and images can be +/// included in the PDF. /// /// Returns the raw bytes making up the PDF file. pub fn pdf(ctx: &Context, frames: &[Rc]) -> Vec { diff --git a/src/export/render.rs b/src/export/render.rs new file mode 100644 index 000000000..c41bcbf2e --- /dev/null +++ b/src/export/render.rs @@ -0,0 +1,515 @@ +//! Rendering into raster images. + +use std::collections::{hash_map::Entry, HashMap}; +use std::io::Read; + +use image::{GenericImageView, Rgba}; +use tiny_skia as sk; +use ttf_parser::{GlyphId, OutlineBuilder}; +use usvg::FitTo; + +use crate::font::{Face, FaceId}; +use crate::frame::{Element, Frame, Geometry, Group, Shape, Stroke, Text}; +use crate::geom::{self, Color, Length, Paint, PathElement, Size, Transform}; +use crate::image::{Image, RasterImage, Svg}; +use crate::Context; + +/// Caches rendering artifacts. +#[derive(Default, Clone)] +pub struct RenderCache { + /// Glyphs prepared for rendering. + glyphs: HashMap<(FaceId, GlyphId), pixglyph::Glyph>, +} + +impl RenderCache { + /// Create a new, empty rendering cache. + pub fn new() -> Self { + Self::default() + } +} + +/// Export a frame into a rendered image. +/// +/// This renders the frame at the given number of pixels per printer's point and +/// returns the resulting `tiny-skia` pixel buffer. +/// +/// In addition to the frame, you need to pass in the context used during +/// compilation so that fonts and images can be rendered and rendering artifacts +/// can be cached. +pub fn render(ctx: &mut Context, frame: &Frame, pixel_per_pt: f32) -> sk::Pixmap { + let pxw = (pixel_per_pt * frame.size.x.to_f32()).round().max(1.0) as u32; + let pxh = (pixel_per_pt * frame.size.y.to_f32()).round().max(1.0) as u32; + + let mut canvas = sk::Pixmap::new(pxw, pxh).unwrap(); + canvas.fill(sk::Color::WHITE); + + let ts = sk::Transform::from_scale(pixel_per_pt, pixel_per_pt); + render_frame(&mut canvas, ts, None, ctx, frame); + + canvas +} + +/// Render all elements in a frame into the canvas. +fn render_frame( + canvas: &mut sk::Pixmap, + ts: sk::Transform, + mask: Option<&sk::ClipMask>, + ctx: &mut Context, + frame: &Frame, +) { + for (pos, element) in &frame.elements { + let x = pos.x.to_f32(); + let y = pos.y.to_f32(); + let ts = ts.pre_translate(x, y); + + match *element { + Element::Group(ref group) => { + render_group(canvas, ts, mask, ctx, group); + } + Element::Text(ref text) => { + render_text(canvas, ts, mask, ctx, text); + } + Element::Shape(ref shape) => { + render_shape(canvas, ts, mask, shape); + } + Element::Image(id, size) => { + render_image(canvas, ts, mask, ctx.images.get(id), size); + } + Element::Link(_, _) => {} + } + } +} + +/// Render a group frame with optional transform and clipping into the canvas. +fn render_group( + canvas: &mut sk::Pixmap, + ts: sk::Transform, + mask: Option<&sk::ClipMask>, + ctx: &mut Context, + group: &Group, +) { + let ts = ts.pre_concat(group.transform.into()); + + let mut mask = mask; + let mut storage; + if group.clips { + let w = group.frame.size.x.to_f32(); + let h = group.frame.size.y.to_f32(); + if let Some(path) = sk::Rect::from_xywh(0.0, 0.0, w, h) + .map(sk::PathBuilder::from_rect) + .and_then(|path| path.transform(ts)) + { + let result = if let Some(mask) = mask { + storage = mask.clone(); + storage.intersect_path(&path, sk::FillRule::default(), false) + } else { + let pxw = canvas.width(); + let pxh = canvas.height(); + storage = sk::ClipMask::new(); + storage.set_path(pxw, pxh, &path, sk::FillRule::default(), false) + }; + + // Clipping fails if clipping rect is empty. In that case we just + // clip everything by returning. + if result.is_none() { + return; + } + + mask = Some(&storage); + } + } + + render_frame(canvas, ts, mask, ctx, &group.frame); +} + +/// Render a text run into the canvas. +fn render_text( + canvas: &mut sk::Pixmap, + ts: sk::Transform, + mask: Option<&sk::ClipMask>, + ctx: &mut Context, + text: &Text, +) { + let face = ctx.fonts.get(text.face_id); + let cache = &mut ctx.render_cache; + + let mut x = 0.0; + for glyph in &text.glyphs { + let id = GlyphId(glyph.id); + let offset = x + glyph.x_offset.resolve(text.size).to_f32(); + let ts = ts.pre_translate(offset, 0.0); + + render_svg_glyph(canvas, ts, mask, text, face, id) + .or_else(|| render_bitmap_glyph(canvas, ts, mask, text, face, id)) + .or_else(|| render_outline_glyph(canvas, ts, mask, cache, text, face, id)); + + x += glyph.x_advance.resolve(text.size).to_f32(); + } +} + +/// Render an SVG glyph into the canvas. +fn render_svg_glyph( + canvas: &mut sk::Pixmap, + ts: sk::Transform, + _: Option<&sk::ClipMask>, + text: &Text, + face: &Face, + id: GlyphId, +) -> Option<()> { + let mut data = face.ttf().glyph_svg_image(id)?; + + // 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 src = std::str::from_utf8(data).ok()?; + let document = roxmltree::Document::parse(src).ok()?; + let root = document.root_element(); + + // Parse SVG. + let opts = usvg::Options::default(); + let tree = usvg::Tree::from_xmltree(&document, &opts.to_ref()).ok()?; + let view_box = tree.svg_node().view_box.rect; + + // If there's no viewbox defined, use the em square for our scale + // transformation ... + let upem = face.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() as f32; + } + + // Same as for width. + if root.has_attribute("viewBox") || root.has_attribute("height") { + height = view_box.height() as f32; + } + + // FIXME: This doesn't respect the clipping mask. + let size = text.size.to_f32(); + let ts = ts.pre_scale(size / width, size / height); + resvg::render(&tree, FitTo::Original, ts, canvas.as_mut()) +} + +/// Render a bitmap glyph into the canvas. +fn render_bitmap_glyph( + canvas: &mut sk::Pixmap, + ts: sk::Transform, + mask: Option<&sk::ClipMask>, + text: &Text, + face: &Face, + id: GlyphId, +) -> Option<()> { + let size = text.size.to_f32(); + let ppem = size * ts.sy; + let raster = face.ttf().glyph_raster_image(id, ppem as u16)?; + let img = RasterImage::parse(&raster.data).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 = (img.width() as f64 / img.height() as f64) * h; + let dx = (raster.x as f32) / (img.width() as f32) * size; + let dy = (raster.y as f32) / (img.height() as f32) * size; + let ts = ts.pre_translate(dx, -size - dy); + render_image(canvas, ts, mask, &Image::Raster(img), Size::new(w, h)) +} + +/// Render an outline glyph into the canvas. This is the "normal" case. +fn render_outline_glyph( + canvas: &mut sk::Pixmap, + ts: sk::Transform, + mask: Option<&sk::ClipMask>, + cache: &mut RenderCache, + text: &Text, + face: &Face, + id: GlyphId, +) -> Option<()> { + 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 { + let path = { + let mut builder = WrappedPathBuilder(sk::PathBuilder::new()); + face.ttf().outline_glyph(id, &mut builder)?; + builder.0.finish()? + }; + + let paint = text.fill.into(); + let rule = sk::FillRule::default(); + + // Flip vertically because font design coordinate + // system is Y-up. + let scale = text.size.to_f32() / face.units_per_em as f32; + let ts = ts.pre_scale(scale, -scale); + canvas.fill_path(&path, &paint, rule, ts, mask)?; + return Some(()); + } + + // Try to retrieve a prepared glyph or prepare it from scratch if it + // doesn't exist, yet. + let glyph = match cache.glyphs.entry((text.face_id, id)) { + Entry::Occupied(entry) => entry.into_mut(), + Entry::Vacant(entry) => { + let glyph = pixglyph::Glyph::load(face.ttf(), id)?; + entry.insert(glyph) + } + }; + + // Rasterize the glyph with `pixglyph`. + let bitmap = glyph.rasterize(ts.tx, ts.ty, ppem); + 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; + + // Premultiply the text color. + let Paint::Solid(Color::Rgba(c)) = text.fill; + let color = sk::ColorU8::from_rgba(c.r, c.g, c.b, 255).premultiply().get(); + + // Blend the glyph bitmap with the existing pixels on the canvas. + // FIXME: This doesn't respect the clipping mask. + 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 pi = (y * cw + x) as usize; + if cov == 255 { + pixels[pi] = color; + continue; + } + + let applied = alpha_mul(color, cov as u32); + pixels[pi] = blend_src_over(applied, pixels[pi]); + } + } + + Some(()) +} + +/// Renders a geometrical shape into the canvas. +fn render_shape( + canvas: &mut sk::Pixmap, + ts: sk::Transform, + mask: Option<&sk::ClipMask>, + shape: &Shape, +) -> Option<()> { + let path = match shape.geometry { + 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::Ellipse(size) => convert_path(&geom::Path::ellipse(size))?, + Geometry::Line(target) => { + let mut builder = sk::PathBuilder::new(); + builder.line_to(target.x.to_f32(), target.y.to_f32()); + builder.finish()? + } + Geometry::Path(ref path) => convert_path(path)?, + }; + + if let Some(fill) = shape.fill { + let mut paint: sk::Paint = fill.into(); + if matches!(shape.geometry, Geometry::Rect(_)) { + paint.anti_alias = false; + } + + let rule = sk::FillRule::default(); + canvas.fill_path(&path, &paint, rule, ts, mask); + } + + if let Some(Stroke { paint, thickness }) = shape.stroke { + let paint = paint.into(); + let mut stroke = sk::Stroke::default(); + stroke.width = thickness.to_f32(); + canvas.stroke_path(&path, &paint, &stroke, ts, mask); + } + + Some(()) +} + +/// Renders a raster or SVG image into the canvas. +fn render_image( + canvas: &mut sk::Pixmap, + ts: sk::Transform, + mask: Option<&sk::ClipMask>, + img: &Image, + size: Size, +) -> Option<()> { + let view_width = size.x.to_f32(); + let view_height = size.y.to_f32(); + + let pixmap = match img { + Image::Raster(img) => { + let w = img.buf.width(); + let h = img.buf.height(); + let mut pixmap = sk::Pixmap::new(w, h)?; + for ((_, _, src), dest) in img.buf.pixels().zip(pixmap.pixels_mut()) { + let Rgba([r, g, b, a]) = src; + *dest = sk::ColorU8::from_rgba(r, g, b, a).premultiply(); + } + pixmap + } + Image::Svg(Svg(tree)) => { + let size = tree.svg_node().size; + let aspect = (size.width() / size.height()) as f32; + let scale = ts.sx.max(ts.sy); + let w = (scale * view_width.max(aspect * view_height)).ceil() as u32; + let h = ((w as f32) / aspect).ceil() as u32; + let mut pixmap = sk::Pixmap::new(w, h)?; + resvg::render( + &tree, + FitTo::Size(w, h), + sk::Transform::identity(), + pixmap.as_mut(), + ); + pixmap + } + }; + + let scale_x = view_width / pixmap.width() as f32; + let scale_y = view_height / pixmap.height() as f32; + + let mut paint = sk::Paint::default(); + paint.shader = sk::Pattern::new( + pixmap.as_ref(), + sk::SpreadMode::Pad, + sk::FilterQuality::Bilinear, + 1.0, + sk::Transform::from_scale(scale_x, scale_y), + ); + + let rect = sk::Rect::from_xywh(0.0, 0.0, view_width, view_height)?; + canvas.fill_rect(rect, &paint, ts, mask); + + Some(()) +} + +/// Convert a Typst path into a tiny-skia path. +fn convert_path(path: &geom::Path) -> Option { + let mut builder = sk::PathBuilder::new(); + for elem in &path.0 { + match elem { + PathElement::MoveTo(p) => { + builder.move_to(p.x.to_f32(), p.y.to_f32()); + } + PathElement::LineTo(p) => { + builder.line_to(p.x.to_f32(), p.y.to_f32()); + } + PathElement::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(), + ); + } + PathElement::ClosePath => { + builder.close(); + } + }; + } + builder.finish() +} + +impl From for sk::Transform { + fn from(transform: Transform) -> Self { + 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(), + ) + } +} + +impl From for sk::Paint<'static> { + fn from(paint: Paint) -> Self { + let mut sk_paint = sk::Paint::default(); + let Paint::Solid(Color::Rgba(c)) = paint; + sk_paint.set_color_rgba8(c.r, c.g, c.b, c.a); + sk_paint.anti_alias = true; + sk_paint + } +} + +/// 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 LengthExt { + /// Convert an em length to a number of points as f32. + fn to_f32(self) -> f32; +} + +impl LengthExt for Length { + 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) +} diff --git a/src/font.rs b/src/font.rs index c2e1beacf..674ffa63b 100644 --- a/src/font.rs +++ b/src/font.rs @@ -13,7 +13,7 @@ use crate::loading::{FileHash, Loader}; use crate::util::decode_mac_roman; /// A unique identifier for a loaded font face. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)] +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] pub struct FaceId(u32); impl FaceId { @@ -37,7 +37,6 @@ pub struct FontStore { faces: Vec>, families: BTreeMap>, buffers: HashMap>>, - on_load: Option>, } impl FontStore { @@ -57,18 +56,9 @@ impl FontStore { faces, families, buffers: HashMap::new(), - on_load: None, } } - /// Register a callback which is invoked each time a font face is loaded. - pub fn on_load(&mut self, f: F) - where - F: Fn(FaceId, &Face) + 'static, - { - self.on_load = Some(Box::new(f)); - } - /// Query for and load the font face from the given `family` that most /// closely matches the given `variant`. pub fn select(&mut self, family: &str, variant: FontVariant) -> Option { @@ -124,10 +114,6 @@ impl FontStore { }; let face = Face::new(Rc::clone(buffer), index)?; - if let Some(callback) = &self.on_load { - callback(id, &face); - } - *slot = Some(face); } diff --git a/src/frame.rs b/src/frame.rs index 46f234468..133ba256d 100644 --- a/src/frame.rs +++ b/src/frame.rs @@ -3,14 +3,12 @@ use std::fmt::{self, Debug, Formatter}; use std::rc::Rc; -use serde::{Deserialize, Serialize}; - use crate::font::FaceId; use crate::geom::{Align, Em, Length, Paint, Path, Point, Size, Spec, Transform}; use crate::image::ImageId; /// A finished layout with elements at fixed positions. -#[derive(Default, Clone, Eq, PartialEq, Serialize, Deserialize)] +#[derive(Default, Clone, Eq, PartialEq)] pub struct Frame { /// The size of the frame. pub size: Size, @@ -133,7 +131,7 @@ impl Debug for Frame { } /// The building block frames are composed of. -#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] +#[derive(Debug, Clone, Eq, PartialEq)] pub enum Element { /// A group of elements. Group(Group), @@ -141,14 +139,14 @@ pub enum Element { Text(Text), /// A geometric shape with optional fill and stroke. Shape(Shape), - /// A raster image and its size. + /// An image and its size. Image(ImageId, Size), /// A link to an external resource and its trigger region. Link(String, Size), } /// A group of elements with optional clipping. -#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] +#[derive(Debug, Clone, Eq, PartialEq)] pub struct Group { /// The group's frame. pub frame: Rc, @@ -170,7 +168,7 @@ impl Group { } /// A run of shaped text. -#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] +#[derive(Debug, Clone, Eq, PartialEq)] pub struct Text { /// The font face the glyphs are contained in. pub face_id: FaceId, @@ -190,7 +188,7 @@ impl Text { } /// A glyph in a run of shaped text. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Serialize, Deserialize)] +#[derive(Debug, Copy, Clone, Eq, PartialEq)] pub struct Glyph { /// The glyph's index in the face. pub id: u16, @@ -201,7 +199,7 @@ pub struct Glyph { } /// A geometric shape with optional fill and stroke. -#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] +#[derive(Debug, Clone, Eq, PartialEq)] pub struct Shape { /// The shape's geometry. pub geometry: Geometry, @@ -228,7 +226,7 @@ impl Shape { } /// A shape's geometry. -#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] +#[derive(Debug, Clone, Eq, PartialEq)] pub enum Geometry { /// A line to a point (relative to its position). Line(Point), @@ -241,7 +239,7 @@ pub enum Geometry { } /// A stroke of a geometric shape. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)] +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] pub struct Stroke { /// The stroke's paint. pub paint: Paint, diff --git a/src/geom/angle.rs b/src/geom/angle.rs index af47e51df..df2aca17d 100644 --- a/src/geom/angle.rs +++ b/src/geom/angle.rs @@ -2,7 +2,6 @@ use super::*; /// An angle. #[derive(Default, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] -#[derive(Serialize, Deserialize)] pub struct Angle(Scalar); impl Angle { diff --git a/src/geom/em.rs b/src/geom/em.rs index 1868222fb..af6be7065 100644 --- a/src/geom/em.rs +++ b/src/geom/em.rs @@ -4,7 +4,6 @@ use super::*; /// /// `1em` is the same as the font size. #[derive(Default, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] -#[derive(Serialize, Deserialize)] pub struct Em(Scalar); impl Em { diff --git a/src/geom/length.rs b/src/geom/length.rs index 210dcce77..b01a7123e 100644 --- a/src/geom/length.rs +++ b/src/geom/length.rs @@ -2,8 +2,6 @@ use super::*; /// An absolute length. #[derive(Default, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] -#[derive(Serialize, Deserialize)] -#[serde(transparent)] pub struct Length(Scalar); impl Length { diff --git a/src/geom/mod.rs b/src/geom/mod.rs index 2f722f162..a03e88b04 100644 --- a/src/geom/mod.rs +++ b/src/geom/mod.rs @@ -43,8 +43,6 @@ use std::hash::{Hash, Hasher}; use std::iter::Sum; use std::ops::*; -use serde::{Deserialize, Serialize}; - /// Generic access to a structure's components. pub trait Get { /// The structure's component type. diff --git a/src/geom/paint.rs b/src/geom/paint.rs index 0eba9f2f5..d906561ce 100644 --- a/src/geom/paint.rs +++ b/src/geom/paint.rs @@ -4,7 +4,7 @@ use std::str::FromStr; use super::*; /// How a fill or stroke should be painted. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)] +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] pub enum Paint { /// A solid color. Solid(Color), @@ -20,7 +20,7 @@ where } /// A color in a dynamic format. -#[derive(Copy, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)] +#[derive(Copy, Clone, Eq, PartialEq, Hash)] pub enum Color { /// An 8-bit RGBA color. Rgba(RgbaColor), @@ -41,7 +41,7 @@ impl From for Color { } /// An 8-bit RGBA color. -#[derive(Copy, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)] +#[derive(Copy, Clone, Eq, PartialEq, Hash)] pub struct RgbaColor { /// Red channel. pub r: u8, diff --git a/src/geom/path.rs b/src/geom/path.rs index 87e20dd12..836be1b49 100644 --- a/src/geom/path.rs +++ b/src/geom/path.rs @@ -1,12 +1,11 @@ use super::*; /// A bezier path. -#[derive(Debug, Default, Clone, Eq, PartialEq, Serialize, Deserialize)] -#[serde(transparent)] +#[derive(Debug, Default, Clone, Eq, PartialEq)] pub struct Path(pub Vec); /// An element in a bezier path. -#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] +#[derive(Debug, Clone, Eq, PartialEq)] pub enum PathElement { MoveTo(Point), LineTo(Point), diff --git a/src/geom/point.rs b/src/geom/point.rs index ab8f4439f..6d77507b4 100644 --- a/src/geom/point.rs +++ b/src/geom/point.rs @@ -1,7 +1,7 @@ use super::*; /// A point in 2D. -#[derive(Default, Copy, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)] +#[derive(Default, Copy, Clone, Eq, PartialEq, Hash)] pub struct Point { /// The x coordinate. pub x: Length, diff --git a/src/geom/relative.rs b/src/geom/relative.rs index c894f4a5e..6f6b152f8 100644 --- a/src/geom/relative.rs +++ b/src/geom/relative.rs @@ -5,7 +5,6 @@ use super::*; /// _Note_: `50%` is represented as `0.5` here, but stored as `50.0` in the /// corresponding [literal](crate::syntax::ast::LitKind::Percent). #[derive(Default, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] -#[derive(Serialize, Deserialize)] pub struct Relative(Scalar); impl Relative { diff --git a/src/geom/scalar.rs b/src/geom/scalar.rs index 948ea7ecd..1435654dd 100644 --- a/src/geom/scalar.rs +++ b/src/geom/scalar.rs @@ -3,8 +3,7 @@ use super::*; /// A 64-bit float that implements `Eq`, `Ord` and `Hash`. /// /// Panics if it's `NaN` during any of those operations. -#[derive(Default, Copy, Clone, Serialize, Deserialize)] -#[serde(transparent)] +#[derive(Default, Copy, Clone)] pub struct Scalar(pub f64); impl From for Scalar { diff --git a/src/geom/spec.rs b/src/geom/spec.rs index cf75f42d9..1b8e13c2a 100644 --- a/src/geom/spec.rs +++ b/src/geom/spec.rs @@ -4,7 +4,7 @@ use std::ops::{BitAnd, BitAndAssign, BitOr, BitOrAssign, Not}; use super::*; /// A container with a horizontal and vertical component. -#[derive(Default, Copy, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)] +#[derive(Default, Copy, Clone, Eq, PartialEq, Hash)] pub struct Spec { /// The horizontal component. pub x: T, diff --git a/src/geom/transform.rs b/src/geom/transform.rs index 76615e755..eed51d460 100644 --- a/src/geom/transform.rs +++ b/src/geom/transform.rs @@ -1,7 +1,7 @@ use super::*; /// A scale-skew-translate transformation. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)] +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] pub struct Transform { pub sx: Relative, pub ky: Relative, diff --git a/src/image.rs b/src/image.rs index 3d80896fc..bd70bf284 100644 --- a/src/image.rs +++ b/src/image.rs @@ -9,12 +9,11 @@ use std::rc::Rc; use image::io::Reader as ImageReader; use image::{DynamicImage, GenericImageView, ImageFormat}; -use serde::{Deserialize, Serialize}; use crate::loading::{FileHash, Loader}; /// A unique identifier for a loaded image. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)] +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] pub struct ImageId(u32); impl ImageId { @@ -37,7 +36,6 @@ pub struct ImageStore { loader: Rc, files: HashMap, images: Vec, - on_load: Option>, } impl ImageStore { @@ -47,18 +45,9 @@ impl ImageStore { loader, files: HashMap::new(), images: vec![], - on_load: None, } } - /// Register a callback which is invoked each time an image is loaded. - pub fn on_load(&mut self, f: F) - where - F: Fn(ImageId, &Image) + 'static, - { - self.on_load = Some(Box::new(f)); - } - /// Load and decode an image file from a path. pub fn load(&mut self, path: &Path) -> io::Result { let hash = self.loader.resolve(path)?; @@ -69,9 +58,6 @@ impl ImageStore { let ext = path.extension().and_then(OsStr::to_str).unwrap_or_default(); let image = Image::parse(&buffer, ext)?; let id = ImageId(self.images.len() as u32); - if let Some(callback) = &self.on_load { - callback(id, &image); - } self.images.push(image); entry.insert(id) } diff --git a/src/layout/mod.rs b/src/layout/mod.rs index d563dafba..e4c29f9b3 100644 --- a/src/layout/mod.rs +++ b/src/layout/mod.rs @@ -79,7 +79,7 @@ pub struct LayoutContext<'a> { pub images: &'a mut ImageStore, /// Caches layouting artifacts. #[cfg(feature = "layout-cache")] - pub layouts: &'a mut LayoutCache, + pub layout_cache: &'a mut LayoutCache, /// How deeply nested the current layout tree position is. #[cfg(feature = "layout-cache")] level: usize, @@ -92,7 +92,7 @@ impl<'a> LayoutContext<'a> { fonts: &mut ctx.fonts, images: &mut ctx.images, #[cfg(feature = "layout-cache")] - layouts: &mut ctx.layouts, + layout_cache: &mut ctx.layout_cache, #[cfg(feature = "layout-cache")] level: 0, }; @@ -220,7 +220,7 @@ impl Layout for PackedNode { }; #[cfg(feature = "layout-cache")] - ctx.layouts.get(hash, regions).unwrap_or_else(|| { + ctx.layout_cache.get(hash, regions).unwrap_or_else(|| { ctx.level += 1; let frames = self.node.layout(ctx, regions, styles); ctx.level -= 1; @@ -238,7 +238,7 @@ impl Layout for PackedNode { panic!("constraints did not match regions they were created for"); } - ctx.layouts.insert(hash, entry); + ctx.layout_cache.insert(hash, entry); frames }) } diff --git a/src/lib.rs b/src/lib.rs index 39507d79f..a764468bf 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -56,6 +56,7 @@ use std::rc::Rc; use crate::diag::TypResult; use crate::eval::{Eval, EvalContext, Module, Scope, StyleMap}; +use crate::export::RenderCache; use crate::font::FontStore; use crate::frame::Frame; use crate::image::ImageStore; @@ -76,7 +77,9 @@ pub struct Context { pub images: ImageStore, /// Caches layouting artifacts. #[cfg(feature = "layout-cache")] - pub layouts: LayoutCache, + pub layout_cache: LayoutCache, + /// Caches rendering artifacts. + pub render_cache: RenderCache, /// The standard library scope. std: Scope, /// The default styles. @@ -131,7 +134,7 @@ impl Context { /// Garbage-collect caches. pub fn turnaround(&mut self) { #[cfg(feature = "layout-cache")] - self.layouts.turnaround(); + self.layout_cache.turnaround(); } } @@ -187,7 +190,8 @@ impl ContextBuilder { images: ImageStore::new(Rc::clone(&loader)), loader, #[cfg(feature = "layout-cache")] - layouts: LayoutCache::new(self.policy, self.max_size), + layout_cache: LayoutCache::new(self.policy, self.max_size), + render_cache: RenderCache::new(), std: self.std.unwrap_or_else(library::new), styles: self.styles.unwrap_or_default(), } diff --git a/src/loading/fs.rs b/src/loading/fs.rs index 12996a696..4c46c80c6 100644 --- a/src/loading/fs.rs +++ b/src/loading/fs.rs @@ -5,7 +5,6 @@ use std::rc::Rc; use memmap2::Mmap; use same_file::Handle; -use serde::{Deserialize, Serialize}; use walkdir::WalkDir; use super::{FileHash, Loader}; @@ -14,8 +13,6 @@ use crate::font::FaceInfo; /// Loads fonts and files from the local file system. /// /// _This is only available when the `fs` feature is enabled._ -#[derive(Default, Serialize, Deserialize)] -#[serde(transparent)] pub struct FsLoader { faces: Vec, } diff --git a/src/source.rs b/src/source.rs index 1e0be4507..fd42c3f7e 100644 --- a/src/source.rs +++ b/src/source.rs @@ -6,8 +6,6 @@ use std::ops::Range; use std::path::{Path, PathBuf}; use std::rc::Rc; -use serde::{Deserialize, Serialize}; - use crate::diag::TypResult; use crate::loading::{FileHash, Loader}; use crate::parse::{is_newline, parse, Reparser, Scanner}; @@ -19,7 +17,7 @@ use crate::util::{PathExt, StrExt}; use codespan_reporting::files::{self, Files}; /// A unique identifier for a loaded source file. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)] +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] pub struct SourceId(u32); impl SourceId { diff --git a/src/syntax/span.rs b/src/syntax/span.rs index 4d5b88195..ab2797f6f 100644 --- a/src/syntax/span.rs +++ b/src/syntax/span.rs @@ -2,12 +2,10 @@ use std::cmp::Ordering; use std::fmt::{self, Debug, Formatter}; use std::ops::Range; -use serde::{Deserialize, Serialize}; - use crate::source::SourceId; /// A value with the span it corresponds to in the source code. -#[derive(Copy, Clone, Eq, PartialEq, Serialize, Deserialize)] +#[derive(Copy, Clone, Eq, PartialEq)] pub struct Spanned { /// The spanned value. pub v: T, @@ -48,7 +46,7 @@ impl Debug for Spanned { } /// Bounds of a slice of source code. -#[derive(Copy, Clone, Eq, PartialEq, Serialize, Deserialize)] +#[derive(Copy, Clone, Eq, PartialEq)] pub struct Span { /// The id of the source file. pub source: SourceId, diff --git a/tests/ref/code/array.png b/tests/ref/code/array.png index 752b23182..613aab97c 100644 Binary files a/tests/ref/code/array.png and b/tests/ref/code/array.png differ diff --git a/tests/ref/code/block.png b/tests/ref/code/block.png index 0a43919d5..9cd0ba36f 100644 Binary files a/tests/ref/code/block.png and b/tests/ref/code/block.png differ diff --git a/tests/ref/code/call.png b/tests/ref/code/call.png index bd6f22f32..eefc8caef 100644 Binary files a/tests/ref/code/call.png and b/tests/ref/code/call.png differ diff --git a/tests/ref/code/closure.png b/tests/ref/code/closure.png index 17b4ef638..7d9330333 100644 Binary files a/tests/ref/code/closure.png and b/tests/ref/code/closure.png differ diff --git a/tests/ref/code/comment.png b/tests/ref/code/comment.png index c5dffd67b..d7b599763 100644 Binary files a/tests/ref/code/comment.png and b/tests/ref/code/comment.png differ diff --git a/tests/ref/code/dict.png b/tests/ref/code/dict.png index 09751ae8e..43cf13703 100644 Binary files a/tests/ref/code/dict.png and b/tests/ref/code/dict.png differ diff --git a/tests/ref/code/for.png b/tests/ref/code/for.png index 852fee49e..1ef08f7ad 100644 Binary files a/tests/ref/code/for.png and b/tests/ref/code/for.png differ diff --git a/tests/ref/code/if.png b/tests/ref/code/if.png index 65adcee21..177e61bdf 100644 Binary files a/tests/ref/code/if.png and b/tests/ref/code/if.png differ diff --git a/tests/ref/code/import.png b/tests/ref/code/import.png index 07140364e..00d3b2ee8 100644 Binary files a/tests/ref/code/import.png and b/tests/ref/code/import.png differ diff --git a/tests/ref/code/include.png b/tests/ref/code/include.png index 62166c642..f5a392e4f 100644 Binary files a/tests/ref/code/include.png and b/tests/ref/code/include.png differ diff --git a/tests/ref/code/let.png b/tests/ref/code/let.png index 0753ae052..07afcb96e 100644 Binary files a/tests/ref/code/let.png and b/tests/ref/code/let.png differ diff --git a/tests/ref/code/ops.png b/tests/ref/code/ops.png index b16ed1de3..abcab1376 100644 Binary files a/tests/ref/code/ops.png and b/tests/ref/code/ops.png differ diff --git a/tests/ref/code/repr.png b/tests/ref/code/repr.png index e0749e128..47f21b033 100644 Binary files a/tests/ref/code/repr.png and b/tests/ref/code/repr.png differ diff --git a/tests/ref/code/while.png b/tests/ref/code/while.png index e96caf958..026f99436 100644 Binary files a/tests/ref/code/while.png and b/tests/ref/code/while.png differ diff --git a/tests/ref/coma.png b/tests/ref/coma.png index d4c6c3def..34a6b30a4 100644 Binary files a/tests/ref/coma.png and b/tests/ref/coma.png differ diff --git a/tests/ref/layout/align.png b/tests/ref/layout/align.png index 57945d5c5..77619b320 100644 Binary files a/tests/ref/layout/align.png and b/tests/ref/layout/align.png differ diff --git a/tests/ref/layout/box-block.png b/tests/ref/layout/box-block.png index f6981fe55..87484c250 100644 Binary files a/tests/ref/layout/box-block.png and b/tests/ref/layout/box-block.png differ diff --git a/tests/ref/layout/columns.png b/tests/ref/layout/columns.png index 34eb19076..8a65443dc 100644 Binary files a/tests/ref/layout/columns.png and b/tests/ref/layout/columns.png differ diff --git a/tests/ref/layout/grid-1.png b/tests/ref/layout/grid-1.png index 1996e25a7..2c57e28c7 100644 Binary files a/tests/ref/layout/grid-1.png and b/tests/ref/layout/grid-1.png differ diff --git a/tests/ref/layout/grid-2.png b/tests/ref/layout/grid-2.png index e04fdadcc..649867972 100644 Binary files a/tests/ref/layout/grid-2.png and b/tests/ref/layout/grid-2.png differ diff --git a/tests/ref/layout/grid-3.png b/tests/ref/layout/grid-3.png index 505ed0edc..740ccf0a2 100644 Binary files a/tests/ref/layout/grid-3.png and b/tests/ref/layout/grid-3.png differ diff --git a/tests/ref/layout/grid-4.png b/tests/ref/layout/grid-4.png index a291658f3..c78eb7210 100644 Binary files a/tests/ref/layout/grid-4.png and b/tests/ref/layout/grid-4.png differ diff --git a/tests/ref/layout/grid-5.png b/tests/ref/layout/grid-5.png index 51703b11a..8afe9446f 100644 Binary files a/tests/ref/layout/grid-5.png and b/tests/ref/layout/grid-5.png differ diff --git a/tests/ref/layout/image.png b/tests/ref/layout/image.png index 60aa5ab2b..3bf5e56f0 100644 Binary files a/tests/ref/layout/image.png and b/tests/ref/layout/image.png differ diff --git a/tests/ref/layout/pad.png b/tests/ref/layout/pad.png index 4fd2ca479..260b4586f 100644 Binary files a/tests/ref/layout/pad.png and b/tests/ref/layout/pad.png differ diff --git a/tests/ref/layout/page.png b/tests/ref/layout/page.png index 57e4b8f1a..20ac630f7 100644 Binary files a/tests/ref/layout/page.png and b/tests/ref/layout/page.png differ diff --git a/tests/ref/layout/pagebreak.png b/tests/ref/layout/pagebreak.png index a6c0725e4..364d92946 100644 Binary files a/tests/ref/layout/pagebreak.png and b/tests/ref/layout/pagebreak.png differ diff --git a/tests/ref/layout/place-background.png b/tests/ref/layout/place-background.png index 2fd0f7118..7ac3b57b2 100644 Binary files a/tests/ref/layout/place-background.png and b/tests/ref/layout/place-background.png differ diff --git a/tests/ref/layout/place.png b/tests/ref/layout/place.png index 4fdb1f704..fc97100f9 100644 Binary files a/tests/ref/layout/place.png and b/tests/ref/layout/place.png differ diff --git a/tests/ref/layout/shape-aspect.png b/tests/ref/layout/shape-aspect.png index f76b49802..719c1e6ed 100644 Binary files a/tests/ref/layout/shape-aspect.png and b/tests/ref/layout/shape-aspect.png differ diff --git a/tests/ref/layout/shape-circle.png b/tests/ref/layout/shape-circle.png index 30734cb59..040c6f0b2 100644 Binary files a/tests/ref/layout/shape-circle.png and b/tests/ref/layout/shape-circle.png differ diff --git a/tests/ref/layout/shape-ellipse.png b/tests/ref/layout/shape-ellipse.png index e3c63427b..740f005f3 100644 Binary files a/tests/ref/layout/shape-ellipse.png and b/tests/ref/layout/shape-ellipse.png differ diff --git a/tests/ref/layout/shape-fill-stroke.png b/tests/ref/layout/shape-fill-stroke.png index 2d04b3dd6..12fcbd55b 100644 Binary files a/tests/ref/layout/shape-fill-stroke.png and b/tests/ref/layout/shape-fill-stroke.png differ diff --git a/tests/ref/layout/shape-rect.png b/tests/ref/layout/shape-rect.png index b6ab89aaf..1fdb0dac9 100644 Binary files a/tests/ref/layout/shape-rect.png and b/tests/ref/layout/shape-rect.png differ diff --git a/tests/ref/layout/shape-square.png b/tests/ref/layout/shape-square.png index 86bc1ff5d..00a0c8488 100644 Binary files a/tests/ref/layout/shape-square.png and b/tests/ref/layout/shape-square.png differ diff --git a/tests/ref/layout/spacing.png b/tests/ref/layout/spacing.png index 09f7d6d90..193be6f55 100644 Binary files a/tests/ref/layout/spacing.png and b/tests/ref/layout/spacing.png differ diff --git a/tests/ref/layout/stack-1.png b/tests/ref/layout/stack-1.png index 162ffc488..106cc7919 100644 Binary files a/tests/ref/layout/stack-1.png and b/tests/ref/layout/stack-1.png differ diff --git a/tests/ref/layout/stack-2.png b/tests/ref/layout/stack-2.png index 470b57ece..52b75bbda 100644 Binary files a/tests/ref/layout/stack-2.png and b/tests/ref/layout/stack-2.png differ diff --git a/tests/ref/layout/table.png b/tests/ref/layout/table.png index 1a576c350..bc70d5485 100644 Binary files a/tests/ref/layout/table.png and b/tests/ref/layout/table.png differ diff --git a/tests/ref/layout/transform.png b/tests/ref/layout/transform.png index 2593a3f2b..65d947584 100644 Binary files a/tests/ref/layout/transform.png and b/tests/ref/layout/transform.png differ diff --git a/tests/ref/markup/emph.png b/tests/ref/markup/emph.png index d05984064..6b3bfb2dc 100644 Binary files a/tests/ref/markup/emph.png and b/tests/ref/markup/emph.png differ diff --git a/tests/ref/markup/enums.png b/tests/ref/markup/enums.png index a201131f2..a257b0dd7 100644 Binary files a/tests/ref/markup/enums.png and b/tests/ref/markup/enums.png differ diff --git a/tests/ref/markup/escape.png b/tests/ref/markup/escape.png index 41b8c4d6a..4d2b570e6 100644 Binary files a/tests/ref/markup/escape.png and b/tests/ref/markup/escape.png differ diff --git a/tests/ref/markup/heading.png b/tests/ref/markup/heading.png index 52911d662..ac229180b 100644 Binary files a/tests/ref/markup/heading.png and b/tests/ref/markup/heading.png differ diff --git a/tests/ref/markup/linebreak.png b/tests/ref/markup/linebreak.png index 4f3678f83..2304ab96a 100644 Binary files a/tests/ref/markup/linebreak.png and b/tests/ref/markup/linebreak.png differ diff --git a/tests/ref/markup/lists.png b/tests/ref/markup/lists.png index 1405b95e6..d39f31641 100644 Binary files a/tests/ref/markup/lists.png and b/tests/ref/markup/lists.png differ diff --git a/tests/ref/markup/math.png b/tests/ref/markup/math.png index 426f3dbfb..448b2d120 100644 Binary files a/tests/ref/markup/math.png and b/tests/ref/markup/math.png differ diff --git a/tests/ref/markup/raw.png b/tests/ref/markup/raw.png index bd8b811ad..4effb3031 100644 Binary files a/tests/ref/markup/raw.png and b/tests/ref/markup/raw.png differ diff --git a/tests/ref/markup/shorthands.png b/tests/ref/markup/shorthands.png index aa6436ddc..db27ef369 100644 Binary files a/tests/ref/markup/shorthands.png and b/tests/ref/markup/shorthands.png differ diff --git a/tests/ref/markup/strong.png b/tests/ref/markup/strong.png index 6e2a84e1e..53062e9a1 100644 Binary files a/tests/ref/markup/strong.png and b/tests/ref/markup/strong.png differ diff --git a/tests/ref/style/construct.png b/tests/ref/style/construct.png index e0dcf409b..b5b3a152f 100644 Binary files a/tests/ref/style/construct.png and b/tests/ref/style/construct.png differ diff --git a/tests/ref/style/set-block.png b/tests/ref/style/set-block.png index 8ee5cfb68..898c9c659 100644 Binary files a/tests/ref/style/set-block.png and b/tests/ref/style/set-block.png differ diff --git a/tests/ref/style/set-site.png b/tests/ref/style/set-site.png index affe2e1c7..3188f7a7d 100644 Binary files a/tests/ref/style/set-site.png and b/tests/ref/style/set-site.png differ diff --git a/tests/ref/style/set-toggle.png b/tests/ref/style/set-toggle.png index ae8101cab..daaa3d6c0 100644 Binary files a/tests/ref/style/set-toggle.png and b/tests/ref/style/set-toggle.png differ diff --git a/tests/ref/text/baseline.png b/tests/ref/text/baseline.png index 6890236fe..71f75d9b9 100644 Binary files a/tests/ref/text/baseline.png and b/tests/ref/text/baseline.png differ diff --git a/tests/ref/text/basic.png b/tests/ref/text/basic.png index 88d3059c5..e7887f07c 100644 Binary files a/tests/ref/text/basic.png and b/tests/ref/text/basic.png differ diff --git a/tests/ref/text/bidi.png b/tests/ref/text/bidi.png index db8f77ac6..7e0aab8f9 100644 Binary files a/tests/ref/text/bidi.png and b/tests/ref/text/bidi.png differ diff --git a/tests/ref/text/chinese.png b/tests/ref/text/chinese.png index c47a8a2f1..aa8801c0d 100644 Binary files a/tests/ref/text/chinese.png and b/tests/ref/text/chinese.png differ diff --git a/tests/ref/text/decorations.png b/tests/ref/text/decorations.png index 183dacf95..3464beb2f 100644 Binary files a/tests/ref/text/decorations.png and b/tests/ref/text/decorations.png differ diff --git a/tests/ref/text/em.png b/tests/ref/text/em.png index 4c168db65..e989eadeb 100644 Binary files a/tests/ref/text/em.png and b/tests/ref/text/em.png differ diff --git a/tests/ref/text/features.png b/tests/ref/text/features.png index 905214275..3da67e2ba 100644 Binary files a/tests/ref/text/features.png and b/tests/ref/text/features.png differ diff --git a/tests/ref/text/font.png b/tests/ref/text/font.png index e194b00a9..cb86f9292 100644 Binary files a/tests/ref/text/font.png and b/tests/ref/text/font.png differ diff --git a/tests/ref/text/linebreaks.png b/tests/ref/text/linebreaks.png index 66697a899..1498a8453 100644 Binary files a/tests/ref/text/linebreaks.png and b/tests/ref/text/linebreaks.png differ diff --git a/tests/ref/text/links.png b/tests/ref/text/links.png index 510c7e98f..a334a435b 100644 Binary files a/tests/ref/text/links.png and b/tests/ref/text/links.png differ diff --git a/tests/ref/text/par.png b/tests/ref/text/par.png index bb705a193..9ea427136 100644 Binary files a/tests/ref/text/par.png and b/tests/ref/text/par.png differ diff --git a/tests/ref/text/shaping.png b/tests/ref/text/shaping.png index 565dcb7cd..1e57afca4 100644 Binary files a/tests/ref/text/shaping.png and b/tests/ref/text/shaping.png differ diff --git a/tests/ref/text/tracking.png b/tests/ref/text/tracking.png index 43eb5e8a9..5c35d94c6 100644 Binary files a/tests/ref/text/tracking.png and b/tests/ref/text/tracking.png differ diff --git a/tests/ref/text/whitespace.png b/tests/ref/text/whitespace.png index 36fb24752..9a1ed3cda 100644 Binary files a/tests/ref/text/whitespace.png and b/tests/ref/text/whitespace.png differ diff --git a/tests/ref/utility/basics.png b/tests/ref/utility/basics.png index 0ac0447b3..a80afe5f0 100644 Binary files a/tests/ref/utility/basics.png and b/tests/ref/utility/basics.png differ diff --git a/tests/typeset.rs b/tests/typeset.rs index a4e20774f..ac9114014 100644 --- a/tests/typeset.rs +++ b/tests/typeset.rs @@ -5,18 +5,13 @@ use std::ops::Range; use std::path::Path; use std::rc::Rc; -use image::{GenericImageView, Rgba}; use tiny_skia as sk; -use ttf_parser::{GlyphId, OutlineBuilder}; -use usvg::FitTo; use walkdir::WalkDir; use typst::diag::Error; use typst::eval::{Smart, StyleMap, Value}; -use typst::font::Face; -use typst::frame::{Element, Frame, Geometry, Shape, Stroke, Text}; -use typst::geom::{self, Color, Length, Paint, PathElement, RgbaColor, Size, Transform}; -use typst::image::{Image, RasterImage, Svg}; +use typst::frame::{Element, Frame}; +use typst::geom::Length; use typst::library::{PageNode, TextNode}; use typst::loading::FsLoader; use typst::parse::Scanner; @@ -229,7 +224,7 @@ fn test( fs::write(pdf_path, pdf_data).unwrap(); } - let canvas = draw(ctx, &frames, 2.0); + let canvas = render(ctx, &frames); fs::create_dir_all(&png_path.parent().unwrap()).unwrap(); canvas.save_png(png_path).unwrap(); @@ -325,149 +320,6 @@ fn test_part( (ok, compare_ref, frames) } -#[cfg(feature = "layout-cache")] -fn test_incremental( - ctx: &mut Context, - i: usize, - tree: &RootNode, - frames: &[Rc], -) -> bool { - let mut ok = true; - - let reference = ctx.layouts.clone(); - for level in 0 .. reference.levels() { - ctx.layouts = reference.clone(); - ctx.layouts.retain(|x| x == level); - if ctx.layouts.is_empty() { - continue; - } - - ctx.layouts.turnaround(); - - let cached = silenced(|| tree.layout(ctx)); - let total = reference.levels() - 1; - let misses = ctx - .layouts - .entries() - .filter(|e| e.level() == level && !e.hit() && e.age() == 2) - .count(); - - if misses > 0 { - println!( - " Subtest {i} relayout had {misses} cache misses on level {level} of {total} ❌", - ); - ok = false; - } - - if cached != frames { - println!( - " Subtest {i} relayout differs from clean pass on level {level} ❌", - ); - ok = false; - } - } - - ctx.layouts = reference; - ctx.layouts.turnaround(); - - ok -} - -/// Pseudorandomly edit the source file and test whether a reparse produces the -/// same result as a clean parse. -/// -/// The method will first inject 10 strings once every 400 source characters -/// and then select 5 leaf node boundries to inject an additional, randomly -/// chosen string from the injection list. -fn test_reparse(src: &str, i: usize, rng: &mut LinearShift) -> bool { - let supplements = [ - "[", - ")", - "#rect()", - "a word", - ", a: 1", - "10.0", - ":", - "if i == 0 {true}", - "for", - "* hello *", - "//", - "/*", - "\\u{12e4}", - "```typst", - " ", - "trees", - "\\", - "$ a $", - "2.", - "-", - "5", - ]; - - let mut ok = true; - - let apply = |replace: std::ops::Range, with| { - let mut incr_source = SourceFile::detached(src); - if incr_source.root().len() != src.len() { - println!( - " Subtest {i} tree length {} does not match string length {} ❌", - incr_source.root().len(), - src.len(), - ); - return false; - } - - incr_source.edit(replace.clone(), with); - let edited_src = incr_source.src(); - - let ref_source = SourceFile::detached(edited_src); - let incr_root = incr_source.root(); - let ref_root = ref_source.root(); - if incr_root != ref_root { - println!( - " Subtest {i} reparse differs from clean parse when inserting '{with}' at {}-{} ❌\n", - replace.start, replace.end, - ); - println!(" Expected reference tree:\n{ref_root:#?}\n"); - println!(" Found incremental tree:\n{incr_root:#?}"); - println!("Full source ({}):\n\"{edited_src:?}\"", edited_src.len()); - false - } else { - true - } - }; - - let mut pick = |range: Range| { - let ratio = rng.next(); - (range.start as f64 + ratio * (range.end - range.start) as f64).floor() as usize - }; - - let insertions = (src.len() as f64 / 400.0).ceil() as usize; - - for _ in 0 .. insertions { - let supplement = supplements[pick(0 .. supplements.len())]; - let start = pick(0 .. src.len()); - let end = pick(start .. src.len()); - - if !src.is_char_boundary(start) || !src.is_char_boundary(end) { - continue; - } - - ok &= apply(start .. end, supplement); - } - - let red = SourceFile::detached(src).red(); - - let leafs = red.as_ref().leafs(); - - let leaf_start = leafs[pick(0 .. leafs.len())].span().start; - let supplement = supplements[pick(0 .. supplements.len())]; - - ok &= apply(leaf_start .. leaf_start, supplement); - - ok -} - fn parse_metadata(source: &SourceFile) -> (Option, Vec) { let mut compare_ref = None; let mut errors = vec![]; @@ -525,393 +377,213 @@ fn print_error(source: &SourceFile, line: usize, error: &Error) { ); } -fn draw(ctx: &Context, frames: &[Rc], dpp: f32) -> sk::Pixmap { - let pad = Length::pt(5.0); - let width = 2.0 * pad + frames.iter().map(|l| l.size.x).max().unwrap_or_default(); - let height = pad + frames.iter().map(|l| l.size.y + pad).sum::(); +/// Pseudorandomly edit the source file and test whether a reparse produces the +/// same result as a clean parse. +/// +/// The method will first inject 10 strings once every 400 source characters +/// and then select 5 leaf node boundries to inject an additional, randomly +/// chosen string from the injection list. +fn test_reparse(src: &str, i: usize, rng: &mut LinearShift) -> bool { + let supplements = [ + "[", + ")", + "#rect()", + "a word", + ", a: 1", + "10.0", + ":", + "if i == 0 {true}", + "for", + "* hello *", + "//", + "/*", + "\\u{12e4}", + "```typst", + " ", + "trees", + "\\", + "$ a $", + "2.", + "-", + "5", + ]; - let pxw = (dpp * width.to_f32()) as u32; - let pxh = (dpp * height.to_f32()) as u32; - if pxw > 4000 || pxh > 4000 { - panic!("overlarge image: {pxw} by {pxh} ({width:?} x {height:?})",); + let mut ok = true; + + let apply = |replace: std::ops::Range, with| { + let mut incr_source = SourceFile::detached(src); + if incr_source.root().len() != src.len() { + println!( + " Subtest {i} tree length {} does not match string length {} ❌", + incr_source.root().len(), + src.len(), + ); + return false; + } + + incr_source.edit(replace.clone(), with); + + let edited_src = incr_source.src(); + let ref_source = SourceFile::detached(edited_src); + let incr_root = incr_source.root(); + let ref_root = ref_source.root(); + let same = incr_root == ref_root; + if !same { + println!( + " Subtest {i} reparse differs from clean parse when inserting '{with}' at {}-{} ❌\n", + replace.start, replace.end, + ); + println!(" Expected reference tree:\n{ref_root:#?}\n"); + println!(" Found incremental tree:\n{incr_root:#?}"); + println!("Full source ({}):\n\"{edited_src:?}\"", edited_src.len()); + } + + same + }; + + let mut pick = |range: Range| { + let ratio = rng.next(); + (range.start as f64 + ratio * (range.end - range.start) as f64).floor() as usize + }; + + let insertions = (src.len() as f64 / 400.0).ceil() as usize; + for _ in 0 .. insertions { + let supplement = supplements[pick(0 .. supplements.len())]; + let start = pick(0 .. src.len()); + let end = pick(start .. src.len()); + + if !src.is_char_boundary(start) || !src.is_char_boundary(end) { + continue; + } + + ok &= apply(start .. end, supplement); } + let red = SourceFile::detached(src).red(); + let leafs = red.as_ref().leafs(); + let leaf_start = leafs[pick(0 .. leafs.len())].span().start; + let supplement = supplements[pick(0 .. supplements.len())]; + ok &= apply(leaf_start .. leaf_start, supplement); + + ok +} + +#[cfg(feature = "layout-cache")] +fn test_incremental( + ctx: &mut Context, + i: usize, + tree: &RootNode, + frames: &[Rc], +) -> bool { + let mut ok = true; + + let reference = ctx.layout_cache.clone(); + for level in 0 .. reference.levels() { + ctx.layout_cache = reference.clone(); + ctx.layout_cache.retain(|x| x == level); + if ctx.layout_cache.is_empty() { + continue; + } + + ctx.layout_cache.turnaround(); + + let cached = silenced(|| tree.layout(ctx)); + let total = reference.levels() - 1; + let misses = ctx + .layout_cache + .entries() + .filter(|e| e.level() == level && !e.hit() && e.age() == 2) + .count(); + + if misses > 0 { + println!( + " Subtest {i} relayout had {misses} cache misses on level {level} of {total} ❌", + ); + ok = false; + } + + if cached != frames { + println!( + " Subtest {i} relayout differs from clean pass on level {level} ❌", + ); + ok = false; + } + } + + ctx.layout_cache = reference; + ctx.layout_cache.turnaround(); + + ok +} + +/// Draw all frames into one image with padding in between. +fn render(ctx: &mut Context, frames: &[Rc]) -> sk::Pixmap { + let pixel_per_pt = 2.0; + let pixmaps: Vec<_> = frames + .iter() + .map(|frame| { + let limit = Length::cm(100.0); + if frame.size.x > limit || frame.size.y > limit { + panic!("overlarge frame: {:?}", frame.size); + } + typst::export::render(ctx, frame, pixel_per_pt) + }) + .collect(); + + let pad = (5.0 * pixel_per_pt).round() as u32; + let pxw = 2 * pad + pixmaps.iter().map(sk::Pixmap::width).max().unwrap_or_default(); + let pxh = pad + pixmaps.iter().map(|pixmap| pixmap.height() + pad).sum::(); + let mut canvas = sk::Pixmap::new(pxw, pxh).unwrap(); canvas.fill(sk::Color::BLACK); - let mut mask = sk::ClipMask::new(); - let rect = sk::Rect::from_xywh(0.0, 0.0, pxw as f32, pxh as f32).unwrap(); - let path = sk::PathBuilder::from_rect(rect); - mask.set_path(pxw, pxh, &path, sk::FillRule::default(), false); + let [x, mut y] = [pad; 2]; + for (frame, mut pixmap) in frames.iter().zip(pixmaps) { + let ts = sk::Transform::from_scale(pixel_per_pt, pixel_per_pt); + render_links(&mut pixmap, ts, ctx, frame); - let mut ts = - sk::Transform::from_scale(dpp, dpp).pre_translate(pad.to_f32(), pad.to_f32()); + canvas.draw_pixmap( + x as i32, + y as i32, + pixmap.as_ref(), + &sk::PixmapPaint::default(), + sk::Transform::identity(), + None, + ); - for frame in frames { - let mut background = sk::Paint::default(); - background.set_color(sk::Color::WHITE); - - let w = frame.size.x.to_f32(); - let h = frame.size.y.to_f32(); - let rect = sk::Rect::from_xywh(0.0, 0.0, w, h).unwrap(); - canvas.fill_rect(rect, &background, ts, None); - - draw_frame(&mut canvas, ts, &mask, ctx, frame, true); - ts = ts.pre_translate(0.0, (frame.size.y + pad).to_f32()); + y += pixmap.height() + pad; } canvas } -fn draw_frame( +/// Draw extra boxes for links so we can see whether they are there. +fn render_links( canvas: &mut sk::Pixmap, ts: sk::Transform, - mask: &sk::ClipMask, ctx: &Context, frame: &Frame, - clip: bool, ) { - let mut storage; - let mut mask = mask; - if clip { - let w = frame.size.x.to_f32(); - let h = frame.size.y.to_f32(); - let rect = sk::Rect::from_xywh(0.0, 0.0, w, h).unwrap(); - let path = sk::PathBuilder::from_rect(rect).transform(ts).unwrap(); - let rule = sk::FillRule::default(); - storage = mask.clone(); - if storage.intersect_path(&path, rule, false).is_none() { - // Fails if clipping rect is empty. In that case we just clip - // everything by returning. - return; - } - mask = &storage; - } - for (pos, element) in &frame.elements { - let x = pos.x.to_f32(); - let y = pos.y.to_f32(); - let ts = ts.pre_translate(x, y); - + let ts = ts.pre_translate(pos.x.to_pt() as f32, pos.y.to_pt() as f32); match *element { Element::Group(ref group) => { - let ts = ts.pre_concat(convert_typst_transform(group.transform)); - draw_frame(canvas, ts, &mask, ctx, &group.frame, group.clips); + let ts = ts.pre_concat(group.transform.into()); + render_links(canvas, ts, ctx, &group.frame); } - Element::Text(ref text) => { - draw_text(canvas, ts, mask, ctx.fonts.get(text.face_id), text); - } - Element::Shape(ref shape) => { - draw_shape(canvas, ts, mask, shape); - } - Element::Image(id, size) => { - draw_image(canvas, ts, mask, ctx.images.get(id), size); - } - Element::Link(_, s) => { - let fill = RgbaColor::new(40, 54, 99, 40).into(); - let shape = Shape::filled(Geometry::Rect(s), fill); - draw_shape(canvas, ts, mask, &shape); + Element::Link(_, size) => { + let w = size.x.to_pt() as f32; + let h = size.y.to_pt() as f32; + let rect = sk::Rect::from_xywh(0.0, 0.0, w, h).unwrap(); + let mut paint = sk::Paint::default(); + paint.set_color_rgba8(40, 54, 99, 40); + canvas.fill_rect(rect, &paint, ts, None); } + _ => {} } } } -fn draw_text( - canvas: &mut sk::Pixmap, - ts: sk::Transform, - mask: &sk::ClipMask, - face: &Face, - text: &Text, -) { - let ttf = face.ttf(); - let size = text.size.to_f32(); - let units_per_em = face.units_per_em as f32; - let pixels_per_em = text.size.to_f32() * ts.sy; - let scale = size / units_per_em; - - let mut x = 0.0; - for glyph in &text.glyphs { - let glyph_id = GlyphId(glyph.id); - let offset = x + glyph.x_offset.resolve(text.size).to_f32(); - let ts = ts.pre_translate(offset, 0.0); - - if let Some(tree) = ttf - .glyph_svg_image(glyph_id) - .and_then(|data| std::str::from_utf8(data).ok()) - .map(|svg| { - let viewbox = format!("viewBox=\"0 0 {0} {0}\" xmlns", units_per_em); - svg.replace("xmlns", &viewbox) - }) - .and_then(|s| { - usvg::Tree::from_str(&s, &usvg::Options::default().to_ref()).ok() - }) - { - for child in tree.root().children() { - if let usvg::NodeKind::Path(node) = &*child.borrow() { - // SVG is already Y-down, no flipping required. - let ts = convert_usvg_transform(node.transform) - .post_scale(scale, scale) - .post_concat(ts); - - if let Some(fill) = &node.fill { - let path = convert_usvg_path(&node.data); - let (paint, fill_rule) = convert_usvg_fill(fill); - canvas.fill_path(&path, &paint, fill_rule, ts, Some(mask)); - } - } - } - } else if let Some(raster) = - ttf.glyph_raster_image(glyph_id, pixels_per_em as u16) - { - // TODO: Vertical alignment isn't quite right for Apple Color Emoji, - // and maybe also for Noto Color Emoji. And: Is the size calculation - // correct? - let img = RasterImage::parse(&raster.data).unwrap(); - let h = text.size; - let w = (img.width() as f64 / img.height() as f64) * h; - let dx = (raster.x as f32) / (img.width() as f32) * size; - let dy = (raster.y as f32) / (img.height() as f32) * size; - let ts = ts.pre_translate(dx, -size - dy); - draw_image(canvas, ts, mask, &Image::Raster(img), Size::new(w, h)); - } else { - // Otherwise, draw normal outline. - let mut builder = WrappedPathBuilder(sk::PathBuilder::new()); - if ttf.outline_glyph(glyph_id, &mut builder).is_some() { - // Flip vertically because font design coordinate system is Y-up. - let ts = ts.pre_scale(scale, -scale); - let path = builder.0.finish().unwrap(); - let paint = convert_typst_paint(text.fill); - canvas.fill_path(&path, &paint, sk::FillRule::default(), ts, Some(mask)); - } - } - - x += glyph.x_advance.resolve(text.size).to_f32(); - } -} - -fn draw_shape( - canvas: &mut sk::Pixmap, - ts: sk::Transform, - mask: &sk::ClipMask, - shape: &Shape, -) { - let path = match shape.geometry { - 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).unwrap(); - sk::PathBuilder::from_rect(rect) - } - Geometry::Ellipse(size) => { - let approx = geom::Path::ellipse(size); - convert_typst_path(&approx) - } - Geometry::Line(target) => { - let mut builder = sk::PathBuilder::new(); - builder.line_to(target.x.to_f32(), target.y.to_f32()); - builder.finish().unwrap() - } - Geometry::Path(ref path) => convert_typst_path(path), - }; - - if let Some(fill) = shape.fill { - let mut paint = convert_typst_paint(fill); - if matches!(shape.geometry, Geometry::Rect(_)) { - paint.anti_alias = false; - } - - let rule = sk::FillRule::default(); - canvas.fill_path(&path, &paint, rule, ts, Some(mask)); - } - - if let Some(Stroke { paint, thickness }) = shape.stroke { - let paint = convert_typst_paint(paint); - let mut stroke = sk::Stroke::default(); - stroke.width = thickness.to_f32(); - canvas.stroke_path(&path, &paint, &stroke, ts, Some(mask)); - } -} - -fn draw_image( - canvas: &mut sk::Pixmap, - ts: sk::Transform, - mask: &sk::ClipMask, - img: &Image, - size: Size, -) { - let view_width = size.x.to_f32(); - let view_height = size.y.to_f32(); - - let pixmap = match img { - Image::Raster(img) => { - let w = img.buf.width(); - let h = img.buf.height(); - let mut pixmap = sk::Pixmap::new(w, h).unwrap(); - for ((_, _, src), dest) in img.buf.pixels().zip(pixmap.pixels_mut()) { - let Rgba([r, g, b, a]) = src; - *dest = sk::ColorU8::from_rgba(r, g, b, a).premultiply(); - } - pixmap - } - Image::Svg(Svg(tree)) => { - let size = tree.svg_node().size; - let aspect = (size.width() / size.height()) as f32; - let scale = ts.sx.max(ts.sy); - let w = (scale * view_width.max(aspect * view_height)).ceil() as u32; - let h = ((w as f32) / aspect).ceil() as u32; - let mut pixmap = sk::Pixmap::new(w, h).unwrap(); - resvg::render( - &tree, - FitTo::Size(w, h), - sk::Transform::identity(), - pixmap.as_mut(), - ); - pixmap - } - }; - - let scale_x = view_width / pixmap.width() as f32; - let scale_y = view_height / pixmap.height() as f32; - - let mut paint = sk::Paint::default(); - paint.shader = sk::Pattern::new( - pixmap.as_ref(), - sk::SpreadMode::Pad, - sk::FilterQuality::Bilinear, - 1.0, - sk::Transform::from_scale(scale_x, scale_y), - ); - - let rect = sk::Rect::from_xywh(0.0, 0.0, view_width, view_height).unwrap(); - canvas.fill_rect(rect, &paint, ts, Some(mask)); -} - -fn convert_typst_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 convert_typst_paint(paint: Paint) -> sk::Paint<'static> { - let Paint::Solid(Color::Rgba(c)) = paint; - let mut paint = sk::Paint::default(); - paint.set_color_rgba8(c.r, c.g, c.b, c.a); - paint.anti_alias = true; - paint -} - -fn convert_typst_path(path: &geom::Path) -> sk::Path { - let mut builder = sk::PathBuilder::new(); - for elem in &path.0 { - match elem { - PathElement::MoveTo(p) => { - builder.move_to(p.x.to_f32(), p.y.to_f32()); - } - PathElement::LineTo(p) => { - builder.line_to(p.x.to_f32(), p.y.to_f32()); - } - PathElement::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(), - ); - } - PathElement::ClosePath => { - builder.close(); - } - }; - } - builder.finish().unwrap() -} - -fn convert_usvg_transform(transform: usvg::Transform) -> sk::Transform { - let usvg::Transform { a, b, c, d, e, f } = transform; - sk::Transform::from_row(a as _, b as _, c as _, d as _, e as _, f as _) -} - -fn convert_usvg_fill(fill: &usvg::Fill) -> (sk::Paint<'static>, sk::FillRule) { - let mut paint = sk::Paint::default(); - paint.anti_alias = true; - - if let usvg::Paint::Color(usvg::Color { red, green, blue }) = fill.paint { - paint.set_color_rgba8(red, green, blue, fill.opacity.to_u8()) - } - - let rule = match fill.rule { - usvg::FillRule::NonZero => sk::FillRule::Winding, - usvg::FillRule::EvenOdd => sk::FillRule::EvenOdd, - }; - - (paint, rule) -} - -fn convert_usvg_path(path: &usvg::PathData) -> sk::Path { - let mut builder = sk::PathBuilder::new(); - for seg in path.iter() { - match *seg { - usvg::PathSegment::MoveTo { x, y } => { - builder.move_to(x as _, y as _); - } - usvg::PathSegment::LineTo { x, y } => { - builder.line_to(x as _, y as _); - } - usvg::PathSegment::CurveTo { x1, y1, x2, y2, x, y } => { - builder.cubic_to(x1 as _, y1 as _, x2 as _, y2 as _, x as _, y as _); - } - usvg::PathSegment::ClosePath => { - builder.close(); - } - } - } - builder.finish().unwrap() -} - -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 LengthExt { - /// Convert an em length to a number of points. - fn to_f32(self) -> f32; -} - -impl LengthExt for Length { - fn to_f32(self) -> f32 { - self.to_pt() as f32 - } -} - /// Disable stdout and stderr during execution of `f`. #[cfg(feature = "layout-cache")] fn silenced(f: F) -> T