From edbf4ac1e4634670b23f60807d1c1e4f4cbd72b1 Mon Sep 17 00:00:00 2001 From: Wez Furlong Date: Tue, 15 Aug 2023 05:46:19 -0700 Subject: [PATCH] add experiemental svg font support The vertical alignment is wonky, and some glyphs have the wrong aspect and are missig colors. eg: the watermelon glyph in Noto Color Emoji (U+1f349). Turn this off by default, and skip loading fonts that have svg by default. --- Cargo.lock | 163 +++++++++++++++- config/src/config.rs | 3 + config/src/font.rs | 10 + deps/freetype/bindings.h | 1 + deps/freetype/freetype2 | 2 +- deps/freetype/regenerate.sh | 6 +- deps/freetype/src/lib.rs | 43 ++++- docs/changelog.md | 1 + wezterm-font/Cargo.toml | 1 + wezterm-font/src/ftwrap.rs | 12 ++ wezterm-font/src/parser.rs | 11 ++ wezterm-font/src/rasterizer/freetype.rs | 236 +++++++++++++++++++++++- 12 files changed, 478 insertions(+), 11 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 569f1690c66..c3b557e3080 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1242,6 +1242,12 @@ version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308" +[[package]] +name = "data-url" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d7439c3735f405729d52c3fbbe4de140eaf938a1fe47d227c27f8254d4302a5" + [[package]] name = "deltae" version = "0.3.2" @@ -2451,6 +2457,12 @@ dependencies = [ "tiff", ] +[[package]] +name = "imagesize" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "029d73f573d8e8d63e6d5020011d3255b28c3ba85d6cf870a07184ed23de9284" + [[package]] name = "indexmap" version = "1.9.3" @@ -2654,6 +2666,15 @@ dependencies = [ "libc", ] +[[package]] +name = "kurbo" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd85a5776cd9500c2e2059c8c76c3b01528566b7fcbaf8098b55a33fc298849b" +dependencies = [ + "arrayvec", +] + [[package]] name = "lab" version = "0.11.0" @@ -3782,6 +3803,12 @@ dependencies = [ "siphasher", ] +[[package]] +name = "pico-args" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315" + [[package]] name = "pin-project" version = "1.1.3" @@ -4174,6 +4201,12 @@ dependencies = [ "yasna", ] +[[package]] +name = "rctree" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b42e27ef78c35d3998403c1d26f3efd9e135d3e5121b0a4845cc5cc27547f4f" + [[package]] name = "redox_syscall" version = "0.2.16" @@ -4296,6 +4329,19 @@ dependencies = [ "rgb", ] +[[package]] +name = "resvg" +version = "0.35.0" +source = "git+https://github.com/wez/resvg.git?rev=d692908d0121c496808eebc83c4519fca4db5331#d692908d0121c496808eebc83c4519fca4db5331" +dependencies = [ + "log", + "pico-args", + "rgb", + "svgtypes", + "tiny-skia 0.10.0", + "usvg", +] + [[package]] name = "rgb" version = "0.8.36" @@ -4326,6 +4372,15 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3582f63211428f83597b51b2ddb88e2a91a9d52d12831f9d08f5e624e8977422" +[[package]] +name = "roxmltree" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8f595a457b6b8c6cda66a48503e92ee8d19342f905948f29c383200ec9eb1d8" +dependencies = [ + "xmlparser", +] + [[package]] name = "rstest" version = "0.18.2" @@ -4744,6 +4799,15 @@ version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" +[[package]] +name = "simplecss" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a11be7c62927d9427e9f40f3444d5499d868648e2edbc4e2116de69e7ec0e89d" +dependencies = [ + "log", +] + [[package]] name = "siphasher" version = "0.3.10" @@ -4967,6 +5031,9 @@ name = "strict-num" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" +dependencies = [ + "float-cmp", +] [[package]] name = "strip-ansi-escapes" @@ -4988,6 +5055,16 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8fb1df15f412ee2e9dfc1c504260fa695c1c3f10fe9f4a6ee2d2184d7d6450e2" +[[package]] +name = "svgtypes" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed4b0611e7f3277f68c0fa18e385d9e2d26923691379690039548f867cef02a7" +dependencies = [ + "kurbo", + "siphasher", +] + [[package]] name = "syn" version = "1.0.109" @@ -5323,6 +5400,21 @@ dependencies = [ "time-core", ] +[[package]] +name = "tiny-skia" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7db11798945fa5c3e5490c794ccca7c6de86d3afdd54b4eb324109939c6f37bc" +dependencies = [ + "arrayref", + "arrayvec", + "bytemuck", + "cfg-if", + "log", + "png", + "tiny-skia-path 0.10.0", +] + [[package]] name = "tiny-skia" version = "0.11.1" @@ -5335,7 +5427,18 @@ dependencies = [ "cfg-if", "log", "png", - "tiny-skia-path", + "tiny-skia-path 0.11.1", +] + +[[package]] +name = "tiny-skia-path" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f60aa35c89ac2687ace1a2556eaaea68e8c0d47408a2e3e7f5c98a489e7281c" +dependencies = [ + "arrayref", + "bytemuck", + "strict-num", ] [[package]] @@ -5630,6 +5733,47 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "usvg" +version = "0.35.0" +source = "git+https://github.com/wez/resvg.git?rev=d692908d0121c496808eebc83c4519fca4db5331#d692908d0121c496808eebc83c4519fca4db5331" +dependencies = [ + "base64 0.21.2", + "log", + "pico-args", + "usvg-parser", + "usvg-tree", + "xmlwriter", +] + +[[package]] +name = "usvg-parser" +version = "0.35.0" +source = "git+https://github.com/wez/resvg.git?rev=d692908d0121c496808eebc83c4519fca4db5331#d692908d0121c496808eebc83c4519fca4db5331" +dependencies = [ + "data-url", + "flate2", + "imagesize", + "kurbo", + "log", + "roxmltree", + "simplecss", + "siphasher", + "svgtypes", + "usvg-tree", +] + +[[package]] +name = "usvg-tree" +version = "0.35.0" +source = "git+https://github.com/wez/resvg.git?rev=d692908d0121c496808eebc83c4519fca4db5331#d692908d0121c496808eebc83c4519fca4db5331" +dependencies = [ + "rctree", + "strict-num", + "svgtypes", + "tiny-skia-path 0.10.0", +] + [[package]] name = "utf8parse" version = "0.2.1" @@ -6061,6 +6205,7 @@ dependencies = [ "objc", "ordered-float", "rangeset", + "resvg", "termwiz", "thiserror", "walkdir", @@ -6134,7 +6279,7 @@ dependencies = [ "termwiz-funcs", "textwrap 0.16.0", "thiserror", - "tiny-skia", + "tiny-skia 0.11.1", "uds_windows", "umask", "unicode-normalization", @@ -6529,7 +6674,7 @@ dependencies = [ "shlex", "smithay-client-toolkit", "thiserror", - "tiny-skia", + "tiny-skia 0.11.1", "url", "wayland-client", "wayland-egl", @@ -6859,6 +7004,18 @@ version = "0.8.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47430998a7b5d499ccee752b41567bc3afc57e1327dc855b1a2aa44ce29b5fa1" +[[package]] +name = "xmlparser" +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 = "yaml-rust" version = "0.4.5" diff --git a/config/src/config.rs b/config/src/config.rs index ff7d53adc00..a9bb63aa3c3 100644 --- a/config/src/config.rs +++ b/config/src/config.rs @@ -718,6 +718,9 @@ pub struct Config { #[dynamic(default)] pub experimental_pixel_positioning: bool, + #[dynamic(default)] + pub experimental_svg_fonts: bool, + #[dynamic(default)] pub bidi_enabled: bool, diff --git a/config/src/font.rs b/config/src/font.rs index 21c136f0337..497d96fb275 100644 --- a/config/src/font.rs +++ b/config/src/font.rs @@ -266,6 +266,8 @@ bitflags! { const MONOCHROME = 4096; /// Disable auto-hinter. const NO_AUTOHINT = 32768; + const NO_SVG = 16777216; + const SVG_ONLY = 8388608; } } @@ -293,6 +295,12 @@ impl ToString for FreeTypeLoadFlags { if self.contains(Self::NO_BITMAP) { s.push("NO_BITMAP"); } + if self.contains(Self::NO_SVG) { + s.push("NO_SVG"); + } + if self.contains(Self::SVG_ONLY) { + s.push("SVG_ONLY"); + } if self.contains(Self::FORCE_AUTOHINT) { s.push("FORCE_AUTOHINT"); } @@ -317,6 +325,8 @@ impl TryFrom for FreeTypeLoadFlags { "DEFAULT" => flags |= Self::DEFAULT, "NO_HINTING" => flags |= Self::NO_HINTING, "NO_BITMAP" => flags |= Self::NO_BITMAP, + "NO_SVG" => flags |= Self::NO_SVG, + "SVG_ONLY" => flags |= Self::SVG_ONLY, "FORCE_AUTOHINT" => flags |= Self::FORCE_AUTOHINT, "MONOCHROME" => flags |= Self::MONOCHROME, "NO_AUTOHINT" => flags |= Self::NO_AUTOHINT, diff --git a/deps/freetype/bindings.h b/deps/freetype/bindings.h index 026682b2822..d11b11aa16b 100644 --- a/deps/freetype/bindings.h +++ b/deps/freetype/bindings.h @@ -8,4 +8,5 @@ #include #include #include +#include #include diff --git a/deps/freetype/freetype2 b/deps/freetype/freetype2 index de8b92dd7ec..e4586d960f3 160000 --- a/deps/freetype/freetype2 +++ b/deps/freetype/freetype2 @@ -1 +1 @@ -Subproject commit de8b92dd7ec634e9e2b25ef534c54a3537555c11 +Subproject commit e4586d960f339cf75e2e0b34aee30a0ed8353c0d diff --git a/deps/freetype/regenerate.sh b/deps/freetype/regenerate.sh index a199a1debce..b0ffe2a0341 100755 --- a/deps/freetype/regenerate.sh +++ b/deps/freetype/regenerate.sh @@ -17,7 +17,7 @@ bindgen bindings.h -o src/lib.rs \ --raw-line "pub type FT_UInt64 = u64;" \ --default-enum-style rust \ --generate=functions,types,vars \ - --allowlist-function="FT_.*" \ - --allowlist-type="[FT]T_.*" \ - --allowlist-var="[FT]T_.*" \ + --allowlist-function="(SVG|FT)_.*" \ + --allowlist-type="(SVG|[FT]T)_.*" \ + --allowlist-var="(SVG|[FT]T)_.*" \ -- -Ifreetype2/include diff --git a/deps/freetype/src/lib.rs b/deps/freetype/src/lib.rs index db48947c299..638653dabe9 100644 --- a/deps/freetype/src/lib.rs +++ b/deps/freetype/src/lib.rs @@ -1,4 +1,4 @@ -/* automatically generated by rust-bindgen 0.65.1 */ +/* automatically generated by rust-bindgen 0.66.1 */ #![allow(non_snake_case)] #![allow(non_camel_case_types)] @@ -14,7 +14,6 @@ pub type FT_UInt64 = u64; pub const FT_RENDER_POOL_SIZE: u32 = 16384; pub const FT_MAX_MODULES: u32 = 32; -pub const TT_CONFIG_OPTION_SUBPIXEL_HINTING: u32 = 2; pub const TT_CONFIG_OPTION_MAX_RUNNABLE_OPCODES: u32 = 1000000; pub const FT_OUTLINE_NONE: u32 = 0; pub const FT_OUTLINE_OWNER: u32 = 1; @@ -89,6 +88,7 @@ pub const FT_LOAD_NO_AUTOHINT: u32 = 32768; pub const FT_LOAD_COLOR: u32 = 1048576; pub const FT_LOAD_COMPUTE_METRICS: u32 = 2097152; pub const FT_LOAD_BITMAP_METRICS_ONLY: u32 = 4194304; +pub const FT_LOAD_NO_SVG: u32 = 16777216; pub const FT_LOAD_ADVANCE_ONLY: u32 = 256; pub const FT_LOAD_SVG_ONLY: u32 = 8388608; pub const FT_SUBGLYPH_FLAG_ARGS_ARE_WORDS: u32 = 1; @@ -2420,9 +2420,15 @@ extern "C" { extern "C" { pub fn FT_Set_Named_Instance(face: FT_Face, instance_index: FT_UInt) -> FT_Error; } +extern "C" { + pub fn FT_Get_Default_Named_Instance(face: FT_Face, instance_index: *mut FT_UInt) -> FT_Error; +} extern "C" { pub fn FT_GlyphSlot_Embolden(slot: FT_GlyphSlot); } +extern "C" { + pub fn FT_GlyphSlot_AdjustWeight(slot: FT_GlyphSlot, xdelta: FT_Fixed, ydelta: FT_Fixed); +} extern "C" { pub fn FT_GlyphSlot_Oblique(slot: FT_GlyphSlot); } @@ -2563,3 +2569,36 @@ extern "C" { alangTag: *mut FT_SfntLangTag, ) -> FT_Error; } +pub type SVG_Lib_Init_Func = + ::std::option::Option FT_Error>; +pub type SVG_Lib_Free_Func = + ::std::option::Option; +pub type SVG_Lib_Render_Func = ::std::option::Option< + unsafe extern "C" fn(slot: FT_GlyphSlot, data_pointer: *mut FT_Pointer) -> FT_Error, +>; +pub type SVG_Lib_Preset_Slot_Func = ::std::option::Option< + unsafe extern "C" fn(slot: FT_GlyphSlot, cache: FT_Bool, state: *mut FT_Pointer) -> FT_Error, +>; +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct SVG_RendererHooks_ { + pub init_svg: SVG_Lib_Init_Func, + pub free_svg: SVG_Lib_Free_Func, + pub render_svg: SVG_Lib_Render_Func, + pub preset_slot: SVG_Lib_Preset_Slot_Func, +} +pub type SVG_RendererHooks = SVG_RendererHooks_; +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct FT_SVG_DocumentRec_ { + pub svg_document: *mut FT_Byte, + pub svg_document_length: FT_ULong, + pub metrics: FT_Size_Metrics, + pub units_per_EM: FT_UShort, + pub start_glyph_id: FT_UShort, + pub end_glyph_id: FT_UShort, + pub transform: FT_Matrix, + pub delta: FT_Vector, +} +pub type FT_SVG_DocumentRec = FT_SVG_DocumentRec_; +pub type FT_SVG_Document = *mut FT_SVG_DocumentRec_; diff --git a/docs/changelog.md b/docs/changelog.md index a4475b47ed3..6ad747c08b3 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -74,6 +74,7 @@ As features stabilize some brief notes about them will accumulate here. #### Updated * Bundled harfbuzz to 8.1.1 +* Bundled freetype to 2.13.1 ### 20230712-072601-f4abf8fd diff --git a/wezterm-font/Cargo.toml b/wezterm-font/Cargo.toml index 9a0ff371fd8..1ec127ad8f9 100644 --- a/wezterm-font/Cargo.toml +++ b/wezterm-font/Cargo.toml @@ -28,6 +28,7 @@ memmap2 = "0.2" metrics = { version="0.17", features=["std"]} ordered-float = "3.0" rangeset = { path = "../rangeset" } +resvg = {version="0.35", default-features=false, git="https://github.com/wez/resvg.git", rev="d692908d0121c496808eebc83c4519fca4db5331"} termwiz = { path = "../termwiz" } thiserror = "1.0" walkdir = "2" diff --git a/wezterm-font/src/ftwrap.rs b/wezterm-font/src/ftwrap.rs index d2b38808041..b3e5afe9d59 100644 --- a/wezterm-font/src/ftwrap.rs +++ b/wezterm-font/src/ftwrap.rs @@ -876,6 +876,18 @@ impl Library { } } + // Hook up our svg rendering + if config::configuration().experimental_svg_fonts { + unsafe { + FT_Property_Set( + lib.lib, + b"ot-svg\0" as *const u8 as *const FT_String, + b"svg-hooks\0" as *const u8 as *const FT_String, + &crate::rasterizer::freetype::SVG_HOOKS as *const SVG_RendererHooks as *const _, + ); + } + } + // Due to patent concerns, the freetype library disables the LCD // filtering feature by default, and since we always build our // own copy of freetype, it is likewise disabled by default for diff --git a/wezterm-font/src/parser.rs b/wezterm-font/src/parser.rs index 7efba2fd8e9..8984952230e 100644 --- a/wezterm-font/src/parser.rs +++ b/wezterm-font/src/parser.rs @@ -366,6 +366,17 @@ impl ParsedFont { let stretch = FontStretch::from_opentype_stretch(width); let cap_height = face.cap_height(); let pixel_sizes = face.pixel_sizes(); + + let has_svg = unsafe { + (((*face.face).face_flags as u32) & (crate::ftwrap::FT_FACE_FLAG_SVG as u32)) != 0 + }; + + if has_svg { + if !config::configuration().experimental_svg_fonts { + anyhow::bail!("skip svg font"); + } + } + let has_color = unsafe { (((*face.face).face_flags as u32) & (crate::ftwrap::FT_FACE_FLAG_COLOR as u32)) != 0 }; diff --git a/wezterm-font/src/rasterizer/freetype.rs b/wezterm-font/src/rasterizer/freetype.rs index 57c0aa2315a..f41e517ef55 100644 --- a/wezterm-font/src/rasterizer/freetype.rs +++ b/wezterm-font/src/rasterizer/freetype.rs @@ -2,13 +2,25 @@ use crate::parser::ParsedFont; use crate::rasterizer::{FontRasterizer, FAKE_ITALIC_SKEW}; use crate::units::*; use crate::{ftwrap, RasterizedGlyph}; -use ::freetype::{FT_GlyphSlotRec_, FT_Matrix}; -use anyhow::bail; +use ::freetype::{ + FT_Bool, FT_Err_Invalid_SVG_Document, FT_Err_Ok, FT_Error, FT_GlyphSlot, FT_GlyphSlotRec_, + FT_Matrix, FT_Pointer, FT_Pos, FT_SVG_Document, FT_SVG_DocumentRec_, SVG_RendererHooks, +}; +use anyhow::{bail, Context}; use config::{DisplayPixelGeometry, FreeTypeLoadFlags, FreeTypeLoadTarget}; +use lfucache::LfuCache; +use resvg::tiny_skia::{Pixmap, PixmapMut, PixmapPaint, Transform}; use std::cell::RefCell; use std::{mem, slice}; use wezterm_color_types::linear_u8_to_srgb8; +pub static SVG_HOOKS: SVG_RendererHooks = SVG_RendererHooks { + init_svg: Some(init_svg_library), + free_svg: Some(free_svg_library), + render_svg: Some(svg_render), + preset_slot: Some(svg_preset_slot), +}; + pub struct FreeTypeRasterizer { has_color: bool, face: RefCell, @@ -327,3 +339,223 @@ impl FreeTypeRasterizer { }) } } + +struct SvgState { + pixmap: Pixmap, + doc_cache: LfuCache, +} + +impl SvgState { + fn new() -> Self { + Self { + pixmap: Pixmap::new(1, 1).expect("to allocate empty pixmap"), + doc_cache: LfuCache::new( + "font.svg.parser.hit", + "font.svg.parser.miss", + |_| 64, + &config::configuration(), + ), + } + } +} + +// The SVG rendering implementation was produced by roughly following +// +// and adapting for resvg + +unsafe extern "C" fn init_svg_library(data_pointer: *mut FT_Pointer) -> FT_Error { + let state = Box::new(SvgState::new()); + *data_pointer = Box::into_raw(state) as *mut std::os::raw::c_void; + FT_Err_Ok as FT_Error +} + +unsafe extern "C" fn free_svg_library(data_pointer: *mut FT_Pointer) { + let state: Box = Box::from_raw((*data_pointer) as *mut SvgState); + drop(state); + *data_pointer = std::ptr::null_mut(); +} + +/* + * This hook is called at two different locations. Firstly, it is called + * when presetting the glyphslot when `FT_Load_Glyph` is called. + * Secondly, it is called right before the render hook is called. When + * `cache` is false, it is the former, when `cache` is true, it is the + * latter. + * + * The job of this function is to preset the slot setting the width, + * height, pitch, `bitmap.left`, and `bitmap.top`. These are all + * necessary for appropriate memory allocation, as well as ultimately + * compositing the glyph later on by client applications. + */ +unsafe extern "C" fn svg_preset_slot( + slot: FT_GlyphSlot, + cache: FT_Bool, + state: *mut FT_Pointer, +) -> FT_Error { + let state: &mut SvgState = &mut *((*state) as *mut SvgState); + let slot: &mut FT_GlyphSlotRec_ = &mut *slot; + let document: &mut FT_SVG_DocumentRec_ = &mut *(slot.other as FT_SVG_Document); + let cache = cache != 0; + + match svg_preset_slot_impl(slot, document, cache, state) { + Ok(_) => FT_Err_Ok as FT_Error, + Err(err) => { + log::error!("svg_preset_slot: {err:#}"); + FT_Err_Invalid_SVG_Document as FT_Error + } + } +} + +#[derive(Hash, Eq, PartialEq, Clone, Copy, Debug)] +struct DocCacheKey { + doc_ptr: usize, + start_glyph_id: u16, + end_glyph_id: u16, +} + +fn svg_preset_slot_impl( + slot: &mut FT_GlyphSlotRec_, + document: &mut FT_SVG_DocumentRec_, + cache: bool, + state: &mut SvgState, +) -> anyhow::Result<()> { + use resvg::usvg::TreeParsing; + + let svg_data = unsafe { + std::slice::from_raw_parts(document.svg_document, document.svg_document_length as usize) + }; + + let document_hash = DocCacheKey { + doc_ptr: svg_data.as_ptr() as usize, + start_glyph_id: document.start_glyph_id, + end_glyph_id: document.end_glyph_id, + }; + + let utree = match state.doc_cache.get(&document_hash) { + Some(t) => t, + None => { + let options = resvg::usvg::Options { + image_href_resolver: resvg::usvg::ImageHrefResolver { + resolve_string: Box::new(|_href, _opts| None), + ..Default::default() + }, + ..Default::default() + }; + + let utree = + resvg::usvg::Tree::from_data(svg_data, &options).context("Tree::from_data")?; + state.doc_cache.put(document_hash, utree); + state + .doc_cache + .get(&document_hash) + .expect("just inserted it") + } + }; + + let tree_node; + let rtree = if document.start_glyph_id < document.end_glyph_id { + tree_node = utree + .node_by_id(&format!("glyph{}", slot.glyph_index)) + .context("missing glyph")?; + resvg::Tree::from_usvg_node(&tree_node).context("from_usvg_node")? + } else { + resvg::Tree::from_usvg(&utree) + }; + + let pixmap_size = rtree.size.to_int_size(); + + let x_svg_to_out = document.metrics.x_ppem as f64 / pixmap_size.width() as f64; + let y_svg_to_out = document.metrics.y_ppem as f64 / pixmap_size.height() as f64; + + let xx = (document.transform.xx as f64) / (1 << 16) as f64; + let xy = -(document.transform.xy as f64) / (1 << 16) as f64; + + let yx = -(document.transform.yx as f64) / (1 << 16) as f64; + let yy = (document.transform.yy as f64) / (1 << 16) as f64; + + let x0 = + document.delta.x as f64 / 64. * pixmap_size.width() as f64 / document.metrics.x_ppem as f64; + let y0 = -document.delta.y as f64 / 64. * pixmap_size.height() as f64 + / document.metrics.y_ppem as f64; + + let transform = resvg::tiny_skia::Transform::from_row( + xx as f32, xy as f32, yx as f32, yy as f32, x0 as f32, y0 as f32, + ) + .post_scale(x_svg_to_out as f32, y_svg_to_out as f32); + + let dimension_x = pixmap_size.width() as f64 * x_svg_to_out; + let dimension_y = pixmap_size.height() as f64 * y_svg_to_out; + + slot.bitmap_left = 0; + slot.bitmap_top = 0; + slot.bitmap.rows = dimension_y as _; + slot.bitmap.width = dimension_x as _; + slot.bitmap.pitch = (dimension_x as i32) * 4; + slot.bitmap.pixel_mode = ftwrap::FT_Pixel_Mode::FT_PIXEL_MODE_BGRA as _; + slot.metrics.width = (dimension_x * 64.0) as FT_Pos; + slot.metrics.height = (dimension_y * 64.0) as FT_Pos; + slot.metrics.horiBearingX = 0; + slot.metrics.horiBearingY = 0; + slot.metrics.vertBearingX = 0; + slot.metrics.vertBearingY = 0; + + if slot.metrics.vertAdvance == 0 { + slot.metrics.vertAdvance = (dimension_y * 1.2 * 64.) as FT_Pos; + } + + if cache { + let mut pixmap = + Pixmap::new(dimension_x as u32, dimension_y as u32).context("Pixmap::new")?; + rtree.render(transform, &mut pixmap.as_mut()); + state.pixmap = pixmap; + } + + Ok(()) +} + +/// The render hook. The job of this hook is to simply render the glyph in +/// the buffer that has been allocated on the FreeType side. +unsafe extern "C" fn svg_render(slot: FT_GlyphSlot, data_pointer: *mut FT_Pointer) -> FT_Error { + let state: &mut SvgState = &mut *((*data_pointer) as *mut SvgState); + let slot: &mut FT_GlyphSlotRec_ = &mut *slot; + + match svg_render_impl(slot, state) { + Ok(_) => FT_Err_Ok as FT_Error, + Err(err) => { + log::error!("svg_render: {err:#}"); + FT_Err_Invalid_SVG_Document as FT_Error + } + } +} + +fn svg_render_impl(slot: &mut FT_GlyphSlotRec_, state: &mut SvgState) -> anyhow::Result<()> { + let bitmap = unsafe { + std::slice::from_raw_parts_mut( + slot.bitmap.buffer, + slot.bitmap.width as usize * slot.bitmap.rows as usize * 4, + ) + }; + let mut pixmap = + PixmapMut::from_bytes(bitmap, slot.bitmap.width, slot.bitmap.rows).context("PixmapMut")?; + pixmap.draw_pixmap( + 0, + 0, + state.pixmap.as_ref(), + &PixmapPaint::default(), + Transform::default(), + None, + ); + + // Post-process: freetype wants BGRA but tiny-skia is RGBA + for pixel in pixmap.pixels_mut() { + let (r, g, b, a) = (pixel.red(), pixel.green(), pixel.blue(), pixel.alpha()); + *pixel = + resvg::tiny_skia::PremultipliedColorU8::from_rgba(b, g, r, a).expect("swap to succeed"); + } + + slot.bitmap.pixel_mode = ftwrap::FT_Pixel_Mode::FT_PIXEL_MODE_BGRA as _; + slot.bitmap.num_grays = 256; + slot.format = crate::ftwrap::FT_Glyph_Format_::FT_GLYPH_FORMAT_BITMAP; + + Ok(()) +}