Skip to main content

atmos/os_lib/
svg.rs

1// src/svg.rs - ミニ SVG ラスタライザ (Fluent UI System Icons 対応)
2//
3// SVG icons from Fluent UI System Icons
4// Copyright (c) 2020 Microsoft Corporation
5// Licensed under the MIT License
6// https://github.com/microsoft/fluentui-system-icons
7//
8// このモジュールは no_std 環境向けの最小限の SVG パーサ&ラスタライザです。
9// 対応コマンド: M, m, L, l, C, c, Z, z
10// ラスタライズアルゴリズム: Even-Odd (vector_font.rs と同じ方式)
11
12#![allow(dead_code)]
13
14extern crate alloc;
15use alloc::collections::BTreeMap;
16use alloc::vec::Vec;
17use spin::Mutex;
18
19// -------------------------------------------------------------------
20// 内部データ構造
21// -------------------------------------------------------------------
22
23#[derive(Copy, Clone)]
24struct Point {
25    x: f32,
26    y: f32,
27}
28
29#[derive(Clone)]
30struct Segment {
31    p1: Point,
32    p2: Point,
33}
34
35// -------------------------------------------------------------------
36// SVG パスパーサ
37// -------------------------------------------------------------------
38
39/// バイト列から数値トークンを読み取るシンプルなイテレータ
40struct PathParser<'a> {
41    data: &'a [u8],
42    pos: usize,
43}
44
45impl<'a> PathParser<'a> {
46    fn new(data: &'a [u8]) -> Self {
47        Self { data, pos: 0 }
48    }
49
50    fn skip_whitespace_and_comma(&mut self) {
51        while self.pos < self.data.len() {
52            let c = self.data[self.pos];
53            if c == b' ' || c == b'\t' || c == b'\n' || c == b'\r' || c == b',' {
54                self.pos += 1;
55            } else {
56                break;
57            }
58        }
59    }
60
61    fn peek(&self) -> Option<u8> {
62        if self.pos < self.data.len() {
63            Some(self.data[self.pos])
64        } else {
65            None
66        }
67    }
68
69    fn next_char(&mut self) -> Option<u8> {
70        if self.pos < self.data.len() {
71            let c = self.data[self.pos];
72            self.pos += 1;
73            Some(c)
74        } else {
75            None
76        }
77    }
78
79    /// 次の浮動小数点数を読み取る
80    fn next_f32(&mut self) -> Option<f32> {
81        self.skip_whitespace_and_comma();
82        if self.pos >= self.data.len() {
83            return None;
84        }
85
86        let start = self.pos;
87        // 符号
88        if self.pos < self.data.len()
89            && (self.data[self.pos] == b'-' || self.data[self.pos] == b'+')
90        {
91            self.pos += 1;
92        }
93        // 整数部
94        while self.pos < self.data.len() && self.data[self.pos].is_ascii_digit() {
95            self.pos += 1;
96        }
97        // 小数部
98        if self.pos < self.data.len() && self.data[self.pos] == b'.' {
99            self.pos += 1;
100            while self.pos < self.data.len() && self.data[self.pos].is_ascii_digit() {
101                self.pos += 1;
102            }
103        }
104        // 指数部 (e/E)
105        if self.pos < self.data.len()
106            && (self.data[self.pos] == b'e' || self.data[self.pos] == b'E')
107        {
108            self.pos += 1;
109            if self.pos < self.data.len()
110                && (self.data[self.pos] == b'-' || self.data[self.pos] == b'+')
111            {
112                self.pos += 1;
113            }
114            while self.pos < self.data.len() && self.data[self.pos].is_ascii_digit() {
115                self.pos += 1;
116            }
117        }
118
119        if self.pos == start {
120            return None;
121        }
122
123        // バイト列→文字列→f32 変換
124        let s = core::str::from_utf8(&self.data[start..self.pos]).ok()?;
125        parse_f32(s)
126    }
127
128    /// 次のコマンド文字を読み取る(数値が来たらNone)
129    fn next_cmd(&mut self) -> Option<u8> {
130        self.skip_whitespace_and_comma();
131        match self.peek() {
132            Some(c) if c.is_ascii_alphabetic() => {
133                self.pos += 1;
134                Some(c)
135            }
136            _ => None,
137        }
138    }
139}
140
141/// no_std 環境向けの簡易 f32 パーサ
142fn parse_f32(s: &str) -> Option<f32> {
143    let bytes = s.as_bytes();
144    let mut i = 0;
145    let neg = if i < bytes.len() && bytes[i] == b'-' {
146        i += 1;
147        true
148    } else if i < bytes.len() && bytes[i] == b'+' {
149        i += 1;
150        false
151    } else {
152        false
153    };
154
155    let mut int_part: f32 = 0.0;
156    while i < bytes.len() && bytes[i].is_ascii_digit() {
157        int_part = int_part * 10.0 + (bytes[i] - b'0') as f32;
158        i += 1;
159    }
160
161    let mut frac_part: f32 = 0.0;
162    let mut frac_div: f32 = 1.0;
163    if i < bytes.len() && bytes[i] == b'.' {
164        i += 1;
165        while i < bytes.len() && bytes[i].is_ascii_digit() {
166            frac_part = frac_part * 10.0 + (bytes[i] - b'0') as f32;
167            frac_div *= 10.0;
168            i += 1;
169        }
170    }
171
172    // 指数部の簡易処理
173    let mut exp: i32 = 0;
174    let mut exp_neg = false;
175    if i < bytes.len() && (bytes[i] == b'e' || bytes[i] == b'E') {
176        i += 1;
177        if i < bytes.len() && bytes[i] == b'-' {
178            exp_neg = true;
179            i += 1;
180        } else if i < bytes.len() && bytes[i] == b'+' {
181            i += 1;
182        }
183        while i < bytes.len() && bytes[i].is_ascii_digit() {
184            exp = exp * 10 + (bytes[i] - b'0') as i32;
185            i += 1;
186        }
187    }
188
189    let mut val = int_part + frac_part / frac_div;
190    if exp != 0 {
191        let mut e: f32 = 1.0;
192        for _ in 0..exp {
193            e *= 10.0;
194        }
195        if exp_neg {
196            val /= e;
197        } else {
198            val *= e;
199        }
200    }
201    if neg {
202        val = -val;
203    }
204
205    if i == 0 {
206        None
207    } else {
208        Some(val)
209    }
210}
211
212/// SVG の `d` 属性文字列をパースして線分リストに変換する
213fn parse_path_d(d: &str) -> Vec<Segment> {
214    let mut segments: Vec<Segment> = Vec::new();
215    let bytes = d.as_bytes();
216    let mut parser = PathParser::new(bytes);
217
218    let mut cur = Point { x: 0.0, y: 0.0 };
219    let mut start = Point { x: 0.0, y: 0.0 };
220    let mut last_cmd: u8 = 0;
221
222    loop {
223        // コマンド文字があれば読む、なければ数値が連続しているので前のコマンドを繰り返す
224        let cmd = if let Some(c) = parser.next_cmd() {
225            last_cmd = c;
226            c
227        } else {
228            // コマンドなし → 数値だけ残っている場合は前のコマンドを繰り返す
229            if parser.peek().is_none() {
230                break;
231            }
232            // L の暗黙繰り返し
233            match last_cmd {
234                b'M' => b'L',
235                b'm' => b'l',
236                c => c,
237            }
238        };
239
240        match cmd {
241            b'M' => {
242                if let (Some(x), Some(y)) = (parser.next_f32(), parser.next_f32()) {
243                    cur = Point { x, y };
244                    start = cur;
245                }
246            }
247            b'm' => {
248                if let (Some(dx), Some(dy)) = (parser.next_f32(), parser.next_f32()) {
249                    cur = Point {
250                        x: cur.x + dx,
251                        y: cur.y + dy,
252                    };
253                    start = cur;
254                }
255            }
256            b'L' => {
257                if let (Some(x), Some(y)) = (parser.next_f32(), parser.next_f32()) {
258                    let to = Point { x, y };
259                    segments.push(Segment { p1: cur, p2: to });
260                    cur = to;
261                } else {
262                    break;
263                }
264            }
265            b'l' => {
266                if let (Some(dx), Some(dy)) = (parser.next_f32(), parser.next_f32()) {
267                    let to = Point {
268                        x: cur.x + dx,
269                        y: cur.y + dy,
270                    };
271                    segments.push(Segment { p1: cur, p2: to });
272                    cur = to;
273                } else {
274                    break;
275                }
276            }
277            b'H' => {
278                if let Some(x) = parser.next_f32() {
279                    let to = Point { x, y: cur.y };
280                    segments.push(Segment { p1: cur, p2: to });
281                    cur = to;
282                } else {
283                    break;
284                }
285            }
286            b'h' => {
287                if let Some(dx) = parser.next_f32() {
288                    let to = Point {
289                        x: cur.x + dx,
290                        y: cur.y,
291                    };
292                    segments.push(Segment { p1: cur, p2: to });
293                    cur = to;
294                } else {
295                    break;
296                }
297            }
298            b'V' => {
299                if let Some(y) = parser.next_f32() {
300                    let to = Point { x: cur.x, y };
301                    segments.push(Segment { p1: cur, p2: to });
302                    cur = to;
303                } else {
304                    break;
305                }
306            }
307            b'v' => {
308                if let Some(dy) = parser.next_f32() {
309                    let to = Point {
310                        x: cur.x,
311                        y: cur.y + dy,
312                    };
313                    segments.push(Segment { p1: cur, p2: to });
314                    cur = to;
315                } else {
316                    break;
317                }
318            }
319            b'C' => {
320                // 3次ベジェ: C x1 y1 x2 y2 x y
321                loop {
322                    let x1 = parser.next_f32();
323                    let y1 = parser.next_f32();
324                    let x2 = parser.next_f32();
325                    let y2 = parser.next_f32();
326                    let x = parser.next_f32();
327                    let y = parser.next_f32();
328                    if let (Some(x1), Some(y1), Some(x2), Some(y2), Some(x), Some(y)) =
329                        (x1, y1, x2, y2, x, y)
330                    {
331                        let p1 = Point { x: x1, y: y1 };
332                        let p2 = Point { x: x2, y: y2 };
333                        let to = Point { x, y };
334                        flatten_cubic(cur, p1, p2, to, &mut segments);
335                        cur = to;
336                    } else {
337                        break;
338                    }
339                    // 次が数値なら引き続き C を繰り返す
340                    parser.skip_whitespace_and_comma();
341                    match parser.peek() {
342                        Some(c) if !c.is_ascii_alphabetic() => continue,
343                        _ => break,
344                    }
345                }
346            }
347            b'c' => loop {
348                let x1 = parser.next_f32();
349                let y1 = parser.next_f32();
350                let x2 = parser.next_f32();
351                let y2 = parser.next_f32();
352                let x = parser.next_f32();
353                let y = parser.next_f32();
354                if let (Some(x1), Some(y1), Some(x2), Some(y2), Some(x), Some(y)) =
355                    (x1, y1, x2, y2, x, y)
356                {
357                    let p1 = Point {
358                        x: cur.x + x1,
359                        y: cur.y + y1,
360                    };
361                    let p2 = Point {
362                        x: cur.x + x2,
363                        y: cur.y + y2,
364                    };
365                    let to = Point {
366                        x: cur.x + x,
367                        y: cur.y + y,
368                    };
369                    flatten_cubic(cur, p1, p2, to, &mut segments);
370                    cur = to;
371                } else {
372                    break;
373                }
374                parser.skip_whitespace_and_comma();
375                match parser.peek() {
376                    Some(c) if !c.is_ascii_alphabetic() => continue,
377                    _ => break,
378                }
379            },
380            b'Z' | b'z' => {
381                if cur.x != start.x || cur.y != start.y {
382                    segments.push(Segment { p1: cur, p2: start });
383                }
384                cur = start;
385            }
386            _ => {
387                // 未対応コマンドは読み飛ばし
388                parser.next_char();
389            }
390        }
391    }
392
393    segments
394}
395
396/// 3次ベジェ曲線を直線セグメント列に展開する(de Casteljau 法)
397fn flatten_cubic(p0: Point, p1: Point, p2: Point, p3: Point, out: &mut Vec<Segment>) {
398    const STEPS: u32 = 8;
399    let mut prev = p0;
400    for i in 1..=STEPS {
401        let t = i as f32 / STEPS as f32;
402        let mt = 1.0 - t;
403        let x = mt * mt * mt * p0.x
404            + 3.0 * mt * mt * t * p1.x
405            + 3.0 * mt * t * t * p2.x
406            + t * t * t * p3.x;
407        let y = mt * mt * mt * p0.y
408            + 3.0 * mt * mt * t * p1.y
409            + 3.0 * mt * t * t * p2.y
410            + t * t * t * p3.y;
411        let cur = Point { x, y };
412        out.push(Segment { p1: prev, p2: cur });
413        prev = cur;
414    }
415}
416
417// -------------------------------------------------------------------
418// ラスタライザ (Even-Odd, 4x4 スーパーサンプリング)
419// -------------------------------------------------------------------
420
421/// パスセグメント群を指定サイズにラスタライズしてアルファマスク(A8)を返す
422fn rasterize(segments: &[Segment], vb_size: f32, out_size: u32) -> Vec<u8> {
423    let scale = out_size as f32 / vb_size;
424    let w = out_size;
425    let h = out_size;
426    let mut data = alloc::vec![0u8; (w * h) as usize];
427
428    for py in 0..h {
429        for px in 0..w {
430            let mut inside = 0u32;
431            for sy in 0..4u32 {
432                let sub_y = py as f32 + (sy as f32 + 0.5) / 4.0;
433                for sx in 0..4u32 {
434                    let sub_x = px as f32 + (sx as f32 + 0.5) / 4.0;
435                    // スケール逆変換: ピクセル座標 → viewBox 座標
436                    let vx = sub_x / scale;
437                    let vy = sub_y / scale;
438                    let mut crossings = 0u32;
439                    for seg in segments {
440                        let y1 = seg.p1.y;
441                        let y2 = seg.p2.y;
442                        let y_min = if y1 < y2 { y1 } else { y2 };
443                        let y_max = if y1 > y2 { y1 } else { y2 };
444                        if vy >= y_min && vy < y_max && y1 != y2 {
445                            let t = (vy - y1) / (y2 - y1);
446                            let ix = seg.p1.x + t * (seg.p2.x - seg.p1.x);
447                            if ix > vx {
448                                crossings += 1;
449                            }
450                        }
451                    }
452                    if !crossings.is_multiple_of(2) {
453                        inside += 1;
454                    }
455                }
456            }
457            data[(py * w + px) as usize] = ((inside * 255) / 16).min(255) as u8;
458        }
459    }
460    data
461}
462
463// -------------------------------------------------------------------
464// SVG 全体パーサ(path d 属性を抽出する)
465// -------------------------------------------------------------------
466
467/// SVG XML から `<path d="...">` の d 属性値を抽出する
468fn extract_path_d(svg: &[u8]) -> Option<alloc::string::String> {
469    let text = core::str::from_utf8(svg).ok()?;
470    // 'd="' を探す
471    let mut search = text;
472    while let Some(pos) = find_substr(search, " d=\"") {
473        let after = search.get(pos + 4..).unwrap_or(""); // `"` の次から
474        if let Some(end) = find_char(after.as_bytes(), b'"') {
475            return Some(alloc::string::String::from(after.get(..end).unwrap_or("")));
476        }
477        search = search.get(pos + 1..).unwrap_or("");
478    }
479    // d=' 形式も試す
480    let mut search2 = text;
481    while let Some(pos) = find_substr(search2, " d='") {
482        let after = search2.get(pos + 4..).unwrap_or("");
483        if let Some(end) = find_char(after.as_bytes(), b'\'') {
484            return Some(alloc::string::String::from(after.get(..end).unwrap_or("")));
485        }
486        search2 = search2.get(pos + 1..).unwrap_or("");
487    }
488    None
489}
490
491/// viewBox の幅を取得する(正方形アイコン前提)
492fn extract_viewbox_size(svg: &[u8]) -> f32 {
493    let text = core::str::from_utf8(svg).unwrap_or("");
494    if let Some(pos) = find_substr(text, "viewBox=\"") {
495        let after = text.get(pos + 9..).unwrap_or("");
496        // "0 0 W H" の形式を想定
497        let mut p = PathParser::new(after.as_bytes());
498        let _ = p.next_f32(); // x_min
499        let _ = p.next_f32(); // y_min
500        if let Some(w) = p.next_f32() {
501            return w;
502        }
503    }
504    20.0 // デフォルト
505}
506
507fn find_substr(haystack: &str, needle: &str) -> Option<usize> {
508    let hb = haystack.as_bytes();
509    let nb = needle.as_bytes();
510    if nb.is_empty() {
511        return Some(0);
512    }
513    hb.windows(nb.len()).position(|w| w == nb)
514}
515
516fn find_char(haystack: &[u8], needle: u8) -> Option<usize> {
517    haystack.iter().position(|&b| b == needle)
518}
519
520// -------------------------------------------------------------------
521// フル SVG 画像デコード(<img src="*.svg">)
522// path/rect/circle/ellipse/line/polygon/polyline を fill 色つきで RGBA 合成する
523// -------------------------------------------------------------------
524
525/// 1 つの塗りつぶし図形(フラット化済みセグメント + fill 色 ARGB)
526struct SvgShape {
527    segments: Vec<Segment>,
528    fill: u32, // 0xAARRGGBB
529}
530
531/// SVG バイト列を target_w × target_h の RGBA8888 にラスタライズする。
532/// viewBox に従ってアスペクトを保ちつつスケールする。
533pub fn decode_svg_image(svg: &[u8], target_w: u32, target_h: u32) -> Option<(u32, u32, Vec<u8>)> {
534    let text = core::str::from_utf8(svg).ok()?;
535    if target_w == 0 || target_h == 0 || target_w > 4096 || target_h > 4096 {
536        return None;
537    }
538
539    // viewBox("minx miny w h")を取得。無ければ width/height、無ければ 100。
540    let (vb_x, vb_y, vb_w, vb_h) = extract_viewbox_rect(text);
541
542    // 図形を収集(描画順 = ソース順 = ペインターズアルゴリズム)
543    let shapes = collect_svg_shapes(text);
544    if shapes.is_empty() {
545        return None;
546    }
547
548    let sx = target_w as f32 / vb_w;
549    let sy = target_h as f32 / vb_h;
550    let mut rgba = alloc::vec![0u8; (target_w * target_h * 4) as usize];
551
552    for shape in &shapes {
553        let a = (shape.fill >> 24) & 0xFF;
554        if a == 0 {
555            continue;
556        }
557        let fr = ((shape.fill >> 16) & 0xFF) as f32;
558        let fg = ((shape.fill >> 8) & 0xFF) as f32;
559        let fb = (shape.fill & 0xFF) as f32;
560        let fa = a as f32 / 255.0;
561
562        for py in 0..target_h {
563            for px in 0..target_w {
564                // ピクセル中心を viewBox 座標へ逆変換し even-odd 判定(2x2 サンプル)
565                let mut inside = 0u32;
566                for sample in 0..4u32 {
567                    let ox = if sample & 1 == 0 { 0.25 } else { 0.75 };
568                    let oy = if sample & 2 == 0 { 0.25 } else { 0.75 };
569                    let vx = (px as f32 + ox) / sx + vb_x;
570                    let vy = (py as f32 + oy) / sy + vb_y;
571                    let mut crossings = 0u32;
572                    for seg in &shape.segments {
573                        let (y1, y2) = (seg.p1.y, seg.p2.y);
574                        let ymin = if y1 < y2 { y1 } else { y2 };
575                        let ymax = if y1 > y2 { y1 } else { y2 };
576                        if vy >= ymin && vy < ymax && y1 != y2 {
577                            let t = (vy - y1) / (y2 - y1);
578                            let ix = seg.p1.x + t * (seg.p2.x - seg.p1.x);
579                            if ix > vx {
580                                crossings += 1;
581                            }
582                        }
583                    }
584                    if crossings % 2 == 1 {
585                        inside += 1;
586                    }
587                }
588                if inside == 0 {
589                    continue;
590                }
591                let cov = (inside as f32 / 4.0) * fa;
592                let o = ((py * target_w + px) * 4) as usize;
593                // src-over 合成
594                let dr = rgba[o] as f32;
595                let dg = rgba[o + 1] as f32;
596                let db = rgba[o + 2] as f32;
597                let da = rgba[o + 3] as f32 / 255.0;
598                let out_a = cov + da * (1.0 - cov);
599                if out_a <= 0.0 {
600                    continue;
601                }
602                rgba[o] = (((fr * cov + dr * da * (1.0 - cov)) / out_a).min(255.0)) as u8;
603                rgba[o + 1] = (((fg * cov + dg * da * (1.0 - cov)) / out_a).min(255.0)) as u8;
604                rgba[o + 2] = (((fb * cov + db * da * (1.0 - cov)) / out_a).min(255.0)) as u8;
605                rgba[o + 3] = (out_a * 255.0).min(255.0) as u8;
606            }
607        }
608    }
609
610    Some((target_w, target_h, rgba))
611}
612
613/// SVG の自然サイズ(w,h)を返す(ラスタライズ目標解像度の決定用)。
614pub fn natural_size(svg: &[u8]) -> (f32, f32) {
615    if let Ok(text) = core::str::from_utf8(svg) {
616        let (_, _, w, h) = extract_viewbox_rect(text);
617        (w, h)
618    } else {
619        (100.0, 100.0)
620    }
621}
622
623/// viewBox を (x, y, w, h) で返す。無ければ width/height、それも無ければ 100x100。
624fn extract_viewbox_rect(text: &str) -> (f32, f32, f32, f32) {
625    if let Some(pos) = find_substr(text, "viewBox=\"") {
626        let after = text.get(pos + 9..).unwrap_or("");
627        if let Some(end) = find_char(after.as_bytes(), b'"') {
628            let mut p = PathParser::new(&after.as_bytes()[..end]);
629            let x = p.next_f32().unwrap_or(0.0);
630            let y = p.next_f32().unwrap_or(0.0);
631            let w = p.next_f32().unwrap_or(100.0);
632            let h = p.next_f32().unwrap_or(100.0);
633            if w > 0.0 && h > 0.0 {
634                return (x, y, w, h);
635            }
636        }
637    }
638    let w = extract_attr_f32(text, "width").unwrap_or(100.0);
639    let h = extract_attr_f32(text, "height").unwrap_or(100.0);
640    (0.0, 0.0, w.max(1.0), h.max(1.0))
641}
642
643/// タグの属性値を数値で取得("<svg ... width=\"NN\"" の最初の出現)
644fn extract_attr_f32(text: &str, name: &str) -> Option<f32> {
645    let needle = alloc::format!(" {}=\"", name);
646    let pos = find_substr(text, &needle)?;
647    let after = text.get(pos + needle.len()..).unwrap_or("");
648    let end = find_char(after.as_bytes(), b'"')?;
649    let mut p = PathParser::new(&after.as_bytes()[..end]);
650    p.next_f32()
651}
652
653/// 1 つの開始タグ("<tag ...>")から属性値文字列を取得
654fn tag_attr<'a>(tag: &'a str, name: &str) -> Option<&'a str> {
655    for q in ['"', '\''] {
656        let needle = alloc::format!("{}={}", name, q);
657        if let Some(pos) = find_substr(tag, &needle) {
658            let after = tag.get(pos + needle.len()..).unwrap_or("");
659            if let Some(end) = find_char(after.as_bytes(), q as u8) {
660                return after.get(..end);
661            }
662        }
663    }
664    None
665}
666
667/// fill 色を決定(fill 属性 + style 内の fill)。無指定は黒。none は透明。
668fn resolve_fill(tag: &str) -> u32 {
669    let mut fill_str: Option<&str> = tag_attr(tag, "fill");
670    // style="fill:#abc;..." も見る
671    if fill_str.is_none() {
672        if let Some(style) = tag_attr(tag, "style") {
673            if let Some(fp) = find_substr(style, "fill:") {
674                let after = style.get(fp + 5..).unwrap_or("");
675                let end = after.find(';').unwrap_or(after.len());
676                fill_str = Some(after.get(..end).unwrap_or("").trim());
677            }
678        }
679    }
680    let opacity = tag_attr(tag, "fill-opacity")
681        .and_then(|s| {
682            let mut p = PathParser::new(s.as_bytes());
683            p.next_f32()
684        })
685        .unwrap_or(1.0);
686    match fill_str {
687        Some(s) if s.trim() == "none" => 0x00000000,
688        Some(s) => {
689            let rgb = crate::os_lib::css::parse_color(s.trim()).unwrap_or(0xFF000000) & 0x00FFFFFF;
690            let a = (opacity.clamp(0.0, 1.0) * 255.0) as u32;
691            (a << 24) | rgb
692        }
693        None => 0xFF000000, // SVG デフォルト fill は黒
694    }
695}
696
697/// SVG 本文から塗りつぶし図形を収集する
698fn collect_svg_shapes(text: &str) -> Vec<SvgShape> {
699    let mut shapes = Vec::new();
700    let bytes = text.as_bytes();
701    let mut i = 0;
702    while i < bytes.len() {
703        if bytes[i] != b'<' {
704            i += 1;
705            continue;
706        }
707        // タグ名を読む
708        let tag_start = i + 1;
709        let mut j = tag_start;
710        while j < bytes.len() && bytes[j].is_ascii_alphabetic() {
711            j += 1;
712        }
713        let name = text.get(tag_start..j).unwrap_or("");
714        // タグ終端 '>'
715        let end = match find_char(&bytes[i..], b'>') {
716            Some(e) => i + e,
717            None => break,
718        };
719        let tag = text.get(i..end + 1).unwrap_or("");
720
721        let fill = resolve_fill(tag);
722        match name {
723            "path" => {
724                if let Some(d) = tag_attr(tag, "d") {
725                    let segs = parse_path_d(d);
726                    if !segs.is_empty() {
727                        shapes.push(SvgShape {
728                            segments: close_segments(segs),
729                            fill,
730                        });
731                    }
732                }
733            }
734            "rect" => {
735                let x = attr_num(tag, "x");
736                let y = attr_num(tag, "y");
737                let w = attr_num(tag, "width");
738                let h = attr_num(tag, "height");
739                if w > 0.0 && h > 0.0 {
740                    let pts = [
741                        Point { x, y },
742                        Point { x: x + w, y },
743                        Point { x: x + w, y: y + h },
744                        Point { x, y: y + h },
745                    ];
746                    shapes.push(SvgShape {
747                        segments: poly_segments(&pts, true),
748                        fill,
749                    });
750                }
751            }
752            "circle" => {
753                let cx = attr_num(tag, "cx");
754                let cy = attr_num(tag, "cy");
755                let r = attr_num(tag, "r");
756                if r > 0.0 {
757                    shapes.push(SvgShape {
758                        segments: ellipse_segments(cx, cy, r, r),
759                        fill,
760                    });
761                }
762            }
763            "ellipse" => {
764                let cx = attr_num(tag, "cx");
765                let cy = attr_num(tag, "cy");
766                let rx = attr_num(tag, "rx");
767                let ry = attr_num(tag, "ry");
768                if rx > 0.0 && ry > 0.0 {
769                    shapes.push(SvgShape {
770                        segments: ellipse_segments(cx, cy, rx, ry),
771                        fill,
772                    });
773                }
774            }
775            "polygon" | "polyline" => {
776                if let Some(pts_str) = tag_attr(tag, "points") {
777                    let pts = parse_points(pts_str);
778                    if pts.len() >= 3 {
779                        shapes.push(SvgShape {
780                            segments: poly_segments(&pts, true),
781                            fill,
782                        });
783                    }
784                }
785            }
786            _ => {}
787        }
788        i = end + 1;
789    }
790    shapes
791}
792
793fn attr_num(tag: &str, name: &str) -> f32 {
794    tag_attr(tag, name)
795        .and_then(|s| {
796            let mut p = PathParser::new(s.as_bytes());
797            p.next_f32()
798        })
799        .unwrap_or(0.0)
800}
801
802fn parse_points(s: &str) -> Vec<Point> {
803    let mut p = PathParser::new(s.as_bytes());
804    let mut pts = Vec::new();
805    while let (Some(x), Some(y)) = (p.next_f32(), p.next_f32()) {
806        pts.push(Point { x, y });
807    }
808    pts
809}
810
811/// 点列を閉路セグメントに変換
812fn poly_segments(pts: &[Point], close: bool) -> Vec<Segment> {
813    let mut segs = Vec::new();
814    for w in pts.windows(2) {
815        segs.push(Segment { p1: w[0], p2: w[1] });
816    }
817    if close && pts.len() >= 2 {
818        segs.push(Segment {
819            p1: pts[pts.len() - 1],
820            p2: pts[0],
821        });
822    }
823    segs
824}
825
826/// パスの始点と終点が開いていれば閉じる(fill 用)
827fn close_segments(mut segs: Vec<Segment>) -> Vec<Segment> {
828    if let (Some(first), Some(last)) = (segs.first().cloned(), segs.last().cloned()) {
829        if first.p1.x != last.p2.x || first.p1.y != last.p2.y {
830            segs.push(Segment {
831                p1: last.p2,
832                p2: first.p1,
833            });
834        }
835    }
836    segs
837}
838
839/// 楕円を多角形近似してセグメント化
840fn ellipse_segments(cx: f32, cy: f32, rx: f32, ry: f32) -> Vec<Segment> {
841    const N: u32 = 32;
842    let mut pts = Vec::with_capacity(N as usize);
843    for i in 0..N {
844        let a = (i as f32) * core::f32::consts::TAU / (N as f32);
845        pts.push(Point {
846            x: cx + rx * libm::cosf(a),
847            y: cy + ry * libm::sinf(a),
848        });
849    }
850    poly_segments(&pts, true)
851}
852
853// -------------------------------------------------------------------
854// グローバルキャッシュと公開 API
855// -------------------------------------------------------------------
856
857/// SVGラスタライズ結果のキャッシュ
858pub struct SvgCache {
859    cache: BTreeMap<usize, Vec<u8>>, // key: svg_data のポインタ値
860}
861
862pub static SVG_CACHE: Mutex<Option<SvgCache>> = Mutex::new(None);
863
864pub fn init_cache() {
865    *SVG_CACHE.lock() = Some(SvgCache {
866        cache: BTreeMap::new(),
867    });
868}
869
870/// SVGバイト列を指定サイズにラスタライズしてアルファマスクを返す(キャッシュ付き)
871/// 戻り値は size×size の A8 バッファ(0-255)
872pub fn get_or_rasterize(svg_data: &[u8], size: u32) -> Option<&'static [u8]> {
873    let key = (svg_data.as_ptr() as usize).wrapping_add(size as usize * 0x10000);
874
875    let mut guard = SVG_CACHE.lock();
876    let cache = guard.as_mut()?;
877
878    if let alloc::collections::btree_map::Entry::Vacant(entry) = cache.cache.entry(key) {
879        // 初回:ラスタライズして保存
880        let d_str = extract_path_d(svg_data)?;
881        let vb_size = extract_viewbox_size(svg_data);
882        let segments = parse_path_d(&d_str);
883        if segments.is_empty() {
884            return None;
885        }
886        let alpha = rasterize(&segments, vb_size, size);
887        entry.insert(alpha);
888    }
889
890    let slice = cache.cache.get(&key)?.as_slice();
891    // BTreeMap からの削除は行われないため、ポインタをスタティックライフタイムにキャストしても安全
892    Some(unsafe { core::mem::transmute::<&[u8], &'static [u8]>(slice) })
893}