diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c8d198..9c2a388 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- **Math in labels** — any label may embed `$...$` math (LaTeX-ish: `$\sigma^2$`, `$\frac{a}{b}$`, `$\sqrt{x}$`). Two tiers: a zero-dependency **lookup tier** (always on) lowers math to inline Unicode (Greek, operators, super/subscripts, `\frac`→`a/b`, `\sqrt`→`√(…)`) for every backend including the terminal; and an opt-in **typst tier** (feature `math`) that typesets the whole label with the Typst compiler (linked as a library — no external binary) for real 2-D math (stacked fractions, radicals, large operators) embedded in SVG/PNG/PDF, with math color following the label color. The `math` feature is excluded from `full` to keep ordinary builds lean. Note: Typst math is not LaTeX (`mc` is one identifier — write `m c`); on a compile failure a label degrades to the lookup tier with a one-time warning. See *Reference → Math in Labels*. +- **`TypstBackend`** (feature `typst`) — emit a CETZ-based Typst document for external compilation with `typst compile`; `$...$` regions become native Typst math. - **Pre-compiled release binaries** — pushing a `vX.Y.Z` tag now builds standalone `kuva` CLI binaries (with the `cli,full` feature set: SVG + PNG + PDF) for Linux (x86_64 gnu/musl, aarch64), macOS (Intel + Apple Silicon) and Windows (x86_64), and attaches them with SHA-256 checksums to the matching GitHub Release. Users can download a binary and run it without installing Rust. See `.github/workflows/release.yml` (resolves #17). - **`ManhattanPlot::with_thin_overlapping_labels()`** — opts the Manhattan x-axis into collision-aware chromosome labelling. By default every chromosome whose band is at least 6px wide is labelled, which can overprint the labels of adjacent small chromosomes (e.g. 17/19/21) on a genome-wide plot. When enabled, labels are placed in a single left-to-right pass and any label whose estimated footprint would overlap the previously drawn one is skipped, automatically thinning crowded regions while keeping the rest readable. Works with both horizontal and rotated (`Layout::with_x_tick_rotate`) labels. Off by default; existing behaviour is unchanged. diff --git a/Cargo.lock b/Cargo.lock index 5e954e2..b928343 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -79,6 +79,24 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "approx" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6" +dependencies = [ + "num-traits", +] + +[[package]] +name = "ar_archive_writer" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7eb93bbb63b9c227414f6eb3a0adfddca591a8ce1e9b60661bb08969b87e340b" +dependencies = [ + "object", +] + [[package]] name = "arrayref" version = "0.3.9" @@ -97,12 +115,56 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "az" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be5eb007b7cacc6c660343e96f650fedf4b5a77512399eb952ca6642cf8d13f7" + [[package]] name = "base64" version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "biblatex" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d0c374feba1b9a59042a7c1cf00ce7c34b977b9134fe7c42b08e5183729f66" +dependencies = [ + "paste", + "roman-numerals-rs", + "strum", + "unic-langid", + "unicode-normalization", + "unscanny", +] + +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + [[package]] name = "bit_field" version = "0.10.3" @@ -120,6 +182,21 @@ name = "bitflags" version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" +dependencies = [ + "serde_core", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "by_address" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64fa3c856b712db6612c019f14756e64e4bcea13337a6b33b696333a9eaa2d06" [[package]] name = "bytemuck" @@ -159,12 +236,40 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" +[[package]] +name = "cc" +version = "1.2.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "556e016178bb5662a08681bbe0f00f8e17631781a4dfc8c45e466e4b185ec27f" +dependencies = [ + "find-msvc-tools", + "shlex", +] + [[package]] name = "cfg-if" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "chinese-number" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e964125508474a83c95eb935697abbeb446ff4e9d62c71ce880e3986d1c606b" +dependencies = [ + "chinese-variant", + "enum-ordinalize", + "num-bigint", + "num-traits", +] + +[[package]] +name = "chinese-variant" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58b52a9840ffff5d4d0058ae529fa066a75e794e3125546acfc61c23ad755e49" + [[package]] name = "chrono" version = "0.4.44" @@ -201,6 +306,16 @@ dependencies = [ "half", ] +[[package]] +name = "citationberg" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f6597e8bdbca37f1f56e5a80d15857b0932aead21a78d20de49e99e74933046" +dependencies = [ + "quick-xml 0.38.4", + "serde", +] + [[package]] name = "clap" version = "4.5.60" @@ -251,6 +366,21 @@ dependencies = [ "roff", ] +[[package]] +name = "cobs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1" +dependencies = [ + "thiserror", +] + +[[package]] +name = "codex" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9589e1effc5cacbea347899645c654158b03b2053d24bb426fd3128ced6e423c" + [[package]] name = "color_quant" version = "1.1.0" @@ -269,6 +399,30 @@ version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e4e18bf7a165bf7028fde98609a0f1e8f7498d762a212598e6c891f6893556ec" +[[package]] +name = "comemo" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c963350b2b08aa4b725d7802593245380ab53dacfedcaa971385fc33306c0d4" +dependencies = [ + "comemo-macros", + "parking_lot", + "rustc-hash", + "siphasher", + "slab", +] + +[[package]] +name = "comemo-macros" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3c400139ba1389ef9e20ad2d87cda68b437a66483aa0da616bdf2cea7413853" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "core_maths" version = "0.1.1" @@ -379,12 +533,73 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be1e0bca6c3637f992fc1cc7cbc52a78c1ef6db076dbf1059c4323d6a2048376" +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "displaydoc" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "ecow" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78e4f79b296fbaab6ce2e22d52cb4c7f010fe0ebe7a32e34fa25885fd797bd02" +dependencies = [ + "serde", +] + [[package]] name = "either" version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +[[package]] +name = "embedded-io" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced" + +[[package]] +name = "embedded-io" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" + +[[package]] +name = "enum-ordinalize" +version = "4.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a1091a7bb1f8f2c4b28f1fe2cef4980ca2d410a3d727d67ecc3178c9b0800f0" +dependencies = [ + "enum-ordinalize-derive", +] + +[[package]] +name = "enum-ordinalize-derive" +version = "4.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ca9601fb2d62598ee17836250842873a413586e5d7ed88b356e38ddbb0ec631" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -415,6 +630,29 @@ dependencies = [ "zune-inflate", ] +[[package]] +name = "fancy-regex" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "998b056554fbe42e03ae0e152895cd1a7e1002aec800fdc6635d20270260c46f" +dependencies = [ + "bit-set", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "fast-srgb8" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd2e7510819d6fbf51a5545c8f922716ecfb14df168a3242f7d33e0239efe6a1" + +[[package]] +name = "fastrand" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" + [[package]] name = "fdeflate" version = "0.3.7" @@ -424,6 +662,12 @@ dependencies = [ "simd-adler32", ] +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + [[package]] name = "flate2" version = "1.1.9" @@ -440,6 +684,12 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + [[package]] name = "foldhash" version = "0.1.5" @@ -488,6 +738,15 @@ dependencies = [ "ttf-parser 0.21.1", ] +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + [[package]] name = "getrandom" version = "0.3.4" @@ -520,6 +779,12 @@ dependencies = [ "weezl", ] +[[package]] +name = "glidesort" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2e102e6eb644d3e0b186fc161e4460417880a0a0b87d235f2e5b8fb30f2e9e0" + [[package]] name = "half" version = "2.7.1" @@ -549,97 +814,448 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" [[package]] -name = "heck" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" - -[[package]] -name = "hermit-abi" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" - -[[package]] -name = "image" -version = "0.24.9" +name = "hayagriva" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5690139d2f55868e080017335e4b94cb7414274c74f1669c84fb5feba2c9f69d" +checksum = "1cb69425736f184173b3ca6e27fcba440a61492a790c786b1c6af7e06a03e575" dependencies = [ - "bytemuck", - "byteorder", - "color_quant", - "exr", - "gif 0.13.3", - "jpeg-decoder", - "num-traits", - "png 0.17.16", - "qoi", - "tiff", + "biblatex", + "ciborium", + "citationberg", + "indexmap", + "paste", + "roman-numerals-rs", + "serde", + "serde_yaml", + "thiserror", + "unic-langid", + "unicode-segmentation", + "unscanny", + "url", ] [[package]] -name = "image" -version = "0.25.9" +name = "hayro" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6506c6c10786659413faa717ceebcb8f70731c0a60cbae39795fdf114519c1a" +checksum = "048488ba88552bb0fb2a7e4001c64d5bed65d1a92167186a1bb9151571f32e60" dependencies = [ "bytemuck", - "byteorder-lite", - "color_quant", - "gif 0.14.1", - "moxcms", - "num-traits", - "png 0.18.1", - "zune-core 0.5.1", - "zune-jpeg 0.5.12", + "hayro-interpret", + "image 0.25.9", + "kurbo 0.12.0", ] [[package]] -name = "image-webp" -version = "0.2.4" +name = "hayro-font" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "525e9ff3e1a4be2fbea1fdf0e98686a6d98b4d8f937e1bf7402245af1909e8c3" +checksum = "10e7e97ce840a6a70e7901e240ec65ba61106b66b37a4a1b899a2ce484248463" dependencies = [ - "byteorder-lite", - "quick-error", + "log", + "phf", ] [[package]] -name = "imagesize" -version = "0.13.0" +name = "hayro-interpret" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edcd27d72f2f071c64249075f42e205ff93c9a4c5f6c6da53e79ed9f9832c285" +checksum = "56204c972d08e844f3db13b1e14be769f846e576699b46d4f4637cc4f8f70102" +dependencies = [ + "bitflags 2.11.0", + "hayro-font", + "hayro-syntax", + "kurbo 0.12.0", + "log", + "moxcms", + "phf", + "rustc-hash", + "siphasher", + "skrifa", + "smallvec", + "yoke 0.8.2", +] [[package]] -name = "indexmap" -version = "2.13.0" +name = "hayro-svg" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +checksum = "e8c673304cec6e0dfd3b4f71fccecd45646899aa70279b62d3f933842abc4ac5" dependencies = [ - "equivalent", - "hashbrown 0.16.1", + "base64", + "hayro-interpret", + "image 0.25.9", + "kurbo 0.12.0", + "siphasher", + "xmlwriter", ] [[package]] -name = "is-terminal" -version = "0.4.17" +name = "hayro-syntax" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" +checksum = "3f9e5c7dbc0f11dc42775d1a6cc00f5f5137b90b6288dd7fe5f71d17b14d10be" dependencies = [ - "hermit-abi", - "libc", - "windows-sys", + "flate2", + "kurbo 0.12.0", + "log", + "rustc-hash", + "smallvec", + "zune-jpeg 0.4.21", ] [[package]] -name = "is_terminal_polyfill" -version = "1.70.2" +name = "heck" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] -name = "itertools" +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "hypher" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef68590049bab63a464eee1a1158ac04c6f6613a546d8d90f78636b8b94f171" + +[[package]] +name = "icu_collections" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" +dependencies = [ + "displaydoc", + "serde", + "yoke 0.7.5", + "zerofrom", + "zerovec 0.10.4", +] + +[[package]] +name = "icu_collections" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke 0.8.2", + "zerofrom", + "zerovec 0.11.6", +] + +[[package]] +name = "icu_locale_core" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = [ + "displaydoc", + "litemap 0.8.2", + "tinystr 0.8.3", + "writeable 0.6.3", + "zerovec 0.11.6", +] + +[[package]] +name = "icu_locid" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" +dependencies = [ + "displaydoc", + "litemap 0.7.5", + "tinystr 0.7.6", + "writeable 0.5.5", + "zerovec 0.10.4", +] + +[[package]] +name = "icu_locid_transform" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_locid_transform_data", + "icu_provider 1.5.0", + "tinystr 0.7.6", + "zerovec 0.10.4", +] + +[[package]] +name = "icu_locid_transform_data" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7515e6d781098bf9f7205ab3fc7e9709d34554ae0b21ddbcb5febfa4bc7df11d" + +[[package]] +name = "icu_normalizer" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +dependencies = [ + "icu_collections 2.2.0", + "icu_normalizer_data", + "icu_properties 2.2.0", + "icu_provider 2.2.0", + "smallvec", + "zerovec 0.11.6", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" + +[[package]] +name = "icu_properties" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" +dependencies = [ + "displaydoc", + "icu_collections 1.5.0", + "icu_locid_transform", + "icu_properties_data 1.5.1", + "icu_provider 1.5.0", + "serde", + "tinystr 0.7.6", + "zerovec 0.10.4", +] + +[[package]] +name = "icu_properties" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +dependencies = [ + "icu_collections 2.2.0", + "icu_locale_core", + "icu_properties_data 2.2.0", + "icu_provider 2.2.0", + "zerotrie 0.2.4", + "zerovec 0.11.6", +] + +[[package]] +name = "icu_properties_data" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85fb8799753b75aee8d2a21d7c14d9f38921b54b3dbda10f5a3c7a7b82dba5e2" + +[[package]] +name = "icu_properties_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" + +[[package]] +name = "icu_provider" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_provider_macros", + "postcard", + "serde", + "stable_deref_trait", + "tinystr 0.7.6", + "writeable 0.5.5", + "yoke 0.7.5", + "zerofrom", + "zerovec 0.10.4", +] + +[[package]] +name = "icu_provider" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable 0.6.3", + "yoke 0.8.2", + "zerofrom", + "zerotrie 0.2.4", + "zerovec 0.11.6", +] + +[[package]] +name = "icu_provider_adapters" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6324dfd08348a8e0374a447ebd334044d766b1839bb8d5ccf2482a99a77c0bc" +dependencies = [ + "icu_locid", + "icu_locid_transform", + "icu_provider 1.5.0", + "tinystr 0.7.6", + "zerovec 0.10.4", +] + +[[package]] +name = "icu_provider_blob" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c24b98d1365f55d78186c205817631a4acf08d7a45bdf5dc9dcf9c5d54dccf51" +dependencies = [ + "icu_provider 1.5.0", + "postcard", + "serde", + "writeable 0.5.5", + "zerotrie 0.1.3", + "zerovec 0.10.4", +] + +[[package]] +name = "icu_provider_macros" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "icu_segmenter" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a717725612346ffc2d7b42c94b820db6908048f39434504cb130e8b46256b0de" +dependencies = [ + "core_maths", + "displaydoc", + "icu_collections 1.5.0", + "icu_locid", + "icu_provider 1.5.0", + "icu_segmenter_data", + "serde", + "utf8_iter", + "zerovec 0.10.4", +] + +[[package]] +name = "icu_segmenter_data" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1e52775179941363cc594e49ce99284d13d6948928d8e72c755f55e98caa1eb" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties 2.2.0", +] + +[[package]] +name = "image" +version = "0.24.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5690139d2f55868e080017335e4b94cb7414274c74f1669c84fb5feba2c9f69d" +dependencies = [ + "bytemuck", + "byteorder", + "color_quant", + "exr", + "gif 0.13.3", + "jpeg-decoder", + "num-traits", + "png 0.17.16", + "qoi", + "tiff", +] + +[[package]] +name = "image" +version = "0.25.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6506c6c10786659413faa717ceebcb8f70731c0a60cbae39795fdf114519c1a" +dependencies = [ + "bytemuck", + "byteorder-lite", + "color_quant", + "gif 0.14.1", + "image-webp", + "moxcms", + "num-traits", + "png 0.18.1", + "zune-core 0.5.1", + "zune-jpeg 0.5.12", +] + +[[package]] +name = "image-webp" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "525e9ff3e1a4be2fbea1fdf0e98686a6d98b4d8f937e1bf7402245af1909e8c3" +dependencies = [ + "byteorder-lite", + "quick-error", +] + +[[package]] +name = "imagesize" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edcd27d72f2f071c64249075f42e205ff93c9a4c5f6c6da53e79ed9f9832c285" + +[[package]] +name = "indexmap" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +dependencies = [ + "equivalent", + "hashbrown 0.16.1", + "serde", + "serde_core", +] + +[[package]] +name = "is-terminal" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" +dependencies = [ + "hermit-abi", + "libc", + "windows-sys", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itertools" version = "0.10.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" @@ -662,6 +1278,15 @@ dependencies = [ "rayon", ] +[[package]] +name = "kamadak-exif" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1130d80c7374efad55a117d715a3af9368f0fa7a2c54573afc15a188cd984837" +dependencies = [ + "mutate_once", +] + [[package]] name = "kurbo" version = "0.11.3" @@ -698,10 +1323,14 @@ dependencies = [ "fontdue", "image 0.24.9", "png 0.18.1", - "rand", + "rand 0.9.4", "rand_distr", "ryu", "svg2pdf", + "typst", + "typst-library", + "typst-render", + "typst-svg", ] [[package]] @@ -722,6 +1351,46 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" +[[package]] +name = "linked-hash-map" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" + +[[package]] +name = "lipsum" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "636860251af8963cc40f6b4baadee105f02e21b28131d76eba8e40ce84ab8064" +dependencies = [ + "rand 0.8.6", + "rand_chacha 0.3.1", +] + +[[package]] +name = "litemap" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23fb14cb19457329c82206317a5663005a4d404783dc74f4252769b0d5f42856" +dependencies = [ + "serde", +] + +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + [[package]] name = "log" version = "0.4.29" @@ -764,33 +1433,126 @@ dependencies = [ ] [[package]] -name = "num-traits" -version = "0.2.19" +name = "mutate_once" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +checksum = "13d2233c9842d08cfe13f9eac96e207ca6a2ea10b80259ebe8ad0268be27d2af" + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" dependencies = [ - "autocfg", - "libm", + "num-integer", + "num-traits", ] [[package]] -name = "once_cell" -version = "1.21.3" +name = "num-conv" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" [[package]] -name = "once_cell_polyfill" -version = "1.70.2" +name = "num-integer" +version = "0.1.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] [[package]] -name = "oorandom" -version = "11.1.5" +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "object" +version = "0.37.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "oorandom" +version = "11.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" +[[package]] +name = "palette" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cbf71184cc5ecc2e4e1baccdb21026c20e5fc3dcf63028a086131b3ab00b6e6" +dependencies = [ + "approx", + "fast-srgb8", + "libm", + "palette_derive", +] + +[[package]] +name = "palette_derive" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5030daf005bface118c096f510ffb781fc28f9ab6a32ab224d8631be6851d30" +dependencies = [ + "by_address", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + [[package]] name = "pdf-writer" version = "0.12.1" @@ -803,12 +1565,83 @@ dependencies = [ "ryu", ] +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "phf" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" +dependencies = [ + "phf_macros", + "phf_shared", + "serde", +] + +[[package]] +name = "phf_generator" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" +dependencies = [ + "fastrand", + "phf_shared", +] + +[[package]] +name = "phf_macros" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "phf_shared" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" +dependencies = [ + "siphasher", +] + [[package]] name = "pico-args" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315" +[[package]] +name = "pixglyph" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c1106193bc18a4b840eb075ff6664c8a0b0270f0531bb12a7e9c803e53b55c5" +dependencies = [ + "ttf-parser 0.25.1", +] + +[[package]] +name = "plist" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "092791278e026273c1b65bbdcfbba3a300f2994c896bd01ab01da613c29c46f1" +dependencies = [ + "base64", + "indexmap", + "quick-xml 0.39.4", + "serde", + "time", +] + [[package]] name = "png" version = "0.17.16" @@ -835,6 +1668,39 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + +[[package]] +name = "postcard" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24" +dependencies = [ + "cobs", + "embedded-io 0.4.0", + "embedded-io 0.6.1", + "serde", +] + +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec 0.11.6", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "ppv-lite86" version = "0.2.21" @@ -844,6 +1710,12 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "proc-macro-hack" +version = "0.5.20+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" + [[package]] name = "proc-macro2" version = "1.0.106" @@ -853,12 +1725,28 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "psm" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "645dbe486e346d9b5de3ef16ede18c26e6c70ad97418f4874b8b1889d6e761ea" +dependencies = [ + "ar_archive_writer", + "cc", +] + [[package]] name = "pxfm" version = "0.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5a041e753da8b807c9255f28de81879c78c876392ff2469cde94799b2896b9d" +[[package]] +name = "qcms" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edecfcd5d755a5e5d98e24cf43113e7cdaec5a070edd0f6b250c03a573da30fa" + [[package]] name = "qoi" version = "0.4.1" @@ -874,6 +1762,25 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" +[[package]] +name = "quick-xml" +version = "0.38.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "quick-xml" +version = "0.39.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdcc8dd4e2f670d309a5f0e83fe36dfdc05af317008fea29144da1a2ac858e5e" +dependencies = [ + "memchr", +] + [[package]] name = "quote" version = "1.0.45" @@ -889,14 +1796,33 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "rand" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" +dependencies = [ + "rand_core 0.6.4", +] + [[package]] name = "rand" version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" dependencies = [ - "rand_chacha", - "rand_core", + "rand_chacha 0.9.0", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", ] [[package]] @@ -906,9 +1832,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.9.5", ] +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" + [[package]] name = "rand_core" version = "0.9.5" @@ -925,7 +1857,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a8615d50dcf34fa31f7ab52692afec947c4dd0ab803cc87cb3b0b4570ff7463" dependencies = [ "num-traits", - "rand", + "rand 0.9.4", ] [[package]] @@ -958,6 +1890,15 @@ dependencies = [ "font-types", ] +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.11.0", +] + [[package]] name = "regex" version = "1.12.3" @@ -1019,12 +1960,28 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "88f8660c1ff60292143c98d08fc6e2f654d722db50410e3f3797d40baaf9d8f3" +[[package]] +name = "roman-numerals-rs" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c85cd47a33a4510b1424fe796498e174c6a9cf94e606460ef022a19f3e4ff85e" + [[package]] name = "roxmltree" version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97" +[[package]] +name = "rust_decimal" +version = "1.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c5108e3d4d903e21aac27f12ba5377b6b34f9f44b325e4894c7924169d06995" +dependencies = [ + "arrayvec", + "num-traits", +] + [[package]] name = "rustc-hash" version = "2.1.1" @@ -1064,6 +2021,12 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + [[package]] name = "serde" version = "1.0.228" @@ -1107,6 +2070,34 @@ dependencies = [ "zmij", ] +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_yaml" +version = "0.9.34+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" +dependencies = [ + "indexmap", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + [[package]] name = "simd-adler32" version = "0.3.8" @@ -1138,6 +2129,12 @@ dependencies = [ "read-fonts", ] +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + [[package]] name = "slotmap" version = "1.1.1" @@ -1153,6 +2150,31 @@ version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "stacker" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "640c8cdd92b6b12f5bcb1803ca3bbf5ab96e5e6b6b96b9ab77dabe9e880b3190" +dependencies = [ + "cc", + "cfg-if", + "libc", + "psm", + "windows-sys", +] + [[package]] name = "strict-num" version = "0.1.1" @@ -1169,133 +2191,655 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] -name = "subsetter" -version = "0.2.3" +name = "strum" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "subsetter" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb6895a12ac5599bb6057362f00e8a3cf1daab4df33f553a55690a44e4fed8d0" +dependencies = [ + "kurbo 0.12.0", + "rustc-hash", + "skrifa", + "write-fonts", +] + +[[package]] +name = "svg2pdf" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e50dc062439cc1a396181059c80932a6e6bd731b130e674c597c0c8874b6df22" +dependencies = [ + "fontdb", + "image 0.25.9", + "log", + "miniz_oxide", + "once_cell", + "pdf-writer", + "resvg", + "siphasher", + "subsetter", + "tiny-skia", + "ttf-parser 0.25.1", + "usvg", +] + +[[package]] +name = "svgtypes" +version = "0.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68c7541fff44b35860c1a7a47a7cadf3e4a304c457b58f9870d9706ece028afc" +dependencies = [ + "kurbo 0.11.3", + "siphasher", +] + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "syntect" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "656b45c05d95a5704399aeef6bd0ddec7b2b3531b7c9e900abbf7c4d2190c925" +dependencies = [ + "bincode", + "fancy-regex", + "flate2", + "fnv", + "once_cell", + "plist", + "regex-syntax", + "serde", + "serde_derive", + "serde_json", + "thiserror", + "walkdir", + "yaml-rust", +] + +[[package]] +name = "thin-vec" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0f7e269b48f0a7dd0146680fa24b50cc67fc0373f086a5b2f99bd084639b482" + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tiff" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba1310fcea54c6a9a4fd1aad794ecc02c31682f6bfbecdf460bf19533eed1e3e" +dependencies = [ + "flate2", + "jpeg-decoder", + "weezl", +] + +[[package]] +name = "time" +version = "0.3.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" + +[[package]] +name = "time-macros" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tiny-skia" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83d13394d44dae3207b52a326c0c85a8bf87f1541f23b0d143811088497b09ab" +dependencies = [ + "arrayref", + "arrayvec", + "bytemuck", + "cfg-if", + "log", + "png 0.17.16", + "tiny-skia-path", +] + +[[package]] +name = "tiny-skia-path" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c9e7fc0c2e86a30b117d0462aa261b72b7a99b7ebd7deb3a14ceda95c5bdc93" +dependencies = [ + "arrayref", + "bytemuck", + "strict-num", +] + +[[package]] +name = "tinystr" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" +dependencies = [ + "displaydoc", + "serde", + "zerovec 0.10.4", +] + +[[package]] +name = "tinystr" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "serde_core", + "zerovec 0.11.6", +] + +[[package]] +name = "tinytemplate" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "tinyvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "toml" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "toml_write", + "winnow", +] + +[[package]] +name = "toml_write" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" + +[[package]] +name = "ttf-parser" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c591d83f69777866b9126b24c6dd9a18351f177e49d625920d19f989fd31cf8" + +[[package]] +name = "ttf-parser" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31" +dependencies = [ + "core_maths", +] + +[[package]] +name = "two-face" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39e51b6e60e545cfdae5a4639ff423818f52372211a8d9a3e892b4b0761f76b2" +dependencies = [ + "serde", + "serde_derive", + "syntect", +] + +[[package]] +name = "typed-arena" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6af6ae20167a9ece4bcb41af5b80f8a1f1df981f6391189ce00fd257af04126a" + +[[package]] +name = "typst" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f6511ee598476f4f322b4d13891083d96dbacb8f9c2b908604c7094ba390653" +dependencies = [ + "comemo", + "ecow", + "rustc-hash", + "typst-eval", + "typst-html", + "typst-layout", + "typst-library", + "typst-macros", + "typst-realize", + "typst-syntax", + "typst-timing", + "typst-utils", +] + +[[package]] +name = "typst-assets" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5613cb719a6222fe9b74027c3625d107767ec187bff26b8fc931cf58942c834f" + +[[package]] +name = "typst-eval" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "687757487dfc0c1e941344d5024cf7a28364e70c3e304faad89ac65597f62526" +dependencies = [ + "comemo", + "ecow", + "indexmap", + "rustc-hash", + "stacker", + "toml", + "typst-library", + "typst-macros", + "typst-syntax", + "typst-timing", + "typst-utils", + "unicode-segmentation", +] + +[[package]] +name = "typst-html" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e29f8da4f964d4c90739c3c1e0288b0ba1bccc3cc50623a6d558300b86ca8aad" +dependencies = [ + "bumpalo", + "comemo", + "ecow", + "palette", + "rustc-hash", + "time", + "typst-assets", + "typst-library", + "typst-macros", + "typst-svg", + "typst-syntax", + "typst-timing", + "typst-utils", +] + +[[package]] +name = "typst-layout" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cab0200105831a9158e63718a0f6141c78cb2c1722ed17d19ad28941e3b8491" +dependencies = [ + "az", + "bumpalo", + "codex", + "comemo", + "ecow", + "either", + "hypher", + "icu_properties 1.5.1", + "icu_provider 1.5.0", + "icu_provider_adapters", + "icu_provider_blob", + "icu_segmenter", + "kurbo 0.12.0", + "memchr", + "rustc-hash", + "rustybuzz", + "smallvec", + "ttf-parser 0.25.1", + "typst-assets", + "typst-library", + "typst-macros", + "typst-syntax", + "typst-timing", + "typst-utils", + "unicode-bidi", + "unicode-math-class", + "unicode-script", + "unicode-segmentation", +] + +[[package]] +name = "typst-library" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e276a5de53020c43efe2111ec236252e54ea4480b5ac18063e663dfbe03d9d1b" +dependencies = [ + "az", + "bitflags 2.11.0", + "bumpalo", + "chinese-number", + "ciborium", + "codex", + "comemo", + "csv", + "ecow", + "flate2", + "fontdb", + "glidesort", + "hayagriva", + "hayro-syntax", + "icu_properties 1.5.1", + "icu_provider 1.5.0", + "icu_provider_blob", + "image 0.25.9", + "indexmap", + "kamadak-exif", + "kurbo 0.12.0", + "lipsum", + "memchr", + "palette", + "phf", + "png 0.17.16", + "qcms", + "rayon", + "regex", + "regex-syntax", + "roxmltree", + "rust_decimal", + "rustc-hash", + "rustybuzz", + "serde", + "serde_json", + "serde_yaml", + "siphasher", + "smallvec", + "syntect", + "time", + "toml", + "ttf-parser 0.25.1", + "two-face", + "typed-arena", + "typst-assets", + "typst-macros", + "typst-syntax", + "typst-timing", + "typst-utils", + "unicode-math-class", + "unicode-normalization", + "unicode-segmentation", + "unscanny", + "usvg", + "utf8_iter", + "wasmi", + "xmlwriter", +] + +[[package]] +name = "typst-macros" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "141cbd1027129fbf6bda1013f52a264df7befc7388cc8f47767d65e803fd3a59" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "typst-realize" +version = "0.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb6895a12ac5599bb6057362f00e8a3cf1daab4df33f553a55690a44e4fed8d0" +checksum = "f7ffe964757fb93d2e98978aa2a74ee85b0f94c8643e8f3550737258b58f39d8" dependencies = [ - "kurbo 0.12.0", - "rustc-hash", - "skrifa", - "write-fonts", + "arrayvec", + "bumpalo", + "comemo", + "ecow", + "regex", + "typst-library", + "typst-macros", + "typst-syntax", + "typst-timing", + "typst-utils", ] [[package]] -name = "svg2pdf" -version = "0.13.0" +name = "typst-render" +version = "0.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e50dc062439cc1a396181059c80932a6e6bd731b130e674c597c0c8874b6df22" +checksum = "1baabef8c01dd7150380592811bf37af9b2498be86043834ddd629d1bcb48ccb" dependencies = [ - "fontdb", + "bytemuck", + "comemo", + "hayro", "image 0.25.9", - "log", - "miniz_oxide", - "once_cell", - "pdf-writer", + "pixglyph", "resvg", - "siphasher", - "subsetter", "tiny-skia", "ttf-parser 0.25.1", - "usvg", + "typst-assets", + "typst-library", + "typst-macros", + "typst-timing", ] [[package]] -name = "svgtypes" -version = "0.15.3" +name = "typst-svg" +version = "0.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68c7541fff44b35860c1a7a47a7cadf3e4a304c457b58f9870d9706ece028afc" +checksum = "e46b811837ade1f0243ef0d8bf3fb06d166443090eac22c28643f374c2ccdc9d" dependencies = [ - "kurbo 0.11.3", - "siphasher", + "base64", + "comemo", + "ecow", + "flate2", + "hayro", + "hayro-svg", + "image 0.25.9", + "rustc-hash", + "ttf-parser 0.25.1", + "typst-assets", + "typst-library", + "typst-macros", + "typst-timing", + "typst-utils", + "xmlparser", + "xmlwriter", ] [[package]] -name = "syn" -version = "2.0.117" +name = "typst-syntax" +version = "0.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +checksum = "a95d9192060e23b1e491b0b94dff676acddc92a4d672aeb8ca3890a5a734e879" dependencies = [ - "proc-macro2", - "quote", + "ecow", + "rustc-hash", + "serde", + "toml", + "typst-timing", + "typst-utils", "unicode-ident", + "unicode-math-class", + "unicode-script", + "unicode-segmentation", + "unscanny", ] [[package]] -name = "tiff" -version = "0.9.1" +name = "typst-timing" +version = "0.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba1310fcea54c6a9a4fd1aad794ecc02c31682f6bfbecdf460bf19533eed1e3e" +checksum = "7be94f8faf19841b49574ef5c7fd7a12e2deb7c3d8deba5a596f35d2222024cd" dependencies = [ - "flate2", - "jpeg-decoder", - "weezl", + "parking_lot", + "serde", + "serde_json", ] [[package]] -name = "tiny-skia" -version = "0.11.4" +name = "typst-utils" +version = "0.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83d13394d44dae3207b52a326c0c85a8bf87f1541f23b0d143811088497b09ab" +checksum = "a3966c92e8fa48c7ce898130d07000d985f18206d92b250f0f939287fbccdee3" dependencies = [ - "arrayref", - "arrayvec", - "bytemuck", - "cfg-if", - "log", - "png 0.17.16", - "tiny-skia-path", + "once_cell", + "portable-atomic", + "rayon", + "rustc-hash", + "siphasher", + "thin-vec", + "unicode-math-class", ] [[package]] -name = "tiny-skia-path" -version = "0.11.4" +name = "unic-langid" +version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c9e7fc0c2e86a30b117d0462aa261b72b7a99b7ebd7deb3a14ceda95c5bdc93" +checksum = "a28ba52c9b05311f4f6e62d5d9d46f094bd6e84cb8df7b3ef952748d752a7d05" dependencies = [ - "arrayref", - "bytemuck", - "strict-num", + "unic-langid-impl", + "unic-langid-macros", ] [[package]] -name = "tinytemplate" -version = "1.2.1" +name = "unic-langid-impl" +version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" +checksum = "dce1bf08044d4b7a94028c93786f8566047edc11110595914de93362559bc658" dependencies = [ "serde", - "serde_json", + "tinystr 0.8.3", ] [[package]] -name = "tinyvec" -version = "1.10.0" +name = "unic-langid-macros" +version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +checksum = "d5957eb82e346d7add14182a3315a7e298f04e1ba4baac36f7f0dbfedba5fc25" dependencies = [ - "tinyvec_macros", + "proc-macro-hack", + "tinystr 0.8.3", + "unic-langid-impl", + "unic-langid-macros-impl", ] [[package]] -name = "tinyvec_macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" - -[[package]] -name = "ttf-parser" -version = "0.21.1" +name = "unic-langid-macros-impl" +version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c591d83f69777866b9126b24c6dd9a18351f177e49d625920d19f989fd31cf8" - -[[package]] -name = "ttf-parser" -version = "0.25.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31" +checksum = "a1249a628de3ad34b821ecb1001355bca3940bcb2f88558f1a8bd82e977f75b5" dependencies = [ - "core_maths", + "proc-macro-hack", + "quote", + "syn", + "unic-langid-impl", ] [[package]] @@ -1322,6 +2866,21 @@ version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +[[package]] +name = "unicode-math-class" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d246cf599d5fae3c8d56e04b20eb519adb89a8af8d0b0fbcded369aa3647d65" + +[[package]] +name = "unicode-normalization" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" +dependencies = [ + "tinyvec", +] + [[package]] name = "unicode-properties" version = "0.1.4" @@ -1334,12 +2893,43 @@ version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "383ad40bb927465ec0ce7720e033cb4ca06912855fc35db31b5755d0de75b1ee" +[[package]] +name = "unicode-segmentation" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" + [[package]] name = "unicode-vo" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b1d386ff53b415b7fe27b50bb44679e2cc4660272694b7b6f3326d8480823a94" +[[package]] +name = "unsafe-libyaml" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" + +[[package]] +name = "unscanny" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9df2af067a7953e9c3831320f35c1cc0600c30d44d9f7a12b01db1cd88d6b47" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", + "serde_derive", +] + [[package]] name = "usvg" version = "0.45.1" @@ -1367,6 +2957,12 @@ dependencies = [ "xmlwriter", ] +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + [[package]] name = "utf8parse" version = "0.2.2" @@ -1398,6 +2994,52 @@ dependencies = [ "wit-bindgen", ] +[[package]] +name = "wasmi" +version = "0.51.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb321403ce594274827657a908e13d1d9918aa02257b8bf8391949d9764023ff" +dependencies = [ + "spin", + "wasmi_collections", + "wasmi_core", + "wasmi_ir", + "wasmparser", +] + +[[package]] +name = "wasmi_collections" +version = "0.51.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9b8e98e45a2a534489f8225e765cbf1cb9a3078072605e58158910cf4749172" + +[[package]] +name = "wasmi_core" +version = "0.51.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c25f375c0cdf14810eab07f532f61f14d4966f09c747a55067fdf3196e8512e6" +dependencies = [ + "libm", +] + +[[package]] +name = "wasmi_ir" +version = "0.51.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "624e2a68a4293ecb8f564260b68394b29cf3b3edba6bce35532889a2cb33c3d9" +dependencies = [ + "wasmi_core", +] + +[[package]] +name = "wasmparser" +version = "0.228.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4abf1132c1fdf747d56bbc1bb52152400c70f336870f968b85e89ea422198ae3" +dependencies = [ + "bitflags 2.11.0", +] + [[package]] name = "weezl" version = "0.1.12" @@ -1428,6 +3070,15 @@ dependencies = [ "windows-link", ] +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" +dependencies = [ + "memchr", +] + [[package]] name = "wit-bindgen" version = "0.51.0" @@ -1447,12 +3098,86 @@ dependencies = [ "read-fonts", ] +[[package]] +name = "writeable" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" + +[[package]] +name = "writeable" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" + +[[package]] +name = "xmlparser" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4" + [[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" +dependencies = [ + "linked-hash-map", +] + +[[package]] +name = "yoke" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive 0.7.5", + "zerofrom", +] + +[[package]] +name = "yoke" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" +dependencies = [ + "stable_deref_trait", + "yoke-derive 0.8.2", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + [[package]] name = "zerocopy" version = "0.8.42" @@ -1473,6 +3198,96 @@ dependencies = [ "syn", ] +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerotrie" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb594dd55d87335c5f60177cee24f19457a5ec10a065e0a3014722ad252d0a1f" +dependencies = [ + "displaydoc", + "litemap 0.7.5", + "serde", + "zerovec 0.10.4", +] + +[[package]] +name = "zerotrie" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +dependencies = [ + "displaydoc", + "yoke 0.8.2", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" +dependencies = [ + "serde", + "yoke 0.7.5", + "zerofrom", + "zerovec-derive 0.10.3", +] + +[[package]] +name = "zerovec" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +dependencies = [ + "serde", + "yoke 0.8.2", + "zerofrom", + "zerovec-derive 0.11.3", +] + +[[package]] +name = "zerovec-derive" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "zmij" version = "1.0.21" diff --git a/Cargo.toml b/Cargo.toml index 2e923de..f560892 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,9 +10,9 @@ keywords = ["plotting", "bioinformatics", "svg", "visualization", "science"] categories = ["science", "visualization", "graphics", "command-line-utilities"] documentation = "https://docs.rs/kuva" readme = "README.md" -# The raw .ttf is kept in the repo for regenerating the .deflate asset, but is -# not published — only the compressed form ships in the crate. -exclude = ["assets/fonts/DejaVuSans.ttf"] +# Raw font files are kept in the repo for regenerating the gzipped assets, but +# are not published — only the compressed forms ship in the crate. +exclude = ["assets/fonts/DejaVuSans.ttf", "assets/fonts/NewCMMath-Regular.otf"] [package.metadata.docs.rs] all-features = true @@ -23,7 +23,24 @@ png = ["dep:fontdue", "dep:png", "dep:flate2"] pdf = ["dep:svg2pdf", "dep:flate2"] embed_font = ["dep:flate2"] cli = ["dep:clap", "dep:clap_mangen", "dep:csv"] -full = ["embed_font", "png", "pdf"] +# Typst markup output backend. Emits .typ source the user compiles externally +# with `typst compile fig.typ` — pure string emission, no deps. The TypstBackend +# itself handles `$...$` math via native Typst math syntax. Upstream issue #16. +typst = [] +# In-process math rendering for SVG/PNG/PDF output. Uses the typst compiler as +# a Rust library to render each `$...$` region of a label to an SVG fragment, +# which kuva embeds at the label position. Result: real math typography +# (proper italic σ, vinculum √, fractions, sums) in every raster/vector +# backend — no external typst CLI required. Pulls `flate2` for the bundled +# math font (it inflates via the `fonts` module, like png/pdf/embed_font). +# +# NOTE: deliberately NOT part of `full`. The typst compiler is a ~200-crate, +# ~15 MB dependency tree, and `full` is what `cargo ci-test` builds — which +# links every dependency into ~30 example binaries. Including `math` there +# bloats `target/` by ~10×. Build/test math on its own instead: +# cargo test --features math,png --test math_png +math = ["dep:typst", "dep:typst-svg", "dep:typst-render", "dep:typst-library", "dep:flate2"] +full = ["embed_font", "png", "pdf", "typst"] # Adds `kuva doom` subcommand and generates a self-contained DOOM SVG. # Build downloads doom.wasm + doom1.wad from the kuva doom-assets release. # Run once: cargo build --bin kuva --features cli,doom @@ -33,13 +50,17 @@ doom = ["cli"] colorous = "=1.0.16" chrono = { version = "0.4", default-features = false, features = ["std"] } ryu = "1" -flate2 = { version = "1", optional = true } -fontdue = { version = "0.9", optional = true } -png = { version = "0.18", optional = true } -svg2pdf = { version = "0.13", optional = true } -clap = { version = "4", features = ["derive"], optional = true } -clap_mangen = { version = "0.2", optional = true } -csv = { version = "1", optional = true } +flate2 = { version = "1", optional = true } +fontdue = { version = "0.9", optional = true } +png = { version = "0.18", optional = true } +svg2pdf = { version = "0.13", optional = true } +clap = { version = "4", features = ["derive"], optional = true } +clap_mangen = { version = "0.2", optional = true } +csv = { version = "1", optional = true } +typst = { version = "0.14", optional = true } +typst-svg = { version = "0.14", optional = true } +typst-render = { version = "0.14", optional = true } +typst-library = { version = "0.14", optional = true } [dev-dependencies] rand = "0.9.3" diff --git a/assets/fonts/LICENSE-NewCM b/assets/fonts/LICENSE-NewCM new file mode 100644 index 0000000..3454fdc --- /dev/null +++ b/assets/fonts/LICENSE-NewCM @@ -0,0 +1,223 @@ +The GUST Font License Version 1.0 applies to: + +* NewComputerModern fonts in files/fonts/NewCM*.otf + +% This is version 1.0, dated 22 June 2009, of the GUST Font License. +% (GUST is the Polish TeX Users Group, http://www.gust.org.pl) +% +% For the most recent version of this license see +% http://www.gust.org.pl/fonts/licenses/GUST-FONT-LICENSE.txt +% or +% http://tug.org/fonts/licenses/GUST-FONT-LICENSE.txt +% +% This work may be distributed and/or modified under the conditions +% of the LaTeX Project Public License, either version 1.3c of this +% license or (at your option) any later version. +% +% Please also observe the following clause: +% 1) it is requested, but not legally required, that derived works be +% distributed only after changing the names of the fonts comprising this +% work and given in an accompanying "manifest", and that the +% files comprising the Work, as listed in the manifest, also be given +% new names. Any exceptions to this request are also given in the +% manifest. +% +% We recommend the manifest be given in a separate file named +% MANIFEST-.txt, where is some unique identification +% of the font family. If a separate "readme" file accompanies the Work, +% we recommend a name of the form README-.txt. +% +% The latest version of the LaTeX Project Public License is in +% http://www.latex-project.org/lppl.txt and version 1.3c or later +% is part of all distributions of LaTeX version 2006/05/20 or later. +================================================================================ + +================================================================================ +The terms below apply to: + +* DejaVu fonts in files/fonts/DejaVu*.ttf + (https://github.com/dejavu-fonts/dejavu-fonts) + +Fonts are (c) Bitstream (see below). DejaVu changes are in public domain. +Glyphs imported from Arev fonts are (c) Tavmjong Bah (see below) + + +Bitstream Vera Fonts Copyright +------------------------------ + +Copyright (c) 2003 by Bitstream, Inc. All Rights Reserved. Bitstream Vera is +a trademark of Bitstream, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of the fonts accompanying this license ("Fonts") and associated +documentation files (the "Font Software"), to reproduce and distribute the +Font Software, including without limitation the rights to use, copy, merge, +publish, distribute, and/or sell copies of the Font Software, and to permit +persons to whom the Font Software is furnished to do so, subject to the +following conditions: + +The above copyright and trademark notices and this permission notice shall +be included in all copies of one or more of the Font Software typefaces. + +The Font Software may be modified, altered, or added to, and in particular +the designs of glyphs or characters in the Fonts may be modified and +additional glyphs or characters may be added to the Fonts, only if the fonts +are renamed to names not containing either the words "Bitstream" or the word +"Vera". + +This License becomes null and void to the extent applicable to Fonts or Font +Software that has been modified and is distributed under the "Bitstream +Vera" names. + +The Font Software may be sold as part of a larger software package but no +copy of one or more of the Font Software typefaces may be sold by itself. + +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, +TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL BITSTREAM OR THE GNOME +FOUNDATION BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING +ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF +THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE +FONT SOFTWARE. + +Except as contained in this notice, the names of Gnome, the Gnome +Foundation, and Bitstream Inc., shall not be used in advertising or +otherwise to promote the sale, use or other dealings in this Font Software +without prior written authorization from the Gnome Foundation or Bitstream +Inc., respectively. For further information, contact: fonts at gnome dot +org. + +Arev Fonts Copyright +------------------------------ + +Copyright (c) 2006 by Tavmjong Bah. All Rights Reserved. + +Permission is hereby granted, free of charge, to any person obtaining +a copy of the fonts accompanying this license ("Fonts") and +associated documentation files (the "Font Software"), to reproduce +and distribute the modifications to the Bitstream Vera Font Software, +including without limitation the rights to use, copy, merge, publish, +distribute, and/or sell copies of the Font Software, and to permit +persons to whom the Font Software is furnished to do so, subject to +the following conditions: + +The above copyright and trademark notices and this permission notice +shall be included in all copies of one or more of the Font Software +typefaces. + +The Font Software may be modified, altered, or added to, and in +particular the designs of glyphs or characters in the Fonts may be +modified and additional glyphs or characters may be added to the +Fonts, only if the fonts are renamed to names not containing either +the words "Tavmjong Bah" or the word "Arev". + +This License becomes null and void to the extent applicable to Fonts +or Font Software that has been modified and is distributed under the +"Tavmjong Bah Arev" names. + +The Font Software may be sold as part of a larger software package but +no copy of one or more of the Font Software typefaces may be sold by +itself. + +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL +TAVMJONG BAH BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. + +Except as contained in this notice, the name of Tavmjong Bah shall not +be used in advertising or otherwise to promote the sale, use or other +dealings in this Font Software without prior written authorization +from Tavmjong Bah. For further information, contact: tavmjong @ free +. fr. + +TeX Gyre DJV Math +----------------- +Fonts are (c) Bitstream (see below). DejaVu changes are in public domain. + +Math extensions done by B. Jackowski, P. Strzelczyk and P. Pianowski +(on behalf of TeX users groups) are in public domain. + +Letters imported from Euler Fraktur from AMSfonts are (c) American +Mathematical Society (see below). +Bitstream Vera Fonts Copyright +Copyright (c) 2003 by Bitstream, Inc. All Rights Reserved. Bitstream Vera +is a trademark of Bitstream, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of the fonts accompanying this license (“Fonts”) and associated +documentation +files (the “Font Software”), to reproduce and distribute the Font Software, +including without limitation the rights to use, copy, merge, publish, +distribute, +and/or sell copies of the Font Software, and to permit persons to whom +the Font Software is furnished to do so, subject to the following +conditions: + +The above copyright and trademark notices and this permission notice +shall be +included in all copies of one or more of the Font Software typefaces. + +The Font Software may be modified, altered, or added to, and in particular +the designs of glyphs or characters in the Fonts may be modified and +additional +glyphs or characters may be added to the Fonts, only if the fonts are +renamed +to names not containing either the words “Bitstream” or the word “Vera”. + +This License becomes null and void to the extent applicable to Fonts or +Font Software +that has been modified and is distributed under the “Bitstream Vera” +names. + +The Font Software may be sold as part of a larger software package but +no copy +of one or more of the Font Software typefaces may be sold by itself. + +THE FONT SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, +TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL BITSTREAM OR THE GNOME +FOUNDATION +BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING ANY GENERAL, +SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, WHETHER IN AN +ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF THE USE OR +INABILITY TO USE +THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE FONT SOFTWARE. +Except as contained in this notice, the names of GNOME, the GNOME +Foundation, +and Bitstream Inc., shall not be used in advertising or otherwise to promote +the sale, use or other dealings in this Font Software without prior written +authorization from the GNOME Foundation or Bitstream Inc., respectively. +For further information, contact: fonts at gnome dot org. + +AMSFonts (v. 2.2) copyright + +The PostScript Type 1 implementation of the AMSFonts produced by and +previously distributed by Blue Sky Research and Y&Y, Inc. are now freely +available for general use. This has been accomplished through the +cooperation +of a consortium of scientific publishers with Blue Sky Research and Y&Y. +Members of this consortium include: + +Elsevier Science IBM Corporation Society for Industrial and Applied +Mathematics (SIAM) Springer-Verlag American Mathematical Society (AMS) + +In order to assure the authenticity of these fonts, copyright will be +held by +the American Mathematical Society. This is not meant to restrict in any way +the legitimate use of the fonts, such as (but not limited to) electronic +distribution of documents containing these fonts, inclusion of these fonts +into other public domain or commercial font collections or computer +applications, use of the outline data to create derivative fonts and/or +faces, etc. However, the AMS does require that the AMS copyright notice be +removed from any derivative versions of the fonts which have been altered in +any way. In addition, to ensure the fidelity of TeX documents using Computer +Modern fonts, Professor Donald Knuth, creator of the Computer Modern faces, diff --git a/assets/fonts/NewCMMath-Regular.otf b/assets/fonts/NewCMMath-Regular.otf new file mode 100644 index 0000000..28f936f Binary files /dev/null and b/assets/fonts/NewCMMath-Regular.otf differ diff --git a/assets/fonts/NewCMMath-Regular.otf.gz b/assets/fonts/NewCMMath-Regular.otf.gz new file mode 100644 index 0000000..a2fa7af Binary files /dev/null and b/assets/fonts/NewCMMath-Regular.otf.gz differ diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md index 7ef36b1..fcc41f3 100644 --- a/docs/src/SUMMARY.md +++ b/docs/src/SUMMARY.md @@ -139,6 +139,7 @@ - [SVG Interactivity](./reference/interactive.md) - [Date & Time Axes](./reference/datetime.md) - [Stats Box](./reference/stats_box.md) +- [Math in Labels](./reference/math.md) # Performance diff --git a/docs/src/assets/math/fraction.svg b/docs/src/assets/math/fraction.svg new file mode 100644 index 0000000..e325107 --- /dev/null +++ b/docs/src/assets/math/fraction.svg @@ -0,0 +1,35 @@ +012340246810 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +rateFraction \ No newline at end of file diff --git a/docs/src/assets/math/greek.svg b/docs/src/assets/math/greek.svg new file mode 100644 index 0000000..67d6df1 --- /dev/null +++ b/docs/src/assets/math/greek.svg @@ -0,0 +1,24 @@ +012340246810 + + + + + + + + + + + + + + + + + + + + + + +countStandard deviation \ No newline at end of file diff --git a/docs/src/assets/math/mixed.svg b/docs/src/assets/math/mixed.svg new file mode 100644 index 0000000..28a45f8 --- /dev/null +++ b/docs/src/assets/math/mixed.svg @@ -0,0 +1,104 @@ +012340246810 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Mixed text and math \ No newline at end of file diff --git a/docs/src/assets/math/quadratic.svg b/docs/src/assets/math/quadratic.svg new file mode 100644 index 0000000..5afbe70 --- /dev/null +++ b/docs/src/assets/math/quadratic.svg @@ -0,0 +1,91 @@ +012340246810 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +rootsQuadratic formula \ No newline at end of file diff --git a/docs/src/assets/math/rotated_ylabel.svg b/docs/src/assets/math/rotated_ylabel.svg new file mode 100644 index 0000000..a7cc0a1 --- /dev/null +++ b/docs/src/assets/math/rotated_ylabel.svg @@ -0,0 +1,66 @@ +012340246810time + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Mass–energy equivalence \ No newline at end of file diff --git a/docs/src/assets/math/sqrt.svg b/docs/src/assets/math/sqrt.svg new file mode 100644 index 0000000..6fec9ba --- /dev/null +++ b/docs/src/assets/math/sqrt.svg @@ -0,0 +1,44 @@ +012340246810 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +distanceSquare root \ No newline at end of file diff --git a/docs/src/assets/math/sum.svg b/docs/src/assets/math/sum.svg new file mode 100644 index 0000000..678c9a1 --- /dev/null +++ b/docs/src/assets/math/sum.svg @@ -0,0 +1,49 @@ +012340246810 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +totalSummation \ No newline at end of file diff --git a/docs/src/assets/math/superscript.svg b/docs/src/assets/math/superscript.svg new file mode 100644 index 0000000..e535ae6 --- /dev/null +++ b/docs/src/assets/math/superscript.svg @@ -0,0 +1,85 @@ +012340246810 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Power law \ No newline at end of file diff --git a/docs/src/reference/math.md b/docs/src/reference/math.md new file mode 100644 index 0000000..3a023ed --- /dev/null +++ b/docs/src/reference/math.md @@ -0,0 +1,85 @@ +# Math in Labels + +Any label — axis titles, the plot title, `TextPlot` bodies — may embed math +inside `$...$` using LaTeX-ish syntax: + +```rust +Layout::new((0.0, 3.0), (0.0, 10.0)) + .with_x_label("Variance, $\\sigma^2$ (units)") + .with_y_label("Energy $E = m c^2$"); +``` + +kuva renders math in one of two tiers depending on the features you build with. + +## Tiers + +### Lookup tier (always on, zero dependencies) + +With no extra features, `$...$` regions are lowered to inline **Unicode** text: + +| Input | Output | +|-------|--------| +| `$\sigma^2$` | σ² | +| `$x_i$` | xᵢ | +| `$a \leq b \cdot c$` | a ≤ b · c | +| `$\frac{a}{b}$` | a/b | +| `$\frac{a+b}{c}$` | (a+b)/c | +| `$\sqrt{x^2+y^2}$` | √(x²+y²) | +| `$\sum_{i=1}^{n} x_i$` | ∑ᵢ₌₁ⁿ xᵢ | + +This tier never emits a stray `\` or `$`. It is the baseline for **every** +backend and the **only** tier the terminal backend can use (a character grid +can't hold typeset math). + +Supported: Greek letters, common operators/relations/arrows, `\frac`, `\sqrt` +(and `\sqrt[n]`), and super/subscripts. Superscripts and subscripts are +**all-or-nothing**: if every character in the group has a Unicode form you get +`x²ⁿ`; if any doesn't (e.g. `q`, most capitals) the whole group falls back to a +clean `x^(2q)`. Fractions and radicals are rendered **inline** (`a/b`, `√(…)`), +never stacked — for stacked 2-D math, use the typst tier. + +### Typst tier (feature `math`) + +Build with the `math` feature and the SVG, PNG, and PDF backends typeset the +**whole label** with the [Typst](https://typst.app) compiler (linked as a +library — no external `typst` binary needed), embedding real 2-D math: + +- math italic, stacked fractions, radicals with vinculum, large operators with + limits +- math color follows the label color +- self-contained output (fonts embedded), so it renders identically everywhere + +```bash +cargo run --features math,png --example ... +``` + +The `math` feature pulls in the Typst compiler (~200 crates) plus a bundled +math font (~1 MB). It is **opt-in** and deliberately excluded from `full` so +ordinary builds and `cargo ci-test` stay lean. The terminal backend always +uses the lookup tier regardless. + +#### Examples + +Rendered with the `math` feature (`cargo run --example math --features math`): + +Fraction in an axis label +Square root in an axis label +Summation with limits in an axis label +Quadratic formula in an axis label +Math in a rotated y-axis title +Mixed text and math in labels + +## Typst math is not LaTeX + +The `$...$` body is LaTeX-flavoured for familiarity, but the typst tier hands +it to Typst, whose math syntax differs in one notable way: a **multi-letter run +is a single identifier**. `mc` is *not* `m × c` — so `$E = mc^2$` fails to +compile (`unknown variable: mc`). Write the factors separated: + +```text +$E = m c^2$ ✓ +$E = mc^2$ ✗ (renders via the lookup tier + a one-time warning) +``` + +When a typst-tier compile fails, kuva falls back to the lookup tier for that +label and prints a one-time warning naming the expression and Typst's own hint. diff --git a/examples/math.rs b/examples/math.rs new file mode 100644 index 0000000..ed4c59d --- /dev/null +++ b/examples/math.rs @@ -0,0 +1,81 @@ +//! Math-in-labels documentation examples. +//! +//! Generates canonical SVG outputs used in the kuva documentation. +//! Run with (the `math` feature is required for real Typst typesetting; +//! without it the always-on lookup tier renders inline Unicode): +//! +//! ```bash +//! cargo run --example math --features math +//! ``` +//! +//! SVGs are written to `docs/src/assets/math/`. + +use kuva::backend::svg::SvgBackend; +use kuva::plot::scatter::ScatterPlot; +use kuva::render::layout::Layout; +use kuva::render::plots::Plot; +use kuva::render::render::render_multiple; +use std::fs; + +const OUT: &str = "docs/src/assets/math"; + +fn write(name: &str, plots: Vec, layout: Layout) { + fs::create_dir_all(OUT).unwrap(); + let svg = SvgBackend.render_scene(&render_multiple(plots, layout)); + fs::write(format!("{OUT}/{name}.svg"), svg).unwrap(); +} + +/// A scatter with a fixed dataset; only the labels vary between scenarios. +fn scatter(title: &str, x_label: &str, y_label: &str) -> (Vec, Layout) { + let plot = ScatterPlot::new() + .with_data(vec![(1.0_f64, 1.0), (2.0, 4.0), (3.0, 9.0)]) + .with_color("steelblue"); + let plots = vec![Plot::Scatter(plot)]; + let layout = Layout::new((0.0, 4.0), (0.0, 10.0)) + .with_title(title) + .with_x_label(x_label) + .with_y_label(y_label); + (plots, layout) +} + +fn main() { + // ── Greek, super/subscripts ─────────────────────────────────────────── + let (p, l) = scatter("Standard deviation", "$\\mu \\pm \\sigma$", "count"); + write("greek", p, l); + + let (p, l) = scatter("Power law", "$x^2 + y^2 = r^2$", "$f(x)$"); + write("superscript", p, l); + + // ── Fractions & radicals ────────────────────────────────────────────── + let (p, l) = scatter("Fraction", "$\\frac{a + b}{c}$", "rate"); + write("fraction", p, l); + + let (p, l) = scatter("Square root", "$\\sqrt{x^2 + y^2}$", "distance"); + write("sqrt", p, l); + + // ── Large operators ─────────────────────────────────────────────────── + let (p, l) = scatter("Summation", "$\\sum_{i=1}^{n} x_i$", "total"); + write("sum", p, l); + + // Note: Typst math treats a multi-letter run as one identifier, so factors + // are spaced (`4 a c`) — otherwise `ac` is an unknown variable. + let (p, l) = scatter( + "Quadratic formula", + "$x = \\frac{-b \\pm \\sqrt{b^2 - 4 a c}}{2 a}$", + "roots", + ); + write("quadratic", p, l); + + // ── Rotated y-axis title + mixed text/math ──────────────────────────── + let (p, l) = scatter("Mass–energy equivalence", "time", "Energy $E = m c^2$"); + write("rotated_ylabel", p, l); + + let (p, l) = scatter( + "Mixed text and math", + "Variance, $\\sigma^2$ (units)", + "$\\nabla \\cdot F$", + ); + write("mixed", p, l); + + println!("Math SVGs written to {OUT}/"); +} diff --git a/scripts/gen_docs.sh b/scripts/gen_docs.sh index 47d5973..f66c8d4 100755 --- a/scripts/gen_docs.sh +++ b/scripts/gen_docs.sh @@ -83,6 +83,11 @@ for ex in "${EXAMPLES[@]}"; do cargo run --features full --example "$ex" --quiet done +# The math example needs the `math` feature (excluded from `full`) for real +# Typst typesetting in its SVGs. +echo " math (with math feature)" +cargo run --features "full,math" --example math --quiet + echo "Done." # ── Benchmark charts ─────────────────────────────────────────────────────────── diff --git a/scripts/smoke_tests.sh b/scripts/smoke_tests.sh index f3f4f31..e89cd07 100755 --- a/scripts/smoke_tests.sh +++ b/scripts/smoke_tests.sh @@ -15,6 +15,12 @@ set -euo pipefail BIN="" SAVE=0 OUTDIR="smoke_test_outputs" +# Optional binary built with the `math` feature, used by the typst-tier math +# section. The default $BIN (cli,full) excludes `math` and exercises the +# always-on lookup tier instead. Build one with, e.g.: +# CARGO_TARGET_DIR=target/math cargo build --bin kuva --features cli,full,math +# bash scripts/smoke_tests.sh --math-bin target/math/debug/kuva +MATH_BIN="${MATH_BIN:-}" # Parse arguments while [[ $# -gt 0 ]]; do @@ -28,6 +34,10 @@ while [[ $# -gt 0 ]]; do fi shift ;; + --math-bin) + MATH_BIN="$2" + shift 2 + ;; *) BIN="$1" shift @@ -950,6 +960,60 @@ check "sunburst hierarchical" \ --label label --value value --parent parent \ --title "Animal Kingdom by Class" +# ── math in labels ────────────────────────────────────────────────────────── +# The CLI (cli,full — `math` is excluded from `full`) uses the always-on lookup +# tier, lowering $...$ to inline Unicode (σ², a/b, √(…), ∑). Exercised across +# plot types and label slots to confirm it is not scatter-specific. +check "math superscript + sqrt" \ + "$BIN" scatter "$DATA/scatter.tsv" --x x --y y \ + --x-label 'Variance, $\sigma^2$ (units)' --y-label '$\sqrt{x^2+y^2}$' + +check "math fraction title" \ + "$BIN" scatter "$DATA/scatter.tsv" --x x --y y \ + --title 'Rate $\frac{a + b}{c}$' + +check "math sum with limits" \ + "$BIN" scatter "$DATA/scatter.tsv" --x x --y y \ + --x-label '$\sum_{i=1}^{n} x_i$' --title 'Summation' + +check "math greek and operators" \ + "$BIN" scatter "$DATA/scatter.tsv" --x x --y y \ + --x-label '$\alpha \leq \beta \neq \gamma$' --y-label '$\mu \pm \sigma$' + +check "math in rotated y-label" \ + "$BIN" scatter "$DATA/scatter.tsv" --x x --y y \ + --y-label 'Energy $E = m c^2$' + +check "math on line plot" \ + "$BIN" line "$DATA/measurements.tsv" --x time --y value \ + --x-label 'Time $t$ ($\mu s$)' --y-label 'Amplitude $A_0$' + +check "math on bar plot" \ + "$BIN" bar "$DATA/bar.tsv" --label-col category --value-col count \ + --y-label 'Count $\times 10^3$' + +# ── math (typst tier) ───────────────────────────────────────────────────────── +# Only runs if a math-enabled binary was provided via --math-bin. These render +# real 2-D math (stacked fractions, radicals, limits) instead of inline Unicode. +if [[ -n "$MATH_BIN" && -x "$MATH_BIN" ]]; then + echo "" + echo "Math binary: $MATH_BIN (typst tier)" + check "typst fraction" \ + "$MATH_BIN" scatter "$DATA/scatter.tsv" --x x --y y \ + --title 'Rate $\frac{a + b}{c}$' + + check "typst sqrt + sum" \ + "$MATH_BIN" scatter "$DATA/scatter.tsv" --x x --y y \ + --x-label '$\sqrt{x^2 + y^2}$' --y-label '$\sum_{i=1}^{n} x_i$' + + check "typst quadratic in rotated y-label" \ + "$MATH_BIN" scatter "$DATA/scatter.tsv" --x x --y y \ + --y-label '$x = \frac{-b \pm \sqrt{b^2 - 4 a c}}{2 a}$' +else + echo "" + echo "Skipping typst-tier math checks (no --math-bin; build with --features cli,full,math)." +fi + # ── summary ─────────────────────────────────────────────────────────────────── echo "" echo "Results: $PASS passed, $FAIL failed" diff --git a/scripts/terminal_plots.sh b/scripts/terminal_plots.sh index 553aefc..cb3f91f 100755 --- a/scripts/terminal_plots.sh +++ b/scripts/terminal_plots.sh @@ -226,4 +226,13 @@ run scatter "$DATA/scatter.tsv" --x x --y y \ --y-label-wrap 20 \ --terminal $W $H +# ── math in labels ──────────────────────────────────────────────────────────── +# Terminal output uses the always-on lookup tier: $...$ regions are lowered to +# inline Unicode (σ², a/b, √, ∑) since a character grid can't embed Typst. +header "math labels" +run scatter "$DATA/scatter.tsv" --x x --y y \ + --title 'Decay $\lambda$ vs $\sigma^2$' \ + --x-label 'Time ($\mu s$)' --y-label '$\sqrt{x^2 + y^2}$' \ + --terminal $W $H + echo diff --git a/src/backend/mod.rs b/src/backend/mod.rs index ea63856..016e965 100644 --- a/src/backend/mod.rs +++ b/src/backend/mod.rs @@ -1,5 +1,7 @@ pub mod interactive_js; pub mod svg; +#[cfg(feature = "math")] +pub mod svg_math; pub mod terminal; #[cfg(feature = "png")] @@ -10,3 +12,6 @@ pub mod raster; #[cfg(feature = "pdf")] pub mod pdf; + +#[cfg(feature = "typst")] +pub mod typst; diff --git a/src/backend/raster.rs b/src/backend/raster.rs index 317f727..967bdcf 100644 --- a/src/backend/raster.rs +++ b/src/backend/raster.rs @@ -14,9 +14,8 @@ //! - Clip rects and translate transforms fully respected use std::collections::HashMap; -use std::sync::OnceLock; -use fontdue::{Font, FontSettings, Metrics}; +use fontdue::{Font, Metrics}; use crate::render::color::Color as KColor; use crate::render::render::{Primitive, Scene, TextAnchor}; @@ -98,6 +97,39 @@ impl Canvas { } } + /// Alpha-blend a premultiplied-RGBA pixmap (as produced by typst-render) + /// into the canvas with its top-left corner at device pixel (dx, dy), + /// clipped to `clip`. Used by the math feature to composite rendered + /// `$...$` regions. + #[cfg(feature = "math")] + fn blit_pixmap(&mut self, src: &crate::render::math::MathPixmap, dx: i32, dy: i32, clip: Clip) { + let sw = src.width_px as i32; + let sh = src.height_px as i32; + for row in 0..sh { + let py = dy + row; + if py < clip.y0 || py >= clip.y1 { + continue; + } + for col in 0..sw { + let px = dx + col; + if px < clip.x0 || px >= clip.x1 { + continue; + } + let si = ((row * sw + col) * 4) as usize; + let a = src.rgba[si + 3]; + if a == 0 { + continue; + } + // typst-render outputs premultiplied RGBA; un-premultiply to + // straight color for blend() (which expects straight rgb + + // coverage). For fully/partly transparent black math this + // keeps edges anti-aliased correctly. + let (r, g, b) = unpremultiply(src.rgba[si], src.rgba[si + 1], src.rgba[si + 2], a); + self.blend(px, py, Rgba { r, g, b, a: 255 }, a as f32 * (1.0 / 255.0)); + } + } + } + /// Composite `color` at `cov` coverage over the pixel at (x, y). /// The background is always opaque after `fill_background`, so dst_a == 255 /// always, collapsing the Porter-Duff "src over" formula to: @@ -897,6 +929,94 @@ impl Canvas { } } + /// Blit a whole-label math pixmap at an arbitrary rotation. + /// + /// The pixmap (premultiplied RGBA from typst-render) already *is* the + /// rendered label, so we treat it as the source buffer directly: + /// inverse-rotation bilinear-sample it into the canvas around the anchor + /// point `(anchor_x, anchor_y)`. The pixmap's anchor is at + /// `(off_ax, baseline_offset_px)`, where `off_ax` depends on text anchor. + #[cfg(feature = "math")] + fn blit_pixmap_rotated( + &mut self, + pm: &crate::render::math::MathPixmap, + anchor_x: f32, + anchor_y: f32, + anchor: TextAnchor, + angle_deg: f32, + clip: Clip, + ) { + let w = pm.width_px as f32; + let h = pm.height_px as f32; + let off_ax = match anchor { + TextAnchor::Start => 0.0, + TextAnchor::Middle => w * 0.5, + TextAnchor::End => w, + }; + let off_ay = pm.baseline_offset_px as f32; + + let rad = angle_deg * std::f32::consts::PI / 180.0; + let cos_a = rad.cos(); + let sin_a = rad.sin(); + + // Bounding box of the rotated pixmap in canvas space. + let corners = [(0.0f32, 0.0f32), (w, 0.0f32), (w, h), (0.0f32, h)]; + let mut min_x = f32::INFINITY; + let mut max_x = f32::NEG_INFINITY; + let mut min_y = f32::INFINITY; + let mut max_y = f32::NEG_INFINITY; + for (cx, cy) in &corners { + let dx = cx - off_ax; + let dy = cy - off_ay; + let wx = anchor_x + cos_a * dx - sin_a * dy; + let wy = anchor_y + sin_a * dx + cos_a * dy; + min_x = min_x.min(wx); + max_x = max_x.max(wx); + min_y = min_y.min(wy); + max_y = max_y.max(wy); + } + + // Keep the rotated label inside the clip. A tall math label (e.g. a + // rotated y-axis title with a superscript) can have a cross-extent + // wider than the reserved margin and would otherwise clip glyphs at + // the canvas edge — nudge it inward instead. + let shift_x = (clip.x0 as f32 - min_x).max(0.0); + let shift_y = (clip.y0 as f32 - min_y).max(0.0); + let anchor_x = anchor_x + shift_x; + let anchor_y = anchor_y + shift_y; + min_x += shift_x; + max_x += shift_x; + min_y += shift_y; + max_y += shift_y; + + let dx0 = (min_x.floor() as i32).max(clip.x0); + let dx1 = (max_x.ceil() as i32).min(clip.x1 - 1); + let dy0 = (min_y.floor() as i32).max(clip.y0); + let dy1 = (max_y.ceil() as i32).min(clip.y1 - 1); + + for dy in dy0..=dy1 { + for dx in dx0..=dx1 { + let dxw = dx as f32 + 0.5 - anchor_x; + let dyw = dy as f32 + 0.5 - anchor_y; + let src_x = off_ax + cos_a * dxw + sin_a * dyw; + let src_y = off_ay - sin_a * dxw + cos_a * dyw; + // Bilinear over premultiplied RGBA, then un-premultiply for blend. + let s = bilinear_rgba( + &pm.rgba, + pm.width_px, + pm.height_px, + src_x - 0.5, + src_y - 0.5, + ); + if s.a == 0 { + continue; + } + let (r, g, b) = unpremultiply(s.r, s.g, s.b, s.a); + self.blend(dx, dy, Rgba { r, g, b, a: 255 }, s.a as f32 * (1.0 / 255.0)); + } + } + } + // ── PNG encode ──────────────────────────────────────────────────────────── fn encode_png(self) -> Result, String> { @@ -958,11 +1078,7 @@ fn anchor_pen_x( // ── Shared font ─────────────────────────────────────────────────────────────── fn shared_font() -> &'static Font { - static FONT: OnceLock = OnceLock::new(); - FONT.get_or_init(|| { - Font::from_bytes(crate::fonts::dejavu_sans(), FontSettings::default()) - .expect("bundled DejaVu Sans TTF is valid") - }) + crate::fonts::shared_font() } // ── SVG path parsing ────────────────────────────────────────────────────────── @@ -1416,6 +1532,24 @@ fn parse_dasharray(s: &str) -> Vec { } /// Bilinear RGBA sample from a pixel buffer. Returns (0,0,0,0) for out-of-bounds. +/// Un-premultiply a premultiplied-alpha pixel to straight RGB. typst-render +/// outputs premultiplied RGBA; `blend()` expects straight color + coverage. +/// Callers must ensure `a > 0`. +#[cfg(feature = "math")] +#[inline] +fn unpremultiply(r: u8, g: u8, b: u8, a: u8) -> (u8, u8, u8) { + if a == 255 { + (r, g, b) + } else { + let af = a as u32; + ( + (r as u32 * 255 / af).min(255) as u8, + (g as u32 * 255 / af).min(255) as u8, + (b as u32 * 255 / af).min(255) as u8, + ) + } +} + fn bilinear_rgba(pixels: &[u8], w: u32, h: u32, x: f32, y: f32) -> Rgba { let x0 = x.floor() as i32; let y0 = y.floor() as i32; @@ -1879,6 +2013,61 @@ impl RasterBackend { .as_ref() .and_then(kcolor_to_rgba) .unwrap_or(default_text); + + // Math routing: typst tier (feature `math`) renders the + // whole label to a pixmap and composites it; otherwise the + // always-on lookup tier lowers `$...$` to inline Unicode + // text drawn through the normal glyph path. + let has_math = crate::render::math::contains_math(content); + + #[cfg(feature = "math")] + if has_math { + if let Some(pm) = crate::render::math::render_label_pixmap( + content, + *size as f64, + color.as_ref(), + s, + ) { + if let Some(angle) = rotate { + canvas.blit_pixmap_rotated( + &pm, + sx!(*x), + sy!(*y), + *anchor, + *angle as f32, + clip, + ); + } else { + // Anchor horizontally by pixmap width; align + // baseline to the label's y. + let shift = match anchor { + TextAnchor::Start => 0.0, + TextAnchor::Middle => -(pm.width_px as f32) * 0.5, + TextAnchor::End => -(pm.width_px as f32), + }; + let dx = (sx!(*x) + shift).round() as i32; + let dy = (sy!(*y) - pm.baseline_offset_px as f32).round() as i32; + // Nudge inward if the label would overflow the + // top/left clip edge, matching the rotated and + // SVG paths (otherwise a wide label near the + // edge clips instead of shifting). + let dx = dx.max(clip.x0); + let dy = dy.max(clip.y0); + canvas.blit_pixmap(&pm, dx, dy, clip); + } + continue; + } + // Compile failed — fall through to the lookup tier. + } + + let lowered; + let content: &str = if has_math { + lowered = crate::render::math::to_unicode(content); + &lowered + } else { + content + }; + canvas.draw_text( sx!(*x), sy!(*y), diff --git a/src/backend/svg.rs b/src/backend/svg.rs index 7f97962..0ada6c0 100644 --- a/src/backend/svg.rs +++ b/src/backend/svg.rs @@ -176,6 +176,9 @@ impl SvgBackend { // Interactive UI is emitted AFTER scene elements (see below) so it renders on top. let mut depth: usize = 1; + // Unique tag per embedded math fragment, to namespace Typst's element IDs. + #[cfg(feature = "math")] + let mut math_uid: usize = 0; for elem in &scene.elements { match elem { Primitive::Circle { @@ -225,6 +228,40 @@ impl SvgBackend { bold, color, } => { + // Math routing: a `$...$` label is either typeset by the + // typst tier (feature `math`) and embedded as a fragment, + // or lowered to inline Unicode by the always-on lookup + // tier and emitted as ordinary text. + let has_math = crate::render::math::contains_math(content); + + #[cfg(feature = "math")] + if has_math { + if let Some(m) = crate::render::math::render_label_svg( + content, + *size as f64, + color.as_ref(), + ) { + write_indent(&mut svg, depth, p); + crate::backend::svg_math::embed_label( + &mut svg, *x, *y, *anchor, *rotate, &m, math_uid, + ); + math_uid += 1; + write_newline(&mut svg, p); + continue; + } + // Compile failed — fall through to the lookup tier. + } + + // Lookup tier (or plain text). When the label has math but + // the typst tier isn't active/failed, substitute to Unicode. + let substituted; + let content: &str = if has_math { + substituted = crate::render::math::to_unicode(content); + &substituted + } else { + content + }; + let anchor_str = match anchor { TextAnchor::Start => "start", TextAnchor::Middle => "middle", diff --git a/src/backend/svg_math.rs b/src/backend/svg_math.rs new file mode 100644 index 0000000..43b6117 --- /dev/null +++ b/src/backend/svg_math.rs @@ -0,0 +1,85 @@ +//! Embed a typst-rendered label (whole-label SVG fragment) into the SVG +//! backend's output. Feature `math`. +//! +//! With whole-label rendering there is no per-segment layout: the entire +//! label was typeset by Typst into one fragment with a known width and +//! baseline. We only place it — anchor horizontally by its width, align its +//! baseline to the label's `y`, and wrap a rotation if requested. + +use std::fmt::Write; + +use crate::render::math::MathSvg; +use crate::render::render::TextAnchor; + +/// Append the positioned math fragment to `svg`. `uid` must be unique per +/// embedded label within the document — it namespaces the fragment's element +/// IDs (see below). +pub fn embed_label( + svg: &mut String, + x: f64, + y: f64, + anchor: TextAnchor, + rotate: Option, + math: &MathSvg, + uid: usize, +) { + let anchor_shift = match anchor { + TextAnchor::Start => 0.0, + TextAnchor::Middle => -math.width_pt / 2.0, + TextAnchor::End => -math.width_pt, + }; + let tx = x + anchor_shift; + let ty = y - math.baseline_offset_pt; + + // Keep a rotated label inside the viewBox: a tall math label (e.g. a + // rotated y-axis title with a superscript) can have a cross-extent wider + // than the reserved margin and would otherwise clip at the left/top edge. + // Nudge it inward by the amount its rotated bbox crosses the origin. + let (shift_x, shift_y) = match rotate { + Some(angle) => { + let rad = angle * std::f64::consts::PI / 180.0; + let (c, s) = (rad.cos(), rad.sin()); + let corners = [ + (tx, ty), + (tx + math.width_pt, ty), + (tx + math.width_pt, ty + math.height_pt), + (tx, ty + math.height_pt), + ]; + let (mut min_x, mut min_y) = (f64::INFINITY, f64::INFINITY); + for (px, py) in corners { + let rx = x + c * (px - x) - s * (py - y); + let ry = y + s * (px - x) + c * (py - y); + min_x = min_x.min(rx); + min_y = min_y.min(ry); + } + ((-min_x).max(0.0), (-min_y).max(0.0)) + } + None => (0.0, 0.0), + }; + let nudge = shift_x > 0.0 || shift_y > 0.0; + + if nudge { + let _ = write!(svg, r#""#); + } + if let Some(angle) = rotate { + // Rotate about the label's anchor point (x, y), matching how the SVG + // backend rotates plain . + let _ = write!(svg, r#""#); + } + // Each Typst fragment carries its own `id="glyph"` defs and content-hashed + // `id="g…"` glyph symbols. With more than one math label per plot those IDs + // collide across fragments → duplicate-ID (invalid) SVG. Namespace every ID + // and local href target with a per-label tag so each fragment is unique. + let tag = format!("m{uid}-"); + let inner = math + .inner_svg + .replace("id=\"", &format!("id=\"{tag}")) + .replace("href=\"#", &format!("href=\"#{tag}")); + let _ = write!(svg, r#"{inner}"#); + if rotate.is_some() { + svg.push_str(""); + } + if nudge { + svg.push_str(""); + } +} diff --git a/src/backend/terminal.rs b/src/backend/terminal.rs index a907287..c4731e6 100644 --- a/src/backend/terminal.rs +++ b/src/backend/terminal.rs @@ -821,6 +821,15 @@ impl Canvas { // reference line/tick. let baseline = *size as f64 * 0.35; let row = self.to_cy(y_s - baseline); + // Lookup tier: the terminal can't typeset, so `$...$` math is + // lowered to inline Unicode (σ, x², √(…)). No-op for plain text. + let lowered; + let content: &str = if crate::render::math::contains_math(content) { + lowered = crate::render::math::to_unicode(content); + &lowered + } else { + content + }; let chars: Vec = content.chars().collect(); let len = chars.len() as isize; diff --git a/src/backend/typst.rs b/src/backend/typst.rs new file mode 100644 index 0000000..19b6129 --- /dev/null +++ b/src/backend/typst.rs @@ -0,0 +1,680 @@ +//! Typst output backend (feature `typst`). +//! +//! Emits a [Typst](https://typst.app) document that draws the [`Scene`] using +//! the CETZ package for vector graphics. The output is plain text — kuva does +//! not embed a Typst compiler. Users run `typst compile fig.typ` themselves +//! to produce PDF/SVG/PNG; this keeps kuva's dependency footprint at zero +//! for this feature. +//! +//! # Why a separate backend? +//! +//! This backend emits Typst *markup* (`.typ`), which the user compiles +//! themselves with `typst compile`. It's the right choice when the plot is +//! destined for a larger Typst document, or when the user wants to hand-edit +//! the output. It has zero compiled dependencies — pure string emission. +//! +//! For rendering math *without* an external toolchain, see the separate +//! `math` feature ([`crate::render::math`]), which links the typst compiler +//! as a library and embeds rendered `$...$` regions directly into kuva's +//! SVG/PNG/PDF output. Both routes use Typst's typesetter; they differ only +//! in whether you run the compiler yourself. +//! +//! # Output shape +//! +//! ```typst +//! #set page(width: 800pt, height: 600pt, margin: 0pt, fill: white) +//! #import "@preview/cetz:0.5.2" +//! #cetz.canvas({ +//! import cetz.draw: * +//! // primitives go here +//! line((0pt, 0pt), (100pt, 100pt), stroke: 1pt + black) +//! circle((50pt, 50pt), radius: 3pt, fill: blue) +//! content((100pt, 200pt), text(size: 14pt)[some label]) +//! }) +//! ``` +//! +//! # Coordinate system +//! +//! Kuva's `Scene` uses SVG-style coordinates: origin at top-left, y grows +//! downward. CETZ uses mathematical coordinates: origin at bottom-left, y +//! grows upward. The backend flips y on emission so the output looks the +//! same as the SVG. +//! +//! # Limitations (T0 scaffold — to be lifted iteratively) +//! +//! - `Path` primitive (arbitrary SVG path data) is not yet translated — kuva +//! uses Paths sparingly (mostly arrowheads and curved bands) so this is a +//! moderate gap to close later. +//! - Batched primitives (`CircleBatch`, `RectBatch`) unroll to one CETZ call +//! per element. Acceptable for typical plot sizes; can be optimized later. +//! - Clip regions (`ClipStart`/`ClipEnd`) are silently dropped. +//! - Interactive features (tooltips, scripts) are not applicable to a +//! typesetting target and are dropped. + +use std::fmt::Write; + +use crate::render::color::Color; +use crate::render::render::{Primitive, Scene, TextAnchor, TextSpan}; + +/// Typst output backend. +/// +/// Construct with [`TypstBackend::default`] and call +/// [`TypstBackend::render_scene`] to get the Typst source as a `String`. +pub struct TypstBackend { + /// CETZ version to import. Pinned to a known-good version so the output + /// keeps working when Typst's package index gains new releases. + cetz_version: &'static str, +} + +impl Default for TypstBackend { + fn default() -> Self { + Self { + cetz_version: "0.5.2", + } + } +} + +impl TypstBackend { + /// Render a [`Scene`] to a Typst source string. The returned string is a + /// complete `.typ` document — write it to a file and run + /// `typst compile that_file.typ` to produce PDF/SVG/PNG. + pub fn render_scene(&self, scene: &Scene) -> String { + let mut out = String::with_capacity(8192); + + // Preamble: page setup, no margin so coordinates align with kuva's + // pixel space exactly. + let _ = write!( + out, + "#set page(width: {}pt, height: {}pt, margin: 0pt", + scene.width, scene.height + ); + if let Some(bg) = &scene.background_color { + let _ = write!(out, ", fill: {}", typst_color_named(bg)); + } + out.push_str(")\n"); + + // Default font for text. + if let Some(ff) = &scene.font_family { + let _ = writeln!(out, "#set text(font: \"{}\")", primary_font(ff)); + } + + // Pull in CETZ for the drawing primitives. + let _ = writeln!(out, "#import \"@preview/cetz:{}\"", self.cetz_version); + + // Open canvas. `length: 1pt` makes coordinates 1:1 with point units. + out.push_str("#cetz.canvas(length: 1pt, {\n import cetz.draw: *\n"); + + let h = scene.height; + for p in &scene.elements { + emit_primitive(&mut out, p, h); + } + + out.push_str("})\n"); + out + } +} + +// ── Per-primitive emission ──────────────────────────────────────────────────── + +fn emit_primitive(out: &mut String, p: &Primitive, scene_h: f64) { + match p { + Primitive::Circle { + cx, + cy, + r, + fill, + fill_opacity: _, + stroke, + stroke_width, + } => { + // CETZ coordinates are bare numbers (multiplied by canvas `length:`). + let _ = write!( + out, + " circle(({}, {}), radius: {}pt, fill: {}", + cx, + flip_y(*cy, scene_h), + r, + typst_color(fill) + ); + if let Some(s) = stroke { + let _ = write!( + out, + ", stroke: {}pt + {}", + stroke_width.unwrap_or(1.0), + typst_color(s) + ); + } else { + out.push_str(", stroke: none"); + } + out.push_str(")\n"); + } + + Primitive::Line { + x1, + y1, + x2, + y2, + stroke, + stroke_width, + stroke_dasharray, + } => { + let _ = write!( + out, + " line(({}, {}), ({}, {}), stroke: {}pt + {}", + x1, + flip_y(*y1, scene_h), + x2, + flip_y(*y2, scene_h), + stroke_width, + typst_color(stroke) + ); + if stroke_dasharray.is_some() { + out.push_str(" + (dash: \"dashed\")"); + } + out.push_str(")\n"); + } + + Primitive::Rect { + x, + y, + width, + height, + fill, + stroke, + stroke_width, + opacity: _, + } => { + let bottom_y = flip_y(*y + *height, scene_h); + let top_y = flip_y(*y, scene_h); + let _ = write!( + out, + " rect(({}, {}), ({}, {}), fill: {}", + x, + bottom_y, + *x + *width, + top_y, + typst_color(fill) + ); + if let Some(s) = stroke { + let _ = write!( + out, + ", stroke: {}pt + {}", + stroke_width.unwrap_or(1.0), + typst_color(s) + ); + } else { + out.push_str(", stroke: none"); + } + out.push_str(")\n"); + } + + Primitive::Text { + x, + y, + content, + size, + anchor, + rotate, + bold, + color, + } => { + emit_text( + out, + *x, + *y, + content, + *size, + *anchor, + *rotate, + *bold, + false, + color.as_ref(), + scene_h, + ); + } + + Primitive::RichText { + x, + y, + spans, + size, + anchor, + color, + } => { + // Flatten spans into a single content with per-span styling. + // For the prototype, concatenate into one Typst markup string + // using #emph/#strong/#underline as appropriate. + let mut flat = String::new(); + for s in spans { + write_styled_span(&mut flat, s); + } + emit_typst_content( + out, + *x, + *y, + &flat, + *size, + *anchor, + None, + false, + false, + color.as_ref(), + scene_h, + true, + ); + } + + Primitive::CircleBatch { + cx, + cy, + r, + fill, + fill_opacity: _, + stroke, + stroke_width, + } => { + for (x, y) in cx.iter().zip(cy.iter()) { + let _ = write!( + out, + " circle(({}, {}), radius: {}pt, fill: {}", + x, + flip_y(*y, scene_h), + r, + typst_color(fill) + ); + if let Some(s) = stroke { + let _ = write!( + out, + ", stroke: {}pt + {}", + stroke_width.unwrap_or(1.0), + typst_color(s) + ); + } else { + out.push_str(", stroke: none"); + } + out.push_str(")\n"); + } + } + + Primitive::RectBatch { x, y, w, h, fills } => { + for (((xi, yi), wi), (hi, fi)) in x + .iter() + .zip(y.iter()) + .zip(w.iter()) + .zip(h.iter().zip(fills.iter())) + { + let bottom_y = flip_y(*yi + *hi, scene_h); + let top_y = flip_y(*yi, scene_h); + let _ = writeln!( + out, + " rect(({}, {}), ({}, {}), fill: {}, stroke: none)", + xi, + bottom_y, + *xi + *wi, + top_y, + typst_color(fi) + ); + } + } + + Primitive::PolyLine { + points, + stroke, + stroke_width, + stroke_dasharray: _, + } => { + if points.len() < 2 { + return; + } + out.push_str(" line("); + for (i, (px, py)) in points.iter().enumerate() { + if i > 0 { + out.push_str(", "); + } + let _ = write!(out, "({}, {})", px, flip_y(*py, scene_h)); + } + let _ = writeln!( + out, + ", stroke: {}pt + {})", + stroke_width, + typst_color(stroke) + ); + } + + // TODO: Path primitive — kuva's Path uses SVG path data which would + // need conversion to CETZ's bezier()/path()/merge-path() syntax. + // Deferred to a follow-up; most kuva plots don't rely on Path. + Primitive::Path(_) => { + out.push_str(" // TODO: Primitive::Path not yet supported in Typst backend\n"); + } + + // Grouping and clipping are dropped — Typst has different concepts + // for these and they're not strictly necessary for static output. + Primitive::GroupStart { .. } + | Primitive::GroupEnd + | Primitive::ClipStart { .. } + | Primitive::ClipEnd => {} + } +} + +#[allow(clippy::too_many_arguments)] +fn emit_text( + out: &mut String, + x: f64, + y: f64, + content: &str, + size: u32, + anchor: TextAnchor, + rotate: Option, + bold: bool, + italic: bool, + color: Option<&Color>, + scene_h: f64, +) { + // Detect `$...$` regions in the label and emit them as native Typst math + // (Typst's own typesetter handles the rendering, with real math fonts). + // Plain text between math regions is escaped so Typst markup specials + // don't trigger. + use crate::render::math::{contains_math, split_segments, to_typst_math, Segment}; + let mut markup = String::with_capacity(content.len() + 8); + if contains_math(content) { + for seg in split_segments(content) { + match seg { + Segment::Text(s) => write_typst_escaped(&mut markup, s), + Segment::Math(body) => { + markup.push('$'); + markup.push_str(&to_typst_math(body)); + markup.push('$'); + } + } + } + emit_typst_content( + out, x, y, &markup, size, anchor, rotate, bold, italic, color, scene_h, true, + ); + } else { + write_typst_escaped(&mut markup, content); + emit_typst_content( + out, x, y, &markup, size, anchor, rotate, bold, italic, color, scene_h, false, + ); + } +} + +#[allow(clippy::too_many_arguments)] +fn emit_typst_content( + out: &mut String, + x: f64, + y: f64, + content_markup: &str, + size: u32, + anchor: TextAnchor, + rotate: Option, + bold: bool, + italic: bool, + color: Option<&Color>, + scene_h: f64, + content_is_markup: bool, +) { + // CETZ: content((x, y), anchor: "...", [markup]) + let anchor_str = match anchor { + TextAnchor::Start => "west", + TextAnchor::Middle => "center", + TextAnchor::End => "east", + }; + + let _ = write!(out, " content(({}, {}), ", x, flip_y(y, scene_h)); + + // Build inline text styling. + let mut style = String::new(); + let _ = write!(style, "size: {}pt", size); + if bold { + style.push_str(", weight: \"bold\""); + } + if italic { + style.push_str(", style: \"italic\""); + } + if let Some(c) = color { + let _ = write!(style, ", fill: {}", typst_color(c)); + } + + if content_is_markup { + let _ = write!(out, "[#text({})[{}]]", style, content_markup); + } else { + let _ = write!(out, "text({})[{}]", style, content_markup); + } + let _ = write!(out, ", anchor: \"{}\"", anchor_str); + if let Some(deg) = rotate { + // CETZ rotation is counter-clockwise positive; SVG rotation is + // clockwise positive. Flip the sign. + let _ = write!(out, ", angle: {}deg", -deg); + } + out.push_str(")\n"); +} + +fn write_styled_span(out: &mut String, span: &TextSpan) { + let mut s = String::new(); + write_typst_escaped(&mut s, &span.text); + if span.bold { + out.push_str("#strong["); + } + if span.italic { + out.push_str("#emph["); + } + if span.underline { + out.push_str("#underline["); + } + out.push_str(&s); + if span.underline { + out.push(']'); + } + if span.italic { + out.push(']'); + } + if span.bold { + out.push(']'); + } +} + +/// Escape Typst markup specials in plain text content. Delegates to the shared +/// escaper so the markup backend and the `math` tier stay in sync (the previous +/// local version missed `_`, `*`, `` ` ``, `<`, `>` — a label like `a_b` or +/// `*x*` was mis-typeset as subscript/emphasis). +fn write_typst_escaped(out: &mut String, s: &str) { + crate::render::math::escape_typst_markup(s, out); +} + +// ── Color & font helpers ────────────────────────────────────────────────────── + +/// Convert a kuva [`Color`] to a Typst color literal: `rgb("#aabbcc")` or +/// `rgb("#aabbccdd")` for alpha. +fn typst_color(c: &Color) -> String { + match c { + Color::Rgb(r, g, b) => format!("rgb(\"#{:02x}{:02x}{:02x}\")", r, g, b), + Color::None => "none".to_string(), + Color::Css(s) => typst_color_named(s), + } +} + +/// Convert an arbitrary CSS color string to a Typst color expression. +/// +/// Typst's `rgb()` constructor accepts hex strings only; bare named colors +/// are identifiers in Typst's standard library. For unrecognised names we +/// fall back to `black` rather than emit a parse error. +fn typst_color_named(s: &str) -> String { + if s == "none" || s.is_empty() { + return "none".to_string(); + } + if s.starts_with('#') { + return format!("rgb(\"{}\")", s); + } + // Typst's standard named colors (matches the CSS basic palette). + match s.to_lowercase().as_str() { + "black" | "gray" | "silver" | "white" | "navy" | "blue" | "aqua" | "teal" | "eastern" + | "purple" | "fuchsia" | "maroon" | "red" | "orange" | "yellow" | "olive" | "green" + | "lime" => s.to_lowercase(), + _ => "black".to_string(), + } +} + +/// Extract the primary font family name from a comma-separated CSS-style +/// `font-family` value: `"DejaVu Sans, Liberation Sans, Arial"` → `"DejaVu Sans"`. +fn primary_font(s: &str) -> String { + s.split(',') + .next() + .unwrap_or(s) + .trim() + .trim_matches('"') + .trim_matches('\'') + .to_string() +} + +/// Flip a y coordinate from SVG-space (top-left origin, y down) to CETZ-space +/// (bottom-left origin, y up). +#[inline] +fn flip_y(y: f64, scene_h: f64) -> f64 { + scene_h - y +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::render::color::Color; + use crate::render::render::{Primitive, Scene, TextAnchor}; + + fn empty_scene(w: f64, h: f64) -> Scene { + Scene { + width: w, + height: h, + background_color: Some("white".into()), + text_color: None, + font_family: Some("DejaVu Sans, Arial".into()), + elements: Vec::new(), + defs: Vec::new(), + has_tooltips: false, + interactive: false, + axis_meta: None, + scripts: Vec::new(), + } + } + + #[test] + fn empty_scene_emits_valid_preamble() { + let s = empty_scene(800.0, 600.0); + let typst = TypstBackend::default().render_scene(&s); + assert!(typst.contains("#set page(width: 800pt, height: 600pt")); + assert!(typst.contains("margin: 0pt")); + assert!(typst.contains("#import \"@preview/cetz:")); + assert!(typst.contains("#cetz.canvas(")); + assert!(typst.contains("})\n")); + } + + #[test] + fn circle_primitive_emits_cetz_circle() { + let mut s = empty_scene(100.0, 100.0); + s.elements.push(Primitive::Circle { + cx: 50.0, + cy: 50.0, + r: 5.0, + fill: Color::Rgb(0, 0, 255), + fill_opacity: None, + stroke: None, + stroke_width: None, + }); + let typst = TypstBackend::default().render_scene(&s); + // y flipped: 100 - 50 = 50 (symmetric case). + assert!(typst.contains("circle((50, 50), radius: 5pt")); + assert!(typst.contains("fill: rgb(\"#0000ff\")")); + assert!(typst.contains("stroke: none")); + } + + #[test] + fn line_primitive_emits_cetz_line_with_y_flip() { + let mut s = empty_scene(200.0, 100.0); + s.elements.push(Primitive::Line { + x1: 0.0, + y1: 10.0, + x2: 200.0, + y2: 10.0, + stroke: Color::Rgb(0, 0, 0), + stroke_width: 1.0, + stroke_dasharray: None, + }); + let typst = TypstBackend::default().render_scene(&s); + // y=10 → flipped = 100-10 = 90. + assert!(typst.contains("line((0, 90), (200, 90)")); + assert!(typst.contains("stroke: 1pt + rgb(\"#000000\")")); + } + + #[test] + fn text_primitive_emits_cetz_content() { + let mut s = empty_scene(200.0, 100.0); + s.elements.push(Primitive::Text { + x: 50.0, + y: 80.0, + content: "hello".into(), + size: 14, + anchor: TextAnchor::Middle, + rotate: None, + bold: false, + color: None, + }); + let typst = TypstBackend::default().render_scene(&s); + assert!(typst.contains("content((50, 20)")); + assert!(typst.contains("text(size: 14pt)[hello]")); + assert!(typst.contains("anchor: \"center\"")); + } + + #[test] + fn text_with_typst_special_chars_is_escaped() { + let mut s = empty_scene(200.0, 100.0); + s.elements.push(Primitive::Text { + x: 10.0, + y: 10.0, + content: "Price: $5 [USD] #1".into(), + size: 12, + anchor: TextAnchor::Start, + rotate: None, + bold: false, + color: None, + }); + let typst = TypstBackend::default().render_scene(&s); + // $, [, ], #, and @ must be backslash-escaped to render literally. + assert!(typst.contains("Price: \\$5 \\[USD\\] \\#1")); + } + + #[test] + fn rect_primitive_emits_cetz_rect() { + let mut s = empty_scene(200.0, 100.0); + s.elements.push(Primitive::Rect { + x: 10.0, + y: 20.0, + width: 50.0, + height: 30.0, + fill: Color::Rgb(255, 0, 0), + stroke: None, + stroke_width: None, + opacity: None, + }); + let typst = TypstBackend::default().render_scene(&s); + // y=20, h=30; top=flip(20)=80, bottom=flip(50)=50. + assert!(typst.contains("rect((10, 50), (60, 80)")); + assert!(typst.contains("fill: rgb(\"#ff0000\")")); + } + + #[test] + fn rotation_sign_is_flipped() { + // SVG rotation is clockwise positive; CETZ is counter-clockwise. + let mut s = empty_scene(100.0, 100.0); + s.elements.push(Primitive::Text { + x: 50.0, + y: 50.0, + content: "rot".into(), + size: 12, + anchor: TextAnchor::Middle, + rotate: Some(-90.0), + bold: false, + color: None, + }); + let typst = TypstBackend::default().render_scene(&s); + assert!(typst.contains("angle: 90deg")); + } +} diff --git a/src/fonts.rs b/src/fonts.rs index bb66c68..0e3313d 100644 --- a/src/fonts.rs +++ b/src/fonts.rs @@ -30,18 +30,51 @@ use flate2::read::GzDecoder; /// Gzip-compressed bytes of DejaVu Sans Regular, embedded at compile time. const DEJAVU_SANS_GZ: &[u8] = include_bytes!("../assets/fonts/DejaVuSans.ttf.gz"); +/// Inflate a gzipped embedded asset into a `Vec`. `what` names the asset +/// for the panic message if the stream is corrupt. +fn inflate_gz(gz: &[u8], capacity: usize, what: &str) -> Vec { + let mut out = Vec::with_capacity(capacity); + GzDecoder::new(gz) + .read_to_end(&mut out) + .unwrap_or_else(|_| panic!("bundled {what} gzip stream is corrupt")); + out +} + /// Returns the inflated DejaVu Sans TTF bytes. Inflated once and cached. pub(crate) fn dejavu_sans() -> &'static [u8] { static BYTES: OnceLock> = OnceLock::new(); - BYTES.get_or_init(|| { - let mut out = Vec::with_capacity(800_000); - GzDecoder::new(DEJAVU_SANS_GZ) - .read_to_end(&mut out) - .expect("bundled DejaVu Sans gzip stream is corrupt"); - out + BYTES.get_or_init(|| inflate_gz(DEJAVU_SANS_GZ, 800_000, "DejaVu Sans")) +} + +/// Gzip-compressed New Computer Modern Math (OFL/GUST), embedded at compile +/// time. Used only by the `math` feature, as the math font fed to the typst +/// compiler. ~1.1 MB inflated; ~0.75 MB on disk. +#[cfg(feature = "math")] +const NEWCM_MATH_GZ: &[u8] = include_bytes!("../assets/fonts/NewCMMath-Regular.otf.gz"); + +/// Returns the inflated New Computer Modern Math OTF bytes. Inflated once and +/// cached. Bundled (rather than pulled from `typst-assets`) so the `math` +/// feature ships ~1 MB of font rather than ~15 MB. +#[cfg(feature = "math")] +pub(crate) fn newcm_math() -> &'static [u8] { + static BYTES: OnceLock> = OnceLock::new(); + BYTES.get_or_init(|| inflate_gz(NEWCM_MATH_GZ, 1_200_000, "NewCM Math")) +} + +/// Bundled DejaVu Sans parsed as a `fontdue::Font`, for the raster backend's +/// glyph rasterisation. +#[cfg(feature = "png")] +pub(crate) fn shared_font() -> &'static fontdue::Font { + static FONT: OnceLock = OnceLock::new(); + FONT.get_or_init(|| { + fontdue::Font::from_bytes(dejavu_sans(), fontdue::FontSettings::default()) + .expect("bundled DejaVu Sans TTF is valid") }) } +// Used only by `dejavu_sans_style_block` (the `embed_font` SVG path); the +// `fonts` module is also compiled for `png`/`pdf`/`math`, where this is dead. +#[cfg(feature = "embed_font")] fn base64_encode(data: &[u8]) -> String { const TABLE: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; let mut out = String::with_capacity(data.len().div_ceil(3) * 4); @@ -76,6 +109,7 @@ fn base64_encode(data: &[u8]) -> String { /// Returns a `