diff --git a/programs/include/gui/svg.hpp b/programs/include/gui/svg.hpp index 7f99a9d..5e43dc1 100644 --- a/programs/include/gui/svg.hpp +++ b/programs/include/gui/svg.hpp @@ -1,13 +1,15 @@ /* * svg.hpp * MontaukOS SVG icon parser and scanline rasterizer - * Handles the Flat-Remix symbolic icon subset (path, circle, rect) + * Handles paths, circles, ellipses, rectangles, inherited paint, + * strokes, gradients, and nested affine transforms. * All math uses 16.16 fixed-point -- NO floating point. * Copyright (c) 2025 Daniel Hammer */ #pragma once #include "gui/gui.hpp" +#include #include namespace gui { @@ -28,19 +30,58 @@ struct SvgEdge { fixed_t x0, y0, x1, y1; }; +// SVG affine transform, stored as the standard [a c e; b d f; 0 0 1] +// matrix. Keeping transforms in user space until an edge is emitted lets +// nested groups and complete transform lists compose correctly. +struct SvgTransform { + fixed_t a, b, c, d, e, f; + + static SvgTransform identity() { + return {int_to_fixed(1), 0, 0, int_to_fixed(1), 0, 0}; + } +}; + +inline SvgTransform svg_transform_multiply(const SvgTransform& lhs, + const SvgTransform& rhs) { + return { + fixed_mul(lhs.a, rhs.a) + fixed_mul(lhs.c, rhs.b), + fixed_mul(lhs.b, rhs.a) + fixed_mul(lhs.d, rhs.b), + fixed_mul(lhs.a, rhs.c) + fixed_mul(lhs.c, rhs.d), + fixed_mul(lhs.b, rhs.c) + fixed_mul(lhs.d, rhs.d), + fixed_mul(lhs.a, rhs.e) + fixed_mul(lhs.c, rhs.f) + lhs.e, + fixed_mul(lhs.b, rhs.e) + fixed_mul(lhs.d, rhs.f) + lhs.f, + }; +} + +inline void svg_transform_point(const SvgTransform& t, fixed_t x, fixed_t y, + fixed_t* out_x, fixed_t* out_y) { + *out_x = fixed_mul(t.a, x) + fixed_mul(t.c, y) + t.e; + *out_y = fixed_mul(t.b, x) + fixed_mul(t.d, y) + t.f; +} + // --------------------------------------------------------------------------- // Constants // --------------------------------------------------------------------------- -static constexpr int SVG_MAX_EDGES = 8192; -static constexpr int SVG_MAX_PATH_LEN = 8192; -static constexpr int SVG_MAX_FILE_SIZE = 32768; +static constexpr int SVG_MAX_EDGES = 131072; +static constexpr int SVG_MAX_FILE_SIZE = 1024 * 1024; static constexpr int SVG_BEZIER_STEPS = 8; static constexpr int SVG_MAX_GRADIENTS = 8; +static constexpr int SVG_MAX_GRADIENT_STOPS = 8; + +struct SvgGradientStop { + fixed_t offset; + Color color; +}; -// Gradient color table — stores first stop color for url(#id) resolution struct SvgGradient { char id[32]; - Color color; // first stop-color + bool radial; + bool user_space; + fixed_t x1, y1, x2, y2; // linear endpoints + fixed_t cx, cy, radius; // radial geometry + SvgTransform transform; + SvgGradientStop stops[SVG_MAX_GRADIENT_STOPS]; + int stop_count; }; struct SvgGradientTable { @@ -49,17 +90,12 @@ struct SvgGradientTable { void clear() { count = 0; } - void add(const char* id, Color c) { + void add(const SvgGradient& gradient) { if (count >= SVG_MAX_GRADIENTS) return; - int i = 0; - while (id[i] && i < 31) { entries[count].id[i] = id[i]; i++; } - entries[count].id[i] = '\0'; - entries[count].color = c; - count++; + entries[count++] = gradient; } - // Look up gradient by id. Returns true if found. - bool lookup(const char* id, Color* out) const { + const SvgGradient* lookup_gradient(const char* id) const { for (int i = 0; i < count; i++) { const char* a = entries[i].id; const char* b = id; @@ -69,11 +105,18 @@ struct SvgGradientTable { a++; b++; } if (match && *a == '\0' && *b == '\0') { - *out = entries[i].color; - return true; + return &entries[i]; } } - return false; + return nullptr; + } + + // Solid-color fallback for unsupported gradient contexts. + bool lookup(const char* id, Color* out) const { + const SvgGradient* gradient = lookup_gradient(id); + if (!gradient || gradient->stop_count == 0) return false; + *out = gradient->stops[0].color; + return true; } }; @@ -151,6 +194,39 @@ inline fixed_t svg_fixed_hypot(fixed_t x, fixed_t y) { return (fixed_t)svg_int64_sqrt((uint64_t)sq); } +inline fixed_t svg_transform_average_scale(const SvgTransform& t) { + fixed_t sx = svg_fixed_hypot(t.a, t.b); + fixed_t sy = svg_fixed_hypot(t.c, t.d); + return (sx + sy) / 2; +} + +// Fixed-point sine for SVG rotate(). A 15-degree table with linear +// interpolation is accurate enough for icon rasterization and keeps the +// freestanding renderer free of floating-point/libm dependencies. +inline fixed_t svg_fixed_sin_degrees(fixed_t degrees) { + const fixed_t full = int_to_fixed(360); + while (degrees < 0) degrees += full; + while (degrees >= full) degrees -= full; + + int quadrant = fixed_to_int(degrees) / 90; + fixed_t local = degrees - int_to_fixed(quadrant * 90); + if (quadrant == 1 || quadrant == 3) local = int_to_fixed(90) - local; + + static const fixed_t sin15[7] = { + 0, 16962, 32768, 46341, 56756, 63303, 65536 + }; + fixed_t slot = fixed_div(local, int_to_fixed(15)); + int index = fixed_to_int(slot); + if (index >= 6) index = 6; + fixed_t value = sin15[index]; + if (index < 6) { + fixed_t fraction = slot - int_to_fixed(index); + value += fixed_mul(sin15[index + 1] - sin15[index], fraction); + } + if (quadrant >= 2) value = -value; + return value; +} + // --------------------------------------------------------------------------- // Fixed-point number parser (NO floating point) // Parses strings like "3.25", "-0.5", ".1115", "16" @@ -318,6 +394,40 @@ inline int svg_get_attr(const char* tag, int tagLen, const char* attr, char* buf return -1; } +// Return an attribute value as a span into the original SVG source. Unlike +// svg_get_attr(), this does not truncate long values such as complex path +// data in full-size illustrations. +inline bool svg_get_attr_span(const char* tag, int tagLen, const char* attr, + const char** out_value, int* out_len) { + int attrLen = svg_strlen(attr); + const char* search_start = tag; + int search_len = tagLen; + while (search_len > 0) { + const char* p = svg_strstr(search_start, search_len, attr); + if (!p) return false; + const char* after_name = p + attrLen; + if (after_name < tag + tagLen && svg_char_is_attrname(*after_name)) { + search_start = after_name; + search_len = tagLen - (int)(search_start - tag); + continue; + } + p = after_name; + while (p < tag + tagLen && svg_char_is_ws(*p)) ++p; + if (p >= tag + tagLen || *p != '=') return false; + ++p; + while (p < tag + tagLen && svg_char_is_ws(*p)) ++p; + if (p >= tag + tagLen || (*p != '"' && *p != '\'')) return false; + char quote = *p++; + const char* value = p; + while (p < tag + tagLen && *p != quote) ++p; + if (p >= tag + tagLen) return false; + *out_value = value; + *out_len = (int)(p - value); + return true; + } + return false; +} + // Parse an integer from a string (no sign, simple decimal). inline int svg_parse_int(const char* s) { int v = 0; @@ -328,6 +438,28 @@ inline int svg_parse_int(const char* s) { return v; } +// Parse an SVG/CSS absolute length into CSS pixels (96 px per inch). +// Percentages need viewport context and therefore return zero so callers can +// fall back to viewBox dimensions. +inline int svg_parse_length_pixels(const char* s) { + fixed_t value = 0; + int consumed = svg_parse_fixed(s, &value); + if (consumed <= 0 || value <= 0) return 0; + const char* unit = s + consumed; + while (svg_char_is_ws(*unit)) ++unit; + + fixed_t factor = int_to_fixed(1); + if (*unit == '%') return 0; + if (svg_strncmp(unit, "pt", 2)) factor = fixed_from_parts(1, 1, 3); // 96/72 + else if (svg_strncmp(unit, "pc", 2)) factor = int_to_fixed(16); + else if (svg_strncmp(unit, "in", 2)) factor = int_to_fixed(96); + else if (svg_strncmp(unit, "cm", 2)) factor = fixed_from_parts(37, 203, 254); // 9600/254 + else if (svg_strncmp(unit, "mm", 2)) factor = fixed_from_parts(3, 199, 254); // 960/254 + + fixed_t pixels = fixed_mul(value, factor); + return (pixels + (1 << 15)) >> 16; +} + // Parse a hex color like "#5c616c" or "#fff" into a Color. inline Color svg_parse_hex_color(const char* s) { if (*s == '#') ++s; @@ -368,15 +500,23 @@ struct SvgEdgeList { void init(int cap) { edges = (SvgEdge*)montauk::malloc(cap * sizeof(SvgEdge)); count = 0; - capacity = cap; + capacity = edges ? cap : 0; } void clear() { count = 0; } void add(fixed_t x0, fixed_t y0, fixed_t x1, fixed_t y1) { - if (count >= capacity) return; - // skip horizontal edges (they don't contribute to scanline crossings) - if (y0 == y1) return; + if (count >= capacity) { + if (capacity >= SVG_MAX_EDGES) return; + int new_capacity = capacity > 0 ? capacity * 2 : 64; + if (new_capacity > SVG_MAX_EDGES) new_capacity = SVG_MAX_EDGES; + auto* grown = (SvgEdge*)montauk::realloc(edges, new_capacity * sizeof(SvgEdge)); + if (!grown) return; + edges = grown; + capacity = new_capacity; + } + // Horizontal edges do not contribute to fill crossings, but must be + // retained because they are visible when a path has a stroke. edges[count++] = {x0, y0, x1, y1}; } }; @@ -472,6 +612,48 @@ inline void svg_circle_edges(SvgEdgeList& el, fixed_t cx, fixed_t cy, fixed_t r) } } +// Ellipse transformed by an arbitrary affine matrix. This also handles +// circles inside translated/scaled groups without losing non-uniform scale. +inline void svg_ellipse_edges(SvgEdgeList& el, fixed_t cx, fixed_t cy, + fixed_t rx, fixed_t ry, + const SvgTransform& transform) { + static const fixed_t cos16[16] = { + 65536, 60547, 46341, 25080, 0, -25080, -46341, -60547, + -65536, -60547, -46341, -25080, 0, 25080, 46341, 60547 + }; + static const fixed_t sin16[16] = { + 0, 25080, 46341, 60547, 65536, 60547, 46341, 25080, + 0, -25080, -46341, -60547, -65536, -60547, -46341, -25080 + }; + + fixed_t px, py; + svg_transform_point(transform, cx + rx, cy, &px, &py); + for (int i = 1; i <= 16; ++i) { + int idx = i & 15; + fixed_t ux = cx + fixed_mul(rx, cos16[idx]); + fixed_t uy = cy + fixed_mul(ry, sin16[idx]); + fixed_t nx, ny; + svg_transform_point(transform, ux, uy, &nx, &ny); + el.add(px, py, nx, ny); + px = nx; + py = ny; + } +} + +inline void svg_rect_edges_transformed(SvgEdgeList& el, fixed_t x, fixed_t y, + fixed_t w, fixed_t h, + const SvgTransform& transform) { + fixed_t x0, y0, x1, y1, x2, y2, x3, y3; + svg_transform_point(transform, x, y, &x0, &y0); + svg_transform_point(transform, x + w, y, &x1, &y1); + svg_transform_point(transform, x + w, y + h, &x2, &y2); + svg_transform_point(transform, x, y + h, &x3, &y3); + el.add(x0, y0, x1, y1); + el.add(x1, y1, x2, y2); + el.add(x2, y2, x3, y3); + el.add(x3, y3, x0, y0); +} + // --------------------------------------------------------------------------- // Rounded rect to edges // --------------------------------------------------------------------------- @@ -579,8 +761,7 @@ struct SvgPathParser { // Process an SVG path 'd' attribute into edges // --------------------------------------------------------------------------- inline void svg_path_to_edges(SvgEdgeList& el, const char* d, int dLen, - fixed_t scale_x, fixed_t scale_y, - fixed_t off_x, fixed_t off_y) { + const SvgTransform& transform) { SvgPathParser pp; pp.init(d, dLen); @@ -590,8 +771,7 @@ inline void svg_path_to_edges(SvgEdgeList& el, const char* d, int dLen, char last_cmd = '\0'; auto scale_pt = [&](fixed_t x, fixed_t y, fixed_t* ox, fixed_t* oy) { - *ox = fixed_mul(x - off_x, scale_x); - *oy = fixed_mul(y - off_y, scale_y); + svg_transform_point(transform, x, y, ox, oy); }; // Emit a circular arc on (cx, cy, r) from p1 to p2 by recursive @@ -954,6 +1134,7 @@ inline void svg_rasterize(const SvgEdgeList& el, uint32_t* pixels, int w, int h, // Allocate enough for all edges (each edge can intersect at most once per scanline) int maxIsect = el.count + 16; fixed_t* isect = (fixed_t*)montauk::malloc(maxIsect * sizeof(fixed_t)); + if (!isect) return; for (int y = 0; y < h; ++y) { // Scanline center in fixed-point @@ -1082,6 +1263,35 @@ inline int svg_resolve_fill_value(const char* val, Color* out_color, const SvgGr return 0; // currentColor or unresolved } +inline const SvgGradient* svg_resolve_gradient_value(const char* val, + const SvgGradientTable* grads) { + if (!grads || !svg_strncmp(val, "url(#", 5)) return nullptr; + char id[32]; + int i = 0; + const char* p = val + 5; + while (p[i] && p[i] != ')' && i < 31) { id[i] = p[i]; ++i; } + id[i] = '\0'; + return grads->lookup_gradient(id); +} + +inline const SvgGradient* svg_get_element_gradient(const char* elem, int elemLen, + const SvgGradientTable* grads) { + char buf[128]; + int style_len = svg_get_attr(elem, elemLen, " style", buf, sizeof(buf)); + if (style_len > 0) { + const char* fill = svg_strstr(buf, style_len, "fill:"); + if (fill && (fill == buf || *(fill - 1) == ';' || svg_char_is_ws(*(fill - 1)))) { + fill += 5; + while (svg_char_is_ws(*fill)) ++fill; + const SvgGradient* gradient = svg_resolve_gradient_value(fill, grads); + if (gradient) return gradient; + } + } + if (svg_get_attr(elem, elemLen, " fill", buf, sizeof(buf)) > 0) + return svg_resolve_gradient_value(buf, grads); + return nullptr; +} + // --------------------------------------------------------------------------- // Per-element fill color extraction // Returns: 1 = color found in out_color, 0 = use default, -1 = fill="none" @@ -1139,20 +1349,102 @@ inline int svg_get_element_fill(const char* elem, int elemLen, Color* out_color, return 0; // no fill specified } +// Stroke paint uses the same color syntax and currentColor behavior as fill. +inline int svg_get_element_stroke(const char* elem, int elemLen, Color* out_color, + const SvgGradientTable* grads = nullptr, + const SvgCssTable* css = nullptr) { + char buf[128]; + int sLen = svg_get_attr(elem, elemLen, " style", buf, sizeof(buf)); + if (sLen > 0) { + const char* sp = svg_strstr(buf, sLen, "stroke:"); + if (sp && (sp == buf || *(sp - 1) == ';' || svg_char_is_ws(*(sp - 1)))) { + sp += 7; + while (svg_char_is_ws(*sp)) ++sp; + if (svg_strncmp(sp, "currentColor", 12)) return 1; + return svg_resolve_fill_value(sp, out_color, grads); + } + } + int len = svg_get_attr(elem, elemLen, " stroke", buf, sizeof(buf)); + if (len <= 0) return -1; + if (svg_strncmp(buf, "currentColor", 12)) { + if (css) { + char cls[64]; + Color c; + if (svg_get_attr(elem, elemLen, " class", cls, sizeof(cls)) > 0 && + css->lookup(cls, &c)) { + *out_color = c; + } + } + return 1; + } + return svg_resolve_fill_value(buf, out_color, grads); +} + +inline fixed_t svg_get_stroke_width(const char* elem, int elemLen) { + char buf[32]; + if (svg_get_attr(elem, elemLen, " stroke-width", buf, sizeof(buf)) > 0) { + fixed_t width = 0; + svg_parse_fixed(buf, &width); + return width; + } + char style[160]; + int style_len = svg_get_attr(elem, elemLen, " style", style, sizeof(style)); + if (style_len > 0) { + const char* value = svg_strstr(style, style_len, "stroke-width:"); + if (value) { + value += 13; + while (svg_char_is_ws(*value)) ++value; + fixed_t width = 0; + svg_parse_fixed(value, &width); + return width; + } + } + return int_to_fixed(1); +} + // --------------------------------------------------------------------------- // Per-element opacity (0-255) // --------------------------------------------------------------------------- inline int svg_get_element_opacity(const char* elem, int elemLen) { char buf[32]; - if (svg_get_attr(elem, elemLen, " opacity", buf, sizeof(buf)) > 0) { + int alpha = 255; + bool has_opacity = svg_get_attr(elem, elemLen, " opacity", buf, sizeof(buf)) > 0; + if (!has_opacity) { + char style[160]; + int style_len = svg_get_attr(elem, elemLen, " style", style, sizeof(style)); + const char* op = style_len > 0 ? svg_strstr(style, style_len, "opacity:") : nullptr; + while (op && op > style && *(op - 1) == '-') { + int used = (int)((op + 8) - style); + op = svg_strstr(style + used, style_len - used, "opacity:"); + } + if (op) { + op += 8; + while (svg_char_is_ws(*op)) ++op; + int i = 0; + while (op[i] && op[i] != ';' && i < (int)sizeof(buf) - 1) { + buf[i] = op[i]; + ++i; + } + buf[i] = '\0'; + has_opacity = true; + } + } + if (has_opacity) { fixed_t val; svg_parse_fixed(buf, &val); - int alpha = (int)(((int64_t)val * 255) >> 16); + alpha = (int)(((int64_t)val * 255) >> 16); if (alpha < 0) alpha = 0; if (alpha > 255) alpha = 255; - return alpha; } - return 255; + if (svg_get_attr(elem, elemLen, " fill-opacity", buf, sizeof(buf)) > 0) { + fixed_t val; + svg_parse_fixed(buf, &val); + int fill_alpha = (int)(((int64_t)val * 255) >> 16); + if (fill_alpha < 0) fill_alpha = 0; + if (fill_alpha > 255) fill_alpha = 255; + alpha = (alpha * fill_alpha + 127) / 255; + } + return alpha; } // --------------------------------------------------------------------------- @@ -1164,39 +1456,101 @@ inline bool svg_element_has_filter(const char* elem, int elemLen) { } // --------------------------------------------------------------------------- -// Parse transform="scale(x)" or transform="scale(x,y)" or translate(x,y) -// Returns true if a scale was found; sx/sy are fixed-point scale factors. +// Parse an SVG transform list. Matrix, translate, and scale cover the +// transforms emitted by the bundled icon set. Multiple operations are +// composed in SVG order (for example, translate(...) scale(...)). // --------------------------------------------------------------------------- -inline bool svg_get_element_scale(const char* elem, int elemLen, - fixed_t* sx, fixed_t* sy) { - char buf[64]; +inline bool svg_parse_transform(const char* value, SvgTransform* out) { + SvgTransform result = SvgTransform::identity(); + const char* p = value; + bool found = false; + + auto skip_ws_comma = [&]() { + while (*p && (svg_char_is_ws(*p) || *p == ',')) ++p; + }; + auto read_arg = [&](fixed_t* value_out) -> bool { + skip_ws_comma(); + if (!svg_char_is_num_start(*p)) return false; + int consumed = svg_parse_fixed(p, value_out); + if (consumed <= 0) return false; + p += consumed; + return true; + }; + + while (*p) { + skip_ws_comma(); + SvgTransform op = SvgTransform::identity(); + + if (svg_strncmp(p, "matrix(", 7)) { + p += 7; + if (!read_arg(&op.a) || !read_arg(&op.b) || + !read_arg(&op.c) || !read_arg(&op.d) || + !read_arg(&op.e) || !read_arg(&op.f)) break; + } else if (svg_strncmp(p, "translate(", 10)) { + p += 10; + if (!read_arg(&op.e)) break; + const char* before_second = p; + if (!read_arg(&op.f)) { + p = before_second; + op.f = 0; + } + } else if (svg_strncmp(p, "scale(", 6)) { + p += 6; + if (!read_arg(&op.a)) break; + const char* before_second = p; + if (!read_arg(&op.d)) { + p = before_second; + op.d = op.a; + } + } else if (svg_strncmp(p, "rotate(", 7)) { + p += 7; + fixed_t angle; + if (!read_arg(&angle)) break; + fixed_t sine = svg_fixed_sin_degrees(angle); + fixed_t cosine = svg_fixed_sin_degrees(int_to_fixed(90) - angle); + op.a = cosine; + op.b = sine; + op.c = -sine; + op.d = cosine; + + const char* before_center = p; + fixed_t cx, cy; + if (read_arg(&cx) && read_arg(&cy)) { + SvgTransform to_center = SvgTransform::identity(); + SvgTransform from_center = SvgTransform::identity(); + to_center.e = cx; + to_center.f = cy; + from_center.e = -cx; + from_center.f = -cy; + op = svg_transform_multiply( + svg_transform_multiply(to_center, op), from_center); + } else { + p = before_center; + } + } else { + while (*p && *p != ')') ++p; + if (*p == ')') ++p; + continue; + } + + while (*p && svg_char_is_ws(*p)) ++p; + if (*p == ')') ++p; + result = svg_transform_multiply(result, op); + found = true; + } + + *out = result; + return found; +} + +inline bool svg_get_element_transform(const char* elem, int elemLen, + SvgTransform* out) { + char buf[160]; if (svg_get_attr(elem, elemLen, " transform", buf, sizeof(buf)) <= 0) { - *sx = int_to_fixed(1); - *sy = int_to_fixed(1); + *out = SvgTransform::identity(); return false; } - // Look for "scale(" - const char* sp = buf; - while (*sp) { - if (sp[0]=='s' && sp[1]=='c' && sp[2]=='a' && sp[3]=='l' && sp[4]=='e' && sp[5]=='(') { - sp += 6; - svg_parse_fixed(sp, sx); - // Skip to comma or closing paren - while (*sp && *sp != ',' && *sp != ')') ++sp; - if (*sp == ',') { - ++sp; - while (*sp == ' ') ++sp; - svg_parse_fixed(sp, sy); - } else { - *sy = *sx; // uniform scale - } - return true; - } - ++sp; - } - *sx = int_to_fixed(1); - *sy = int_to_fixed(1); - return false; + return svg_parse_transform(buf, out); } // --------------------------------------------------------------------------- @@ -1208,6 +1562,7 @@ inline void svg_rasterize_blend(const SvgEdgeList& el, uint32_t* pixels, int w, int maxIsect = el.count + 16; fixed_t* isect = (fixed_t*)montauk::malloc(maxIsect * sizeof(fixed_t)); + if (!isect) return; uint32_t fr = (fill >> 16) & 0xFF; uint32_t fg = (fill >> 8) & 0xFF; @@ -1261,9 +1616,15 @@ inline void svg_rasterize_blend(const SvgEdgeList& el, uint32_t* pixels, int w, uint32_t dg = (dst >> 8) & 0xFF; uint32_t db = dst & 0xFF; uint32_t out_a = sa + (da * inv_sa + 127) / 255; - uint32_t rr = (fr * sa + dr * inv_sa + 128) / 255; - uint32_t gg = (fg * sa + dg * inv_sa + 128) / 255; - uint32_t bb = (fb * sa + db * inv_sa + 128) / 255; + // Source and destination pixels use straight alpha. Blend + // in premultiplied space, then convert back so translucent + // SVG paints retain their original RGB values. + uint32_t pr = fr * sa + (dr * da * inv_sa + 127) / 255; + uint32_t pg = fg * sa + (dg * da * inv_sa + 127) / 255; + uint32_t pb = fb * sa + (db * da * inv_sa + 127) / 255; + uint32_t rr = out_a ? (pr + out_a / 2) / out_a : 0; + uint32_t gg = out_a ? (pg + out_a / 2) / out_a : 0; + uint32_t bb = out_a ? (pb + out_a / 2) / out_a : 0; pixels[y * w + x] = (out_a << 24) | (rr << 16) | (gg << 8) | bb; } } @@ -1273,16 +1634,186 @@ inline void svg_rasterize_blend(const SvgEdgeList& el, uint32_t* pixels, int w, montauk::mfree(isect); } +inline void svg_rasterize_mask(const SvgEdgeList& el, uint8_t* mask, int w, int h) { + if (el.count == 0 || !mask) return; + int max_isect = el.count + 16; + fixed_t* isect = (fixed_t*)montauk::malloc(max_isect * sizeof(fixed_t)); + if (!isect) return; + for (int y = 0; y < h; ++y) { + fixed_t scan_y = int_to_fixed(y) + (1 << 15); + int count = 0; + for (int i = 0; i < el.count; ++i) { + const SvgEdge& edge = el.edges[i]; + fixed_t low = edge.y0 < edge.y1 ? edge.y0 : edge.y1; + fixed_t high = edge.y0 > edge.y1 ? edge.y0 : edge.y1; + if (scan_y < low || scan_y >= high || edge.y0 == edge.y1) continue; + fixed_t x = edge.x0 + (fixed_t)(((int64_t)(edge.x1 - edge.x0) * + (scan_y - edge.y0)) / + (edge.y1 - edge.y0)); + if (count < max_isect) isect[count++] = x; + } + for (int i = 1; i < count; ++i) { + fixed_t key = isect[i]; + int j = i - 1; + while (j >= 0 && isect[j] > key) { + isect[j + 1] = isect[j]; + --j; + } + isect[j + 1] = key; + } + for (int i = 0; i + 1 < count; i += 2) { + int x0 = fixed_to_int(isect[i]); + int x1 = fixed_to_int(isect[i + 1]); + if (x0 < 0) x0 = 0; + if (x1 > w) x1 = w; + for (int x = x0; x < x1; ++x) mask[y * w + x] = 255; + } + } + montauk::mfree(isect); +} + +inline void svg_blend_straight_pixel(uint32_t* destination, Color source, int alpha) { + uint32_t sa = (uint32_t)((alpha * source.a + 127) / 255); + if (sa == 0) return; + uint32_t dst = *destination; + uint32_t da = (dst >> 24) & 0xff; + uint32_t inv_sa = 255 - sa; + uint32_t out_a = sa + (da * inv_sa + 127) / 255; + uint32_t pr = source.r * sa + (((dst >> 16) & 0xff) * da * inv_sa + 127) / 255; + uint32_t pg = source.g * sa + (((dst >> 8) & 0xff) * da * inv_sa + 127) / 255; + uint32_t pb = source.b * sa + ((dst & 0xff) * da * inv_sa + 127) / 255; + uint32_t r = out_a ? (pr + out_a / 2) / out_a : 0; + uint32_t g = out_a ? (pg + out_a / 2) / out_a : 0; + uint32_t b = out_a ? (pb + out_a / 2) / out_a : 0; + *destination = (out_a << 24) | (r << 16) | (g << 8) | b; +} + +inline Color svg_gradient_color_at(const SvgGradient& gradient, fixed_t offset) { + if (gradient.stop_count <= 0) return Color::from_rgba(0, 0, 0, 0); + if (offset <= gradient.stops[0].offset) return gradient.stops[0].color; + for (int i = 1; i < gradient.stop_count; ++i) { + if (offset <= gradient.stops[i].offset) { + const SvgGradientStop& left = gradient.stops[i - 1]; + const SvgGradientStop& right = gradient.stops[i]; + fixed_t span = right.offset - left.offset; + fixed_t t = span > 0 ? fixed_div(offset - left.offset, span) : 0; + fixed_t inverse = int_to_fixed(1) - t; + return Color::from_rgba( + (uint8_t)((fixed_mul(int_to_fixed(left.color.r), inverse) + + fixed_mul(int_to_fixed(right.color.r), t)) >> 16), + (uint8_t)((fixed_mul(int_to_fixed(left.color.g), inverse) + + fixed_mul(int_to_fixed(right.color.g), t)) >> 16), + (uint8_t)((fixed_mul(int_to_fixed(left.color.b), inverse) + + fixed_mul(int_to_fixed(right.color.b), t)) >> 16), + (uint8_t)((fixed_mul(int_to_fixed(left.color.a), inverse) + + fixed_mul(int_to_fixed(right.color.a), t)) >> 16)); + } + } + return gradient.stops[gradient.stop_count - 1].color; +} + +inline bool svg_rasterize_linear_gradient(const SvgEdgeList& el, uint32_t* pixels, + int w, int h, const SvgGradient& gradient, + const SvgTransform& element_transform, + int alpha) { + if (gradient.radial || !gradient.user_space || gradient.stop_count == 0) return false; + uint64_t pixel_count = (uint64_t)(uint32_t)w * (uint64_t)(uint32_t)h; + uint8_t* mask = (uint8_t*)montauk::malloc(pixel_count); + if (!mask) return false; + svg_memset(mask, 0, (int)pixel_count); + svg_rasterize_mask(el, mask, w, h); + + SvgTransform gradient_to_pixels = svg_transform_multiply(element_transform, + gradient.transform); + fixed_t x1, y1, x2, y2; + svg_transform_point(gradient_to_pixels, gradient.x1, gradient.y1, &x1, &y1); + svg_transform_point(gradient_to_pixels, gradient.x2, gradient.y2, &x2, &y2); + fixed_t dx = x2 - x1; + fixed_t dy = y2 - y1; + fixed_t length = svg_fixed_hypot(dx, dy); + if (length <= 0) { + montauk::mfree(mask); + return false; + } + fixed_t ux = fixed_div(dx, length); + fixed_t uy = fixed_div(dy, length); + for (int y = 0; y < h; ++y) { + for (int x = 0; x < w; ++x) { + if (!mask[y * w + x]) continue; + fixed_t vx = int_to_fixed(x) + (1 << 15) - x1; + fixed_t vy = int_to_fixed(y) + (1 << 15) - y1; + fixed_t projection = fixed_mul(vx, ux) + fixed_mul(vy, uy); + fixed_t offset = fixed_div(projection, length); + if (offset < 0) offset = 0; + if (offset > int_to_fixed(1)) offset = int_to_fixed(1); + Color color = svg_gradient_color_at(gradient, offset); + svg_blend_straight_pixel(&pixels[y * w + x], color, alpha); + } + } + montauk::mfree(mask); + return true; +} + +// Rasterize a centered stroke around every flattened edge. Segment quads are +// joined with small discs, which gives paths continuous round joins/caps and +// works for lines, curves, arcs, and closed contours alike. +inline void svg_rasterize_stroke(const SvgEdgeList& path, uint32_t* pixels, + int w, int h, fixed_t width, + uint32_t color, int alpha) { + if (path.count == 0 || width <= 0) return; + fixed_t half = width / 2; + uint64_t pixel_count = (uint64_t)(uint32_t)w * (uint64_t)(uint32_t)h; + uint8_t* mask = (uint8_t*)montauk::malloc(pixel_count); + if (!mask) return; + svg_memset(mask, 0, (int)pixel_count); + SvgEdgeList shape; + shape.init(20); + if (!shape.edges) { montauk::mfree(mask); return; } + + for (int i = 0; i < path.count; ++i) { + const SvgEdge& edge = path.edges[i]; + fixed_t dx = edge.x1 - edge.x0; + fixed_t dy = edge.y1 - edge.y0; + fixed_t length = svg_fixed_hypot(dx, dy); + if (length == 0) continue; + fixed_t nx = fixed_mul(-dy, fixed_div(half, length)); + fixed_t ny = fixed_mul( dx, fixed_div(half, length)); + + shape.clear(); + shape.add(edge.x0 + nx, edge.y0 + ny, edge.x1 + nx, edge.y1 + ny); + shape.add(edge.x1 + nx, edge.y1 + ny, edge.x1 - nx, edge.y1 - ny); + shape.add(edge.x1 - nx, edge.y1 - ny, edge.x0 - nx, edge.y0 - ny); + shape.add(edge.x0 - nx, edge.y0 - ny, edge.x0 + nx, edge.y0 + ny); + svg_rasterize_mask(shape, mask, w, h); + + shape.clear(); + svg_circle_edges(shape, edge.x1, edge.y1, half); + svg_rasterize_mask(shape, mask, w, h); + } + Color stroke_color = Color::from_rgb((color >> 16) & 0xff, + (color >> 8) & 0xff, + color & 0xff); + for (uint64_t i = 0; i < pixel_count; ++i) { + if (mask[i]) svg_blend_straight_pixel(&pixels[i], stroke_color, alpha); + } + montauk::mfree(shape.edges); + montauk::mfree(mask); +} + // --------------------------------------------------------------------------- // SVG document parser: extract paths, circles, rects and rasterize // --------------------------------------------------------------------------- inline SvgIcon svg_render(const char* svg_data, int svg_len, int target_w, int target_h, Color fill_color) { - SvgIcon icon; + SvgIcon icon = {nullptr, 0, 0}; + if (!svg_data || svg_len <= 0 || target_w <= 0 || target_h <= 0) return icon; + uint64_t pixel_count = (uint64_t)(uint32_t)target_w * (uint64_t)(uint32_t)target_h; + if (pixel_count > 536870911ULL) return icon; // byte count must fit svg_memset(int) icon.width = target_w; icon.height = target_h; - icon.pixels = (uint32_t*)montauk::malloc(target_w * target_h * sizeof(uint32_t)); + icon.pixels = (uint32_t*)montauk::malloc(pixel_count * sizeof(uint32_t)); + if (!icon.pixels) return {nullptr, 0, 0}; // Clear to transparent - svg_memset(icon.pixels, 0, target_w * target_h * sizeof(uint32_t)); + svg_memset(icon.pixels, 0, (int)(pixel_count * sizeof(uint32_t))); // Parse SVG dimensions: width and height int svg_w = 16, svg_h = 16; @@ -1302,11 +1833,13 @@ inline SvgIcon svg_render(const char* svg_data, int svg_len, int target_w, int t // width if (svg_get_attr(svg_tag, tag_len, " width", attr_buf, sizeof(attr_buf)) > 0) { - svg_w = svg_parse_int(attr_buf); + int parsed_w = svg_parse_length_pixels(attr_buf); + if (parsed_w > 0) svg_w = parsed_w; } // height if (svg_get_attr(svg_tag, tag_len, " height", attr_buf, sizeof(attr_buf)) > 0) { - svg_h = svg_parse_int(attr_buf); + int parsed_h = svg_parse_length_pixels(attr_buf); + if (parsed_h > 0) svg_h = parsed_h; } // viewBox if (svg_get_attr(svg_tag, tag_len, " viewBox", attr_buf, sizeof(attr_buf)) > 0) { @@ -1330,6 +1863,11 @@ inline SvgIcon svg_render(const char* svg_data, int svg_len, int target_w, int t // Compute scale: pixel = (svg_coord - vb_origin) * target_size / vb_size fixed_t scale_x = vb_w > 0 ? fixed_div(int_to_fixed(target_w), vb_w) : int_to_fixed(1); fixed_t scale_y = vb_h > 0 ? fixed_div(int_to_fixed(target_h), vb_h) : int_to_fixed(1); + SvgTransform view_transform = { + scale_x, 0, 0, scale_y, + -fixed_mul(vb_x, scale_x), + -fixed_mul(vb_y, scale_y) + }; // Pre-parse gradient definitions from for url(#id) fill resolution SvgGradientTable grads; @@ -1338,15 +1876,16 @@ inline SvgIcon svg_render(const char* svg_data, int svg_len, int target_w, int t const char* dp = svg_data; const char* dend = svg_data + svg_len; while (dp < dend) { - const char* gp = svg_strstr(dp, (int)(dend - dp), " or ) - const char* gend = svg_strstr(gp, (int)(dend - gp), ""); - if (!gend) gend = svg_strstr(gp, (int)(dend - gp), ""); + // Find the matching end of this gradient block. + bool is_linear = svg_strncmp(gp, "" : ""); if (!gend) gend = svg_strstr(gp, (int)(dend - gp), "/>"); if (!gend) break; @@ -1357,18 +1896,77 @@ inline SvgIcon svg_render(const char* svg_data, int svg_len, int target_w, int t int id_len = svg_get_attr(gp, gtag_end + 1, " id", grad_id, sizeof(grad_id)); if (id_len > 0) { - // Find first 0 && + svg_strncmp(attr, "userSpaceOnUse", 14); + if (is_linear) { + gradient.x1 = 0; + gradient.y1 = 0; + gradient.x2 = int_to_fixed(1); + gradient.y2 = 0; + if (svg_get_attr(gp, gtag_end + 1, " x1", attr, sizeof(attr)) > 0) + svg_parse_fixed(attr, &gradient.x1); + if (svg_get_attr(gp, gtag_end + 1, " y1", attr, sizeof(attr)) > 0) + svg_parse_fixed(attr, &gradient.y1); + if (svg_get_attr(gp, gtag_end + 1, " x2", attr, sizeof(attr)) > 0) + svg_parse_fixed(attr, &gradient.x2); + if (svg_get_attr(gp, gtag_end + 1, " y2", attr, sizeof(attr)) > 0) + svg_parse_fixed(attr, &gradient.y2); + } else { + if (svg_get_attr(gp, gtag_end + 1, " cx", attr, sizeof(attr)) > 0) + svg_parse_fixed(attr, &gradient.cx); + if (svg_get_attr(gp, gtag_end + 1, " cy", attr, sizeof(attr)) > 0) + svg_parse_fixed(attr, &gradient.cy); + if (svg_get_attr(gp, gtag_end + 1, " r", attr, sizeof(attr)) > 0) + svg_parse_fixed(attr, &gradient.radius); + } + if (svg_get_attr(gp, gtag_end + 1, " gradientTransform", attr, sizeof(attr)) > 0) + svg_parse_transform(attr, &gradient.transform); + + const char* stop_search = gp + gtag_end + 1; + while (stop_search < gend && gradient.stop_count < SVG_MAX_GRADIENT_STOPS) { + const char* stop = svg_strstr(stop_search, (int)(gend - stop_search), " 0) { - if (sc_buf[0] == '#') { - grads.add(grad_id, svg_parse_hex_color(sc_buf)); + char sc_buf[32], offset_buf[32], opacity_buf[32]; + if (svg_get_attr(stop, stop_end + 1, " stop-color", sc_buf, sizeof(sc_buf)) > 0 && + sc_buf[0] == '#') { + SvgGradientStop& parsed = gradient.stops[gradient.stop_count]; + parsed.color = svg_parse_hex_color(sc_buf); + parsed.offset = 0; + if (svg_get_attr(stop, stop_end + 1, " offset", offset_buf, sizeof(offset_buf)) > 0) { + svg_parse_fixed(offset_buf, &parsed.offset); + const char* percent = offset_buf; + while (*percent && *percent != '%') ++percent; + if (*percent == '%') parsed.offset /= 100; } + if (parsed.offset < 0) parsed.offset = 0; + if (parsed.offset > int_to_fixed(1)) parsed.offset = int_to_fixed(1); + if (svg_get_attr(stop, stop_end + 1, " stop-opacity", opacity_buf, sizeof(opacity_buf)) > 0) { + fixed_t opacity; + svg_parse_fixed(opacity_buf, &opacity); + int stop_alpha = (int)(((int64_t)opacity * 255) >> 16); + if (stop_alpha < 0) stop_alpha = 0; + if (stop_alpha > 255) stop_alpha = 255; + parsed.color.a = (uint8_t)stop_alpha; + } + ++gradient.stop_count; } + stop_search = stop + stop_end + 1; } + if (gradient.stop_count > 0) grads.add(gradient); } dp = gend + 1; } @@ -1391,15 +1989,18 @@ inline SvgIcon svg_render(const char* svg_data, int svg_len, int target_w, int t // Shared edge list (cleared per element for multi-color support) SvgEdgeList el; - el.init(SVG_MAX_EDGES); + el.init(512); - // Heap-allocated path data buffer (avoids 8 KiB on the stack) - char* d_buf = (char*)montauk::malloc(SVG_MAX_PATH_LEN); - - // Group fill inheritance stack: track so child elements inherit - static constexpr int MAX_GROUP_DEPTH = 8; - struct GroupFill { Color color; bool has_fill; }; - GroupFill g_stack[MAX_GROUP_DEPTH]; + // Presentation and transform state inherited through nested groups. + static constexpr int MAX_GROUP_DEPTH = 32; + struct GroupState { + SvgTransform transform; + Color color; + bool has_fill; + bool fill_none; + int alpha; + }; + GroupState g_stack[MAX_GROUP_DEPTH]; int g_depth = 0; // Scan for 0) { - int fr = svg_resolve_fill_value(fbuf, &gc, &grads); - if (fr == 1) { - g_stack[g_depth].color = gc; - g_stack[g_depth].has_fill = true; - } else if (fr == -1) { - // fill="none" on group — children inherit none - g_stack[g_depth].has_fill = false; - } + GroupState state = { + SvgTransform::identity(), fill_color, false, false, 255 + }; + if (g_depth > 0) state = g_stack[g_depth - 1]; + + SvgTransform local_transform; + svg_get_element_transform(g_start, g_len, &local_transform); + state.transform = svg_transform_multiply(state.transform, local_transform); + + Color gc = state.has_fill ? state.color : fill_color; + int fr = svg_get_element_fill(g_start, g_len, &gc, &grads, &css); + if (fr == 1) { + state.color = gc; + state.has_fill = true; + state.fill_none = false; + } else if (fr == -1) { + state.fill_none = true; } + state.alpha = (state.alpha * svg_get_element_opacity(g_start, g_len) + 127) / 255; + g_stack[g_depth] = state; g_depth++; } p = g_end; @@ -1478,35 +2085,60 @@ inline SvgIcon svg_render(const char* svg_data, int svg_len, int target_w, int t // Determine fill color: element's own fill, or inherit from nearest Color elem_color = fill_color; // Apply inherited group fill as default - for (int gi = g_depth - 1; gi >= 0; gi--) { - if (g_stack[gi].has_fill) { elem_color = g_stack[gi].color; break; } + bool inherited_none = false; + if (g_depth > 0) { + if (g_stack[g_depth - 1].has_fill) elem_color = g_stack[g_depth - 1].color; + inherited_none = g_stack[g_depth - 1].fill_none; } int fillResult = svg_get_element_fill(elem_start, elem_len, &elem_color, &grads, &css); - if (fillResult == -1) { p = elem_end; continue; } // fill="none" + bool draw_fill = fillResult != -1 && !(fillResult == 0 && inherited_none); + const SvgGradient* fill_gradient = svg_get_element_gradient(elem_start, elem_len, &grads); + + Color stroke_color = elem_color; + int stroke_result = svg_get_element_stroke(elem_start, elem_len, &stroke_color, &grads, &css); int alpha = svg_get_element_opacity(elem_start, elem_len); + if (g_depth > 0) alpha = (alpha * g_stack[g_depth - 1].alpha + 127) / 255; - // Per-element transform (scale) - fixed_t elem_sx, elem_sy; - svg_get_element_scale(elem_start, elem_len, &elem_sx, &elem_sy); - fixed_t eff_sx = fixed_mul(scale_x, elem_sx); - fixed_t eff_sy = fixed_mul(scale_y, elem_sy); + SvgTransform user_transform = g_depth > 0 + ? g_stack[g_depth - 1].transform : SvgTransform::identity(); + SvgTransform elem_transform; + svg_get_element_transform(elem_start, elem_len, &elem_transform); + user_transform = svg_transform_multiply(user_transform, elem_transform); + SvgTransform final_transform = svg_transform_multiply(view_transform, user_transform); // Extract and rasterize path - int d_len = svg_get_attr(elem_start, elem_len, " d", d_buf, SVG_MAX_PATH_LEN); - if (d_len > 0) { + const char* d_data = nullptr; + int d_len = 0; + if (svg_get_attr_span(elem_start, elem_len, " d", &d_data, &d_len) && d_len > 0) { el.clear(); - svg_path_to_edges(el, d_buf, d_len, eff_sx, eff_sy, vb_x, vb_y); - if (el.count > 0) - svg_rasterize_blend(el, icon.pixels, target_w, target_h, elem_color.to_pixel(), alpha); + svg_path_to_edges(el, d_data, d_len, final_transform); + if (el.count > 0 && draw_fill) { + bool rendered_gradient = fill_gradient && svg_rasterize_linear_gradient( + el, icon.pixels, target_w, target_h, *fill_gradient, + final_transform, alpha); + if (!rendered_gradient) + svg_rasterize_blend(el, icon.pixels, target_w, target_h, + elem_color.to_pixel(), alpha); + } + if (el.count > 0 && stroke_result == 1) { + fixed_t stroke_width = svg_get_stroke_width(elem_start, elem_len); + stroke_width = fixed_mul(stroke_width, svg_transform_average_scale(final_transform)); + svg_rasterize_stroke(el, icon.pixels, target_w, target_h, + stroke_width, stroke_color.to_pixel(), alpha); + } } p = elem_end; continue; } - // Check for 7 && svg_strncmp(p, " 7 && svg_strncmp(p, " 8 && svg_strncmp(p, "= 0; gi--) { - if (g_stack[gi].has_fill) { elem_color = g_stack[gi].color; break; } + bool inherited_none = false; + if (g_depth > 0) { + if (g_stack[g_depth - 1].has_fill) elem_color = g_stack[g_depth - 1].color; + inherited_none = g_stack[g_depth - 1].fill_none; } int fillResult = svg_get_element_fill(elem_start, elem_len, &elem_color, &grads, &css); - if (fillResult == -1) { p = elem_end; continue; } + bool draw_fill = fillResult != -1 && !(fillResult == 0 && inherited_none); + const SvgGradient* fill_gradient = svg_get_element_gradient(elem_start, elem_len, &grads); + + Color stroke_color = elem_color; + int stroke_result = svg_get_element_stroke(elem_start, elem_len, &stroke_color, &grads, &css); int alpha = svg_get_element_opacity(elem_start, elem_len); + if (g_depth > 0) alpha = (alpha * g_stack[g_depth - 1].alpha + 127) / 255; - fixed_t elem_sx, elem_sy; - svg_get_element_scale(elem_start, elem_len, &elem_sx, &elem_sy); - fixed_t eff_sx = fixed_mul(scale_x, elem_sx); - fixed_t eff_sy = fixed_mul(scale_y, elem_sy); + SvgTransform user_transform = g_depth > 0 + ? g_stack[g_depth - 1].transform : SvgTransform::identity(); + SvgTransform elem_transform; + svg_get_element_transform(elem_start, elem_len, &elem_transform); + user_transform = svg_transform_multiply(user_transform, elem_transform); + SvgTransform final_transform = svg_transform_multiply(view_transform, user_transform); char attr_buf[32]; - fixed_t cx = 0, cy = 0, r = 0; + fixed_t cx = 0, cy = 0, rx = 0, ry = 0; if (svg_get_attr(elem_start, elem_len, " cx", attr_buf, sizeof(attr_buf)) > 0) svg_parse_fixed(attr_buf, &cx); if (svg_get_attr(elem_start, elem_len, " cy", attr_buf, sizeof(attr_buf)) > 0) svg_parse_fixed(attr_buf, &cy); - if (svg_get_attr(elem_start, elem_len, " r", attr_buf, sizeof(attr_buf)) > 0) - svg_parse_fixed(attr_buf, &r); - - fixed_t scx = fixed_mul(cx - vb_x, eff_sx); - fixed_t scy = fixed_mul(cy - vb_y, eff_sy); - fixed_t srx = fixed_mul(r, eff_sx); - fixed_t sry = fixed_mul(r, eff_sy); - fixed_t sr = (srx + sry) >> 1; + if (is_circle) { + if (svg_get_attr(elem_start, elem_len, " r", attr_buf, sizeof(attr_buf)) > 0) + svg_parse_fixed(attr_buf, &rx); + ry = rx; + } else { + if (svg_get_attr(elem_start, elem_len, " rx", attr_buf, sizeof(attr_buf)) > 0) + svg_parse_fixed(attr_buf, &rx); + if (svg_get_attr(elem_start, elem_len, " ry", attr_buf, sizeof(attr_buf)) > 0) + svg_parse_fixed(attr_buf, &ry); + } el.clear(); - svg_circle_edges(el, scx, scy, sr); - if (el.count > 0) - svg_rasterize_blend(el, icon.pixels, target_w, target_h, elem_color.to_pixel(), alpha); + svg_ellipse_edges(el, cx, cy, rx, ry, final_transform); + if (el.count > 0 && draw_fill) { + bool rendered_gradient = fill_gradient && svg_rasterize_linear_gradient( + el, icon.pixels, target_w, target_h, *fill_gradient, + final_transform, alpha); + if (!rendered_gradient) + svg_rasterize_blend(el, icon.pixels, target_w, target_h, + elem_color.to_pixel(), alpha); + } + if (el.count > 0 && stroke_result == 1) { + fixed_t stroke_width = svg_get_stroke_width(elem_start, elem_len); + stroke_width = fixed_mul(stroke_width, svg_transform_average_scale(final_transform)); + svg_rasterize_stroke(el, icon.pixels, target_w, target_h, + stroke_width, stroke_color.to_pixel(), alpha); + } p = elem_end; continue; @@ -1570,18 +2225,27 @@ inline SvgIcon svg_render(const char* svg_data, int svg_len, int target_w, int t } Color elem_color = fill_color; - for (int gi = g_depth - 1; gi >= 0; gi--) { - if (g_stack[gi].has_fill) { elem_color = g_stack[gi].color; break; } + bool inherited_none = false; + if (g_depth > 0) { + if (g_stack[g_depth - 1].has_fill) elem_color = g_stack[g_depth - 1].color; + inherited_none = g_stack[g_depth - 1].fill_none; } int fillResult = svg_get_element_fill(elem_start, elem_len, &elem_color, &grads, &css); - if (fillResult == -1) { p = elem_end; continue; } + bool draw_fill = fillResult != -1 && !(fillResult == 0 && inherited_none); + const SvgGradient* fill_gradient = svg_get_element_gradient(elem_start, elem_len, &grads); + + Color stroke_color = elem_color; + int stroke_result = svg_get_element_stroke(elem_start, elem_len, &stroke_color, &grads, &css); int alpha = svg_get_element_opacity(elem_start, elem_len); + if (g_depth > 0) alpha = (alpha * g_stack[g_depth - 1].alpha + 127) / 255; - fixed_t elem_sx, elem_sy; - svg_get_element_scale(elem_start, elem_len, &elem_sx, &elem_sy); - fixed_t eff_sx = fixed_mul(scale_x, elem_sx); - fixed_t eff_sy = fixed_mul(scale_y, elem_sy); + SvgTransform user_transform = g_depth > 0 + ? g_stack[g_depth - 1].transform : SvgTransform::identity(); + SvgTransform elem_transform; + svg_get_element_transform(elem_start, elem_len, &elem_transform); + user_transform = svg_transform_multiply(user_transform, elem_transform); + SvgTransform final_transform = svg_transform_multiply(view_transform, user_transform); char attr_buf[32]; fixed_t rx_val = 0, ry_val = 0, rw = 0, rh = 0, rrx = 0, rry = 0; @@ -1599,17 +2263,41 @@ inline SvgIcon svg_render(const char* svg_data, int svg_len, int target_w, int t if (svg_get_attr(elem_start, elem_len, " ry", attr_buf, sizeof(attr_buf)) > 0) svg_parse_fixed(attr_buf, &rry); - fixed_t sx = fixed_mul(rx_val - vb_x, eff_sx); - fixed_t sy = fixed_mul(ry_val - vb_y, eff_sy); - fixed_t sw = fixed_mul(rw, eff_sx); - fixed_t sh = fixed_mul(rh, eff_sy); - fixed_t srx = fixed_mul(rrx, eff_sx); - fixed_t sry = fixed_mul(rry, eff_sy); - el.clear(); - svg_rect_edges(el, sx, sy, sw, sh, srx, sry); - if (el.count > 0) - svg_rasterize_blend(el, icon.pixels, target_w, target_h, elem_color.to_pixel(), alpha); + if (rrx > 0 || rry > 0) { + // Preserve rounded corners for the axis-aligned transforms + // used by icon artwork; fall back to a transformed polygon + // for rotated/sheared rectangles. + if (final_transform.b == 0 && final_transform.c == 0) { + fixed_t sx, sy; + svg_transform_point(final_transform, rx_val, ry_val, &sx, &sy); + fixed_t sw = fixed_mul(rw, final_transform.a); + fixed_t sh = fixed_mul(rh, final_transform.d); + fixed_t srx = fixed_mul(rrx, final_transform.a); + fixed_t sry = fixed_mul(rry, final_transform.d); + if (sw < 0) { sx += sw; sw = -sw; srx = -srx; } + if (sh < 0) { sy += sh; sh = -sh; sry = -sry; } + svg_rect_edges(el, sx, sy, sw, sh, srx, sry); + } else { + svg_rect_edges_transformed(el, rx_val, ry_val, rw, rh, final_transform); + } + } else { + svg_rect_edges_transformed(el, rx_val, ry_val, rw, rh, final_transform); + } + if (el.count > 0 && draw_fill) { + bool rendered_gradient = fill_gradient && svg_rasterize_linear_gradient( + el, icon.pixels, target_w, target_h, *fill_gradient, + final_transform, alpha); + if (!rendered_gradient) + svg_rasterize_blend(el, icon.pixels, target_w, target_h, + elem_color.to_pixel(), alpha); + } + if (el.count > 0 && stroke_result == 1) { + fixed_t stroke_width = svg_get_stroke_width(elem_start, elem_len); + stroke_width = fixed_mul(stroke_width, svg_transform_average_scale(final_transform)); + svg_rasterize_stroke(el, icon.pixels, target_w, target_h, + stroke_width, stroke_color.to_pixel(), alpha); + } p = elem_end; continue; @@ -1618,7 +2306,6 @@ inline SvgIcon svg_render(const char* svg_data, int svg_len, int target_w, int t ++p; } - montauk::mfree(d_buf); montauk::mfree(el.edges); return icon; } @@ -1642,9 +2329,9 @@ inline void svg_get_natural_size(const char* svg_data, int svg_len, char attr_buf[64]; if (svg_get_attr(svg_tag, tag_len, " width", attr_buf, sizeof(attr_buf)) > 0) - w = svg_parse_int(attr_buf); + w = svg_parse_length_pixels(attr_buf); if (svg_get_attr(svg_tag, tag_len, " height", attr_buf, sizeof(attr_buf)) > 0) - h = svg_parse_int(attr_buf); + h = svg_parse_length_pixels(attr_buf); if ((w <= 0 || h <= 0) && svg_get_attr(svg_tag, tag_len, " viewBox", attr_buf, sizeof(attr_buf)) > 0) { @@ -1695,6 +2382,10 @@ inline SvgIcon svg_load(const char* vfs_path, int target_w, int target_h, Color // Allocate final icon at target resolution uint32_t* out = (uint32_t*)montauk::malloc(target_w * target_h * 4); + if (!out) { + montauk::mfree(hi.pixels); + return {nullptr, 0, 0}; + } for (int i = 0; i < target_w * target_h; i++) out[i] = 0; // Downsample: average each SSxSS block using premultiplied alpha diff --git a/programs/src/imageviewer/svg_doc.cpp b/programs/src/imageviewer/svg_doc.cpp index dfedecd..1b2dcfa 100644 --- a/programs/src/imageviewer/svg_doc.cpp +++ b/programs/src/imageviewer/svg_doc.cpp @@ -49,10 +49,56 @@ bool svgdoc_render(SvgDoc* doc, float target_scale, int max_edge) { if (new_w < 1) new_w = 1; if (new_h < 1) new_h = 1; - gui::SvgIcon icon = gui::svg_render( + // Supersample ordinary viewing sizes to smooth path and stroke edges. + // Very large zoomed renders stay at 1x to keep memory bounded. + static constexpr int SS = 2; + static constexpr int SS_MAX_OUTPUT_EDGE = 1024; + int longest = new_w > new_h ? new_w : new_h; + int render_ss = longest <= SS_MAX_OUTPUT_EDGE ? SS : 1; + + gui::SvgIcon rendered = gui::svg_render( (const char*)doc->source, doc->source_len, - new_w, new_h, gui::Color::from_rgb(0, 0, 0)); - if (!icon.pixels) return false; + new_w * render_ss, new_h * render_ss, + gui::Color::from_rgb(0, 0, 0)); + if (!rendered.pixels) return false; + + gui::SvgIcon icon = rendered; + if (render_ss > 1) { + uint32_t* downsampled = (uint32_t*)montauk::malloc( + (uint64_t)new_w * (uint64_t)new_h * sizeof(uint32_t)); + if (downsampled) { + for (int y = 0; y < new_h; ++y) { + for (int x = 0; x < new_w; ++x) { + uint32_t sum_a = 0, sum_pr = 0, sum_pg = 0, sum_pb = 0; + for (int sy = 0; sy < render_ss; ++sy) { + for (int sx = 0; sx < render_ss; ++sx) { + uint32_t pixel = rendered.pixels[ + (y * render_ss + sy) * rendered.width + x * render_ss + sx]; + uint32_t a = (pixel >> 24) & 0xff; + sum_a += a; + sum_pr += ((pixel >> 16) & 0xff) * a; + sum_pg += ((pixel >> 8) & 0xff) * a; + sum_pb += (pixel & 0xff) * a; + } + } + uint32_t samples = render_ss * render_ss; + uint32_t a = sum_a / samples; + uint32_t r = sum_a ? sum_pr / sum_a : 0; + uint32_t g = sum_a ? sum_pg / sum_a : 0; + uint32_t b = sum_a ? sum_pb / sum_a : 0; + downsampled[y * new_w + x] = (a << 24) | (r << 16) | (g << 8) | b; + } + } + montauk::mfree(rendered.pixels); + icon = {downsampled, new_w, new_h}; + } else { + montauk::mfree(rendered.pixels); + icon = gui::svg_render( + (const char*)doc->source, doc->source_len, + new_w, new_h, gui::Color::from_rgb(0, 0, 0)); + if (!icon.pixels) return false; + } + } if (doc->pixels) montauk::mfree(doc->pixels); doc->pixels = icon.pixels; diff --git a/template/sysroot/include/gui/svg.hpp b/template/sysroot/include/gui/svg.hpp index 7f99a9d..5e43dc1 100644 --- a/template/sysroot/include/gui/svg.hpp +++ b/template/sysroot/include/gui/svg.hpp @@ -1,13 +1,15 @@ /* * svg.hpp * MontaukOS SVG icon parser and scanline rasterizer - * Handles the Flat-Remix symbolic icon subset (path, circle, rect) + * Handles paths, circles, ellipses, rectangles, inherited paint, + * strokes, gradients, and nested affine transforms. * All math uses 16.16 fixed-point -- NO floating point. * Copyright (c) 2025 Daniel Hammer */ #pragma once #include "gui/gui.hpp" +#include #include namespace gui { @@ -28,19 +30,58 @@ struct SvgEdge { fixed_t x0, y0, x1, y1; }; +// SVG affine transform, stored as the standard [a c e; b d f; 0 0 1] +// matrix. Keeping transforms in user space until an edge is emitted lets +// nested groups and complete transform lists compose correctly. +struct SvgTransform { + fixed_t a, b, c, d, e, f; + + static SvgTransform identity() { + return {int_to_fixed(1), 0, 0, int_to_fixed(1), 0, 0}; + } +}; + +inline SvgTransform svg_transform_multiply(const SvgTransform& lhs, + const SvgTransform& rhs) { + return { + fixed_mul(lhs.a, rhs.a) + fixed_mul(lhs.c, rhs.b), + fixed_mul(lhs.b, rhs.a) + fixed_mul(lhs.d, rhs.b), + fixed_mul(lhs.a, rhs.c) + fixed_mul(lhs.c, rhs.d), + fixed_mul(lhs.b, rhs.c) + fixed_mul(lhs.d, rhs.d), + fixed_mul(lhs.a, rhs.e) + fixed_mul(lhs.c, rhs.f) + lhs.e, + fixed_mul(lhs.b, rhs.e) + fixed_mul(lhs.d, rhs.f) + lhs.f, + }; +} + +inline void svg_transform_point(const SvgTransform& t, fixed_t x, fixed_t y, + fixed_t* out_x, fixed_t* out_y) { + *out_x = fixed_mul(t.a, x) + fixed_mul(t.c, y) + t.e; + *out_y = fixed_mul(t.b, x) + fixed_mul(t.d, y) + t.f; +} + // --------------------------------------------------------------------------- // Constants // --------------------------------------------------------------------------- -static constexpr int SVG_MAX_EDGES = 8192; -static constexpr int SVG_MAX_PATH_LEN = 8192; -static constexpr int SVG_MAX_FILE_SIZE = 32768; +static constexpr int SVG_MAX_EDGES = 131072; +static constexpr int SVG_MAX_FILE_SIZE = 1024 * 1024; static constexpr int SVG_BEZIER_STEPS = 8; static constexpr int SVG_MAX_GRADIENTS = 8; +static constexpr int SVG_MAX_GRADIENT_STOPS = 8; + +struct SvgGradientStop { + fixed_t offset; + Color color; +}; -// Gradient color table — stores first stop color for url(#id) resolution struct SvgGradient { char id[32]; - Color color; // first stop-color + bool radial; + bool user_space; + fixed_t x1, y1, x2, y2; // linear endpoints + fixed_t cx, cy, radius; // radial geometry + SvgTransform transform; + SvgGradientStop stops[SVG_MAX_GRADIENT_STOPS]; + int stop_count; }; struct SvgGradientTable { @@ -49,17 +90,12 @@ struct SvgGradientTable { void clear() { count = 0; } - void add(const char* id, Color c) { + void add(const SvgGradient& gradient) { if (count >= SVG_MAX_GRADIENTS) return; - int i = 0; - while (id[i] && i < 31) { entries[count].id[i] = id[i]; i++; } - entries[count].id[i] = '\0'; - entries[count].color = c; - count++; + entries[count++] = gradient; } - // Look up gradient by id. Returns true if found. - bool lookup(const char* id, Color* out) const { + const SvgGradient* lookup_gradient(const char* id) const { for (int i = 0; i < count; i++) { const char* a = entries[i].id; const char* b = id; @@ -69,11 +105,18 @@ struct SvgGradientTable { a++; b++; } if (match && *a == '\0' && *b == '\0') { - *out = entries[i].color; - return true; + return &entries[i]; } } - return false; + return nullptr; + } + + // Solid-color fallback for unsupported gradient contexts. + bool lookup(const char* id, Color* out) const { + const SvgGradient* gradient = lookup_gradient(id); + if (!gradient || gradient->stop_count == 0) return false; + *out = gradient->stops[0].color; + return true; } }; @@ -151,6 +194,39 @@ inline fixed_t svg_fixed_hypot(fixed_t x, fixed_t y) { return (fixed_t)svg_int64_sqrt((uint64_t)sq); } +inline fixed_t svg_transform_average_scale(const SvgTransform& t) { + fixed_t sx = svg_fixed_hypot(t.a, t.b); + fixed_t sy = svg_fixed_hypot(t.c, t.d); + return (sx + sy) / 2; +} + +// Fixed-point sine for SVG rotate(). A 15-degree table with linear +// interpolation is accurate enough for icon rasterization and keeps the +// freestanding renderer free of floating-point/libm dependencies. +inline fixed_t svg_fixed_sin_degrees(fixed_t degrees) { + const fixed_t full = int_to_fixed(360); + while (degrees < 0) degrees += full; + while (degrees >= full) degrees -= full; + + int quadrant = fixed_to_int(degrees) / 90; + fixed_t local = degrees - int_to_fixed(quadrant * 90); + if (quadrant == 1 || quadrant == 3) local = int_to_fixed(90) - local; + + static const fixed_t sin15[7] = { + 0, 16962, 32768, 46341, 56756, 63303, 65536 + }; + fixed_t slot = fixed_div(local, int_to_fixed(15)); + int index = fixed_to_int(slot); + if (index >= 6) index = 6; + fixed_t value = sin15[index]; + if (index < 6) { + fixed_t fraction = slot - int_to_fixed(index); + value += fixed_mul(sin15[index + 1] - sin15[index], fraction); + } + if (quadrant >= 2) value = -value; + return value; +} + // --------------------------------------------------------------------------- // Fixed-point number parser (NO floating point) // Parses strings like "3.25", "-0.5", ".1115", "16" @@ -318,6 +394,40 @@ inline int svg_get_attr(const char* tag, int tagLen, const char* attr, char* buf return -1; } +// Return an attribute value as a span into the original SVG source. Unlike +// svg_get_attr(), this does not truncate long values such as complex path +// data in full-size illustrations. +inline bool svg_get_attr_span(const char* tag, int tagLen, const char* attr, + const char** out_value, int* out_len) { + int attrLen = svg_strlen(attr); + const char* search_start = tag; + int search_len = tagLen; + while (search_len > 0) { + const char* p = svg_strstr(search_start, search_len, attr); + if (!p) return false; + const char* after_name = p + attrLen; + if (after_name < tag + tagLen && svg_char_is_attrname(*after_name)) { + search_start = after_name; + search_len = tagLen - (int)(search_start - tag); + continue; + } + p = after_name; + while (p < tag + tagLen && svg_char_is_ws(*p)) ++p; + if (p >= tag + tagLen || *p != '=') return false; + ++p; + while (p < tag + tagLen && svg_char_is_ws(*p)) ++p; + if (p >= tag + tagLen || (*p != '"' && *p != '\'')) return false; + char quote = *p++; + const char* value = p; + while (p < tag + tagLen && *p != quote) ++p; + if (p >= tag + tagLen) return false; + *out_value = value; + *out_len = (int)(p - value); + return true; + } + return false; +} + // Parse an integer from a string (no sign, simple decimal). inline int svg_parse_int(const char* s) { int v = 0; @@ -328,6 +438,28 @@ inline int svg_parse_int(const char* s) { return v; } +// Parse an SVG/CSS absolute length into CSS pixels (96 px per inch). +// Percentages need viewport context and therefore return zero so callers can +// fall back to viewBox dimensions. +inline int svg_parse_length_pixels(const char* s) { + fixed_t value = 0; + int consumed = svg_parse_fixed(s, &value); + if (consumed <= 0 || value <= 0) return 0; + const char* unit = s + consumed; + while (svg_char_is_ws(*unit)) ++unit; + + fixed_t factor = int_to_fixed(1); + if (*unit == '%') return 0; + if (svg_strncmp(unit, "pt", 2)) factor = fixed_from_parts(1, 1, 3); // 96/72 + else if (svg_strncmp(unit, "pc", 2)) factor = int_to_fixed(16); + else if (svg_strncmp(unit, "in", 2)) factor = int_to_fixed(96); + else if (svg_strncmp(unit, "cm", 2)) factor = fixed_from_parts(37, 203, 254); // 9600/254 + else if (svg_strncmp(unit, "mm", 2)) factor = fixed_from_parts(3, 199, 254); // 960/254 + + fixed_t pixels = fixed_mul(value, factor); + return (pixels + (1 << 15)) >> 16; +} + // Parse a hex color like "#5c616c" or "#fff" into a Color. inline Color svg_parse_hex_color(const char* s) { if (*s == '#') ++s; @@ -368,15 +500,23 @@ struct SvgEdgeList { void init(int cap) { edges = (SvgEdge*)montauk::malloc(cap * sizeof(SvgEdge)); count = 0; - capacity = cap; + capacity = edges ? cap : 0; } void clear() { count = 0; } void add(fixed_t x0, fixed_t y0, fixed_t x1, fixed_t y1) { - if (count >= capacity) return; - // skip horizontal edges (they don't contribute to scanline crossings) - if (y0 == y1) return; + if (count >= capacity) { + if (capacity >= SVG_MAX_EDGES) return; + int new_capacity = capacity > 0 ? capacity * 2 : 64; + if (new_capacity > SVG_MAX_EDGES) new_capacity = SVG_MAX_EDGES; + auto* grown = (SvgEdge*)montauk::realloc(edges, new_capacity * sizeof(SvgEdge)); + if (!grown) return; + edges = grown; + capacity = new_capacity; + } + // Horizontal edges do not contribute to fill crossings, but must be + // retained because they are visible when a path has a stroke. edges[count++] = {x0, y0, x1, y1}; } }; @@ -472,6 +612,48 @@ inline void svg_circle_edges(SvgEdgeList& el, fixed_t cx, fixed_t cy, fixed_t r) } } +// Ellipse transformed by an arbitrary affine matrix. This also handles +// circles inside translated/scaled groups without losing non-uniform scale. +inline void svg_ellipse_edges(SvgEdgeList& el, fixed_t cx, fixed_t cy, + fixed_t rx, fixed_t ry, + const SvgTransform& transform) { + static const fixed_t cos16[16] = { + 65536, 60547, 46341, 25080, 0, -25080, -46341, -60547, + -65536, -60547, -46341, -25080, 0, 25080, 46341, 60547 + }; + static const fixed_t sin16[16] = { + 0, 25080, 46341, 60547, 65536, 60547, 46341, 25080, + 0, -25080, -46341, -60547, -65536, -60547, -46341, -25080 + }; + + fixed_t px, py; + svg_transform_point(transform, cx + rx, cy, &px, &py); + for (int i = 1; i <= 16; ++i) { + int idx = i & 15; + fixed_t ux = cx + fixed_mul(rx, cos16[idx]); + fixed_t uy = cy + fixed_mul(ry, sin16[idx]); + fixed_t nx, ny; + svg_transform_point(transform, ux, uy, &nx, &ny); + el.add(px, py, nx, ny); + px = nx; + py = ny; + } +} + +inline void svg_rect_edges_transformed(SvgEdgeList& el, fixed_t x, fixed_t y, + fixed_t w, fixed_t h, + const SvgTransform& transform) { + fixed_t x0, y0, x1, y1, x2, y2, x3, y3; + svg_transform_point(transform, x, y, &x0, &y0); + svg_transform_point(transform, x + w, y, &x1, &y1); + svg_transform_point(transform, x + w, y + h, &x2, &y2); + svg_transform_point(transform, x, y + h, &x3, &y3); + el.add(x0, y0, x1, y1); + el.add(x1, y1, x2, y2); + el.add(x2, y2, x3, y3); + el.add(x3, y3, x0, y0); +} + // --------------------------------------------------------------------------- // Rounded rect to edges // --------------------------------------------------------------------------- @@ -579,8 +761,7 @@ struct SvgPathParser { // Process an SVG path 'd' attribute into edges // --------------------------------------------------------------------------- inline void svg_path_to_edges(SvgEdgeList& el, const char* d, int dLen, - fixed_t scale_x, fixed_t scale_y, - fixed_t off_x, fixed_t off_y) { + const SvgTransform& transform) { SvgPathParser pp; pp.init(d, dLen); @@ -590,8 +771,7 @@ inline void svg_path_to_edges(SvgEdgeList& el, const char* d, int dLen, char last_cmd = '\0'; auto scale_pt = [&](fixed_t x, fixed_t y, fixed_t* ox, fixed_t* oy) { - *ox = fixed_mul(x - off_x, scale_x); - *oy = fixed_mul(y - off_y, scale_y); + svg_transform_point(transform, x, y, ox, oy); }; // Emit a circular arc on (cx, cy, r) from p1 to p2 by recursive @@ -954,6 +1134,7 @@ inline void svg_rasterize(const SvgEdgeList& el, uint32_t* pixels, int w, int h, // Allocate enough for all edges (each edge can intersect at most once per scanline) int maxIsect = el.count + 16; fixed_t* isect = (fixed_t*)montauk::malloc(maxIsect * sizeof(fixed_t)); + if (!isect) return; for (int y = 0; y < h; ++y) { // Scanline center in fixed-point @@ -1082,6 +1263,35 @@ inline int svg_resolve_fill_value(const char* val, Color* out_color, const SvgGr return 0; // currentColor or unresolved } +inline const SvgGradient* svg_resolve_gradient_value(const char* val, + const SvgGradientTable* grads) { + if (!grads || !svg_strncmp(val, "url(#", 5)) return nullptr; + char id[32]; + int i = 0; + const char* p = val + 5; + while (p[i] && p[i] != ')' && i < 31) { id[i] = p[i]; ++i; } + id[i] = '\0'; + return grads->lookup_gradient(id); +} + +inline const SvgGradient* svg_get_element_gradient(const char* elem, int elemLen, + const SvgGradientTable* grads) { + char buf[128]; + int style_len = svg_get_attr(elem, elemLen, " style", buf, sizeof(buf)); + if (style_len > 0) { + const char* fill = svg_strstr(buf, style_len, "fill:"); + if (fill && (fill == buf || *(fill - 1) == ';' || svg_char_is_ws(*(fill - 1)))) { + fill += 5; + while (svg_char_is_ws(*fill)) ++fill; + const SvgGradient* gradient = svg_resolve_gradient_value(fill, grads); + if (gradient) return gradient; + } + } + if (svg_get_attr(elem, elemLen, " fill", buf, sizeof(buf)) > 0) + return svg_resolve_gradient_value(buf, grads); + return nullptr; +} + // --------------------------------------------------------------------------- // Per-element fill color extraction // Returns: 1 = color found in out_color, 0 = use default, -1 = fill="none" @@ -1139,20 +1349,102 @@ inline int svg_get_element_fill(const char* elem, int elemLen, Color* out_color, return 0; // no fill specified } +// Stroke paint uses the same color syntax and currentColor behavior as fill. +inline int svg_get_element_stroke(const char* elem, int elemLen, Color* out_color, + const SvgGradientTable* grads = nullptr, + const SvgCssTable* css = nullptr) { + char buf[128]; + int sLen = svg_get_attr(elem, elemLen, " style", buf, sizeof(buf)); + if (sLen > 0) { + const char* sp = svg_strstr(buf, sLen, "stroke:"); + if (sp && (sp == buf || *(sp - 1) == ';' || svg_char_is_ws(*(sp - 1)))) { + sp += 7; + while (svg_char_is_ws(*sp)) ++sp; + if (svg_strncmp(sp, "currentColor", 12)) return 1; + return svg_resolve_fill_value(sp, out_color, grads); + } + } + int len = svg_get_attr(elem, elemLen, " stroke", buf, sizeof(buf)); + if (len <= 0) return -1; + if (svg_strncmp(buf, "currentColor", 12)) { + if (css) { + char cls[64]; + Color c; + if (svg_get_attr(elem, elemLen, " class", cls, sizeof(cls)) > 0 && + css->lookup(cls, &c)) { + *out_color = c; + } + } + return 1; + } + return svg_resolve_fill_value(buf, out_color, grads); +} + +inline fixed_t svg_get_stroke_width(const char* elem, int elemLen) { + char buf[32]; + if (svg_get_attr(elem, elemLen, " stroke-width", buf, sizeof(buf)) > 0) { + fixed_t width = 0; + svg_parse_fixed(buf, &width); + return width; + } + char style[160]; + int style_len = svg_get_attr(elem, elemLen, " style", style, sizeof(style)); + if (style_len > 0) { + const char* value = svg_strstr(style, style_len, "stroke-width:"); + if (value) { + value += 13; + while (svg_char_is_ws(*value)) ++value; + fixed_t width = 0; + svg_parse_fixed(value, &width); + return width; + } + } + return int_to_fixed(1); +} + // --------------------------------------------------------------------------- // Per-element opacity (0-255) // --------------------------------------------------------------------------- inline int svg_get_element_opacity(const char* elem, int elemLen) { char buf[32]; - if (svg_get_attr(elem, elemLen, " opacity", buf, sizeof(buf)) > 0) { + int alpha = 255; + bool has_opacity = svg_get_attr(elem, elemLen, " opacity", buf, sizeof(buf)) > 0; + if (!has_opacity) { + char style[160]; + int style_len = svg_get_attr(elem, elemLen, " style", style, sizeof(style)); + const char* op = style_len > 0 ? svg_strstr(style, style_len, "opacity:") : nullptr; + while (op && op > style && *(op - 1) == '-') { + int used = (int)((op + 8) - style); + op = svg_strstr(style + used, style_len - used, "opacity:"); + } + if (op) { + op += 8; + while (svg_char_is_ws(*op)) ++op; + int i = 0; + while (op[i] && op[i] != ';' && i < (int)sizeof(buf) - 1) { + buf[i] = op[i]; + ++i; + } + buf[i] = '\0'; + has_opacity = true; + } + } + if (has_opacity) { fixed_t val; svg_parse_fixed(buf, &val); - int alpha = (int)(((int64_t)val * 255) >> 16); + alpha = (int)(((int64_t)val * 255) >> 16); if (alpha < 0) alpha = 0; if (alpha > 255) alpha = 255; - return alpha; } - return 255; + if (svg_get_attr(elem, elemLen, " fill-opacity", buf, sizeof(buf)) > 0) { + fixed_t val; + svg_parse_fixed(buf, &val); + int fill_alpha = (int)(((int64_t)val * 255) >> 16); + if (fill_alpha < 0) fill_alpha = 0; + if (fill_alpha > 255) fill_alpha = 255; + alpha = (alpha * fill_alpha + 127) / 255; + } + return alpha; } // --------------------------------------------------------------------------- @@ -1164,39 +1456,101 @@ inline bool svg_element_has_filter(const char* elem, int elemLen) { } // --------------------------------------------------------------------------- -// Parse transform="scale(x)" or transform="scale(x,y)" or translate(x,y) -// Returns true if a scale was found; sx/sy are fixed-point scale factors. +// Parse an SVG transform list. Matrix, translate, and scale cover the +// transforms emitted by the bundled icon set. Multiple operations are +// composed in SVG order (for example, translate(...) scale(...)). // --------------------------------------------------------------------------- -inline bool svg_get_element_scale(const char* elem, int elemLen, - fixed_t* sx, fixed_t* sy) { - char buf[64]; +inline bool svg_parse_transform(const char* value, SvgTransform* out) { + SvgTransform result = SvgTransform::identity(); + const char* p = value; + bool found = false; + + auto skip_ws_comma = [&]() { + while (*p && (svg_char_is_ws(*p) || *p == ',')) ++p; + }; + auto read_arg = [&](fixed_t* value_out) -> bool { + skip_ws_comma(); + if (!svg_char_is_num_start(*p)) return false; + int consumed = svg_parse_fixed(p, value_out); + if (consumed <= 0) return false; + p += consumed; + return true; + }; + + while (*p) { + skip_ws_comma(); + SvgTransform op = SvgTransform::identity(); + + if (svg_strncmp(p, "matrix(", 7)) { + p += 7; + if (!read_arg(&op.a) || !read_arg(&op.b) || + !read_arg(&op.c) || !read_arg(&op.d) || + !read_arg(&op.e) || !read_arg(&op.f)) break; + } else if (svg_strncmp(p, "translate(", 10)) { + p += 10; + if (!read_arg(&op.e)) break; + const char* before_second = p; + if (!read_arg(&op.f)) { + p = before_second; + op.f = 0; + } + } else if (svg_strncmp(p, "scale(", 6)) { + p += 6; + if (!read_arg(&op.a)) break; + const char* before_second = p; + if (!read_arg(&op.d)) { + p = before_second; + op.d = op.a; + } + } else if (svg_strncmp(p, "rotate(", 7)) { + p += 7; + fixed_t angle; + if (!read_arg(&angle)) break; + fixed_t sine = svg_fixed_sin_degrees(angle); + fixed_t cosine = svg_fixed_sin_degrees(int_to_fixed(90) - angle); + op.a = cosine; + op.b = sine; + op.c = -sine; + op.d = cosine; + + const char* before_center = p; + fixed_t cx, cy; + if (read_arg(&cx) && read_arg(&cy)) { + SvgTransform to_center = SvgTransform::identity(); + SvgTransform from_center = SvgTransform::identity(); + to_center.e = cx; + to_center.f = cy; + from_center.e = -cx; + from_center.f = -cy; + op = svg_transform_multiply( + svg_transform_multiply(to_center, op), from_center); + } else { + p = before_center; + } + } else { + while (*p && *p != ')') ++p; + if (*p == ')') ++p; + continue; + } + + while (*p && svg_char_is_ws(*p)) ++p; + if (*p == ')') ++p; + result = svg_transform_multiply(result, op); + found = true; + } + + *out = result; + return found; +} + +inline bool svg_get_element_transform(const char* elem, int elemLen, + SvgTransform* out) { + char buf[160]; if (svg_get_attr(elem, elemLen, " transform", buf, sizeof(buf)) <= 0) { - *sx = int_to_fixed(1); - *sy = int_to_fixed(1); + *out = SvgTransform::identity(); return false; } - // Look for "scale(" - const char* sp = buf; - while (*sp) { - if (sp[0]=='s' && sp[1]=='c' && sp[2]=='a' && sp[3]=='l' && sp[4]=='e' && sp[5]=='(') { - sp += 6; - svg_parse_fixed(sp, sx); - // Skip to comma or closing paren - while (*sp && *sp != ',' && *sp != ')') ++sp; - if (*sp == ',') { - ++sp; - while (*sp == ' ') ++sp; - svg_parse_fixed(sp, sy); - } else { - *sy = *sx; // uniform scale - } - return true; - } - ++sp; - } - *sx = int_to_fixed(1); - *sy = int_to_fixed(1); - return false; + return svg_parse_transform(buf, out); } // --------------------------------------------------------------------------- @@ -1208,6 +1562,7 @@ inline void svg_rasterize_blend(const SvgEdgeList& el, uint32_t* pixels, int w, int maxIsect = el.count + 16; fixed_t* isect = (fixed_t*)montauk::malloc(maxIsect * sizeof(fixed_t)); + if (!isect) return; uint32_t fr = (fill >> 16) & 0xFF; uint32_t fg = (fill >> 8) & 0xFF; @@ -1261,9 +1616,15 @@ inline void svg_rasterize_blend(const SvgEdgeList& el, uint32_t* pixels, int w, uint32_t dg = (dst >> 8) & 0xFF; uint32_t db = dst & 0xFF; uint32_t out_a = sa + (da * inv_sa + 127) / 255; - uint32_t rr = (fr * sa + dr * inv_sa + 128) / 255; - uint32_t gg = (fg * sa + dg * inv_sa + 128) / 255; - uint32_t bb = (fb * sa + db * inv_sa + 128) / 255; + // Source and destination pixels use straight alpha. Blend + // in premultiplied space, then convert back so translucent + // SVG paints retain their original RGB values. + uint32_t pr = fr * sa + (dr * da * inv_sa + 127) / 255; + uint32_t pg = fg * sa + (dg * da * inv_sa + 127) / 255; + uint32_t pb = fb * sa + (db * da * inv_sa + 127) / 255; + uint32_t rr = out_a ? (pr + out_a / 2) / out_a : 0; + uint32_t gg = out_a ? (pg + out_a / 2) / out_a : 0; + uint32_t bb = out_a ? (pb + out_a / 2) / out_a : 0; pixels[y * w + x] = (out_a << 24) | (rr << 16) | (gg << 8) | bb; } } @@ -1273,16 +1634,186 @@ inline void svg_rasterize_blend(const SvgEdgeList& el, uint32_t* pixels, int w, montauk::mfree(isect); } +inline void svg_rasterize_mask(const SvgEdgeList& el, uint8_t* mask, int w, int h) { + if (el.count == 0 || !mask) return; + int max_isect = el.count + 16; + fixed_t* isect = (fixed_t*)montauk::malloc(max_isect * sizeof(fixed_t)); + if (!isect) return; + for (int y = 0; y < h; ++y) { + fixed_t scan_y = int_to_fixed(y) + (1 << 15); + int count = 0; + for (int i = 0; i < el.count; ++i) { + const SvgEdge& edge = el.edges[i]; + fixed_t low = edge.y0 < edge.y1 ? edge.y0 : edge.y1; + fixed_t high = edge.y0 > edge.y1 ? edge.y0 : edge.y1; + if (scan_y < low || scan_y >= high || edge.y0 == edge.y1) continue; + fixed_t x = edge.x0 + (fixed_t)(((int64_t)(edge.x1 - edge.x0) * + (scan_y - edge.y0)) / + (edge.y1 - edge.y0)); + if (count < max_isect) isect[count++] = x; + } + for (int i = 1; i < count; ++i) { + fixed_t key = isect[i]; + int j = i - 1; + while (j >= 0 && isect[j] > key) { + isect[j + 1] = isect[j]; + --j; + } + isect[j + 1] = key; + } + for (int i = 0; i + 1 < count; i += 2) { + int x0 = fixed_to_int(isect[i]); + int x1 = fixed_to_int(isect[i + 1]); + if (x0 < 0) x0 = 0; + if (x1 > w) x1 = w; + for (int x = x0; x < x1; ++x) mask[y * w + x] = 255; + } + } + montauk::mfree(isect); +} + +inline void svg_blend_straight_pixel(uint32_t* destination, Color source, int alpha) { + uint32_t sa = (uint32_t)((alpha * source.a + 127) / 255); + if (sa == 0) return; + uint32_t dst = *destination; + uint32_t da = (dst >> 24) & 0xff; + uint32_t inv_sa = 255 - sa; + uint32_t out_a = sa + (da * inv_sa + 127) / 255; + uint32_t pr = source.r * sa + (((dst >> 16) & 0xff) * da * inv_sa + 127) / 255; + uint32_t pg = source.g * sa + (((dst >> 8) & 0xff) * da * inv_sa + 127) / 255; + uint32_t pb = source.b * sa + ((dst & 0xff) * da * inv_sa + 127) / 255; + uint32_t r = out_a ? (pr + out_a / 2) / out_a : 0; + uint32_t g = out_a ? (pg + out_a / 2) / out_a : 0; + uint32_t b = out_a ? (pb + out_a / 2) / out_a : 0; + *destination = (out_a << 24) | (r << 16) | (g << 8) | b; +} + +inline Color svg_gradient_color_at(const SvgGradient& gradient, fixed_t offset) { + if (gradient.stop_count <= 0) return Color::from_rgba(0, 0, 0, 0); + if (offset <= gradient.stops[0].offset) return gradient.stops[0].color; + for (int i = 1; i < gradient.stop_count; ++i) { + if (offset <= gradient.stops[i].offset) { + const SvgGradientStop& left = gradient.stops[i - 1]; + const SvgGradientStop& right = gradient.stops[i]; + fixed_t span = right.offset - left.offset; + fixed_t t = span > 0 ? fixed_div(offset - left.offset, span) : 0; + fixed_t inverse = int_to_fixed(1) - t; + return Color::from_rgba( + (uint8_t)((fixed_mul(int_to_fixed(left.color.r), inverse) + + fixed_mul(int_to_fixed(right.color.r), t)) >> 16), + (uint8_t)((fixed_mul(int_to_fixed(left.color.g), inverse) + + fixed_mul(int_to_fixed(right.color.g), t)) >> 16), + (uint8_t)((fixed_mul(int_to_fixed(left.color.b), inverse) + + fixed_mul(int_to_fixed(right.color.b), t)) >> 16), + (uint8_t)((fixed_mul(int_to_fixed(left.color.a), inverse) + + fixed_mul(int_to_fixed(right.color.a), t)) >> 16)); + } + } + return gradient.stops[gradient.stop_count - 1].color; +} + +inline bool svg_rasterize_linear_gradient(const SvgEdgeList& el, uint32_t* pixels, + int w, int h, const SvgGradient& gradient, + const SvgTransform& element_transform, + int alpha) { + if (gradient.radial || !gradient.user_space || gradient.stop_count == 0) return false; + uint64_t pixel_count = (uint64_t)(uint32_t)w * (uint64_t)(uint32_t)h; + uint8_t* mask = (uint8_t*)montauk::malloc(pixel_count); + if (!mask) return false; + svg_memset(mask, 0, (int)pixel_count); + svg_rasterize_mask(el, mask, w, h); + + SvgTransform gradient_to_pixels = svg_transform_multiply(element_transform, + gradient.transform); + fixed_t x1, y1, x2, y2; + svg_transform_point(gradient_to_pixels, gradient.x1, gradient.y1, &x1, &y1); + svg_transform_point(gradient_to_pixels, gradient.x2, gradient.y2, &x2, &y2); + fixed_t dx = x2 - x1; + fixed_t dy = y2 - y1; + fixed_t length = svg_fixed_hypot(dx, dy); + if (length <= 0) { + montauk::mfree(mask); + return false; + } + fixed_t ux = fixed_div(dx, length); + fixed_t uy = fixed_div(dy, length); + for (int y = 0; y < h; ++y) { + for (int x = 0; x < w; ++x) { + if (!mask[y * w + x]) continue; + fixed_t vx = int_to_fixed(x) + (1 << 15) - x1; + fixed_t vy = int_to_fixed(y) + (1 << 15) - y1; + fixed_t projection = fixed_mul(vx, ux) + fixed_mul(vy, uy); + fixed_t offset = fixed_div(projection, length); + if (offset < 0) offset = 0; + if (offset > int_to_fixed(1)) offset = int_to_fixed(1); + Color color = svg_gradient_color_at(gradient, offset); + svg_blend_straight_pixel(&pixels[y * w + x], color, alpha); + } + } + montauk::mfree(mask); + return true; +} + +// Rasterize a centered stroke around every flattened edge. Segment quads are +// joined with small discs, which gives paths continuous round joins/caps and +// works for lines, curves, arcs, and closed contours alike. +inline void svg_rasterize_stroke(const SvgEdgeList& path, uint32_t* pixels, + int w, int h, fixed_t width, + uint32_t color, int alpha) { + if (path.count == 0 || width <= 0) return; + fixed_t half = width / 2; + uint64_t pixel_count = (uint64_t)(uint32_t)w * (uint64_t)(uint32_t)h; + uint8_t* mask = (uint8_t*)montauk::malloc(pixel_count); + if (!mask) return; + svg_memset(mask, 0, (int)pixel_count); + SvgEdgeList shape; + shape.init(20); + if (!shape.edges) { montauk::mfree(mask); return; } + + for (int i = 0; i < path.count; ++i) { + const SvgEdge& edge = path.edges[i]; + fixed_t dx = edge.x1 - edge.x0; + fixed_t dy = edge.y1 - edge.y0; + fixed_t length = svg_fixed_hypot(dx, dy); + if (length == 0) continue; + fixed_t nx = fixed_mul(-dy, fixed_div(half, length)); + fixed_t ny = fixed_mul( dx, fixed_div(half, length)); + + shape.clear(); + shape.add(edge.x0 + nx, edge.y0 + ny, edge.x1 + nx, edge.y1 + ny); + shape.add(edge.x1 + nx, edge.y1 + ny, edge.x1 - nx, edge.y1 - ny); + shape.add(edge.x1 - nx, edge.y1 - ny, edge.x0 - nx, edge.y0 - ny); + shape.add(edge.x0 - nx, edge.y0 - ny, edge.x0 + nx, edge.y0 + ny); + svg_rasterize_mask(shape, mask, w, h); + + shape.clear(); + svg_circle_edges(shape, edge.x1, edge.y1, half); + svg_rasterize_mask(shape, mask, w, h); + } + Color stroke_color = Color::from_rgb((color >> 16) & 0xff, + (color >> 8) & 0xff, + color & 0xff); + for (uint64_t i = 0; i < pixel_count; ++i) { + if (mask[i]) svg_blend_straight_pixel(&pixels[i], stroke_color, alpha); + } + montauk::mfree(shape.edges); + montauk::mfree(mask); +} + // --------------------------------------------------------------------------- // SVG document parser: extract paths, circles, rects and rasterize // --------------------------------------------------------------------------- inline SvgIcon svg_render(const char* svg_data, int svg_len, int target_w, int target_h, Color fill_color) { - SvgIcon icon; + SvgIcon icon = {nullptr, 0, 0}; + if (!svg_data || svg_len <= 0 || target_w <= 0 || target_h <= 0) return icon; + uint64_t pixel_count = (uint64_t)(uint32_t)target_w * (uint64_t)(uint32_t)target_h; + if (pixel_count > 536870911ULL) return icon; // byte count must fit svg_memset(int) icon.width = target_w; icon.height = target_h; - icon.pixels = (uint32_t*)montauk::malloc(target_w * target_h * sizeof(uint32_t)); + icon.pixels = (uint32_t*)montauk::malloc(pixel_count * sizeof(uint32_t)); + if (!icon.pixels) return {nullptr, 0, 0}; // Clear to transparent - svg_memset(icon.pixels, 0, target_w * target_h * sizeof(uint32_t)); + svg_memset(icon.pixels, 0, (int)(pixel_count * sizeof(uint32_t))); // Parse SVG dimensions: width and height int svg_w = 16, svg_h = 16; @@ -1302,11 +1833,13 @@ inline SvgIcon svg_render(const char* svg_data, int svg_len, int target_w, int t // width if (svg_get_attr(svg_tag, tag_len, " width", attr_buf, sizeof(attr_buf)) > 0) { - svg_w = svg_parse_int(attr_buf); + int parsed_w = svg_parse_length_pixels(attr_buf); + if (parsed_w > 0) svg_w = parsed_w; } // height if (svg_get_attr(svg_tag, tag_len, " height", attr_buf, sizeof(attr_buf)) > 0) { - svg_h = svg_parse_int(attr_buf); + int parsed_h = svg_parse_length_pixels(attr_buf); + if (parsed_h > 0) svg_h = parsed_h; } // viewBox if (svg_get_attr(svg_tag, tag_len, " viewBox", attr_buf, sizeof(attr_buf)) > 0) { @@ -1330,6 +1863,11 @@ inline SvgIcon svg_render(const char* svg_data, int svg_len, int target_w, int t // Compute scale: pixel = (svg_coord - vb_origin) * target_size / vb_size fixed_t scale_x = vb_w > 0 ? fixed_div(int_to_fixed(target_w), vb_w) : int_to_fixed(1); fixed_t scale_y = vb_h > 0 ? fixed_div(int_to_fixed(target_h), vb_h) : int_to_fixed(1); + SvgTransform view_transform = { + scale_x, 0, 0, scale_y, + -fixed_mul(vb_x, scale_x), + -fixed_mul(vb_y, scale_y) + }; // Pre-parse gradient definitions from for url(#id) fill resolution SvgGradientTable grads; @@ -1338,15 +1876,16 @@ inline SvgIcon svg_render(const char* svg_data, int svg_len, int target_w, int t const char* dp = svg_data; const char* dend = svg_data + svg_len; while (dp < dend) { - const char* gp = svg_strstr(dp, (int)(dend - dp), " or ) - const char* gend = svg_strstr(gp, (int)(dend - gp), ""); - if (!gend) gend = svg_strstr(gp, (int)(dend - gp), ""); + // Find the matching end of this gradient block. + bool is_linear = svg_strncmp(gp, "" : ""); if (!gend) gend = svg_strstr(gp, (int)(dend - gp), "/>"); if (!gend) break; @@ -1357,18 +1896,77 @@ inline SvgIcon svg_render(const char* svg_data, int svg_len, int target_w, int t int id_len = svg_get_attr(gp, gtag_end + 1, " id", grad_id, sizeof(grad_id)); if (id_len > 0) { - // Find first 0 && + svg_strncmp(attr, "userSpaceOnUse", 14); + if (is_linear) { + gradient.x1 = 0; + gradient.y1 = 0; + gradient.x2 = int_to_fixed(1); + gradient.y2 = 0; + if (svg_get_attr(gp, gtag_end + 1, " x1", attr, sizeof(attr)) > 0) + svg_parse_fixed(attr, &gradient.x1); + if (svg_get_attr(gp, gtag_end + 1, " y1", attr, sizeof(attr)) > 0) + svg_parse_fixed(attr, &gradient.y1); + if (svg_get_attr(gp, gtag_end + 1, " x2", attr, sizeof(attr)) > 0) + svg_parse_fixed(attr, &gradient.x2); + if (svg_get_attr(gp, gtag_end + 1, " y2", attr, sizeof(attr)) > 0) + svg_parse_fixed(attr, &gradient.y2); + } else { + if (svg_get_attr(gp, gtag_end + 1, " cx", attr, sizeof(attr)) > 0) + svg_parse_fixed(attr, &gradient.cx); + if (svg_get_attr(gp, gtag_end + 1, " cy", attr, sizeof(attr)) > 0) + svg_parse_fixed(attr, &gradient.cy); + if (svg_get_attr(gp, gtag_end + 1, " r", attr, sizeof(attr)) > 0) + svg_parse_fixed(attr, &gradient.radius); + } + if (svg_get_attr(gp, gtag_end + 1, " gradientTransform", attr, sizeof(attr)) > 0) + svg_parse_transform(attr, &gradient.transform); + + const char* stop_search = gp + gtag_end + 1; + while (stop_search < gend && gradient.stop_count < SVG_MAX_GRADIENT_STOPS) { + const char* stop = svg_strstr(stop_search, (int)(gend - stop_search), " 0) { - if (sc_buf[0] == '#') { - grads.add(grad_id, svg_parse_hex_color(sc_buf)); + char sc_buf[32], offset_buf[32], opacity_buf[32]; + if (svg_get_attr(stop, stop_end + 1, " stop-color", sc_buf, sizeof(sc_buf)) > 0 && + sc_buf[0] == '#') { + SvgGradientStop& parsed = gradient.stops[gradient.stop_count]; + parsed.color = svg_parse_hex_color(sc_buf); + parsed.offset = 0; + if (svg_get_attr(stop, stop_end + 1, " offset", offset_buf, sizeof(offset_buf)) > 0) { + svg_parse_fixed(offset_buf, &parsed.offset); + const char* percent = offset_buf; + while (*percent && *percent != '%') ++percent; + if (*percent == '%') parsed.offset /= 100; } + if (parsed.offset < 0) parsed.offset = 0; + if (parsed.offset > int_to_fixed(1)) parsed.offset = int_to_fixed(1); + if (svg_get_attr(stop, stop_end + 1, " stop-opacity", opacity_buf, sizeof(opacity_buf)) > 0) { + fixed_t opacity; + svg_parse_fixed(opacity_buf, &opacity); + int stop_alpha = (int)(((int64_t)opacity * 255) >> 16); + if (stop_alpha < 0) stop_alpha = 0; + if (stop_alpha > 255) stop_alpha = 255; + parsed.color.a = (uint8_t)stop_alpha; + } + ++gradient.stop_count; } + stop_search = stop + stop_end + 1; } + if (gradient.stop_count > 0) grads.add(gradient); } dp = gend + 1; } @@ -1391,15 +1989,18 @@ inline SvgIcon svg_render(const char* svg_data, int svg_len, int target_w, int t // Shared edge list (cleared per element for multi-color support) SvgEdgeList el; - el.init(SVG_MAX_EDGES); + el.init(512); - // Heap-allocated path data buffer (avoids 8 KiB on the stack) - char* d_buf = (char*)montauk::malloc(SVG_MAX_PATH_LEN); - - // Group fill inheritance stack: track so child elements inherit - static constexpr int MAX_GROUP_DEPTH = 8; - struct GroupFill { Color color; bool has_fill; }; - GroupFill g_stack[MAX_GROUP_DEPTH]; + // Presentation and transform state inherited through nested groups. + static constexpr int MAX_GROUP_DEPTH = 32; + struct GroupState { + SvgTransform transform; + Color color; + bool has_fill; + bool fill_none; + int alpha; + }; + GroupState g_stack[MAX_GROUP_DEPTH]; int g_depth = 0; // Scan for 0) { - int fr = svg_resolve_fill_value(fbuf, &gc, &grads); - if (fr == 1) { - g_stack[g_depth].color = gc; - g_stack[g_depth].has_fill = true; - } else if (fr == -1) { - // fill="none" on group — children inherit none - g_stack[g_depth].has_fill = false; - } + GroupState state = { + SvgTransform::identity(), fill_color, false, false, 255 + }; + if (g_depth > 0) state = g_stack[g_depth - 1]; + + SvgTransform local_transform; + svg_get_element_transform(g_start, g_len, &local_transform); + state.transform = svg_transform_multiply(state.transform, local_transform); + + Color gc = state.has_fill ? state.color : fill_color; + int fr = svg_get_element_fill(g_start, g_len, &gc, &grads, &css); + if (fr == 1) { + state.color = gc; + state.has_fill = true; + state.fill_none = false; + } else if (fr == -1) { + state.fill_none = true; } + state.alpha = (state.alpha * svg_get_element_opacity(g_start, g_len) + 127) / 255; + g_stack[g_depth] = state; g_depth++; } p = g_end; @@ -1478,35 +2085,60 @@ inline SvgIcon svg_render(const char* svg_data, int svg_len, int target_w, int t // Determine fill color: element's own fill, or inherit from nearest Color elem_color = fill_color; // Apply inherited group fill as default - for (int gi = g_depth - 1; gi >= 0; gi--) { - if (g_stack[gi].has_fill) { elem_color = g_stack[gi].color; break; } + bool inherited_none = false; + if (g_depth > 0) { + if (g_stack[g_depth - 1].has_fill) elem_color = g_stack[g_depth - 1].color; + inherited_none = g_stack[g_depth - 1].fill_none; } int fillResult = svg_get_element_fill(elem_start, elem_len, &elem_color, &grads, &css); - if (fillResult == -1) { p = elem_end; continue; } // fill="none" + bool draw_fill = fillResult != -1 && !(fillResult == 0 && inherited_none); + const SvgGradient* fill_gradient = svg_get_element_gradient(elem_start, elem_len, &grads); + + Color stroke_color = elem_color; + int stroke_result = svg_get_element_stroke(elem_start, elem_len, &stroke_color, &grads, &css); int alpha = svg_get_element_opacity(elem_start, elem_len); + if (g_depth > 0) alpha = (alpha * g_stack[g_depth - 1].alpha + 127) / 255; - // Per-element transform (scale) - fixed_t elem_sx, elem_sy; - svg_get_element_scale(elem_start, elem_len, &elem_sx, &elem_sy); - fixed_t eff_sx = fixed_mul(scale_x, elem_sx); - fixed_t eff_sy = fixed_mul(scale_y, elem_sy); + SvgTransform user_transform = g_depth > 0 + ? g_stack[g_depth - 1].transform : SvgTransform::identity(); + SvgTransform elem_transform; + svg_get_element_transform(elem_start, elem_len, &elem_transform); + user_transform = svg_transform_multiply(user_transform, elem_transform); + SvgTransform final_transform = svg_transform_multiply(view_transform, user_transform); // Extract and rasterize path - int d_len = svg_get_attr(elem_start, elem_len, " d", d_buf, SVG_MAX_PATH_LEN); - if (d_len > 0) { + const char* d_data = nullptr; + int d_len = 0; + if (svg_get_attr_span(elem_start, elem_len, " d", &d_data, &d_len) && d_len > 0) { el.clear(); - svg_path_to_edges(el, d_buf, d_len, eff_sx, eff_sy, vb_x, vb_y); - if (el.count > 0) - svg_rasterize_blend(el, icon.pixels, target_w, target_h, elem_color.to_pixel(), alpha); + svg_path_to_edges(el, d_data, d_len, final_transform); + if (el.count > 0 && draw_fill) { + bool rendered_gradient = fill_gradient && svg_rasterize_linear_gradient( + el, icon.pixels, target_w, target_h, *fill_gradient, + final_transform, alpha); + if (!rendered_gradient) + svg_rasterize_blend(el, icon.pixels, target_w, target_h, + elem_color.to_pixel(), alpha); + } + if (el.count > 0 && stroke_result == 1) { + fixed_t stroke_width = svg_get_stroke_width(elem_start, elem_len); + stroke_width = fixed_mul(stroke_width, svg_transform_average_scale(final_transform)); + svg_rasterize_stroke(el, icon.pixels, target_w, target_h, + stroke_width, stroke_color.to_pixel(), alpha); + } } p = elem_end; continue; } - // Check for 7 && svg_strncmp(p, " 7 && svg_strncmp(p, " 8 && svg_strncmp(p, "= 0; gi--) { - if (g_stack[gi].has_fill) { elem_color = g_stack[gi].color; break; } + bool inherited_none = false; + if (g_depth > 0) { + if (g_stack[g_depth - 1].has_fill) elem_color = g_stack[g_depth - 1].color; + inherited_none = g_stack[g_depth - 1].fill_none; } int fillResult = svg_get_element_fill(elem_start, elem_len, &elem_color, &grads, &css); - if (fillResult == -1) { p = elem_end; continue; } + bool draw_fill = fillResult != -1 && !(fillResult == 0 && inherited_none); + const SvgGradient* fill_gradient = svg_get_element_gradient(elem_start, elem_len, &grads); + + Color stroke_color = elem_color; + int stroke_result = svg_get_element_stroke(elem_start, elem_len, &stroke_color, &grads, &css); int alpha = svg_get_element_opacity(elem_start, elem_len); + if (g_depth > 0) alpha = (alpha * g_stack[g_depth - 1].alpha + 127) / 255; - fixed_t elem_sx, elem_sy; - svg_get_element_scale(elem_start, elem_len, &elem_sx, &elem_sy); - fixed_t eff_sx = fixed_mul(scale_x, elem_sx); - fixed_t eff_sy = fixed_mul(scale_y, elem_sy); + SvgTransform user_transform = g_depth > 0 + ? g_stack[g_depth - 1].transform : SvgTransform::identity(); + SvgTransform elem_transform; + svg_get_element_transform(elem_start, elem_len, &elem_transform); + user_transform = svg_transform_multiply(user_transform, elem_transform); + SvgTransform final_transform = svg_transform_multiply(view_transform, user_transform); char attr_buf[32]; - fixed_t cx = 0, cy = 0, r = 0; + fixed_t cx = 0, cy = 0, rx = 0, ry = 0; if (svg_get_attr(elem_start, elem_len, " cx", attr_buf, sizeof(attr_buf)) > 0) svg_parse_fixed(attr_buf, &cx); if (svg_get_attr(elem_start, elem_len, " cy", attr_buf, sizeof(attr_buf)) > 0) svg_parse_fixed(attr_buf, &cy); - if (svg_get_attr(elem_start, elem_len, " r", attr_buf, sizeof(attr_buf)) > 0) - svg_parse_fixed(attr_buf, &r); - - fixed_t scx = fixed_mul(cx - vb_x, eff_sx); - fixed_t scy = fixed_mul(cy - vb_y, eff_sy); - fixed_t srx = fixed_mul(r, eff_sx); - fixed_t sry = fixed_mul(r, eff_sy); - fixed_t sr = (srx + sry) >> 1; + if (is_circle) { + if (svg_get_attr(elem_start, elem_len, " r", attr_buf, sizeof(attr_buf)) > 0) + svg_parse_fixed(attr_buf, &rx); + ry = rx; + } else { + if (svg_get_attr(elem_start, elem_len, " rx", attr_buf, sizeof(attr_buf)) > 0) + svg_parse_fixed(attr_buf, &rx); + if (svg_get_attr(elem_start, elem_len, " ry", attr_buf, sizeof(attr_buf)) > 0) + svg_parse_fixed(attr_buf, &ry); + } el.clear(); - svg_circle_edges(el, scx, scy, sr); - if (el.count > 0) - svg_rasterize_blend(el, icon.pixels, target_w, target_h, elem_color.to_pixel(), alpha); + svg_ellipse_edges(el, cx, cy, rx, ry, final_transform); + if (el.count > 0 && draw_fill) { + bool rendered_gradient = fill_gradient && svg_rasterize_linear_gradient( + el, icon.pixels, target_w, target_h, *fill_gradient, + final_transform, alpha); + if (!rendered_gradient) + svg_rasterize_blend(el, icon.pixels, target_w, target_h, + elem_color.to_pixel(), alpha); + } + if (el.count > 0 && stroke_result == 1) { + fixed_t stroke_width = svg_get_stroke_width(elem_start, elem_len); + stroke_width = fixed_mul(stroke_width, svg_transform_average_scale(final_transform)); + svg_rasterize_stroke(el, icon.pixels, target_w, target_h, + stroke_width, stroke_color.to_pixel(), alpha); + } p = elem_end; continue; @@ -1570,18 +2225,27 @@ inline SvgIcon svg_render(const char* svg_data, int svg_len, int target_w, int t } Color elem_color = fill_color; - for (int gi = g_depth - 1; gi >= 0; gi--) { - if (g_stack[gi].has_fill) { elem_color = g_stack[gi].color; break; } + bool inherited_none = false; + if (g_depth > 0) { + if (g_stack[g_depth - 1].has_fill) elem_color = g_stack[g_depth - 1].color; + inherited_none = g_stack[g_depth - 1].fill_none; } int fillResult = svg_get_element_fill(elem_start, elem_len, &elem_color, &grads, &css); - if (fillResult == -1) { p = elem_end; continue; } + bool draw_fill = fillResult != -1 && !(fillResult == 0 && inherited_none); + const SvgGradient* fill_gradient = svg_get_element_gradient(elem_start, elem_len, &grads); + + Color stroke_color = elem_color; + int stroke_result = svg_get_element_stroke(elem_start, elem_len, &stroke_color, &grads, &css); int alpha = svg_get_element_opacity(elem_start, elem_len); + if (g_depth > 0) alpha = (alpha * g_stack[g_depth - 1].alpha + 127) / 255; - fixed_t elem_sx, elem_sy; - svg_get_element_scale(elem_start, elem_len, &elem_sx, &elem_sy); - fixed_t eff_sx = fixed_mul(scale_x, elem_sx); - fixed_t eff_sy = fixed_mul(scale_y, elem_sy); + SvgTransform user_transform = g_depth > 0 + ? g_stack[g_depth - 1].transform : SvgTransform::identity(); + SvgTransform elem_transform; + svg_get_element_transform(elem_start, elem_len, &elem_transform); + user_transform = svg_transform_multiply(user_transform, elem_transform); + SvgTransform final_transform = svg_transform_multiply(view_transform, user_transform); char attr_buf[32]; fixed_t rx_val = 0, ry_val = 0, rw = 0, rh = 0, rrx = 0, rry = 0; @@ -1599,17 +2263,41 @@ inline SvgIcon svg_render(const char* svg_data, int svg_len, int target_w, int t if (svg_get_attr(elem_start, elem_len, " ry", attr_buf, sizeof(attr_buf)) > 0) svg_parse_fixed(attr_buf, &rry); - fixed_t sx = fixed_mul(rx_val - vb_x, eff_sx); - fixed_t sy = fixed_mul(ry_val - vb_y, eff_sy); - fixed_t sw = fixed_mul(rw, eff_sx); - fixed_t sh = fixed_mul(rh, eff_sy); - fixed_t srx = fixed_mul(rrx, eff_sx); - fixed_t sry = fixed_mul(rry, eff_sy); - el.clear(); - svg_rect_edges(el, sx, sy, sw, sh, srx, sry); - if (el.count > 0) - svg_rasterize_blend(el, icon.pixels, target_w, target_h, elem_color.to_pixel(), alpha); + if (rrx > 0 || rry > 0) { + // Preserve rounded corners for the axis-aligned transforms + // used by icon artwork; fall back to a transformed polygon + // for rotated/sheared rectangles. + if (final_transform.b == 0 && final_transform.c == 0) { + fixed_t sx, sy; + svg_transform_point(final_transform, rx_val, ry_val, &sx, &sy); + fixed_t sw = fixed_mul(rw, final_transform.a); + fixed_t sh = fixed_mul(rh, final_transform.d); + fixed_t srx = fixed_mul(rrx, final_transform.a); + fixed_t sry = fixed_mul(rry, final_transform.d); + if (sw < 0) { sx += sw; sw = -sw; srx = -srx; } + if (sh < 0) { sy += sh; sh = -sh; sry = -sry; } + svg_rect_edges(el, sx, sy, sw, sh, srx, sry); + } else { + svg_rect_edges_transformed(el, rx_val, ry_val, rw, rh, final_transform); + } + } else { + svg_rect_edges_transformed(el, rx_val, ry_val, rw, rh, final_transform); + } + if (el.count > 0 && draw_fill) { + bool rendered_gradient = fill_gradient && svg_rasterize_linear_gradient( + el, icon.pixels, target_w, target_h, *fill_gradient, + final_transform, alpha); + if (!rendered_gradient) + svg_rasterize_blend(el, icon.pixels, target_w, target_h, + elem_color.to_pixel(), alpha); + } + if (el.count > 0 && stroke_result == 1) { + fixed_t stroke_width = svg_get_stroke_width(elem_start, elem_len); + stroke_width = fixed_mul(stroke_width, svg_transform_average_scale(final_transform)); + svg_rasterize_stroke(el, icon.pixels, target_w, target_h, + stroke_width, stroke_color.to_pixel(), alpha); + } p = elem_end; continue; @@ -1618,7 +2306,6 @@ inline SvgIcon svg_render(const char* svg_data, int svg_len, int target_w, int t ++p; } - montauk::mfree(d_buf); montauk::mfree(el.edges); return icon; } @@ -1642,9 +2329,9 @@ inline void svg_get_natural_size(const char* svg_data, int svg_len, char attr_buf[64]; if (svg_get_attr(svg_tag, tag_len, " width", attr_buf, sizeof(attr_buf)) > 0) - w = svg_parse_int(attr_buf); + w = svg_parse_length_pixels(attr_buf); if (svg_get_attr(svg_tag, tag_len, " height", attr_buf, sizeof(attr_buf)) > 0) - h = svg_parse_int(attr_buf); + h = svg_parse_length_pixels(attr_buf); if ((w <= 0 || h <= 0) && svg_get_attr(svg_tag, tag_len, " viewBox", attr_buf, sizeof(attr_buf)) > 0) { @@ -1695,6 +2382,10 @@ inline SvgIcon svg_load(const char* vfs_path, int target_w, int target_h, Color // Allocate final icon at target resolution uint32_t* out = (uint32_t*)montauk::malloc(target_w * target_h * 4); + if (!out) { + montauk::mfree(hi.pixels); + return {nullptr, 0, 0}; + } for (int i = 0; i < target_w * target_h; i++) out[i] = 0; // Downsample: average each SSxSS block using premultiplied alpha diff --git a/tests/Makefile b/tests/Makefile index 7536695..88cf963 100644 --- a/tests/Makefile +++ b/tests/Makefile @@ -2,17 +2,22 @@ CXX ?= g++ CXXFLAGS ?= -std=c++20 -O2 -Wall -Wextra HTTP_TEST := http_test +SVG_TEST := svg_renderer_test .PHONY: all check clean -all: $(HTTP_TEST) +all: $(HTTP_TEST) $(SVG_TEST) $(HTTP_TEST): http_test.cpp $(CXX) $(CXXFLAGS) -I../programs/include \ -I../programs/lib/bearssl/inc $< -o $@ -check: $(HTTP_TEST) +$(SVG_TEST): svg_renderer_test.cpp ../programs/include/gui/svg.hpp + $(CXX) $(CXXFLAGS) -I../programs/include $< -o $@ + +check: $(HTTP_TEST) $(SVG_TEST) ./$(HTTP_TEST) + ./$(SVG_TEST) clean: - rm -f $(HTTP_TEST) + rm -f $(HTTP_TEST) $(SVG_TEST) diff --git a/tests/svg_renderer_test.cpp b/tests/svg_renderer_test.cpp new file mode 100644 index 0000000..62f85ff --- /dev/null +++ b/tests/svg_renderer_test.cpp @@ -0,0 +1,223 @@ +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace { + +int failures = 0; + +void expect(bool condition, const char* message) { + if (!condition) { + std::cerr << "FAIL: " << message << '\n'; + ++failures; + } +} + +bool opaque_at(const gui::SvgIcon& icon, int x, int y) { + return ((icon.pixels[y * icon.width + x] >> 24) & 0xff) != 0; +} + +void test_transform_parser() { + gui::SvgTransform transform; + expect(gui::svg_parse_transform("translate(3 4) scale(2)", &transform), + "transform list parses"); + gui::fixed_t x, y; + gui::svg_transform_point(transform, gui::int_to_fixed(1), + gui::int_to_fixed(2), &x, &y); + expect(gui::fixed_to_int(x) == 5 && gui::fixed_to_int(y) == 8, + "transform list uses SVG composition order"); + + expect(gui::svg_parse_transform("matrix(2 0 0 3 4 5)", &transform), + "matrix transform parses"); + gui::svg_transform_point(transform, gui::int_to_fixed(2), + gui::int_to_fixed(3), &x, &y); + expect(gui::fixed_to_int(x) == 8 && gui::fixed_to_int(y) == 14, + "matrix coefficients and translation apply"); + + expect(gui::svg_parse_transform("rotate(90 5 5)", &transform), + "rotation around a center parses"); + gui::svg_transform_point(transform, gui::int_to_fixed(7), + gui::int_to_fixed(5), &x, &y); + expect(gui::fixed_to_int(x) == 5 && gui::fixed_to_int(y) == 7, + "rotation around a center applies"); +} + +void test_natural_size_units() { + const char svg[] = ""; + int width = 0, height = 0; + gui::svg_get_natural_size(svg, sizeof(svg) - 1, &width, &height); + expect(width == 96 && height == 96, "absolute SVG units convert to CSS pixels"); + + const char percent_svg[] = ""; + gui::svg_get_natural_size(percent_svg, sizeof(percent_svg) - 1, &width, &height); + expect(width == 320 && height == 180, "percentage dimensions fall back to viewBox"); +} + +void test_nested_group_transform() { + const char svg[] = + "" + "" + "" + ""; + gui::SvgIcon icon = gui::svg_render(svg, sizeof(svg) - 1, 20, 20, + gui::Color::from_rgb(0, 0, 0)); + expect(opaque_at(icon, 7, 6), "nested transformed rectangle is rendered at destination"); + expect(!opaque_at(icon, 2, 2), "nested transformed rectangle is absent at source position"); + gui::svg_free(icon); +} + +void test_matrix_group_and_stroke() { + const char svg[] = + "" + "" + "" + ""; + gui::SvgIcon icon = gui::svg_render(svg, sizeof(svg) - 1, 20, 20, + gui::Color::from_rgb(0, 0, 0)); + expect(opaque_at(icon, 8, 8), "horizontal stroke in a matrix-transformed group renders"); + expect(!opaque_at(icon, 8, 3), "matrix-transformed stroke is not left untransformed"); + gui::svg_free(icon); +} + +void test_translucent_stroke_is_composited_once() { + const char svg[] = + ""; + gui::SvgIcon icon = gui::svg_render(svg, sizeof(svg) - 1, 20, 20, + gui::Color::from_rgb(0, 0, 0)); + int body_alpha = (icon.pixels[10 * icon.width + 5] >> 24) & 0xff; + int join_alpha = (icon.pixels[10 * icon.width + 10] >> 24) & 0xff; + expect(body_alpha >= 75 && body_alpha <= 77, "translucent stroke has expected opacity"); + expect(join_alpha == body_alpha, "flattened stroke joins do not accumulate opacity"); + gui::svg_free(icon); +} + +void test_linear_gradient() { + const char svg[] = + "" + "" + "" + ""; + gui::SvgIcon icon = gui::svg_render(svg, sizeof(svg) - 1, 20, 10, + gui::Color::from_rgb(0, 0, 0)); + uint32_t left = icon.pixels[5 * icon.width + 1]; + uint32_t right = icon.pixels[5 * icon.width + 18]; + expect(((left >> 16) & 0xff) > (left & 0xff), "linear gradient starts at first stop"); + expect((right & 0xff) > ((right >> 16) & 0xff), "linear gradient reaches final stop"); + gui::svg_free(icon); +} + +void test_ellipse_and_paint_inheritance() { + const char svg[] = + "" + "" + ""; + gui::SvgIcon icon = gui::svg_render(svg, sizeof(svg) - 1, 20, 20, + gui::Color::from_rgb(0, 0, 0)); + uint32_t center = icon.pixels[10 * icon.width + 10]; + int alpha = (center >> 24) & 0xff; + expect(alpha >= 126 && alpha <= 128, "ellipse renders with explicit fill over inherited none"); + expect(((center >> 8) & 0xff) == 255, "ellipse fill color is preserved"); + gui::svg_free(icon); +} + +void test_long_path_is_not_truncated() { + std::string svg = ""; + expect(svg.size() > 8192, "long-path regression fixture exceeds former limit"); + gui::SvgIcon icon = gui::svg_render(svg.data(), (int)svg.size(), 20, 20, + gui::Color::from_rgb(0, 0, 0)); + expect(opaque_at(icon, 16, 16), "geometry after 8 KiB in a path still renders"); + gui::svg_free(icon); +} + +std::string read_test_asset(const char* name) { + const char* prefixes[] = {"programs/bin/icons/", "../programs/bin/icons/"}; + for (const char* prefix : prefixes) { + std::ifstream input(std::string(prefix) + name, std::ios::binary); + if (input) return std::string(std::istreambuf_iterator(input), {}); + } + return {}; +} + +void test_bundled_system_icons() { + std::string executable = read_test_asset("application-x-executable.svg"); + expect(!executable.empty(), "bundled executable icon is available to regression test"); + if (!executable.empty()) { + gui::SvgIcon icon = gui::svg_render(executable.data(), (int)executable.size(), 36, 36, + gui::Color::from_rgb(0, 0, 0)); + int painted = 0; + for (int i = 0; i < icon.width * icon.height; ++i) + if ((icon.pixels[i] >> 24) != 0) ++painted; + expect(painted > 350, "matrix-transformed executable icon renders substantial geometry"); + gui::svg_free(icon); + } + + std::string browser = read_test_asset("web-browser.svg"); + expect(!browser.empty(), "bundled browser icon is available to regression test"); + if (!browser.empty()) { + gui::SvgIcon icon = gui::svg_render(browser.data(), (int)browser.size(), 64, 64, + gui::Color::from_rgb(0, 0, 0)); + uint32_t equator = icon.pixels[33 * icon.width + 32]; + expect(((equator >> 16) & 0xff) > 180 && ((equator >> 8) & 0xff) > 180, + "stroke-only globe geometry appears in bundled browser icon"); + gui::svg_free(icon); + } +} + +void test_all_bundled_icons_render() { + namespace fs = std::filesystem; + fs::path directory = fs::path("programs/bin/icons"); + if (!fs::exists(directory)) directory = fs::path("../programs/bin/icons"); + expect(fs::exists(directory), "bundled icon directory is available"); + if (!fs::exists(directory)) return; + + int checked = 0; + for (const fs::directory_entry& entry : fs::directory_iterator(directory)) { + if (entry.path().extension() != ".svg") continue; + std::ifstream input(entry.path(), std::ios::binary); + std::string source(std::istreambuf_iterator(input), {}); + gui::SvgIcon icon = gui::svg_render(source.data(), (int)source.size(), 64, 64, + gui::Color::from_rgb(92, 97, 108)); + bool painted = false; + if (icon.pixels) { + for (int i = 0; i < icon.width * icon.height; ++i) { + if ((icon.pixels[i] >> 24) != 0) { painted = true; break; } + } + } + if (!painted) { + std::string message = "bundled icon renders visible pixels: " + entry.path().filename().string(); + expect(false, message.c_str()); + } + gui::svg_free(icon); + ++checked; + } + expect(checked >= 100, "complete bundled system icon set was audited"); +} + +} // namespace + +int main() { + test_transform_parser(); + test_natural_size_units(); + test_nested_group_transform(); + test_matrix_group_and_stroke(); + test_translucent_stroke_is_composited_once(); + test_linear_gradient(); + test_ellipse_and_paint_inheritance(); + test_long_path_is_not_truncated(); + test_bundled_system_icons(); + test_all_bundled_icons_render(); + if (failures != 0) return 1; + std::cout << "SVG renderer tests passed\n"; + return 0; +}