Handle SVG with text

This commit is contained in:
Laurenz 2023-04-18 19:04:46 +02:00
parent bce83d330f
commit 35302d2004
12 changed files with 268 additions and 60 deletions

24
Cargo.lock generated
View File

@ -546,6 +546,17 @@ version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]]
name = "fontdb"
version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d52186a39c335aa6f79fc0bf1c3cf854870b6ad4e50a7bb8a59b4ba1331f478a"
dependencies = [
"log",
"memmap2",
"ttf-parser 0.17.1",
]
[[package]] [[package]]
name = "form_urlencoded" name = "form_urlencoded"
version = "1.1.0" version = "1.1.0"
@ -1597,6 +1608,7 @@ dependencies = [
"comemo", "comemo",
"ecow", "ecow",
"flate2", "flate2",
"fontdb",
"if_chain", "if_chain",
"image", "image",
"indexmap", "indexmap",
@ -1807,6 +1819,12 @@ version = "1.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36"
[[package]]
name = "unicode-vo"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1d386ff53b415b7fe27b50bb44679e2cc4660272694b7b6f3326d8480823a94"
[[package]] [[package]]
name = "unicode-width" name = "unicode-width"
version = "0.1.10" version = "0.1.10"
@ -1852,14 +1870,20 @@ dependencies = [
"data-url", "data-url",
"flate2", "flate2",
"float-cmp", "float-cmp",
"fontdb",
"kurbo", "kurbo",
"log", "log",
"pico-args", "pico-args",
"rctree", "rctree",
"roxmltree", "roxmltree",
"rustybuzz",
"simplecss", "simplecss",
"siphasher", "siphasher",
"svgtypes", "svgtypes",
"ttf-parser 0.15.2",
"unicode-bidi",
"unicode-script",
"unicode-vo",
] ]
[[package]] [[package]]

View File

@ -26,6 +26,7 @@ bytemuck = "1"
comemo = "0.2.2" comemo = "0.2.2"
ecow = "0.1" ecow = "0.1"
flate2 = "1" flate2 = "1"
fontdb = "0.9"
if_chain = "1" if_chain = "1"
image = { version = "0.24", default-features = false, features = ["png", "jpeg", "gif"] } image = { version = "0.24", default-features = false, features = ["png", "jpeg", "gif"] }
log = "0.4" log = "0.4"
@ -48,7 +49,7 @@ unicode-math-class = "0.1"
unicode-segmentation = "1" unicode-segmentation = "1"
unicode-xid = "0.2" unicode-xid = "0.2"
unscanny = "0.1" unscanny = "0.1"
usvg = { version = "0.22", default-features = false } usvg = { version = "0.22", default-features = false, features = ["text"] }
xmp-writer = "0.1" xmp-writer = "0.1"
indexmap = "1.9.3" indexmap = "1.9.3"

14
assets/files/diagram.svg Normal file
View File

