973 lines
34 KiB
C++
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
|