Export into rendered images
11
Cargo.lock
generated
@ -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",
|
||||
|
12
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]
|
||||
|
34
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.
|
||||
================================================================================
|
||||
|
@ -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))
|
||||
}
|
||||
|
@ -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>),
|
||||
|
@ -1,7 +1,8 @@
|
||||
//! Exporting into external formats.
|
||||
|
||||
mod pdf;
|
||||
mod render;
|
||||
mod subset;
|
||||
|
||||
pub use pdf::*;
|
||||
pub use subset::*;
|
||||
pub use render::*;
|
||||
|
@ -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
@ -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)
|
||||
}
|
16
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);
|
||||
}
|
||||
|
||||
|
20
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,
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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.
|
||||
|
@ -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,
|
||||
|
@ -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),
|
||||
|
@ -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,
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
16
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)
|
||||
}
|
||||
|
@ -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
|
||||
})
|
||||
}
|
||||
|
10
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(),
|
||||
}
|
||||
|
@ -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>,
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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,
|
||||
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 2.5 KiB |
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 2.0 KiB |
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 3.3 KiB |
Before Width: | Height: | Size: 591 B After Width: | Height: | Size: 801 B |
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 2.5 KiB |
Before Width: | Height: | Size: 894 B After Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 3.0 KiB |
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 2.3 KiB |
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 4.6 KiB |
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 47 KiB |
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 3.1 KiB |
Before Width: | Height: | Size: 689 B After Width: | Height: | Size: 977 B |
Before Width: | Height: | Size: 9.7 KiB After Width: | Height: | Size: 29 KiB |
Before Width: | Height: | Size: 761 B After Width: | Height: | Size: 1.0 KiB |
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 86 KiB |
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 2.3 KiB |
Before Width: | Height: | Size: 5.1 KiB After Width: | Height: | Size: 7.4 KiB |
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 106 KiB |
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 8.7 KiB |
Before Width: | Height: | Size: 9.5 KiB After Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 64 KiB |
Before Width: | Height: | Size: 431 B After Width: | Height: | Size: 430 B |
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 3.7 KiB |
Before Width: | Height: | Size: 182 KiB After Width: | Height: | Size: 180 KiB |
Before Width: | Height: | Size: 51 KiB After Width: | Height: | Size: 53 KiB |
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 5.0 KiB |
Before Width: | Height: | Size: 78 KiB After Width: | Height: | Size: 78 KiB |
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 43 KiB |
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 4.6 KiB |
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 40 KiB |
Before Width: | Height: | Size: 9.2 KiB After Width: | Height: | Size: 22 KiB |
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 2.8 KiB |
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 7.1 KiB |
Before Width: | Height: | Size: 6.4 KiB After Width: | Height: | Size: 18 KiB |
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 3.8 KiB |
Before Width: | Height: | Size: 709 B After Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 8.5 KiB |
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 53 KiB After Width: | Height: | Size: 55 KiB |
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 3.7 KiB |
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 6.1 KiB |
Before Width: | Height: | Size: 5.1 KiB After Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 8.9 KiB After Width: | Height: | Size: 25 KiB |
Before Width: | Height: | Size: 515 B After Width: | Height: | Size: 641 B |
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 21 KiB |
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 3.4 KiB |
Before Width: | Height: | Size: 6.6 KiB After Width: | Height: | Size: 9.9 KiB |
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 3.8 KiB |
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 3.4 KiB |
Before Width: | Height: | Size: 710 B After Width: | Height: | Size: 935 B |
Before Width: | Height: | Size: 812 B After Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 968 B After Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 886 B After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 58 KiB |
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 24 KiB |
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 22 KiB |
Before Width: | Height: | Size: 9.4 KiB After Width: | Height: | Size: 29 KiB |
Before Width: | Height: | Size: 878 B After Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 7.9 KiB After Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 38 KiB |
Before Width: | Height: | Size: 9.2 KiB After Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 34 KiB |
Before Width: | Height: | Size: 9.1 KiB After Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 17 KiB |
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 3.8 KiB |
Before Width: | Height: | Size: 4.6 KiB After Width: | Height: | Size: 6.1 KiB |
Before Width: | Height: | Size: 805 B After Width: | Height: | Size: 1.1 KiB |
694
tests/typeset.rs
@ -5,18 +5,13 @@ use std::ops::Range;
|
||||
use std::path::Path;
|
||||
use std::rc::Rc;
|
||||
|
||||
use image::{GenericImageView, Rgba};
|
||||
use tiny_skia as sk;
|
||||
use ttf_parser::{GlyphId, OutlineBuilder};
|
||||
use usvg::FitTo;
|
||||
use walkdir::WalkDir;
|
||||
|
||||
use typst::diag::Error;
|
||||
use typst::eval::{Smart, StyleMap, Value};
|
||||
use typst::font::Face;
|
||||
use typst::frame::{Element, Frame, Geometry, Shape, Stroke, Text};
|
||||
use typst::geom::{self, Color, Length, Paint, PathElement, RgbaColor, Size, Transform};
|
||||
use typst::image::{Image, RasterImage, Svg};
|
||||
use typst::frame::{Element, Frame};
|
||||
use typst::geom::Length;
|
||||
use typst::library::{PageNode, TextNode};
|
||||
use typst::loading::FsLoader;
|
||||
use typst::parse::Scanner;
|
||||
@ -229,7 +224,7 @@ fn test(
|
||||
fs::write(pdf_path, pdf_data).unwrap();
|
||||
}
|
||||
|
||||
let canvas = draw(ctx, &frames, 2.0);
|
||||
let canvas = render(ctx, &frames);
|
||||
fs::create_dir_all(&png_path.parent().unwrap()).unwrap();
|
||||
canvas.save_png(png_path).unwrap();
|
||||
|
||||
@ -325,149 +320,6 @@ fn test_part(
|
||||
(ok, compare_ref, frames)
|
||||
}
|
||||
|
||||
#[cfg(feature = "layout-cache")]
|
||||
fn test_incremental(
|
||||
ctx: &mut Context,
|
||||
i: usize,
|
||||
tree: &RootNode,
|
||||
frames: &[Rc<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
|
||||
|