From 10a7a4f2a4c206195ff3587297a71a6154c2614e Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Tue, 13 Oct 2026 06:27:13 -0400 Subject: [PATCH] Render: pattern fills blend neighbouring cells between whole-pixel sizes Pattern tiles were sampled nearest-cell, exact when a pattern cell (0.75 pt) is a whole number of output pixels (a 960-wide export of a 720 pt slide) but harsh otherwise: at 960 px for a 960 pt slide (0.75 px per cell) PowerPoint shows greys where a pixel straddles two cells, and DeckCraft showed only the two colours, in different places. The tile is now sampled bilinearly. Fitted against PowerPoint's export of all 54 presets at 0.75 px per cell: mean error 0.067 (bilinear) against 0.082 (nearest) and 0.095 (area average); still not exact (narVert reads 255/128/0 in PowerPoint, 213/128/42 here). Unchanged at whole-pixel sizes. Wild synthetic set: patterned backgrounds 21.15 -> 11.31 and 18.00 -> 9.75, pattern galleries 22.50 -> 19.44 and 13.96 -> 11.64. Co-Authored-By: Claude Opus 5.5 (1M context) --- crates/render/src/pattern.rs | 5 ++++- crates/render/src/tests_wild.rs | 22 ++++++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/crates/render/src/pattern.rs b/crates/render/src/pattern.rs index 99c6b29..1c26c97 100644 --- a/crates/render/src/pattern.rs +++ b/crates/render/src/pattern.rs @@ -109,7 +109,10 @@ pub fn fill(ctx: &mut RenderContext, name: &str, fg: Rgba, bg: Rgba, alpha: f64, sampler: peniko::ImageSampler { x_extend: peniko::Extend::Repeat, y_extend: peniko::Extend::Repeat, - quality: peniko::ImageQuality::Low, + // Bilinear, as PowerPoint samples the tile: exact at a whole number of output pixels + // per cell; between, a pixel straddling two cells takes a blend of both (fitted to its + // export at 0.75 px per cell: mean error 0.067 against 0.082 for the nearest cell). + quality: peniko::ImageQuality::Medium, alpha: 1.0, }, }); diff --git a/crates/render/src/tests_wild.rs b/crates/render/src/tests_wild.rs index 1611302..53deada 100644 --- a/crates/render/src/tests_wild.rs +++ b/crates/render/src/tests_wild.rs @@ -535,3 +535,25 @@ fn a_groups_shadow_is_cast_by_its_children() { assert!(close(img.pixel(310, 150), Rgba::BLACK, 30), "{:?}", img.pixel(310, 150)); assert!(close(img.pixel(150, 150), RED, 10), "children drawn over it"); } + +#[test] +fn patterns_between_whole_pixel_cells_blend_neighbouring_cells() { + use deckcraft_model::style::PatternFill; + // narVert (alternate cells) at 0.75 output pixels per cell: PowerPoint's export reads + // 255, 128, 0 repeating across; drawing the nearest cell gave only black and white. Bilinear + // sampling gives about 213, 128, 42 (closer on average over all presets, not exact). + let g = |v: u8| ColorRef::rgb(Rgba::rgb(v, v, v)); + let sh = Shape { + id: ShapeId(25), + xfrm: Some(Xfrm::new(0.0, 0.0, 60.0, 60.0)), + fill: Some(Fill::Pattern(PatternFill { preset: "narVert".into(), fg: g(0), bg: g(255) })), + line: no_line(), + ..Default::default() + }; + let img = render(vec![sh]); + let row: Vec = (12..24).map(|x| img.pixel(x, 20)[0]).collect(); + assert!(row.iter().any(|v| (100..=156).contains(v)), "a blended column: {row:?}"); + assert!(row.iter().any(|v| *v < 60) && row.iter().any(|v| *v > 200), "{row:?}"); + // The pattern repeats every 6 px (8 cells). + assert!(row.iter().zip(row.iter().skip(6)).all(|(a, b)| (*a as i32 - *b as i32).abs() <= 2), "{row:?}"); +}