diff --git a/Cargo.lock b/Cargo.lock index d0f8e8c..7689d75 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -116,6 +116,12 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + [[package]] name = "bitflags" version = "2.11.1" @@ -185,6 +191,20 @@ name = "bytemuck" version = "1.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" +dependencies = [ + "bytemuck_derive", +] + +[[package]] +name = "bytemuck_derive" +version = "1.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] [[package]] name = "bytes" @@ -433,6 +453,12 @@ version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + [[package]] name = "errno" version = "0.3.14" @@ -443,12 +469,22 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "euclid" +version = "0.22.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1a05365e3b1c6d1650318537c7460c6923f1abdd272ad6842baa2b509957a06" +dependencies = [ + "num-traits", +] + [[package]] name = "evp" version = "0.7.0" dependencies = [ "ab_glyph", "anyhow", + "base64", "clap", "clap_complete", "crossbeam-channel", @@ -463,12 +499,15 @@ dependencies = [ "nix", "regex", "rgb", + "rustybuzz", "serde", "serde_json", "shell-words", + "subsetter", "thiserror 2.0.18", "tracing", "tracing-subscriber", + "ttf-parser", "vergen-gitcl", "woff2-patched", ] @@ -483,6 +522,15 @@ dependencies = [ "zlib-rs", ] +[[package]] +name = "font-types" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39a654f404bbcbd48ea58c617c2993ee91d1cb63727a37bf2323a4edeed1b8c5" +dependencies = [ + "bytemuck", +] + [[package]] name = "fontconfig-parser" version = "0.5.8" @@ -584,6 +632,12 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + [[package]] name = "heck" version = "0.5.0" @@ -621,6 +675,16 @@ version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40fac9d56ed6437b198fddba683305e8e2d651aa42647f00f5ae542e7f5c94a2" +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown", +] + [[package]] name = "int-enum" version = "1.2.0" @@ -645,6 +709,17 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" +[[package]] +name = "kurbo" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce9729cc38c18d86123ab736fd2e7151763ba226ac2490ec092d1dd148825e32" +dependencies = [ + "arrayvec", + "euclid", + "smallvec", +] + [[package]] name = "lazy_static" version = "1.5.0" @@ -1008,6 +1083,16 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "read-fonts" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6717cf23b488adf64b9d711329542ba34de147df262370221940dfabc2c91358" +dependencies = [ + "bytemuck", + "font-types", +] + [[package]] name = "redox_syscall" version = "0.5.18" @@ -1071,6 +1156,12 @@ version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97" +[[package]] +name = "rustc-hash" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" + [[package]] name = "rustc_version" version = "0.4.1" @@ -1099,6 +1190,24 @@ version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +[[package]] +name = "rustybuzz" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd3c7c96f8a08ee34eff8857b11b49b07d71d1c3f4e88f8a88d4c9e9f90b1702" +dependencies = [ + "bitflags", + "bytemuck", + "core_maths", + "log", + "smallvec", + "ttf-parser", + "unicode-bidi-mirroring", + "unicode-ccc", + "unicode-properties", + "unicode-script", +] + [[package]] name = "safer-bytes" version = "0.2.0" @@ -1217,6 +1326,16 @@ version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" +[[package]] +name = "skrifa" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c31071dedf532758ecf3fed987cdb4bd9509f900e026ab684b4ecb81ea49841" +dependencies = [ + "bytemuck", + "read-fonts", +] + [[package]] name = "slotmap" version = "1.1.1" @@ -1238,6 +1357,18 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "subsetter" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb6895a12ac5599bb6057362f00e8a3cf1daab4df33f553a55690a44e4fed8d0" +dependencies = [ + "kurbo", + "rustc-hash", + "skrifa", + "write-fonts", +] + [[package]] name = "syn" version = "2.0.117" @@ -1422,12 +1553,36 @@ dependencies = [ "core_maths", ] +[[package]] +name = "unicode-bidi-mirroring" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dfa6e8c60bb66d49db113e0125ee8711b7647b5579dc7f5f19c42357ed039fe" + +[[package]] +name = "unicode-ccc" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce61d488bcdc9bc8b5d1772c404828b17fc481c0a582b5581e95fb233aef503e" + [[package]] name = "unicode-ident" version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +[[package]] +name = "unicode-properties" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" + +[[package]] +name = "unicode-script" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "383ad40bb927465ec0ce7720e033cb4ca06912855fc35db31b5755d0de75b1ee" + [[package]] name = "unicode-segmentation" version = "1.13.2" @@ -1564,6 +1719,19 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "write-fonts" +version = "0.43.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "886614b5ce857341226aa091f3c285e450683894acaaa7887f366c361efef79d" +dependencies = [ + "font-types", + "indexmap", + "kurbo", + "log", + "read-fonts", +] + [[package]] name = "wyz" version = "0.5.1" diff --git a/Cargo.toml b/Cargo.toml index f6592b1..3f45daf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -53,6 +53,10 @@ ab_glyph = "0.2" fontdb = "0.23" woff2-patched = "0.4" lodepng = "3.12.2" +base64 = "0.22.1" +subsetter = "0.2.3" +ttf-parser = "0.25.1" +rustybuzz = "0.20.1" diff --git a/src/render_svg.rs b/src/render_svg.rs index 4b5bea1..920539a 100644 --- a/src/render_svg.rs +++ b/src/render_svg.rs @@ -43,12 +43,110 @@ use crate::render_common::is_box_drawing; use anyhow::{Context, Result, anyhow}; use crossbeam_channel::{Receiver, Sender, bounded}; +use std::collections::HashSet; +use base64::prelude::*; +use subsetter::{subset, GlyphRemapper}; +use ttf_parser::Face; +use woff2_patched::convert_woff2_to_ttf; + use crate::{ recording::{RawFrame, Recording, style_flags}, render_common::{RAW_FRAME_CONSUMER_CHANNEL_CAPACITY, ViewportConfig}, style::{rgb_hex, window_bar_dot_metrics}, }; +const EMBEDDED_JETBRAINS_NERD_MONO_REGULAR_WOFF2: &[u8] = include_bytes!(concat!( + env!("OUT_DIR"), + "/JetBrainsMonoNerdFontMono-Regular.woff2" +)); +const EMBEDDED_JETBRAINS_NERD_MONO_BOLD_WOFF2: &[u8] = include_bytes!(concat!( + env!("OUT_DIR"), + "/JetBrainsMonoNerdFontMono-Bold.woff2" +)); +const EMBEDDED_JETBRAINS_NERD_MONO_ITALIC_WOFF2: &[u8] = include_bytes!(concat!( + env!("OUT_DIR"), + "/JetBrainsMonoNerdFontMono-Italic.woff2" +)); +const EMBEDDED_JETBRAINS_NERD_MONO_BOLD_ITALIC_WOFF2: &[u8] = include_bytes!(concat!( + env!("OUT_DIR"), + "/JetBrainsMonoNerdFontMono-BoldItalic.woff2" +)); +const EMBEDDED_UNIFONT_UPPER_WOFF2: &[u8] = + include_bytes!(concat!(env!("OUT_DIR"), "/unifont_upper-17.0.04.woff2")); +const EMBEDDED_UNIFONT_CSUR_WOFF2: &[u8] = + include_bytes!(concat!(env!("OUT_DIR"), "/unifont_csur-17.0.04.woff2")); +const EMBEDDED_NOTO_SANS_MONO_REGULAR_WOFF2: &[u8] = + include_bytes!(concat!(env!("OUT_DIR"), "/NotoSansMono-Regular.woff2")); +const EMBEDDED_NOTO_SANS_SYMBOLS2_REGULAR_WOFF2: &[u8] = + include_bytes!(concat!(env!("OUT_DIR"), "/NotoSansSymbols2-Regular.woff2")); +const EMBEDDED_NOTO_SANS_MONO_CJK_JP_SUBSET_WOFF2: &[u8] = + include_bytes!(concat!(env!("OUT_DIR"), "/NotoSansMonoCJKjp-Subset.woff2")); + +fn subset_font(woff2_data: &[u8], chars: &HashSet) -> Option> { + let ttf = convert_woff2_to_ttf(&mut std::io::Cursor::new(woff2_data)).ok()?; + let face = Face::parse(&ttf, 0).ok()?; + + let mut glyphs = Vec::new(); + glyphs.push(0); // .notdef + for c in chars { + if let Some(glyph_id) = face.glyph_index(*c) { + glyphs.push(glyph_id.0); + } + } + + glyphs.sort_unstable(); + glyphs.dedup(); + + let mapper = GlyphRemapper::new_from_glyphs_sorted(&glyphs); + subset(&ttf, 0, &mapper).ok() +} + +fn generate_style_block(frames: &[RawFrame]) -> String { + let mut used_chars = HashSet::new(); + for frame in frames { + for cell in &frame.cells { + for c in cell.text.chars() { + used_chars.insert(c); + } + } + } + + let subset = |data: &[u8]| -> String { + match subset_font(data, &used_chars) { + Some(subset_data) => format!("url(data:font/ttf;base64,{}) format('truetype')", BASE64_STANDARD.encode(&subset_data)), + None => format!("url(data:font/woff2;base64,{}) format('woff2')", BASE64_STANDARD.encode(data)), + } + }; + + // Note: We decode the embedded WOFF2 files to TTF, subset them, and then + // embed the resulting TTF instead of WOFF2. This is because a pure-Rust WOFF2 + // encoder is currently not readily available, making it hard to compress the + // subsetted data back to WOFF2 without adding complex C dependencies to the build. + format!( + r#" +"#, + jb_reg = subset(EMBEDDED_JETBRAINS_NERD_MONO_REGULAR_WOFF2), + jb_bold = subset(EMBEDDED_JETBRAINS_NERD_MONO_BOLD_WOFF2), + jb_ital = subset(EMBEDDED_JETBRAINS_NERD_MONO_ITALIC_WOFF2), + jb_bold_ital = subset(EMBEDDED_JETBRAINS_NERD_MONO_BOLD_ITALIC_WOFF2), + ns_mono = subset(EMBEDDED_NOTO_SANS_MONO_REGULAR_WOFF2), + ns_sym2 = subset(EMBEDDED_NOTO_SANS_SYMBOLS2_REGULAR_WOFF2), + ns_cjk = subset(EMBEDDED_NOTO_SANS_MONO_CJK_JP_SUBSET_WOFF2), + uni_upper = subset(EMBEDDED_UNIFONT_UPPER_WOFF2), + uni_csur = subset(EMBEDDED_UNIFONT_CSUR_WOFF2), + ) +} + /// Tunables for the SVG renderer. #[derive(Debug, Clone)] pub struct SvgOptions { @@ -91,7 +189,7 @@ pub fn spawn_svg_stream( impl Default for SvgOptions { fn default() -> Self { Self { - font_family: "ui-monospace, Menlo, Consolas, 'DejaVu Sans Mono', monospace".to_string(), + font_family: "'JetBrainsMono Nerd Font Mono', 'Noto Sans Mono', 'Noto Sans Symbols 2', 'Noto Sans Mono CJK JP', 'unifont_upper', 'unifont_csur', ui-monospace, Menlo, Consolas, 'DejaVu Sans Mono', monospace".to_string(), font_size: 16.0, } } @@ -188,11 +286,12 @@ pub fn render_svg_to_string(rec: &Recording, opts: &SvgOptions) -> Result -"#, +{style}"#, w = canvas_w, h = canvas_h, font = escape_attr(&opts.font_family), fs = opts.font_size, + style = generate_style_block(&frames) )); // Canvas background. @@ -299,11 +398,12 @@ fn run_svg_stream_worker( s.push_str(&format!( r#" -"#, +{style}"#, w = canvas_w, h = canvas_h, font = escape_attr(&opts.font_family), fs = opts.font_size, + style = generate_style_block(&frames) )); s.push_str(&format!( r#"