Skip to main content

atmos/os_lib/css/
color.rs

1// 分割: css.rs より機械的に移動(2026-07-16 リファクタ フェーズ3)。
2// ロジック不変。可視性のみ pub(crate) へ昇格し、親が pub(crate) use で再エクスポート。
3use super::*;
4
5/// `rgb()`/`rgba()` の1成分を 0-255 の整数へ解決する。CSS1 の頃から有効な構文である
6/// パーセント表記(`100%`→255、`50%`→128)と、通常の整数表記(`0`〜`255`、範囲外はクランプ)
7/// の両方に対応する。
8pub(crate) fn parse_rgb_component(s: &str) -> u32 {
9    let t = s.trim();
10    if let Some(pct) = t.strip_suffix('%') {
11        let p = pct.trim().parse::<f32>().unwrap_or(0.0);
12        let p = if p.is_finite() { p } else { 0.0 };
13        ((p / 100.0) * 255.0).clamp(0.0, 255.0) as u32
14    } else {
15        let val = t.parse::<f32>().unwrap_or(0.0);
16        let val = if val.is_finite() { val } else { 0.0 };
17        val.clamp(0.0, 255.0) as u32
18    }
19}
20
21pub fn parse_color(s: &str) -> Option<u32> {
22    let s_trim = s.trim();
23    if s_trim.is_empty() {
24        return None;
25    }
26
27    // #RRGGBB, #RGB, #RRGGBBAA, #RGBA
28    if let Some(hex) = s_trim.strip_prefix('#') {
29        let bytes = hex.as_bytes();
30        let from_hex_digit = |b: u8| -> Option<u32> {
31            match b {
32                b'0'..=b'9' => Some((b - b'0') as u32),
33                b'a'..=b'f' => Some((b - b'a' + 10) as u32),
34                b'A'..=b'F' => Some((b - b'A' + 10) as u32),
35                _ => None,
36            }
37        };
38
39        if bytes.len() == 8 {
40            let mut val = 0u32;
41            for &b in bytes {
42                val = (val << 4) | from_hex_digit(b)?;
43            }
44            let r = (val >> 24) & 0xFF;
45            let g = (val >> 16) & 0xFF;
46            let b = (val >> 8) & 0xFF;
47            let a = val & 0xFF;
48            return Some((a << 24) | (r << 16) | (g << 8) | b);
49        } else if bytes.len() == 6 {
50            let mut val = 0u32;
51            for &b in bytes {
52                val = (val << 4) | from_hex_digit(b)?;
53            }
54            return Some(0xFF000000 | val);
55        } else if bytes.len() == 3 {
56            let r = from_hex_digit(bytes[0])?;
57            let g = from_hex_digit(bytes[1])?;
58            let b = from_hex_digit(bytes[2])?;
59            let r_byte = (r << 4) | r;
60            let g_byte = (g << 4) | g;
61            let b_byte = (b << 4) | b;
62            return Some(0xFF000000 | (r_byte << 16) | (g_byte << 8) | b_byte);
63        } else if bytes.len() == 4 {
64            let r = from_hex_digit(bytes[0])?;
65            let g = from_hex_digit(bytes[1])?;
66            let b = from_hex_digit(bytes[2])?;
67            let a = from_hex_digit(bytes[3])?;
68            let r_byte = (r << 4) | r;
69            let g_byte = (g << 4) | g;
70            let b_byte = (b << 4) | b;
71            let a_byte = (a << 4) | a;
72            return Some((a_byte << 24) | (r_byte << 16) | (g_byte << 8) | b_byte);
73        }
74        return None;
75    }
76
77    let s_lower = s_trim.to_lowercase();
78
79    // light-dark(light-color, dark-color) — CSS Color Module Level 5。丸ごと未対応
80    // だった。`prefers-color-scheme` メディアクエリと同じ `PREFERS_DARK` を真実源とし、
81    // 現在の配色設定に応じてどちらか一方の色式(ネストした `light-dark()` も可)を
82    // 選んで再帰的に解決する。
83    if let Some(inner) = s_lower.strip_prefix("light-dark(").and_then(|s| s.strip_suffix(')')) {
84        let mut depth = 0i32;
85        let mut split_at = None;
86        for (i, ch) in inner.char_indices() {
87            match ch {
88                '(' => depth += 1,
89                ')' => depth -= 1,
90                ',' if depth == 0 => {
91                    split_at = Some(i);
92                    break;
93                }
94                _ => {}
95            }
96        }
97        if let Some(i) = split_at {
98            let light = inner.get(..i).unwrap_or("").trim();
99            let dark = inner.get(i + 1..).unwrap_or("").trim();
100            let is_dark = PREFERS_DARK.load(AtomicOrd::Relaxed);
101            return parse_color(if is_dark { dark } else { light });
102        }
103        return None;
104    }
105
106    // oklab(L a b [/ alpha]) / oklch(L C H [/ alpha]) — CSS Color Module Level 4。
107    // Björn Ottosson の OKLab⇄線形sRGB変換式で近似する(色域外は素直に0-255へ
108    // クランプする簡略実装。`color-mix()` の色空間非対応と同じ「厳密な知覚均等
109    // 補間は非対応」の割り切り)。`L` は 0-1(`%` 表記可)、`oklch` の `H` は度。
110    if let Some(inner) = s_lower
111        .strip_prefix("oklab(")
112        .or_else(|| s_lower.strip_prefix("oklch("))
113        .and_then(|s| s.strip_suffix(')'))
114    {
115        let is_lch = s_lower.starts_with("oklch(");
116        let parts: alloc::vec::Vec<&str> = if inner.contains(',') {
117            inner.split(',').collect()
118        } else {
119            inner.split_whitespace().filter(|s| *s != "/").collect()
120        };
121        if parts.len() >= 3 {
122            let parse_l = |s: &str| -> f32 {
123                let s = s.trim();
124                if let Some(pct) = s.strip_suffix('%') {
125                    pct.parse::<f32>().unwrap_or(0.0) / 100.0
126                } else {
127                    s.parse::<f32>().unwrap_or(0.0)
128                }
129            };
130            let l = parse_l(parts[0]);
131            let (ok_a, ok_b) = if is_lch {
132                let c = parse_l(parts[1]).max(0.0);
133                let h_deg = parts[2]
134                    .trim()
135                    .trim_end_matches("deg")
136                    .parse::<f32>()
137                    .unwrap_or(0.0);
138                let h_rad = h_deg * core::f32::consts::PI / 180.0;
139                (c * libm::cosf(h_rad), c * libm::sinf(h_rad))
140            } else {
141                (parse_l(parts[1]), parse_l(parts[2]))
142            };
143            let a_val = if parts.len() >= 4 {
144                let a_str = parts[3].trim();
145                if let Some(pct) = a_str.strip_suffix('%') {
146                    pct.parse::<f32>().unwrap_or(100.0) / 100.0
147                } else {
148                    a_str.parse::<f32>().unwrap_or(1.0)
149                }
150            } else {
151                1.0
152            };
153            let l_ = l + 0.396_337_78 * ok_a + 0.215_803_76 * ok_b;
154            let m_ = l - 0.105_561_346 * ok_a - 0.063_854_17 * ok_b;
155            let s_ = l - 0.089_484_18 * ok_a - 1.291_485_5 * ok_b;
156            let (l3, m3, s3) = (l_ * l_ * l_, m_ * m_ * m_, s_ * s_ * s_);
157            let lin_r = 4.076_741_7 * l3 - 3.307_711_6 * m3 + 0.230_969_93 * s3;
158            let lin_g = -1.268_438 * l3 + 2.609_757_4 * m3 - 0.341_319_4 * s3;
159            let lin_b = -0.004_196_086_3 * l3 - 0.703_418_6 * m3 + 1.707_614_7 * s3;
160            // 線形light → sRGBガンマ符号化。
161            let to_srgb = |c: f32| -> u32 {
162                let c = c.clamp(0.0, 1.0);
163                let enc = if c <= 0.003_130_8 {
164                    c * 12.92
165                } else {
166                    1.055 * libm::powf(c, 1.0 / 2.4) - 0.055
167                };
168                // 行列係数を有限桁で丸めているための誤差蓄積を吸収するため、
169                // 単純truncateではなく四捨五入する(truncate だと L=1・彩度0 の
170                // 白などが 255 のはずが 254 に化ける丸め落ちが起きていた)。
171                libm::roundf((enc * 255.0).clamp(0.0, 255.0)) as u32
172            };
173            let r = to_srgb(lin_r);
174            let g = to_srgb(lin_g);
175            let b_out = to_srgb(lin_b);
176            let a = (a_val * 255.0).clamp(0.0, 255.0) as u32;
177            return Some((a << 24) | (r << 16) | (g << 8) | b_out);
178        }
179    }
180
181    // hwb(h w% b% [/ a]) — CSS Color Module Level 4。彩度100%・輝度50%相当の純色相RGBを
182    // 6分割セクターで求め、white(w)/black(b) の割合でブレンドする(HSLより単純な変換)。
183    if let Some(inner) = s_lower.strip_prefix("hwb(").and_then(|s| s.strip_suffix(')')) {
184        let parts: alloc::vec::Vec<&str> = if inner.contains(',') {
185            inner.split(',').collect()
186        } else {
187            inner.split_whitespace().filter(|s| *s != "/").collect()
188        };
189        if parts.len() >= 3 {
190            let h_raw = parts[0]
191                .trim()
192                .trim_end_matches("deg")
193                .parse::<f32>()
194                .unwrap_or(0.0);
195            // hue は360を法とする値のため hsl() と同じく正規化する。
196            let h = ((h_raw % 360.0) + 360.0) % 360.0;
197            let mut w = parts[1]
198                .trim()
199                .trim_end_matches('%')
200                .parse::<f32>()
201                .unwrap_or(0.0)
202                / 100.0;
203            let mut bl = parts[2]
204                .trim()
205                .trim_end_matches('%')
206                .parse::<f32>()
207                .unwrap_or(0.0)
208                / 100.0;
209            w = w.clamp(0.0, 1.0);
210            bl = bl.clamp(0.0, 1.0);
211            // w+b が1を超える場合は仕様通り比率を保ったまま正規化する。
212            if w + bl > 1.0 {
213                let sum = w + bl;
214                w /= sum;
215                bl /= sum;
216            }
217            let a_val = if parts.len() >= 4 {
218                let a_str = parts[3].trim();
219                if a_str.ends_with('%') {
220                    a_str.trim_end_matches('%').parse::<f32>().unwrap_or(100.0) / 100.0
221                } else {
222                    a_str.parse::<f32>().unwrap_or(1.0)
223                }
224            } else {
225                1.0
226            };
227            let h_prime = h / 60.0;
228            let x = 1.0 - libm::fabsf(h_prime % 2.0 - 1.0);
229            let (r1, g1, b1) = if h_prime < 1.0 {
230                (1.0, x, 0.0)
231            } else if h_prime < 2.0 {
232                (x, 1.0, 0.0)
233            } else if h_prime < 3.0 {
234                (0.0, 1.0, x)
235            } else if h_prime < 4.0 {
236                (0.0, x, 1.0)
237            } else if h_prime < 5.0 {
238                (x, 0.0, 1.0)
239            } else {
240                (1.0, 0.0, x)
241            };
242            let r = ((r1 * (1.0 - w - bl) + w) * 255.0).clamp(0.0, 255.0) as u32;
243            let g = ((g1 * (1.0 - w - bl) + w) * 255.0).clamp(0.0, 255.0) as u32;
244            let b_out = ((b1 * (1.0 - w - bl) + w) * 255.0).clamp(0.0, 255.0) as u32;
245            let a = (a_val * 255.0).clamp(0.0, 255.0) as u32;
246            return Some((a << 24) | (r << 16) | (g << 8) | b_out);
247        }
248    }
249
250    // color(COLORSPACE c1 c2 c3 [/ a]) — CSS Color Module Level 4の述語関数。
251    // 丸ごと未対応だった(`spec/TODO.md`では`oklch()`と同格の「知覚均等色空間の
252    // 変換数学が無い」大物として記載されていたが、`srgb`/`srgb-linear`だけなら
253    // 行列変換不要で対応できるため部分的に実装する)。`srgb`は`rgb()`と同じ
254    // ガンマ符号化済み値(0-1範囲)、`srgb-linear`は`oklab()`側で既に書いた
255    // `to_srgb`と同じ線形→sRGBガンマ符号化を再利用するだけで対応できる。
256    // `display-p3`/`rec2020`/`a98-rgb`/`prophoto-rgb`/`xyz`等の広色域・XYZ空間は
257    // 本物のガマット変換行列が必要で、oklch側と同じ「ラスタライズ結果を目視
258    // 確認できないため誤変換を自分で検知できない」リスクがあるため対象外の
259    // ままとする(未知の色空間名は`None`を返し、呼び出し元は色指定そのものを
260    // 無視する既存の安全側フォールバックに委ねる)。
261    if let Some(inner) = s_lower.strip_prefix("color(").and_then(|s| s.strip_suffix(')')) {
262        let mut parts = inner.split_whitespace().filter(|s| *s != "/");
263        let space_name = parts.next().unwrap_or("");
264        // 【2026-08-31】以前は `srgb`/`srgb-linear` の 2 つだけを扱い、
265        // 広色域(`display-p3` 等)と XYZ は「変換行列が必要」として未対応
266        // だった。行列と伝達関数を `css/color_space.rs`(純粋モジュール・
267        // 単体試験 33 件)へ切り出したので、全色空間を通す。
268        let Some(space) = crate::os_lib::css::color_space::parse_color_space(space_name) else {
269            // 未知の色空間名は従来どおり None(色指定そのものを無視する)。
270            return None;
271        };
272        let rest: alloc::vec::Vec<&str> = parts.collect();
273        if rest.len() >= 3 {
274            let parse_c = |s: &str| -> f32 {
275                let s = s.trim();
276                if let Some(pct) = s.strip_suffix('%') {
277                    pct.parse::<f32>().unwrap_or(0.0) / 100.0
278                } else {
279                    s.parse::<f32>().unwrap_or(0.0)
280                }
281            };
282            let c0 = parse_c(rest[0]);
283            let c1 = parse_c(rest[1]);
284            let c2 = parse_c(rest[2]);
285            let a_val = if rest.len() >= 4 { parse_c(rest[3]) } else { 1.0 };
286            let (lr, lg, lb) = crate::os_lib::css::color_space::components_to_linear_srgb(space, c0, c1, c2);
287            let (r, g, b_out) = crate::os_lib::css::color_space::linear_srgb_to_rgb8(lr, lg, lb);
288            let a = (a_val.clamp(0.0, 1.0) * 255.0) as u32;
289            return Some((a << 24) | (r << 16) | (g << 8) | b_out);
290        }
291        return None;
292    }
293
294    // lab(L a b [/ alpha]) / lch(L C H [/ alpha]) — CSS Color Module Level 4。
295    //
296    // `oklab()`/`oklch()` とは**白色点が違う**(こちらは CIE Lab の D50)。
297    // D50→D65 の Bradford 色順応を挟まないと全体が黄色寄りにずれるので、
298    // 変換は `css/color_space.rs` の実装(試験あり)へ委ねる。
299    //
300    // `L` は 0-100(`%` 表記可)、`a`/`b` はおおむね -125〜125、
301    // `lch` の `C` は 0 以上、`H` は度。
302    if let Some(inner) = s_lower
303        .strip_prefix("lab(")
304        .or_else(|| s_lower.strip_prefix("lch("))
305        .and_then(|s| s.strip_suffix(')'))
306    {
307        let is_lch = s_lower.starts_with("lch(");
308        let parts: alloc::vec::Vec<&str> = if inner.contains(',') {
309            inner.split(',').collect()
310        } else {
311            inner.split_whitespace().filter(|s| *s != "/").collect()
312        };
313        if parts.len() >= 3 {
314            // `L` は % 表記なら 100 分率、素の数値ならそのまま 0-100。
315            let l = {
316                let s = parts[0].trim();
317                if let Some(pct) = s.strip_suffix('%') {
318                    pct.parse::<f32>().unwrap_or(0.0)
319                } else {
320                    s.parse::<f32>().unwrap_or(0.0)
321                }
322            };
323            let p1 = parts[1].trim().trim_end_matches('%').parse::<f32>().unwrap_or(0.0);
324            let p2 = parts[2]
325                .trim()
326                .trim_end_matches("deg")
327                .trim_end_matches('%')
328                .parse::<f32>()
329                .unwrap_or(0.0);
330            let a_val = if parts.len() >= 4 {
331                let s = parts[3].trim();
332                if let Some(pct) = s.strip_suffix('%') {
333                    pct.parse::<f32>().unwrap_or(100.0) / 100.0
334                } else {
335                    s.parse::<f32>().unwrap_or(1.0)
336                }
337            } else {
338                1.0
339            };
340            let (l, aa, bb) = if is_lch {
341                // 色相は 360 を法とする値なので hsl()/hwb() と同様に正規化する。
342                let h = ((p2 % 360.0) + 360.0) % 360.0;
343                crate::os_lib::css::color_space::lch_to_lab(l, p1, h)
344            } else {
345                (l, p1, p2)
346            };
347            let (lr, lg, lb) = crate::os_lib::css::color_space::lab_to_linear_srgb(l, aa, bb);
348            let (r, g, b_out) = crate::os_lib::css::color_space::linear_srgb_to_rgb8(lr, lg, lb);
349            let a = (a_val.clamp(0.0, 1.0) * 255.0) as u32;
350            return Some((a << 24) | (r << 16) | (g << 8) | b_out);
351        }
352        return None;
353    }
354
355    // hsl(h, s%, l%) / hsla(h, s%, l%, a)
356    if s_lower.starts_with("hsl") {
357        let has_alpha = s_lower.starts_with("hsla(");
358        let inner_start = if has_alpha { 5 } else { 4 };
359        if let Some(inner) = s_lower.get(inner_start..) {
360            let inner = inner.trim_end_matches(')');
361            // カンマ区切りまたはスペース区切り(CSS Color Level 4)
362            let parts: alloc::vec::Vec<&str> = if inner.contains(',') {
363                inner.split(',').collect()
364            } else {
365                inner.split_whitespace().filter(|s| *s != "/").collect()
366            };
367            if parts.len() >= 3 {
368                // hue は 360 を法とする値(負値・360以上も有効)のため、以降の6分割
369                // セクター判定(h_prime = h/60 が [0,6) にあることを前提とする)の前に
370                // [0,360) へ正規化しておく。Rust の `%` は剰余(符号は被除数に従う)なので
371                // 負値のときそのままでは正しく巻き戻らず、2回 `%` を取ることで真の mod にする。
372                let h_raw = parts[0]
373                    .trim()
374                    .trim_end_matches("deg")
375                    .parse::<f32>()
376                    .unwrap_or(0.0);
377                let h = ((h_raw % 360.0) + 360.0) % 360.0;
378                let s_pct = parts[1]
379                    .trim()
380                    .trim_end_matches('%')
381                    .parse::<f32>()
382                    .unwrap_or(0.0)
383                    / 100.0;
384                let l_pct = parts[2]
385                    .trim()
386                    .trim_end_matches('%')
387                    .parse::<f32>()
388                    .unwrap_or(0.0)
389                    / 100.0;
390                let a_val = if parts.len() >= 4 {
391                    let a_str = parts[3].trim();
392                    if a_str.ends_with('%') {
393                        a_str.trim_end_matches('%').parse::<f32>().unwrap_or(100.0) / 100.0
394                    } else {
395                        a_str.parse::<f32>().unwrap_or(1.0)
396                    }
397                } else {
398                    1.0
399                };
400
401                // HSL to RGB conversion
402                let c = (1.0 - libm::fabsf(2.0 * l_pct - 1.0)) * s_pct;
403                let h_prime = h / 60.0;
404                let x = c * (1.0 - libm::fabsf(h_prime % 2.0 - 1.0));
405                let (r1, g1, b1) = if h_prime < 1.0 {
406                    (c, x, 0.0)
407                } else if h_prime < 2.0 {
408                    (x, c, 0.0)
409                } else if h_prime < 3.0 {
410                    (0.0, c, x)
411                } else if h_prime < 4.0 {
412                    (0.0, x, c)
413                } else if h_prime < 5.0 {
414                    (x, 0.0, c)
415                } else {
416                    (c, 0.0, x)
417                };
418                let m = l_pct - c / 2.0;
419                let r = ((r1 + m) * 255.0).clamp(0.0, 255.0) as u32;
420                let g = ((g1 + m) * 255.0).clamp(0.0, 255.0) as u32;
421                let b = ((b1 + m) * 255.0).clamp(0.0, 255.0) as u32;
422                let a = (a_val * 255.0).clamp(0.0, 255.0) as u32;
423                return Some((a << 24) | (r << 16) | (g << 8) | b);
424            }
425        }
426    }
427
428    // color-mix(in <space>, <color> [<pct>]?, <color> [<pct>]?) — CSS Color Module Level 5。
429    // 簡略実装: 宣言された色空間(srgb/oklab/oklch 等)に関わらず常に単純な sRGB 0-255
430    // 空間で線形補間する(知覚均等空間での補間は非対応。`:is()`/`:where()` と同じ
431    // 「複雑な構文は簡略化して割り切る」既存方針に沿う)。
432    if let Some(inner) = s_lower
433        .strip_prefix("color-mix(")
434        .and_then(|s| s.strip_suffix(')'))
435    {
436        if let Some((_space_clause, rest)) = inner.split_once(',') {
437            let segs = split_top_level_commas(rest);
438            if segs.len() == 2 {
439                fn split_color_and_pct(seg: &str) -> (&str, Option<f32>) {
440                    let seg = seg.trim();
441                    if let Some(ws) = seg.rfind(char::is_whitespace) {
442                        let (color_part, pct_part) = seg.split_at(ws);
443                        let pct_part = pct_part.trim();
444                        if let Some(num) = pct_part.strip_suffix('%') {
445                            if let Ok(p) = num.trim().parse::<f32>() {
446                                return (color_part.trim(), Some(p));
447                            }
448                        }
449                    }
450                    (seg, None)
451                }
452                let (c1_str, p1) = split_color_and_pct(segs[0]);
453                let (c2_str, p2) = split_color_and_pct(segs[1]);
454                let (w1, w2) = match (p1, p2) {
455                    (None, None) => (50.0f32, 50.0f32),
456                    (Some(a), None) => (a, 100.0 - a),
457                    (None, Some(b)) => (100.0 - b, b),
458                    (Some(a), Some(b)) => {
459                        let sum = a + b;
460                        if sum <= 0.0 {
461                            (50.0, 50.0)
462                        } else {
463                            (a / sum * 100.0, b / sum * 100.0)
464                        }
465                    }
466                };
467                if let (Some(c1), Some(c2)) = (parse_color(c1_str), parse_color(c2_str)) {
468                    let mix_channel = |shift: u32| -> u32 {
469                        let v1 = ((c1 >> shift) & 0xFF) as f32;
470                        let v2 = ((c2 >> shift) & 0xFF) as f32;
471                        ((v1 * w1 + v2 * w2) / 100.0).clamp(0.0, 255.0) as u32
472                    };
473                    let a = mix_channel(24);
474                    let r = mix_channel(16);
475                    let g = mix_channel(8);
476                    let b = mix_channel(0);
477                    return Some((a << 24) | (r << 16) | (g << 8) | b);
478                }
479            }
480        }
481    }
482
483    // rgb(r, g, b) / rgba(r, g, b, a) — hsl()/hwb() と同じくカンマ区切り(レガシー)と
484    // スペース区切り+任意の `/ alpha`(CSS Color Level 4)の両方を受理する。
485    // 以前は `inner.split(',')` 固定でスペース区切り(`rgb(255 0 0)` 等)が
486    // parts.len() の条件に一致せず静かに色なし(None)へ落ちるバグだった。
487    let rgb_inner = s_lower
488        .strip_prefix("rgba(")
489        .or_else(|| s_lower.strip_prefix("rgb("))
490        .and_then(|s| s.strip_suffix(')'));
491    if let Some(inner) = rgb_inner {
492        let parts: Vec<&str> = if inner.contains(',') {
493            inner.split(',').collect()
494        } else {
495            inner.split_whitespace().filter(|s| *s != "/").collect()
496        };
497        if parts.len() >= 3 {
498            let r = parse_rgb_component(parts[0]);
499            let g = parse_rgb_component(parts[1]);
500            let b = parse_rgb_component(parts[2]);
501            let a_val = if parts.len() >= 4 {
502                let a_str = parts[3].trim();
503                if let Some(pct) = a_str.strip_suffix('%') {
504                    pct.parse::<f32>().unwrap_or(100.0) / 100.0
505                } else {
506                    a_str.parse::<f32>().unwrap_or(1.0)
507                }
508            } else {
509                1.0
510            };
511            let a = (a_val * 255.0).clamp(0.0, 255.0) as u32;
512            return Some((a << 24) | (r << 16) | (g << 8) | b);
513        }
514    }
515
516    // 名前付き色 (CSS Level 1-4 から実用的なものを網羅)
517    match s_lower.as_str() {
518        "black" => Some(0xFF000000),
519        "white" => Some(0xFFFFFFFF),
520        "red" => Some(0xFFFF0000),
521        "lime" => Some(0xFF00FF00),
522        "blue" => Some(0xFF0000FF),
523        "yellow" => Some(0xFFFFFF00),
524        "cyan" | "aqua" => Some(0xFF00FFFF),
525        "magenta" | "fuchsia" => Some(0xFFFF00FF),
526        "silver" => Some(0xFFC0C0C0),
527        "gray" | "grey" => Some(0xFF808080),
528        "maroon" => Some(0xFF800000),
529        "olive" => Some(0xFF808000),
530        "green" => Some(0xFF008000),
531        "purple" => Some(0xFF800080),
532        "teal" => Some(0xFF008080),
533        "navy" => Some(0xFF000080),
534        "orange" => Some(0xFFFFA500),
535        "pink" => Some(0xFFFFC0CB),
536        "brown" => Some(0xFFA52A2A),
537        "coral" => Some(0xFFFF7F50),
538        "tomato" => Some(0xFFFF6347),
539        "gold" => Some(0xFFFFD700),
540        "indigo" => Some(0xFF4B0082),
541        "violet" => Some(0xFFEE82EE),
542        "crimson" => Some(0xFFDC143C),
543        "darkred" => Some(0xFF8B0000),
544        "darkgreen" => Some(0xFF006400),
545        "darkblue" => Some(0xFF00008B),
546        "darkgray" | "darkgrey" => Some(0xFFA9A9A9),
547        "lightgray" | "lightgrey" => Some(0xFFD3D3D3),
548        "lightblue" => Some(0xFFADD8E6),
549        "lightgreen" => Some(0xFF90EE90),
550        "lightyellow" => Some(0xFFFFFFE0),
551        "steelblue" => Some(0xFF4682B4),
552        "slategray" | "slategrey" => Some(0xFF708090),
553        "indianred" => Some(0xFFCD5C5C),
554        "royalblue" => Some(0xFF4169E1),
555        "dodgerblue" => Some(0xFF1E90FF),
556        "skyblue" => Some(0xFF87CEEB),
557        "whitesmoke" => Some(0xFFF5F5F5),
558        "wheat" => Some(0xFFF5DEB3),
559        "tan" => Some(0xFFD2B48C),
560        "salmon" => Some(0xFFFA8072),
561        "plum" => Some(0xFFDDA0DD),
562        "orchid" => Some(0xFFDA70D6),
563        "linen" => Some(0xFFFAF0E6),
564        "khaki" => Some(0xFFF0E68C),
565        "ivory" => Some(0xFFFFFFF0),
566        "honeydew" => Some(0xFFF0FFF0),
567        "hotpink" => Some(0xFFFF69B4),
568        "deeppink" => Some(0xFFFF1493),
569        "beige" => Some(0xFFF5F5DC),
570        "aliceblue" => Some(0xFFF0F8FF),
571        "transparent" => Some(0x00000000),
572        // CSS Level 4 extended colors
573        "rebeccapurple" => Some(0xFF663399),
574        "mintcream" => Some(0xFFF5FFFA),
575        "azure" => Some(0xFFF0FFFF),
576        "floralwhite" => Some(0xFFFFFAF0),
577        "ghostwhite" => Some(0xFFF8F8FF),
578        "seashell" => Some(0xFFFFF5EE),
579        "oldlace" => Some(0xFFFDF5E6),
580        "antiquewhite" => Some(0xFFFAEBD7),
581        "bisque" => Some(0xFFFFE4C4),
582        "moccasin" => Some(0xFFFFE4B5),
583        "navajowhite" => Some(0xFFFFDEAD),
584        "papayawhip" => Some(0xFFFFEFD5),
585        "peachpuff" => Some(0xFFFFDAB9),
586        "mistyrose" => Some(0xFFFFE4E1),
587        "lavender" => Some(0xFFE6E6FA),
588        "lavenderblush" => Some(0xFFFFF0F5),
589        "thistle" => Some(0xFFD8BFD8),
590        "powderblue" => Some(0xFFB0E0E6),
591        "lightcyan" => Some(0xFFE0FFFF),
592        "paleturquoise" => Some(0xFFAFEEEE),
593        "turquoise" => Some(0xFF40E0D0),
594        "mediumturquoise" => Some(0xFF48D1CC),
595        "darkturquoise" => Some(0xFF00CED1),
596        "cadetblue" => Some(0xFF5F9EA0),
597        "darkcyan" => Some(0xFF008B8B),
598        "lightseagreen" => Some(0xFF20B2AA),
599        "mediumaquamarine" => Some(0xFF66CDAA),
600        "aquamarine" => Some(0xFF7FFFD4),
601        "springgreen" => Some(0xFF00FF7F),
602        "mediumspringgreen" => Some(0xFF00FA9A),
603        "lawngreen" => Some(0xFF7CFC00),
604        "chartreuse" => Some(0xFF7FFF00),
605        "greenyellow" => Some(0xFFADFF2F),
606        "yellowgreen" => Some(0xFF9ACD32),
607        "darkolivegreen" => Some(0xFF556B2F),
608        "olivedrab" => Some(0xFF6B8E23),
609        "palegreen" => Some(0xFF98FB98),
610        "mediumseagreen" => Some(0xFF3CB371),
611        "seagreen" => Some(0xFF2E8B57),
612        "forestgreen" => Some(0xFF228B22),
613        "limegreen" => Some(0xFF32CD32),
614        "darkkhaki" => Some(0xFFBDB76B),
615        "goldenrod" => Some(0xFFDAA520),
616        "darkgoldenrod" => Some(0xFFB8860B),
617        "saddlebrown" => Some(0xFF8B4513),
618        "sienna" => Some(0xFFA0522D),
619        "peru" => Some(0xFFCD853F),
620        "burlywood" => Some(0xFFDEB887),
621        "sandybrown" => Some(0xFFF4A460),
622        "chocolate" => Some(0xFFD2691E),
623        "rosybrown" => Some(0xFFBC8F8F),
624        "darkmagenta" => Some(0xFF8B008B),
625        "darkviolet" => Some(0xFF9400D3),
626        "blueviolet" => Some(0xFF8A2BE2),
627        "mediumvioletred" => Some(0xFFC71585),
628        "palevioletred" => Some(0xFFDB7093),
629        "mediumorchid" => Some(0xFFBA55D3),
630        "mediumpurple" => Some(0xFF9370DB),
631        "slateblue" => Some(0xFF6A5ACD),
632        "mediumslateblue" => Some(0xFF7B68EE),
633        "darkslateblue" => Some(0xFF483D8B),
634        "midnightblue" => Some(0xFF191970),
635        "cornflowerblue" => Some(0xFF6495ED),
636        "deepskyblue" => Some(0xFF00BFFF),
637        "lightskyblue" => Some(0xFF87CEFA),
638        "lightsteelblue" => Some(0xFFB0C4DE),
639        "darkslategray" | "darkslategrey" => Some(0xFF2F4F4F),
640        "dimgray" | "dimgrey" => Some(0xFF696969),
641        "gainsboro" => Some(0xFFDCDCDC),
642        "snow" => Some(0xFFFFFAFA),
643        "lightcoral" => Some(0xFFF08080),
644        "darksalmon" => Some(0xFFE9967A),
645        "lightsalmon" => Some(0xFFFFA07A),
646        "orangered" => Some(0xFFFF4500),
647        "darkorange" => Some(0xFFFF8C00),
648        "firebrick" => Some(0xFFB22222),
649        "currentcolor" => None, // コンテキスト依存のため呼び出し側で処理
650        _ => None,
651    }
652}
653