feat: expand and fix issues in SVG renderer
This commit is contained in:
+8
-3
@@ -2,17 +2,22 @@ CXX ?= g++
|
||||
CXXFLAGS ?= -std=c++20 -O2 -Wall -Wextra
|
||||
|
||||
HTTP_TEST := http_test
|
||||
SVG_TEST := svg_renderer_test
|
||||
|
||||
.PHONY: all check clean
|
||||
|
||||
all: $(HTTP_TEST)
|
||||
all: $(HTTP_TEST) $(SVG_TEST)
|
||||
|
||||
$(HTTP_TEST): http_test.cpp
|
||||
$(CXX) $(CXXFLAGS) -I../programs/include \
|
||||
-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)
|
||||
./$(SVG_TEST)
|
||||
|
||||
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