M2.1: check text-shadow geometry with translated masks

This commit is contained in:
Fin Warman
2026-10-21 09:06:00 +01:00
parent 5f5a12bf78
commit cd3944c36e
2 changed files with 45 additions and 23 deletions
+43 -23
View File
@@ -1577,41 +1577,61 @@ mod tests {
#[test]
fn legacy_font_effects_are_drawn() {
use wordcraft_doc::CharProps;
// Dark and grey pixel counts, and the grey pixels' mean offset from the dark ones.
let shot = |c: CharProps| {
use wordcraft_doc::wordart::{TextEffects, TextFill};
const SIZE: f32 = 40.0;
const SCALE: f32 = 2.0;
const MIN_DIRECTION_PIXELS: usize = 100;
let shot = |c: CharProps, offset: f32| {
let mut d = Document::from_text("x");
let cp = CharProps { size: Some(40.0), bold: Some(true), ..c };
let cp = CharProps { size: Some(SIZE), bold: Some(true), ..c };
d.body = vec![wordcraft_doc::para_block(wordcraft_doc::Paragraph::with_text("HHHH", cp))];
let l = layout(&d, &mut LayoutCache::new(), &LayoutOptions::default());
let img = render_page(&d, &l.pages[0], 2.0, &RenderOptions::default());
let (mut dark, mut grey) = ((0.0, 0.0, 0.0f64), (0.0, 0.0, 0.0f64));
render_area(&d, &l.pages[0], -offset, -offset, 400.0, 150.0, SCALE, &RenderOptions::default())
};
let counts = |img: &Rendered| {
let (mut dark, mut grey) = (0usize, 0usize);
for (x, y) in (100..800).flat_map(|x| (100..300).map(move |y| (x, y))) {
let v = img.pixel(x, y)[0];
// The copy is solid RGB 128; a wider range would count the glyph's own
// antialiasing, whose centroid hides the copy's offset on some fonts.
let acc = match v {
0..60 => &mut dark,
120..=136 => &mut grey,
_ => continue,
};
*acc = (acc.0 + 1.0, acc.1 + f64::from(x), acc.2 + f64::from(y));
dark += usize::from(v < 60);
grey += usize::from((120..=136).contains(&v));
}
let mean = |a: (f64, f64, f64)| (a.1 / a.0.max(1.0), a.2 / a.0.max(1.0));
let ((dx, dy), (gx, gy)) = (mean(dark), mean(grey));
(dark.0, grey.0, (gx - dx, gy - dy))
(dark, grey)
};
let (plain_dark, plain_grey, _) = shot(CharProps::default());
assert!(plain_dark > 2000.0, "plain {plain_dark}");
let (dark, _, _) = shot(CharProps { outline: Some(true), ..Default::default() });
assert!(dark < plain_dark / 3.0, "outline is hollow: {dark} vs {plain_dark}");
let (plain_dark, plain_grey) = counts(&shot(CharProps::default(), 0.0));
assert!(plain_dark > 2000, "plain {plain_dark}");
let (dark, _) = counts(&shot(CharProps { outline: Some(true), ..Default::default() }, 0.0));
assert!(dark < plain_dark / 3, "outline is hollow: {dark} vs {plain_dark}");
// An explicit fill keeps the reference on the same Art path as legacy effects.
let reference = || CharProps {
text_effects: Some(Box::new(TextEffects { fill: Some(TextFill::Solid { color: Rgb::BLACK, transparency: 0.0 }), ..Default::default() })),
..Default::default()
};
let base = shot(reference(), 0.0);
for (c, sign) in [
(CharProps { shadow: Some(true), ..Default::default() }, 1.0),
(CharProps { emboss: Some(true), ..Default::default() }, 1.0),
(CharProps { engrave: Some(true), ..Default::default() }, -1.0),
] {
let (_, grey, (ox, oy)) = shot(c);
assert!(grey > plain_grey * 2.0, "a grey copy: {grey} vs {plain_grey}");
assert!(ox * sign > 1.0 && oy * sign > 1.0, "grey copy offset {ox} {oy}");
let img = shot(c, 0.0);
let (_, grey) = counts(&img);
assert!(grey > plain_grey * 2, "a grey copy: {grey} vs {plain_grey}");
// A 2pt diagonal displacement at 40pt has sqrt(2)pt on each axis.
// Check the visible copy against its translated mask, accounting for occlusion.
let offset = sign * std::f32::consts::SQRT_2;
let expected = shot(reference(), offset);
let opposite = shot(reference(), -offset);
let (mut visible, mut directional) = (0usize, 0usize);
for (x, y) in (100..800).flat_map(|x| (100..300).map(move |y| (x, y))) {
let v = img.pixel(x, y)[0];
if !(120..=136).contains(&v) || base.pixel(x, y)[0] != 255 {
continue;
}
visible += 1;
assert!(expected.pixel(x, y)[0] < 255, "grey outside translated Art mask: sign {sign}, pixel {x},{y}");
directional += usize::from(expected.pixel(x, y)[0] < 60 && opposite.pixel(x, y)[0] == 255);
}
assert!(directional > MIN_DIRECTION_PIXELS, "directional grey: sign {sign}, {directional} of {visible}");
eprintln!("spatial shadow sign={sign} grey={grey} visible={visible} directional={directional}");
}
}
+2
View File
@@ -214,6 +214,8 @@ Page-relative footer tables keep their physical wrap bounds without adding to fo
The deleted/hidden-text wrapping, ellipse wrapping and widow-control regressions request Source Sans 3 explicitly. Their fixed widths and word counts use that font's metrics. This changes test inputs only, not renderer font substitution or Word parity claims.
The legacy text-shadow control compares visible grey pixels with a translated Art-path mask. Foreground occlusion makes visible pixel centroids unsuitable for measuring the shadow offset. A reversed-offset mutation must fail this spatial check. Renderer behavior is unchanged.
## Revision history
| Date | Change | Summary |