Viewing is open to everyone. Downloading and starring need a GitHub sign-in.
Sign in with GitHub
render_tests.cpp
v0.1.0-build.85
Latest
Commit ede38f5
Sign in with GitHub to download. Each download is recorded with your GitHub username.
| 1 | // Native backends (specification §2, acceptance criteria 2, 4 and 5): the |
| 2 | // direct raster agrees with the rasterized SVG export, decodes, is |
| 3 | // deterministic, composites transparently; PNG and PDF are well formed. |
| 4 | "ckey/ckey.h" |
| 5 | "check.h" |
| 6 | "svg_raster.h" |
| 7 | |
| 8 | <cstdio> |
| 9 | <cstring> |
| 10 | <string> |
| 11 | <tuple> |
| 12 | <algorithm> |
| 13 | <cmath> |
| 14 | <vector> |
| 15 | |
| 16 | namespace { |
| 17 | |
| 18 | std::vector<uint8_t> pseudo(size_t n, uint32_t seed) { |
| 19 | std::vector<uint8_t> out(n); |
| 20 | for (auto &b : out) { seed = seed * 1664525u + 1013904223u; b = static_cast<uint8_t>(seed >> 24); } |
| 21 | return out; |
| 22 | } |
| 23 | |
| 24 | ckey_scalable_symbol encode(uint32_t version, ckey_scalable_layout layout, size_t bytes, uint32_t seed) { |
| 25 | ckey_scalable_symbol s{}; ckey_diagnostics d{}; |
| 26 | const auto payload = pseudo(bytes, seed); |
| 27 | CKEY_CHECK(ckey_encode_scalable(payload.data(), payload.size(), version, CKEY_ECC_M, layout, &s, &d) == CKEY_OK); |
| 28 | return s; |
| 29 | } |
| 30 | |
| 31 | std::vector<uint8_t> raster(const ckey_scalable_symbol &s, const ckey_style *st, ckey_raster_options o) { |
| 32 | size_t need = 0; |
| 33 | CKEY_CHECK(ckey_render_scalable_raster(&s, st, &o, nullptr, 0, &need) == CKEY_INSUFFICIENT_BUFFER); |
| 34 | std::vector<uint8_t> px(need); |
| 35 | CKEY_CHECK(ckey_render_scalable_raster(&s, st, &o, px.data(), px.size(), &need) == CKEY_OK); |
| 36 | return px; |
| 37 | } |
| 38 | |
| 39 | std::string svg_of(const ckey_scalable_symbol &s, const ckey_style &st) { |
| 40 | size_t n = 0; CKEY_CHECK(ckey_render_scalable_styled_svg(&s, &st, 8, nullptr, 0, &n) == CKEY_INSUFFICIENT_BUFFER); |
| 41 | std::string svg(n, '\0'); CKEY_CHECK(ckey_render_scalable_styled_svg(&s, &st, 8, svg.data(), n, &n) == CKEY_OK); |
| 42 | svg.resize(n - 1); return svg; |
| 43 | } |
| 44 | |
| 45 | bool decodes(const std::vector<uint8_t> &gray, int side, const std::vector<uint8_t> &payload) { |
| 46 | const double k = side / 31.0, lo = k, hi = 30 * k; |
| 47 | const double q[8] = {lo, lo, hi, lo, hi, hi, lo, hi}; |
| 48 | std::vector<uint8_t> out(65535); size_t len = 0; uint32_t version = 0; ckey_diagnostics d{}; |
| 49 | if (ckey_decode_scalable_gray8(gray.data(), side, side, side, q, out.data(), out.size(), &len, &version, nullptr, &d) != CKEY_OK) return false; |
| 50 | return len == payload.size() && std::equal(payload.begin(), payload.end(), out.begin()); |
| 51 | } |
| 52 | |
| 53 | uint64_t fnv1a(const std::vector<uint8_t> &v) { uint64_t h = 0xcbf29ce484222325ull; for (uint8_t b : v) { h ^= b; h *= 0x100000001b3ull; } return h; } |
| 54 | |
| 55 | // Minimal inflate for fixed-Huffman DEFLATE streams (what the PNG writer emits). |
| 56 | struct BitReader { |
| 57 | const uint8_t *p; size_t n, pos = 0; int bit = 0; |
| 58 | int get() { if (pos >= n) throw 1; const int v = (p[pos] >> bit) & 1; if (++bit == 8) { bit = 0; ++pos; } return v; } |
| 59 | int bits(int k) { int v = 0; for (int i = 0; i < k; ++i) v |= get() << i; return v; } |
| 60 | int huff(int k) { int v = 0; for (int i = 0; i < k; ++i) v = (v << 1) | get(); return v; } |
| 61 | }; |
| 62 | std::vector<uint8_t> inflate_fixed(const uint8_t *data, size_t n) { |
| 63 | static const int lbase[29] = {3, 4, 5, 6, 7, 8, 9, 10, 11, 13, 15, 17, 19, 23, 27, 31, 35, 43, 51, 59, 67, 83, 99, 115, 131, 163, 195, 227, 258}; |
| 64 | static const int lextra[29] = {0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 0}; |
| 65 | static const int dbase[30] = {1, 2, 3, 4, 5, 7, 9, 13, 17, 25, 33, 49, 65, 97, 129, 193, 257, 385, 513, 769, 1025, 1537, 2049, 3073, 4097, 6145, 8193, 12289, 16385, 24577}; |
| 66 | static const int dextra[30] = {0, 0, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, 12, 13, 13}; |
| 67 | BitReader r{data + 2, n - 6}; |
| 68 | std::vector<uint8_t> out; |
| 69 | CKEY_CHECK(r.bits(1) == 1 && r.bits(2) == 1); |
| 70 | for (;;) { |
| 71 | int code = r.huff(7), sym; |
| 72 | if (code <= 0x17) sym = 256 + code; |
| 73 | else { code = (code << 1) | r.get(); |
| 74 | if (code >= 0x30 && code <= 0xbf) sym = code - 0x30; |
| 75 | else if (code >= 0xc0 && code <= 0xc7) sym = 280 + code - 0xc0; |
| 76 | else { code = (code << 1) | r.get(); sym = 144 + code - 0x190; } } |
| 77 | if (sym < 256) { out.push_back(static_cast<uint8_t>(sym)); continue; } |
| 78 | if (sym == 256) break; |
| 79 | const int li = sym - 257, len = lbase[li] + r.bits(lextra[li]); |
| 80 | const int di = r.huff(5), dist = dbase[di] + r.bits(dextra[di]); |
| 81 | for (int i = 0; i < len; ++i) out.push_back(out[out.size() - dist]); |
| 82 | } |
| 83 | return out; |
| 84 | } |
| 85 | |
| 86 | uint32_t be32(const uint8_t *p) { return (uint32_t(p[0]) << 24) | (uint32_t(p[1]) << 16) | (uint32_t(p[2]) << 8) | p[3]; } |
| 87 | |
| 88 | // Decodes the writer's own PNG back to pixels (8-bit gray/RGBA, filters 0/2). |
| 89 | std::vector<uint8_t> read_png(const std::vector<uint8_t> &png, uint32_t &w, uint32_t &h, int &channels) { |
| 90 | CKEY_CHECK(png.size() > 33 && std::memcmp(png.data(), "\x89PNG\r\n\x1a\n", 8) == 0); |
| 91 | size_t at = 8; std::vector<uint8_t> idat; |
| 92 | while (at + 12 <= png.size()) { |
| 93 | const uint32_t len = be32(&png[at]); const std::string type(reinterpret_cast<const char *>(&png[at + 4]), 4); |
| 94 | if (type == "IHDR") { w = be32(&png[at + 8]); h = be32(&png[at + 12]); channels = png[at + 17] == 6 ? 4 : 1; } |
| 95 | if (type == "IDAT") idat.insert(idat.end(), png.begin() + static_cast<long>(at + 8), png.begin() + static_cast<long>(at + 8 + len)); |
| 96 | at += 12 + len; |
| 97 | if (type == "IEND") break; |
| 98 | } |
| 99 | const auto raw = inflate_fixed(idat.data(), idat.size()); |
| 100 | const size_t row = static_cast<size_t>(w) * channels; |
| 101 | CKEY_CHECK(raw.size() == (row + 1) * h); |
| 102 | std::vector<uint8_t> px(row * h), above(row, 0); |
| 103 | for (uint32_t y = 0; y < h; ++y) { |
| 104 | const uint8_t f = raw[y * (row + 1)]; |
| 105 | for (size_t i = 0; i < row; ++i) { const uint8_t v = raw[y * (row + 1) + 1 + i]; px[y * row + i] = f == 2 ? static_cast<uint8_t>(v + above[i]) : v; } |
| 106 | std::copy(px.begin() + static_cast<long>(y * row), px.begin() + static_cast<long>((y + 1) * row), above.begin()); |
| 107 | } |
| 108 | return px; |
| 109 | } |
| 110 | |
| 111 | } // namespace |
| 112 | |
| 113 | int main() { |
| 114 | ckey_style plain; ckey_style_default(&plain); |
| 115 | |
| 116 | // 1. The direct raster draws exactly what the SVG export describes: |
| 117 | // binary 3x3-sampled pixels agree with the reference SVG rasterizer. |
| 118 | for (const auto &[version, layout, shape] : {std::tuple{2u, CKEY_LAYOUT_STANDARD, CKEY_MODULE_ROUNDED}, std::tuple{11u, CKEY_LAYOUT_STANDARD, CKEY_MODULE_DIAMOND}, |
| 119 | std::tuple{20u, CKEY_LAYOUT_PRINT, CKEY_MODULE_CIRCLE}, std::tuple{31u, CKEY_LAYOUT_STANDARD, CKEY_MODULE_SQUARE}}) { |
| 120 | const auto s = encode(version, layout, 20, version); |
| 121 | ckey_style st = plain; st.modules = shape; st.rings = version == 11 ? CKEY_RING_DOTS : CKEY_RING_JOINED; st.rays = version == 20 ? CKEY_RAY_FLAT : CKEY_RAY_ROUND; |
| 122 | const int side = 31 * 10 * static_cast<int>((version + 9) / 10); |
| 123 | ckey_raster_options o; ckey_raster_options_default(&o); |
| 124 | o.size_px = static_cast<uint32_t>(side); o.policy = CKEY_RASTER_BINARY; o.format = CKEY_PIXELS_GRAY8; o.supersample = 3; |
| 125 | const auto direct = raster(s, &st, o); |
| 126 | const auto ref = svgtest::rasterize(svgtest::parse(svg_of(s, st)), side, side, 3, 0, 255); |
| 127 | size_t differ = 0; |
| 128 | for (size_t i = 0; i < direct.size(); ++i) differ += (direct[i] < 128) != (ref[i] >= 128); |
| 129 | std::printf("v%u layout %d: direct raster vs SVG raster, %zu of %zu pixels differ\n", version, layout, differ, direct.size()); |
| 130 | CKEY_CHECK(differ * 2000 <= direct.size()); // <= 0.05%: only SVG's 4-decimal rounding |
| 131 | } |
| 132 | |
| 133 | // 2. Every policy decodes; determinism; GRAY8 vs RGBA8 agree. |
| 134 | for (uint32_t version : {1u, 9u, 31u}) { |
| 135 | for (auto layout : {CKEY_LAYOUT_STANDARD, CKEY_LAYOUT_PRINT}) { |
| 136 | const auto payload = pseudo(std::min<size_t>(20, ckey_scalable_capacity(version, CKEY_ECC_M, layout)), 77 + version); |
| 137 | ckey_scalable_symbol s{}; ckey_diagnostics d{}; |
| 138 | CKEY_CHECK(ckey_encode_scalable(payload.data(), payload.size(), version, CKEY_ECC_M, layout, &s, &d) == CKEY_OK); |
| 139 | const uint32_t modules = ckey_scalable_modules_across(version); |
| 140 | for (auto policy : {CKEY_RASTER_COVERAGE, CKEY_RASTER_BINARY, CKEY_RASTER_HYBRID}) { |
| 141 | ckey_raster_options o; ckey_raster_options_default(&o); |
| 142 | o.size_px = static_cast<uint32_t>(std::ceil(31.0 * 7 * modules / 29.0)); // 7 px per module |
| 143 | o.policy = policy; o.format = CKEY_PIXELS_GRAY8; |
| 144 | const auto a = raster(s, nullptr, o), b = raster(s, nullptr, o); |
| 145 | CKEY_CHECK(a == b); |
| 146 | CKEY_CHECK(decodes(a, static_cast<int>(o.size_px), payload)); |
| 147 | o.format = CKEY_PIXELS_RGBA8; |
| 148 | const auto rgba = raster(s, nullptr, o); |
| 149 | for (size_t i = 0; i < a.size(); i += 97) CKEY_CHECK(rgba[4 * i] == a[i] && rgba[4 * i + 3] == 255); |
| 150 | } |
| 151 | } |
| 152 | } |
| 153 | std::printf("coverage, binary and hybrid rasters decode at 7 px/module for v1, v9, v31 (both layouts); output is deterministic\n"); |
| 154 | { |
| 155 | const auto s = encode(2, CKEY_LAYOUT_STANDARD, 20, 5); |
| 156 | ckey_raster_options o; ckey_raster_options_default(&o); o.size_px = 600; |
| 157 | std::printf("v2 600 px coverage raster hash %016llx\n", static_cast<unsigned long long>(fnv1a(raster(s, nullptr, o)))); |
| 158 | } |
| 159 | |
| 160 | // 3. HYBRID keeps thin features: at a small size it paints at least as |
| 161 | // much ink as COVERAGE, and every v31 ray stays visible. |
| 162 | { |
| 163 | const auto s = encode(31, CKEY_LAYOUT_STANDARD, 100, 9); |
| 164 | ckey_raster_options o; ckey_raster_options_default(&o); o.size_px = 400; o.format = CKEY_PIXELS_GRAY8; |
| 165 | const auto cov = raster(s, nullptr, o); o.policy = CKEY_RASTER_HYBRID; const auto hyb = raster(s, nullptr, o); |
| 166 | size_t ink_cov = 0, ink_hyb = 0; for (size_t i = 0; i < cov.size(); ++i) { ink_cov += 255 - cov[i]; ink_hyb += 255 - hyb[i]; } |
| 167 | CKEY_CHECK(ink_hyb > ink_cov); |
| 168 | o.min_feature_px = 0; CKEY_CHECK(ckey_render_scalable_raster(&s, nullptr, &o, nullptr, 0, nullptr) == CKEY_INVALID_ARGUMENT); |
| 169 | } |
| 170 | |
| 171 | // 4. Transparency (spec §2, §11.5): no background, unpainted alpha 0. |
| 172 | { |
| 173 | const auto s = encode(5, CKEY_LAYOUT_STANDARD, 20, 3); |
| 174 | ckey_raster_options o; ckey_raster_options_default(&o); o.size_px = 310; o.transparent = 1; |
| 175 | const auto px = raster(s, nullptr, o); |
| 176 | CKEY_CHECK(px[3] == 0 && px[4 * (310 * 155 + 2) + 3] == 0); // quiet margin |
| 177 | const size_t frame = 4 * (static_cast<size_t>(310) * 155 + 15); // on the frame stroke (x = 0.5 units) |
| 178 | CKEY_CHECK(px[frame + 3] == 255 && px[frame] == 0); |
| 179 | size_t zero = 0; for (size_t i = 3; i < px.size(); i += 4) zero += px[i] == 0; |
| 180 | CKEY_CHECK(zero > px.size() / 8); |
| 181 | o.format = CKEY_PIXELS_GRAY8; size_t need = 0; |
| 182 | CKEY_CHECK(ckey_render_scalable_raster(&s, nullptr, &o, nullptr, 0, &need) == CKEY_INVALID_ARGUMENT); // gray has no alpha |
| 183 | } |
| 184 | |
| 185 | // 5. Materials and a logo image render natively and still decode. |
| 186 | { |
| 187 | const auto payload = pseudo(40, 11); |
| 188 | ckey_scalable_symbol s{}; ckey_diagnostics d{}; |
| 189 | CKEY_CHECK(ckey_encode_scalable(payload.data(), payload.size(), 11, CKEY_ECC_Q, CKEY_LAYOUT_STANDARD, &s, &d) == CKEY_OK); |
| 190 | ckey_style art; CKEY_CHECK(ckey_style_preset_fill(&art, CKEY_PRESET_CONCEPT) == CKEY_OK); |
| 191 | std::vector<uint8_t> logo(64 * 64 * 4); |
| 192 | for (size_t i = 0; i < 64 * 64; ++i) { logo[4 * i] = 200; logo[4 * i + 1] = 40; logo[4 * i + 2] = 40; logo[4 * i + 3] = 255; } |
| 193 | art.logo_href = "logo.png"; art.logo_plate = 1; art.logo_radius = 3.0; |
| 194 | CKEY_CHECK(ckey_check_scalable_style(&s, &art, &d) == CKEY_OK); |
| 195 | ckey_logo_image img{logo.data(), 64, 64, 0}; |
| 196 | ckey_raster_options o; ckey_raster_options_default(&o); o.size_px = 31 * 14 * 67 / 29; o.logo = &img; |
| 197 | const auto rgba = raster(s, &art, o); |
| 198 | const size_t c = 4 * (static_cast<size_t>(o.size_px / 2) * o.size_px + o.size_px / 2); |
| 199 | CKEY_CHECK(rgba[c] == 200 && rgba[c + 1] == 40); // the logo is drawn at the centre |
| 200 | std::vector<uint8_t> gray(rgba.size() / 4); |
| 201 | for (size_t i = 0; i < gray.size(); ++i) gray[i] = static_cast<uint8_t>((54 * rgba[4 * i] + 183 * rgba[4 * i + 1] + 19 * rgba[4 * i + 2]) >> 8); |
| 202 | CKEY_CHECK(decodes(gray, static_cast<int>(o.size_px), payload)); |
| 203 | std::printf("concept preset with depth, grain and a logo: native raster decodes\n"); |
| 204 | } |
| 205 | |
| 206 | // 6. Geometry API. |
| 207 | { |
| 208 | const auto s = encode(3, CKEY_LAYOUT_STANDARD, 10, 1); |
| 209 | size_t count = 0; |
| 210 | CKEY_CHECK(ckey_scalable_geometry(&s, nullptr, nullptr, 0, &count) == CKEY_INSUFFICIENT_BUFFER && count > 100); |
| 211 | std::vector<ckey_shape> shapes(count); |
| 212 | CKEY_CHECK(ckey_scalable_geometry(&s, nullptr, shapes.data(), shapes.size(), &count) == CKEY_OK); |
| 213 | CKEY_CHECK(shapes[0].kind == CKEY_SHAPE_RECT_OUTLINE && shapes[0].layer == CKEY_LAYER_FRAME); |
| 214 | size_t finders = 0; for (const auto &sh : shapes) { finders += sh.layer == CKEY_LAYER_FINDERS; CKEY_CHECK(sh.kind != CKEY_SHAPE_LOGO); } |
| 215 | CKEY_CHECK(finders == 5); // three corner arcs plus the split top-left pair |
| 216 | ckey_style bad = plain; bad.ink = {0xee, 0xee, 0xee}; |
| 217 | CKEY_CHECK(ckey_scalable_geometry(&s, &bad, nullptr, 0, &count) == CKEY_INVALID_ARGUMENT); |
| 218 | } |
| 219 | |
| 220 | // 7. PNG round trip through an independent inflate; 1-bit and pHYs. |
| 221 | { |
| 222 | const auto s = encode(7, CKEY_LAYOUT_STANDARD, 30, 2); |
| 223 | ckey_raster_options o; ckey_raster_options_default(&o); o.size_px = 257; |
| 224 | const auto rgba = raster(s, nullptr, o); |
| 225 | size_t need = 0; |
| 226 | CKEY_CHECK(ckey_encode_png(rgba.data(), 257, 257, 0, CKEY_PNG_RGBA8, 300, nullptr, 0, &need) == CKEY_INSUFFICIENT_BUFFER); |
| 227 | std::vector<uint8_t> png(need); |
| 228 | CKEY_CHECK(ckey_encode_png(rgba.data(), 257, 257, 0, CKEY_PNG_RGBA8, 300, png.data(), png.size(), &need) == CKEY_OK); |
| 229 | uint32_t w = 0, h = 0; int ch = 0; |
| 230 | CKEY_CHECK(read_png(png, w, h, ch) == rgba && w == 257 && h == 257 && ch == 4); |
| 231 | CKEY_CHECK(png.size() < rgba.size() / 4); // compressed |
| 232 | std::string bytes(png.begin(), png.end()); |
| 233 | CKEY_CHECK(bytes.find("pHYs") != std::string::npos); |
| 234 | std::vector<uint8_t> gray(257 * 257); for (size_t i = 0; i < gray.size(); ++i) gray[i] = rgba[4 * i]; |
| 235 | CKEY_CHECK(ckey_encode_png(gray.data(), 257, 257, 0, CKEY_PNG_GRAY8, 0, png.data(), png.size(), &need) == CKEY_OK); |
| 236 | png.resize(need); CKEY_CHECK(read_png(png, w, h, ch) == gray && ch == 1); |
| 237 | std::vector<uint8_t> one(64 * 1024); |
| 238 | CKEY_CHECK(ckey_encode_png(gray.data(), 257, 257, 0, CKEY_PNG_GRAY1, 600, one.data(), one.size(), &need) == CKEY_OK && one[24] == 1); |
| 239 | std::printf("PNG: RGBA8 %zu bytes for %zu pixel bytes; decodes back exactly\n", png.size(), rgba.size()); |
| 240 | } |
| 241 | |
| 242 | // 8. PDF: valid structure and cross-reference offsets. |
| 243 | { |
| 244 | const auto s = encode(4, CKEY_LAYOUT_STANDARD, 20, 4); |
| 245 | size_t need = 0; |
| 246 | CKEY_CHECK(ckey_render_scalable_pdf(&s, nullptr, 144, 0, nullptr, 0, &need) == CKEY_INSUFFICIENT_BUFFER); |
| 247 | std::vector<uint8_t> pdf(need); |
| 248 | CKEY_CHECK(ckey_render_scalable_pdf(&s, nullptr, 144, 0, pdf.data(), pdf.size(), &need) == CKEY_OK); |
| 249 | const std::string text(pdf.begin(), pdf.end()); |
| 250 | CKEY_CHECK(text.rfind("%PDF-1.4", 0) == 0 && text.find("%%EOF") != std::string::npos && text.find("/MediaBox [0 0 144 144]") != std::string::npos); |
| 251 | const size_t sx = text.rfind("startxref\n"); |
| 252 | const size_t xref = std::stoul(text.substr(sx + 10)); |
| 253 | CKEY_CHECK(text.compare(xref, 4, "xref") == 0); |
| 254 | for (int obj = 1; obj <= 5; ++obj) { |
| 255 | const size_t line = xref + 5 + std::to_string(6).size() + 3 + 20 * obj; // "xref\n0 6\n" then 20-byte entries |
| 256 | const size_t off = std::stoul(text.substr(line, 10)); |
| 257 | CKEY_CHECK(text.compare(off, std::to_string(obj).size() + 6, std::to_string(obj) + " 0 obj") == 0); |
| 258 | } |
| 259 | CKEY_CHECK(ckey_render_scalable_pdf(&s, nullptr, 144, 1, pdf.data(), pdf.size(), &need) == CKEY_OK); |
| 260 | CKEY_CHECK(std::string(pdf.begin(), pdf.begin() + static_cast<long>(need)).find("1 1 1 rg\n0 0 144 144 re f") == std::string::npos); |
| 261 | std::printf("PDF: %zu bytes, valid xref; transparent mode omits the background\n", need); |
| 262 | } |
| 263 | |
| 264 | // 9. The print path no longer needs SVG and still emits pure ink/paper. |
| 265 | { |
| 266 | const auto s = encode(3, CKEY_LAYOUT_PRINT, 20, 6); |
| 267 | size_t side = 0; |
| 268 | CKEY_CHECK(ckey_render_scalable_print(&s, nullptr, 0.5, 600, nullptr, 0, &side) == CKEY_INSUFFICIENT_BUFFER && side == 300); |
| 269 | std::vector<uint8_t> px(side * side); |
| 270 | CKEY_CHECK(ckey_render_scalable_print(&s, nullptr, 0.5, 600, px.data(), px.size(), &side) == CKEY_OK); |
| 271 | for (uint8_t v : px) CKEY_CHECK(v == 0 || v == 255); |
| 272 | } |
| 273 | std::printf("native render tests passed\n"); |
| 274 | return 0; |
| 275 | } |