Viewing is open to everyone. Downloading and starring need a GitHub sign-in.
Sign in with GitHub
api_certify.inc
v0.1.0-build.85
Latest
Commit ede38f5
Sign in with GitHub to download. Each download is recorded with your GitHub username.
| 1 | // Public C ABI: optical certification (specification §4). Included after |
| 2 | // core_internal.inc. It renders with ckey_render and decodes with ckey_scan, |
| 3 | // so it is its own library, ckey_certify. |
| 4 | |
| 5 | namespace { |
| 6 | |
| 7 | |
| 8 | // Straight-alpha RGBA over an opaque backdrop colour or image, to luma 0..1. |
| 9 | std::vector<double> composite_luma(const std::vector<uint8_t> &rgba, int side, const ckey_rgb *colour, const ckey_backdrop *image) { |
| 10 | std::vector<double> luma(static_cast<size_t>(side) * side); |
| 11 | const size_t stride = image && image->rgba ? (image->stride ? image->stride : static_cast<size_t>(image->width) * 4) : 0; |
| 12 | for (int y = 0; y < side; ++y) for (int x = 0; x < side; ++x) { |
| 13 | const size_t i = static_cast<size_t>(y) * side + x; |
| 14 | double br = 1, bg = 1, bb = 1; |
| 15 | if (colour) { br = colour->r / 255.0; bg = colour->g / 255.0; bb = colour->b / 255.0; } |
| 16 | else if (image && image->rgba) { |
| 17 | const uint32_t sx = std::min<uint32_t>(image->width - 1, static_cast<uint32_t>((x + .5) * image->width / side)); |
| 18 | const uint32_t sy = std::min<uint32_t>(image->height - 1, static_cast<uint32_t>((y + .5) * image->height / side)); |
| 19 | const uint8_t *p = image->rgba + sy * stride + static_cast<size_t>(sx) * 4; |
| 20 | const double a = p[3] / 255.0; // the host's pixels are composited over white |
| 21 | br = 1 - a + a * p[0] / 255.0; bg = 1 - a + a * p[1] / 255.0; bb = 1 - a + a * p[2] / 255.0; |
| 22 | } |
| 23 | const double a = rgba[4 * i + 3] / 255.0; |
| 24 | const double r = a * rgba[4 * i] / 255.0 + (1 - a) * br, g = a * rgba[4 * i + 1] / 255.0 + (1 - a) * bg, b = a * rgba[4 * i + 2] / 255.0 + (1 - a) * bb; |
| 25 | luma[i] = 0.2126 * r + 0.7152 * g + 0.0722 * b; |
| 26 | } |
| 27 | return luma; |
| 28 | } |
| 29 | |
| 30 | void gaussian_blur(std::vector<double> &img, int side, double sigma) { |
| 31 | if (sigma <= 0.05) return; |
| 32 | const int r = std::max(1, static_cast<int>(std::ceil(3 * sigma))); |
| 33 | std::vector<double> k(static_cast<size_t>(2 * r + 1)); double sum = 0; |
| 34 | for (int i = -r; i <= r; ++i) { k[static_cast<size_t>(i + r)] = std::exp(-0.5 * i * i / (sigma * sigma)); sum += k[static_cast<size_t>(i + r)]; } |
| 35 | for (auto &v : k) v /= sum; |
| 36 | std::vector<double> tmp(img.size()); |
| 37 | for (int y = 0; y < side; ++y) for (int x = 0; x < side; ++x) { |
| 38 | double acc = 0; for (int i = -r; i <= r; ++i) acc += k[static_cast<size_t>(i + r)] * img[static_cast<size_t>(y) * side + std::clamp(x + i, 0, side - 1)]; |
| 39 | tmp[static_cast<size_t>(y) * side + x] = acc; |
| 40 | } |
| 41 | for (int y = 0; y < side; ++y) for (int x = 0; x < side; ++x) { |
| 42 | double acc = 0; for (int i = -r; i <= r; ++i) acc += k[static_cast<size_t>(i + r)] * tmp[static_cast<size_t>(std::clamp(y + i, 0, side - 1)) * side + x]; |
| 43 | img[static_cast<size_t>(y) * side + x] = acc; |
| 44 | } |
| 45 | } |
| 46 | |
| 47 | double percentile(std::vector<double> v, double q) { |
| 48 | if (v.empty()) return 0; |
| 49 | const size_t k = std::min(v.size() - 1, static_cast<size_t>(q * (v.size() - 1))); |
| 50 | std::nth_element(v.begin(), v.begin() + static_cast<long>(k), v.end()); |
| 51 | return v[k]; |
| 52 | } |
| 53 | |
| 54 | // ---- Realistic scan sweep ---- |
| 55 | // A style is judged by how small it can be before a real reader can no longer |
| 56 | // find and decode it in a photograph, compared with plain black on white. |
| 57 | // Each scene places the symbol in a cluttered frame, tilts it (rotation and |
| 58 | // keystone), adds glare, defocus and sensor noise, and runs the full reader |
| 59 | // pipeline: ckey_find_borders_gray8 -> ckey_refine_quad_gray8 -> decode. The |
| 60 | // sweep walks down a ladder of camera resolutions (pixels per module). |
| 61 | |
| 62 | constexpr double kLadder[] = {9.0, 7.0, 5.5, 4.5, 3.75, 3.0}; |
| 63 | constexpr int kLadderSteps = 6; |
| 64 | constexpr int kMaxFrame = 2600; // keeps the sweep fast enough for browsers |
| 65 | constexpr int kStartStep = 2; // the search starts mid-ladder (5.5 px per module) and walks out |
| 66 | |
| 67 | struct Scene { double rotation_deg, keystone; uint32_t seed; }; |
| 68 | constexpr Scene kScenes[] = {{8.0, 0.04, 0x53434e31u}, {-5.0, 0.0, 0x53434e32u}}; |
| 69 | |
| 70 | bool scene_decodes(const ckey_scalable_symbol &sym, const ckey_style &draw, bool transparent, const ckey_rgb *ground_colour, |
| 71 | const ckey_backdrop *ground_image, const ckey_scan_model &m, double ppm, const Scene &sc, |
| 72 | const std::vector<uint8_t> &payload, bool &too_big) { |
| 73 | const ScalableLayout &L = scalable_layout(static_cast<int>(sym.version), static_cast<int>(sym.layout)); |
| 74 | const int side = std::max(64, static_cast<int>(std::ceil(31.0 * ppm / L.pitch))); |
| 75 | const int margin = std::max(40, side < 900 ? side * 3 / 10 : side * 3 / 25); // dense symbols: a tighter crop |
| 76 | const int F = side + 2 * margin; |
| 77 | too_big = F > kMaxFrame; |
| 78 | if (too_big) return false; |
| 79 | ckey_raster_options o; ckey_raster_options_default(&o); |
| 80 | o.size_px = static_cast<uint32_t>(side); o.supersample = 2; o.transparent = transparent ? 1u : 0u; |
| 81 | std::vector<uint8_t> rgba(static_cast<size_t>(side) * side * 4); size_t need = 0; |
| 82 | if (ckey_render_scalable_raster(&sym, &draw, &o, rgba.data(), rgba.size(), &need) != CKEY_OK) return false; |
| 83 | const auto symbol = composite_luma(rgba, side, ground_colour, ground_image); |
| 84 | // The frame: a lit desk with clutter (deterministic per scene). |
| 85 | std::vector<double> frame(static_cast<size_t>(F) * F); |
| 86 | uint32_t seed = sc.seed; |
| 87 | const auto rnd = [&] { seed = seed * 1664525u + 1013904223u; return (seed >> 8) / 16777216.0; }; |
| 88 | for (int y = 0; y < F; ++y) for (int x = 0; x < F; ++x) frame[static_cast<size_t>(y) * F + x] = 0.42 + 0.22 * x / F + 0.12 * y / F; |
| 89 | for (int k = 0; k < 10; ++k) { |
| 90 | const int x0 = static_cast<int>(rnd() * F), y0 = static_cast<int>(rnd() * F), w = 8 + static_cast<int>(rnd() * F / 4), h = 8 + static_cast<int>(rnd() * F / 4); |
| 91 | const double v = rnd(); |
| 92 | for (int y = y0; y < std::min(F, y0 + h); ++y) for (int x = x0; x < std::min(F, x0 + w); ++x) frame[static_cast<size_t>(y) * F + x] = v; |
| 93 | } |
| 94 | // Symbol corners in the frame: rotated about the centre, top edge narrowed. |
| 95 | const double c = F / 2.0, half = side / 2.0, a = sc.rotation_deg * kTau / 360; |
| 96 | const double corner[4][2] = {{-1, -1}, {1, -1}, {1, 1}, {-1, 1}}; |
| 97 | double dst[8]; |
| 98 | for (int i = 0; i < 4; ++i) { |
| 99 | double x = corner[i][0] * half, y = corner[i][1] * half; |
| 100 | if (corner[i][1] < 0) x *= 1.0 - sc.keystone; |
| 101 | dst[2 * i] = c + x * std::cos(a) - y * std::sin(a); dst[2 * i + 1] = c + x * std::sin(a) + y * std::cos(a); |
| 102 | } |
| 103 | const double src[8] = {0, 0, static_cast<double>(side), 0, static_cast<double>(side), static_cast<double>(side), 0, static_cast<double>(side)}; |
| 104 | double h[8]; |
| 105 | if (!solve_homography(dst, src, h)) return false; |
| 106 | for (int y = 0; y < F; ++y) for (int x = 0; x < F; ++x) { |
| 107 | ImagePoint p; |
| 108 | if (!map_point(h, x + 0.5, y + 0.5, p)) continue; |
| 109 | const double u = p.x - 0.5, v = p.y - 0.5; |
| 110 | if (u < 0 || v < 0 || u >= side - 1 || v >= side - 1) continue; |
| 111 | const int iu = static_cast<int>(u), iv = static_cast<int>(v); const double fu = u - iu, fv = v - iv; |
| 112 | const auto at = [&](int X, int Y) { return symbol[static_cast<size_t>(Y) * side + X]; }; |
| 113 | frame[static_cast<size_t>(y) * F + x] = (at(iu, iv) * (1 - fu) + at(iu + 1, iv) * fu) * (1 - fv) + (at(iu, iv + 1) * (1 - fu) + at(iu + 1, iv + 1) * fu) * fv; |
| 114 | } |
| 115 | for (auto &v : frame) v = v * (1 - m.glare) + m.glare; |
| 116 | gaussian_blur(frame, F, m.blur_px); |
| 117 | std::vector<uint8_t> gray(frame.size()); |
| 118 | for (size_t i = 0; i < frame.size(); ++i) { |
| 119 | const double u1 = rnd() + 1e-9, u2 = rnd(); |
| 120 | const double n = std::sqrt(-2 * std::log(u1)) * std::cos(kTau * u2) * m.noise; |
| 121 | gray[i] = static_cast<uint8_t>(std::lround(std::clamp(frame[i] + n, 0.0, 1.0) * 255)); |
| 122 | } |
| 123 | double borders[6 * 8]; |
| 124 | const size_t found = ckey_find_borders_gray8(gray.data(), F, F, F, borders, 6); |
| 125 | std::vector<uint8_t> out(65535); |
| 126 | for (size_t i = 0; i < found; ++i) { |
| 127 | double q[8]; size_t len = 0; uint32_t version = 0, layout = 0; ckey_diagnostics d{}; |
| 128 | if (ckey_refine_quad_gray8(gray.data(), F, F, F, borders + 8 * i, CKEY_FIELD_SCALABLE, q) > 0 && |
| 129 | ckey_decode_scalable_gray8(gray.data(), F, F, F, q, out.data(), out.size(), &len, &version, &layout, &d) == CKEY_OK) |
| 130 | return len == payload.size() && std::equal(payload.begin(), payload.end(), out.begin()); |
| 131 | } |
| 132 | return false; |
| 133 | } |
| 134 | |
| 135 | struct Sweep { double smallest_ppm = 0; int smallest_step = -1; uint32_t scenes = 0, passed = 0; }; |
| 136 | |
| 137 | // Walks the ladder from large to small; the result is the smallest resolution |
| 138 | // at which every scene decoded (and every larger one did too). |
| 139 | Sweep sweep(const ckey_scalable_symbol &sym, const ckey_style &style, bool transparent, const ckey_rgb *colour, |
| 140 | const ckey_backdrop *image, const ckey_scan_model &m, const std::vector<uint8_t> &payload) { |
| 141 | Sweep r; |
| 142 | ckey_style draw = style; draw.policy = CKEY_POLICY_OVERRIDE; // test exactly this style |
| 143 | // 1 = every scene decoded, 0 = one failed, -1 = too large to simulate (treated as decoding |
| 144 | // when a smaller size decodes, since a larger image of the same symbol only gets easier). |
| 145 | const auto step_result = [&](int step) { |
| 146 | for (const Scene &sc : kScenes) { |
| 147 | bool too_big = false; |
| 148 | const bool ok = scene_decodes(sym, draw, transparent, colour, image, m, kLadder[step], sc, payload, too_big); |
| 149 | if (too_big) return -1; |
| 150 | ++r.scenes; r.passed += ok ? 1 : 0; |
| 151 | if (!ok) return 0; |
| 152 | } |
| 153 | return 1; |
| 154 | }; |
| 155 | int start = kStartStep; |
| 156 | int res = step_result(start); |
| 157 | while (res == -1 && start + 1 < kLadderSteps) res = step_result(++start); // huge symbols: start smaller |
| 158 | if (res == 1) { |
| 159 | r.smallest_step = start; r.smallest_ppm = kLadder[start]; |
| 160 | for (int step = start + 1; step < kLadderSteps && step_result(step) == 1; ++step) { r.smallest_step = step; r.smallest_ppm = kLadder[step]; } |
| 161 | } else { |
| 162 | for (int step = start - 1; step >= 0; --step) { |
| 163 | const int up = step_result(step); |
| 164 | if (up == 1) { r.smallest_step = step; r.smallest_ppm = kLadder[step]; break; } |
| 165 | } |
| 166 | } |
| 167 | return r; |
| 168 | } |
| 169 | |
| 170 | // Plain black on white for the same symbol: the reference a style is held to. |
| 171 | Sweep plain_sweep(const ckey_scalable_symbol &sym, const ckey_scan_model &m, const std::vector<uint8_t> &payload) { |
| 172 | // Cached per symbol and model: the reference does not depend on the style being judged. |
| 173 | static std::mutex lock; |
| 174 | static std::vector<std::pair<std::string, Sweep>> cache; |
| 175 | std::string key(reinterpret_cast<const char *>(sym.codeword), sym.codeword_length); |
| 176 | char tail[96]; std::snprintf(tail, sizeof(tail), "|%u|%u|%.3f|%.3f|%.3f|%.3f", sym.version, sym.layout, m.px_per_module, m.blur_px, m.noise, m.glare); |
| 177 | key += tail; |
| 178 | { std::lock_guard<std::mutex> g(lock); for (const auto &[k, v] : cache) if (k == key) return v; } |
| 179 | ckey_style plain; ckey_style_default(&plain); |
| 180 | const Sweep r = sweep(sym, plain, false, nullptr, nullptr, m, payload); |
| 181 | std::lock_guard<std::mutex> g(lock); |
| 182 | if (cache.size() >= 16) cache.erase(cache.begin()); |
| 183 | cache.push_back({key, r}); |
| 184 | return r; |
| 185 | } |
| 186 | |
| 187 | // Luma separation of marks vs gaps after compositing, at the ladder's top size. |
| 188 | double separation_of(const ckey_scalable_symbol &sym, const ckey_style &style, bool transparent, const ckey_rgb *colour, const ckey_backdrop *image) { |
| 189 | const ScalableLayout &L = scalable_layout(static_cast<int>(sym.version), static_cast<int>(sym.layout)); |
| 190 | const int side = std::clamp(static_cast<int>(std::ceil(31.0 * 6.0 / L.pitch)), 64, 1600); |
| 191 | ckey_style draw = style; draw.policy = CKEY_POLICY_OVERRIDE; |
| 192 | ckey_raster_options o; ckey_raster_options_default(&o); |
| 193 | o.size_px = static_cast<uint32_t>(side); o.supersample = 2; o.transparent = transparent ? 1u : 0u; |
| 194 | std::vector<uint8_t> rgba(static_cast<size_t>(side) * side * 4), mask(static_cast<size_t>(side) * side); size_t need = 0; |
| 195 | if (ckey_render_scalable_raster(&sym, &draw, &o, rgba.data(), rgba.size(), &need) != CKEY_OK) return 0; |
| 196 | ckey_style mask_style; ckey_style_default(&mask_style); |
| 197 | mask_style.modules = style.modules; mask_style.rings = style.rings; mask_style.rays = style.rays; |
| 198 | mask_style.logo_href = style.logo_href; mask_style.logo_radius = style.logo_radius; mask_style.logo_plate = style.logo_plate; |
| 199 | mask_style.policy = CKEY_POLICY_OVERRIDE; |
| 200 | ckey_raster_options mo = o; mo.transparent = 0; mo.format = CKEY_PIXELS_GRAY8; mo.materials = 0; |
| 201 | if (ckey_render_scalable_raster(&sym, &mask_style, &mo, mask.data(), mask.size(), &need) != CKEY_OK) return 0; |
| 202 | const auto luma = composite_luma(rgba, side, colour, image); |
| 203 | std::vector<double> marks, gaps; |
| 204 | for (size_t i = 0; i < luma.size(); i += 3) { if (mask[i] < 16) marks.push_back(luma[i]); else if (mask[i] > 239) gaps.push_back(luma[i]); } |
| 205 | const bool dark_marks = percentile(marks, 0.5) < percentile(gaps, 0.5); |
| 206 | return dark_marks ? percentile(gaps, 0.05) - percentile(marks, 0.95) : percentile(marks, 0.05) - percentile(gaps, 0.95); |
| 207 | } |
| 208 | |
| 209 | const char *grade_name(int g) { return g >= 3 ? "excellent" : g == 2 ? "good" : g == 1 ? "marginal" : "fails"; } |
| 210 | |
| 211 | void strip_materials(ckey_style &st) { |
| 212 | st.ink_shade = st.ink; st.background_edge = st.background; |
| 213 | st.bevel = st.shadow = st.texture = 0; |
| 214 | } |
| 215 | |
| 216 | } // namespace |
| 217 | |
| 218 | extern "C" { |
| 219 | |
| 220 | void ckey_scan_model_default(ckey_scan_model *m) { |
| 221 | if (!m) return; |
| 222 | *m = ckey_scan_model{}; |
| 223 | m->abi_version = CKEY_ABI_VERSION; m->px_per_module = 8.0; m->blur_px = 0.9; m->noise = 0.03; m->glare = 0.2; m->trials = 3; |
| 224 | } |
| 225 | |
| 226 | ckey_status ckey_certify_scalable_style(const ckey_scalable_symbol *s, const ckey_style *style, uint32_t transparent, |
| 227 | const ckey_backdrop *backdrop, const ckey_scan_model *model, ckey_style *out_style, |
| 228 | ckey_certification *out) { |
| 229 | if (!out) return CKEY_INVALID_ARGUMENT; |
| 230 | *out = ckey_certification{}; out->abi_version = CKEY_ABI_VERSION; |
| 231 | const auto refuse = [&](const char *why) { std::snprintf(out->message, sizeof(out->message), "%s", why); return CKEY_INVALID_ARGUMENT; }; |
| 232 | if (!s || s->abi_version != CKEY_ABI_VERSION || s->version < 1 || s->version > CKEY_SCALABLE_VERSION_COUNT || s->ecc_level > 3 || |
| 233 | s->layout >= static_cast<uint32_t>(kScalableFamilies)) return refuse("certification needs a Profile 0 symbol"); |
| 234 | const ScalableLayout &L = scalable_layout(static_cast<int>(s->version), static_cast<int>(s->layout)); |
| 235 | if (s->codeword_length != L.codeword_bytes) return refuse("symbol codeword does not match its version"); |
| 236 | ckey_scan_model m; ckey_scan_model_default(&m); |
| 237 | if (model) m = *model; |
| 238 | if (m.abi_version != CKEY_ABI_VERSION || !std::isfinite(m.px_per_module) || m.px_per_module < 2 || m.px_per_module > 40 || |
| 239 | !std::isfinite(m.blur_px) || m.blur_px < 0 || m.blur_px > 10 || !std::isfinite(m.noise) || m.noise < 0 || m.noise > 0.5 || |
| 240 | !std::isfinite(m.glare) || m.glare < 0 || m.glare > 0.9 || m.trials < 1 || m.trials > 16) return refuse("invalid scan model"); |
| 241 | if (backdrop && (backdrop->colour_count > 16 || (backdrop->colour_count && !backdrop->colours) || |
| 242 | (backdrop->rgba && (!backdrop->width || !backdrop->height || (backdrop->stride && backdrop->stride < static_cast<size_t>(backdrop->width) * 4))))) |
| 243 | return refuse("invalid backdrop"); |
| 244 | ckey_style st; if (style) st = *style; else ckey_style_default(&st); |
| 245 | out->policy = st.policy; |
| 246 | const std::string structural = structural_problem(L, st); |
| 247 | if (!structural.empty()) return refuse(structural.c_str()); |
| 248 | std::vector<uint8_t> payload(65535); size_t payload_len = 0; ckey_diagnostics d{}; |
| 249 | if (ckey_decode_scalable(s->codeword, s->codeword_length, s->version, static_cast<ckey_scalable_layout>(s->layout), payload.data(), payload.size(), &payload_len, &d) != CKEY_OK) |
| 250 | return refuse("the symbol's own codeword does not decode"); |
| 251 | payload.resize(payload_len); |
| 252 | |
| 253 | std::vector<ckey_rgb> extra; |
| 254 | const bool has_backdrop = backdrop && (backdrop->colour_count || backdrop->rgba); |
| 255 | if (transparent && !has_backdrop) { |
| 256 | std::snprintf(out->message, sizeof(out->message), "background unknown: transparent output cannot be certified; pass the host backdrop"); |
| 257 | return st.policy == CKEY_POLICY_OVERRIDE ? CKEY_OK : CKEY_INVALID_ARGUMENT; |
| 258 | } |
| 259 | if (transparent) { |
| 260 | // The symbol's own background is not drawn: rules test the backdrop. |
| 261 | if (backdrop->colour_count) { st.background = backdrop->colours[0]; st.background_edge = backdrop->colours[0]; } |
| 262 | for (uint32_t i = 0; i < backdrop->colour_count; ++i) extra.push_back(backdrop->colours[i]); |
| 263 | if (backdrop->rgba) { // the backdrop image's darkest and lightest pixels join the contrast rules |
| 264 | const size_t stride = backdrop->stride ? backdrop->stride : static_cast<size_t>(backdrop->width) * 4; |
| 265 | ckey_rgb dark{255, 255, 255}, light{0, 0, 0}; double dl = 2, ll = -1; |
| 266 | for (uint32_t y = 0; y < backdrop->height; y += std::max(1u, backdrop->height / 64)) for (uint32_t x = 0; x < backdrop->width; x += std::max(1u, backdrop->width / 64)) { |
| 267 | const uint8_t *p = backdrop->rgba + y * stride + static_cast<size_t>(x) * 4; const double a = p[3] / 255.0; |
| 268 | const ckey_rgb c{static_cast<uint8_t>(std::lround(255 * (1 - a) + a * p[0])), static_cast<uint8_t>(std::lround(255 * (1 - a) + a * p[1])), static_cast<uint8_t>(std::lround(255 * (1 - a) + a * p[2]))}; |
| 269 | const double l = relative_luminance(c); |
| 270 | if (l < dl) { dl = l; dark = c; } if (l > ll) { ll = l; light = c; } |
| 271 | } |
| 272 | extra.push_back(dark); extra.push_back(light); |
| 273 | if (!backdrop->colour_count) { st.background = st.background_edge = dark; } |
| 274 | } |
| 275 | } |
| 276 | ckey_style work = st; |
| 277 | double contrast = 0, load = 0; |
| 278 | std::string advice; |
| 279 | // Adaptive repairs a hard failure up front; anything else is left for the sweep to judge. |
| 280 | if (st.policy == CKEY_POLICY_ADAPTIVE && !safety_problem(L, *s, work, extra, contrast, load).empty()) |
| 281 | out->adjustments |= adapt_style(L, *s, work, extra); |
| 282 | std::string problem = safety_problem(L, *s, work, extra, contrast, load, &advice); |
| 283 | // Grounds the symbol is judged on: its own background, or every backdrop. |
| 284 | std::vector<std::pair<const ckey_rgb *, const ckey_backdrop *>> grounds; |
| 285 | if (!transparent) grounds.push_back({nullptr, nullptr}); |
| 286 | else { |
| 287 | for (uint32_t i = 0; i < backdrop->colour_count; ++i) grounds.push_back({&backdrop->colours[i], nullptr}); |
| 288 | if (backdrop->rgba && backdrop->width && backdrop->height) grounds.push_back({nullptr, backdrop}); |
| 289 | } |
| 290 | const auto evaluate = [&](const ckey_style &w, Sweep &worst, double &sep) { |
| 291 | worst = Sweep{}; worst.smallest_step = kLadderSteps; sep = 1.0; bool first = true; |
| 292 | for (const auto &[colour, image] : grounds) { |
| 293 | const Sweep r = sweep(*s, w, transparent != 0, colour, image, m, payload); |
| 294 | worst.scenes += r.scenes; worst.passed += r.passed; |
| 295 | if (first || r.smallest_step < worst.smallest_step) { worst.smallest_step = r.smallest_step; worst.smallest_ppm = r.smallest_ppm; } |
| 296 | first = false; |
| 297 | sep = std::min(sep, separation_of(*s, w, transparent != 0, colour, image)); |
| 298 | } |
| 299 | }; |
| 300 | const Sweep plain = plain_sweep(*s, m, payload); |
| 301 | const auto grade_of = [&](const Sweep &r) { |
| 302 | if (r.smallest_step < 0 || r.smallest_ppm > m.px_per_module + 1e-9) return 0; |
| 303 | if (plain.smallest_step < 0) return 2; // the reference itself fails: judge by the style alone |
| 304 | const int behind = plain.smallest_step - r.smallest_step; // ladder steps larger than plain needs |
| 305 | return behind <= 0 ? 3 : behind == 1 ? 2 : behind == 2 ? 1 : 0; |
| 306 | }; |
| 307 | Sweep result; double separation = 1.0; |
| 308 | evaluate(work, result, separation); |
| 309 | int grade = grade_of(result); |
| 310 | if (st.policy == CKEY_POLICY_ADAPTIVE && (!problem.empty() || grade < 2)) { |
| 311 | out->adjustments |= adapt_style(L, *s, work, extra); // now also the advisory contrast repairs |
| 312 | strip_materials(work); out->adjustments |= CKEY_ADJUST_MATERIALS; |
| 313 | problem = safety_problem(L, *s, work, extra, contrast, load, &advice); |
| 314 | evaluate(work, result, separation); |
| 315 | grade = grade_of(result); |
| 316 | } |
| 317 | out->min_contrast = contrast; out->logo_load = load; |
| 318 | out->trials = result.scenes; out->trials_passed = result.passed; out->feature_separation = separation; |
| 319 | out->certified = problem.empty() && grade >= 2 ? 1u : 0u; |
| 320 | char sweep_text[320]; |
| 321 | const auto ppm_text = [](const Sweep &r) { char b[48]; if (r.smallest_step < 0) std::snprintf(b, sizeof(b), "not even at %.0f px per module", kLadder[0]); else std::snprintf(b, sizeof(b), "down to %.2g px per module", r.smallest_ppm); return std::string(b); }; |
| 322 | std::snprintf(sweep_text, sizeof(sweep_text), "%s scan margin: found and decoded in modelled photos %s; plain black on white %s", |
| 323 | grade_name(grade), ppm_text(result).c_str(), ppm_text(plain).c_str()); |
| 324 | if (!advice.empty()) { std::strncat(sweep_text, "; note: ", sizeof(sweep_text) - std::strlen(sweep_text) - 1); std::strncat(sweep_text, advice.c_str(), sizeof(sweep_text) - std::strlen(sweep_text) - 1); } |
| 325 | std::string text = out->certified ? std::string("certified (") + grade_name(grade) + "); " + sweep_text |
| 326 | : !problem.empty() ? "not certified: " + problem + "; " + sweep_text |
| 327 | : std::string("not certified: ") + sweep_text; |
| 328 | if (out->adjustments) { |
| 329 | text += "; adaptive changes:"; |
| 330 | if (out->adjustments & CKEY_ADJUST_INK) text += " ink"; |
| 331 | if (out->adjustments & CKEY_ADJUST_BACKGROUND) text += " background"; |
| 332 | if (out->adjustments & CKEY_ADJUST_ACCENTS) text += " accents"; |
| 333 | if (out->adjustments & CKEY_ADJUST_EFFECTS) text += " depth"; |
| 334 | if (out->adjustments & CKEY_ADJUST_LOGO) text += " logo"; |
| 335 | if (out->adjustments & CKEY_ADJUST_MATERIALS) text += " materials"; |
| 336 | } |
| 337 | if (st.logo_href && *st.logo_href) text += "; the logo image itself is not modelled"; |
| 338 | std::snprintf(out->message, sizeof(out->message), "%s", text.c_str()); |
| 339 | if (out_style && st.policy == CKEY_POLICY_ADAPTIVE && out->certified) { |
| 340 | // Hand back the repaired style with the caller's own background |
| 341 | // fields restored for transparent output (they are not drawn). |
| 342 | if (transparent && style) { work.background = style->background; work.background_edge = style->background_edge; } |
| 343 | work.policy = CKEY_POLICY_STRICT; *out_style = work; |
| 344 | } else if (out_style && st.policy != CKEY_POLICY_ADAPTIVE) *out_style = style ? *style : st; |
| 345 | if (st.policy == CKEY_POLICY_OVERRIDE) return CKEY_OK; |
| 346 | return out->certified ? CKEY_OK : CKEY_INVALID_ARGUMENT; |
| 347 | } |
| 348 | |
| 349 | } // extern "C" |