mirror of
https://github.com/typst/typst
synced 2025-06-28 00:03:17 +08:00
Remove image store
This commit is contained in:
parent
30be75c668
commit
59f67b79c7
@ -9,7 +9,7 @@ use crate::util::SliceExt;
|
|||||||
|
|
||||||
/// Embed all used fonts into the PDF.
|
/// Embed all used fonts into the PDF.
|
||||||
pub fn write_fonts(ctx: &mut PdfContext) {
|
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 type0_ref = ctx.alloc.bump();
|
||||||
let cid_ref = ctx.alloc.bump();
|
let cid_ref = ctx.alloc.bump();
|
||||||
let descriptor_ref = ctx.alloc.bump();
|
let descriptor_ref = ctx.alloc.bump();
|
||||||
|
@ -1,25 +1,26 @@
|
|||||||
use std::io::Cursor;
|
use std::io::Cursor;
|
||||||
|
|
||||||
use image::{DynamicImage, GenericImageView, ImageFormat, ImageResult, Rgba};
|
use image::{DynamicImage, GenericImageView, ImageResult, Rgba};
|
||||||
use pdf_writer::{Filter, Finish};
|
use pdf_writer::{Filter, Finish};
|
||||||
|
|
||||||
use super::{deflate, PdfContext, RefExt};
|
use super::{deflate, PdfContext, RefExt};
|
||||||
use crate::image::{Image, RasterImage};
|
use crate::image::{DecodedImage, ImageFormat};
|
||||||
|
|
||||||
/// Embed all used images into the PDF.
|
/// Embed all used images into the PDF.
|
||||||
pub fn write_images(ctx: &mut PdfContext) {
|
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();
|
let image_ref = ctx.alloc.bump();
|
||||||
ctx.image_refs.push(image_ref);
|
ctx.image_refs.push(image_ref);
|
||||||
|
|
||||||
let img = ctx.images.get(image_id);
|
let width = image.width();
|
||||||
let width = img.width();
|
let height = image.height();
|
||||||
let height = img.height();
|
|
||||||
|
|
||||||
// Add the primary image.
|
// Add the primary image.
|
||||||
match img {
|
match image.decode().unwrap() {
|
||||||
Image::Raster(img) => {
|
DecodedImage::Raster(dynamic) => {
|
||||||
if let Ok((data, filter, has_color)) = encode_image(img) {
|
if let Ok((data, filter, has_color)) =
|
||||||
|
encode_image(image.format(), &dynamic)
|
||||||
|
{
|
||||||
let mut image = ctx.writer.image_xobject(image_ref, &data);
|
let mut image = ctx.writer.image_xobject(image_ref, &data);
|
||||||
image.filter(filter);
|
image.filter(filter);
|
||||||
image.width(width as i32);
|
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
|
// Add a second gray-scale image containing the alpha values if
|
||||||
// this image has an alpha channel.
|
// this image has an alpha channel.
|
||||||
if img.buf.color().has_alpha() {
|
if dynamic.color().has_alpha() {
|
||||||
let (alpha_data, alpha_filter) = encode_alpha(img);
|
let (alpha_data, alpha_filter) = encode_alpha(&dynamic);
|
||||||
let mask_ref = ctx.alloc.bump();
|
let mask_ref = ctx.alloc.bump();
|
||||||
image.s_mask(mask_ref);
|
image.s_mask(mask_ref);
|
||||||
image.finish();
|
image.finish();
|
||||||
@ -59,9 +60,9 @@ pub fn write_images(ctx: &mut PdfContext) {
|
|||||||
.device_gray();
|
.device_gray();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Image::Svg(img) => {
|
DecodedImage::Svg(svg) => {
|
||||||
let next_ref = svg2pdf::convert_tree_into(
|
let next_ref = svg2pdf::convert_tree_into(
|
||||||
&img.0,
|
&svg,
|
||||||
svg2pdf::Options::default(),
|
svg2pdf::Options::default(),
|
||||||
&mut ctx.writer,
|
&mut ctx.writer,
|
||||||
image_ref,
|
image_ref,
|
||||||
@ -76,19 +77,22 @@ pub fn write_images(ctx: &mut PdfContext) {
|
|||||||
/// whether the image has color.
|
/// whether the image has color.
|
||||||
///
|
///
|
||||||
/// Skips the alpha channel as that's encoded separately.
|
/// Skips the alpha channel as that's encoded separately.
|
||||||
fn encode_image(img: &RasterImage) -> ImageResult<(Vec<u8>, Filter, bool)> {
|
fn encode_image(
|
||||||
Ok(match (img.format, &img.buf) {
|
format: ImageFormat,
|
||||||
|
dynamic: &DynamicImage,
|
||||||
|
) -> ImageResult<(Vec<u8>, Filter, bool)> {
|
||||||
|
Ok(match (format, dynamic) {
|
||||||
// 8-bit gray JPEG.
|
// 8-bit gray JPEG.
|
||||||
(ImageFormat::Jpeg, DynamicImage::ImageLuma8(_)) => {
|
(ImageFormat::Jpg, DynamicImage::ImageLuma8(_)) => {
|
||||||
let mut data = Cursor::new(vec![]);
|
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)
|
(data.into_inner(), Filter::DctDecode, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 8-bit RGB JPEG (CMYK JPEGs get converted to RGB earlier).
|
// 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![]);
|
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)
|
(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.
|
/// Encode an image's alpha channel if present.
|
||||||
fn encode_alpha(img: &RasterImage) -> (Vec<u8>, Filter) {
|
fn encode_alpha(dynamic: &DynamicImage) -> (Vec<u8>, Filter) {
|
||||||
let pixels: Vec<_> = img.buf.pixels().map(|(_, _, Rgba([_, _, _, a]))| a).collect();
|
let pixels: Vec<_> = dynamic.pixels().map(|(_, _, Rgba([_, _, _, a]))| a).collect();
|
||||||
(deflate(&pixels), Filter::FlateDecode)
|
(deflate(&pixels), Filter::FlateDecode)
|
||||||
}
|
}
|
||||||
|
@ -17,7 +17,7 @@ use self::page::Page;
|
|||||||
use crate::font::{FontId, FontStore};
|
use crate::font::{FontId, FontStore};
|
||||||
use crate::frame::Frame;
|
use crate::frame::Frame;
|
||||||
use crate::geom::{Dir, Em, Length};
|
use crate::geom::{Dir, Em, Length};
|
||||||
use crate::image::{ImageId, ImageStore};
|
use crate::image::Image;
|
||||||
use crate::library::text::Lang;
|
use crate::library::text::Lang;
|
||||||
use crate::Context;
|
use crate::Context;
|
||||||
|
|
||||||
@ -46,7 +46,6 @@ const D65_GRAY: Name<'static> = Name(b"d65gray");
|
|||||||
pub struct PdfContext<'a> {
|
pub struct PdfContext<'a> {
|
||||||
writer: PdfWriter,
|
writer: PdfWriter,
|
||||||
fonts: &'a FontStore,
|
fonts: &'a FontStore,
|
||||||
images: &'a ImageStore,
|
|
||||||
pages: Vec<Page>,
|
pages: Vec<Page>,
|
||||||
page_heights: Vec<f32>,
|
page_heights: Vec<f32>,
|
||||||
alloc: Ref,
|
alloc: Ref,
|
||||||
@ -55,7 +54,7 @@ pub struct PdfContext<'a> {
|
|||||||
image_refs: Vec<Ref>,
|
image_refs: Vec<Ref>,
|
||||||
page_refs: Vec<Ref>,
|
page_refs: Vec<Ref>,
|
||||||
font_map: Remapper<FontId>,
|
font_map: Remapper<FontId>,
|
||||||
image_map: Remapper<ImageId>,
|
image_map: Remapper<Image>,
|
||||||
glyph_sets: HashMap<FontId, HashSet<u16>>,
|
glyph_sets: HashMap<FontId, HashSet<u16>>,
|
||||||
languages: HashMap<Lang, usize>,
|
languages: HashMap<Lang, usize>,
|
||||||
heading_tree: Vec<HeadingNode>,
|
heading_tree: Vec<HeadingNode>,
|
||||||
@ -68,7 +67,6 @@ impl<'a> PdfContext<'a> {
|
|||||||
Self {
|
Self {
|
||||||
writer: PdfWriter::new(),
|
writer: PdfWriter::new(),
|
||||||
fonts: &ctx.fonts,
|
fonts: &ctx.fonts,
|
||||||
images: &ctx.images,
|
|
||||||
pages: vec![],
|
pages: vec![],
|
||||||
page_heights: vec![],
|
page_heights: vec![],
|
||||||
alloc,
|
alloc,
|
||||||
@ -147,36 +145,33 @@ fn deflate(data: &[u8]) -> Vec<u8> {
|
|||||||
miniz_oxide::deflate::compress_to_vec_zlib(data, COMPRESSION_LEVEL)
|
miniz_oxide::deflate::compress_to_vec_zlib(data, COMPRESSION_LEVEL)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Assigns new, consecutive PDF-internal indices to things.
|
/// Assigns new, consecutive PDF-internal indices to items.
|
||||||
struct Remapper<Index> {
|
struct Remapper<T> {
|
||||||
/// Forwards from the old indices to the new pdf indices.
|
/// Forwards from the items to the pdf indices.
|
||||||
to_pdf: HashMap<Index, usize>,
|
to_pdf: HashMap<T, usize>,
|
||||||
/// Backwards from the pdf indices to the old indices.
|
/// Backwards from the pdf indices to the items.
|
||||||
to_layout: Vec<Index>,
|
to_items: Vec<T>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<Index> Remapper<Index>
|
impl<T> Remapper<T>
|
||||||
where
|
where
|
||||||
Index: Copy + Eq + Hash,
|
T: Eq + Hash + Clone,
|
||||||
{
|
{
|
||||||
fn new() -> Self {
|
fn new() -> Self {
|
||||||
Self {
|
Self { to_pdf: HashMap::new(), to_items: vec![] }
|
||||||
to_pdf: HashMap::new(),
|
|
||||||
to_layout: vec![],
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn insert(&mut self, index: Index) {
|
fn insert(&mut self, item: T) {
|
||||||
let to_layout = &mut self.to_layout;
|
let to_layout = &mut self.to_items;
|
||||||
self.to_pdf.entry(index).or_insert_with(|| {
|
self.to_pdf.entry(item.clone()).or_insert_with(|| {
|
||||||
let pdf_index = to_layout.len();
|
let pdf_index = to_layout.len();
|
||||||
to_layout.push(index);
|
to_layout.push(item);
|
||||||
pdf_index
|
pdf_index
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn map(&self, index: Index) -> usize {
|
fn map(&self, item: T) -> usize {
|
||||||
self.to_pdf[&index]
|
self.to_pdf[&item]
|
||||||
}
|
}
|
||||||
|
|
||||||
fn pdf_indices<'a>(
|
fn pdf_indices<'a>(
|
||||||
@ -186,8 +181,8 @@ where
|
|||||||
refs.iter().copied().zip(0 .. self.to_pdf.len())
|
refs.iter().copied().zip(0 .. self.to_pdf.len())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn layout_indices(&self) -> impl Iterator<Item = Index> + '_ {
|
fn items(&self) -> impl Iterator<Item = &T> + '_ {
|
||||||
self.to_layout.iter().copied()
|
self.to_items.iter()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -11,7 +11,7 @@ use crate::geom::{
|
|||||||
self, Color, Em, Geometry, Length, Numeric, Paint, Point, Ratio, Shape, Size, Stroke,
|
self, Color, Em, Geometry, Length, Numeric, Paint, Point, Ratio, Shape, Size, Stroke,
|
||||||
Transform,
|
Transform,
|
||||||
};
|
};
|
||||||
use crate::image::ImageId;
|
use crate::image::Image;
|
||||||
|
|
||||||
/// Construct page objects.
|
/// Construct page objects.
|
||||||
pub fn construct_pages(ctx: &mut PdfContext, frames: &[Frame]) {
|
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() {
|
for &(pos, ref element) in frame.elements() {
|
||||||
let x = pos.x.to_f32();
|
let x = pos.x.to_f32();
|
||||||
let y = pos.y.to_f32();
|
let y = pos.y.to_f32();
|
||||||
match *element {
|
match element {
|
||||||
Element::Group(ref group) => write_group(ctx, pos, group),
|
Element::Group(group) => write_group(ctx, pos, group),
|
||||||
Element::Text(ref text) => write_text(ctx, x, y, text),
|
Element::Text(text) => write_text(ctx, x, y, text),
|
||||||
Element::Shape(ref shape) => write_shape(ctx, x, y, shape),
|
Element::Shape(shape) => write_shape(ctx, x, y, shape),
|
||||||
Element::Image(id, size) => write_image(ctx, x, y, id, size),
|
Element::Image(image, size) => write_image(ctx, x, y, image, *size),
|
||||||
Element::Link(ref dest, size) => write_link(ctx, pos, dest, size),
|
Element::Link(dest, size) => write_link(ctx, pos, dest, *size),
|
||||||
Element::Pin(_) => {}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -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.
|
/// Encode a vector or raster image into the content stream.
|
||||||
fn write_image(ctx: &mut PageContext, x: f32, y: f32, id: ImageId, size: Size) {
|
fn write_image(ctx: &mut PageContext, x: f32, y: f32, image: &Image, size: Size) {
|
||||||
ctx.parent.image_map.insert(id);
|
ctx.parent.image_map.insert(image.clone());
|
||||||
let name = format_eco!("Im{}", ctx.parent.image_map.map(id));
|
let name = format_eco!("Im{}", ctx.parent.image_map.map(image.clone()));
|
||||||
let w = size.x.to_f32();
|
let w = size.x.to_f32();
|
||||||
let h = size.y.to_f32();
|
let h = size.y.to_f32();
|
||||||
ctx.content.save_state();
|
ctx.content.save_state();
|
||||||
|
@ -12,7 +12,7 @@ use crate::frame::{Element, Frame, Group, Text};
|
|||||||
use crate::geom::{
|
use crate::geom::{
|
||||||
self, Geometry, Length, Paint, PathElement, Shape, Size, Stroke, Transform,
|
self, Geometry, Length, Paint, PathElement, Shape, Size, Stroke, Transform,
|
||||||
};
|
};
|
||||||
use crate::image::{Image, RasterImage, Svg};
|
use crate::image::{DecodedImage, Image};
|
||||||
use crate::Context;
|
use crate::Context;
|
||||||
|
|
||||||
/// Export a frame into a rendered image.
|
/// Export a frame into a rendered image.
|
||||||
@ -49,21 +49,20 @@ fn render_frame(
|
|||||||
let y = pos.y.to_f32();
|
let y = pos.y.to_f32();
|
||||||
let ts = ts.pre_translate(x, y);
|
let ts = ts.pre_translate(x, y);
|
||||||
|
|
||||||
match *element {
|
match element {
|
||||||
Element::Group(ref group) => {
|
Element::Group(group) => {
|
||||||
render_group(canvas, ts, mask, ctx, group);
|
render_group(canvas, ts, mask, ctx, group);
|
||||||
}
|
}
|
||||||
Element::Text(ref text) => {
|
Element::Text(text) => {
|
||||||
render_text(canvas, ts, mask, ctx, text);
|
render_text(canvas, ts, mask, ctx, text);
|
||||||
}
|
}
|
||||||
Element::Shape(ref shape) => {
|
Element::Shape(shape) => {
|
||||||
render_shape(canvas, ts, mask, shape);
|
render_shape(canvas, ts, mask, shape);
|
||||||
}
|
}
|
||||||
Element::Image(id, size) => {
|
Element::Image(image, size) => {
|
||||||
render_image(canvas, ts, mask, ctx.images.get(id), size);
|
render_image(canvas, ts, mask, image, *size);
|
||||||
}
|
}
|
||||||
Element::Link(_, _) => {}
|
Element::Link(_, _) => {}
|
||||||
Element::Pin(_) => {}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -197,17 +196,20 @@ fn render_bitmap_glyph(
|
|||||||
let ppem = size * ts.sy;
|
let ppem = size * ts.sy;
|
||||||
let font = ctx.fonts.get(text.font_id);
|
let font = ctx.fonts.get(text.font_id);
|
||||||
let raster = font.ttf().glyph_raster_image(id, ppem as u16)?;
|
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,
|
// FIXME: Vertical alignment isn't quite right for Apple Color Emoji,
|
||||||
// and maybe also for Noto Color Emoji. And: Is the size calculation
|
// and maybe also for Noto Color Emoji. And: Is the size calculation
|
||||||
// correct?
|
// correct?
|
||||||
let h = text.size;
|
let h = text.size;
|
||||||
let w = (img.width() as f64 / img.height() as f64) * h;
|
let w = (image.width() as f64 / image.height() as f64) * h;
|
||||||
let dx = (raster.x as f32) / (img.width() as f32) * size;
|
let dx = (raster.x as f32) / (image.width() as f32) * size;
|
||||||
let dy = (raster.y as f32) / (img.height() as f32) * size;
|
let dy = (raster.y as f32) / (image.height() as f32) * size;
|
||||||
let ts = ts.pre_translate(dx, -size - dy);
|
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.
|
/// Render an outline glyph into the canvas. This is the "normal" case.
|
||||||
@ -338,33 +340,33 @@ fn render_image(
|
|||||||
canvas: &mut sk::Pixmap,
|
canvas: &mut sk::Pixmap,
|
||||||
ts: sk::Transform,
|
ts: sk::Transform,
|
||||||
mask: Option<&sk::ClipMask>,
|
mask: Option<&sk::ClipMask>,
|
||||||
img: &Image,
|
image: &Image,
|
||||||
size: Size,
|
size: Size,
|
||||||
) -> Option<()> {
|
) -> Option<()> {
|
||||||
let view_width = size.x.to_f32();
|
let view_width = size.x.to_f32();
|
||||||
let view_height = size.y.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 scale = ts.sx.max(ts.sy);
|
||||||
let w = (scale * view_width.max(aspect * view_height)).ceil() as u32;
|
let w = (scale * view_width.max(aspect * view_height)).ceil() as u32;
|
||||||
let h = ((w as f32) / aspect).ceil() as u32;
|
let h = ((w as f32) / aspect).ceil() as u32;
|
||||||
|
|
||||||
let mut pixmap = sk::Pixmap::new(w, h)?;
|
let mut pixmap = sk::Pixmap::new(w, h)?;
|
||||||
match img {
|
match image.decode().unwrap() {
|
||||||
Image::Raster(img) => {
|
DecodedImage::Raster(dynamic) => {
|
||||||
let downscale = w < img.width();
|
let downscale = w < image.width();
|
||||||
let filter = if downscale {
|
let filter = if downscale {
|
||||||
FilterType::Lanczos3
|
FilterType::Lanczos3
|
||||||
} else {
|
} else {
|
||||||
FilterType::CatmullRom
|
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()) {
|
for ((_, _, src), dest) in buf.pixels().zip(pixmap.pixels_mut()) {
|
||||||
let Rgba([r, g, b, a]) = src;
|
let Rgba([r, g, b, a]) = src;
|
||||||
*dest = sk::ColorU8::from_rgba(r, g, b, a).premultiply();
|
*dest = sk::ColorU8::from_rgba(r, g, b, a).premultiply();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Image::Svg(Svg(tree)) => {
|
DecodedImage::Svg(tree) => {
|
||||||
resvg::render(
|
resvg::render(
|
||||||
&tree,
|
&tree,
|
||||||
FitTo::Size(w, h),
|
FitTo::Size(w, h),
|
||||||
|
22
src/font.rs
22
src/font.rs
@ -13,7 +13,7 @@ use ttf_parser::{name_id, GlyphId, PlatformId, Tag};
|
|||||||
use unicode_segmentation::UnicodeSegmentation;
|
use unicode_segmentation::UnicodeSegmentation;
|
||||||
|
|
||||||
use crate::geom::Em;
|
use crate::geom::Em;
|
||||||
use crate::loading::{FileHash, Loader};
|
use crate::loading::{Buffer, FileHash, Loader};
|
||||||
|
|
||||||
/// A unique identifier for a loaded font.
|
/// A unique identifier for a loaded font.
|
||||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
|
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
|
||||||
@ -40,7 +40,7 @@ pub struct FontStore {
|
|||||||
failed: Vec<bool>,
|
failed: Vec<bool>,
|
||||||
fonts: Vec<Option<Font>>,
|
fonts: Vec<Option<Font>>,
|
||||||
families: BTreeMap<String, Vec<FontId>>,
|
families: BTreeMap<String, Vec<FontId>>,
|
||||||
buffers: HashMap<FileHash, Arc<Vec<u8>>>,
|
buffers: HashMap<FileHash, Buffer>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FontStore {
|
impl FontStore {
|
||||||
@ -214,11 +214,11 @@ impl FontStore {
|
|||||||
Entry::Occupied(entry) => entry.into_mut(),
|
Entry::Occupied(entry) => entry.into_mut(),
|
||||||
Entry::Vacant(entry) => {
|
Entry::Vacant(entry) => {
|
||||||
let buffer = self.loader.load(path).ok()?;
|
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);
|
*slot = Some(font);
|
||||||
self.failed[idx] = false;
|
self.failed[idx] = false;
|
||||||
|
|
||||||
@ -239,7 +239,7 @@ pub struct Font {
|
|||||||
/// The raw font data, possibly shared with other fonts from the same
|
/// The raw font data, possibly shared with other fonts from the same
|
||||||
/// collection. The vector's allocation must not move, because `ttf` points
|
/// collection. The vector's allocation must not move, because `ttf` points
|
||||||
/// into it using unsafe code.
|
/// into it using unsafe code.
|
||||||
buffer: Arc<Vec<u8>>,
|
data: Buffer,
|
||||||
/// The font's index in the collection (zero if not a collection).
|
/// The font's index in the collection (zero if not a collection).
|
||||||
index: u32,
|
index: u32,
|
||||||
/// The underlying ttf-parser/rustybuzz face.
|
/// The underlying ttf-parser/rustybuzz face.
|
||||||
@ -251,8 +251,8 @@ pub struct Font {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Font {
|
impl Font {
|
||||||
/// Parse a font from a buffer and collection index.
|
/// Parse a font from data and collection index.
|
||||||
pub fn new(buffer: Arc<Vec<u8>>, index: u32) -> Option<Self> {
|
pub fn new(data: Buffer, index: u32) -> Option<Self> {
|
||||||
// Safety:
|
// Safety:
|
||||||
// - The slices's location is stable in memory:
|
// - The slices's location is stable in memory:
|
||||||
// - We don't move the underlying vector
|
// - We don't move the underlying vector
|
||||||
@ -260,13 +260,13 @@ impl Font {
|
|||||||
// - The internal 'static lifetime is not leaked because its rewritten
|
// - The internal 'static lifetime is not leaked because its rewritten
|
||||||
// to the self-lifetime in `ttf()`.
|
// to the self-lifetime in `ttf()`.
|
||||||
let slice: &'static [u8] =
|
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 ttf = rustybuzz::Face::from_slice(slice, index)?;
|
||||||
let metrics = FontMetrics::from_ttf(&ttf);
|
let metrics = FontMetrics::from_ttf(&ttf);
|
||||||
|
|
||||||
Some(Self {
|
Some(Self {
|
||||||
buffer,
|
data,
|
||||||
index,
|
index,
|
||||||
ttf,
|
ttf,
|
||||||
metrics,
|
metrics,
|
||||||
@ -275,8 +275,8 @@ impl Font {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// The underlying buffer.
|
/// The underlying buffer.
|
||||||
pub fn buffer(&self) -> &Arc<Vec<u8>> {
|
pub fn buffer(&self) -> &Buffer {
|
||||||
&self.buffer
|
&self.data
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The collection index.
|
/// The collection index.
|
||||||
|
@ -9,7 +9,7 @@ use crate::font::FontId;
|
|||||||
use crate::geom::{
|
use crate::geom::{
|
||||||
Align, Em, Length, Numeric, Paint, Point, Shape, Size, Spec, Transform,
|
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::library::text::Lang;
|
||||||
use crate::util::EcoString;
|
use crate::util::EcoString;
|
||||||
|
|
||||||
@ -303,12 +303,9 @@ pub enum Element {
|
|||||||
/// A geometric shape with optional fill and stroke.
|
/// A geometric shape with optional fill and stroke.
|
||||||
Shape(Shape),
|
Shape(Shape),
|
||||||
/// An image and its size.
|
/// An image and its size.
|
||||||
Image(ImageId, Size),
|
Image(Image, Size),
|
||||||
/// A link to an external resource and its trigger region.
|
/// A link to an external resource and its trigger region.
|
||||||
Link(Destination, Size),
|
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 {
|
impl Debug for Element {
|
||||||
@ -319,7 +316,6 @@ impl Debug for Element {
|
|||||||
Self::Shape(shape) => write!(f, "{shape:?}"),
|
Self::Shape(shape) => write!(f, "{shape:?}"),
|
||||||
Self::Image(image, _) => write!(f, "{image:?}"),
|
Self::Image(image, _) => write!(f, "{image:?}"),
|
||||||
Self::Link(dest, _) => write!(f, "Link({dest:?})"),
|
Self::Link(dest, _) => write!(f, "Link({dest:?})"),
|
||||||
Self::Pin(idx) => write!(f, "Pin({idx})"),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
294
src/image.rs
294
src/image.rs
@ -1,213 +1,139 @@
|
|||||||
//! Image handling.
|
//! Image handling.
|
||||||
|
|
||||||
use std::collections::{hash_map::Entry, HashMap};
|
|
||||||
use std::ffi::OsStr;
|
|
||||||
use std::fmt::{self, Debug, Formatter};
|
|
||||||
use std::io;
|
use std::io;
|
||||||
use std::path::Path;
|
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
use image::io::Reader as ImageReader;
|
use crate::loading::Buffer;
|
||||||
use image::{DynamicImage, ImageFormat};
|
|
||||||
|
|
||||||
use crate::diag::{failed_to_load, StrResult};
|
/// A raster or vector image.
|
||||||
use crate::loading::{FileHash, Loader};
|
///
|
||||||
|
/// Values of this type are cheap to clone and hash.
|
||||||
/// A unique identifier for a loaded image.
|
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
|
||||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
|
pub struct Image {
|
||||||
pub struct ImageId(u32);
|
/// The raw, undecoded image data.
|
||||||
|
data: Buffer,
|
||||||
impl ImageId {
|
/// The format of the encoded `buffer`.
|
||||||
/// Create an image id from the raw underlying value.
|
format: ImageFormat,
|
||||||
///
|
/// The width in pixels.
|
||||||
/// This should only be called with values returned by
|
width: u32,
|
||||||
/// [`into_raw`](Self::into_raw).
|
/// The height in pixels.
|
||||||
pub const fn from_raw(v: u32) -> Self {
|
height: u32,
|
||||||
Self(v)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Convert into the raw underlying value.
|
|
||||||
pub const fn into_raw(self) -> u32 {
|
|
||||||
self.0
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Storage for loaded and decoded images.
|
/// A decoded image.
|
||||||
pub struct ImageStore {
|
pub enum DecodedImage {
|
||||||
loader: Arc<dyn Loader>,
|
|
||||||
files: HashMap<FileHash, ImageId>,
|
|
||||||
images: Vec<Image>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ImageStore {
|
|
||||||
/// Create a new, empty image store.
|
|
||||||
pub fn new(loader: Arc<dyn Loader>) -> Self {
|
|
||||||
Self {
|
|
||||||
loader,
|
|
||||||
files: HashMap::new(),
|
|
||||||
images: vec![],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get a reference to a loaded image.
|
|
||||||
///
|
|
||||||
/// 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]
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
};
|
|
||||||
|
|
||||||
try_load().map_err(|err| failed_to_load("image", path, err))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A loaded image.
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub enum Image {
|
|
||||||
/// A pixel raster format, like PNG or JPEG.
|
/// A pixel raster format, like PNG or JPEG.
|
||||||
Raster(RasterImage),
|
Raster(image::DynamicImage),
|
||||||
/// An SVG vector graphic.
|
/// An SVG vector graphic.
|
||||||
Svg(Svg),
|
Svg(usvg::Tree),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Image {
|
impl Image {
|
||||||
/// Parse an image from raw data. The file extension is used as a hint for
|
/// Create an image from a raw buffer and a file extension.
|
||||||
/// which error message describes the problem best.
|
///
|
||||||
pub fn parse(data: &[u8], ext: &str) -> io::Result<Self> {
|
/// The file extension is used to determine the format.
|
||||||
match Svg::parse(data) {
|
pub fn new(data: Buffer, ext: &str) -> io::Result<Self> {
|
||||||
Ok(svg) => return Ok(Self::Svg(svg)),
|
let format = match ext {
|
||||||
Err(err) if matches!(ext, "svg" | "svgz") => return Err(err),
|
"svg" | "svgz" => ImageFormat::Svg,
|
||||||
Err(_) => {}
|
"png" => ImageFormat::Png,
|
||||||
}
|
"jpg" | "jpeg" => ImageFormat::Jpg,
|
||||||
|
"gif" => ImageFormat::Gif,
|
||||||
|
_ => {
|
||||||
|
return Err(io::Error::new(
|
||||||
|
io::ErrorKind::InvalidData,
|
||||||
|
"unknown image format",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
match RasterImage::parse(data) {
|
let (width, height) = match format {
|
||||||
Ok(raster) => return Ok(Self::Raster(raster)),
|
ImageFormat::Svg => {
|
||||||
Err(err) if matches!(ext, "png" | "jpg" | "jpeg" | "gif") => return Err(err),
|
let opts = usvg::Options::default();
|
||||||
Err(_) => {}
|
let tree =
|
||||||
}
|
usvg::Tree::from_data(&data, &opts.to_ref()).map_err(invalid)?;
|
||||||
|
|
||||||
Err(io::Error::new(
|
let size = tree.svg_node().size;
|
||||||
io::ErrorKind::InvalidData,
|
let width = size.width().ceil() as u32;
|
||||||
"unknown image format",
|
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)?
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Self { data, format, width, height })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The raw image data.
|
||||||
|
pub fn data(&self) -> &Buffer {
|
||||||
|
&self.data
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The format of the image.
|
||||||
|
pub fn format(&self) -> ImageFormat {
|
||||||
|
self.format
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The width of the image in pixels.
|
/// The width of the image in pixels.
|
||||||
pub fn width(&self) -> u32 {
|
pub fn width(&self) -> u32 {
|
||||||
match self {
|
self.width
|
||||||
Self::Raster(image) => image.width(),
|
|
||||||
Self::Svg(image) => image.width(),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The height of the image in pixels.
|
/// The height of the image in pixels.
|
||||||
pub fn height(&self) -> u32 {
|
pub fn height(&self) -> u32 {
|
||||||
match self {
|
self.height
|
||||||
Self::Raster(image) => image.height(),
|
}
|
||||||
Self::Svg(image) => image.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.
|
/// Convert a raster image format to the image crate's format.
|
||||||
pub struct RasterImage {
|
fn convert_format(format: ImageFormat) -> image::ImageFormat {
|
||||||
/// The original format the image was encoded in.
|
match format {
|
||||||
pub format: ImageFormat,
|
ImageFormat::Png => image::ImageFormat::Png,
|
||||||
/// The decoded image.
|
ImageFormat::Jpg => image::ImageFormat::Jpeg,
|
||||||
pub buf: DynamicImage,
|
ImageFormat::Gif => image::ImageFormat::Gif,
|
||||||
}
|
ImageFormat::Svg => panic!("must be a raster format"),
|
||||||
|
|
||||||
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 {
|
/// Turn any error into an I/O error.
|
||||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
fn invalid<E>(error: E) -> io::Error
|
||||||
f.debug_struct("Image")
|
where
|
||||||
.field("format", &self.format)
|
E: std::error::Error + Send + Sync + 'static,
|
||||||
.field("color", &self.buf.color())
|
{
|
||||||
.field("width", &self.width())
|
io::Error::new(io::ErrorKind::InvalidData, error)
|
||||||
.field("height", &self.height())
|
|
||||||
.finish()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -57,7 +57,6 @@ use crate::diag::TypResult;
|
|||||||
use crate::eval::Scope;
|
use crate::eval::Scope;
|
||||||
use crate::font::FontStore;
|
use crate::font::FontStore;
|
||||||
use crate::frame::Frame;
|
use crate::frame::Frame;
|
||||||
use crate::image::ImageStore;
|
|
||||||
use crate::loading::Loader;
|
use crate::loading::Loader;
|
||||||
use crate::model::StyleMap;
|
use crate::model::StyleMap;
|
||||||
use crate::source::{SourceId, SourceStore};
|
use crate::source::{SourceId, SourceStore};
|
||||||
@ -80,8 +79,6 @@ pub struct Context {
|
|||||||
pub sources: SourceStore,
|
pub sources: SourceStore,
|
||||||
/// Stores parsed fonts.
|
/// Stores parsed fonts.
|
||||||
pub fonts: FontStore,
|
pub fonts: FontStore,
|
||||||
/// Stores decoded images.
|
|
||||||
pub images: ImageStore,
|
|
||||||
/// The context's configuration.
|
/// The context's configuration.
|
||||||
config: Config,
|
config: Config,
|
||||||
}
|
}
|
||||||
@ -93,7 +90,6 @@ impl Context {
|
|||||||
loader: Arc::clone(&loader),
|
loader: Arc::clone(&loader),
|
||||||
sources: SourceStore::new(Arc::clone(&loader)),
|
sources: SourceStore::new(Arc::clone(&loader)),
|
||||||
fonts: FontStore::new(Arc::clone(&loader)),
|
fonts: FontStore::new(Arc::clone(&loader)),
|
||||||
images: ImageStore::new(loader),
|
|
||||||
config,
|
config,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
use crate::image::ImageId;
|
use std::ffi::OsStr;
|
||||||
|
|
||||||
|
use crate::image::Image;
|
||||||
use crate::library::prelude::*;
|
use crate::library::prelude::*;
|
||||||
use crate::library::text::TextNode;
|
use crate::library::text::TextNode;
|
||||||
|
|
||||||
/// Show a raster or vector graphic.
|
/// Show a raster or vector graphic.
|
||||||
#[derive(Debug, Hash)]
|
#[derive(Debug, Hash)]
|
||||||
pub struct ImageNode(pub ImageId);
|
pub struct ImageNode(pub Image);
|
||||||
|
|
||||||
#[node]
|
#[node]
|
||||||
impl ImageNode {
|
impl ImageNode {
|
||||||
@ -16,12 +18,20 @@ impl ImageNode {
|
|||||||
args.expect::<Spanned<EcoString>>("path to image file")?;
|
args.expect::<Spanned<EcoString>>("path to image file")?;
|
||||||
|
|
||||||
let full = vm.locate(&path).at(span)?;
|
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 width = args.named("width")?;
|
||||||
let height = args.named("height")?;
|
let height = args.named("height")?;
|
||||||
|
|
||||||
Ok(Content::inline(
|
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 {
|
impl Layout for ImageNode {
|
||||||
fn layout(
|
fn layout(
|
||||||
&self,
|
&self,
|
||||||
ctx: &mut Context,
|
_: &mut Context,
|
||||||
regions: &Regions,
|
regions: &Regions,
|
||||||
styles: StyleChain,
|
styles: StyleChain,
|
||||||
) -> TypResult<Vec<Frame>> {
|
) -> TypResult<Vec<Frame>> {
|
||||||
let img = ctx.images.get(self.0);
|
let pxw = self.0.width() as f64;
|
||||||
let pxw = img.width() as f64;
|
let pxh = self.0.height() as f64;
|
||||||
let pxh = img.height() as f64;
|
|
||||||
let px_ratio = pxw / pxh;
|
let px_ratio = pxw / pxh;
|
||||||
|
|
||||||
// Find out whether the image is wider or taller than the target size.
|
// 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
|
// the frame to the target size, center aligning the image in the
|
||||||
// process.
|
// process.
|
||||||
let mut frame = Frame::new(fitted);
|
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);
|
frame.resize(target, Align::CENTER_HORIZON);
|
||||||
|
|
||||||
// Create a clipping group if only part of the image should be visible.
|
// Create a clipping group if only part of the image should be visible.
|
||||||
|
@ -25,8 +25,6 @@ pub enum ParChild {
|
|||||||
Spacing(Spacing),
|
Spacing(Spacing),
|
||||||
/// An arbitrary inline-level node.
|
/// An arbitrary inline-level node.
|
||||||
Node(LayoutNode),
|
Node(LayoutNode),
|
||||||
/// A pin identified by index.
|
|
||||||
Pin(usize),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[node]
|
#[node]
|
||||||
@ -101,7 +99,6 @@ impl Debug for ParChild {
|
|||||||
Self::Quote { double } => write!(f, "Quote({double})"),
|
Self::Quote { double } => write!(f, "Quote({double})"),
|
||||||
Self::Spacing(kind) => write!(f, "{:?}", kind),
|
Self::Spacing(kind) => write!(f, "{:?}", kind),
|
||||||
Self::Node(node) => node.fmt(f),
|
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.
|
// paragraph's full text.
|
||||||
const SPACING_REPLACE: char = ' '; // Space
|
const SPACING_REPLACE: char = ' '; // Space
|
||||||
const NODE_REPLACE: char = '\u{FFFC}'; // Object Replacement Character
|
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
|
/// A paragraph representation in which children are already layouted and text
|
||||||
/// is already preshaped.
|
/// is already preshaped.
|
||||||
@ -278,8 +274,6 @@ enum Segment<'a> {
|
|||||||
Spacing(Spacing),
|
Spacing(Spacing),
|
||||||
/// An arbitrary inline-level layout node.
|
/// An arbitrary inline-level layout node.
|
||||||
Node(&'a LayoutNode),
|
Node(&'a LayoutNode),
|
||||||
/// A pin identified by index.
|
|
||||||
Pin(usize),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Segment<'_> {
|
impl Segment<'_> {
|
||||||
@ -289,7 +283,6 @@ impl Segment<'_> {
|
|||||||
Self::Text(len) => len,
|
Self::Text(len) => len,
|
||||||
Self::Spacing(_) => SPACING_REPLACE.len_utf8(),
|
Self::Spacing(_) => SPACING_REPLACE.len_utf8(),
|
||||||
Self::Node(_) => NODE_REPLACE.len_utf8(),
|
Self::Node(_) => NODE_REPLACE.len_utf8(),
|
||||||
Self::Pin(_) => PIN_REPLACE.len_utf8(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -307,8 +300,6 @@ enum Item<'a> {
|
|||||||
Frame(Frame),
|
Frame(Frame),
|
||||||
/// A repeating node that fills the remaining space.
|
/// A repeating node that fills the remaining space.
|
||||||
Repeat(&'a RepeatNode, StyleChain<'a>),
|
Repeat(&'a RepeatNode, StyleChain<'a>),
|
||||||
/// A pin identified by index.
|
|
||||||
Pin(usize),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Item<'a> {
|
impl<'a> Item<'a> {
|
||||||
@ -326,7 +317,6 @@ impl<'a> Item<'a> {
|
|||||||
Self::Text(shaped) => shaped.text.len(),
|
Self::Text(shaped) => shaped.text.len(),
|
||||||
Self::Absolute(_) | Self::Fractional(_) => SPACING_REPLACE.len_utf8(),
|
Self::Absolute(_) | Self::Fractional(_) => SPACING_REPLACE.len_utf8(),
|
||||||
Self::Frame(_) | Self::Repeat(_, _) => NODE_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::Text(shaped) => shaped.width,
|
||||||
Self::Absolute(v) => *v,
|
Self::Absolute(v) => *v,
|
||||||
Self::Frame(frame) => frame.width(),
|
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::Quote { .. } => Some('"'),
|
||||||
ParChild::Spacing(_) => Some(SPACING_REPLACE),
|
ParChild::Spacing(_) => Some(SPACING_REPLACE),
|
||||||
ParChild::Node(_) => Some(NODE_REPLACE),
|
ParChild::Node(_) => Some(NODE_REPLACE),
|
||||||
ParChild::Pin(_) => Some(PIN_REPLACE),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
full.push_str(quoter.quote("es, double, peeked));
|
full.push_str(quoter.quote("es, double, peeked));
|
||||||
@ -484,10 +473,6 @@ fn collect<'a>(
|
|||||||
full.push(NODE_REPLACE);
|
full.push(NODE_REPLACE);
|
||||||
Segment::Node(node)
|
Segment::Node(node)
|
||||||
}
|
}
|
||||||
&ParChild::Pin(idx) => {
|
|
||||||
full.push(PIN_REPLACE);
|
|
||||||
Segment::Pin(idx)
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(last) = full.chars().last() {
|
if let Some(last) = full.chars().last() {
|
||||||
@ -556,7 +541,6 @@ fn prepare<'a>(
|
|||||||
items.push(Item::Frame(frame));
|
items.push(Item::Frame(frame));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Segment::Pin(idx) => items.push(Item::Pin(idx)),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
cursor = end;
|
cursor = end;
|
||||||
@ -1187,11 +1171,6 @@ fn commit(
|
|||||||
}
|
}
|
||||||
offset = before + fill;
|
offset = before + fill;
|
||||||
}
|
}
|
||||||
Item::Pin(idx) => {
|
|
||||||
let mut frame = Frame::new(Size::zero());
|
|
||||||
frame.push(Point::zero(), Element::Pin(*idx));
|
|
||||||
push(&mut offset, frame);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6,7 +6,7 @@ use memmap2::Mmap;
|
|||||||
use same_file::Handle;
|
use same_file::Handle;
|
||||||
use walkdir::WalkDir;
|
use walkdir::WalkDir;
|
||||||
|
|
||||||
use super::{FileHash, Loader};
|
use super::{Buffer, FileHash, Loader};
|
||||||
use crate::font::FontInfo;
|
use crate::font::FontInfo;
|
||||||
|
|
||||||
/// Loads fonts and files from the local file system.
|
/// 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>> {
|
fn load(&self, path: &Path) -> io::Result<Buffer> {
|
||||||
fs::read(path)
|
Ok(fs::read(path)?.into())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,7 @@ use std::collections::HashMap;
|
|||||||
use std::io;
|
use std::io;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
use super::{FileHash, Loader};
|
use super::{Buffer, FileHash, Loader};
|
||||||
use crate::font::FontInfo;
|
use crate::font::FontInfo;
|
||||||
use crate::util::PathExt;
|
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
|
self.files
|
||||||
.get(&path.normalize())
|
.get(&path.normalize())
|
||||||
.map(|cow| cow.clone().into_owned())
|
.map(|cow| cow.clone().into_owned().into())
|
||||||
.ok_or_else(|| io::ErrorKind::NotFound.into())
|
.ok_or_else(|| io::ErrorKind::NotFound.into())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -90,7 +90,7 @@ mod tests {
|
|||||||
|
|
||||||
// Test that the file can be loaded.
|
// Test that the file can be loaded.
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
loader.load(Path::new("directory/../PTSans.ttf")).unwrap(),
|
loader.load(Path::new("directory/../PTSans.ttf")).unwrap().as_slice(),
|
||||||
data
|
data
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -8,10 +8,14 @@ mod mem;
|
|||||||
pub use fs::*;
|
pub use fs::*;
|
||||||
pub use mem::*;
|
pub use mem::*;
|
||||||
|
|
||||||
|
use std::fmt::{self, Debug, Formatter};
|
||||||
use std::io;
|
use std::io;
|
||||||
|
use std::ops::Deref;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
use crate::font::FontInfo;
|
use crate::font::FontInfo;
|
||||||
|
use crate::util::Prehashed;
|
||||||
|
|
||||||
/// A hash that identifies a file.
|
/// A hash that identifies a file.
|
||||||
///
|
///
|
||||||
@ -29,7 +33,7 @@ pub trait Loader {
|
|||||||
fn resolve(&self, path: &Path) -> io::Result<FileHash>;
|
fn resolve(&self, path: &Path) -> io::Result<FileHash>;
|
||||||
|
|
||||||
/// Load a file from a path.
|
/// 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.
|
/// A loader which serves nothing.
|
||||||
@ -44,7 +48,61 @@ impl Loader for BlankLoader {
|
|||||||
Err(io::ErrorKind::NotFound.into())
|
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())
|
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(..)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -90,8 +90,6 @@ pub enum Content {
|
|||||||
/// A node that can be realized with styles, optionally with attached
|
/// A node that can be realized with styles, optionally with attached
|
||||||
/// properties.
|
/// properties.
|
||||||
Show(ShowNode, Option<Dict>),
|
Show(ShowNode, Option<Dict>),
|
||||||
/// A pin identified by index.
|
|
||||||
Pin(usize),
|
|
||||||
/// Content with attached styles.
|
/// Content with attached styles.
|
||||||
Styled(Arc<(Self, StyleMap)>),
|
Styled(Arc<(Self, StyleMap)>),
|
||||||
/// A sequence of multiple nodes.
|
/// A sequence of multiple nodes.
|
||||||
@ -281,7 +279,6 @@ impl Debug for Content {
|
|||||||
Self::Pagebreak { weak } => write!(f, "Pagebreak({weak})"),
|
Self::Pagebreak { weak } => write!(f, "Pagebreak({weak})"),
|
||||||
Self::Page(page) => page.fmt(f),
|
Self::Page(page) => page.fmt(f),
|
||||||
Self::Show(node, _) => node.fmt(f),
|
Self::Show(node, _) => node.fmt(f),
|
||||||
Self::Pin(idx) => write!(f, "Pin({idx})"),
|
|
||||||
Self::Styled(styled) => {
|
Self::Styled(styled) => {
|
||||||
let (sub, map) = styled.as_ref();
|
let (sub, map) = styled.as_ref();
|
||||||
map.fmt(f)?;
|
map.fmt(f)?;
|
||||||
@ -651,9 +648,6 @@ impl<'a> ParBuilder<'a> {
|
|||||||
Content::Inline(node) => {
|
Content::Inline(node) => {
|
||||||
self.0.supportive(ParChild::Node(node.clone()), styles);
|
self.0.supportive(ParChild::Node(node.clone()), styles);
|
||||||
}
|
}
|
||||||
&Content::Pin(idx) => {
|
|
||||||
self.0.ignorant(ParChild::Pin(idx), styles);
|
|
||||||
}
|
|
||||||
_ => return false,
|
_ => return false,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -673,7 +667,7 @@ impl<'a> ParBuilder<'a> {
|
|||||||
&& children
|
&& children
|
||||||
.items()
|
.items()
|
||||||
.find_map(|child| match child {
|
.find_map(|child| match child {
|
||||||
ParChild::Spacing(_) | ParChild::Pin(_) => None,
|
ParChild::Spacing(_) => None,
|
||||||
ParChild::Text(_) | ParChild::Quote { .. } => Some(true),
|
ParChild::Text(_) | ParChild::Quote { .. } => Some(true),
|
||||||
ParChild::Node(_) => Some(false),
|
ParChild::Node(_) => Some(false),
|
||||||
})
|
})
|
||||||
|
@ -73,7 +73,7 @@ impl SourceStore {
|
|||||||
/// root.
|
/// root.
|
||||||
///
|
///
|
||||||
/// If there already exists a source file for this path, it is
|
/// 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> {
|
pub fn load(&mut self, path: &Path) -> StrResult<SourceId> {
|
||||||
let mut try_load = || -> io::Result<SourceId> {
|
let mut try_load = || -> io::Result<SourceId> {
|
||||||
let hash = self.loader.resolve(path)?;
|
let hash = self.loader.resolve(path)?;
|
||||||
@ -82,7 +82,7 @@ impl SourceStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let data = self.loader.load(path)?;
|
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")
|
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`].
|
/// 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
|
/// 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 {
|
pub fn provide(&mut self, path: impl AsRef<Path>, src: String) -> SourceId {
|
||||||
let path = path.as_ref();
|
let path = path.as_ref();
|
||||||
let hash = self.loader.resolve(path).ok();
|
let hash = self.loader.resolve(path).ok();
|
||||||
@ -122,7 +122,7 @@ impl SourceStore {
|
|||||||
id
|
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.
|
/// This panics if no source file with this `id` exists.
|
||||||
#[track_caller]
|
#[track_caller]
|
||||||
@ -130,7 +130,7 @@ impl SourceStore {
|
|||||||
self.sources[id.0 as usize].replace(src)
|
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`
|
/// This panics if no source file with this `id` exists or if the `replace`
|
||||||
/// range is out of bounds.
|
/// range is out of bounds.
|
||||||
@ -144,7 +144,7 @@ impl SourceStore {
|
|||||||
self.sources[id.0 as usize].edit(replace, with)
|
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.
|
/// source store to a byte range.
|
||||||
///
|
///
|
||||||
/// Panics if the span does not point into this source store.
|
/// Panics if the span does not point into this source store.
|
||||||
|
Loading…
x
Reference in New Issue
Block a user