bwm is a Rust library for embedding and extracting a blind text or byte payload from raw RGB8/RGBA8 image buffers.
Add the crate from crates.io:
cargo add bwmIn Rust code, import it as bwm:
use bwm::{
embed_text, extract_text, ColorMode, ImageBufferRef, PixelFormat, RobustnessProfile,
ThreadConfig, WatermarkConfig, WatermarkKey, WatermarkStrength,
};
let width = 384;
let height = 384;
let stride = width * 3;
let image = vec![128u8; stride * height];
let config = WatermarkConfig {
key: WatermarkKey::from_bytes([7u8; 32]),
strength: WatermarkStrength::High,
robustness: RobustnessProfile::Balanced,
color_mode: ColorMode::LumaOnly,
threads: ThreadConfig::Single,
};
let input = ImageBufferRef {
data: &image,
width: width as u32,
height: height as u32,
stride,
pixel_format: PixelFormat::Rgb8,
};
let watermarked = embed_text(input, "hello watermark", &config)?;
let recovered = extract_text(watermarked.as_ref(), &config)?;
assert_eq!(recovered, "hello watermark");
# Ok::<(), bwm::WatermarkError>(())Blind means extraction requires only the watermarked image buffer and the same key/config. It does not require the original image, original text, or caller-provided watermark length.
This crate is an initial v0.1 core implementation. It uses a deterministic key-derived block permutation, a self-describing authenticated payload frame, luma-only Haar + DCT coefficient-pair embedding, and repeated-bit recovery. It does not claim arbitrary edit-proof behavior.
embed_text(image, text, config) -> Result<WatermarkedImage, WatermarkError>extract_text(image, config) -> Result<String, WatermarkError>embed_payload(image, payload_bytes, config) -> Result<WatermarkedImage, WatermarkError>extract_payload(image, config) -> Result<Vec<u8>, WatermarkError>capacity(image, config) -> Result<CapacityInfo, WatermarkError>estimate_robust_capacity(image, config) -> Result<CapacityInfo, WatermarkError>
The core API works on borrowed input buffers and returns an owned output buffer. RGBA alpha bytes are preserved exactly.
The crate also builds a small bwm binary for JPEG-oriented smoke tests:
cargo run --bin bwm -- embed \
--key 0707070707070707070707070707070707070707070707070707070707070707 \
--input tests/fixtures/test.jpg \
--output tests/artifacts/test-watermarked.jpg \
--text 'bwm-ok-2026'
cargo run --bin bwm -- extract \
--key 0707070707070707070707070707070707070707070707070707070707070707 \
--input tests/artifacts/test-watermarked.jpgbwm extract tries the original image orientation plus 180, 90, and 270 degree rotations. It does not perform arbitrary crop resynchronization.
GitHub Actions are configured for:
- CI on pushes, pull requests, and manual dispatch: format check, Clippy, full tests, and the explicit JPEG robustness test.
- Release on
v*.*.*tags: verify, buildbwmrelease binaries for Linux x86_64, macOS x86_64, macOS aarch64, and Windows x86_64, attach them to the GitHub release, and publish the crate to crates.io.
crates.io publishing prefers crates.io Trusted Publishing through GitHub Actions OIDC. For first-time publishing, create a fresh crates.io API token and store it as a repository secret named CARGO_REGISTRY_TOKEN.
Before embedding, payload bytes are framed as:
- magic bytes:
BWMK - format version:
1 - flags:
u16 - payload length:
u32 - payload type: text UTF-8 or binary
- keyed BLAKE3 MAC truncated to 128 bits
- payload bytes
The full frame is whitened with a key-derived stream before embedding. Extraction decodes the repeated bits, dewhitens the frame, checks the header and MAC, and then returns the payload.
Robustness is probabilistic and depends on image size, payload size, strength, and the transformations applied after embedding.
Current tested behavior covers:
- no-attack RGB/RGBA round trips
- UTF-8 text round trips
- wrong-key failures
- capacity rejection
- alpha preservation
- deterministic output
- mild brightness and small deterministic noise tests
- fixture JPEG embed/edit/extract through the
bwmbinary, including JPEG quality-85 re-encoding, 10% same-canvas border crop damage, visible diagonal and rectangular occlusion, stronger brightness/contrast/noise changes, and a 180-degree rotated edited JPEG
The Robust profile uses high repetition and has much lower payload capacity than Fast or Balanced. The current version does not implement arbitrary geometric synchronization, so shifted crops, resizes, screenshot pipelines, and social media recompression should be evaluated against your own image set before relying on this crate.
For each selected 8x8 tile, the current implementation:
- converts RGB/RGBA pixels to luma,
- applies one local Haar transform,
- applies a 4x4 DCT to the low-frequency subband,
- embeds one bit by enforcing an ordering margin between two mid-frequency DCT coefficients,
- applies inverse DCT and inverse Haar,
- shifts RGB channels by the resulting luma delta while leaving alpha unchanged.
This is a compact transform-domain implementation aligned with the brief's v0.1 goal, but it is not a full DWT-DCT-SVD port yet. The crate keeps the public API and module boundaries ready for a future SVD or ECC upgrade.
No watermark can survive arbitrary destructive edits. This crate intentionally uses "robust blind watermark" language instead of "edit-proof watermark." If the image is heavily cropped, rotated, downscaled, blurred, compressed, overpainted, or color-clipped, extraction can fail.
Capacity is image-size dependent. Use capacity before embedding large payloads.