daxrpm/TexTeller-Rust

Native Rust port of TexTeller - convert images of math formulas into LaTeX, byte-exact with the upstream Python implementation.

Rust

0

19 commits

updated May 2, 2026

See the code

README

TexTeller-Rust

Native Rust port of TexTeller — convert images of math formulas into LaTeX, byte-exact with the upstream Python implementation.

License: GPL v3+ Rust 2024

TexTeller-Rust is a clean reimplementation in Rust of the inference pipeline used by OleehyO/TexTeller. It loads the same official ONNX models from HuggingFace and produces token-by-token identical output to the reference Python implementation (optimum.ORTModelForVision2Seq).

Why a Rust port

  • Primary motivation: integrating handwritten-equation recognition into the Rnote project, which is written in Rust.
  • Single binary distribution — no Python interpreter, no virtualenv, no pip install. One executable embeds the ONNX Runtime native bindings.
  • Embeddable as a library — call TexTeller::predict() from any Rust application (drawing apps, note-taking tools, document scanners).
  • Faster cold start — no Python import chain; ≈1 s to load every model, then sub-second per inference.
  • Type safety end-to-end — the borrow checker keeps tensor lifetimes and KV cache state honest.

Example

Inputsamples/equation.png:

equation example

Command:

./target/release/texteller-rust samples/equation.png

Raw output (to stdout):

\frac{ \sqrt{2x}}{2x}= \sum_{i=0}^{ \infty}3i+2+5x

Rendered:

$$\frac{\sqrt{2x}}{2x} = \sum_{i=0}^{\infty} 3i + 2 + 5x$$

Output parity with upstream

On samples/equation.png, the tokens generated and the resulting LaTeX string match the upstream Python implementation byte-by-byte:

PYTHON TexTeller: \frac{ \sqrt{2x}}{2x}= \sum_{i=0}^{ \infty}3i+2+5x
RUST    our:      \frac{ \sqrt{2x}}{2x}= \sum_{i=0}^{ \infty}3i+2+5x

Verified by dumping intermediate tensors and diffing them numerically across the entire pipeline (preprocessing → encoder → decoder → postprocessing).

Pipeline