@ -0,0 +1,14 @@
<svg width="550" height="356" viewBox="0 0 550 356" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="550" height="356" fill="white"/>
<path d="M19.7071 18.2929C19.3166 17.9024 18.6834 17.9024 18.2929 18.2929L11.9289 24.6569C11.5384 25.0474 11.5384 25.6805 11.9289 26.0711C12.3194 26.4616 12.9526 26.4616 13.3431 26.0711L19 20.4142L24.6568 26.0711C25.0474 26.4616 25.6805 26.4616 26.0711 26.0711C26.4616 25.6805 26.4616 25.0474 26.0711 24.6569L19.7071 18.2929ZM20 336L20 19L18 19L18 336L20 336Z" fill="black"/>
<path d="M525.707 336.707C526.098 336.317 526.098 335.683 525.707 335.293L519.343 328.929C518.953 328.538 518.319 328.538 517.929 328.929C517.538 329.319 517.538 329.953 517.929 330.343L523.586 336L517.929 341.657C517.538 342.047 517.538 342.681 517.929 343.071C518.319 343.462 518.953 343.462 519.343 343.071L525.707 336.707ZM19 337H525V335H19V337Z" fill="black"/>
<text fill="black" font-family="Stupid, Inria Serif" font-size="24" letter-spacing="0em"><tspan x="34.0469" y="43.9274">Height</tspan></text>
<text fill="black" font-family="Stupid, Inria Serif" font-size="24" font-style="italic" letter-spacing="0em"><tspan x="34.0469" y="72.9274">Height</tspan></text>
<text fill="black" font-family="Stupid, Inria Serif" font-size="24" font-weight="bold" letter-spacing="0em"><tspan x="34.0469" y="101.927">Height</tspan></text>
<text fill="black" font-family="Stupid, Inria Serif" font-size="24" font-style="italic" font-weight="bold" letter-spacing="0em"><tspan x="34.0469" y="130.927">Height</tspan></text>
<text fill="black" font-size="22" font-weight="bold" letter-spacing="0em"><tspan x="99.0469" y="278.783">Without family</tspan></text>
<text fill="black" font-family="Inter" font-size="22" font-style="italic" letter-spacing="0em"><tspan x="58.0469" y="315">With non-existing family</tspan></text>
<text fill="black" font-family="Roboto" font-size="24" letter-spacing="0em" text-decoration="underline"><tspan x="466" y="310.703">Time</tspan></text>
<path d="M20 335C20 335 59.8833 265.479 102 241C143.386 216.945 162.368 211.763 210 207C270 201 321.161 208.851 374 178C398.284 163.821 431 134 431 134L518 65" stroke="#2B80FF" stroke-width="2"/>
<text transform="translate(428.859 89.5114) rotate(-38.8045)" fill="#2B80FF" xml:space="preserve" style="white-space: pre" font-family="DejaVu Sans Mono" font-size="24" font-weight="bold" letter-spacing="0em"><tspan x="0" y="22.3086">Curve</tspan></text>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Binary file not shown.

View File

