1use std::collections::{BTreeMap, BTreeSet};
32use std::fs;
33use std::path::{Path, PathBuf};
34
35use serde::Serialize;
36use shuyi_assets::font::{
37 AtlasRect, CellWidths, FontAtlasDef, WideSet, describe, is_combining_mark, is_wide_punctuation,
38};
39use shuyi_assets::image::Image;
40use shuyi_assets::lang::Lang;
41
42use crate::codec::encode_png;
43use crate::project::FontAtlas;
44
45pub const ATLAS_RANGES: &[(char, char)] = &[
47 (' ', '~'), ('\u{a0}', '\u{ff}'), ('\u{2c7}', '\u{2c7}'), ('\u{391}', '\u{3c9}'), ('\u{2010}', '\u{206f}'), ('\u{2190}', '\u{21ff}'), ('\u{2200}', '\u{22ff}'), ('\u{2300}', '\u{23ff}'), ('\u{2500}', '\u{25ff}'), ('\u{2600}', '\u{26ff}'), ('\u{2715}', '\u{2715}'), ];
59
60pub const COLUMNS: u32 = 32;
62
63#[derive(Debug, Clone, Default, PartialEq, Eq)]
70pub struct AtlasChars {
71 used: BTreeSet<char>,
73 required: BTreeSet<char>,
75 used_by_set: BTreeMap<Option<String>, BTreeSet<char>>,
78 punctuation_by_set: BTreeMap<Option<String>, BTreeSet<char>>,
81 wide: BTreeMap<Option<String>, BTreeSet<char>>,
84}
85
86fn keepable(c: char) -> bool {
89 !c.is_control() && !is_combining_mark(c)
90}
91
92impl AtlasChars {
93 pub fn of_lang(lang: &Lang, required: &str) -> Self {
96 let mut chars = Self {
97 required: required.chars().filter(|&c| keepable(c)).collect(),
98 ..Self::default()
99 };
100 let source = &lang.source_lang().code;
101 for (at, text) in lang.texts() {
102 let widths = lang.widths(at.lang.unwrap_or(source), &CellWidths::NARROW);
105 let set = widths.wide_font().map(str::to_owned);
106 let punctuation = widths.wide_punctuation();
107 for c in text.chars().filter(|&c| keepable(c)) {
108 chars.used.insert(c);
109 chars.used_by_set.entry(set.clone()).or_default().insert(c);
110 if punctuation && is_wide_punctuation(c) {
111 let by_set = chars.punctuation_by_set.entry(set.clone());
112 by_set.or_default().insert(c);
113 }
114 }
115 }
116 chars
117 }
118
119 #[must_use]
122 pub fn with_wide(self, wide: &str) -> Self {
123 self.with_wide_set(None, wide)
124 }
125
126 #[must_use]
129 pub fn with_wide_set(mut self, set: Option<&str>, wide: &str) -> Self {
130 let wide = wide.chars().filter(|&c| keepable(c)).collect();
131 self.wide.insert(set.map(str::to_owned), wide);
132 self
133 }
134
135 pub fn wants_wide(&self, c: char, narrow: &BitmapFont, set: Option<&str>) -> bool {
141 let of = |by_set: &BTreeMap<Option<String>, BTreeSet<char>>| {
142 by_set
143 .get(&set.map(str::to_owned))
144 .is_some_and(|chars| chars.contains(&c))
145 };
146 keepable(c)
147 && (of(&self.punctuation_by_set)
148 || (!(narrow.glyphs.contains_key(&c) && self.wants(c))
149 && (of(&self.used_by_set) || of(&self.wide))))
150 }
151
152 pub fn wants(&self, c: char) -> bool {
154 keepable(c)
155 && (ATLAS_RANGES.iter().any(|(lo, hi)| (lo..=hi).contains(&&c))
156 || self.used.contains(&c)
157 || self.required.contains(&c))
158 }
159
160 pub fn used(&self) -> &BTreeSet<char> {
163 &self.used
164 }
165}
166
167const LACKED_SHOWN: usize = 20;
170
171pub const ONE_BIT_FILE: &str = "atlas_1bit.bin";
173
174#[derive(Debug, Clone, PartialEq, Eq)]
177pub struct BitmapFont {
178 pub cell_w: u32,
180 pub cell_h: u32,
182 pub glyphs: BTreeMap<char, Vec<bool>>,
184}
185
186impl BitmapFont {
187 pub fn add(&mut self, extra: Self) -> Result<(), String> {
191 if (extra.cell_w, extra.cell_h) != (self.cell_w, self.cell_h) {
192 return Err(format!(
193 "cell size {}×{}, expected {}×{}",
194 extra.cell_w, extra.cell_h, self.cell_w, self.cell_h
195 ));
196 }
197 for (c, pixels) in extra.glyphs {
198 if self.glyphs.insert(c, pixels).is_some() {
199 return Err(format!("U+{:04X} is already in the font", u32::from(c)));
200 }
201 }
202 Ok(())
203 }
204}
205
206type Lines<'a> = dyn Iterator<Item = (usize, &'a str)> + 'a;
208
209type BoundingBox = [i64; 4];
211
212#[derive(Debug, Clone, Copy, PartialEq, Eq)]
214enum Encoding {
215 Unicode,
217 Jis,
219}
220
221impl Encoding {
222 fn of_registry(registry: &str) -> Self {
224 let registry = registry.trim_matches('"').to_ascii_uppercase();
225 if registry.starts_with("JISX0208") {
226 Self::Jis
227 } else {
228 Self::Unicode
229 }
230 }
231
232 fn char_of(self, code: i64) -> Option<char> {
234 match self {
235 Self::Unicode => u32::try_from(code).ok().and_then(char::from_u32),
236 Self::Jis => jis_char(code),
237 }
238 }
239
240 const fn read_here(self) -> bool {
243 matches!(self, Self::Unicode) || cfg!(feature = "jis")
244 }
245}
246
247const NO_JIS: &str = "the font is in JIS X 0208 (CHARSET_REGISTRY JISX0208): turn on \
249 shuyi-cli's `jis` feature for the game's tools, or convert the font to \
250 Unicode";
251
252#[cfg(not(feature = "jis"))]
257fn jis_char(_: i64) -> Option<char> {
258 None
259}
260
261#[cfg(feature = "jis")]
265fn jis_char(code: i64) -> Option<char> {
266 let [_, _, _, _, _, _, row, cell] = code.to_be_bytes();
267 let inside = |b: u8| (0x21..=0x7e).contains(&b);
268 if !(inside(row) && inside(cell)) || code >> 16 != 0 {
269 return None;
270 }
271 let bytes = [row | 0x80, cell | 0x80];
272 let (text, malformed) = encoding_rs::EUC_JP.decode_without_bom_handling(&bytes);
273 let mut chars = text.chars();
274 match (chars.next(), chars.next()) {
275 (Some(c), None) if !malformed => Some(c),
276 _ => None,
277 }
278}
279
280pub fn parse_bdf(text: &str) -> Result<BitmapFont, String> {
286 let mut lines = text.lines().enumerate().map(|(i, l)| (i + 1, l.trim()));
287 let mut font_box: Option<BoundingBox> = None;
288 let mut encoding = Encoding::Unicode;
289 let mut glyphs = BTreeMap::new();
290 while let Some((n, line)) = lines.next() {
291 let mut words = line.split_whitespace();
292 match words.next() {
293 Some("FONTBOUNDINGBOX") => font_box = Some(numbers(words, n)?),
294 Some("CHARSET_REGISTRY") => {
295 encoding = Encoding::of_registry(words.next().unwrap_or_default());
296 if !encoding.read_here() {
297 return Err(format!("line {n}: {NO_JIS}"));
298 }
299 }
300 Some("STARTCHAR") => {
301 let font_box =
302 font_box.ok_or(format!("line {n}: STARTCHAR before FONTBOUNDINGBOX"))?;
303 if let (Some(c), pixels) = parse_char(&mut lines, font_box, encoding)? {
304 glyphs.insert(c, pixels);
305 }
306 }
307 _ => {}
308 }
309 }
310 let [w, h, _, _] = font_box.ok_or("no FONTBOUNDINGBOX")?;
311 let size = |v: i64| u32::try_from(v).ok().filter(|&v| v > 0);
312 let (Some(cell_w), Some(cell_h)) = (size(w), size(h)) else {
313 return Err(format!("bad FONTBOUNDINGBOX size {w}×{h}"));
314 };
315 Ok(BitmapFont {
316 cell_w,
317 cell_h,
318 glyphs,
319 })
320}
321
322fn parse_char(
325 lines: &mut Lines<'_>,
326 font_box: BoundingBox,
327 font_encoding: Encoding,
328) -> Result<(Option<char>, Vec<bool>), String> {
329 let [cell_w, cell_h, _, _] = font_box;
330 let mut encoding = None;
331 let mut glyph_box = None;
332 let mut pixels = vec![false; to_usize(cell_w * cell_h)];
333 while let Some((n, line)) = lines.next() {
334 let mut words = line.split_whitespace();
335 match words.next() {
336 Some("ENCODING") => {
337 let [e] = numbers(words, n)?;
338 encoding = font_encoding.char_of(e);
339 }
340 Some("BBX") => glyph_box = Some(numbers(words, n)?),
341 Some("BITMAP") => {
342 let glyph_box = glyph_box.ok_or(format!("line {n}: BITMAP before BBX"))?;
343 parse_bitmap(lines, n, font_box, glyph_box, &mut pixels)?;
344 }
345 Some("ENDCHAR") => break,
346 _ => {}
347 }
348 }
349 Ok((encoding, pixels))
350}
351
352fn parse_bitmap(
355 lines: &mut Lines<'_>,
356 start: usize,
357 [cell_w, cell_h, font_x, font_y]: BoundingBox,
358 [width, height, glyph_x, glyph_y]: BoundingBox,
359 pixels: &mut [bool],
360) -> Result<(), String> {
361 let top = (cell_h + font_y) - (glyph_y + height);
363 for row in 0..height {
364 let (n, hex) = lines
365 .next()
366 .ok_or(format!("line {start}: bitmap ends early"))?;
367 let bits =
368 u128::from_str_radix(hex, 16).map_err(|e| format!("line {n}: bad bitmap row: {e}"))?;
369 let row_bits = i64::try_from(hex.len() * 4).unwrap_or(0);
370 for col in 0..width.min(row_bits) {
371 let (x, y) = (glyph_x - font_x + col, top + row);
372 let on = (bits >> (row_bits - 1 - col)) & 1 == 1;
373 if on && (0..cell_w).contains(&x) && (0..cell_h).contains(&y) {
374 pixels[to_usize(y * cell_w + x)] = true;
375 }
376 }
377 }
378 Ok(())
379}
380
381fn numbers<'a, const N: usize>(
383 words: impl Iterator<Item = &'a str>,
384 line: usize,
385) -> Result<[i64; N], String> {
386 let v: Vec<i64> = words
387 .map(str::parse)
388 .collect::<Result<_, _>>()
389 .map_err(|e| format!("line {line}: {e}"))?;
390 v.try_into()
391 .map_err(|v: Vec<i64>| format!("line {line}: expected {N} numbers, got {}", v.len()))
392}
393
394fn to_usize(v: i64) -> usize {
396 usize::try_from(v).unwrap_or(0)
397}
398
399#[derive(Debug, Clone, PartialEq, Eq)]
402pub struct Atlas {
403 pub def: FontAtlasDef,
405 pub image: Image,
407}
408
409pub fn build_atlas(font: &BitmapFont, chars: &AtlasChars) -> Atlas {
412 build_wide_atlas(font, None, chars)
413}
414
415pub fn build_wide_atlas(font: &BitmapFont, wide: Option<&BitmapFont>, chars: &AtlasChars) -> Atlas {
420 let wide: Vec<(Option<&str>, &BitmapFont)> =
421 wide.map(|wide| (None, wide)).into_iter().collect();
422 build_atlas_with_sets(font, &wide, chars)
423}
424
425pub fn build_atlas_with_sets(
431 font: &BitmapFont,
432 wides: &[(Option<&str>, &BitmapFont)],
433 chars: &AtlasChars,
434) -> Atlas {
435 let chosen: Vec<(&char, &Vec<bool>)> = font
436 .glyphs
437 .iter()
438 .filter(|&(&c, _)| chars.wants(c))
439 .collect();
440 let mut next = u32::try_from(chosen.len()).unwrap_or(u32::MAX);
441 let mut def = FontAtlasDef {
442 cell_w: font.cell_w,
443 cell_h: font.cell_h,
444 columns: COLUMNS,
445 glyphs: chosen.iter().map(|&(&c, _)| c).zip(0..).collect(),
446 ..FontAtlasDef::default()
447 };
448 let mut placed_wide: Vec<(u32, &Vec<bool>)> = Vec::new();
449 for &(set, wide) in wides {
450 let mut cells = WideSet::new();
451 let wanted = wide
452 .glyphs
453 .iter()
454 .filter(|&(&c, _)| chars.wants_wide(c, font, set));
455 for (&c, pixels) in wanted {
456 if next % COLUMNS == COLUMNS - 1 {
457 next += 1; }
459 cells.insert(c, next);
460 placed_wide.push((next, pixels));
461 next += 2;
462 }
463 match set {
464 None => def.wide = cells,
465 Some(name) => {
466 def.wide_sets.insert(name.to_owned(), cells);
467 }
468 }
469 }
470 let rows = next.div_ceil(COLUMNS).max(1);
471 let (width, height) = (COLUMNS * font.cell_w, rows * font.cell_h);
472 let mut rgba = vec![0u8; to_usize(i64::from(width) * i64::from(height) * 4)];
473 let wide_rect = |index: u32| {
474 let rect = def.cell_rect(index);
475 AtlasRect {
476 w: rect.w * 2,
477 ..rect
478 }
479 };
480 let placed = chosen
481 .iter()
482 .map(|&(&c, pixels)| (def.glyph_rect(c), pixels))
483 .chain(
484 placed_wide
485 .iter()
486 .map(|&(i, pixels)| (Some(wide_rect(i)), pixels)),
487 );
488 for (rect, pixels) in placed {
489 let Some(rect) = rect else {
490 continue;
491 };
492 for (i, _) in pixels.iter().enumerate().filter(|&(_, &on)| on) {
493 let i = u32::try_from(i).unwrap_or(0);
494 let (x, y) = (rect.x + i % rect.w, rect.y + i / rect.w);
495 let at = to_usize((i64::from(y) * i64::from(width) + i64::from(x)) * 4);
496 rgba[at..at + 4].copy_from_slice(&[255; 4]);
497 }
498 }
499 Atlas {
500 def,
501 image: Image {
502 width,
503 height,
504 rgba,
505 },
506 }
507}
508
509pub fn check_wide(font: &BitmapFont, wide: &BitmapFont) -> Result<(), String> {
512 let want = (font.cell_w * 2, font.cell_h);
513 if (wide.cell_w, wide.cell_h) == want {
514 Ok(())
515 } else {
516 Err(format!(
517 "a wide font's cells are twice as wide as the font's: {}×{}, not {}×{}",
518 want.0, want.1, wide.cell_w, wide.cell_h
519 ))
520 }
521}
522
523pub fn one_bit(def: &FontAtlasDef, image: &Image) -> Result<Vec<u8>, String> {
530 if def.cell_w > 8 {
531 return Err(format!(
532 "the one-bit atlas holds cells up to 8 pixels wide, not {}",
533 def.cell_w
534 ));
535 }
536 let count = def.cell_end();
537 let mut out = Vec::with_capacity(to_usize(i64::from(count) * i64::from(def.cell_h)));
538 for index in 0..count {
539 let cell = def.cell_rect(index);
540 for y in cell.y..cell.y + cell.h {
541 let mut row = 0u8;
542 for x in cell.x..cell.x + cell.w {
543 let alpha =
544 to_usize((i64::from(y) * i64::from(image.width) + i64::from(x)) * 4 + 3);
545 let lit = image.rgba.get(alpha).is_some_and(|&a| a > 0);
546 row |= u8::from(lit) << (7 - (x - cell.x));
547 }
548 out.push(row);
549 }
550 }
551 Ok(out)
552}
553
554#[derive(Serialize)]
558struct AtlasRonBody<'a> {
559 cell_w: u32,
560 cell_h: u32,
561 columns: u32,
562 glyphs: &'a BTreeMap<char, u32>,
563 #[serde(skip_serializing_if = "BTreeMap::is_empty")]
564 wide: &'a WideSet,
565 #[serde(skip_serializing_if = "BTreeMap::is_empty")]
566 wide_sets: &'a BTreeMap<String, WideSet>,
567}
568
569pub fn atlas_ron(
572 def: &FontAtlasDef,
573 source_name: &str,
574 command: &str,
575 readme: Option<&str>,
576) -> Result<String, String> {
577 let config = ron::ser::PrettyConfig::default().new_line("\n");
579 let body = AtlasRonBody {
580 cell_w: def.cell_w,
581 cell_h: def.cell_h,
582 columns: def.columns,
583 glyphs: &def.glyphs,
584 wide: &def.wide,
585 wide_sets: &def.wide_sets,
586 };
587 let body = ron::ser::to_string_pretty(&body, config).map_err(|e| e.to_string())?;
588 let first = format!("// Generated by `{command} font-atlas` from {source_name}. Do not edit");
589 let maps = "Maps each glyph to its cell in atlas.png.";
590 Ok(match readme {
591 Some(readme) => format!("{first};\n// see {readme}. {maps}\n{body}\n"),
592 None => format!("{first}.\n// {maps}\n{body}\n"),
593 })
594}
595
596pub fn read_font(path: &Path) -> Result<BitmapFont, String> {
598 let text = fs::read_to_string(path).map_err(|e| format!("reading {}: {e}", path.display()))?;
599 parse_bdf(&text).map_err(|e| format!("{}: {e}", path.display()))
600}
601
602#[derive(Debug, Clone, PartialEq, Eq)]
604pub struct WideFont {
605 pub set: Option<String>,
608 pub path: PathBuf,
610}
611
612impl WideFont {
613 pub fn parse(arg: &str) -> Self {
617 match arg.split_once('=') {
618 Some((set, path)) if !set.is_empty() && !set.contains(['/', '\\']) => Self {
619 set: Some(set.to_owned()),
620 path: PathBuf::from(path),
621 },
622 _ => Self {
623 set: None,
624 path: PathBuf::from(arg),
625 },
626 }
627 }
628}
629
630fn lacked(font: &BitmapFont, wides: &[BitmapFont], chars: &AtlasChars) -> Option<String> {
634 let lacked: Vec<char> = chars
635 .used()
636 .iter()
637 .copied()
638 .filter(|c| !font.glyphs.contains_key(c))
639 .filter(|c| !wides.iter().any(|wide| wide.glyphs.contains_key(c)))
640 .collect();
641 if lacked.is_empty() {
642 return None;
643 }
644 let mut named: Vec<String> = lacked
645 .iter()
646 .take(LACKED_SHOWN)
647 .map(|&c| describe(c))
648 .collect();
649 if lacked.len() > LACKED_SHOWN {
650 named.push(format!("and {} more", lacked.len() - LACKED_SHOWN));
651 }
652 Some(format!(
653 "font-atlas: the fonts lack {} character(s) the text uses: {}",
654 lacked.len(),
655 named.join(", ")
656 ))
657}
658
659pub fn run(
667 rules: &FontAtlas,
668 command: &str,
669 font_paths: &[PathBuf],
670 wides: &[WideFont],
671 out_dir: &Path,
672 chars: &AtlasChars,
673) -> Result<String, String> {
674 let (font_path, extras) = font_paths.split_first().ok_or("no font given")?;
675 let mut font = read_font(font_path)?;
676 for path in extras {
677 let extra = read_font(path)?;
678 font.add(extra)
679 .map_err(|e| format!("{}: {e}", path.display()))?;
680 }
681 let mut sets = BTreeSet::new();
682 let mut wide_fonts = Vec::new();
683 for wide in wides {
684 if !sets.insert(wide.set.as_deref()) {
685 return Err(match &wide.set {
686 Some(set) => format!("two wide fonts for the set {set:?}"),
687 None => "two wide fonts for the unnamed set".to_owned(),
688 });
689 }
690 let path = &wide.path;
691 let read = read_font(path)?;
692 check_wide(&font, &read).map_err(|e| format!("{}: {e}", path.display()))?;
693 wide_fonts.push(read);
694 }
695 let by_set: Vec<(Option<&str>, &BitmapFont)> = wides
696 .iter()
697 .map(|wide| wide.set.as_deref())
698 .zip(&wide_fonts)
699 .collect();
700 let atlas = build_atlas_with_sets(&font, &by_set, chars);
701 let problems = atlas.def.problems(&rules.required_glyphs);
702 if !problems.is_empty() {
703 return Err(format!(
704 "{}: unusable atlas:\n {}",
705 font_path.display(),
706 problems.join("\n ")
707 ));
708 }
709 let names: Vec<String> = font_paths
710 .iter()
711 .chain(wides.iter().map(|wide| &wide.path))
712 .filter_map(|p| p.file_name())
713 .map(|n| n.to_string_lossy().into_owned())
714 .collect();
715 let ron = atlas_ron(
716 &atlas.def,
717 &names.join(" + "),
718 command,
719 rules.readme.as_deref(),
720 )?;
721 let png = encode_png(atlas.image.width, atlas.image.height, &atlas.image.rgba)?;
722 let bits = one_bit(&atlas.def, &atlas.image)?;
723 fs::create_dir_all(out_dir).map_err(|e| format!("creating {}: {e}", out_dir.display()))?;
724 for (file, bytes) in [
725 ("atlas.png", png.as_slice()),
726 ("atlas.ron", ron.as_bytes()),
727 (ONE_BIT_FILE, bits.as_slice()),
728 ] {
729 let path = out_dir.join(file);
730 fs::write(&path, bytes).map_err(|e| format!("writing {}: {e}", path.display()))?;
731 }
732 let wide_count = wide_count(&atlas.def, wides);
733 let mut summary = format!(
734 "font-atlas: {} glyphs{wide_count}, {}×{} px → {}",
735 atlas.def.glyphs.len(),
736 atlas.image.width,
737 atlas.image.height,
738 out_dir.display()
739 );
740 if let Some(lacked) = lacked(&font, &wide_fonts, chars) {
741 summary.push('\n');
742 summary.push_str(&lacked);
743 }
744 Ok(summary)
745}
746
747fn wide_count(def: &FontAtlasDef, wides: &[WideFont]) -> String {
751 let count = |set: Option<&str>| def.wide_set(set).map_or(0, BTreeMap::len);
752 match wides {
753 [] => String::new(),
754 [WideFont { set: None, .. }] => format!(" and {} wide", def.wide.len()),
755 _ => {
756 let sets: Vec<String> = wides
757 .iter()
758 .map(|wide| {
759 let name = wide.set.as_deref().unwrap_or("unnamed");
760 format!("{name} {}", count(wide.set.as_deref()))
761 })
762 .collect();
763 format!(" and wide sets {}", sets.join(", "))
764 }
765 }
766}
767
768#[cfg(test)]
769mod tests {
770 use super::*;
771 use crate::codec::decode_png;
772
773 fn rules() -> FontAtlas {
775 FontAtlas {
776 required_glyphs: "?Ab".to_owned(),
777 ..FontAtlas::default()
778 }
779 }
780
781 const TINY: &str = "STARTFONT 2.1\n\
784 FONTBOUNDINGBOX 4 4 0 -1\n\
785 CHARS 3\n\
786 STARTCHAR A\nENCODING 65\nBBX 2 2 0 0\nBITMAP\nC0\n80\nENDCHAR\n\
787 STARTCHAR b\nENCODING 98\nBBX 1 1 2 -1\nBITMAP\n80\nENDCHAR\n\
788 STARTCHAR none\nENCODING -1\nBBX 1 1 0 0\nBITMAP\n80\nENDCHAR\n\
789 ENDFONT\n";
790
791 fn grid(font: &BitmapFont, c: char) -> Vec<String> {
792 let px = &font.glyphs[&c];
793 px.chunks(to_usize(i64::from(font.cell_w)))
794 .map(|row| row.iter().map(|&on| if on { '#' } else { '.' }).collect())
795 .collect()
796 }
797
798 #[test]
799 fn parses_and_positions_glyphs() {
800 let font = parse_bdf(TINY).unwrap();
801 assert_eq!((font.cell_w, font.cell_h), (4, 4));
802 assert_eq!(font.glyphs.keys().collect::<String>(), "Ab");
803 assert_eq!(grid(&font, 'A'), ["....", "##..", "#...", "...."]);
804 assert_eq!(grid(&font, 'b'), ["....", "....", "....", "..#."]);
805 }
806
807 #[test]
808 fn font_x_offset_shifts_glyphs() {
809 let bdf = "FONTBOUNDINGBOX 4 2 -1 0\n\
812 STARTCHAR a\nENCODING 97\nBBX 1 1 0 0\nBITMAP\n80\nENDCHAR\n";
813 let font = parse_bdf(bdf).unwrap();
814 assert_eq!(grid(&font, 'a'), ["....", ".#.."]);
815 }
816
817 #[test]
818 fn parse_errors() {
819 assert!(
820 parse_bdf("STARTCHAR A\n")
821 .unwrap_err()
822 .contains("before FONTBOUNDINGBOX")
823 );
824 assert_eq!(parse_bdf("").unwrap_err(), "no FONTBOUNDINGBOX");
825 assert!(
826 parse_bdf("FONTBOUNDINGBOX 0 4 0 0\n")
827 .unwrap_err()
828 .contains("bad FONTBOUNDINGBOX")
829 );
830 assert!(
831 parse_bdf("FONTBOUNDINGBOX 4 4 0\n")
832 .unwrap_err()
833 .contains("expected 4 numbers, got 3")
834 );
835 assert!(
836 parse_bdf("FONTBOUNDINGBOX 4 x 0 0\n")
837 .unwrap_err()
838 .starts_with("line 1: ")
839 );
840 let no_bbx = "FONTBOUNDINGBOX 4 4 0 0\nSTARTCHAR A\nBITMAP\n";
841 assert!(
842 parse_bdf(no_bbx)
843 .unwrap_err()
844 .contains("line 3: BITMAP before BBX")
845 );
846 let short = "FONTBOUNDINGBOX 4 4 0 0\nSTARTCHAR A\nBBX 1 2 0 0\nBITMAP\n80\n";
847 assert!(parse_bdf(short).unwrap_err().contains("bitmap ends early"));
848 let bad = "FONTBOUNDINGBOX 4 4 0 0\nSTARTCHAR A\nBBX 1 1 0 0\nBITMAP\nzz\n";
849 assert!(parse_bdf(bad).unwrap_err().contains("bad bitmap row"));
850 }
851
852 #[test]
853 fn atlas_layout_and_pixels() {
854 let atlas = build_atlas(&parse_bdf(TINY).unwrap(), &AtlasChars::default());
855 assert_eq!(atlas.def.columns, COLUMNS);
856 assert_eq!(atlas.def.glyphs, [('A', 0), ('b', 1)].into());
857 assert_eq!((atlas.image.width, atlas.image.height), (COLUMNS * 4, 4));
858 let lit = |x: u32, y: u32| {
859 atlas.image.rgba[to_usize(i64::from((y * atlas.image.width + x) * 4 + 3))] == 255
860 };
861 assert!(lit(0, 1) && lit(1, 1) && lit(0, 2));
862 assert!(!lit(1, 2) && !lit(0, 0));
863 assert!(lit(4 + 2, 3));
864 let lit_count = atlas
865 .image
866 .rgba
867 .chunks(4)
868 .filter(|p| p == &[255; 4])
869 .count();
870 assert_eq!(lit_count, 4);
871 assert!(
872 atlas
873 .image
874 .rgba
875 .chunks(4)
876 .all(|p| p == [255; 4] || p == [0; 4])
877 );
878 }
879
880 #[test]
881 fn ron_round_trips_with_header() {
882 let def = build_atlas(&parse_bdf(TINY).unwrap(), &AtlasChars::default()).def;
883 let text = atlas_ron(&def, "tiny.bdf", "shuyi", None).unwrap();
884 assert!(text.starts_with(
885 "// Generated by `shuyi font-atlas` from tiny.bdf. Do not edit.\n\
886 // Maps each glyph to its cell in atlas.png.\n("
887 ));
888 assert!(!text.contains("\r\n"), "LF only");
889 assert_eq!(ron::from_str::<FontAtlasDef>(&text).unwrap(), def);
890 let text = atlas_ron(&def, "a + b", "tools", Some("fonts/README.md")).unwrap();
891 assert!(text.starts_with(
892 "// Generated by `tools font-atlas` from a + b. Do not edit;\n\
893 // see fonts/README.md. Maps each glyph to its cell in atlas.png.\n("
894 ));
895 assert_eq!(ron::from_str::<FontAtlasDef>(&text).unwrap(), def);
896 }
897
898 #[test]
899 fn run_rejects_fonts_missing_required_glyphs() {
900 let dir = std::env::temp_dir().join(format!("shuyi-font-atlas-{}", std::process::id()));
901 fs::create_dir_all(&dir).unwrap();
902 let bdf = dir.join("tiny.bdf");
903 fs::write(&bdf, TINY).unwrap();
904 let run = |fonts: &[PathBuf], out: &Path| {
905 run(&rules(), "shuyi", fonts, &[], out, &AtlasChars::default())
906 };
907 let err = run(std::slice::from_ref(&bdf), &dir.join("out")).unwrap_err();
908 assert!(err.contains("missing required glyphs"), "{err}");
909 assert!(!dir.join("out").exists());
910 assert!(
911 run(&[dir.join("missing.bdf")], &dir)
912 .unwrap_err()
913 .starts_with("reading ")
914 );
915 let err = run(&[bdf.clone(), dir.join("missing.bdf")], &dir).unwrap_err();
917 assert!(err.starts_with("reading ") && err.contains("missing.bdf"));
918 let err = run(&[bdf.clone(), bdf], &dir).unwrap_err();
919 assert!(err.contains("tiny.bdf: U+0041 is already in the font"));
920 assert_eq!(run(&[], &dir).unwrap_err(), "no font given");
921 fs::remove_dir_all(&dir).unwrap();
922 }
923
924 #[test]
925 fn run_writes_the_atlas_its_one_bit_copy_and_the_ron() {
926 let dir = std::env::temp_dir().join(format!("shuyi-font-run-{}", std::process::id()));
927 fs::create_dir_all(&dir).unwrap();
928 let bdf = dir.join("tiny.bdf");
929 let question = "STARTCHAR q\nENCODING 63\nBBX 1 1 0 0\nBITMAP\n80\nENDCHAR\n";
930 fs::write(&bdf, TINY.replace("ENDFONT", question)).unwrap();
931 let out = dir.join("out");
932 let said = run(
933 &rules(),
934 "shuyi",
935 std::slice::from_ref(&bdf),
936 &[],
937 &out,
938 &AtlasChars::default(),
939 )
940 .unwrap();
941 assert_eq!(
942 said,
943 format!("font-atlas: 3 glyphs, 128×4 px → {}", out.display())
944 );
945 let (w, h, _) = decode_png(&fs::read(out.join("atlas.png")).unwrap()).unwrap();
946 assert_eq!((w, h), (128, 4));
947 let ron = fs::read_to_string(out.join("atlas.ron")).unwrap();
948 assert!(ron.starts_with("// Generated by `shuyi font-atlas` from tiny.bdf."));
949 assert_eq!(fs::read(out.join(ONE_BIT_FILE)).unwrap().len(), 3 * 4);
950 fs::remove_dir_all(&dir).unwrap();
951 }
952
953 #[test]
954 fn extra_fonts_add_glyphs_and_never_replace_one() {
955 let extra = |body: &str| parse_bdf(&format!("FONTBOUNDINGBOX 4 4 0 -1\n{body}")).unwrap();
956 let mut font = parse_bdf(TINY).unwrap();
957 let c = "STARTCHAR c\nENCODING 99\nBBX 1 1 0 0\nBITMAP\n80\nENDCHAR\n";
958 font.add(extra(c)).unwrap();
959 assert_eq!(font.glyphs.keys().collect::<String>(), "Abc");
960 assert_eq!(grid(&font, 'c'), ["....", "....", "#...", "...."]);
961 assert_eq!(grid(&font, 'A'), ["....", "##..", "#...", "...."]);
962 assert_eq!(
964 font.add(extra(c)).unwrap_err(),
965 "U+0063 is already in the font"
966 );
967 let wide = parse_bdf("FONTBOUNDINGBOX 5 4 0 -1\n").unwrap();
968 assert_eq!(font.add(wide).unwrap_err(), "cell size 5×4, expected 4×4");
969 let tall = parse_bdf("FONTBOUNDINGBOX 4 5 0 -1\n").unwrap();
970 assert_eq!(font.add(tall).unwrap_err(), "cell size 4×5, expected 4×4");
971 }
972
973 fn wider() -> String {
976 let glyph = |name: &str, code: u32| {
977 format!("STARTCHAR {name}\nENCODING {code}\nBBX 1 1 0 0\nBITMAP\n80\nENDCHAR\n")
978 };
979 let extra = [
980 ("q", 63),
981 ("zhe", 0x416),
982 ("lstroke", 0x142),
983 ("acute", 0x301),
984 ]
985 .map(|(name, code)| glyph(name, code))
986 .concat();
987 TINY.replace("ENDFONT", &extra)
988 }
989
990 fn text(text: &str) -> Lang {
992 let english = [("menu.title".to_owned(), text.to_owned())];
993 Lang::new(english.into(), BTreeMap::new())
994 }
995
996 #[test]
997 fn the_atlas_keeps_the_ranges_the_text_and_the_required_glyphs() {
998 let chars = AtlasChars::of_lang(&text("Жar\u{301}\n"), "Ł");
999 let used: String = chars.used().iter().collect();
1000 assert_eq!(used, "arЖ", "no control character or combining mark");
1001 for c in ['A', 'é', '♣', 'Ж', 'Ł'] {
1002 assert!(chars.wants(c), "{c:?}");
1003 }
1004 for c in ['ł', '\u{301}', '\n', '\u{7}'] {
1005 assert!(!chars.wants(c), "{c:?}");
1006 }
1007 let ranges = AtlasChars::default();
1009 assert!(ranges.used().is_empty());
1010 assert!(ranges.wants('A') && ranges.wants('é') && !ranges.wants('Ж'));
1011 assert!(!AtlasChars::of_lang(&Lang::default(), "\u{301}").wants('\u{301}'));
1012 }
1013
1014 #[test]
1015 fn the_atlas_has_the_texts_glyphs_in_code_point_order() {
1016 let font = parse_bdf(&wider()).unwrap();
1017 let plain = build_atlas(&font, &AtlasChars::default());
1018 assert_eq!(plain.def.drawn().collect::<String>(), "?Ab");
1019 let chars = AtlasChars::of_lang(&text("Ж ł e\u{301}"), "");
1020 let atlas = build_atlas(&font, &chars);
1021 assert_eq!(atlas.def.drawn().collect::<String>(), "?AbłЖ");
1022 assert_eq!(atlas.def.glyphs[&'b'], plain.def.glyphs[&'b']);
1024 assert_eq!(atlas.def.glyphs[&'Ж'], 4);
1025 }
1026
1027 #[test]
1028 fn run_keeps_the_texts_characters_and_names_those_the_fonts_lack() {
1029 let dir = std::env::temp_dir().join(format!("shuyi-font-text-{}", std::process::id()));
1030 fs::create_dir_all(&dir).unwrap();
1031 let bdf = dir.join("wider.bdf");
1032 fs::write(&bdf, wider()).unwrap();
1033 let out = dir.join("out");
1034 let chars = AtlasChars::of_lang(&text("ЖłЖЯ∑b\u{301}"), "");
1035 let said = run(
1036 &rules(),
1037 "shuyi",
1038 std::slice::from_ref(&bdf),
1039 &[],
1040 &out,
1041 &chars,
1042 )
1043 .unwrap();
1044 assert_eq!(
1045 said,
1046 format!(
1047 "font-atlas: 5 glyphs, 128×4 px → {}\nfont-atlas: the fonts lack 2 character(s) \
1048 the text uses: 'Я' (U+042F), '∑' (U+2211)",
1049 out.display()
1050 )
1051 );
1052 let ron = fs::read_to_string(out.join("atlas.ron")).unwrap();
1053 let def: FontAtlasDef = ron::from_str(&ron).unwrap();
1054 assert_eq!(def.drawn().collect::<String>(), "?AbłЖ");
1055 let many: String = ('\u{4e00}'..'\u{4e19}').collect();
1057 let chars = AtlasChars::of_lang(&text(&many), "");
1058 let said = run(
1059 &rules(),
1060 "shuyi",
1061 std::slice::from_ref(&bdf),
1062 &[],
1063 &out,
1064 &chars,
1065 )
1066 .unwrap();
1067 assert!(said.contains("lack 25 character(s)"), "{said}");
1068 let last = format!("{}, and 5 more", describe('\u{4e13}'));
1069 assert!(said.ends_with(&last), "{said}");
1070 fs::remove_dir_all(&dir).unwrap();
1071 }
1072
1073 #[test]
1074 fn the_templates_atlas_is_what_font_atlas_makes_of_its_bdf() {
1075 let base = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../templates/shuyi-default/base");
1076 let shuyi_ron = fs::read_to_string(base.join("shuyi.ron")).unwrap();
1077 let config = crate::project::Config::parse(&shuyi_ron).unwrap();
1078 let rules = config.font_atlas.unwrap();
1079 assert_eq!(rules.required_glyphs, shuyi_ui::basic::BASIC_GLYPHS);
1080 let font = read_font(&base.join("files-src/ter-u16n.bdf")).unwrap();
1081 let atlas = build_atlas(&font, &AtlasChars::default());
1084 let committed = base.join("crates/game/files/font");
1085 let png = fs::read(committed.join("atlas.png")).unwrap();
1086 let (width, height, rgba) = decode_png(&png).unwrap();
1087 assert_eq!((width, height), (atlas.image.width, atlas.image.height));
1088 assert!(
1089 rgba == atlas.image.rgba,
1090 "the template's atlas.png is stale"
1091 );
1092 let command = config.command.as_deref().unwrap_or("shuyi");
1093 let ron = atlas_ron(&atlas.def, "ter-u16n.bdf", command, rules.readme.as_deref()).unwrap();
1094 let written = fs::read_to_string(committed.join("atlas.ron")).unwrap();
1095 assert_eq!(
1096 written.replace("\r\n", "\n"),
1097 ron,
1098 "the template's atlas.ron is stale"
1099 );
1100 let found = atlas.def.problems(&rules.required_glyphs);
1101 assert!(found.is_empty(), "{found:?}");
1102 }
1103
1104 proptest::proptest! {
1105 #[test]
1108 fn every_character_of_the_text_is_used_but_controls_and_marks(
1109 texts in proptest::collection::vec("\\PC{0,8}|[a\\n\u{301}é\u{7}]{0,4}", 1..4),
1110 ) {
1111 let english: BTreeMap<String, String> = texts
1112 .iter()
1113 .enumerate()
1114 .map(|(i, text)| (format!("k.{i}"), text.clone()))
1115 .collect();
1116 let chars = AtlasChars::of_lang(&Lang::new(english, BTreeMap::new()), "");
1117 for c in texts.iter().flat_map(|t| t.chars()) {
1118 let kept = !c.is_control() && !is_combining_mark(c);
1119 proptest::prop_assert_eq!(chars.used().contains(&c), kept, "{:?}", c);
1120 }
1121 }
1122 }
1123
1124 fn wide_fixture(jis: bool) -> BitmapFont {
1126 let name = if jis {
1127 "wide-16-jis.bdf"
1128 } else {
1129 "wide-16.bdf"
1130 };
1131 read_font(&fixtures().join(name)).unwrap()
1132 }
1133
1134 fn unnamed(path: &Path) -> WideFont {
1136 WideFont {
1137 set: None,
1138 path: path.to_owned(),
1139 }
1140 }
1141
1142 fn fixtures() -> PathBuf {
1144 Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/fonts")
1145 }
1146
1147 fn narrow_8_by_16() -> BitmapFont {
1149 parse_bdf(&TINY.replace("FONTBOUNDINGBOX 4 4 0 -1", "FONTBOUNDINGBOX 8 16 0 -2")).unwrap()
1150 }
1151
1152 #[test]
1153 fn a_jis_font_is_read_as_unicode() {
1154 let (unicode, jis) = (wide_fixture(false), wide_fixture(true));
1155 assert_eq!(unicode.glyphs.keys().collect::<String>(), "一口日田");
1156 assert_eq!(jis, unicode);
1157 assert_eq!((jis.cell_w, jis.cell_h), (16, 16));
1158 assert_eq!(jis_char(0x2422), Some('あ'));
1159 assert_eq!(jis_char(0x3021), Some('亜'));
1160 for none in [0x2020, 0x7f21, 0x0024_2121, -1, 0x2e21] {
1161 assert_eq!(jis_char(none), None, "{none:#x}");
1162 }
1163 assert_eq!(Encoding::of_registry("\"jisx0208.1983\""), Encoding::Jis);
1164 assert_eq!(Encoding::of_registry("\"ISO10646\""), Encoding::Unicode);
1165 }
1166
1167 #[test]
1168 fn the_wide_glyphs_are_the_texts_the_narrow_font_lacks_and_the_projects() {
1169 let narrow = narrow_8_by_16();
1170 let wide = wide_fixture(false);
1171 let chars = AtlasChars::of_lang(&text("田日A"), "").with_wide("一");
1174 let atlas = build_wide_atlas(&narrow, Some(&wide), &chars);
1175 assert_eq!(atlas.def.glyphs.keys().collect::<String>(), "Ab");
1176 assert_eq!(atlas.def.wide.keys().collect::<String>(), "一日田");
1177 assert_eq!(atlas.def.wide[&'一'], 2);
1179 assert_eq!(atlas.def.wide[&'日'], 4);
1180 assert_eq!(atlas.def.problems(""), Vec::<String>::new());
1181 assert_eq!(
1182 atlas
1183 .def
1184 .image_problems(atlas.image.width, atlas.image.height),
1185 Vec::<String>::new()
1186 );
1187 let lit = |x: u32, y: u32| {
1189 atlas.image.rgba[to_usize(i64::from((y * atlas.image.width + x) * 4 + 3))]
1190 };
1191 let rect = atlas.def.wide_rect('一').unwrap();
1192 assert_eq!((rect.x, rect.w), (16, 16));
1193 assert!((rect.x + 1..rect.x + 15).all(|x| lit(x, 7) == 255 && lit(x, 8) == 255));
1194 assert!((rect.x..rect.x + 16).all(|x| lit(x, 6) == 0));
1195 let plain = build_atlas(&narrow, &chars);
1197 assert!(plain.def.wide.is_empty());
1198 let both = AtlasChars::of_lang(&text("田"), "").with_wide("A");
1199 let atlas = build_wide_atlas(&narrow, Some(&wide), &both);
1200 assert_eq!(atlas.def.wide.keys().collect::<String>(), "田");
1201 assert!(atlas.def.glyphs.contains_key(&'A'));
1202 }
1203
1204 #[test]
1205 fn a_wide_glyph_never_starts_in_a_rows_last_cell() {
1206 let glyph = |c: char| {
1209 let code = u32::from(c);
1210 format!("STARTCHAR {c}\nENCODING {code}\nBBX 1 1 0 0\nBITMAP\n80\nENDCHAR\n")
1211 };
1212 let narrow: String = ('!'..'@').map(glyph).collect();
1213 let narrow = parse_bdf(&format!("FONTBOUNDINGBOX 8 16 0 -2\n{narrow}")).unwrap();
1214 assert_eq!(narrow.glyphs.len(), 31);
1215 let chars = AtlasChars::default().with_wide("一口");
1216 let atlas = build_wide_atlas(&narrow, Some(&wide_fixture(false)), &chars);
1217 assert_eq!(atlas.def.wide[&'一'], COLUMNS);
1218 assert_eq!(atlas.def.wide[&'口'], COLUMNS + 2);
1219 assert_eq!(atlas.def.problems(""), Vec::<String>::new());
1220 let bits = one_bit(&atlas.def, &atlas.image).unwrap();
1223 assert_eq!(bits.len(), to_usize(i64::from(COLUMNS + 4) * 16));
1224 let right_half_of_one = to_usize(i64::from(COLUMNS + 1) * 16);
1225 assert_eq!(bits[right_half_of_one + 7], 0b1111_1110);
1226 }
1227
1228 #[test]
1229 fn run_writes_the_wide_glyphs_and_refuses_a_wide_font_of_another_size() {
1230 let dir = std::env::temp_dir().join(format!("shuyi-font-wide-{}", std::process::id()));
1231 fs::create_dir_all(&dir).unwrap();
1232 let bdf = dir.join("narrow.bdf");
1233 let question = "STARTCHAR q\nENCODING 63\nBBX 1 1 0 0\nBITMAP\n80\nENDCHAR\n";
1234 let narrow = TINY
1235 .replace("FONTBOUNDINGBOX 4 4 0 -1", "FONTBOUNDINGBOX 8 16 0 -2")
1236 .replace("ENDFONT", question);
1237 fs::write(&bdf, narrow).unwrap();
1238 let out = dir.join("out");
1239 let wide = fixtures().join("wide-16-jis.bdf");
1240 let chars = AtlasChars::of_lang(&text("日本"), "");
1241 let fonts = std::slice::from_ref(&bdf);
1242 let said = run(&rules(), "shuyi", fonts, &[unnamed(&wide)], &out, &chars).unwrap();
1243 assert_eq!(
1244 said,
1245 format!(
1246 "font-atlas: 3 glyphs and 1 wide, 256×16 px → {}\nfont-atlas: the fonts lack \
1247 1 character(s) the text uses: '本' (U+672C)",
1248 out.display()
1249 )
1250 );
1251 let ron = fs::read_to_string(out.join("atlas.ron")).unwrap();
1252 assert!(
1253 ron.starts_with(
1254 "// Generated by `shuyi font-atlas` from narrow.bdf + wide-16-jis.bdf."
1255 )
1256 );
1257 let def: FontAtlasDef = ron::from_str(&ron).unwrap();
1258 assert_eq!(def.wide, [('日', 3)].into());
1259 assert_eq!(fs::read(out.join(ONE_BIT_FILE)).unwrap().len(), 5 * 16);
1260 let plain = build_atlas(&narrow_8_by_16(), &AtlasChars::default()).def;
1262 let text = atlas_ron(&plain, "a", "shuyi", None).unwrap();
1263 assert!(!text.contains("wide"), "{text}");
1264 assert_eq!(ron::from_str::<FontAtlasDef>(&text).unwrap(), plain);
1265 let err = run(&rules(), "shuyi", fonts, &[unnamed(&bdf)], &out, &chars).unwrap_err();
1267 assert!(
1268 err.ends_with("a wide font's cells are twice as wide as the font's: 16×16, not 8×16"),
1269 "{err}"
1270 );
1271 fs::remove_dir_all(&dir).unwrap();
1272 }
1273
1274 fn packs(list: &[(&str, &str, &str)]) -> Lang {
1277 use shuyi_assets::lang::{LangCode, PackRules, pack_from_sources};
1278 let english: BTreeMap<String, String> =
1279 [("menu.title".to_owned(), "Start".to_owned())].into();
1280 let rules = PackRules {
1281 data_problem: |_, _| None,
1282 tokens: &shuyi_assets::dialogue::NoTokens,
1283 limits: shuyi_assets::dialogue::DialogueLimits {
1284 box_width: 40,
1285 box_lines: 3,
1286 max_text: 200,
1287 max_option: 30,
1288 min_options: 2,
1289 max_options: 4,
1290 max_reaction_texts: 3,
1291 },
1292 widths: &shuyi_assets::font::CellWidths::NARROW,
1293 variants: &[],
1294 };
1295 let packs = list
1296 .iter()
1297 .map(|&(code, extra, text)| {
1298 let info = format!(r#"(name: "{code}", made_by: Machine{extra})"#);
1299 let ui = format!(r#"[(key: "menu.title", source: "Start", text: "{text}")]"#);
1300 let pack =
1301 pack_from_sources(("lang.ron", &info), ("ui.ron", &ui), &english, &rules);
1302 (LangCode::new(code).unwrap(), pack.unwrap())
1303 })
1304 .collect();
1305 Lang::new(english, packs)
1306 }
1307
1308 #[test]
1309 fn a_wide_font_names_its_set_before_an_equals_sign() {
1310 let parsed = |arg: &str| {
1311 let wide = WideFont::parse(arg);
1312 (wide.set, wide.path)
1313 };
1314 let path = |p: &str| PathBuf::from(p);
1315 assert_eq!(
1316 parsed("ja=fonts/ja.bdf"),
1317 (Some("ja".to_owned()), path("fonts/ja.bdf"))
1318 );
1319 assert_eq!(
1320 parsed("zh-Hans=zh.bdf"),
1321 (Some("zh-Hans".to_owned()), path("zh.bdf"))
1322 );
1323 assert_eq!(parsed("fonts/ja.bdf"), (None, path("fonts/ja.bdf")));
1324 assert_eq!(parsed("a=b/c.bdf"), (Some("a".to_owned()), path("b/c.bdf")));
1326 assert_eq!(parsed("fonts/a=b.bdf"), (None, path("fonts/a=b.bdf")));
1327 assert_eq!(parsed("fonts\\a=b.bdf"), (None, path("fonts\\a=b.bdf")));
1328 assert_eq!(parsed("=b.bdf"), (None, path("=b.bdf")));
1329 }
1330
1331 #[test]
1332 fn each_set_keeps_the_characters_of_the_languages_that_draw_from_it() {
1333 let narrow = narrow_8_by_16();
1334 let (ja, zh) = (
1335 read_font(&fixtures().join("wide-16-ja.bdf")).unwrap(),
1336 read_font(&fixtures().join("wide-16-zh.bdf")).unwrap(),
1337 );
1338 let lang = packs(&[
1339 ("ja", r#", wide_font: Some("ja")"#, "直日"),
1340 ("ko", "", "田"),
1341 (
1342 "zh-Hans",
1343 r#", wide_font: Some("zh-Hans"), wide_punctuation: true"#,
1344 "“直简”…",
1345 ),
1346 ]);
1347 let chars = AtlasChars::of_lang(&lang, "").with_wide_set(Some("ja"), "一");
1348 let mut with_ellipsis = narrow.clone();
1351 with_ellipsis.glyphs.insert('…', Vec::new());
1352 assert!(chars.wants_wide('…', &with_ellipsis, Some("zh-Hans")));
1353 assert!(!chars.wants_wide('…', &with_ellipsis, Some("ja")));
1354 assert!(!chars.wants_wide('…', &with_ellipsis, None));
1355 assert!(chars.wants_wide('一', &narrow, Some("ja")));
1356 assert!(!chars.wants_wide('一', &narrow, Some("zh-Hans")));
1357 let wides = [
1358 (Some("ja"), &ja),
1359 (Some("zh-Hans"), &zh),
1360 (None, &wide_fixture(false)),
1361 ];
1362 let atlas = build_atlas_with_sets(&narrow, &wides, &chars);
1363 let def = &atlas.def;
1364 let keys = |set: Option<&str>| def.wide_set(set).unwrap().keys().collect::<String>();
1365 assert_eq!(keys(Some("ja")), "日直");
1367 assert_eq!(keys(Some("zh-Hans")), "“”…直简");
1368 assert_eq!(keys(None), "田");
1370 assert_eq!(def.glyphs.keys().collect::<String>(), "Ab");
1372 assert_eq!(
1373 (
1374 def.wide_cell(Some("ja"), '日'),
1375 def.wide_cell(Some("ja"), '直')
1376 ),
1377 (Some(2), Some(4))
1378 );
1379 assert_eq!(def.wide_cell(Some("zh-Hans"), '直'), Some(12));
1380 assert_eq!(def.wide_cell(None, '田'), Some(16));
1381 assert_eq!(def.problems(""), Vec::<String>::new());
1382 assert_eq!(
1383 def.image_problems(atlas.image.width, atlas.image.height),
1384 Vec::<String>::new()
1385 );
1386 let undrawn = shuyi_assets::font::undrawn(def, &lang);
1389 for c in "日直简“”…田".chars() {
1390 let said = describe(c);
1391 assert!(
1392 undrawn.iter().all(|e| !e.message.starts_with(&said)),
1393 "{undrawn:?}"
1394 );
1395 }
1396 let ron = atlas_ron(def, "a", "shuyi", None).unwrap();
1398 assert!(ron.contains("wide_sets"), "{ron}");
1399 assert_eq!(ron::from_str::<FontAtlasDef>(&ron).unwrap(), *def);
1400 let bits = one_bit(def, &atlas.image).unwrap();
1401 assert_eq!(bits.len(), to_usize(i64::from(18 * 16)));
1402 }
1403
1404 #[test]
1405 fn the_source_keeps_its_characters_in_the_set_it_names() {
1406 use shuyi_assets::lang::{LangCode, SourceLang};
1407 let source = SourceLang::new(LangCode::new("zh-Hans").unwrap(), "中文")
1408 .with_wide_font("zh-Hans")
1409 .with_wide_punctuation(true);
1410 let ui: BTreeMap<String, String> = [("menu.title".to_owned(), "“直简”".to_owned())].into();
1411 let lang = Lang::new(ui.clone(), BTreeMap::new()).with_source_lang(source);
1412 let chars = AtlasChars::of_lang(&lang, "");
1413 let mut narrow = narrow_8_by_16();
1414 narrow.glyphs.insert('“', Vec::new());
1415 for c in "直简“".chars() {
1417 assert!(chars.wants_wide(c, &narrow, Some("zh-Hans")), "{c}");
1418 assert!(!chars.wants_wide(c, &narrow, None), "{c}");
1419 }
1420 let plain = Lang::new(ui, BTreeMap::new());
1422 let chars = AtlasChars::of_lang(&plain, "");
1423 assert!(chars.wants_wide('直', &narrow, None));
1424 assert!(!chars.wants_wide('“', &narrow, None));
1425 assert!(!chars.wants_wide('直', &narrow, Some("zh-Hans")));
1426 }
1427
1428 #[test]
1429 fn run_writes_named_sets_and_refuses_two_fonts_for_one() {
1430 let dir = std::env::temp_dir().join(format!("shuyi-font-sets-{}", std::process::id()));
1431 fs::create_dir_all(&dir).unwrap();
1432 let bdf = dir.join("narrow.bdf");
1433 let question = "STARTCHAR q\nENCODING 63\nBBX 1 1 0 0\nBITMAP\n80\nENDCHAR\n";
1434 let narrow = TINY
1435 .replace("FONTBOUNDINGBOX 4 4 0 -1", "FONTBOUNDINGBOX 8 16 0 -2")
1436 .replace("ENDFONT", question);
1437 fs::write(&bdf, narrow).unwrap();
1438 let out = dir.join("out");
1439 let set = |set: &str, file: &str| WideFont {
1440 set: Some(set.to_owned()),
1441 path: fixtures().join(file),
1442 };
1443 let lang = packs(&[
1444 ("ja", r#", wide_font: Some("ja")"#, "直"),
1445 ("zh-Hans", r#", wide_font: Some("zh-Hans")"#, "直简"),
1446 ]);
1447 let chars = AtlasChars::of_lang(&lang, "");
1448 let fonts = std::slice::from_ref(&bdf);
1449 let wides = [
1450 set("ja", "wide-16-ja.bdf"),
1451 set("zh-Hans", "wide-16-zh.bdf"),
1452 ];
1453 let said = run(&rules(), "shuyi", fonts, &wides, &out, &chars).unwrap();
1454 assert!(
1455 said.starts_with("font-atlas: 3 glyphs and wide sets ja 1, zh-Hans 2, 256×16 px"),
1456 "{said}"
1457 );
1458 let ron = fs::read_to_string(out.join("atlas.ron")).unwrap();
1459 assert!(
1460 ron.starts_with(
1461 "// Generated by `shuyi font-atlas` from narrow.bdf + wide-16-ja.bdf + \
1462 wide-16-zh.bdf."
1463 ),
1464 "{ron}"
1465 );
1466 let def: FontAtlasDef = ron::from_str(&ron).unwrap();
1467 assert!(def.wide.is_empty());
1468 assert_eq!(def.wide_cell(Some("zh-Hans"), '简'), Some(7));
1469 let both = [
1471 set("ja", "wide-16-ja.bdf"),
1472 unnamed(&fixtures().join("wide-16.bdf")),
1473 ];
1474 let said = run(&rules(), "shuyi", fonts, &both, &out, &chars).unwrap();
1475 assert!(said.contains(" and wide sets ja 1, unnamed 0,"), "{said}");
1476 let twice = [set("ja", "wide-16-ja.bdf"), set("ja", "wide-16-zh.bdf")];
1478 let err = run(&rules(), "shuyi", fonts, &twice, &out, &chars).unwrap_err();
1479 assert_eq!(err, "two wide fonts for the set \"ja\"");
1480 let twice = [
1481 unnamed(&fixtures().join("wide-16.bdf")),
1482 unnamed(&fixtures().join("wide-16.bdf")),
1483 ];
1484 let err = run(&rules(), "shuyi", fonts, &twice, &out, &chars).unwrap_err();
1485 assert_eq!(err, "two wide fonts for the unnamed set");
1486 fs::remove_dir_all(&dir).unwrap();
1487 }
1488
1489 #[test]
1490 fn the_one_bit_atlas_packs_rows_left_pixel_highest() {
1491 let atlas = build_atlas(&parse_bdf(TINY).unwrap(), &AtlasChars::default());
1492 let bits = one_bit(&atlas.def, &atlas.image).unwrap();
1493 assert_eq!(bits, [0, 0b1100_0000, 0b1000_0000, 0, 0, 0, 0, 0b0010_0000]);
1495 let wide = FontAtlasDef {
1496 cell_w: 9,
1497 ..atlas.def
1498 };
1499 assert_eq!(
1500 one_bit(&wide, &atlas.image).unwrap_err(),
1501 "the one-bit atlas holds cells up to 8 pixels wide, not 9"
1502 );
1503 let empty = FontAtlasDef::default();
1504 let found = one_bit(&empty, &Image::default()).unwrap();
1505 assert!(found.is_empty(), "{found:?}");
1506 }
1507}