summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLaurenz <laurmaedje@gmail.com>2022-01-24 16:48:24 +0100
committerLaurenz <laurmaedje@gmail.com>2022-01-24 17:39:49 +0100
commit3739ab77207e0e54edb55a110a16a1eb925b84f4 (patch)
tree06c8e5987d2fe070ad273ef94641161bbaef7095
parentdb158719d67fdef1d2c76300fb232cf2d4bfb35d (diff)
Export into rendered images
-rw-r--r--Cargo.lock11
-rw-r--r--Cargo.toml12
-rw-r--r--NOTICE34
-rw-r--r--benches/oneshot.rs29
-rw-r--r--src/diag.rs6
-rw-r--r--src/export/mod.rs3
-rw-r--r--src/export/pdf.rs6
-rw-r--r--src/export/render.rs515
-rw-r--r--src/font.rs16
-rw-r--r--src/frame.rs20
-rw-r--r--src/geom/angle.rs1
-rw-r--r--src/geom/em.rs1
-rw-r--r--src/geom/length.rs2
-rw-r--r--src/geom/mod.rs2
-rw-r--r--src/geom/paint.rs6
-rw-r--r--src/geom/path.rs5
-rw-r--r--src/geom/point.rs2
-rw-r--r--src/geom/relative.rs1
-rw-r--r--src/geom/scalar.rs3
-rw-r--r--src/geom/spec.rs2
-rw-r--r--src/geom/transform.rs2
-rw-r--r--src/image.rs16
-rw-r--r--src/layout/mod.rs8
-rw-r--r--src/lib.rs10
-rw-r--r--src/loading/fs.rs3
-rw-r--r--src/source.rs4
-rw-r--r--src/syntax/span.rs6
-rw-r--r--tests/ref/code/array.pngbin1899 -> 2534 bytes
-rw-r--r--tests/ref/code/block.pngbin1374 -> 2005 bytes
-rw-r--r--tests/ref/code/call.pngbin2397 -> 3388 bytes
-rw-r--r--tests/ref/code/closure.pngbin591 -> 801 bytes
-rw-r--r--tests/ref/code/comment.pngbin1691 -> 2585 bytes
-rw-r--r--tests/ref/code/dict.pngbin894 -> 1126 bytes
-rw-r--r--tests/ref/code/for.pngbin2128 -> 3123 bytes
-rw-r--r--tests/ref/code/if.pngbin1825 -> 2336 bytes
-rw-r--r--tests/ref/code/import.pngbin1810 -> 4675 bytes
-rw-r--r--tests/ref/code/include.pngbin29204 -> 48470 bytes
-rw-r--r--tests/ref/code/let.pngbin1239 -> 3170 bytes
-rw-r--r--tests/ref/code/ops.pngbin689 -> 977 bytes
-rw-r--r--tests/ref/code/repr.pngbin9933 -> 29464 bytes
-rw-r--r--tests/ref/code/while.pngbin761 -> 1058 bytes
-rw-r--r--tests/ref/coma.pngbin55412 -> 88177 bytes
-rw-r--r--tests/ref/layout/align.pngbin1355 -> 2388 bytes
-rw-r--r--tests/ref/layout/box-block.pngbin5194 -> 7628 bytes
-rw-r--r--tests/ref/layout/columns.pngbin37999 -> 109097 bytes
-rw-r--r--tests/ref/layout/grid-1.pngbin3250 -> 8863 bytes
-rw-r--r--tests/ref/layout/grid-2.pngbin9721 -> 16566 bytes
-rw-r--r--tests/ref/layout/grid-3.pngbin37234 -> 65891 bytes
-rw-r--r--tests/ref/layout/grid-4.pngbin431 -> 430 bytes
-rw-r--r--tests/ref/layout/grid-5.pngbin2565 -> 3814 bytes
-rw-r--r--tests/ref/layout/image.pngbin186196 -> 184771 bytes
-rw-r--r--tests/ref/layout/pad.pngbin52011 -> 53964 bytes
-rw-r--r--tests/ref/layout/page.pngbin4157 -> 12418 bytes
-rw-r--r--tests/ref/layout/pagebreak.pngbin3788 -> 5140 bytes
-rw-r--r--tests/ref/layout/place-background.pngbin80166 -> 80409 bytes
-rw-r--r--tests/ref/layout/place.pngbin33917 -> 44132 bytes
-rw-r--r--tests/ref/layout/shape-aspect.pngbin3601 -> 4663 bytes
-rw-r--r--tests/ref/layout/shape-circle.pngbin18861 -> 40653 bytes
-rw-r--r--tests/ref/layout/shape-ellipse.pngbin9455 -> 22046 bytes
-rw-r--r--tests/ref/layout/shape-fill-stroke.pngbin1942 -> 2893 bytes
-rw-r--r--tests/ref/layout/shape-rect.pngbin2914 -> 7261 bytes
-rw-r--r--tests/ref/layout/shape-square.pngbin6585 -> 18777 bytes
-rw-r--r--tests/ref/layout/spacing.pngbin2927 -> 3912 bytes
-rw-r--r--tests/ref/layout/stack-1.pngbin709 -> 1076 bytes
-rw-r--r--tests/ref/layout/stack-2.pngbin3298 -> 8673 bytes
-rw-r--r--tests/ref/layout/table.pngbin1313 -> 1566 bytes
-rw-r--r--tests/ref/layout/transform.pngbin54712 -> 56525 bytes
-rw-r--r--tests/ref/markup/emph.pngbin2499 -> 3772 bytes
-rw-r--r--tests/ref/markup/enums.pngbin4123 -> 6209 bytes
-rw-r--r--tests/ref/markup/escape.pngbin5203 -> 12375 bytes
-rw-r--r--tests/ref/markup/heading.pngbin9141 -> 25715 bytes
-rw-r--r--tests/ref/markup/linebreak.pngbin515 -> 641 bytes
-rw-r--r--tests/ref/markup/lists.pngbin14701 -> 21645 bytes
-rw-r--r--tests/ref/markup/math.pngbin2448 -> 3465 bytes
-rw-r--r--tests/ref/markup/raw.pngbin6770 -> 10127 bytes
-rw-r--r--tests/ref/markup/shorthands.pngbin2555 -> 3856 bytes
-rw-r--r--tests/ref/markup/strong.pngbin2287 -> 3481 bytes
-rw-r--r--tests/ref/style/construct.pngbin710 -> 935 bytes
-rw-r--r--tests/ref/style/set-block.pngbin812 -> 1151 bytes
-rw-r--r--tests/ref/style/set-site.pngbin4252 -> 12421 bytes
-rw-r--r--tests/ref/style/set-toggle.pngbin968 -> 1188 bytes
-rw-r--r--tests/ref/text/baseline.pngbin886 -> 1325 bytes
-rw-r--r--tests/ref/text/basic.pngbin35194 -> 58937 bytes
-rw-r--r--tests/ref/text/bidi.pngbin13945 -> 24694 bytes
-rw-r--r--tests/ref/text/chinese.pngbin14390 -> 22527 bytes
-rw-r--r--tests/ref/text/decorations.pngbin9668 -> 29451 bytes
-rw-r--r--tests/ref/text/em.pngbin878 -> 1193 bytes
-rw-r--r--tests/ref/text/features.pngbin8140 -> 11892 bytes
-rw-r--r--tests/ref/text/font.pngbin22792 -> 39190 bytes
-rw-r--r--tests/ref/text/linebreaks.pngbin9450 -> 14308 bytes
-rw-r--r--tests/ref/text/links.pngbin23388 -> 35066 bytes
-rw-r--r--tests/ref/text/par.pngbin9364 -> 13798 bytes
-rw-r--r--tests/ref/text/shaping.pngbin12021 -> 17047 bytes
-rw-r--r--tests/ref/text/tracking.pngbin2609 -> 3854 bytes
-rw-r--r--tests/ref/text/whitespace.pngbin4674 -> 6273 bytes
-rw-r--r--tests/ref/utility/basics.pngbin805 -> 1170 bytes
-rw-r--r--tests/typeset.rs590
97 files changed, 758 insertions, 558 deletions
diff --git a/Cargo.lock b/Cargo.lock
index cb6a1c93..706cd481 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -428,6 +428,14 @@ 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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -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 3d2d7706..bfeeb060 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 847dac8f..d4b52a16 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 4f71b944..8a5cec1b 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 f0efa500..be431e12 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<T> = Result<T, Box<Vec<Error>>>;
pub type StrResult<T> = Result<T, String>;
/// 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<String>),
diff --git a/src/export/mod.rs b/src/export/mod.rs
index d3836859..b782ae13 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 f0fd10f6..1477e283 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<Frame>]) -> Vec<u8> {
diff --git a/src/export/render.rs b/src/export/render.rs
new file mode 100644
index 00000000..c41bcbf2
--- /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::<u8, u32>(canvas.data_mut());
+ for x in left.clamp(0, cw) .. right.clamp(0, cw) {
+ for y in top.clamp(0, ch) .. bottom.clamp(0, ch) {
+ let ai = ((y - top) * mw + (x - left)) as usize;
+ let cov = bitmap.coverage[ai];
+ if cov == 0 {
+ continue;
+ }
+
+ let 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<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()
+}
+
+impl From<Transform> 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<Paint> 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 c2e1beac..674ffa63 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<Option<Face>>,
families: BTreeMap<String, Vec<FaceId>>,
buffers: HashMap<FileHash, Rc<Vec<u8>>>,
- on_load: Option<Box<dyn Fn(FaceId, &Face)>>,
}
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<F>(&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<FaceId> {
@@ -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 46f23446..133ba256 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<Frame>,
@@ -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 af47e51d..df2aca17 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 1868222f..af6be706 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 210dcce7..b01a7123 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 2f722f16..a03e88b0 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<Index> {
/// The structure's component type.
diff --git a/src/geom/paint.rs b/src/geom/paint.rs
index 0eba9f2f..d906561c 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<RgbaColor> 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 87e20dd1..836be1b4 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<PathElement>);
/// 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 ab8f4439..6d77507b 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 c894f4a5..6f6b152f 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 948ea7ec..1435654d 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<f64> for Scalar {
diff --git a/src/geom/spec.rs b/src/geom/spec.rs
index cf75f42d..1b8e13c2 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<T> {
/// The horizontal component.
pub x: T,
diff --git a/src/geom/transform.rs b/src/geom/transform.rs
index 76615e75..eed51d46 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 3d80896f..bd70bf28 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<dyn Loader>,
files: HashMap<FileHash, ImageId>,
images: Vec<Image>,
- on_load: Option<Box<dyn Fn(ImageId, &Image)>>,
}
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<F>(&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<ImageId> {
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 d563dafb..e4c29f9b 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 39507d79..a764468b 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 12996a69..4c46c80c 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<FaceInfo>,
}
diff --git a/src/source.rs b/src/source.rs
index 1e0be450..fd42c3f7 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 4d5b8819..ab2797f6 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<T> {
/// The spanned value.
pub v: T,
@@ -48,7 +46,7 @@ impl<T: Debug> Debug for Spanned<T> {
}
/// 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 752b2318..613aab97 100644
--- a/tests/ref/code/array.png
+++ b/tests/ref/code/array.png
Binary files differ
diff --git a/tests/ref/code/block.png b/tests/ref/code/block.png
index 0a43919d..9cd0ba36 100644
--- a/tests/ref/code/block.png
+++ b/tests/ref/code/block.png
Binary files differ
diff --git a/tests/ref/code/call.png b/tests/ref/code/call.png
index bd6f22f3..eefc8cae 100644
--- a/tests/ref/code/call.png
+++ b/tests/ref/code/call.png
Binary files differ
diff --git a/tests/ref/code/closure.png b/tests/ref/code/closure.png
index 17b4ef63..7d933033 100644
--- a/tests/ref/code/closure.png
+++ b/tests/ref/code/closure.png
Binary files differ
diff --git a/tests/ref/code/comment.png b/tests/ref/code/comment.png
index c5dffd67..d7b59976 100644
--- a/tests/ref/code/comment.png
+++ b/tests/ref/code/comment.png
Binary files differ
diff --git a/tests/ref/code/dict.png b/tests/ref/code/dict.png
index 09751ae8..43cf1370 100644
--- a/tests/ref/code/dict.png
+++ b/tests/ref/code/dict.png
Binary files differ
diff --git a/tests/ref/code/for.png b/tests/ref/code/for.png
index 852fee49..1ef08f7a 100644
--- a/tests/ref/code/for.png
+++ b/tests/ref/code/for.png
Binary files differ
diff --git a/tests/ref/code/if.png b/tests/ref/code/if.png
index 65adcee2..177e61bd 100644
--- a/tests/ref/code/if.png
+++ b/tests/ref/code/if.png
Binary files differ
diff --git a/tests/ref/code/import.png b/tests/ref/code/import.png
index 07140364..00d3b2ee 100644
--- a/tests/ref/code/import.png
+++ b/tests/ref/code/import.png
Binary files differ
diff --git a/tests/ref/code/include.png b/tests/ref/code/include.png
index 62166c64..f5a392e4 100644
--- a/tests/ref/code/include.png
+++ b/tests/ref/code/include.png
Binary files differ
diff --git a/tests/ref/code/let.png b/tests/ref/code/let.png
index 0753ae05..07afcb96 100644
--- a/tests/ref/code/let.png
+++ b/tests/ref/code/let.png
Binary files differ
diff --git a/tests/ref/code/ops.png b/tests/ref/code/ops.png
index b16ed1de..abcab137 100644
--- a/tests/ref/code/ops.png
+++ b/tests/ref/code/ops.png
Binary files differ
diff --git a/tests/ref/code/repr.png b/tests/ref/code/repr.png
index e0749e12..47f21b03 100644
--- a/tests/ref/code/repr.png
+++ b/tests/ref/code/repr.png
Binary files differ
diff --git a/tests/ref/code/while.png b/tests/ref/code/while.png
index e96caf95..026f9943 100644
--- a/tests/ref/code/while.png
+++ b/tests/ref/code/while.png
Binary files differ
diff --git a/tests/ref/coma.png b/tests/ref/coma.png
index d4c6c3de..34a6b30a 100644
--- a/tests/ref/coma.png
+++ b/tests/ref/coma.png
Binary files differ
diff --git a/tests/ref/layout/align.png b/tests/ref/layout/align.png
index 57945d5c..77619b32 100644
--- a/tests/ref/layout/align.png
+++ b/tests/ref/layout/align.png
Binary files differ
diff --git a/tests/ref/layout/box-block.png b/tests/ref/layout/box-block.png
index f6981fe5..87484c25 100644
--- a/tests/ref/layout/box-block.png
+++ b/tests/ref/layout/box-block.png
Binary files differ
diff --git a/tests/ref/layout/columns.png b/tests/ref/layout/columns.png
index 34eb1907..8a65443d 100644
--- a/tests/ref/layout/columns.png
+++ b/tests/ref/layout/columns.png
Binary files differ
diff --git a/tests/ref/layout/grid-1.png b/tests/ref/layout/grid-1.png
index 1996e25a..2c57e28c 100644
--- a/tests/ref/layout/grid-1.png
+++ b/tests/ref/layout/grid-1.png
Binary files differ
diff --git a/tests/ref/layout/grid-2.png b/tests/ref/layout/grid-2.png
index e04fdadc..64986797 100644
--- a/tests/ref/layout/grid-2.png
+++ b/tests/ref/layout/grid-2.png
Binary files differ
diff --git a/tests/ref/layout/grid-3.png b/tests/ref/layout/grid-3.png
index 505ed0ed..740ccf0a 100644
--- a/tests/ref/layout/grid-3.png
+++ b/tests/ref/layout/grid-3.png
Binary files differ
diff --git a/tests/ref/layout/grid-4.png b/tests/ref/layout/grid-4.png
index a291658f..c78eb721 100644
--- a/tests/ref/layout/grid-4.png
+++ b/tests/ref/layout/grid-4.png
Binary files differ
diff --git a/tests/ref/layout/grid-5.png b/tests/ref/layout/grid-5.png
index 51703b11..8afe9446 100644
--- a/tests/ref/layout/grid-5.png
+++ b/tests/ref/layout/grid-5.png
Binary files differ
diff --git a/tests/ref/layout/image.png b/tests/ref/layout/image.png
index 60aa5ab2..3bf5e56f 100644
--- a/tests/ref/layout/image.png
+++ b/tests/ref/layout/image.png
Binary files differ
diff --git a/tests/ref/layout/pad.png b/tests/ref/layout/pad.png
index 4fd2ca47..260b4586 100644
--- a/tests/ref/layout/pad.png
+++ b/tests/ref/layout/pad.png
Binary files differ
diff --git a/tests/ref/layout/page.png b/tests/ref/layout/page.png
index 57e4b8f1..20ac630f 100644
--- a/tests/ref/layout/page.png
+++ b/tests/ref/layout/page.png
Binary files differ
diff --git a/tests/ref/layout/pagebreak.png b/tests/ref/layout/pagebreak.png
index a6c0725e..364d9294 100644
--- a/tests/ref/layout/pagebreak.png
+++ b/tests/ref/layout/pagebreak.png
Binary files differ
diff --git a/tests/ref/layout/place-background.png b/tests/ref/layout/place-background.png
index 2fd0f711..7ac3b57b 100644
--- a/tests/ref/layout/place-background.png
+++ b/tests/ref/layout/place-background.png
Binary files differ
diff --git a/tests/ref/layout/place.png b/tests/ref/layout/place.png
index 4fdb1f70..fc97100f 100644
--- a/tests/ref/layout/place.png
+++ b/tests/ref/layout/place.png
Binary files differ
diff --git a/tests/ref/layout/shape-aspect.png b/tests/ref/layout/shape-aspect.png
index f76b4980..719c1e6e 100644
--- a/tests/ref/layout/shape-aspect.png
+++ b/tests/ref/layout/shape-aspect.png
Binary files differ
diff --git a/tests/ref/layout/shape-circle.png b/tests/ref/layout/shape-circle.png
index 30734cb5..040c6f0b 100644
--- a/tests/ref/layout/shape-circle.png
+++ b/tests/ref/layout/shape-circle.png
Binary files differ
diff --git a/tests/ref/layout/shape-ellipse.png b/tests/ref/layout/shape-ellipse.png
index e3c63427..740f005f 100644
--- a/tests/ref/layout/shape-ellipse.png
+++ b/tests/ref/layout/shape-ellipse.png
Binary files differ
diff --git a/tests/ref/layout/shape-fill-stroke.png b/tests/ref/layout/shape-fill-stroke.png
index 2d04b3dd..12fcbd55 100644
--- a/tests/ref/layout/shape-fill-stroke.png
+++ b/tests/ref/layout/shape-fill-stroke.png
Binary files differ
diff --git a/tests/ref/layout/shape-rect.png b/tests/ref/layout/shape-rect.png
index b6ab89aa..1fdb0dac 100644
--- a/tests/ref/layout/shape-rect.png
+++ b/tests/ref/layout/shape-rect.png
Binary files differ
diff --git a/tests/ref/layout/shape-square.png b/tests/ref/layout/shape-square.png
index 86bc1ff5..00a0c848 100644
--- a/tests/ref/layout/shape-square.png
+++ b/tests/ref/layout/shape-square.png
Binary files differ
diff --git a/tests/ref/layout/spacing.png b/tests/ref/layout/spacing.png
index 09f7d6d9..193be6f5 100644
--- a/tests/ref/layout/spacing.png
+++ b/tests/ref/layout/spacing.png
Binary files differ
diff --git a/tests/ref/layout/stack-1.png b/tests/ref/layout/stack-1.png
index 162ffc48..106cc791 100644
--- a/tests/ref/layout/stack-1.png
+++ b/tests/ref/layout/stack-1.png
Binary files differ
diff --git a/tests/ref/layout/stack-2.png b/tests/ref/layout/stack-2.png
index 470b57ec..52b75bbd 100644
--- a/tests/ref/layout/stack-2.png
+++ b/tests/ref/layout/stack-2.png
Binary files differ
diff --git a/tests/ref/layout/table.png b/tests/ref/layout/table.png
index 1a576c35..bc70d548 100644
--- a/tests/ref/layout/table.png
+++ b/tests/ref/layout/table.png
Binary files differ
diff --git a/tests/ref/layout/transform.png b/tests/ref/layout/transform.png
index 2593a3f2..65d94758 100644
--- a/tests/ref/layout/transform.png
+++ b/tests/ref/layout/transform.png
Binary files differ
diff --git a/tests/ref/markup/emph.png b/tests/ref/markup/emph.png
index d0598406..6b3bfb2d 100644
--- a/tests/ref/markup/emph.png
+++ b/tests/ref/markup/emph.png
Binary files differ
diff --git a/tests/ref/markup/enums.png b/tests/ref/markup/enums.png
index a201131f..a257b0dd 100644
--- a/tests/ref/markup/enums.png
+++ b/tests/ref/markup/enums.png
Binary files differ
diff --git a/tests/ref/markup/escape.png b/tests/ref/markup/escape.png
index 41b8c4d6..4d2b570e 100644
--- a/tests/ref/markup/escape.png
+++ b/tests/ref/markup/escape.png
Binary files differ
diff --git a/tests/ref/markup/heading.png b/tests/ref/markup/heading.png
index 52911d66..ac229180 100644
--- a/tests/ref/markup/heading.png
+++ b/tests/ref/markup/heading.png
Binary files differ
diff --git a/tests/ref/markup/linebreak.png b/tests/ref/markup/linebreak.png
index 4f3678f8..2304ab96 100644
--- a/tests/ref/markup/linebreak.png
+++ b/tests/ref/markup/linebreak.png
Binary files differ
diff --git a/tests/ref/markup/lists.png b/tests/ref/markup/lists.png
index 1405b95e..d39f3164 100644
--- a/tests/ref/markup/lists.png
+++ b/tests/ref/markup/lists.png
Binary files differ
diff --git a/tests/ref/markup/math.png b/tests/ref/markup/math.png
index 426f3dbf..448b2d12 100644
--- a/tests/ref/markup/math.png
+++ b/tests/ref/markup/math.png
Binary files differ
diff --git a/tests/ref/markup/raw.png b/tests/ref/markup/raw.png
index bd8b811a..4effb303 100644
--- a/tests/ref/markup/raw.png
+++ b/tests/ref/markup/raw.png
Binary files differ
diff --git a/tests/ref/markup/shorthands.png b/tests/ref/markup/shorthands.png
index aa6436dd..db27ef36 100644
--- a/tests/ref/markup/shorthands.png
+++ b/tests/ref/markup/shorthands.png
Binary files differ
diff --git a/tests/ref/markup/strong.png b/tests/ref/markup/strong.png
index 6e2a84e1..53062e9a 100644
--- a/tests/ref/markup/strong.png
+++ b/tests/ref/markup/strong.png
Binary files differ
diff --git a/tests/ref/style/construct.png b/tests/ref/style/construct.png
index e0dcf409..b5b3a152 100644
--- a/tests/ref/style/construct.png
+++ b/tests/ref/style/construct.png
Binary files differ
diff --git a/tests/ref/style/set-block.png b/tests/ref/style/set-block.png
index 8ee5cfb6..898c9c65 100644
--- a/tests/ref/style/set-block.png
+++ b/tests/ref/style/set-block.png
Binary files differ
diff --git a/tests/ref/style/set-site.png b/tests/ref/style/set-site.png
index affe2e1c..3188f7a7 100644
--- a/tests/ref/style/set-site.png
+++ b/tests/ref/style/set-site.png
Binary files differ
diff --git a/tests/ref/style/set-toggle.png b/tests/ref/style/set-toggle.png
index ae8101ca..daaa3d6c 100644
--- a/tests/ref/style/set-toggle.png
+++ b/tests/ref/style/set-toggle.png
Binary files differ
diff --git a/tests/ref/text/baseline.png b/tests/ref/text/baseline.png
index 6890236f..71f75d9b 100644
--- a/tests/ref/text/baseline.png
+++ b/tests/ref/text/baseline.png
Binary files differ
diff --git a/tests/ref/text/basic.png b/tests/ref/text/basic.png
index 88d3059c..e7887f07 100644
--- a/tests/ref/text/basic.png
+++ b/tests/ref/text/basic.png
Binary files differ
diff --git a/tests/ref/text/bidi.png b/tests/ref/text/bidi.png
index db8f77ac..7e0aab8f 100644
--- a/tests/ref/text/bidi.png
+++ b/tests/ref/text/bidi.png
Binary files differ
diff --git a/tests/ref/text/chinese.png b/tests/ref/text/chinese.png
index c47a8a2f..aa8801c0 100644
--- a/tests/ref/text/chinese.png
+++ b/tests/ref/text/chinese.png
Binary files differ
diff --git a/tests/ref/text/decorations.png b/tests/ref/text/decorations.png
index 183dacf9..3464beb2 100644
--- a/tests/ref/text/decorations.png
+++ b/tests/ref/text/decorations.png
Binary files differ
diff --git a/tests/ref/text/em.png b/tests/ref/text/em.png
index 4c168db6..e989eade 100644
--- a/tests/ref/text/em.png
+++ b/tests/ref/text/em.png
Binary files differ
diff --git a/tests/ref/text/features.png b/tests/ref/text/features.png
index 90521427..3da67e2b 100644
--- a/tests/ref/text/features.png
+++ b/tests/ref/text/features.png
Binary files differ
diff --git a/tests/ref/text/font.png b/tests/ref/text/font.png
index e194b00a..cb86f929 100644
--- a/tests/ref/text/font.png
+++ b/tests/ref/text/font.png
Binary files differ
diff --git a/tests/ref/text/linebreaks.png b/tests/ref/text/linebreaks.png
index 66697a89..1498a845 100644
--- a/tests/ref/text/linebreaks.png
+++ b/tests/ref/text/linebreaks.png
Binary files differ
diff --git a/tests/ref/text/links.png b/tests/ref/text/links.png
index 510c7e98..a334a435 100644
--- a/tests/ref/text/links.png
+++ b/tests/ref/text/links.png
Binary files differ
diff --git a/tests/ref/text/par.png b/tests/ref/text/par.png
index bb705a19..9ea42713 100644
--- a/tests/ref/text/par.png
+++ b/tests/ref/text/par.png
Binary files differ
diff --git a/tests/ref/text/shaping.png b/tests/ref/text/shaping.png
index 565dcb7c..1e57afca 100644
--- a/tests/ref/text/shaping.png
+++ b/tests/ref/text/shaping.png
Binary files differ
diff --git a/tests/ref/text/tracking.png b/tests/ref/text/tracking.png
index 43eb5e8a..5c35d94c 100644
--- a/tests/ref/text/tracking.png
+++ b/tests/ref/text/tracking.png
Binary files differ
diff --git a/tests/ref/text/whitespace.png b/tests/ref/text/whitespace.png
index 36fb2475..9a1ed3cd 100644
--- a/tests/ref/text/whitespace.png
+++ b/tests/ref/text/whitespace.png
Binary files differ
diff --git a/tests/ref/utility/basics.png b/tests/ref/utility/basics.png
index 0ac0447b..a80afe5f 100644
--- a/tests/ref/utility/basics.png
+++ b/tests/ref/utility/basics.png
Binary files differ
diff --git a/tests/typeset.rs b/tests/typeset.rs
index a4e20774..ac911401 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,52 +320,61 @@ fn test_part(
(ok, compare_ref, frames)
}
-#[cfg(feature = "layout-cache")]
-fn test_incremental(
- ctx: &mut Context,
- i: usize,
- tree: &RootNode,
- frames: &[Rc<Frame>],
-) -> bool {
- let mut ok = true;
+fn parse_metadata(source: &SourceFile) -> (Option<bool>, Vec<Error>) {
+ let mut compare_ref = None;
+ let mut errors = vec![];
- 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;
+ let lines: Vec<_> = source.src().lines().map(str::trim).collect();
+ for (i, line) in lines.iter().enumerate() {
+ if line.starts_with("// Ref: false") {
+ compare_ref = Some(false);
}
- ctx.layouts.turnaround();
+ if line.starts_with("// Ref: true") {
+ compare_ref = Some(true);
+ }
- 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();
+ let rest = if let Some(rest) = line.strip_prefix("// Error: ") {
+ rest
+ } else {
+ continue;
+ };
- if misses > 0 {
- println!(
- " Subtest {i} relayout had {misses} cache misses on level {level} of {total} ❌",
- );
- ok = false;
+ fn num(s: &mut Scanner) -> usize {
+ s.eat_while(|c| c.is_numeric()).parse().unwrap()
}
- if cached != frames {
- println!(
- " Subtest {i} relayout differs from clean pass on level {level} ❌",
- );
- ok = false;
- }
+ let comments =
+ lines[i ..].iter().take_while(|line| line.starts_with("//")).count();
+
+ let pos = |s: &mut Scanner| -> usize {
+ let first = num(s) - 1;
+ let (delta, column) =
+ if s.eat_if(':') { (first, num(s) - 1) } else { (0, first) };
+ let line = (i + comments) + delta;
+ source.line_column_to_byte(line, column).unwrap()
+ };
+
+ let mut s = Scanner::new(rest);
+ let start = pos(&mut s);
+ let end = if s.eat_if('-') { pos(&mut s) } else { start };
+ let span = Span::new(source.id(), start, end);
+
+ errors.push(Error::new(span, s.rest().trim()));
}
- ctx.layouts = reference;
- ctx.layouts.turnaround();
+ (compare_ref, errors)
+}
- ok
+fn print_error(source: &SourceFile, line: usize, error: &Error) {
+ let start_line = 1 + line + source.byte_to_line(error.span.start).unwrap();
+ let start_col = 1 + source.byte_to_column(error.span.start).unwrap();
+ let end_line = 1 + line + source.byte_to_line(error.span.end).unwrap();
+ let end_col = 1 + source.byte_to_column(error.span.end).unwrap();
+ println!(
+ "Error: {start_line}:{start_col}-{end_line}:{end_col}: {}",
+ error.message,
+ );
}
/// Pseudorandomly edit the source file and test whether a reparse produces the
@@ -418,12 +422,13 @@ fn test_reparse(src: &str, i: usize, rng: &mut LinearShift) -> bool {
}
incr_source.edit(replace.clone(), with);
- let edited_src = incr_source.src();
+ 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 {
+ 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,
@@ -431,10 +436,9 @@ fn test_reparse(src: &str, i: usize, rng: &mut LinearShift) -> bool {
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
}
+
+ same
};
let mut pick = |range: Range<usize>| {
@@ -443,7 +447,6 @@ fn test_reparse(src: &str, i: usize, rng: &mut LinearShift) -> bool {
};
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());
@@ -457,459 +460,128 @@ fn test_reparse(src: &str, i: usize, rng: &mut LinearShift) -> bool {
}
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<bool>, Vec<Error>) {
- let mut compare_ref = None;
- let mut errors = vec![];
-
- let lines: Vec<_> = source.src().lines().map(str::trim).collect();
- for (i, line) in lines.iter().enumerate() {
- if line.starts_with("// Ref: false") {
- compare_ref = Some(false);
- }
-
- if line.starts_with("// Ref: true") {
- compare_ref = Some(true);
- }
+#[cfg(feature = "layout-cache")]
+fn test_incremental(
+ ctx: &mut Context,
+ i: usize,
+ tree: &RootNode,
+ frames: &[Rc<Frame>],
+) -> bool {
+ let mut ok = true;
- let rest = if let Some(rest) = line.strip_prefix("// Error: ") {
- rest
- } else {
+ 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;
- };
-
- fn num(s: &mut Scanner) -> usize {
- s.eat_while(|c| c.is_numeric()).parse().unwrap()
}
- let comments =
- lines[i ..].iter().take_while(|line| line.starts_with("//")).count();
+ ctx.layout_cache.turnaround();
- let pos = |s: &mut Scanner| -> usize {
- let first = num(s) - 1;
- let (delta, column) =
- if s.eat_if(':') { (first, num(s) - 1) } else { (0, first) };
- let line = (i + comments) + delta;
- source.line_column_to_byte(line, column).unwrap()
- };
+ 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();
- let mut s = Scanner::new(rest);
- let start = pos(&mut s);
- let end = if s.eat_if('-') { pos(&mut s) } else { start };
- let span = Span::new(source.id(), start, end);
+ if misses > 0 {
+ println!(
+ " Subtest {i} relayout had {misses} cache misses on level {level} of {total} ❌",
+ );
+ ok = false;
+ }
- errors.push(Error::new(span, s.rest().trim()));
+ if cached != frames {
+ println!(
+ " Subtest {i} relayout differs from clean pass on level {level} ❌",
+ );
+ ok = false;
+ }
}
- (compare_ref, errors)
-}
+ ctx.layout_cache = reference;
+ ctx.layout_cache.turnaround();
-fn print_error(source: &SourceFile, line: usize, error: &Error) {
- let start_line = 1 + line + source.byte_to_line(error.span.start).unwrap();
- let start_col = 1 + source.byte_to_column(error.span.start).unwrap();
- let end_line = 1 + line + source.byte_to_line(error.span.end).unwrap();
- let end_col = 1 + source.byte_to_column(error.span.end).unwrap();
- println!(
- "Error: {start_line}:{start_col}-{end_line}:{end_col}: {}",
- error.message,
- );
+ ok
}
-fn draw(ctx: &Context, frames: &[Rc<Frame>], 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::<Length>();
+/// Draw all frames into one image with padding in between.
+fn render(ctx: &mut Context, frames: &[Rc<Frame>]) -> 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 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 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::<u32>();
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 mut ts =
- sk::Transform::from_scale(dpp, dpp).pre_translate(pad.to_f32(), pad.to_f32());
-
- for frame in frames {
- let mut background = sk::Paint::default();
- background.set_color(sk::Color::WHITE);
+ 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 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);
+ canvas.draw_pixmap(
+ x as i32,
+ y as i32,
+ pixmap.as_ref(),
+ &sk::PixmapPaint::default(),
+ sk::Transform::identity(),
+ 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);
- }
- 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);
- }
- }
- }
-}
-
-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());
+ let ts = ts.pre_concat(group.transform.into());
+ render_links(canvas, ts, ctx, &group.frame);
}
- 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();
+ 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);
}
+ _ => {}
}
}
- 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`.