Viewing is open to everyone. Downloading and starring need a GitHub sign-in.
Sign in with GitHub
scalable_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 | // Profile 0 (scalable family) tests. White-box: includes the core |
| 2 | // translation unit to check layout invariants; built without ckey_core. |
| 3 | "../src/ckey.cpp" |
| 4 | "check.h" |
| 5 | "svg_raster.h" |
| 6 | "print_sim.h" |
| 7 | |
| 8 | <cstdio> |
| 9 | |
| 10 | namespace { |
| 11 | |
| 12 | constexpr uint64_t kPrintKatV3 = 0x1908543413f369b8ull, kPrintKatV31 = 0x684c56624d3558a8ull; // stated in spec/CKey-Format-v0.28.0.md |
| 13 | constexpr uint64_t kScalableKatV2 = 0xb97ef7acabd3c8d7ull, kScalableKatV31 = 0xc7df4dfd1c36458bull; // stated in spec/CKey-Format-v0.27.0.md |
| 14 | |
| 15 | std::vector<uint8_t> pseudo(size_t n, uint32_t seed) { |
| 16 | std::vector<uint8_t> out(n); |
| 17 | for (auto &b : out) { seed = seed * 1664525u + 1013904223u; b = static_cast<uint8_t>(seed >> 24); } |
| 18 | return out; |
| 19 | } |
| 20 | |
| 21 | std::string render_styled(const ckey_scalable_symbol &s, const ckey_style &st) { |
| 22 | size_t n = 0; |
| 23 | if (ckey_render_scalable_styled_svg(&s, &st, 8, nullptr, 0, &n) != CKEY_INSUFFICIENT_BUFFER) return {}; |
| 24 | std::string svg(n, '\0'); CKEY_CHECK(ckey_render_scalable_styled_svg(&s, &st, 8, svg.data(), n, &n) == CKEY_OK); |
| 25 | svg.resize(n - 1); return svg; |
| 26 | } |
| 27 | |
| 28 | // Decodes a rendered SVG with the exact quad; returns the detected version or 0. |
| 29 | uint32_t decode_svg(const std::string &svg, double unit, const std::vector<uint8_t> &payload) { |
| 30 | const int side = static_cast<int>(std::ceil(31 * unit)); |
| 31 | const auto px = svgtest::rasterize(svgtest::parse(svg), side, side, 3); |
| 32 | const double k = static_cast<double>(side) / 31.0, lo = k, hi = 30 * k; |
| 33 | const double q[8] = {lo, lo, hi, lo, hi, hi, lo, hi}; |
| 34 | std::vector<uint8_t> out(65535); size_t len = 0; uint32_t version = 0; ckey_diagnostics d{}; |
| 35 | if (ckey_decode_scalable_gray8(px.data(), side, side, side, q, out.data(), out.size(), &len, &version, nullptr, &d) != CKEY_OK) return 0; |
| 36 | return len == payload.size() && std::equal(payload.begin(), payload.end(), out.begin()) ? version : 0; |
| 37 | } |
| 38 | |
| 39 | std::string render(const ckey_scalable_symbol &s) { |
| 40 | size_t n = 0; CKEY_CHECK(ckey_render_scalable_svg(&s, 8, nullptr, 0, &n) == CKEY_INSUFFICIENT_BUFFER); |
| 41 | std::string svg(n, '\0'); CKEY_CHECK(ckey_render_scalable_svg(&s, 8, svg.data(), n, &n) == CKEY_OK); |
| 42 | svg.resize(n - 1); return svg; |
| 43 | } |
| 44 | |
| 45 | // Renders and decodes with the exact quad; returns the detected version or 0. |
| 46 | uint32_t image_round_trip(const ckey_scalable_symbol &s, const std::vector<uint8_t> &payload, double px_per_module, bool blur) { |
| 47 | const ScalableLayout &L = scalable_layout(static_cast<int>(s.version)); |
| 48 | const double unit = px_per_module / L.pitch; // pixels per canonical unit |
| 49 | const int side = static_cast<int>(std::ceil(31 * unit)); |
| 50 | auto px = svgtest::rasterize(svgtest::parse(render(s)), side, side, 3); |
| 51 | if (blur) { |
| 52 | auto out = px; |
| 53 | for (int y = 1; y < side - 1; ++y) for (int x = 1; x < side - 1; ++x) { |
| 54 | int t = 0; for (int dy = -1; dy <= 1; ++dy) for (int dx = -1; dx <= 1; ++dx) t += px[static_cast<size_t>(y + dy) * side + x + dx]; |
| 55 | out[static_cast<size_t>(y) * side + x] = static_cast<uint8_t>(t / 9); |
| 56 | } |
| 57 | px.swap(out); |
| 58 | } |
| 59 | const double k = static_cast<double>(side) / 31.0, lo = k, hi = 30 * k; |
| 60 | const double q[8] = {lo, lo, hi, lo, hi, hi, lo, hi}; |
| 61 | std::vector<uint8_t> out(65535); size_t len = 0; uint32_t version = 0; ckey_diagnostics d{}; |
| 62 | if (ckey_decode_scalable_gray8(px.data(), side, side, side, q, out.data(), out.size(), &len, &version, nullptr, &d) != CKEY_OK) return 0; |
| 63 | if (len != payload.size() || !std::equal(payload.begin(), payload.end(), out.begin())) return 0; |
| 64 | return version; |
| 65 | } |
| 66 | |
| 67 | } // namespace |
| 68 | |
| 69 | int main() { |
| 70 | // ---- Table invariants (both layout families) ---- |
| 71 | for (int f = 0; f < kScalableFamilies; ++f) for (int v = 1; v <= kScalableVersions; ++v) { |
| 72 | const ScalableLayout &L = scalable_layout(v, f); const double p = L.pitch; |
| 73 | const double ring_limit = kScalableDialInner - (f == 0 ? 2.38 : 2.4) * p; |
| 74 | CKEY_CHECK(L.dial_slots % 8 == 0); |
| 75 | for (int S : L.ring_slots) CKEY_CHECK(S % 8 == 0); |
| 76 | CKEY_CHECK(L.ring_radius(L.rings - 1) <= ring_limit + 1e-9); // ring band clears the dial |
| 77 | CKEY_CHECK(L.ring_radius(L.rings) > ring_limit - 1e-9); // and uses all the room it has |
| 78 | CKEY_CHECK(L.ring_key_slot() + L.ring_key_span <= L.ring_slots.back()); |
| 79 | CKEY_CHECK(L.codeword_bytes <= CKEY_SCALABLE_MAX_CODEWORD_BYTES); |
| 80 | if (f == 1) { |
| 81 | // Print family: every mark and gap is at least about one module. |
| 82 | for (int k = 0; k < L.rings; ++k) { |
| 83 | const double spacing = kTau * L.ring_radius(k) / L.ring_slots[static_cast<size_t>(k)]; |
| 84 | CKEY_CHECK(spacing >= p - 1e-9 && spacing - 2 * L.dot >= 0.39 * p); |
| 85 | const double exact = kTau * L.ring_radius(k) / (8.0 * p); // slot count decided with slack |
| 86 | CKEY_CHECK(std::abs(exact - std::round(exact)) > 1e-6); |
| 87 | } |
| 88 | CKEY_CHECK(kTau * kScalableDialInner / L.dial_slots - L.ray_width >= 0.6 * p); |
| 89 | CKEY_CHECK(L.ring_pitch - 2 * L.dot >= 0.39 * p); |
| 90 | } |
| 91 | // Grid Key ink keeps a pitch of clearance from the frame, finders, and radial alphabet. |
| 92 | const DenseGeometry &g = L.geom; double worst = 1e9; |
| 93 | for (double x = g.grid_x0; x <= g.grid_x1 + 1e-12; x += (g.grid_x1 - g.grid_x0) / 8) |
| 94 | for (double y = g.grid_y0; y <= g.grid_y1 + 1e-12; y += (g.grid_y1 - g.grid_y0) / 8) { |
| 95 | DenseGeometry probe = g; probe.grid_x0 = probe.grid_y0 = 1e6; probe.grid_x1 = probe.grid_y1 = 1e6; // ignore itself |
| 96 | probe.module_half = 0; probe.frame_clear = probe.finder_clear = probe.polar_clear = 0; |
| 97 | worst = std::min(worst, dense_site_margin(probe, x, y)); |
| 98 | } |
| 99 | CKEY_CHECK(worst >= p - 1e-9); |
| 100 | // Site set decided with slack (interoperable across libm implementations). |
| 101 | double slack = 1e9; |
| 102 | for (int row = 0; row < L.grid; ++row) for (int col = 0; col < L.grid; ++col) |
| 103 | slack = std::min(slack, std::abs(dense_site_margin(L.geom, (col + .5) * p, (row + .5) * p))); |
| 104 | CKEY_CHECK(slack > 1e-6); |
| 105 | } |
| 106 | |
| 107 | // ---- P0.1 (print family): capacities, and every version decodes ---- |
| 108 | std::printf("P0.1 (print) payload bytes at level M:"); |
| 109 | size_t previous_print = 0; |
| 110 | for (uint32_t v = 1; v <= kScalableVersions; ++v) { |
| 111 | const size_t m = ckey_scalable_capacity(v, CKEY_ECC_M, CKEY_LAYOUT_PRINT); |
| 112 | std::printf(" %zu", m); |
| 113 | CKEY_CHECK(m > previous_print && m < ckey_scalable_capacity(v, CKEY_ECC_M, CKEY_LAYOUT_STANDARD)); |
| 114 | previous_print = m; |
| 115 | } |
| 116 | std::printf("\n"); |
| 117 | { |
| 118 | ckey_scalable_symbol ps{}; ckey_diagnostics pd{}; |
| 119 | for (uint32_t v = 1; v <= kScalableVersions; ++v) { |
| 120 | const auto payload = pseudo(ckey_scalable_capacity(v, CKEY_ECC_M, CKEY_LAYOUT_PRINT), 3000u + v); |
| 121 | CKEY_CHECK(ckey_encode_scalable(payload.data(), payload.size(), v, CKEY_ECC_M, CKEY_LAYOUT_PRINT, &ps, &pd) == CKEY_OK && ps.layout == CKEY_LAYOUT_PRINT); |
| 122 | const ScalableLayout &L = scalable_layout(static_cast<int>(v), 1); |
| 123 | const double unit = 6.0 / L.pitch; const int side = static_cast<int>(std::ceil(31 * unit)); |
| 124 | const auto px = svgtest::rasterize(svgtest::parse(render(ps)), side, side, 3); |
| 125 | const double k = static_cast<double>(side) / 31.0, q[8] = {k, k, 30 * k, k, 30 * k, 30 * k, k, 30 * k}; |
| 126 | std::vector<uint8_t> out(65535); size_t len = 0; uint32_t version = 0, layout = 9; |
| 127 | const ckey_status st = ckey_decode_scalable_gray8(px.data(), side, side, side, q, out.data(), out.size(), &len, &version, &layout, &pd); |
| 128 | if (st != CKEY_OK || version != v || layout != CKEY_LAYOUT_PRINT) std::printf("P0.1 v%u: status %d version %u layout %u\n", v, st, version, layout); |
| 129 | CKEY_CHECK(st == CKEY_OK && version == v && layout == CKEY_LAYOUT_PRINT && len == payload.size() && std::equal(payload.begin(), payload.end(), out.begin())); |
| 130 | } |
| 131 | std::printf("P0.1: all 31 versions decode at 6 px/module and are told apart from P0 by their keys\n"); |
| 132 | } |
| 133 | |
| 134 | // ---- Capacity grows smoothly ---- |
| 135 | std::printf(" v modules sites L M Q H (payload bytes)\n"); |
| 136 | size_t previous_m = 0; |
| 137 | for (int v = 1; v <= kScalableVersions; ++v) { |
| 138 | const size_t m = ckey_scalable_capacity(static_cast<uint32_t>(v), CKEY_ECC_M, CKEY_LAYOUT_STANDARD); |
| 139 | std::printf("%2d %6d %7zu %5zu %5zu %5zu %5zu\n", v, kScalableGrid[v - 1], scalable_layout(v).nodes.size(), |
| 140 | ckey_scalable_capacity(v, CKEY_ECC_L, CKEY_LAYOUT_STANDARD), m, ckey_scalable_capacity(v, CKEY_ECC_Q, CKEY_LAYOUT_STANDARD), ckey_scalable_capacity(v, CKEY_ECC_H, CKEY_LAYOUT_STANDARD)); |
| 141 | CKEY_CHECK(m > previous_m); |
| 142 | if (previous_m) CKEY_CHECK(static_cast<double>(m) / previous_m < 1.6); // no large jumps |
| 143 | CKEY_CHECK(ckey_scalable_capacity(v, CKEY_ECC_L, CKEY_LAYOUT_STANDARD) > m && m > ckey_scalable_capacity(v, CKEY_ECC_Q, CKEY_LAYOUT_STANDARD) && ckey_scalable_capacity(v, CKEY_ECC_Q, CKEY_LAYOUT_STANDARD) > ckey_scalable_capacity(v, CKEY_ECC_H, CKEY_LAYOUT_STANDARD)); |
| 144 | previous_m = m; |
| 145 | } |
| 146 | CKEY_CHECK(ckey_scalable_capacity(0, CKEY_ECC_M, CKEY_LAYOUT_STANDARD) == 0 && ckey_scalable_capacity(32, CKEY_ECC_M, CKEY_LAYOUT_STANDARD) == 0); |
| 147 | |
| 148 | // ---- Auto version selection is the smallest that fits ---- |
| 149 | ckey_scalable_symbol s{}; ckey_diagnostics d{}; |
| 150 | const std::string text33 = "https://citeory.com/tour?ref=ckey"; // 33 characters |
| 151 | CKEY_CHECK(text33.size() == 33); |
| 152 | CKEY_CHECK(ckey_encode_scalable(reinterpret_cast<const uint8_t *>(text33.data()), text33.size(), 0, CKEY_ECC_M, CKEY_LAYOUT_STANDARD, &s, &d) == CKEY_OK); |
| 153 | CKEY_CHECK(ckey_scalable_capacity(s.version, CKEY_ECC_M, CKEY_LAYOUT_STANDARD) >= 33 && (s.version == 1 || ckey_scalable_capacity(s.version - 1, CKEY_ECC_M, CKEY_LAYOUT_STANDARD) < 33)); |
| 154 | std::printf("33-character payload -> Profile 0 v%u (%d modules across) at level M\n", s.version, kScalableGrid[s.version - 1]); |
| 155 | |
| 156 | // ---- Byte-level ECC: correct up to parity/2 errors in every block ---- |
| 157 | for (int level = 0; level < 4; ++level) { |
| 158 | const auto payload = pseudo(ckey_scalable_capacity(14, static_cast<ckey_ecc_level>(level), CKEY_LAYOUT_STANDARD), 5u + level); |
| 159 | CKEY_CHECK(ckey_encode_scalable(payload.data(), payload.size(), 14, static_cast<ckey_ecc_level>(level), CKEY_LAYOUT_STANDARD, &s, &d) == CKEY_OK); |
| 160 | const auto blocks = scalable_blocks(s.codeword_length, level); |
| 161 | auto damaged = s; |
| 162 | // Interleaved position of (block b, byte j) is the count of earlier bytes. |
| 163 | for (size_t b = 0; b < blocks.size(); ++b) { |
| 164 | const size_t t = (blocks[b].n - blocks[b].k) / 2; |
| 165 | for (size_t e = 0; e < t; ++e) { |
| 166 | const size_t j = (e * 7 + 3) % blocks[b].n; size_t pos = 0; |
| 167 | for (size_t jj = 0; jj < j; ++jj) for (size_t bb = 0; bb < blocks.size(); ++bb) pos += jj < blocks[bb].n; |
| 168 | for (size_t bb = 0; bb < b; ++bb) pos += j < blocks[bb].n; |
| 169 | damaged.codeword[pos] ^= static_cast<uint8_t>(0x5a + e); |
| 170 | } |
| 171 | } |
| 172 | std::vector<uint8_t> out(65535); size_t len = 0; |
| 173 | CKEY_CHECK(ckey_decode_scalable(damaged.codeword, damaged.codeword_length, 14, CKEY_LAYOUT_STANDARD, out.data(), out.size(), &len, &d) == CKEY_OK); |
| 174 | CKEY_CHECK(len == payload.size() && std::equal(payload.begin(), payload.end(), out.begin())); |
| 175 | } |
| 176 | |
| 177 | // ---- Every version: render, rasterize, decode; the keys identify the version ---- |
| 178 | for (int v = 1; v <= kScalableVersions; ++v) { |
| 179 | for (int kind = 0; kind < 3; ++kind) { |
| 180 | const size_t cap = ckey_scalable_capacity(v, CKEY_ECC_M, CKEY_LAYOUT_STANDARD); |
| 181 | const auto payload = kind == 0 ? pseudo(cap, 100u + v) : std::vector<uint8_t>(cap, kind == 1 ? 0x00 : 0xff); |
| 182 | CKEY_CHECK(ckey_encode_scalable(payload.data(), payload.size(), static_cast<uint32_t>(v), CKEY_ECC_M, CKEY_LAYOUT_STANDARD, &s, &d) == CKEY_OK); |
| 183 | const uint32_t detected = image_round_trip(s, payload, 6.0, false); |
| 184 | if (detected != static_cast<uint32_t>(v)) std::printf("version %d payload kind %d: detected %u\n", v, kind, detected); |
| 185 | CKEY_CHECK(detected == static_cast<uint32_t>(v)); |
| 186 | } |
| 187 | const auto payload = pseudo(ckey_scalable_capacity(v, CKEY_ECC_M, CKEY_LAYOUT_STANDARD), 900u + v); |
| 188 | CKEY_CHECK(ckey_encode_scalable(payload.data(), payload.size(), static_cast<uint32_t>(v), CKEY_ECC_M, CKEY_LAYOUT_STANDARD, &s, &d) == CKEY_OK); |
| 189 | CKEY_CHECK(image_round_trip(s, payload, 8.0, true) == static_cast<uint32_t>(v)); |
| 190 | } |
| 191 | std::printf("all %d versions decode from rendered images (6 px/module clean, 8 px/module blurred) and are identified by their keys\n", kScalableVersions); |
| 192 | // ---- Known-answer vectors: FNV-1a-64 over every site in placement order |
| 193 | // of (identity little-endian, printed value), payload "CKey renderer |
| 194 | // test" at level M. Integer-only, so platform independent. ---- |
| 195 | { |
| 196 | const std::string text = "CKey renderer test"; |
| 197 | for (const auto &[version, expected] : {std::pair{2u, kScalableKatV2}, std::pair{31u, kScalableKatV31}}) { |
| 198 | CKEY_CHECK(ckey_encode_scalable(reinterpret_cast<const uint8_t *>(text.data()), text.size(), version, CKEY_ECC_M, CKEY_LAYOUT_STANDARD, &s, &d) == CKEY_OK); |
| 199 | const ScalableLayout &L = scalable_layout(static_cast<int>(version)); |
| 200 | uint64_t hash = 0xcbf29ce484222325ull; |
| 201 | const auto fnv = [&](uint8_t b) { hash = (hash ^ b) * 0x100000001b3ull; }; |
| 202 | for (size_t i = 0; i < L.nodes.size(); ++i) { for (int k = 0; k < 4; ++k) fnv(static_cast<uint8_t>(L.nodes[i].id >> (8 * k))); fnv(scalable_site_value(L, &s, i) ? 1 : 0); } |
| 203 | std::printf("Profile 0 v%u conformance hash %016llx\n", version, static_cast<unsigned long long>(hash)); |
| 204 | CKEY_CHECK(hash == expected); |
| 205 | } |
| 206 | } |
| 207 | // ---- Design system ---- |
| 208 | { |
| 209 | ckey_style base; ckey_style_default(&base); |
| 210 | const auto payload = pseudo(ckey_scalable_capacity(11, CKEY_ECC_M, CKEY_LAYOUT_STANDARD), 4242); |
| 211 | CKEY_CHECK(ckey_encode_scalable(payload.data(), payload.size(), 11, CKEY_ECC_M, CKEY_LAYOUT_STANDARD, &s, &d) == CKEY_OK); |
| 212 | CKEY_CHECK(ckey_check_scalable_style(&s, &base, &d) == CKEY_OK); |
| 213 | CKEY_CHECK(render_styled(s, base) == render(s)); // the plain renderer is the default style |
| 214 | |
| 215 | // Every payload-mark shape option decodes, at a coarse and a fine version. |
| 216 | for (uint32_t version : {2u, 20u}) { |
| 217 | const auto data = pseudo(ckey_scalable_capacity(version, CKEY_ECC_M, CKEY_LAYOUT_STANDARD), 77 + version); |
| 218 | CKEY_CHECK(ckey_encode_scalable(data.data(), data.size(), version, CKEY_ECC_M, CKEY_LAYOUT_STANDARD, &s, &d) == CKEY_OK); |
| 219 | const double unit = 7.0 / (29.0 / ckey_scalable_modules_across(version)); |
| 220 | for (int shape = 0; shape < 4; ++shape) for (int rings = 0; rings < 2; ++rings) for (int rays = 0; rays < 2; ++rays) { |
| 221 | ckey_style st = base; |
| 222 | st.modules = static_cast<ckey_module_shape>(shape); st.rings = static_cast<ckey_ring_style>(rings); st.rays = static_cast<ckey_ray_cap>(rays); |
| 223 | const uint32_t got = decode_svg(render_styled(s, st), unit, data); |
| 224 | if (got != version) std::printf("style shape %d rings %d rays %d failed at v%u\n", shape, rings, rays, version); |
| 225 | CKEY_CHECK(got == version); |
| 226 | } |
| 227 | } |
| 228 | std::printf("design system: 16 mark-shape combinations decode at v2 and v20\n"); |
| 229 | |
| 230 | // Concept-art look: champagne sheen, navy vignette, sage/grey accents, depth, grain. |
| 231 | ckey_style art; CKEY_CHECK(ckey_style_preset_fill(&art, CKEY_PRESET_CONCEPT) == CKEY_OK); |
| 232 | CKEY_CHECK(ckey_style_preset_fill(&art, static_cast<ckey_style_preset>(7)) == CKEY_INVALID_ARGUMENT); |
| 233 | CKEY_CHECK(ckey_style_preset_fill(&art, CKEY_PRESET_CONCEPT) == CKEY_OK); |
| 234 | // The default is black on white (specification §4). |
| 235 | CKEY_CHECK(base.ink.r == 0 && base.ink.g == 0 && base.background.r == 255 && base.background.b == 255); |
| 236 | CKEY_CHECK(ckey_encode_scalable(payload.data(), payload.size(), 11, CKEY_ECC_M, CKEY_LAYOUT_STANDARD, &s, &d) == CKEY_OK); |
| 237 | CKEY_CHECK(ckey_check_scalable_style(&s, &art, &d) == CKEY_OK); |
| 238 | std::printf("concept style: %s\n", d.message); |
| 239 | CKEY_CHECK(decode_svg(render_styled(s, art), 7.0 / 0.5, payload) == 11); |
| 240 | |
| 241 | // The checker refuses near-invisible styles, with a reason; lower contrast is a warning |
| 242 | // (certification's photo sweep decides whether it scans). |
| 243 | ckey_style low = base; low.ink = low.ink_shade = {0xe8, 0xe8, 0xe8}; |
| 244 | CKEY_CHECK(ckey_check_scalable_style(&s, &low, &d) == CKEY_INVALID_ARGUMENT); |
| 245 | std::printf("refused: %s\n", d.message); |
| 246 | ckey_style mixed = base; mixed.background = mixed.background_edge = {0x80, 0x80, 0x80}; mixed.accents[0] = {0xff, 0xff, 0xff}; mixed.accent_count = 1; mixed.accent_percent = 10; |
| 247 | CKEY_CHECK(ckey_check_scalable_style(&s, &mixed, &d) == CKEY_INVALID_ARGUMENT); |
| 248 | ckey_style light = base; light.background = light.background_edge = {0xf4, 0xf1, 0xe8}; light.ink = light.ink_shade = {0x10, 0x10, 0x10}; |
| 249 | CKEY_CHECK(ckey_check_scalable_style(&s, &light, &d) == CKEY_OK); |
| 250 | light.shadow = 0.6; CKEY_CHECK(ckey_check_scalable_style(&s, &light, &d) == CKEY_OK && std::string(d.message).find("certify it") != std::string::npos); |
| 251 | size_t n = 0; CKEY_CHECK(ckey_render_scalable_styled_svg(&s, &low, 8, nullptr, 0, &n) == CKEY_INVALID_ARGUMENT); |
| 252 | |
| 253 | // Logos: the empty centre costs nothing; a larger logo needs a plate and |
| 254 | // must stay within half the worst block's correction. The largest logo |
| 255 | // the checker accepts at each level still decodes (hidden marks are |
| 256 | // corrected by Reed-Solomon, as with QR). |
| 257 | ckey_style logo = base; logo.logo_href = "data:image/png;base64,iVBORw0KGgo="; |
| 258 | CKEY_CHECK(ckey_check_scalable_style(&s, &logo, &d) == CKEY_OK && ckey_scalable_logo_load(&s, 0) == 0); |
| 259 | logo.logo_radius = 5.0; CKEY_CHECK(ckey_check_scalable_style(&s, &logo, &d) == CKEY_INVALID_ARGUMENT); // no plate |
| 260 | logo.logo_plate = 1; logo.logo_radius = ckey_scalable_logo_max_radius(11, CKEY_LAYOUT_STANDARD) + 0.1; |
| 261 | CKEY_CHECK(ckey_check_scalable_style(&s, &logo, &d) == CKEY_INVALID_ARGUMENT); // would hide the Ring Key |
| 262 | for (uint32_t version : {6u, 11u, 20u}) for (int level = 0; level < 4; ++level) { |
| 263 | const auto data = pseudo(ckey_scalable_capacity(version, static_cast<ckey_ecc_level>(level), CKEY_LAYOUT_STANDARD), 900 + version * 4 + level); |
| 264 | CKEY_CHECK(ckey_encode_scalable(data.data(), data.size(), version, static_cast<ckey_ecc_level>(level), CKEY_LAYOUT_STANDARD, &s, &d) == CKEY_OK); |
| 265 | double best = 0; |
| 266 | for (double r = 3.0; r <= ckey_scalable_logo_max_radius(version, CKEY_LAYOUT_STANDARD); r += 0.05) { logo.logo_radius = r; if (ckey_check_scalable_style(&s, &logo, &d) == CKEY_OK) best = r; } |
| 267 | logo.logo_radius = best; |
| 268 | const uint32_t got = best > 0 ? decode_svg(render_styled(s, logo), 7.0 / (29.0 / ckey_scalable_modules_across(version)), data) : version; |
| 269 | std::printf("v%u level %c: largest accepted logo radius %.2f (load %.0f%%) -> %s\n", version, "LMQH"[level], best, |
| 270 | 100 * ckey_scalable_logo_load(&s, best), got == version ? "decodes" : "FAILS"); |
| 271 | CKEY_CHECK(got == version); |
| 272 | } |
| 273 | } |
| 274 | // ---- Print: device-grid bitmaps, and P0.1's advantage on a printer model ---- |
| 275 | { |
| 276 | const std::string text = "CKEY-CAL P0.1 120B 0.42in "; |
| 277 | std::string payload = text; while (payload.size() < 120) payload += "0123456789"; |
| 278 | payload.resize(120); |
| 279 | const std::vector<uint8_t> bytes(payload.begin(), payload.end()); |
| 280 | // Old inkjet on plain paper (600 dpi, 0.045 mm ink spread), black on paper, camera 23 px/mm. |
| 281 | const printsim::Printer inkjet{600, 0.045, 0.03}; const printsim::Camera camera{23, 0.7, 4}; |
| 282 | bool decoded[2] = {false, false}; |
| 283 | for (int family = 0; family < 2; ++family) { |
| 284 | ckey_scalable_symbol ps{}; ckey_diagnostics pd{}; |
| 285 | CKEY_CHECK(ckey_encode_scalable(bytes.data(), bytes.size(), 0, CKEY_ECC_M, static_cast<ckey_scalable_layout>(family), &ps, &pd) == CKEY_OK); |
| 286 | size_t side = 0; |
| 287 | CKEY_CHECK(ckey_render_scalable_print(&ps, nullptr, 0.5, 600, nullptr, 0, &side) == CKEY_INSUFFICIENT_BUFFER && side == 300); |
| 288 | std::vector<uint8_t> bitmap(side * side); |
| 289 | CKEY_CHECK(ckey_render_scalable_print(&ps, nullptr, 0.5, 600, bitmap.data(), bitmap.size(), &side) == CKEY_OK); |
| 290 | for (uint8_t v : bitmap) CKEY_CHECK(v == 0 || v == 255); // no grey for a printer to halftone |
| 291 | int cam_side = 0; |
| 292 | const auto frame = printsim::print_and_capture(render(ps), 0.42 * 25.4, inkjet, camera, true, 99, cam_side); |
| 293 | const double k = cam_side / 31.0, q[8] = {k, k, 30 * k, k, 30 * k, 30 * k, k, 30 * k}; |
| 294 | std::vector<uint8_t> out(65535); size_t len = 0; uint32_t version = 0, layout = 0; |
| 295 | decoded[family] = ckey_decode_scalable_gray8(frame.data(), cam_side, cam_side, cam_side, q, out.data(), out.size(), &len, &version, &layout, &pd) == CKEY_OK && |
| 296 | len == bytes.size() && std::equal(bytes.begin(), bytes.end(), out.begin()); |
| 297 | } |
| 298 | std::printf("120-byte payload printed 0.42 in wide on the old-inkjet model: P0 %s, P0.1 %s\n", decoded[0] ? "decodes" : "fails", decoded[1] ? "decodes" : "fails"); |
| 299 | CKEY_CHECK(!decoded[0] && decoded[1]); |
| 300 | |
| 301 | // P0.1 conformance vectors (same method as Profile 0's). |
| 302 | const std::string kat = "CKey renderer test"; |
| 303 | for (const auto &[version, expected] : {std::pair{3u, kPrintKatV3}, std::pair{31u, kPrintKatV31}}) { |
| 304 | ckey_scalable_symbol ps{}; ckey_diagnostics pd{}; |
| 305 | CKEY_CHECK(ckey_encode_scalable(reinterpret_cast<const uint8_t *>(kat.data()), kat.size(), version, CKEY_ECC_M, CKEY_LAYOUT_PRINT, &ps, &pd) == CKEY_OK); |
| 306 | const ScalableLayout &L = scalable_layout(static_cast<int>(version), 1); |
| 307 | uint64_t hash = 0xcbf29ce484222325ull; |
| 308 | const auto fnv = [&](uint8_t b) { hash = (hash ^ b) * 0x100000001b3ull; }; |
| 309 | for (size_t i = 0; i < L.nodes.size(); ++i) { for (int k = 0; k < 4; ++k) fnv(static_cast<uint8_t>(L.nodes[i].id >> (8 * k))); fnv(scalable_site_value(L, &ps, i) ? 1 : 0); } |
| 310 | std::printf("Profile 0.1 v%u conformance hash %016llx\n", version, static_cast<unsigned long long>(hash)); |
| 311 | CKEY_CHECK(hash == expected); |
| 312 | } |
| 313 | } |
| 314 | std::printf("CKey Profile 0 tests passed\n"); |
| 315 | } |