From 6ac9ae01b091217565f8b1a426d3d12c32ff7261 Mon Sep 17 00:00:00 2001 From: Daniel Hammer Date: Mon, 18 May 2026 20:07:37 +0200 Subject: [PATCH] feat: SVG support in Image viewer app --- programs/include/gui/svg.hpp | 40 +++++++++++++++ .../src/desktop/apps/filemanager/helpers.cpp | 4 +- programs/src/imageviewer/main.cpp | 50 +++++++++++++++++++ 3 files changed, 93 insertions(+), 1 deletion(-) diff --git a/programs/include/gui/svg.hpp b/programs/include/gui/svg.hpp index 8dde7bf..7f99a9d 100644 --- a/programs/include/gui/svg.hpp +++ b/programs/include/gui/svg.hpp @@ -1626,6 +1626,46 @@ inline SvgIcon svg_render(const char* svg_data, int svg_len, int target_w, int t // --------------------------------------------------------------------------- // Load SVG from VFS and render // --------------------------------------------------------------------------- +// Extract intrinsic dimensions from an SVG document. Prefers width/height +// attributes, falls back to viewBox, then to a 256x256 default. Used by the +// image viewer to render SVGs at their natural size rather than a fixed icon +// size. +inline void svg_get_natural_size(const char* svg_data, int svg_len, + int* out_w, int* out_h) { + int w = 0, h = 0; + const char* svg_tag = svg_strstr(svg_data, svg_len, " 0) + w = svg_parse_int(attr_buf); + if (svg_get_attr(svg_tag, tag_len, " height", attr_buf, sizeof(attr_buf)) > 0) + h = svg_parse_int(attr_buf); + + if ((w <= 0 || h <= 0) && + svg_get_attr(svg_tag, tag_len, " viewBox", attr_buf, sizeof(attr_buf)) > 0) { + fixed_t vbx, vby, vbw, vbh; + const char* vp = attr_buf; + int c; + c = svg_parse_fixed(vp, &vbx); vp += c; while (*vp && svg_char_is_sep(*vp)) ++vp; + c = svg_parse_fixed(vp, &vby); vp += c; while (*vp && svg_char_is_sep(*vp)) ++vp; + c = svg_parse_fixed(vp, &vbw); vp += c; while (*vp && svg_char_is_sep(*vp)) ++vp; + svg_parse_fixed(vp, &vbh); + (void)vbx; (void)vby; + if (w <= 0) w = fixed_to_int(vbw); + if (h <= 0) h = fixed_to_int(vbh); + } + } + if (w <= 0) w = 256; + if (h <= 0) h = 256; + if (out_w) *out_w = w; + if (out_h) *out_h = h; +} + inline SvgIcon svg_load(const char* vfs_path, int target_w, int target_h, Color fill_color) { int fd = montauk::open(vfs_path); if (fd < 0) { diff --git a/programs/src/desktop/apps/filemanager/helpers.cpp b/programs/src/desktop/apps/filemanager/helpers.cpp index 0722513..f3eca1d 100644 --- a/programs/src/desktop/apps/filemanager/helpers.cpp +++ b/programs/src/desktop/apps/filemanager/helpers.cpp @@ -45,7 +45,9 @@ bool str_ends_with(const char* s, const char* suffix) { } bool is_image_file(const char* name) { - return str_ends_with(name, ".jpg") || str_ends_with(name, ".jpeg"); + return str_ends_with(name, ".jpg") + || str_ends_with(name, ".jpeg") + || str_ends_with(name, ".svg"); } bool is_font_file(const char* name) { diff --git a/programs/src/imageviewer/main.cpp b/programs/src/imageviewer/main.cpp index daa951b..58f065e 100644 --- a/programs/src/imageviewer/main.cpp +++ b/programs/src/imageviewer/main.cpp @@ -10,6 +10,7 @@ #include #include #include +#include extern "C" { #include @@ -99,6 +100,20 @@ static const char* basename(const char* path) { return last; } +static bool path_has_ext_ci(const char* path, const char* ext) { + int pl = 0; while (path[pl]) pl++; + int el = 0; while (ext[el]) el++; + if (pl < el) return false; + for (int i = 0; i < el; i++) { + char a = path[pl - el + i]; + char b = ext[i]; + if (a >= 'A' && a <= 'Z') a = (char)(a - 'A' + 'a'); + if (b >= 'A' && b <= 'Z') b = (char)(b - 'A' + 'a'); + if (a != b) return false; + } + return true; +} + // ============================================================================ // Image loading // ============================================================================ @@ -135,6 +150,41 @@ static bool load_image(const char* path) { return false; } + // SVG: vector path through our own rasterizer. stb_image can't decode SVG. + if (path_has_ext_ci(path, ".svg")) { + int nat_w = 0, nat_h = 0; + svg_get_natural_size((const char*)filedata, bytes_read, &nat_w, &nat_h); + + // Cap longest edge to keep memory bounded (4 bytes/pixel). + constexpr int MAX_EDGE = 2048; + int max_edge = nat_w > nat_h ? nat_w : nat_h; + if (max_edge > MAX_EDGE) { + nat_w = (int)((int64_t)nat_w * MAX_EDGE / max_edge); + nat_h = (int)((int64_t)nat_h * MAX_EDGE / max_edge); + } + if (nat_w < 1) nat_w = 1; + if (nat_h < 1) nat_h = 1; + + // fill_color is the currentColor substitute. Real SVG files mostly + // specify their own fills, so this only matters for paths that use + // currentColor; default to black for sensible fallback. + SvgIcon icon = svg_render((const char*)filedata, bytes_read, + nat_w, nat_h, Color::from_rgb(0, 0, 0)); + montauk::mfree(filedata); + + if (!icon.pixels) { + montauk::strcpy(g_status, "Error: SVG render failed"); + return false; + } + + g_image = icon.pixels; + g_img_w = icon.width; + g_img_h = icon.height; + g_has_alpha = true; + snprintf(g_status, 256, "%s %dx%d SVG", basename(path), g_img_w, g_img_h); + return true; + } + int w, h, channels; unsigned char* rgba = stbi_load_from_memory(filedata, bytes_read, &w, &h, &channels, 4); montauk::mfree(filedata);