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

Size
17 KB
Lines
275
Language
cpp
SHA-256
9954b7d64831…
View on GitHub

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#include "ckey/ckey.h"
5#include "check.h"
6#include "svg_raster.h"
7
8#include <cstdio>
9#include <cstring>
10#include <string>
11#include <tuple>
12#include <algorithm>
13#include <cmath>
14#include <vector>
15
16namespace {
17
18std::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
24ckey_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
31std::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
39std::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
45bool 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
53uint64_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).
56struct 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};
62std::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
86uint32_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).
89std::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
113int 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}