From c02203fd2503954b765dbd4f60a42a2654e0674b Mon Sep 17 00:00:00 2001 From: Pierre Le Fevre Date: Thu, 16 Jul 2026 19:14:59 +0800 Subject: [PATCH] Improve JPEG chroma upsampling for Bing parity Refs isu issue 280 Refs isu issue 382 --- .isu/issues.json | 15 ++++++++- crates/image/src/jpeg.rs | 66 +++++++++++++++++++++++++++++++++++++--- 2 files changed, 75 insertions(+), 6 deletions(-) diff --git a/.isu/issues.json b/.isu/issues.json index c92046d..c280251 100644 --- a/.isu/issues.json +++ b/.isu/issues.json @@ -1,5 +1,5 @@ { - "next_id": 382, + "next_id": 383, "issues": [ { "id": 1, @@ -4673,6 +4673,19 @@ "author": "piefev", "state": "closed", "created_at": "2026-07-16T09:55:15Z" + }, + { + "id": 382, + "repo": "we", + "title": "Bing parity remains above threshold after JPEG chroma smoothing", + "body": "Parent: isu issue 280.\n\nAfter adding center-aligned linear chroma upsampling in the JPEG decoder, the Bing real-web scenario still cannot remove its xfail.\n\nRepro:\n`cargo run -p we-e2e -- --scenario crates/e2e/scenarios/real-web/bing.com.we --out-dir crates/e2e/artifacts`\n\nCurrent result after the JPEG chroma smoothing pass:\n- committed Chromium golden comparison still fails because the committed goldens show a different live Bing page state than the current Sichuan Tea offline snapshot:\n - desktop: `4.85% match (1168914/1228500 px differ, tol=4, max_diff=0.1000%)`\n - mobile: `5.17% match (312140/329160 px differ, tol=4, max_diff=0.1000%)`\n- same-cache Chromium artifacts remain materially different even when daily-content drift is removed:\n - desktop vs `desktop.scenario-cache.chromium.png`: `50.70% match (605673/1228500 px differ, tol=4)`\n - mobile vs `mobile.scenario-cache.chromium.png`: `43.92% match (184603/329160 px differ, tol=4)`\n\nArtifacts:\n- `crates/e2e/artifacts/real-web/bing.com/desktop.png`\n- `crates/e2e/artifacts/real-web/bing.com/desktop.png.diff.png`\n- `crates/e2e/artifacts/real-web/bing.com/mobile.png`\n- `crates/e2e/artifacts/real-web/bing.com/mobile.png.diff.png`\n- `crates/e2e/artifacts/real-web/bing.com/desktop.scenario-cache.chromium.png`\n- `crates/e2e/artifacts/real-web/bing.com/mobile.scenario-cache.chromium.png`\n\nLikely remaining areas:\n- rebaseline or replace the stale committed Bing Chromium goldens per isu issue 305,\n- render CSS linear gradients per-fragment instead of approximating them with corner colors, especially Bing's shader overlays with stops like `-20%`, `30%`, and `100%`,\n- continue image/text raster parity work for the strict Chromium threshold.\n\nAcceptance: `crates/e2e/scenarios/real-web/bing.com.we` can remove `# xfail` and pass both screenshot assertions within the default harness threshold against deterministic Chromium references.", + "labels": [ + "real-web" + ], + "assigned": [], + "author": "piefev", + "state": "open", + "created_at": "2026-07-16T11:10:08Z" } ] } diff --git a/crates/image/src/jpeg.rs b/crates/image/src/jpeg.rs index 2e2b70e..b7beb15 100644 --- a/crates/image/src/jpeg.rs +++ b/crates/image/src/jpeg.rs @@ -897,15 +897,55 @@ fn upsample( let mut out = vec![0u8; target_width * target_height]; for y in 0..target_height { - let sy = (y / v_ratio).min(sample_height - 1); + let (y0, y1, wy) = lerp_indices(y, v_ratio, sample_height); for x in 0..target_width { - let sx = (x / h_ratio).min(sample_width - 1); - out[y * target_width + x] = samples[sy * sample_width + sx]; + let (x0, x1, wx) = lerp_indices(x, h_ratio, sample_width); + let top = lerp_u8( + samples[y0 * sample_width + x0], + samples[y0 * sample_width + x1], + wx, + ); + let bottom = lerp_u8( + samples[y1 * sample_width + x0], + samples[y1 * sample_width + x1], + wx, + ); + out[y * target_width + x] = lerp_u8(top, bottom, wy); } } out } +fn lerp_indices(dst: usize, ratio: usize, source_len: usize) -> (usize, usize, u16) { + if source_len <= 1 || ratio <= 1 { + let idx = dst.min(source_len.saturating_sub(1)); + return (idx, idx, 0); + } + + // Map destination pixel centers back into source space: + // source = (dst + 0.5) / ratio - 0.5. + let numerator = (2 * dst + 1) as isize - ratio as isize; + if numerator <= 0 { + return (0, 0, 0); + } + + let denominator = 2 * ratio; + let i0 = (numerator as usize / denominator).min(source_len - 1); + if i0 + 1 >= source_len { + return (source_len - 1, source_len - 1, 0); + } + + let rem = numerator as usize % denominator; + let weight = ((rem * 256 + denominator / 2) / denominator) as u16; + (i0, i0 + 1, weight) +} + +fn lerp_u8(a: u8, b: u8, weight_b: u16) -> u8 { + let wa = 256u32 - weight_b as u32; + let wb = weight_b as u32; + (((a as u32 * wa) + (b as u32 * wb) + 128) >> 8) as u8 +} + // --------------------------------------------------------------------------- // YCbCr to RGB conversion // --------------------------------------------------------------------------- @@ -1311,10 +1351,11 @@ mod tests { #[test] fn upsample_2x_horizontal() { - // 1x2 upsampled to 2x2 + // Two chroma samples expanded to four luma pixels using center-aligned + // linear interpolation rather than nearest-neighbor replication. let samples = vec![10, 20]; let result = upsample(&samples, 2, 1, 1, 1, 2, 1, 4, 1); - assert_eq!(result, vec![10, 10, 20, 20]); + assert_eq!(result, vec![10, 13, 18, 20]); } #[test] @@ -1325,6 +1366,21 @@ mod tests { assert_eq!(result, vec![42, 42, 42, 42]); } + #[test] + fn upsample_2x_both_smooths_between_samples() { + let samples = vec![0, 100, 200, 255]; + let result = upsample(&samples, 2, 2, 1, 1, 2, 2, 4, 4); + assert_eq!( + result, + vec![ + 0, 25, 75, 100, // + 50, 72, 117, 139, // + 150, 167, 200, 216, // + 200, 214, 241, 255, + ] + ); + } + // -- Minimal JPEG construction and decoding -- /// Build a minimal 1-component (grayscale) 8x8 JPEG. -- 2.51.2