//! Rows of glyphs drawn onto an RGB canvas and encoded as PNG. use crate::layout::Row; use arborium_theme::theme::Color; use fontdue::{Font, FontSettings, Metrics}; use std::collections::HashMap; const FONT: &[u8] = include_bytes!("../fonts/JetBrainsMonoNL-Medium.ttf"); /// Large enough to stay legible after a provider scales an 80-row page down. const FONT_PX: f32 = 16.0; const PADDING: usize = 12; pub struct Painter { font: Font, cache: HashMap)>, cell: usize, row: usize, baseline: usize, } pub struct Page<'a> { pub rows: &'a [Row], /// Cells left of the code, so every page of one view aligns. pub gutter: usize, /// False for text whose own line numbers are not the source's, like a diff. pub numbers: bool, pub columns: usize, pub foreground: Color, pub background: Color, } impl Painter { pub fn new() -> Painter { let font = Font::from_bytes(FONT, FontSettings::default()).expect("bundled font parses"); let line = font .horizontal_line_metrics(FONT_PX) .expect("bundled font is horizontal"); let cell = font.metrics('M', FONT_PX).advance_width.ceil() as usize; let row = (FONT_PX * 1.4).ceil() as usize; let glyph = line.ascent - line.descent; let baseline = ((row as f32 - glyph) / 2.0 + line.ascent).round() as usize; Painter { font, cache: HashMap::new(), cell, row, baseline, } } pub fn png(&mut self, page: &Page) -> Vec { let (width, height, pixels) = self.paint(page); let mut out = Vec::new(); let mut encoder = png::Encoder::new(&mut out, width as u32, height as u32); encoder.set_color(png::ColorType::Rgb); encoder.set_depth(png::BitDepth::Eight); encoder.set_compression(png::Compression::Balanced); let mut writer = encoder.write_header().expect("header fits in memory"); writer .write_image_data(&pixels) .expect("pixels match the header"); drop(writer); out } fn paint(&mut self, page: &Page) -> (usize, usize, Vec) { // the gutter, a gap holding the separator, then the code. let code_x = PADDING + (page.gutter + 2) * self.cell; let width = code_x + page.columns * self.cell + PADDING; let height = 2 * PADDING + page.rows.len().max(1) * self.row; let mut canvas = Canvas { width, pixels: vec![0; width * height * 3], }; canvas.fill(0, 0, width, height, page.background); let number = mix(page.foreground, page.background, 0.5); let rule = mix(page.foreground, page.background, 0.85); let x = PADDING + (page.gutter + 1) * self.cell; canvas.fill(x, PADDING, 1, height - 2 * PADDING, rule); for (index, row) in page.rows.iter().enumerate() { let top = PADDING + index * self.row; // A wrapped line's later rows say so, since they start at column 0. let label = match (row.continued, page.numbers) { (true, _) => "↪".to_string(), (false, true) => row.line.to_string(), (false, false) => String::new(), }; let first = page.gutter.saturating_sub(label.chars().count()); for (offset, ch) in label.chars().enumerate() { let x = PADDING + (first + offset) * self.cell; self.glyph(&mut canvas, x, top, ch, number); } for glyph in &row.glyphs { self.glyph( &mut canvas, code_x + glyph.column * self.cell, top, glyph.ch, glyph.color, ); } } (width, height, canvas.pixels) } fn glyph(&mut self, canvas: &mut Canvas, x: usize, top: usize, ch: char, color: Color) { let font = &self.font; let (metrics, coverage) = self .cache .entry(ch) .or_insert_with(|| font.rasterize(ch, FONT_PX)); let left = x as i32 + metrics.xmin; let top = (top + self.baseline) as i32 - metrics.height as i32 - metrics.ymin; for row in 0..metrics.height { for column in 0..metrics.width { let alpha = coverage[row * metrics.width + column]; if alpha > 0 { canvas.blend(left + column as i32, top + row as i32, color, alpha); } } } } } struct Canvas { width: usize, pixels: Vec, } impl Canvas { fn fill(&mut self, x: usize, y: usize, width: usize, height: usize, color: Color) { for row in y..y + height { for column in x..x + width { let at = (row * self.width + column) * 3; self.pixels[at..at + 3].copy_from_slice(&[color.r, color.g, color.b]); } } } /// Glyphs may overhang their cell; anything off the canvas is dropped. fn blend(&mut self, x: i32, y: i32, color: Color, alpha: u8) { let height = self.pixels.len() / 3 / self.width; if x < 0 || y < 0 || x as usize >= self.width || y as usize >= height { return; } let at = (y as usize * self.width + x as usize) * 3; let alpha = alpha as u32; for (channel, value) in [color.r, color.g, color.b].into_iter().enumerate() { let under = self.pixels[at + channel] as u32; self.pixels[at + channel] = ((value as u32 * alpha + under * (255 - alpha)) / 255) as u8; } } } /// `color` moved `amount` of the way toward `toward`. fn mix(color: Color, toward: Color, amount: f32) -> Color { let channel = |a: u8, b: u8| (a as f32 + (b as f32 - a as f32) * amount).round() as u8; Color::new( channel(color.r, toward.r), channel(color.g, toward.g), channel(color.b, toward.b), ) }