djvu-rs 0.16.1

Pure-Rust DjVu codec — decode and encode DjVu documents. MIT licensed, no GPL dependencies.
Documentation
name: Differential vs DjVuLibre

# Compares djvu-rs page renders against `ddjvu` for a small fixed corpus on a
# weekly schedule. The harness lives at examples/diff_djvulibre.rs; the same
# binary is documented in tests/diff_tolerance.md.
#
# Why a separate workflow:
# - DjVuLibre install + render is too slow for per-PR gating (#192 Option B).
# - The fuzz.yml workflow runs in-process libfuzzer; subprocess `ddjvu` would
#   dominate iteration time there.
#
# This is the CI-integration item from #192's DoD. A future Option-C
# pre-rendered corpus would replace the subprocess call with a static
# byte compare and let the diff move into fuzz.yml.

on:
  schedule:
    - cron: '0 4 * * 1'   # every Monday at 04:00 UTC (one hour after fuzz)
  workflow_dispatch:
    inputs:
      tolerance:
        description: 'Per-channel tolerance (0..255)'
        required: false
        default: '4'
      width:
        description: 'Render width in px'
        required: false
        default: '1024'

env:
  CARGO_TERM_COLOR: always
  RUST_BACKTRACE: 1

jobs:
  diff:
    name: ddjvu pixel diff
    runs-on: ubuntu-latest
    timeout-minutes: 20
    steps:
      - uses: actions/checkout@v6

      - name: Install Rust stable
        uses: dtolnay/rust-toolchain@stable

      - name: Install DjVuLibre
        run: |
          sudo apt-get update
          sudo apt-get install -y --no-install-recommends djvulibre-bin

      - name: Cache Rust dependencies
        uses: Swatinem/rust-cache@v2
        with:
          key: diff-djvulibre

      - name: Build diff harness
        run: cargo build --release --features cli --example diff_djvulibre

      # Native-resolution corpus. Files where djvu-rs is currently
      # bit-perfect or near-bit-perfect against ddjvu — see
      # tests/diff_tolerance.md for the empirical baseline.
      #
      # Excluded for now (tracked separately):
      #   colorbook.djvu — residual ≈3.45% on p0, likely glyph-edge
      #                    bilinear-interpolation drift (#199 follow-up)
      #
      # navm_fgbz.djvu was re-added after #248 fixed the page→plane-space
      # coord conversion (#199); worst page p4 measures 0.12% mismatch /
      # mean Δ 0.024, comfortably within the gate below.
      #
      # Width 99999 caps to native page width; small fixtures render at
      # their native dimensions. This is the only mode in which the diff
      # measures *decoder* differences instead of resampling differences.
      - name: Run diff
        env:
          TOL: ${{ github.event.inputs.tolerance || '4' }}
          WIDTH: ${{ github.event.inputs.width || '99999' }}
        run: |
          ./target/release/examples/diff_djvulibre \
            --width "$WIDTH" --tolerance "$TOL" \
            tests/fixtures/boy.djvu \
            tests/fixtures/boy_jb2.djvu \
            tests/fixtures/chicken.djvu \
            tests/fixtures/ccitt_2.djvu \
            tests/fixtures/links.djvu \
            tests/fixtures/problem_page.djvu \
            tests/fixtures/big-scanned-page.djvu \
            tests/fixtures/navm_fgbz.djvu \
              | tee diff_results.jsonl

      # CI gate: any page over the per-codec ceiling fails. See
      # tests/diff_tolerance.md. Tight thresholds because every file in
      # the CI corpus is bit-perfect today; any non-zero mismatch is a
      # regression worth investigating.
      - name: Check thresholds
        run: |
          python3 - <<'PYEOF'
          import json, sys
          PAGE_CEILING_PCT = 0.5      # bit-perfect today; tight gate
          MEAN_DELTA_CEILING = 0.2
          fails = []
          for line in open("diff_results.jsonl"):
              if not line.strip():
                  continue
              d = json.loads(line)
              if d["mismatch_pct"] > PAGE_CEILING_PCT or \
                 d["mean_abs_diff"] > MEAN_DELTA_CEILING:
                  fails.append(d)
          if fails:
              for f in fails:
                  print(f"FAIL {f['file']} p{f['page']}: "
                        f"{f['mismatch_pct']:.2f}% mismatched, "
                        f"mean Δ={f['mean_abs_diff']:.2f}, "
                        f"max Δ={f['max_abs_diff']}")
              sys.exit(1)
          print("All pages within documented tolerance")
          PYEOF

      - name: Upload jsonl artifact
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: diff-results
          path: diff_results.jsonl
          retention-days: 30