Add ICC profiles to images in PDF and update usvg, svg2pdf (#822)

This commit is contained in:
Martin Haug 2023-05-02 13:53:20 +02:00 committed by GitHub
parent 021694de23
commit 17cef8dcee
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
40 changed files with 311 additions and 213 deletions

264
Cargo.lock generated
View file

@ -93,12 +93,6 @@ version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6b4930d2cb77ce62f89ee5d5289b4ac049559b1c45539271f5ed4fdc7db34545"
[[package]]
name = "arrayvec"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b"
[[package]]
name = "arrayvec"
version = "0.7.2"
@ -128,6 +122,12 @@ version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8"
[[package]]
name = "base64"
version = "0.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4a4ddaa51a5bc52a6948f74c06d20aaaddb71924eab79b8c97a8c556e942d6a"
[[package]]
name = "biblatex"
version = "0.8.0"
@ -519,12 +519,9 @@ dependencies = [
[[package]]
name = "data-url"
version = "0.1.1"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a30bfce702bcfa94e906ef82421f2c0e61c076ad76030c16ee5d2e9a32fe193"
dependencies = [
"matches",
]
checksum = "8d7439c3735f405729d52c3fbbe4de140eaf938a1fe47d227c27f8254d4302a5"
[[package]]
name = "dirs"
@ -686,13 +683,14 @@ checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]]
name = "fontdb"
version = "0.9.3"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d52186a39c335aa6f79fc0bf1c3cf854870b6ad4e50a7bb8a59b4ba1331f478a"
checksum = "237ff9f0813bbfc9de836016472e0c9ae7802f174a51594607e5f4ff334cb2f5"
dependencies = [
"log",
"memmap2",
"ttf-parser 0.17.1",
"slotmap",
"ttf-parser",
]
[[package]]
@ -739,16 +737,6 @@ dependencies = [
"wasi",
]
[[package]]
name = "gif"
version = "0.11.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3edd93c6756b4dfaf2709eafcc345ba2636565295c198a9cfbf75fa5e3e00b06"
dependencies = [
"color_quant",
"weezl",
]
[[package]]
name = "gif"
version = "0.12.0"
@ -885,13 +873,19 @@ dependencies = [
"bytemuck",
"byteorder",
"color_quant",
"gif 0.12.0",
"jpeg-decoder 0.3.0",
"gif",
"jpeg-decoder",
"num-rational",
"num-traits",
"png",
]
[[package]]
name = "imagesize"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b72ad49b554c1728b1e83254a1b1565aea4161e28dabbfa171fc15fe62299caf"
[[package]]
name = "include_dir"
version = "0.7.3"
@ -1040,12 +1034,6 @@ version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6"
[[package]]
name = "jpeg-decoder"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9478aa10f73e7528198d75109c8be5cd7d15fb530238040148d5f9a22d4c5b3b"
[[package]]
name = "jpeg-decoder"
version = "0.3.0"
@ -1083,11 +1071,11 @@ dependencies = [
[[package]]
name = "kurbo"
version = "0.8.3"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a53776d271cfb873b17c618af0298445c88afc52837f3e948fa3fafd131f449"
checksum = "d676038719d1c892f91e6e85121550143c75880b42f7feff6d413a078cf91fb3"
dependencies = [
"arrayvec 0.7.2",
"arrayvec",
]
[[package]]
@ -1170,12 +1158,6 @@ dependencies = [
"cfg-if",
]
[[package]]
name = "matches"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5"
[[package]]
name = "memchr"
version = "2.5.0"
@ -1200,15 +1182,6 @@ dependencies = [
"autocfg",
]
[[package]]
name = "miniz_oxide"
version = "0.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96590ba8f175222643a85693f33d26e9c8a015f599c216509b1a6894af675d34"
dependencies = [
"adler",
]
[[package]]
name = "miniz_oxide"
version = "0.6.2"
@ -1285,7 +1258,7 @@ version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a652d9771a63711fd3c3deb670acfbe5c30a4072e664d7a3bf5a9e1056ac72c3"
dependencies = [
"arrayvec 0.7.2",
"arrayvec",
"itoa",
]
@ -1414,9 +1387,9 @@ checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd"
[[package]]
name = "pdf-writer"
version = "0.6.0"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "249f9b33a3192626f2cd9f4b0cd66c1ec32d65968d58cf4d8239977feddddead"
checksum = "63f45f7c7538e67c58cb4977e4f97bbd75fbd3990d827d28d597ec746291f644"
dependencies = [
"bitflags 1.3.2",
"itoa",
@ -1449,9 +1422,9 @@ dependencies = [
[[package]]
name = "pico-args"
version = "0.4.2"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db8bcd96cb740d03149cbad5518db9fd87126a10ab519c011893b1754134c468"
checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315"
[[package]]
name = "pin-project-lite"
@ -1465,7 +1438,7 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9eefadd393715fe315c8cdcd587f893b818a6dfe4f6f9faeb44b764c7c38fd8b"
dependencies = [
"ttf-parser 0.18.1",
"ttf-parser",
]
[[package]]
@ -1590,9 +1563,9 @@ dependencies = [
[[package]]
name = "rctree"
version = "0.4.0"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ae028b272a6e99d9f8260ceefa3caa09300a8d6c8d2b2001316474bc52122e9"
checksum = "3b42e27ef78c35d3998403c1d26f3efd9e135d3e5121b0a4845cc5cc27547f4f"
[[package]]
name = "redox_syscall"
@ -1648,15 +1621,12 @@ checksum = "a5996294f19bd3aae0453a862ad728f60e6600695733dd5df01da90c54363a3c"
[[package]]
name = "resvg"
version = "0.22.0"
version = "0.32.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e702d1e8e00a3a0717b96244cba840f34f542d8f23097c8903266c4e2975658"
checksum = "142e83d8ae8c8c639f304698a5567b229ba65caba867bf4387bbc0ae158827cf"
dependencies = [
"gif 0.11.4",
"jpeg-decoder 0.2.6",
"log",
"pico-args",
"png",
"rgb",
"svgtypes",
"tiny-skia",
@ -1694,10 +1664,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b833d8d034ea094b1ea68aa6d5c740e0d04bad9d16568d08ba6f76823a114316"
[[package]]
name = "roxmltree"
version = "0.14.1"
name = "rosvgtree"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "921904a62e410e37e215c40381b7117f830d9d89ba60ab5236170541dd25646b"
checksum = "ad747e7384940e7bf33b15ba433b7bad9f44c0c6d5287a67c2cb22cd1743d497"
dependencies = [
"log",
"roxmltree",
"simplecss",
"siphasher",
"svgtypes",
]
[[package]]
name = "roxmltree"
version = "0.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8f595a457b6b8c6cda66a48503e92ee8d19342f905948f29c383200ec9eb1d8"
dependencies = [
"xmlparser",
]
@ -1751,14 +1734,14 @@ checksum = "4f3208ce4d8448b3f3e7d168a73f5e0c43a61e32930de3bceeccedb388b6bf06"
[[package]]
name = "rustybuzz"
version = "0.5.1"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a617c811f5c9a7060fe511d35d13bf5b9f0463ce36d63ce666d05779df2b4eba"
checksum = "162bdf42e261bee271b3957691018634488084ef577dddeb6420a9684cab2a6a"
dependencies = [
"bitflags 1.3.2",
"bytemuck",
"smallvec",
"ttf-parser 0.15.2",
"ttf-parser",
"unicode-bidi-mirroring",
"unicode-ccc",
"unicode-general-category",
@ -1771,15 +1754,6 @@ version = "1.0.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041"
[[package]]
name = "safe_arch"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1ff3d6d9696af502cc3110dacce942840fb06ff4514cad92236ecc455f2ce05"
dependencies = [
"bytemuck",
]
[[package]]
name = "same-file"
version = "1.0.6"
@ -1899,6 +1873,15 @@ version = "0.3.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7bd3e3206899af3f8b12af284fafc038cc1dc2b41d1b89dd17297221c5d225de"
[[package]]
name = "slotmap"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1e08e261d0e8f5c43123b7adf3e4ca1690d655377ac93a03b2c9d3e98de1342"
dependencies = [
"version_check",
]
[[package]]
name = "smallvec"
version = "1.10.0"
@ -1948,6 +1931,15 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9091b6114800a5f2141aee1d1b9d6ca3592ac062dc5decb3764ec5895a47b4eb"
[[package]]
name = "strict-num"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9df65f20698aeed245efdde3628a6b559ea1239bbb871af1b6e3b58c413b2bd1"
dependencies = [
"float-cmp",
]
[[package]]
name = "strsim"
version = "0.10.0"
@ -1985,21 +1977,21 @@ checksum = "09eab8a83bff89ba2200bd4c59be45c7c787f988431b936099a5a266c957f2f9"
[[package]]
name = "svg2pdf"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd5736713f3850a24060c3cdd7ac9efdc0c5138779386c6c8975b46d54d2d3d5"
source = "git+https://github.com/typst/svg2pdf#fec20fb9e2ab42f76060f349688005af1d182444"
dependencies = [
"image",
"miniz_oxide 0.5.4",
"miniz_oxide 0.7.1",
"pdf-writer",
"usvg",
]
[[package]]
name = "svgtypes"
version = "0.8.2"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22975e8a2bac6a76bb54f898a6b18764633b00e780330f0b689f65afb3975564"
checksum = "ed4b0611e7f3277f68c0fa18e385d9e2d26923691379690039548f867cef02a7"
dependencies = [
"kurbo",
"siphasher",
]
@ -2112,16 +2104,28 @@ dependencies = [
[[package]]
name = "tiny-skia"
version = "0.6.6"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d049bfef0eaa2521e75d9ffb5ce86ad54480932ae19b85f78bec6f52c4d30d78"
checksum = "5b610cd8b9a29feb9029c30f1e7bff634651b6e4e925388ee6cff4c68d901a3e"
dependencies = [
"arrayref",
"arrayvec 0.5.2",
"arrayvec",
"bytemuck",
"cfg-if",
"log",
"png",
"safe_arch",
"tiny-skia-path",
]
[[package]]
name = "tiny-skia-path"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7acb0ccda1ac91084353a56d0b69b0e29c311fd809d2088b1ed2f9ae1841c47"
dependencies = [
"arrayref",
"bytemuck",
"strict-num",
]
[[package]]
@ -2260,18 +2264,6 @@ dependencies = [
"tracing-log",
]
[[package]]
name = "ttf-parser"
version = "0.15.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b3e06c9b9d80ed6b745c7159c40b311ad2916abb34a49e9be2653b90db0d8dd"
[[package]]
name = "ttf-parser"
version = "0.17.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "375812fa44dab6df41c195cd2f7fecb488f6c09fbaafb62807488cefab642bff"
[[package]]
name = "ttf-parser"
version = "0.18.1"
@ -2313,7 +2305,7 @@ dependencies = [
"svg2pdf",
"tiny-skia",
"tracing",
"ttf-parser 0.18.1",
"ttf-parser",
"typst-macros",
"unicode-math-class",
"unicode-segmentation",
@ -2393,7 +2385,7 @@ dependencies = [
"syntect",
"toml",
"tracing",
"ttf-parser 0.18.1",
"ttf-parser",
"typed-arena",
"typst",
"unicode-bidi",
@ -2425,7 +2417,7 @@ dependencies = [
"oxipng",
"rayon",
"tiny-skia",
"ttf-parser 0.18.1",
"ttf-parser",
"typst",
"typst-library",
"unscanny",
@ -2479,9 +2471,9 @@ checksum = "cc2520efa644f8268dce4dcd3050eaa7fc044fca03961e9998ac7e2e92b77cf1"
[[package]]
name = "unicode-general-category"
version = "0.4.0"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07547e3ee45e28326cc23faac56d44f58f16ab23e413db526debce3b0bfd2742"
checksum = "2281c8c1d221438e373249e065ca4989c4c36952c211ff21a0ee91c44a3869e7"
[[package]]
name = "unicode-ident"
@ -2558,7 +2550,7 @@ version = "2.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "338b31dd1314f68f3aabf3ed57ab922df95ffcd902476ca7ba3c4ce7b908c46d"
dependencies = [
"base64",
"base64 0.13.1",
"flate2",
"log",
"once_cell",
@ -2583,28 +2575,62 @@ dependencies = [
[[package]]
name = "usvg"
version = "0.22.0"
version = "0.32.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a261d60a7215fa339482047cc3dafd4e22e2bf34396aaebef2b707355bbb39c0"
checksum = "4b44e14b7678bcc5947b397991432d0c4e02a103958a0ed5e1b9b961ddd08b21"
dependencies = [
"base64 0.21.0",
"log",
"pico-args",
"usvg-parser",
"usvg-text-layout",
"usvg-tree",
"xmlwriter",
]
[[package]]
name = "usvg-parser"
version = "0.32.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90c8251d965c2882a636ffcc054340b1f13a6bce68779cb5b2084d8ffc2535be"
dependencies = [
"base64",
"data-url",
"flate2",
"float-cmp",
"imagesize",
"kurbo",
"log",
"rosvgtree",
"strict-num",
"svgtypes",
"usvg-tree",
]
[[package]]
name = "usvg-text-layout"
version = "0.32.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c4fed019d1af07bfe0f3bac13d120d7b51bc65b38cb24809cf4ed0b8b631138"
dependencies = [
"fontdb",
"kurbo",
"log",
"pico-args",
"rctree",
"roxmltree",
"rustybuzz",
"simplecss",
"siphasher",
"svgtypes",
"ttf-parser 0.15.2",
"unicode-bidi",
"unicode-script",
"unicode-vo",
"usvg-tree",
]
[[package]]
name = "usvg-tree"
version = "0.32.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7371265c467cdae0ccc3655e2e3f310c695fb9f717c0d25187bf3b333f7b5159"
dependencies = [
"kurbo",
"rctree",
"strict-num",
"svgtypes",
]
[[package]]
@ -2956,6 +2982,12 @@ version = "0.13.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4d25c75bf9ea12c4040a97f829154768bbbce366287e2dc044af160cd79a13fd"
[[package]]
name = "xmlwriter"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9"
[[package]]
name = "xmp-writer"
version = "0.1.0"

View file

@ -26,31 +26,31 @@ bytemuck = "1"
comemo = "0.2.2"
ecow = "0.1"
flate2 = "1"
fontdb = "0.9"
fontdb = "0.13"
if_chain = "1"
image = { version = "0.24", default-features = false, features = ["png", "jpeg", "gif"] }
indexmap = "1.9.3"
log = "0.4"
miniz_oxide = "0.7"
once_cell = "1"
pdf-writer = "0.6"
pdf-writer = "0.7"
pixglyph = "0.1"
regex = "1"
resvg = { version = "0.22", default-features = false }
roxmltree = "0.14"
rustybuzz = "0.5"
resvg = { version = "0.32", default-features = false }
roxmltree = "0.18"
rustybuzz = "0.7"
serde = { version = "1", features = ["derive"] }
siphasher = "0.3"
subsetter = "0.1.1"
svg2pdf = "0.4"
tiny-skia = "0.6.6"
svg2pdf = { git = "https://github.com/typst/svg2pdf" }
tiny-skia = "0.9.0"
tracing = "0.1.37"
ttf-parser = "0.18.1"
unicode-math-class = "0.1"
unicode-segmentation = "1"
unicode-xid = "0.2"
unscanny = "0.1"
usvg = { version = "0.22", default-features = false, features = ["text"] }
usvg = { version = "0.32", default-features = false, features = ["text"] }
xmp-writer = "0.1"
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]

View file

@ -22,12 +22,12 @@ csv = "1"
ecow = "0.1"
hayagriva = "0.3"
hypher = "0.1"
kurbo = "0.8"
kurbo = "0.9"
lipsum = "0.9"
log = "0.4"
once_cell = "1"
roxmltree = "0.14"
rustybuzz = "0.5"
roxmltree = "0.18"
rustybuzz = "0.7"
serde_json = "1"
serde_yaml = "0.8"
smallvec = "1.10"

View file

@ -448,7 +448,6 @@ fn convert_xml(node: roxmltree::Node) -> Value {
let tag: Str = node.tag_name().name().into();
let attrs: Dict = node
.attributes()
.iter()
.map(|attr| (attr.name().into(), attr.value().into()))
.collect();

View file

@ -273,7 +273,6 @@ pub fn format_xml_like_error(format: &str, error: roxmltree::Error) -> EcoString
roxmltree::Error::NoRootNode => {
eco_format!("failed to parse {format}: missing root node")
}
roxmltree::Error::SizeLimit => "file is too large".into(),
_ => eco_format!("failed to parse {format}"),
}
}

View file

@ -11,6 +11,7 @@ use crate::image::{DecodedImage, RasterFormat};
pub fn write_images(ctx: &mut PdfContext) {
for image in ctx.image_map.items() {
let image_ref = ctx.alloc.bump();
let icc_ref = ctx.alloc.bump();
ctx.image_refs.push(image_ref);
let width = image.width();
@ -19,7 +20,7 @@ pub fn write_images(ctx: &mut PdfContext) {
// Add the primary image.
// TODO: Error if image could not be encoded.
match image.decoded() {
DecodedImage::Raster(dynamic, format) => {
DecodedImage::Raster(dynamic, icc, format) => {
// TODO: Error if image could not be encoded.
let (data, filter, has_color) = encode_image(*format, dynamic).unwrap();
let mut image = ctx.writer.image_xobject(image_ref, &data);
@ -29,7 +30,9 @@ pub fn write_images(ctx: &mut PdfContext) {
image.bits_per_component(8);
let space = image.color_space();
if has_color {
if icc.is_some() {
space.icc_based(icc_ref);
} else if has_color {
space.device_rgb();
} else {
space.device_gray();
@ -49,6 +52,21 @@ pub fn write_images(ctx: &mut PdfContext) {
mask.height(height as i32);
mask.color_space().device_gray();
mask.bits_per_component(8);
} else {
image.finish();
}
if let Some(icc) = icc {
let compressed = deflate(&icc.0);
let mut stream = ctx.writer.icc_profile(icc_ref, &compressed);
stream.filter(Filter::FlateDecode);
if has_color {
stream.n(3);
stream.alternate().srgb();
} else {
stream.n(1);
stream.alternate().d65_gray();
}
}
}
DecodedImage::Svg(svg) => {

View file

@ -118,7 +118,7 @@ fn write_outline_item(
let index = pos.page.get() - 1;
if let Some(&height) = ctx.page_heights.get(index) {
let y = (pos.point.y - Abs::pt(10.0)).max(Abs::zero());
outline.dest_direct().page(ctx.page_refs[index]).xyz(
outline.dest().page(ctx.page_refs[index]).xyz(
pos.point.x.to_f32(),
height - y.to_f32(),
None,

View file

@ -139,7 +139,7 @@ fn write_page(ctx: &mut PdfContext, page: Page) {
annotation
.action()
.action_type(ActionType::GoTo)
.destination_direct()
.destination()
.page(ctx.page_refs[index])
.xyz(pos.point.x.to_f32(), height - y.to_f32(), None);
}
@ -499,7 +499,7 @@ fn write_image(ctx: &mut PageContext, x: f32, y: f32, image: &Image, size: Size)
if let Some(alt) = image.alt() {
let mut image_span =
ctx.content.begin_marked_content_with_properties(Name(b"Span"));
let mut image_alt = image_span.properties_direct();
let mut image_alt = image_span.properties();
image_alt.pair(Name(b"Alt"), pdf_writer::Str(alt.as_bytes()));
image_alt.finish();
image_span.finish();

View file

@ -5,9 +5,10 @@ use std::sync::Arc;
use image::imageops::FilterType;
use image::{GenericImageView, Rgba};
use resvg::FitTo;
use tiny_skia as sk;
use ttf_parser::{GlyphId, OutlineBuilder};
use usvg::{FitTo, NodeExt};
use usvg::{NodeExt, TreeParsing};
use crate::doc::{Frame, FrameItem, GroupItem, Meta, TextItem};
use crate::geom::{
@ -38,7 +39,7 @@ pub fn render(frame: &Frame, pixel_per_pt: f32, fill: Color) -> sk::Pixmap {
fn render_frame(
canvas: &mut sk::Pixmap,
ts: sk::Transform,
mask: Option<&sk::ClipMask>,
mask: Option<&sk::Mask>,
frame: &Frame,
) {
for (pos, item) in frame.items() {
@ -73,13 +74,13 @@ fn render_frame(
fn render_group(
canvas: &mut sk::Pixmap,
ts: sk::Transform,
mask: Option<&sk::ClipMask>,
mask: Option<&sk::Mask>,
group: &GroupItem,
) {
let ts = ts.pre_concat(group.transform.into());
let mut mask = mask;
let mut storage;
let storage;
if group.clips {
let size = group.frame.size();
let w = size.x.to_f32();
@ -88,21 +89,32 @@ fn render_group(
.map(sk::PathBuilder::from_rect)
.and_then(|path| path.transform(ts))
{
let result = if let Some(mask) = mask {
storage = mask.clone();
storage.intersect_path(&path, sk::FillRule::default(), false)
if let Some(mask) = mask {
let mut mask = mask.clone();
mask.intersect_path(
&path,
sk::FillRule::default(),
false,
sk::Transform::default(),
);
storage = mask;
} else {
let pxw = canvas.width();
let pxh = canvas.height();
storage = sk::ClipMask::new();
storage.set_path(pxw, pxh, &path, sk::FillRule::default(), false)
};
let Some(mut mask) = sk::Mask::new(pxw, pxh) else {
// Fails if clipping rect is empty. In that case we just
// clip everything by returning.
return;
};
// Clipping fails if clipping rect is empty. In that case we just
// clip everything by returning.
if result.is_none() {
return;
}
mask.fill_path(
&path,
sk::FillRule::default(),
false,
sk::Transform::default(),
);
storage = mask;
};
mask = Some(&storage);
}
@ -115,7 +127,7 @@ fn render_group(
fn render_text(
canvas: &mut sk::Pixmap,
ts: sk::Transform,
mask: Option<&sk::ClipMask>,
mask: Option<&sk::Mask>,
text: &TextItem,
) {
let mut x = 0.0;
@ -136,7 +148,7 @@ fn render_text(
fn render_svg_glyph(
canvas: &mut sk::Pixmap,
ts: sk::Transform,
mask: Option<&sk::ClipMask>,
mask: Option<&sk::Mask>,
text: &TextItem,
id: GlyphId,
) -> Option<()> {
@ -157,8 +169,8 @@ fn render_svg_glyph(
// Parse SVG.
let opts = usvg::Options::default();
let tree = usvg::Tree::from_xmltree(&document, &opts.to_ref()).ok()?;
let view_box = tree.svg_node().view_box.rect;
let tree = usvg::Tree::from_xmltree(&document, &opts).ok()?;
let view_box = tree.view_box.rect;
// If there's no viewbox defined, use the em square for our scale
// transformation ...
@ -182,7 +194,7 @@ fn render_svg_glyph(
// See https://github.com/RazrFalcon/resvg/issues/602 for why
// using the svg size is problematic here.
let mut bbox = usvg::Rect::new_bbox();
for node in tree.root().descendants() {
for node in tree.root.descendants() {
if let Some(rect) = node.calculate_bbox().and_then(|b| b.to_rect()) {
bbox = bbox.expand(rect);
}
@ -224,14 +236,16 @@ fn render_svg_glyph(
&sk::PixmapPaint::default(),
sk::Transform::identity(),
mask,
)
);
Some(())
}
/// Render a bitmap glyph into the canvas.
fn render_bitmap_glyph(
canvas: &mut sk::Pixmap,
ts: sk::Transform,
mask: Option<&sk::ClipMask>,
mask: Option<&sk::Mask>,
text: &TextItem,
id: GlyphId,
) -> Option<()> {
@ -255,7 +269,7 @@ fn render_bitmap_glyph(
fn render_outline_glyph(
canvas: &mut sk::Pixmap,
ts: sk::Transform,
mask: Option<&sk::ClipMask>,
mask: Option<&sk::Mask>,
text: &TextItem,
id: GlyphId,
) -> Option<()> {
@ -278,7 +292,7 @@ fn render_outline_glyph(
// system is Y-up.
let scale = text.size.to_f32() / text.font.units_per_em() as f32;
let ts = ts.pre_scale(scale, -scale);
canvas.fill_path(&path, &paint, rule, ts, mask)?;
canvas.fill_path(&path, &paint, rule, ts, mask);
return Some(());
}
@ -318,7 +332,9 @@ fn render_outline_glyph(
&sk::PixmapPaint::default(),
sk::Transform::identity(),
mask,
)
);
Some(())
} else {
let cw = canvas.width() as i32;
let ch = canvas.height() as i32;
@ -365,7 +381,7 @@ fn render_outline_glyph(
fn render_shape(
canvas: &mut sk::Pixmap,
ts: sk::Transform,
mask: Option<&sk::ClipMask>,
mask: Option<&sk::Mask>,
shape: &Shape,
) -> Option<()> {
let path = match shape.geometry {
@ -465,7 +481,7 @@ fn convert_path(path: &geom::Path) -> Option<sk::Path> {
fn render_image(
canvas: &mut sk::Pixmap,
ts: sk::Transform,
mask: Option<&sk::ClipMask>,
mask: Option<&sk::Mask>,
image: &Image,
size: Size,
) -> Option<()> {
@ -503,7 +519,7 @@ fn render_image(
fn scaled_texture(image: &Image, w: u32, h: u32) -> Option<Arc<sk::Pixmap>> {
let mut pixmap = sk::Pixmap::new(w, h)?;
match image.decoded() {
DecodedImage::Raster(dynamic, _) => {
DecodedImage::Raster(dynamic, _, _) => {
let downscale = w < image.width();
let filter =
if downscale { FilterType::Lanczos3 } else { FilterType::CatmullRom };

View file

@ -1,6 +1,6 @@
//! Image handling.
use std::collections::BTreeSet;
use std::collections::BTreeMap;
use std::fmt::{self, Debug, Formatter};
use std::hash::{Hash, Hasher};
use std::io;
@ -8,6 +8,12 @@ use std::sync::Arc;
use comemo::Tracked;
use ecow::EcoString;
use image::codecs::gif::GifDecoder;
use image::codecs::jpeg::JpegDecoder;
use image::codecs::png::PngDecoder;
use image::io::Limits;
use image::{ImageDecoder, ImageResult};
use usvg::{TreeParsing, TreeTextToPath};
use crate::diag::{format_xml_like_error, StrResult};
use crate::util::Buffer;
@ -171,8 +177,8 @@ impl From<ttf_parser::RasterImageFormat> for ImageFormat {
/// A decoded image.
pub enum DecodedImage {
/// A decoded pixel raster.
Raster(image::DynamicImage, RasterFormat),
/// A decoded pixel raster with its ICC profile.
Raster(image::DynamicImage, Option<IccProfile>, RasterFormat),
/// An decoded SVG tree.
Svg(usvg::Tree),
}
@ -181,34 +187,52 @@ 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,
Self::Raster(dynamic, _, _) => dynamic.width(),
Self::Svg(tree) => tree.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,
Self::Raster(dynamic, _, _) => dynamic.height(),
Self::Svg(tree) => tree.size.height().ceil() as u32,
}
}
}
/// Raw data for of an ICC profile.
pub struct IccProfile(pub Vec<u8>);
/// Decode a raster image.
#[comemo::memoize]
fn decode_raster(data: &Buffer, format: RasterFormat) -> StrResult<Arc<DecodedImage>> {
let cursor = io::Cursor::new(&data);
let reader = image::io::Reader::with_format(cursor, format.into());
let dynamic = reader.decode().map_err(format_image_error)?;
Ok(Arc::new(DecodedImage::Raster(dynamic, format)))
fn decode_with<'a, T: ImageDecoder<'a>>(
decoder: ImageResult<T>,
) -> ImageResult<(image::DynamicImage, Option<IccProfile>)> {
let mut decoder = decoder?;
let icc = decoder.icc_profile().map(IccProfile);
decoder.set_limits(Limits::default())?;
let dynamic = image::DynamicImage::from_decoder(decoder)?;
Ok((dynamic, icc))
}
let cursor = io::Cursor::new(data);
let (dynamic, icc) = match format {
RasterFormat::Jpg => decode_with(JpegDecoder::new(cursor)),
RasterFormat::Png => decode_with(PngDecoder::new(cursor)),
RasterFormat::Gif => decode_with(GifDecoder::new(cursor)),
}
.map_err(format_image_error)?;
Ok(Arc::new(DecodedImage::Raster(dynamic, icc, format)))
}
/// Decode an SVG image.
#[comemo::memoize]
fn decode_svg(data: &Buffer) -> StrResult<Arc<DecodedImage>> {
let opts = usvg::Options::default();
let tree = usvg::Tree::from_data(data, &opts.to_ref()).map_err(format_usvg_error)?;
let tree = usvg::Tree::from_data(data, &opts).map_err(format_usvg_error)?;
Ok(Arc::new(DecodedImage::Svg(tree)))
}
@ -219,79 +243,89 @@ fn decode_svg_with_fonts(
world: Tracked<dyn World>,
fallback_family: Option<&str>,
) -> StrResult<Arc<DecodedImage>> {
// 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()
};
let mut opts = usvg::Options::default();
// Recover the non-lowercased version of the family because
// usvg is case sensitive.
let book = world.book();
if let Some(family) = fallback_family
let fallback_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;
.map(|info| info.family.clone());
if let Some(family) = &fallback_family {
opts.font_family = family.clone();
}
let tree =
usvg::Tree::from_xmltree(&document, &opts.to_ref()).map_err(format_usvg_error)?;
let mut tree = usvg::Tree::from_data(data, &opts).map_err(format_usvg_error)?;
if tree.has_text_nodes() {
let fontdb = load_svg_fonts(&tree, world, fallback_family.as_deref());
tree.convert_text(&fontdb);
}
Ok(Arc::new(DecodedImage::Svg(tree)))
}
/// Discover and load the fonts referenced by an SVG.
fn load_svg_fonts(
document: &roxmltree::Document,
tree: &usvg::Tree,
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 referenced = BTreeMap::<EcoString, bool>::new();
let mut fontdb = fontdb::Database::new();
for family in referenced.iter().map(|family| family.as_str()).chain(fallback_family) {
let mut load = |family: &str| {
let lower = EcoString::from(family.trim()).to_lowercase();
if let Some(&success) = referenced.get(&lower) {
return success;
}
// We load all variants for the family, since we don't know which will
// be used.
for id in world.book().select_family(family) {
let mut success = false;
for id in world.book().select_family(&lower) {
if let Some(font) = world.font(id) {
let source = Arc::new(font.data().clone());
fontdb.load_font_source(fontdb::Source::Binary(source));
success = true;
}
}
referenced.insert(lower, success);
success
};
// Load fallback family.
if let Some(family) = fallback_family {
load(family);
}
// Find out which font families are referenced by the SVG.
traverse_svg(&tree.root, &mut |node| {
let usvg::NodeKind::Text(text) = &mut *node.borrow_mut() else { return };
for chunk in &mut text.chunks {
for span in &mut chunk.spans {
for family in &mut span.font.families {
if !load(family) {
let Some(fallback) = fallback_family else { continue };
*family = fallback.into();
}
}
}
}
});
fontdb
}
/// Search for all font families referenced by an SVG.
fn traverse_xml<F>(node: &roxmltree::Node, f: &mut F)
fn traverse_svg<F>(node: &usvg::Node, f: &mut F)
where
F: FnMut(&roxmltree::Node),
F: FnMut(&usvg::Node),
{
f(node);
for child in node.children() {
traverse_xml(&child, f);
traverse_svg(&child, f);
}
}

View file

@ -14,7 +14,7 @@ iai = { git = "https://github.com/reknih/iai" }
once_cell = "1"
oxipng = "8.0.0"
rayon = "1.7.0"
tiny-skia = "0.6.6"
tiny-skia = "0.9.0"
ttf-parser = "0.18.1"
unscanny = "0.1"
walkdir = "2"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8 KiB

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.1 KiB

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 141 KiB

After

Width:  |  Height:  |  Size: 142 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 115 KiB

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 101 KiB

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 90 KiB

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 79 KiB

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 176 KiB

After

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 282 KiB

After

Width:  |  Height:  |  Size: 284 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 118 KiB

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 175 KiB

After

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 172 KiB

After

Width:  |  Height:  |  Size: 172 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB