1use crate::assets::RgbaImage;
9use crate::image_manager::{ImageId, ImageManager};
10use ab_glyph::{point, Font, FontArc, PxScale, ScaleFont};
11use std::path::{Path, PathBuf};
12
13mod embedded_font {
14 pub const EMBEDDED_DEFAULT_FONT: Option<&'static [u8]> =
15 Some(include_bytes!("../assets/fonts/default.ttf") as &'static [u8]);
16 pub const EMBEDDED_DEFAULT_FONT_SOURCE: Option<&'static str> = Some("assets/fonts/default.ttf");
17 pub const EMBEDDED_DEFAULT_FONT_ALIASES: &[&str] = &[
18 "MS Pゴシック",
19 "MS PGothic",
20 "MS-PGothic",
21 "MSPGothic",
22 "msgothic",
23 "default",
24 ];
25}
26
27#[derive(Debug, Clone, Copy)]
28pub struct TextStyle {
29 pub color: (u8, u8, u8),
30 pub shadow_color: (u8, u8, u8),
31 pub fuchi_color: (u8, u8, u8),
32 pub shadow: bool,
33 pub fuchi: bool,
34 pub bold: bool,
35}
36
37impl Default for TextStyle {
38 fn default() -> Self {
39 Self {
40 color: (255, 255, 255),
41 shadow_color: (0, 0, 0),
42 fuchi_color: (0, 0, 0),
43 shadow: true,
44 fuchi: false,
45 bold: false,
46 }
47 }
48}
49
50
51#[derive(Debug, Default)]
52pub struct FontCache {
53 font: Option<FontArc>,
54 loaded_from: Option<PathBuf>,
55}
56
57impl FontCache {
58 pub fn new() -> Self {
59 Self {
60 font: None,
61 loaded_from: None,
62 }
63 }
64
65 pub fn is_loaded(&self) -> bool {
66 self.font.is_some()
67 }
68
69 pub fn loaded_from(&self) -> Option<&Path> {
70 self.loaded_from.as_deref()
71 }
72
73 pub fn load_for_project(&mut self, project_dir: &Path) -> bool {
74 if self.font.is_some() {
75 return true;
76 }
77
78 let mut dirs = Vec::new();
79 dirs.push(project_dir.join("font"));
80 dirs.push(project_dir.join("fonts"));
81
82 if let Ok(exe) = std::env::current_exe() {
83 if let Some(exe_dir) = exe.parent() {
84 dirs.push(exe_dir.join("font"));
85 dirs.push(exe_dir.join("fonts"));
86 }
87 }
88
89 let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
90 dirs.push(manifest_dir.join("assets").join("font"));
91 dirs.push(manifest_dir.join("assets").join("fonts"));
92
93 for dir in dirs {
94 if self.load_from_font_dir(&dir) {
95 return true;
96 }
97 }
98
99 if self.try_load_embedded_default_font() {
100 return true;
101 }
102
103 for path in platform_font_candidates() {
104 if self.try_load_font_file(&path) {
105 return true;
106 }
107 }
108
109 false
110 }
111
112 pub fn load_from_font_dir(&mut self, font_dir: &Path) -> bool {
113 if self.font.is_some() {
114 return true;
115 }
116 let Ok(entries) = std::fs::read_dir(font_dir) else {
117 return false;
118 };
119
120 let mut files = Vec::new();
121 for entry in entries.flatten() {
122 let path = entry.path();
123 if path.is_file() && is_supported_font_path(&path) {
124 files.push(path);
125 }
126 }
127 files.sort_by_key(|path| font_path_priority(path));
128
129 for path in files {
130 if self.try_load_font_file(&path) {
131 return true;
132 }
133 }
134 false
135 }
136
137 fn try_load_font_file(&mut self, path: &Path) -> bool {
138 if self.font.is_some() {
139 return true;
140 }
141 if !path.is_file() || !is_supported_font_path(path) {
142 return false;
143 }
144 let Ok(bytes) = std::fs::read(path) else {
145 return false;
146 };
147 match FontArc::try_from_vec(bytes) {
148 Ok(font) => {
149 self.font = Some(font);
150 self.loaded_from = Some(path.to_path_buf());
151 true
152 }
153 Err(_) => false,
154 }
155 }
156
157 fn try_load_embedded_default_font(&mut self) -> bool {
158 if self.font.is_some() {
159 return true;
160 }
161 let Some(bytes) = embedded_font::EMBEDDED_DEFAULT_FONT else {
162 return false;
163 };
164 match FontArc::try_from_vec(bytes.to_vec()) {
165 Ok(font) => {
166 self.font = Some(font);
167 let source = embedded_font::EMBEDDED_DEFAULT_FONT_SOURCE.unwrap_or("embedded:default-font");
168 self.loaded_from = Some(PathBuf::from(source));
169 true
170 }
171 Err(_) => false,
172 }
173 }
174
175 pub fn render_text(
176 &self,
177 images: &mut ImageManager,
178 text: &str,
179 font_px: f32,
180 max_w: u32,
181 max_h: u32,
182 ) -> Option<ImageId> {
183 self.render_text_into(images, None, text, font_px, max_w, max_h)
184 }
185
186 pub fn render_mwnd_text(
187 &self,
188 images: &mut ImageManager,
189 text: &str,
190 font_px: f32,
191 max_w: u32,
192 max_h: u32,
193 moji_space: Option<(i64, i64)>,
194 ) -> Option<ImageId> {
195 let img = self.render_mwnd_text_rgba(text, font_px, max_w, max_h, moji_space)?;
196 Some(images.insert_image(img))
197 }
198
199 pub fn render_mwnd_text_styled(
200 &self,
201 images: &mut ImageManager,
202 text: &str,
203 font_px: f32,
204 max_w: u32,
205 max_h: u32,
206 moji_space: Option<(i64, i64)>,
207 style: TextStyle,
208 ) -> Option<ImageId> {
209 self.render_mwnd_text_styled_into(images, None, text, font_px, max_w, max_h, moji_space, style)
210 }
211
212 pub fn render_mwnd_text_styled_into(
213 &self,
214 images: &mut ImageManager,
215 target: Option<ImageId>,
216 text: &str,
217 font_px: f32,
218 max_w: u32,
219 max_h: u32,
220 moji_space: Option<(i64, i64)>,
221 style: TextStyle,
222 ) -> Option<ImageId> {
223 let img = self.render_mwnd_text_rgba_styled(text, font_px, max_w, max_h, moji_space, style)?;
224 match target {
225 Some(id) => {
226 images.replace_image(id, img).ok()?;
227 Some(id)
228 }
229 None => Some(images.insert_image(img)),
230 }
231 }
232
233 pub fn render_text_into(
234 &self,
235 images: &mut ImageManager,
236 target: Option<ImageId>,
237 text: &str,
238 font_px: f32,
239 max_w: u32,
240 max_h: u32,
241 ) -> Option<ImageId> {
242 let img = self.render_text_rgba(text, font_px, max_w, max_h)?;
243 match target {
244 Some(id) => {
245 images.replace_image(id, img).ok()?;
246 Some(id)
247 }
248 None => Some(images.insert_image(img)),
249 }
250 }
251
252 pub fn render_text_rgba(
253 &self,
254 text: &str,
255 font_px: f32,
256 max_w: u32,
257 max_h: u32,
258 ) -> Option<RgbaImage> {
259 let Some(font) = self.font.as_ref() else {
260 return render_text_image_basic_rgba(text, font_px as u32, max_w, max_h);
261 };
262 render_text_ab_glyph_rgba(font, text, font_px, max_w, max_h)
263 }
264
265 pub fn render_mwnd_text_rgba(
266 &self,
267 text: &str,
268 font_px: f32,
269 max_w: u32,
270 max_h: u32,
271 moji_space: Option<(i64, i64)>,
272 ) -> Option<RgbaImage> {
273 self.render_mwnd_text_rgba_styled(text, font_px, max_w, max_h, moji_space, TextStyle::default())
274 }
275
276 pub fn render_mwnd_text_rgba_styled(
277 &self,
278 text: &str,
279 font_px: f32,
280 max_w: u32,
281 max_h: u32,
282 moji_space: Option<(i64, i64)>,
283 style: TextStyle,
284 ) -> Option<RgbaImage> {
285 let Some(font) = self.font.as_ref() else {
286 return render_text_image_basic_rgba(text, font_px as u32, max_w, max_h);
287 };
288 render_mwnd_text_ab_glyph_rgba_styled(font, text, font_px, max_w, max_h, moji_space, style)
289 }
290}
291
292pub fn render_text_image_basic(
293 images: &mut ImageManager,
294 text: &str,
295 font_px: u32,
296 max_w: u32,
297 max_h: u32,
298) -> Option<ImageId> {
299 let img = render_text_image_basic_rgba(text, font_px, max_w, max_h)?;
300 Some(images.insert_image(img))
301}
302
303pub fn render_text_image_basic_rgba(
304 text: &str,
305 font_px: u32,
306 max_w: u32,
307 max_h: u32,
308) -> Option<RgbaImage> {
309 if text.is_empty() || max_w == 0 || max_h == 0 {
310 return None;
311 }
312 let scale = (font_px / 7).max(1);
313 let glyph_w = 5 * scale;
314 let glyph_h = 7 * scale;
315 let advance = glyph_w + scale;
316 let line_height = glyph_h + scale;
317
318 let mut rgba = vec![0u8; (max_w * max_h * 4) as usize];
319 let mut x = 0u32;
320 let mut y = 0u32;
321
322 for ch in text.chars() {
323 if ch == '\n' {
324 x = 0;
325 y = y.saturating_add(line_height);
326 if y >= max_h {
327 break;
328 }
329 continue;
330 }
331 if ch == '\t' {
332 x = x.saturating_add(advance * 2);
333 continue;
334 }
335 if x + glyph_w > max_w {
336 x = 0;
337 y = y.saturating_add(line_height);
338 if y >= max_h {
339 break;
340 }
341 }
342 draw_glyph_5x7(&mut rgba, max_w, max_h, x, y, ch, scale);
343 x = x.saturating_add(advance);
344 }
345
346 Some(RgbaImage {
347 width: max_w,
348 height: max_h,
349 center_x: 0,
350 center_y: 0,
351 rgba,
352 })
353}
354
355
356#[derive(Debug, Clone)]
357struct RasterGlyph {
358 width: usize,
359 height: usize,
360 xmin: i32,
361 ymin: i32,
362 bitmap: Vec<u8>,
363}
364
365fn rasterize_ab_glyph(font: &FontArc, ch: char, font_px: f32) -> RasterGlyph {
366 let scale = PxScale::from(font_px.max(1.0));
367 let scaled = font.as_scaled(scale);
368 let glyph_id = scaled.glyph_id(ch);
369 let glyph = glyph_id.with_scale_and_position(scale, point(0.0, 0.0));
370 let Some(outlined) = scaled.outline_glyph(glyph) else {
371 return RasterGlyph {
372 width: 0,
373 height: 0,
374 xmin: 0,
375 ymin: 0,
376 bitmap: Vec::new(),
377 };
378 };
379
380 let bounds = outlined.px_bounds();
381 let xmin = bounds.min.x.floor() as i32;
382 let ymin = bounds.min.y.floor() as i32;
383 let xmax = bounds.max.x.ceil() as i32;
384 let ymax = bounds.max.y.ceil() as i32;
385 let width = (xmax - xmin).max(0) as usize;
386 let height = (ymax - ymin).max(0) as usize;
387 if width == 0 || height == 0 {
388 return RasterGlyph {
389 width: 0,
390 height: 0,
391 xmin,
392 ymin,
393 bitmap: Vec::new(),
394 };
395 }
396
397 let shifted_glyph = glyph_id.with_scale_and_position(scale, point((-xmin) as f32, (-ymin) as f32));
398 let Some(shifted) = scaled.outline_glyph(shifted_glyph) else {
399 return RasterGlyph {
400 width: 0,
401 height: 0,
402 xmin,
403 ymin,
404 bitmap: Vec::new(),
405 };
406 };
407
408 let mut bitmap = vec![0u8; width * height];
409 shifted.draw(|gx, gy, cov| {
410 let x = gx as usize;
411 let y = gy as usize;
412 if x < width && y < height {
413 bitmap[y * width + x] = (cov * 255.0).round().clamp(0.0, 255.0) as u8;
414 }
415 });
416
417 RasterGlyph {
418 width,
419 height,
420 xmin,
421 ymin,
422 bitmap,
423 }
424}
425
426
427fn render_mwnd_text_ab_glyph_rgba(
428 font: &FontArc,
429 text: &str,
430 font_px: f32,
431 max_w: u32,
432 max_h: u32,
433 moji_space: Option<(i64, i64)>,
434) -> Option<RgbaImage> {
435 render_mwnd_text_ab_glyph_rgba_styled(font, text, font_px, max_w, max_h, moji_space, TextStyle::default())
436}
437
438fn render_mwnd_text_ab_glyph_rgba_styled(
439 font: &FontArc,
440 text: &str,
441 font_px: f32,
442 max_w: u32,
443 max_h: u32,
444 moji_space: Option<(i64, i64)>,
445 style: TextStyle,
446) -> Option<RgbaImage> {
447 if text.is_empty() || max_w == 0 || max_h == 0 {
448 return None;
449 }
450
451 let (space_x, space_y) = moji_space.unwrap_or((-1, 10));
452 let font_cell = font_px.round().max(1.0) as i32;
453 let line_h = (font_cell + space_y as i32).max(font_cell).max(1);
454 let scaled = font.as_scaled(PxScale::from(font_px.max(1.0)));
455 let baseline_y = scaled.ascent().ceil().max(1.0) as i32;
456 let effect_pad = text_effect_padding(font_cell, style);
457 let render_w = max_w.saturating_add(effect_pad.max(0) as u32 + 2);
458 let render_h = max_h.saturating_add((baseline_y + effect_pad).max(font_cell / 4 + effect_pad + 2).max(0) as u32);
459 let mut rgba = vec![0u8; (render_w * render_h * 4) as usize];
460
461 for placed in layout_mwnd_text(text, font_cell, space_x as i32, line_h, max_w, max_h) {
462 let glyph = rasterize_ab_glyph(font, placed.ch, font_px);
463 if glyph.width == 0 || glyph.height == 0 {
464 continue;
465 }
466
467 let draw_x = placed.x + glyph.xmin;
468 let draw_y = placed.y + baseline_y + glyph.ymin;
469
470 if style.fuchi {
471 for (ox, oy) in [
472 (-1, -1), (0, -1), (1, -1),
473 (-1, 0), (1, 0),
474 (-1, 1), (0, 1), (1, 1),
475 ] {
476 draw_glyph_bitmap(
477 &mut rgba,
478 render_w,
479 render_h,
480 draw_x + ox,
481 draw_y + oy,
482 glyph.width,
483 glyph.height,
484 &glyph.bitmap,
485 (style.fuchi_color.0, style.fuchi_color.1, style.fuchi_color.2, 255),
486 );
487 }
488 }
489 if style.shadow {
490 let shadow_offset = shadow_offset_for_size(font_cell);
491 draw_glyph_bitmap(
492 &mut rgba,
493 render_w,
494 render_h,
495 draw_x + shadow_offset,
496 draw_y + shadow_offset,
497 glyph.width,
498 glyph.height,
499 &glyph.bitmap,
500 (style.shadow_color.0, style.shadow_color.1, style.shadow_color.2, 255),
501 );
502 }
503 draw_glyph_bitmap(
504 &mut rgba,
505 render_w,
506 render_h,
507 draw_x,
508 draw_y,
509 glyph.width,
510 glyph.height,
511 &glyph.bitmap,
512 (style.color.0, style.color.1, style.color.2, 255),
513 );
514 if style.bold {
515 draw_glyph_bitmap(
516 &mut rgba,
517 render_w,
518 render_h,
519 draw_x + 1,
520 draw_y,
521 glyph.width,
522 glyph.height,
523 &glyph.bitmap,
524 (style.color.0, style.color.1, style.color.2, 220),
525 );
526 }
527 }
528
529 Some(RgbaImage {
530 width: render_w,
531 height: render_h,
532 center_x: 0,
533 center_y: 0,
534 rgba,
535 })
536}
537
538#[derive(Debug, Clone, Copy)]
539struct MwndPlacedChar {
540 ch: char,
541 x: i32,
542 y: i32,
543 cell_w: i32,
544}
545
546fn layout_mwnd_text(
547 text: &str,
548 font_cell: i32,
549 space_x: i32,
550 line_h: i32,
551 max_w: u32,
552 max_h: u32,
553) -> Vec<MwndPlacedChar> {
554 let full_cell_w = font_cell.max(1);
555 let half_cell_w = ((font_cell + 1) / 2).max(1);
556 let max_w = max_w as i32;
557 let max_h = max_h as i32;
558 let mut out = Vec::new();
559 let mut x = 0i32;
560 let mut y = 0i32;
561 let mut indent_x = 0i32;
562 let mut line_head = true;
563
564 for ch in text.chars() {
565 match ch {
566 '\r' => continue,
567 '\n' => {
568 x = indent_x;
569 y += line_h;
570 line_head = true;
571 if y >= max_h {
572 break;
573 }
574 continue;
575 }
576 '\u{0007}' => {
577 indent_x = 0;
578 x = 0;
579 y += line_h;
580 line_head = true;
581 if y >= max_h {
582 break;
583 }
584 continue;
585 }
586 '\t' => {
587 x += (full_cell_w + space_x).max(1) * 2;
588 line_head = false;
589 continue;
590 }
591 _ => {}
592 }
593
594 let cell_w = if is_hankaku(ch) { half_cell_w } else { full_cell_w };
595 let check_size = cell_w + space_x;
596 let force_wrap = x > 0 && x + check_size > max_w + full_cell_w;
597 let soft_wrap = x > 0 && x + check_size > max_w && !is_siglus_forbidden_line_head(ch);
598 if force_wrap || soft_wrap {
599 x = indent_x;
600 y += line_h;
601 line_head = true;
602 if y >= max_h {
603 break;
604 }
605 if ch == ' ' || ch == '\u{3000}' {
606 continue;
607 }
608 }
609
610 if line_head {
611 if is_siglus_indent_open(ch) {
612 indent_x = full_cell_w;
613 } else if is_siglus_indent_close(ch) {
614 indent_x = 0;
615 }
616 }
617
618 out.push(MwndPlacedChar { ch, x, y, cell_w });
619 x += (cell_w + space_x).max(1);
620 line_head = false;
621 }
622 out
623}
624
625fn is_siglus_indent_open(ch: char) -> bool {
626 matches!(ch, '「' | '『' | '(')
627}
628
629fn is_siglus_indent_close(ch: char) -> bool {
630 matches!(ch, '」' | '』' | ')')
631}
632
633fn is_siglus_forbidden_line_head(ch: char) -> bool {
634 matches!(
635 ch,
636 '、' | '。' | ',' | '.' | '・' | ':' | ';' | '?' | '!' |
637 '」' | '』' | ')' | ']' | '}' | '〉' | '》' | '】' | '〕' |
638 'ぁ' | 'ぃ' | 'ぅ' | 'ぇ' | 'ぉ' | 'っ' | 'ゃ' | 'ゅ' | 'ょ' | 'ゎ' |
639 'ァ' | 'ィ' | 'ゥ' | 'ェ' | 'ォ' | 'ッ' | 'ャ' | 'ュ' | 'ョ' | 'ヮ' |
640 'ー' | 'ー' | '~' | '…' | '‥'
641 )
642}
643
644fn shadow_offset_for_size(size: i32) -> i32 {
645 if size <= 0 {
646 return 1;
647 }
648 let t = (size as f32 / 32.0).clamp(0.0, 1.0);
650 (0.5 + (2.0 - 0.5) * t).round().max(1.0) as i32
651}
652
653fn text_effect_padding(size: i32, style: TextStyle) -> i32 {
654 let mut pad = 1;
655 if style.fuchi {
656 pad = pad.max(1);
657 }
658 if style.shadow {
659 pad = pad.max(shadow_offset_for_size(size));
660 }
661 pad + 1
662}
663
664fn is_hankaku(ch: char) -> bool {
665 ch.is_ascii() || matches!(ch as u32, 0xFF61..=0xFF9F)
666}
667
668fn draw_glyph_bitmap(
669 rgba: &mut [u8],
670 w: u32,
671 h: u32,
672 x: i32,
673 y: i32,
674 glyph_w: usize,
675 glyph_h: usize,
676 glyph: &[u8],
677 color: (u8, u8, u8, u8),
678) {
679 for gy in 0..glyph_h {
680 let py = y + gy as i32;
681 if py < 0 || py as u32 >= h {
682 continue;
683 }
684 for gx in 0..glyph_w {
685 let px = x + gx as i32;
686 if px < 0 || px as u32 >= w {
687 continue;
688 }
689 let src = glyph[gy * glyph_w + gx];
690 if src == 0 {
691 continue;
692 }
693 let src_a = ((src as u16 * color.3 as u16) / 255) as u8;
694 blend_rgba_pixel(rgba, w, px as u32, py as u32, color.0, color.1, color.2, src_a);
695 }
696 }
697}
698
699fn blend_rgba_pixel(
700 rgba: &mut [u8],
701 w: u32,
702 x: u32,
703 y: u32,
704 sr: u8,
705 sg: u8,
706 sb: u8,
707 sa: u8,
708) {
709 let idx = ((y * w + x) * 4) as usize;
710 let da = rgba[idx + 3] as u16;
711 let sa_u = sa as u16;
712 let inv_sa = 255u16.saturating_sub(sa_u);
713 let out_a = sa_u + (da * inv_sa + 127) / 255;
714 if out_a == 0 {
715 rgba[idx] = 0;
716 rgba[idx + 1] = 0;
717 rgba[idx + 2] = 0;
718 rgba[idx + 3] = 0;
719 return;
720 }
721 let blend = |src: u8, dst: u8| -> u8 {
722 let src_p = src as u16 * sa_u;
723 let dst_p = dst as u16 * da * inv_sa / 255;
724 ((src_p + dst_p + out_a / 2) / out_a).min(255) as u8
725 };
726 rgba[idx] = blend(sr, rgba[idx]);
727 rgba[idx + 1] = blend(sg, rgba[idx + 1]);
728 rgba[idx + 2] = blend(sb, rgba[idx + 2]);
729 rgba[idx + 3] = out_a.min(255) as u8;
730}
731
732fn render_text_ab_glyph_rgba(
733 font: &FontArc,
734 text: &str,
735 font_px: f32,
736 max_w: u32,
737 max_h: u32,
738) -> Option<RgbaImage> {
739 if text.is_empty() || max_w == 0 || max_h == 0 {
740 return None;
741 }
742 let mut rgba = vec![0u8; (max_w * max_h * 4) as usize];
743
744 let scaled = font.as_scaled(PxScale::from(font_px.max(1.0)));
745 let ascent = scaled.ascent().max(1.0);
746 let line_height = (scaled.height() + scaled.line_gap()).max(1.0);
747
748 let mut x = 0.0f32;
749 let mut baseline_y = ascent.max(1.0);
750
751 for ch in text.chars() {
752 match ch {
753 '\r' => continue,
754 '\n' => {
755 x = 0.0;
756 baseline_y += line_height;
757 if baseline_y - ascent >= max_h as f32 {
758 break;
759 }
760 continue;
761 }
762 '\t' => {
763 x += scaled.h_advance(scaled.glyph_id(' ')).max(0.0) * 2.0;
764 continue;
765 }
766 _ => {}
767 }
768
769 let advance = scaled.h_advance(scaled.glyph_id(ch)).max(0.0);
770 if x > 0.0 && x + advance > max_w as f32 {
771 x = 0.0;
772 baseline_y += line_height;
773 if baseline_y - ascent >= max_h as f32 {
774 break;
775 }
776 }
777
778 let glyph = rasterize_ab_glyph(font, ch, font_px);
779 let gx = x + glyph.xmin as f32;
780 let gy = baseline_y + glyph.ymin as f32;
781 for gy_i in 0..glyph.height {
782 let py = gy as i32 + gy_i as i32;
783 if py < 0 || py as u32 >= max_h {
784 continue;
785 }
786 for gx_i in 0..glyph.width {
787 let px = gx as i32 + gx_i as i32;
788 if px < 0 || px as u32 >= max_w {
789 continue;
790 }
791 let src = glyph.bitmap[gy_i * glyph.width + gx_i];
792 if src == 0 {
793 continue;
794 }
795 let idx = ((py as u32 * max_w + px as u32) * 4) as usize;
796 rgba[idx] = 255;
797 rgba[idx + 1] = 255;
798 rgba[idx + 2] = 255;
799 rgba[idx + 3] = src;
800 }
801 }
802 x += advance;
803 }
804
805 Some(RgbaImage {
806 width: max_w,
807 height: max_h,
808 center_x: 0,
809 center_y: 0,
810 rgba,
811 })
812}
813
814pub fn embedded_default_font_available() -> bool {
815 embedded_font::EMBEDDED_DEFAULT_FONT.is_some()
816}
817
818pub fn embedded_default_font_names() -> &'static [&'static str] {
819 if embedded_default_font_available() {
820 embedded_font::EMBEDDED_DEFAULT_FONT_ALIASES
821 } else {
822 &[]
823 }
824}
825
826pub fn font_name_matches_embedded_default(name: &str) -> bool {
827 if !embedded_default_font_available() {
828 return false;
829 }
830 let needle = normalize_font_name_for_match(name);
831 if needle.is_empty() {
832 return false;
833 }
834 embedded_font::EMBEDDED_DEFAULT_FONT_ALIASES
835 .iter()
836 .any(|alias| normalize_font_name_for_match(alias) == needle)
837}
838
839fn normalize_font_name_for_match(name: &str) -> String {
840 name.chars()
841 .filter(|ch| !ch.is_whitespace() && *ch != '-' && *ch != '_' && *ch != '.')
842 .flat_map(|ch| ch.to_lowercase())
843 .collect()
844}
845
846fn is_supported_font_path(path: &Path) -> bool {
847 matches!(
848 path.extension()
849 .and_then(|s| s.to_str())
850 .map(|s| s.to_ascii_lowercase())
851 .as_deref(),
852 Some("ttf" | "otf" | "ttc")
853 )
854}
855
856fn font_path_priority(path: &Path) -> (u8, u8, String) {
857 let name_original = path.file_name().and_then(|s| s.to_str()).unwrap_or("");
858 let name = name_original.to_ascii_lowercase();
859 let ext_score = match path
860 .extension()
861 .and_then(|s| s.to_str())
862 .map(|s| s.to_ascii_lowercase())
863 .as_deref()
864 {
865 Some("ttf") | Some("otf") => 0,
866 Some("ttc") => 1,
867 _ => 2,
868 };
869 let family_score = if name.contains("ms pgothic")
870 || name.contains("mspgothic")
871 || name.contains("ms-pgothic")
872 || name.contains("msgothic")
873 || name_original.contains("MS Pゴシック")
874 || name_original.contains("MS PGothic")
875 {
876 0
877 } else if name.contains("pgothic") || name_original.contains("Pゴシック") {
878 1
879 } else if name.contains("gothic") || name_original.contains("ゴシック") {
880 2
881 } else {
882 3
883 };
884 (family_score, ext_score, name)
885}
886
887fn platform_font_candidates() -> Vec<PathBuf> {
888 let mut out = Vec::new();
889
890 #[cfg(target_os = "windows")]
891 {
892 let windir = std::env::var_os("WINDIR")
893 .map(PathBuf::from)
894 .unwrap_or_else(|| PathBuf::from(r"C:\Windows"));
895 let fonts = windir.join("Fonts");
896 out.push(fonts.join("msgothic.ttc"));
897 out.push(fonts.join("msgothic.ttf"));
898 out.push(fonts.join("YuGothM.ttc"));
899 out.push(fonts.join("YuGothR.ttc"));
900 }
901
902 #[cfg(target_os = "macos")]
903 {
904 out.push(PathBuf::from("/System/Library/Fonts/ヒラギノ角ゴシック W3.ttc"));
905 out.push(PathBuf::from("/System/Library/Fonts/ヒラギノ角ゴシック W4.ttc"));
906 out.push(PathBuf::from("/System/Library/Fonts/Supplemental/Arial Unicode.ttf"));
907 out.push(PathBuf::from("/System/Library/Fonts/Supplemental/Osaka.ttf"));
908 }
909
910 #[cfg(any(target_os = "linux", target_os = "freebsd", target_os = "android"))]
911 {
912 out.push(PathBuf::from("/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc"));
913 out.push(PathBuf::from("/usr/share/fonts/truetype/noto/NotoSansCJK-Regular.ttc"));
914 out.push(PathBuf::from("/usr/share/fonts/opentype/noto/NotoSansCJKjp-Regular.otf"));
915 out.push(PathBuf::from("/usr/share/fonts/truetype/fonts-japanese-gothic.ttf"));
916 }
917
918 out
919}
920
921fn draw_glyph_5x7(rgba: &mut [u8], w: u32, h: u32, x: u32, y: u32, ch: char, scale: u32) {
922 let glyph = glyph_5x7(ch);
923 for (row, bits) in glyph.iter().enumerate() {
924 for col in 0..5 {
925 if (bits >> (4 - col)) & 1 == 0 {
926 continue;
927 }
928 let px = x + col as u32 * scale;
929 let py = y + row as u32 * scale;
930 for sy in 0..scale {
931 let yy = py + sy;
932 if yy >= h {
933 continue;
934 }
935 for sx in 0..scale {
936 let xx = px + sx;
937 if xx >= w {
938 continue;
939 }
940 let idx = ((yy * w + xx) * 4) as usize;
941 rgba[idx] = 255;
942 rgba[idx + 1] = 255;
943 rgba[idx + 2] = 255;
944 rgba[idx + 3] = 255;
945 }
946 }
947 }
948 }
949}
950
951fn glyph_5x7(ch: char) -> [u8; 7] {
952 match ch.to_ascii_uppercase() {
953 'A' => [0x0E, 0x11, 0x11, 0x1F, 0x11, 0x11, 0x11],
954 'B' => [0x1E, 0x11, 0x11, 0x1E, 0x11, 0x11, 0x1E],
955 'C' => [0x0E, 0x11, 0x10, 0x10, 0x10, 0x11, 0x0E],
956 'D' => [0x1E, 0x11, 0x11, 0x11, 0x11, 0x11, 0x1E],
957 'E' => [0x1F, 0x10, 0x10, 0x1E, 0x10, 0x10, 0x1F],
958 'F' => [0x1F, 0x10, 0x10, 0x1E, 0x10, 0x10, 0x10],
959 'G' => [0x0E, 0x11, 0x10, 0x17, 0x11, 0x11, 0x0E],
960 'H' => [0x11, 0x11, 0x11, 0x1F, 0x11, 0x11, 0x11],
961 'I' => [0x0E, 0x04, 0x04, 0x04, 0x04, 0x04, 0x0E],
962 'J' => [0x01, 0x01, 0x01, 0x01, 0x11, 0x11, 0x0E],
963 'K' => [0x11, 0x12, 0x14, 0x18, 0x14, 0x12, 0x11],
964 'L' => [0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x1F],
965 'M' => [0x11, 0x1B, 0x15, 0x15, 0x11, 0x11, 0x11],
966 'N' => [0x11, 0x19, 0x15, 0x13, 0x11, 0x11, 0x11],
967 'O' => [0x0E, 0x11, 0x11, 0x11, 0x11, 0x11, 0x0E],
968 'P' => [0x1E, 0x11, 0x11, 0x1E, 0x10, 0x10, 0x10],
969 'Q' => [0x0E, 0x11, 0x11, 0x11, 0x15, 0x12, 0x0D],
970 'R' => [0x1E, 0x11, 0x11, 0x1E, 0x14, 0x12, 0x11],
971 'S' => [0x0F, 0x10, 0x10, 0x0E, 0x01, 0x01, 0x1E],
972 'T' => [0x1F, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04],
973 'U' => [0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x0E],
974 'V' => [0x11, 0x11, 0x11, 0x11, 0x11, 0x0A, 0x04],
975 'W' => [0x11, 0x11, 0x11, 0x15, 0x15, 0x15, 0x0A],
976 'X' => [0x11, 0x11, 0x0A, 0x04, 0x0A, 0x11, 0x11],
977 'Y' => [0x11, 0x11, 0x0A, 0x04, 0x04, 0x04, 0x04],
978 'Z' => [0x1F, 0x01, 0x02, 0x04, 0x08, 0x10, 0x1F],
979 '0' => [0x0E, 0x11, 0x13, 0x15, 0x19, 0x11, 0x0E],
980 '1' => [0x04, 0x0C, 0x04, 0x04, 0x04, 0x04, 0x0E],
981 '2' => [0x0E, 0x11, 0x01, 0x02, 0x04, 0x08, 0x1F],
982 '3' => [0x1E, 0x01, 0x01, 0x06, 0x01, 0x01, 0x1E],
983 '4' => [0x02, 0x06, 0x0A, 0x12, 0x1F, 0x02, 0x02],
984 '5' => [0x1F, 0x10, 0x10, 0x1E, 0x01, 0x01, 0x1E],
985 '6' => [0x0E, 0x10, 0x10, 0x1E, 0x11, 0x11, 0x0E],
986 '7' => [0x1F, 0x01, 0x02, 0x04, 0x08, 0x08, 0x08],
987 '8' => [0x0E, 0x11, 0x11, 0x0E, 0x11, 0x11, 0x0E],
988 '9' => [0x0E, 0x11, 0x11, 0x0F, 0x01, 0x01, 0x0E],
989 ':' => [0x00, 0x04, 0x04, 0x00, 0x04, 0x04, 0x00],
990 '.' => [0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x06],
991 ',' => [0x00, 0x00, 0x00, 0x00, 0x06, 0x06, 0x04],
992 '-' => [0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x00],
993 '_' => [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F],
994 '/' => [0x01, 0x02, 0x04, 0x08, 0x10, 0x00, 0x00],
995 '\\' => [0x10, 0x08, 0x04, 0x02, 0x01, 0x00, 0x00],
996 '[' => [0x0E, 0x08, 0x08, 0x08, 0x08, 0x08, 0x0E],
997 ']' => [0x0E, 0x02, 0x02, 0x02, 0x02, 0x02, 0x0E],
998 '(' => [0x02, 0x04, 0x08, 0x08, 0x08, 0x04, 0x02],
999 ')' => [0x08, 0x04, 0x02, 0x02, 0x02, 0x04, 0x08],
1000 '#' => [0x0A, 0x0A, 0x1F, 0x0A, 0x1F, 0x0A, 0x0A],
1001 '+' => [0x00, 0x04, 0x04, 0x1F, 0x04, 0x04, 0x00],
1002 '=' => [0x00, 0x1F, 0x00, 0x1F, 0x00, 0x00, 0x00],
1003 '*' => [0x00, 0x11, 0x0A, 0x1F, 0x0A, 0x11, 0x00],
1004 '?' => [0x0E, 0x11, 0x01, 0x02, 0x04, 0x00, 0x04],
1005 '!' => [0x04, 0x04, 0x04, 0x04, 0x04, 0x00, 0x04],
1006 '>' => [0x10, 0x08, 0x04, 0x02, 0x04, 0x08, 0x10],
1007 '<' => [0x01, 0x02, 0x04, 0x08, 0x04, 0x02, 0x01],
1008 '|' => [0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04],
1009 ' ' => [0x00; 7],
1010 _ => [0x1F, 0x11, 0x15, 0x15, 0x15, 0x11, 0x1F],
1011 }
1012}