Viewing is open to everyone. Downloading and starring need a GitHub sign-in.
Sign in with GitHub
ckey_jni.cpp
v0.1.0-build.85
Latest
Commit ede38f5
Sign in with GitHub to download. Each download is recorded with your GitHub username.
| 1 | // JNI bridge for the CKey Android SDK (org.openckey:ckey). |
| 2 | // |
| 3 | // A thin layer over the C ABI in include/ckey/ckey.h and include/ckey/telemetry.h. |
| 4 | // It never reimplements encoding, decoding or rendering: it converts Java |
| 5 | // arrays to C buffers, calls the core, and converts the results back. |
| 6 | // |
| 7 | // The Kotlin side is org.openckey.ckey.CKeyNative (an `object` whose |
| 8 | // @JvmStatic external funs compile to static native methods). Every function |
| 9 | // here must match one of those declarations; sdk/android/jni-host-test checks |
| 10 | // the signatures against javac -h output and runs them on a desktop JVM. |
| 11 | // |
| 12 | // Build switches (set by src/main/cpp/CMakeLists.txt from Gradle properties): |
| 13 | // CKEY_JNI_SCAN grayscale localization + decoding (ckey_scan) |
| 14 | // CKEY_JNI_RENDER styles, raster, PNG, SVG, PDF (ckey_render) |
| 15 | // CKEY_JNI_TELEMETRY aggregate counters (ckey_telemetry) |
| 16 | // CKEY_JNI_CERTIFY optical certification (needs scan + render) |
| 17 | // The codec (encode, capacities) is always present. A function whose module is |
| 18 | // compiled out is simply absent; the Kotlin wrapper checks nativeFeatures() |
| 19 | // first and reports UnsupportedOperationException instead of linking errors. |
| 20 | // |
| 21 | // Symbols are passed between Java and C as an opaque byte[] that holds a |
| 22 | // ckey_scalable_symbol verbatim. That avoids native handles (no leaks, no |
| 23 | // finalizers); every incoming blob is size- and ABI-checked before use. |
| 24 | |
| 25 | <jni.h> |
| 26 | |
| 27 | <algorithm> |
| 28 | <cstdint> |
| 29 | <cstring> |
| 30 | <limits> |
| 31 | <memory> |
| 32 | <new> |
| 33 | <string> |
| 34 | <vector> |
| 35 | |
| 36 | |
| 37 | 1 |
| 38 | |
| 39 | |
| 40 | 1 |
| 41 | |
| 42 | |
| 43 | 1 |
| 44 | |
| 45 | |
| 46 | 0 |
| 47 | |
| 48 | |
| 49 | "ckey/ckey.h" |
| 50 | |
| 51 | "ckey/telemetry.h" |
| 52 | |
| 53 | |
| 54 | |
| 55 | (name) JNIEXPORT JNICALL Java_org_openckey_ckey_CKeyNative_##name |
| 56 | |
| 57 | namespace { |
| 58 | |
| 59 | // Feature bits returned by nativeFeatures(); mirrored in CKeyNative.kt. |
| 60 | constexpr jint kFeatureScan = 1; |
| 61 | constexpr jint kFeatureRender = 2; |
| 62 | constexpr jint kFeatureTelemetry = 4; |
| 63 | constexpr jint kFeatureCertify = 8; |
| 64 | |
| 65 | // Style marshalling: ints and doubles, in this order (mirrored in CKeyStyle.kt). |
| 66 | // ints: 0 background, 1 background_edge, 2 ink, 3 ink_shade (0xRRGGBB), |
| 67 | // 4..7 accents[0..3], 8 accent_count, 9 accent_percent, |
| 68 | // 10 modules, 11 rings, 12 rays, 13 logo_plate, 14 policy |
| 69 | // doubles: 0 bevel, 1 shadow, 2 texture, 3 logo_radius |
| 70 | constexpr jsize kStyleInts = 15; |
| 71 | constexpr jsize kStyleDoubles = 4; |
| 72 | |
| 73 | // Decode info (mirrored in CKeyDecoder.kt): |
| 74 | // 0 profile code (ckey_telemetry_profile numbering: 0 P0, 1 P0.1, 2 P1, 3 P2, 4 P3) |
| 75 | // 1 version (Profile 0 only, else 0), 2 layout (Profile 0 only, else 0), |
| 76 | // 3 border candidates found, 4 last core status |
| 77 | constexpr jsize kDecodeInfo = 5; |
| 78 | constexpr size_t kMaxCandidates = 16; |
| 79 | // Profile 0 payloads reach 4,619 bytes (v31 at level M); like the iOS reader, |
| 80 | // give the decoder a 64 KB buffer so no valid symbol is refused for space. |
| 81 | constexpr size_t kScalableOutput = 65535; |
| 82 | |
| 83 | jclass g_ckey_exception = nullptr; // org.openckey.ckey.CKeyException |
| 84 | jmethodID g_ckey_exception_ctor = nullptr; // (ILjava/lang/String;)V |
| 85 | jclass g_string = nullptr; |
| 86 | jmethodID g_string_from_bytes = nullptr; // String(byte[], String) |
| 87 | jmethodID g_string_get_bytes = nullptr; // byte[] getBytes(String) |
| 88 | jstring g_utf8 = nullptr; // "UTF-8" |
| 89 | |
| 90 | // ---- Exceptions ------------------------------------------------------- |
| 91 | |
| 92 | void throw_class(JNIEnv *env, const char *cls, const char *message) { |
| 93 | if (env->ExceptionCheck()) return; |
| 94 | jclass c = env->FindClass(cls); |
| 95 | if (c) env->ThrowNew(c, message); |
| 96 | } |
| 97 | |
| 98 | jstring new_utf8_string(JNIEnv *env, const char *text, size_t length); |
| 99 | |
| 100 | void throw_ckey(JNIEnv *env, int status, const char *message) { |
| 101 | if (env->ExceptionCheck()) return; |
| 102 | if (!g_ckey_exception || !g_ckey_exception_ctor) { |
| 103 | throw_class(env, "java/lang/IllegalStateException", message); |
| 104 | return; |
| 105 | } |
| 106 | const size_t len = message ? strnlen(message, 1024) : 0; |
| 107 | jstring jmessage = new_utf8_string(env, message ? message : "", len); |
| 108 | if (!jmessage) return; // OutOfMemoryError pending |
| 109 | jobject ex = env->NewObject(g_ckey_exception, g_ckey_exception_ctor, static_cast<jint>(status), jmessage); |
| 110 | if (ex) env->Throw(static_cast<jthrowable>(ex)); |
| 111 | } |
| 112 | |
| 113 | void throw_ckey(JNIEnv *env, int status, const ckey_diagnostics &diag, const char *fallback) { |
| 114 | char message[sizeof(diag.message) + 1]; |
| 115 | std::memcpy(message, diag.message, sizeof(diag.message)); |
| 116 | message[sizeof(diag.message)] = '\0'; |
| 117 | throw_ckey(env, status, message[0] ? message : fallback); |
| 118 | } |
| 119 | |
| 120 | void throw_iae(JNIEnv *env, const char *message) { throw_class(env, "java/lang/IllegalArgumentException", message); } |
| 121 | void throw_oom(JNIEnv *env) { throw_class(env, "java/lang/OutOfMemoryError", "CKey native allocation failed"); } |
| 122 | |
| 123 | // Run a body and convert any C++ exception into a Java exception, so nothing |
| 124 | // ever unwinds through the JNI boundary. |
| 125 | template <typename R, typename F> |
| 126 | R guarded(JNIEnv *env, R fallback, F &&body) { |
| 127 | try { |
| 128 | return body(); |
| 129 | } catch (const std::bad_alloc &) { |
| 130 | throw_oom(env); |
| 131 | } catch (...) { |
| 132 | throw_class(env, "java/lang/IllegalStateException", "CKey native error"); |
| 133 | } |
| 134 | return fallback; |
| 135 | } |
| 136 | |
| 137 | // ---- Strings ------------------------------------------------------------ |
| 138 | // NewStringUTF/GetStringUTFChars use *modified* UTF-8, which differs from |
| 139 | // standard UTF-8 for supplementary characters and NUL. Diagnostics, SVG and |
| 140 | // logo URLs are converted through java.lang.String's UTF-8 codec instead. |
| 141 | |
| 142 | jstring new_utf8_string(JNIEnv *env, const char *text, size_t length) { |
| 143 | if (length > static_cast<size_t>(std::numeric_limits<jsize>::max())) { throw_oom(env); return nullptr; } |
| 144 | jbyteArray bytes = env->NewByteArray(static_cast<jsize>(length)); |
| 145 | if (!bytes) return nullptr; |
| 146 | if (length) env->SetByteArrayRegion(bytes, 0, static_cast<jsize>(length), reinterpret_cast<const jbyte *>(text)); |
| 147 | jstring s = static_cast<jstring>(env->NewObject(g_string, g_string_from_bytes, bytes, g_utf8)); |
| 148 | env->DeleteLocalRef(bytes); |
| 149 | return s; |
| 150 | } |
| 151 | |
| 152 | // Returns false with a Java exception pending on failure. A null jstring |
| 153 | // yields ok = true and present = false. |
| 154 | bool utf8_from_jstring(JNIEnv *env, jstring s, std::string &out, bool &present) { |
| 155 | out.clear(); |
| 156 | present = s != nullptr; |
| 157 | if (!s) return true; |
| 158 | jbyteArray bytes = static_cast<jbyteArray>(env->CallObjectMethod(s, g_string_get_bytes, g_utf8)); |
| 159 | if (env->ExceptionCheck() || !bytes) return false; |
| 160 | const jsize n = env->GetArrayLength(bytes); |
| 161 | out.resize(static_cast<size_t>(n)); |
| 162 | if (n) env->GetByteArrayRegion(bytes, 0, n, reinterpret_cast<jbyte *>(&out[0])); |
| 163 | env->DeleteLocalRef(bytes); |
| 164 | if (out.find('\0') != std::string::npos) { throw_iae(env, "string contains NUL"); return false; } |
| 165 | return true; |
| 166 | } |
| 167 | |
| 168 | jbyteArray new_byte_array(JNIEnv *env, const uint8_t *data, size_t length) { |
| 169 | if (length > static_cast<size_t>(std::numeric_limits<jsize>::max())) { throw_oom(env); return nullptr; } |
| 170 | jbyteArray out = env->NewByteArray(static_cast<jsize>(length)); |
| 171 | if (out && length) env->SetByteArrayRegion(out, 0, static_cast<jsize>(length), reinterpret_cast<const jbyte *>(data)); |
| 172 | return out; |
| 173 | } |
| 174 | |
| 175 | // ---- Symbol blob -------------------------------------------------------- |
| 176 | |
| 177 | bool read_symbol(JNIEnv *env, jbyteArray blob, ckey_scalable_symbol &symbol) { |
| 178 | if (!blob) { throw_class(env, "java/lang/NullPointerException", "symbol"); return false; } |
| 179 | if (env->GetArrayLength(blob) != static_cast<jsize>(sizeof(ckey_scalable_symbol))) { |
| 180 | throw_iae(env, "not a CKey symbol (size mismatch)"); |
| 181 | return false; |
| 182 | } |
| 183 | env->GetByteArrayRegion(blob, 0, static_cast<jsize>(sizeof symbol), reinterpret_cast<jbyte *>(&symbol)); |
| 184 | if (symbol.abi_version != CKEY_ABI_VERSION || symbol.version < 1 || symbol.version > CKEY_SCALABLE_VERSION_COUNT || |
| 185 | symbol.ecc_level > CKEY_ECC_H || symbol.codeword_length > CKEY_SCALABLE_MAX_CODEWORD_BYTES) { |
| 186 | throw_iae(env, "not a CKey symbol (header mismatch)"); |
| 187 | return false; |
| 188 | } |
| 189 | return true; |
| 190 | } |
| 191 | |
| 192 | |
| 193 | // ---- Style -------------------------------------------------------------- |
| 194 | |
| 195 | ckey_rgb rgb_from_int(jint v) { |
| 196 | const uint32_t u = static_cast<uint32_t>(v); |
| 197 | return ckey_rgb{static_cast<uint8_t>((u >> 16) & 0xff), static_cast<uint8_t>((u >> 8) & 0xff), static_cast<uint8_t>(u & 0xff)}; |
| 198 | } |
| 199 | jint int_from_rgb(ckey_rgb c) { |
| 200 | return static_cast<jint>((static_cast<uint32_t>(c.r) << 16) | (static_cast<uint32_t>(c.g) << 8) | c.b); |
| 201 | } |
| 202 | |
| 203 | // A style read from Java, keeping its logo URL alive for the duration of a call. |
| 204 | struct StyleArg { |
| 205 | ckey_style style{}; |
| 206 | std::string logo; |
| 207 | bool present = false; // false: use NULL (the core's default style) |
| 208 | const ckey_style *get() const { return present ? &style : nullptr; } |
| 209 | }; |
| 210 | |
| 211 | bool read_style(JNIEnv *env, jintArray ints, jdoubleArray doubles, jstring logo_href, StyleArg &out) { |
| 212 | ckey_style_default(&out.style); // never hard-code fields the ABI may add |
| 213 | out.present = false; |
| 214 | if (!ints && !doubles && !logo_href) return true; |
| 215 | if (!ints || !doubles || env->GetArrayLength(ints) != kStyleInts || env->GetArrayLength(doubles) != kStyleDoubles) { |
| 216 | throw_iae(env, "style arrays have the wrong shape"); |
| 217 | return false; |
| 218 | } |
| 219 | jint i[kStyleInts]; |
| 220 | jdouble d[kStyleDoubles]; |
| 221 | env->GetIntArrayRegion(ints, 0, kStyleInts, i); |
| 222 | env->GetDoubleArrayRegion(doubles, 0, kStyleDoubles, d); |
| 223 | ckey_style &s = out.style; |
| 224 | s.background = rgb_from_int(i[0]); |
| 225 | s.background_edge = rgb_from_int(i[1]); |
| 226 | s.ink = rgb_from_int(i[2]); |
| 227 | s.ink_shade = rgb_from_int(i[3]); |
| 228 | for (int k = 0; k < 4; ++k) s.accents[k] = rgb_from_int(i[4 + k]); |
| 229 | // Range checks are the core's job (ckey_check_scalable_style); negative |
| 230 | // values are mapped to an out-of-range value so the core refuses them. |
| 231 | auto u32 = [](jint v) { return v < 0 ? 0xffffffffu : static_cast<uint32_t>(v); }; |
| 232 | s.accent_count = u32(i[8]); |
| 233 | s.accent_percent = u32(i[9]); |
| 234 | s.modules = static_cast<ckey_module_shape>(u32(i[10])); |
| 235 | s.rings = static_cast<ckey_ring_style>(u32(i[11])); |
| 236 | s.rays = static_cast<ckey_ray_cap>(u32(i[12])); |
| 237 | s.logo_plate = u32(i[13]); |
| 238 | s.policy = u32(i[14]); |
| 239 | s.bevel = d[0]; |
| 240 | s.shadow = d[1]; |
| 241 | s.texture = d[2]; |
| 242 | s.logo_radius = d[3]; |
| 243 | bool has_logo = false; |
| 244 | if (!utf8_from_jstring(env, logo_href, out.logo, has_logo)) return false; |
| 245 | s.logo_href = has_logo && !out.logo.empty() ? out.logo.c_str() : nullptr; |
| 246 | out.present = true; |
| 247 | return true; |
| 248 | } |
| 249 | |
| 250 | void write_style(JNIEnv *env, const ckey_style &s, jintArray ints, jdoubleArray doubles) { |
| 251 | jint i[kStyleInts]; |
| 252 | jdouble d[kStyleDoubles]; |
| 253 | i[0] = int_from_rgb(s.background); |
| 254 | i[1] = int_from_rgb(s.background_edge); |
| 255 | i[2] = int_from_rgb(s.ink); |
| 256 | i[3] = int_from_rgb(s.ink_shade); |
| 257 | for (int k = 0; k < 4; ++k) i[4 + k] = int_from_rgb(s.accents[k]); |
| 258 | i[8] = static_cast<jint>(s.accent_count); |
| 259 | i[9] = static_cast<jint>(s.accent_percent); |
| 260 | i[10] = static_cast<jint>(s.modules); |
| 261 | i[11] = static_cast<jint>(s.rings); |
| 262 | i[12] = static_cast<jint>(s.rays); |
| 263 | i[13] = static_cast<jint>(s.logo_plate); |
| 264 | i[14] = static_cast<jint>(s.policy); |
| 265 | d[0] = s.bevel; |
| 266 | d[1] = s.shadow; |
| 267 | d[2] = s.texture; |
| 268 | d[3] = s.logo_radius; |
| 269 | env->SetIntArrayRegion(ints, 0, kStyleInts, i); |
| 270 | env->SetDoubleArrayRegion(doubles, 0, kStyleDoubles, d); |
| 271 | } |
| 272 | |
| 273 | bool check_style_shape(JNIEnv *env, jintArray ints, jdoubleArray doubles) { |
| 274 | if (!ints || !doubles || env->GetArrayLength(ints) != kStyleInts || env->GetArrayLength(doubles) != kStyleDoubles) { |
| 275 | throw_iae(env, "style arrays have the wrong shape"); |
| 276 | return false; |
| 277 | } |
| 278 | return true; |
| 279 | } |
| 280 | |
| 281 | // Raster options + optional logo bitmap (straight RGBA8, tightly packed). |
| 282 | struct RasterArg { |
| 283 | ckey_raster_options options{}; |
| 284 | ckey_logo_image logo{}; |
| 285 | std::vector<uint8_t> logo_pixels; |
| 286 | }; |
| 287 | |
| 288 | bool read_raster(JNIEnv *env, jint size_px, jint policy, bool gray, jboolean transparent, jint supersample, |
| 289 | jdouble min_feature_px, jboolean materials, jbyteArray logo_rgba, jint logo_w, jint logo_h, |
| 290 | RasterArg &out) { |
| 291 | ckey_raster_options_default(&out.options); // sets abi_version = CKEY_ABI_VERSION |
| 292 | if (size_px <= 0 || policy < 0 || supersample <= 0) { throw_iae(env, "invalid raster options"); return false; } |
| 293 | out.options.size_px = static_cast<uint32_t>(size_px); |
| 294 | out.options.policy = static_cast<uint32_t>(policy); |
| 295 | out.options.format = gray ? CKEY_PIXELS_GRAY8 : CKEY_PIXELS_RGBA8; |
| 296 | out.options.transparent = transparent ? 1u : 0u; |
| 297 | out.options.supersample = static_cast<uint32_t>(supersample); |
| 298 | out.options.min_feature_px = min_feature_px; |
| 299 | out.options.materials = materials ? 1u : 0u; |
| 300 | out.options.logo = nullptr; |
| 301 | if (logo_rgba) { |
| 302 | if (logo_w <= 0 || logo_h <= 0 || |
| 303 | static_cast<int64_t>(env->GetArrayLength(logo_rgba)) != static_cast<int64_t>(logo_w) * logo_h * 4) { |
| 304 | throw_iae(env, "logo pixels must be width * height * 4 bytes"); |
| 305 | return false; |
| 306 | } |
| 307 | out.logo_pixels.resize(static_cast<size_t>(logo_w) * static_cast<size_t>(logo_h) * 4u); |
| 308 | env->GetByteArrayRegion(logo_rgba, 0, static_cast<jsize>(out.logo_pixels.size()), |
| 309 | reinterpret_cast<jbyte *>(out.logo_pixels.data())); |
| 310 | out.logo.rgba = out.logo_pixels.data(); |
| 311 | out.logo.width = static_cast<uint32_t>(logo_w); |
| 312 | out.logo.height = static_cast<uint32_t>(logo_h); |
| 313 | out.logo.stride = 0; |
| 314 | out.options.logo = &out.logo; |
| 315 | } |
| 316 | return true; |
| 317 | } |
| 318 | |
| 319 | bool render_raster(JNIEnv *env, const ckey_scalable_symbol &symbol, const StyleArg &style, const RasterArg &raster, |
| 320 | std::vector<uint8_t> &pixels) { |
| 321 | size_t required = 0; |
| 322 | ckey_status st = ckey_render_scalable_raster(&symbol, style.get(), &raster.options, nullptr, 0, &required); |
| 323 | if (st != CKEY_OK && st != CKEY_INSUFFICIENT_BUFFER) { |
| 324 | throw_ckey(env, st, "render refused (style failed the safety check or invalid options)"); |
| 325 | return false; |
| 326 | } |
| 327 | pixels.resize(required); |
| 328 | st = ckey_render_scalable_raster(&symbol, style.get(), &raster.options, pixels.data(), pixels.size(), &required); |
| 329 | if (st != CKEY_OK) { throw_ckey(env, st, "render failed"); return false; } |
| 330 | return true; |
| 331 | } |
| 332 | |
| 333 | // Explain a refused style with the core's own diagnostic (the raster API has |
| 334 | // no diagnostics parameter). |
| 335 | void explain_style(JNIEnv *env, const ckey_scalable_symbol &symbol, const StyleArg &style, int status) { |
| 336 | if (!style.present) { throw_ckey(env, status, "render failed"); return; } |
| 337 | ckey_diagnostics diag{}; |
| 338 | diag.abi_version = CKEY_ABI_VERSION; |
| 339 | const ckey_status st = ckey_check_scalable_style(&symbol, style.get(), &diag); |
| 340 | if (st != CKEY_OK) throw_ckey(env, st, diag, "style refused"); |
| 341 | else throw_ckey(env, status, "render failed"); |
| 342 | } |
| 343 | // CKEY_JNI_RENDER |
| 344 | |
| 345 | |
| 346 | // ---- Scanning ----------------------------------------------------------- |
| 347 | // The pipeline of ios/CKeyReader/CameraScanner.swift: find border candidates |
| 348 | // (both polarities), then for each candidate, most border-like first, refine |
| 349 | // it to Profile 0's field and decode Profile 0 (identifies version and layout |
| 350 | // from the keys); then refine and decode Profiles 1, 2 and 3, each against its |
| 351 | // own field (never using one profile's fit for another). |
| 352 | |
| 353 | struct Decoded { |
| 354 | std::vector<uint8_t> bytes; |
| 355 | int profile = -1; |
| 356 | uint32_t version = 0, layout = 0; |
| 357 | double quad[8] = {0}; |
| 358 | int last_status = CKEY_UNCORRECTABLE; |
| 359 | }; |
| 360 | |
| 361 | bool refine(const uint8_t *gray, size_t w, size_t h, size_t stride, const double *border, ckey_field field, double *out) { |
| 362 | return ckey_refine_quad_gray8(gray, w, h, stride, border, field, out) >= 0.0; |
| 363 | } |
| 364 | |
| 365 | size_t decode_frame(const uint8_t *gray, size_t w, size_t h, size_t stride, size_t max_candidates, Decoded &out) { |
| 366 | double borders[kMaxCandidates * 8]; |
| 367 | const size_t count = std::min(ckey_find_borders_gray8(gray, w, h, stride, borders, max_candidates), max_candidates); |
| 368 | std::vector<uint8_t> buffer(kScalableOutput); |
| 369 | for (size_t c = 0; c < count; ++c) { |
| 370 | const double *border = borders + c * 8; |
| 371 | double quad[8]; |
| 372 | ckey_diagnostics diag{}; |
| 373 | diag.abi_version = CKEY_ABI_VERSION; |
| 374 | size_t length = 0; |
| 375 | |
| 376 | if (refine(gray, w, h, stride, border, CKEY_FIELD_SCALABLE, quad)) { |
| 377 | uint32_t version = 0, layout = 0; |
| 378 | const ckey_status st = ckey_decode_scalable_gray8(gray, w, h, stride, quad, buffer.data(), buffer.size(), |
| 379 | &length, &version, &layout, &diag); |
| 380 | out.last_status = st; |
| 381 | if (st == CKEY_OK) { |
| 382 | out.bytes.assign(buffer.begin(), buffer.begin() + static_cast<std::ptrdiff_t>(std::min(length, buffer.size()))); |
| 383 | out.profile = layout == CKEY_LAYOUT_PRINT ? 1 : 0; |
| 384 | out.version = version; |
| 385 | out.layout = layout; |
| 386 | std::memcpy(out.quad, quad, sizeof quad); |
| 387 | return count; |
| 388 | } |
| 389 | } |
| 390 | if (refine(gray, w, h, stride, border, CKEY_FIELD_LOCATOR, quad)) { |
| 391 | const ckey_status st = ckey_decode_gray8(gray, w, h, stride, quad, buffer.data(), CKEY_LOCATOR_PAYLOAD_BYTES, &diag); |
| 392 | out.last_status = st; |
| 393 | if (st == CKEY_OK) { |
| 394 | out.bytes.assign(buffer.begin(), buffer.begin() + CKEY_LOCATOR_PAYLOAD_BYTES); |
| 395 | out.profile = 2; |
| 396 | std::memcpy(out.quad, quad, sizeof quad); |
| 397 | return count; |
| 398 | } |
| 399 | } |
| 400 | if (refine(gray, w, h, stride, border, CKEY_FIELD_TEXT, quad)) { |
| 401 | length = 0; |
| 402 | const ckey_status st = ckey_decode_text_gray8(gray, w, h, stride, quad, buffer.data(), |
| 403 | CKEY_EXTENDED_PAYLOAD_MAX_BYTES, &length, &diag); |
| 404 | out.last_status = st; |
| 405 | if (st == CKEY_OK) { |
| 406 | out.bytes.assign(buffer.begin(), buffer.begin() + static_cast<std::ptrdiff_t>(std::min<size_t>(length, CKEY_EXTENDED_PAYLOAD_MAX_BYTES))); |
| 407 | out.profile = 3; |
| 408 | std::memcpy(out.quad, quad, sizeof quad); |
| 409 | return count; |
| 410 | } |
| 411 | } |
| 412 | if (refine(gray, w, h, stride, border, CKEY_FIELD_EXTENDED, quad)) { |
| 413 | length = 0; |
| 414 | const ckey_status st = ckey_decode_extended_gray8(gray, w, h, stride, quad, buffer.data(), |
| 415 | CKEY_EXTENDED_PAYLOAD_MAX_BYTES, &length, &diag); |
| 416 | out.last_status = st; |
| 417 | if (st == CKEY_OK) { |
| 418 | out.bytes.assign(buffer.begin(), buffer.begin() + static_cast<std::ptrdiff_t>(std::min<size_t>(length, CKEY_EXTENDED_PAYLOAD_MAX_BYTES))); |
| 419 | out.profile = 4; |
| 420 | std::memcpy(out.quad, quad, sizeof quad); |
| 421 | return count; |
| 422 | } |
| 423 | } |
| 424 | } |
| 425 | return count; |
| 426 | } |
| 427 | |
| 428 | // Validates a frame description against the bytes available after offset. |
| 429 | bool check_frame(JNIEnv *env, int64_t available, jint offset, jint width, jint height, jint stride, jint max_candidates, |
| 430 | jintArray out_info, jdoubleArray out_quad) { |
| 431 | if (width <= 0 || height <= 0 || stride < width || offset < 0 || max_candidates <= 0) { |
| 432 | throw_iae(env, "invalid frame geometry (width, height > 0; stride >= width; offset >= 0)"); |
| 433 | return false; |
| 434 | } |
| 435 | const int64_t needed = static_cast<int64_t>(offset) + static_cast<int64_t>(height - 1) * stride + width; |
| 436 | if (needed > available) { throw_iae(env, "frame buffer is smaller than offset + (height - 1) * stride + width"); return false; } |
| 437 | if (!out_info || env->GetArrayLength(out_info) < kDecodeInfo || !out_quad || env->GetArrayLength(out_quad) < 8) { |
| 438 | throw_iae(env, "decode output arrays are too small"); |
| 439 | return false; |
| 440 | } |
| 441 | return true; |
| 442 | } |
| 443 | |
| 444 | jbyteArray finish_decode(JNIEnv *env, size_t candidates, const Decoded &d, jintArray out_info, jdoubleArray out_quad) { |
| 445 | jint info[kDecodeInfo] = {d.profile, static_cast<jint>(d.version), static_cast<jint>(d.layout), |
| 446 | static_cast<jint>(candidates), d.last_status}; |
| 447 | env->SetIntArrayRegion(out_info, 0, kDecodeInfo, info); |
| 448 | if (d.profile < 0) return nullptr; |
| 449 | env->SetDoubleArrayRegion(out_quad, 0, 8, d.quad); |
| 450 | return new_byte_array(env, d.bytes.data(), d.bytes.size()); |
| 451 | } |
| 452 | // CKEY_JNI_SCAN |
| 453 | |
| 454 | } // namespace |
| 455 | |
| 456 | extern "C" { |
| 457 | |
| 458 | JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *) { |
| 459 | JNIEnv *env = nullptr; |
| 460 | if (vm->GetEnv(reinterpret_cast<void **>(&env), JNI_VERSION_1_6) != JNI_OK) return JNI_ERR; |
| 461 | jclass s = env->FindClass("java/lang/String"); |
| 462 | if (!s) return JNI_ERR; |
| 463 | g_string = static_cast<jclass>(env->NewGlobalRef(s)); |
| 464 | g_string_from_bytes = env->GetMethodID(g_string, "<init>", "([BLjava/lang/String;)V"); |
| 465 | g_string_get_bytes = env->GetMethodID(g_string, "getBytes", "(Ljava/lang/String;)[B"); |
| 466 | jstring utf8 = env->NewStringUTF("UTF-8"); |
| 467 | if (!g_string_from_bytes || !g_string_get_bytes || !utf8) return JNI_ERR; |
| 468 | g_utf8 = static_cast<jstring>(env->NewGlobalRef(utf8)); |
| 469 | jclass e = env->FindClass("org/openckey/ckey/CKeyException"); |
| 470 | if (e) { |
| 471 | g_ckey_exception = static_cast<jclass>(env->NewGlobalRef(e)); |
| 472 | g_ckey_exception_ctor = env->GetMethodID(g_ckey_exception, "<init>", "(ILjava/lang/String;)V"); |
| 473 | } |
| 474 | if (env->ExceptionCheck()) env->ExceptionClear(); // fall back to IllegalStateException |
| 475 | return JNI_VERSION_1_6; |
| 476 | } |
| 477 | |
| 478 | // ---- Library ------------------------------------------------------------ |
| 479 | |
| 480 | jint CKEY_JNI_FN(nativeFeatures)(JNIEnv *, jclass) { |
| 481 | jint f = 0; |
| 482 | |
| 483 | f |= kFeatureScan; |
| 484 | |
| 485 | |
| 486 | f |= kFeatureRender; |
| 487 | |
| 488 | |
| 489 | f |= kFeatureTelemetry; |
| 490 | |
| 491 | |
| 492 | f |= kFeatureCertify; |
| 493 | |
| 494 | return f; |
| 495 | } |
| 496 | |
| 497 | jstring CKEY_JNI_FN(nativeSoftwareVersion)(JNIEnv *env, jclass) { |
| 498 | const char *v = ckey_get_software_version(); |
| 499 | return new_utf8_string(env, v ? v : "", v ? std::strlen(v) : 0); |
| 500 | } |
| 501 | |
| 502 | jint CKEY_JNI_FN(nativeAbiVersion)(JNIEnv *, jclass) { return static_cast<jint>(ckey_get_abi_version()); } |
| 503 | |
| 504 | // ---- Codec -------------------------------------------------------------- |
| 505 | |
| 506 | jint CKEY_JNI_FN(nativeCapacity)(JNIEnv *, jclass, jint version, jint level, jint layout) { |
| 507 | if (version < 1 || level < 0 || layout < 0) return 0; |
| 508 | const size_t c = ckey_scalable_capacity(static_cast<uint32_t>(version), static_cast<ckey_ecc_level>(level), |
| 509 | static_cast<ckey_scalable_layout>(layout)); |
| 510 | return static_cast<jint>(std::min<size_t>(c, static_cast<size_t>(std::numeric_limits<jint>::max()))); |
| 511 | } |
| 512 | |
| 513 | jint CKEY_JNI_FN(nativeModulesAcross)(JNIEnv *, jclass, jint version) { |
| 514 | return version < 1 ? 0 : static_cast<jint>(ckey_scalable_modules_across(static_cast<uint32_t>(version))); |
| 515 | } |
| 516 | |
| 517 | jbyteArray CKEY_JNI_FN(nativeEncode)(JNIEnv *env, jclass, jbyteArray payload, jint version, jint level, jint layout) { |
| 518 | return guarded<jbyteArray>(env, nullptr, [&]() -> jbyteArray { |
| 519 | if (!payload) { throw_class(env, "java/lang/NullPointerException", "payload"); return nullptr; } |
| 520 | if (version < 0 || level < 0 || layout < 0) { throw_iae(env, "invalid version, level or layout"); return nullptr; } |
| 521 | const jsize n = env->GetArrayLength(payload); |
| 522 | std::vector<uint8_t> bytes(static_cast<size_t>(n)); |
| 523 | if (n) env->GetByteArrayRegion(payload, 0, n, reinterpret_cast<jbyte *>(bytes.data())); |
| 524 | auto symbol = std::make_unique<ckey_scalable_symbol>(); |
| 525 | std::memset(symbol.get(), 0, sizeof(ckey_scalable_symbol)); |
| 526 | ckey_diagnostics diag{}; |
| 527 | diag.abi_version = CKEY_ABI_VERSION; |
| 528 | const ckey_status st = ckey_encode_scalable(bytes.data(), bytes.size(), static_cast<uint32_t>(version), |
| 529 | static_cast<ckey_ecc_level>(level), |
| 530 | static_cast<ckey_scalable_layout>(layout), symbol.get(), &diag); |
| 531 | if (st != CKEY_OK) { throw_ckey(env, st, diag, "encode failed"); return nullptr; } |
| 532 | return new_byte_array(env, reinterpret_cast<const uint8_t *>(symbol.get()), sizeof(ckey_scalable_symbol)); |
| 533 | }); |
| 534 | } |
| 535 | |
| 536 | // [version, ecc_level, layout, payload_length, codeword_length] |
| 537 | jintArray CKEY_JNI_FN(nativeSymbolInfo)(JNIEnv *env, jclass, jbyteArray blob) { |
| 538 | return guarded<jintArray>(env, nullptr, [&]() -> jintArray { |
| 539 | auto symbol = std::make_unique<ckey_scalable_symbol>(); |
| 540 | if (!read_symbol(env, blob, *symbol)) return nullptr; |
| 541 | const jint info[5] = {static_cast<jint>(symbol->version), static_cast<jint>(symbol->ecc_level), |
| 542 | static_cast<jint>(symbol->layout), static_cast<jint>(symbol->payload_length), |
| 543 | static_cast<jint>(symbol->codeword_length)}; |
| 544 | jintArray out = env->NewIntArray(5); |
| 545 | if (out) env->SetIntArrayRegion(out, 0, 5, info); |
| 546 | return out; |
| 547 | }); |
| 548 | } |
| 549 | |
| 550 | // ---- Safe Actions (spec/CKey-Safe-Actions.md) --------------------------- |
| 551 | // A refusal is an ordinary outcome here, not an exception: out_status[0] |
| 552 | // receives the core status and the returned string is the core's diagnostic. |
| 553 | // On CKEY_OK the string is the verified fields, one per line (every field is |
| 554 | // restricted ASCII, params are hex): scope, issuer, app, action, key_id, |
| 555 | // request_id, issued_at, expires_at, params. A null public key parses only; |
| 556 | // CKeySafeActions never treats a parsed-only envelope as verified. |
| 557 | |
| 558 | jstring CKEY_JNI_FN(nativeSafeAction)(JNIEnv *env, jclass, jbyteArray payload, jbyteArray public_key, jlong now_seconds, |
| 559 | jint max_lifetime, jintArray out_status) { |
| 560 | return guarded<jstring>(env, nullptr, [&]() -> jstring { |
| 561 | if (!payload || !out_status) { throw_class(env, "java/lang/NullPointerException", "payload"); return nullptr; } |
| 562 | if (env->GetArrayLength(out_status) < 1) { throw_iae(env, "out_status needs one element"); return nullptr; } |
| 563 | if (public_key && env->GetArrayLength(public_key) != 32) { throw_iae(env, "a public key is 32 bytes"); return nullptr; } |
| 564 | if (now_seconds < 0 || max_lifetime < 1) { throw_iae(env, "invalid time or lifetime"); return nullptr; } |
| 565 | const jsize n = env->GetArrayLength(payload); |
| 566 | std::vector<uint8_t> bytes(static_cast<size_t>(n)); |
| 567 | if (n) env->GetByteArrayRegion(payload, 0, n, reinterpret_cast<jbyte *>(bytes.data())); |
| 568 | uint8_t key[32]; |
| 569 | if (public_key) env->GetByteArrayRegion(public_key, 0, 32, reinterpret_cast<jbyte *>(key)); |
| 570 | auto sa = std::make_unique<ckey_safe_action>(); |
| 571 | std::memset(sa.get(), 0, sizeof(ckey_safe_action)); |
| 572 | ckey_diagnostics diag{}; |
| 573 | diag.abi_version = CKEY_ABI_VERSION; |
| 574 | const ckey_status st = public_key |
| 575 | ? ckey_safe_action_verify(bytes.data(), bytes.size(), key, static_cast<uint64_t>(now_seconds), |
| 576 | static_cast<uint32_t>(max_lifetime), sa.get(), &diag) |
| 577 | : ckey_safe_action_parse(bytes.data(), bytes.size(), sa.get(), &diag); |
| 578 | const jint status = static_cast<jint>(st); |
| 579 | env->SetIntArrayRegion(out_status, 0, 1, &status); |
| 580 | if (st != CKEY_OK) { |
| 581 | const size_t len = strnlen(diag.message, sizeof(diag.message)); |
| 582 | return new_utf8_string(env, diag.message, len); |
| 583 | } |
| 584 | std::string out; |
| 585 | out.reserve(1024 + 2 * sa->params_len); |
| 586 | auto line = [&](const std::string &v) { out += v; out += '\n'; }; |
| 587 | line(std::to_string(sa->scope)); |
| 588 | line(sa->issuer); |
| 589 | line(sa->app); |
| 590 | line(sa->action); |
| 591 | line(sa->key_id); |
| 592 | line(sa->request_id); |
| 593 | line(std::to_string(sa->issued_at)); |
| 594 | line(std::to_string(sa->expires_at)); |
| 595 | static const char digits[] = "0123456789abcdef"; |
| 596 | for (size_t i = 0; i < sa->params_len && i < sizeof(sa->params); ++i) { |
| 597 | out += digits[sa->params[i] >> 4]; |
| 598 | out += digits[sa->params[i] & 15]; |
| 599 | } |
| 600 | return new_utf8_string(env, out.data(), out.size()); |
| 601 | }); |
| 602 | } |
| 603 | |
| 604 | // ---- Rendering ---------------------------------------------------------- |
| 605 | |
| 606 | |
| 607 | void CKEY_JNI_FN(nativeStylePreset)(JNIEnv *env, jclass, jint preset, jintArray out_ints, jdoubleArray out_doubles) { |
| 608 | if (!check_style_shape(env, out_ints, out_doubles)) return; |
| 609 | ckey_style s; |
| 610 | ckey_style_default(&s); |
| 611 | const ckey_status st = ckey_style_preset_fill(&s, static_cast<ckey_style_preset>(preset)); |
| 612 | if (st != CKEY_OK) { throw_ckey(env, st, "unknown style preset"); return; } |
| 613 | write_style(env, s, out_ints, out_doubles); |
| 614 | } |
| 615 | |
| 616 | jstring CKEY_JNI_FN(nativeCheckStyle)(JNIEnv *env, jclass, jbyteArray blob, jintArray ints, jdoubleArray doubles, |
| 617 | jstring logo_href) { |
| 618 | return guarded<jstring>(env, nullptr, [&]() -> jstring { |
| 619 | auto symbol = std::make_unique<ckey_scalable_symbol>(); |
| 620 | StyleArg style; |
| 621 | if (!read_symbol(env, blob, *symbol) || !read_style(env, ints, doubles, logo_href, style)) return nullptr; |
| 622 | ckey_style fallback; |
| 623 | ckey_style_default(&fallback); |
| 624 | ckey_diagnostics diag{}; |
| 625 | diag.abi_version = CKEY_ABI_VERSION; |
| 626 | const ckey_status st = ckey_check_scalable_style(symbol.get(), style.present ? &style.style : &fallback, &diag); |
| 627 | if (st != CKEY_OK) { throw_ckey(env, st, diag, "style refused"); return nullptr; } |
| 628 | return new_utf8_string(env, diag.message, strnlen(diag.message, sizeof diag.message)); |
| 629 | }); |
| 630 | } |
| 631 | |
| 632 | // Straight RGBA8 (or GRAY8). premultiply = true converts RGBA to the |
| 633 | // premultiplied form Bitmap.copyPixelsFromBuffer expects for ARGB_8888. |
| 634 | jbyteArray CKEY_JNI_FN(nativeRenderRaster)(JNIEnv *env, jclass, jbyteArray blob, jintArray ints, jdoubleArray doubles, |
| 635 | jstring logo_href, jint size_px, jint policy, jboolean gray, |
| 636 | jboolean transparent, jboolean premultiply, jint supersample, |
| 637 | jdouble min_feature_px, jboolean materials, jbyteArray logo_rgba, |
| 638 | jint logo_w, jint logo_h) { |
| 639 | return guarded<jbyteArray>(env, nullptr, [&]() -> jbyteArray { |
| 640 | auto symbol = std::make_unique<ckey_scalable_symbol>(); |
| 641 | StyleArg style; |
| 642 | RasterArg raster; |
| 643 | if (!read_symbol(env, blob, *symbol) || !read_style(env, ints, doubles, logo_href, style) || |
| 644 | !read_raster(env, size_px, policy, gray, transparent, supersample, min_feature_px, materials, logo_rgba, |
| 645 | logo_w, logo_h, raster)) |
| 646 | return nullptr; |
| 647 | std::vector<uint8_t> pixels; |
| 648 | size_t required = 0; |
| 649 | ckey_status st = ckey_render_scalable_raster(symbol.get(), style.get(), &raster.options, nullptr, 0, &required); |
| 650 | if (st != CKEY_OK && st != CKEY_INSUFFICIENT_BUFFER) { explain_style(env, *symbol, style, st); return nullptr; } |
| 651 | if (!render_raster(env, *symbol, style, raster, pixels)) return nullptr; |
| 652 | if (premultiply && !gray) { |
| 653 | for (size_t p = 0; p + 3 < pixels.size(); p += 4) { |
| 654 | const uint32_t a = pixels[p + 3]; |
| 655 | if (a == 255) continue; |
| 656 | for (int c = 0; c < 3; ++c) pixels[p + c] = static_cast<uint8_t>((pixels[p + c] * a + 127) / 255); |
| 657 | } |
| 658 | } |
| 659 | return new_byte_array(env, pixels.data(), pixels.size()); |
| 660 | }); |
| 661 | } |
| 662 | |
| 663 | jbyteArray CKEY_JNI_FN(nativeRenderPng)(JNIEnv *env, jclass, jbyteArray blob, jintArray ints, jdoubleArray doubles, |
| 664 | jstring logo_href, jint size_px, jint policy, jboolean transparent, |
| 665 | jint supersample, jdouble min_feature_px, jboolean materials, |
| 666 | jbyteArray logo_rgba, jint logo_w, jint logo_h, jdouble dpi) { |
| 667 | return guarded<jbyteArray>(env, nullptr, [&]() -> jbyteArray { |
| 668 | auto symbol = std::make_unique<ckey_scalable_symbol>(); |
| 669 | StyleArg style; |
| 670 | RasterArg raster; |
| 671 | if (!read_symbol(env, blob, *symbol) || !read_style(env, ints, doubles, logo_href, style) || |
| 672 | !read_raster(env, size_px, policy, false, transparent, supersample, min_feature_px, materials, logo_rgba, |
| 673 | logo_w, logo_h, raster)) |
| 674 | return nullptr; |
| 675 | size_t required = 0; |
| 676 | ckey_status st = ckey_render_scalable_raster(symbol.get(), style.get(), &raster.options, nullptr, 0, &required); |
| 677 | if (st != CKEY_OK && st != CKEY_INSUFFICIENT_BUFFER) { explain_style(env, *symbol, style, st); return nullptr; } |
| 678 | std::vector<uint8_t> pixels; |
| 679 | if (!render_raster(env, *symbol, style, raster, pixels)) return nullptr; |
| 680 | const uint32_t side = raster.options.size_px; |
| 681 | size_t png_size = 0; |
| 682 | st = ckey_encode_png(pixels.data(), side, side, static_cast<size_t>(side) * 4u, CKEY_PNG_RGBA8, dpi, nullptr, 0, &png_size); |
| 683 | if (st != CKEY_OK && st != CKEY_INSUFFICIENT_BUFFER) { throw_ckey(env, st, "PNG encoding failed"); return nullptr; } |
| 684 | std::vector<uint8_t> png(png_size); |
| 685 | st = ckey_encode_png(pixels.data(), side, side, static_cast<size_t>(side) * 4u, CKEY_PNG_RGBA8, dpi, png.data(), |
| 686 | png.size(), &png_size); |
| 687 | if (st != CKEY_OK) { throw_ckey(env, st, "PNG encoding failed"); return nullptr; } |
| 688 | return new_byte_array(env, png.data(), std::min(png_size, png.size())); |
| 689 | }); |
| 690 | } |
| 691 | |
| 692 | jstring CKEY_JNI_FN(nativeRenderSvg)(JNIEnv *env, jclass, jbyteArray blob, jintArray ints, jdoubleArray doubles, |
| 693 | jstring logo_href, jint scale) { |
| 694 | return guarded<jstring>(env, nullptr, [&]() -> jstring { |
| 695 | auto symbol = std::make_unique<ckey_scalable_symbol>(); |
| 696 | StyleArg style; |
| 697 | if (!read_symbol(env, blob, *symbol) || !read_style(env, ints, doubles, logo_href, style)) return nullptr; |
| 698 | if (scale <= 0) { throw_iae(env, "scale must be positive"); return nullptr; } |
| 699 | ckey_style fallback; |
| 700 | ckey_style_default(&fallback); |
| 701 | const ckey_style *st_ptr = style.present ? &style.style : &fallback; |
| 702 | size_t required = 0; |
| 703 | ckey_status st = ckey_render_scalable_styled_svg(symbol.get(), st_ptr, static_cast<uint32_t>(scale), nullptr, 0, &required); |
| 704 | if (st != CKEY_OK && st != CKEY_INSUFFICIENT_BUFFER) { explain_style(env, *symbol, style, st); return nullptr; } |
| 705 | std::vector<char> svg(required + 1); |
| 706 | st = ckey_render_scalable_styled_svg(symbol.get(), st_ptr, static_cast<uint32_t>(scale), svg.data(), svg.size(), &required); |
| 707 | if (st != CKEY_OK) { throw_ckey(env, st, "SVG rendering failed"); return nullptr; } |
| 708 | return new_utf8_string(env, svg.data(), strnlen(svg.data(), svg.size())); |
| 709 | }); |
| 710 | } |
| 711 | |
| 712 | jbyteArray CKEY_JNI_FN(nativeRenderPdf)(JNIEnv *env, jclass, jbyteArray blob, jintArray ints, jdoubleArray doubles, |
| 713 | jstring logo_href, jdouble width_pt, jboolean transparent) { |
| 714 | return guarded<jbyteArray>(env, nullptr, [&]() -> jbyteArray { |
| 715 | auto symbol = std::make_unique<ckey_scalable_symbol>(); |
| 716 | StyleArg style; |
| 717 | if (!read_symbol(env, blob, *symbol) || !read_style(env, ints, doubles, logo_href, style)) return nullptr; |
| 718 | size_t required = 0; |
| 719 | ckey_status st = ckey_render_scalable_pdf(symbol.get(), style.get(), width_pt, transparent ? 1u : 0u, nullptr, 0, &required); |
| 720 | if (st != CKEY_OK && st != CKEY_INSUFFICIENT_BUFFER) { explain_style(env, *symbol, style, st); return nullptr; } |
| 721 | std::vector<uint8_t> pdf(required); |
| 722 | st = ckey_render_scalable_pdf(symbol.get(), style.get(), width_pt, transparent ? 1u : 0u, pdf.data(), pdf.size(), &required); |
| 723 | if (st != CKEY_OK) { throw_ckey(env, st, "PDF rendering failed"); return nullptr; } |
| 724 | return new_byte_array(env, pdf.data(), std::min(required, pdf.size())); |
| 725 | }); |
| 726 | } |
| 727 | |
| 728 | // CKEY_JNI_RENDER |
| 729 | |
| 730 | // ---- Certification (optional) ------------------------------------------ |
| 731 | &&&& |
| 732 | // model: null for the default, else [px_per_module, blur_px, noise, glare, trials] |
| 733 | // out_cert: [status, certified, policy, adjustments, trials, trials_passed, |
| 734 | // min_contrast, feature_separation, logo_load] |
| 735 | // Returns the certification message; never throws for a refusal (status says). |
| 736 | jstring CKEY_JNI_FN(nativeCertify)(JNIEnv *env, jclass, jbyteArray blob, jintArray ints, jdoubleArray doubles, |
| 737 | jstring logo_href, jboolean transparent, jintArray backdrop_colors, |
| 738 | jbyteArray backdrop_rgba, jint backdrop_w, jint backdrop_h, jdoubleArray model, |
| 739 | jintArray out_style_ints, jdoubleArray out_style_doubles, jdoubleArray out_cert) { |
| 740 | return guarded<jstring>(env, nullptr, [&]() -> jstring { |
| 741 | auto symbol = std::make_unique<ckey_scalable_symbol>(); |
| 742 | StyleArg style; |
| 743 | if (!read_symbol(env, blob, *symbol) || !read_style(env, ints, doubles, logo_href, style)) return nullptr; |
| 744 | if (!check_style_shape(env, out_style_ints, out_style_doubles)) return nullptr; |
| 745 | if (!out_cert || env->GetArrayLength(out_cert) < 9) { throw_iae(env, "certification output array too small"); return nullptr; } |
| 746 | std::vector<ckey_rgb> colours; |
| 747 | if (backdrop_colors) { |
| 748 | const jsize n = env->GetArrayLength(backdrop_colors); |
| 749 | if (n > 16) { throw_iae(env, "at most 16 backdrop colours"); return nullptr; } |
| 750 | std::vector<jint> raw(static_cast<size_t>(n)); |
| 751 | if (n) env->GetIntArrayRegion(backdrop_colors, 0, n, raw.data()); |
| 752 | for (jint c : raw) colours.push_back(rgb_from_int(c)); |
| 753 | } |
| 754 | std::vector<uint8_t> pixels; |
| 755 | if (backdrop_rgba) { |
| 756 | if (backdrop_w <= 0 || backdrop_h <= 0 || |
| 757 | static_cast<int64_t>(env->GetArrayLength(backdrop_rgba)) != static_cast<int64_t>(backdrop_w) * backdrop_h * 4) { |
| 758 | throw_iae(env, "backdrop pixels must be width * height * 4 bytes"); |
| 759 | return nullptr; |
| 760 | } |
| 761 | pixels.resize(static_cast<size_t>(backdrop_w) * static_cast<size_t>(backdrop_h) * 4u); |
| 762 | env->GetByteArrayRegion(backdrop_rgba, 0, static_cast<jsize>(pixels.size()), reinterpret_cast<jbyte *>(pixels.data())); |
| 763 | } |
| 764 | ckey_backdrop backdrop{}; |
| 765 | backdrop.colours = colours.empty() ? nullptr : colours.data(); |
| 766 | backdrop.colour_count = static_cast<uint32_t>(colours.size()); |
| 767 | backdrop.rgba = pixels.empty() ? nullptr : pixels.data(); |
| 768 | backdrop.width = pixels.empty() ? 0u : static_cast<uint32_t>(backdrop_w); |
| 769 | backdrop.height = pixels.empty() ? 0u : static_cast<uint32_t>(backdrop_h); |
| 770 | backdrop.stride = 0; |
| 771 | const bool has_backdrop = !colours.empty() || !pixels.empty(); |
| 772 | ckey_scan_model scan; |
| 773 | ckey_scan_model_default(&scan); |
| 774 | if (model) { |
| 775 | if (env->GetArrayLength(model) != 5) { throw_iae(env, "scan model needs 5 values"); return nullptr; } |
| 776 | jdouble m[5]; |
| 777 | env->GetDoubleArrayRegion(model, 0, 5, m); |
| 778 | scan.px_per_module = m[0]; |
| 779 | scan.blur_px = m[1]; |
| 780 | scan.noise = m[2]; |
| 781 | scan.glare = m[3]; |
| 782 | scan.trials = m[4] >= 0 && m[4] <= 1e6 ? static_cast<uint32_t>(m[4]) : 0u; |
| 783 | } |
| 784 | ckey_style effective; |
| 785 | ckey_style_default(&effective); |
| 786 | ckey_certification cert{}; |
| 787 | cert.abi_version = CKEY_ABI_VERSION; |
| 788 | const ckey_status st = ckey_certify_scalable_style(symbol.get(), style.get(), transparent ? 1u : 0u, |
| 789 | has_backdrop ? &backdrop : nullptr, &scan, &effective, &cert); |
| 790 | // The effective style may point at our logo string; Java gets the URL |
| 791 | // back from its own input, so only the plain fields are copied. |
| 792 | write_style(env, effective, out_style_ints, out_style_doubles); |
| 793 | const jdouble result[9] = {static_cast<jdouble>(st), static_cast<jdouble>(cert.certified), |
| 794 | static_cast<jdouble>(cert.policy), static_cast<jdouble>(cert.adjustments), |
| 795 | static_cast<jdouble>(cert.trials), static_cast<jdouble>(cert.trials_passed), |
| 796 | cert.min_contrast, cert.feature_separation, cert.logo_load}; |
| 797 | env->SetDoubleArrayRegion(out_cert, 0, 9, result); |
| 798 | return new_utf8_string(env, cert.message, strnlen(cert.message, sizeof cert.message)); |
| 799 | }); |
| 800 | } |
| 801 | |
| 802 | |
| 803 | // ---- Scanning ----------------------------------------------------------- |
| 804 | |
| 805 | |
| 806 | jbyteArray CKEY_JNI_FN(nativeDecodeArray)(JNIEnv *env, jclass, jbyteArray gray, jint offset, jint width, jint height, |
| 807 | jint stride, jint max_candidates, jintArray out_info, jdoubleArray out_quad) { |
| 808 | return guarded<jbyteArray>(env, nullptr, [&]() -> jbyteArray { |
| 809 | if (!gray) { throw_class(env, "java/lang/NullPointerException", "gray"); return nullptr; } |
| 810 | if (!check_frame(env, env->GetArrayLength(gray), offset, width, height, stride, max_candidates, out_info, out_quad)) |
| 811 | return nullptr; |
| 812 | // GetByteArrayElements (not a critical section): decoding can take tens |
| 813 | // of milliseconds and must not stall the garbage collector. |
| 814 | jbyte *data = env->GetByteArrayElements(gray, nullptr); |
| 815 | if (!data) return nullptr; |
| 816 | Decoded d; |
| 817 | size_t candidates = 0; |
| 818 | try { |
| 819 | candidates = decode_frame(reinterpret_cast<const uint8_t *>(data) + offset, static_cast<size_t>(width), |
| 820 | static_cast<size_t>(height), static_cast<size_t>(stride), |
| 821 | std::min<size_t>(static_cast<size_t>(max_candidates), kMaxCandidates), d); |
| 822 | } catch (...) { |
| 823 | env->ReleaseByteArrayElements(gray, data, JNI_ABORT); |
| 824 | throw; |
| 825 | } |
| 826 | env->ReleaseByteArrayElements(gray, data, JNI_ABORT); |
| 827 | return finish_decode(env, candidates, d, out_info, out_quad); |
| 828 | }); |
| 829 | } |
| 830 | |
| 831 | // A direct ByteBuffer (for example android.media.Image's Y plane): no copy. |
| 832 | jbyteArray CKEY_JNI_FN(nativeDecodeBuffer)(JNIEnv *env, jclass, jobject buffer, jint offset, jint width, jint height, |
| 833 | jint stride, jint max_candidates, jintArray out_info, jdoubleArray out_quad) { |
| 834 | return guarded<jbyteArray>(env, nullptr, [&]() -> jbyteArray { |
| 835 | if (!buffer) { throw_class(env, "java/lang/NullPointerException", "buffer"); return nullptr; } |
| 836 | auto *data = static_cast<const uint8_t *>(env->GetDirectBufferAddress(buffer)); |
| 837 | const jlong capacity = env->GetDirectBufferCapacity(buffer); |
| 838 | if (!data || capacity < 0) { throw_iae(env, "decodeBuffer needs a direct ByteBuffer"); return nullptr; } |
| 839 | if (!check_frame(env, capacity, offset, width, height, stride, max_candidates, out_info, out_quad)) return nullptr; |
| 840 | Decoded d; |
| 841 | const size_t candidates = decode_frame(data + offset, static_cast<size_t>(width), static_cast<size_t>(height), |
| 842 | static_cast<size_t>(stride), |
| 843 | std::min<size_t>(static_cast<size_t>(max_candidates), kMaxCandidates), d); |
| 844 | return finish_decode(env, candidates, d, out_info, out_quad); |
| 845 | }); |
| 846 | } |
| 847 | |
| 848 | // CKEY_JNI_SCAN |
| 849 | |
| 850 | // ---- Telemetry ---------------------------------------------------------- |
| 851 | |
| 852 | |
| 853 | jint CKEY_JNI_FN(nativeTelemetrySetMode)(JNIEnv *, jclass, jint mode) { return ckey_telemetry_set_mode(mode); } |
| 854 | jint CKEY_JNI_FN(nativeTelemetryGetMode)(JNIEnv *, jclass) { return ckey_telemetry_get_mode(); } |
| 855 | |
| 856 | jint CKEY_JNI_FN(nativeTelemetrySetClient)(JNIEnv *env, jclass, jstring sdk, jstring version, jstring platform) { |
| 857 | return guarded<jint>(env, CKEY_INTERNAL_ERROR, [&]() -> jint { |
| 858 | std::string s, v, p; |
| 859 | bool hs = false, hv = false, hp = false; |
| 860 | if (!utf8_from_jstring(env, sdk, s, hs) || !utf8_from_jstring(env, version, v, hv) || |
| 861 | !utf8_from_jstring(env, platform, p, hp)) |
| 862 | return CKEY_INVALID_ARGUMENT; |
| 863 | if (!hs || !hv || !hp) return CKEY_INVALID_ARGUMENT; |
| 864 | return ckey_telemetry_set_client(s.c_str(), v.c_str(), p.c_str()); |
| 865 | }); |
| 866 | } |
| 867 | |
| 868 | static uint64_t ms(jlong v) { return v < 0 ? 0u : static_cast<uint64_t>(v); } |
| 869 | |
| 870 | void CKEY_JNI_FN(nativeTelemetrySessionBegin)(JNIEnv *, jclass, jlong now_ms) { ckey_telemetry_session_begin(ms(now_ms)); } |
| 871 | |
| 872 | void CKEY_JNI_FN(nativeTelemetryFrame)(JNIEnv *, jclass, jlong now_ms, jint processing_ms) { |
| 873 | ckey_telemetry_frame(ms(now_ms), processing_ms < 0 ? 0u : static_cast<uint32_t>(processing_ms)); |
| 874 | } |
| 875 | |
| 876 | jint CKEY_JNI_FN(nativeTelemetrySessionSuccess)(JNIEnv *, jclass, jlong now_ms, jint profile) { |
| 877 | if (profile < 0) return CKEY_INVALID_ARGUMENT; |
| 878 | return ckey_telemetry_session_success(ms(now_ms), static_cast<uint32_t>(profile)); |
| 879 | } |
| 880 | |
| 881 | void CKEY_JNI_FN(nativeTelemetrySessionEnd)(JNIEnv *, jclass, jlong now_ms) { ckey_telemetry_session_end(ms(now_ms)); } |
| 882 | |
| 883 | jboolean CKEY_JNI_FN(nativeTelemetryShouldSend)(JNIEnv *, jclass, jlong now_ms) { |
| 884 | return ckey_telemetry_should_send(ms(now_ms)) ? JNI_TRUE : JNI_FALSE; |
| 885 | } |
| 886 | |
| 887 | // Returns the batch JSON and writes its id to out_id[0]; null when there is |
| 888 | // nothing to send. |
| 889 | jstring CKEY_JNI_FN(nativeTelemetryTakeBatch)(JNIEnv *env, jclass, jstring utc_day, jlongArray out_id) { |
| 890 | return guarded<jstring>(env, nullptr, [&]() -> jstring { |
| 891 | std::string day; |
| 892 | bool has_day = false; |
| 893 | if (!utf8_from_jstring(env, utc_day, day, has_day)) return nullptr; |
| 894 | if (!has_day || !out_id || env->GetArrayLength(out_id) < 1) { throw_iae(env, "takeBatch needs a day and an id array"); return nullptr; } |
| 895 | std::vector<char> json(CKEY_TELEMETRY_MAX_BODY_BYTES + 1); |
| 896 | size_t len = 0; |
| 897 | uint64_t id = 0; |
| 898 | ckey_status st = ckey_telemetry_take_batch(day.c_str(), json.data(), json.size(), &len, &id); |
| 899 | if (st == CKEY_INSUFFICIENT_BUFFER) { |
| 900 | json.resize(len + 1); |
| 901 | st = ckey_telemetry_take_batch(day.c_str(), json.data(), json.size(), &len, &id); |
| 902 | } |
| 903 | if (st != CKEY_OK) { throw_ckey(env, st, "telemetry batch refused (invalid day?)"); return nullptr; } |
| 904 | if (len == 0 || id == 0) return nullptr; |
| 905 | const jlong jid = static_cast<jlong>(id); |
| 906 | env->SetLongArrayRegion(out_id, 0, 1, &jid); |
| 907 | return new_utf8_string(env, json.data(), std::min(len, json.size())); |
| 908 | }); |
| 909 | } |
| 910 | |
| 911 | jint CKEY_JNI_FN(nativeTelemetryBatchResult)(JNIEnv *, jclass, jlong batch_id, jint http_status, jlong now_ms) { |
| 912 | return ckey_telemetry_batch_result(static_cast<uint64_t>(batch_id), http_status, ms(now_ms)); |
| 913 | } |
| 914 | |
| 915 | static jlong clamp64(uint64_t v) { |
| 916 | return v > static_cast<uint64_t>(std::numeric_limits<jlong>::max()) ? std::numeric_limits<jlong>::max() : static_cast<jlong>(v); |
| 917 | } |
| 918 | |
| 919 | // out (33 longs): started, succeeded, failed, frames, decoded[5], |
| 920 | // time_to_decode {count, sum, buckets[10]}, frame_ms {count, sum, buckets[10]} |
| 921 | jint CKEY_JNI_FN(nativeTelemetrySnapshot)(JNIEnv *env, jclass, jlongArray out) { |
| 922 | constexpr jsize kLen = 4 + CKEY_TELEMETRY_PROFILES + 2 * (2 + CKEY_TELEMETRY_BUCKETS); |
| 923 | if (!out || env->GetArrayLength(out) < kLen) { throw_iae(env, "snapshot array too small"); return CKEY_INVALID_ARGUMENT; } |
| 924 | ckey_telemetry_counters c{}; |
| 925 | const ckey_status st = ckey_telemetry_snapshot(&c); |
| 926 | if (st != CKEY_OK) return st; |
| 927 | jlong v[kLen]; |
| 928 | jsize k = 0; |
| 929 | v[k++] = clamp64(c.sessions_started); |
| 930 | v[k++] = clamp64(c.sessions_succeeded); |
| 931 | v[k++] = clamp64(c.sessions_failed); |
| 932 | v[k++] = clamp64(c.frames); |
| 933 | for (unsigned p = 0; p < CKEY_TELEMETRY_PROFILES; ++p) v[k++] = clamp64(c.decoded[p]); |
| 934 | for (const ckey_telemetry_histogram *h : {&c.time_to_decode_ms, &c.frame_ms}) { |
| 935 | v[k++] = clamp64(h->count); |
| 936 | v[k++] = clamp64(h->sum); |
| 937 | for (unsigned b = 0; b < CKEY_TELEMETRY_BUCKETS; ++b) v[k++] = clamp64(h->buckets[b]); |
| 938 | } |
| 939 | env->SetLongArrayRegion(out, 0, kLen, v); |
| 940 | return CKEY_OK; |
| 941 | } |
| 942 | |
| 943 | jstring CKEY_JNI_FN(nativeTelemetryExportState)(JNIEnv *env, jclass) { |
| 944 | return guarded<jstring>(env, nullptr, [&]() -> jstring { |
| 945 | size_t len = 0; |
| 946 | std::vector<char> out(4096); |
| 947 | ckey_status st = ckey_telemetry_export_state(out.data(), out.size(), &len); |
| 948 | if (st == CKEY_INSUFFICIENT_BUFFER) { |
| 949 | out.resize(len + 1); |
| 950 | st = ckey_telemetry_export_state(out.data(), out.size(), &len); |
| 951 | } |
| 952 | if (st != CKEY_OK) { throw_ckey(env, st, "telemetry export failed"); return nullptr; } |
| 953 | return new_utf8_string(env, out.data(), std::min(len, out.size())); |
| 954 | }); |
| 955 | } |
| 956 | |
| 957 | jint CKEY_JNI_FN(nativeTelemetryImportState)(JNIEnv *env, jclass, jstring json) { |
| 958 | return guarded<jint>(env, CKEY_INTERNAL_ERROR, [&]() -> jint { |
| 959 | std::string s; |
| 960 | bool has = false; |
| 961 | if (!utf8_from_jstring(env, json, s, has)) return CKEY_INVALID_ARGUMENT; |
| 962 | if (!has) return CKEY_INVALID_ARGUMENT; |
| 963 | return ckey_telemetry_import_state(s.c_str()); |
| 964 | }); |
| 965 | } |
| 966 | |
| 967 | jstring CKEY_JNI_FN(nativeTelemetryEndpoint)(JNIEnv *env, jclass) { |
| 968 | return new_utf8_string(env, CKEY_TELEMETRY_ENDPOINT, std::strlen(CKEY_TELEMETRY_ENDPOINT)); |
| 969 | } |
| 970 | |
| 971 | // TEST ONLY (instrumented and host tests): ckey_telemetry_reset_for_testing. |
| 972 | void CKEY_JNI_FN(nativeTelemetryResetForTesting)(JNIEnv *, jclass) { ckey_telemetry_reset_for_testing(); } |
| 973 | |
| 974 | // CKEY_JNI_TELEMETRY |
| 975 | |
| 976 | } // extern "C" |