1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
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