Skip to main content

atmos/kernel/
vector_font.rs

1// src/vector_font.rs - ベクトルフォント(TrueType)レンダリングエンジン
2// ttf-parser を使用したアウトライン抽出、4x4スーパーサンプリングEven-Oddラスタライザ、グリフキャッシュ。
3
4#![allow(dead_code)]
5
6extern crate alloc;
7use alloc::collections::BTreeMap;
8use alloc::string::String;
9use alloc::vec::Vec;
10
11/// Brotli 圧縮されたデータを取り、展開されたバイト列を返す。
12/// Brotli 展開用のヒープ確保アロケータ。
13///
14/// 【2026-08-03 発見・修正】従来は `alloc_no_stdlib::StackAllocator` に
15/// **型ごとにスロットを 1 個**しか渡していなかった(`[&mut [u8]; 1]`)。
16/// Brotli は同じ型の領域を複数同時に要求するため、2 個目以降が取れず
17/// `BrotliDecompressStream` から**戻ってこなくなる**。
18///
19/// 実測(`fa-v4compatibility.woff2`, 4132 バイト):
20/// ```text
21/// [FONT][BR] u8 プール確保 完了
22/// [FONT][BR] u32 プール確保 完了
23/// [FONT][BR] HuffmanCode プール確保 完了
24/// [FONT][BR] BrotliState 構築 完了
25/// [FONT][BR] 展開開始 in=4060 out_cap=8750
26/// (以降なし。待機 300/420/540 秒すべて同じ地点で停止)
27/// ```
28///
29/// カーネルには 512MB のヒープがあるので、固定プールをやめて
30/// **必要なだけヒープから確保する**。プールサイズの見積りが不要になり、
31/// 「大きいフォントで枯渇して落ちる/止まる」という失敗モードごと消える。
32struct HeapBuf<T>(Vec<T>);
33
34impl<T> Default for HeapBuf<T> {
35    fn default() -> Self {
36        Self(Vec::new())
37    }
38}
39
40impl<T> alloc_no_stdlib::SliceWrapper<T> for HeapBuf<T> {
41    fn slice(&self) -> &[T] {
42        &self.0
43    }
44}
45
46impl<T> alloc_no_stdlib::SliceWrapperMut<T> for HeapBuf<T> {
47    fn slice_mut(&mut self) -> &mut [T] {
48        &mut self.0
49    }
50}
51
52struct HeapAllocator;
53
54impl<T: Clone + Default> alloc_no_stdlib::Allocator<T> for HeapAllocator {
55    type AllocatedMemory = HeapBuf<T>;
56
57    /// 計算量: **O(len)**(ゼロ初期化)。
58    fn alloc_cell(&mut self, len: usize) -> HeapBuf<T> {
59        HeapBuf(alloc::vec![T::default(); len])
60    }
61
62    /// 解放は `Vec` の Drop に任せる。
63    fn free_cell(&mut self, _data: HeapBuf<T>) {}
64}
65
66/// WOFF2 の Brotli ストリームを展開する。
67///
68/// 計算量: **O(展開後サイズ)**。
69///
70/// 失敗は必ずログに残す(`spec/logging_policy.md` L-1)。
71pub fn decompress_brotli(compressed: &[u8], decompressed_size: usize) -> Option<Vec<u8>> {
72    let mut s = brotli_decompressor::BrotliState::new(
73        HeapAllocator,
74        HeapAllocator,
75        HeapAllocator,
76    );
77
78    // 展開結果が 0 バイトの場合に `available_out` が最初から 0 だと
79    // ストリーミングデコーダが `ResultSuccess` ではなく `NeedsMoreOutput` を
80    // 返すため、最小 16 バイトの余裕を持たせる(2026-07-22 に自己テストで確認)。
81    let mut out_vec = alloc::vec![0u8; decompressed_size.max(16)];
82
83    let mut available_in: usize = compressed.len();
84    let mut input_offset: usize = 0;
85    let mut available_out: usize = out_vec.len();
86    let mut output_offset: usize = 0;
87    let mut total_out: usize = 0;
88
89    let result = brotli_decompressor::BrotliDecompressStream(
90        &mut available_in,
91        &mut input_offset,
92        compressed,
93        &mut available_out,
94        &mut output_offset,
95        &mut out_vec,
96        &mut total_out,
97        &mut s,
98    );
99
100    if matches!(result, brotli_decompressor::BrotliResult::ResultSuccess) {
101        out_vec.truncate(total_out);
102        Some(out_vec)
103    } else {
104        crate::warn!(
105            "[FONT][BR] Brotli 展開に失敗 result={:?} in={} total_out={} in_left={}",
106            result,
107            compressed.len(),
108            total_out,
109            available_in
110        );
111        None
112    }
113}
114
115static WOFF2_KNOWN_TAGS: [&[u8; 4]; 63] = [
116    b"cmap", b"head", b"hhea", b"hmtx", b"maxp", b"name", b"OS/2", b"post",
117    b"cvt ", b"fpgm", b"glyf", b"loca", b"prep", b"CFF ", b"VORG", b"EBDT",
118    b"EBLC", b"gasp", b"hdmx", b"kern", b"LTSH", b"PCLT", b"VDMX", b"vhea",
119    b"vmtx", b"BASE", b"GDEF", b"GPOS", b"GSUB", b"JSTF", b"DSIG", b"MATH",
120    b"CBDT", b"CBLC", b"COLR", b"CPAL", b"SVG ", b"sbix", b"acnt", b"avar",
121    b"bdat", b"bloc", b"bsln", b"cvar", b"fdsc", b"fmtx", b"fvar", b"gvar",
122    b"hsty", b"just", b"lcar", b"mort", b"morx", b"opbd", b"prop", b"trak",
123    b"Zapf", b"Silf", b"Gloc", b"Glat", b"Feat", b"Sill", b"SVG ",
124];
125
126fn read_woff2_uint16(data: &[u8], offset: &mut usize) -> Option<u16> {
127    if *offset + 2 > data.len() {
128        return None;
129    }
130    let val = u16::from_be_bytes([data[*offset], data[*offset + 1]]);
131    *offset += 2;
132    Some(val)
133}
134
135fn read_woff2_uint32(data: &[u8], offset: &mut usize) -> Option<u32> {
136    if *offset + 4 > data.len() {
137        return None;
138    }
139    let val = u32::from_be_bytes([
140        data[*offset],
141        data[*offset + 1],
142        data[*offset + 2],
143        data[*offset + 3],
144    ]);
145    *offset += 4;
146    Some(val)
147}
148
149fn read_woff2_base128(data: &[u8], offset: &mut usize) -> Option<u32> {
150    let mut accum: u32 = 0;
151    for _ in 0..5 {
152        if *offset >= data.len() {
153            return None;
154        }
155        let code = data[*offset];
156        *offset += 1;
157        if accum & 0xFE00_0000 != 0 {
158            return None;
159        }
160        accum = (accum << 7) | ((code & 0x7F) as u32);
161        if (code & 0x80) == 0 {
162            return Some(accum);
163        }
164    }
165    None
166}
167
168struct Woff2Table {
169    tag: [u8; 4],
170    orig_length: u32,
171    transform_length: Option<u32>,
172    flags: u8,
173}
174
175/// WOFF2 形式のフォントデータを受取り、TTF/OTF (sfnt) バイト列に展開する。
176pub fn decompress_woff2(bytes: &[u8]) -> Option<Vec<u8>> {
177    if bytes.len() < 48 || &bytes[0..4] != b"wOF2" {
178        return None;
179    }
180    let mut offset = 4;
181    let flavor = read_woff2_uint32(bytes, &mut offset)?;
182    let _length = read_woff2_uint32(bytes, &mut offset)?;
183    let num_tables = read_woff2_uint16(bytes, &mut offset)? as usize;
184    let _reserved = read_woff2_uint16(bytes, &mut offset)?;
185    let total_sfnt_size = read_woff2_uint32(bytes, &mut offset)? as usize;
186    let total_compressed_size = read_woff2_uint32(bytes, &mut offset)? as usize;
187    let _major_version = read_woff2_uint16(bytes, &mut offset)?;
188    let _minor_version = read_woff2_uint16(bytes, &mut offset)?;
189    let _meta_offset = read_woff2_uint32(bytes, &mut offset)?;
190    let _meta_length = read_woff2_uint32(bytes, &mut offset)?;
191    let _meta_orig_length = read_woff2_uint32(bytes, &mut offset)?;
192    let _priv_offset = read_woff2_uint32(bytes, &mut offset)?;
193    let _priv_length = read_woff2_uint32(bytes, &mut offset)?;
194
195    let mut tables = Vec::with_capacity(num_tables);
196    let mut total_uncompressed_stream_len: usize = 0;
197
198    for _ in 0..num_tables {
199        if offset >= bytes.len() {
200            return None;
201        }
202        let flags = bytes[offset];
203        offset += 1;
204        let tag_idx = flags & 0x3F;
205
206        let tag = if tag_idx == 63 {
207            if offset + 4 > bytes.len() {
208                return None;
209            }
210            let t = [bytes[offset], bytes[offset + 1], bytes[offset + 2], bytes[offset + 3]];
211            offset += 4;
212            t
213        } else if (tag_idx as usize) < WOFF2_KNOWN_TAGS.len() {
214            *WOFF2_KNOWN_TAGS[tag_idx as usize]
215        } else {
216            return None;
217        };
218
219        let orig_length = read_woff2_base128(bytes, &mut offset)?;
220        let transform_version = (flags >> 6) & 0x03;
221
222        // 【2026-08-05 発見・修正】変換の有無の判定が仕様と逆だった。
223        //
224        // WOFF2 仕様では **`glyf` と `loca` だけ意味が反転**する:
225        //
226        // | テーブル        | version 0 | version 3 |
227        // |-----------------|-----------|-----------|
228        // | `glyf` / `loca` | **変換あり** | 変換なし |
229        // | その他          | 変換なし  | 変換あり  |
230        //
231        // 以前は「0 = 変換なし」と決め打ちしていたため、変換された `glyf` を
232        // 「変換なし」と誤判定し、`transformLength` を読まずに元長で切り出していた。
233        // **ストリーム上の位置がそこから全部ずれる**うえ、変換済みバイト列を
234        // そのまま SFNT の `glyf` として書いていた。
235        //
236        // 結果、`head`/`cmap` は無事なのでフォントはパースに成功し
237        // `glyph_index()` も引けるが、輪郭データが壊れているため
238        // Font Awesome のアイコンが箱(☐)として描かれていた。
239        let is_glyf_or_loca = &tag == b"glyf" || &tag == b"loca";
240        let transformed = if is_glyf_or_loca {
241            transform_version == 0
242        } else {
243            transform_version != 0
244        };
245
246        let transform_length = if transformed {
247            let tl = read_woff2_base128(bytes, &mut offset)?;
248            total_uncompressed_stream_len += tl as usize;
249            Some(tl)
250        } else {
251            total_uncompressed_stream_len += orig_length as usize;
252            None
253        };
254
255        tables.push(Woff2Table {
256            tag,
257            orig_length,
258            transform_length,
259            flags,
260        });
261    }
262
263    if offset + total_compressed_size > bytes.len() {
264        return None;
265    }
266
267    let compressed_data = &bytes[offset..offset + total_compressed_size];
268    // 【2026-07-31診断】Font Awesome の woff2 展開で OOM した件。
269    // 必要な展開後サイズを記録し、スクラッチバッファが足りているか確認する。
270    crate::warn!(
271        "[WOFF2] decompress compressed={} expected_uncompressed={}",
272        compressed_data.len(),
273        total_uncompressed_stream_len
274    );
275    let decompressed_stream = decompress_brotli(compressed_data, total_uncompressed_stream_len)?;
276
277    // sfnt ファイルの再構築
278    let mut sfnt = Vec::with_capacity(total_sfnt_size.max(12 + num_tables * 16));
279    
280    // sfnt Header (12 bytes)
281    sfnt.extend_from_slice(&flavor.to_be_bytes());
282    sfnt.extend_from_slice(&(num_tables as u16).to_be_bytes());
283
284    let mut max_p2 = 1u16;
285    while max_p2 * 2 <= (num_tables as u16) {
286        max_p2 *= 2;
287    }
288    let search_range = max_p2 * 16;
289    let entry_selector = (16 - max_p2.leading_zeros() - 1) as u16;
290    let range_shift = (num_tables as u16) * 16 - search_range;
291
292    sfnt.extend_from_slice(&search_range.to_be_bytes());
293    sfnt.extend_from_slice(&entry_selector.to_be_bytes());
294    sfnt.extend_from_slice(&range_shift.to_be_bytes());
295
296    // sfnt Table Directory
297    let table_dir_offset = 12;
298    let data_start_offset = table_dir_offset + num_tables * 16;
299
300    let mut sorted_indices: Vec<usize> = (0..num_tables).collect();
301    sorted_indices.sort_by_key(|&idx| tables[idx].tag);
302
303    // 空の Table Directory を確保
304    sfnt.resize(data_start_offset, 0);
305
306    // 【2026-08-03 発見・修正】展開ストリームを**タグ順**に読んでいた。
307    //
308    // WOFF2 仕様では、テーブルデータは**テーブルディレクトリに現れた順**で
309    // 連結されている。タグ順に読むと各テーブルへ別のテーブルのバイト列が
310    // 割り当たり、`head` が壊れる。
311    // 実測: Brotli 展開は成功しているのに `VectorFont::new` が
312    // **`NoHeadTable`** を返していた(Font Awesome 全ファイル)。
313    //
314    // 正しくは「ストリームの切り出しはディレクトリ順」「ディレクトリの
315    // 出力はタグ順」と**分ける**。
316    let mut stream_offset = 0usize;
317    let mut table_ranges: Vec<(usize, usize)> = Vec::with_capacity(num_tables);
318    for table in tables.iter().take(num_tables) {
319        let len = table.orig_length as usize;
320        let read_len = table.transform_length.map(|l| l as usize).unwrap_or(len);
321        if stream_offset + read_len > decompressed_stream.len() {
322            crate::warn!(
323                "[FONT][WOFF2] 展開ストリームが短い tag={:?} 必要={} 残り={}",
324                core::str::from_utf8(&table.tag).unwrap_or("????"),
325                read_len,
326                decompressed_stream.len().saturating_sub(stream_offset)
327            );
328            return None;
329        }
330        table_ranges.push((stream_offset, stream_offset + read_len));
331        stream_offset += read_len;
332    }
333
334    // 【2026-08-05】変換された `glyf` を復元し、`loca` も作り直す。
335    //
336    // WOFF2 は `glyf`/`loca` を変換して格納する(仕様 §5.1)。
337    // 復元しないと輪郭データが壊れたままで、フォントはパースに成功するのに
338    // 字形が箱(☐)になる(実測: Font Awesome のアイコン全部)。
339    //
340    // 復元に失敗したら**フォント全体を諦める**(W-3)。
341    // 壊れたテーブルを書くより、代替フォントで描く方がまし。
342    let mut rebuilt: alloc::collections::BTreeMap<usize, Vec<u8>> =
343        alloc::collections::BTreeMap::new();
344    {
345        let glyf_idx = tables.iter().take(num_tables).position(|t| {
346            &t.tag == b"glyf" && t.transform_length.is_some()
347        });
348        if let Some(gi) = glyf_idx {
349            let (rs, re) = table_ranges[gi];
350            let raw = decompressed_stream.get(rs..re)?;
351            match crate::kernel::woff2_glyf::reconstruct_glyf(raw) {
352                Some((glyf_data, loca_data, _fmt)) => {
353                    crate::warn!(
354                        "[FONT][WOFF2] glyf 変換を復元 {} -> {} バイト(loca {} バイト)",
355                        raw.len(),
356                        glyf_data.len(),
357                        loca_data.len()
358                    );
359                    rebuilt.insert(gi, glyf_data);
360                    if let Some(li) = tables
361                        .iter()
362                        .take(num_tables)
363                        .position(|t| &t.tag == b"loca")
364                    {
365                        rebuilt.insert(li, loca_data);
366                    }
367                }
368                None => {
369                    crate::warn!(
370                        "[FONT][WOFF2] glyf 変換の復元に失敗({} バイト)",
371                        raw.len()
372                    );
373                    return None;
374                }
375            }
376        }
377    }
378
379    let mut current_data_offset = data_start_offset;
380
381    for &idx in &sorted_indices {
382        let table = &tables[idx];
383        let len = table.orig_length as usize;
384        let (rs, re) = table_ranges[idx];
385        // 復元済みテーブル(`glyf`/`loca`)があればそちらを使う。
386        let table_raw: &[u8] = match rebuilt.get(&idx) {
387            Some(v) => v.as_slice(),
388            None => &decompressed_stream[rs..re],
389        };
390        let len = if rebuilt.contains_key(&idx) {
391            table_raw.len()
392        } else {
393            len
394        };
395
396        // チェックサムの計算
397        let mut sum: u32 = 0;
398        let mut i = 0;
399        while i < table_raw.len() {
400            let b0 = table_raw[i] as u32;
401            let b1 = table_raw.get(i + 1).copied().unwrap_or(0) as u32;
402            let b2 = table_raw.get(i + 2).copied().unwrap_or(0) as u32;
403            let b3 = table_raw.get(i + 3).copied().unwrap_or(0) as u32;
404            sum = sum.wrapping_add((b0 << 24) | (b1 << 16) | (b2 << 8) | b3);
405            i += 4;
406        }
407
408        let dir_entry_pos = table_dir_offset + sorted_indices.iter().position(|&x| x == idx)? * 16;
409        sfnt[dir_entry_pos..dir_entry_pos + 4].copy_from_slice(&table.tag);
410        sfnt[dir_entry_pos + 4..dir_entry_pos + 8].copy_from_slice(&sum.to_be_bytes());
411        sfnt[dir_entry_pos + 8..dir_entry_pos + 12].copy_from_slice(&(current_data_offset as u32).to_be_bytes());
412        sfnt[dir_entry_pos + 12..dir_entry_pos + 16].copy_from_slice(&(len as u32).to_be_bytes());
413
414        sfnt.extend_from_slice(table_raw);
415        let padding = (4 - (table_raw.len() % 4)) % 4;
416        sfnt.extend(core::iter::repeat_n(0u8, padding));
417        current_data_offset += table_raw.len() + padding;
418    }
419
420    Some(sfnt)
421}
422
423use spin::Mutex;
424use ttf_parser::{Face, OutlineBuilder};
425
426fn f32_floor(x: f32) -> i32 {
427    if x >= 0.0 {
428        x as i32
429    } else {
430        (x - 1.0) as i32
431    }
432}
433
434fn f32_ceil(x: f32) -> i32 {
435    if x >= 0.0 {
436        (x + 1.0) as i32
437    } else {
438        x as i32
439    }
440}
441
442#[derive(Debug, Clone, Copy, PartialEq)]
443struct Point {
444    x: f32,
445    y: f32,
446}
447
448#[derive(Debug, Clone)]
449enum Segment {
450    Line(Point, Point),
451}
452
453/// グリフのアウトライン(直線線分リスト)を構築するビルダー
454struct GlyphOutlineBuilder {
455    segments: Vec<Segment>,
456    current: Point,
457    start: Point,
458    scale: f32,
459}
460
461impl GlyphOutlineBuilder {
462    fn new(scale: f32) -> Self {
463        Self {
464            segments: Vec::new(),
465            current: Point { x: 0.0, y: 0.0 },
466            start: Point { x: 0.0, y: 0.0 },
467            scale,
468        }
469    }
470
471    /// 2次ベジェ曲線を再帰/ステップ分割して直線セグメントにフラット化します
472    fn flatten_quad(&mut self, p0: Point, p1: Point, p2: Point) {
473        let steps = 6; // 計算量と品質のバランスが最適なステップ数
474        let mut prev = p0;
475        for i in 1..=steps {
476            let t = (i as f32) / (steps as f32);
477            let mt = 1.0 - t;
478            // 2次ベジェ曲線の方程式
479            let x = mt * mt * p0.x + 2.0 * mt * t * p1.x + t * t * p2.x;
480            let y = mt * mt * p0.y + 2.0 * mt * t * p1.y + t * t * p2.y;
481            let current = Point { x, y };
482            self.segments.push(Segment::Line(prev, current));
483            prev = current;
484        }
485    }
486}
487
488impl OutlineBuilder for GlyphOutlineBuilder {
489    fn move_to(&mut self, x: f32, y: f32) {
490        let p = Point {
491            x: x * self.scale,
492            y: y * self.scale,
493        };
494        self.current = p;
495        self.start = p;
496    }
497
498    fn line_to(&mut self, x: f32, y: f32) {
499        let p = Point {
500            x: x * self.scale,
501            y: y * self.scale,
502        };
503        self.segments.push(Segment::Line(self.current, p));
504        self.current = p;
505    }
506
507    fn quad_to(&mut self, x1: f32, y1: f32, x: f32, y: f32) {
508        let p1 = Point {
509            x: x1 * self.scale,
510            y: y1 * self.scale,
511        };
512        let p2 = Point {
513            x: x * self.scale,
514            y: y * self.scale,
515        };
516        self.flatten_quad(self.current, p1, p2);
517        self.current = p2;
518    }
519
520    fn curve_to(&mut self, _x1: f32, _y1: f32, _x2: f32, _y2: f32, x: f32, y: f32) {
521        // TrueTypeフォント(2次ベジェ)では基本的に呼ばれないが、
522        // 3次ベジェが含まれる場合は線形補間で対応
523        let p2 = Point {
524            x: x * self.scale,
525            y: y * self.scale,
526        };
527        self.segments.push(Segment::Line(self.current, p2));
528        self.current = p2;
529    }
530
531    fn close(&mut self) {
532        if self.current != self.start {
533            self.segments.push(Segment::Line(self.current, self.start));
534        }
535    }
536}
537
538/// ヒープメモリにキャッシュされるラスタライズ済みグリフデータ
539#[derive(Clone)]
540pub struct CachedGlyph {
541    pub width: u32,
542    pub height: u32,
543    pub x_offset: i32,
544    pub y_offset: i32,
545    pub advance: u32,
546    pub data: Vec<u8>, // A8 フォーマットのアルファマスク (0-255)
547}
548
549/// アルファブレンド済みのカラーグリフデータ
550#[derive(Clone)]
551pub struct CachedColorGlyph {
552    pub width: u32,
553    pub height: u32,
554    pub x_offset: i32,
555    pub y_offset: i32,
556    pub advance: u32,
557    pub data: Vec<u32>, // ARGB8888 形式のブレンド済みピクセルカラー (透明部分は0)
558}
559
560/// 動的フォントキャッシュとフォントロードを管理するマネージャ
561pub struct VectorFont {
562    face: Face<'static>,
563    /// フォールバックフォント(プライマリにグリフが無い文字をこの順で補う)。
564    fallbacks: Vec<Face<'static>>,
565    cache: BTreeMap<(char, u32), CachedGlyph>, // (文字, フォントサイズ) -> キャッシュグリフ
566    color_cache: BTreeMap<(char, u32, u32, u32), CachedColorGlyph>, // (文字, サイズ, 文字色, 背景色) -> キャッシュカラーグリフ
567    advance_cache: BTreeMap<(char, u32), u32>,                      // (文字, サイズ) -> advance幅
568}
569
570/// フォントに存在しない記号類を、視覚的に近い代替文字へマッピングする。
571/// (M PLUS 2 などに含まれない矢印・約物・記号の文字化け/欠落を防ぐ)
572fn fallback_glyph_char(c: char) -> Option<char> {
573    let sub = match c {
574        // 不可視文字可視化記号のフォールバック
575        '␣' => '_', // 半角空白 (Open Box) はアンダーバー
576        '⇥' => '>', // タブは不等号
577        '↵' => '<', // CRLFは不等号
578        '⇠' => '<', // CRは不等号
579        // ダッシュ・ハイフン類
580        '–' | '—' | '―' | '‐' | '‑' | '−' => '-',
581        // 引用符
582        '\u{2018}' | '\u{2019}' | '‚' | '‛' | '`' => '\'',
583        '\u{201C}' | '\u{201D}' | '„' | '‟' => '"',
584        // 中点・箇条書き
585        '•' | '◦' | '‣' | '·' | '∙' => '*',
586        // 乗算・除算
587        '×' => 'x',
588        '÷' => '/',
589        // 省略記号(簡易的に1点へ)
590        '…' => '.',
591        // 全角スペースは呼び出し側で除外済みだが念のため通常空白へ
592        '\u{00A0}' | '\u{202F}' | '\u{2009}' | '\u{200A}' => ' ',
593        // FontAwesome(Private Use Area)アイコンのうち、明確で恣意的でない
594        // テキスト等価物を持つものだけの限定的なフォールバック。
595        //
596        // 【2026-08-05 修正】絵文字への代替(📄 🛡 🎓 💻 ⚙ 🏠 …)を **50 件撤去**した。
597        //
598        // 内蔵の日本語フォントはこれらの字形を持たないが **cmap には登録されていて
599        // 豆腐(□)が返る**。その結果、アイコン位置に ☐ が並んでいた
600        // (実測: `#apps` の一覧、実サイトには無い)。
601        //
602        // アイコンが出せないなら**何も描かない**方が実ブラウザの挙動に近い。
603        // 残すのは内蔵フォントが確実に持つ範囲(U+2600 未満の記号)だけ:
604        // 矢印・山形・チェック・引用符など。
605        //
606        // なお `@font-face` / WOFF2 の読込は**実装済み**になった
607        // (下記コメントは当時のもの)。Font Awesome は CFF ベースで、
608        // 取得・展開・登録まで通っている。
609        // アイコンフォントのグリフは常に欠落するが、矢印・情報記号のように
610        // 一般的なUnicode記号で意味が一意に近似できるものに限り代替する
611        // (それ以外の大半のアイコン、例えば microchip/leaf/home 等は恣意的な
612        // 代替になり「アイコンが無い」より「別の意味の文字が出る」方が誤解を
613        // 招くため、あえてマッピングせず既存の安全な欠落挙動のままにする)。
614        // コードポイントは実サイト www.sugi-lab.net(Font Awesome 7.0.0)の
615        // 実際のDOMから`getComputedStyle(el, '::before').content`で直接
616        // 検証済み(2026-07-21、記憶からの推測ではない)。
617        '\u{f060}' => '\u{2190}', // fa-arrow-left → ←
618        '\u{f061}' => '\u{2192}', // fa-arrow-right → →
619        '\u{f062}' => '\u{2191}', // fa-arrow-up → ↑
620        '\u{f063}' => '\u{2193}', // fa-arrow-down → ↓
621        '\u{f053}' => '\u{2039}', // fa-chevron-left → ‹
622        '\u{f054}' => '\u{203A}', // fa-chevron-right → ›
623        '\u{f077}' => '^',        // fa-chevron-up → ^
624        '\u{f078}' => 'v',        // fa-chevron-down → v(下向き山形に近似)
625        '\u{f104}' => '\u{2039}', // fa-angle-left → ‹
626        '\u{f105}' => '\u{203A}', // fa-angle-right → ›
627        '\u{f106}' => '^',        // fa-angle-up → ^
628        '\u{f107}' => 'v',        // fa-angle-down → v
629        '\u{f00c}' => '\u{2713}', // fa-check → ✓
630        '\u{f00d}' => '\u{2715}', // fa-times / fa-xmark → ✕
631        '\u{f05a}' => 'i',        // fa-info-circle → i(情報記号として一般的)
632        // 2026-07-24: 実サイト www.sugi-lab.net の全ページ(index/profile/
633        // smarthome/timeseries/forms/papers/access)を横断的に走査し、これまで
634        // 未マッピングだった FontAwesome アイコンクラスのコードポイントを
635        // `getComputedStyle(el, '::before').content`で実測・追加。ブランド
636        // アイコン(facebook/github/google/notion等)や、意味が一意に近似
637        // できないもの(filter/mouse-pointer/journal-whills等)は既存方針
638        // どおり対象外のまま。
639        '\u{f35d}' => '↗',        // fa-external-link-alt
640        '\u{f10d}' => '"',        // fa-quote-left
641        '\u{f10e}' => '"',        // fa-quote-right
642        _ => return None,
643    };
644    Some(sub)
645}
646
647impl VectorFont {
648    pub fn new(font_bytes: &'static [u8]) -> Result<Self, ttf_parser::FaceParsingError> {
649        let face = Face::parse(font_bytes, 0)?;
650        Ok(Self {
651            face,
652            fallbacks: Vec::new(),
653            cache: BTreeMap::new(),
654            color_cache: BTreeMap::new(),
655            advance_cache: BTreeMap::new(),
656        })
657    }
658
659    /// プライマリ + フォールバック群でフォントを構築する。
660    /// `fallback_bytes` の各フォントは、プライマリにグリフが無い文字を順に補う。
661    /// パース失敗したフォールバックは無視する(プライマリが失敗したらエラー)。
662    pub fn new_with_fallbacks(
663        font_bytes: &'static [u8],
664        fallback_bytes: &[&'static [u8]],
665    ) -> Result<Self, ttf_parser::FaceParsingError> {
666        let face = Face::parse(font_bytes, 0)?;
667        let mut fallbacks = Vec::new();
668        for fb in fallback_bytes {
669            if let Ok(f) = Face::parse(fb, 0) {
670                fallbacks.push(f);
671            }
672        }
673        Ok(Self {
674            face,
675            fallbacks,
676            cache: BTreeMap::new(),
677            color_cache: BTreeMap::new(),
678            advance_cache: BTreeMap::new(),
679        })
680    }
681
682    /// 新しいプライマリフォントへ切り替える。旧プライマリはフォールバック
683    /// チェーンの先頭へ退避するため、既存のグリフカバレッジは失われない
684    /// (`@font-face`で実行時に取得したフォントを、ページ全体で使う
685    /// 主要フォントとして採用する用途。2026-07-22新設)。キャッシュ済み
686    /// グリフは全てプライマリ変更前のフォントに基づくため破棄する。
687    pub fn set_primary(&mut self, font_bytes: &'static [u8]) -> Result<(), ttf_parser::FaceParsingError> {
688        let new_face = Face::parse(font_bytes, 0)?;
689        let old_primary = core::mem::replace(&mut self.face, new_face);
690        self.fallbacks.insert(0, old_primary);
691        self.cache.clear();
692        self.color_cache.clear();
693        self.advance_cache.clear();
694        Ok(())
695    }
696
697    /// 指定されたサイズでグリフをラスタライズ(またはキャッシュから取得)します
698    /// 【2026-08-05診断】この符号位置の字形を持っているか。
699    /// プライマリと代替の両方を見る(`get_glyph` と同じ探索)。
700    /// 【2026-08-05】**プライマリだけ**が字形を持つか。
701    ///
702    /// Google Fonts は unicode-range でファイルを分割して配信するため、
703    /// 「ウェイト 700 の Noto Sans JP」が Latin だけのサブセットということがある。
704    /// ウェイト指定で選んだフォントがその文字を持たないと、
705    /// 見出しの漢字が欠ける(実測: 「開発アプリケーション」→「□ 発プリケーション」)。
706    /// 選ぶ前にこれで確かめる。
707    pub fn primary_has_glyph(&self, c: char) -> bool {
708        self.face.glyph_index(c).is_some()
709    }
710
711    pub fn has_glyph(&self, c: char) -> bool {
712        // 【2026-08-05】私用領域は**プライマリだけ**を見る。
713        // 代替フォント(内蔵和文)は私用領域に豆腐の字形を持つため、
714        // 含めると「字形がある」と誤って報告してしまう
715        // (実際 `Noto Sans JP` でも `字形F02D=true` になっていた)。
716        if ('\u{e000}'..='\u{f8ff}').contains(&c) {
717            return self.face.glyph_index(c).is_some();
718        }
719        if self.face.glyph_index(c).is_some() {
720            return true;
721        }
722        self.fallbacks.iter().any(|f| f.glyph_index(c).is_some())
723    }
724
725    pub fn get_glyph(&mut self, c: char, size_px: u32) -> Option<&CachedGlyph> {
726        let key = (c, size_px);
727        if self.cache.contains_key(&key) {
728            return self.cache.get(&key);
729        }
730
731        if self.cache.len() >= 8000 {
732            crate::info!(
733                "[vector_font] Cache limit (8000) reached. Clearing glyph cache to free memory."
734            );
735            self.cache.clear();
736        }
737
738        // キャッシュミス:グリフを持つ face を プライマリ→フォールバック の順で選ぶ。
739        // face_idx 0 = プライマリ、1.. = fallbacks[idx-1]。
740        let (face_idx, glyph_id) = {
741            let find = |ch: char| -> Option<(usize, ttf_parser::GlyphId)> {
742                if let Some(id) = self.face.glyph_index(ch) {
743                    return Some((0, id));
744                }
745                // 【2026-08-05 発見・修正】私用領域(U+E000..U+F8FF)は
746                // アイコンフォント専用の領域。ここを内蔵フォントへ
747                // フォールバックさせてはいけない。
748                //
749                // 2026-08-04 に「字形が無い文字が消える」対策として
750                // 内蔵フォントチェーンを代替に加えたが、内蔵フォントは
751                // 私用領域に**箱(tofu)の字形を持っている**。その結果
752                // Font Awesome のアイコンが ☐ として描かれていた
753                // (実測: `字形F02D=true` が Noto Sans JP でも真だった)。
754                //
755                // アイコンが無いときは**何も描かない**のが正しい
756                // (実ブラウザもアイコンフォント未取得時は空白になる)。
757                if ('\u{e000}'..='\u{f8ff}').contains(&ch) {
758                    return None;
759                }
760                for (i, f) in self.fallbacks.iter().enumerate() {
761                    if let Some(id) = f.glyph_index(ch) {
762                        return Some((i + 1, id));
763                    }
764                }
765                None
766            };
767            match find(c) {
768                Some(x) => x,
769                None => {
770                    // どのフォントにも無い文字は、視覚的に近い代替文字で再探索する
771                    fallback_glyph_char(c).and_then(find)?
772                }
773            }
774        };
775        let face: &Face = if face_idx == 0 {
776            &self.face
777        } else {
778            &self.fallbacks[face_idx - 1]
779        };
780
781        let units_per_em = face.units_per_em() as f32;
782        let scale = (size_px as f32) / units_per_em;
783
784        // メトリクスの取得
785        // 四捨五入(`font_select::scaled_advance`。切り捨てると文字列が系統的に狭くなる)。
786        let advance = crate::kernel::font_select::scaled_advance(
787            face.glyph_hor_advance(glyph_id)?,
788            face.units_per_em(),
789            size_px,
790        );
791
792        // アウトラインを先に抽出する。スペース等の輪郭なし文字は glyph_bounding_box が None を
793        // 返すため、? で early return してしまい advance が捨てられるのを防ぐ。
794        let mut builder = GlyphOutlineBuilder::new(scale);
795        face.outline_glyph(glyph_id, &mut builder);
796
797        // 輪郭が存在しない文字(空白など): advance だけ持つ空グリフとしてキャッシュする。
798        // これを bounding_box の取得より前に行うことで、スペースの advance が正しく適用される。
799        if builder.segments.is_empty() {
800            let empty_glyph = CachedGlyph {
801                width: 0,
802                height: 0,
803                x_offset: 0,
804                y_offset: 0,
805                advance,
806                data: Vec::new(),
807            };
808            self.cache.insert(key, empty_glyph);
809            return self.cache.get(&key);
810        }
811
812        // 輪郭がある文字は必ず bounding_box が存在する。
813        let bounding_box = face.glyph_bounding_box(glyph_id)?;
814
815        // ttf-parserのY軸は上方向が正、画面座標は下方向が正であるため、スケーリングとY軸反転を行います
816        let scale_y = -scale;
817
818        // Y軸方向の座標変換(反転)をセグメントに適用
819        let mut segments = builder.segments;
820        for seg in &mut segments {
821            match seg {
822                Segment::Line(p1, p2) => {
823                    p1.y = -p1.y;
824                    p2.y = -p2.y;
825                }
826            }
827        }
828
829        // Bounding Box のスケーリングと画面座標への変換
830        let bx0 = bounding_box.x_min as f32 * scale;
831        let by0 = bounding_box.y_max as f32 * scale_y; // Y軸反転
832        let bx1 = bounding_box.x_max as f32 * scale;
833        let by1 = bounding_box.y_min as f32 * scale_y; // Y軸反転
834
835        let gx0 = f32_floor(bx0) - 1;
836        let gy0 = f32_floor(by0) - 1;
837        let gx1 = f32_ceil(bx1) + 1;
838        let gy1 = f32_ceil(by1) + 1;
839
840        let width = (gx1 - gx0).max(1) as u32;
841        let height = (gy1 - gy0).max(1) as u32;
842        let x_offset = gx0;
843        let y_offset = gy0;
844
845        if width > 500 || height > 500 || width == 0 || height == 0 {
846            crate::warn!(
847                "[NET][DIAG] glyph c={:?} units_per_em={} scale={} bbox=({},{},{},{}) w={} h={} face_idx={}",
848                c,
849                units_per_em,
850                scale,
851                bounding_box.x_min,
852                bounding_box.y_min,
853                bounding_box.x_max,
854                bounding_box.y_max,
855                width,
856                height,
857                face_idx
858            );
859        }
860
861        // 4x4 スーパーサンプリング ラスタライザ(スキャンライン方式、non-zero 規則)。
862        // 【2026-10-03】even-odd から non-zero へ(`glyph_fill` 参照。重なった画の交差部が抜けていた)。
863        // セグメント走査を「ピクセルごと」ではなく「サブスキャンライン(sub_y)ごと」に一度だけ行い、
864        // 交点 x をスパンに変換して各ピクセルのカバレッジを O(交点数) で求める。
865        // これにより従来の 幅×高さ×16×全セグメント数 から、係数 幅 と 全セグメント数 を大幅に削減する。
866        let mut data = alloc::vec![0u8; (width * height) as usize];
867        let mut coverage = alloc::vec![0u8; width as usize];
868        let mut xs: alloc::vec::Vec<(f32, i8)> = alloc::vec::Vec::new();
869        let mut spans: alloc::vec::Vec<(f32, f32)> = alloc::vec::Vec::new();
870
871        for py in 0..height {
872            let screen_y = (gy0 + py as i32) as f32;
873            for c in coverage.iter_mut() {
874                *c = 0;
875            }
876
877            // 4 本のサブスキャンライン
878            for sy in 0..4 {
879                let sub_y = screen_y + (sy as f32 + 0.5) / 4.0;
880
881                // このスキャンラインと交わる全セグメントの交点 x を一度だけ収集(ピクセル非依存)
882                xs.clear();
883                for seg in &segments {
884                    match seg {
885                        Segment::Line(p1, p2) => {
886                            let y_min = p1.y.min(p2.y);
887                            let y_max = p1.y.max(p2.y);
888                            if sub_y >= y_min && sub_y < y_max && p1.y != p2.y {
889                                let t = (sub_y - p1.y) / (p2.y - p1.y);
890                                let dir: i8 = if p2.y > p1.y { 1 } else { -1 };
891                                xs.push((p1.x + t * (p2.x - p1.x), dir));
892                            }
893                        }
894                    }
895                }
896                if xs.is_empty() {
897                    continue;
898                }
899                // 巻き数が 0 でない区間を塗る(non-zero 規則)。
900                crate::kernel::glyph_fill::nonzero_spans(&mut xs, &mut spans);
901
902                // 各スパンが覆うサブピクセル列にカバレッジを加算する。
903                for &(span_start, span_end) in spans.iter() {
904                    // span_start <= sub_x < span_end を満たすサブピクセル (px, sx) を加算
905                    // sub_x = (gx0 + px) + (sx + 0.5)/4
906                    // px 範囲を概算してクランプ
907                    let px_lo = (f32_floor(span_start) - gx0 - 1).max(0) as u32;
908                    let px_hi = ((f32_ceil(span_end) - gx0 + 1).max(0) as u32).min(width);
909                    for px in px_lo..px_hi {
910                        let screen_x = (gx0 + px as i32) as f32;
911                        for sx in 0..4 {
912                            let sub_x = screen_x + (sx as f32 + 0.5) / 4.0;
913                            if sub_x >= span_start && sub_x < span_end {
914                                coverage[px as usize] += 1;
915                            }
916                        }
917                    }
918                }
919            }
920
921            for px in 0..width {
922                let alpha = ((coverage[px as usize] as u32 * 255) / 16).min(255) as u8;
923                data[(py * width + px) as usize] = alpha;
924            }
925        }
926
927        let cached = CachedGlyph {
928            width,
929            height,
930            x_offset,
931            y_offset,
932            advance,
933            data,
934        };
935
936        self.cache.insert(key, cached);
937        self.cache.get(&key)
938    }
939
940    /// 指定されたサイズ、文字色、背景色でアルファブレンド済みのカラーグリフを取得(またはキャッシュから取得)します
941    ///
942    /// `bg_color` に [`BG_FROM_FRAMEBUFFER`] を渡すと、縁を事前合成せず
943    /// 被覆率をアルファに持つ半透明の画素で返す。転送側(`draw.rs` の
944    /// `draw_glyphs_with_font`)は半透明の画素を画面の実際の画素と合成する。
945    pub fn get_color_glyph(
946        &mut self,
947        c: char,
948        size_px: u32,
949        fg_color: u32,
950        bg_color: u32,
951    ) -> Option<&CachedColorGlyph> {
952        let key = (c, size_px, fg_color, bg_color);
953        if self.color_cache.contains_key(&key) {
954            return self.color_cache.get(&key);
955        }
956
957        if self.color_cache.len() >= 16000 {
958            self.color_cache.clear();
959        }
960
961        let glyph = self.get_glyph(c, size_px)?.clone();
962
963        let mut color_data = Vec::with_capacity(glyph.data.len());
964        let fg_r = (fg_color >> 16) & 0xFF;
965        let fg_g = (fg_color >> 8) & 0xFF;
966        let fg_b = fg_color & 0xFF;
967        let bg_r = (bg_color >> 16) & 0xFF;
968        let bg_g = (bg_color >> 8) & 0xFF;
969        let bg_b = bg_color & 0xFF;
970
971        let from_framebuffer = bg_color == BG_FROM_FRAMEBUFFER;
972        for &alpha in &glyph.data {
973            if alpha == 0 {
974                color_data.push(0x00000000); // 完全透明マーク
975            } else if from_framebuffer && alpha != 255 {
976                // 下地を 1 色で表せない(画像・グラデーション)。被覆率をアルファに持たせ、
977                // 転送時に画面の実際の画素と合成させる。
978                color_data.push(((alpha as u32) << 24) | (fg_color & 0x00FF_FFFF));
979            } else if alpha == 255 {
980                color_data.push(fg_color | 0xFF000000); // 完全不透明
981            } else {
982                let eff_alpha = crate::os_lib::web_engine::font_gamma::correct_glyph_coverage(
983                    alpha,
984                    fg_color,
985                    bg_color,
986                );
987                let a = eff_alpha as u32;
988                let inv_a = 255 - a;
989                let r = ((fg_r * a + bg_r * inv_a) / 255) & 0xFF;
990                let g = ((fg_g * a + bg_g * inv_a) / 255) & 0xFF;
991                let b = ((fg_b * a + bg_b * inv_a) / 255) & 0xFF;
992                color_data.push(0xFF000000 | (r << 16) | (g << 8) | b);
993            }
994        }
995
996        let cached = CachedColorGlyph {
997            width: glyph.width,
998            height: glyph.height,
999            x_offset: glyph.x_offset,
1000            y_offset: glyph.y_offset,
1001            advance: glyph.advance,
1002            data: color_data,
1003        };
1004
1005        self.color_cache.insert(key, cached);
1006        self.color_cache.get(&key)
1007    }
1008
1009    /// 指定されたサイズでグリフのアドバンス(幅)のみを高速に取得(またはキャッシュから取得)します。
1010    /// レイアウトの折り返し計算等で重いラスタライズ(Even-Odd法)を回避するために使用します。
1011    pub fn get_glyph_advance(&mut self, c: char, size_px: u32) -> u32 {
1012        let key = (c, size_px);
1013        if let Some(glyph) = self.cache.get(&key) {
1014            return glyph.advance;
1015        }
1016        if let Some(&adv) = self.advance_cache.get(&key) {
1017            return adv;
1018        }
1019
1020        let (face_idx, glyph_id) = {
1021            let find = |ch: char| -> Option<(usize, ttf_parser::GlyphId)> {
1022                if let Some(id) = self.face.glyph_index(ch) {
1023                    return Some((0, id));
1024                }
1025                // 【2026-08-05 発見・修正】私用領域(U+E000..U+F8FF)は
1026                // アイコンフォント専用の領域。ここを内蔵フォントへ
1027                // フォールバックさせてはいけない。
1028                //
1029                // 2026-08-04 に「字形が無い文字が消える」対策として
1030                // 内蔵フォントチェーンを代替に加えたが、内蔵フォントは
1031                // 私用領域に**箱(tofu)の字形を持っている**。その結果
1032                // Font Awesome のアイコンが ☐ として描かれていた
1033                // (実測: `字形F02D=true` が Noto Sans JP でも真だった)。
1034                //
1035                // アイコンが無いときは**何も描かない**のが正しい
1036                // (実ブラウザもアイコンフォント未取得時は空白になる)。
1037                if ('\u{e000}'..='\u{f8ff}').contains(&ch) {
1038                    return None;
1039                }
1040                for (i, f) in self.fallbacks.iter().enumerate() {
1041                    if let Some(id) = f.glyph_index(ch) {
1042                        return Some((i + 1, id));
1043                    }
1044                }
1045                None
1046            };
1047            match find(c) {
1048                Some(x) => x,
1049                None => {
1050                    if let Some(alt) = fallback_glyph_char(c).and_then(find) {
1051                        alt
1052                    } else {
1053                        let default_adv = size_px / 2;
1054                        self.advance_cache.insert(key, default_adv);
1055                        return default_adv;
1056                    }
1057                }
1058            }
1059        };
1060
1061        let face: &Face = if face_idx == 0 {
1062            &self.face
1063        } else {
1064            &self.fallbacks[face_idx - 1]
1065        };
1066        let advance = if let Some(adv) = face.glyph_hor_advance(glyph_id) {
1067            crate::kernel::font_select::scaled_advance(adv, face.units_per_em(), size_px)
1068        } else {
1069            size_px / 2
1070        };
1071
1072        if self.advance_cache.len() >= 10000 {
1073            self.advance_cache.clear();
1074        }
1075        self.advance_cache.insert(key, advance);
1076        advance
1077    }
1078
1079    /// 指定したサイズでの文字列全体のピクセル幅(advance合計)を計算します
1080    pub fn get_string_width(&mut self, s: &str, size_px: u32) -> u32 {
1081        self.get_string_width_ls(s, size_px, 0)
1082    }
1083
1084    /// letter-spacing 対応版。各文字の advance に ls(px, 負も可) を加算する。
1085    pub fn get_string_width_ls(&mut self, s: &str, size_px: u32, ls: i32) -> u32 {
1086        self.get_string_width_ls_ws(s, size_px, ls, 0)
1087    }
1088
1089    /// letter-spacing + word-spacing 対応版。半角スペース1文字ごとに ws(px, 負も可) も加算する。
1090    pub fn get_string_width_ls_ws(&mut self, s: &str, size_px: u32, ls: i32, ws: i32) -> u32 {
1091        let mut width = 0i32;
1092        for c in s.chars() {
1093            width += self.get_glyph_advance(c, size_px) as i32 + ls + if c == ' ' { ws } else { 0 };
1094        }
1095        width.max(0) as u32
1096    }
1097
1098    /// 折返し後の (最大行幅, 全体高さ) を返す。
1099    ///
1100    /// 重要: この行分割規則は web_engine.rs flatten_layout のテキスト折返し
1101    /// (`'\n'` は強制改行・行頭/行末禁則・改行時に先頭文字を即消費) と
1102    /// **完全に同一** でなければならない。ここがズレると layout が計算する
1103    /// 段落高さと flatten が生成する行数が食い違い、後続要素 (<hr> 等) が
1104    /// テキストに重なる。どちらかを変更する際は必ず両方を同期すること。
1105    pub fn get_string_wrapped_size(&mut self, s: &str, size_px: u32, max_width: u32) -> (u32, u32) {
1106        self.get_string_wrapped_size_ls_ws(s, size_px, max_width, 0, 0)
1107    }
1108
1109    /// letter-spacing 対応版。各文字 advance に ls を加算して折返し計算する。
1110    pub fn get_string_wrapped_size_ls(
1111        &mut self,
1112        s: &str,
1113        size_px: u32,
1114        max_width: u32,
1115        ls: i32,
1116    ) -> (u32, u32) {
1117        self.get_string_wrapped_size_ls_ws(s, size_px, max_width, ls, 0)
1118    }
1119
1120    pub fn get_string_wrapped_size_ls_ws(
1121        &mut self,
1122        s: &str,
1123        size_px: u32,
1124        max_width: u32,
1125        ls: i32,
1126        ws: i32,
1127    ) -> (u32, u32) {
1128        self.get_string_wrapped_size_ext(s, size_px, max_width, ls, ws, false, false, false)
1129    }
1130
1131    /// word-break / overflow-wrap / white-space 対応版
1132    pub fn get_string_wrapped_size_ext(
1133        &mut self,
1134        s: &str,
1135        size_px: u32,
1136        max_width: u32,
1137        ls: i32,
1138        ws: i32,
1139        word_break_all: bool,
1140        overflow_wrap: bool,
1141        _white_space_pre_wrap: bool,
1142    ) -> (u32, u32) {
1143        // 【2026-09-26】折り返し位置は描画側と共通の純粋関数 `line_break::break_positions`
1144        // で決める(単語ごと次の行へ送る・禁則・行末空白のぶら下げ)。従来はここと描画側に
1145        // 「単語を送らずはみ出させる」同趣旨の実装が 2 つあった。
1146        // `\n` は(pre-wrap に限らず、flatten が送ってくる場合も)強制改行として数える。
1147        let line_height = size_px + 4;
1148        let chars: alloc::vec::Vec<char> = s.chars().collect();
1149        let advances: alloc::vec::Vec<i32> = chars
1150            .iter()
1151            .map(|&c| {
1152                if c == '\n' || c == '\u{00AD}' {
1153                    0
1154                } else {
1155                    let extra = ls + if c == ' ' { ws } else { 0 };
1156                    (self.get_glyph_advance(c, size_px) as i32 + extra).max(0)
1157                }
1158            })
1159            .collect();
1160        let breaks = crate::os_lib::layout::line_break::break_positions_ex(
1161            &chars,
1162            &advances,
1163            max_width.min(i32::MAX as u32) as i32,
1164            word_break_all,
1165            overflow_wrap,
1166            |c| !word_break_all && is_line_start_forbidden(c),
1167            |c| !word_break_all && is_line_end_forbidden(c),
1168        );
1169        let mut max_w = 0u32;
1170        let mut emitted_lines = 0u32;
1171        let mut line_w = 0i32;
1172        // 行末の空白(ぶら下げ)を除いた幅。
1173        let mut line_w_no_trailing = 0i32;
1174        let mut seg_has_content = false;
1175        let mut bi = 0usize;
1176        for (i, &c) in chars.iter().enumerate() {
1177            if c == '\n' {
1178                max_w = max_w.max(line_w_no_trailing.max(0) as u32);
1179                emitted_lines += 1;
1180                line_w = 0;
1181                line_w_no_trailing = 0;
1182                seg_has_content = false;
1183                continue;
1184            }
1185            if bi < breaks.len() && breaks[bi] == i {
1186                bi += 1;
1187                max_w = max_w.max(line_w_no_trailing.max(0) as u32);
1188                emitted_lines += 1;
1189                line_w = 0;
1190                line_w_no_trailing = 0;
1191            }
1192            line_w += advances[i];
1193            if !c.is_whitespace() {
1194                line_w_no_trailing = line_w;
1195            }
1196            seg_has_content = true;
1197        }
1198        max_w = max_w.max(line_w_no_trailing.max(0) as u32);
1199        if seg_has_content {
1200            emitted_lines += 1;
1201        }
1202        (max_w, emitted_lines.max(1) * line_height)
1203    }
1204}
1205
1206// 静的グローバルフォントマネージャ
1207pub static GLOBAL_VECTOR_FONT: Mutex<Option<VectorFont>> = Mutex::new(None);
1208pub static GLOBAL_TERMINAL_FONT: Mutex<Option<VectorFont>> = Mutex::new(None);
1209
1210// ============ 実行時カスタムフォント登録(`@font-face`用) ============
1211//
1212// 従来はフォントが`include_bytes!`によるコンパイル時埋め込みのみに限られており
1213// (上記コメント参照)、`@font-face`でページが指定するWebフォントを実行時に
1214// 読み込む手段が丸ごと存在しなかった。TTF/OTF形式(WOFF2は非対応。`glyf`/`loca`
1215// テーブル変換を伴うため別途デコーダが必要で本対応の範囲外)に限定した実行時
1216// フォント登録レジストリを新設する(2026-07-21、www.sugi-lab.net互換性調査より。
1217// 実測ではGoogle FontsはAtmOSのUser-Agent文字列に対しTTF形式で応答するため
1218// (WOFF2形式でのみ配信されるFontAwesomeとは異なる)、この経路だけでも本文用
1219// Webフォントの読込が可能になる)。
1220//
1221// `VectorFont::new`は`&'static [u8]`を要求するが、実行時に取得したバイト列は
1222// 本来 'static ではない。ここでは`Box::leak`で意図的にリークして 'static
1223// 参照へ変換する(この処理系にはページ遷移でフォントを解放する仕組みが元々無く、
1224// 埋め込みフォントも実質的にプロセス終了まで生き続ける想定と同じ性質のため、
1225// 新規リークを許容する)。
1226pub static CUSTOM_FONTS: Mutex<BTreeMap<String, VectorFont>> = Mutex::new(BTreeMap::new());
1227
1228/// バイト列の先頭シグネチャからTTF/OTFかどうかを判定する(WOFF2等は`false`)。
1229fn looks_like_ttf_or_otf(bytes: &[u8]) -> bool {
1230    if bytes.len() < 4 {
1231        return false;
1232    }
1233    matches!(
1234        &bytes[0..4],
1235        [0x00, 0x01, 0x00, 0x00] | [b'O', b'T', b'T', b'O'] | [b't', b'r', b'u', b'e']
1236    )
1237}
1238
1239/// `@font-face`で参照されたフォントを実行時に登録する。TTF/OTF以外(WOFF2等)は
1240/// 安全に`false`を返して拒否する(`ttf_parser`側でも不正な形式は`Err`で弾かれ
1241/// クラッシュしないが、シグネチャの時点で早期に判定した方が無駄なパースを避けられる)。
1242/// 同じ`family`名で複数回呼ばれた場合は後勝ちで上書きする。
1243///
1244/// **`GLOBAL_VECTOR_FONT`のプライマリを新フォントへ切り替える**(`set_primary`。
1245/// 旧プライマリはフォールバックへ退避するため既存のグリフカバレッジは失わない)。
1246/// この処理系はまだ要素/ノード単位の`font-family`対応フォント選択
1247/// (`kernel/draw.rs`の描画箇所・幅計測箇所すべてに手を入れる大規模な
1248/// レンダラ統合が必要)を実装しておらず、それを安全に完成させるにはページ全体の
1249/// テキスト描画・幅計測の整合性(描画に使うフォントと幅計測に使うフォントが
1250/// 常に一致していないと文字が重なる既知の脆さがある)を壊さないよう慎重な
1251/// 段階的実装が必要と判断し見送っている。代わりに「取得できたWebフォントを
1252/// ページ全体の主要フォントとして採用する」という単純だが安全な戦略を採る
1253/// (`draw.rs`/`render.rs`側の変更が一切不要で、既存のグローバルフォント経由
1254/// の描画・幅計測パイプラインをそのまま使い回せるため、描画と幅計測の
1255/// 不整合が原理的に発生しない)。将来的に複数の`@font-face`ファミリーを
1256/// 要素ごとに使い分ける必要が生じた場合は、別途`font-family`対応の
1257/// レンダラ統合に着手すること。2026-07-22実装。
1258/// バイト列の先頭シグネチャからWOFF2かどうかを判定する。
1259fn looks_like_woff2(bytes: &[u8]) -> bool {
1260    bytes.len() >= 4 && &bytes[0..4] == b"wOF2"
1261}
1262
1263/// `@font-face`で参照されたフォントを実行時に登録する。
1264/// TTF/OTF に加え、WOFF2 (Brotli圧縮) フォントの自動展開・登録に対応。
1265/// 同じ`family`名で複数回呼ばれた場合は後勝ちで上書きする。
1266///
1267/// **`GLOBAL_VECTOR_FONT`のプライマリを新フォントへ切り替える**(`set_primary`。
1268/// 旧プライマリはフォールバックへ退避するため既存のグリフカバレッジは失わない)。
1269/// 文字の下地を 1 色で表せないときに `get_color_glyph` へ渡す合図の値。
1270///
1271/// 【2026-10-04】画像やグラデーションの上の文字は、縁を 1 色と事前合成すると
1272/// 色づいたり太さが変わったりする(index.htm のヒーロー文字が細く青みがかった)。
1273/// この値を下地に渡すと縁を半透明のまま返し、画面の実際の画素と合成させる。
1274/// アルファ 1 の色は実在の背景として使われないので合図に使う
1275/// (アルファ 0 は「不透明扱い」の互換規則があるため使えない)。
1276pub const BG_FROM_FRAMEBUFFER: u32 = 0x0100_0000;
1277
1278/// `family` と `weight` から `CUSTOM_FONTS` のキーを作る。
1279///
1280/// 【2026-08-05】以前は family 名だけをキーにしていたため、
1281/// 同じ family でウェイト違いを定義する CSS(Font Awesome など)で
1282/// **後勝ちで上書き**され、solid の字形が失われていた。
1283pub fn font_key(family: &str, weight: u16) -> alloc::string::String {
1284    alloc::format!("{}|{}", family.trim().to_lowercase(), weight)
1285}
1286
1287/// ウェイトを指定して登録する。
1288pub fn register_custom_font_weighted(family: &str, weight: u16, bytes: Vec<u8>) -> bool {
1289    register_custom_font_inner(family, weight, bytes)
1290}
1291
1292pub fn register_custom_font(family: &str, bytes: Vec<u8>) -> bool {
1293    register_custom_font_inner(family, 400, bytes)
1294}
1295
1296/// 登録済みのフォント(キー・バイト数・内容の FNV-1a)。同じものを二度登録しない。
1297///
1298/// 【2026-10-19】スタイルシートの追加(YouTube の視聴ページ)で `@font-face` が
1299/// 適用し直されるたびに、同じフォント(YouTube Sans 2.8MB × 7)を登録し直していた。
1300/// 登録はバイト列を `Vec::leak` するので、そのたびに 20MB ずつヒープが減っていた。
1301static REGISTERED_FONTS: spin::Mutex<Vec<(alloc::string::String, usize, u64)>> = spin::Mutex::new(Vec::new());
1302
1303fn fnv1a64(bytes: &[u8]) -> u64 {
1304    let mut h: u64 = 0xcbf2_9ce4_8422_2325;
1305    for &b in bytes {
1306        h ^= b as u64;
1307        h = h.wrapping_mul(0x0000_0100_0000_01b3);
1308    }
1309    h
1310}
1311
1312fn register_custom_font_inner(family: &str, weight: u16, bytes: Vec<u8>) -> bool {
1313    let fingerprint = (font_key(family, weight), bytes.len(), fnv1a64(&bytes));
1314    if REGISTERED_FONTS.lock().contains(&fingerprint) {
1315        return true;
1316    }
1317    // **安全性ゲート解除(2026-07-24)**: 2026-07-22に発見された「`decompress_brotli`が
1318    // 特定の入力でQEMU起動をハングさせる」問題の根本原因は、2026-07-23に別途
1319    // 特定・修正済みである:`decompress_brotli`内部の3つのプールバッファ(合計
1320    // 約160KB)がスタック上の固定配列として確保されており、64KBしかない起動時
1321    // コアスタックを溢れさせていた(スタックオーバーフロー)。この関数自体を
1322    // ヒープ確保(`Vec`)へ移行したことで根本解決済み(詳細は`decompress_brotli`
1323    // 冒頭のコメント参照)。あわせて`main.rs`にソフトウェアスタック番兵による
1324    // 検知機構も追加済みのため、万一同種の問題が再発してもハングではなく明確な
1325    // 検知・報告に変わる。以上を踏まえ、WOFF2の自動展開を再度有効化する。
1326    // 【2026-08-03】この関数は失敗を**4 箇所すべて無言**で返していた
1327    // (WOFF2 展開失敗・未知の形式・パース失敗・`set_primary` の失敗)。
1328    // そのため「フォントを取得できているのに適用されない」ときに
1329    // どこで落ちたのか分からなかった。規約は `spec/logging_policy.md`(L-1/L-2)。
1330    // 【2026-08-03】キャッシュ命中後、成功も失敗も一切ログに出ないまま
1331    // 停止する事象を追う。300/420/540 秒すべて同じ地点で止まったので
1332    // 時間不足ではない。入口・各段の通過を記録する。
1333    crate::warn!(
1334        "[FONT] 登録開始 family={} weight={} bytes={}",
1335        family,
1336        weight,
1337        bytes.len()
1338    );
1339    // 内蔵フォントチェーン(字形が無い文字の代替)。
1340    // ローカル変数 `font_bytes` が関数 `font_bytes()` を隠すため、
1341    // シャドウイングされる前にここで用意しておく。
1342    let fallback_chain: Vec<&'static [u8]> = DEFAULT_FONT_CHAIN
1343        .iter()
1344        .filter_map(|n| font_bytes(n))
1345        .collect();
1346    let mut font_bytes = if looks_like_woff2(&bytes) {
1347        match decompress_woff2(&bytes) {
1348            Some(sfnt) => sfnt,
1349            None => {
1350                crate::warn!(
1351                    "[FONT] WOFF2 の展開に失敗 family={} bytes={}",
1352                    family,
1353                    bytes.len()
1354                );
1355                return false;
1356            }
1357        }
1358    } else if looks_like_ttf_or_otf(&bytes) {
1359        bytes
1360    } else {
1361        crate::warn!(
1362            "[FONT] 未知のフォント形式 family={} bytes={} head={:02x?}",
1363            family,
1364            bytes.len(),
1365            &bytes[..bytes.len().min(4)]
1366        );
1367        return false;
1368    };
1369
1370    font_bytes.shrink_to_fit();
1371    let leaked: &'static [u8] = Vec::leak(font_bytes);
1372    let _pf = crate::os_lib::web_engine::perf::start();
1373    crate::warn!("[FONT] 解析開始 family={} bytes={}", family, leaked.len());
1374    // 【2026-08-04 発見・修正】`VectorFont::new` は `fallbacks` が空。
1375    //
1376    // `@font-face` で登録したフォントで文字を描くようにした結果、
1377    // **そのフォントに字形が無い文字が何も描かれなくなった**
1378    // (Font Awesome に日本語の字形は無い)。
1379    // 実ブラウザは字形が無ければ次のフォントへ落とす。
1380    //
1381    // 内蔵フォントチェーンを代替として与える(`fallback_chain` は関数冒頭で用意)。
1382    // これで「Web フォントで描けるものはそれで、無いものは内蔵で」になる。
1383    let _parsed = VectorFont::new_with_fallbacks(leaked, &fallback_chain);
1384    crate::os_lib::web_engine::perf::add(
1385        crate::os_lib::web_engine::perf::Slot::FontParse,
1386        _pf,
1387    );
1388    match _parsed {
1389        Ok(font) => {
1390            REGISTERED_FONTS.lock().push(fingerprint);
1391            {
1392                let mut map = CUSTOM_FONTS.lock();
1393                // ウェイト付きキーで登録する(同名ファミリの後勝ちを防ぐ)。
1394                map.insert(font_key(family, weight), font);
1395                // family 名だけのキーも用意する(ウェイト指定が無い描画のため)。
1396                // `VectorFont` は `Clone` ではないので同じバイト列から作り直す。
1397                // **既に入っていれば上書きしない**(最初に届いたものを保つ)。
1398                let plain = family.trim().to_lowercase();
1399                if !map.contains_key(&plain) {
1400                    if let Ok(f2) = VectorFont::new_with_fallbacks(leaked, &fallback_chain) {
1401                        map.insert(plain, f2);
1402                    }
1403                }
1404            }
1405            // 【2026-08-03 発見・修正】ここで `set_primary` して
1406            // **グローバル既定フォントを差し替えていた**。
1407            //
1408            // WOFF2 の展開・再構築を直して Font Awesome が実際に登録できるように
1409            // なった途端、**アイコンフォントが全文字を乗っ取る**という
1410            // 明白なリグレッションが出た(OS のタイトルバーやタブまで
1411            // アイコン書体になった。スクリーンショットで確認)。
1412            //
1413            // `@font-face` の登録は「この family でこのフォントが使える」と
1414            // 記録するだけであって、**文書全体の既定フォントを変える操作ではない**。
1415            // 差し替えはしない。
1416            //
1417            // 【残作業】描画側(`draw_string_ja_size_ext_bg_ls_ws`)は
1418            // `GLOBAL_VECTOR_FONT` しか見ておらず `font-family` を受け取らない。
1419            // そのため登録しただけでは Web フォントは字形に反映されない。
1420            // family 指定で `CUSTOM_FONTS` から選ぶ経路が本来の実装
1421            // (`spec/webfont_loading.md`)。
1422            // 節目のログ(1 ファミリにつき事実上 1 回。L-3)。
1423            //
1424            // 【2026-08-05】Font Awesome のアイコンが ☐ になる件の切り分け。
1425            // カスケード側(family・content・エスケープ解除)は単体試験で
1426            // 正常と確定済み。残る疑いは**字形そのものが無い**こと。
1427            // 代表的な符号位置(U+F02D=fa-book, U+F015=fa-home)を引いて記録する。
1428            let probe = |c: char| -> bool {
1429                CUSTOM_FONTS
1430                    .lock()
1431                    .get(&family.trim().to_lowercase())
1432                    .map(|f| f.has_glyph(c))
1433                    .unwrap_or(false)
1434            };
1435            crate::warn!(
1436                "[FONT] 登録完了 family={} bytes={} 字形F02D={} 字形F015={}",
1437                family,
1438                leaked.len(),
1439                probe('\u{f02d}'),
1440                probe('\u{f015}')
1441            );
1442            true
1443        }
1444        Err(e) => {
1445            crate::warn!(
1446                "[FONT] フォントのパースに失敗 family={} bytes={} err={:?}",
1447                family,
1448                leaked.len(),
1449                e
1450            );
1451            false
1452        }
1453    }
1454}
1455
1456/// 指定した`font-family`名で登録済みのカスタムフォントが存在するかを返す
1457/// (大小文字を区別しない)。
1458/// 【2026-08-05】ウェイトまで見て登録済みかを返す。
1459///
1460/// `has_custom_font` は family 名だけを見るため、
1461/// `@font-face` の取り込みループが「同じ family は登録済み」と判断し、
1462/// **別ウェイト(solid 900)を一度も取り込まなかった**。
1463/// 実測: `Font Awesome 7 Free` に regular(400) だけが入り、
1464/// `.fas`(solid)のアイコンが描けなかった。
1465pub fn has_custom_font_weighted(family: &str, weight: u16) -> bool {
1466    CUSTOM_FONTS.lock().contains_key(&font_key(family, weight))
1467}
1468
1469pub fn has_custom_font(family: &str) -> bool {
1470    CUSTOM_FONTS.lock().contains_key(&family.trim().to_lowercase())
1471}
1472
1473// ============ フォントレジストリ ============
1474//
1475// 実行時にフォントフォルダは存在しないため、選択可能なフォントは include_bytes! で
1476// 埋め込んだものに限る。設定アプリのセレクトボックスはこの一覧を提示する。
1477// (フォントを増やすとカーネルバイナリ(.rodata)が肥大するため、代表的なものに絞る)
1478
1479/// 設定アプリで選択できるフォント名の一覧(フォントフォルダ相当)。
1480pub const AVAILABLE_FONTS: &[&str] = &[
1481    "Mplus1Code-Regular",
1482    "HackGenConsoleNF-Regular",
1483    "ipam",
1484    "ipag",
1485    "MPLUS2-Regular",
1486    "MPLUS2-Bold",
1487];
1488
1489/// OS レベルのデフォルトのフォントフォールバック順。
1490pub const DEFAULT_FONT_CHAIN: [&str; 3] =
1491    ["Mplus1Code-Regular", "HackGenConsoleNF-Regular", "ipam"];
1492
1493/// フォント名から埋め込みバイト列を解決する。未知の名前は None。
1494pub fn font_bytes(name: &str) -> Option<&'static [u8]> {
1495    Some(match name {
1496        "Mplus1Code-Regular" => include_bytes!("../../fonts/Mplus1Code/Mplus1Code-Regular.ttf"),
1497        "HackGenConsoleNF-Regular" => {
1498            include_bytes!("../../fonts/HackGen/HackGenConsoleNF-Regular.ttf")
1499        }
1500        "ipam" => include_bytes!("../../fonts/ipa/ipam.ttf"),
1501        "ipag" | "IPAg" => include_bytes!("../../fonts/ipa/ipag.ttf"),
1502        "MPLUS2-Regular" => include_bytes!("../../fonts/MPLUS2/MPLUS2-Regular.ttf"),
1503        "MPLUS2-Bold" => include_bytes!("../../fonts/MPLUS2/MPLUS2-Bold.ttf"),
1504        _ => return None,
1505    })
1506}
1507
1508/// フォント名のフォールバックチェーンからグローバルフォントを初期化する。
1509/// 先頭がプライマリ、以降がフォールバック。未知/解決不能な名前は読み飛ばす。
1510/// 有効なフォントが 1 つも無ければデフォルトチェーンにフォールバックする。
1511pub fn init_global_font_chain(names: &[&str]) {
1512    let mut bytes: Vec<&'static [u8]> = names.iter().filter_map(|n| font_bytes(n)).collect();
1513    if bytes.is_empty() {
1514        bytes = DEFAULT_FONT_CHAIN
1515            .iter()
1516            .filter_map(|n| font_bytes(n))
1517            .collect();
1518    }
1519    if bytes.is_empty() {
1520        crate::info!("[vector_font] No embedded fonts available for chain init!");
1521        return;
1522    }
1523    let primary = bytes[0];
1524    let fallbacks: Vec<&'static [u8]> = bytes[1..].to_vec();
1525    match VectorFont::new_with_fallbacks(primary, &fallbacks) {
1526        Ok(font) => {
1527            crate::info!(
1528                "[vector_font] Global font chain initialized ({} fonts).",
1529                bytes.len()
1530            );
1531            *GLOBAL_VECTOR_FONT.lock() = Some(font);
1532        }
1533        Err(e) => {
1534            crate::info!("[vector_font] Failed to init font chain: {:?}", e);
1535        }
1536    }
1537}
1538
1539/// 起動時にフォントデータをロードしてグローバルインスタンスを初期化します
1540pub fn init_global_font(font_bytes: &'static [u8]) {
1541    match VectorFont::new(font_bytes) {
1542        Ok(mut font) => {
1543            crate::info!("[vector_font] Successfully initialized global vector font.");
1544            crate::info!("[vector_font] Running test get_glyph('o', 16) for global font...");
1545            if font.get_glyph('o', 16).is_some() {
1546                crate::info!("[vector_font] Test get_glyph('o', 16) succeeded for global font!");
1547            } else {
1548                crate::info!("[vector_font] Test get_glyph('o', 16) failed for global font!");
1549            }
1550            *GLOBAL_VECTOR_FONT.lock() = Some(font);
1551        }
1552        Err(e) => {
1553            crate::info!(
1554                "[vector_font] Failed to initialize global vector font: {:?}",
1555                e
1556            );
1557        }
1558    }
1559}
1560
1561/// ターミナル専用の等幅ベクターフォントをロードして初期化します
1562pub fn init_terminal_font() {
1563    let font_bytes = include_bytes!("../../fonts/Mplus1Code/Mplus1Code-Regular.ttf");
1564    match VectorFont::new(font_bytes) {
1565        Ok(mut font) => {
1566            crate::info!("[vector_font] Successfully initialized terminal monospace font.");
1567            crate::info!("[vector_font] Running test get_glyph('o', 16)...");
1568            if font.get_glyph('o', 16).is_some() {
1569                crate::info!("[vector_font] Test get_glyph('o', 16) succeeded!");
1570            } else {
1571                crate::info!("[vector_font] Test get_glyph('o', 16) failed!");
1572            }
1573            *GLOBAL_TERMINAL_FONT.lock() = Some(font);
1574        }
1575        Err(e) => {
1576            crate::info!(
1577                "[vector_font] Failed to initialize terminal monospace font: {:?}",
1578                e
1579            );
1580        }
1581    }
1582}
1583
1584/// グローバルフォントを用いて、指定したサイズでの文字列全体のピクセル幅を計算します
1585/// `family_list` と太字指定から、`draw::draw_string_family` と**同じ規則**で Web フォントを選び、
1586/// `f` に渡す(希望ウェイト → 文字列を描ける最も近いウェイト → family 名だけのキー)。
1587/// 登録済みの family が無ければ `None`(呼び出し側は内蔵フォントへ落とす)。
1588pub fn with_family_font<R>(
1589    s: &str,
1590    family_list: &str,
1591    weight: u16,
1592    f: impl FnOnce(&mut VectorFont) -> R,
1593) -> Option<R> {
1594    with_family_font_ext(s, family_list, weight, |font, _| f(font))
1595}
1596
1597/// `with_family_font` と同じ選択で、実際に選ばれたウェイトも `f` に渡す
1598/// (ウェイト無しのキーで当たったときは 400 とみなす)。疑似太字を重ねるかの判断に使う。
1599pub fn with_family_font_ext<R>(
1600    s: &str,
1601    family_list: &str,
1602    weight: u16,
1603    f: impl FnOnce(&mut VectorFont, u16) -> R,
1604) -> Option<R> {
1605    if family_list.is_empty() {
1606        return None;
1607    }
1608    // 【2026-10-03】太字か否かの 2 値ではなく数値のウェイトで、CSS Fonts の照合規則で選ぶ
1609    // (`font_select::pick_weight_css`)。`font-weight:500` が 400 の書体で描かれていた。
1610    let mut fonts = CUSTOM_FONTS.lock();
1611    for fam in family_list.split(',') {
1612        let key = fam.trim().trim_matches('"').trim_matches('\'').to_lowercase();
1613        if key.is_empty() {
1614            continue;
1615        }
1616        let prefix = alloc::format!("{}|", key);
1617        let available: Vec<(u16, bool)> = fonts
1618            .range(prefix.clone()..)
1619            .take_while(|(k, _)| k.starts_with(&prefix))
1620            .filter_map(|(k, font)| {
1621                let w = k.get(prefix.len()..)?.parse::<u16>().ok()?;
1622                Some((w, crate::kernel::font_select::covers_all(s, |c| font.primary_has_glyph(c))))
1623            })
1624            .collect();
1625        let picked = crate::kernel::font_select::pick_weight_css(weight, &available);
1626        let hit = picked.map(|w| font_key(&key, w)).unwrap_or(key.clone());
1627        if fonts.contains_key(&hit) {
1628            if let Some(font) = fonts.get_mut(&hit) {
1629                return Some(f(font, picked.unwrap_or(400)));
1630            }
1631        }
1632        // 総称名(sans-serif 等)は、このページで読み込まれた該当系統の Web フォントへ割り当てる
1633        // (`font_select::generic_family_candidates`。内蔵フォントは等幅のため)。
1634        for cand in crate::kernel::font_select::generic_family_candidates(&key) {
1635            let prefix = alloc::format!("{}|", cand);
1636            let available: Vec<(u16, bool)> = fonts
1637                .range(prefix.clone()..)
1638                .take_while(|(k, _)| k.starts_with(&prefix))
1639                .filter_map(|(k, font)| {
1640                    let w = k.get(prefix.len()..)?.parse::<u16>().ok()?;
1641                    Some((w, crate::kernel::font_select::covers_all(s, |c| font.primary_has_glyph(c))))
1642                })
1643                .collect();
1644            if let Some(w) = crate::kernel::font_select::pick_weight_css(weight, &available) {
1645                if let Some(font) = fonts.get_mut(&font_key(cand, w)) {
1646                    return Some(f(font, w));
1647                }
1648            }
1649        }
1650    }
1651    None
1652}
1653
1654/// `font-family` を考慮した文字列幅(`draw::draw_string_family` と**同じ書体選択**で測る)。
1655///
1656/// 【2026-09-26 バグ修正】行の組み立て(`web_engine::layout` の折り返し)は内蔵フォントの
1657/// 幅で測り、描画は Web フォント(Noto Sans JP 等)で描いていた。書体によって文字幅が違うため
1658/// 行が実際より短く見積もられ、描画時に窓の右端を超えた分が描画関数の中で勝手に改行されて、
1659/// 次の行と重なっていた(sugi-lab.net/profile.htm の研究概要)。
1660/// 登録済みの family が無ければ内蔵フォントの幅(従来どおり)。
1661pub fn get_vector_string_width_family(
1662    s: &str,
1663    size_px: u32,
1664    family_list: &str,
1665    weight: u16,
1666    ls: i32,
1667    ws: i32,
1668) -> u32 {
1669    with_family_font(s, family_list, weight, |f| f.get_string_width_ls_ws(s, size_px, ls, ws))
1670        .unwrap_or_else(|| get_vector_string_width_ls_ws(s, size_px, ls, ws))
1671}
1672
1673/// `get_vector_string_wrapped_size_ext` の `font-family` 考慮版(レイアウトの箱の計測用。
1674/// 描画と同じ書体で折り返し行数を測る)。
1675#[allow(clippy::too_many_arguments)]
1676pub fn get_vector_string_wrapped_size_family(
1677    s: &str,
1678    size_px: u32,
1679    max_width: u32,
1680    ls: i32,
1681    ws: i32,
1682    word_break_all: bool,
1683    overflow_wrap: bool,
1684    white_space_pre_wrap: bool,
1685    family_list: &str,
1686    weight: u16,
1687) -> (u32, u32) {
1688    with_family_font(s, family_list, weight, |f| {
1689        f.get_string_wrapped_size_ext(s, size_px, max_width, ls, ws, word_break_all, overflow_wrap, white_space_pre_wrap)
1690    })
1691    .unwrap_or_else(|| {
1692        get_vector_string_wrapped_size_ext(s, size_px, max_width, ls, ws, word_break_all, overflow_wrap, white_space_pre_wrap)
1693    })
1694}
1695
1696pub fn get_vector_string_width(s: &str, size_px: u32) -> u32 {
1697    if let Some(ref mut font) = *GLOBAL_VECTOR_FONT.lock() {
1698        font.get_string_width(s, size_px)
1699    } else {
1700        s.len() as u32 * (size_px / 2) // 静的フォールバック値
1701    }
1702}
1703
1704/// letter-spacing 対応版の文字列幅。ls は文字間に加える px(負も可)。
1705pub fn get_vector_string_width_ls(s: &str, size_px: u32, ls: i32) -> u32 {
1706    get_vector_string_width_ls_ws(s, size_px, ls, 0)
1707}
1708
1709/// letter-spacing + word-spacing 対応版の文字列幅。ws は半角スペース1文字ごとに加える px(負も可)。
1710pub fn get_vector_string_width_ls_ws(s: &str, size_px: u32, ls: i32, ws: i32) -> u32 {
1711    if let Some(ref mut font) = *GLOBAL_VECTOR_FONT.lock() {
1712        font.get_string_width_ls_ws(s, size_px, ls, ws)
1713    } else {
1714        let space_count = s.chars().filter(|&c| c == ' ').count() as i32;
1715        let base = s.len() as i32 * (size_px / 2) as i32
1716            + s.chars().count() as i32 * ls
1717            + space_count * ws;
1718        base.max(0) as u32
1719    }
1720}
1721
1722/// 1 文字ぶんの advance 幅を返す。
1723/// 折返し計算で 1 文字ずつ幅を加算する用途向け(行全体の再計測を避けて O(n) にする)。
1724pub fn get_vector_char_width(c: char, size_px: u32) -> u32 {
1725    if let Some(ref mut font) = *GLOBAL_VECTOR_FONT.lock() {
1726        font.get_glyph_advance(c, size_px)
1727    } else {
1728        size_px / 2
1729    }
1730}
1731
1732pub fn get_vector_string_wrapped_size(s: &str, size_px: u32, max_width: u32) -> (u32, u32) {
1733    if let Some(ref mut font) = *GLOBAL_VECTOR_FONT.lock() {
1734        font.get_string_wrapped_size(s, size_px, max_width)
1735    } else {
1736        let char_w = size_px / 2;
1737        let max_chars = (max_width / char_w).max(1);
1738        let chars_count = s.chars().count() as u32;
1739        let lines = chars_count.div_ceil(max_chars);
1740        (
1741            max_width.min(chars_count * char_w),
1742            lines.max(1) * (size_px + 4),
1743        )
1744    }
1745}
1746
1747/// letter-spacing 対応版の折返しサイズ。ls は文字間に加える px(負も可)。
1748pub fn get_vector_string_wrapped_size_ls(
1749    s: &str,
1750    size_px: u32,
1751    max_width: u32,
1752    ls: i32,
1753) -> (u32, u32) {
1754    get_vector_string_wrapped_size_ls_ws(s, size_px, max_width, ls, 0)
1755}
1756
1757/// letter-spacing + word-spacing 対応版の折返しサイズ。ws は半角スペース1文字ごとに加える px(負も可)。
1758pub fn get_vector_string_wrapped_size_ls_ws(
1759    s: &str,
1760    size_px: u32,
1761    max_width: u32,
1762    ls: i32,
1763    ws: i32,
1764) -> (u32, u32) {
1765    if let Some(ref mut font) = *GLOBAL_VECTOR_FONT.lock() {
1766        font.get_string_wrapped_size_ls_ws(s, size_px, max_width, ls, ws)
1767    } else {
1768        let char_w = ((size_px / 2) as i32 + ls).max(1) as u32;
1769        let max_chars = (max_width / char_w).max(1);
1770        let chars_count = s.chars().count() as u32;
1771        let lines = chars_count.div_ceil(max_chars);
1772        (
1773            max_width.min(chars_count * char_w),
1774            lines.max(1) * (size_px + 4),
1775        )
1776    }
1777}
1778
1779pub fn get_vector_string_wrapped_size_ext(
1780    s: &str,
1781    size_px: u32,
1782    max_width: u32,
1783    ls: i32,
1784    ws: i32,
1785    word_break_all: bool,
1786    overflow_wrap: bool,
1787    white_space_pre_wrap: bool,
1788) -> (u32, u32) {
1789    if let Some(ref mut font) = *GLOBAL_VECTOR_FONT.lock() {
1790        font.get_string_wrapped_size_ext(s, size_px, max_width, ls, ws, word_break_all, overflow_wrap, white_space_pre_wrap)
1791    } else {
1792        let char_w = ((size_px / 2) as i32 + ls).max(1) as u32;
1793        let max_chars = (max_width / char_w).max(1);
1794        let chars_count = s.chars().count() as u32;
1795        let lines = chars_count.div_ceil(max_chars);
1796        (
1797            max_width.min(chars_count * char_w),
1798            lines.max(1) * (size_px + 4),
1799        )
1800    }
1801}
1802
1803pub fn is_line_start_forbidden(c: char) -> bool {
1804    matches!(
1805        c,
1806        '、' | '。'
1807            | ','
1808            | '.'
1809            | '・'
1810            | ':'
1811            | ';'
1812            | '?'
1813            | '!'
1814            | ')'
1815            | ']'
1816            | '}'
1817            | '〉'
1818            | '》'
1819            | '』'
1820            | '」'
1821            | '〕'
1822            | 'ー'
1823            | '〜'
1824            | '々'
1825            | '〆'
1826            | 'ぁ'
1827            | 'ぃ'
1828            | 'ぅ'
1829            | 'ぇ'
1830            | 'ぉ'
1831            | 'っ'
1832            | 'ゃ'
1833            | 'ゅ'
1834            | 'ょ'
1835            | 'ゎ'
1836            | 'ァ'
1837            | 'ィ'
1838            | 'ゥ'
1839            | 'ェ'
1840            | 'ォ'
1841            | 'ッ'
1842            | 'ャ'
1843            | 'ュ'
1844            | 'ョ'
1845            | 'ヮ'
1846            | 'ヶ'
1847            | 'ヵ'
1848    )
1849}
1850
1851pub fn is_line_end_forbidden(c: char) -> bool {
1852    matches!(c, '(' | '[' | '{' | '〈' | '《' | '『' | '「' | '〔')
1853}