Add SVG capabilities
This commit is contained in:
parent
2982020480
commit
f15ee7efb6
@ -23,13 +23,15 @@ fxhash = "0.2.1"
|
||||
image = { version = "0.23", default-features = false, features = ["png", "jpeg"] }
|
||||
itertools = "0.10"
|
||||
miniz_oxide = "0.4"
|
||||
pdf-writer = "0.4"
|
||||
pdf-writer = { git = "https://github.com/typst/pdf-writer", rev = "e1ec200" }
|
||||
rustybuzz = "0.4"
|
||||
serde = { version = "1", features = ["derive", "rc"] }
|
||||
svg2pdf = { git = "https://github.com/typst/svg2pdf", rev = "a127d6f", default-features = false, features = ["text", "png", "jpeg"] }
|
||||
ttf-parser = "0.12"
|
||||
unicode-bidi = "0.3.5"
|
||||
unicode-segmentation = "1.8"
|
||||
unicode-xid = "0.2"
|
||||
usvg = { version = "0.19", default-features = false, features = ["text"] }
|
||||
xi-unicode = "0.3"
|
||||
anyhow = { version = "1", optional = true }
|
||||
codespan-reporting = { version = "0.11", optional = true }
|
||||
@ -43,8 +45,8 @@ walkdir = { version = "2", optional = true }
|
||||
[dev-dependencies]
|
||||
filedescriptor = "0.8"
|
||||
iai = { git = "https://github.com/reknih/iai" }
|
||||
tiny-skia = "0.6"
|
||||
usvg = { version = "0.15", default-features = false }
|
||||
resvg = { version = "0.19", default-features = false, features = ["text"] }
|
||||
tiny-skia = "0.6.1"
|
||||
walkdir = "2"
|
||||
|
||||
[[bin]]
|
||||
|
@ -10,13 +10,14 @@ use pdf_writer::types::{
|
||||
ActionType, AnnotationType, CidFontType, FontFlags, SystemInfo, UnicodeCmap,
|
||||
};
|
||||
use pdf_writer::{Content, Filter, Finish, Name, PdfWriter, Rect, Ref, Str, TextStr};
|
||||
use svg2pdf::{convert_tree_into, Options};
|
||||
use ttf_parser::{name_id, GlyphId, Tag};
|
||||
|
||||
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 +91,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 +219,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);
|
||||
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();
|
||||
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 = convert_tree_into(
|
||||
&img.0,
|
||||
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 +651,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 +692,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)
|
||||
}
|
||||
|
105
src/image.rs
105
src/image.rs
@ -9,6 +9,7 @@ use std::rc::Rc;
|
||||
use image::io::Reader as ImageReader;
|
||||
use image::{DynamicImage, GenericImageView, ImageFormat};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use usvg::{Error as USvgError, Tree};
|
||||
|
||||
use crate::loading::{FileHash, Loader};
|
||||
|
||||
@ -88,14 +89,112 @@ impl ImageStore {
|
||||
}
|
||||
|
||||
/// A loaded image.
|
||||
pub struct Image {
|
||||
#[derive(Debug)]
|
||||
pub enum Image {
|
||||
Raster(RasterImage),
|
||||
Svg(Svg),
|
||||
}
|
||||
|
||||
impl Image {
|
||||
/// Parse an image from raw data. This will prioritize SVG images and then
|
||||
/// try to decode a supported raster format.
|
||||
pub fn parse(data: &[u8]) -> io::Result<Self> {
|
||||
match Svg::parse(data) {
|
||||
Ok(svg) => Ok(Self::Svg(svg)),
|
||||
Err(e) if e.kind() == io::ErrorKind::InvalidData => {
|
||||
Ok(Self::Raster(RasterImage::parse(data)?))
|
||||
}
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
}
|
||||
|
||||
/// 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(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_vector(&self) -> bool {
|
||||
match self {
|
||||
Self::Raster(_) => false,
|
||||
Self::Svg(_) => true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An SVG image, supported through the usvg crate.
|
||||
pub struct Svg(pub 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();
|
||||
let tree = Tree::from_data(data, &usvg_opts.to_ref()).map_err(|e| match e {
|
||||
USvgError::NotAnUtf8Str => {
|
||||
io::Error::new(io::ErrorKind::InvalidData, "file is not valid utf-8")
|
||||
}
|
||||
USvgError::MalformedGZip => io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
"could not extract gzipped SVG",
|
||||
),
|
||||
USvgError::ElementsLimitReached => io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
"SVG file has more than 1 million elements",
|
||||
),
|
||||
USvgError::InvalidSize => io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
"SVG width or height not greater than zero",
|
||||
),
|
||||
USvgError::ParsingFailed(error) => io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
format!("SVG parsing error: {}", error.to_string()),
|
||||
),
|
||||
})?;
|
||||
|
||||
Ok(Self(tree))
|
||||
}
|
||||
|
||||
/// 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()
|
||||
}
|
||||
}
|
||||
|
||||
/// 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.
|
||||
@ -124,7 +223,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)
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 206 KiB After Width: | Height: | Size: 182 KiB |
57
tests/res/monkey.svg
Normal file
57
tests/res/monkey.svg
Normal file
@ -0,0 +1,57 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Monkey emoji by Vincent Le Moign of the Streamline Emoji Project. Sourced
|
||||
from [Wikimedia Commons](https://commons.wikimedia.org/wiki/File:440-monkey.svg)
|
||||
on 2021-06-12 and partially minified using SVGO. Used under the Creative
|
||||
Commons Attribution 4.0 International license -->
|
||||
<!--Generator: Adobe Illustrator 21.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)-->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" x="0" y="0" viewBox="0 0 48 48" style="enable-background:new 0 0 48 48" xml:space="preserve">
|
||||
<style>
|
||||
.st3 {
|
||||
fill: none;
|
||||
stroke: #45413c;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
stroke-miterlimit: 10
|
||||
}
|
||||
.st8 {
|
||||
fill: #fff48c
|
||||
}
|
||||
.st34 {
|
||||
fill: #bf8256
|
||||
}
|
||||
.st35 {
|
||||
fill: #dea47a
|
||||
}
|
||||
.st84 {
|
||||
fill: #45413c
|
||||
}
|
||||
</style>
|
||||
<g>
|
||||
<ellipse cx="20.5" cy="44.5" rx="18" ry="1.5" style="opacity:.15;fill:#45413c"/>
|
||||
<path d="M8.7 28.8 8 27.1c-.3-.7-.9-1.1-1.6-1-.9.1-1.6.9-1.5 1.9.7 5-.6 6.6-3 9.4-.5.5-.5 1.3-.1 1.9.4.6 1 .8 1.9.6 4.5-1.6 6.9-6.6 5-11.1z" style="fill:#ffe500"/>
|
||||
<g>
|
||||
<path class="st8" d="M5 29.6c.2-.6.8-1 1.4-1 .7-.1 1.4.4 1.6 1l.7 1.8c.3.6.5 1.1.6 1.7.2-1.4 0-2.9-.6-4.3L8 27.1c-.3-.7-.9-1.1-1.6-1-.9.1-1.6.9-1.5 1.9 0 .5.1 1.1.1 1.6zM2.1 39.6c2.1-2.5 3.2-4.1 2.8-8.3-.2 2.6-1.3 4-3.1 6-.5.5-.5 1.3-.1 1.9l.4.4z"/>
|
||||
</g>
|
||||
<path class="st3" d="M8.7 28.8 8 27.1c-.3-.7-.9-1.1-1.6-1-.9.1-1.6.9-1.5 1.9.7 5-.6 6.6-3 9.4-.5.5-.5 1.3-.1 1.9.4.6 1 .8 1.9.6 4.5-1.6 6.9-6.6 5-11.1z"/>
|
||||
<path class="st3" d="m6.1 26.1-.7-1.8"/>
|
||||
<path class="st3" d="m7.1 23.6-3.5 1.5"/>
|
||||
<path class="st34" d="M30.7 19.4c-.5.9-.2 2.1.7 2.6.9.5 2.1.2 2.6-.7.9-1.7 3-2.3 4.7-1.6 3.5 1.5 5.4 5.4 4.4 9.1-1.7 6.7-7.7 11.3-14.6 11.3h-8l-.6 3.9h8.8c8.7 0 16.4-5.9 18.5-14.4 1.3-5.4-1.3-10.9-6.3-13.3-3.7-1.8-8.1-.5-10.2 3.1z"/>
|
||||
<g >
|
||||
<path class="st35" d="M43.4 27.9c0 .3-.1.6-.2.8C41.5 35.4 35.5 40 28.6 40h-8l-.4 2.2h8.4c6.9 0 12.9-4.6 14.6-11.3.3-1 .3-2 .2-3zM30.7 19.4c-.4.7-.3 1.5.1 2.1 2.1-3.4 6.5-4.7 10.1-3 3.8 1.8 6.2 5.4 6.6 9.4.4-4.8-2.1-9.4-6.6-11.6-3.7-1.8-8.1-.5-10.2 3.1z"/>
|
||||
</g>
|
||||
<path class="st3" d="M30.7 19.4c-.5.9-.2 2.1.7 2.6.9.5 2.1.2 2.6-.7.9-1.7 3-2.3 4.7-1.6 3.5 1.5 5.4 5.4 4.4 9.1-1.7 6.7-7.7 11.3-14.6 11.3h-8l-.6 3.9h8.8c8.7 0 16.4-5.9 18.5-14.4 1.3-5.4-1.3-10.9-6.3-13.3-3.7-1.8-8.1-.5-10.2 3.1z"/>
|
||||
<path class="st34" d="M31.2 30.6c-1.6-8.2-8.8-14.2-17.2-14.2v12l-4.3-.8c-1.6-.3-2.9 1.2-2.4 2.7.2.8.9 1.4 1.7 1.5l5 .8V35c-1 1-1.6 2.5-1.5 4.1l.1 1h-.8c-1.7 0-3 1.6-2.7 3.2.1.5.5.8 1 .8h14.1c4.2 0 7.6-3.4 7.6-7.6-.1-2-.3-4-.6-5.9z"/>
|
||||
<path d="M14 26.3c4.5 0 8.4-2.7 10.2-6.5-2.9-2.1-6.4-3.3-10.2-3.3v9.8z" style="fill:#915e3a"/>
|
||||
<path class="st3" d="M31.2 30.6c-1.6-8.2-8.8-14.2-17.2-14.2v12l-4.3-.8c-1.6-.3-2.9 1.2-2.4 2.7.2.8.9 1.4 1.7 1.5l5 .8V35c-1 1-1.6 2.5-1.5 4.1l.1 1h-.8c-1.7 0-3 1.6-2.7 3.2.1.5.5.8 1 .8h14.1c4.2 0 7.6-3.4 7.6-7.6-.1-2-.3-4-.6-5.9z"/>
|
||||
<path class="st34" d="M22.5 9.2h-.2C21.4 5.4 18 2.6 14 2.6S6.5 5.4 5.7 9.2h-.2c-1.8 0-3.3 1.5-3.3 3.3v.7c0 1.8 1.5 3.3 3.3 3.3h.2C6.5 20.2 9.9 23 14 23s7.4-2.8 8.3-6.6h.2c1.8 0 3.3-1.5 3.3-3.3v-.7c0-1.8-1.5-3.2-3.3-3.2z"/>
|
||||
<path class="st35" d="M22.5 9.2h-.2C21.4 5.4 18 2.6 14 2.6c-4 0-7.4 2.8-8.3 6.6h-.2c-1.8 0-3.3 1.5-3.3 3.3v.7c0 .4.1.7.2 1 .4-1.3 1.7-2.3 3.1-2.3h.2c.8-3.8 4.2-6.6 8.3-6.6 4 0 7.4 2.8 8.3 6.6h.2c1.5 0 2.7 1 3.1 2.3.1-.3.2-.7.2-1v-.7c0-1.9-1.5-3.3-3.3-3.3z"/>
|
||||
<path class="st3" d="M22.5 9.2h-.2C21.4 5.4 18 2.6 14 2.6S6.5 5.4 5.7 9.2h-.2c-1.8 0-3.3 1.5-3.3 3.3v.7c0 1.8 1.5 3.3 3.3 3.3h.2C6.5 20.2 9.9 23 14 23s7.4-2.8 8.3-6.6h.2c1.8 0 3.3-1.5 3.3-3.3v-.7c0-1.8-1.5-3.2-3.3-3.2z"/>
|
||||
<path d="M18.6 15.1c1.1 1 1.7 2.4 1.7 4 0 .5-.1.9-.2 1.3C18.5 22 16.4 23 14 23c-2.8 0-5.2-1.3-6.8-3.3 0 0 0 0 0 0 0-.2-.1-.4-.1-.6 0-1.5.7-2.9 1.7-4-1-.7-1.7-1.9-1.7-3.3 0-2.2 1.8-3.9 3.9-3.9 1 0 1.9.4 2.6 1 .7-.6 1.6-1 2.6-1 2.2 0 3.9 1.8 3.9 3.9.2 1.4-.5 2.6-1.5 3.3z" style="fill:#ffdcd1;stroke:#45413c;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10"/>
|
||||
<circle transform="matrix(.05447 -.9985 .9985 .05447 4.528 28.393)" class="st84" cx="17.3" cy="11.8" r="1.3"/>
|
||||
<circle transform="matrix(.05447 -.9985 .9985 .05447 -1.682 21.835)" class="st84" cx="10.7" cy="11.8" r="1.3"/>
|
||||
<path class="st3" d="M11.3 18.4s1.1 1.3 2.6 1.3c1.6 0 2.6-1.3 2.6-1.3"/>
|
||||
<path class="st3" d="m14 28.4 3.5.6"/>
|
||||
<path class="st3" d="m21.4 25 1.6 4.4c.8 2.2-1.1 4.4-3.3 4l-5.7-.8"/>
|
||||
<path class="st3" d="M22.9 39.3c.2-3.2-2.4-5.9-5.7-5.8-1.3.1-2.4.6-3.2 1.5"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 4.5 KiB |
22
tests/res/pattern.svg
Normal file
22
tests/res/pattern.svg
Normal file
@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Adapted from
|
||||
https://developer.mozilla.org/en-US/docs/Web/SVG/Tutorial/Patterns under
|
||||
CC0 / Public Domain Licensing -->
|
||||
<svg width="200" height="150" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<linearGradient id="Gradient1">
|
||||
<stop offset="5%" stop-color="white"/>
|
||||
<stop offset="95%" stop-color="blue"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="Gradient2" x1="0" x2="0" y1="0" y2="1">
|
||||
<stop offset="5%" stop-color="red"/>
|
||||
<stop offset="95%" stop-color="orange"/>
|
||||
</linearGradient>
|
||||
<pattern id="Pattern" x="40" y="10" width="50" height="50" patternUnits="userSpaceOnUse">
|
||||
<rect x="0" y="0" width="50" height="50" fill="skyblue"/>
|
||||
<rect x="0" y="0" width="25" height="25" fill="url(#Gradient2)"/>
|
||||
<circle cx="25" cy="25" r="20" fill="url(#Gradient1)" fill-opacity="0.5"/>
|
||||
</pattern>
|
||||
</defs>
|
||||
<rect fill="url(#Pattern)" stroke="black" width="200" height="150"/>
|
||||
</svg>
|
After Width: | Height: | Size: 948 B |
@ -18,7 +18,7 @@
|
||||
#image("../../res/rhino.png", height: 30pt)
|
||||
|
||||
// Set width and height explicitly and force stretching.
|
||||
#image("../../res/tiger.jpg", width: 100%, height: 20pt, fit: "stretch")
|
||||
#image("../../res/monkey.svg", width: 100%, height: 20pt, fit: "stretch")
|
||||
|
||||
// Make sure the bounding-box of the image is correct.
|
||||
#align(bottom + right, image("../../res/tiger.jpg", width: 40pt))
|
||||
@ -32,7 +32,7 @@
|
||||
gutter: 3pt,
|
||||
image("../../res/tiger.jpg", width: 100%, height: 100%, fit: "contain"),
|
||||
image("../../res/tiger.jpg", width: 100%, height: 100%, fit: "cover"),
|
||||
image("../../res/tiger.jpg", width: 100%, height: 100%, fit: "stretch"),
|
||||
image("../../res/monkey.svg", width: 100%, height: 100%, fit: "stretch"),
|
||||
)
|
||||
|
||||
---
|
||||
@ -46,6 +46,10 @@ Stuff
|
||||
// Test baseline.
|
||||
A #image("../../res/tiger.jpg", height: 1cm, width: 80%) B
|
||||
|
||||
---
|
||||
// Test advanced SVG features.
|
||||
#image("../../res/pattern.svg")
|
||||
|
||||
---
|
||||
// Error: 8-29 file not found
|
||||
#image("path/does/not/exist")
|
||||
|
@ -8,6 +8,7 @@ use filedescriptor::{FileDescriptor, StdioDescriptor::*};
|
||||
use image::{GenericImageView, Rgba};
|
||||
use tiny_skia as sk;
|
||||
use ttf_parser::{GlyphId, OutlineBuilder};
|
||||
use usvg::FitTo;
|
||||
use walkdir::WalkDir;
|
||||
|
||||
use typst::diag::Error;
|
||||
@ -17,7 +18,7 @@ use typst::frame::{Element, Frame, Geometry, Group, Shape, Stroke, Text};
|
||||
use typst::geom::{
|
||||
self, Color, Length, Paint, PathElement, RgbaColor, Sides, Size, Transform,
|
||||
};
|
||||
use typst::image::Image;
|
||||
use typst::image::{Image, RasterImage, Svg};
|
||||
use typst::layout::layout;
|
||||
#[cfg(feature = "layout-cache")]
|
||||
use typst::library::DocumentNode;
|
||||
@ -513,7 +514,9 @@ fn draw_text(
|
||||
let viewbox = format!("viewBox=\"0 0 {0} {0}\" xmlns", units_per_em);
|
||||
svg.replace("xmlns", &viewbox)
|
||||
})
|
||||
.and_then(|s| usvg::Tree::from_str(&s, &usvg::Options::default()).ok())
|
||||
.and_then(|s| {
|
||||
usvg::Tree::from_str(&s, &usvg::Options::default().to_ref()).ok()
|
||||
})
|
||||
{
|
||||
for child in tree.root().children() {
|
||||
if let usvg::NodeKind::Path(node) = &*child.borrow() {
|
||||
@ -535,13 +538,13 @@ fn draw_text(
|
||||
// TODO: Vertical alignment isn't quite right for Apple Color Emoji,
|
||||
// and maybe also for Noto Color Emoji. And: Is the size calculation
|
||||
// correct?
|
||||
let img = Image::parse(&raster.data).unwrap();
|
||||
let img = RasterImage::parse(&raster.data).unwrap();
|
||||
let h = text.size;
|
||||
let w = (img.width() as f64 / img.height() as f64) * h;
|
||||
let dx = (raster.x as f32) / (img.width() as f32) * size;
|
||||
let dy = (raster.y as f32) / (img.height() as f32) * size;
|
||||
let ts = ts.pre_translate(dx, -size - dy);
|
||||
draw_image(canvas, ts, mask, &img, Size::new(w, h));
|
||||
draw_image(canvas, ts, mask, &Image::Raster(img), Size::new(w, h));
|
||||
} else {
|
||||
// Otherwise, draw normal outline.
|
||||
let mut builder = WrappedPathBuilder(sk::PathBuilder::new());
|
||||
@ -608,16 +611,34 @@ fn draw_image(
|
||||
img: &Image,
|
||||
size: Size,
|
||||
) {
|
||||
let mut pixmap = sk::Pixmap::new(img.buf.width(), img.buf.height()).unwrap();
|
||||
for ((_, _, src), dest) in img.buf.pixels().zip(pixmap.pixels_mut()) {
|
||||
let Rgba([r, g, b, a]) = src;
|
||||
*dest = sk::ColorU8::from_rgba(r, g, b, a).premultiply();
|
||||
}
|
||||
|
||||
let view_width = size.x.to_f32();
|
||||
let view_height = size.y.to_f32();
|
||||
let scale_x = view_width as f32 / pixmap.width() as f32;
|
||||
let scale_y = view_height as f32 / pixmap.height() as f32;
|
||||
|
||||
let pixmap = match img {
|
||||
Image::Raster(img) => {
|
||||
let w = img.buf.width();
|
||||
let h = img.buf.height();
|
||||
let mut pixmap = sk::Pixmap::new(w, h).unwrap();
|
||||
for ((_, _, src), dest) in img.buf.pixels().zip(pixmap.pixels_mut()) {
|
||||
let Rgba([r, g, b, a]) = src;
|
||||
*dest = sk::ColorU8::from_rgba(r, g, b, a).premultiply();
|
||||
}
|
||||
pixmap
|
||||
}
|
||||
Image::Svg(Svg(tree)) => {
|
||||
let size = tree.svg_node().size;
|
||||
let aspect = (size.width() / size.height()) as f32;
|
||||
let scale = ts.sx.max(ts.sy);
|
||||
let w = (scale * view_width.max(aspect * view_height)).ceil() as u32;
|
||||
let h = ((w as f32) / aspect).ceil() as u32;
|
||||
let mut pixmap = sk::Pixmap::new(w, h).unwrap();
|
||||
resvg::render(&tree, FitTo::Size(w, h), pixmap.as_mut());
|
||||
pixmap
|
||||
}
|
||||
};
|
||||
|
||||
let scale_x = view_width / pixmap.width() as f32;
|
||||
let scale_y = view_height / pixmap.height() as f32;
|
||||
|
||||
let mut paint = sk::Paint::default();
|
||||
paint.shader = sk::Pattern::new(
|
||||
@ -689,7 +710,7 @@ fn convert_usvg_fill(fill: &usvg::Fill) -> (sk::Paint<'static>, sk::FillRule) {
|
||||
let mut paint = sk::Paint::default();
|
||||
paint.anti_alias = true;
|
||||
|
||||
if let usvg::Paint::Color(usvg::Color { red, green, blue }) = fill.paint {
|
||||
if let usvg::Paint::Color(usvg::Color { red, green, blue, alpha: _ }) = fill.paint {
|
||||
paint.set_color_rgba8(red, green, blue, fill.opacity.to_u8())
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user