Files
MontaukOS/tests/svg_renderer_test.cpp

224 lines
9.4 KiB
C++

#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;
}