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

README.md

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
# 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](./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.

- [LICENSE-MIT](./LICENSE-MIT)
- [LICENSE-APACHE](./LICENSE-APACHE)

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](./font/GUST-FONT-LICENSE.txt). The font is bundled for tests and
samples only; the library itself never reads it.