rollpie get rust/suki@0.1.1
A Rust library that typesets LaTeX math into HTML and SVG. It has no dependencies.
The font decides how the math is set. suki reads the OpenType file directly and takes advances and glyph outlines from it, together with the axis height, rule thickness, script size and shift, and the radical gaps from the MATH table. Stretchy fences and radicals are never scaled; they are built from the size variants the font carries, and from assembled parts when no variant is tall enough. Change the typeface and the typesetting changes with it.
The Japanese documentation is in README.ja.md.
No dependencies. OpenType parsing, CFF and TrueType glyphs, and the MATH table are all read here. One crate is all you add.
The metrics come from the font. The axis, rule thickness, script placement and radical gaps are values the typeface carries. Nothing is baked in, so a different typeface sets the math differently.
Stretchy fences are not stretched. suki picks a size variant the font provides, and stacks parts when none is large enough. Nothing is squashed or pulled, so stroke weights stay true.
The reader needs no font. Written as outlines, a formula looks the same wherever it is opened. Writing glyphs as text is still available when selectable, copyable output matters more.
All three outputs share one geometry. Positions and sizes are decided once during layout, and each writer only transcribes them. HTML and SVG cannot disagree about shape.
It is fast. Typesetting 1000 formulas finishes in a sixteenth of MathJax's time and a fifth of KaTeX's. The numbers and the method are under "Speed".
Nothing is shared or mutated. A loaded font never changes, so one font can be read from any number of threads at once, with no locking.
cargo run > sample.html # write the built-in samples
cargo run -- 'x^2 + y^2 = r^2' > out.html # typeset the argument
cargo run -- --font /path/to/other-math.otf > out.html # use another typeface
cargo test
As a library:
use suki::{Mode, Shape};
let font = suki::load_font(std::fs::read("font/latinmodern-math.otf")?)?;
let html = suki::render_html(r"\frac{a}{b}", &font, Mode::Inline)?;
let svg = suki::render_svg(r"\sum_{k=1}^{n} k", &font, Mode::Display, Shape::Outline)?;
Mode::Display sets the formula on a line of its own, where large operators grow and the scripts of
\sum and \lim move above and below. Mode::Inline sets it within running text. Both write their
dimensions in em, so a formula scales with the surrounding font size.
Call load_font once and reuse the Font it returns. Loading takes 8.7 ms while typesetting one
formula takes 23 µs, so reloading per formula costs about four hundred times the work.
On failure you get a suki::Error. It carries a message and the character position in the source
where the parse stopped.
There are three writers, and one call produces one of them. Mode chooses how the math is set, not
what format comes out.
render_html returns HTML. Glyphs are placed absolutely, so the CSS from suki::style(&font) and
the same font are needed on the page. That came to 4,205 bytes per formula.
render_svg with Shape::Text puts glyphs in <text> elements. The result is selectable, and the
reader needs the font.
render_svg with Shape::Outline draws glyphs as <path> and <use>. No font is needed and
the result looks the same anywhere, but it cannot be selected. That is 5,829 bytes per formula.
All three agree on geometry. Stretched fences, radicals and display-size large operators are drawn as outlines in every writer, because those glyphs have no character assigned to them.
To learn the size of a formula without writing it, use suki::metric::size_of_source.
MathML and raster images such as PNG are not produced.
Measured on an Apple silicon machine with a release build. The formulas are the 22 samples in
src/main.rs, repeated in order. Each job was run seven times, from process start until the output
was written, and the fastest run is reported. The others are KaTeX 0.16.47 and mathjax-full 3.2.2,
both on node 22.14.0.
1000 formulas as outline SVG:
suki render_svg(Shape::Outline) 36.7 ms 5,828,987 bytes
MathJax tex2svg (fontCache: local) 604.2 ms 6,048,527 bytes
1000 formulas as HTML with glyphs as text:
suki render_html 23.1 ms 4,205,164 bytes
KaTeX renderToString 116.3 ms 2,797,925 bytes
At 3000 formulas the ratios hold: 90.2 ms against 1463.0 ms for outline SVG, and 47.5 ms against 232.3 ms for HTML.
Two notes on how this was counted. KaTeX was measured with output: 'html'; its default
htmlAndMathml adds a MathML tree for assistive technology and produces more bytes. And suki is
native Rust while the others run on node, so this compares what a caller waits for, not the quality
of the algorithms.
Output size runs the other way for HTML. In SVG suki is slightly smaller, while in HTML it is 1.5 times larger, because it places an absolutely positioned box and a strut for every glyph.
Measured inside the process, loading a font takes 8.7 ms and typesetting one formula into SVG takes 23 µs. Almost all of the loading time is measuring the vertical extent of all 4,802 glyphs; reading the tables together stays under 0.2 ms. The largest part of typesetting is turning glyph outlines into SVG path data, where coordinates are rounded, converted to integers and written digit by digit.
Starting a process per formula erases the difference. For a single formula suki takes 10.3 ms, KaTeX
27.1 ms and MathJax 83.9 ms. Almost all of suki's 10.3 ms is the font load, so pass formulas in
batches, or keep a process alive that holds the Font.
A Font never changes after it is loaded. Layout and writing only read through &Font, so one font
can serve any number of threads at once with no synchronisation.
Sharing one font across 8 threads, typesetting 8 formulas 400 times each, produced results identical to the single-threaded run and finished 3.94 times faster.
The library never starts threads of its own. Running in parallel is the caller's decision.
OpenType is what suki reads. Both CFF outlines (usually .otf) and glyf outlines (.ttf) work. For a font collection (.ttc) the first font is used.
A typeface with a MATH table is strongly recommended: Latin Modern Math, the TeX Gyre Math faces,
Libertinus Math, STIX Two Math and Cambria Math among them. A face without one still works, but then
the constants are derived from the x-height to approximate TeX's defaults, fences are scaled instead
of substituted, and variables are set upright rather than in math italic. Run
cargo run --example compare to see two faces side by side.
font/latinmodern-math.otf is here for tests and samples. The library itself never reads it.
$…$ $$…$$ \(…\) \[…\] equation equation* displaymath math\sin \log \lim \det and so on)^, subscript _, groups { ... }, \limits and \nolimits\frac, binomials \binom, radicals \sqrt and \sqrt[n]\left( ... \right)matrix pmatrix bmatrix Bmatrix vmatrix Vmatrixarray (column spec {lcr} and \hline), case distinctions cases\overline \underline \bar\hat \widehat \tilde \widetilde \vec \dot \ddot\check \breve \acute \grave\overbrace \underbrace, stacking \overset \underset \stackrel\mathrm \mathbf \mathit \mathbb \mathcal \mathscr\mathfrak \mathsf \mathtt \boldsymbol\text, operator names \operatorname\phantom \hphantom \vphantom\big \Big \bigg \Bigg (with \bigl \bigr \bigm)\xrightarrow \xleftarrow \xhookrightarrow \xrightleftharpoons
(both [below]{above})\overrightarrow \overleftarrow\displaystyle \textstyle \scriptstyle \scriptscriptstyle\cfrac, stacked subscripts \substack, \pmod \mod \bmod\, \: \; \! \quad \qquadVariables and bold are set with the math alphanumeric glyphs drawn for that purpose, not by slanting or emboldening upright ones. Only when the typeface lacks those glyphs is the plain letter slanted.
Multi-line environments such as align gather split aligned, commands that change colour or
size, arrows that stretch vertically, and macro definitions are not supported. The multi-line
environments are out because there is no mechanism for aligning across rows.
examples/ holds the commands for checking the output.
cargo run --example check > check.html # a page to inspect by eye
cargo run --example corpus # see whether well-known formulas all parse
cargo run --example corpus -- page > corpus.html
cargo run --example compare -- a.otf b.otf # the same formulas in two typefaces
cargo run --example probe -- a.otf # what a typeface provides
cargo run --release --example bench # measure loading and typesetting
cargo run --release --example many -- svg 1000 > out.svg
The page from check stacks ten representative formulas in all three writers. The three should
look identical, so whichever one differs is the mistake. A faint line marks the baseline, so
vertical alignment against surrounding text can be checked too. Each formula says what to look at.
many exists to give KaTeX and MathJax the same job. Their side lives in test/: run
cd test && npm install, then node test/many.js mathjax 1000.
src/
├── lib.rs public API
├── error.rs the error type
├── token.rs tokenizer
├── node.rs syntax tree types
├── parse.rs parser
├── symbol.rs command to character and class
├── opentype.rs OpenType tables (head hhea maxp hmtx cmap name)
├── cff.rs CFF glyphs and Type 2 charstrings
├── glyf.rs TrueType glyphs
├── math.rs the MATH table
├── outline.rs outlines and their bounding boxes
├── font.rs one font's metrics and glyphs
├── layout.rs gives the syntax tree sizes and positions
├── metric.rs the size of a typeset formula
├── html.rs writes a laid-out formula as HTML
├── svg.rs writes a laid-out formula as SVG
├── text.rs escaping and number formatting
└── main.rs the command that writes the sample page
Layout decides width, height above the baseline, depth below it, and where each child goes, all in em with the root font size as 1. Positive y points down.
HTML places glyphs absolutely. Vertical position comes from a strut of pure height placed first to fix the line's baseline, which avoids depending on the font's ascent and descent.
This library only typesets. It never computes a value.
Dual licensed under MIT and Apache-2.0. Take whichever you prefer.
The bundled font is licensed separately. font/latinmodern-math.otf is Latin Modern Math 1.959
from GUST, the Polish TeX Users Group, under the GUST Font License, which follows the LaTeX
Project Public License 1.3c. The text is in
font/GUST-FONT-LICENSE.txt. The font is bundled for tests and
samples only; the library itself never reads it.