Export into rendered images

This commit is contained in:
Laurenz 2022-01-24 16:48:24 +01:00
parent db158719d6
commit 3739ab7720
97 changed files with 810 additions and 610 deletions

11
Cargo.lock generated
View File

@ -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",

View File

@ -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]

34
NOTICE
View File

@ -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.
================================================================================

View File

@ -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))
}

View File

@ -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>),

View File

@ -1,7 +1,8 @@
//! Exporting into external formats.
mod pdf;
mod render;
mod subset;
pub use pdf::*;
pub use subset::*;
pub use render::*;

View File

@ -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> {

515
src/export/render.rs Normal file
View File

@ -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)
}

View File

@ -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);
}

View File

@ -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,

View File

@ -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 {

View File

@ -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 {

View File

@ -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 {

View File

@ -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.

View File

@ -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,

View File

@ -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),

View File

@ -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,

View File

@ -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 {

View File

@ -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 {

View File

@ -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,

View File

@ -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,

View File

@ -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)
}

View File

@ -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
})
}

View File

@ -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(),
}

View File

@ -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>,
}

View File

@ -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 {

View File

@ -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,

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 591 B

After

Width:  |  Height:  |  Size: 801 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 894 B

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 689 B

After

Width:  |  Height:  |  Size: 977 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.7 KiB

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 761 B

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.5 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 431 B

After

Width:  |  Height:  |  Size: 430 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 182 KiB

After

Width:  |  Height:  |  Size: 180 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.2 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.4 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 709 B

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.9 KiB

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 515 B

After

Width:  |  Height:  |  Size: 641 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.6 KiB

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 710 B

After

Width:  |  Height:  |  Size: 935 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 812 B

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 968 B

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 886 B

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 878 B

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.9 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.2 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.1 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 805 B

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -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<Frame>],
) -> 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<usize>, 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<usize>| {
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<bool>, Vec<Error>) {
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<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>();
/// 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<usize>, 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<usize>| {
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<Frame>],
) -> 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<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 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 [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, T>(f: F) -> T