1use crate::assets::RgbaImage;
9use crate::image_manager::{ImageHandle, ImageManager};
10use ab_glyph::{Font, FontArc, FontRef, PxScale, ScaleFont, point};
11use std::collections::HashSet;
12use std::path::{Path, PathBuf};
13use std::sync::{Mutex, OnceLock};
14
15static MISSING_CONFIGURED_FONTS_LOGGED: OnceLock<Mutex<HashSet<String>>> = OnceLock::new();
16
17pub(crate) static DEFAULT_FONT_BYTES: &[u8] = include_bytes!("../assets/fonts/default.ttf");
19
20fn log_missing_configured_font_once(requested_name: &str, fallback: Option<&Path>) {
21 let key = normalize_font_name_for_match(requested_name.trim_start_matches('@'));
22 if key.is_empty() {
23 return;
24 }
25
26 let seen = MISSING_CONFIGURED_FONTS_LOGGED.get_or_init(|| Mutex::new(HashSet::new()));
27 let first = match seen.lock() {
28 Ok(mut seen) => seen.insert(key),
29 Err(poisoned) => poisoned.into_inner().insert(key),
30 };
31 if !first {
32 return;
33 }
34
35 match fallback {
36 Some(path) => log::error!(
37 "configured font {:?} was not found; using fallback {:?}",
38 requested_name,
39 path
40 ),
41 None => log::error!(
42 "configured font {:?} was not found; using embedded default font",
43 requested_name
44 ),
45 }
46}
47
48mod embedded_font {
49 pub const EMBEDDED_DEFAULT_FONT: Option<&'static [u8]> = Some(super::DEFAULT_FONT_BYTES);
50 pub const EMBEDDED_DEFAULT_FONT_SOURCE: Option<&'static str> = Some("assets/fonts/default.ttf");
51 pub const EMBEDDED_DEFAULT_FONT_ALIASES: &[&str] = &[
52 "MS Pゴシック",
53 "MS PGothic",
54 "MS-PGothic",
55 "MSPGothic",
56 "msgothic",
57 "default",
58 ];
59}
60
61pub const TNM_FONT_SHADOW_MODE_NONE: i64 = 0;
62pub const TNM_FONT_SHADOW_MODE_SHADOW: i64 = 1;
63pub const TNM_FONT_SHADOW_MODE_FUCHI: i64 = 2;
64pub const TNM_FONT_SHADOW_MODE_FUCHI_SHADOW: i64 = 3;
65
66pub fn normalize_font_shadow_mode(mode: i64) -> i64 {
67 mode.clamp(TNM_FONT_SHADOW_MODE_NONE, TNM_FONT_SHADOW_MODE_FUCHI_SHADOW)
68}
69
70pub fn font_shadow_mode_flags(mode: i64) -> (bool, bool) {
71 match normalize_font_shadow_mode(mode) {
72 TNM_FONT_SHADOW_MODE_SHADOW => (true, false),
73 TNM_FONT_SHADOW_MODE_FUCHI => (false, true),
74 TNM_FONT_SHADOW_MODE_FUCHI_SHADOW => (true, true),
75 _ => (false, false),
76 }
77}
78
79#[derive(Debug, Clone, Copy)]
80pub struct TextStyle {
81 pub color: (u8, u8, u8),
82 pub shadow_color: (u8, u8, u8),
83 pub fuchi_color: (u8, u8, u8),
84 pub shadow_mode: i64,
86 pub shadow: bool,
87 pub fuchi: bool,
88 pub bold: bool,
89}
90
91#[derive(Debug, Clone, Copy, PartialEq, Eq)]
92pub enum TextSpriteLayer {
93 Shadow,
94 Fuchi,
95 Body,
96}
97
98#[derive(Debug, Clone, Copy)]
99pub struct PositionedTextGlyph {
100 pub ch: char,
101 pub x: i32,
102 pub y: i32,
103 pub size: f32,
104 pub vertical: bool,
109 pub style: TextStyle,
110}
111
112#[derive(Debug, Clone)]
113pub struct PositionedTextRender {
114 pub image: ImageHandle,
115 pub offset_x: i32,
116 pub offset_y: i32,
117}
118
119impl Default for TextStyle {
120 fn default() -> Self {
121 Self {
122 color: (255, 255, 255),
123 shadow_color: (0, 0, 0),
124 fuchi_color: (0, 0, 0),
125 shadow_mode: TNM_FONT_SHADOW_MODE_SHADOW,
126 shadow: true,
127 fuchi: false,
128 bold: false,
129 }
130 }
131}
132
133#[derive(Debug, Default)]
134pub struct FontCache {
135 font: Option<FontArc>,
136 loaded_from: Option<PathBuf>,
137 loaded_face_index: u32,
138 requested_name: String,
145 resolved: std::collections::HashMap<String, (FontArc, Option<PathBuf>, u32)>,
149}
150
151impl FontCache {
152 pub fn new() -> Self {
153 Self {
154 font: None,
155 loaded_from: None,
156 loaded_face_index: 0,
157 requested_name: String::new(),
158 resolved: std::collections::HashMap::new(),
159 }
160 }
161
162 pub fn is_loaded(&self) -> bool {
163 self.font.is_some()
164 }
165
166 pub fn loaded_from(&self) -> Option<&Path> {
167 self.loaded_from.as_deref()
168 }
169
170 pub fn loaded_face_index(&self) -> u32 {
171 self.loaded_face_index
172 }
173
174 pub fn load_for_project(&mut self, project_dir: &Path) -> bool {
175 self.load_for_project_named(project_dir, "")
176 }
177
178 pub fn load_for_project_named(&mut self, project_dir: &Path, requested_name: &str) -> bool {
185 let normalized = normalize_font_name_for_match(requested_name.trim_start_matches('@'));
186 if self.font.is_some() && self.requested_name == normalized {
187 return true;
188 }
189
190 if let Some((font, path, face)) = self.resolved.get(&normalized) {
191 self.font = Some(font.clone());
192 self.loaded_from = path.clone();
193 self.loaded_face_index = *face;
194 self.requested_name = normalized;
195 return true;
196 }
197 let found = self.resolve_for_project_named(project_dir, requested_name, &normalized);
198 if found && let Some(font) = &self.font {
199 self.resolved.insert(
200 normalized,
201 (
202 font.clone(),
203 self.loaded_from.clone(),
204 self.loaded_face_index,
205 ),
206 );
207 }
208 found
209 }
210
211 fn resolve_for_project_named(
212 &mut self,
213 project_dir: &Path,
214 requested_name: &str,
215 normalized: &str,
216 ) -> bool {
217 let normalized = normalized.to_owned();
218 self.font = None;
219 self.loaded_from = None;
220 self.loaded_face_index = 0;
221 self.requested_name = normalized.clone();
222
223 let dirs = project_font_dirs(project_dir);
224
225 if !normalized.is_empty() {
229 for dir in &dirs {
230 if self.load_named_from_font_dir(dir, &normalized) {
231 return true;
232 }
233 }
234 if font_name_matches_embedded_default(requested_name.trim_start_matches('@'))
235 && self.try_load_embedded_default_font()
236 {
237 return true;
238 }
239 for path in platform_font_candidates_for_name(requested_name) {
240 if self.try_load_font_file_named(&path, &normalized) {
241 return true;
242 }
243 }
244 }
245
246 for dir in dirs {
251 if self.load_from_font_dir(&dir) {
252 if !normalized.is_empty() {
253 log_missing_configured_font_once(requested_name, self.loaded_from());
254 }
255 return true;
256 }
257 }
258
259 if self.try_load_embedded_default_font() {
260 if !normalized.is_empty() {
261 log_missing_configured_font_once(requested_name, None);
262 }
263 return true;
264 }
265
266 for path in platform_font_candidates() {
267 if self.try_load_font_file(&path) {
268 return true;
269 }
270 }
271
272 false
273 }
274
275 pub fn requested_name(&self) -> &str {
276 &self.requested_name
277 }
278
279 fn load_named_from_font_dir(&mut self, font_dir: &Path, normalized_name: &str) -> bool {
280 let Some(font_dir) = crate::resource::resolve_game_path(font_dir).ok().flatten() else {
281 return false;
282 };
283 let Ok(entries) = std::fs::read_dir(&font_dir) else {
284 return false;
285 };
286 let mut exact_file = Vec::new();
287 let mut other = Vec::new();
288 for entry in entries.flatten() {
289 let path = entry.path();
290 if !path.is_file() || !is_supported_font_path(&path) {
291 continue;
292 }
293 let file_name = path.file_name().and_then(|s| s.to_str()).unwrap_or("");
294 let stem = path.file_stem().and_then(|s| s.to_str()).unwrap_or("");
295 let file_key = normalize_font_name_for_match(file_name.trim_start_matches('@'));
296 let stem_key = normalize_font_name_for_match(stem.trim_start_matches('@'));
297 if file_key == normalized_name || stem_key == normalized_name {
298 exact_file.push(path);
299 } else {
300 other.push(path);
301 }
302 }
303 exact_file.sort_by_key(|path| font_path_priority(path));
304 other.sort_by_key(|path| font_path_priority(path));
305
306 exact_file
310 .into_iter()
311 .chain(other)
312 .any(|path| self.try_load_font_file_named(&path, normalized_name))
313 }
314
315 pub fn load_from_font_dir(&mut self, font_dir: &Path) -> bool {
316 if self.font.is_some() {
317 return true;
318 }
319 let Some(font_dir) = crate::resource::resolve_game_path(font_dir).ok().flatten() else {
320 return false;
321 };
322 let Ok(entries) = std::fs::read_dir(&font_dir) else {
323 return false;
324 };
325
326 let mut files = Vec::new();
327 for entry in entries.flatten() {
328 let path = entry.path();
329 if path.is_file() && is_supported_font_path(&path) {
330 files.push(path);
331 }
332 }
333 files.sort_by_key(|path| font_path_priority(path));
334
335 for path in files {
336 if self.try_load_font_file(&path) {
337 return true;
338 }
339 }
340 false
341 }
342
343 fn install_font_face(&mut self, path: &Path, data: &'static [u8], face_index: u32) -> bool {
344 match FontRef::try_from_slice_and_index(data, face_index) {
345 Ok(font) => {
346 self.font = Some(FontArc::from(font));
347 self.loaded_from = Some(path.to_path_buf());
348 self.loaded_face_index = face_index;
349 true
350 }
351 Err(_) => false,
352 }
353 }
354
355 fn try_load_font_file_named(&mut self, path: &Path, normalized_name: &str) -> bool {
356 if self.font.is_some() {
357 return true;
358 }
359 if !crate::resource::game_file_exists(path) || !is_supported_font_path(path) {
360 return false;
361 }
362 let Some((data, face_index)) = font_file_face(path, normalized_name) else {
363 return false;
364 };
365 self.install_font_face(path, data, face_index)
366 }
367
368 fn try_load_font_file(&mut self, path: &Path) -> bool {
369 if self.font.is_some() {
370 return true;
371 }
372 if !crate::resource::game_file_exists(path) || !is_supported_font_path(path) {
373 return false;
374 }
375 let Some((data, face_index)) = font_file_face(path, "") else {
376 return false;
377 };
378 self.install_font_face(path, data, face_index)
379 }
380
381 fn try_load_embedded_default_font(&mut self) -> bool {
382 if self.font.is_some() {
383 return true;
384 }
385 let Some(bytes) = embedded_font::EMBEDDED_DEFAULT_FONT else {
386 return false;
387 };
388 match FontRef::try_from_slice(bytes) {
391 Ok(font) => {
392 self.font = Some(FontArc::from(font));
393 let source =
394 embedded_font::EMBEDDED_DEFAULT_FONT_SOURCE.unwrap_or("embedded:default-font");
395 self.loaded_from = Some(PathBuf::from(source));
396 self.loaded_face_index = 0;
397 true
398 }
399 Err(_) => false,
400 }
401 }
402
403 pub fn render_text(
404 &self,
405 images: &mut ImageManager,
406 text: &str,
407 font_px: f32,
408 max_w: u32,
409 max_h: u32,
410 ) -> Option<ImageHandle> {
411 self.render_text_into(images, None, text, font_px, max_w, max_h)
412 }
413
414 pub fn render_mwnd_text(
415 &self,
416 images: &mut ImageManager,
417 text: &str,
418 font_px: f32,
419 max_w: u32,
420 max_h: u32,
421 moji_space: Option<(i64, i64)>,
422 ) -> Option<ImageHandle> {
423 let img = self.render_mwnd_text_rgba(text, font_px, max_w, max_h, moji_space)?;
424 Some(images.insert_image(img))
425 }
426
427 pub fn render_mwnd_text_styled(
428 &self,
429 images: &mut ImageManager,
430 text: &str,
431 font_px: f32,
432 max_w: u32,
433 max_h: u32,
434 moji_space: Option<(i64, i64)>,
435 style: TextStyle,
436 ) -> Option<ImageHandle> {
437 self.render_mwnd_text_styled_into(
438 images, None, text, font_px, max_w, max_h, moji_space, style,
439 )
440 }
441
442 pub fn render_mwnd_text_styled_into(
443 &self,
444 images: &mut ImageManager,
445 target: Option<ImageHandle>,
446 text: &str,
447 font_px: f32,
448 max_w: u32,
449 max_h: u32,
450 moji_space: Option<(i64, i64)>,
451 style: TextStyle,
452 ) -> Option<ImageHandle> {
453 let img =
454 self.render_mwnd_text_rgba_styled(text, font_px, max_w, max_h, moji_space, style)?;
455 match target {
456 Some(id) => {
457 images.replace_image(&id, img).ok()?;
458 Some(id)
459 }
460 None => Some(images.insert_image(img)),
461 }
462 }
463
464 pub fn render_positioned_glyphs_into(
465 &self,
466 images: &mut ImageManager,
467 target: Option<ImageHandle>,
468 glyphs: &[PositionedTextGlyph],
469 min_w: u32,
470 min_h: u32,
471 ) -> Option<PositionedTextRender> {
472 self.render_positioned_glyph_layer_into(images, target, glyphs, min_w, min_h, None)
473 }
474
475 pub fn render_positioned_glyph_layer_into(
476 &self,
477 images: &mut ImageManager,
478 target: Option<ImageHandle>,
479 glyphs: &[PositionedTextGlyph],
480 min_w: u32,
481 min_h: u32,
482 layer: Option<TextSpriteLayer>,
483 ) -> Option<PositionedTextRender> {
484 if glyphs.is_empty() || min_w == 0 || min_h == 0 {
485 return None;
486 }
487 let (img, offset_x, offset_y) =
488 render_positioned_glyphs_rgba(self.font.as_ref(), glyphs, min_w, min_h, layer)?;
489 let image = match target {
490 Some(id) => {
491 images.replace_image(&id, img).ok()?;
492 id
493 }
494 None => images.insert_image(img),
495 };
496 Some(PositionedTextRender {
497 image,
498 offset_x,
499 offset_y,
500 })
501 }
502
503 pub fn render_single_glyph_layer_into(
508 &self,
509 images: &mut ImageManager,
510 target: Option<ImageHandle>,
511 glyph: PositionedTextGlyph,
512 layer: TextSpriteLayer,
513 ) -> Option<PositionedTextRender> {
514 let mut local = glyph;
515 local.x = 0;
516 local.y = 0;
517 self.render_positioned_glyph_layer_into(images, target, &[local], 1, 1, Some(layer))
518 }
519
520 pub fn render_text_into(
521 &self,
522 images: &mut ImageManager,
523 target: Option<ImageHandle>,
524 text: &str,
525 font_px: f32,
526 max_w: u32,
527 max_h: u32,
528 ) -> Option<ImageHandle> {
529 let img = self.render_text_rgba(text, font_px, max_w, max_h)?;
530 match target {
531 Some(id) => {
532 images.replace_image(&id, img).ok()?;
533 Some(id)
534 }
535 None => Some(images.insert_image(img)),
536 }
537 }
538
539 #[allow(clippy::too_many_arguments)]
540 pub fn render_editbox_into(
541 &self,
542 images: &mut ImageManager,
543 target: Option<ImageHandle>,
544 text: &str,
545 cursor_pos: usize,
546 selection: Option<(usize, usize)>,
547 composition_text: &str,
548 composition_cursor: Option<(usize, usize)>,
549 composition_range: Option<(usize, usize)>,
550 scroll_x_px: i32,
551 caret_visible: bool,
552 font_px: f32,
553 max_w: u32,
554 max_h: u32,
555 ) -> Option<ImageHandle> {
556 let img = render_editbox_rgba(
557 self.font.as_ref(),
558 text,
559 cursor_pos,
560 selection,
561 composition_text,
562 composition_cursor,
563 composition_range,
564 scroll_x_px,
565 caret_visible,
566 font_px,
567 max_w,
568 max_h,
569 )?;
570 match target {
571 Some(id) => {
572 images.replace_image(&id, img).ok()?;
573 Some(id)
574 }
575 None => Some(images.insert_image(img)),
576 }
577 }
578
579 pub fn render_text_rgba(
580 &self,
581 text: &str,
582 font_px: f32,
583 max_w: u32,
584 max_h: u32,
585 ) -> Option<RgbaImage> {
586 let Some(font) = self.font.as_ref() else {
587 return render_text_image_basic_rgba(text, font_px as u32, max_w, max_h);
588 };
589 render_text_ab_glyph_rgba(font, text, font_px, max_w, max_h)
590 }
591
592 pub fn text_line_metrics(
600 &self,
601 text: &str,
602 font_px: f32,
603 max_w: u32,
604 max_h: u32,
605 ) -> (usize, f32) {
606 match self.font.as_ref() {
607 Some(font) => text_line_metrics_ab_glyph(font, text, font_px, max_w, max_h),
608 None => (
610 text.lines().count().max(1),
611 (font_px.max(1.0) * 1.3).max(1.0),
612 ),
613 }
614 }
615
616 pub fn render_mwnd_text_rgba(
617 &self,
618 text: &str,
619 font_px: f32,
620 max_w: u32,
621 max_h: u32,
622 moji_space: Option<(i64, i64)>,
623 ) -> Option<RgbaImage> {
624 self.render_mwnd_text_rgba_styled(
625 text,
626 font_px,
627 max_w,
628 max_h,
629 moji_space,
630 TextStyle::default(),
631 )
632 }
633
634 pub fn render_mwnd_text_rgba_styled(
635 &self,
636 text: &str,
637 font_px: f32,
638 max_w: u32,
639 max_h: u32,
640 moji_space: Option<(i64, i64)>,
641 style: TextStyle,
642 ) -> Option<RgbaImage> {
643 self.render_mwnd_text_rgba_layer_styled(
644 text, font_px, max_w, max_h, moji_space, style, false, None,
645 )
646 }
647
648 pub fn render_mwnd_text_layer_styled_into(
649 &self,
650 images: &mut ImageManager,
651 target: Option<ImageHandle>,
652 text: &str,
653 font_px: f32,
654 max_w: u32,
655 max_h: u32,
656 moji_space: Option<(i64, i64)>,
657 style: TextStyle,
658 vertical: bool,
659 layer: TextSpriteLayer,
660 ) -> Option<ImageHandle> {
661 let img = self.render_mwnd_text_rgba_layer_styled(
662 text,
663 font_px,
664 max_w,
665 max_h,
666 moji_space,
667 style,
668 vertical,
669 Some(layer),
670 )?;
671 match target {
672 Some(id) => {
673 images.replace_image(&id, img).ok()?;
674 Some(id)
675 }
676 None => Some(images.insert_image(img)),
677 }
678 }
679
680 fn render_mwnd_text_rgba_layer_styled(
681 &self,
682 text: &str,
683 font_px: f32,
684 max_w: u32,
685 max_h: u32,
686 moji_space: Option<(i64, i64)>,
687 style: TextStyle,
688 vertical: bool,
689 layer: Option<TextSpriteLayer>,
690 ) -> Option<RgbaImage> {
691 let Some(font) = self.font.as_ref() else {
692 if layer.is_none() || layer == Some(TextSpriteLayer::Body) {
693 return render_text_image_basic_rgba(text, font_px as u32, max_w, max_h);
694 }
695 return Some(RgbaImage {
696 width: max_w.max(1),
697 height: max_h.max(1),
698 center_x: 0,
699 center_y: 0,
700 rgba: vec![0; max_w.max(1) as usize * max_h.max(1) as usize * 4],
701 });
702 };
703 render_mwnd_text_ab_glyph_rgba_styled(
704 font, text, font_px, max_w, max_h, moji_space, style, layer, vertical,
705 )
706 }
707}
708
709pub fn editbox_cell_width_px(ch: char, font_px: i32) -> i32 {
710 let full = font_px.max(1);
711 if is_hankaku(ch) {
712 ((full + 1) / 2).max(1)
713 } else {
714 full
715 }
716}
717
718#[allow(clippy::too_many_arguments)]
719fn render_editbox_rgba(
720 font: Option<&FontArc>,
721 text: &str,
722 cursor_pos: usize,
723 selection: Option<(usize, usize)>,
724 composition_text: &str,
725 composition_cursor: Option<(usize, usize)>,
726 composition_range: Option<(usize, usize)>,
727 scroll_x_px: i32,
728 caret_visible: bool,
729 font_px: f32,
730 max_w: u32,
731 max_h: u32,
732) -> Option<RgbaImage> {
733 if max_w == 0 || max_h == 0 {
734 return None;
735 }
736
737 const PAD_X: i32 = 3;
738 let mut rgba = vec![255u8; (max_w * max_h * 4) as usize];
739 let font_cell = font_px.round().max(1.0) as i32;
740 let (baseline_y, glyph_top, glyph_bottom) = if let Some(font) = font {
741 let scaled = font.as_scaled(PxScale::from(font_px.max(1.0)));
742 let line_h = (scaled.height() + scaled.line_gap()).ceil().max(1.0) as i32;
743 let top = ((max_h as i32 - line_h) / 2).max(0);
744 let baseline = top + scaled.ascent().ceil() as i32;
745 (baseline, top, (top + line_h).min(max_h as i32))
746 } else {
747 let glyph_h = (font_cell * 7 / 8).max(7);
748 let top = ((max_h as i32 - glyph_h) / 2).max(0);
749 (top + glyph_h, top, (top + glyph_h).min(max_h as i32))
750 };
751
752 let normalize = |value: usize, source: &str| {
753 let mut value = value.min(source.len());
754 while value > 0 && !source.is_char_boundary(value) {
755 value -= 1;
756 }
757 value
758 };
759 let cursor_pos = normalize(cursor_pos, text);
760 let selection = selection.map(|(a, b)| {
761 let a = normalize(a, text);
762 let b = normalize(b, text);
763 if a <= b { (a, b) } else { (b, a) }
764 });
765 let composition_range = composition_range.map(|(a, b)| {
766 let a = normalize(a, text);
767 let b = normalize(b, text);
768 if a <= b { (a, b) } else { (b, a) }
769 });
770 let composition_cursor = composition_cursor.map(|(a, b)| {
771 let a = normalize(a, composition_text);
772 let b = normalize(b, composition_text);
773 if a <= b { (a, b) } else { (b, a) }
774 });
775
776 let mut x = PAD_X.saturating_sub(scroll_x_px.max(0));
777 let mut caret_x = None;
778
779 if let Some((comp_start, comp_end)) = composition_range {
780 x = draw_editbox_committed_segment(
781 &mut rgba,
782 font,
783 text,
784 0,
785 comp_start,
786 cursor_pos,
787 None,
788 &mut caret_x,
789 x,
790 font_px,
791 font_cell,
792 baseline_y,
793 glyph_top,
794 glyph_bottom,
795 max_w,
796 max_h,
797 );
798 let comp_caret = composition_cursor
799 .map(|(_, end)| end)
800 .unwrap_or(composition_text.len());
801 for (idx, ch) in composition_text.char_indices() {
802 if idx == comp_caret {
803 caret_x = Some(x);
804 }
805 let next = idx + ch.len_utf8();
806 let selected = composition_cursor
807 .map(|(start, end)| start != end && idx < end && next > start)
808 .unwrap_or(false);
809 let cell_w = editbox_cell_width_px(ch, font_cell);
810 draw_editbox_cell(
811 &mut rgba,
812 font,
813 ch,
814 x,
815 cell_w,
816 selected,
817 true,
818 font_px,
819 font_cell,
820 baseline_y,
821 glyph_top,
822 glyph_bottom,
823 max_w,
824 max_h,
825 );
826 x = x.saturating_add(cell_w);
827 }
828 if comp_caret == composition_text.len() {
829 caret_x = Some(x);
830 }
831 x = draw_editbox_committed_segment(
832 &mut rgba,
833 font,
834 text,
835 comp_end,
836 text.len(),
837 cursor_pos,
838 None,
839 &mut caret_x,
840 x,
841 font_px,
842 font_cell,
843 baseline_y,
844 glyph_top,
845 glyph_bottom,
846 max_w,
847 max_h,
848 );
849 } else {
850 x = draw_editbox_committed_segment(
851 &mut rgba,
852 font,
853 text,
854 0,
855 text.len(),
856 cursor_pos,
857 selection,
858 &mut caret_x,
859 x,
860 font_px,
861 font_cell,
862 baseline_y,
863 glyph_top,
864 glyph_bottom,
865 max_w,
866 max_h,
867 );
868 if cursor_pos == text.len() {
869 caret_x = Some(x);
870 }
871 }
872
873 if caret_visible {
874 let caret_x = caret_x.unwrap_or(PAD_X).clamp(0, max_w as i32 - 1);
875 fill_rgba_rect(
876 &mut rgba,
877 max_w,
878 max_h,
879 caret_x,
880 glyph_top.max(1),
881 (caret_x + 1).min(max_w as i32),
882 glyph_bottom.max(glyph_top + 1).min(max_h as i32 - 1),
883 (0, 0, 0, 255),
884 );
885 }
886
887 Some(RgbaImage {
888 width: max_w,
889 height: max_h,
890 center_x: 0,
891 center_y: 0,
892 rgba,
893 })
894}
895
896#[allow(clippy::too_many_arguments)]
897fn draw_editbox_committed_segment(
898 rgba: &mut [u8],
899 font: Option<&FontArc>,
900 text: &str,
901 range_start: usize,
902 range_end: usize,
903 cursor_pos: usize,
904 selection: Option<(usize, usize)>,
905 caret_x: &mut Option<i32>,
906 mut x: i32,
907 font_px: f32,
908 font_cell: i32,
909 baseline_y: i32,
910 glyph_top: i32,
911 glyph_bottom: i32,
912 max_w: u32,
913 max_h: u32,
914) -> i32 {
915 for (relative, ch) in text[range_start..range_end].char_indices() {
916 let idx = range_start + relative;
917 if idx == cursor_pos {
918 *caret_x = Some(x);
919 }
920 let next = idx + ch.len_utf8();
921 let selected = selection
922 .map(|(start, end)| idx < end && next > start)
923 .unwrap_or(false);
924 let cell_w = editbox_cell_width_px(ch, font_cell);
925 draw_editbox_cell(
926 rgba,
927 font,
928 ch,
929 x,
930 cell_w,
931 selected,
932 false,
933 font_px,
934 font_cell,
935 baseline_y,
936 glyph_top,
937 glyph_bottom,
938 max_w,
939 max_h,
940 );
941 x = x.saturating_add(cell_w);
942 }
943 x
944}
945
946#[allow(clippy::too_many_arguments)]
947fn draw_editbox_cell(
948 rgba: &mut [u8],
949 font: Option<&FontArc>,
950 ch: char,
951 cell_x: i32,
952 cell_w: i32,
953 selected: bool,
954 composing: bool,
955 font_px: f32,
956 font_cell: i32,
957 baseline_y: i32,
958 glyph_top: i32,
959 glyph_bottom: i32,
960 max_w: u32,
961 max_h: u32,
962) {
963 let left = cell_x.max(0);
964 let right = cell_x.saturating_add(cell_w).min(max_w as i32);
965 if selected && right > left {
966 fill_rgba_rect(
967 rgba,
968 max_w,
969 max_h,
970 left,
971 1,
972 right,
973 max_h as i32 - 1,
974 (0, 120, 215, 255),
975 );
976 }
977
978 let color = if selected {
979 (255, 255, 255, 255)
980 } else {
981 (0, 0, 0, 255)
982 };
983 if let Some(font) = font {
984 let glyph = rasterize_ab_glyph(font, ch, font_px);
985 if glyph.width > 0 && glyph.height > 0 {
986 let draw_x = cell_x + ((cell_w - glyph.width as i32) / 2).max(0);
987 let draw_y = baseline_y + glyph.ymin;
988 draw_glyph_bitmap(
989 rgba,
990 max_w,
991 max_h,
992 draw_x,
993 draw_y,
994 glyph.width,
995 glyph.height,
996 &glyph.bitmap,
997 color,
998 );
999 }
1000 } else {
1001 draw_basic_glyph_color(
1002 rgba,
1003 max_w,
1004 max_h,
1005 cell_x,
1006 glyph_top,
1007 ch,
1008 (font_cell / 7).max(1) as u32,
1009 color,
1010 );
1011 }
1012
1013 if composing && right > left {
1014 let underline_y = (glyph_bottom + 1).clamp(0, max_h as i32 - 1);
1015 fill_rgba_rect(
1016 rgba,
1017 max_w,
1018 max_h,
1019 left,
1020 underline_y,
1021 right,
1022 (underline_y + 1).min(max_h as i32),
1023 (0, 0, 0, 255),
1024 );
1025 }
1026}
1027
1028fn fill_rgba_rect(
1029 rgba: &mut [u8],
1030 width: u32,
1031 height: u32,
1032 x0: i32,
1033 y0: i32,
1034 x1: i32,
1035 y1: i32,
1036 color: (u8, u8, u8, u8),
1037) {
1038 let x0 = x0.clamp(0, width as i32);
1039 let y0 = y0.clamp(0, height as i32);
1040 let x1 = x1.clamp(x0, width as i32);
1041 let y1 = y1.clamp(y0, height as i32);
1042 for y in y0..y1 {
1043 for x in x0..x1 {
1044 let idx = (((y as u32 * width) + x as u32) * 4) as usize;
1045 rgba[idx] = color.0;
1046 rgba[idx + 1] = color.1;
1047 rgba[idx + 2] = color.2;
1048 rgba[idx + 3] = color.3;
1049 }
1050 }
1051}
1052
1053fn draw_basic_glyph_color(
1054 rgba: &mut [u8],
1055 width: u32,
1056 height: u32,
1057 x: i32,
1058 y: i32,
1059 ch: char,
1060 scale: u32,
1061 color: (u8, u8, u8, u8),
1062) {
1063 let glyph = glyph_5x7(ch);
1064 for (row, bits) in glyph.iter().enumerate() {
1065 for col in 0..5 {
1066 if (bits >> (4 - col)) & 1 == 0 {
1067 continue;
1068 }
1069 let px = x + col * scale as i32;
1070 let py = y + row as i32 * scale as i32;
1071 for sy in 0..scale as i32 {
1072 for sx in 0..scale as i32 {
1073 let tx = px + sx;
1074 let ty = py + sy;
1075 if tx < 0 || ty < 0 || tx >= width as i32 || ty >= height as i32 {
1076 continue;
1077 }
1078 blend_rgba_pixel(
1079 rgba, width, tx as u32, ty as u32, color.0, color.1, color.2, color.3,
1080 );
1081 }
1082 }
1083 }
1084 }
1085}
1086
1087fn draw_basic_glyph_face(
1088 rgba: &mut [u8],
1089 width: u32,
1090 height: u32,
1091 x: i32,
1092 y: i32,
1093 ch: char,
1094 scale: u32,
1095 face_extent: i32,
1096 color: (u8, u8, u8, u8),
1097) {
1098 for oy in 0..=face_extent.max(0) {
1099 for ox in 0..=face_extent.max(0) {
1100 draw_basic_glyph_color(rgba, width, height, x + ox, y + oy, ch, scale, color);
1101 }
1102 }
1103}
1104
1105pub fn render_text_image_basic(
1106 images: &mut ImageManager,
1107 text: &str,
1108 font_px: u32,
1109 max_w: u32,
1110 max_h: u32,
1111) -> Option<ImageHandle> {
1112 let img = render_text_image_basic_rgba(text, font_px, max_w, max_h)?;
1113 Some(images.insert_image(img))
1114}
1115
1116pub fn render_text_image_basic_rgba(
1117 text: &str,
1118 font_px: u32,
1119 max_w: u32,
1120 max_h: u32,
1121) -> Option<RgbaImage> {
1122 if text.is_empty() || max_w == 0 || max_h == 0 {
1123 return None;
1124 }
1125 let scale = (font_px / 7).max(1);
1126 let glyph_w = 5 * scale;
1127 let glyph_h = 7 * scale;
1128 let advance = glyph_w + scale;
1129 let line_height = glyph_h + scale;
1130
1131 let mut rgba = vec![0u8; (max_w * max_h * 4) as usize];
1132 let mut x = 0u32;
1133 let mut y = 0u32;
1134
1135 for ch in text.chars() {
1136 if ch == '\n' {
1137 x = 0;
1138 y = y.saturating_add(line_height);
1139 if y >= max_h {
1140 break;
1141 }
1142 continue;
1143 }
1144 if ch == '\t' {
1145 x = x.saturating_add(advance * 2);
1146 continue;
1147 }
1148 if x + glyph_w > max_w {
1149 x = 0;
1150 y = y.saturating_add(line_height);
1151 if y >= max_h {
1152 break;
1153 }
1154 }
1155 draw_glyph_5x7(&mut rgba, max_w, max_h, x, y, ch, scale);
1156 x = x.saturating_add(advance);
1157 }
1158
1159 Some(RgbaImage {
1160 width: max_w,
1161 height: max_h,
1162 center_x: 0,
1163 center_y: 0,
1164 rgba,
1165 })
1166}
1167
1168fn render_positioned_glyphs_rgba(
1169 font: Option<&FontArc>,
1170 glyphs: &[PositionedTextGlyph],
1171 min_w: u32,
1172 min_h: u32,
1173 layer: Option<TextSpriteLayer>,
1174) -> Option<(RgbaImage, i32, i32)> {
1175 if glyphs.is_empty() || min_w == 0 || min_h == 0 {
1176 return None;
1177 }
1178
1179 let mut min_x = 0i32;
1180 let mut min_y = 0i32;
1181 let mut max_x = min_w as i32;
1182 let mut max_y = min_h as i32;
1183
1184 if let Some(font) = font {
1185 for placed in glyphs {
1186 let size = placed.size.max(1.0);
1187 let scaled = font.as_scaled(PxScale::from(size));
1188 let baseline = scaled.ascent().ceil().max(1.0) as i32;
1189 let glyph = rasterize_ab_glyph_oriented(font, placed.ch, size, placed.vertical);
1190 let pad = text_effect_padding(size.round().max(1.0) as i32, placed.style);
1191 let (origin_x, origin_y) = positioned_glyph_origin(placed, &glyph, baseline);
1192 let x0 = origin_x.saturating_sub(pad);
1193 let y0 = origin_y.saturating_sub(pad);
1194 let x1 = origin_x
1195 .saturating_add(glyph.width as i32)
1196 .saturating_add(pad);
1197 let y1 = origin_y
1198 .saturating_add(glyph.height as i32)
1199 .saturating_add(pad);
1200 min_x = min_x.min(x0);
1201 min_y = min_y.min(y0);
1202 max_x = max_x.max(x1);
1203 max_y = max_y.max(y1);
1204 }
1205 } else {
1206 for placed in glyphs {
1207 let size = placed.size.round().max(1.0) as i32;
1208 let scale = ((size + 6) / 7).max(1);
1209 min_x = min_x.min(placed.x - 2);
1210 min_y = min_y.min(placed.y - 2);
1211 max_x = max_x.max(placed.x + 5 * scale + 3);
1212 max_y = max_y.max(placed.y + 7 * scale + 3);
1213 }
1214 }
1215
1216 let render_w = max_x.saturating_sub(min_x).max(1) as u32;
1217 let render_h = max_y.saturating_sub(min_y).max(1) as u32;
1218 let mut rgba = vec![
1219 0u8;
1220 (render_w as usize)
1221 .saturating_mul(render_h as usize)
1222 .saturating_mul(4)
1223 ];
1224
1225 if let Some(font) = font {
1226 for placed in glyphs {
1227 let size = placed.size.max(1.0);
1228 let scaled = font.as_scaled(PxScale::from(size));
1229 let baseline = scaled.ascent().ceil().max(1.0) as i32;
1230 let glyph = rasterize_ab_glyph_oriented(font, placed.ch, size, placed.vertical);
1231 if glyph.width == 0 || glyph.height == 0 {
1232 continue;
1233 }
1234 let (origin_x, origin_y) = positioned_glyph_origin(placed, &glyph, baseline);
1235 let draw_x = origin_x - min_x;
1236 let draw_y = origin_y - min_y;
1237 let style = placed.style;
1238 if style.shadow && (layer.is_none() || layer == Some(TextSpriteLayer::Shadow)) {
1241 let off = shadow_offset_for_style(size.round().max(1.0) as i32, style);
1242 draw_glyph_bitmap_face(
1243 &mut rgba,
1244 render_w,
1245 render_h,
1246 draw_x + off,
1247 draw_y + off,
1248 glyph.width,
1249 glyph.height,
1250 &glyph.bitmap,
1251 shadow_face_extent(style),
1252 (
1253 style.shadow_color.0,
1254 style.shadow_color.1,
1255 style.shadow_color.2,
1256 255,
1257 ),
1258 );
1259 }
1260 if style.fuchi && (layer.is_none() || layer == Some(TextSpriteLayer::Fuchi)) {
1261 let off = fuchi_texture_offset();
1262 draw_glyph_bitmap_face(
1263 &mut rgba,
1264 render_w,
1265 render_h,
1266 draw_x + off,
1267 draw_y + off,
1268 glyph.width,
1269 glyph.height,
1270 &glyph.bitmap,
1271 fuchi_face_extent(style),
1272 (
1273 style.fuchi_color.0,
1274 style.fuchi_color.1,
1275 style.fuchi_color.2,
1276 255,
1277 ),
1278 );
1279 }
1280 if layer.is_none() || layer == Some(TextSpriteLayer::Body) {
1281 draw_glyph_bitmap_face(
1282 &mut rgba,
1283 render_w,
1284 render_h,
1285 draw_x,
1286 draw_y,
1287 glyph.width,
1288 glyph.height,
1289 &glyph.bitmap,
1290 body_face_extent(style),
1291 (style.color.0, style.color.1, style.color.2, 255),
1292 );
1293 }
1294 }
1295 } else {
1296 for placed in glyphs {
1297 let style = placed.style;
1298 let scale = ((placed.size.round().max(1.0) as i32 + 6) / 7).max(1) as u32;
1299 let x = placed.x - min_x;
1300 let y = placed.y - min_y;
1301 if style.shadow && (layer.is_none() || layer == Some(TextSpriteLayer::Shadow)) {
1302 let off = shadow_offset_for_style(placed.size.round().max(1.0) as i32, style);
1303 draw_basic_glyph_face(
1304 &mut rgba,
1305 render_w,
1306 render_h,
1307 x + off,
1308 y + off,
1309 placed.ch,
1310 scale,
1311 shadow_face_extent(style),
1312 (
1313 style.shadow_color.0,
1314 style.shadow_color.1,
1315 style.shadow_color.2,
1316 255,
1317 ),
1318 );
1319 }
1320 if style.fuchi && (layer.is_none() || layer == Some(TextSpriteLayer::Fuchi)) {
1321 let off = fuchi_texture_offset();
1322 draw_basic_glyph_face(
1323 &mut rgba,
1324 render_w,
1325 render_h,
1326 x + off,
1327 y + off,
1328 placed.ch,
1329 scale,
1330 fuchi_face_extent(style),
1331 (
1332 style.fuchi_color.0,
1333 style.fuchi_color.1,
1334 style.fuchi_color.2,
1335 255,
1336 ),
1337 );
1338 }
1339 if layer.is_none() || layer == Some(TextSpriteLayer::Body) {
1340 draw_basic_glyph_face(
1341 &mut rgba,
1342 render_w,
1343 render_h,
1344 x,
1345 y,
1346 placed.ch,
1347 scale,
1348 body_face_extent(style),
1349 (style.color.0, style.color.1, style.color.2, 255),
1350 );
1351 }
1352 }
1353 }
1354
1355 Some((
1356 RgbaImage {
1357 width: render_w,
1358 height: render_h,
1359 center_x: 0,
1360 center_y: 0,
1361 rgba,
1362 },
1363 min_x,
1364 min_y,
1365 ))
1366}
1367
1368#[derive(Debug, Clone)]
1369struct RasterGlyph {
1370 width: usize,
1371 height: usize,
1372 xmin: i32,
1373 ymin: i32,
1374 bitmap: Vec<u8>,
1375}
1376
1377fn positioned_glyph_origin(
1378 placed: &PositionedTextGlyph,
1379 glyph: &RasterGlyph,
1380 horizontal_baseline: i32,
1381) -> (i32, i32) {
1382 if placed.vertical {
1383 let cell = placed.size.round().max(1.0) as i32;
1384 (
1389 placed.x + (cell - glyph.width as i32) / 2,
1390 placed.y + (cell - glyph.height as i32) / 2,
1391 )
1392 } else {
1393 (
1394 placed.x + glyph.xmin,
1395 placed.y + horizontal_baseline + glyph.ymin,
1396 )
1397 }
1398}
1399
1400fn rasterize_ab_glyph_oriented(
1401 font: &FontArc,
1402 ch: char,
1403 font_px: f32,
1404 vertical: bool,
1405) -> RasterGlyph {
1406 if !vertical {
1407 return rasterize_ab_glyph(font, ch, font_px);
1408 }
1409
1410 let (vertical_ch, rotate) = vertical_glyph_mapping(font, ch);
1411 let mut glyph = rasterize_ab_glyph(font, vertical_ch, font_px);
1412 if rotate && glyph.width != 0 && glyph.height != 0 {
1413 glyph = rotate_raster_glyph_clockwise(glyph);
1414 }
1415 glyph.xmin = 0;
1416 glyph.ymin = 0;
1417 glyph
1418}
1419
1420fn vertical_glyph_mapping(font: &FontArc, ch: char) -> (char, bool) {
1421 let mapped = match ch {
1422 '、' => '︑',
1423 '。' | '.' => '︒',
1424 ',' => '︐',
1425 ':' | ':' => '︓',
1426 ';' | ';' => '︔',
1427 '!' | '!' => '︕',
1428 '?' | '?' => '︖',
1429 '…' => '︙',
1430 '—' | '―' | 'ー' => '︱',
1431 '_' => '︳',
1432 '(' | '(' => '︵',
1433 ')' | ')' => '︶',
1434 '{' | '{' => '︷',
1435 '}' | '}' => '︸',
1436 '〔' => '︹',
1437 '〕' => '︺',
1438 '【' => '︻',
1439 '】' => '︼',
1440 '《' => '︽',
1441 '》' => '︾',
1442 '〈' => '︿',
1443 '〉' => '﹀',
1444 '「' => '﹁',
1445 '」' => '﹂',
1446 '『' => '﹃',
1447 '』' => '﹄',
1448 '[' | '[' => '﹇',
1449 ']' | ']' => '﹈',
1450 _ => ch,
1451 };
1452 let mapped_available = font.glyph_id(mapped).0 != 0;
1453 if mapped != ch && mapped_available {
1454 return (mapped, false);
1455 }
1456
1457 let rotate = ch.is_ascii_alphanumeric()
1461 || matches!(ch, 'A'..='Z' | 'a'..='z' | '0'..='9')
1462 || matches!(ch, '-' | '=' | '<' | '>' | '/' | '\\' | '~');
1463 (ch, rotate)
1464}
1465
1466fn rotate_raster_glyph_clockwise(src: RasterGlyph) -> RasterGlyph {
1467 let width = src.height;
1468 let height = src.width;
1469 let mut bitmap = vec![0u8; width.saturating_mul(height)];
1470 for y in 0..src.height {
1471 for x in 0..src.width {
1472 let dst_x = src.height - 1 - y;
1473 let dst_y = x;
1474 bitmap[dst_y * width + dst_x] = src.bitmap[y * src.width + x];
1475 }
1476 }
1477 RasterGlyph {
1478 width,
1479 height,
1480 xmin: 0,
1481 ymin: 0,
1482 bitmap,
1483 }
1484}
1485
1486fn rasterize_ab_glyph(font: &FontArc, ch: char, font_px: f32) -> RasterGlyph {
1487 let scale = PxScale::from(font_px.max(1.0));
1488 let scaled = font.as_scaled(scale);
1489 let glyph_id = scaled.glyph_id(ch);
1490 let glyph = glyph_id.with_scale_and_position(scale, point(0.0, 0.0));
1491 let Some(outlined) = scaled.outline_glyph(glyph) else {
1492 return RasterGlyph {
1493 width: 0,
1494 height: 0,
1495 xmin: 0,
1496 ymin: 0,
1497 bitmap: Vec::new(),
1498 };
1499 };
1500
1501 let bounds = outlined.px_bounds();
1502 let xmin = bounds.min.x.floor() as i32;
1503 let ymin = bounds.min.y.floor() as i32;
1504 let xmax = bounds.max.x.ceil() as i32;
1505 let ymax = bounds.max.y.ceil() as i32;
1506 let width = (xmax - xmin).max(0) as usize;
1507 let height = (ymax - ymin).max(0) as usize;
1508 if width == 0 || height == 0 {
1509 return RasterGlyph {
1510 width: 0,
1511 height: 0,
1512 xmin,
1513 ymin,
1514 bitmap: Vec::new(),
1515 };
1516 }
1517
1518 let shifted_glyph =
1519 glyph_id.with_scale_and_position(scale, point((-xmin) as f32, (-ymin) as f32));
1520 let Some(shifted) = scaled.outline_glyph(shifted_glyph) else {
1521 return RasterGlyph {
1522 width: 0,
1523 height: 0,
1524 xmin,
1525 ymin,
1526 bitmap: Vec::new(),
1527 };
1528 };
1529
1530 let mut bitmap = vec![0u8; width * height];
1531 shifted.draw(|gx, gy, cov| {
1532 let x = gx as usize;
1533 let y = gy as usize;
1534 if x < width && y < height {
1535 bitmap[y * width + x] = gdi_gray8_alpha_from_coverage(cov);
1536 }
1537 });
1538
1539 RasterGlyph {
1540 width,
1541 height,
1542 xmin,
1543 ymin,
1544 bitmap,
1545 }
1546}
1547
1548fn render_mwnd_text_ab_glyph_rgba(
1549 font: &FontArc,
1550 text: &str,
1551 font_px: f32,
1552 max_w: u32,
1553 max_h: u32,
1554 moji_space: Option<(i64, i64)>,
1555) -> Option<RgbaImage> {
1556 render_mwnd_text_ab_glyph_rgba_styled(
1557 font,
1558 text,
1559 font_px,
1560 max_w,
1561 max_h,
1562 moji_space,
1563 TextStyle::default(),
1564 None,
1565 false,
1566 )
1567}
1568
1569fn render_mwnd_text_ab_glyph_rgba_styled(
1570 font: &FontArc,
1571 text: &str,
1572 font_px: f32,
1573 max_w: u32,
1574 max_h: u32,
1575 moji_space: Option<(i64, i64)>,
1576 style: TextStyle,
1577 layer: Option<TextSpriteLayer>,
1578 vertical: bool,
1579) -> Option<RgbaImage> {
1580 if text.is_empty() || max_w == 0 || max_h == 0 {
1581 return None;
1582 }
1583
1584 let (space_x, space_y) = moji_space.unwrap_or((-1, 10));
1585 let font_cell = font_px.round().max(1.0) as i32;
1586 let line_h = (font_cell + space_y as i32).max(font_cell).max(1);
1587 let scaled = font.as_scaled(PxScale::from(font_px.max(1.0)));
1588 let baseline_y = scaled.ascent().ceil().max(1.0) as i32;
1589 let effect_pad = text_effect_padding(font_cell, style);
1590 let render_w = max_w.saturating_add(effect_pad.max(0) as u32 + 2);
1591 let render_h = max_h.saturating_add(
1592 (baseline_y + effect_pad)
1593 .max(font_cell / 4 + effect_pad + 2)
1594 .max(0) as u32,
1595 );
1596 let mut rgba = vec![0u8; (render_w * render_h * 4) as usize];
1597
1598 let placed_chars = if vertical {
1599 layout_mwnd_text_vertical(text, font_cell, space_x as i32, line_h, max_w, max_h)
1600 } else {
1601 layout_mwnd_text(text, font_cell, space_x as i32, line_h, max_w, max_h)
1602 };
1603 for placed in placed_chars {
1604 let glyph = rasterize_ab_glyph_oriented(font, placed.ch, font_px, vertical);
1605 if glyph.width == 0 || glyph.height == 0 {
1606 continue;
1607 }
1608
1609 let (draw_x, draw_y) = if vertical {
1610 (
1611 placed.x + (font_cell - glyph.width as i32) / 2,
1612 placed.y + (font_cell - glyph.height as i32) / 2,
1613 )
1614 } else {
1615 (placed.x + glyph.xmin, placed.y + baseline_y + glyph.ymin)
1616 };
1617
1618 if style.shadow && (layer.is_none() || layer == Some(TextSpriteLayer::Shadow)) {
1619 let shadow_offset = shadow_offset_for_style(font_cell, style);
1620 draw_glyph_bitmap_face(
1621 &mut rgba,
1622 render_w,
1623 render_h,
1624 draw_x + shadow_offset,
1625 draw_y + shadow_offset,
1626 glyph.width,
1627 glyph.height,
1628 &glyph.bitmap,
1629 shadow_face_extent(style),
1630 (
1631 style.shadow_color.0,
1632 style.shadow_color.1,
1633 style.shadow_color.2,
1634 255,
1635 ),
1636 );
1637 }
1638 if style.fuchi && (layer.is_none() || layer == Some(TextSpriteLayer::Fuchi)) {
1639 draw_glyph_bitmap_face(
1640 &mut rgba,
1641 render_w,
1642 render_h,
1643 draw_x,
1644 draw_y,
1645 glyph.width,
1646 glyph.height,
1647 &glyph.bitmap,
1648 fuchi_face_extent(style),
1649 (
1650 style.fuchi_color.0,
1651 style.fuchi_color.1,
1652 style.fuchi_color.2,
1653 255,
1654 ),
1655 );
1656 }
1657 if layer.is_none() || layer == Some(TextSpriteLayer::Body) {
1658 draw_glyph_bitmap_face(
1659 &mut rgba,
1660 render_w,
1661 render_h,
1662 draw_x,
1663 draw_y,
1664 glyph.width,
1665 glyph.height,
1666 &glyph.bitmap,
1667 body_face_extent(style),
1668 (style.color.0, style.color.1, style.color.2, 255),
1669 );
1670 }
1671 }
1672
1673 Some(RgbaImage {
1674 width: render_w,
1675 height: render_h,
1676 center_x: 0,
1677 center_y: 0,
1678 rgba,
1679 })
1680}
1681
1682#[derive(Debug, Clone, Copy)]
1683struct MwndPlacedChar {
1684 ch: char,
1685 x: i32,
1686 y: i32,
1687 cell_w: i32,
1688}
1689
1690fn layout_mwnd_text(
1691 text: &str,
1692 font_cell: i32,
1693 space_x: i32,
1694 line_h: i32,
1695 max_w: u32,
1696 max_h: u32,
1697) -> Vec<MwndPlacedChar> {
1698 let full_cell_w = font_cell.max(1);
1699 let half_cell_w = ((font_cell + 1) / 2).max(1);
1700 let max_w = max_w as i32;
1701 let max_h = max_h as i32;
1702 let mut out = Vec::new();
1703 let mut x = 0i32;
1704 let mut y = 0i32;
1705 let mut indent_x = 0i32;
1706 let mut line_head = true;
1707
1708 for ch in text.chars() {
1709 match ch {
1710 '\r' => continue,
1711 '\n' => {
1712 x = indent_x;
1713 y += line_h;
1714 line_head = true;
1715 if y >= max_h {
1716 break;
1717 }
1718 continue;
1719 }
1720 '\u{0007}' => {
1721 indent_x = 0;
1722 x = 0;
1723 y += line_h;
1724 line_head = true;
1725 if y >= max_h {
1726 break;
1727 }
1728 continue;
1729 }
1730 '\t' => {
1731 x += (full_cell_w + space_x).max(1) * 2;
1732 line_head = false;
1733 continue;
1734 }
1735 _ => {}
1736 }
1737
1738 let cell_w = if is_hankaku(ch) {
1739 half_cell_w
1740 } else {
1741 full_cell_w
1742 };
1743 let check_size = cell_w + space_x;
1744 let force_wrap = x > 0 && x + check_size > max_w + full_cell_w;
1745 let soft_wrap = x > 0 && x + check_size > max_w && !is_siglus_forbidden_line_head(ch);
1746 if force_wrap || soft_wrap {
1747 x = indent_x;
1748 y += line_h;
1749 line_head = true;
1750 if y >= max_h {
1751 break;
1752 }
1753 if ch == ' ' || ch == '\u{3000}' {
1754 continue;
1755 }
1756 }
1757
1758 if line_head {
1759 if is_siglus_indent_open(ch) {
1760 indent_x = full_cell_w;
1761 } else if is_siglus_indent_close(ch) {
1762 indent_x = 0;
1763 }
1764 }
1765
1766 out.push(MwndPlacedChar { ch, x, y, cell_w });
1767 x += (cell_w + space_x).max(1);
1768 line_head = false;
1769 }
1770 out
1771}
1772
1773fn layout_mwnd_text_vertical(
1774 text: &str,
1775 font_cell: i32,
1776 space_y: i32,
1777 column_w: i32,
1778 max_w: u32,
1779 max_h: u32,
1780) -> Vec<MwndPlacedChar> {
1781 let full_cell_h = font_cell.max(1);
1782 let half_cell_h = ((font_cell + 1) / 2).max(1);
1783 let max_w = max_w as i32;
1784 let max_h = max_h as i32;
1785 let mut out = Vec::new();
1786 let mut x = (max_w - full_cell_h).max(0);
1789 let mut y = 0i32;
1790
1791 for ch in text.chars() {
1792 match ch {
1793 '\r' => continue,
1794 '\n' | '\u{0007}' => {
1795 x -= column_w.max(1);
1796 y = 0;
1797 if x + full_cell_h <= 0 {
1798 break;
1799 }
1800 continue;
1801 }
1802 '\t' => {
1803 y += (full_cell_h + space_y).max(1) * 2;
1804 continue;
1805 }
1806 _ => {}
1807 }
1808
1809 let cell_h = if is_hankaku(ch) {
1810 half_cell_h
1811 } else {
1812 full_cell_h
1813 };
1814 let advance = (cell_h + space_y).max(1);
1815 if y > 0 && y + cell_h > max_h {
1816 x -= column_w.max(1);
1817 y = 0;
1818 if x + full_cell_h <= 0 {
1819 break;
1820 }
1821 }
1822 out.push(MwndPlacedChar {
1823 ch,
1824 x,
1825 y,
1826 cell_w: full_cell_h,
1827 });
1828 y += advance;
1829 }
1830 out
1831}
1832
1833fn is_siglus_indent_open(ch: char) -> bool {
1834 matches!(ch, '「' | '『' | '(')
1835}
1836
1837fn is_siglus_indent_close(ch: char) -> bool {
1838 matches!(ch, '」' | '』' | ')')
1839}
1840
1841fn is_siglus_forbidden_line_head(ch: char) -> bool {
1842 matches!(
1843 ch,
1844 '、' | '。'
1845 | ','
1846 | '.'
1847 | '・'
1848 | ':'
1849 | ';'
1850 | '?'
1851 | '!'
1852 | '」'
1853 | '』'
1854 | ')'
1855 | ']'
1856 | '}'
1857 | '〉'
1858 | '》'
1859 | '】'
1860 | '〕'
1861 | 'ぁ'
1862 | 'ぃ'
1863 | 'ぅ'
1864 | 'ぇ'
1865 | 'ぉ'
1866 | 'っ'
1867 | 'ゃ'
1868 | 'ゅ'
1869 | 'ょ'
1870 | 'ゎ'
1871 | 'ァ'
1872 | 'ィ'
1873 | 'ゥ'
1874 | 'ェ'
1875 | 'ォ'
1876 | 'ッ'
1877 | 'ャ'
1878 | 'ュ'
1879 | 'ョ'
1880 | 'ヮ'
1881 | 'ー'
1882 | 'ー'
1883 | '~'
1884 | '…'
1885 | '‥'
1886 )
1887}
1888
1889fn body_face_extent(style: TextStyle) -> i32 {
1890 if style.bold { 1 } else { 0 }
1891}
1892
1893fn fuchi_texture_offset() -> i32 {
1898 -1
1899}
1900
1901fn fuchi_face_extent(style: TextStyle) -> i32 {
1902 if style.bold { 3 } else { 2 }
1903}
1904
1905fn shadow_face_extent(style: TextStyle) -> i32 {
1906 if style.shadow_mode == TNM_FONT_SHADOW_MODE_FUCHI_SHADOW {
1907 if style.bold { 3 } else { 2 }
1908 } else {
1909 body_face_extent(style)
1910 }
1911}
1912
1913fn shadow_offset_for_style(size: i32, style: TextStyle) -> i32 {
1914 let size = size.max(0);
1915 let t = (size as f32 / 32.0).clamp(0.0, 1.0);
1916 if style.shadow_mode == TNM_FONT_SHADOW_MODE_FUCHI_SHADOW {
1917 (0.5 + (1.5 - 0.5) * t - 1.0).round() as i32
1919 } else {
1920 (0.5 + (2.0 - 0.5) * t).round().max(1.0) as i32
1922 }
1923}
1924
1925fn text_effect_padding(size: i32, style: TextStyle) -> i32 {
1926 let mut pad = body_face_extent(style);
1927 if style.fuchi {
1928 pad = pad.max(fuchi_face_extent(style));
1929 }
1930 if style.shadow {
1931 pad = pad.max(
1932 shadow_offset_for_style(size, style)
1933 .max(0)
1934 .saturating_add(shadow_face_extent(style)),
1935 );
1936 }
1937 pad + 1
1938}
1939
1940pub fn is_hankaku(ch: char) -> bool {
1941 ch.is_ascii() || matches!(ch as u32, 0xFF61..=0xFF9F)
1942}
1943
1944fn draw_glyph_bitmap(
1945 rgba: &mut [u8],
1946 w: u32,
1947 h: u32,
1948 x: i32,
1949 y: i32,
1950 glyph_w: usize,
1951 glyph_h: usize,
1952 glyph: &[u8],
1953 color: (u8, u8, u8, u8),
1954) {
1955 for gy in 0..glyph_h {
1956 let py = y + gy as i32;
1957 if py < 0 || py as u32 >= h {
1958 continue;
1959 }
1960 for gx in 0..glyph_w {
1961 let px = x + gx as i32;
1962 if px < 0 || px as u32 >= w {
1963 continue;
1964 }
1965 let src = glyph[gy * glyph_w + gx];
1966 if src == 0 {
1967 continue;
1968 }
1969 let src_a = ((src as u16 * color.3 as u16) / 255) as u8;
1970 blend_rgba_pixel(
1971 rgba, w, px as u32, py as u32, color.0, color.1, color.2, src_a,
1972 );
1973 }
1974 }
1975}
1976
1977fn gdi_gray8_alpha_from_coverage(coverage: f32) -> u8 {
1983 let level = (coverage.clamp(0.0, 1.0) * 64.0).round().clamp(0.0, 64.0) as u16;
1984 ((255u16 * level) / 65) as u8
1985}
1986
1987fn draw_glyph_bitmap_face(
1988 rgba: &mut [u8],
1989 w: u32,
1990 h: u32,
1991 x: i32,
1992 y: i32,
1993 glyph_w: usize,
1994 glyph_h: usize,
1995 glyph: &[u8],
1996 face_extent: i32,
1997 color: (u8, u8, u8, u8),
1998) {
1999 for oy in 0..=face_extent.max(0) {
2000 for ox in 0..=face_extent.max(0) {
2001 draw_glyph_bitmap(rgba, w, h, x + ox, y + oy, glyph_w, glyph_h, glyph, color);
2002 }
2003 }
2004}
2005
2006fn blend_rgba_pixel(rgba: &mut [u8], w: u32, x: u32, y: u32, sr: u8, sg: u8, sb: u8, sa: u8) {
2007 let idx = ((y * w + x) * 4) as usize;
2008 let da = rgba[idx + 3] as u32;
2012 let sa_u = sa as u32;
2013 let inv_sa = 255u32.saturating_sub(sa_u);
2014 let out_a = sa_u + da - (sa_u * da) / 255;
2018 if out_a == 0 {
2019 rgba[idx] = 0;
2020 rgba[idx + 1] = 0;
2021 rgba[idx + 2] = 0;
2022 rgba[idx + 3] = 0;
2023 return;
2024 }
2025 let blend = |src: u8, dst: u8| -> u8 {
2026 let src_p = src as u32 * sa_u;
2027 let dst_p = dst as u32 * da * inv_sa / 255;
2028 ((src_p + dst_p + out_a / 2) / out_a).min(255) as u8
2029 };
2030 rgba[idx] = blend(sr, rgba[idx]);
2031 rgba[idx + 1] = blend(sg, rgba[idx + 1]);
2032 rgba[idx + 2] = blend(sb, rgba[idx + 2]);
2033 rgba[idx + 3] = out_a.min(255) as u8;
2034}
2035
2036pub fn text_line_metrics_ab_glyph(
2042 font: &FontArc,
2043 text: &str,
2044 font_px: f32,
2045 max_w: u32,
2046 max_h: u32,
2047) -> (usize, f32) {
2048 let scaled = font.as_scaled(PxScale::from(font_px.max(1.0)));
2049 let ascent = scaled.ascent().max(1.0);
2050 let line_height = (scaled.height() + scaled.line_gap()).max(1.0);
2051 if text.is_empty() || max_w == 0 || max_h == 0 {
2052 return (0, line_height);
2053 }
2054
2055 let mut lines = 1usize;
2056 let mut x = 0.0f32;
2057 let mut baseline_y = ascent.max(1.0);
2058 for ch in text.chars() {
2059 match ch {
2060 '\r' => continue,
2061 '\n' => {
2062 x = 0.0;
2063 baseline_y += line_height;
2064 if baseline_y - ascent >= max_h as f32 {
2065 break;
2066 }
2067 lines += 1;
2068 continue;
2069 }
2070 '\t' => {
2071 x += scaled.h_advance(scaled.glyph_id(' ')).max(0.0) * 2.0;
2072 continue;
2073 }
2074 _ => {}
2075 }
2076
2077 let advance = scaled.h_advance(scaled.glyph_id(ch)).max(0.0);
2078 if x > 0.0 && x + advance > max_w as f32 {
2079 x = 0.0;
2080 baseline_y += line_height;
2081 if baseline_y - ascent >= max_h as f32 {
2082 break;
2083 }
2084 lines += 1;
2085 }
2086 x += advance;
2087 }
2088 (lines, line_height)
2089}
2090
2091fn render_text_ab_glyph_rgba(
2092 font: &FontArc,
2093 text: &str,
2094 font_px: f32,
2095 max_w: u32,
2096 max_h: u32,
2097) -> Option<RgbaImage> {
2098 if text.is_empty() || max_w == 0 || max_h == 0 {
2099 return None;
2100 }
2101 let mut rgba = vec![0u8; (max_w * max_h * 4) as usize];
2102
2103 let scaled = font.as_scaled(PxScale::from(font_px.max(1.0)));
2104 let ascent = scaled.ascent().max(1.0);
2105 let line_height = (scaled.height() + scaled.line_gap()).max(1.0);
2106
2107 let mut x = 0.0f32;
2108 let mut baseline_y = ascent.max(1.0);
2109
2110 for ch in text.chars() {
2111 match ch {
2112 '\r' => continue,
2113 '\n' => {
2114 x = 0.0;
2115 baseline_y += line_height;
2116 if baseline_y - ascent >= max_h as f32 {
2117 break;
2118 }
2119 continue;
2120 }
2121 '\t' => {
2122 x += scaled.h_advance(scaled.glyph_id(' ')).max(0.0) * 2.0;
2123 continue;
2124 }
2125 _ => {}
2126 }
2127
2128 let advance = scaled.h_advance(scaled.glyph_id(ch)).max(0.0);
2129 if x > 0.0 && x + advance > max_w as f32 {
2130 x = 0.0;
2131 baseline_y += line_height;
2132 if baseline_y - ascent >= max_h as f32 {
2133 break;
2134 }
2135 }
2136
2137 let glyph = rasterize_ab_glyph(font, ch, font_px);
2138 let gx = x + glyph.xmin as f32;
2139 let gy = baseline_y + glyph.ymin as f32;
2140 for gy_i in 0..glyph.height {
2141 let py = gy as i32 + gy_i as i32;
2142 if py < 0 || py as u32 >= max_h {
2143 continue;
2144 }
2145 for gx_i in 0..glyph.width {
2146 let px = gx as i32 + gx_i as i32;
2147 if px < 0 || px as u32 >= max_w {
2148 continue;
2149 }
2150 let src = glyph.bitmap[gy_i * glyph.width + gx_i];
2151 if src == 0 {
2152 continue;
2153 }
2154 let idx = ((py as u32 * max_w + px as u32) * 4) as usize;
2155 rgba[idx] = 255;
2156 rgba[idx + 1] = 255;
2157 rgba[idx + 2] = 255;
2158 rgba[idx + 3] = src;
2159 }
2160 }
2161 x += advance;
2162 }
2163
2164 Some(RgbaImage {
2165 width: max_w,
2166 height: max_h,
2167 center_x: 0,
2168 center_y: 0,
2169 rgba,
2170 })
2171}
2172
2173pub fn embedded_default_font_available() -> bool {
2174 embedded_font::EMBEDDED_DEFAULT_FONT.is_some()
2175}
2176
2177pub fn embedded_default_font_names() -> &'static [&'static str] {
2178 if embedded_default_font_available() {
2179 embedded_font::EMBEDDED_DEFAULT_FONT_ALIASES
2180 } else {
2181 &[]
2182 }
2183}
2184
2185pub fn font_name_matches_embedded_default(name: &str) -> bool {
2186 if !embedded_default_font_available() {
2187 return false;
2188 }
2189 let needle = normalize_font_name_for_match(name);
2190 if needle.is_empty() {
2191 return false;
2192 }
2193 embedded_font::EMBEDDED_DEFAULT_FONT_ALIASES
2194 .iter()
2195 .any(|alias| normalize_font_name_for_match(alias) == needle)
2196}
2197
2198pub fn normalized_font_name(name: &str) -> String {
2199 normalize_font_name_for_match(name.trim_start_matches('@'))
2200}
2201
2202fn normalize_font_name_for_match(name: &str) -> String {
2203 name.chars()
2204 .filter(|ch| !ch.is_whitespace() && *ch != '-' && *ch != '_' && *ch != '.')
2205 .flat_map(|ch| ch.to_lowercase())
2206 .collect()
2207}
2208
2209struct FontFileInfo {
2212 face_names: Vec<Vec<String>>,
2213 data: Option<&'static [u8]>,
2214}
2215
2216static FONT_FILES: OnceLock<Mutex<std::collections::HashMap<PathBuf, FontFileInfo>>> =
2222 OnceLock::new();
2223
2224fn font_file_face(path: &Path, normalized_name: &str) -> Option<(&'static [u8], u32)> {
2227 let files = FONT_FILES.get_or_init(|| Mutex::new(std::collections::HashMap::new()));
2228 let mut files = files.lock().ok()?;
2229 if let Some(info) = files.get_mut(path) {
2230 let face_index = matching_face_in_names(&info.face_names, normalized_name)?;
2231 let data = match info.data {
2232 Some(data) => data,
2233 None => {
2234 let data: &'static [u8] = Box::leak(
2235 crate::resource::read_file_bytes(path)
2236 .ok()?
2237 .into_boxed_slice(),
2238 );
2239 info.data = Some(data);
2240 data
2241 }
2242 };
2243 return Some((data, face_index));
2244 }
2245 let bytes = crate::resource::read_file_bytes(path).ok()?;
2246 let face_names = font_face_names(&bytes);
2247 let face_index = matching_face_in_names(&face_names, normalized_name);
2248 let data = face_index.map(|_| &*Box::leak(bytes.into_boxed_slice()));
2250 files.insert(path.to_path_buf(), FontFileInfo { face_names, data });
2251 Some((data?, face_index?))
2252}
2253
2254fn font_face_names(bytes: &[u8]) -> Vec<Vec<String>> {
2257 let face_count = ttf_parser::fonts_in_collection(bytes).unwrap_or(1);
2258 (0..face_count)
2259 .map(|face_index| {
2260 let Ok(face) = ttf_parser::Face::parse(bytes, face_index) else {
2261 return Vec::new();
2262 };
2263 face.names()
2264 .into_iter()
2265 .filter(|name| {
2266 matches!(
2267 name.name_id,
2268 ttf_parser::name::name_id::FAMILY
2269 | ttf_parser::name::name_id::FULL_NAME
2270 | ttf_parser::name::name_id::POST_SCRIPT_NAME
2271 | ttf_parser::name::name_id::TYPOGRAPHIC_FAMILY
2272 | ttf_parser::name::name_id::WWS_FAMILY
2273 | ttf_parser::name::name_id::COMPATIBLE_FULL
2274 )
2275 })
2276 .filter_map(|name| name.to_string())
2277 .map(|value| normalize_font_name_for_match(value.trim_start_matches('@')))
2278 .collect()
2279 })
2280 .collect()
2281}
2282
2283fn matching_face_in_names(face_names: &[Vec<String>], normalized_name: &str) -> Option<u32> {
2284 if normalized_name.is_empty() {
2285 return Some(0);
2286 }
2287 let mut partial_match = None;
2288 for (face_index, names) in face_names.iter().enumerate() {
2289 let face_index = face_index as u32;
2290 for key in names {
2291 if key == normalized_name {
2292 return Some(face_index);
2293 }
2294 if !key.is_empty()
2295 && partial_match.is_none()
2296 && (key.contains(normalized_name) || normalized_name.contains(key.as_str()))
2297 {
2298 partial_match = Some(face_index);
2299 }
2300 }
2301 }
2302 partial_match
2303}
2304
2305fn is_supported_font_path(path: &Path) -> bool {
2306 matches!(
2307 path.extension()
2308 .and_then(|s| s.to_str())
2309 .map(|s| s.to_ascii_lowercase())
2310 .as_deref(),
2311 Some("ttf" | "otf" | "ttc")
2312 )
2313}
2314
2315fn font_path_priority(path: &Path) -> (u8, u8, String) {
2316 let name_original = path.file_name().and_then(|s| s.to_str()).unwrap_or("");
2317 let name = name_original.to_ascii_lowercase();
2318 let ext_score = match path
2319 .extension()
2320 .and_then(|s| s.to_str())
2321 .map(|s| s.to_ascii_lowercase())
2322 .as_deref()
2323 {
2324 Some("ttf") | Some("otf") => 0,
2325 Some("ttc") => 1,
2326 _ => 2,
2327 };
2328 let family_score = if name.contains("ms pgothic")
2329 || name.contains("mspgothic")
2330 || name.contains("ms-pgothic")
2331 || name.contains("msgothic")
2332 || name_original.contains("MS Pゴシック")
2333 || name_original.contains("MS PGothic")
2334 {
2335 0
2336 } else if name.contains("pgothic") || name_original.contains("Pゴシック") {
2337 1
2338 } else if name.contains("gothic") || name_original.contains("ゴシック") {
2339 2
2340 } else {
2341 3
2342 };
2343 (family_score, ext_score, name)
2344}
2345
2346fn project_font_dirs(project_dir: &Path) -> Vec<PathBuf> {
2347 let mut dirs = vec![project_dir.join("font"), project_dir.join("fonts")];
2348 if let Ok(exe) = std::env::current_exe()
2349 && let Some(exe_dir) = exe.parent()
2350 {
2351 dirs.push(exe_dir.join("font"));
2352 dirs.push(exe_dir.join("fonts"));
2353 }
2354 let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
2355 dirs.push(manifest_dir.join("assets").join("font"));
2356 dirs.push(manifest_dir.join("assets").join("fonts"));
2357 dirs
2358}
2359
2360fn platform_font_candidates_for_name(name: &str) -> Vec<PathBuf> {
2361 let key = normalize_font_name_for_match(name.trim_start_matches('@'));
2362 let mut out = Vec::new();
2363
2364 #[cfg(target_os = "windows")]
2365 {
2366 let windir = std::env::var_os("WINDIR")
2367 .map(PathBuf::from)
2368 .unwrap_or_else(|| PathBuf::from(r"C:\Windows"));
2369 let fonts = windir.join("Fonts");
2370 let names: &[&str] = if key.contains("msgothic") || key.contains("msゴシック") {
2371 &["msgothic.ttc", "msgothic.ttf"]
2372 } else if key.contains("msmincho") || key.contains("ms明朝") {
2373 &["msmincho.ttc", "msmincho.ttf"]
2374 } else if key.contains("meiryo") || key.contains("メイリオ") {
2375 &["meiryo.ttc", "meiryob.ttc"]
2376 } else if key.contains("yugoth") || key.contains("游ゴシック") {
2377 &["YuGothM.ttc", "YuGothR.ttc"]
2378 } else if key.contains("yumin") || key.contains("游明朝") {
2379 &["YuMincho.ttc"]
2380 } else {
2381 &[]
2382 };
2383 out.extend(names.iter().map(|file| fonts.join(file)));
2384 }
2385
2386 #[cfg(target_os = "macos")]
2387 {
2388 let dirs = [
2389 PathBuf::from("/System/Library/Fonts"),
2390 PathBuf::from("/Library/Fonts"),
2391 ];
2392 let names: &[&str] = if key.contains("gothic")
2393 || key.contains("ゴシック")
2394 || key.contains("meiryo")
2395 || key.contains("メイリオ")
2396 {
2397 &[
2398 "ヒラギノ角ゴシック W3.ttc",
2399 "ヒラギノ角ゴシック W6.ttc",
2400 "Hiragino Sans GB.ttc",
2401 ]
2402 } else if key.contains("mincho") || key.contains("明朝") {
2403 &["ヒラギノ明朝 ProN.ttc", "Hiragino Mincho ProN.ttc"]
2404 } else {
2405 &[]
2406 };
2407 for dir in dirs {
2408 out.extend(names.iter().map(|file| dir.join(file)));
2409 }
2410 }
2411
2412 #[cfg(target_os = "linux")]
2413 {
2414 let names: &[&str] = if key.contains("mincho") || key.contains("明朝") {
2415 &[
2416 "/usr/share/fonts/opentype/noto/NotoSerifCJK-Regular.ttc",
2417 "/usr/share/fonts/truetype/dejavu/DejaVuSerif.ttf",
2418 ]
2419 } else {
2420 &[
2421 "/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc",
2422 "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
2423 ]
2424 };
2425 out.extend(names.iter().map(|name| PathBuf::from(*name)));
2426 }
2427
2428 out
2429}
2430
2431fn platform_font_candidates() -> Vec<PathBuf> {
2432 let mut out = Vec::new();
2433
2434 #[cfg(target_os = "windows")]
2435 {
2436 let windir = std::env::var_os("WINDIR")
2437 .map(PathBuf::from)
2438 .unwrap_or_else(|| PathBuf::from(r"C:\Windows"));
2439 let fonts = windir.join("Fonts");
2440 out.push(fonts.join("msgothic.ttc"));
2441 out.push(fonts.join("msgothic.ttf"));
2442 out.push(fonts.join("YuGothM.ttc"));
2443 out.push(fonts.join("YuGothR.ttc"));
2444 }
2445
2446 #[cfg(target_os = "macos")]
2447 {
2448 out.push(PathBuf::from(
2449 "/System/Library/Fonts/ヒラギノ角ゴシック W3.ttc",
2450 ));
2451 out.push(PathBuf::from(
2452 "/System/Library/Fonts/ヒラギノ角ゴシック W4.ttc",
2453 ));
2454 out.push(PathBuf::from(
2455 "/System/Library/Fonts/Supplemental/Arial Unicode.ttf",
2456 ));
2457 out.push(PathBuf::from(
2458 "/System/Library/Fonts/Supplemental/Osaka.ttf",
2459 ));
2460 }
2461
2462 #[cfg(any(target_os = "linux", target_os = "freebsd", target_os = "android"))]
2463 {
2464 out.push(PathBuf::from(
2465 "/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc",
2466 ));
2467 out.push(PathBuf::from(
2468 "/usr/share/fonts/truetype/noto/NotoSansCJK-Regular.ttc",
2469 ));
2470 out.push(PathBuf::from(
2471 "/usr/share/fonts/opentype/noto/NotoSansCJKjp-Regular.otf",
2472 ));
2473 out.push(PathBuf::from(
2474 "/usr/share/fonts/truetype/fonts-japanese-gothic.ttf",
2475 ));
2476 }
2477
2478 out
2479}
2480
2481fn draw_glyph_5x7(rgba: &mut [u8], w: u32, h: u32, x: u32, y: u32, ch: char, scale: u32) {
2482 let glyph = glyph_5x7(ch);
2483 for (row, bits) in glyph.iter().enumerate() {
2484 for col in 0..5 {
2485 if (bits >> (4 - col)) & 1 == 0 {
2486 continue;
2487 }
2488 let px = x + col as u32 * scale;
2489 let py = y + row as u32 * scale;
2490 for sy in 0..scale {
2491 let yy = py + sy;
2492 if yy >= h {
2493 continue;
2494 }
2495 for sx in 0..scale {
2496 let xx = px + sx;
2497 if xx >= w {
2498 continue;
2499 }
2500 let idx = ((yy * w + xx) * 4) as usize;
2501 rgba[idx] = 255;
2502 rgba[idx + 1] = 255;
2503 rgba[idx + 2] = 255;
2504 rgba[idx + 3] = 255;
2505 }
2506 }
2507 }
2508 }
2509}
2510
2511fn glyph_5x7(ch: char) -> [u8; 7] {
2512 match ch.to_ascii_uppercase() {
2513 'A' => [0x0E, 0x11, 0x11, 0x1F, 0x11, 0x11, 0x11],
2514 'B' => [0x1E, 0x11, 0x11, 0x1E, 0x11, 0x11, 0x1E],
2515 'C' => [0x0E, 0x11, 0x10, 0x10, 0x10, 0x11, 0x0E],
2516 'D' => [0x1E, 0x11, 0x11, 0x11, 0x11, 0x11, 0x1E],
2517 'E' => [0x1F, 0x10, 0x10, 0x1E, 0x10, 0x10, 0x1F],
2518 'F' => [0x1F, 0x10, 0x10, 0x1E, 0x10, 0x10, 0x10],
2519 'G' => [0x0E, 0x11, 0x10, 0x17, 0x11, 0x11, 0x0E],
2520 'H' => [0x11, 0x11, 0x11, 0x1F, 0x11, 0x11, 0x11],
2521 'I' => [0x0E, 0x04, 0x04, 0x04, 0x04, 0x04, 0x0E],
2522 'J' => [0x01, 0x01, 0x01, 0x01, 0x11, 0x11, 0x0E],
2523 'K' => [0x11, 0x12, 0x14, 0x18, 0x14, 0x12, 0x11],
2524 'L' => [0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x1F],
2525 'M' => [0x11, 0x1B, 0x15, 0x15, 0x11, 0x11, 0x11],
2526 'N' => [0x11, 0x19, 0x15, 0x13, 0x11, 0x11, 0x11],
2527 'O' => [0x0E, 0x11, 0x11, 0x11, 0x11, 0x11, 0x0E],
2528 'P' => [0x1E, 0x11, 0x11, 0x1E, 0x10, 0x10, 0x10],
2529 'Q' => [0x0E, 0x11, 0x11, 0x11, 0x15, 0x12, 0x0D],
2530 'R' => [0x1E, 0x11, 0x11, 0x1E, 0x14, 0x12, 0x11],
2531 'S' => [0x0F, 0x10, 0x10, 0x0E, 0x01, 0x01, 0x1E],
2532 'T' => [0x1F, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04],
2533 'U' => [0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x0E],
2534 'V' => [0x11, 0x11, 0x11, 0x11, 0x11, 0x0A, 0x04],
2535 'W' => [0x11, 0x11, 0x11, 0x15, 0x15, 0x15, 0x0A],
2536 'X' => [0x11, 0x11, 0x0A, 0x04, 0x0A, 0x11, 0x11],
2537 'Y' => [0x11, 0x11, 0x0A, 0x04, 0x04, 0x04, 0x04],
2538 'Z' => [0x1F, 0x01, 0x02, 0x04, 0x08, 0x10, 0x1F],
2539 '0' => [0x0E, 0x11, 0x13, 0x15, 0x19, 0x11, 0x0E],
2540 '1' => [0x04, 0x0C, 0x04, 0x04, 0x04, 0x04, 0x0E],
2541 '2' => [0x0E, 0x11, 0x01, 0x02, 0x04, 0x08, 0x1F],
2542 '3' => [0x1E, 0x01, 0x01, 0x06, 0x01, 0x01, 0x1E],
2543 '4' => [0x02, 0x06, 0x0A, 0x12, 0x1F, 0x02, 0x02],
2544 '5' => [0x1F, 0x10, 0x10, 0x1E, 0x01, 0x01, 0x1E],
2545 '6' => [0x0E, 0x10, 0x10, 0x1E, 0x11, 0x11, 0x0E],
2546 '7' => [0x1F, 0x01, 0x02, 0x04, 0x08, 0x08, 0x08],
2547 '8' => [0x0E, 0x11, 0x11, 0x0E, 0x11, 0x11, 0x0E],
2548 '9' => [0x0E, 0x11, 0x11, 0x0F, 0x01, 0x01, 0x0E],
2549 ':' => [0x00, 0x04, 0x04, 0x00, 0x04, 0x04, 0x00],
2550 '.' => [0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x06],
2551 ',' => [0x00, 0x00, 0x00, 0x00, 0x06, 0x06, 0x04],
2552 '-' => [0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x00],
2553 '_' => [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F],
2554 '/' => [0x01, 0x02, 0x04, 0x08, 0x10, 0x00, 0x00],
2555 '\\' => [0x10, 0x08, 0x04, 0x02, 0x01, 0x00, 0x00],
2556 '[' => [0x0E, 0x08, 0x08, 0x08, 0x08, 0x08, 0x0E],
2557 ']' => [0x0E, 0x02, 0x02, 0x02, 0x02, 0x02, 0x0E],
2558 '(' => [0x02, 0x04, 0x08, 0x08, 0x08, 0x04, 0x02],
2559 ')' => [0x08, 0x04, 0x02, 0x02, 0x02, 0x04, 0x08],
2560 '#' => [0x0A, 0x0A, 0x1F, 0x0A, 0x1F, 0x0A, 0x0A],
2561 '+' => [0x00, 0x04, 0x04, 0x1F, 0x04, 0x04, 0x00],
2562 '=' => [0x00, 0x1F, 0x00, 0x1F, 0x00, 0x00, 0x00],
2563 '*' => [0x00, 0x11, 0x0A, 0x1F, 0x0A, 0x11, 0x00],
2564 '?' => [0x0E, 0x11, 0x01, 0x02, 0x04, 0x00, 0x04],
2565 '!' => [0x04, 0x04, 0x04, 0x04, 0x04, 0x00, 0x04],
2566 '>' => [0x10, 0x08, 0x04, 0x02, 0x04, 0x08, 0x10],
2567 '<' => [0x01, 0x02, 0x04, 0x08, 0x04, 0x02, 0x01],
2568 '|' => [0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04],
2569 ' ' => [0x00; 7],
2570 _ => [0x1F, 0x11, 0x15, 0x15, 0x15, 0x11, 0x1F],
2571 }
2572}
2573
2574#[cfg(test)]
2575mod font_shadow_mode_tests {
2576 use super::*;
2577
2578 #[test]
2579 fn original_four_state_shadow_mode_mapping() {
2580 assert_eq!(font_shadow_mode_flags(0), (false, false));
2581 assert_eq!(font_shadow_mode_flags(1), (true, false));
2582 assert_eq!(font_shadow_mode_flags(2), (false, true));
2583 assert_eq!(font_shadow_mode_flags(3), (true, true));
2584 }
2585
2586 #[test]
2587 fn invalid_shadow_modes_are_clamped_like_runtime_config() {
2588 assert_eq!(font_shadow_mode_flags(-1), (false, false));
2589 assert_eq!(font_shadow_mode_flags(99), (true, true));
2590 }
2591
2592 #[test]
2593 fn gdi_gray8_uses_the_original_65_level_alpha_table() {
2594 assert_eq!(gdi_gray8_alpha_from_coverage(0.0), 0);
2595 assert_eq!(gdi_gray8_alpha_from_coverage(1.0), 251);
2596 assert_eq!(gdi_gray8_alpha_from_coverage(32.0 / 64.0), 125);
2597 }
2598
2599 #[test]
2600 fn normal_fuchi_is_centered_around_the_body_pixel() {
2601 let mut rgba = vec![0u8; 5 * 5 * 4];
2602 draw_glyph_bitmap_face(
2603 &mut rgba,
2604 5,
2605 5,
2606 2 + fuchi_texture_offset(),
2607 2 + fuchi_texture_offset(),
2608 1,
2609 1,
2610 &[255],
2611 2,
2612 (0, 0, 0, 255),
2613 );
2614
2615 for y in 0..5usize {
2616 for x in 0..5usize {
2617 let alpha = rgba[(y * 5 + x) * 4 + 3];
2618 let expected = (1..=3).contains(&x) && (1..=3).contains(&y);
2619 assert_eq!(alpha != 0, expected, "unexpected fuchi pixel at ({x},{y})");
2620 }
2621 }
2622 }
2623
2624 #[test]
2625 fn repeated_face_alpha_uses_tona3_integer_truncation() {
2626 let mut rgba = vec![0u8; 4];
2627 blend_rgba_pixel(&mut rgba, 1, 0, 0, 0, 0, 0, 128);
2628 blend_rgba_pixel(&mut rgba, 1, 0, 0, 0, 0, 0, 128);
2629 assert_eq!(rgba[3], 192);
2630 }
2631
2632 #[test]
2633 fn blend_over_opaque_destination_does_not_overflow() {
2634 let mut rgba = vec![255u8; 4]; blend_rgba_pixel(&mut rgba, 1, 0, 0, 255, 255, 255, 128);
2639 assert_eq!(rgba[0], 255);
2641 assert_eq!(rgba[3], 255);
2642 }
2643
2644 #[test]
2645 fn coloured_glyph_blend_uses_wide_original_integer_arithmetic() {
2646 let mut rgba = [255, 220, 192, 255];
2647 blend_rgba_pixel(&mut rgba, 1, 0, 0, 80, 160, 240, 128);
2648 assert_eq!(rgba, [167, 190, 216, 255]);
2649 }
2650}