Remove resource abstraction and handle images natively

This commit is contained in:
Laurenz 2021-05-14 11:14:28 +02:00
parent 33733fd1ef
commit e65c2b949c
9 changed files with 184 additions and 166 deletions

6
src/env/fs.rs vendored
View File

@ -12,7 +12,7 @@ use walkdir::WalkDir;
use super::{Buffer, Loader};
use crate::font::{FaceInfo, FontStretch, FontStyle, FontVariant, FontWeight};
/// Loads fonts and resources from the local file system.
/// Loads fonts and images from the local file system.
///
/// _This is only available when the `fs` feature is enabled._
#[derive(Default, Debug, Clone, Serialize, Deserialize)]
@ -175,8 +175,8 @@ impl Loader for FsLoader {
load(&mut self.cache, &self.files[idx])
}
fn load_file(&mut self, url: &str) -> Option<Buffer> {
load(&mut self.cache, Path::new(url))
fn load_file(&mut self, path: &str) -> Option<Buffer> {
load(&mut self.cache, Path::new(path))
}
}

33
src/env/image.rs vendored
View File

@ -4,37 +4,44 @@ use std::io::Cursor;
use image::io::Reader as ImageReader;
use image::{DynamicImage, GenericImageView, ImageFormat};
use super::Buffer;
/// A loaded image resource.
pub struct ImageResource {
/// A loaded image.
pub struct Image {
/// The original format the image was encoded in.
pub format: ImageFormat,
/// The decoded image.
pub buf: DynamicImage,
}
impl ImageResource {
/// Parse an image resource from raw data in a supported format.
impl Image {
/// Parse an image from raw data in a supported format.
///
/// The image format is determined automatically.
pub fn parse(data: Buffer) -> Option<Self> {
let cursor = Cursor::new(data.as_ref());
pub fn parse(data: &[u8]) -> Option<Self> {
let cursor = Cursor::new(data);
let reader = ImageReader::new(cursor).with_guessed_format().ok()?;
let format = reader.format()?;
let buf = reader.decode().ok()?;
Some(Self { format, buf })
}
/// The width of the image.
pub fn width(&self) -> u32 {
self.buf.width()
}
/// The height of the image.
pub fn height(&self) -> u32 {
self.buf.height()
}
}
impl Debug for ImageResource {
impl Debug for Image {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
let (width, height) = self.buf.dimensions();
f.debug_struct("ImageResource")
f.debug_struct("Image")
.field("format", &self.format)
.field("color", &self.buf.color())
.field("width", &width)
.field("height", &height)
.field("width", &self.width())
.field("height", &self.height())
.finish()
}
}

122
src/env/mod.rs vendored
View File

@ -1,4 +1,4 @@
//! Font and resource loading.
//! Font and image loading.
#[cfg(feature = "fs")]
mod fs;
@ -8,7 +8,6 @@ pub use self::image::*;
#[cfg(feature = "fs")]
pub use fs::*;
use std::any::Any;
use std::collections::{hash_map::Entry, HashMap};
use std::rc::Rc;
@ -16,18 +15,22 @@ use serde::{Deserialize, Serialize};
use crate::font::{Face, FaceInfo, FontVariant};
/// Handles font and resource loading.
/// Handles font and image loading.
pub struct Env {
/// The loader that serves the font face and file buffers.
loader: Box<dyn Loader>,
/// Loaded resources indexed by [`ResourceId`].
resources: Vec<Box<dyn Any>>,
/// Maps from URL to loaded resource.
urls: HashMap<String, ResourceId>,
/// Faces indexed by [`FaceId`]. `None` if not yet loaded.
faces: Vec<Option<Face>>,
/// Maps a family name to the ids of all faces that are part of the family.
families: HashMap<String, Vec<FaceId>>,
/// Loaded images indexed by [`ImageId`].
images: Vec<Image>,
/// Maps from paths to loaded images.
paths: HashMap<String, ImageId>,
/// Callback for loaded font faces.
on_face_load: Option<Box<dyn Fn(FaceId, &Face)>>,
/// Callback for loaded images.
on_image_load: Option<Box<dyn Fn(ImageId, &Image)>>,
}
impl Env {
@ -49,10 +52,12 @@ impl Env {
Self {
loader: Box::new(loader),
resources: vec![],
urls: HashMap::new(),
faces,
families,
images: vec![],
paths: HashMap::new(),
on_face_load: None,
on_image_load: None,
}
}
@ -113,41 +118,24 @@ impl Env {
}
// Load the face if it's not already loaded.
let idx = best?.0 as usize;
let id = best?;
let idx = id.0 as usize;
let slot = &mut self.faces[idx];
if slot.is_none() {
let index = infos[idx].index;
let buffer = self.loader.load_face(idx)?;
let face = Face::new(buffer, index)?;
if let Some(callback) = &self.on_face_load {
callback(id, &face);
}
*slot = Some(face);
}
best
}
/// Load a file from a local or remote URL, parse it into a cached resource
/// and return a unique identifier that allows to retrieve the parsed
/// resource through [`resource()`](Self::resource).
pub fn load_resource<F, R>(&mut self, url: &str, parse: F) -> Option<ResourceId>
where
F: FnOnce(Buffer) -> Option<R>,
R: 'static,
{
Some(match self.urls.entry(url.to_string()) {
Entry::Occupied(entry) => *entry.get(),
Entry::Vacant(entry) => {
let buffer = self.loader.load_file(url)?;
let resource = parse(buffer)?;
let len = self.resources.len();
self.resources.push(Box::new(resource));
*entry.insert(ResourceId(len as u32))
}
})
}
/// Get a reference to a queried face.
///
/// # Panics
/// This panics if no face with this id was loaded. This function should
/// only be called with ids returned by [`query_face()`](Self::query_face).
#[track_caller]
@ -155,20 +143,50 @@ impl Env {
self.faces[id.0 as usize].as_ref().expect("font face was not loaded")
}
/// Get a reference to a loaded resource.
/// Register a callback which is invoked when a font face was loaded.
pub fn on_face_load<F>(&mut self, f: F)
where
F: Fn(FaceId, &Face) + 'static,
{
self.on_face_load = Some(Box::new(f));
}
/// Load and decode an image file from a path.
pub fn load_image(&mut self, path: &str) -> Option<ImageId> {
Some(match self.paths.entry(path.to_string()) {
Entry::Occupied(entry) => *entry.get(),
Entry::Vacant(entry) => {
let buffer = self.loader.load_file(path)?;
let image = Image::parse(&buffer)?;
let id = ImageId(self.images.len() as u32);
if let Some(callback) = &self.on_image_load {
callback(id, &image);
}
self.images.push(image);
*entry.insert(id)
}
})
}
/// Get a reference to a loaded image.
///
/// This panics if no resource with this id was loaded. This function should
/// only be called with ids returned by
/// [`load_resource()`](Self::load_resource).
/// This panics if no image with this id was loaded. This function should
/// only be called with ids returned by [`load_image()`](Self::load_image).
#[track_caller]
pub fn resource<R: 'static>(&self, id: ResourceId) -> &R {
self.resources[id.0 as usize]
.downcast_ref()
.expect("bad resource type")
pub fn image(&self, id: ImageId) -> &Image {
&self.images[id.0 as usize]
}
/// Register a callback which is invoked when an image was loaded.
pub fn on_image_load<F>(&mut self, f: F)
where
F: Fn(ImageId, &Image) + 'static,
{
self.on_image_load = Some(Box::new(f));
}
}
/// Loads fonts and resources from a remote or local source.
/// Loads fonts and images from a remote or local source.
pub trait Loader {
/// Descriptions of all font faces this loader serves.
fn faces(&self) -> &[FaceInfo];
@ -176,8 +194,8 @@ pub trait Loader {
/// Load the font face with the given index in [`faces()`](Self::faces).
fn load_face(&mut self, idx: usize) -> Option<Buffer>;
/// Load a file from a URL.
fn load_file(&mut self, url: &str) -> Option<Buffer>;
/// Load a file from a path.
fn load_file(&mut self, path: &str) -> Option<Buffer>;
}
/// A shared byte buffer.
@ -205,11 +223,21 @@ impl FaceId {
}
}
/// A unique identifier for a loaded resource.
/// A unique identifier for a loaded image.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)]
pub struct ResourceId(u32);
pub struct ImageId(u32);
impl ResourceId {
/// A blank initialization value.
pub const MAX: Self = Self(u32::MAX);
impl ImageId {
/// Create an image id from the raw underlying value.
///
/// This should only be called with values returned by
/// [`into_raw`](Self::into_raw).
pub fn from_raw(v: u32) -> Self {
Self(v)
}
/// Convert into the raw underlying value.
pub fn into_raw(self) -> u32 {
self.0
}
}

View File

@ -30,7 +30,7 @@ impl Layout for BackgroundNode {
}
};
let element = Element::Geometry(Geometry { shape, fill: self.fill });
let element = Element::Geometry(shape, self.fill);
frame.elements.insert(0, (point, element));
}

View File

@ -1,5 +1,5 @@
use crate::color::Color;
use crate::env::{FaceId, ResourceId};
use crate::env::{FaceId, ImageId};
use crate::geom::{Length, Path, Point, Size};
use serde::{Deserialize, Serialize};
@ -41,9 +41,9 @@ pub enum Element {
/// Shaped text.
Text(Text),
/// A geometric shape.
Geometry(Geometry),
Geometry(Shape, Fill),
/// A raster image.
Image(Image),
Image(ImageId, Size),
}
/// A run of shaped text.
@ -54,7 +54,7 @@ pub struct Text {
/// The font size.
pub size: Length,
/// The glyph fill color / texture.
pub color: Fill,
pub fill: Fill,
/// The glyphs.
pub glyphs: Vec<Glyph>,
}
@ -83,19 +83,6 @@ impl Text {
}
}
/// A shape with some kind of fill.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Geometry {
/// The shape to draw.
pub shape: Shape,
/// How the shape looks on the inside.
//
// TODO: This could be made into a Vec<Fill> or something such that
// the user can compose multiple fills with alpha values less
// than one to achieve cool effects.
pub fill: Fill,
}
/// Some shape.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum Shape {
@ -113,12 +100,3 @@ pub enum Fill {
/// The fill is a color.
Color(Color),
}
/// An image element.
#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)]
pub struct Image {
/// The image resource.
pub id: ResourceId,
/// The size of the image in the document.
pub size: Size,
}

