Skip to main content

TrustMark — Rust implementation

An implementation in Rust of TrustMark watermarking, as described in TrustMark - Universal Watermarking for Arbitrary Resolution Images (arXiv:2311.18297) by Tu Bui1, Shruti Agarwal2, and John Collomosse1 2.

This crate implements a subset of the functionality of the TrustMark Python implementation, including encoding and decoding of watermarks for all variants in binary mode. The Rust implementation provides the same levels of error correction as the Python implementation.

Text mode watermarks are not implemented. Watermark removal is available through the explicitly loaded WatermarkRemover and the CLI remove command.

Open an issue if there's something in the Python version that want added to this crate!

Platform Support​

Building this crate requires Rust 1.88 or newer. TrustMark 0.4.0 uses ort 2.0.0-rc.10 as its ONNX runtime, so it can only be used on platforms supported by that crate. See the documentation for supported platforms.

In particular, the ONNX runtime requires either:

  • A recent version of Windows 10/11 & Visual Studio 2022 (≥ 17.11)
  • glibc ≥ 2.35 & libstdc++ >= 12 (Ubuntu ≥ 22.04, Debian ≥ 12 ‘Bookworm’)
  • macOS ≥ 10.15

Quick start​

Download models​

In order to encode or decode watermarks, you'll need to fetch the model files. The models are distributed as ONNX files.

From the workspace root (the rust/ directory), run:

cargo xtask fetch-models

This command downloads models to the models/ directory. You can move them from there as needed.

For watermark removal with P or Q, download the published remover ONNX models:

cargo xtask fetch-removers

This downloads remover_P.onnx and remover_Q.onnx from the same CDN as the encoder/decoder models. Python is not required to download or use them. Both download commands refuse to overwrite existing files.

Other variants require separately supplied, matching ONNX files named remover_{variant}.onnx.

Run the CLI​

As a first step, you can run the trustmark-cli which is defined in this repository.

From the workspace root, run:

cargo run --release -p trustmark-cli -- -m ./models encode -i ../images/ghost.png -o ../images/encoded.png
cargo run --release -p trustmark-cli -- -m ./models decode -i ../images/encoded.png
cargo run --release -p trustmark-cli -- -m ./models remove -i ../images/encoded.png -o ../images/recovered.png --variant Q

The argument to the -m option is the path to the models downloaded; if you moved them, pass the relative file path as the option value.

Use the library​

Add trustmark to your project's cargo manifest with:

cargo add trustmark

This installs the version available on crates.io; TrustMark 0.4.0 contains the ORT rc.10 change.

A basic example of using trustmark is:

use trustmark::{Trustmark, Version, Variant};

let tm = Trustmark::new("./models", Variant::Q, Version::Bch5).unwrap();
let input = image::open("../images/ghost.png").unwrap();
let output = tm.encode("0010101".to_owned(), input, 0.95);

encode and decode take &self, so a loaded model can be shared between callers. Inference on the same encoder or decoder session is serialized; encoding and decoding use separate sessions and can run concurrently.

To remove a watermark, load only the remover for the variant used to embed it:

use trustmark::{WatermarkRemover, Variant};

let remover = WatermarkRemover::new("./models", Variant::Q)?;
let input = image::open("watermarked.png")?;
let recovered = remover.remove(input)?;
recovered.to_rgba8().save("recovered.png")?;

Removal preserves image dimensions and alpha, processes RGB at 8-bit precision like the Python implementation, supports all four model variants, and uses Python's default strength (1.0, or 1.25 for P). Images without a detectable watermark can also be processed, but may still change. Removal is lossy: it does not guarantee the original pixels or erase arbitrary third-party watermarks. The CLI writes PNG or JPEG (--quality 90 by default for JPEG); the library accepts decoded DynamicImage values. Missing or incompatible remover models return errors. A shared remover serializes inference independently of encoders and decoders. Encode/decode-only callers that do not construct a WatermarkRemover incur no additional removal-model startup or memory cost. Constructing a remover loads a separate model session; removal also allocates image and inference buffers. The library returns a floating-point RGB/RGBA image; convert it to 8-bit pixels before saving PNG/JPEG. The CLI performs this conversion and replaces the output only after encoding succeeds. --quality is accepted only for JPEG output. PNG output retains RGB/RGBA channel selection. When replacing an existing removal output, the CLI preserves its ordinary file permissions (not ownership or ACLs). New removal outputs use private temporary-file permissions (owner read/write on Unix); on Unix, replacement does not retain special permission bits.

Running the benchmarks​

Rust benchmarks​

To run the Rust benchmarks, run the following from the workspace root:

cargo bench --bench load --bench encode

After downloading the remover models, use cargo bench --bench remove to measure removal separately. Run real-model removal and re-watermarking tests with:

cargo test --test removal -- --ignored

These tests are opt-in because they require the remover models. P/Q can be downloaded with cargo xtask fetch-removers; B/C require separately supplied matching ONNX files. The normal test suite covers residual interpolation, cropping, alpha preservation, output clamping, invalid model layouts and the CLI without those model files. Rust CI uses the published P/Q files to test removal, re-watermarking, dimensions, alpha and JPEG output. It does not install Python, export models or run Python-reference parity checks.

The Python-reference parity suite remains available for optional manual checks across tiny, odd-sized, wide and tall RGB/RGBA images, including both sides of the square-crop threshold. Run it locally after building the CLI:

cargo build -p trustmark-cli
/path/to/venv/bin/python ../python/test-rust-removal.py

The Python-owned parity test includes the reference loader and output checker. It permits small raster-decoder/resizer differences: at most four 8-bit RGB levels per channel and half a level on average. Use --variant Q to limit the suite to one variant, or --cache to reuse checkpoints in another directory.

Python benchmarks​

To run the Python benchmarks, run the following from the workspace root:

benches/load.sh && benches/encode.sh

Version 0.5.0 compatibility​

Watermark removal adds Error::RemoverModel and Error::InvalidRemoverModel to the public Error enum. This is a breaking change for exhaustive matches, so the crate uses a 0.x minor version bump rather than a patch release. Update exhaustive matches to handle the new variants (or use a wildcard arm). Existing encode/decode behavior and model loading remain unchanged. These removal APIs require version 0.5.0; they are not available in the previously published 0.4.0.

Releasing​

Changes to this crate are validated by the Rust CI workflow, and releases to crates.io are performed by the Rust release workflow when a rust-v* tag is pushed.

See RELEASING.md for the full process, including the manual fallback and the setup required on crates.io. Notable changes are recorded in CHANGELOG.md.

Footnotes​

  1. DECaDE Centre for the Decentralized Digital Economy, University of Surrey, UK. ↩ ↩2

  2. Adobe Research, San Jose, CA. ↩ ↩2