diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 68659ebe..cdc4ab71 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -78,9 +78,9 @@ jobs: - name: Build library (no features) at MSRV run: cargo +${{ steps.msrv.outputs.version }} build - # Excludes `pdf`: krilla (the pdf feature's backend) needs a newer Rust - # than package.rust-version — verified separately by the pdf-msrv job - # below, at its own higher floor. + # Excludes `pdf`: krilla (the pdf feature's backend) and typst (its + # math typesetter) need a newer Rust than package.rust-version — + # verified separately by the pdf-msrv job below, at its own higher floor. - name: Build library (CLI + non-PDF backends) at MSRV run: cargo +${{ steps.msrv.outputs.version }} build --features cli,png,embed_font @@ -101,9 +101,11 @@ jobs: - uses: Swatinem/rust-cache@v2 - # `full` = png + pdf + embed_font; this is the combination the release - # workflow's pre-built binaries use (features: cli,full), so verify it - # builds cleanly at the pdf feature's own real MSRV floor rather than - # package.rust-version (which no longer covers it). - - name: Build library (CLI + all backends, including pdf) at pdf's MSRV + # `full` = png + pdf + embed_font + typst; this is the combination the + # release workflow's pre-built binaries use (features: cli,full), so + # verify it builds cleanly at the pdf feature's own real MSRV floor + # rather than package.rust-version (which no longer covers it). Since + # `pdf` also carries the typst math tier, this job covers typst's MSRV + # too (typst 0.14 needs 1.89 < krilla's 1.92). + - name: Build library (CLI + all backends, including pdf + typst math) at pdf's MSRV run: cargo +${{ steps.pdf_msrv.outputs.version }} build --features cli,full diff --git a/CHANGELOG.md b/CHANGELOG.md index fb2dc760..dc8d6833 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- **Typst tier for math in labels** (part of the `pdf` feature) — labels containing `$...$` may now be typeset with the Typst compiler (linked as a library — no external binary) for real 2-D math: stacked fractions, radicals with vinculum, large operators, math italic. The whole label is compiled and embedded as an SVG fragment (SVG backend), pixmap (PNG), or through the PDF pipeline, with math color following the label color. The typesetter rides the `pdf` feature rather than a feature of its own: krilla (the PDF backend) is Typst's own PDF stack, so `pdf` was already the crate's one deliberately heavy, Rust >= 1.92 feature — enabling it now buys typeset math in every vector/raster backend, and `cargo build --features pdf` (or `full`) is the only switch to flip. Builds on the always-on inline-Unicode lookup tier from 0.3.0, which remains the fallback everywhere: the terminal backend, builds without `pdf`, and any label Typst fails to compile (one warning per distinct label). Bundles New Computer Modern Math (~0.75 MB compressed) as the math font. Note: Typst math is not LaTeX (`mc` is one identifier — write `m c`). See *Reference → Math in Labels*. +- **Math in `TextPlot` bodies (typst tier)** — `$...$` inside a markdown body is spliced into the wrapped paragraph as a typeset fragment: kuva's own word-wrap treats each fragment as one unbreakable word (sized by its typeset width), surrounding words stay real selectable text in SVG/PDF, punctuation directly after math stays flush (`$x^2$.`), and a line holding a tall fragment (stacked fraction, radical) grows its leading so neighbouring lines never collide. Terminal output and builds without `pdf` lower body math to inline Unicode exactly as before. +- **`TypstBackend`** (feature `typst`, zero deps) — emit a CETZ-based Typst document for external compilation with `typst compile`; `$...$` regions become native Typst math, typeset with fonts matching the surrounding document. Full primitive coverage: `Path` (arrowheads, chord ribbons, sankey flows, venn outlines) lowers to CETZ `merge-path` calls with arcs converted to cubic Béziers, and clip regions map to Typst's `box(clip: true)` — so quiver/network arrowheads and plot-area clipping survive the round trip. - **`CoveragePlot` coverage-threshold lines** — `with_coverage_threshold(depth)` / `with_coverage_threshold_labeled(depth, label)` draw a dashed horizontal line at a given depth on every coverage track (e.g. a minimum-coverage cutoff). CLI: `kuva coverage --min-coverage ` (repeatable). Built on new `PlotTrack::with_hline` / `with_reference_line` support in the `TrackStack` primitive, so any `PlotTrack` can carry horizontal reference lines in its own y-scale. - **`BrickPlot` marker lines** — `with_vline(x)` / `with_vline_labeled(x, label)` draw a dashed vertical line across all rows at a reference coordinate (locus, primer boundary, variant position), and `with_marker_line(ReferenceLine)` takes a fully-styled line. Drawn in the brick's own reference-coordinate system, so they respect `with_x_offset`/`with_x_origin`. (`ReferenceLine`/`Orientation` now derive `Debug`.) - **Survival clinical figures.** `SurvivalPlot` gains three survminer-style options for publication-quality Kaplan-Meier figures: `.with_risk_table(true)` draws a number-at-risk table below the plot (one row per group, counts at each x-axis tick; space is reserved automatically via a new `Layout::risk_table_rows` / `ComputedLayout::risk_table_extra` bottom-margin mechanism); `.with_median_lines(true)` draws dashed median-survival reference lines; and `.with_logrank_pvalue(true)` computes the log-rank test p-value from the raw data (exact Mantel-Haenszel for two groups, `Σ (O−E)²/E` approximation for 3+) via a new chi-square survival function, annotating it and overriding any `with_pvalue_text`. CLI: `kuva survival --risk-table --median-lines --logrank`. diff --git a/Cargo.lock b/Cargo.lock index 877cbf73..a9db40a0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -117,6 +117,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.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4087686b4b0a3427190bae57a1d9a478dbb2d40c5dc1bd6e2b6d797913bdd348" +dependencies = [ + "object", +] + [[package]] name = "arrayref" version = "0.3.9" @@ -155,7 +173,7 @@ checksum = "c12b576ef18c1deb80925a248b25ad84f419198d791b8e293fc6aaa60441fe90" dependencies = [ "bytes", "half", - "num-bigint", + "num-bigint 0.5.1", "num-traits", ] @@ -212,6 +230,12 @@ 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" @@ -224,6 +248,44 @@ version = "0.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5" +[[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" @@ -241,6 +303,9 @@ name = "bitflags" version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" +dependencies = [ + "serde_core", +] [[package]] name = "brotli" @@ -269,6 +334,12 @@ version = "3.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" +[[package]] +name = "by_address" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64fa3c856b712db6612c019f14756e64e4bcea13337a6b33b696333a9eaa2d06" + [[package]] name = "bytemuck" version = "1.25.0" @@ -286,7 +357,7 @@ checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -331,6 +402,24 @@ 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 0.4.6", + "num-traits", +] + +[[package]] +name = "chinese-variant" +version = "1.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d1275808335c5583ea45a4141d631dafcb36b79fa8a5e10b00e356b6af3e08a" + [[package]] name = "chrono" version = "0.4.44" @@ -369,6 +458,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" @@ -400,7 +499,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -419,6 +518,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" @@ -437,6 +551,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 2.0.117", +] + [[package]] name = "const-random" version = "0.1.18" @@ -527,9 +665,9 @@ dependencies = [ [[package]] name = "crossbeam-epoch" -version = "0.9.20" +version = "0.9.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" dependencies = [ "crossbeam-utils", ] @@ -573,12 +711,70 @@ 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" + +[[package]] +name = "displaydoc" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[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.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07f808d588c10e464ea6f7d3eaed500049eff30aaac103460f61828c2d65b3eb" +dependencies = [ + "enum-ordinalize-derive", +] + +[[package]] +name = "enum-ordinalize-derive" +version = "4.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42e528e2d34ba8a67a1a650b86beae8ef69fc5fdb638016f386b973226590432" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -609,6 +805,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.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" + [[package]] name = "fdeflate" version = "0.3.7" @@ -654,12 +873,27 @@ dependencies = [ "num-traits", ] +[[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" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[package]] +name = "font-types" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39a654f404bbcbd48ea58c617c2993ee91d1cb63727a37bf2323a4edeed1b8c5" +dependencies = [ + "bytemuck", +] + [[package]] name = "font-types" version = "0.11.3" @@ -702,6 +936,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 = "futures-core" version = "0.3.32" @@ -769,6 +1012,12 @@ dependencies = [ "weezl", ] +[[package]] +name = "glidesort" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2e102e6eb644d3e0b186fc161e4460417880a0a0b87d235f2e5b8fb30f2e9e0" + [[package]] name = "glob" version = "0.3.3" @@ -810,6 +1059,97 @@ version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +[[package]] +name = "hayagriva" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cb69425736f184173b3ca6e27fcba440a61492a790c786b1c6af7e06a03e575" +dependencies = [ + "biblatex", + "ciborium", + "citationberg", + "indexmap", + "paste", + "roman-numerals-rs", + "serde", + "serde_yaml", + "thiserror", + "unic-langid", + "unicode-segmentation", + "unscanny", + "url", +] + +[[package]] +name = "hayro" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "048488ba88552bb0fb2a7e4001c64d5bed65d1a92167186a1bb9151571f32e60" +dependencies = [ + "bytemuck", + "hayro-interpret", + "image 0.25.10", + "kurbo 0.12.0", +] + +[[package]] +name = "hayro-font" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10e7e97ce840a6a70e7901e240ec65ba61106b66b37a4a1b899a2ce484248463" +dependencies = [ + "log", + "phf", +] + +[[package]] +name = "hayro-interpret" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56204c972d08e844f3db13b1e14be769f846e576699b46d4f4637cc4f8f70102" +dependencies = [ + "bitflags 2.11.0", + "hayro-font", + "hayro-syntax", + "kurbo 0.12.0", + "log", + "moxcms 0.7.11", + "phf", + "rustc-hash", + "siphasher", + "skrifa 0.37.0", + "smallvec", + "yoke 0.8.3", +] + +[[package]] +name = "hayro-svg" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8c673304cec6e0dfd3b4f71fccecd45646899aa70279b62d3f933842abc4ac5" +dependencies = [ + "base64 0.22.1", + "hayro-interpret", + "image 0.25.10", + "kurbo 0.12.0", + "siphasher", + "xmlwriter", +] + +[[package]] +name = "hayro-syntax" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9e5c7dbc0f11dc42775d1a6cc00f5f5137b90b6288dd7fe5f71d17b14d10be" +dependencies = [ + "flate2", + "kurbo 0.12.0", + "log", + "rustc-hash", + "smallvec", + "zune-jpeg 0.4.21", +] + [[package]] name = "heck" version = "0.5.0" @@ -822,6 +1162,12 @@ 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 = "iana-time-zone" version = "0.1.65" @@ -847,68 +1193,345 @@ dependencies = [ ] [[package]] -name = "image" -version = "0.24.9" +name = "icu_collections" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5690139d2f55868e080017335e4b94cb7414274c74f1669c84fb5feba2c9f69d" +checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" dependencies = [ - "bytemuck", - "byteorder", - "color_quant", - "exr", - "gif 0.13.3", - "jpeg-decoder", - "num-traits", - "png 0.17.16", - "qoi", - "tiff", + "displaydoc", + "serde", + "yoke 0.7.5", + "zerofrom", + "zerovec 0.10.4", ] [[package]] -name = "image-webp" -version = "0.2.4" +name = "icu_collections" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "525e9ff3e1a4be2fbea1fdf0e98686a6d98b4d8f937e1bf7402245af1909e8c3" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" dependencies = [ - "byteorder-lite", - "quick-error", + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke 0.8.3", + "zerofrom", + "zerovec 0.11.6", ] [[package]] -name = "imagesize" -version = "0.14.0" +name = "icu_locale_core" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09e54e57b4c48b40f7aec75635392b12b3421fa26fe8b4332e63138ed278459c" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = [ + "displaydoc", + "litemap 0.8.2", + "tinystr 0.8.3", + "writeable 0.6.3", + "zerovec 0.11.6", +] [[package]] -name = "indexmap" -version = "2.13.0" +name = "icu_locid" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" dependencies = [ - "equivalent", - "hashbrown 0.16.1", + "displaydoc", + "litemap 0.7.5", + "tinystr 0.7.6", + "writeable 0.5.5", + "zerovec 0.10.4", ] [[package]] -name = "is-terminal" -version = "0.4.17" +name = "icu_locid_transform" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" +checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" dependencies = [ - "hermit-abi", - "libc", - "windows-sys", + "displaydoc", + "icu_locid", + "icu_locid_transform_data", + "icu_provider 1.5.0", + "tinystr 0.7.6", + "zerovec 0.10.4", ] [[package]] -name = "is_terminal_polyfill" -version = "1.70.2" +name = "icu_locid_transform_data" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" +checksum = "7515e6d781098bf9f7205ab3fc7e9709d34554ae0b21ddbcb5febfa4bc7df11d" [[package]] -name = "itertools" +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.3", + "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 2.0.117", +] + +[[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.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104" +dependencies = [ + "bytemuck", + "byteorder-lite", + "color_quant", + "gif 0.14.2", + "image-webp", + "moxcms 0.8.1", + "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 = "imagesize" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09e54e57b4c48b40f7aec75635392b12b3421fa26fe8b4332e63138ed278459c" + +[[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" @@ -953,6 +1576,15 @@ dependencies = [ "wasm-bindgen", ] +[[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 = "krilla" version = "0.8.2" @@ -965,20 +1597,20 @@ dependencies = [ "float-cmp", "gif 0.14.2", "image-webp", - "imagesize", + "imagesize 0.14.0", "indexmap", "pdf-writer", "png 0.18.1", "rustc-hash", "rustybuzz", "siphasher", - "skrifa", + "skrifa 0.42.1", "smallvec", "subsetter", - "tiny-skia-path", + "tiny-skia-path 0.12.0", "xmp-writer", - "yoke", - "zune-jpeg", + "yoke 0.8.3", + "zune-jpeg 0.5.12", ] [[package]] @@ -990,11 +1622,33 @@ dependencies = [ "flate2", "fontdb", "krilla", - "resvg", - "skrifa", + "resvg 0.47.0", + "skrifa 0.42.1", + "smallvec", + "tiny-skia 0.12.0", + "usvg 0.47.0", +] + +[[package]] +name = "kurbo" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c62026ae44756f8a599ba21140f350303d4f08dcdcc71b5ad9c9bb8128c13c62" +dependencies = [ + "arrayvec", + "euclid", + "smallvec", +] + +[[package]] +name = "kurbo" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce9729cc38c18d86123ab736fd2e7151763ba226ac2490ec092d1dd148825e32" +dependencies = [ + "arrayvec", + "euclid", "smallvec", - "tiny-skia", - "usvg", ] [[package]] @@ -1024,17 +1678,21 @@ dependencies = [ "csv", "flate2", "fontdue", - "image", + "image 0.24.9", "krilla", "krilla-svg", "parquet", "png 0.18.1", - "rand", + "rand 0.9.4", "rand_distr", "ryu", "trybuild", "ttf-parser 0.25.1", - "usvg", + "typst", + "typst-library", + "typst-render", + "typst-svg", + "usvg 0.47.0", ] [[package]] @@ -1055,6 +1713,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.7", + "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" @@ -1078,9 +1776,9 @@ checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] name = "memmap2" -version = "0.9.11" +version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1219ed1b7f229ee7104d281dd01d6802fe28bb6e95d292942c4daacdeb798c0" +checksum = "714098028fe011992e1c3962653c96b2d578c4b4bce9036e15ff220319b1e0e3" dependencies = [ "libc", ] @@ -1095,6 +1793,42 @@ dependencies = [ "simd-adler32", ] +[[package]] +name = "moxcms" +version = "0.7.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac9557c559cd6fc9867e122e20d2cbefc9ca29d80d027a8e39310920ed2f0a97" +dependencies = [ + "num-traits", + "pxfm", +] + +[[package]] +name = "moxcms" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b" +dependencies = [ + "num-traits", + "pxfm", +] + +[[package]] +name = "mutate_once" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13d2233c9842d08cfe13f9eac96e207ca6a2ea10b80259ebe8ad0268be27d2af" + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + [[package]] name = "num-bigint" version = "0.5.1" @@ -1114,6 +1848,12 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + [[package]] name = "num-integer" version = "0.1.46" @@ -1133,6 +1873,15 @@ dependencies = [ "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" @@ -1152,10 +1901,57 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" [[package]] -name = "parquet" -version = "59.2.0" +name = "palette" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7065842956a20c2a536924ce8e4d9955f7422451511b9eb7500d7bfe5077e59c" +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 2.0.117", +] + +[[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 = "parquet" +version = "59.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7065842956a20c2a536924ce8e4d9955f7422451511b9eb7500d7bfe5077e59c" dependencies = [ "ahash", "arrow-array", @@ -1172,7 +1968,7 @@ dependencies = [ "half", "hashbrown 0.17.1", "lz4_flex", - "num-bigint", + "num-bigint 0.5.1", "num-integer", "num-traits", "seq-macro", @@ -1182,6 +1978,12 @@ dependencies = [ "zstd", ] +[[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.15.0" @@ -1194,6 +1996,55 @@ 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 2.0.117", +] + +[[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" @@ -1206,12 +2057,34 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" +[[package]] +name = "pixglyph" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47945e80a49d08350e4a007ac4294c6498d23956c18ea5e6ff480dc93d31643c" +dependencies = [ + "ttf-parser 0.25.1", +] + [[package]] name = "pkg-config" version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" +[[package]] +name = "plist" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da1d65da6dd5d1e44199ac0f58712d241c0f439f80adea8924d832384087f85" +dependencies = [ + "base64 0.22.1", + "indexmap", + "quick-xml 0.41.0", + "serde", + "time", +] + [[package]] name = "png" version = "0.17.16" @@ -1247,6 +2120,39 @@ dependencies = [ "arrayvec", ] +[[package]] +name = "portable-atomic" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d20d5497ef88037a52ff98267d066e7f11fcc5e99bbfbd58a42336193aacec3" + +[[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" @@ -1256,6 +2162,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" @@ -1265,6 +2177,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.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d55d956fa96f5ec02be2e13af0e20391a5aa83d6a074e3ad368959d0fab299ea" + +[[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" @@ -1280,6 +2214,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.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e660451e55124f798a69a5af3f49ccfbefbd41910eefd25caf2393e1f3473ec1" +dependencies = [ + "memchr", +] + [[package]] name = "quote" version = "1.0.45" @@ -1295,14 +2248,33 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "rand" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22f6172bdec972074665ed81ed53b71da00bfc44b65a753cfde883ec4c702a1a" +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]] @@ -1312,9 +2284,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" @@ -1331,7 +2309,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a8615d50dcf34fa31f7ab52692afec947c4dd0ab803cc87cb3b0b4570ff7463" dependencies = [ "num-traits", - "rand", + "rand 0.9.4", ] [[package]] @@ -1354,6 +2332,16 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "read-fonts" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6717cf23b488adf64b9d711329542ba34de147df262370221940dfabc2c91358" +dependencies = [ + "bytemuck", + "font-types 0.10.1", +] + [[package]] name = "read-fonts" version = "0.39.2" @@ -1361,7 +2349,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4ed38b89c2c77ff968c524145ad65fb010f38af5c7a224b53b81d47ac2daa81" dependencies = [ "bytemuck", - "font-types", + "font-types 0.11.3", +] + +[[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]] @@ -1393,6 +2390,23 @@ version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" +[[package]] +name = "resvg" +version = "0.45.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8928798c0a55e03c9ca6c4c6846f76377427d2c1e1f7e6de3c06ae57942df43" +dependencies = [ + "gif 0.13.3", + "image-webp", + "log", + "pico-args", + "rgb", + "svgtypes 0.15.3", + "tiny-skia 0.11.4", + "usvg 0.45.1", + "zune-jpeg 0.4.21", +] + [[package]] name = "resvg" version = "0.47.0" @@ -1404,10 +2418,10 @@ dependencies = [ "log", "pico-args", "rgb", - "svgtypes", - "tiny-skia", - "usvg", - "zune-jpeg", + "svgtypes 0.16.1", + "tiny-skia 0.12.0", + "usvg 0.47.0", + "zune-jpeg 0.5.12", ] [[package]] @@ -1425,6 +2439,12 @@ 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" @@ -1440,6 +2460,16 @@ dependencies = [ "memchr", ] +[[package]] +name = "rust_decimal" +version = "1.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be2a24f50780bc85f09cc6ac299bdf1424302742d77221106859c9d8b102126a" +dependencies = [ + "arrayvec", + "num-traits", +] + [[package]] name = "rustc-hash" version = "2.1.1" @@ -1494,6 +2524,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 = "semver" version = "1.0.28" @@ -1533,7 +2569,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -1549,6 +2585,15 @@ 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_spanned" version = "1.1.1" @@ -1558,6 +2603,19 @@ dependencies = [ "serde_core", ] +[[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 = "1.3.0" @@ -1591,6 +2649,16 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" +[[package]] +name = "skrifa" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c31071dedf532758ecf3fed987cdb4bd9509f900e026ab684b4ecb81ea49841" +dependencies = [ + "bytemuck", + "read-fonts 0.35.0", +] + [[package]] name = "skrifa" version = "0.42.1" @@ -1598,7 +2666,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c34617370ae968efb7161bb2beb517d9084659aae19e24b89e3db25b46e4564" dependencies = [ "bytemuck", - "read-fonts", + "read-fonts 0.39.2", ] [[package]] @@ -1628,12 +2696,31 @@ version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b6b67fb9a61334225b5b790716f609cd58395f895b3fe8b328786812a40bc3b" +[[package]] +name = "spin" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3763264f6b73151db08c50ff20d7d8a0b8796e021cdea7ceedad07b80155fa0e" + [[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" @@ -1649,25 +2736,56 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +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 2.0.117", +] + [[package]] name = "subsetter" version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38803281d1c23166c5ebcb455439a5d2afe711cc909cf88af72448c297756ad6" dependencies = [ - "kurbo", + "kurbo 0.13.1", "rustc-hash", - "skrifa", + "skrifa 0.42.1", "write-fonts", ] +[[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 = "svgtypes" version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "695b5790b3131dafa99b3bbfd25a216edb3d216dad9ca208d4657bfb8f2abc3d" dependencies = [ - "kurbo", + "kurbo 0.13.1", "siphasher", ] @@ -1683,31 +2801,89 @@ dependencies = [ ] [[package]] -name = "synstructure" -version = "0.13.2" +name = "syn" +version = "3.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" dependencies = [ "proc-macro2", "quote", - "syn", + "unicode-ident", ] [[package]] -name = "target-triple" -version = "1.0.0" +name = "synstructure" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "591ef38edfb78ca4771ee32cf494cb8771944bee237a9b91fc9c1424ac4b777b" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] [[package]] -name = "termcolor" -version = "1.4.1" +name = "syntect" +version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +checksum = "656b45c05d95a5704399aeef6bd0ddec7b2b3531b7c9e900abbf7c4d2190c925" +dependencies = [ + "bincode", + "fancy-regex", + "flate2", + "fnv", + "once_cell", + "plist", + "regex-syntax", + "serde", + "serde_derive", + "serde_json", + "thiserror", + "walkdir", + "yaml-rust", +] + +[[package]] +name = "target-triple" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "591ef38edfb78ca4771ee32cf494cb8771944bee237a9b91fc9c1424ac4b777b" + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" dependencies = [ "winapi-util", ] +[[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.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09a43598840e33d5b0331f38c5e30d13bb11c11210a4b58f0d9b18a5a5eefcd9" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43cbfe0cf76104d42a574802844187e84a305e531ed54455f11fbde0f10541cd" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + [[package]] name = "tiff" version = "0.9.1" @@ -1719,6 +2895,36 @@ dependencies = [ "weezl", ] +[[package]] +name = "time" +version = "0.3.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e1d5e639ff6bab73cb6885cc7e7b1de96c3f32c68ec55f3952614bec1092244" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" + +[[package]] +name = "time-macros" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85" +dependencies = [ + "num-conv", + "time-core", +] + [[package]] name = "tiny-keccak" version = "2.0.2" @@ -1728,6 +2934,21 @@ dependencies = [ "crunchy", ] +[[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 0.11.4", +] + [[package]] name = "tiny-skia" version = "0.12.0" @@ -1740,7 +2961,18 @@ dependencies = [ "cfg-if", "log", "png 0.18.1", - "tiny-skia-path", + "tiny-skia-path 0.12.0", +] + +[[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]] @@ -1755,104 +2987,516 @@ dependencies = [ ] [[package]] -name = "tinytemplate" -version = "1.2.1" +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 0.6.9", + "toml_datetime 0.6.11", + "toml_edit", +] + +[[package]] +name = "toml" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee" +dependencies = [ + "indexmap", + "serde_core", + "serde_spanned 1.1.1", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 1.0.4", +] + +[[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_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[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 0.6.9", + "toml_datetime 0.6.11", + "toml_write", + "winnow 0.7.15", +] + +[[package]] +name = "toml_parser" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +dependencies = [ + "winnow 1.0.4", +] + +[[package]] +name = "toml_write" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" + +[[package]] +name = "toml_writer" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" + +[[package]] +name = "trybuild" +version = "1.0.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06649c6f63d86604ba0c8950d5a1829fc9a17afd70fc6629f481d75b6a624c78" +dependencies = [ + "glob", + "serde", + "serde_derive", + "serde_json", + "target-triple", + "termcolor", + "toml 1.1.2+spec-1.1.0", +] + +[[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 = "twox-hash" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ea3136b675547379c4bd395ca6b938e5ad3c3d20fad76e7fe85f9e0d011419c" + +[[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 0.8.23", + "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.10", + "indexmap", + "kamadak-exif", + "kurbo 0.12.0", + "lipsum", + "memchr", + "palette", + "phf", + "png 0.17.16", + "qcms", + "rayon", + "regex", + "regex-syntax", + "roxmltree 0.20.0", + "rust_decimal", + "rustc-hash", + "rustybuzz", + "serde", + "serde_json", + "serde_yaml", + "siphasher", + "smallvec", + "syntect", + "time", + "toml 0.8.23", + "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 0.45.1", + "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 2.0.117", +] + +[[package]] +name = "typst-realize" +version = "0.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" +checksum = "f7ffe964757fb93d2e98978aa2a74ee85b0f94c8643e8f3550737258b58f39d8" dependencies = [ - "serde", - "serde_json", + "arrayvec", + "bumpalo", + "comemo", + "ecow", + "regex", + "typst-library", + "typst-macros", + "typst-syntax", + "typst-timing", + "typst-utils", ] [[package]] -name = "tinyvec" -version = "1.10.0" +name = "typst-render" +version = "0.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +checksum = "1baabef8c01dd7150380592811bf37af9b2498be86043834ddd629d1bcb48ccb" dependencies = [ - "tinyvec_macros", + "bytemuck", + "comemo", + "hayro", + "image 0.25.10", + "pixglyph", + "resvg 0.45.1", + "tiny-skia 0.11.4", + "ttf-parser 0.25.1", + "typst-assets", + "typst-library", + "typst-macros", + "typst-timing", ] [[package]] -name = "tinyvec_macros" -version = "0.1.1" +name = "typst-svg" +version = "0.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" +checksum = "e46b811837ade1f0243ef0d8bf3fb06d166443090eac22c28643f374c2ccdc9d" +dependencies = [ + "base64 0.22.1", + "comemo", + "ecow", + "flate2", + "hayro", + "hayro-svg", + "image 0.25.10", + "rustc-hash", + "ttf-parser 0.25.1", + "typst-assets", + "typst-library", + "typst-macros", + "typst-timing", + "typst-utils", + "xmlparser", + "xmlwriter", +] [[package]] -name = "toml" -version = "1.1.2+spec-1.1.0" +name = "typst-syntax" +version = "0.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee" +checksum = "a95d9192060e23b1e491b0b94dff676acddc92a4d672aeb8ca3890a5a734e879" dependencies = [ - "indexmap", - "serde_core", - "serde_spanned", - "toml_datetime", - "toml_parser", - "toml_writer", - "winnow", + "ecow", + "rustc-hash", + "serde", + "toml 0.8.23", + "typst-timing", + "typst-utils", + "unicode-ident", + "unicode-math-class", + "unicode-script", + "unicode-segmentation", + "unscanny", ] [[package]] -name = "toml_datetime" -version = "1.1.1+spec-1.1.0" +name = "typst-timing" +version = "0.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +checksum = "7be94f8faf19841b49574ef5c7fd7a12e2deb7c3d8deba5a596f35d2222024cd" dependencies = [ - "serde_core", + "parking_lot", + "serde", + "serde_json", ] [[package]] -name = "toml_parser" -version = "1.1.2+spec-1.1.0" +name = "typst-utils" +version = "0.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +checksum = "a3966c92e8fa48c7ce898130d07000d985f18206d92b250f0f939287fbccdee3" dependencies = [ - "winnow", + "once_cell", + "portable-atomic", + "rayon", + "rustc-hash", + "siphasher", + "thin-vec", + "unicode-math-class", ] [[package]] -name = "toml_writer" -version = "1.1.1+spec-1.1.0" +name = "unic-langid" +version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" +checksum = "a28ba52c9b05311f4f6e62d5d9d46f094bd6e84cb8df7b3ef952748d752a7d05" +dependencies = [ + "unic-langid-impl", + "unic-langid-macros", +] [[package]] -name = "trybuild" -version = "1.0.118" +name = "unic-langid-impl" +version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06649c6f63d86604ba0c8950d5a1829fc9a17afd70fc6629f481d75b6a624c78" +checksum = "dce1bf08044d4b7a94028c93786f8566047edc11110595914de93362559bc658" dependencies = [ - "glob", "serde", - "serde_derive", - "serde_json", - "target-triple", - "termcolor", - "toml", + "tinystr 0.8.3", ] [[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" +name = "unic-langid-macros" +version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31" +checksum = "d5957eb82e346d7add14182a3315a7e298f04e1ba4baac36f7f0dbfedba5fc25" dependencies = [ - "core_maths", + "proc-macro-hack", + "tinystr 0.8.3", + "unic-langid-impl", + "unic-langid-macros-impl", ] [[package]] -name = "twox-hash" -version = "2.1.2" +name = "unic-langid-macros-impl" +version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ea3136b675547379c4bd395ca6b938e5ad3c3d20fad76e7fe85f9e0d011419c" +checksum = "a1249a628de3ad34b821ecb1001355bca3940bcb2f88558f1a8bd82e977f75b5" +dependencies = [ + "proc-macro-hack", + "quote", + "syn 2.0.117", + "unic-langid-impl", +] [[package]] name = "unicode-bidi" @@ -1878,6 +3522,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" @@ -1890,12 +3549,70 @@ version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "383ad40bb927465ec0ce7720e033cb4ca06912855fc35db31b5755d0de75b1ee" +[[package]] +name = "unicode-segmentation" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" + [[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80be9b06fbae3b8b303400ab20778c80bbaf338f563afe567cf3c9eea17b47ef" +dependencies = [ + "base64 0.22.1", + "data-url", + "flate2", + "fontdb", + "imagesize 0.13.0", + "kurbo 0.11.3", + "log", + "pico-args", + "roxmltree 0.20.0", + "rustybuzz", + "simplecss", + "siphasher", + "strict-num", + "svgtypes 0.15.3", + "tiny-skia-path 0.11.4", + "unicode-bidi", + "unicode-script", + "unicode-vo", + "xmlwriter", +] + [[package]] name = "usvg" version = "0.47.0" @@ -1906,8 +3623,8 @@ dependencies = [ "data-url", "flate2", "fontdb", - "imagesize", - "kurbo", + "imagesize 0.14.0", + "kurbo 0.13.1", "log", "pico-args", "roxmltree 0.21.1", @@ -1915,8 +3632,8 @@ dependencies = [ "simplecss", "siphasher", "strict-num", - "svgtypes", - "tiny-skia-path", + "svgtypes 0.16.1", + "tiny-skia-path 0.12.0", "ttf-parser 0.25.1", "unicode-bidi", "unicode-script", @@ -1924,6 +3641,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" @@ -1993,7 +3716,7 @@ dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn", + "syn 2.0.117", "wasm-bindgen-shared", ] @@ -2006,6 +3729,52 @@ dependencies = [ "unicode-ident", ] +[[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" @@ -2042,7 +3811,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -2053,7 +3822,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -2089,6 +3858,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 = "winnow" version = "1.0.4" @@ -2107,13 +3885,31 @@ version = "0.48.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb731d4c4d93eacc69a1ad2f270f905788a98e4a3438267bcafbe08d3431c8d8" dependencies = [ - "font-types", + "font-types 0.11.3", "indexmap", - "kurbo", + "kurbo 0.13.1", "log", - "read-fonts", + "read-fonts 0.39.2", ] +[[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" @@ -2126,6 +3922,27 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9440ea3e5aeabb0ac63af70daf835274065238cdd0cec83418f417eae38bacee" +[[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.3" @@ -2133,10 +3950,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" dependencies = [ "stable_deref_trait", - "yoke-derive", + "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 2.0.117", + "synstructure", +] + [[package]] name = "yoke-derive" version = "0.8.2" @@ -2145,7 +3974,7 @@ checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", "synstructure", ] @@ -2166,7 +3995,7 @@ checksum = "7e6cc098ea4d3bd6246687de65af3f920c430e236bee1e3bf2e441463f08a02f" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -2186,10 +4015,79 @@ checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", "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.3", + "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.3", + "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 2.0.117", +] + +[[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 2.0.117", +] + [[package]] name = "zlib-rs" version = "0.6.3" @@ -2230,6 +4128,12 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "zune-core" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a" + [[package]] name = "zune-core" version = "0.5.1" @@ -2245,11 +4149,20 @@ dependencies = [ "simd-adler32", ] +[[package]] +name = "zune-jpeg" +version = "0.4.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29ce2c8a9384ad323cf564b67da86e21d3cfdff87908bc1223ed5c99bc792713" +dependencies = [ + "zune-core 0.4.12", +] + [[package]] name = "zune-jpeg" version = "0.5.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "410e9ecef634c709e3831c2cfdb8d9c32164fae1c67496d5b68fff728eec37fe" dependencies = [ - "zune-core", + "zune-core 0.5.1", ] diff --git a/Cargo.toml b/Cargo.toml index 7d917476..5d3eaa9a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,7 @@ readme = "README.md" # and helper scripts are excluded to stay within the crates.io 10 MB limit. exclude = [ "assets/fonts/*.ttf", # raw TTFs — only .ttf.gz are used by include_bytes! + "assets/fonts/*.otf", # raw OTFs (NewCM Math) — only .otf.gz is embedded "docs/", # mdBook source + generated SVG/PNG/GIF/PDF assets "scripts/", # smoke test and generation scripts "examples/*.svg", # generated SVG outputs @@ -31,8 +32,10 @@ all-features = true # The `pdf` feature's real minimum Rust version — higher than `package.rust-version` # above, which intentionally excludes it (see README.md's note and CHANGELOG.md). # krilla (the `pdf` feature's PDF backend) sets this floor; bump it in lockstep -# with krilla's own MSRV. Read by .github/workflows/ci.yml's `pdf-msrv` job so -# CI actually verifies `--features cli,full` builds at *this* version, not at +# with krilla's own MSRV. typst (the feature's math typesetter) also rides this +# floor but currently needs less (1.89 as of typst 0.14). Read by +# .github/workflows/ci.yml's `pdf-msrv` job so CI actually verifies +# `--features cli,full` builds at *this* version, not at # `package.rust-version` (which is no longer sufficient for that feature combo). pdf_feature = "1.92" @@ -47,11 +50,33 @@ png = ["dep:fontdue", "dep:png", "dep:flate2"] # a newer MSRV via Cargo's rust-version-aware resolver. Building with # `--features pdf` (or `full`) genuinely requires Rust >= 1.92; documented in # README/CHANGELOG rather than enforced through Cargo metadata. -pdf = ["dep:krilla", "dep:krilla-svg", "dep:usvg", "dep:flate2"] +# +# `pdf` also carries the in-process math typesetter (the "typst tier"): each +# label containing `$...$` is typeset by the Typst compiler (linked as a +# library) and embedded in SVG/PNG/PDF output — real math typography (proper +# italic σ, vinculum √, stacked fractions, sums) with no external typst CLI. +# It rides `pdf` rather than being its own feature because krilla *is* Typst's +# PDF stack — this is the one deliberately heavy, higher-MSRV feature, and +# splitting it in two bought nothing but combinatorics. Builds without `pdf` +# lower `$...$` to inline Unicode via the always-on lookup tier instead. +pdf = [ + "dep:krilla", + "dep:krilla-svg", + "dep:usvg", + "dep:flate2", + "dep:typst", + "dep:typst-svg", + "dep:typst-render", + "dep:typst-library", +] embed_font = ["dep:flate2"] cli = ["dep:clap", "dep:clap_mangen", "dep:csv"] -full = ["embed_font", "png", "pdf"] +full = ["embed_font", "png", "pdf", "typst"] parquet = ["cli", "dep:parquet", "dep:bytes", "dep:arrow-array", "dep:arrow-schema"] +# 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 = [] # 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 @@ -82,6 +107,10 @@ usvg = { version = "=0.47.0", 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 00000000..3454fdc0 --- /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 00000000..28f936f1 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 00000000..a2fa7af8 Binary files /dev/null and b/assets/fonts/NewCMMath-Regular.otf.gz differ diff --git a/docs/src/assets/math/exp_decay.svg b/docs/src/assets/math/exp_decay.svg index 7ec99b01..b0346536 100644 --- a/docs/src/assets/math/exp_decay.svg +++ b/docs/src/assets/math/exp_decay.svg @@ -1 +1,48 @@ -0123400.250.50.751time (s)exp(-t)Exponential decay \ No newline at end of file +0123400.250.50.751time (s) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Exponential decay \ No newline at end of file diff --git a/docs/src/assets/math/fraction.svg b/docs/src/assets/math/fraction.svg index d1f19a1e..4a61dad8 100644 --- a/docs/src/assets/math/fraction.svg +++ b/docs/src/assets/math/fraction.svg @@ -1 +1,35 @@ -012340246810(a + b)/crateFraction \ No newline at end of file +012340246810 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +rateFraction \ No newline at end of file diff --git a/docs/src/assets/math/greek.svg b/docs/src/assets/math/greek.svg index 59df9bc0..aefdc3da 100644 --- a/docs/src/assets/math/greek.svg +++ b/docs/src/assets/math/greek.svg @@ -1 +1,24 @@ -012340246810μ ± σcountStandard deviation \ No newline at end of file +012340246810 + + + + + + + + + + + + + + + + + + + + + + +countStandard deviation \ No newline at end of file diff --git a/docs/src/assets/math/log_axes.svg b/docs/src/assets/math/log_axes.svg index f871d756..61857cec 100644 --- a/docs/src/assets/math/log_axes.svg +++ b/docs/src/assets/math/log_axes.svg @@ -1 +1,127 @@ --5-2.502.5502.557.51012.5log₂ fold change-log₁₀(p)Differential expression \ No newline at end of file +-5-2.502.5502.557.51012.5 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Differential expression \ No newline at end of file diff --git a/docs/src/assets/math/mixed.svg b/docs/src/assets/math/mixed.svg index fc5bdaec..9948aff0 100644 --- a/docs/src/assets/math/mixed.svg +++ b/docs/src/assets/math/mixed.svg @@ -1 +1,104 @@ -012340246810Variance, σ² (units)∇ · FMixed text and math \ No newline at end of file +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 index 4682bfab..5ac8be50 100644 --- a/docs/src/assets/math/quadratic.svg +++ b/docs/src/assets/math/quadratic.svg @@ -1 +1,91 @@ -012340246810x = (-b ± √(b² - 4ac))/(2a)rootsQuadratic formula \ No newline at end of file +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 index f86917ac..5d37ae22 100644 --- a/docs/src/assets/math/rotated_ylabel.svg +++ b/docs/src/assets/math/rotated_ylabel.svg @@ -1 +1,66 @@ -012340246810timeEnergy E = mc²Mass–energy equivalence \ No newline at end of file +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 index 02f4668b..75c58255 100644 --- a/docs/src/assets/math/sqrt.svg +++ b/docs/src/assets/math/sqrt.svg @@ -1 +1,44 @@ -012340246810√(x² + y²)distanceSquare root \ No newline at end of file +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 index 6fdea491..5e39481c 100644 --- a/docs/src/assets/math/sum.svg +++ b/docs/src/assets/math/sum.svg @@ -1 +1,49 @@ -012340246810∑ᵢ₌₁ⁿ xᵢtotalSummation \ No newline at end of file +012340246810 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +totalSummation \ No newline at end of file diff --git a/docs/src/assets/math/superscript.svg b/docs/src/assets/math/superscript.svg index 57d6db0d..290d1c6d 100644 --- a/docs/src/assets/math/superscript.svg +++ b/docs/src/assets/math/superscript.svg @@ -1 +1,85 @@ -012340246810x² + y² = r²f(x)Power law \ No newline at end of file +012340246810 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Power law \ No newline at end of file diff --git a/docs/src/assets/math/textplot_body.svg b/docs/src/assets/math/textplot_body.svg new file mode 100644 index 00000000..5f57d189 --- /dev/null +++ b/docs/src/assets/math/textplot_body.svg @@ -0,0 +1,157 @@ +Model summaryThe estimator + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + has variance + + + + + + + + + + + + + + + + + + + + + + + + + + + +,and the bound + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + shrinks as + + + + + + + + + + + grows. \ No newline at end of file diff --git a/docs/src/assets/math/trig.svg b/docs/src/assets/math/trig.svg index 6244c76a..9536f84d 100644 --- a/docs/src/assets/math/trig.svg +++ b/docs/src/assets/math/trig.svg @@ -1 +1,129 @@ -01234567-1.5-1-0.500.511.5θ (radians)sin(θ)sin(θ) \ No newline at end of file +01234567-1.5-1-0.500.511.5 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/src/reference/math.md b/docs/src/reference/math.md index 6f90b308..7e11aed1 100644 --- a/docs/src/reference/math.md +++ b/docs/src/reference/math.md @@ -1,9 +1,21 @@ # Math in Labels Any label — plot title, axis labels, `TextPlot` markdown bodies — may embed math -inside `$...$` using LaTeX-ish syntax. Math regions are lowered to inline -**Unicode** text at render time: zero dependencies, no feature flag, always on, -in every backend including the terminal. +inside `$...$` using LaTeX-ish syntax. There are two rendering tiers, chosen by +how you build kuva: + +- **Lookup tier** (always available, zero dependencies): math is lowered to + inline **Unicode** text at render time, in every backend including the + terminal. The tables below show its output. +- **Typst tier** (feature `pdf`, included in `full` and the release CLI + binaries): the whole label is typeset by the [Typst](https://typst.app) + compiler — linked as a library, no external binary — and embedded in + SVG/PNG/PDF output as real 2-D math: stacked fractions, radicals with + vinculum, large operators, math italic. The terminal always uses the lookup + tier (a character grid can't hold typeset math). + +The same `$...$` source drives both tiers, so code never changes — only the +fidelity of the output does. ```rust Layout::new((0.0, 3.0), (0.0, 10.0)) @@ -15,7 +27,7 @@ Layout::new((0.0, 3.0), (0.0, 10.0)) A literal dollar sign is written `\$`. A `$` without a closing partner is left untouched as plain text. -## Quick examples +## Quick examples (lookup tier) | Input | Output | |-------|--------| @@ -108,9 +120,38 @@ anywhere a label can go, including rotated y-axis titles and terminal grids. `\sqrt{x}` → `√x`, `\sqrt{x+y}` → `√(x+y)`, `\sqrt[3]{x}` → `³√x`. +## Typst tier + +With the `pdf` feature, `$...$` labels upgrade automatically from the inline +Unicode above to real typeset math. A compile failure never breaks a plot: +the label falls back to the lookup tier and a one-time warning names the +expression and Typst's hint. + +**Typst math is not LaTeX.** The `$...$` body is LaTeX-flavoured for +familiarity, but in Typst a multi-letter run is a single identifier: `mc` is +not `m × c`, so `$E = mc^2$` fails to compile (and falls back to the lookup +tier). Write the factors separated: + +```text +$E = m c^2$ ✓ +$E = mc^2$ ✗ (lookup tier + a one-time warning) +``` + +### Math in TextPlot bodies + +`$...$` inside a markdown `TextPlot` body is spliced into the wrapped +paragraph as a typeset fragment: kuva's own word-wrap treats each fragment as +one unbreakable word, the surrounding words stay real (selectable) text, and +a line holding a tall fragment (stacked fraction, radical) grows its leading +so neighbouring lines never collide. + +Math spliced into wrapped TextPlot body text + ## Plot examples -Generated by `cargo run --example math`: +Generated by `cargo run --features full --example math` — built with `full`, +so these show the **typst tier**. Without `pdf` the same code produces the +lookup tier's inline-Unicode forms instead: ### Scientific axes @@ -163,7 +204,7 @@ axis pair: ### Quadratic formula -`$x = \frac{-b \pm \sqrt{b^2 - 4ac}}{2a}$` → `x = (-b ± √(b² - 4ac))/(2a)`: +`$x = \frac{-b \pm \sqrt{b^2 - 4 a c}}{2 a}$` — the lookup tier lowers this to `x = (-b ± √(b² - 4 a c))/(2 a)`: Quadratic formula in an axis label @@ -173,7 +214,7 @@ axis pair: ### Rotated y-axis title -`Energy $E = mc^2$` — math works in rotated labels too: +`Energy $E = m c^2$` — math works in rotated labels too: Math in a rotated y-axis title diff --git a/examples/math.rs b/examples/math.rs index 6bed114b..1e5fb83a 100644 --- a/examples/math.rs +++ b/examples/math.rs @@ -4,9 +4,13 @@ //! Run with: //! //! ```bash -//! cargo run --example math +//! cargo run --features full --example math //! ``` //! +//! With `full` (which includes `pdf`), labels render through the typst tier +//! — real typeset math. Without features the same code emits the lookup +//! tier's inline-Unicode forms. +//! //! SVGs are written to `docs/src/assets/math/`. use kuva::backend::svg::SvgBackend; @@ -58,13 +62,13 @@ fn main() { let (p, l) = scatter( "Quadratic formula", - "$x = \\frac{-b \\pm \\sqrt{b^2 - 4ac}}{2a}$", + "$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 = mc^2$"); + let (p, l) = scatter("Mass–energy equivalence", "time", "Energy $E = m c^2$"); write("rotated_ylabel", p, l); let (p, l) = scatter( @@ -130,5 +134,20 @@ fn main() { write("exp_decay", vec![Plot::Line(plot)], layout); } + // ── TextPlot body: math spliced into wrapped prose ──────────────────── + { + use kuva::plot::text::TextPlot; + let tp = TextPlot::new() + .with_title("Model summary") + .with_body( + "The estimator $\\hat{x} = \\frac{1}{n} \\sum_{i=1}^{n} x_i$ has \ + variance $\\frac{\\sigma^2}{n}$, and the **bound** \ + $\\sqrt{x^2 + y^2}$ shrinks as $n$ grows.", + ) + .with_border("#999", 1.0); + let layout = Layout::new((0.0, 1.0), (0.0, 1.0)).with_width(420.0); + write("textplot_body", vec![Plot::Text(tp)], layout); + } + println!("Math SVGs written to {OUT}/"); } diff --git a/scripts/smoke_tests.sh b/scripts/smoke_tests.sh index ce00e15f..f63c17d1 100755 --- a/scripts/smoke_tests.sh +++ b/scripts/smoke_tests.sh @@ -15,7 +15,6 @@ set -euo pipefail BIN="" SAVE=0 OUTDIR="smoke_test_outputs" - # Parse arguments while [[ $# -gt 0 ]]; do case "$1" in @@ -1126,10 +1125,13 @@ check "quiver grid on + tight bounds" \ --tight-bounds --pivot middle \ --title "Quiver Grid + Clip" + # ── math in labels ────────────────────────────────────────────────────────── -# $...$ math regions in labels are lowered to inline Unicode (σ², a/b, √(…), ∑) -# by every backend. Exercised across plot types and label slots to confirm it -# is not scatter-specific. +# $...$ math regions in labels. A cli,full binary includes `pdf`, so these are +# typeset by the typst tier (real 2-D math embedded in the SVG); a build +# without `pdf` lowers them to inline Unicode (σ², a/b, √(…), ∑) via the +# always-on lookup tier. Same commands either way — 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}$' @@ -1148,7 +1150,7 @@ check "math greek and operators" \ check "math in rotated y-label" \ "$BIN" scatter "$DATA/scatter.tsv" --x x --y y \ - --y-label 'Energy $E = mc^2$' + --y-label 'Energy $E = m c^2$' check "math on line plot" \ "$BIN" line "$DATA/measurements.tsv" --x time --y value \ @@ -1337,6 +1339,22 @@ check "coverage real pools overlaid" \ "$BIN" coverage "$DATA/covar_depth.tsv" --x pos --samples pool1,pool2 --overlay-samples \ --regions "$DATA/covar_genes.tsv" --region-name genes --x-label "MN908947.3" +# ── math (typst tier) ───────────────────────────────────────────────────────── +# Deeper 2-D math (stacked fractions, radicals, limits). With cli,full these +# come back as embedded typst fragments; a pdf-less build still passes via the +# lookup tier's inline forms. +check "typst fraction" \ + "$BIN" scatter "$DATA/scatter.tsv" --x x --y y \ + --title 'Rate $\frac{a + b}{c}$' + +check "typst sqrt + sum" \ + "$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" \ + "$BIN" scatter "$DATA/scatter.tsv" --x x --y y \ + --y-label '$x = \frac{-b \pm \sqrt{b^2 - 4 a c}}{2 a}$' + # ── summary ─────────────────────────────────────────────────────────────────── echo "" echo "Results: $PASS passed, $FAIL failed" diff --git a/scripts/terminal_plots.sh b/scripts/terminal_plots.sh index 08a9bca4..f87d4c32 100755 --- a/scripts/terminal_plots.sh +++ b/scripts/terminal_plots.sh @@ -233,8 +233,8 @@ run scatter "$DATA/scatter.tsv" --x x --y y \ --terminal $W $H # ── math in labels ──────────────────────────────────────────────────────────── -# $...$ regions are lowered to inline Unicode (σ², a/b, √, ∑) before drawing -# on the character grid. +# 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$' \ diff --git a/src/backend/mod.rs b/src/backend/mod.rs index ea638568..6f8c909d 100644 --- a/src/backend/mod.rs +++ b/src/backend/mod.rs @@ -1,5 +1,7 @@ pub mod interactive_js; pub mod svg; +#[cfg(feature = "pdf")] +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 5acb436a..5227a29f 100644 --- a/src/backend/raster.rs +++ b/src/backend/raster.rs @@ -14,9 +14,9 @@ //! - Clip rects and translate transforms fully respected use std::collections::HashMap; -use std::sync::OnceLock; use fontdue::{Font, FontSettings, Metrics}; +use std::sync::OnceLock; use crate::plot::FillPattern; use crate::render::color::Color as KColor; @@ -99,6 +99,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 = "pdf")] + 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: @@ -1031,6 +1064,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 = "pdf")] + 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> { @@ -1581,6 +1702,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 = "pdf")] +#[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; @@ -2074,9 +2213,52 @@ impl RasterBackend { .as_ref() .and_then(kcolor_to_rgba) .unwrap_or(default_text); - // Lower any `$...$` math regions to inline Unicode - // (σ², a/b, √(…)) so they draw through the normal glyph - // path. No-op for plain labels. + // Math routing: typst tier (feature `pdf`) 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. + #[cfg(feature = "pdf")] + if crate::render::math::contains_math(content) { + 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. + } + + // Lookup tier (or plain text); needs_rewrite also catches + // escaped `\$`. let lowered; let content: &str = if crate::render::math::needs_rewrite(content) { lowered = crate::render::math::to_unicode(content); @@ -2112,10 +2294,55 @@ impl RasterBackend { .unwrap_or(default_text); let sz = *size as f32 * s; + // Typeset math spans up front (typst tier): the pixmap + // width feeds anchor alignment, and the same pixmap is + // blitted in the pen loop. A failed compile leaves None + // and the span degrades to lookup-tier text below. + #[cfg(feature = "pdf")] + let math_frags: Vec< + Option, + > = spans + .iter() + .map(|sp| { + if sp.math { + crate::render::math::render_label_pixmap( + &format!("${}$", sp.text), + *size as f64, + color.as_ref(), + s, + ) + } else { + None + } + }) + .collect(); + // Lookup-tier text for math spans whose fragment is + // unavailable (compile failure, or no `pdf` feature — + // in which case spans never carry math anyway). + let lowered_math = |sp: &crate::render::render::TextSpan| { + crate::render::math::to_unicode(&format!("${}$", sp.text)) + }; + // Compute total width for anchor alignment using metrics (no cache needed). + // (the index is only read when `pdf` is compiled in) + #[allow(clippy::unused_enumerate_index)] let total_w: f32 = spans .iter() - .map(|sp| { + .enumerate() + .map(|(_i, sp)| { + if sp.math { + #[cfg(feature = "pdf")] + if let Some(pm) = &math_frags[_i] { + // Advance minus the fragment's baked-in + // margin (see math::FRAGMENT_MARGIN_EM). + let margin = (crate::render::math::FRAGMENT_MARGIN_EM + * *size as f64) + as f32 + * s; + return (pm.width_px as f32 - 2.0 * margin).max(1.0); + } + return measure_text_direct(&lowered_math(sp), sz, font); + } let f = if sp.bold { shared_font_bold() } else if sp.code { @@ -2136,7 +2363,43 @@ impl RasterBackend { }; let mut pen_x = start_x; - for sp in spans { + #[allow(clippy::unused_enumerate_index)] + for (_i, sp) in spans.iter().enumerate() { + if sp.math { + #[cfg(feature = "pdf")] + if let Some(pm) = &math_frags[_i] { + // Blit at the shared baseline, shifted a + // margin early; advance minus both margins so + // the fragment spaces like a word. + let margin = + (crate::render::math::FRAGMENT_MARGIN_EM * *size as f64) as f32 + * s; + let dx = (pen_x - margin).round() as i32; + let dy = (sy!(*y) - pm.baseline_offset_px as f32).round() as i32; + let dx = dx.max(clip.x0); + let dy = dy.max(clip.y0); + canvas.blit_pixmap(pm, dx, dy, clip); + pen_x += (pm.width_px as f32 - 2.0 * margin).max(1.0); + continue; + } + // Fallback: draw the lookup-tier form as text. + let lowered = lowered_math(sp); + let w = measure_text_direct(&lowered, sz, font); + canvas.draw_text( + pen_x, + sy!(*y), + &lowered, + sz, + rgba, + TextAnchor::Start, + None, + font, + &mut glyph_cache, + clip, + ); + pen_x += w; + continue; + } // Draw with the appropriate font/cache; advance pen by span width. let w = if sp.bold { let w = measure_text_direct(&sp.text, sz, shared_font_bold()); diff --git a/src/backend/svg.rs b/src/backend/svg.rs index ac67e21e..b22e295e 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 = "pdf")] + let mut math_uid: usize = 0; for elem in &scene.elements { match elem { Primitive::Circle { @@ -225,9 +228,31 @@ impl SvgBackend { bold, color, } => { - // Lower any `$...$` math regions to inline Unicode - // (σ², a/b, √(…)) so they render as ordinary text. - // No-op for plain labels. + // Math routing: a `$...$` label is either typeset by the + // typst tier (feature `pdf`) and embedded as a fragment, + // or lowered to inline Unicode by the always-on lookup + // tier and emitted as ordinary text. + #[cfg(feature = "pdf")] + if crate::render::math::contains_math(content) { + 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 + // (needs_rewrite also catches escaped `\$`). let lowered; let content: &str = if crate::render::math::needs_rewrite(content) { lowered = crate::render::math::to_unicode(content); @@ -281,6 +306,28 @@ impl SvgBackend { anchor, color, } => { + // Splice path (typst tier): a line containing math spans + // is laid out manually — text runs measured with the + // bundled-font metrics, fragments with their typeset + // width — so each piece can be positioned absolutely and + // the math embeds inline at the shared baseline. + #[cfg(feature = "pdf")] + if spans.iter().any(|sp| sp.math) { + rich_text_with_math( + &mut svg, + *x, + *y, + spans, + *size, + *anchor, + color.as_ref(), + depth, + p, + &mut math_uid, + ); + continue; + } + let anchor_str = match anchor { TextAnchor::Start => "start", TextAnchor::Middle => "middle", @@ -662,3 +709,123 @@ impl SvgBackend { // TODO: To phase out later: add #[deprecated(note = "Use SvgBackend::new()")] here. #[allow(non_upper_case_globals)] pub const SvgBackend: SvgBackend = SvgBackend::new(); + +/// Draw one rich-text line that contains typeset math spans (typst tier). +/// +/// The tspan flow used for plain rich text cannot host an embedded SVG +/// fragment, so this path lays the line out itself: every span gets an +/// absolute x from the bundled-font metrics (text) or the fragment's typeset +/// width (math), all sharing the baseline `y`. Anchoring shifts the whole +/// line by its total width. A fragment that fails to compile degrades to its +/// lookup-tier text in place. +#[cfg(feature = "pdf")] +#[allow(clippy::too_many_arguments)] +fn rich_text_with_math( + svg: &mut String, + x: f64, + y: f64, + spans: &[crate::render::render::TextSpan], + size: u32, + anchor: TextAnchor, + color: Option<&crate::render::color::Color>, + depth: usize, + pretty: bool, + math_uid: &mut usize, +) { + use crate::render::math; + use crate::render::text_metrics::{measure_text_width, FontStyle}; + + // Resolve each span to (drawable, advance) up front so anchoring can use + // the exact total width. + enum Piece<'a> { + Text(&'a crate::render::render::TextSpan, String), + Math(math::MathSvg), + } + let fs = size as f64; + // The fragment page bakes in a margin (descender/overhang safety); + // inline, the fragment must advance like a word — subtract it from the + // advance and start the embed a margin early so glyphs align optically. + let frag_margin = math::FRAGMENT_MARGIN_EM * fs; + let mut pieces: Vec<(Piece, f64)> = Vec::with_capacity(spans.len()); + for sp in spans { + if sp.math { + match math::render_label_svg(&format!("${}$", sp.text), fs, color) { + Some(m) => { + let w = (m.width_pt - 2.0 * frag_margin).max(1.0); + pieces.push((Piece::Math(m), w)); + continue; + } + None => { + // Lookup-tier fallback, drawn as ordinary text. + let lowered = math::to_unicode(&format!("${}$", sp.text)); + let w = + measure_text_width(&lowered, fs, FontStyle::from_flags(sp.bold, sp.italic)); + pieces.push((Piece::Text(sp, lowered), w)); + continue; + } + } + } + let w = measure_text_width(&sp.text, fs, FontStyle::from_flags(sp.bold, sp.italic)); + pieces.push((Piece::Text(sp, sp.text.clone()), w)); + } + + let total: f64 = pieces.iter().map(|(_, w)| w).sum(); + let mut cursor = match anchor { + TextAnchor::Start => x, + TextAnchor::Middle => x - total / 2.0, + TextAnchor::End => x - total, + }; + + for (piece, w) in &pieces { + match piece { + Piece::Text(sp, text) => { + write_indent(svg, depth, pretty); + svg.push_str(r#" element). + svg.push_str(r#" xml:space="preserve">"#); + write_escaped(svg, text); + svg.push_str(""); + write_newline(svg, pretty); + } + Piece::Math(m) => { + write_indent(svg, depth, pretty); + crate::backend::svg_math::embed_label( + svg, + cursor - frag_margin, + y, + TextAnchor::Start, + None, + m, + *math_uid, + ); + *math_uid += 1; + write_newline(svg, pretty); + } + } + cursor += w; + } +} diff --git a/src/backend/svg_math.rs b/src/backend/svg_math.rs new file mode 100644 index 00000000..43b6117b --- /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 c3e283e4..694233e9 100644 --- a/src/backend/terminal.rs +++ b/src/backend/terminal.rs @@ -827,9 +827,9 @@ impl Canvas { // hardcoded 0.35) keeps this in lockstep with the renderer. let baseline = center_offset(*size as f64, FontStyle::Regular); let row = self.to_cy(y_s - baseline); - // Lower any `$...$` math regions to inline Unicode (σ, x², - // √(…)) — the character grid renders the result directly. - // No-op for plain labels. + // Lookup tier: the terminal can't typeset, so `$...$` math is + // lowered to inline Unicode (σ, x², √(…)). No-op for plain + // labels (needs_rewrite also catches escaped `\$`). let lowered; let content: &str = if crate::render::math::needs_rewrite(content) { lowered = crate::render::math::to_unicode(content); @@ -909,8 +909,19 @@ impl Canvas { anchor, .. } => { - // Flatten spans to plain text; terminal doesn't support inline styling. - let content: String = spans.iter().map(|s| s.text.as_str()).collect(); + // Flatten spans to plain text; terminal doesn't support inline + // styling, and math spans always take the lookup tier here (a + // character grid can't hold a typeset fragment). + let content: String = spans + .iter() + .map(|s| { + if s.math { + crate::render::math::to_unicode(&format!("${}$", s.text)) + } else { + s.text.clone() + } + }) + .collect(); let rgb = self.text_color; let x_s = x + tx; let y_s = y + ty; diff --git a/src/backend/typst.rs b/src/backend/typst.rs new file mode 100644 index 00000000..6dc1a014 --- /dev/null +++ b/src/backend/typst.rs @@ -0,0 +1,1176 @@ +//! 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 +//! `pdf` 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. +//! +//! # Path and clip support +//! +//! - `Path` primitives (SVG path data: arrowheads, chord ribbons, sankey +//! flows, venn outlines) are lowered to CETZ `merge-path` calls — lines +//! and cubic Béziers, with quadratics promoted and arcs converted via the +//! standard endpoint→center parameterisation. +//! - Clip regions (`ClipStart`/`ClipEnd`) split the output into stacked +//! `#place`d canvases; a clipped chunk is wrapped in Typst's +//! `box(clip: true)` at the clip rectangle. +//! +//! # Limitations +//! +//! - Batched primitives (`CircleBatch`, `RectBatch`) unroll to one CETZ call +//! per element. Acceptable for typical plot sizes; can be optimized later. +//! - 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); + + let h = scene.height; + + // Split the element stream into chunks at clip boundaries. A scene + // without clip regions stays a single chunk and renders as one plain + // canvas (the common case). With clips, each chunk becomes its own + // `#place`d canvas so that a clipped chunk can be wrapped in a + // `box(clip: true)` — Typst's clipping primitive — while later + // chunks still stack above it in paint order. + let chunks = split_clip_chunks(&scene.elements); + let single = chunks.len() == 1 && chunks[0].0.is_none(); + + // An empty scene still emits one (empty) canvas so the document is a + // valid, non-degenerate Typst file. + if single && chunks[0].1.is_empty() { + open_canvas(&mut out, scene.width, h); + out.push_str("})\n"); + return out; + } + + for (clip, elems) in &chunks { + if elems.is_empty() { + continue; + } + match clip { + None => { + if !single { + out.push_str("#place(top + left, dx: 0pt, dy: 0pt)["); + } + open_canvas(&mut out, scene.width, h); + for p in elems { + emit_primitive(&mut out, p, h); + } + out.push_str("})"); + if !single { + out.push(']'); + } + out.push('\n'); + } + Some(r) => { + // Clip box at the region's page position; inside it, the + // canvas keeps page-absolute coordinates and is shifted + // back by the box offset so geometry lands where the SVG + // backend's would put it. + let _ = write!( + out, + "#place(top + left, dx: {}pt, dy: {}pt)[#box(width: {}pt, height: {}pt, clip: true)[#place(top + left, dx: {}pt, dy: {}pt)[", + r.x, r.y, r.width, r.height, -r.x, -r.y + ); + open_canvas(&mut out, scene.width, h); + for p in elems { + emit_primitive(&mut out, p, h); + } + out.push_str("})]]]\n"); + } + } + } + out + } +} + +/// Rectangular clip region carried between `ClipStart`/`ClipEnd`. +struct ClipRect { + x: f64, + y: f64, + width: f64, + height: f64, +} + +/// Split the primitive stream into `(clip, elements)` chunks at clip +/// boundaries. Kuva emits flat (non-nested) clip regions — the plot-area +/// clip around data marks; nested `ClipStart`s are intersected defensively. +fn split_clip_chunks(elements: &[Primitive]) -> Vec<(Option, Vec<&Primitive>)> { + let mut chunks: Vec<(Option, Vec<&Primitive>)> = vec![(None, Vec::new())]; + let mut stack: Vec = Vec::new(); + for p in elements { + match p { + Primitive::ClipStart { + x, + y, + width, + height, + id: _, + } => { + let r = match stack.last() { + // Nested clip: intersect with the enclosing rect. + Some(outer) => { + let x0 = x.max(outer.x); + let y0 = y.max(outer.y); + let x1 = (x + width).min(outer.x + outer.width); + let y1 = (y + height).min(outer.y + outer.height); + ClipRect { + x: x0, + y: y0, + width: (x1 - x0).max(0.0), + height: (y1 - y0).max(0.0), + } + } + None => ClipRect { + x: *x, + y: *y, + width: *width, + height: *height, + }, + }; + chunks.push(( + Some(ClipRect { + x: r.x, + y: r.y, + width: r.width, + height: r.height, + }), + Vec::new(), + )); + stack.push(r); + } + Primitive::ClipEnd => { + stack.pop(); + match stack.last() { + None => chunks.push((None, Vec::new())), + Some(outer) => chunks.push(( + Some(ClipRect { + x: outer.x, + y: outer.y, + width: outer.width, + height: outer.height, + }), + Vec::new(), + )), + } + } + other => chunks.last_mut().unwrap().1.push(other), + } + } + chunks +} + +/// Open a `cetz.canvas` block. The invisible full-page rect anchors the +/// canvas's bounding box to the page extent, so drawings keep their absolute +/// positions even when nothing touches the page corners. +fn open_canvas(out: &mut String, w: f64, h: f64) { + let _ = write!( + out, + "#cetz.canvas(length: 1pt, {{\n import cetz.draw: *\n rect((0, 0), ({w}, {h}), stroke: none)\n" + ); +} + +// ── 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) + ); + } + + Primitive::Path(data) => emit_path(out, data, scene_h), + + // Grouping carries no visual state of its own; clipping is handled + // structurally in `render_scene` (chunks + `box(clip: true)`), so + // both marker pairs are no-ops at the per-primitive level. + 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 +} + +// ── Path primitive ──────────────────────────────────────────────────────────── +// +// Kuva's `PathData.d` is SVG path data, but from a closed vocabulary: the +// render layer only ever emits absolute `M`, `L`, `C`, `Q`, `A` (circular, +// unrotated), and `Z` — see `build_path` and the arrowhead/chord/sankey +// emitters. CETZ has no SVG-path element, so each subpath is lowered to a +// `merge-path` of `line`/`bezier` calls: quadratics are promoted to cubics +// and arcs are converted (endpoint → center parameterisation, then split +// into ≤90° cubic approximations with the standard `4/3·tan(Δ/4)` factor). + +/// One parsed segment of SVG path data (absolute coordinates). +enum PathSeg { + Move(f64, f64), + LineTo(f64, f64), + Cubic([f64; 6]), + Quad([f64; 4]), + /// rx, ry, large-arc flag, sweep flag, end x, end y (x-rotation is always + /// 0 in kuva's output and is ignored). + Arc(f64, f64, bool, bool, f64, f64), + Close, +} + +/// Parse kuva-emitted SVG path data. Unknown/relative commands end parsing +/// gracefully (returns what was read so far) rather than panicking — the +/// backend then draws the prefix, which is still better than dropping the +/// whole path. +fn parse_svg_path(d: &str) -> Vec { + let b = d.as_bytes(); + let mut i = 0usize; + let mut cmd = 0u8; + let mut nums: Vec = Vec::with_capacity(8); + let mut segs = Vec::new(); + let mut first_pair_done = false; + while i < b.len() { + let c = b[i]; + if c.is_ascii_alphabetic() { + match c { + b'M' | b'L' | b'C' | b'Q' | b'A' => { + cmd = c; + first_pair_done = false; + nums.clear(); + } + b'Z' | b'z' => segs.push(PathSeg::Close), + _ => return segs, // relative/unsupported command: stop here + } + i += 1; + continue; + } + if c == b' ' || c == b',' || c == b'\t' || c == b'\n' { + i += 1; + continue; + } + // Read one float. + let start = i; + i += 1; + while i < b.len() { + let ch = b[i]; + let numeric = ch.is_ascii_digit() || ch == b'.' || ch == b'e' || ch == b'E'; + let signed_exp = (ch == b'-' || ch == b'+') && (b[i - 1] == b'e' || b[i - 1] == b'E'); + if numeric || signed_exp { + i += 1; + } else { + break; + } + } + match d[start..i].parse::() { + Ok(v) => nums.push(v), + Err(_) => return segs, + } + let need = match cmd { + b'M' | b'L' => 2, + b'C' => 6, + b'Q' => 4, + b'A' => 7, + _ => return segs, // number before any command + }; + if nums.len() == need { + match cmd { + b'M' => { + if first_pair_done { + // Extra pairs after an M are implicit line-tos. + segs.push(PathSeg::LineTo(nums[0], nums[1])); + } else { + segs.push(PathSeg::Move(nums[0], nums[1])); + first_pair_done = true; + } + } + b'L' => segs.push(PathSeg::LineTo(nums[0], nums[1])), + b'C' => segs.push(PathSeg::Cubic([ + nums[0], nums[1], nums[2], nums[3], nums[4], nums[5], + ])), + b'Q' => segs.push(PathSeg::Quad([nums[0], nums[1], nums[2], nums[3]])), + b'A' => segs.push(PathSeg::Arc( + nums[0], + nums[1], + nums[3] != 0.0, + nums[4] != 0.0, + nums[5], + nums[6], + )), + _ => {} + } + nums.clear(); + } + } + segs +} + +/// Convert one SVG arc (endpoint parameterisation, x-rotation 0) starting at +/// `(x1, y1)` into cubic Bézier segments, appended as `[c1x, c1y, c2x, c2y, +/// x, y]` tuples. Follows the SVG spec's F.6.5/F.6.6 conversion. +#[allow(clippy::too_many_arguments)] +fn arc_to_cubics( + x1: f64, + y1: f64, + rx: f64, + ry: f64, + large: bool, + sweep: bool, + x2: f64, + y2: f64, + out: &mut Vec<[f64; 6]>, +) { + let (mut rx, mut ry) = (rx.abs(), ry.abs()); + if rx == 0.0 || ry == 0.0 || (x1 == x2 && y1 == y2) { + out.push([x1, y1, x2, y2, x2, y2]); // degenerate: straight line + return; + } + let x1p = (x1 - x2) / 2.0; + let y1p = (y1 - y2) / 2.0; + // Scale radii up if the endpoints can't be connected at the given size. + let lambda = (x1p * x1p) / (rx * rx) + (y1p * y1p) / (ry * ry); + if lambda > 1.0 { + let s = lambda.sqrt(); + rx *= s; + ry *= s; + } + let num = (rx * rx) * (ry * ry) - (rx * rx) * (y1p * y1p) - (ry * ry) * (x1p * x1p); + let den = (rx * rx) * (y1p * y1p) + (ry * ry) * (x1p * x1p); + let mut co = (num.max(0.0) / den).sqrt(); + if large == sweep { + co = -co; + } + let cxp = co * rx * y1p / ry; + let cyp = -co * ry * x1p / rx; + let cx = cxp + (x1 + x2) / 2.0; + let cy = cyp + (y1 + y2) / 2.0; + + let ang = |ux: f64, uy: f64, vx: f64, vy: f64| (ux * vy - uy * vx).atan2(ux * vx + uy * vy); + let theta1 = ang(1.0, 0.0, (x1p - cxp) / rx, (y1p - cyp) / ry); + let mut dtheta = ang( + (x1p - cxp) / rx, + (y1p - cyp) / ry, + (-x1p - cxp) / rx, + (-y1p - cyp) / ry, + ); + if !sweep && dtheta > 0.0 { + dtheta -= std::f64::consts::TAU; + } else if sweep && dtheta < 0.0 { + dtheta += std::f64::consts::TAU; + } + + let n = (dtheta.abs() / std::f64::consts::FRAC_PI_2).ceil().max(1.0) as usize; + let step = dtheta / n as f64; + let k = 4.0 / 3.0 * (step / 4.0).tan(); + let point = |a: f64| (cx + rx * a.cos(), cy + ry * a.sin()); + let deriv = |a: f64| (-rx * a.sin(), ry * a.cos()); + let mut a1 = theta1; + for _ in 0..n { + let a2 = a1 + step; + let (px1, py1) = point(a1); + let (px2, py2) = point(a2); + let (dx1, dy1) = deriv(a1); + let (dx2, dy2) = deriv(a2); + out.push([ + px1 + k * dx1, + py1 + k * dy1, + px2 - k * dx2, + py2 - k * dy2, + px2, + py2, + ]); + a1 = a2; + } +} + +/// Emit a `Primitive::Path` as one CETZ `merge-path` per subpath. +fn emit_path(out: &mut String, data: &crate::render::render::PathData, scene_h: f64) { + let segs = parse_svg_path(&data.d); + if segs.is_empty() { + return; + } + + // Style arguments shared by every subpath. + let mut style = String::new(); + match &data.fill { + Some(f) => { + let _ = write!(style, "fill: {}", paint(f, data.opacity)); + } + None => style.push_str("fill: none"), + } + if data.stroke_width > 0.0 && !matches!(data.stroke, Color::None) { + match &data.stroke_dasharray { + Some(dash) => { + let _ = write!( + style, + ", stroke: (thickness: {}pt, paint: {}, dash: ({}))", + data.stroke_width, + paint(&data.stroke, data.opacity), + dash_lengths(dash) + ); + } + None => { + let _ = write!( + style, + ", stroke: {}pt + {}", + data.stroke_width, + paint(&data.stroke, data.opacity) + ); + } + } + } else { + style.push_str(", stroke: none"); + } + + // Walk the segments, flushing one merge-path per subpath. + let mut body = String::new(); + let mut cur = (0.0f64, 0.0f64); + let mut start = cur; + let mut closed = false; + let flush = |body: &mut String, closed: bool, out: &mut String| { + if body.is_empty() { + return; + } + let _ = writeln!(out, " merge-path({style}, close: {closed}, {{"); + out.push_str(body); + out.push_str(" })\n"); + body.clear(); + }; + let mut cubics: Vec<[f64; 6]> = Vec::new(); + for seg in &segs { + match seg { + PathSeg::Move(x, y) => { + flush(&mut body, closed, out); + closed = false; + cur = (*x, *y); + start = cur; + } + PathSeg::LineTo(x, y) => { + let _ = writeln!( + body, + " line(({}, {}), ({}, {}))", + r2(cur.0), + r2(flip_y(cur.1, scene_h)), + r2(*x), + r2(flip_y(*y, scene_h)) + ); + cur = (*x, *y); + } + PathSeg::Cubic([c1x, c1y, c2x, c2y, x, y]) => { + write_bezier( + &mut body, + cur, + (*c1x, *c1y), + (*c2x, *c2y), + (*x, *y), + scene_h, + ); + cur = (*x, *y); + } + PathSeg::Quad([qx, qy, x, y]) => { + // Promote to cubic: c1 = p + 2/3 (q - p), c2 = e + 2/3 (q - e). + let c1 = ( + cur.0 + 2.0 / 3.0 * (qx - cur.0), + cur.1 + 2.0 / 3.0 * (qy - cur.1), + ); + let c2 = (x + 2.0 / 3.0 * (qx - x), y + 2.0 / 3.0 * (qy - y)); + write_bezier(&mut body, cur, c1, c2, (*x, *y), scene_h); + cur = (*x, *y); + } + PathSeg::Arc(rx, ry, large, sweep, x, y) => { + cubics.clear(); + arc_to_cubics(cur.0, cur.1, *rx, *ry, *large, *sweep, *x, *y, &mut cubics); + for [c1x, c1y, c2x, c2y, ex, ey] in &cubics { + write_bezier( + &mut body, + cur, + (*c1x, *c1y), + (*c2x, *c2y), + (*ex, *ey), + scene_h, + ); + cur = (*ex, *ey); + } + } + PathSeg::Close => { + closed = true; + cur = start; + } + } + } + flush(&mut body, closed, out); +} + +/// Write one CETZ cubic `bezier(start, end, ctrl1, ctrl2)` call with y-flip. +fn write_bezier( + body: &mut String, + from: (f64, f64), + c1: (f64, f64), + c2: (f64, f64), + to: (f64, f64), + scene_h: f64, +) { + let _ = writeln!( + body, + " bezier(({}, {}), ({}, {}), ({}, {}), ({}, {}))", + r2(from.0), + r2(flip_y(from.1, scene_h)), + r2(to.0), + r2(flip_y(to.1, scene_h)), + r2(c1.0), + r2(flip_y(c1.1, scene_h)), + r2(c2.0), + r2(flip_y(c2.1, scene_h)) + ); +} + +/// A Typst paint expression for a color with optional SVG-style opacity. +fn paint(c: &Color, opacity: Option) -> String { + let base = typst_color(c); + match opacity { + Some(o) if o < 1.0 && base != "none" => { + format!("{}.transparentize({}%)", base, r2((1.0 - o) * 100.0)) + } + _ => base, + } +} + +/// Convert an SVG `stroke-dasharray` value (`"4 3"`, `"2,2"`) to a Typst +/// dash-pattern tuple body (`"4pt, 3pt"`). +fn dash_lengths(dasharray: &str) -> String { + let parts: Vec = dasharray + .split([' ', ',']) + .filter(|s| !s.is_empty()) + .map(|s| format!("{s}pt")) + .collect(); + // A one-element SVG dasharray means equal on/off runs; Typst tuples need + // two entries to mean the same thing. + if parts.len() == 1 { + format!("{}, {}", parts[0], parts[0]) + } else { + parts.join(", ") + } +} + +/// Round to 2 decimals for compact output (matches the SVG backend's floats). +#[inline] +fn r2(v: f64) -> f64 { + (v * 100.0).round() / 100.0 +} + +#[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 c907c582..4adfda05 100644 --- a/src/fonts.rs +++ b/src/fonts.rs @@ -66,6 +66,24 @@ pub(crate) fn dejavu_sans_mono() -> &'static [u8] { BYTES.get_or_init(|| inflate(DEJAVU_SANS_MONO_GZ, 380_000, "DejaVu Sans Mono")) } +/// Gzip-compressed New Computer Modern Math (OFL/GUST), embedded at compile +/// time. Used only by the `pdf` feature's typst tier, as the math font fed to the typst +/// compiler. ~1.1 MB inflated; ~0.75 MB on disk. +#[cfg(feature = "pdf")] +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 typst tier +/// ships ~1 MB of font rather than ~15 MB. +#[cfg(feature = "pdf")] +pub(crate) fn newcm_math() -> &'static [u8] { + static BYTES: OnceLock> = OnceLock::new(); + BYTES.get_or_init(|| inflate(NEWCM_MATH_GZ, 1_200_000, "NewCM Math")) +} + +// 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); @@ -100,6 +118,7 @@ fn base64_encode(data: &[u8]) -> String { /// Returns a `