@ -3,10 +3,9 @@ use std::path::Path;
use typst::image::{Image, ImageFormat, RasterFormat, VectorFormat}; use typst::image::{Image, ImageFormat, RasterFormat, VectorFormat};
use crate::{ use crate::meta::{Figurable, LocalName};
meta::{Figurable, LocalName}, use crate::prelude::*;
prelude::*, use crate::text::families;
};
/// A raster or vector graphic. /// A raster or vector graphic.
/// ///
@ -33,7 +32,7 @@ pub struct ImageElem {
let Spanned { v: path, span } = let Spanned { v: path, span } =
args.expect::<Spanned<EcoString>>("path to image file")?; args.expect::<Spanned<EcoString>>("path to image file")?;
let path: EcoString = vm.locate(&path).at(span)?.to_string_lossy().into(); let path: EcoString = vm.locate(&path).at(span)?.to_string_lossy().into();
let _ = load(vm.world(), &path).at(span)?; let _ = load(vm.world(), &path, None).at(span)?;
path path
)] )]
pub path: EcoString, pub path: EcoString,
@ -56,7 +55,9 @@ impl Layout for ImageElem {
styles: StyleChain, styles: StyleChain,
regions: Regions, regions: Regions,
) -> SourceResult<Fragment> { ) -> SourceResult<Fragment> {
let image = load(vt.world, &self.path()).unwrap(); let first = families(styles).next();
let fallback_family = first.as_ref().map(|f| f.as_str());
let image = load(vt.world, &self.path(), fallback_family).unwrap();
let sizing = Axes::new(self.width(styles), self.height(styles)); let sizing = Axes::new(self.width(styles), self.height(styles));
let region = sizing let region = sizing
.zip(regions.base()) .zip(regions.base())
@ -158,7 +159,11 @@ pub enum ImageFit {
/// Load an image from a path. /// Load an image from a path.
#[comemo::memoize] #[comemo::memoize]
fn load(world: Tracked<dyn World>, full: &str) -> StrResult<Image> { fn load(
world: Tracked<dyn World>,
full: &str,
fallback_family: Option<&str>,
) -> StrResult<Image> {
let full = Path::new(full); let full = Path::new(full);
let buffer = world.file(full)?; let buffer = world.file(full)?;
let ext = full.extension().and_then(OsStr::to_str).unwrap_or_default(); let ext = full.extension().and_then(OsStr::to_str).unwrap_or_default();
@ -169,5 +174,5 @@ fn load(world: Tracked<dyn World>, full: &str) -> StrResult<Image> {
"svg" | "svgz" => ImageFormat::Vector(VectorFormat::Svg), "svg" | "svgz" => ImageFormat::Vector(VectorFormat::Svg),
_ => return Err("unknown image format".into()), _ => return Err("unknown image format".into()),
}; };
Image::new(buffer, format) Image::with_fonts(buffer, format, world, fallback_family)
} }

View File

@ -17,7 +17,7 @@ pub fn write_images(ctx: &mut PdfContext) {
// Add the primary image. // Add the primary image.
// TODO: Error if image could not be encoded. // TODO: Error if image could not be encoded.
match image.decode().unwrap().as_ref() { match image.decoded() {
DecodedImage::Raster(dynamic, format) => { DecodedImage::Raster(dynamic, format) => {
// TODO: Error if image could not be encoded. // TODO: Error if image could not be encoded.
let (data, filter, has_color) = encode_image(*format, dynamic).unwrap(); let (data, filter, has_color) = encode_image(*format, dynamic).unwrap();

View File

@ -499,7 +499,7 @@ fn render_image(
#[comemo::memoize] #[comemo::memoize]
fn scaled_texture(image: &Image, w: u32, h: u32) -> Option<Arc<sk::Pixmap>> { fn scaled_texture(image: &Image, w: u32, h: u32) -> Option<Arc<sk::Pixmap>> {
let mut pixmap = sk::Pixmap::new(w, h)?; let mut pixmap = sk::Pixmap::new(w, h)?;
match image.decode().unwrap().as_ref() { match image.decoded() {
DecodedImage::Raster(dynamic, _) => { DecodedImage::Raster(dynamic, _) => {
let downscale = w < image.width(); let downscale = w < image.width();
let filter = let filter =

View File

@ -39,6 +39,11 @@ impl FontBook {
self.infos.push(info); self.infos.push(info);
} }
/// Get the font info for the given index.
pub fn info(&self, index: usize) -> Option<&FontInfo> {
self.infos.get(index)
}
/// An ordered iterator over all font families this book knows and details /// An ordered iterator over all font families this book knows and details
/// about the fonts that are part of them. /// about the fonts that are part of them.
pub fn families( pub fn families(
@ -53,8 +58,8 @@ impl FontBook {
}) })
} }
/// Try to find and load a font from the given `family` that matches /// Try to find a font from the given `family` that matches the given
/// the given `variant` as closely as possible. /// `variant` as closely as possible.
/// ///
/// The `family` should be all lowercase. /// The `family` should be all lowercase.
pub fn select(&self, family: &str, variant: FontVariant) -> Option<usize> { pub fn select(&self, family: &str, variant: FontVariant) -> Option<usize> {
@ -62,6 +67,16 @@ impl FontBook {
self.find_best_variant(None, variant, ids.iter().copied()) self.find_best_variant(None, variant, ids.iter().copied())
} }
/// Iterate over all variants of a family.
pub fn select_family(&self, family: &str) -> impl Iterator<Item = usize> + '_ {
self.families
.get(family)
.map(|vec| vec.as_slice())
.unwrap_or_default()
.iter()
.copied()
}
/// Try to find and load a fallback font that /// Try to find and load a fallback font that
/// - is as close as possible to the font `like` (if any) /// - is as close as possible to the font `like` (if any)
/// - is as close as possible to the given `variant` /// - is as close as possible to the given `variant`

View File

@ -1,74 +1,106 @@
//! Image handling. //! Image handling.
use std::collections::BTreeSet;
use std::fmt::{self, Debug, Formatter};
use std::hash::{Hash, Hasher};
use std::io; use std::io;
use std::sync::Arc; use std::sync::Arc;
use comemo::Tracked;
use ecow::EcoString; use ecow::EcoString;
use crate::diag::{format_xml_like_error, StrResult}; use crate::diag::{format_xml_like_error, StrResult};
use crate::util::Buffer; use crate::util::Buffer;
use crate::World;
/// A raster or vector image. /// A raster or vector image.
/// ///
/// Values of this type are cheap to clone and hash. /// Values of this type are cheap to clone and hash.
#[derive(Debug, Clone, Eq, PartialEq, Hash)] #[derive(Clone)]
pub struct Image { pub struct Image(Arc<Repr>);
/// The internal representation.
struct Repr {
/// The raw, undecoded image data. /// The raw, undecoded image data.
data: Buffer, data: Buffer,
/// The format of the encoded `buffer`. /// The format of the encoded `buffer`.
format: ImageFormat, format: ImageFormat,
/// The width in pixels. /// The decoded image.
width: u32, decoded: DecodedImage,
/// The height in pixels.
height: u32,
} }
impl Image { impl Image {
/// Create an image from a buffer and a format. /// Create an image from a buffer and a format.
///
/// Extracts the width and height.
pub fn new(data: Buffer, format: ImageFormat) -> StrResult<Self> { pub fn new(data: Buffer, format: ImageFormat) -> StrResult<Self> {
let (width, height) = determine_size(&data, format)?; match format {
Ok(Self { data, format, width, height }) ImageFormat::Raster(format) => decode_raster(data, format),
ImageFormat::Vector(VectorFormat::Svg) => decode_svg(data),
}
}
/// Create a font-dependant image from a buffer and a format.
pub fn with_fonts(
data: Buffer,
format: ImageFormat,
world: Tracked<dyn World>,
fallback_family: Option<&str>,
) -> StrResult<Self> {
match format {
ImageFormat::Raster(format) => decode_raster(data, format),
ImageFormat::Vector(VectorFormat::Svg) => {
decode_svg_with_fonts(data, world, fallback_family)
}
}
} }
/// The raw image data. /// The raw image data.
pub fn data(&self) -> &Buffer { pub fn data(&self) -> &Buffer {
&self.data &self.0.data
} }
/// The format of the image. /// The format of the image.
pub fn format(&self) -> ImageFormat { pub fn format(&self) -> ImageFormat {
self.format self.0.format
}
/// The decoded version of the image.
pub fn decoded(&self) -> &DecodedImage {
&self.0.decoded
} }
/// The width of the image in pixels. /// The width of the image in pixels.
pub fn width(&self) -> u32 { pub fn width(&self) -> u32 {
self.width self.decoded().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 {
self.height self.decoded().height()
} }
}
/// Decode the image. impl Debug for Image {
#[comemo::memoize] fn fmt(&self, f: &mut Formatter) -> fmt::Result {
pub fn decode(&self) -> StrResult<Arc<DecodedImage>> { f.debug_struct("Image")
Ok(Arc::new(match self.format { .field("format", &self.format())
ImageFormat::Vector(VectorFormat::Svg) => { .field("width", &self.width())
let opts = usvg::Options::default(); .field("height", &self.height())
let tree = usvg::Tree::from_data(&self.data, &opts.to_ref()) .finish()
.map_err(format_usvg_error)?;
DecodedImage::Svg(tree)
} }
ImageFormat::Raster(format) => { }
let cursor = io::Cursor::new(&self.data);
let reader = image::io::Reader::with_format(cursor, format.into()); impl Eq for Image {}
let dynamic = reader.decode().map_err(format_image_error)?;
DecodedImage::Raster(dynamic, format) impl PartialEq for Image {
fn eq(&self, other: &Self) -> bool {
self.data() == other.data() && self.format() == other.format()
} }
})) }
impl Hash for Image {
fn hash<H: Hasher>(&self, state: &mut H) {
self.data().hash(state);
self.format().hash(state);
} }
} }
@ -131,25 +163,133 @@ pub enum DecodedImage {
Svg(usvg::Tree), Svg(usvg::Tree),
} }
/// Determine the image size in pixels. impl DecodedImage {
/// The width of the image in pixels.
pub fn width(&self) -> u32 {
match self {
Self::Raster(dynamic, _) => dynamic.width(),
Self::Svg(tree) => tree.svg_node().size.width().ceil() as u32,
}
}
/// The height of the image in pixels.
pub fn height(&self) -> u32 {
match self {
Self::Raster(dynamic, _) => dynamic.height(),
Self::Svg(tree) => tree.svg_node().size.height().ceil() as u32,
}
}
}
/// Decode a raster image.
#[comemo::memoize] #[comemo::memoize]
fn determine_size(data: &Buffer, format: ImageFormat) -> StrResult<(u32, u32)> { fn decode_raster(data: Buffer, format: RasterFormat) -> StrResult<Image> {
match format {
ImageFormat::Raster(format) => {
let cursor = io::Cursor::new(&data); let cursor = io::Cursor::new(&data);
let reader = image::io::Reader::with_format(cursor, format.into()); let reader = image::io::Reader::with_format(cursor, format.into());
Ok(reader.into_dimensions().map_err(format_image_error)?) let dynamic = reader.decode().map_err(format_image_error)?;
} Ok(Image(Arc::new(Repr {
ImageFormat::Vector(VectorFormat::Svg) => { data,
let opts = usvg::Options::default(); format: ImageFormat::Raster(format),
let tree = decoded: DecodedImage::Raster(dynamic, format),
usvg::Tree::from_data(data, &opts.to_ref()).map_err(format_usvg_error)?; })))
}
let size = tree.svg_node().size; /// Decode an SVG image.
let width = size.width().ceil() as u32; #[comemo::memoize]
let height = size.height().ceil() as u32; fn decode_svg(data: Buffer) -> StrResult<Image> {
Ok((width, height)) let opts = usvg::Options::default();
let tree = usvg::Tree::from_data(&data, &opts.to_ref()).map_err(format_usvg_error)?;
Ok(Image(Arc::new(Repr {
data,
format: ImageFormat::Vector(VectorFormat::Svg),
decoded: DecodedImage::Svg(tree),
})))
}
/// Decode an SVG image with access to fonts.
#[comemo::memoize]
fn decode_svg_with_fonts(
data: Buffer,
world: Tracked<dyn World>,
fallback_family: Option<&str>,
) -> StrResult<Image> {
// Parse XML.
let xml = std::str::from_utf8(&data)
.map_err(|_| format_usvg_error(usvg::Error::NotAnUtf8Str))?;
let document = roxmltree::Document::parse(xml)
.map_err(|err| format_xml_like_error("svg", err))?;
// Parse SVG.
let mut opts = usvg::Options {
fontdb: load_svg_fonts(&document, world, fallback_family),
..Default::default()
};
// Recover the non-lowercased version of the family because
// usvg is case sensitive.
let book = world.book();
if let Some(family) = fallback_family
.and_then(|lowercase| book.select_family(lowercase).next())
.and_then(|index| book.info(index))
.map(|info| info.family.clone())
{
opts.font_family = family;
} }
let tree =
usvg::Tree::from_xmltree(&document, &opts.to_ref()).map_err(format_usvg_error)?;
Ok(Image(Arc::new(Repr {
data,
format: ImageFormat::Vector(VectorFormat::Svg),
decoded: DecodedImage::Svg(tree),
})))
}
/// Discover and load the fonts referenced by an SVG.
fn load_svg_fonts(
document: &roxmltree::Document,
world: Tracked<dyn World>,
fallback_family: Option<&str>,
) -> fontdb::Database {
// Find out which font families are referenced by the SVG. We simply do a
// search for `font-family` attributes. This won't help with CSS, but usvg
// 22.0 doesn't seem to support it anyway. Once we bump to the latest usvg,
// this can be replaced by a scan for text elements in the SVG:
// https://github.com/RazrFalcon/resvg/issues/555
let mut referenced = BTreeSet::<EcoString>::new();
traverse_xml(&document.root(), &mut |node| {
if let Some(list) = node.attribute("font-family") {
for family in list.split(',') {
referenced.insert(EcoString::from(family.trim()).to_lowercase());
}
}
});
// Prepare font database.
let mut fontdb = fontdb::Database::new();
for family in referenced.iter().map(|family| family.as_str()).chain(fallback_family) {
// We load all variants for the family, since we don't know which will
// be used.
for id in world.book().select_family(family) {
if let Some(font) = world.font(id) {
let source = Arc::new(font.data().clone());
fontdb.load_font_source(fontdb::Source::Binary(source));
}
}
}
fontdb
}
/// Search for all font families referenced by an SVG.
fn traverse_xml<F>(node: &roxmltree::Node, f: &mut F)
where
F: FnMut(&roxmltree::Node),
{
f(node);
for child in node.children() {
traverse_xml(&child, f);
} }
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@ -0,0 +1,9 @@
// Test SVG with text.
---
#set page(width: 250pt)
#figure(
image("/diagram.svg"),
caption: [A textful diagram],
)