//! A proof sheet: the built face rasterised, so a shape can be looked at. //! //! Every assertion in this repo measures a bounding box or an ink area, and //! three real defects have already walked past all of them — arcs curling the //! wrong way, a dashed glyph drawn as two arms with a seam, and an up-down //! arrow whose heads met in the middle and read as a bowtie. Each had a correct //! bbox. So the last check is a person looking at the glyph, and this is what //! gives them something to look at without installing the face first. //! //! Deliberately no image dependency. A grayscale PNG is a signature, three //! chunks and a zlib stream, and the stream may be stored blocks, so the whole //! encoder is under a hundred lines and adds nothing to the dependency tree of //! a pipeline whose whole point is being reproducible from a checkout. use std::collections::BTreeMap; use read_fonts::types::GlyphId; use read_fonts::{FontRef, TableProvider}; use skrifa::MetadataProvider; use skrifa::instance::Size; use crate::Error; use crate::compose; /// Cell rules are the lightest thing on the page, so the ink reads over them. const PAPER: u8 = 0xff; const RULE: u8 = 0xdc; const BASELINE: u8 = 0xc0; /// One rasterised page. pub struct Sheet { pub width: usize, pub height: usize, pixels: Vec, } impl Sheet { fn new(width: usize, height: usize) -> Self { Self { width, height, pixels: vec![PAPER; width * height], } } /// How many pixels carry ink darker than the cell rules, which is the /// question "did this glyph draw anything" in the form a sheet can answer. pub fn ink(&self) -> usize { self.pixels.iter().filter(|&&p| p < BASELINE).count() } fn darken(&mut self, x: usize, y: usize, value: u8) { if x < self.width && y < self.height { let at = y * self.width + x; self.pixels[at] = self.pixels[at].min(value); } } fn rule_v(&mut self, x: usize, y0: usize, y1: usize, value: u8) { for y in y0..y1 { self.darken(x, y, value); } } fn rule_h(&mut self, y: usize, x0: usize, x1: usize, value: u8) { for x in x0..x1 { self.darken(x, y, value); } } } /// What to draw, in the order it is drawn. /// /// A row is a run of codepoints set adjacently on the cell grid, which is the /// arrangement that shows a seam: box drawing that does not tile leaves a gap /// between two cells, and no single-glyph rendering can show that. pub struct Page { pub rows: Vec>, pub px: f32, pub wght: Option, } /// The rows a proof of the house set wants: the authored marks one per cell, /// then the cell furniture set adjacently so its seams show. pub fn house_rows(covered: &BTreeMap) -> Vec> { let present = |row: Vec| -> Vec { row.into_iter() .filter(|c| covered.contains_key(c)) .collect() }; let mut rows = vec![ // The authored marks, beside the base's own glyphs they were fitted // against: the X against `x`, the triangles against `+`, the whitespace // renders among lowercase, which is where helix draws them. present(vec![ 0x25B2, 0x25BC, 0x25B8, 0x25C2, 0x2718, 0x2423, 0x23CE, 0x2191, 0x2193, 0x2192, 0x2195, ]), // The runs, each set beside its own siblings: four arrows that have to // read as one set, and six triangles that have to read as two sizes of // one drawing rather than as six drawings. present(vec![ 0x2190, 0x2192, 0x2191, 0x2193, 0x2195, 0x0020, 0x25B2, 0x25BC, 0x25B6, 0x25C4, 0x25BA, 0x25BE, 0x25B8, 0x25C2, ]), present(vec![ 0x0078, 0x00D7, 0x2718, 0x002B, 0x25B2, 0x003D, 0x2423, 0x0061, 0x0062, 0x0063, 0x23CE, ]), ]; // Box drawing, tiled. Three boxes side by side in light, heavy and double, // which is where a wrong arm or a seam shows up at once. for weights in [ [0x250C, 0x2500, 0x252C, 0x2510, 0x2502, 0x2524], [0x250F, 0x2501, 0x2533, 0x2513, 0x2503, 0x252B], [0x2554, 0x2550, 0x2566, 0x2557, 0x2551, 0x2563], ] { let [tl, h, t, tr, v, r] = weights; rows.push(present(vec![tl, h, t, h, tr, 0x0020, v, 0x0020, v, r])); } rows.push(present(vec![ 0x2514, 0x2500, 0x2534, 0x2500, 0x2518, 0x0020, 0x2517, 0x2501, 0x251B, ])); // Arcs and dashes, the two that were drawn wrong and passed their tests. rows.push(present(vec![ 0x256D, 0x2500, 0x256E, 0x0020, 0x2570, 0x2500, 0x256F, 0x0020, 0x2504, 0x2505, 0x2508, ])); // Block elements: a full cell run, the eighths in order, and the shades. rows.push(present(vec![ 0x2588, 0x2588, 0x2588, 0x0020, 0x2580, 0x2584, 0x2580, 0x2584, 0x0020, 0x2591, 0x2592, 0x2593, ])); rows.push(present(vec![ 0x258F, 0x258E, 0x258D, 0x258C, 0x258B, 0x258A, 0x2589, 0x2588, 0x0020, 0x2596, 0x2597, 0x2598, 0x259D, ])); // A row that lost every glyph but its spaces is a row the face does not // carry — a body slot takes no cell furniture, so its box-drawing rows come // back blank rather than absent. rows.retain(|row| row.iter().any(|&c| c != 0x0020)); rows } /// Rasterise one page of a face. /// /// `wght` is a location on the axis for a variable face and is ignored by a /// static one. /// /// Glyphs are set on a grid of the face's widest advance rounded to whole /// pixels, which for a monospace face is the cell a terminal lays out with — so /// furniture that only tiles at its exact design size shows its seam here the /// same way it would on a screen. For a proportional face the grid is a /// specimen layout and nothing more: its glyphs carry their own widths and the /// sheet is not a text setting. pub fn render(bytes: &[u8], page: &Page) -> Result { let font = FontRef::new(bytes).map_err(|e| Error::Font(e.to_string()))?; let coverage = compose::coverage(bytes)?; let upem = f32::from( font.head() .map_err(|e| Error::Font(e.to_string()))? .units_per_em(), ); let hhea = font.hhea().map_err(|e| Error::Font(e.to_string()))?; let ascent = f32::from(i16::from(hhea.ascender())); let descent = f32::from(i16::from(hhea.descender())); let scale = page.px / upem; let advance = { let hmtx = font.hmtx().map_err(|e| Error::Font(e.to_string()))?; let widest = hmtx .h_metrics() .last() .map_or(upem / 2.0, |m| f32::from(m.advance())); (widest * scale).round().max(1.0) }; let cell_h = ((ascent - descent) * scale).round().max(1.0); let baseline = (ascent * scale).round(); let cols = page.rows.iter().map(Vec::len).max().unwrap_or(1); let pad = (page.px * 0.5).round().max(4.0); let width = (advance * cols as f32 + pad * 2.0) as usize; let height = (cell_h * page.rows.len() as f32 + pad * 2.0) as usize; let mut sheet = Sheet::new(width, height); // A static face has no axes, so an empty location is its only location and // asking for `wght` on one is answered rather than refused. let location = font .axes() .location(page.wght.map(|w| ("wght", w)).as_slice()); for (r, row) in page.rows.iter().enumerate() { let top = pad + cell_h * r as f32; // The cell rules, under the ink: a glyph that overruns its cell or sits // off the baseline says so against them. sheet.rule_h(top as usize, pad as usize, width - pad as usize, RULE); sheet.rule_h( (top + baseline) as usize, pad as usize, width - pad as usize, BASELINE, ); for c in 0..=row.len() { sheet.rule_v( (pad + advance * c as f32) as usize, top as usize, (top + cell_h) as usize, RULE, ); } for (c, codepoint) in row.iter().enumerate() { let Some(gid) = coverage.get(codepoint) else { continue; }; let origin = (pad + advance * c as f32, top + baseline); draw_glyph(&mut sheet, &font, *gid, &location, scale, origin)?; } } Ok(sheet) } /// Fill one glyph, 3x3 supersampled. fn draw_glyph( sheet: &mut Sheet, font: &FontRef, gid: GlyphId, location: &skrifa::instance::Location, scale: f32, origin: (f32, f32), ) -> Result<(), Error> { let mut pen = Flatten { scale, origin, ..Flatten::default() }; font.outline_glyphs() .get(gid) .ok_or_else(|| Error::Font(format!("glyph {gid} has no outline")))? .draw( skrifa::outline::DrawSettings::unhinted(Size::unscaled(), location), &mut pen, ) .map_err(|e| Error::Font(e.to_string()))?; pen.close_open(); fill(sheet, &pen.edges); Ok(()) } /// Nonzero-winding scanline fill over straight edges, three samples per pixel /// in each axis. Coverage is the share of the nine samples inside the outline, /// which is enough to judge a shape and far short of a hinted rasteriser. fn fill(sheet: &mut Sheet, edges: &[(f32, f32, f32, f32)]) { if edges.is_empty() { return; } const SUB: usize = 3; let step = 1.0 / SUB as f32; let (mut y0, mut y1) = (f32::MAX, f32::MIN); let (mut x0, mut x1) = (f32::MAX, f32::MIN); for &(ax, ay, bx, by) in edges { y0 = y0.min(ay).min(by); y1 = y1.max(ay).max(by); x0 = x0.min(ax).min(bx); x1 = x1.max(ax).max(bx); } let row0 = y0.floor().max(0.0) as usize; let row1 = (y1.ceil() as usize).min(sheet.height); let col0 = x0.floor().max(0.0) as usize; let col1 = (x1.ceil() as usize).min(sheet.width); if row0 >= row1 || col0 >= col1 { return; } let mut coverage = vec![0u8; (col1 - col0) * (row1 - row0)]; let mut crossings: Vec<(f32, i32)> = Vec::new(); for row in row0..row1 { for sy in 0..SUB { let y = row as f32 + (sy as f32 + 0.5) * step; crossings.clear(); for &(ax, ay, bx, by) in edges { if (ay <= y) == (by <= y) { continue; } let t = (y - ay) / (by - ay); crossings.push((ax + t * (bx - ax), if by > ay { 1 } else { -1 })); } if crossings.len() < 2 { continue; } crossings.sort_by(|a, b| a.0.total_cmp(&b.0)); let mut winding = 0; for pair in crossings.windows(2) { winding += pair[0].1; if winding == 0 { continue; } let (span0, span1) = (pair[0].0, pair[1].0); for col in col0..col1 { for sx in 0..SUB { let x = col as f32 + (sx as f32 + 0.5) * step; if x >= span0 && x < span1 { coverage[(row - row0) * (col1 - col0) + (col - col0)] += 1; } } } } } } for row in row0..row1 { for col in col0..col1 { let hits = coverage[(row - row0) * (col1 - col0) + (col - col0)]; if hits > 0 { let value = PAPER as f32 * (1.0 - f32::from(hits) / (SUB * SUB) as f32); sheet.darken(col, row, value.round() as u8); } } } } /// Flattens an outline into straight edges in device space, y down. #[derive(Default)] struct Flatten { scale: f32, origin: (f32, f32), edges: Vec<(f32, f32, f32, f32)>, start: (f32, f32), at: (f32, f32), } impl Flatten { fn map(&self, x: f32, y: f32) -> (f32, f32) { ( self.origin.0 + x * self.scale, self.origin.1 - y * self.scale, ) } fn edge(&mut self, to: (f32, f32)) { self.edges.push((self.at.0, self.at.1, to.0, to.1)); self.at = to; } /// Curves are flattened by subdivision. The house marks are all /// straight-edged, but a base's own glyphs are not and the sheet sets them /// beside the marks on purpose. fn curve(&mut self, points: &[(f32, f32)]) { const STEPS: usize = 16; let from = self.at; for step in 1..=STEPS { let t = step as f32 / STEPS as f32; let mut work: Vec<(f32, f32)> = std::iter::once(from) .chain(points.iter().copied()) .collect(); while work.len() > 1 { for i in 0..work.len() - 1 { work[i] = ( work[i].0 + (work[i + 1].0 - work[i].0) * t, work[i].1 + (work[i + 1].1 - work[i].1) * t, ); } work.pop(); } self.edge(work[0]); } } fn close_open(&mut self) { if self.at != self.start { let start = self.start; self.edge(start); } } } impl skrifa::outline::OutlinePen for Flatten { fn move_to(&mut self, x: f32, y: f32) { self.close_open(); let to = self.map(x, y); self.start = to; self.at = to; } fn line_to(&mut self, x: f32, y: f32) { let to = self.map(x, y); self.edge(to); } fn quad_to(&mut self, cx: f32, cy: f32, x: f32, y: f32) { let c = self.map(cx, cy); let to = self.map(x, y); self.curve(&[c, to]); } fn curve_to(&mut self, cx0: f32, cy0: f32, cx1: f32, cy1: f32, x: f32, y: f32) { let c0 = self.map(cx0, cy0); let c1 = self.map(cx1, cy1); let to = self.map(x, y); self.curve(&[c0, c1, to]); } fn close(&mut self) { self.close_open(); } } /// Encode a grayscale sheet as a PNG. /// /// Stored deflate blocks rather than a compressor: the file is a build artifact /// somebody looks at once, and a stored stream is a correct zlib stream that /// costs no dependency. pub fn png(sheet: &Sheet) -> Vec { let mut raw = Vec::with_capacity((sheet.width + 1) * sheet.height); for row in sheet.pixels.chunks(sheet.width) { raw.push(0); // filter: none raw.extend_from_slice(row); } let mut out = Vec::new(); out.extend_from_slice(&[0x89, b'P', b'N', b'G', 0x0d, 0x0a, 0x1a, 0x0a]); let mut ihdr = Vec::new(); ihdr.extend_from_slice(&(sheet.width as u32).to_be_bytes()); ihdr.extend_from_slice(&(sheet.height as u32).to_be_bytes()); ihdr.extend_from_slice(&[8, 0, 0, 0, 0]); // 8-bit grayscale, no interlace chunk(&mut out, *b"IHDR", &ihdr); let mut zlib = vec![0x78, 0x01]; for (i, block) in raw.chunks(0xffff).enumerate() { let last = u8::from((i + 1) * 0xffff >= raw.len()); zlib.push(last); zlib.extend_from_slice(&(block.len() as u16).to_le_bytes()); zlib.extend_from_slice(&(!(block.len() as u16)).to_le_bytes()); zlib.extend_from_slice(block); } zlib.extend_from_slice(&adler32(&raw).to_be_bytes()); chunk(&mut out, *b"IDAT", &zlib); chunk(&mut out, *b"IEND", &[]); out } fn chunk(out: &mut Vec, kind: [u8; 4], data: &[u8]) { out.extend_from_slice(&(data.len() as u32).to_be_bytes()); out.extend_from_slice(&kind); out.extend_from_slice(data); let mut crc = crc32(kind.as_slice()); crc = crc32_continue(crc, data); out.extend_from_slice(&crc.to_be_bytes()); } fn adler32(data: &[u8]) -> u32 { let (mut a, mut b) = (1u32, 0u32); for &byte in data { a = (a + u32::from(byte)) % 65521; b = (b + a) % 65521; } (b << 16) | a } /// The CRC of one run, which is the running form started from zero. fn crc32(data: &[u8]) -> u32 { crc32_continue(0, data) } fn crc32_continue(previous: u32, data: &[u8]) -> u32 { let mut crc = previous ^ 0xffff_ffff; for &byte in data { crc ^= u32::from(byte); for _ in 0..8 { crc = if crc & 1 == 1 { (crc >> 1) ^ 0xedb8_8320 } else { crc >> 1 }; } } crc ^ 0xffff_ffff } #[cfg(test)] mod tests { use super::*; /// A hand-decoded PNG, because an encoder nobody reads back is an encoder /// that ships a file no viewer opens. Stored deflate blocks make the check /// as short as the encoder: header, then length-prefixed literal runs. fn decode(png: &[u8]) -> (usize, usize, Vec) { assert_eq!( &png[0..8], &[0x89, b'P', b'N', b'G', 0x0d, 0x0a, 0x1a, 0x0a] ); let mut at = 8; let (mut width, mut height, mut raw) = (0usize, 0usize, Vec::new()); while at < png.len() { let length = u32::from_be_bytes(png[at..at + 4].try_into().unwrap()) as usize; let kind = &png[at + 4..at + 8]; let data = &png[at + 8..at + 8 + length]; let stated = u32::from_be_bytes(png[at + 8 + length..at + 12 + length].try_into().unwrap()); let mut crc = crc32(kind); crc = crc32_continue(crc, data); assert_eq!( crc, stated, "chunk {} has a bad crc", String::from_utf8_lossy(kind) ); match kind { b"IHDR" => { width = u32::from_be_bytes(data[0..4].try_into().unwrap()) as usize; height = u32::from_be_bytes(data[4..8].try_into().unwrap()) as usize; assert_eq!(&data[8..], &[8, 0, 0, 0, 0]); } b"IDAT" => { assert_eq!(&data[0..2], &[0x78, 0x01]); let mut cursor = 2; loop { let last = data[cursor]; let len = u16::from_le_bytes(data[cursor + 1..cursor + 3].try_into().unwrap()) as usize; let nlen = u16::from_le_bytes(data[cursor + 3..cursor + 5].try_into().unwrap()); assert_eq!( nlen, !(len as u16), "a stored block's length is not negated" ); raw.extend_from_slice(&data[cursor + 5..cursor + 5 + len]); cursor += 5 + len; if last == 1 { break; } } assert_eq!( u32::from_be_bytes(data[cursor..cursor + 4].try_into().unwrap()), adler32(&raw) ); } _ => {} } at += 12 + length; } (width, height, raw) } #[test] fn the_png_is_one_a_decoder_can_read_back() { let mut sheet = Sheet::new(3, 2); sheet.darken(1, 0, 0); let file = png(&sheet); let (width, height, raw) = decode(&file); assert_eq!((width, height), (3, 2)); // One filter byte per row, then the row. assert_eq!(raw, vec![0, PAPER, 0, PAPER, 0, PAPER, PAPER, PAPER]); } /// Over 64KB of pixels the stream needs more than one stored block, and a /// wrong last-block flag there is a truncated image rather than an error. #[test] fn a_sheet_past_one_stored_block_still_decodes() { let sheet = Sheet::new(400, 400); let (width, height, raw) = decode(&png(&sheet)); assert_eq!((width, height), (400, 400)); assert_eq!(raw.len(), 400 * 401); } }