summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/export/pdf/font.rs2
-rw-r--r--src/export/pdf/image.rs46
-rw-r--r--src/export/pdf/mod.rs43
-rw-r--r--src/export/pdf/page.rs21
-rw-r--r--src/export/render.rs42
-rw-r--r--src/font.rs22
-rw-r--r--src/frame.rs8
-rw-r--r--src/image.rs290
-rw-r--r--src/lib.rs4
-rw-r--r--src/library/graphics/image.rs27
-rw-r--r--src/library/text/par.rs23
-rw-r--r--src/loading/fs.rs6
-rw-r--r--src/loading/mem.rs8
-rw-r--r--src/loading/mod.rs62
-rw-r--r--src/model/content.rs8
-rw-r--r--src/source.rs12
16 files changed, 291 insertions, 333 deletions
diff --git a/src/export/pdf/font.rs b/src/export/pdf/font.rs
index 99268f9c..b3481c43 100644
--- a/src/export/pdf/font.rs
+++ b/src/export/pdf/font.rs
@@ -9,7 +9,7 @@ use crate::util::SliceExt;
/// Embed all used fonts into the PDF.
pub fn write_fonts(ctx: &mut PdfContext) {
- for font_id in ctx.font_map.layout_indices() {
+ for &font_id in ctx.font_map.items() {
let type0_ref = ctx.alloc.bump();
let cid_ref = ctx.alloc.bump();
let descriptor_ref = ctx.alloc.bump();
diff --git a/src/export/pdf/image.rs b/src/export/pdf/image.rs
index 90ab228f..7886524c 100644
--- a/src/export/pdf/image.rs
+++ b/src/export/pdf/image.rs
@@ -1,25 +1,26 @@
use std::io::Cursor;
-use image::{DynamicImage, GenericImageView, ImageFormat, ImageResult, Rgba};
+use image::{DynamicImage, GenericImageView, ImageResult, Rgba};
use pdf_writer::{Filter, Finish};
use super::{deflate, PdfContext, RefExt};
-use crate::image::{Image, RasterImage};
+use crate::image::{DecodedImage, ImageFormat};
/// Embed all used images into the PDF.
pub fn write_images(ctx: &mut PdfContext) {
- for image_id in ctx.image_map.layout_indices() {
+ for image in ctx.image_map.items() {
let image_ref = ctx.alloc.bump();
ctx.image_refs.push(image_ref);
- let img = ctx.images.get(image_id);
- let width = img.width();
- let height = img.height();
+ let width = image.width();
+ let height = image.height();
// Add the primary image.
- match img {
- Image::Raster(img) => {
- if let Ok((data, filter, has_color)) = encode_image(img) {
+ match image.decode().unwrap() {
+ DecodedImage::Raster(dynamic) => {
+ if let Ok((data, filter, has_color)) =
+ encode_image(image.format(), &dynamic)
+ {
let mut image = ctx.writer.image_xobject(image_ref, &data);
image.filter(filter);
image.width(width as i32);
@@ -35,8 +36,8 @@ pub fn write_images(ctx: &mut PdfContext) {
// Add a second gray-scale image containing the alpha values if
// this image has an alpha channel.
- if img.buf.color().has_alpha() {
- let (alpha_data, alpha_filter) = encode_alpha(img);
+ if dynamic.color().has_alpha() {
+ let (alpha_data, alpha_filter) = encode_alpha(&dynamic);
let mask_ref = ctx.alloc.bump();
image.s_mask(mask_ref);
image.finish();
@@ -59,9 +60,9 @@ pub fn write_images(ctx: &mut PdfContext) {
.device_gray();
}
}
- Image::Svg(img) => {
+ DecodedImage::Svg(svg) => {
let next_ref = svg2pdf::convert_tree_into(
- &img.0,
+ &svg,
svg2pdf::Options::default(),
&mut ctx.writer,
image_ref,
@@ -76,19 +77,22 @@ pub fn write_images(ctx: &mut PdfContext) {
/// whether the image has color.
///
/// Skips the alpha channel as that's encoded separately.
-fn encode_image(img: &RasterImage) -> ImageResult<(Vec<u8>, Filter, bool)> {
- Ok(match (img.format, &img.buf) {
+fn encode_image(
+ format: ImageFormat,
+ dynamic: &DynamicImage,
+) -> ImageResult<(Vec<u8>, Filter, bool)> {
+ Ok(match (format, dynamic) {
// 8-bit gray JPEG.
- (ImageFormat::Jpeg, DynamicImage::ImageLuma8(_)) => {
+ (ImageFormat::Jpg, DynamicImage::ImageLuma8(_)) => {
let mut data = Cursor::new(vec![]);
- img.buf.write_to(&mut data, img.format)?;
+ dynamic.write_to(&mut data, image::ImageFormat::Jpeg)?;
(data.into_inner(), Filter::DctDecode, false)
}
// 8-bit RGB JPEG (CMYK JPEGs get converted to RGB earlier).
- (ImageFormat::Jpeg, DynamicImage::ImageRgb8(_)) => {
+ (ImageFormat::Jpg, DynamicImage::ImageRgb8(_)) => {
let mut data = Cursor::new(vec![]);
- img.buf.write_to(&mut data, img.format)?;
+ dynamic.write_to(&mut data, image::ImageFormat::Jpeg)?;
(data.into_inner(), Filter::DctDecode, true)
}
@@ -117,7 +121,7 @@ fn encode_image(img: &RasterImage) -> ImageResult<(Vec<u8>, Filter, bool)> {
}
/// Encode an image's alpha channel if present.
-fn encode_alpha(img: &RasterImage) -> (Vec<u8>, Filter) {
- let pixels: Vec<_> = img.buf.pixels().map(|(_, _, Rgba([_, _, _, a]))| a).collect();
+fn encode_alpha(dynamic: &DynamicImage) -> (Vec<u8>, Filter) {
+ let pixels: Vec<_> = dynamic.pixels().map(|(_, _, Rgba([_, _, _, a]))| a).collect();
(deflate(&pixels), Filter::FlateDecode)
}
diff --git a/src/export/pdf/mod.rs b/src/export/pdf/mod.rs
index 9ab3df24..7468f7d7 100644
--- a/src/export/pdf/mod.rs
+++ b/src/export/pdf/mod.rs
@@ -17,7 +17,7 @@ use self::page::Page;
use crate::font::{FontId, FontStore};
use crate::frame::Frame;
use crate::geom::{Dir, Em, Length};
-use crate::image::{ImageId, ImageStore};
+use crate::image::Image;
use crate::library::text::Lang;
use crate::Context;
@@ -46,7 +46,6 @@ const D65_GRAY: Name<'static> = Name(b"d65gray");
pub struct PdfContext<'a> {
writer: PdfWriter,
fonts: &'a FontStore,
- images: &'a ImageStore,
pages: Vec<Page>,
page_heights: Vec<f32>,
alloc: Ref,
@@ -55,7 +54,7 @@ pub struct PdfContext<'a> {
image_refs: Vec<Ref>,
page_refs: Vec<Ref>,
font_map: Remapper<FontId>,
- image_map: Remapper<ImageId>,
+ image_map: Remapper<Image>,
glyph_sets: HashMap<FontId, HashSet<u16>>,
languages: HashMap<Lang, usize>,
heading_tree: Vec<HeadingNode>,
@@ -68,7 +67,6 @@ impl<'a> PdfContext<'a> {
Self {
writer: PdfWriter::new(),
fonts: &ctx.fonts,
- images: &ctx.images,
pages: vec![],
page_heights: vec![],
alloc,
@@ -147,36 +145,33 @@ fn deflate(data: &[u8]) -> Vec<u8> {
miniz_oxide::deflate::compress_to_vec_zlib(data, COMPRESSION_LEVEL)
}
-/// Assigns new, consecutive PDF-internal indices to things.
-struct Remapper<Index> {
- /// Forwards from the old indices to the new pdf indices.
- to_pdf: HashMap<Index, usize>,
- /// Backwards from the pdf indices to the old indices.
- to_layout: Vec<Index>,
+/// Assigns new, consecutive PDF-internal indices to items.
+struct Remapper<T> {
+ /// Forwards from the items to the pdf indices.
+ to_pdf: HashMap<T, usize>,
+ /// Backwards from the pdf indices to the items.
+ to_items: Vec<T>,
}
-impl<Index> Remapper<Index>
+impl<T> Remapper<T>
where
- Index: Copy + Eq + Hash,
+ T: Eq + Hash + Clone,
{
fn new() -> Self {
- Self {
- to_pdf: HashMap::new(),
- to_layout: vec![],
- }
+ Self { to_pdf: HashMap::new(), to_items: vec![] }
}
- fn insert(&mut self, index: Index) {
- let to_layout = &mut self.to_layout;
- self.to_pdf.entry(index).or_insert_with(|| {
+ fn insert(&mut self, item: T) {
+ let to_layout = &mut self.to_items;
+ self.to_pdf.entry(item.clone()).or_insert_with(|| {
let pdf_index = to_layout.len();
- to_layout.push(index);
+ to_layout.push(item);
pdf_index
});
}
- fn map(&self, index: Index) -> usize {
- self.to_pdf[&index]
+ fn map(&self, item: T) -> usize {
+ self.to_pdf[&item]
}
fn pdf_indices<'a>(
@@ -186,8 +181,8 @@ where
refs.iter().copied().zip(0 .. self.to_pdf.len())
}
- fn layout_indices(&self) -> impl Iterator<Item = Index> + '_ {
- self.to_layout.iter().copied()
+ fn items(&self) -> impl Iterator<Item = &T> + '_ {
+ self.to_items.iter()
}
}
diff --git a/src/export/pdf/page.rs b/src/export/pdf/page.rs
index e5739a82..6df7531b 100644
--- a/src/export/pdf/page.rs
+++ b/src/export/pdf/page.rs
@@ -11,7 +11,7 @@ use crate::geom::{
self, Color, Em, Geometry, Length, Numeric, Paint, Point, Ratio, Shape, Size, Stroke,
Transform,
};
-use crate::image::ImageId;
+use crate::image::Image;
/// Construct page objects.
pub fn construct_pages(ctx: &mut PdfContext, frames: &[Frame]) {
@@ -290,13 +290,12 @@ fn write_frame(ctx: &mut PageContext, frame: &Frame) {
for &(pos, ref element) in frame.elements() {
let x = pos.x.to_f32();
let y = pos.y.to_f32();
- match *element {
- Element::Group(ref group) => write_group(ctx, pos, group),
- Element::Text(ref text) => write_text(ctx, x, y, text),
- Element::Shape(ref shape) => write_shape(ctx, x, y, shape),
- Element::Image(id, size) => write_image(ctx, x, y, id, size),
- Element::Link(ref dest, size) => write_link(ctx, pos, dest, size),
- Element::Pin(_) => {}
+ match element {
+ Element::Group(group) => write_group(ctx, pos, group),
+ Element::Text(text) => write_text(ctx, x, y, text),
+ Element::Shape(shape) => write_shape(ctx, x, y, shape),
+ Element::Image(image, size) => write_image(ctx, x, y, image, *size),
+ Element::Link(dest, size) => write_link(ctx, pos, dest, *size),
}
}
}
@@ -449,9 +448,9 @@ fn write_path(ctx: &mut PageContext, x: f32, y: f32, path: &geom::Path) {
}
/// Encode a vector or raster image into the content stream.
-fn write_image(ctx: &mut PageContext, x: f32, y: f32, id: ImageId, size: Size) {
- ctx.parent.image_map.insert(id);
- let name = format_eco!("Im{}", ctx.parent.image_map.map(id));
+fn write_image(ctx: &mut PageContext, x: f32, y: f32, image: &Image, size: Size) {
+ ctx.parent.image_map.insert(image.clone());
+ let name = format_eco!("Im{}", ctx.parent.image_map.map(image.clone()));
let w = size.x.to_f32();
let h = size.y.to_f32();
ctx.content.save_state();
diff --git a/src/export/render.rs b/src/export/render.rs
index 525d764d..688cf979 100644
--- a/src/export/render.rs
+++ b/src/export/render.rs
@@ -12,7 +12,7 @@ use crate::frame::{Element, Frame, Group, Text};
use crate::geom::{
self, Geometry, Length, Paint, PathElement, Shape, Size, Stroke, Transform,
};
-use crate::image::{Image, RasterImage, Svg};
+use crate::image::{DecodedImage, Image};
use crate::Context;
/// Export a frame into a rendered image.
@@ -49,21 +49,20 @@ fn render_frame(
let y = pos.y.to_f32();
let ts = ts.pre_translate(x, y);
- match *element {
- Element::Group(ref group) => {
+ match element {
+ Element::Group(group) => {
render_group(canvas, ts, mask, ctx, group);
}
- Element::Text(ref text) => {
+ Element::Text(text) => {
render_text(canvas, ts, mask, ctx, text);
}
- Element::Shape(ref shape) => {
+ Element::Shape(shape) => {
render_shape(canvas, ts, mask, shape);
}
- Element::Image(id, size) => {
- render_image(canvas, ts, mask, ctx.images.get(id), size);
+ Element::Image(image, size) => {
+ render_image(canvas, ts, mask, image, *size);
}
Element::Link(_, _) => {}
- Element::Pin(_) => {}
}
}
}
@@ -197,17 +196,20 @@ fn render_bitmap_glyph(
let ppem = size * ts.sy;
let font = ctx.fonts.get(text.font_id);
let raster = font.ttf().glyph_raster_image(id, ppem as u16)?;
- let img = RasterImage::parse(&raster.data).ok()?;
+ let ext = match raster.format {
+ ttf_parser::RasterImageFormat::PNG => "png",
+ };
+ let image = Image::new(raster.data.into(), ext).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 w = (image.width() as f64 / image.height() as f64) * h;
+ let dx = (raster.x as f32) / (image.width() as f32) * size;
+ let dy = (raster.y as f32) / (image.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_image(canvas, ts, mask, &image, Size::new(w, h))
}
/// Render an outline glyph into the canvas. This is the "normal" case.
@@ -338,33 +340,33 @@ fn render_image(
canvas: &mut sk::Pixmap,
ts: sk::Transform,
mask: Option<&sk::ClipMask>,
- img: &Image,
+ image: &Image,
size: Size,
) -> Option<()> {
let view_width = size.x.to_f32();
let view_height = size.y.to_f32();
- let aspect = (img.width() as f32) / (img.height() as f32);
+ let aspect = (image.width() as f32) / (image.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)?;
- match img {
- Image::Raster(img) => {
- let downscale = w < img.width();
+ match image.decode().unwrap() {
+ DecodedImage::Raster(dynamic) => {
+ let downscale = w < image.width();
let filter = if downscale {
FilterType::Lanczos3
} else {
FilterType::CatmullRom
};
- let buf = img.buf.resize(w, h, filter);
+ let buf = dynamic.resize(w, h, filter);
for ((_, _, src), dest) in buf.pixels().zip(pixmap.pixels_mut()) {
let Rgba([r, g, b, a]) = src;
*dest = sk::ColorU8::from_rgba(r, g, b, a).premultiply();
}
}
- Image::Svg(Svg(tree)) => {
+ DecodedImage::Svg(tree) => {
resvg::render(
&tree,
FitTo::Size(w, h),
diff --git a/src/font.rs b/src/font.rs
index 8f440c52..1fa86ba6 100644
--- a/src/font.rs
+++ b/src/font.rs
@@ -13,7 +13,7 @@ use ttf_parser::{name_id, GlyphId, PlatformId, Tag};
use unicode_segmentation::UnicodeSegmentation;
use crate::geom::Em;
-use crate::loading::{FileHash, Loader};
+use crate::loading::{Buffer, FileHash, Loader};
/// A unique identifier for a loaded font.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
@@ -40,7 +40,7 @@ pub struct FontStore {
failed: Vec<bool>,
fonts: Vec<Option<Font>>,
families: BTreeMap<String, Vec<FontId>>,
- buffers: HashMap<FileHash, Arc<Vec<u8>>>,
+ buffers: HashMap<FileHash, Buffer>,
}
impl FontStore {
@@ -214,11 +214,11 @@ impl FontStore {
Entry::Occupied(entry) => entry.into_mut(),
Entry::Vacant(entry) => {
let buffer = self.loader.load(path).ok()?;
- entry.insert(Arc::new(buffer))
+ entry.insert(buffer)
}
};
- let font = Font::new(Arc::clone(buffer), index)?;
+ let font = Font::new(buffer.clone(), index)?;
*slot = Some(font);
self.failed[idx] = false;
@@ -239,7 +239,7 @@ pub struct Font {
/// The raw font data, possibly shared with other fonts from the same
/// collection. The vector's allocation must not move, because `ttf` points
/// into it using unsafe code.
- buffer: Arc<Vec<u8>>,
+ data: Buffer,
/// The font's index in the collection (zero if not a collection).
index: u32,
/// The underlying ttf-parser/rustybuzz face.
@@ -251,8 +251,8 @@ pub struct Font {
}
impl Font {
- /// Parse a font from a buffer and collection index.
- pub fn new(buffer: Arc<Vec<u8>>, index: u32) -> Option<Self> {
+ /// Parse a font from data and collection index.
+ pub fn new(data: Buffer, index: u32) -> Option<Self> {
// Safety:
// - The slices's location is stable in memory:
// - We don't move the underlying vector
@@ -260,13 +260,13 @@ impl Font {
// - The internal 'static lifetime is not leaked because its rewritten
// to the self-lifetime in `ttf()`.
let slice: &'static [u8] =
- unsafe { std::slice::from_raw_parts(buffer.as_ptr(), buffer.len()) };
+ unsafe { std::slice::from_raw_parts(data.as_ptr(), data.len()) };
let ttf = rustybuzz::Face::from_slice(slice, index)?;
let metrics = FontMetrics::from_ttf(&ttf);
Some(Self {
- buffer,
+ data,
index,
ttf,
metrics,
@@ -275,8 +275,8 @@ impl Font {
}
/// The underlying buffer.
- pub fn buffer(&self) -> &Arc<Vec<u8>> {
- &self.buffer
+ pub fn buffer(&self) -> &Buffer {
+ &self.data
}
/// The collection index.
diff --git a/src/frame.rs b/src/frame.rs
index 8e0cb0dc..7a5fb9e4 100644
--- a/src/frame.rs
+++ b/src/frame.rs
@@ -9,7 +9,7 @@ use crate::font::FontId;
use crate::geom::{
Align, Em, Length, Numeric, Paint, Point, Shape, Size, Spec, Transform,
};
-use crate::image::ImageId;
+use crate::image::Image;
use crate::library::text::Lang;
use crate::util::EcoString;
@@ -303,12 +303,9 @@ pub enum Element {
/// A geometric shape with optional fill and stroke.
Shape(Shape),
/// An image and its size.
- Image(ImageId, Size),
+ Image(Image, Size),
/// A link to an external resource and its trigger region.
Link(Destination, Size),
- /// A pin identified by index. This is used to find elements on the pages
- /// and use their location in formatting. Exporters can just ignore it.
- Pin(usize),
}
impl Debug for Element {
@@ -319,7 +316,6 @@ impl Debug for Element {
Self::Shape(shape) => write!(f, "{shape:?}"),
Self::Image(image, _) => write!(f, "{image:?}"),
Self::Link(dest, _) => write!(f, "Link({dest:?})"),
- Self::Pin(idx) => write!(f, "Pin({idx})"),
}
}
}
diff --git a/src/image.rs b/src/image.rs
index 8ef404fe..c2631477 100644
--- a/src/image.rs
+++ b/src/image.rs
@@ -1,213 +1,139 @@
//! Image handling.
-use std::collections::{hash_map::Entry, HashMap};
-use std::ffi::OsStr;
-use std::fmt::{self, Debug, Formatter};
use std::io;
-use std::path::Path;
-use std::sync::Arc;
-use image::io::Reader as ImageReader;
-use image::{DynamicImage, ImageFormat};
-
-use crate::diag::{failed_to_load, StrResult};
-use crate::loading::{FileHash, Loader};
-
-/// A unique identifier for a loaded image.
-#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
-pub struct ImageId(u32);
-
-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 const fn from_raw(v: u32) -> Self {
- Self(v)
- }
-
- /// Convert into the raw underlying value.
- pub const fn into_raw(self) -> u32 {
- self.0
- }
+use crate::loading::Buffer;
+
+/// A raster or vector image.
+///
+/// Values of this type are cheap to clone and hash.
+#[derive(Debug, Clone, Eq, PartialEq, Hash)]
+pub struct Image {
+ /// The raw, undecoded image data.
+ data: Buffer,
+ /// The format of the encoded `buffer`.
+ format: ImageFormat,
+ /// The width in pixels.
+ width: u32,
+ /// The height in pixels.
+ height: u32,
}
-/// Storage for loaded and decoded images.
-pub struct ImageStore {
- loader: Arc<dyn Loader>,
- files: HashMap<FileHash, ImageId>,
- images: Vec<Image>,
+/// A decoded image.
+pub enum DecodedImage {
+ /// A pixel raster format, like PNG or JPEG.
+ Raster(image::DynamicImage),
+ /// An SVG vector graphic.
+ Svg(usvg::Tree),
}
-impl ImageStore {
- /// Create a new, empty image store.
- pub fn new(loader: Arc<dyn Loader>) -> Self {
- Self {
- loader,
- files: HashMap::new(),
- images: vec![],
- }
- }
+/// A raster or vector image format.
+#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
+pub enum ImageFormat {
+ /// Raster format for illustrations and transparent graphics.
+ Png,
+ /// Lossy raster format suitable for photos.
+ Jpg,
+ /// Raster format that is typically used for short animated clips.
+ Gif,
+ /// The vector graphics format of the web.
+ Svg,
+}
- /// Get a reference to a loaded image.
+impl Image {
+ /// Create an image from a raw buffer and a file extension.
///
- /// This panics if no image with this `id` was loaded. This function should
- /// only be called with ids returned by this store's [`load()`](Self::load)
- /// method.
- #[track_caller]
- pub fn get(&self, id: ImageId) -> &Image {
- &self.images[id.0 as usize]
- }
+ /// The file extension is used to determine the format.
+ pub fn new(data: Buffer, ext: &str) -> io::Result<Self> {
+ let format = match ext {
+ "svg" | "svgz" => ImageFormat::Svg,
+ "png" => ImageFormat::Png,
+ "jpg" | "jpeg" => ImageFormat::Jpg,
+ "gif" => ImageFormat::Gif,
+ _ => {
+ return Err(io::Error::new(
+ io::ErrorKind::InvalidData,
+ "unknown image format",
+ ));
+ }
+ };
- /// Load and decode an image file from a path relative to the compilation
- /// environment's root.
- pub fn load(&mut self, path: &Path) -> StrResult<ImageId> {
- let mut try_load = || -> io::Result<ImageId> {
- let hash = self.loader.resolve(path)?;
- Ok(*match self.files.entry(hash) {
- Entry::Occupied(entry) => entry.into_mut(),
- Entry::Vacant(entry) => {
- let buffer = self.loader.load(path)?;
- 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);
- self.images.push(image);
- entry.insert(id)
- }
- })
+ let (width, height) = match format {
+ ImageFormat::Svg => {
+ let opts = usvg::Options::default();
+ let tree =
+ usvg::Tree::from_data(&data, &opts.to_ref()).map_err(invalid)?;
+
+ let size = tree.svg_node().size;
+ let width = size.width().ceil() as u32;
+ let height = size.height().ceil() as u32;
+ (width, height)
+ }
+ _ => {
+ let cursor = io::Cursor::new(&data);
+ let format = convert_format(format);
+ let reader = image::io::Reader::with_format(cursor, format);
+ reader.into_dimensions().map_err(invalid)?
+ }
};
- try_load().map_err(|err| failed_to_load("image", path, err))
+ Ok(Self { data, format, width, height })
}
-}
-/// A loaded image.
-#[derive(Debug)]
-pub enum Image {
- /// A pixel raster format, like PNG or JPEG.
- Raster(RasterImage),
- /// An SVG vector graphic.
- Svg(Svg),
-}
+ /// The raw image data.
+ pub fn data(&self) -> &Buffer {
+ &self.data
+ }
-impl Image {
- /// Parse an image from raw data. The file extension is used as a hint for
- /// which error message describes the problem best.
- pub fn parse(data: &[u8], ext: &str) -> io::Result<Self> {
- match Svg::parse(data) {
- Ok(svg) => return Ok(Self::Svg(svg)),
- Err(err) if matches!(ext, "svg" | "svgz") => return Err(err),
- Err(_) => {}
- }
-
- match RasterImage::parse(data) {
- Ok(raster) => return Ok(Self::Raster(raster)),
- Err(err) if matches!(ext, "png" | "jpg" | "jpeg" | "gif") => return Err(err),
- Err(_) => {}
- }
-
- Err(io::Error::new(
- io::ErrorKind::InvalidData,
- "unknown image format",
- ))
+ /// The format of the image.
+ pub fn format(&self) -> ImageFormat {
+ self.format
}
/// The width of the image in pixels.
pub fn width(&self) -> u32 {
- match self {
- Self::Raster(image) => image.width(),
- Self::Svg(image) => image.width(),
- }
+ self.width
}
/// The height of the image in pixels.
pub fn height(&self) -> u32 {
- match self {
- Self::Raster(image) => image.height(),
- Self::Svg(image) => image.height(),
- }
+ self.height
+ }
+
+ /// Decode the image.
+ pub fn decode(&self) -> io::Result<DecodedImage> {
+ Ok(match self.format {
+ ImageFormat::Svg => {
+ let opts = usvg::Options::default();
+ let tree =
+ usvg::Tree::from_data(&self.data, &opts.to_ref()).map_err(invalid)?;
+ DecodedImage::Svg(tree)
+ }
+ _ => {
+ let cursor = io::Cursor::new(&self.data);
+ let format = convert_format(self.format);
+ let reader = image::io::Reader::with_format(cursor, format);
+ let dynamic = reader.decode().map_err(invalid)?;
+ DecodedImage::Raster(dynamic)
+ }
+ })
}
}
-/// A raster image, supported through the image crate.
-pub struct RasterImage {
- /// The original format the image was encoded in.
- pub format: ImageFormat,
- /// The decoded image.
- pub buf: DynamicImage,
-}
-
-impl RasterImage {
- /// Parse an image from raw data in a supported format (PNG, JPEG or GIF).
- ///
- /// The image format is determined automatically.
- pub fn parse(data: &[u8]) -> io::Result<Self> {
- let cursor = io::Cursor::new(data);
- let reader = ImageReader::new(cursor).with_guessed_format()?;
- let format = reader
- .format()
- .ok_or_else(|| io::Error::from(io::ErrorKind::InvalidData))?;
-
- let buf = reader
- .decode()
- .map_err(|err| io::Error::new(io::ErrorKind::InvalidData, err))?;
-
- Ok(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 RasterImage {
- fn fmt(&self, f: &mut Formatter) -> fmt::Result {
- f.debug_struct("Image")
- .field("format", &self.format)
- .field("color", &self.buf.color())
- .field("width", &self.width())
- .field("height", &self.height())
- .finish()
+/// Convert a raster image format to the image crate's format.
+fn convert_format(format: ImageFormat) -> image::ImageFormat {
+ match format {
+ ImageFormat::Png => image::ImageFormat::Png,
+ ImageFormat::Jpg => image::ImageFormat::Jpeg,
+ ImageFormat::Gif => image::ImageFormat::Gif,
+ ImageFormat::Svg => panic!("must be a raster format"),
}
}
-/// An SVG image, supported through the usvg crate.
-pub struct Svg(pub usvg::Tree);
-
-impl Svg {
- /// Parse an SVG file from a data buffer. This also handles `.svgz`
- /// compressed files.
- pub fn parse(data: &[u8]) -> io::Result<Self> {
- let usvg_opts = usvg::Options::default();
- usvg::Tree::from_data(data, &usvg_opts.to_ref())
- .map(Self)
- .map_err(|err| io::Error::new(io::ErrorKind::InvalidData, err))
- }
-
- /// The width of the image in rounded-up nominal SVG pixels.
- pub fn width(&self) -> u32 {
- self.0.svg_node().size.width().ceil() as u32
- }
-
- /// The height of the image in rounded-up nominal SVG pixels.
- pub fn height(&self) -> u32 {
- self.0.svg_node().size.height().ceil() as u32
- }
-}
-
-impl Debug for Svg {
- fn fmt(&self, f: &mut Formatter) -> fmt::Result {
- f.debug_struct("Svg")
- .field("width", &self.0.svg_node().size.width())
- .field("height", &self.0.svg_node().size.height())
- .field("viewBox", &self.0.svg_node().view_box)
- .finish()
- }
+/// Turn any error into an I/O error.
+fn invalid<E>(error: E) -> io::Error
+where
+ E: std::error::Error + Send + Sync + 'static,
+{
+ io::Error::new(io::ErrorKind::InvalidData, error)
}
diff --git a/src/lib.rs b/src/lib.rs
index 572a0541..9da37df7 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -57,7 +57,6 @@ use crate::diag::TypResult;
use crate::eval::Scope;
use crate::font::FontStore;
use crate::frame::Frame;
-use crate::image::ImageStore;
use crate::loading::Loader;
use crate::model::StyleMap;
use crate::source::{SourceId, SourceStore};
@@ -80,8 +79,6 @@ pub struct Context {
pub sources: SourceStore,
/// Stores parsed fonts.
pub fonts: FontStore,
- /// Stores decoded images.
- pub images: ImageStore,
/// The context's configuration.
config: Config,
}
@@ -93,7 +90,6 @@ impl Context {
loader: Arc::clone(&loader),
sources: SourceStore::new(Arc::clone(&loader)),
fonts: FontStore::new(Arc::clone(&loader)),
- images: ImageStore::new(loader),
config,
}
}
diff --git a/src/library/graphics/image.rs b/src/library/graphics/image.rs
index feb37ae6..784afa01 100644
--- a/src/library/graphics/image.rs
+++ b/src/library/graphics/image.rs
@@ -1,10 +1,12 @@
-use crate::image::ImageId;
+use std::ffi::OsStr;
+
+use crate::image::Image;
use crate::library::prelude::*;
use crate::library::text::TextNode;
/// Show a raster or vector graphic.
#[derive(Debug, Hash)]
-pub struct ImageNode(pub ImageId);
+pub struct ImageNode(pub Image);
#[node]
impl ImageNode {
@@ -16,12 +18,20 @@ impl ImageNode {
args.expect::<Spanned<EcoString>>("path to image file")?;
let full = vm.locate(&path).at(span)?;
- let id = vm.ctx.images.load(&full).at(span)?;
+ let ext = full.extension().and_then(OsStr::to_str).unwrap_or_default();
+ let image = vm
+ .ctx
+ .loader
+ .load(&full)
+ .and_then(|buffer| Image::new(buffer, ext))
+ .map_err(|err| failed_to_load("image", &full, err))
+ .at(span)?;
+
let width = args.named("width")?;
let height = args.named("height")?;
Ok(Content::inline(
- ImageNode(id).pack().sized(Spec::new(width, height)),
+ ImageNode(image).pack().sized(Spec::new(width, height)),
))
}
}
@@ -29,13 +39,12 @@ impl ImageNode {
impl Layout for ImageNode {
fn layout(
&self,
- ctx: &mut Context,
+ _: &mut Context,
regions: &Regions,
styles: StyleChain,
) -> TypResult<Vec<Frame>> {
- let img = ctx.images.get(self.0);
- let pxw = img.width() as f64;
- let pxh = img.height() as f64;
+ let pxw = self.0.width() as f64;
+ let pxh = self.0.height() as f64;
let px_ratio = pxw / pxh;
// Find out whether the image is wider or taller than the target size.
@@ -71,7 +80,7 @@ impl Layout for ImageNode {
// the frame to the target size, center aligning the image in the
// process.
let mut frame = Frame::new(fitted);
- frame.push(Point::zero(), Element::Image(self.0, fitted));
+ frame.push(Point::zero(), Element::Image(self.0.clone(), fitted));
frame.resize(target, Align::CENTER_HORIZON);
// Create a clipping group if only part of the image should be visible.
diff --git a/src/library/text/par.rs b/src/library/text/par.rs
index 6c5c9765..168aca26 100644
--- a/src/library/text/par.rs
+++ b/src/library/text/par.rs
@@ -25,8 +25,6 @@ pub enum ParChild {
Spacing(Spacing),
/// An arbitrary inline-level node.
Node(LayoutNode),
- /// A pin identified by index.
- Pin(usize),
}
#[node]
@@ -101,7 +99,6 @@ impl Debug for ParChild {
Self::Quote { double } => write!(f, "Quote({double})"),
Self::Spacing(kind) => write!(f, "{:?}", kind),
Self::Node(node) => node.fmt(f),
- Self::Pin(idx) => write!(f, "Pin({idx})"),
}
}
}
@@ -197,7 +194,6 @@ type Range = std::ops::Range<usize>;
// paragraph's full text.
const SPACING_REPLACE: char = ' '; // Space
const NODE_REPLACE: char = '\u{FFFC}'; // Object Replacement Character
-const PIN_REPLACE: char = '\u{200D}'; // Zero Width Joiner
/// A paragraph representation in which children are already layouted and text
/// is already preshaped.
@@ -278,8 +274,6 @@ enum Segment<'a> {
Spacing(Spacing),
/// An arbitrary inline-level layout node.
Node(&'a LayoutNode),
- /// A pin identified by index.
- Pin(usize),
}
impl Segment<'_> {
@@ -289,7 +283,6 @@ impl Segment<'_> {
Self::Text(len) => len,
Self::Spacing(_) => SPACING_REPLACE.len_utf8(),
Self::Node(_) => NODE_REPLACE.len_utf8(),
- Self::Pin(_) => PIN_REPLACE.len_utf8(),
}
}
}
@@ -307,8 +300,6 @@ enum Item<'a> {
Frame(Frame),
/// A repeating node that fills the remaining space.
Repeat(&'a RepeatNode, StyleChain<'a>),
- /// A pin identified by index.
- Pin(usize),
}
impl<'a> Item<'a> {
@@ -326,7 +317,6 @@ impl<'a> Item<'a> {
Self::Text(shaped) => shaped.text.len(),
Self::Absolute(_) | Self::Fractional(_) => SPACING_REPLACE.len_utf8(),
Self::Frame(_) | Self::Repeat(_, _) => NODE_REPLACE.len_utf8(),
- Self::Pin(_) => PIN_REPLACE.len_utf8(),
}
}
@@ -336,7 +326,7 @@ impl<'a> Item<'a> {
Self::Text(shaped) => shaped.width,
Self::Absolute(v) => *v,
Self::Frame(frame) => frame.width(),
- Self::Fractional(_) | Self::Repeat(_, _) | Self::Pin(_) => Length::zero(),
+ Self::Fractional(_) | Self::Repeat(_, _) => Length::zero(),
}
}
}
@@ -467,7 +457,6 @@ fn collect<'a>(
ParChild::Quote { .. } => Some('"'),
ParChild::Spacing(_) => Some(SPACING_REPLACE),
ParChild::Node(_) => Some(NODE_REPLACE),
- ParChild::Pin(_) => Some(PIN_REPLACE),
});
full.push_str(quoter.quote(&quotes, double, peeked));
@@ -484,10 +473,6 @@ fn collect<'a>(
full.push(NODE_REPLACE);
Segment::Node(node)
}
- &ParChild::Pin(idx) => {
- full.push(PIN_REPLACE);
- Segment::Pin(idx)
- }
};
if let Some(last) = full.chars().last() {
@@ -556,7 +541,6 @@ fn prepare<'a>(
items.push(Item::Frame(frame));
}
}
- Segment::Pin(idx) => items.push(Item::Pin(idx)),
}
cursor = end;
@@ -1187,11 +1171,6 @@ fn commit(
}
offset = before + fill;
}
- Item::Pin(idx) => {
- let mut frame = Frame::new(Size::zero());
- frame.push(Point::zero(), Element::Pin(*idx));
- push(&mut offset, frame);
- }
}
}
diff --git a/src/loading/fs.rs b/src/loading/fs.rs
index 70ab5e53..55aa967b 100644
--- a/src/loading/fs.rs
+++ b/src/loading/fs.rs
@@ -6,7 +6,7 @@ use memmap2::Mmap;
use same_file::Handle;
use walkdir::WalkDir;
-use super::{FileHash, Loader};
+use super::{Buffer, FileHash, Loader};
use crate::font::FontInfo;
/// Loads fonts and files from the local file system.
@@ -130,7 +130,7 @@ impl Loader for FsLoader {
}
}
- fn load(&self, path: &Path) -> io::Result<Vec<u8>> {
- fs::read(path)
+ fn load(&self, path: &Path) -> io::Result<Buffer> {
+ Ok(fs::read(path)?.into())
}
}
diff --git a/src/loading/mem.rs b/src/loading/mem.rs
index 320de349..36e920d9 100644
--- a/src/loading/mem.rs
+++ b/src/loading/mem.rs
@@ -3,7 +3,7 @@ use std::collections::HashMap;
use std::io;
use std::path::{Path, PathBuf};
-use super::{FileHash, Loader};
+use super::{Buffer, FileHash, Loader};
use crate::font::FontInfo;
use crate::util::PathExt;
@@ -61,10 +61,10 @@ impl Loader for MemLoader {
}
}
- fn load(&self, path: &Path) -> io::Result<Vec<u8>> {
+ fn load(&self, path: &Path) -> io::Result<Buffer> {
self.files
.get(&path.normalize())
- .map(|cow| cow.clone().into_owned())
+ .map(|cow| cow.clone().into_owned().into())
.ok_or_else(|| io::ErrorKind::NotFound.into())
}
}
@@ -90,7 +90,7 @@ mod tests {
// Test that the file can be loaded.
assert_eq!(
- loader.load(Path::new("directory/../PTSans.ttf")).unwrap(),
+ loader.load(Path::new("directory/../PTSans.ttf")).unwrap().as_slice(),
data
);
}
diff --git a/src/loading/mod.rs b/src/loading/mod.rs
index d37dd1fc..ecc1e8d5 100644
--- a/src/loading/mod.rs
+++ b/src/loading/mod.rs
@@ -8,10 +8,14 @@ mod mem;
pub use fs::*;
pub use mem::*;
+use std::fmt::{self, Debug, Formatter};
use std::io;
+use std::ops::Deref;
use std::path::Path;
+use std::sync::Arc;
use crate::font::FontInfo;
+use crate::util::Prehashed;
/// A hash that identifies a file.
///
@@ -29,7 +33,7 @@ pub trait Loader {
fn resolve(&self, path: &Path) -> io::Result<FileHash>;
/// Load a file from a path.
- fn load(&self, path: &Path) -> io::Result<Vec<u8>>;
+ fn load(&self, path: &Path) -> io::Result<Buffer>;
}
/// A loader which serves nothing.
@@ -44,7 +48,61 @@ impl Loader for BlankLoader {
Err(io::ErrorKind::NotFound.into())
}
- fn load(&self, _: &Path) -> io::Result<Vec<u8>> {
+ fn load(&self, _: &Path) -> io::Result<Buffer> {
Err(io::ErrorKind::NotFound.into())
}
}
+
+/// A shared buffer that is cheap to clone.
+#[derive(Clone, Hash, Eq, PartialEq)]
+pub struct Buffer(Prehashed<Arc<Vec<u8>>>);
+
+impl Buffer {
+ /// Return a view into the buffer.
+ pub fn as_slice(&self) -> &[u8] {
+ self
+ }
+
+ /// Return a copy of the buffer as a vector.
+ pub fn to_vec(&self) -> Vec<u8> {
+ self.0.to_vec()
+ }
+}
+
+impl From<&[u8]> for Buffer {
+ fn from(slice: &[u8]) -> Self {
+ Self(Prehashed::new(Arc::new(slice.to_vec())))
+ }
+}
+
+impl From<Vec<u8>> for Buffer {
+ fn from(vec: Vec<u8>) -> Self {
+ Self(Prehashed::new(Arc::new(vec)))
+ }
+}
+
+impl From<Arc<Vec<u8>>> for Buffer {
+ fn from(arc: Arc<Vec<u8>>) -> Self {
+ Self(Prehashed::new(arc))
+ }
+}
+
+impl Deref for Buffer {
+ type Target = [u8];
+
+ fn deref(&self) -> &Self::Target {
+ &self.0
+ }
+}
+
+impl AsRef<[u8]> for Buffer {
+ fn as_ref(&self) -> &[u8] {
+ self
+ }
+}
+
+impl Debug for Buffer {
+ fn fmt(&self, f: &mut Formatter) -> fmt::Result {
+ f.pad("Buffer(..)")
+ }
+}
diff --git a/src/model/content.rs b/src/model/content.rs
index efbaed0e..ae86e279 100644
--- a/src/model/content.rs
+++ b/src/model/content.rs
@@ -90,8 +90,6 @@ pub enum Content {
/// A node that can be realized with styles, optionally with attached
/// properties.
Show(ShowNode, Option<Dict>),
- /// A pin identified by index.
- Pin(usize),
/// Content with attached styles.
Styled(Arc<(Self, StyleMap)>),
/// A sequence of multiple nodes.
@@ -281,7 +279,6 @@ impl Debug for Content {
Self::Pagebreak { weak } => write!(f, "Pagebreak({weak})"),
Self::Page(page) => page.fmt(f),
Self::Show(node, _) => node.fmt(f),
- Self::Pin(idx) => write!(f, "Pin({idx})"),
Self::Styled(styled) => {
let (sub, map) = styled.as_ref();
map.fmt(f)?;
@@ -651,9 +648,6 @@ impl<'a> ParBuilder<'a> {
Content::Inline(node) => {
self.0.supportive(ParChild::Node(node.clone()), styles);
}
- &Content::Pin(idx) => {
- self.0.ignorant(ParChild::Pin(idx), styles);
- }
_ => return false,
}
@@ -673,7 +667,7 @@ impl<'a> ParBuilder<'a> {
&& children
.items()
.find_map(|child| match child {
- ParChild::Spacing(_) | ParChild::Pin(_) => None,
+ ParChild::Spacing(_) => None,
ParChild::Text(_) | ParChild::Quote { .. } => Some(true),
ParChild::Node(_) => Some(false),
})
diff --git a/src/source.rs b/src/source.rs
index 145791b2..22648019 100644
--- a/src/source.rs
+++ b/src/source.rs
@@ -73,7 +73,7 @@ impl SourceStore {
/// root.
///
/// If there already exists a source file for this path, it is
- /// [replaced](SourceFile::replace).
+ /// [replaced](Source::replace).
pub fn load(&mut self, path: &Path) -> StrResult<SourceId> {
let mut try_load = || -> io::Result<SourceId> {
let hash = self.loader.resolve(path)?;
@@ -82,7 +82,7 @@ impl SourceStore {
}
let data = self.loader.load(path)?;
- let src = String::from_utf8(data).map_err(|_| {
+ let src = String::from_utf8(data.to_vec()).map_err(|_| {
io::Error::new(io::ErrorKind::InvalidData, "file is not valid utf-8")
})?;
@@ -99,7 +99,7 @@ impl SourceStore {
/// will use the inserted file instead of going through [`Loader::load`].
///
/// If the path is resolvable and points to an existing source file, it is
- /// [replaced](SourceFile::replace).
+ /// [replaced](Source::replace).
pub fn provide(&mut self, path: impl AsRef<Path>, src: String) -> SourceId {
let path = path.as_ref();
let hash = self.loader.resolve(path).ok();
@@ -122,7 +122,7 @@ impl SourceStore {
id
}
- /// Fully [replace](SourceFile::replace) the source text of a file.
+ /// Fully [replace](Source::replace) the source text of a file.
///
/// This panics if no source file with this `id` exists.
#[track_caller]
@@ -130,7 +130,7 @@ impl SourceStore {
self.sources[id.0 as usize].replace(src)
}
- /// [Edit](SourceFile::edit) a source file by replacing the given range.
+ /// [Edit](Source::edit) a source file by replacing the given range.
///
/// This panics if no source file with this `id` exists or if the `replace`
/// range is out of bounds.
@@ -144,7 +144,7 @@ impl SourceStore {
self.sources[id.0 as usize].edit(replace, with)
}
- /// Map a span that points into a [file](SourceFile::range) stored in this
+ /// Map a span that points into a [file](Source::range) stored in this
/// source store to a byte range.
///
/// Panics if the span does not point into this source store.