Registry / RepositoryTypeScriptRustPython
Rollpie
ReadmeFiles
Versions
Info
Download
0.2.0579.9 KB2026-09-150.1.1576.6 KB2026-09-12
Version
0.2.0
Copyright
Rollpie, Irohabook
Publisher
math
Published
2026-09-15
Size
579.9 KB
Downloads
2
Checksum
58312d92e63a92784e82a974317afd849e59d143a6320c3cc62faa0980834a7b
Dependencies
None

suki@0.2.0

rollpie get rust/suki@0.2.0

suki

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.

What is different

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.

Usage

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.

Output

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.

Speed

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.

Typesetting in parallel

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.

Fonts

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.

Supported notation

  • Wrappers $…$ $$…$$ \(…\) \[…\] equation equation* displaymath math
  • Letters (italic), digits, symbols, Greek, function names (\sin \log \lim \det and so on)
  • Superscript ^, subscript _, groups { ... }, \limits and \nolimits
  • Fractions \frac, binomials \binom, radicals \sqrt and \sqrt[n]
  • Stretchy fences \left( ... \right)
  • Matrices matrix pmatrix bmatrix Bmatrix vmatrix Vmatrix
  • Tables array (column spec {lcr} and \hline), case distinctions cases
  • Aligned rows align align* aligned alignat alignat* alignedat gather gather* gathered split (align, alignat and gather also set the formula in display)
  • Lines above and below \overline \underline \bar
  • Accents \hat \widehat \tilde \widetilde \vec \dot \ddot
  • More accents \check \breve \acute \grave
  • Braces \overbrace \underbrace, stacking \overset \underset \stackrel
  • Alphabets \mathrm \mathbf \mathit \mathbb \mathcal \mathscr
  • More alphabets \mathfrak \mathsf \mathtt \boldsymbol
  • Text \text, operator names \operatorname and \operatorname* (spaced like \sin and the other function names)
  • Reserved space \phantom \hphantom \vphantom
  • Sized fences \big \Big \bigg \Bigg (with \bigl \bigr \bigm)
  • Stretchy arrows \xrightarrow \xleftarrow \xhookrightarrow \xrightleftharpoons (both [below]{above})
  • Arrows above \overrightarrow \overleftarrow
  • Style switches \displaystyle \textstyle \scriptstyle \scriptscriptstyle
  • Continued fractions \cfrac, stacked subscripts \substack, \pmod \mod \bmod
  • Spacing \, \: \; \! \quad \qquad

Variables 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.

Unsupported notation

Commands that change colour or size, arrows that stretch vertically, macro definitions, and the multline and eqnarray environments are not supported. Equation numbers are not drawn, so align and align* come out the same, as do gather and gather*.

Trying it

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.

Layout

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.

Evaluating math

This library only typesets. It never computes a value.

License

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.