Viewing is open to everyone. Downloading and starring need a GitHub sign-in.
Sign in with GitHub
CKeyDecoder.cs
v0.1.0-build.85
Latest
Commit ede38f5
Sign in with GitHub to download. Each download is recorded with your GitHub username.
| 1 | using System; |
| 2 | using System.Buffers; |
| 3 | using OpenCKey.Interop; |
| 4 | |
| 5 | namespace OpenCKey |
| 6 | { |
| 7 | /// <summary>A decoded symbol.</summary> |
| 8 | public sealed class CKeyResult |
| 9 | { |
| 10 | private CKeyPayloadAction? _action; |
| 11 | private bool _classified; |
| 12 | |
| 13 | internal CKeyResult(byte[] payload, CKeyProfile profile, int version, Layout layout, double[] quad) |
| 14 | { |
| 15 | Payload = payload; |
| 16 | Text = CKeyPayload.ReadableText(payload); |
| 17 | Profile = profile; |
| 18 | Version = version; |
| 19 | Layout = layout; |
| 20 | Quad = quad; |
| 21 | } |
| 22 | |
| 23 | /// <summary>The payload bytes.</summary> |
| 24 | public byte[] Payload { get; } |
| 25 | /// <summary>The payload as text when it is valid UTF-8 per the payload conventions; null for binary payloads.</summary> |
| 26 | public string? Text { get; } |
| 27 | /// <summary>The profile the symbol was read as.</summary> |
| 28 | public CKeyProfile Profile { get; } |
| 29 | /// <summary>Profile 0 version (1..31); 0 for Profiles 1–3.</summary> |
| 30 | public int Version { get; } |
| 31 | /// <summary>Profile 0 layout (standard or print); Standard for Profiles 1–3.</summary> |
| 32 | public Layout Layout { get; } |
| 33 | /// <summary>The refined quadrilateral the decode used: x0,y0,…,x3,y3 in frame pixels.</summary> |
| 34 | public double[] Quad { get; } |
| 35 | |
| 36 | /// <summary>A display name, e.g. "Profile 0 · v3" or "Profile 0.1 · print · v5".</summary> |
| 37 | public string ProfileName => Profile switch |
| 38 | { |
| 39 | CKeyProfile.Scalable => Layout == Layout.Print ? $"Profile 0.1 · print · v{Version}" : $"Profile 0 · v{Version}", |
| 40 | CKeyProfile.Locator => "Profile 1 · Locator", |
| 41 | CKeyProfile.Text => "Profile 2 · Text", |
| 42 | _ => "Profile 3 · Extended", |
| 43 | }; |
| 44 | |
| 45 | /// <summary>Lower-case hex of the payload.</summary> |
| 46 | public string PayloadHex => CKeyPayload.ToHex(Payload); |
| 47 | |
| 48 | /// <summary>The proposed action for a text payload (never performed automatically), or null.</summary> |
| 49 | public CKeyPayloadAction? Action |
| 50 | { |
| 51 | get |
| 52 | { |
| 53 | if (!_classified) |
| 54 | { |
| 55 | _action = CKeyPayload.Classify(Text); |
| 56 | _classified = true; |
| 57 | } |
| 58 | return _action; |
| 59 | } |
| 60 | } |
| 61 | |
| 62 | /// <summary>The telemetry profile code (p0, p0_1, p1, p2, p3 = 0..4).</summary> |
| 63 | internal uint TelemetryProfileCode => Profile switch |
| 64 | { |
| 65 | CKeyProfile.Scalable => Layout == Layout.Print ? 1u : 0u, |
| 66 | CKeyProfile.Locator => 2u, |
| 67 | CKeyProfile.Text => 3u, |
| 68 | _ => 4u, |
| 69 | }; |
| 70 | } |
| 71 | |
| 72 | /// <summary> |
| 73 | /// Headless full-frame scanner. Needs only the scan part of the core: no |
| 74 | /// renderer, no telemetry, no network. The pipeline is the iOS reader's |
| 75 | /// (ios/CKeyReader/CameraScanner.swift): find up to six border candidates, |
| 76 | /// and for each one refine the quadrilateral to Profile 0's field first and |
| 77 | /// decode it, then try the Profile 1, 2 and 3 fields. |
| 78 | /// </summary> |
| 79 | public static unsafe class CKeyDecoder |
| 80 | { |
| 81 | /// <summary>Border candidates examined per frame (as in the iOS reader).</summary> |
| 82 | public const int MaxCandidates = 6; |
| 83 | |
| 84 | // Profile 0 payloads reach 4,619 bytes (v31, level M), beyond the |
| 85 | // 2,048-byte Profile 3 maximum, so Profile 0 gets its own 64 KB buffer. |
| 86 | private const int ScalableBufferBytes = 65535; |
| 87 | |
| 88 | /// <summary> |
| 89 | /// Finds and decodes a CKey in an 8-bit grayscale frame (either contrast |
| 90 | /// polarity). Returns null when no symbol decodes. Thread-safe. |
| 91 | /// </summary> |
| 92 | /// <param name="gray">Pixels, at least stride * (height - 1) + width bytes.</param> |
| 93 | /// <param name="width">Width in pixels.</param> |
| 94 | /// <param name="height">Height in pixels.</param> |
| 95 | /// <param name="stride">Bytes per row (>= width).</param> |
| 96 | public static CKeyResult? Decode(ReadOnlySpan<byte> gray, int width, int height, int stride) |
| 97 | { |
| 98 | Validate(gray, width, height, stride); |
| 99 | Native.EnsureAbi(); |
| 100 | double* borders = stackalloc double[MaxCandidates * 8]; |
| 101 | fixed (byte* g = gray) |
| 102 | { |
| 103 | int n = (int)NativeMethods.ckey_find_borders_gray8(g, (nuint)width, (nuint)height, (nuint)stride, borders, MaxCandidates); |
| 104 | n = Math.Min(n, MaxCandidates); |
| 105 | for (int i = 0; i < n; i++) |
| 106 | { |
| 107 | CKeyResult? result = DecodeCandidate(g, width, height, stride, borders + 8 * i); |
| 108 | if (result != null) return result; |
| 109 | } |
| 110 | } |
| 111 | return null; |
| 112 | } |
| 113 | |
| 114 | /// <summary> |
| 115 | /// Detection only: candidate border quadrilaterals (8 doubles each, the |
| 116 | /// frame stroke's outer edge), most border-like first. Accepts no payload. |
| 117 | /// </summary> |
| 118 | public static double[][] FindBorders(ReadOnlySpan<byte> gray, int width, int height, int stride, int maxQuads = MaxCandidates) |
| 119 | { |
| 120 | Validate(gray, width, height, stride); |
| 121 | if (maxQuads < 1 || maxQuads > 64) throw new ArgumentOutOfRangeException(nameof(maxQuads), "maxQuads must be 1..64."); |
| 122 | Native.EnsureAbi(); |
| 123 | var buffer = new double[maxQuads * 8]; |
| 124 | int n; |
| 125 | fixed (byte* g = gray) |
| 126 | fixed (double* b = buffer) |
| 127 | { |
| 128 | n = (int)NativeMethods.ckey_find_borders_gray8(g, (nuint)width, (nuint)height, (nuint)stride, b, (nuint)maxQuads); |
| 129 | } |
| 130 | n = Math.Min(n, maxQuads); |
| 131 | var quads = new double[n][]; |
| 132 | for (int i = 0; i < n; i++) |
| 133 | { |
| 134 | quads[i] = new double[8]; |
| 135 | Array.Copy(buffer, i * 8, quads[i], 0, 8); |
| 136 | } |
| 137 | return quads; |
| 138 | } |
| 139 | |
| 140 | /// <summary> |
| 141 | /// Decodes one border candidate (from <see cref="FindBorders"/> or the |
| 142 | /// host's own detector): refines it per profile and decodes, Profile 0 first. |
| 143 | /// </summary> |
| 144 | public static CKeyResult? DecodeCandidate(ReadOnlySpan<byte> gray, int width, int height, int stride, ReadOnlySpan<double> borderQuad) |
| 145 | { |
| 146 | Validate(gray, width, height, stride); |
| 147 | if (borderQuad.Length != 8) throw new ArgumentException("A quadrilateral is 8 doubles (x0,y0,…,x3,y3).", nameof(borderQuad)); |
| 148 | Native.EnsureAbi(); |
| 149 | fixed (byte* g = gray) |
| 150 | fixed (double* b = borderQuad) |
| 151 | { |
| 152 | return DecodeCandidate(g, width, height, stride, b); |
| 153 | } |
| 154 | } |
| 155 | |
| 156 | private static CKeyResult? DecodeCandidate(byte* g, int width, int height, int stride, double* border) |
| 157 | { |
| 158 | nuint w = (nuint)width, h = (nuint)height, s = (nuint)stride; |
| 159 | double* quad = stackalloc double[8]; |
| 160 | NativeDiagnostics diagnostics; |
| 161 | |
| 162 | // Profile 0 first: the core aligns the frame edges, identifies the |
| 163 | // version and layout from the Grid/Ring/Ray Keys, then decodes. |
| 164 | if (NativeMethods.ckey_refine_quad_gray8(g, w, h, s, border, NativeConstants.FieldScalable, quad) >= 0) |
| 165 | { |
| 166 | byte[] output = ArrayPool<byte>.Shared.Rent(ScalableBufferBytes); |
| 167 | try |
| 168 | { |
| 169 | nuint length = 0; |
| 170 | uint version = 0, layout = 0; |
| 171 | diagnostics = default; |
| 172 | int status; |
| 173 | fixed (byte* o = output) |
| 174 | { |
| 175 | status = NativeMethods.ckey_decode_scalable_gray8(g, w, h, s, quad, o, (nuint)ScalableBufferBytes, |
| 176 | &length, &version, &layout, &diagnostics); |
| 177 | } |
| 178 | if (status == 0 && length <= (nuint)ScalableBufferBytes) |
| 179 | { |
| 180 | return new CKeyResult(Copy(output, (int)length), CKeyProfile.Scalable, (int)version, |
| 181 | layout == 1 ? Layout.Print : Layout.Standard, ToArray(quad)); |
| 182 | } |
| 183 | } |
| 184 | finally |
| 185 | { |
| 186 | ArrayPool<byte>.Shared.Return(output); |
| 187 | } |
| 188 | } |
| 189 | |
| 190 | // Compact and dense profiles have deliberately different calibration |
| 191 | // fields: each decoder gets the quad refined to its own field. The |
| 192 | // compact profile goes first so ordinary locator reads stay fast. |
| 193 | if (NativeMethods.ckey_refine_quad_gray8(g, w, h, s, border, NativeConstants.FieldLocator, quad) >= 0) |
| 194 | { |
| 195 | byte* locator = stackalloc byte[NativeConstants.LocatorPayloadBytes]; |
| 196 | diagnostics = default; |
| 197 | if (NativeMethods.ckey_decode_gray8(g, w, h, s, quad, locator, (nuint)NativeConstants.LocatorPayloadBytes, &diagnostics) == 0) |
| 198 | { |
| 199 | var payload = new byte[NativeConstants.LocatorPayloadBytes]; |
| 200 | for (int i = 0; i < payload.Length; i++) payload[i] = locator[i]; |
| 201 | return new CKeyResult(payload, CKeyProfile.Locator, 0, Layout.Standard, ToArray(quad)); |
| 202 | } |
| 203 | } |
| 204 | |
| 205 | byte[] dense = ArrayPool<byte>.Shared.Rent(NativeConstants.ExtendedPayloadMaxBytes); |
| 206 | try |
| 207 | { |
| 208 | fixed (byte* o = dense) |
| 209 | { |
| 210 | nuint length = 0; |
| 211 | if (NativeMethods.ckey_refine_quad_gray8(g, w, h, s, border, NativeConstants.FieldText, quad) >= 0) |
| 212 | { |
| 213 | diagnostics = default; |
| 214 | if (NativeMethods.ckey_decode_text_gray8(g, w, h, s, quad, o, (nuint)NativeConstants.ExtendedPayloadMaxBytes, |
| 215 | &length, &diagnostics) == 0 && length <= (nuint)NativeConstants.ExtendedPayloadMaxBytes) |
| 216 | { |
| 217 | return new CKeyResult(Copy(dense, (int)length), CKeyProfile.Text, 0, Layout.Standard, ToArray(quad)); |
| 218 | } |
| 219 | } |
| 220 | length = 0; |
| 221 | if (NativeMethods.ckey_refine_quad_gray8(g, w, h, s, border, NativeConstants.FieldExtended, quad) >= 0) |
| 222 | { |
| 223 | diagnostics = default; |
| 224 | if (NativeMethods.ckey_decode_extended_gray8(g, w, h, s, quad, o, (nuint)NativeConstants.ExtendedPayloadMaxBytes, |
| 225 | &length, &diagnostics) == 0 && length <= (nuint)NativeConstants.ExtendedPayloadMaxBytes) |
| 226 | { |
| 227 | return new CKeyResult(Copy(dense, (int)length), CKeyProfile.Extended, 0, Layout.Standard, ToArray(quad)); |
| 228 | } |
| 229 | } |
| 230 | } |
| 231 | } |
| 232 | finally |
| 233 | { |
| 234 | ArrayPool<byte>.Shared.Return(dense); |
| 235 | } |
| 236 | return null; |
| 237 | } |
| 238 | |
| 239 | private static void Validate(ReadOnlySpan<byte> gray, int width, int height, int stride) |
| 240 | { |
| 241 | if (width <= 0) throw new ArgumentOutOfRangeException(nameof(width)); |
| 242 | if (height <= 0) throw new ArgumentOutOfRangeException(nameof(height)); |
| 243 | if (stride < width) throw new ArgumentOutOfRangeException(nameof(stride), "stride must be at least width."); |
| 244 | if (gray.Length < (long)stride * (height - 1) + width) |
| 245 | throw new ArgumentException("gray is smaller than stride * (height - 1) + width bytes.", nameof(gray)); |
| 246 | } |
| 247 | |
| 248 | private static byte[] Copy(byte[] source, int length) |
| 249 | { |
| 250 | var bytes = new byte[length]; |
| 251 | Array.Copy(source, bytes, length); |
| 252 | return bytes; |
| 253 | } |
| 254 | |
| 255 | private static double[] ToArray(double* quad) |
| 256 | { |
| 257 | var q = new double[8]; |
| 258 | for (int i = 0; i < 8; i++) q[i] = quad[i]; |
| 259 | return q; |
| 260 | } |
| 261 | } |
| 262 | } |