okami.mom/albedo
git clone https://git.nkp.pet/okami.mom/albedo
b2e29fd
main
1//! Dense 1-bit pixel-font text frames on a fixed grid, bundled X11 8x13. 2 3use std:: collections:: HashMap ; 4use std:: sync:: OnceLock ; 5 6const FONT : & [ u8 ] =include_bytes! ( "../fonts/8x13.bdf" ); 7const MARGIN : usize =0 ; 8 9/// The 8x13 font box's 11px ascent; extra pitch leading sits below it. 10const BASELINE : usize =11 ; 11const MAX_EDGE : usize =8000 ; 12 13pub struct Frame { 14pub width : u32 , 15pub height : u32 , 16pub png : Vec < u8 >, 17} 18 19struct Glyph { 20width : usize , 21height : usize , 22off_x : i32 , 23off_y : i32 , 24/// One packed bit row per glyph row; column c is bit (width-1-c). 25rows : Vec < u16 >, 26} 27 28struct Bdf { 29glyphs : HashMap < u32 , Glyph >, 30} 31 32fn bdf () ->&' static Bdf { 33static BDF : OnceLock < Bdf > =OnceLock :: new (); 34BDF . get_or_init ( ||parse_bdf ( FONT ). expect ( "bundled 8x13.bdf parses" )) 35} 36 37fn parse_bdf ( bytes : & [ u8 ]) ->Result < Bdf , String > { 38let text = std:: str:: from_utf8 ( bytes). map_err ( |e| e. to_string ()) ?; 39let mut glyphs =HashMap :: new (); 40let mut encoding: Option < u32 > =None ; 41let mut box_: Option <( usize , usize , i32 , i32 )> =None ; 42let mut bitmap: Option < Vec < u16 >> =None ; 43for linein text. lines () { 44let mut fields = line. split_whitespace (); 45match fields. next () { 46Some ( "ENCODING" ) =>{ 47 encoding = fields 48. next () 49. and_then ( |n| n. parse ::< i64 >(). ok ()) 50. and_then ( |n|{ 51if ( 0 ..=0x10FFFF ). contains ( & n) { 52Some ( nas u32 ) 53} else { 54None 55} 56}); 57} 58Some ( "BBX" ) =>{ 59let mut number = || fields. next (). and_then ( |n| n. parse ::< i64 >(). ok ()); 60 box_ =match ( number (), number (), number (), number ()) { 61( Some ( w), Some ( h), Some ( x), Some ( y)) if w >0 && h >0 =>{ 62Some (( was usize , has usize , xas i32 , yas i32 )) 63} 64 _ =>None , 65}; 66} 67Some ( "BITMAP" ) =>{ 68let ( w, h, x, y) = box_. ok_or ( "BITMAP without a BBX" ) ?; 69 bitmap =Some ( Vec :: with_capacity ( h)); 70let _ =( w, x, y); 71} 72Some ( "ENDCHAR" ) =>{ 73if let ( Some ( code), Some (( w, h, x, y)), Some ( rows)) = 74( encoding, box_, bitmap. take ()) 75{ 76if rows. len () == h{ 77 glyphs. insert ( 78 code, 79Glyph { 80width : w, 81height : h, 82off_x : x, 83off_y : y, 84 rows, 85}, 86); 87} 88} 89 encoding =None ; 90 box_ =None ; 91} 92Some ( hex) if bitmap. is_some () && !hex. is_empty () =>{ 93if let Some ( row) = bitmap. as_mut () { 94 row. push ( parse_hex_row ( hex)); 95} 96} 97 _ =>{} 98} 99} 100if glyphs. is_empty () { 101return Err ( "no glyphs in the bundled font" . into ()); 102} 103Ok ( Bdf { glyphs}) 104} 105 106/// Hex bitmap bytes packed big-endian into one bit row (fonts here are 107/// <= 16 wide). Column c of a glyph of BBX width w is bit 108/// `(w + 7) / 8 * 8 - 1 - c` of the row. 109fn parse_hex_row ( hex : & str ) ->u16 { 110let mut row =0u16 ; 111for iin 0 ..hex. len () /2 { 112match u16:: from_str_radix ( & hex[ 2 * i..2 * i +2 ], 16 ) { 113Ok ( byte) => row =( row <<8 ) | byte, 114Err ( _) =>return row, 115} 116} 117 row 118} 119 120pub fn render ( text : & str , advance : usize , pitch : usize , width : usize ) ->Result < Frame , String > { 121let ( width, height, pixels) =paint ( text, advance, pitch, width) ?; 122encode ( width, height, pixels) 123} 124 125fn paint ( 126text : & str , 127advance : usize , 128pitch : usize , 129width : usize , 130) ->Result <( usize , usize , Vec < u8 >), String > { 131if !( 6 ..=24 ). contains ( & advance) { 132return Err ( format! ( "advance must be 6 to 24, not {advance}" )); 133} 134if !( 10 ..=64 ). contains ( & pitch) { 135return Err ( format! ( "pitch must be 10 to 64, not {pitch}" )); 136} 137if !( 256 ..=4096 ). contains ( & width) || width <2 * MARGIN + advance{ 138return Err ( format! ( "width must be 256 to 4096, not {width}" )); 139} 140let columns =( width -2 * MARGIN ) / advance; 141let font =& bdf (). glyphs ; 142let question = font. get ( & u32:: from ( b'?' )). ok_or ( "the font lacks '?'" ) ?; 143 144let mut rows: Vec < Vec < char >> =Vec :: new (); 145for linein text. split ( '\n' ) { 146let chars: Vec < char > = line. chars (). collect (); 147if chars. is_empty () { 148 rows. push ( Vec :: new ()); 149continue ; 150} 151for chunkin chars. chunks ( columns) { 152 rows. push ( chunk. to_vec ()); 153} 154} 155if rows. is_empty () { 156 rows. push ( Vec :: new ()); 157} 158let height =2 * MARGIN + rows. len () * pitch; 159if height >MAX_EDGE { 160return Err ( format! ( 161"{height}px exceeds the {MAX_EDGE}px image edge; chunk the text into smaller frames" 162)); 163} 164let mut pixels =vec! [ 255u8 ; width* height]; 165for ( index, row) in rows. iter (). enumerate () { 166let top =MARGIN + index* pitch; 167for ( column, ch) in row. iter (). enumerate () { 168let cell =MARGIN + column* advance; 169// U+2588 marks a newline: fill the whole cell box with ink. 170if * chas u32 ==0x2588 { 171for pyin top..top + pitch{ 172for pxin cell..cell + advance{ 173if px < width && py < height{ 174 pixels[ py* width + px] =0 ; 175} 176} 177} 178continue ; 179} 180let glyph = font. get ( & ( * chas u32 )). unwrap_or ( question); 181let x = cellas isize + glyph. off_x as isize ; 182let baseline =( top +BASELINE ) as isize ; 183let glyph_top = baseline -( glyph. height as isize + glyph. off_y as isize ); 184let byte_bits =8 * (( glyph. width +7 ) /8 ); 185for ( r, bits) in glyph. rows . iter (). enumerate () { 186for cin 0 ..glyph. width { 187if bits& ( 1 <<( byte_bits -1 - c)) !=0 { 188let px = x + cas isize ; 189let py = glyph_top + ras isize ; 190if px >=0 && py >=0 &&( pxas usize ) < width &&( pyas usize ) < height{ 191 pixels[ pyas usize * width + pxas usize ] =0 ; 192} 193} 194} 195} 196} 197} 198 199Ok (( width, height, pixels)) 200} 201 202fn encode ( width : usize , height : usize , pixels : Vec < u8 >) ->Result < Frame , String > { 203// One bit per pixel, MSB first: ink is the 0 bit, white the 1 bit. 204let stride =( width +7 ) /8 ; 205let mut bits =vec! [ 0xFFu8 ; stride* height]; 206for ( y, row) in pixels. chunks ( width). enumerate () { 207for ( x, ink) in row. iter (). enumerate () { 208if * ink <128 { 209 bits[ y* stride + x /8 ] &= !( 0x80 >>( x %8 )); 210} 211} 212} 213let mut out =Vec :: new (); 214let mut encoder = png:: Encoder :: new ( & mut out, widthas u32 , heightas u32 ); 215 encoder. set_color ( png:: ColorType :: Grayscale ); 216 encoder. set_depth ( png:: BitDepth :: One ); 217 encoder. set_compression ( png:: Compression :: High ); 218let mut writer = encoder. write_header (). expect ( "header fits in memory" ); 219 writer 220. write_image_data ( & bits) 221. expect ( "pixels match the header" ); 222drop ( writer); 223Ok ( Frame { 224width : widthas u32 , 225height : heightas u32 , 226png : out, 227}) 228} 229 230# [ cfg ( test )] 231mod tests{ 232use super :: * ; 233 234fn ink ( text : & str ) ->usize { 235let ( _, _, pixels) =paint ( text, 11 , 16 , 1568 ). unwrap (); 236 pixels. iter (). filter ( |&& p| p <128 ). count () 237} 238 239# [ test ] 240fn glyphs_leave_ink () { 241assert! ( ink ( "hello world" ) >50 , "text must leave ink" ); 242assert_eq! ( ink ( "" ), 0 ); 243assert! ( ink ( "A" ) >5 , "one glyph leaves ink" ); 244} 245 246# [ test ] 247fn full_block_fills_its_whole_cell () { 248let ( width, height, pixels) =paint ( "\u{2588}" , 11 , 16 , 1568 ). unwrap (); 249assert_eq! (( width, height), ( 1568 , 16 )); 250let ink = pixels. iter (). filter ( |&& p| p <128 ). count (); 251assert_eq! ( ink, 11 * 16 , "the block fills exactly one cell box" ); 252} 253 254# [ test ] 255fn every_ascii_glyph_leaves_ink () { 256let text: String =( 0x21u32 ..=0x7e ) 257. map ( |c| char:: from_u32 ( c). unwrap ()) 258. collect (); 259assert! ( ink ( & text) >1000 , "the printable ASCII range is covered" ); 260} 261}