image (PNG/JPG)
   │
   │ preprocess.rs
   │   1. RGB load
   │   2. trim white border (matches Python's Counter.most_common ordering)
   │   3. RGB → grayscale (ITU-R 601, truncation, exact torchvision coeffs)
   │   4. resize aspect-preserving (BICUBIC + antialias, fast_image_resize)
   │   5. normalize (mean=0.9545, std=0.154)
   │   6. pad to 448×448 with 0.0 (top-left placement)
   ▼
tensor [1, 1, 448, 448]
   │
   │ encoder.rs (encoder_model.onnx, ViT)
   ▼
encoder_hidden_states [1, 785, 768]
   │
   │ decoder.rs (decoder_model.onnx + decoder_with_past_model.onnx, TrOCR)
   │   - First step: input_ids = [bos_token_id], no past
   │   - Subsequent: input_ids = [last_token], past_key_values from prev step
   │   - Greedy argmax until EOS or max_len (1024)
   ▼
token_ids
   │
   │ tokenizer.json (HuggingFace tokenizers)
   ▼
raw LaTeX string
   │
   │ postprocess.rs (faithful port of texteller's to_katex + remove_style)
   ▼
clean LaTeX string

Installation

1. Clone and build

git clone https://github.com/daxrpm/texteller-rust.git
cd texteller-rust
cargo build --release

The first build downloads the ONNX Runtime native library automatically (via ort's download-binaries feature) and takes ~3 minutes. Subsequent builds are seconds.

2. That's it

Run the binary on any image — the model files (~2 GB) are downloaded transparently from HuggingFace on the first run and cached under ~/.cache/huggingface/hub/. Subsequent runs use the cache.

./target/release/texteller-rust samples/equation.png

First run output:

Downloading 6 file(s) from 'OleehyO/TexTeller' on HuggingFace Hub...
[1/6] encoder_model.onnx                [==============================] 344MB/344MB     45MB/s  ETA 0s
[2/6] decoder_model.onnx                [==============================] 909MB/909MB     50MB/s  ETA 0s
[3/6] decoder_with_past_model.onnx      [==============================] 833MB/833MB     50MB/s  ETA 0s
[4/6] tokenizer.json                    [==============================] 1.0MB/1.0MB    300MB/s  ETA 0s
[5/6] config.json                       [==============================] 4.5KB/4.5KB    100MB/s  ETA 0s
[6/6] generation_config.json            [==============================] 154B/154B      100KB/s  ETA 0s
\frac{ \sqrt{2x}}{2x}= \sum_{i=0}^{ \infty}3i+2+5x

Cache is shared with the Python huggingface_hub package, so if you already pulled the models with another tool, no re-download happens.

CLI usage

# Default: auto-downloads models on first run, prints KaTeX-friendly LaTeX.
./target/release/texteller-rust samples/equation.png
# → \frac{ \sqrt{2x}}{2x}= \sum_{i=0}^{ \infty}3i+2+5x

# Verbose: progress logs to stderr.
./target/release/texteller-rust -v samples/equation.png

# Raw output (no postprocessing — keeps \[...\] wrappers).
./target/release/texteller-rust -f raw samples/equation.png
# → \[\frac{ \sqrt{2x}}{2x}= \sum_{i=0}^{ \infty}3i+2+5x\]

# Use a local directory of models (skip the HuggingFace cache lookup).
./target/release/texteller-rust --models-dir /path/to/models my_image.png

# Pin to a different HuggingFace repository (e.g. a fine-tune).
./target/release/texteller-rust --hf-repo my-org/my-checkpoint my_image.png

--help prints the full reference.

Library usage

TexTeller-Rust is also a Rust library you can embed in your own application:

# Cargo.toml
[dependencies]
texteller-rust = "0.1"
use texteller_rust::TexTeller;

fn main() -> anyhow::Result<()> {
    // First call downloads ~2 GB from HuggingFace (cached for next time);
    // subsequent calls return immediately. Build once, reuse.
    let mut model = TexTeller::from_huggingface()?;

    let latex = model.predict("samples/equation.png")?;
    println!("{latex}");

    for img in &["a.png", "b.png", "c.png"] {
        println!("{}", model.predict(img)?);
    }
    Ok(())
}

Three loaders are available:

  • [TexTeller::from_huggingface] — download from OleehyO/TexTeller and cache.
  • [TexTeller::from_huggingface_repo] — same with a custom repo (e.g. a fine-tune).
  • [TexTeller::from_directory] — point at a local directory of model files.

predict_raw() returns the model output without to_katex / remove_style post-processing for callers that need full control.

Project layout

src/
├── lib.rs          Public API (TexTeller struct, top-level docs)
├── main.rs         Thin CLI binary (clap-based)
├── config.rs       Constants tied to the upstream model checkpoint
├── download.rs     HuggingFace Hub auto-download with byte-level progress
├── ort_util.rs     ort::Error<T> → anyhow::Error helper
├── preprocess.rs   Image trim + grayscale + resize + normalize + pad
├── encoder.rs      ViT ONNX session wrapper
├── decoder.rs      TrOCR ONNX greedy decoder with KV cache
└── postprocess.rs  to_katex + remove_style + change_all (faithful port)

Tests: 13 unit tests + 1 doctest. Run with:

cargo test --release

Performance

On an x86_64 CPU (no GPU), release build, samples/equation.png (1401×354 px → 30 tokens):

PhaseTime
Cold start (model load)~1.0 s
Encoder forward~0.3 s
Decoder generation (30 tokens, KV cache)~1.5 s
Total per inference~1.8 s

GPU support is available via ONNX Runtime providers (CUDA, ROCm, DirectML, CoreML, TensorRT) but is not exposed through the CLI yet — see ort documentation if you need it.

Acknowledgments

All the heavy lifting — the model architecture, training data, training code, and ONNX checkpoints used at runtime — is the work of OleehyO and the TexTeller contributors.

This project is purely a Rust runtime port. The upstream documentation, paper references, and training scripts are the source of truth for everything model-related.

License

This Rust crate is licensed under the GNU General Public License v3.0 or later, aligning with the free-software values of the project and with downstream consumers such as Rnote (also GPL-3.0-or-later).

The Rust source here is an independent reimplementation of the upstream TexTeller algorithms; no upstream code is copied verbatim. Apache-2.0 → GPL is one-way compatible, so basing this port on Apache-licensed work and releasing it under GPL is permitted. See NOTICE for attribution details.

Model weights are downloaded at runtime from HuggingFace under their original Apache-2.0 license; they are NOT redistributed by this project, and consuming them in your own projects falls under their own Apache terms.

Contributors

daxrpm

19 commits

daxrpm/TexTeller-Rust

Native Rust port of TexTeller - convert images of math formulas into LaTeX, byte-exact with the upstream Python implementation.

Rust

0

19 commits

updated May 2, 2026

See the code

README

TexTeller-Rust

Native Rust port of TexTeller — convert images of math formulas into LaTeX, byte-exact with the upstream Python implementation.

License: GPL v3+ Rust 2024

TexTeller-Rust is a clean reimplementation in Rust of the inference pipeline used by OleehyO/TexTeller. It loads the same official ONNX models from HuggingFace and produces token-by-token identical output to the reference Python implementation (optimum.ORTModelForVision2Seq).

Why a Rust port

  • Primary motivation: integrating handwritten-equation recognition into the Rnote project, which is written in Rust.
  • Single binary distribution — no Python interpreter, no virtualenv, no pip install. One executable embeds the ONNX Runtime native bindings.
  • Embeddable as a library — call TexTeller::predict() from any Rust application (drawing apps, note-taking tools, document scanners).
  • Faster cold start — no Python import chain; ≈1 s to load every model, then sub-second per inference.
  • Type safety end-to-end — the borrow checker keeps tensor lifetimes and KV cache state honest.

Example

Inputsamples/equation.png:

equation example

Command:

./target/release/texteller-rust samples/equation.png

Raw output (to stdout):

\frac{ \sqrt{2x}}{2x}= \sum_{i=0}^{ \infty}3i+2+5x

Rendered:

$$\frac{\sqrt{2x}}{2x} = \sum_{i=0}^{\infty} 3i + 2 + 5x$$

Output parity with upstream

On samples/equation.png, the tokens generated and the resulting LaTeX string match the upstream Python implementation byte-by-byte:

PYTHON TexTeller: \frac{ \sqrt{2x}}{2x}= \sum_{i=0}^{ \infty}3i+2+5x
RUST    our:      \frac{ \sqrt{2x}}{2x}= \sum_{i=0}^{ \infty}3i+2+5x

Verified by dumping intermediate tensors and diffing them numerically across the entire pipeline (preprocessing → encoder → decoder → postprocessing).

Pipeline

image (PNG/JPG)
   │
   │ preprocess.rs
   │   1. RGB load
   │   2. trim white border (matches Python's Counter.most_common ordering)
   │   3. RGB → grayscale (ITU-R 601, truncation, exact torchvision coeffs)
   │   4. resize aspect-preserving (BICUBIC + antialias, fast_image_resize)
   │   5. normalize (mean=0.9545, std=0.154)
   │   6. pad to 448×448 with 0.0 (top-left placement)
   ▼
tensor [1, 1, 448, 448]
   │
   │ encoder.rs (encoder_model.onnx, ViT)
   ▼
encoder_hidden_states [1, 785, 768]
   │
   │ decoder.rs (decoder_model.onnx + decoder_with_past_model.onnx, TrOCR)
   │   - First step: input_ids = [bos_token_id], no past
   │   - Subsequent: input_ids = [last_token], past_key_values from prev step
   │   - Greedy argmax until EOS or max_len (1024)
   ▼
token_ids
   │
   │ tokenizer.json (HuggingFace tokenizers)
   ▼
raw LaTeX string
   │
   │ postprocess.rs (faithful port of texteller's to_katex + remove_style)
   ▼
clean LaTeX string

Installation

1. Clone and build

git clone https://github.com/daxrpm/texteller-rust.git
cd texteller-rust
cargo build --release

The first build downloads the ONNX Runtime native library automatically (via ort's download-binaries feature) and takes ~3 minutes. Subsequent builds are seconds.

2. That's it

Run the binary on any image — the model files (~2 GB) are downloaded transparently from HuggingFace on the first run and cached under ~/.cache/huggingface/hub/. Subsequent runs use the cache.

./target/release/texteller-rust samples/equation.png

First run output:

Downloading 6 file(s) from 'OleehyO/TexTeller' on HuggingFace Hub...
[1/6] encoder_model.onnx                [==============================] 344MB/344MB     45MB/s  ETA 0s
[2/6] decoder_model.onnx                [==============================] 909MB/909MB     50MB/s  ETA 0s
[3/6] decoder_with_past_model.onnx      [==============================] 833MB/833MB     50MB/s  ETA 0s
[4/6] tokenizer.json                    [==============================] 1.0MB/1.0MB    300MB/s  ETA 0s
[5/6] config.json                       [==============================] 4.5KB/4.5KB    100MB/s  ETA 0s
[6/6] generation_config.json            [==============================] 154B/154B      100KB/s  ETA 0s
\frac{ \sqrt{2x}}{2x}= \sum_{i=0}^{ \infty}3i+2+5x

Cache is shared with the Python huggingface_hub package, so if you already pulled the models with another tool, no re-download happens.

CLI usage

# Default: auto-downloads models on first run, prints KaTeX-friendly LaTeX.
./target/release/texteller-rust samples/equation.png
# → \frac{ \sqrt{2x}}{2x}= \sum_{i=0}^{ \infty}3i+2+5x

# Verbose: progress logs to stderr.
./target/release/texteller-rust -v samples/equation.png

# Raw output (no postprocessing — keeps \[...\] wrappers).
./target/release/texteller-rust -f raw samples/equation.png
# → \[\frac{ \sqrt{2x}}{2x}= \sum_{i=0}^{ \infty}3i+2+5x\]

# Use a local directory of models (skip the HuggingFace cache lookup).
./target/release/texteller-rust --models-dir /path/to/models my_image.png

# Pin to a different HuggingFace repository (e.g. a fine-tune).
./target/release/texteller-rust --hf-repo my-org/my-checkpoint my_image.png

--help prints the full reference.

Library usage

TexTeller-Rust is also a Rust library you can embed in your own application:

# Cargo.toml
[dependencies]
texteller-rust = "0.1"
use texteller_rust::TexTeller;

fn main() -> anyhow::Result<()> {
    // First call downloads ~2 GB from HuggingFace (cached for next time);
    // subsequent calls return immediately. Build once, reuse.
    let mut model = TexTeller::from_huggingface()?;

    let latex = model.predict("samples/equation.png")?;
    println!("{latex}");

    for img in &["a.png", "b.png", "c.png"] {
        println!("{}", model.predict(img)?);
    }
    Ok(())
}

Three loaders are available:

  • [TexTeller::from_huggingface] — download from OleehyO/TexTeller and cache.
  • [TexTeller::from_huggingface_repo] — same with a custom repo (e.g. a fine-tune).
  • [TexTeller::from_directory] — point at a local directory of model files.

predict_raw() returns the model output without to_katex / remove_style post-processing for callers that need full control.

Project layout

src/
├── lib.rs          Public API (TexTeller struct, top-level docs)
├── main.rs         Thin CLI binary (clap-based)
├── config.rs       Constants tied to the upstream model checkpoint
├── download.rs     HuggingFace Hub auto-download with byte-level progress
├── ort_util.rs     ort::Error<T> → anyhow::Error helper
├── preprocess.rs   Image trim + grayscale + resize + normalize + pad
├── encoder.rs      ViT ONNX session wrapper
├── decoder.rs      TrOCR ONNX greedy decoder with KV cache
└── postprocess.rs  to_katex + remove_style + change_all (faithful port)

Tests: 13 unit tests + 1 doctest. Run with:

cargo test --release

Performance

On an x86_64 CPU (no GPU), release build, samples/equation.png (1401×354 px → 30 tokens):

PhaseTime
Cold start (model load)~1.0 s
Encoder forward~0.3 s
Decoder generation (30 tokens, KV cache)~1.5 s
Total per inference~1.8 s

GPU support is available via ONNX Runtime providers (CUDA, ROCm, DirectML, CoreML, TensorRT) but is not exposed through the CLI yet — see ort documentation if you need it.

Acknowledgments

All the heavy lifting — the model architecture, training data, training code, and ONNX checkpoints used at runtime — is the work of OleehyO and the TexTeller contributors.

This project is purely a Rust runtime port. The upstream documentation, paper references, and training scripts are the source of truth for everything model-related.

License

This Rust crate is licensed under the GNU General Public License v3.0 or later, aligning with the free-software values of the project and with downstream consumers such as Rnote (also GPL-3.0-or-later).

The Rust source here is an independent reimplementation of the upstream TexTeller algorithms; no upstream code is copied verbatim. Apache-2.0 → GPL is one-way compatible, so basing this port on Apache-licensed work and releasing it under GPL is permitted. See NOTICE for attribution details.

Model weights are downloaded at runtime from HuggingFace under their original Apache-2.0 license; they are NOT redistributed by this project, and consuming them in your own projects falls under their own Apache terms.

Contributors

daxrpm

19 commits

Languages

Rust

100.0%