regoli
Import NumPy .npz archives into Typst as numerical arrays.
Both
numpy.savez (stored ZIP) and
numpy.savez_compressed (DEFLATE ZIP) work.
The bundled WebAssembly decoder runs inside Typst: users do not need
Python, NumPy, Rust, filesystem plugins, or preprocessing.
Requires Typst 0.15.0 or newer. This package is not yet published on Typst Universe. It is currently published on the alternative package registry TPIX.
Quick start
Generate an archive in Python:
import numpy as np
np.savez_compressed(
"results.npz",
matrix=np.array([[1.0, 2.0], [3.0, 4.0]]),
counts=np.array([10, 20], dtype=np.int32),
)
In your typst document, import the library:
#import "@sneps/regoli:0.1.1" as regoli
#let data = regoli.npz(path("results.npz"))
#assert.eq(results.matrix, ((1.0, 2.0), (3.0, 4.0)))
#assert.eq(results.counts, (10, 20))
#table(columns: 2, ..results.matrix.flatten().map(x => [#x]))
Then, before compiling the document, have TPIX fetch your project's dependencies (including regoli):
tpix pull
This will download the regoli package and any other dependencies specified in Typst's local cache,
making them available for your project.
You can now compile your document as usual.
API
decode-npz(source, metadata: false, every: 1, start: 0, max-rows: none)
source: rawbytes, normally obtained withread(path, encoding: none).metadata: boolean, defaultfalse.every: positive integer; return one row everyNrows, default1(all rows).start: non-negative, zero-based index of the first row to return, default0.max-rows: non-negative maximum number of rows to return after applyingeveryandstart;none(the default) returns all matching rows.- Returns a dictionary keyed by archive member name, removing only the final
.npysuffix. Names such asnested/nameare preserved; use.at(name)when a name is not a Typst identifier. Positionalnumpy.savezarrays becomearr_0,arr_1, and so on.
By default, values are nested Typst arrays, indexed in logical NumPy axis
order regardless of C/Fortran storage order. Row selection applies to the
first axis of each non-scalar array; a zero-dimensional NumPy array remains a
scalar. For example, decode-npz(source, every: 2, start: 1, max-rows: 10)
returns rows 1, 3, 5, and so on, capped at ten rows per array. An empty
dimension becomes an empty Typst array. Selection happens in the decoder
before output values are constructed; the archive is still fully read and
validated, including values in omitted rows.
Enable metadata to retain exact shapes, including dimensions after a zero:
#import "lib.typ": decode-npz
#let results = decode-npz(read("results.npz", encoding: none), metadata: true)
#let matrix = results.matrix
#assert.eq(matrix.shape, (2, 2))
#assert.eq(matrix.data, ((1.0, 2.0), (3.0, 4.0)))
// matrix.dtype is the NPY descriptor, for example "<f8".
// matrix.fortran-order records the file's storage order.
Each metadata value is a dictionary with data, shape, dtype, and
fortran-order. shape is an array of dimensions; scalars have shape ().
An archive with no arrays returns (:).
npz(source, metadata: false, every: 1, start: 0, max-rows: none)
Wrapper around decode-npz that accepts either a file path or raw bytes and
forwards the same options.
Supported data and limitations
| NumPy dtype | Typst value |
|---|---|
bool |
bool |
int8, int16, int32, int64 |
Exact int |
uint8, uint16, uint32, uint64 |
Exact int, subject to the range below |
float16, float32, float64 |
float (64-bit) |
complex64, complex128 |
Dictionary (real: float, imag: float) per element |
- NPY versions 1.0, 2.0, and 3.0, explicit little/big endian descriptors,
and C/Fortran ordering are supported. Byte-order-independent descriptors
(
|) are supported for single-byte types. Native-endian (=) descriptors are rejected because the writer's byte order cannot be inferred portably; standard NumPy writers emit explicit byte order. - NaN, positive/negative infinity, and signed zero are preserved through CBOR. Floating-point conversion retains numerical values, not NaN payload bits.
uint64values above9223372036854775807fail explicitly rather than silently losing precision, since Typst integers are signed 64-bit.- Object/pickled arrays are never executed or deserialized. String, structured/record, void, datetime, timedelta, extended-precision float, and other unsupported dtypes produce errors. The NPY header reader accepts NumPy's standard unescaped ASCII keys and primitive dtype strings, not arbitrary Python expressions or escaped spellings.
- Only stored and DEFLATE compression are enabled. Encrypted archives and unsupported compression methods fail. Directory entries are ignored; other non-NPY entries, duplicate array names, corrupt checksums, malformed headers, and mismatched data lengths fail with Typst diagnostics.
- Limits per call: 64 MiB archive input, 64 MiB total decompressed member data, 1,024 ZIP entries, 64 KiB per NPY header, rank 32, and 1,000,000 logical array nodes (containers plus elements, summed across arrays). Limits are fixed to bound memory use; this is intended for document-sized datasets, not large-scale numerical processing. Typst compilation memory can be substantially larger than the source archive.
Build and test
The ready-to-use regoli.wasm is included. To rebuild it, install a
Rust toolchain and the WebAssembly target:
rustup target add wasm32-unknown-unknown
Then, you can start a build with just:
just build
The build targets a pure
wasm32-unknown-unknown module with no WASI, filesystem, network, or Python
imports, implementing Typst's minimal plugin protocol.
Development tests require Rust with the WebAssembly target, Python with NumPy,
just, and Tytanic 0.4.1 (which includes its Typst compiler). Install them
with:
rustup target add wasm32-unknown-unknown
python -m pip install numpy
cargo install --locked just
cargo install --locked --version 0.4.1 tytanic
If typstyle is installed, just test also checks the formatting of Typst
sources and tests.
Run the full Rust and Typst package test suite with:
just test
.\scripts\test.ps1
The Tytanic tests are tracked, compile-only Typst scripts for numerical
decoding, error diagnostics, the example, and an isolated @local package
import. The test command generates binary NumPy fixtures, runs Rust unit tests,
Clippy and formatting checks, builds the WASM, then runs tt. Fixtures cover
both ZIP compression modes, NPY header versions, all supported dtype widths
and endiannesses, scalars, empty dimensions, Fortran ordering, integer bounds,
special floats, and invalid input. Generated test fixtures and package files
stay under tests/output and tests/fixtures; they are not part of the
published package.
On Windows, the just recipes require a POSIX-compatible shell such as Git
Bash. The PowerShell wrapper runs the same just test command.
After changing Rust dependencies, regenerate bundled license notices:
python scripts\generate_licenses.py
Local package installation
Following the Typst package repository guidelines,
you can test the package locally during development or before publishing by
copying the project runtime files into Typst's local package directory, e.g.,
%APPDATA%\typst\packages\local\regoli\{version-num}\ on Windows.
For more information, refer to the relevant documentation section.
Then import with:
#import "@local/regoli:0.1.1": decode-npz
#let results = decode-npz(read("results.npz", encoding: none))
Use typst info to locate your actual package data directory when supported
by your compiler.
Publishing to a package registry
The root typst.toml declares the package entrypoint, version, compiler
requirement, author, license, and exclusions for build/test-only files.
The distinctive package name follows the repository's naming guidelines.
Name availability and acceptance still need to be confirmed at submission.
- Rebuild and run all tests; regenerate dependency license notices.
- Bump the project version
- Publishing paths differ depending on the target registry:
TPIX
You can directly bundle and publish the package with the corresponding just recipe.
just publish
Typst Universe
- Fork/clone typst/packages and add the
package under
packages/preview/regoli/0.1.1. - Include the runtime files (those not explicitly excluded in
typst.toml) and, optionally,examples/with itssample.npz. Keep build outputs, test fixtures, and PDFs out of the bundle. Keep Rust sources and build scripts in your development repository so reviewers can reproduce the binary. - Test an absolute package import using a temporary
previewpackage path, then open a pull request with the binary's source/build instructions. - Only after acceptance is
@preview/regoli:{version}publicly available.
License
Original source: AGPL-3.0-only, copyright 2026 carliwal; see LICENSE. The bundled WASM includes third-party dependencies under their respective licenses, including Apache-2.0. Their notices are preserved in THIRD-PARTY-LICENSES.txt; retain that file when redistributing the binary.