Pictures: WMF/EMF text, pattern and hatch brushes draw; ActiveX previews show (#336)

* ActiveX controls show their pictures

A slide's p:controls are read: each control with a p:pic (the image PowerPoint shows for it,
often a WMF) becomes a picture above the slide's shapes.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Metafiles draw their text, pattern and hatch brushes

deckcraft-metafile (still kurbo + thiserror only, so it builds for wasm):
- Text: WMF EXTTEXTOUT/TEXTOUT and EMF EXTTEXTOUTW/EXTTEXTOUTA/SMALLTEXTOUT with the
  selected font (CREATEFONTINDIRECT, EXTCREATEFONTINDIRECTW: face, signed LOGFONT height,
  width, weight, italic, underline, strike-out, escapement, charset), text and background
  colour, background mode, TA_* alignment and TA_UPDATECP, the dx array (ETO_PDY pairs too),
  ETO_OPAQUE fills and ETO_CLIPPED clips. 8-bit strings decode through their charset
  (Windows-1252/1250/1251; symbol fonts keep their codes at U+F000; double-byte sets keep
  their advances). The neutral Item::Text carries a text-space-to-picture transform, so the
  mapping and escapement are folded in and glyphs are never mirrored.
- Brushes: WMF DIBCREATEPATTERNBRUSH, EMF CREATEDIBPATTERNBRUSHPT and CREATEMONOBRUSH, and
  BS_HATCHED, as Item::Pattern tiles in 96-dpi pixels; WMF PATBLT. The XOR / AND-pattern /
  XOR see-through shadow of old PowerPoint WMFs draws its pattern in the XOR colour.
- MFCOMMENT escapes with an embedded EMF are ignored: PowerPoint draws the WMF's records.
- Pattern pixels and text count against the byte budget; strings are capped.

deckcraft-render draws text through deckcraft-fonts with the face slide text would use
(document_face), symbol fonts mapped as bullets map them (deckcraft_text::symbol_char, now
public, with Webdings), per-character fallback, the file's advances, GDI cell metrics for
top/bottom alignment, underline/strike-out and synthetic bold/italic. Tiles below 1.5 output
pixels per cell are drawn as their average colour.

