okami.mom/albedo

git clone https://git.nkp.pet/okami.mom/albedo

nekomimi.petview: review the diff first, and fix what a model found using it2e306d5

main
5.9 KiB164 linesraw
1//! Rows of glyphs drawn onto an RGB canvas and encoded as PNG.
2
3use crate::layout::Row;
4use arborium_theme::theme::Color;
5use fontdue::{Font, FontSettings, Metrics};
6use std::collections::HashMap;
7
8const FONT: &[u8] = include_bytes!("../fonts/JetBrainsMonoNL-Medium.ttf");
9/// Large enough to stay legible after a provider scales an 80-row page down.
10const FONT_PX: f32 = 16.0;
11const PADDING: usize = 12;
12
13pub struct Painter {
14    font: Font,
15    cache: HashMap<char, (Metrics, Vec<u8>)>,
16    cell: usize,
17    row: usize,
18    baseline: usize,
19}
20
21pub struct Page<'a> {
22    pub rows: &'a [Row],
23    /// Cells left of the code, so every page of one view aligns.
24    pub gutter: usize,
25    /// False for text whose own line numbers are not the source's, like a diff.
26    pub numbers: bool,
27    pub columns: usize,
28    pub foreground: Color,
29    pub background: Color,
30}
31
32impl Painter {
33    pub fn new() -> Painter {
34        let font = Font::from_bytes(FONT, FontSettings::default()).expect("bundled font parses");
35        let line = font
36            .horizontal_line_metrics(FONT_PX)
37            .expect("bundled font is horizontal");
38        let cell = font.metrics('M', FONT_PX).advance_width.ceil() as usize;
39        let row = (FONT_PX * 1.4).ceil() as usize;
40        let glyph = line.ascent - line.descent;
41        let baseline = ((row as f32 - glyph) / 2.0 + line.ascent).round() as usize;
42        Painter {
43            font,
44            cache: HashMap::new(),
45            cell,
46            row,
47            baseline,
48        }
49    }
50
51    pub fn png(&mut self, page: &Page) -> Vec<u8> {
52        let (width, height, pixels) = self.paint(page);
53        let mut out = Vec::new();
54        let mut encoder = png::Encoder::new(&mut out, width as u32, height as u32);
55        encoder.set_color(png::ColorType::Rgb);
56        encoder.set_depth(png::BitDepth::Eight);
57        encoder.set_compression(png::Compression::Balanced);
58        let mut writer = encoder.write_header().expect("header fits in memory");
59        writer
60            .write_image_data(&pixels)
61            .expect("pixels match the header");
62        drop(writer);
63        out
64    }
65
66    fn paint(&mut self, page: &Page) -> (usize, usize, Vec<u8>) {
67        // the gutter, a gap holding the separator, then the code.
68        let code_x = PADDING + (page.gutter + 2) * self.cell;
69        let width = code_x + page.columns * self.cell + PADDING;
70        let height = 2 * PADDING + page.rows.len().max(1) * self.row;
71        let mut canvas = Canvas {
72            width,
73            pixels: vec![0; width * height * 3],
74        };
75        canvas.fill(0, 0, width, height, page.background);
76        let number = mix(page.foreground, page.background, 0.5);
77        let rule = mix(page.foreground, page.background, 0.85);
78        let x = PADDING + (page.gutter + 1) * self.cell;
79        canvas.fill(x, PADDING, 1, height - 2 * PADDING, rule);
80        for (index, row) in page.rows.iter().enumerate() {
81            let top = PADDING + index * self.row;
82            // A wrapped line's later rows say so, since they start at column 0.
83            let label = match (row.continued, page.numbers) {
84                (true, _) => "↪".to_string(),
85                (false, true) => row.line.to_string(),
86                (false, false) => String::new(),
87            };
88            let first = page.gutter.saturating_sub(label.chars().count());
89            for (offset, ch) in label.chars().enumerate() {
90                let x = PADDING + (first + offset) * self.cell;
91                self.glyph(&mut canvas, x, top, ch, number);
92            }
93            for glyph in &row.glyphs {
94                self.glyph(
95                    &mut canvas,
96                    code_x + glyph.column * self.cell,
97                    top,
98                    glyph.ch,
99                    glyph.color,
100                );
101            }
102        }
103        (width, height, canvas.pixels)
104    }
105
106    fn glyph(&mut self, canvas: &mut Canvas, x: usize, top: usize, ch: char, color: Color) {
107        let font = &self.font;
108        let (metrics, coverage) = self
109            .cache
110            .entry(ch)
111            .or_insert_with(|| font.rasterize(ch, FONT_PX));
112        let left = x as i32 + metrics.xmin;
113        let top = (top + self.baseline) as i32 - metrics.height as i32 - metrics.ymin;
114        for row in 0..metrics.height {
115            for column in 0..metrics.width {
116                let alpha = coverage[row * metrics.width + column];
117                if alpha > 0 {
118                    canvas.blend(left + column as i32, top + row as i32, color, alpha);
119                }
120            }
121        }
122    }
123}
124
125struct Canvas {
126    width: usize,
127    pixels: Vec<u8>,
128}
129
130impl Canvas {
131    fn fill(&mut self, x: usize, y: usize, width: usize, height: usize, color: Color) {
132        for row in y..y + height {
133            for column in x..x + width {
134                let at = (row * self.width + column) * 3;
135                self.pixels[at..at + 3].copy_from_slice(&[color.r, color.g, color.b]);
136            }
137        }
138    }
139
140    /// Glyphs may overhang their cell; anything off the canvas is dropped.
141    fn blend(&mut self, x: i32, y: i32, color: Color, alpha: u8) {
142        let height = self.pixels.len() / 3 / self.width;
143        if x < 0 || y < 0 || x as usize >= self.width || y as usize >= height {
144            return;
145        }
146        let at = (y as usize * self.width + x as usize) * 3;
147        let alpha = alpha as u32;
148        for (channel, value) in [color.r, color.g, color.b].into_iter().enumerate() {
149            let under = self.pixels[at + channel] as u32;
150            self.pixels[at + channel] =
151                ((value as u32 * alpha + under * (255 - alpha)) / 255) as u8;
152        }
153    }
154}
155
156/// `color` moved `amount` of the way toward `toward`.
157fn mix(color: Color, toward: Color, amount: f32) -> Color {
158    let channel = |a: u8, b: u8| (a as f32 + (b as f32 - a as f32) * amount).round() as u8;
159    Color::new(
160        channel(color.r, toward.r),
161        channel(color.g, toward.g),
162        channel(color.b, toward.b),
163    )
164}