Viewing is open to everyone. Downloading and starring need a GitHub sign-in.
Sign in with GitHub
ckey_web.cpp
v0.1.0-build.85
Latest
Commit ede38f5
Sign in with GitHub to download. Each download is recorded with your GitHub username.
| 1 | // CKey Web SDK bridge: a thin, malloc-friendly C surface over the public C ABI |
| 2 | // (include/ckey/ckey.h, include/ckey/telemetry.h) for the JavaScript wrapper in |
| 3 | // sdk/web/js/. It never reimplements encoding, decoding or rendering; it only |
| 4 | // marshals arguments and orchestrates the reader pipeline that |
| 5 | // ios/CKeyReader/CameraScanner.swift uses (find borders -> refine per field -> |
| 6 | // decode, Profile 0 first). |
| 7 | // |
| 8 | // Conventions for JS callers: |
| 9 | // * Every parameter and result is an int, uint32, double or pointer: no |
| 10 | // 64-bit integers cross the boundary (times and batch ids are doubles, |
| 11 | // which are exact up to 2^53), so no BigInt support is needed. |
| 12 | // * Functions that produce variable-length output return a malloc'd buffer |
| 13 | // (or 0 on failure) and set ckw_last_len(); the caller copies it out and |
| 14 | // frees it with ckw_free(). |
| 15 | // * On failure ckw_last_error() holds a NUL-terminated message (from the |
| 16 | // core's diagnostics where it gives one). |
| 17 | |
| 18 | "ckey/ckey.h" |
| 19 | |
| 20 | "ckey/telemetry.h" |
| 21 | |
| 22 | |
| 23 | <cmath> |
| 24 | <cstdint> |
| 25 | <cstdio> |
| 26 | <cstdlib> |
| 27 | <cstring> |
| 28 | <initializer_list> |
| 29 | <new> |
| 30 | <memory> |
| 31 | <string> |
| 32 | <vector> |
| 33 | |
| 34 | |
| 35 | <emscripten/emscripten.h> |
| 36 | extern "C" EMSCRIPTEN_KEEPALIVE |
| 37 | |
| 38 | extern "C" |
| 39 | |
| 40 | |
| 41 | namespace { |
| 42 | |
| 43 | char g_error[300] = ""; |
| 44 | size_t g_last_len = 0; |
| 45 | double g_last_batch_id = 0; |
| 46 | |
| 47 | void set_error(const char *msg) { |
| 48 | std::snprintf(g_error, sizeof g_error, "%s", msg ? msg : ""); |
| 49 | } |
| 50 | |
| 51 | void set_error_diag(const ckey_diagnostics &d, const char *fallback) { |
| 52 | if (d.message[0]) { |
| 53 | char tmp[sizeof d.message + 1]; |
| 54 | std::memcpy(tmp, d.message, sizeof d.message); |
| 55 | tmp[sizeof d.message] = '\0'; |
| 56 | set_error(tmp); |
| 57 | } else { |
| 58 | set_error(fallback); |
| 59 | } |
| 60 | } |
| 61 | |
| 62 | // A style plus storage for its logo URL (ckey_style holds a borrowed pointer). |
| 63 | struct WebStyle { |
| 64 | ckey_style style; |
| 65 | std::string logo_href; |
| 66 | }; |
| 67 | |
| 68 | uint8_t *dup_bytes(const void *src, size_t n) { |
| 69 | uint8_t *out = static_cast<uint8_t *>(std::malloc(n ? n : 1)); |
| 70 | if (!out) { set_error("out of memory"); return nullptr; } |
| 71 | if (n) std::memcpy(out, src, n); |
| 72 | g_last_len = n; |
| 73 | return out; |
| 74 | } |
| 75 | |
| 76 | uint64_t to_ms(double v) { |
| 77 | if (!(v > 0)) return 0; // also NaN |
| 78 | if (v >= 18446744073709549568.0) return UINT64_MAX; |
| 79 | return static_cast<uint64_t>(v); |
| 80 | } |
| 81 | |
| 82 | const ckey_style *style_of(const WebStyle *ws, ckey_style &fallback) { |
| 83 | if (ws) return &ws->style; |
| 84 | ckey_style_default(&fallback); |
| 85 | return &fallback; |
| 86 | } |
| 87 | |
| 88 | } // namespace |
| 89 | |
| 90 | // ---- Memory and errors ------------------------------------------------------ |
| 91 | |
| 92 | CKW_EXPORT void *ckw_malloc(uint32_t n) { return std::malloc(n ? n : 1); } |
| 93 | CKW_EXPORT void ckw_free(void *p) { std::free(p); } |
| 94 | CKW_EXPORT const char *ckw_last_error(void) { return g_error; } |
| 95 | CKW_EXPORT uint32_t ckw_last_len(void) { return static_cast<uint32_t>(g_last_len); } |
| 96 | CKW_EXPORT const char *ckw_software_version(void) { return ckey_get_software_version(); } |
| 97 | CKW_EXPORT uint32_t ckw_abi_version(void) { return ckey_get_abi_version(); } |
| 98 | |
| 99 | // ---- Safe Actions ------------------------------------------------------------- |
| 100 | |
| 101 | // Parses (key == null) or verifies a Safe Action and returns a malloc'd JSON |
| 102 | // object of its fields, or null with the reason in ckw_last_error(). The status |
| 103 | // is in "status": 0 OK, 6 bad signature, 7 expired. Every text field is limited by |
| 104 | // the core to a JSON-safe character set; params are hexadecimal. |
| 105 | CKW_EXPORT char *ckw_safe_action(const uint8_t *payload, uint32_t len, const uint8_t *key, double now, uint32_t max_lifetime) { |
| 106 | auto sa = std::make_unique<ckey_safe_action>(); |
| 107 | ckey_diagnostics diag{}; |
| 108 | const ckey_status st = key |
| 109 | ? ckey_safe_action_verify(payload, len, key, static_cast<uint64_t>(now), max_lifetime, sa.get(), &diag) |
| 110 | : ckey_safe_action_parse(payload, len, sa.get(), &diag); |
| 111 | if (st != CKEY_OK) { set_error_diag(diag, "not a valid Safe Action"); return nullptr; } |
| 112 | static const char hexd[] = "0123456789abcdef"; |
| 113 | std::string params; |
| 114 | for (size_t i = 0; i < sa->params_len; ++i) { params.push_back(hexd[sa->params[i] >> 4]); params.push_back(hexd[sa->params[i] & 15]); } |
| 115 | std::string json = std::string("{\"scope\":") + std::to_string(sa->scope) + ",\"issuer\":\"" + sa->issuer + "\",\"app\":\"" + sa->app + |
| 116 | "\",\"action\":\"" + sa->action + "\",\"key_id\":\"" + sa->key_id + "\",\"request_id\":\"" + sa->request_id + |
| 117 | "\",\"issued_at\":" + std::to_string(sa->issued_at) + ",\"expires_at\":" + std::to_string(sa->expires_at) + |
| 118 | ",\"params\":\"" + params + "\"}"; |
| 119 | char *out = static_cast<char *>(std::malloc(json.size() + 1)); |
| 120 | if (!out) { set_error("out of memory"); return nullptr; } |
| 121 | std::memcpy(out, json.c_str(), json.size() + 1); |
| 122 | g_last_len = json.size(); |
| 123 | return out; |
| 124 | } |
| 125 | |
| 126 | // ---- Symbols ---------------------------------------------------------------- |
| 127 | |
| 128 | CKW_EXPORT uint32_t ckw_scalable_capacity(uint32_t version, uint32_t level, uint32_t layout) { |
| 129 | return static_cast<uint32_t>(ckey_scalable_capacity(version, static_cast<ckey_ecc_level>(level), |
| 130 | static_cast<ckey_scalable_layout>(layout))); |
| 131 | } |
| 132 | |
| 133 | CKW_EXPORT uint32_t ckw_scalable_modules_across(uint32_t version) { return ckey_scalable_modules_across(version); } |
| 134 | |
| 135 | // Encodes a Profile 0 symbol. Returns a heap ckey_scalable_symbol (free with |
| 136 | // ckw_symbol_free) or 0 with ckw_last_error() set. |
| 137 | CKW_EXPORT ckey_scalable_symbol *ckw_encode(const uint8_t *payload, uint32_t len, uint32_t version, |
| 138 | uint32_t level, uint32_t layout) { |
| 139 | auto *sym = static_cast<ckey_scalable_symbol *>(std::calloc(1, sizeof(ckey_scalable_symbol))); |
| 140 | if (!sym) { set_error("out of memory"); return nullptr; } |
| 141 | ckey_diagnostics d{}; |
| 142 | const ckey_status st = ckey_encode_scalable(payload, len, version, static_cast<ckey_ecc_level>(level), |
| 143 | static_cast<ckey_scalable_layout>(layout), sym, &d); |
| 144 | if (st != CKEY_OK) { |
| 145 | set_error_diag(d, "encode failed"); |
| 146 | std::free(sym); |
| 147 | return nullptr; |
| 148 | } |
| 149 | return sym; |
| 150 | } |
| 151 | |
| 152 | CKW_EXPORT void ckw_symbol_free(ckey_scalable_symbol *sym) { std::free(sym); } |
| 153 | |
| 154 | // out[0..4] = version, ecc_level, layout, payload_length, codeword_length. |
| 155 | CKW_EXPORT void ckw_symbol_info(const ckey_scalable_symbol *sym, uint32_t *out) { |
| 156 | if (!sym || !out) return; |
| 157 | out[0] = sym->version; |
| 158 | out[1] = sym->ecc_level; |
| 159 | out[2] = sym->layout; |
| 160 | out[3] = sym->payload_length; |
| 161 | out[4] = sym->codeword_length; |
| 162 | } |
| 163 | |
| 164 | // ---- Styles ----------------------------------------------------------------- |
| 165 | |
| 166 | // Returns a style filled from a preset (0 plain, 1 concept), or 0. |
| 167 | CKW_EXPORT WebStyle *ckw_style_new(uint32_t preset) { |
| 168 | auto *ws = new (std::nothrow) WebStyle(); |
| 169 | if (!ws) { set_error("out of memory"); return nullptr; } |
| 170 | if (ckey_style_preset_fill(&ws->style, static_cast<ckey_style_preset>(preset)) != CKEY_OK) { |
| 171 | set_error("unknown style preset"); |
| 172 | delete ws; |
| 173 | return nullptr; |
| 174 | } |
| 175 | return ws; |
| 176 | } |
| 177 | |
| 178 | CKW_EXPORT void ckw_style_free(WebStyle *ws) { delete ws; } |
| 179 | |
| 180 | // Colour fields: 0 background, 1 background_edge, 2 ink, 3 ink_shade, 4..7 accents[0..3]. |
| 181 | CKW_EXPORT int ckw_style_set_rgb(WebStyle *ws, uint32_t field, uint32_t r, uint32_t g, uint32_t b) { |
| 182 | if (!ws || r > 255 || g > 255 || b > 255) return 0; |
| 183 | ckey_rgb c{static_cast<uint8_t>(r), static_cast<uint8_t>(g), static_cast<uint8_t>(b)}; |
| 184 | ckey_style &s = ws->style; |
| 185 | switch (field) { |
| 186 | case 0: s.background = c; break; |
| 187 | case 1: s.background_edge = c; break; |
| 188 | case 2: s.ink = c; break; |
| 189 | case 3: s.ink_shade = c; break; |
| 190 | case 4: case 5: case 6: case 7: s.accents[field - 4] = c; break; |
| 191 | default: return 0; |
| 192 | } |
| 193 | return 1; |
| 194 | } |
| 195 | |
| 196 | // Numeric fields: 0 accent_count, 1 accent_percent, 2 modules, 3 rings, 4 rays, |
| 197 | // 5 bevel, 6 shadow, 7 texture, 8 logo_radius, 9 logo_plate, 10 policy. |
| 198 | // Range checks beyond the type are the core style check's job. |
| 199 | CKW_EXPORT int ckw_style_set_num(WebStyle *ws, uint32_t field, double v) { |
| 200 | if (!ws || !std::isfinite(v)) return 0; |
| 201 | ckey_style &s = ws->style; |
| 202 | const auto u = [&](uint32_t &dst) { if (v < 0 || v > 4294967295.0) return 0; dst = static_cast<uint32_t>(v); return 1; }; |
| 203 | switch (field) { |
| 204 | case 0: return u(s.accent_count); |
| 205 | case 1: return u(s.accent_percent); |
| 206 | case 2: if (v < 0 || v > 3) return 0; s.modules = static_cast<ckey_module_shape>(static_cast<int>(v)); return 1; |
| 207 | case 3: if (v < 0 || v > 1) return 0; s.rings = static_cast<ckey_ring_style>(static_cast<int>(v)); return 1; |
| 208 | case 4: if (v < 0 || v > 1) return 0; s.rays = static_cast<ckey_ray_cap>(static_cast<int>(v)); return 1; |
| 209 | case 5: s.bevel = v; return 1; |
| 210 | case 6: s.shadow = v; return 1; |
| 211 | case 7: s.texture = v; return 1; |
| 212 | case 8: s.logo_radius = v; return 1; |
| 213 | case 9: return u(s.logo_plate); |
| 214 | case 10: |
| 215 | >= 4 |
| 216 | return u(s.policy); |
| 217 | |
| 218 | return v == 0 ? 1 : 0; // this core has only the strict behaviour |
| 219 | |
| 220 | default: return 0; |
| 221 | } |
| 222 | } |
| 223 | |
| 224 | CKW_EXPORT int ckw_style_set_logo(WebStyle *ws, const char *href) { |
| 225 | if (!ws) return 0; |
| 226 | ws->logo_href = href ? href : ""; |
| 227 | ws->style.logo_href = ws->logo_href.empty() ? nullptr : ws->logo_href.c_str(); |
| 228 | return 1; |
| 229 | } |
| 230 | |
| 231 | // CKEY_OK (0) or a status with ckw_last_error() explaining the refusal. |
| 232 | CKW_EXPORT int ckw_style_check(const ckey_scalable_symbol *sym, const WebStyle *ws) { |
| 233 | ckey_style fallback; |
| 234 | ckey_diagnostics d{}; |
| 235 | const ckey_status st = ckey_check_scalable_style(sym, style_of(ws, fallback), &d); |
| 236 | if (st != CKEY_OK) set_error_diag(d, "style check failed"); |
| 237 | else set_error_diag(d, ""); |
| 238 | return st; |
| 239 | } |
| 240 | |
| 241 | // ---- Rendering -------------------------------------------------------------- |
| 242 | // Renderers apply the style's own policy (strict refuses, adaptive repairs, |
| 243 | // override draws as given), so the bridge does not pre-check the style; when |
| 244 | // a render is refused it asks the style check for the reason. |
| 245 | |
| 246 | namespace { |
| 247 | void explain_refusal(const ckey_scalable_symbol *sym, const WebStyle *ws, const char *fallback) { |
| 248 | ckey_style fallback_style; |
| 249 | ckey_diagnostics d{}; |
| 250 | if (ckey_check_scalable_style(sym, style_of(ws, fallback_style), &d) != CKEY_OK) set_error_diag(d, fallback); |
| 251 | else set_error(fallback); |
| 252 | } |
| 253 | } // namespace |
| 254 | |
| 255 | // Renders RGBA8 (format 0) or GRAY8 (format 1) pixels: size x size, rows top to |
| 256 | // bottom. logo_rgba (optional, straight RGBA8, logo_w x logo_h) is drawn into |
| 257 | // the style's logo circle. Returns malloc'd pixels or 0. |
| 258 | CKW_EXPORT uint8_t *ckw_render_raster(const ckey_scalable_symbol *sym, const WebStyle *ws, uint32_t size, |
| 259 | uint32_t policy, uint32_t transparent, uint32_t supersample, |
| 260 | double min_feature_px, uint32_t materials, uint32_t format, |
| 261 | const uint8_t *logo_rgba, uint32_t logo_w, uint32_t logo_h) { |
| 262 | if (!sym) { set_error("missing symbol"); return nullptr; } |
| 263 | ckey_style fallback; |
| 264 | const ckey_style *style = style_of(ws, fallback); |
| 265 | ckey_raster_options o; |
| 266 | ckey_raster_options_default(&o); |
| 267 | o.size_px = size; |
| 268 | o.policy = policy; |
| 269 | o.format = format; |
| 270 | o.transparent = transparent; |
| 271 | o.supersample = supersample; |
| 272 | o.min_feature_px = min_feature_px; |
| 273 | o.materials = materials; |
| 274 | ckey_logo_image logo{}; |
| 275 | if (logo_rgba && logo_w && logo_h) { |
| 276 | logo.rgba = logo_rgba; |
| 277 | logo.width = logo_w; |
| 278 | logo.height = logo_h; |
| 279 | logo.stride = 0; |
| 280 | o.logo = &logo; |
| 281 | } |
| 282 | size_t need = 0; |
| 283 | ckey_status st = ckey_render_scalable_raster(sym, style, &o, nullptr, 0, &need); |
| 284 | if (st != CKEY_INSUFFICIENT_BUFFER || need == 0) { |
| 285 | explain_refusal(sym, ws, "invalid raster options (size 16..16384, supersample 1..8, " |
| 286 | "min feature > 0; transparent needs RGBA)"); |
| 287 | return nullptr; |
| 288 | } |
| 289 | auto *out = static_cast<uint8_t *>(std::malloc(need)); |
| 290 | if (!out) { set_error("out of memory"); return nullptr; } |
| 291 | st = ckey_render_scalable_raster(sym, style, &o, out, need, &need); |
| 292 | if (st != CKEY_OK) { std::free(out); set_error("raster render failed"); return nullptr; } |
| 293 | g_last_len = need; |
| 294 | return out; |
| 295 | } |
| 296 | |
| 297 | // Encodes RGBA8 pixels (stride = width * 4) as a PNG. dpi <= 0 omits pHYs. |
| 298 | CKW_EXPORT uint8_t *ckw_encode_png_rgba(const uint8_t *rgba, uint32_t width, uint32_t height, double dpi) { |
| 299 | size_t need = 0; |
| 300 | const size_t stride = static_cast<size_t>(width) * 4; |
| 301 | ckey_status st = ckey_encode_png(rgba, width, height, stride, CKEY_PNG_RGBA8, dpi, nullptr, 0, &need); |
| 302 | if ((st != CKEY_INSUFFICIENT_BUFFER && st != CKEY_OK) || need == 0) { set_error("PNG encode failed"); return nullptr; } |
| 303 | auto *out = static_cast<uint8_t *>(std::malloc(need)); |
| 304 | if (!out) { set_error("out of memory"); return nullptr; } |
| 305 | st = ckey_encode_png(rgba, width, height, stride, CKEY_PNG_RGBA8, dpi, out, need, &need); |
| 306 | if (st != CKEY_OK) { std::free(out); set_error("PNG encode failed"); return nullptr; } |
| 307 | g_last_len = need; |
| 308 | return out; |
| 309 | } |
| 310 | |
| 311 | // Styled SVG; scale is pixels per canonical unit (width = 31 * scale). |
| 312 | // Returns malloc'd UTF-8 (NUL-terminated; ckw_last_len excludes the NUL). |
| 313 | CKW_EXPORT char *ckw_render_svg(const ckey_scalable_symbol *sym, const WebStyle *ws, uint32_t scale) { |
| 314 | if (!sym) { set_error("missing symbol"); return nullptr; } |
| 315 | ckey_style fallback; |
| 316 | const ckey_style *style = style_of(ws, fallback); |
| 317 | size_t need = 0; |
| 318 | ckey_status st = ckey_render_scalable_styled_svg(sym, style, scale, nullptr, 0, &need); |
| 319 | if (st != CKEY_INSUFFICIENT_BUFFER || need == 0) { explain_refusal(sym, ws, "SVG render failed (scale must be 1..256)"); return nullptr; } |
| 320 | auto *out = static_cast<char *>(std::malloc(need)); |
| 321 | if (!out) { set_error("out of memory"); return nullptr; } |
| 322 | st = ckey_render_scalable_styled_svg(sym, style, scale, out, need, &need); |
| 323 | if (st != CKEY_OK) { std::free(out); set_error("SVG render failed"); return nullptr; } |
| 324 | g_last_len = std::strlen(out); |
| 325 | return out; |
| 326 | } |
| 327 | |
| 328 | CKW_EXPORT uint8_t *ckw_render_pdf(const ckey_scalable_symbol *sym, const WebStyle *ws, double width_pt, |
| 329 | uint32_t transparent) { |
| 330 | if (!sym) { set_error("missing symbol"); return nullptr; } |
| 331 | ckey_style fallback; |
| 332 | const ckey_style *style = style_of(ws, fallback); |
| 333 | size_t need = 0; |
| 334 | ckey_status st = ckey_render_scalable_pdf(sym, style, width_pt, transparent, nullptr, 0, &need); |
| 335 | if (st != CKEY_INSUFFICIENT_BUFFER || need == 0) { explain_refusal(sym, ws, "PDF render failed (invalid width)"); return nullptr; } |
| 336 | auto *out = static_cast<uint8_t *>(std::malloc(need)); |
| 337 | if (!out) { set_error("out of memory"); return nullptr; } |
| 338 | st = ckey_render_scalable_pdf(sym, style, width_pt, transparent, out, need, &need); |
| 339 | if (st != CKEY_OK) { std::free(out); set_error("PDF render failed"); return nullptr; } |
| 340 | g_last_len = need; |
| 341 | return out; |
| 342 | } |
| 343 | |
| 344 | // ---- Scanning --------------------------------------------------------------- |
| 345 | |
| 346 | namespace { |
| 347 | |
| 348 | constexpr size_t kMaxBorders = 6; |
| 349 | constexpr size_t kScalableOut = 65535; // P0 reaches 4,619 bytes at v31/M; same buffer as the iOS reader |
| 350 | |
| 351 | // out_info[0] = profile code (0 P0, 1 P0.1 print, 2 P1, 3 P2, 4 P3: the |
| 352 | // telemetry profile codes), out_info[1] = P0 version (0 for P1..P3), |
| 353 | // out_info[2] = P0 layout (0 standard, 1 print; 0 for P1..P3), |
| 354 | // out_info[3] = number of border candidates found. |
| 355 | uint8_t *decode_gray(const uint8_t *gray, size_t w, size_t h, size_t stride, uint32_t *out_info) { |
| 356 | if (!gray || !out_info || w == 0 || h == 0) { set_error("invalid frame"); return nullptr; } |
| 357 | double borders[kMaxBorders * 8]; |
| 358 | const size_t n = ckey_find_borders_gray8(gray, w, h, stride, borders, kMaxBorders); |
| 359 | out_info[3] = static_cast<uint32_t>(n); |
| 360 | if (n == 0) { set_error("no CKey border found"); return nullptr; } |
| 361 | std::vector<uint8_t> out(kScalableOut); |
| 362 | ckey_diagnostics d{}; |
| 363 | for (size_t i = 0; i < n; ++i) { |
| 364 | const double *border = borders + 8 * i; |
| 365 | double q[8]; |
| 366 | size_t len = 0; |
| 367 | // Profile 0 (scalable family) first: the core aligns the frame, |
| 368 | // identifies layout and version from the keys, then decodes. |
| 369 | uint32_t version = 0, layout = 0; |
| 370 | if (ckey_refine_quad_gray8(gray, w, h, stride, border, CKEY_FIELD_SCALABLE, q) >= 0 && |
| 371 | ckey_decode_scalable_gray8(gray, w, h, stride, q, out.data(), out.size(), &len, &version, &layout, &d) == CKEY_OK) { |
| 372 | out_info[0] = layout == CKEY_LAYOUT_PRINT ? 1u : 0u; |
| 373 | out_info[1] = version; |
| 374 | out_info[2] = layout; |
| 375 | return dup_bytes(out.data(), len); |
| 376 | } |
| 377 | // Legacy profiles, each refined against its own calibration field |
| 378 | // (never let one profile's fit pick another's transform). |
| 379 | if (ckey_refine_quad_gray8(gray, w, h, stride, border, CKEY_FIELD_LOCATOR, q) >= 0 && |
| 380 | ckey_decode_gray8(gray, w, h, stride, q, out.data(), CKEY_LOCATOR_PAYLOAD_BYTES, &d) == CKEY_OK) { |
| 381 | out_info[0] = 2; out_info[1] = 0; out_info[2] = 0; |
| 382 | return dup_bytes(out.data(), CKEY_LOCATOR_PAYLOAD_BYTES); |
| 383 | } |
| 384 | if (ckey_refine_quad_gray8(gray, w, h, stride, border, CKEY_FIELD_TEXT, q) >= 0 && |
| 385 | ckey_decode_text_gray8(gray, w, h, stride, q, out.data(), out.size(), &len, &d) == CKEY_OK) { |
| 386 | out_info[0] = 3; out_info[1] = 0; out_info[2] = 0; |
| 387 | return dup_bytes(out.data(), len); |
| 388 | } |
| 389 | if (ckey_refine_quad_gray8(gray, w, h, stride, border, CKEY_FIELD_EXTENDED, q) >= 0 && |
| 390 | ckey_decode_extended_gray8(gray, w, h, stride, q, out.data(), out.size(), &len, &d) == CKEY_OK) { |
| 391 | out_info[0] = 4; out_info[1] = 0; out_info[2] = 0; |
| 392 | return dup_bytes(out.data(), len); |
| 393 | } |
| 394 | } |
| 395 | set_error_diag(d, "no CKey decoded"); |
| 396 | return nullptr; |
| 397 | } |
| 398 | |
| 399 | } // namespace |
| 400 | |
| 401 | // Decodes an 8-bit grayscale frame. Returns malloc'd payload bytes or 0. |
| 402 | CKW_EXPORT uint8_t *ckw_decode_gray(const uint8_t *gray, uint32_t width, uint32_t height, uint32_t stride, |
| 403 | uint32_t *out_info) { |
| 404 | return decode_gray(gray, width, height, stride ? stride : width, out_info); |
| 405 | } |
| 406 | |
| 407 | // Decodes an RGBA8 frame (as from ImageData). Pixels are composited over |
| 408 | // white (so a transparent render reads as ink on paper) and converted to |
| 409 | // Rec. 709 luma, then decoded as by ckw_decode_gray. |
| 410 | CKW_EXPORT uint8_t *ckw_decode_rgba(const uint8_t *rgba, uint32_t width, uint32_t height, uint32_t stride, |
| 411 | uint32_t *out_info) { |
| 412 | if (!rgba || width == 0 || height == 0) { set_error("invalid frame"); return nullptr; } |
| 413 | const size_t row = stride ? stride : static_cast<size_t>(width) * 4; |
| 414 | std::vector<uint8_t> gray(static_cast<size_t>(width) * height); |
| 415 | for (size_t y = 0; y < height; ++y) { |
| 416 | const uint8_t *p = rgba + y * row; |
| 417 | uint8_t *g = gray.data() + y * width; |
| 418 | for (size_t x = 0; x < width; ++x, p += 4) { |
| 419 | // Rec. 709 luma in 8.8 fixed point: 0.2126, 0.7152, 0.0722. |
| 420 | const uint32_t luma = (54u * p[0] + 183u * p[1] + 19u * p[2] + 128u) >> 8; |
| 421 | const uint32_t a = p[3]; |
| 422 | g[x] = static_cast<uint8_t>((luma * a + 255u * (255u - a) + 127u) / 255u); |
| 423 | } |
| 424 | } |
| 425 | return decode_gray(gray.data(), width, height, width, out_info); |
| 426 | } |
| 427 | |
| 428 | // ---- Optical certification (ckey_certify; present from ABI 4) --------------- |
| 429 | |
| 430 | |
| 431 | CKW_EXPORT uint32_t ckw_has_certify(void) { return 1; } |
| 432 | |
| 433 | // Certifies a style (0 = default) under its policy. colours: colour_count RGB |
| 434 | // triplets (3 bytes each) the symbol may sit on; bd_rgba (optional) host |
| 435 | // pixels behind the symbol. Model parameters < 0 keep the default (trials 0). |
| 436 | // out (8 doubles): certified, policy, adjustments, trials, trials_passed, |
| 437 | // min_contrast, feature_separation, logo_load. The certification message is |
| 438 | // in ckw_last_error(). If out_style is non-zero, the style to render (the |
| 439 | // adaptive repair) is written into it. Returns the core status. |
| 440 | CKW_EXPORT int ckw_certify(const ckey_scalable_symbol *sym, const WebStyle *ws, uint32_t transparent, |
| 441 | const uint8_t *colours, uint32_t colour_count, const uint8_t *bd_rgba, |
| 442 | uint32_t bd_w, uint32_t bd_h, double px_per_module, double blur_px, double noise, |
| 443 | double glare, uint32_t trials, double *out, WebStyle *out_style) { |
| 444 | ckey_style fallback; |
| 445 | const ckey_style *style = style_of(ws, fallback); |
| 446 | std::vector<ckey_rgb> cs; |
| 447 | for (uint32_t i = 0; colours && i < colour_count; ++i) cs.push_back({colours[3 * i], colours[3 * i + 1], colours[3 * i + 2]}); |
| 448 | ckey_backdrop bd{}; |
| 449 | bd.colours = cs.empty() ? nullptr : cs.data(); |
| 450 | bd.colour_count = static_cast<uint32_t>(cs.size()); |
| 451 | if (bd_rgba && bd_w && bd_h) { bd.rgba = bd_rgba; bd.width = bd_w; bd.height = bd_h; bd.stride = 0; } |
| 452 | const bool has_backdrop = bd.colour_count || bd.rgba; |
| 453 | ckey_scan_model m; |
| 454 | ckey_scan_model_default(&m); |
| 455 | if (px_per_module >= 0) m.px_per_module = px_per_module; |
| 456 | if (blur_px >= 0) m.blur_px = blur_px; |
| 457 | if (noise >= 0) m.noise = noise; |
| 458 | if (glare >= 0) m.glare = glare; |
| 459 | if (trials) m.trials = trials; |
| 460 | ckey_style repaired; |
| 461 | std::memcpy(&repaired, style, sizeof repaired); |
| 462 | ckey_certification c{}; |
| 463 | const ckey_status st = ckey_certify_scalable_style(sym, style, transparent, has_backdrop ? &bd : nullptr, &m, |
| 464 | &repaired, &c); |
| 465 | char msg[sizeof c.message + 1]; |
| 466 | std::memcpy(msg, c.message, sizeof c.message); |
| 467 | msg[sizeof c.message] = '\0'; |
| 468 | set_error(msg[0] ? msg : (st == CKEY_OK ? "" : "certification failed")); |
| 469 | if (out) { |
| 470 | out[0] = c.certified; out[1] = c.policy; out[2] = c.adjustments; out[3] = c.trials; |
| 471 | out[4] = c.trials_passed; out[5] = c.min_contrast; out[6] = c.feature_separation; out[7] = c.logo_load; |
| 472 | } |
| 473 | if (out_style && st == CKEY_OK) { |
| 474 | out_style->style = repaired; |
| 475 | // Keep the logo string owned by out_style. |
| 476 | out_style->logo_href = repaired.logo_href ? repaired.logo_href : ""; |
| 477 | out_style->style.logo_href = out_style->logo_href.empty() ? nullptr : out_style->logo_href.c_str(); |
| 478 | } |
| 479 | return st; |
| 480 | } |
| 481 | |
| 482 | CKW_EXPORT uint32_t ckw_has_certify(void) { return 0; } |
| 483 | |
| 484 | |
| 485 | // ---- Telemetry -------------------------------------------------------------- |
| 486 | // Thin double-typed wrappers; the core does no networking and reads no clock. |
| 487 | |
| 488 | |
| 489 | CKW_EXPORT int ckw_tel_set_mode(int mode) { return ckey_telemetry_set_mode(mode); } |
| 490 | CKW_EXPORT int ckw_tel_get_mode(void) { return ckey_telemetry_get_mode(); } |
| 491 | CKW_EXPORT int ckw_tel_set_client(const char *sdk, const char *version, const char *platform) { |
| 492 | return ckey_telemetry_set_client(sdk, version, platform); |
| 493 | } |
| 494 | CKW_EXPORT void ckw_tel_session_begin(double now_ms) { ckey_telemetry_session_begin(to_ms(now_ms)); } |
| 495 | CKW_EXPORT void ckw_tel_frame(double now_ms, double processing_ms) { |
| 496 | const uint64_t p = to_ms(processing_ms); |
| 497 | ckey_telemetry_frame(to_ms(now_ms), p > UINT32_MAX ? UINT32_MAX : static_cast<uint32_t>(p)); |
| 498 | } |
| 499 | CKW_EXPORT int ckw_tel_session_success(double now_ms, uint32_t profile_code) { |
| 500 | return ckey_telemetry_session_success(to_ms(now_ms), profile_code); |
| 501 | } |
| 502 | CKW_EXPORT void ckw_tel_session_end(double now_ms) { ckey_telemetry_session_end(to_ms(now_ms)); } |
| 503 | CKW_EXPORT int ckw_tel_should_send(double now_ms) { return ckey_telemetry_should_send(to_ms(now_ms)); } |
| 504 | |
| 505 | // Takes a batch. Returns malloc'd JSON (NUL-terminated) and sets ckw_last_len() |
| 506 | // and ckw_tel_last_batch_id(); returns 0 when there is nothing to send. |
| 507 | CKW_EXPORT char *ckw_tel_take_batch(const char *utc_day) { |
| 508 | g_last_batch_id = 0; |
| 509 | g_last_len = 0; |
| 510 | char buf[CKEY_TELEMETRY_MAX_BODY_BYTES + 1]; |
| 511 | size_t len = 0; |
| 512 | uint64_t id = 0; |
| 513 | const ckey_status st = ckey_telemetry_take_batch(utc_day, buf, sizeof buf, &len, &id); |
| 514 | if (st != CKEY_OK) { set_error("telemetry batch failed"); return nullptr; } |
| 515 | if (id == 0 || len == 0) return nullptr; |
| 516 | char *out = static_cast<char *>(std::malloc(len + 1)); |
| 517 | if (!out) { set_error("out of memory"); return nullptr; } |
| 518 | std::memcpy(out, buf, len + 1); |
| 519 | g_last_len = len; |
| 520 | g_last_batch_id = static_cast<double>(id); |
| 521 | return out; |
| 522 | } |
| 523 | CKW_EXPORT double ckw_tel_last_batch_id(void) { return g_last_batch_id; } |
| 524 | CKW_EXPORT int ckw_tel_batch_result(double batch_id, int http_status, double now_ms) { |
| 525 | return ckey_telemetry_batch_result(to_ms(batch_id), http_status, to_ms(now_ms)); |
| 526 | } |
| 527 | |
| 528 | // out (33 doubles): sessions started, succeeded, failed, frames, decoded[5], |
| 529 | // time_to_decode {count, sum, buckets[10]}, frame_ms {count, sum, buckets[10]}. |
| 530 | CKW_EXPORT int ckw_tel_snapshot(double *out) { |
| 531 | ckey_telemetry_counters c{}; |
| 532 | const ckey_status st = ckey_telemetry_snapshot(&c); |
| 533 | if (st != CKEY_OK || !out) return st; |
| 534 | size_t k = 0; |
| 535 | out[k++] = static_cast<double>(c.sessions_started); |
| 536 | out[k++] = static_cast<double>(c.sessions_succeeded); |
| 537 | out[k++] = static_cast<double>(c.sessions_failed); |
| 538 | out[k++] = static_cast<double>(c.frames); |
| 539 | for (uint64_t v : c.decoded) out[k++] = static_cast<double>(v); |
| 540 | for (const ckey_telemetry_histogram *hgm : {&c.time_to_decode_ms, &c.frame_ms}) { |
| 541 | out[k++] = static_cast<double>(hgm->count); |
| 542 | out[k++] = static_cast<double>(hgm->sum); |
| 543 | for (uint64_t v : hgm->buckets) out[k++] = static_cast<double>(v); |
| 544 | } |
| 545 | return CKEY_OK; |
| 546 | } |
| 547 | |
| 548 | // TEST ONLY: restores the telemetry defaults. |
| 549 | CKW_EXPORT void ckw_tel_reset_for_testing(void) { ckey_telemetry_reset_for_testing(); } |
| 550 |