View File

@ -70,7 +70,7 @@ impl<'a> ShapedText<'a> {
let mut text = Text {
face_id,
size: self.props.size,
color: self.props.color,
fill: self.props.color,
glyphs: vec![],
};

View File

@ -1,8 +1,8 @@
use ::image::GenericImageView;
use super::*;
use crate::env::{ImageResource, ResourceId};
use crate::layout::{AnyNode, Areas, Element, Frame, Image, Layout, LayoutContext};
use crate::env::ImageId;
use crate::layout::{AnyNode, Areas, Element, Frame, Layout, LayoutContext};
/// `image`: An image.
///
@ -20,9 +20,8 @@ pub fn image(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value {
Value::template("image", move |ctx| {
if let Some(path) = &path {
let loaded = ctx.env.load_resource(&path.v, ImageResource::parse);
if let Some(id) = loaded {
let img = ctx.env.resource::<ImageResource>(id);
if let Some(id) = ctx.env.load_image(&path.v) {
let img = ctx.env.image(id);
let dimensions = img.buf.dimensions();
ctx.push(ImageNode { id, dimensions, width, height });
} else {
@ -35,8 +34,8 @@ pub fn image(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value {
/// An image node.
#[derive(Debug, Clone, PartialEq)]
struct ImageNode {
/// The resource id of the image file.
id: ResourceId,
/// The id of the image file.
id: ImageId,
/// The pixel dimensions of the image.
dimensions: (u32, u32),
/// The fixed width, if any.
@ -75,7 +74,7 @@ impl Layout for ImageNode {
};
let mut frame = Frame::new(size, size.height);
frame.push(Point::ZERO, Element::Image(Image { id: self.id, size }));
frame.push(Point::ZERO, Element::Image(self.id, size));
vec![frame]
}

View File

@ -13,10 +13,10 @@ use pdf_writer::{
use ttf_parser::{name_id, GlyphId};
use crate::color::Color;
use crate::env::{Env, FaceId, ImageResource, ResourceId};
use crate::env::{Env, FaceId, Image, ImageId};
use crate::font::{Em, VerticalFontMetric};
use crate::geom::{self, Length, Size};
use crate::layout::{Element, Fill, Frame, Image, Shape};
use crate::layout::{Element, Fill, Frame, Shape};
/// Export a collection of frames into a PDF document.
///
@ -35,7 +35,7 @@ struct PdfExporter<'a> {
env: &'a Env,
refs: Refs,
fonts: Remapper<FaceId>,
images: Remapper<ResourceId>,
images: Remapper<ImageId>,
}
impl<'a> PdfExporter<'a> {
@ -49,16 +49,16 @@ impl<'a> PdfExporter<'a> {
for frame in frames {
for (_, element) in &frame.elements {
match element {
Element::Text(shaped) => fonts.insert(shaped.face_id),
Element::Image(image) => {
let img = env.resource::<ImageResource>(image.id);
match *element {
Element::Text(ref shaped) => fonts.insert(shaped.face_id),
Element::Geometry(_, _) => {}
Element::Image(id, _) => {
let img = env.image(id);
if img.buf.color().has_alpha() {
alpha_masks += 1;
}
images.insert(image.id);
images.insert(id);
}
Element::Geometry(_) => {}
}
}
}
@ -139,23 +139,35 @@ impl<'a> PdfExporter<'a> {
let x = pos.x.to_pt() as f32;
let y = (page.size.height - pos.y).to_pt() as f32;
match element {
&Element::Image(Image { id, size: Size { width, height } }) => {
let name = format!("Im{}", self.images.map(id));
let w = width.to_pt() as f32;
let h = height.to_pt() as f32;
match *element {
Element::Text(ref shaped) => {
if fill != Some(shaped.fill) {
write_fill(&mut content, shaped.fill);
fill = Some(shaped.fill);
}
content.save_state();
content.matrix(w, 0.0, 0.0, h, x, y - h);
content.x_object(Name(name.as_bytes()));
content.restore_state();
let mut text = content.text();
// Then, also check if we need to issue a font switching
// action.
if shaped.face_id != face || shaped.size != size {
face = shaped.face_id;
size = shaped.size;
let name = format!("F{}", self.fonts.map(shaped.face_id));
text.font(Name(name.as_bytes()), size.to_pt() as f32);
}
// TODO: Respect individual glyph offsets.
text.matrix(1.0, 0.0, 0.0, 1.0, x, y);
text.show(&shaped.encode_glyphs_be());
}
Element::Geometry(geometry) => {
Element::Geometry(ref shape, fill) => {
content.save_state();
write_fill(&mut content, geometry.fill);
write_fill(&mut content, fill);
match geometry.shape {
match *shape {
Shape::Rect(Size { width, height }) => {
let w = width.to_pt() as f32;
let h = height.to_pt() as f32;
@ -177,27 +189,15 @@ impl<'a> PdfExporter<'a> {
content.restore_state();
}
Element::Text(shaped) => {
if fill != Some(shaped.color) {
write_fill(&mut content, shaped.color);
fill = Some(shaped.color);
}
Element::Image(id, Size { width, height }) => {
let name = format!("Im{}", self.images.map(id));
let w = width.to_pt() as f32;
let h = height.to_pt() as f32;
let mut text = content.text();
// Then, also check if we need to issue a font switching
// action.
if shaped.face_id != face || shaped.size != size {
face = shaped.face_id;
size = shaped.size;
let name = format!("F{}", self.fonts.map(shaped.face_id));
text.font(Name(name.as_bytes()), size.to_pt() as f32);
}
// TODO: Respect individual glyph offsets.
text.matrix(1.0, 0.0, 0.0, 1.0, x, y);
text.show(&shaped.encode_glyphs_be());
content.save_state();
content.matrix(w, 0.0, 0.0, h, x, y - h);
content.x_object(Name(name.as_bytes()));
content.restore_state();
}
}
}
@ -311,8 +311,8 @@ impl<'a> PdfExporter<'a> {
fn write_images(&mut self) {
let mut masks_seen = 0;
for (id, resource) in self.refs.images().zip(self.images.layout_indices()) {
let img = self.env.resource::<ImageResource>(resource);
for (id, image_id) in self.refs.images().zip(self.images.layout_indices()) {
let img = self.env.image(image_id);
let (width, height) = img.buf.dimensions();
// Add the primary image.
@ -397,7 +397,7 @@ const DEFLATE_LEVEL: u8 = 6;
/// Encode an image with a suitable filter.
///
/// Skips the alpha channel as that's encoded separately.
fn encode_image(img: &ImageResource) -> ImageResult<(Vec<u8>, Filter, ColorSpace)> {
fn encode_image(img: &Image) -> ImageResult<(Vec<u8>, Filter, ColorSpace)> {
let mut data = vec![];
let (filter, space) = match (img.format, &img.buf) {
// 8-bit gray JPEG.
@ -438,7 +438,7 @@ fn encode_image(img: &ImageResource) -> ImageResult<(Vec<u8>, Filter, ColorSpace
}
/// Encode an image's alpha channel if present.
fn encode_alpha(img: &ImageResource) -> (Vec<u8>, Filter) {
fn encode_alpha(img: &Image) -> (Vec<u8>, Filter) {
let pixels: Vec<_> = img.buf.pixels().map(|(_, _, Rgba([_, _, _, a]))| a).collect();
let data = deflate::compress_to_vec_zlib(&pixels, DEFLATE_LEVEL);
(data, Filter::FlateDecode)

View File

@ -15,11 +15,11 @@ use walkdir::WalkDir;
use typst::color;
use typst::diag::{Diag, DiagSet, Level, Pass};
use typst::env::{Env, FsLoader, ImageResource};
use typst::env::{Env, FsLoader, ImageId};
use typst::eval::{EvalContext, FuncArgs, FuncValue, Scope, Value};
use typst::exec::State;
use typst::geom::{self, Length, Point, Sides, Size};
use typst::layout::{Element, Fill, Frame, Geometry, Image, Shape, Text};
use typst::layout::{Element, Fill, Frame, Shape, Text};
use typst::library;
use typst::parse::{LineMap, Scanner};
use typst::pdf;
@ -385,15 +385,21 @@ fn draw(env: &Env, frames: &[Frame], dpi: f32) -> Pixmap {
None,
);
for &(pos, ref element) in &frame.elements {
let pos = origin + pos;
let x = pos.x.to_pt() as f32;
let y = pos.y.to_pt() as f32;
for (pos, element) in &frame.elements {
let global = origin + *pos;
let x = global.x.to_pt() as f32;
let y = global.y.to_pt() as f32;
let ts = ts.pre_translate(x, y);
match element {
Element::Text(shaped) => draw_text(&mut canvas, env, ts, shaped),
Element::Image(image) => draw_image(&mut canvas, env, ts, image),
Element::Geometry(geom) => draw_geometry(&mut canvas, ts, geom),
match *element {
Element::Text(ref text) => {
draw_text(&mut canvas, env, ts, text);
}
Element::Geometry(ref shape, fill) => {
draw_geometry(&mut canvas, ts, shape, fill);
}
Element::Image(id, size) => {
draw_image(&mut canvas, env, ts, id, size);
}
}
}
@ -403,13 +409,13 @@ fn draw(env: &Env, frames: &[Frame], dpi: f32) -> Pixmap {
canvas
}
fn draw_text(canvas: &mut Pixmap, env: &Env, ts: Transform, shaped: &Text) {
let ttf = env.face(shaped.face_id).ttf();
fn draw_text(canvas: &mut Pixmap, env: &Env, ts: Transform, text: &Text) {
let ttf = env.face(text.face_id).ttf();
let mut x = 0.0;
for glyph in &shaped.glyphs {
for glyph in &text.glyphs {
let units_per_em = ttf.units_per_em();
let s = shaped.size.to_pt() as f32 / units_per_em as f32;
let s = text.size.to_pt() as f32 / units_per_em as f32;
let dx = glyph.x_offset.to_pt() as f32;
let ts = ts.pre_translate(x + dx, 0.0);
@ -441,7 +447,7 @@ fn draw_text(canvas: &mut Pixmap, env: &Env, ts: Transform, shaped: &Text) {
if ttf.outline_glyph(GlyphId(glyph.id), &mut builder).is_some() {
let path = builder.0.finish().unwrap();
let ts = ts.pre_scale(s, -s);
let mut paint = convert_typst_fill(shaped.color);
let mut paint = convert_typst_fill(text.fill);
paint.anti_alias = true;
canvas.fill_path(&path, &paint, FillRule::default(), ts, None);
}
@ -451,11 +457,11 @@ fn draw_text(canvas: &mut Pixmap, env: &Env, ts: Transform, shaped: &Text) {
}
}
fn draw_geometry(canvas: &mut Pixmap, ts: Transform, element: &Geometry) {
let paint = convert_typst_fill(element.fill);
fn draw_geometry(canvas: &mut Pixmap, ts: Transform, shape: &Shape, fill: Fill) {
let paint = convert_typst_fill(fill);
let rule = FillRule::default();
match element.shape {
match *shape {
Shape::Rect(Size { width, height }) => {
let w = width.to_pt() as f32;
let h = height.to_pt() as f32;
@ -473,8 +479,8 @@ fn draw_geometry(canvas: &mut Pixmap, ts: Transform, element: &Geometry) {
};
}
fn draw_image(canvas: &mut Pixmap, env: &Env, ts: Transform, element: &Image) {
let img = &env.resource::<ImageResource>(element.id);
fn draw_image(canvas: &mut Pixmap, env: &Env, ts: Transform, id: ImageId, size: Size) {
let img = env.image(id);
let mut pixmap = Pixmap::new(img.buf.width(), img.buf.height()).unwrap();
for ((_, _, src), dest) in img.buf.pixels().zip(pixmap.pixels_mut()) {
@ -482,8 +488,8 @@ fn draw_image(canvas: &mut Pixmap, env: &Env, ts: Transform, element: &Image) {
*dest = ColorU8::from_rgba(r, g, b, a).premultiply();
}
let view_width = element.size.width.to_pt() as f32;
let view_height = element.size.height.to_pt() as f32;
let view_width = size.width.to_pt() as f32;
let view_height = size.height.to_pt() as f32;
let scale_x = view_width as f32 / pixmap.width() as f32;
let scale_y = view_height as f32 / pixmap.height() as f32;