From 341437e6648978d18643a707a760c245028dcd16 Mon Sep 17 00:00:00 2001 From: Pierre Le Fevre Date: Sat, 18 Jul 2026 20:39:15 +0800 Subject: [PATCH] Refine glyph curve subdivision for Opera parity Refs isu issue 281. Refs isu issue 413. --- .isu/issues.json | 17 ++++++++++++- crates/text/src/font/rasterizer.rs | 39 +++++++++++++++++++++++++++--- 2 files changed, 52 insertions(+), 4 deletions(-) diff --git a/.isu/issues.json b/.isu/issues.json index a3daccd..bfa4d3d 100644 --- a/.isu/issues.json +++ b/.isu/issues.json @@ -1,5 +1,5 @@ { - "next_id": 413, + "next_id": 414, "issues": [ { "id": 1, @@ -5063,6 +5063,21 @@ "author": "piefev", "state": "open", "created_at": "2026-07-18T11:45:02Z" + }, + { + "id": 413, + "repo": "we", + "title": "Opera parity remains blocked after glyph curve subdivision", + "body": "Parent: isu issue 281.\n\nThis pass tested a safer glyph-rasterizer improvement: adaptive quadratic curve subdivision in crates/text/src/font/rasterizer.rs. The change improves outline flattening fidelity for high-curvature glyph segments, but the opera.com Chromium parity scenario remains blocked and should keep its xfail.\n\nRepro:\n cargo run -p we-e2e -- --scenario crates/e2e/scenarios/real-web/opera.com.we --out-dir crates/e2e/artifacts\n\nResult after the pass:\n- desktop L96: 88.09% match (146342/1228500 px differ, tol=4, max_diff=0.1000%)\n- mobile L111: 80.11% match (65454/329160 px differ, tol=4, max_diff=0.1000%)\n- DOM and keyboard interactivity assertions pass; console output is limited to expected offline GTM misses.\n\nArtifacts:\n- crates/e2e/artifacts/real-web/opera.com/desktop.png\n- crates/e2e/artifacts/real-web/opera.com/desktop.png.diff.png\n- crates/e2e/artifacts/real-web/opera.com/mobile.png\n- crates/e2e/artifacts/real-web/opera.com/mobile.png.diff.png\n\nInvestigation notes:\n- A render-side glyph baseline offset improved raw pixel counts but was rejected as too global and not tied to a defensible font metric or rasterizer invariant.\n- A metric-aware CSS line-height: normal experiment regressed desktop parity to 85.17%, so it was backed out.\n- Remaining visible gaps still point at broader glyph hinting/antialiasing fidelity and Opera first-viewport image/WebP parity, tracked by isu issues 411 and 412.\n\nAcceptance: resolve the remaining WebP and glyph rendering blockers, then remove the xfail from crates/e2e/scenarios/real-web/opera.com.we only once both Chromium screenshot assertions pass within the committed threshold.", + "labels": [ + "real-web", + "text", + "render" + ], + "assigned": [], + "author": "piefev", + "state": "open", + "created_at": "2026-07-18T12:33:09Z" } ] } diff --git a/crates/text/src/font/rasterizer.rs b/crates/text/src/font/rasterizer.rs index d220abf..58da6eb 100644 --- a/crates/text/src/font/rasterizer.rs +++ b/crates/text/src/font/rasterizer.rs @@ -129,12 +129,12 @@ fn flatten_quadratic( y2: f32, segments: &mut Vec, ) { - const STEPS: usize = 8; + let steps = quadratic_subdivision_steps(x0, y0, x1, y1, x2, y2); let mut prev_x = x0; let mut prev_y = y0; - for i in 1..=STEPS { - let t = i as f32 / STEPS as f32; + for i in 1..=steps { + let t = i as f32 / steps as f32; let t_inv = 1.0 - t; // Quadratic formula: P = (1-t)^2*P0 + 2t(1-t)*P1 + t^2*P2 @@ -153,6 +153,24 @@ fn flatten_quadratic( } } +fn quadratic_subdivision_steps(x0: f32, y0: f32, x1: f32, y1: f32, x2: f32, y2: f32) -> usize { + const MIN_STEPS: usize = 8; + const MAX_STEPS: usize = 32; + const TARGET_FLATNESS_PX: f32 = 0.125; + + let chord_x = x2 - x0; + let chord_y = y2 - y0; + let chord_len = (chord_x * chord_x + chord_y * chord_y).sqrt(); + if chord_len <= f32::EPSILON { + return MIN_STEPS; + } + + let control_x = x1 - x0; + let control_y = y1 - y0; + let flatness = (chord_x * control_y - chord_y * control_x).abs() / chord_len; + ((flatness / TARGET_FLATNESS_PX).sqrt().ceil() as usize).clamp(MIN_STEPS, MAX_STEPS) +} + /// Rasterizes a scaled outline into a 0-255 coverage bitmap using 16x16 supersampling. pub fn rasterize(outline: &GlyphOutline, scale: f32) -> Option { if outline.contours.is_empty() { @@ -258,6 +276,21 @@ mod tests { crate::font::load_system_font().expect("failed to load system font") } + #[test] + fn quadratic_subdivision_keeps_flat_curves_at_minimum() { + assert_eq!( + quadratic_subdivision_steps(0.0, 0.0, 20.0, 0.001, 40.0, 0.0), + 8 + ); + } + + #[test] + fn quadratic_subdivision_increases_for_large_curves() { + let steps = quadratic_subdivision_steps(0.0, 0.0, 20.0, 40.0, 40.0, 0.0); + assert!(steps > 8, "curved glyph segments should subdivide more"); + assert!(steps <= 32, "subdivision must stay bounded"); + } + #[test] fn rasterize_basic_glyph() { let font = load_test_font(); -- 2.51.2