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

Size
12 KB
Lines
262
Language
csharp
SHA-256
9ffc2a4bb639…
View on GitHub

Sign in with GitHub to download. Each download is recorded with your GitHub username.

1using System;
2using System.Buffers;
3using OpenCKey.Interop;
4
5namespace 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 (&gt;= 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}