feat: vector zoom for SVG in image viewer app
This commit is contained in:
@@ -12,6 +12,8 @@
|
||||
#include <gui/truetype.hpp>
|
||||
#include <gui/svg.hpp>
|
||||
|
||||
#include "svg_doc.hpp"
|
||||
|
||||
extern "C" {
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
@@ -59,12 +61,24 @@ static constexpr float ZOOM_MAX = 16.0f;
|
||||
static int g_win_w = INIT_W;
|
||||
static int g_win_h = INIT_H;
|
||||
|
||||
// Image data (original, unscaled)
|
||||
static uint32_t* g_image = nullptr;
|
||||
static int g_img_w = 0;
|
||||
static int g_img_h = 0;
|
||||
// Image data.
|
||||
// g_img_w/h are the *natural* (display-reference) dimensions: layout and
|
||||
// "Actual size" use them. g_buf_w/h are the raster cache's actual dimensions
|
||||
// in pixels; for raster images they match g_img_w/h, for SVG they equal
|
||||
// natural * g_render_scale and update on every zoom-driven re-render.
|
||||
static uint32_t* g_image = nullptr;
|
||||
static int g_img_w = 0;
|
||||
static int g_img_h = 0;
|
||||
static int g_buf_w = 0;
|
||||
static int g_buf_h = 0;
|
||||
static float g_render_scale = 1.0f;
|
||||
static bool g_has_alpha = false;
|
||||
|
||||
// Vector mode: SvgDoc owns g_image + its raster, free via svgdoc_free not mfree.
|
||||
static imageviewer::SvgDoc g_svg_doc{};
|
||||
static bool g_is_svg = false;
|
||||
static constexpr int SVG_MAX_RENDER_EDGE = 4096;
|
||||
|
||||
// Zoom and pan
|
||||
static float g_zoom = 1.0f;
|
||||
static int g_pan_x = 0;
|
||||
@@ -118,8 +132,33 @@ static bool path_has_ext_ci(const char* path, const char* ext) {
|
||||
// Image loading
|
||||
// ============================================================================
|
||||
|
||||
static void release_image() {
|
||||
if (g_is_svg) {
|
||||
imageviewer::svgdoc_free(&g_svg_doc);
|
||||
} else if (g_image) {
|
||||
montauk::mfree(g_image);
|
||||
}
|
||||
g_image = nullptr;
|
||||
g_img_w = g_img_h = 0;
|
||||
g_buf_w = g_buf_h = 0;
|
||||
g_render_scale = 1.0f;
|
||||
g_is_svg = false;
|
||||
}
|
||||
|
||||
// Re-rasterize the SVG cache for the current zoom. Updates g_image,
|
||||
// g_buf_w/h, and g_render_scale to match. Cheap no-op when the cache is
|
||||
// already close to the requested scale. Safe to call when not in SVG mode.
|
||||
static void refresh_svg_raster() {
|
||||
if (!g_is_svg) return;
|
||||
if (!imageviewer::svgdoc_render(&g_svg_doc, g_zoom, SVG_MAX_RENDER_EDGE)) return;
|
||||
g_image = g_svg_doc.pixels;
|
||||
g_buf_w = g_svg_doc.render_w;
|
||||
g_buf_h = g_svg_doc.render_h;
|
||||
g_render_scale = g_svg_doc.render_scale;
|
||||
}
|
||||
|
||||
static bool load_image(const char* path) {
|
||||
if (g_image) { montauk::mfree(g_image); g_image = nullptr; }
|
||||
release_image();
|
||||
|
||||
int fd = montauk::open(path);
|
||||
if (fd < 0) {
|
||||
@@ -152,35 +191,26 @@ static bool load_image(const char* path) {
|
||||
|
||||
// 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);
|
||||
// Transfer ownership of filedata to the SvgDoc so it stays alive for
|
||||
// zoom-driven re-rasterization.
|
||||
if (!imageviewer::svgdoc_init(&g_svg_doc, filedata, bytes_read)) {
|
||||
montauk::mfree(filedata);
|
||||
montauk::strcpy(g_status, "Error: SVG init failed");
|
||||
return false;
|
||||
}
|
||||
if (nat_w < 1) nat_w = 1;
|
||||
if (nat_h < 1) nat_h = 1;
|
||||
g_is_svg = true;
|
||||
g_img_w = g_svg_doc.natural_w;
|
||||
g_img_h = g_svg_doc.natural_h;
|
||||
g_has_alpha = true;
|
||||
|
||||
// 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) {
|
||||
// Initial rasterization at zoom 1.0; later zoom changes call
|
||||
// refresh_svg_raster() to re-rasterize at the new scale.
|
||||
refresh_svg_raster();
|
||||
if (!g_image) {
|
||||
release_image();
|
||||
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;
|
||||
}
|
||||
@@ -216,6 +246,9 @@ static bool load_image(const char* path) {
|
||||
|
||||
g_img_w = w;
|
||||
g_img_h = h;
|
||||
g_buf_w = w;
|
||||
g_buf_h = h;
|
||||
g_render_scale = 1.0f;
|
||||
|
||||
snprintf(g_status, 256, "%s %dx%d %dch", basename(path), w, h, channels);
|
||||
return true;
|
||||
@@ -266,6 +299,7 @@ static void zoom_to(float new_zoom, int focus_x, int focus_y) {
|
||||
g_pan_x = focus_x - (int)((focus_x - g_pan_x) * ratio);
|
||||
g_pan_y = focus_y - (int)((focus_y - g_pan_y) * ratio);
|
||||
g_zoom = new_zoom;
|
||||
refresh_svg_raster();
|
||||
clamp_pan();
|
||||
}
|
||||
|
||||
@@ -297,6 +331,7 @@ static void zoom_fit() {
|
||||
float zy = (float)vp_h / g_img_h;
|
||||
g_zoom = zx < zy ? zx : zy;
|
||||
if (g_zoom > ZOOM_MAX) g_zoom = ZOOM_MAX;
|
||||
refresh_svg_raster();
|
||||
center_image();
|
||||
}
|
||||
|
||||
@@ -328,20 +363,26 @@ static void render(Canvas& canvas) {
|
||||
int draw_x1 = g_pan_x + scaled_w > g_win_w ? g_win_w : g_pan_x + scaled_w;
|
||||
int draw_y1 = g_pan_y + scaled_h > vp_y1 ? vp_y1 : g_pan_y + scaled_h;
|
||||
|
||||
float inv_zoom = 1.0f / g_zoom;
|
||||
// Buffer pixels per screen pixel. For raster images render_scale = 1
|
||||
// and this reduces to 1/g_zoom (the original behavior). For SVGs the
|
||||
// raster cache is regenerated to match g_zoom, so this is ~1.0 and the
|
||||
// viewer draws at native buffer resolution.
|
||||
float sample_step = g_render_scale / g_zoom;
|
||||
int buf_w = g_buf_w > 0 ? g_buf_w : g_img_w;
|
||||
int buf_h = g_buf_h > 0 ? g_buf_h : g_img_h;
|
||||
|
||||
for (int dy = draw_y0; dy < draw_y1; dy++) {
|
||||
int src_y = (int)((dy - g_pan_y) * inv_zoom);
|
||||
int src_y = (int)((dy - g_pan_y) * sample_step);
|
||||
if (src_y < 0) src_y = 0;
|
||||
if (src_y >= g_img_h) src_y = g_img_h - 1;
|
||||
if (src_y >= buf_h) src_y = buf_h - 1;
|
||||
|
||||
const uint32_t* src_row = &g_image[src_y * g_img_w];
|
||||
const uint32_t* src_row = &g_image[src_y * buf_w];
|
||||
uint32_t* dst_row = &canvas.pixels[dy * g_win_w];
|
||||
|
||||
for (int dx = draw_x0; dx < draw_x1; dx++) {
|
||||
int src_x = (int)((dx - g_pan_x) * inv_zoom);
|
||||
int src_x = (int)((dx - g_pan_x) * sample_step);
|
||||
if (src_x < 0) src_x = 0;
|
||||
if (src_x >= g_img_w) src_x = g_img_w - 1;
|
||||
if (src_x >= buf_w) src_x = buf_w - 1;
|
||||
|
||||
uint32_t spx = src_row[src_x];
|
||||
uint8_t a = (spx >> 24) & 0xFF;
|
||||
|
||||
Reference in New Issue
Block a user