feat: expand and fix issues in SVG renderer
This commit is contained in:
+842
-151
File diff suppressed because it is too large
Load Diff
@@ -49,10 +49,56 @@ bool svgdoc_render(SvgDoc* doc, float target_scale, int max_edge) {
|
|||||||
if (new_w < 1) new_w = 1;
|
if (new_w < 1) new_w = 1;
|
||||||
if (new_h < 1) new_h = 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 * 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,
|
(const char*)doc->source, doc->source_len,
|
||||||
new_w, new_h, gui::Color::from_rgb(0, 0, 0));
|
new_w, new_h, gui::Color::from_rgb(0, 0, 0));
|
||||||
if (!icon.pixels) return false;
|
if (!icon.pixels) return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (doc->pixels) montauk::mfree(doc->pixels);
|
if (doc->pixels) montauk::mfree(doc->pixels);
|
||||||
doc->pixels = icon.pixels;
|
doc->pixels = icon.pixels;
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
+8
-3
@@ -2,17 +2,22 @@ CXX ?= g++
|
|||||||
CXXFLAGS ?= -std=c++20 -O2 -Wall -Wextra
|
CXXFLAGS ?= -std=c++20 -O2 -Wall -Wextra
|
||||||
|
|
||||||
HTTP_TEST := http_test
|
HTTP_TEST := http_test
|
||||||
|
SVG_TEST := svg_renderer_test
|
||||||
|
|
||||||
.PHONY: all check clean
|
.PHONY: all check clean
|
||||||
|
|
||||||
all: $(HTTP_TEST)
|
all: $(HTTP_TEST) $(SVG_TEST)
|
||||||
|
|
||||||
$(HTTP_TEST): http_test.cpp
|
$(HTTP_TEST): http_test.cpp
|
||||||
$(CXX) $(CXXFLAGS) -I../programs/include \
|
$(CXX) $(CXXFLAGS) -I../programs/include \
|
||||||
-I../programs/lib/bearssl/inc $< -o $@
|
-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)
|
./$(HTTP_TEST)
|
||||||
|
./$(SVG_TEST)
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
rm -f $(HTTP_TEST)
|
rm -f $(HTTP_TEST) $(SVG_TEST)
|
||||||
|
|||||||
@@ -0,0 +1,223 @@
|
|||||||
|
#include <cstdlib>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <fstream>
|
||||||
|
#include <iostream>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include <montauk/heap.h>
|
||||||
|
#include <gui/svg.hpp>
|
||||||
|
|
||||||
|
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[] = "<svg width='72pt' height='25.4mm' viewBox='0 0 72 72'/>";
|
||||||
|
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[] = "<svg width='100%' height='100%' viewBox='0 0 320 180'/>";
|
||||||
|
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[] =
|
||||||
|
"<svg viewBox='0 0 20 20'>"
|
||||||
|
"<g transform='translate(4 3)'><g transform='scale(2)'>"
|
||||||
|
"<rect x='1' y='1' width='3' height='2' fill='#ff0000'/>"
|
||||||
|
"</g></g></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[] =
|
||||||
|
"<svg viewBox='0 0 20 20'>"
|
||||||
|
"<g transform='matrix(2 0 0 2 2 4)'>"
|
||||||
|
"<path d='M1 2 H7' fill='none' stroke='#ffffff' stroke-width='2'/>"
|
||||||
|
"</g></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[] =
|
||||||
|
"<svg viewBox='0 0 20 20'><path d='M2 10H10H18' fill='none' "
|
||||||
|
"stroke='#000000' stroke-width='4' opacity='.3'/></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[] =
|
||||||
|
"<svg viewBox='0 0 20 10'><defs>"
|
||||||
|
"<linearGradient id='paint' x1='0' y1='0' x2='20' y2='0' "
|
||||||
|
"gradientUnits='userSpaceOnUse'>"
|
||||||
|
"<stop offset='0' stop-color='#ff0000'/><stop offset='1' stop-color='#0000ff'/>"
|
||||||
|
"</linearGradient></defs><rect width='20' height='10' fill='url(#paint)'/></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[] =
|
||||||
|
"<svg viewBox='0 0 20 20'>"
|
||||||
|
"<g fill='none'><ellipse cx='10' cy='10' rx='6' ry='3' "
|
||||||
|
"fill='#00ff00' fill-opacity='.5'/></g>"
|
||||||
|
"</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 = "<svg viewBox='0 0 20 20'><path d='";
|
||||||
|
for (int i = 0; i < 700; ++i) svg += "M0 0H1V1H0Z ";
|
||||||
|
svg += "M14 14H19V19H14Z' fill='#ffffff'/></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<char>(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<char>(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;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user