djvu-rs 0.35.0

Read, render, convert, and create DjVu files. Pure-Rust DjVu decoder/encoder with CLI, WebAssembly, and Python bindings. DjVu to PDF, EPUB, TIFF, PNG, and text. MIT licensed, no GPL dependencies.
Documentation
# Packaging contract (Python + npm)

This document is the release contract for issue #692: every compatible
`djvu-rs` crate release should produce version-matched Python wheels and an
npm WebAssembly package, with smoke tests against the *installed* artifacts.

## Version policy

| Artifact | Version source |
|----------|----------------|
| Rust crates (`djvu-rs` + workspace) | root `Cargo.toml` / release-please |
| Python package `djvu-rs` on PyPI | `djvu-py/Cargo.toml` via maturin (`pyproject.toml` uses `dynamic = ["version"]`) |
| npm package `djvu-rs` | `package.json` generated by `make wasm` from the crate version |

`scripts/check_package_versions.sh` fails the packaging workflow when the
Python crate version or release-please manifest drifts from the root crate.
After `make wasm`, the same script with `CHECK_NPM_PACKAGE=1` verifies the
generated npm `package.json`.

## Python (`djvu-py`)

### Supported wheels

Release CI builds CPython wheels for:

- manylinux (x86_64, aarch64)
- musllinux (x86_64, aarch64)
- macOS (x86_64, arm64)
- Windows (x64)

Documented Python versions: **3.9–3.13** (`requires-python = ">=3.9"`).

### Source distribution

The sdist builds with [maturin](https://www.maturin.rs/) and needs:

- a Rust toolchain (MSRV from root `Cargo.toml` `rust-version`)
- a CPython interpreter matching the target
- network access to crates.io on first build

```sh
pip install djvu-rs          # preferred once published
# or from an sdist / checkout:
pip install ./djvu-py        # requires Rust + maturin
```

### Stable surface

Exposed today: open / `from_bytes`, page metadata, render (including region /
progressive / coarse), text extraction, typed exceptions, zero-copy buffer
views, and document export — `to_pdf` / `write_pdf`, `to_epub` / `write_epub`,
`to_cbz` / `write_cbz`, `to_tiff` / `write_tiff`. Each `to_*` returns bytes;
each `write_*` streams to a file and holds one page at a time.

**Not exposed in the Python bindings** (use the Rust crate / CLI instead):

- encode / `PageEncoder`
- document mutation (`DjVuDocumentMut`)

### Typed exceptions

| Exception | When |
|-----------|------|
| `djvu_rs.Error` | Base class for binding errors |
| `djvu_rs.DecodeError` | Parse / decode / render failures |
| `djvu_rs.IoError` | Filesystem failures from `Document.open` and the `write_*` exporters |
| `djvu_rs.ExportError` | PDF / EPUB / CBZ / TIFF conversion failures |
| `djvu_rs.PageIndexError` | Out-of-range `Document.page` (also an `IndexError`) |

### Smoke tests

```sh
pip install dist/djvu_rs-*.whl
python scripts/smoke_python_wheel.py \
  --fixture tests/fixtures/boy.djvu \
  --expect-version "$VERSION" \
  --repo-root "$PWD"
```

The smoke script refuses to load `djvu-py` sources from the checkout.

## npm (WebAssembly)

Build the dual scalar + `simd128` package:

```sh
make wasm                    # → examples/wasm/pkg
# or: OUT=target/npm-pkg make wasm
```

The package includes TypeScript declarations (`djvu_rs.d.ts`) and both wasm
artifacts under `scalar/` and `simd128/`. Runtime selection uses
`WebAssembly.validate()` on a SIMD probe.

### Smoke tests

```sh
make wasm
npm pack --pack-destination target/npm-dist --workspace=false
# from examples/wasm/pkg:
cd examples/wasm/pkg && npm pack --pack-destination ../../../target/npm-dist

node scripts/smoke_npm_package.mjs \
  --package target/npm-dist/djvu-rs-*.tgz \
  --fixture tests/fixtures/boy.djvu \
  --expect-version "$VERSION" \
  --bundler

node scripts/smoke_npm_browser.mjs \
  --package examples/wasm/pkg \
  --fixture tests/fixtures/boy.djvu
```

## Release workflow

`.github/workflows/publish-packages.yml` runs on version tags (`v*`) and:

1. Checks package versions
2. Builds the full Python wheel + sdist matrix
3. Builds the npm package (`make wasm`) and packs a tarball
4. Runs install-time smokes (Python wheel, Node, bundler, browser)
5. Writes `SHA256SUMS` and GitHub artifact attestations
6. Publishes to PyPI / npm **only if** the corresponding secrets are configured
   and the gate job succeeded

If any platform wheel, the sdist, the npm tarball, or a smoke test fails, the
gate fails and publish jobs do not run — an incomplete set is never advertised
as released.

### Required secrets / trust

| Secret / trust | Purpose |
|----------------|---------|
| `PYPI_API_TOKEN` or PyPI trusted publishing (`id-token`) | Upload wheels + sdist |
| `NPM_TOKEN` | `npm publish` |

Until those are configured, tag builds still produce attested artifacts and
checksums as workflow artifacts for manual verification.

## Related docs

- [`RELEASING.md`]../RELEASING.md — crates.io tag flow
- [`djvu-py/README.md`]../djvu-py/README.md — Python quick start
- [`examples/wasm/README.md`]../examples/wasm/README.md — wasm demo + npm layout