On the 24 corpus slides that show a metafile, mean abs diff against PowerPoint goes from
28.31 to 5.06 (with the OLE/ActiveX preview commits).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Brandon Thomas
2026-10-12 22:40:56 -04:00
committed by GitHub
co-authored by Claude Opus 5.5
parent d6ab3d10da
commit e28851d2f1
17 changed files with 1979 additions and 67 deletions
+257 -2
View File
@@ -1,17 +1,30 @@
//! Windows Metafile and Enhanced Metafile pictures (`image/x-wmf`, `image/x-emf`): parsed by
//! `deckcraft-metafile` and drawn as vectors into a bitmap of the size the slide needs, so they go
//! through the same crop, tile and recolour path as any other picture.
//!
//! Text items name the font the file asks for; they are drawn with the face slide text would use
//! for that family (`FontDb::document_face`, so a missing font falls back the same way), symbol
//! fonts mapped as bullets map them, and the file's own advances (the dx array) placing each
//! character.
use std::collections::HashMap;
use std::sync::Arc;
use deckcraft_metafile::PlacedItem;
use kurbo::{Affine, Rect, Shape as _};
use deckcraft_fonts::{FontDb, FontFace};
use deckcraft_metafile::{HAlign, Item, PlacedItem, Text, Tile, VAlign};
use kurbo::{Affine, BezPath, Rect, Shape as _};
use skrifa::MetadataProvider as _;
use vello_cpu::{Pixmap, RenderContext, Resources, peniko};
/// Longest side of a metafile drawn without a target size (export, thumbnails of the picture).
pub const DEFAULT_SIDE: u32 = 1024;
/// Largest side drawn.
const MAX_SIDE: u32 = 8192;
/// Pattern cells smaller than this (output pixels) are drawn as the tile's average colour: a
/// sub-pixel pattern would only alias.
const MIN_CELL_PX: f64 = 1.5;
/// Size of text whose font gives no height (LOGFONT height 0), in text units.
const DEFAULT_EM: f64 = 12.0;
/// Is `bytes` a WMF or EMF file?
pub fn is_metafile(bytes: &[u8]) -> bool {
@@ -31,6 +44,8 @@ pub fn rasterize(bytes: &[u8], side: u32) -> Option<image::RgbaImage> {
let w = (pw * k).round().clamp(1.0, MAX_SIDE as f64) as u16;
let h = (ph * k).round().clamp(1.0, MAX_SIDE as f64) as u16;
let pixmaps: Vec<Option<Arc<Pixmap>>> = pic.items.iter_mut().map(|it| it.take_pixels().and_then(|(w, h, px)| premul(w, h, &px))).collect();
// Pattern tiles shared by several fills are converted once.
let mut tiles: HashMap<*const Tile, (Option<Arc<Pixmap>>, peniko::Color)> = HashMap::new();
let mut ctx = RenderContext::new_with(w, h, vello_cpu::RenderSettings { num_threads: 0, ..Default::default() });
let r = Rect::new(0.0, 0.0, w as f64, h as f64);
ctx.push_clip_layer(&r.to_path(0.1));
@@ -60,6 +75,43 @@ pub fn rasterize(bytes: &[u8], side: u32) -> Option<image::RgbaImage> {
ctx.fill_rect(&rect);
ctx.reset_paint_transform();
}
PlacedItem::Pattern { index, path, even_odd, tile } => {
let Some(Item::Pattern { tile: t, .. }) = pic.items.get(index) else { continue };
let key = Arc::as_ptr(t);
let (pm, avg) = tiles.entry(key).or_insert_with(|| (premul(t.width, t.height, &t.rgba), average(&t.rgba))).clone();
ctx.set_fill_rule(if even_odd { peniko::Fill::EvenOdd } else { peniko::Fill::NonZero });
let cell = tile.determinant().abs().sqrt();
match pm {
Some(pm) if cell >= MIN_CELL_PX => {
ctx.set_paint(vello_cpu::Image {
image: vello_cpu::ImageSource::Pixmap(pm),
sampler: peniko::ImageSampler {
x_extend: peniko::Extend::Repeat,
y_extend: peniko::Extend::Repeat,
quality: peniko::ImageQuality::Low,
alpha: 1.0,
},
});
ctx.set_paint_transform(tile);
ctx.fill_path(&path);
ctx.reset_paint_transform();
}
_ => {
ctx.set_paint(avg);
ctx.fill_path(&path);
}
}
}
PlacedItem::Text { index, transform, clip } => {
let Some(Item::Text(t)) = pic.items.get(index) else { continue };
if let Some(c) = clip {
ctx.push_clip_layer(&c.to_path(0.1));
}
draw_text(&mut ctx, t, transform);
if clip.is_some() {
ctx.pop_layer();
}
}
}
}
ctx.set_fill_rule(peniko::Fill::NonZero);
@@ -81,10 +133,213 @@ pub fn rasterize(bytes: &[u8], side: u32) -> Option<image::RgbaImage> {
image::RgbaImage::from_raw(w as u32, h as u32, px)
}
/// One glyph placed in text space (x along the baseline from the string's start, font units at
/// `scale` text units each).
struct Glyph {
face: Arc<FontFace>,
gid: u32,
x: f64,
y: f64,
}
/// Draws a text item; `m` maps its text space to the output.
fn draw_text(ctx: &mut RenderContext, t: &Text, m: Affine) {
let db = FontDb::global();
let font = &t.font;
let family = if font.family.trim().is_empty() { "Arial" } else { font.family.trim() };
let (bold, italic) = (font.weight >= 600, font.italic);
let style = match (bold, italic) {
(true, true) => "Bold Italic",
(true, false) => "Bold",
(false, true) => "Italic",
(false, false) => "Regular",
};
let face = db.document_face(family, style);
let upem = face.upem.max(1.0);
let (asc, desc) = win_metrics(&face);
// LOGFONT height: negative is the em, positive the cell (ascent plus descent).
let h = f64::from(font.height);
let em = if h < 0.0 {
-h
} else if h > 0.0 {
h * upem / (asc + desc).max(1.0)
} else {
DEFAULT_EM
};
let k = em / upem;
if !k.is_finite() || k <= 0.0 {
return;
}
// Characters as drawn: symbol fonts (Wingdings…) map like bullets; each character takes the
// face that has it.
let chars: Vec<char> = t.text.chars().map(|c| deckcraft_text::symbol_char(&face, family, c).unwrap_or(c)).collect();
let faces: Vec<Arc<FontFace>> = chars
.iter()
.map(|c| {
if c.is_whitespace() || c.is_control() || face.covers(*c) { face.clone() } else { db.cascade(&[*c], std::slice::from_ref(&face), style) }
})
.collect();
// Shape runs of one face; record each glyph's pen position and its character.
let mut shaped: Vec<(Glyph, usize)> = Vec::with_capacity(chars.len());
let mut char_pen: Vec<f64> = vec![0.0; chars.len() + 1];
let mut pen = 0.0;
let mut i = 0;
while i < chars.len() {
let Some(f) = faces.get(i).cloned() else { break };
let mut j = i + 1;
while faces.get(j).is_some_and(|g| g.id() == f.id()) {
j += 1;
}
let run: String = chars.get(i..j).unwrap_or(&[]).iter().collect();
// Byte offset in the run → character index in the string.
let byte_char: Vec<usize> = run.char_indices().map(|(b, _)| b).collect();
let ku = f.upem.max(1.0);
for g in deckcraft_fonts::shape(&f, &run, &[], |c| c) {
let ci = i + byte_char.partition_point(|b| *b < g.cluster).min(j - i - 1);
let s = em / ku;
shaped.push((Glyph { face: f.clone(), gid: g.gid, x: pen + f64::from(g.x_offset) * s, y: -f64::from(g.y_offset) * s }, ci));
pen += f64::from(g.x_advance) * s;
if let Some(p) = char_pen.get_mut(ci + 1) {
*p = pen;
}
}
i = j;
}
// Character start pens: a character's start is the previous one's end.
for ci in 1..char_pen.len() {
let prev = char_pen.get(ci - 1).copied().unwrap_or(0.0);
if let Some(p) = char_pen.get_mut(ci)
&& *p < prev
{
*p = prev;
}
}
// The file's advances place each character; glyphs keep their offset within their character.
let use_dx = !t.dx.is_empty() && t.dx.len() == chars.len();
let mut width = pen;
let mut x_scale = 1.0;
if use_dx {
let mut starts = Vec::with_capacity(chars.len() + 1);
let mut x = 0.0;
for d in &t.dx {
starts.push(x);
x += f64::from(*d);
}
width = x;
for (g, ci) in &mut shaped {
let (start, pen_start) = (starts.get(*ci).copied().unwrap_or(0.0), char_pen.get(*ci).copied().unwrap_or(0.0));
g.x = start + (g.x - pen_start);
}
} else if font.width > 0.0 {
// A LOGFONT width narrows or widens the font against its average character width.
let avg = avg_char_width(&face) * k;
if avg > 0.0 {
x_scale = (f64::from(font.width) / avg).clamp(0.05, 20.0);
width *= x_scale;
for (g, _) in &mut shaped {
g.x *= x_scale;
}
}
}
let x0 = match t.h_align {
HAlign::Left => 0.0,
HAlign::Center => -width / 2.0,
HAlign::Right => -width,
};
let (a, d) = (asc * k, desc * k);
let base = match t.v_align {
VAlign::Baseline => 0.0,
VAlign::Top => a,
VAlign::Bottom => -d,
};
let at = m * Affine::translate((x0, base));
ctx.set_fill_rule(peniko::Fill::NonZero);
if let Some(bg) = t.background
&& width > 0.0
{
ctx.set_paint(rgba(bg));
ctx.fill_path(&(at * Rect::new(0.0, -a, width, d).to_path(0.1)));
}
let color = rgba(t.color);
ctx.set_paint(color);
for (g, _) in &shaped {
let path = db.outline(&g.face, g.gid);
if path.elements().is_empty() {
continue;
}
let s = em / g.face.upem.max(1.0);
let skew = if italic && !g.face.italic { Affine::new([1.0, 0.0, -0.2, 1.0, 0.0, 0.0]) } else { Affine::IDENTITY };
let gm = at * Affine::translate((g.x, g.y)) * Affine::scale_non_uniform(x_scale, 1.0) * skew * Affine::scale(s);
let p: BezPath = gm * path.as_ref().clone();
ctx.fill_path(&p);
if bold && g.face.weight < 600.0 {
ctx.set_stroke(kurbo::Stroke::new(em * 0.025 * scale_of(at)).with_join(kurbo::Join::Round));
ctx.stroke_path(&p);
}
}
// Underline and strike-out: the font's own positions and thickness.
let metrics = face.skrifa().map(|f| f.metrics(skrifa::instance::Size::unscaled(), face.location()));
let deco = |on: bool, d: Option<skrifa::metrics::Decoration>, fallback: (f64, f64)| -> Option<Rect> {
if !on || width <= 0.0 {
return None;
}
let (off, th) = d.map_or(fallback, |d| (f64::from(d.offset) * k, f64::from(d.thickness) * k));
let th = if th > 0.0 { th } else { em / 20.0 };
// Offsets are y-up from the baseline; text space is y-down.
Some(Rect::new(0.0, -off - th / 2.0, width, -off + th / 2.0))
};
let lines = [
deco(font.underline, metrics.as_ref().and_then(|m| m.underline), (-em * 0.1, em / 20.0)),
deco(font.strike, metrics.as_ref().and_then(|m| m.strikeout), (em * 0.26, em / 20.0)),
];
for r in lines.into_iter().flatten() {
ctx.fill_path(&(at * r.to_path(0.1)));
}
}
/// Mean scale of an affine transform's linear part.
fn scale_of(m: Affine) -> f64 {
m.determinant().abs().sqrt()
}
/// The face's Windows ascent and descent (OS/2 `usWinAscent`/`usWinDescent`, as GDI measures the
/// cell), in font units; the typographic ones when the font has no OS/2 table.
fn win_metrics(face: &FontFace) -> (f64, f64) {
use skrifa::raw::TableProvider as _;
let os2 = face.skrifa().and_then(|f| f.os2().ok()).map(|o| (f64::from(o.us_win_ascent()), f64::from(o.us_win_descent())));
match os2 {
Some((a, d)) if a + d > 0.0 => (a, d),
_ => (face.ascent.max(0.0), face.descent.max(0.0)),
}
}
/// OS/2 `xAvgCharWidth` in font units (half an em without it).
fn avg_char_width(face: &FontFace) -> f64 {
use skrifa::raw::TableProvider as _;
face.skrifa().and_then(|f| f.os2().ok()).map(|o| f64::from(o.x_avg_char_width())).filter(|w| *w > 0.0).unwrap_or(face.upem * 0.5)
}
fn rgba(c: [u8; 4]) -> peniko::Color {
peniko::Color::from_rgba8(c[0], c[1], c[2], c[3])
}
/// The average colour of straight RGBA pixels (alpha-weighted).
fn average(px: &[u8]) -> peniko::Color {
let (mut r, mut g, mut b, mut a, mut n) = (0u64, 0u64, 0u64, 0u64, 0u64);
for p in px.as_chunks::<4>().0 {
let w = u64::from(p[3]);
r += u64::from(p[0]) * w;
g += u64::from(p[1]) * w;
b += u64::from(p[2]) * w;
a += w;
n += 1;
}
if a == 0 || n == 0 {
return peniko::Color::from_rgba8(0, 0, 0, 0);
}
peniko::Color::from_rgba8((r / a) as u8, (g / a) as u8, (b / a) as u8, (a / n) as u8)
}
/// Straight RGBA rows to a premultiplied pixmap.
fn premul(w: u32, h: u32, px: &[u8]) -> Option<Arc<Pixmap>> {
if w == 0 || h == 0 || w > u16::MAX as u32 || h > u16::MAX as u32 || px.len() < w as usize * h as usize * 4 {
+175
View File
@@ -1268,3 +1268,178 @@ fn text_spilling_out_of_its_box_below_the_slide_is_reported() {
let (_, drawn) = render_slide_drawn(&Presentation::default(), 0, &RenderOpts { scale: 0.5, edit: true, ..Default::default() });
assert_eq!(drawn, Rect::new(0.0, 0.0, 960.0, 540.0));
}
/// A placeable WMF (1440 units per inch, bounds `w` × `h`) holding `records` (function, parameter
/// bytes).
fn wmf_with(w: i16, h: i16, records: &[(u16, Vec<u8>)]) -> Vec<u8> {
let mut body = Vec::new();
for (func, params) in records {
let mut p = params.clone();
if p.len() % 2 == 1 {
p.push(0);
}
body.extend(((6 + p.len()) as u32 / 2).to_le_bytes());
body.extend(func.to_le_bytes());
body.extend(p);
}
body.extend([3, 0, 0, 0, 0, 0]);
let mut f = 0x9AC6_CDD7u32.to_le_bytes().to_vec();
f.extend(0u16.to_le_bytes());
for v in [0i16, 0, w, h] {
f.extend(v.to_le_bytes());
}
f.extend(1440u16.to_le_bytes());
f.extend(0u32.to_le_bytes());
f.extend(0u16.to_le_bytes());
let total = (f.len() + 18 + body.len()) / 2;
for v in [1u16, 9, 0x300] {
f.extend(v.to_le_bytes());
}
f.extend((total as u32).to_le_bytes());
f.extend(16u16.to_le_bytes());
f.extend(0u32.to_le_bytes());
f.extend(0u16.to_le_bytes());
f.extend(body);
f
}
fn i16s(v: &[i32]) -> Vec<u8> {
v.iter().flat_map(|x| (*x as i16).to_le_bytes()).collect()
}
/// CREATEFONTINDIRECT (height, escapement, face) then SELECTOBJECT of it (handle 0), transparent
/// background, and the alignment `ta`.
fn font_records(height: i32, esc: i32, face: &str, ta: i32) -> Vec<(u16, Vec<u8>)> {
let mut f = i16s(&[height, 0, esc, esc, 400]);
f.extend([0, 0, 0, if face.contains("dings") { 2 } else { 0 }, 0, 0, 0, 0]);
f.extend(face.as_bytes());
f.push(0);
vec![(0x02FB, f), (0x012D, i16s(&[0])), (0x0102, i16s(&[1])), (0x012E, i16s(&[ta]))]
}
/// EXTTEXTOUT at (x, y) with an optional dx array.
fn text_record(x: i32, y: i32, s: &str, dx: &[i32]) -> (u16, Vec<u8>) {
let mut v = i16s(&[y, x, s.len() as i32, 0]);
v.extend(s.as_bytes());
if s.len() % 2 == 1 {
v.push(0);
}
v.extend(i16s(dx));
(0x0A32, v)
}
/// Bounding box (x0, y0, x1, y1) of the pixels at least half opaque and darker than mid-grey.
fn mf_ink(img: &image::RgbaImage) -> Option<(u32, u32, u32, u32)> {
let mut b: Option<(u32, u32, u32, u32)> = None;
for (x, y, p) in img.enumerate_pixels() {
if p[3] > 128 && (p[0] as u32 + p[1] as u32 + p[2] as u32) < 384 {
b = Some(match b {
None => (x, y, x, y),
Some((a, c, d, e)) => (a.min(x), c.min(y), d.max(x), e.max(y)),
});
}
}
b
}
#[test]
fn metafile_text_ink_lands_at_its_reference_point() {
// 2 × 1 inch (144 × 72 pt) drawn 288 px wide: 2 px per point. A 36-point "HH", top-left
// aligned at (72, 18) pt.
let mut recs = font_records(-720, 0, "Arial", 0);
recs.push(text_record(1440, 360, "HH", &[]));
let img = crate::metafile::rasterize(&wmf_with(2880, 1440, &recs), 288).expect("raster");
let (x0, y0, x1, y1) = mf_ink(&img).expect("text ink");
assert!((144..156).contains(&x0), "left edge at the reference point: {x0}");
assert!(x1 > x0 + 40 && x1 < 288, "two capitals wide: {x1}");
// The cell's top is at 36 px; a capital starts below the cell top and sits on the baseline.
assert!((38..72).contains(&y0), "cap top below the cell top: {y0}");
assert!((88..112).contains(&y1), "baseline one ascent below the top: {y1}");
}
#[test]
fn metafile_text_centre_baseline_and_escapement() {
// TA_CENTER | TA_BASELINE at (72, 36) pt: the ink is centred on 144 px and sits on 72 px.
let mut recs = font_records(-720, 0, "Arial", 24 | 6);
recs.push(text_record(1440, 720, "HH", &[]));
let img = crate::metafile::rasterize(&wmf_with(2880, 1440, &recs), 288).expect("raster");
let (x0, _, x1, y1) = mf_ink(&img).expect("ink");
assert!(((x0 + x1) as i32 / 2 - 144).abs() <= 4, "centred: {x0}..{x1}");
assert!((69..=74).contains(&y1), "on the baseline: {y1}");
// Escapement 90°: the string runs up the picture.
let mut recs = font_records(-360, 900, "Arial", 24);
recs.push(text_record(1440, 1300, "HHHH", &[]));
let img = crate::metafile::rasterize(&wmf_with(2880, 1440, &recs), 288).expect("raster");
let (x0, y0, x1, y1) = mf_ink(&img).expect("ink");
assert!(y1 - y0 > 2 * (x1 - x0), "a vertical run: {x0} {y0} {x1} {y1}");
assert!(x1 <= 146 && y1 <= 132, "drawn up and left of the baseline origin (144, 130): {x1} {y1}");
}
#[test]
fn metafile_text_follows_its_dx_array() {
// "HH" with the second H a full inch (144 px) after the first: two clumps of ink, a gap between.
let mut recs = font_records(-360, 0, "Arial", 24);
recs.push(text_record(360, 720, "HH", &[1440, 100]));
let img = crate::metafile::rasterize(&wmf_with(2880, 1440, &recs), 288).expect("raster");
let col_has_ink = |x: u32| (0..img.height()).any(|y| img.get_pixel(x, y)[3] > 128);
assert!((36..56).any(col_has_ink), "first H at 36 px");
assert!((180..200).any(col_has_ink), "second H one inch later");
assert!(!(70..170).any(col_has_ink), "nothing between them");
}
#[test]
fn metafile_symbol_text_draws_a_bullet_not_a_box() {
// Webdings "n" is a filled circle (installed, or mapped to a black circle where it is not), as
// is Wingdings "l" in a symbol font that is not installed anywhere.
for (face, s) in [("Webdings", "n"), ("Wingdings Not Installed", "l")] {
let mut recs = font_records(-1080, 0, face, 0);
recs.push(text_record(360, 0, s, &[]));
let img = crate::metafile::rasterize(&wmf_with(2880, 1440, &recs), 288).expect("raster");
let (x0, y0, x1, y1) = mf_ink(&img).expect("ink");
assert!(x1 - x0 > 10 && y1 - y0 > 10, "{face}: a sizeable mark: {x0} {y0} {x1} {y1}");
let (cx, cy) = ((x0 + x1) / 2, (y0 + y1) / 2);
assert!(img.get_pixel(cx, cy)[3] > 200, "{face}: filled in the middle");
for (x, y) in [(x0, y0), (x1, y0), (x0, y1), (x1, y1)] {
assert!(img.get_pixel(x, y)[3] < 128, "{face}: round, not a box: corner ({x}, {y})");
}
}
}
#[test]
fn metafile_opaque_text_fills_its_cell() {
// OPAQUE background in red behind a space.
let mut recs = font_records(-720, 0, "Arial", 0);
recs.push((0x0102, i16s(&[2])));
recs.push((0x0201, 0x0000_00FFu32.to_le_bytes().to_vec()));
recs.push(text_record(1440, 360, " ", &[]));
let img = crate::metafile::rasterize(&wmf_with(2880, 1440, &recs), 288).expect("raster");
let p = img.get_pixel(148, 60);
assert!(p[0] > 200 && p[1] < 60 && p[3] > 200, "cell filled red: {p:?}");
assert!(img.get_pixel(140, 60)[3] < 50, "nothing left of the cell");
}
#[test]
fn metafile_pattern_brush_tiles_and_averages_when_tiny() {
// A 1-bit 8 × 8 checker (palette white, black), a null pen and a one-inch square.
let mut dib: Vec<u8> = [40u32, 8, 8].iter().flat_map(|v| v.to_le_bytes()).collect();
dib.extend(1u16.to_le_bytes());
dib.extend(1u16.to_le_bytes());
dib.extend([0u8; 24]);
dib.extend([255, 255, 255, 0, 0, 0, 0, 0]);
for r in 0..8 {
dib.extend([if r % 2 == 0 { 0x55 } else { 0xAA }, 0, 0, 0]);
}
let mut brush = i16s(&[5, 0]);
brush.extend(dib);
let recs = vec![(0x0142, brush), (0x012D, i16s(&[0])), (0x012D, i16s(&[0x8008])), (0x041B, i16s(&[1440, 1440, 0, 0]))];
let bytes = wmf_with(1440, 1440, &recs);
// 4 px per point: a 0.75 pt cell is 3 px.
let img = crate::metafile::rasterize(&bytes, 288).expect("raster");
let (a, b) = (img.get_pixel(1, 1), img.get_pixel(4, 1));
assert!(a[3] > 200 && b[3] > 200);
assert!((a[0] as i32 - b[0] as i32).abs() > 200, "neighbouring cells alternate: {a:?} {b:?}");
// Far below one pixel per cell: the average grey.
let img = crate::metafile::rasterize(&bytes, 32).expect("raster");
let p = img.get_pixel(16, 16);
assert!((100..156).contains(&p[0]) && p[3] > 200, "average grey: {p:?}");
}