summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorLaurenz <laurmaedje@gmail.com>2021-12-07 00:10:05 +0100
committerGitHub <noreply@github.com>2021-12-07 00:10:05 +0100
commit1b67887db0c2f2cd17d60d70c83d56e9e81f4a41 (patch)
tree7930718d6dbdf92d1feaa92df97830ef3dfc4ed5 /src
parent2982020480fc3d9061e26ca24dd467f315981dff (diff)
parent7c829c5c1b67ac8e8fbe4fc4ba01468d100bfb47 (diff)
Merge pull request #49 from typst/svg
Add SVG capabilities
Diffstat (limited to 'src')
-rw-r--r--src/export/pdf.rs94
-rw-r--r--src/image.rs98
2 files changed, 145 insertions, 47 deletions
diff --git a/src/export/pdf.rs b/src/export/pdf.rs
index 8df31660..b5f4c009 100644
--- a/src/export/pdf.rs
+++ b/src/export/pdf.rs
@@ -16,7 +16,7 @@ use super::subset;
use crate::font::{find_name, FaceId, FontStore};
use crate::frame::{Element, Frame, Geometry, Group, Shape, Stroke, Text};
use crate::geom::{self, Color, Em, Length, Paint, Point, Size, Transform};
-use crate::image::{Image, ImageId, ImageStore};
+use crate::image::{Image, ImageId, ImageStore, RasterImage};
use crate::Context;
/// Export a collection of frames into a PDF file.
@@ -90,7 +90,7 @@ impl<'a> PdfExporter<'a> {
let postscript_name = find_name(ttf.names(), name_id::POST_SCRIPT_NAME)
.unwrap_or_else(|| "unknown".to_string());
- let base_font = format!("ABCDEF+{}", postscript_name);
+ let base_font = format_eco!("ABCDEF+{}", postscript_name);
let base_font = Name(base_font.as_bytes());
let cmap_name = Name(b"Custom");
let system_info = SystemInfo {
@@ -218,44 +218,58 @@ impl<'a> PdfExporter<'a> {
let height = img.height();
// Add the primary image.
- if let Ok((data, filter, has_color)) = encode_image(img) {
- let mut image = self.writer.image_xobject(image_ref, &data);
- image.filter(filter);
- image.width(width as i32);
- image.height(height as i32);
- image.bits_per_component(8);
-
- let space = image.color_space();
- if has_color {
- space.device_rgb();
- } else {
- space.device_gray();
+ match img {
+ Image::Raster(img) => {
+ if let Ok((data, filter, has_color)) = encode_image(img) {
+ let mut image = self.writer.image_xobject(image_ref, &data);
+ image.filter(filter);
+ image.width(width as i32);
+ image.height(height as i32);
+ image.bits_per_component(8);
+
+ let space = image.color_space();
+ if has_color {
+ space.device_rgb();
+ } else {
+ space.device_gray();
+ }
+
+ // 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);
+ let mask_ref = self.alloc.bump();
+ image.s_mask(mask_ref);
+ image.finish();
+
+ let mut mask =
+ self.writer.image_xobject(mask_ref, &alpha_data);
+ mask.filter(alpha_filter);
+ mask.width(width as i32);
+ mask.height(height as i32);
+ mask.color_space().device_gray();
+ mask.bits_per_component(8);
+ }
+ } else {
+ // TODO: Warn that image could not be encoded.
+ self.writer
+ .image_xobject(image_ref, &[])
+ .width(0)
+ .height(0)
+ .bits_per_component(1)
+ .color_space()
+ .device_gray();
+ }
}
-
- // 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);
- let mask_ref = self.alloc.bump();
- image.s_mask(mask_ref);
- image.finish();
-
- let mut mask = self.writer.image_xobject(mask_ref, &alpha_data);
- mask.filter(alpha_filter);
- mask.width(width as i32);
- mask.height(height as i32);
- mask.color_space().device_gray();
- mask.bits_per_component(8);
+ Image::Svg(img) => {
+ let next_ref = svg2pdf::convert_tree_into(
+ &img.0,
+ svg2pdf::Options::default(),
+ &mut self.writer,
+ image_ref,
+ );
+ self.alloc = next_ref;
}
- } else {
- // TODO: Warn that image could not be encoded.
- self.writer
- .image_xobject(image_ref, &[])
- .width(0)
- .height(0)
- .bits_per_component(1)
- .color_space()
- .device_gray();
}
}
}
@@ -636,7 +650,7 @@ impl<'a> PageExporter<'a> {
/// whether the image has color.
///
/// Skips the alpha channel as that's encoded separately.
-fn encode_image(img: &Image) -> ImageResult<(Vec<u8>, Filter, bool)> {
+fn encode_image(img: &RasterImage) -> ImageResult<(Vec<u8>, Filter, bool)> {
Ok(match (img.format, &img.buf) {
// 8-bit gray JPEG.
(ImageFormat::Jpeg, DynamicImage::ImageLuma8(_)) => {
@@ -677,7 +691,7 @@ fn encode_image(img: &Image) -> ImageResult<(Vec<u8>, Filter, bool)> {
}
/// Encode an image's alpha channel if present.
-fn encode_alpha(img: &Image) -> (Vec<u8>, Filter) {
+fn encode_alpha(img: &RasterImage) -> (Vec<u8>, Filter) {
let pixels: Vec<_> = img.buf.pixels().map(|(_, _, Rgba([_, _, _, a]))| a).collect();
(deflate(&pixels), Filter::FlateDecode)
}
diff --git a/src/image.rs b/src/image.rs
index 512b24b1..a7b62503 100644
--- a/src/image.rs
+++ b/src/image.rs
@@ -1,6 +1,7 @@
//! 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;
@@ -65,7 +66,8 @@ impl ImageStore {
Entry::Occupied(entry) => entry.into_mut(),
Entry::Vacant(entry) => {
let buffer = self.loader.load(path)?;
- let image = Image::parse(&buffer)?;
+ let ext = path.extension().and_then(OsStr::to_str).unwrap_or_default();
+ let image = Image::parse(&buffer, &ext)?;
let id = ImageId(self.images.len() as u32);
if let Some(callback) = &self.on_load {
callback(id, &image);
@@ -88,23 +90,71 @@ impl ImageStore {
}
/// A loaded image.
-pub struct Image {
+#[derive(Debug)]
+pub enum Image {
+ /// A pixel raster format, like PNG or JPEG.
+ Raster(RasterImage),
+ /// An SVG vector graphic.
+ Svg(Svg),
+}
+
+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") => return Err(err),
+ Err(_) => {}
+ }
+
+ Err(io::Error::new(
+ io::ErrorKind::InvalidData,
+ "unknown image 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(),
+ }
+ }
+
+ /// The height of the image in pixels.
+ pub fn height(&self) -> u32 {
+ match self {
+ Self::Raster(image) => image.height(),
+ Self::Svg(image) => image.height(),
+ }
+ }
+}
+
+/// 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 Image {
+impl RasterImage {
/// Parse an image from raw data in a supported format (PNG or JPEG).
///
/// 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::new(io::ErrorKind::InvalidData, "unknown image format")
- })?;
+ let format = reader
+ .format()
+ .ok_or_else(|| io::Error::from(io::ErrorKind::InvalidData))?;
let buf = reader
.decode()
@@ -124,7 +174,7 @@ impl Image {
}
}
-impl Debug for Image {
+impl Debug for RasterImage {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
f.debug_struct("Image")
.field("format", &self.format)
@@ -134,3 +184,37 @@ impl Debug for Image {
.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()
+ }
+}