Files
MontaukOS/programs/include/gui/svg.hpp
T

973 lines
34 KiB
C++

/*
* svg.hpp
* ZenithOS SVG icon parser and scanline rasterizer
* Handles the Flat-Remix symbolic icon subset (path, circle, rect)
* All math uses 16.16 fixed-point -- NO floating point.
* Copyright (c) 2025 Daniel Hammer
*/
#pragma once
#include "gui/gui.hpp"
#include <zenith/syscall.h>
namespace gui {
// ---------------------------------------------------------------------------
// SVG icon result
// ---------------------------------------------------------------------------
struct SvgIcon {
uint32_t* pixels; // ARGB pixel data (heap-allocated)
int width;
int height;
};
// ---------------------------------------------------------------------------
// Edge used by the scanline rasterizer
// ---------------------------------------------------------------------------
struct SvgEdge {
fixed_t x0, y0, x1, y1;
};
// ---------------------------------------------------------------------------
// Constants
// ---------------------------------------------------------------------------
static constexpr int SVG_MAX_EDGES = 4096;
static constexpr int SVG_MAX_PATH_LEN = 4096;
static constexpr int SVG_MAX_FILE_SIZE = 32768;
static constexpr int SVG_BEZIER_STEPS = 8;
// ---------------------------------------------------------------------------
// Fixed-point number parser (NO floating point)
// Parses strings like "3.25", "-0.5", ".1115", "16"
// Returns the number of characters consumed.
// ---------------------------------------------------------------------------
inline int svg_parse_fixed(const char* s, fixed_t* out) {
const char* p = s;
bool neg = false;
if (*p == '-') { neg = true; ++p; }
else if (*p == '+') { ++p; }
// Integer part
int32_t integer = 0;
while (*p >= '0' && *p <= '9') {
integer = integer * 10 + (*p - '0');
++p;
}
// Fractional part
int32_t frac = 0;
int32_t frac_div = 1;
if (*p == '.') {
++p;
while (*p >= '0' && *p <= '9') {
if (frac_div < 100000) { // prevent overflow
frac = frac * 10 + (*p - '0');
frac_div *= 10;
}
++p;
}
}
fixed_t val = int_to_fixed(integer);
if (frac_div > 1) {
val += (int32_t)(((int64_t)frac << 16) / frac_div);
}
if (neg) val = -val;
*out = val;
return (int)(p - s);
}
// ---------------------------------------------------------------------------
// String helpers (no stdlib)
// ---------------------------------------------------------------------------
inline bool svg_char_is_ws(char c) {
return c == ' ' || c == '\t' || c == '\n' || c == '\r';
}
inline bool svg_char_is_sep(char c) {
return svg_char_is_ws(c) || c == ',';
}
inline bool svg_char_is_num_start(char c) {
return (c >= '0' && c <= '9') || c == '-' || c == '+' || c == '.';
}
inline bool svg_char_is_cmd(char c) {
return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z');
}
inline int svg_strlen(const char* s) {
int n = 0;
while (s[n]) ++n;
return n;
}
inline bool svg_strncmp(const char* a, const char* b, int n) {
for (int i = 0; i < n; ++i)
if (a[i] != b[i]) return false;
return true;
}
inline void svg_memset(void* dst, uint8_t val, int n) {
auto* d = (uint8_t*)dst;
for (int i = 0; i < n; ++i) d[i] = val;
}
inline void svg_memcpy(void* dst, const void* src, int n) {
auto* d = (uint8_t*)dst;
auto* s = (const uint8_t*)src;
for (int i = 0; i < n; ++i) d[i] = s[i];
}
// ---------------------------------------------------------------------------
// Mini XML attribute extraction
// ---------------------------------------------------------------------------
// Find the next occurrence of needle in haystack (haystack has length hLen).
// Returns pointer to start of match, or nullptr.
inline const char* svg_strstr(const char* haystack, int hLen, const char* needle) {
int nLen = svg_strlen(needle);
if (nLen == 0 || nLen > hLen) return nullptr;
for (int i = 0; i <= hLen - nLen; ++i) {
if (svg_strncmp(haystack + i, needle, nLen))
return haystack + i;
}
return nullptr;
}
// Check if a character can be part of an XML attribute name
inline bool svg_char_is_attrname(char c) {
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') ||
(c >= '0' && c <= '9') || c == '-' || c == '_' || c == ':';
}
// Extract the value of an XML attribute: attr="value"
// The attr string should include a leading space (e.g., " cx") to distinguish
// from substrings of other attribute names.
// Writes into buf (up to maxLen-1 chars), returns length or -1 if not found.
inline int svg_get_attr(const char* tag, int tagLen, const char* attr, char* buf, int maxLen) {
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 -1;
// Ensure this is the exact attribute name, not a prefix of another
// (e.g., " r" should not match " rx" or " ry")
const char* after_name = p + attrLen;
if (after_name < tag + tagLen && svg_char_is_attrname(*after_name)) {
// This is a prefix of a longer attribute name, skip and search again
search_start = after_name;
search_len = tagLen - (int)(search_start - tag);
continue;
}
p = after_name;
// skip whitespace around '='
while (p < tag + tagLen && svg_char_is_ws(*p)) ++p;
if (p >= tag + tagLen || *p != '=') return -1;
++p;
while (p < tag + tagLen && svg_char_is_ws(*p)) ++p;
if (p >= tag + tagLen) return -1;
char quote = *p;
if (quote != '"' && quote != '\'') return -1;
++p;
int len = 0;
while (p < tag + tagLen && *p != quote && len < maxLen - 1) {
buf[len++] = *p++;
}
buf[len] = '\0';
return len;
}
return -1;
}
// Parse an integer from a string (no sign, simple decimal).
inline int svg_parse_int(const char* s) {
int v = 0;
while (*s >= '0' && *s <= '9') {
v = v * 10 + (*s - '0');
++s;
}
return v;
}
// Parse a hex color like "#5c616c" into a Color.
inline Color svg_parse_hex_color(const char* s) {
if (*s == '#') ++s;
auto hexval = [](char c) -> uint8_t {
if (c >= '0' && c <= '9') return c - '0';
if (c >= 'a' && c <= 'f') return 10 + (c - 'a');
if (c >= 'A' && c <= 'F') return 10 + (c - 'A');
return 0;
};
uint8_t r = (hexval(s[0]) << 4) | hexval(s[1]);
uint8_t g = (hexval(s[2]) << 4) | hexval(s[3]);
uint8_t b = (hexval(s[4]) << 4) | hexval(s[5]);
return Color::from_rgb(r, g, b);
}
// ---------------------------------------------------------------------------
// Edge list builder
// ---------------------------------------------------------------------------
struct SvgEdgeList {
SvgEdge* edges;
int count;
int capacity;
void init(int cap) {
edges = (SvgEdge*)zenith::alloc(cap * sizeof(SvgEdge));
count = 0;
capacity = cap;
}
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;
edges[count++] = {x0, y0, x1, y1};
}
};
// ---------------------------------------------------------------------------
// Bezier flattening (fixed-point)
// ---------------------------------------------------------------------------
// Cubic bezier: add line segments approximating B(t) for t in [0,1]
inline void svg_flatten_cubic(SvgEdgeList& el,
fixed_t x0, fixed_t y0,
fixed_t x1, fixed_t y1,
fixed_t x2, fixed_t y2,
fixed_t x3, fixed_t y3) {
constexpr int N = SVG_BEZIER_STEPS;
fixed_t px = x0, py = y0;
for (int i = 1; i <= N; ++i) {
// t = i/N in 16.16: (i << 16) / N
fixed_t t = (int32_t)(((int64_t)i << 16) / N);
fixed_t omt = int_to_fixed(1) - t; // 1 - t
// (1-t)^2 and t^2
fixed_t omt2 = fixed_mul(omt, omt);
fixed_t t2 = fixed_mul(t, t);
// (1-t)^3 and t^3
fixed_t omt3 = fixed_mul(omt2, omt);
fixed_t t3 = fixed_mul(t2, t);
// 3*(1-t)^2*t and 3*(1-t)*t^2
fixed_t c1 = fixed_mul(omt2, t) * 3;
fixed_t c2 = fixed_mul(omt, t2) * 3;
fixed_t nx = fixed_mul(omt3, x0) + fixed_mul(c1, x1) + fixed_mul(c2, x2) + fixed_mul(t3, x3);
fixed_t ny = fixed_mul(omt3, y0) + fixed_mul(c1, y1) + fixed_mul(c2, y2) + fixed_mul(t3, y3);
el.add(px, py, nx, ny);
px = nx;
py = ny;
}
}
// Quadratic bezier: add line segments approximating B(t) for t in [0,1]
inline void svg_flatten_quad(SvgEdgeList& el,
fixed_t x0, fixed_t y0,
fixed_t x1, fixed_t y1,
fixed_t x2, fixed_t y2) {
constexpr int N = SVG_BEZIER_STEPS;
fixed_t px = x0, py = y0;
for (int i = 1; i <= N; ++i) {
fixed_t t = (int32_t)(((int64_t)i << 16) / N);
fixed_t omt = int_to_fixed(1) - t;
// (1-t)^2*P0 + 2*(1-t)*t*P1 + t^2*P2
fixed_t omt2 = fixed_mul(omt, omt);
fixed_t t2 = fixed_mul(t, t);
fixed_t c1 = fixed_mul(omt, t) * 2;
fixed_t nx = fixed_mul(omt2, x0) + fixed_mul(c1, x1) + fixed_mul(t2, x2);
fixed_t ny = fixed_mul(omt2, y0) + fixed_mul(c1, y1) + fixed_mul(t2, y2);
el.add(px, py, nx, ny);
px = nx;
py = ny;
}
}
// ---------------------------------------------------------------------------
// Circle to edges: approximate a circle as N line segments
// ---------------------------------------------------------------------------
inline void svg_circle_edges(SvgEdgeList& el, fixed_t cx, fixed_t cy, fixed_t r) {
// Approximate circle with 16 segments using a precomputed sin/cos table
// for angles 0, 22.5, 45, ... 337.5 degrees.
// sin/cos in 16.16 fixed-point for 16 evenly-spaced angles:
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 = cx + fixed_mul(r, cos16[0]);
fixed_t py = cy + fixed_mul(r, sin16[0]);
for (int i = 1; i <= 16; ++i) {
int idx = i & 15;
fixed_t nx = cx + fixed_mul(r, cos16[idx]);
fixed_t ny = cy + fixed_mul(r, sin16[idx]);
el.add(px, py, nx, ny);
px = nx;
py = ny;
}
}
// ---------------------------------------------------------------------------
// Rounded rect to edges
// ---------------------------------------------------------------------------
inline void svg_rect_edges(SvgEdgeList& el, fixed_t x, fixed_t y, fixed_t w, fixed_t h,
fixed_t rx, fixed_t ry) {
if (rx <= 0 && ry <= 0) {
// Simple rectangle: 4 edges
fixed_t x2 = x + w;
fixed_t y2 = y + h;
el.add(x, y, x2, y); // top
el.add(x2, y, x2, y2); // right
el.add(x2, y2, x, y2); // bottom
el.add(x, y2, x, y); // left
return;
}
// Clamp radii
fixed_t half_w = w >> 1;
fixed_t half_h = h >> 1;
if (rx > half_w) rx = half_w;
if (ry > half_h) ry = half_h;
// Quarter-circle corner with 4 segments per corner.
// cos/sin for 0, 22.5, 45, 67.5, 90 degrees (5 points, 4 segments):
static const fixed_t qcos[5] = { 65536, 60547, 46341, 25080, 0 };
static const fixed_t qsin[5] = { 0, 25080, 46341, 60547, 65536 };
// Corners: top-right, bottom-right, bottom-left, top-left
// Each corner has a center and a quadrant direction for cos/sin application
struct Corner { fixed_t cx, cy; int sx, sy; };
Corner corners[4] = {
{ x + w - rx, y + ry, 1, -1 }, // top-right
{ x + w - rx, y + h - ry, 1, 1 }, // bottom-right
{ x + rx, y + h - ry, -1, 1 }, // bottom-left
{ x + rx, y + ry, -1, -1 }, // top-left
};
for (int c = 0; c < 4; ++c) {
Corner& cn = corners[c];
fixed_t px = cn.cx + fixed_mul(rx, qcos[0]) * cn.sx;
fixed_t py = cn.cy + fixed_mul(ry, qsin[0]) * cn.sy;
for (int i = 1; i <= 4; ++i) {
fixed_t nx = cn.cx + fixed_mul(rx, qcos[i]) * cn.sx;
fixed_t ny = cn.cy + fixed_mul(ry, qsin[i]) * cn.sy;
el.add(px, py, nx, ny);
px = nx;
py = ny;
}
}
// Straight edges between corners
// Top edge: top-left corner end -> top-right corner start
el.add(x + rx, y, x + w - rx, y);
// Right edge: top-right corner end -> bottom-right corner start
el.add(x + w, y + ry, x + w, y + h - ry);
// Bottom edge: bottom-right corner end -> bottom-left corner start
el.add(x + w - rx, y + h, x + rx, y + h);
// Left edge: bottom-left corner end -> top-left corner start
el.add(x, y + h - ry, x, y + ry);
}
// ---------------------------------------------------------------------------
// Path command parser: tokenize the 'd' attribute
// ---------------------------------------------------------------------------
struct SvgPathParser {
const char* data;
int len;
int pos;
void init(const char* d, int l) { data = d; len = l; pos = 0; }
void skip_separators() {
while (pos < len && svg_char_is_sep(data[pos])) ++pos;
}
bool has_more() const { return pos < len; }
// Peek at what's next: command letter, number, or end
bool next_is_number() {
skip_separators();
if (pos >= len) return false;
return svg_char_is_num_start(data[pos]);
}
char read_command() {
skip_separators();
if (pos >= len) return '\0';
if (svg_char_is_cmd(data[pos]) && data[pos] != 'e' && data[pos] != 'E') {
return data[pos++];
}
return '\0';
}
fixed_t read_number() {
skip_separators();
if (pos >= len) return 0;
fixed_t val = 0;
int consumed = svg_parse_fixed(data + pos, &val);
pos += consumed;
return val;
}
};
// ---------------------------------------------------------------------------
// 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) {
SvgPathParser pp;
pp.init(d, dLen);
fixed_t cur_x = 0, cur_y = 0; // current point
fixed_t start_x = 0, start_y = 0; // subpath start
fixed_t last_cx = 0, last_cy = 0; // last control point (for S/T)
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);
};
while (pp.has_more()) {
char cmd = '\0';
// Try reading a command letter
pp.skip_separators();
if (pp.pos < pp.len && svg_char_is_cmd(pp.data[pp.pos]) &&
pp.data[pp.pos] != 'e' && pp.data[pp.pos] != 'E') {
cmd = pp.data[pp.pos++];
} else if (pp.next_is_number()) {
// Implicit repeat of last command
// After M, implicit repeat is L; after m, implicit repeat is l
if (last_cmd == 'M') cmd = 'L';
else if (last_cmd == 'm') cmd = 'l';
else cmd = last_cmd;
} else {
// Skip unknown character
if (pp.pos < pp.len) pp.pos++;
continue;
}
if (cmd == '\0') break;
switch (cmd) {
case 'M': {
fixed_t x = pp.read_number();
fixed_t y = pp.read_number();
cur_x = x; cur_y = y;
start_x = x; start_y = y;
last_cmd = 'M';
break;
}
case 'm': {
fixed_t dx = pp.read_number();
fixed_t dy = pp.read_number();
cur_x += dx; cur_y += dy;
start_x = cur_x; start_y = cur_y;
last_cmd = 'm';
break;
}
case 'L': {
fixed_t x = pp.read_number();
fixed_t y = pp.read_number();
fixed_t sx0, sy0, sx1, sy1;
scale_pt(cur_x, cur_y, &sx0, &sy0);
scale_pt(x, y, &sx1, &sy1);
el.add(sx0, sy0, sx1, sy1);
cur_x = x; cur_y = y;
last_cmd = 'L';
break;
}
case 'l': {
fixed_t dx = pp.read_number();
fixed_t dy = pp.read_number();
fixed_t nx = cur_x + dx, ny = cur_y + dy;
fixed_t sx0, sy0, sx1, sy1;
scale_pt(cur_x, cur_y, &sx0, &sy0);
scale_pt(nx, ny, &sx1, &sy1);
el.add(sx0, sy0, sx1, sy1);
cur_x = nx; cur_y = ny;
last_cmd = 'l';
break;
}
case 'H': {
fixed_t x = pp.read_number();
fixed_t sx0, sy0, sx1, sy1;
scale_pt(cur_x, cur_y, &sx0, &sy0);
scale_pt(x, cur_y, &sx1, &sy1);
el.add(sx0, sy0, sx1, sy1);
cur_x = x;
last_cmd = 'H';
break;
}
case 'h': {
fixed_t dx = pp.read_number();
fixed_t nx = cur_x + dx;
fixed_t sx0, sy0, sx1, sy1;
scale_pt(cur_x, cur_y, &sx0, &sy0);
scale_pt(nx, cur_y, &sx1, &sy1);
el.add(sx0, sy0, sx1, sy1);
cur_x = nx;
last_cmd = 'h';
break;
}
case 'V': {
fixed_t y = pp.read_number();
fixed_t sx0, sy0, sx1, sy1;
scale_pt(cur_x, cur_y, &sx0, &sy0);
scale_pt(cur_x, y, &sx1, &sy1);
el.add(sx0, sy0, sx1, sy1);
cur_y = y;
last_cmd = 'V';
break;
}
case 'v': {
fixed_t dy = pp.read_number();
fixed_t ny = cur_y + dy;
fixed_t sx0, sy0, sx1, sy1;
scale_pt(cur_x, cur_y, &sx0, &sy0);
scale_pt(cur_x, ny, &sx1, &sy1);
el.add(sx0, sy0, sx1, sy1);
cur_y = ny;
last_cmd = 'v';
break;
}
case 'C': {
fixed_t x1 = pp.read_number(), y1 = pp.read_number();
fixed_t x2 = pp.read_number(), y2 = pp.read_number();
fixed_t x3 = pp.read_number(), y3 = pp.read_number();
fixed_t sx0, sy0, sx1, sy1, sx2, sy2, sx3, sy3;
scale_pt(cur_x, cur_y, &sx0, &sy0);
scale_pt(x1, y1, &sx1, &sy1);
scale_pt(x2, y2, &sx2, &sy2);
scale_pt(x3, y3, &sx3, &sy3);
svg_flatten_cubic(el, sx0, sy0, sx1, sy1, sx2, sy2, sx3, sy3);
last_cx = x2; last_cy = y2;
cur_x = x3; cur_y = y3;
last_cmd = 'C';
break;
}
case 'c': {
fixed_t dx1 = pp.read_number(), dy1 = pp.read_number();
fixed_t dx2 = pp.read_number(), dy2 = pp.read_number();
fixed_t dx3 = pp.read_number(), dy3 = pp.read_number();
fixed_t x1 = cur_x + dx1, y1 = cur_y + dy1;
fixed_t x2 = cur_x + dx2, y2 = cur_y + dy2;
fixed_t x3 = cur_x + dx3, y3 = cur_y + dy3;
fixed_t sx0, sy0, sx1, sy1, sx2, sy2, sx3, sy3;
scale_pt(cur_x, cur_y, &sx0, &sy0);
scale_pt(x1, y1, &sx1, &sy1);
scale_pt(x2, y2, &sx2, &sy2);
scale_pt(x3, y3, &sx3, &sy3);
svg_flatten_cubic(el, sx0, sy0, sx1, sy1, sx2, sy2, sx3, sy3);
last_cx = x2; last_cy = y2;
cur_x = x3; cur_y = y3;
last_cmd = 'c';
break;
}
case 'S': {
// Smooth cubic: reflect last control point
fixed_t rcx = cur_x * 2 - last_cx;
fixed_t rcy = cur_y * 2 - last_cy;
if (last_cmd != 'C' && last_cmd != 'c' && last_cmd != 'S' && last_cmd != 's') {
rcx = cur_x; rcy = cur_y;
}
fixed_t x2 = pp.read_number(), y2 = pp.read_number();
fixed_t x3 = pp.read_number(), y3 = pp.read_number();
fixed_t sx0, sy0, sx1, sy1, sx2, sy2, sx3, sy3;
scale_pt(cur_x, cur_y, &sx0, &sy0);
scale_pt(rcx, rcy, &sx1, &sy1);
scale_pt(x2, y2, &sx2, &sy2);
scale_pt(x3, y3, &sx3, &sy3);
svg_flatten_cubic(el, sx0, sy0, sx1, sy1, sx2, sy2, sx3, sy3);
last_cx = x2; last_cy = y2;
cur_x = x3; cur_y = y3;
last_cmd = 'S';
break;
}
case 's': {
fixed_t rcx = cur_x * 2 - last_cx;
fixed_t rcy = cur_y * 2 - last_cy;
if (last_cmd != 'C' && last_cmd != 'c' && last_cmd != 'S' && last_cmd != 's') {
rcx = cur_x; rcy = cur_y;
}
fixed_t dx2 = pp.read_number(), dy2 = pp.read_number();
fixed_t dx3 = pp.read_number(), dy3 = pp.read_number();
fixed_t x2 = cur_x + dx2, y2 = cur_y + dy2;
fixed_t x3 = cur_x + dx3, y3 = cur_y + dy3;
fixed_t sx0, sy0, sx1, sy1, sx2, sy2, sx3, sy3;
scale_pt(cur_x, cur_y, &sx0, &sy0);
scale_pt(rcx, rcy, &sx1, &sy1);
scale_pt(x2, y2, &sx2, &sy2);
scale_pt(x3, y3, &sx3, &sy3);
svg_flatten_cubic(el, sx0, sy0, sx1, sy1, sx2, sy2, sx3, sy3);
last_cx = x2; last_cy = y2;
cur_x = x3; cur_y = y3;
last_cmd = 's';
break;
}
case 'Q': {
fixed_t x1 = pp.read_number(), y1 = pp.read_number();
fixed_t x2 = pp.read_number(), y2 = pp.read_number();
fixed_t sx0, sy0, sx1, sy1, sx2, sy2;
scale_pt(cur_x, cur_y, &sx0, &sy0);
scale_pt(x1, y1, &sx1, &sy1);
scale_pt(x2, y2, &sx2, &sy2);
svg_flatten_quad(el, sx0, sy0, sx1, sy1, sx2, sy2);
last_cx = x1; last_cy = y1;
cur_x = x2; cur_y = y2;
last_cmd = 'Q';
break;
}
case 'q': {
fixed_t dx1 = pp.read_number(), dy1 = pp.read_number();
fixed_t dx2 = pp.read_number(), dy2 = pp.read_number();
fixed_t x1 = cur_x + dx1, y1 = cur_y + dy1;
fixed_t x2 = cur_x + dx2, y2 = cur_y + dy2;
fixed_t sx0, sy0, sx1, sy1, sx2, sy2;
scale_pt(cur_x, cur_y, &sx0, &sy0);
scale_pt(x1, y1, &sx1, &sy1);
scale_pt(x2, y2, &sx2, &sy2);
svg_flatten_quad(el, sx0, sy0, sx1, sy1, sx2, sy2);
last_cx = x1; last_cy = y1;
cur_x = x2; cur_y = y2;
last_cmd = 'q';
break;
}
case 'A': case 'a': {
// Arc command: consume parameters but approximate as a line
// (arcs are rare in these icons)
fixed_t rx = pp.read_number();
fixed_t ry = pp.read_number();
pp.read_number(); // x-rotation
pp.read_number(); // large-arc-flag
pp.read_number(); // sweep-flag
fixed_t x = pp.read_number();
fixed_t y = pp.read_number();
if (cmd == 'a') { x += cur_x; y += cur_y; }
fixed_t sx0, sy0, sx1, sy1;
scale_pt(cur_x, cur_y, &sx0, &sy0);
scale_pt(x, y, &sx1, &sy1);
el.add(sx0, sy0, sx1, sy1);
cur_x = x; cur_y = y;
last_cmd = cmd;
(void)rx; (void)ry;
break;
}
case 'Z': case 'z': {
if (cur_x != start_x || cur_y != start_y) {
fixed_t sx0, sy0, sx1, sy1;
scale_pt(cur_x, cur_y, &sx0, &sy0);
scale_pt(start_x, start_y, &sx1, &sy1);
el.add(sx0, sy0, sx1, sy1);
}
cur_x = start_x; cur_y = start_y;
last_cmd = 'Z';
break;
}
default:
// Unknown command, skip
break;
}
}
}
// ---------------------------------------------------------------------------
// Scanline rasterizer (even-odd fill rule)
// ---------------------------------------------------------------------------
inline void svg_rasterize(const SvgEdgeList& el, uint32_t* pixels, int w, int h, uint32_t fill) {
// Temporary array for x-intersections on each scanline
// Allocate enough for all edges (each edge can intersect at most once per scanline)
int maxIsect = el.count + 16;
fixed_t* isect = (fixed_t*)zenith::alloc(maxIsect * sizeof(fixed_t));
for (int y = 0; y < h; ++y) {
// Scanline center in fixed-point
fixed_t scanY = int_to_fixed(y) + (1 << 15); // y + 0.5
int isectCount = 0;
// Find intersections with all edges
for (int i = 0; i < el.count; ++i) {
const SvgEdge& e = el.edges[i];
fixed_t ey0 = e.y0, ey1 = e.y1;
// Ensure ey0 <= ey1 for the range check
fixed_t emin = ey0 < ey1 ? ey0 : ey1;
fixed_t emax = ey0 > ey1 ? ey0 : ey1;
// Does this edge cross scanY?
if (scanY < emin || scanY >= emax) continue;
// Compute x at intersection: x = x0 + (scanY - y0) * (x1 - x0) / (y1 - y0)
fixed_t dy = ey1 - ey0;
if (dy == 0) continue; // horizontal, skip
fixed_t dx = e.x1 - e.x0;
fixed_t t_num = scanY - ey0;
// x_intersect = x0 + dx * t_num / dy
fixed_t x_int = e.x0 + (int32_t)(((int64_t)dx * t_num) / dy);
if (isectCount < maxIsect)
isect[isectCount++] = x_int;
}
// Sort intersections (simple insertion sort -- usually very few)
for (int i = 1; i < isectCount; ++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;
}
// Fill between pairs (even-odd rule)
for (int i = 0; i + 1 < isectCount; i += 2) {
int x0 = fixed_to_int(isect[i]);
int x1 = fixed_to_int(isect[i + 1]);
// Clamp to pixel bounds
if (x0 < 0) x0 = 0;
if (x1 > w) x1 = w;
for (int x = x0; x < x1; ++x) {
pixels[y * w + x] = fill;
}
}
}
zenith::free(isect);
}
// ---------------------------------------------------------------------------
// 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;
icon.width = target_w;
icon.height = target_h;
icon.pixels = (uint32_t*)zenith::alloc(target_w * target_h * sizeof(uint32_t));
// Clear to transparent
svg_memset(icon.pixels, 0, target_w * target_h * sizeof(uint32_t));
uint32_t fill_px = fill_color.to_pixel();
// Parse SVG dimensions: width and height
int svg_w = 16, svg_h = 16;
fixed_t vb_x = 0, vb_y = 0, vb_w = 0, vb_h = 0;
bool has_viewbox = false;
// Find <svg tag
const char* svg_tag = svg_strstr(svg_data, svg_len, "<svg");
if (svg_tag) {
// Find the end of the <svg ...> tag
int tag_offset = (int)(svg_tag - svg_data);
int tag_end = tag_offset;
while (tag_end < svg_len && svg_data[tag_end] != '>') ++tag_end;
int tag_len = tag_end - tag_offset + 1;
char attr_buf[64];
// width
if (svg_get_attr(svg_tag, tag_len, " width", attr_buf, sizeof(attr_buf)) > 0) {
svg_w = svg_parse_int(attr_buf);
}
// height
if (svg_get_attr(svg_tag, tag_len, " height", attr_buf, sizeof(attr_buf)) > 0) {
svg_h = svg_parse_int(attr_buf);
}
// viewBox
if (svg_get_attr(svg_tag, tag_len, " viewBox", attr_buf, sizeof(attr_buf)) > 0) {
has_viewbox = true;
const char* vp = attr_buf;
int c;
c = svg_parse_fixed(vp, &vb_x); vp += c; while (*vp && svg_char_is_sep(*vp)) ++vp;
c = svg_parse_fixed(vp, &vb_y); vp += c; while (*vp && svg_char_is_sep(*vp)) ++vp;
c = svg_parse_fixed(vp, &vb_w); vp += c; while (*vp && svg_char_is_sep(*vp)) ++vp;
svg_parse_fixed(vp, &vb_h);
}
}
if (!has_viewbox) {
vb_x = 0;
vb_y = 0;
vb_w = int_to_fixed(svg_w);
vb_h = int_to_fixed(svg_h);
}
// 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);
// Edge list
SvgEdgeList el;
el.init(SVG_MAX_EDGES);
// Scan for <path, <circle, <rect elements
const char* p = svg_data;
const char* end = svg_data + svg_len;
while (p < end) {
// Find next '<'
while (p < end && *p != '<') ++p;
if (p >= end) break;
int remaining = (int)(end - p);
// Check for <path
if (remaining > 5 && svg_strncmp(p, "<path", 5) && (svg_char_is_ws(p[5]) || p[5] == '/')) {
// Find end of this element
const char* elem_start = p;
const char* elem_end = p;
while (elem_end < end && *elem_end != '>') ++elem_end;
if (elem_end < end) ++elem_end; // include '>'
int elem_len = (int)(elem_end - elem_start);
// Extract d attribute
char d_buf[SVG_MAX_PATH_LEN];
int d_len = svg_get_attr(elem_start, elem_len, " d", d_buf, SVG_MAX_PATH_LEN);
if (d_len > 0) {
svg_path_to_edges(el, d_buf, d_len, scale_x, scale_y, vb_x, vb_y);
}
p = elem_end;
continue;
}
// Check for <circle
if (remaining > 7 && svg_strncmp(p, "<circle", 7) && (svg_char_is_ws(p[7]) || p[7] == '/')) {
const char* elem_start = p;
const char* elem_end = p;
while (elem_end < end && *elem_end != '>') ++elem_end;
if (elem_end < end) ++elem_end;
int elem_len = (int)(elem_end - elem_start);
char attr_buf[32];
fixed_t cx = 0, cy = 0, r = 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);
// Scale to target coordinates
fixed_t scx = fixed_mul(cx - vb_x, scale_x);
fixed_t scy = fixed_mul(cy - vb_y, scale_y);
fixed_t srx = fixed_mul(r, scale_x);
fixed_t sry = fixed_mul(r, scale_y);
// Use average of scaled radii
fixed_t sr = (srx + sry) >> 1;
svg_circle_edges(el, scx, scy, sr);
p = elem_end;
continue;
}
// Check for <rect
if (remaining > 5 && svg_strncmp(p, "<rect", 5) && (svg_char_is_ws(p[5]) || p[5] == '/')) {
const char* elem_start = p;
const char* elem_end = p;
while (elem_end < end && *elem_end != '>') ++elem_end;
if (elem_end < end) ++elem_end;
int elem_len = (int)(elem_end - elem_start);
char attr_buf[32];
fixed_t rx_val = 0, ry_val = 0, rw = 0, rh = 0, rrx = 0, rry = 0;
if (svg_get_attr(elem_start, elem_len, " x", attr_buf, sizeof(attr_buf)) > 0)
svg_parse_fixed(attr_buf, &rx_val);
if (svg_get_attr(elem_start, elem_len, " y", attr_buf, sizeof(attr_buf)) > 0)
svg_parse_fixed(attr_buf, &ry_val);
if (svg_get_attr(elem_start, elem_len, " width", attr_buf, sizeof(attr_buf)) > 0)
svg_parse_fixed(attr_buf, &rw);
if (svg_get_attr(elem_start, elem_len, " height", attr_buf, sizeof(attr_buf)) > 0)
svg_parse_fixed(attr_buf, &rh);
if (svg_get_attr(elem_start, elem_len, " rx", attr_buf, sizeof(attr_buf)) > 0)
svg_parse_fixed(attr_buf, &rrx);
if (svg_get_attr(elem_start, elem_len, " ry", attr_buf, sizeof(attr_buf)) > 0)
svg_parse_fixed(attr_buf, &rry);
// Scale all to target pixel space
fixed_t sx = fixed_mul(rx_val - vb_x, scale_x);
fixed_t sy = fixed_mul(ry_val - vb_y, scale_y);
fixed_t sw = fixed_mul(rw, scale_x);
fixed_t sh = fixed_mul(rh, scale_y);
fixed_t srx = fixed_mul(rrx, scale_x);
fixed_t sry = fixed_mul(rry, scale_y);
svg_rect_edges(el, sx, sy, sw, sh, srx, sry);
p = elem_end;
continue;
}
++p;
}
// Rasterize all accumulated edges
if (el.count > 0) {
svg_rasterize(el, icon.pixels, target_w, target_h, fill_px);
}
zenith::free(el.edges);
return icon;
}
// ---------------------------------------------------------------------------
// Load SVG from VFS and render
// ---------------------------------------------------------------------------
inline SvgIcon svg_load(const char* vfs_path, int target_w, int target_h, Color fill_color) {
int fd = zenith::open(vfs_path);
if (fd < 0) {
return {nullptr, 0, 0};
}
uint64_t size = zenith::getsize(fd);
if (size == 0 || size > SVG_MAX_FILE_SIZE) {
zenith::close(fd);
return {nullptr, 0, 0};
}
char* buf = (char*)zenith::alloc(size + 1);
zenith::read(fd, (uint8_t*)buf, 0, size);
zenith::close(fd);
buf[size] = '\0';
SvgIcon icon = svg_render(buf, (int)size, target_w, target_h, fill_color);
zenith::free(buf);
return icon;
}
// ---------------------------------------------------------------------------
// Free icon pixel data
// ---------------------------------------------------------------------------
inline void svg_free(SvgIcon& icon) {
if (icon.pixels) zenith::free(icon.pixels);
icon.pixels = nullptr;
icon.width = 0;
icon.height = 0;
}
} // namespace gui