1
#[allow(clippy::wildcard_imports)] // widget/render module pulls in the css property/value types it builds with
2
use super::*;
3

            
4
use azul_core::resources::ImageRef;
5
use agg_rust::basics::{FillingRule, PATH_FLAGS_NONE};
6
use agg_rust::color::Rgba8;
7
use agg_rust::conv_stroke::ConvStroke;
8
use agg_rust::conv_transform::ConvTransform;
9
use agg_rust::path_storage::PathStorage;
10
use agg_rust::trans_affine::TransAffine;
11

            
12
/// Render raw SVG bytes to a PNG image.
13
///
14
/// Parses the SVG XML, walks the element tree, extracts path geometry +
15
/// fill/stroke attributes, and rasterizes via agg-rust directly (no CSS
16
/// layout involved).
17
#[cfg(all(feature = "std", feature = "xml"))]
18
/// # Errors
19
///
20
/// Returns an error string if the SVG cannot be parsed or rendered.
21
106
pub fn render_svg_to_png(
22
106
    svg_data: &[u8],
23
106
    target_width: u32,
24
106
    target_height: u32,
25
106
) -> Result<Vec<u8>, String> {
26
106
    let svg_str =
27
106
        core::str::from_utf8(svg_data).map_err(|e| format!("SVG is not valid UTF-8: {e}"))?;
28

            
29
106
    let nodes =
30
106
        crate::xml::parse_xml_string(svg_str).map_err(|e| format!("XML parse error: {e}"))?;
31

            
32
    // Find the <svg> root
33
106
    let node_slice: &[azul_core::xml::XmlNodeChild] = nodes.as_ref();
34
106
    let svg_node = node_slice
35
106
        .iter()
36
106
        .find_map(|n| {
37
106
            if let azul_core::xml::XmlNodeChild::Element(e) = n {
38
106
                let tag = e.node_type.as_str().to_lowercase();
39
106
                if tag == "svg" {
40
106
                    Some(e)
41
                } else {
42
                    None
43
                }
44
            } else {
45
                None
46
            }
47
106
        })
48
106
        .ok_or_else(|| "No <svg> root element found".to_string())?;
49

            
50
    // Parse viewBox for coordinate mapping
51
106
    let vb = parse_viewbox(svg_node);
52
106
    let (vb_x, vb_y, vb_w, vb_h) =
53
106
        vb.unwrap_or_else(|| (0.0, 0.0, f64::from(target_width), f64::from(target_height)));
54

            
55
106
    let sx = f64::from(target_width) / vb_w;
56
106
    let sy = f64::from(target_height) / vb_h;
57
106
    let scale = sx.min(sy);
58

            
59
106
    let root_transform =
60
106
        TransAffine::new_custom(scale, 0.0, 0.0, scale, -vb_x * scale, -vb_y * scale);
61

            
62
106
    let mut pixmap = AzulPixmap::new(target_width, target_height)
63
106
        .ok_or_else(|| "Failed to create pixmap".to_string())?;
64
106
    pixmap.fill(255, 255, 255, 255);
65

            
66
106
    render_svg_group(svg_node, &mut pixmap, &root_transform);
67

            
68
106
    pixmap
69
106
        .encode_png()
70
106
        .map_err(|e| format!("PNG encode error: {e}"))
71
106
}
72

            
73
/// Like [`render_svg_to_png`] but returns the rendered pixmap as an [`ImageRef`]
74
/// (RGBA8) directly — no PNG round-trip.
75
///
76
/// The `MapWidget` uses this to render each
77
/// decoded tile SVG to a colour image node: `SvgNodeData::Path` in the DOM only
78
/// produces a clip mask (not a filled shape), so reuse the same `render_svg_group`
79
/// rasteriser the tiger uses (which reads SVG fill/stroke attrs) and embed the
80
/// result as an image.
81
/// # Errors
82
///
83
/// Returns an error string if the SVG cannot be parsed or rendered.
84
pub fn render_svg_to_imageref(
85
    svg_data: &[u8],
86
    target_width: u32,
87
    target_height: u32,
88
) -> Result<ImageRef, String> {
89
    let svg_str =
90
        core::str::from_utf8(svg_data).map_err(|e| format!("SVG is not valid UTF-8: {e}"))?;
91
    let nodes =
92
        crate::xml::parse_xml_string(svg_str).map_err(|e| format!("XML parse error: {e}"))?;
93
    let node_slice: &[azul_core::xml::XmlNodeChild] = nodes.as_ref();
94
    let svg_node = node_slice
95
        .iter()
96
        .find_map(|n| {
97
            if let azul_core::xml::XmlNodeChild::Element(e) = n {
98
                if e.node_type.as_str().to_lowercase() == "svg" {
99
                    Some(e)
100
                } else {
101
                    None
102
                }
103
            } else {
104
                None
105
            }
106
        })
107
        .ok_or_else(|| "No <svg> root element found".to_string())?;
108

            
109
    let vb = parse_viewbox(svg_node);
110
    let (vb_x, vb_y, vb_w, vb_h) =
111
        vb.unwrap_or_else(|| (0.0, 0.0, f64::from(target_width), f64::from(target_height)));
112
    let scale = (f64::from(target_width) / vb_w).min(f64::from(target_height) / vb_h);
113
    let root_transform =
114
        TransAffine::new_custom(scale, 0.0, 0.0, scale, -vb_x * scale, -vb_y * scale);
115

            
116
    let mut pixmap = AzulPixmap::new(target_width, target_height)
117
        .ok_or_else(|| "Failed to create pixmap".to_string())?;
118
    // Transparent background so the tile container shows through any gaps.
119
    pixmap.fill(0, 0, 0, 0);
120
    render_svg_group(svg_node, &mut pixmap, &root_transform);
121

            
122
    let rgba = pixmap.data().to_vec();
123
    let raw = azul_core::resources::RawImage {
124
        pixels: azul_core::resources::RawImageData::U8(rgba.into()),
125
        width: target_width as usize,
126
        height: target_height as usize,
127
        premultiplied_alpha: false,
128
        data_format: azul_core::resources::RawImageFormat::RGBA8,
129
        tag: Vec::new().into(),
130
    };
131
    ImageRef::new_rawimage(raw).ok_or_else(|| "Failed to build ImageRef from pixmap".to_string())
132
}
133

            
134
#[cfg(all(feature = "std", feature = "xml"))]
135
106
fn parse_viewbox(node: &azul_core::xml::XmlNode) -> Option<(f64, f64, f64, f64)> {
136
106
    let vb = node
137
106
        .attributes
138
106
        .get_key("viewbox")
139
106
        .or_else(|| node.attributes.get_key("viewBox"))?;
140
106
    let nums: Vec<f64> = vb
141
106
        .as_str()
142
1643
        .split(|c: char| c == ',' || c.is_ascii_whitespace())
143
424
        .filter(|s| !s.is_empty())
144
424
        .filter_map(|s| s.parse().ok())
145
106
        .collect();
146
106
    if nums.len() == 4 {
147
106
        Some((nums[0], nums[1], nums[2], nums[3]))
148
    } else {
149
        None
150
    }
151
106
}
152

            
153
/// Inherited SVG style (fill, stroke, stroke-width) that cascades from parent groups.
154
#[cfg(all(feature = "std", feature = "xml"))]
155
#[derive(Clone)]
156
#[derive(Default)]
157
struct SvgInheritedStyle {
158
    fill: Option<String>,   // None = not set (inherit default black)
159
    stroke: Option<String>, // None = not set (inherit default none)
160
    stroke_width: Option<f64>,
161
}
162

            
163
#[cfg(all(feature = "std", feature = "xml"))]
164
106
fn render_svg_group(
165
106
    node: &azul_core::xml::XmlNode,
166
106
    pixmap: &mut AzulPixmap,
167
106
    parent_transform: &TransAffine,
168
106
) {
169
106
    render_svg_group_with_style(
170
106
        node,
171
106
        pixmap,
172
106
        parent_transform,
173
106
        &SvgInheritedStyle::default(),
174
    );
175
106
}
176

            
177
#[cfg(all(feature = "std", feature = "xml"))]
178
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] // bounded graphics/coord/font/fixed-point/debug-marker cast
179
#[allow(clippy::too_many_lines)] // large but cohesive: single-purpose layout/render/parse routine (one branch per case)
180
13515
fn render_svg_group_with_style(
181
13515
    node: &azul_core::xml::XmlNode,
182
13515
    pixmap: &mut AzulPixmap,
183
13515
    parent_transform: &TransAffine,
184
13515
    parent_style: &SvgInheritedStyle,
185
13515
) {
186
    use agg_rust::math_stroke::{LineCap, LineJoin};
187
    use azul_core::xml::{XmlNode, XmlNodeChild};
188

            
189
13515
    let group_transform = node.attributes.get_key("transform").map_or(*parent_transform, |t| {
190
106
        let mut tf = parse_svg_transform(t.as_str());
191
106
        tf.premultiply(parent_transform);
192
106
        tf
193
106
    });
194

            
195
    // Inherit style from this group's attributes
196
13515
    let group_style = SvgInheritedStyle {
197
13515
        fill: node
198
13515
            .attributes
199
13515
            .get_key("fill")
200
13515
            .map(|s| s.as_str().to_string())
201
13515
            .or_else(|| parent_style.fill.clone()),
202
13515
        stroke: node
203
13515
            .attributes
204
13515
            .get_key("stroke")
205
13515
            .map(|s| s.as_str().to_string())
206
13515
            .or_else(|| parent_style.stroke.clone()),
207
13515
        stroke_width: node
208
13515
            .attributes
209
13515
            .get_key("stroke-width")
210
13515
            .and_then(|s| s.as_str().parse().ok())
211
13515
            .or(parent_style.stroke_width),
212
    };
213

            
214
65773
    for child in node.children.as_ref() {
215
65773
        let XmlNodeChild::Element(child_node) = child else {
216
39432
            continue;
217
        };
218

            
219
26341
        let tag = child_node.node_type.as_str().to_lowercase();
220

            
221
26341
        match tag.as_str() {
222
26341
            "g" | "svg" => {
223
12826
                render_svg_group_with_style(child_node, pixmap, &group_transform, &group_style);
224
12826
            }
225
13515
            "path" | "circle" | "rect" | "ellipse" | "line" | "polygon" | "polyline" => {
226
12932
                let Some(path_storage) = build_agg_path(child_node) else {
227
                    continue;
228
                };
229

            
230
                // Flatten bezier curves into line segments for the rasterizer
231
12932
                let mut curved = agg_rust::conv_curve::ConvCurve::new(path_storage);
232

            
233
                // Per-element transform
234
12932
                let elem_transform = child_node.attributes.get_key("transform").map_or(group_transform, |t| {
235
                    let mut tf = parse_svg_transform(t.as_str());
236
                    tf.premultiply(&group_transform);
237
                    tf
238
                });
239

            
240
                // Fill: element overrides group
241
12932
                let fill_attr = child_node
242
12932
                    .attributes
243
12932
                    .get_key("fill")
244
12932
                    .map(|s| s.as_str().to_string())
245
12932
                    .or_else(|| group_style.fill.clone());
246
12932
                let fill_color = match fill_attr.as_deref() {
247
12720
                    Some("none") => None,
248
12031
                    Some(c) => parse_svg_color(c),
249
212
                    None => Some(Rgba8 {
250
212
                        r: 0,
251
212
                        g: 0,
252
212
                        b: 0,
253
212
                        a: 255,
254
212
                    }), // SVG default
255
                };
256

            
257
12932
                let fill_opacity = child_node
258
12932
                    .attributes
259
12932
                    .get_key("fill-opacity")
260
12932
                    .and_then(|s| s.as_str().parse::<f64>().ok())
261
12932
                    .unwrap_or(1.0);
262

            
263
12932
                let opacity = child_node
264
12932
                    .attributes
265
12932
                    .get_key("opacity")
266
12932
                    .and_then(|s| s.as_str().parse::<f64>().ok())
267
12932
                    .unwrap_or(1.0);
268

            
269
12932
                if let Some(mut color) = fill_color {
270
12243
                    color.a = (f64::from(color.a) * fill_opacity * opacity).min(255.0) as u8;
271

            
272
12243
                    let fill_rule_str = child_node
273
12243
                        .attributes
274
12243
                        .get_key("fill-rule")
275
12243
                        .map(|s| s.as_str().to_string());
276
12243
                    let rule = match fill_rule_str.as_deref() {
277
                        Some("evenodd") => FillingRule::EvenOdd,
278
12243
                        _ => FillingRule::NonZero,
279
                    };
280

            
281
12243
                    let mut transformed = ConvTransform::new(&mut curved, elem_transform);
282
12243
                    agg_fill_path(pixmap, &mut transformed, &color, rule);
283
689
                }
284

            
285
                // Stroke: element overrides group
286
12932
                let stroke_attr = child_node
287
12932
                    .attributes
288
12932
                    .get_key("stroke")
289
12932
                    .map(|s| s.as_str().to_string())
290
12932
                    .or_else(|| group_style.stroke.clone());
291
12932
                let stroke_color = match stroke_attr.as_deref() {
292
8798
                    Some("none") | None => None,
293
4134
                    Some(c) => parse_svg_color(c),
294
                };
295

            
296
12932
                if let Some(mut color) = stroke_color {
297
4134
                    let stroke_opacity = child_node
298
4134
                        .attributes
299
4134
                        .get_key("stroke-opacity")
300
4134
                        .and_then(|s| s.as_str().parse::<f64>().ok())
301
4134
                        .unwrap_or(1.0);
302
4134
                    color.a = (f64::from(color.a) * stroke_opacity * opacity).min(255.0) as u8;
303

            
304
4134
                    let stroke_width = child_node
305
4134
                        .attributes
306
4134
                        .get_key("stroke-width")
307
4134
                        .and_then(|s| s.as_str().parse::<f64>().ok())
308
4134
                        .or(group_style.stroke_width)
309
4134
                        .unwrap_or(1.0);
310

            
311
4134
                    let mut conv_stroke = ConvStroke::new(&mut curved);
312
4134
                    conv_stroke.set_width(stroke_width);
313
4134
                    conv_stroke.set_line_cap(LineCap::Round);
314
4134
                    conv_stroke.set_line_join(LineJoin::Round);
315

            
316
4134
                    let mut transformed =
317
4134
                        ConvTransform::new(&mut conv_stroke, elem_transform);
318
4134
                    agg_fill_path(pixmap, &mut transformed, &color, FillingRule::NonZero);
319
8798
                }
320
            }
321
583
            _ => {
322
583
                // Recurse into unknown containers (defs, symbol, etc.)
323
583
                render_svg_group_with_style(child_node, pixmap, &group_transform, &group_style);
324
583
            }
325
        }
326
    }
327
13515
}
328

            
329
/// Build an agg `PathStorage` from an SVG shape element's attributes.
330
#[cfg(all(feature = "std", feature = "xml"))]
331
#[allow(clippy::cast_possible_truncation)] // bounded graphics/coord/font/fixed-point/debug-marker cast
332
12932
fn build_agg_path(node: &azul_core::xml::XmlNode) -> Option<PathStorage> {
333
    const KAPPA: f64 = 0.552_284_749_8;
334
12932
    let tag = node.node_type.as_str().to_lowercase();
335
12932
    match tag.as_str() {
336
12932
        "path" => {
337
12826
            let d = node.attributes.get_key("d")?;
338
12826
            let mp = azul_core::path_parser::parse_svg_path_d(d.as_str()).ok()?;
339
12826
            Some(svg_multi_polygon_to_path_storage(&mp))
340
        }
341
106
        "circle" => {
342
53
            let cx = attr_f64(node, "cx");
343
53
            let cy = attr_f64(node, "cy");
344
53
            let r = attr_f64(node, "r");
345
53
            if r <= 0.0 {
346
                return None;
347
53
            }
348
53
            let mp = azul_core::path_parser::svg_circle_to_paths(cx as f32, cy as f32, r as f32);
349
53
            let multi = azul_core::svg::SvgMultiPolygon {
350
53
                rings: azul_core::svg::SvgPathVec::from_vec(vec![mp]),
351
53
            };
352
53
            Some(svg_multi_polygon_to_path_storage(&multi))
353
        }
354
53
        "rect" => {
355
53
            let x = attr_f64(node, "x");
356
53
            let y = attr_f64(node, "y");
357
53
            let w = attr_f64(node, "width");
358
53
            let h = attr_f64(node, "height");
359
53
            let rx = attr_f64(node, "rx");
360
53
            let ry = node.attributes.get_key("ry").map_or(rx, |v| v.as_str().parse().unwrap_or(rx));
361
53
            if w <= 0.0 || h <= 0.0 {
362
                return None;
363
53
            }
364
53
            let mp = azul_core::path_parser::svg_rect_to_path(
365
53
                x as f32, y as f32, w as f32, h as f32, rx as f32, ry as f32,
366
            );
367
53
            let multi = azul_core::svg::SvgMultiPolygon {
368
53
                rings: azul_core::svg::SvgPathVec::from_vec(vec![mp]),
369
53
            };
370
53
            Some(svg_multi_polygon_to_path_storage(&multi))
371
        }
372
        "ellipse" => {
373
            let cx = attr_f64(node, "cx");
374
            let cy = attr_f64(node, "cy");
375
            let rx = attr_f64(node, "rx");
376
            let ry = attr_f64(node, "ry");
377
            if rx <= 0.0 || ry <= 0.0 {
378
                return None;
379
            }
380
            // Use circle path with scaling
381
            let mp = azul_core::path_parser::svg_circle_to_paths(cx as f32, cy as f32, 1.0);
382
            let multi = azul_core::svg::SvgMultiPolygon {
383
                rings: azul_core::svg::SvgPathVec::from_vec(vec![mp]),
384
            };
385
            let mut ps = svg_multi_polygon_to_path_storage(&multi);
386
            // Scale ellipse: we'll just build it directly instead
387
            let mut path = PathStorage::new();
388
            let kx = rx * KAPPA;
389
            let ky = ry * KAPPA;
390
            path.move_to(cx, cy - ry);
391
            path.curve4(cx + kx, cy - ry, cx + rx, cy - ky, cx + rx, cy);
392
            path.curve4(cx + rx, cy + ky, cx + kx, cy + ry, cx, cy + ry);
393
            path.curve4(cx - kx, cy + ry, cx - rx, cy + ky, cx - rx, cy);
394
            path.curve4(cx - rx, cy - ky, cx - kx, cy - ry, cx, cy - ry);
395
            path.close_polygon(PATH_FLAGS_NONE);
396
            Some(path)
397
        }
398
        "line" => {
399
            let x1 = attr_f64(node, "x1");
400
            let y1 = attr_f64(node, "y1");
401
            let x2 = attr_f64(node, "x2");
402
            let y2 = attr_f64(node, "y2");
403
            let mut path = PathStorage::new();
404
            path.move_to(x1, y1);
405
            path.line_to(x2, y2);
406
            Some(path)
407
        }
408
        "polygon" | "polyline" => {
409
            let pts_str = node.attributes.get_key("points")?;
410
            let nums: Vec<f64> = pts_str
411
                .as_str()
412
                .split(|c: char| c == ',' || c.is_ascii_whitespace())
413
                .filter(|s| !s.is_empty())
414
                .filter_map(|s| s.parse().ok())
415
                .collect();
416
            if nums.len() < 4 {
417
                return None;
418
            }
419
            let mut path = PathStorage::new();
420
            path.move_to(nums[0], nums[1]);
421
            for chunk in nums[2..].chunks_exact(2) {
422
                path.line_to(chunk[0], chunk[1]);
423
            }
424
            if tag == "polygon" {
425
                path.close_polygon(PATH_FLAGS_NONE);
426
            }
427
            Some(path)
428
        }
429
        _ => None,
430
    }
431
12932
}
432

            
433
#[cfg(all(feature = "std", feature = "xml"))]
434
424
fn attr_f64(node: &azul_core::xml::XmlNode, key: &str) -> f64 {
435
424
    node.attributes
436
424
        .get_key(key)
437
424
        .and_then(|s| s.as_str().parse().ok())
438
424
        .unwrap_or(0.0)
439
424
}
440

            
441
/// Convert `SvgMultiPolygon` to agg `PathStorage`.
442
#[cfg(all(feature = "std", feature = "xml"))]
443
12932
fn svg_multi_polygon_to_path_storage(mp: &azul_core::svg::SvgMultiPolygon) -> PathStorage {
444
12932
    let mut path = PathStorage::new();
445
12932
    for ring in mp.rings.as_ref() {
446
12879
        let mut first = true;
447
111459
        for item in ring.items.as_ref() {
448
111459
            match item {
449
11236
                azul_core::svg::SvgPathElement::Line(l) => {
450
11236
                    if first {
451
636
                        path.move_to(f64::from(l.start.x), f64::from(l.start.y));
452
636
                        first = false;
453
10600
                    }
454
11236
                    path.line_to(f64::from(l.end.x), f64::from(l.end.y));
455
                }
456
                azul_core::svg::SvgPathElement::QuadraticCurve(q) => {
457
                    if first {
458
                        path.move_to(f64::from(q.start.x), f64::from(q.start.y));
459
                        first = false;
460
                    }
461
                    path.curve3(
462
                        f64::from(q.ctrl.x),
463
                        f64::from(q.ctrl.y),
464
                        f64::from(q.end.x),
465
                        f64::from(q.end.y),
466
                    );
467
                }
468
100223
                azul_core::svg::SvgPathElement::CubicCurve(c) => {
469
100223
                    if first {
470
12243
                        path.move_to(f64::from(c.start.x), f64::from(c.start.y));
471
12243
                        first = false;
472
87980
                    }
473
100223
                    path.curve4(
474
100223
                        f64::from(c.ctrl_1.x),
475
100223
                        f64::from(c.ctrl_1.y),
476
100223
                        f64::from(c.ctrl_2.x),
477
100223
                        f64::from(c.ctrl_2.y),
478
100223
                        f64::from(c.end.x),
479
100223
                        f64::from(c.end.y),
480
                    );
481
                }
482
            }
483
        }
484
12879
        path.close_polygon(PATH_FLAGS_NONE);
485
    }
486
12932
    path
487
12932
}
488

            
489
/// Parse SVG transform attribute (supports matrix, translate, scale, rotate).
490
#[cfg(all(feature = "std", feature = "xml"))]
491
106
fn parse_svg_transform(s: &str) -> TransAffine {
492
106
    let s = s.trim();
493

            
494
106
    let parse_nums = |inner: &str| -> Vec<f64> {
495
106
        inner
496
3392
            .split(|c: char| c == ',' || c.is_ascii_whitespace())
497
424
            .filter(|s| !s.is_empty())
498
424
            .filter_map(|s| s.parse().ok())
499
106
            .collect()
500
106
    };
501

            
502
106
    if let Some(inner) = s.strip_prefix("matrix(").and_then(|s| s.strip_suffix(')')) {
503
53
        let nums = parse_nums(inner);
504
53
        if nums.len() == 6 {
505
53
            return TransAffine::new_custom(nums[0], nums[1], nums[2], nums[3], nums[4], nums[5]);
506
        }
507
53
    } else if let Some(inner) = s
508
53
        .strip_prefix("translate(")
509
53
        .and_then(|s| s.strip_suffix(')'))
510
    {
511
53
        let nums = parse_nums(inner);
512
53
        let tx = nums.first().copied().unwrap_or(0.0);
513
53
        let ty = nums.get(1).copied().unwrap_or(0.0);
514
53
        return TransAffine::new_custom(1.0, 0.0, 0.0, 1.0, tx, ty);
515
    } else if let Some(inner) = s.strip_prefix("scale(").and_then(|s| s.strip_suffix(')')) {
516
        let nums = parse_nums(inner);
517
        let sx = nums.first().copied().unwrap_or(1.0);
518
        let sy = nums.get(1).copied().unwrap_or(sx);
519
        return TransAffine::new_custom(sx, 0.0, 0.0, sy, 0.0, 0.0);
520
    } else if let Some(inner) = s.strip_prefix("rotate(").and_then(|s| s.strip_suffix(')')) {
521
        let nums = parse_nums(inner);
522
        let angle = nums.first().copied().unwrap_or(0.0).to_radians();
523
        let cos_a = angle.cos();
524
        let sin_a = angle.sin();
525
        return TransAffine::new_custom(cos_a, sin_a, -sin_a, cos_a, 0.0, 0.0);
526
    }
527
    TransAffine::new()
528
106
}
529

            
530
/// Parse SVG color string (#RRGGBB, #RGB, named colors).
531
#[cfg(all(feature = "std", feature = "xml"))]
532
16165
fn parse_svg_color(s: &str) -> Option<Rgba8> {
533
16165
    let s = s.trim();
534
16165
    if let Some(hex) = s.strip_prefix('#') {
535
16165
        return match hex.len() {
536
            6 => {
537
3074
                let r = u8::from_str_radix(&hex[0..2], 16).ok()?;
538
3074
                let g = u8::from_str_radix(&hex[2..4], 16).ok()?;
539
3074
                let b = u8::from_str_radix(&hex[4..6], 16).ok()?;
540
3074
                Some(Rgba8 { r, g, b, a: 255 })
541
            }
542
            3 => {
543
13091
                let r = u8::from_str_radix(&hex[0..1], 16).ok()? * 17;
544
13091
                let g = u8::from_str_radix(&hex[1..2], 16).ok()? * 17;
545
13091
                let b = u8::from_str_radix(&hex[2..3], 16).ok()? * 17;
546
13091
                Some(Rgba8 { r, g, b, a: 255 })
547
            }
548
            _ => None,
549
        };
550
    }
551
    match s.to_lowercase().as_str() {
552
        "black" => Some(Rgba8 {
553
            r: 0,
554
            g: 0,
555
            b: 0,
556
            a: 255,
557
        }),
558
        "white" => Some(Rgba8 {
559
            r: 255,
560
            g: 255,
561
            b: 255,
562
            a: 255,
563
        }),
564
        "red" => Some(Rgba8 {
565
            r: 255,
566
            g: 0,
567
            b: 0,
568
            a: 255,
569
        }),
570
        "green" => Some(Rgba8 {
571
            r: 0,
572
            g: 128,
573
            b: 0,
574
            a: 255,
575
        }),
576
        "blue" => Some(Rgba8 {
577
            r: 0,
578
            g: 0,
579
            b: 255,
580
            a: 255,
581
        }),
582
        "yellow" => Some(Rgba8 {
583
            r: 255,
584
            g: 255,
585
            b: 0,
586
            a: 255,
587
        }),
588
        "orange" => Some(Rgba8 {
589
            r: 255,
590
            g: 165,
591
            b: 0,
592
            a: 255,
593
        }),
594
        "gold" => Some(Rgba8 {
595
            r: 255,
596
            g: 215,
597
            b: 0,
598
            a: 255,
599
        }),
600
        _ => None,
601
    }
602
16165
}
603