Only colour 7 flips with the background (fixes #272) (#285)

display_rgb flipped every near-white colour to black on paper and every
near-black colour to white on the dark canvas, so ACI 51/255 and
true-colour white printed black and dark index colours showed white.
Display-list primitives now carry an aci7 flag set when the colour
resolves to 7 (directly, by layer or by block); only those flip.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Valentin Klein 2026-10-10 22:11:24 +02:00 committed by GitHub
parent 3e5e5ac3ce
commit f1743df2dd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 171 additions and 47 deletions

View File

@ -208,16 +208,16 @@ pub fn nearest_aci(c: Rgb) -> u8 {
best.1
}
/// Canvas display colour: ACI 7 (and any near-white/near-black) flips to contrast with the
/// background, as CAD programs do on dark or light model space.
pub fn display_rgb(c: Rgb, background: Rgb) -> Rgb {
let dark_bg = background.luma() < 0.5;
if dark_bg && c.luma() < 0.06 {
Rgb(255, 255, 255)
} else if !dark_bg && c.luma() > 0.94 {
Rgb(0, 0, 0)
} else {
/// Display colour of `c` on `background`. Colour 7 (`aci7`) flips to contrast with the
/// background, white on dark and black on light (paper), as CAD programs draw it; every other
/// colour, including a true-colour or index white or black, keeps its RGB.
pub fn display_rgb(c: Rgb, aci7: bool, background: Rgb) -> Rgb {
if !aci7 {
c
} else if background.luma() < 0.5 {
Rgb(255, 255, 255)
} else {
Rgb(0, 0, 0)
}
}
@ -259,7 +259,10 @@ mod tests {
#[test]
fn white_flips_on_light_background() {
assert_eq!(display_rgb(Rgb(255, 255, 255), Rgb(255, 255, 255)), Rgb(0, 0, 0));
assert_eq!(display_rgb(Rgb(255, 255, 255), Rgb(33, 40, 48)), Rgb(255, 255, 255));
assert_eq!(display_rgb(Rgb(255, 255, 255), true, Rgb(255, 255, 255)), Rgb(0, 0, 0));
assert_eq!(display_rgb(Rgb(255, 255, 255), true, Rgb(33, 40, 48)), Rgb(255, 255, 255));
// Only colour 7 flips: ACI 255 / true-colour white and dark index colours keep their RGB.
assert_eq!(display_rgb(Rgb(255, 255, 255), false, Rgb(255, 255, 255)), Rgb(255, 255, 255));
assert_eq!(display_rgb(aci_rgb(250), false, Rgb(33, 40, 48)), aci_rgb(250));
}
}

View File

@ -7,7 +7,7 @@
use std::fmt::Write as _;
use cadcraft_color::{Rgb, display_rgb};
use cadcraft_color::Rgb;
use cadcraft_doc::{Drawing, PageSetup, Space};
use cadcraft_geom::{Bounds2, Vec2};
use cadcraft_render::{Kind, Sheet, clip, paper};
@ -238,7 +238,7 @@ fn content_stream(list: &cadcraft_render::DisplayList, map: &Map, clip_pt: &Boun
}
};
for p in &list.prims {
let rgb = display_rgb(p.color, white);
let rgb = p.display_rgb(white);
let raw = list.points(p);
if raw.iter().any(|q| !q.is_finite()) {
continue;

View File

@ -2,7 +2,7 @@
use std::fmt::Write as _;
use cadcraft_color::{Rgb, display_rgb};
use cadcraft_color::Rgb;
use cadcraft_doc::{Drawing, Space};
use cadcraft_geom::Bounds2;
use cadcraft_render::Kind;
@ -35,7 +35,7 @@ pub fn export_window(d: &Drawing, space: &Space, window: Option<Bounds2>) -> Str
let tx = |x: f64| x - x0;
let ty = |y: f64| y1 - y;
for p in &list.prims {
let c = display_rgb(p.color, white).hex();
let c = p.display_rgb(white).hex();
let pts = list.points(p);
match p.kind {
Kind::Polyline => {

View File

@ -46,6 +46,9 @@ pub enum Kind {
pub struct DPrim {
pub handle: Handle,
pub color: Rgb,
/// Colour 7 (directly, by layer or by block): drawn white on a dark background and black on
/// a light one, see [`DPrim::display_rgb`]. Other colours keep their RGB.
pub aci7: bool,
/// Lineweight in mm (0 = thinnest).
pub lw: f32,
pub kind: Kind,
@ -66,6 +69,32 @@ pub struct DisplayList {
pub expanded: usize,
}
impl DPrim {
/// The colour to draw with on `background`.
pub fn display_rgb(&self, background: Rgb) -> Rgb {
cadcraft_color::display_rgb(self.color, self.aci7, background)
}
}
/// A resolved colour: its RGB and whether it is colour 7.
#[derive(Clone, Copy, Debug, PartialEq)]
struct Ink {
rgb: Rgb,
aci7: bool,
}
/// Resolve `c` like [`Color::resolve`], noting whether it comes out as colour 7.
fn ink(c: Color, layer: Color, block: Color) -> Ink {
// ByLayer / ByBlock left unresolved (layer or block colour itself logical) mean colour 7.
let is7 = |c: Color| matches!(c, Color::ByLayer | Color::ByBlock | Color::Index(7));
let aci7 = match c {
Color::ByLayer => is7(layer),
Color::ByBlock => is7(block),
c => c == Color::Index(7),
};
Ink { rgb: c.resolve(layer, block), aci7 }
}
impl DisplayList {
pub fn segment_count(&self) -> usize {
self.prims.iter().filter(|p| p.kind == Kind::Polyline).map(|p| p.len.saturating_sub(1) as usize).sum()
@ -146,7 +175,7 @@ struct Builder<'a> {
pub const MAX_BLOCK_EXPANSION: usize = 2_000_000;
impl Builder<'_> {
fn polyline(&mut self, ctx: &Ctx, color: Rgb, lw: f32, pts: &[Vec2]) {
fn polyline(&mut self, ctx: &Ctx, color: Ink, lw: f32, pts: &[Vec2]) {
if pts.len() < 2 {
return;
}
@ -156,9 +185,9 @@ impl Builder<'_> {
self.list.bounds.add(q);
self.list.verts.push(q);
}
self.list.prims.push(DPrim { handle: ctx.top, color, lw, kind: Kind::Polyline, start, len: pts.len() as u32 });
self.list.prims.push(DPrim { handle: ctx.top, color: color.rgb, aci7: color.aci7, lw, kind: Kind::Polyline, start, len: pts.len() as u32 });
}
fn tris(&mut self, ctx: &Ctx, color: Rgb, tris: &[Vec2]) {
fn tris(&mut self, ctx: &Ctx, color: Ink, tris: &[Vec2]) {
if tris.len() < 3 {
return;
}
@ -168,10 +197,10 @@ impl Builder<'_> {
self.list.bounds.add(q);
self.list.tris.push(q);
}
self.list.prims.push(DPrim { handle: ctx.top, color, lw: 0.0, kind: Kind::Tris, start, len: tris.len() as u32 });
self.list.prims.push(DPrim { handle: ctx.top, color: color.rgb, aci7: color.aci7, lw: 0.0, kind: Kind::Tris, start, len: tris.len() as u32 });
}
/// Shaped text: strokes as polylines; TrueType glyphs filled (TEXTFILL) or outlined.
fn shaped(&mut self, ctx: &Ctx, color: Rgb, lw: f32, sh: &cadcraft_fonts::Shaped) {
fn shaped(&mut self, ctx: &Ctx, color: Ink, lw: f32, sh: &cadcraft_fonts::Shaped) {
for s in &sh.strokes {
self.polyline(ctx, color, lw, s);
}
@ -190,18 +219,18 @@ impl Builder<'_> {
}
}
}
fn point(&mut self, ctx: &Ctx, color: Rgb, p: Vec2) {
fn point(&mut self, ctx: &Ctx, color: Ink, p: Vec2) {
let q = ctx.xf.apply(p);
self.list.bounds.add(q);
let start = self.list.verts.len() as u32;
self.list.verts.push(q);
self.list.prims.push(DPrim { handle: ctx.top, color, lw: 0.0, kind: Kind::Point, start, len: 1 });
self.list.prims.push(DPrim { handle: ctx.top, color: color.rgb, aci7: color.aci7, lw: 0.0, kind: Kind::Point, start, len: 1 });
}
fn infinite(&mut self, ctx: &Ctx, color: Rgb, base: Vec2, dir: Vec2, ray: bool) {
fn infinite(&mut self, ctx: &Ctx, color: Ink, base: Vec2, dir: Vec2, ray: bool) {
let start = self.list.verts.len() as u32;
self.list.verts.push(ctx.xf.apply(base));
self.list.verts.push(ctx.xf.apply_vec(dir).normalized());
self.list.prims.push(DPrim { handle: ctx.top, color, lw: 0.0, kind: Kind::Infinite { ray }, start, len: 2 });
self.list.prims.push(DPrim { handle: ctx.top, color: color.rgb, aci7: color.aci7, lw: 0.0, kind: Kind::Infinite { ray }, start, len: 2 });
}
/// Take one unit of the block-expansion budget; false once it is spent.
fn expand(&mut self) -> bool {
@ -372,7 +401,7 @@ fn push_raw(dst: &mut DisplayList, p: &DPrim, kind: Kind, pts: &[Vec2]) {
} else {
dst.verts.extend_from_slice(pts);
}
dst.prims.push(DPrim { handle: p.handle, color: p.color, lw: p.lw, kind, start, len: pts.len() as u32 });
dst.prims.push(DPrim { handle: p.handle, color: p.color, aci7: p.aci7, lw: p.lw, kind, start, len: pts.len() as u32 });
}
/// Build the display list for a set of loose entities (previews, rubber bands).
@ -393,7 +422,7 @@ pub fn build_entities<'a, I: IntoIterator<Item = &'a Entity>>(d: &Drawing, ents:
b.list
}
fn resolve(ctx: &Ctx, e: &Entity, plotting: bool) -> (Rgb, f32, Option<cadcraft_doc::Linetype>, f64, bool) {
fn resolve(ctx: &Ctx, e: &Entity, plotting: bool) -> (Ink, f32, Option<cadcraft_doc::Linetype>, f64, bool) {
let d = ctx.d;
// Layer "0" inside a block takes the insert's layer.
let layer_name = if e.common.layer == "0" { ctx.block_layer.as_deref().unwrap_or("0") } else { e.common.layer.as_str() };
@ -402,7 +431,7 @@ fn resolve(ctx: &Ctx, e: &Entity, plotting: bool) -> (Rgb, f32, Option<cadcraft_
&& layer.is_none_or(|l| l.visible() && (!plotting || l.plot))
&& !ctx.frozen.iter().any(|f| f.eq_ignore_ascii_case(layer_name));
let layer_color = ctx.layer_color(layer_name).unwrap_or(Color::Index(7));
let rgb = e.common.color.resolve(layer_color, ctx.block_color);
let rgb = ink(e.common.color, layer_color, ctx.block_color);
let lw = match e.common.lineweight {
Lineweight::ByLayer => layer.map(|l| l.lineweight).unwrap_or(Lineweight::Default),
Lineweight::ByBlock => ctx.block_lw,
@ -495,7 +524,7 @@ fn entity(b: &mut Builder, ctx: &Ctx, e: &Entity) {
h.loops.iter().map(|l| Polyline { vertices: l.vertices.clone(), closed: true }.tessellate(tol)).collect();
let tris = fill::triangulate_evenodd(&loops);
let c = match &h.gradient {
Some(g) => g.color1.resolve(Color::Index(7), ctx.block_color),
Some(g) => ink(g.color1, Color::Index(7), ctx.block_color),
None => rgb,
};
b.tris(ctx, c, &tris);
@ -674,17 +703,19 @@ pub fn layout_mtext_entity(d: &Drawing, t: &cadcraft_doc::MText) -> cadcraft_fon
cadcraft_fonts::layout_mtext_with(&t.contents, &params)
}
fn mtext_color(c: Option<cadcraft_fonts::MTextColor>, rgb: Rgb) -> Rgb {
fn mtext_color(c: Option<cadcraft_fonts::MTextColor>, rgb: Ink) -> Ink {
match c {
Some(cadcraft_fonts::MTextColor::Aci(i)) => {
u8::try_from(i).ok().filter(|i| *i > 0).map(|i| Color::Index(i).resolve(Color::Index(7), Color::Index(7))).unwrap_or(rgb)
u8::try_from(i).ok().filter(|i| *i > 0).map(|i| ink(Color::Index(i), Color::Index(7), Color::Index(7))).unwrap_or(rgb)
}
Some(cadcraft_fonts::MTextColor::Rgb(v)) => {
Ink { rgb: Rgb(((v >> 16) & 0xFF) as u8, ((v >> 8) & 0xFF) as u8, (v & 0xFF) as u8), aci7: false }
}
Some(cadcraft_fonts::MTextColor::Rgb(v)) => Rgb(((v >> 16) & 0xFF) as u8, ((v >> 8) & 0xFF) as u8, (v & 0xFF) as u8),
None => rgb,
}
}
fn mtext(b: &mut Builder, ctx: &Ctx, t: &cadcraft_doc::MText, rgb: Rgb, lw: f32) {
fn mtext(b: &mut Builder, ctx: &Ctx, t: &cadcraft_doc::MText, rgb: Ink, lw: f32) {
let l = layout_mtext_entity(ctx.d, t);
for p in &l.pieces {
b.shaped(ctx, mtext_color(p.color, rgb), lw, &p.shaped);
@ -716,9 +747,9 @@ fn dimension(b: &mut Builder, ctx: &Ctx, e: &Entity, dm: &cadcraft_doc::Dimensio
Color::ByBlock => ctx.block_color,
c => c,
};
let dim_rgb = style.dim_line_color.resolve(layer_color, own);
let ext_rgb = style.ext_line_color.resolve(layer_color, own);
let txt_rgb = style.text_color.resolve(layer_color, own);
let dim_rgb = ink(style.dim_line_color, layer_color, own);
let ext_rgb = ink(style.ext_line_color, layer_color, own);
let txt_rgb = ink(style.text_color, layer_color, own);
for (l, role) in g.lines.iter().zip(g.line_roles.iter().chain(std::iter::repeat(&LineRole::Dim))) {
b.polyline(ctx, if *role == LineRole::Ext { ext_rgb } else { dim_rgb }, lw, l);
}
@ -754,7 +785,7 @@ pub fn table_covered(t: &cadcraft_doc::Table) -> Vec<Vec<bool>> {
cov
}
fn table(b: &mut Builder, ctx: &Ctx, t: &cadcraft_doc::Table, rgb: Rgb, lw: f32) {
fn table(b: &mut Builder, ctx: &Ctx, t: &cadcraft_doc::Table, rgb: Ink, lw: f32) {
let o = t.insert.xy();
let rows = t.row_heights.len().min(10_000);
let cols = t.col_widths.len().min(10_000);
@ -867,7 +898,7 @@ fn sub_ctx<'a>(ctx: &Ctx<'a>, e: &Entity, m: Mat3) -> Ctx<'a> {
}
}
fn insert(b: &mut Builder, ctx: &Ctx, e: &Entity, ins: &cadcraft_doc::Insert, rgb: Rgb) {
fn insert(b: &mut Builder, ctx: &Ctx, e: &Entity, ins: &cadcraft_doc::Insert, rgb: Ink) {
if ctx.depth >= cadcraft_doc::MAX_BLOCK_DEPTH {
return;
}

View File

@ -1,6 +1,6 @@
//! CPU rasteriser: draws a display list with tiny-skia (PNG export, plot preview, tests).
use cadcraft_color::{Rgb, display_rgb};
use cadcraft_color::Rgb;
use cadcraft_geom::{Bounds2, Vec2};
use tiny_skia::{FillRule, LineCap, LineJoin, Paint, PathBuilder, Pixmap, Stroke, Transform};
@ -63,7 +63,7 @@ pub fn render(list: &DisplayList, view: &View, o: &RasterOptions) -> Option<Pixm
pm.fill(tiny_skia::Color::from_rgba8(o.background.0, o.background.1, o.background.2, 255));
let vis = view.world_bounds();
for p in &list.prims {
let c = display_rgb(p.color, o.background);
let c = p.display_rgb(o.background);
let mut paint = Paint::default();
paint.set_color_rgba8(c.0, c.1, c.2, 255);
paint.anti_alias = o.antialias;

View File

@ -0,0 +1,90 @@
//! Only colour 7 flips between white and black with the background; every other colour,
//! however light or dark, is drawn as its own RGB (issue #272).
use cadcraft_color::{Color, Rgb, aci_rgb};
use cadcraft_doc::{Block, Common, Drawing, Entity, EntityKind, Handle, Insert, Line, Space};
use cadcraft_geom::{Vec2, Vec3};
use cadcraft_render::raster::{RasterOptions, View, render};
use cadcraft_render::{Options, build};
const WHITE: Rgb = Rgb(255, 255, 255);
const BLACK: Rgb = Rgb(0, 0, 0);
const DARK: Rgb = Rgb(33, 40, 48);
fn line(y: f64) -> EntityKind {
EntityKind::Line(Line { a: Vec3::new(0.0, y, 0.0), b: Vec3::new(10.0, y, 0.0) })
}
/// An insert of block "B" (one ByBlock line along y = 0) at height `y`.
fn insert(y: f64) -> EntityKind {
EntityKind::Insert(Insert {
block: "B".into(),
insert: Vec3::new(0.0, y, 0.0),
scale: Vec3::new(1.0, 1.0, 1.0),
rotation: 0.0,
attribs: vec![],
cols: 1,
rows: 1,
col_spacing: 0.0,
row_spacing: 0.0,
})
}
/// One horizontal line per case at y = 0, 1, 2, ... and, per case, its name and the expected
/// colour on paper (white) and on the dark canvas.
fn cases() -> (Drawing, Vec<(&'static str, Rgb, Rgb)>) {
let mut d = Drawing::new_imperial();
let mut blk = Block::new("B");
let mut by_block = Entity::new(Handle(1), line(0.0));
by_block.common.color = Color::ByBlock;
blk.entities.push(by_block);
d.blocks.insert("B".into(), std::sync::Arc::new(blk));
let light_yellow = aci_rgb(51);
let dark_grey = aci_rgb(250);
let c = |color| Common { color, ..Default::default() };
let rows: Vec<(&str, Common, EntityKind, Rgb, Rgb)> = vec![
("ByLayer on layer 0 (colour 7)", Common::default(), line(0.0), BLACK, WHITE),
("index 7", c(Color::Index(7)), line(1.0), BLACK, WHITE),
("ByBlock in a colour-7 insert", c(Color::Index(7)), insert(2.0), BLACK, WHITE),
("index 51 (light yellow)", c(Color::Index(51)), line(3.0), light_yellow, light_yellow),
("index 255 (white)", c(Color::Index(255)), line(4.0), WHITE, WHITE),
("true-colour white", c(Color::True(WHITE)), line(5.0), WHITE, WHITE),
("index 250 (dark grey)", c(Color::Index(250)), line(6.0), dark_grey, dark_grey),
("true-colour black", c(Color::True(BLACK)), line(7.0), BLACK, BLACK),
("ByBlock in an index-255 insert", c(Color::Index(255)), insert(8.0), WHITE, WHITE),
];
let mut out = Vec::new();
for (name, common, kind, paper, canvas) in rows {
d.add(&Space::Model, common, kind).unwrap();
out.push((name, paper, canvas));
}
(d, out)
}
#[test]
fn only_colour_7_flips_with_the_background() {
let (d, cases) = cases();
let l = build(&d, &Space::Model, &Options::default());
assert_eq!(l.prims.len(), cases.len());
for (i, (name, paper, canvas)) in cases.iter().enumerate() {
let p = l.prims.iter().find(|p| (l.points(p)[0].y - i as f64).abs() < 1e-9).unwrap();
assert_eq!(p.display_rgb(WHITE), *paper, "{name} on paper");
assert_eq!(p.display_rgb(DARK), *canvas, "{name} on the dark canvas");
}
}
#[test]
fn png_draws_true_colours() {
let (d, cases) = cases();
let l = build(&d, &Space::Model, &Options::default());
let view = View { center: Vec2::new(5.0, 4.0), scale: 10.0, width: 120, height: 120 };
for (bg, paper) in [(WHITE, true), (DARK, false)] {
let pm = render(&l, &view, &RasterOptions { background: bg, antialias: false, ..Default::default() }).unwrap();
for (i, (name, on_paper, on_canvas)) in cases.iter().enumerate() {
let (x, y) = view.to_screen(Vec2::new(5.0, i as f64));
let px = pm.pixel(x as u32, y as u32).unwrap();
let want = if paper { on_paper } else { on_canvas };
assert_eq!(Rgb(px.red(), px.green(), px.blue()), *want, "{name} on {bg:?}");
}
}
}

View File

@ -1,7 +1,7 @@
//! The drawing area: grid, entities, selection highlighting, grips, rubber bands, object-snap
//! markers, crosshair cursor, ViewCube, UCS icon and mouse handling.
use cadcraft_color::{Rgb, display_rgb};
use cadcraft_color::Rgb;
use cadcraft_doc::Handle;
use cadcraft_engine::Input;
use cadcraft_engine::snap::{self, SnapHit};
@ -255,7 +255,7 @@ fn draw_list(p: &egui::Painter, xf: &Xf, list: &DisplayList, bg: Rgb, lwdisplay:
let mut shapes: Vec<Shape> = Vec::with_capacity(list.prims.len());
let px = 1.0 / xf.scale;
for prim in &list.prims {
let col = color32(display_rgb(prim.color, bg));
let col = color32(prim.display_rgb(bg));
let pts = list.points(prim);
match prim.kind {
Kind::Polyline => {
@ -351,7 +351,7 @@ fn draw_list_gpu(c: &mut CanvasState, p: &egui::Painter, xf: &Xf, bg: Rgb, lwdis
if let (Kind::Infinite { ray }, Some(b), Some(d)) = (prim.kind, pts.first(), pts.get(1))
&& let Some(seg) = clip_infinite(xf, *b, *d, ray)
{
shapes.push(Shape::line_segment(seg, Stroke::new(1.0, color32(display_rgb(prim.color, bg)))));
shapes.push(Shape::line_segment(seg, Stroke::new(1.0, color32(prim.display_rgb(bg)))));
}
}
p.extend(shapes);
@ -1138,7 +1138,7 @@ mod tests {
use cadcraft_render::DPrim;
fn list(handles: &[u64]) -> DisplayList {
let prim = |h| DPrim { handle: Handle(h), color: Rgb(255, 255, 255), lw: 0.0, kind: Kind::Polyline, start: 0, len: 0 };
let prim = |h| DPrim { handle: Handle(h), color: Rgb(255, 255, 255), aci7: true, lw: 0.0, kind: Kind::Polyline, start: 0, len: 0 };
DisplayList { prims: handles.iter().map(|&h| prim(h)).collect(), ..Default::default() }
}

View File

@ -15,7 +15,7 @@
use std::sync::{Arc, Mutex};
use cadcraft_color::{Rgb, display_rgb};
use cadcraft_color::Rgb;
use cadcraft_geom::{Bounds2, Vec2};
use cadcraft_render::{DisplayList, Kind};
use egui_wgpu::wgpu;
@ -147,7 +147,7 @@ pub fn build_mesh(list: &DisplayList, origin: Vec2, bg: Rgb, lwdisplay: bool) ->
quads: Vec::new(),
};
for prim in &list.prims {
let c = display_rgb(prim.color, bg);
let c = prim.display_rgb(bg);
let col = [c.0, c.1, c.2, 255];
let pts = list.points(prim);
match prim.kind {
@ -515,7 +515,7 @@ mod tests {
tris: vec![Vec2::new(0.0, 0.0), Vec2::new(1.0, 0.0), Vec2::new(0.0, 1.0)],
..Default::default()
};
let p = |kind, start, len, lw| DPrim { handle: Handle(1), color: Rgb(255, 255, 255), lw, kind, start, len };
let p = |kind, start, len, lw| DPrim { handle: Handle(1), color: Rgb(255, 255, 255), aci7: true, lw, kind, start, len };
l.prims =
vec![p(Kind::Polyline, 0, 3, 0.0), p(Kind::Tris, 0, 3, 0.0), p(Kind::Point, 3, 1, 0.0), p(Kind::Infinite { ray: false }, 0, 2, 0.0)];
for v in l.verts.clone() {
@ -534,7 +534,7 @@ mod tests {
assert_eq!(m.quad_instances(), 1, "the point marker");
// First vertex is stored relative to the origin, so it's exactly zero.
assert_eq!(&m.lines[0..8], &[0u8; 8]);
// White on a white background inverts to black (display_rgb).
// Colour 7 on a white background inverts to black (display_rgb).
assert_eq!(&m.lines[8..12], &[0, 0, 0, 255]);
let x = f32::from_le_bytes([m.lines[12], m.lines[13], m.lines[14], m.lines[15]]);
assert_eq!(x, 1.0);