Viewing is open to everyone. Downloading and starring need a GitHub sign-in. Sign in with GitHub

CKeyDecoder.swift

v0.1.0-build.85 Latest Commit ede38f5

Size
11 KB
Lines
213
Language
swift
SHA-256
bd81dcc17a11…
View on GitHub

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

1import CKeyCodec
2import CoreGraphics
3import CoreVideo
4import Foundation
5
6/// Headless decoding of still images and camera frames.
7///
8/// The pipeline is the iOS reader's (ios/CKeyReader/CameraScanner.swift),
9/// entirely in the C++ core: find up to six border candidates in the whole
10/// frame (`ckey_find_borders_gray8`, both polarities), fit each one to
11/// Profile 0's field first (`ckey_refine_quad_gray8` with
12/// `CKEY_FIELD_SCALABLE`) and decode it, then try Profiles 1, 2 and 3, each
13/// refined against its own field. A payload is returned only after the
14/// core's Reed-Solomon correction and CRC-32C checks accept it.
15///
16/// All functions are synchronous, reentrant and safe to call from any
17/// thread; call them off the main thread for camera-sized frames.
18public enum CKeyDecoder {
19 /// Maximum border candidates tried per frame (as in the iOS reader).
20 public static let maxCandidates = 6
21
22 /// Decodes an 8-bit grayscale frame (`stride` bytes per row). Returns
23 /// `nil` when no symbol decodes.
24 public static func decode(gray: UnsafePointer<UInt8>, width: Int, height: Int, stride: Int) -> CKeyResult? {
25 guard width > 0, height > 0, stride >= width else { return nil }
26 var borders = [Double](repeating: 0, count: maxCandidates * 8)
27 let count = borders.withUnsafeMutableBufferPointer { output in
28 ckey_find_borders_gray8(gray, width, height, stride, output.baseAddress, maxCandidates)
29 }
30 guard count > 0 else { return nil }
31 // Profile 0 payloads reach 4,619 bytes (v31, level M): 64 KB output.
32 var output = [UInt8](repeating: 0, count: 65535)
33 for index in 0..<min(count, maxCandidates) {
34 let border = Array(borders[(index * 8)..<(index * 8 + 8)])
35 if let quad = refine(border, gray, width, height, stride, CKEY_FIELD_SCALABLE),
36 let result = decodeScalable(gray, width, height, stride, quad, &output) {
37 return result
38 }
39 if let result = decodeLegacy(gray, width, height, stride, border, &output) {
40 return result
41 }
42 }
43 return nil
44 }
45
46 /// Decodes an image. It is drawn over white into an 8-bit gray buffer,
47 /// so a transparent rendering (dark ink, alpha 0 elsewhere) reads as
48 /// dark-on-white. Returns `nil` when no symbol decodes.
49 public static func decode(cgImage image: CGImage) -> CKeyResult? {
50 guard let frame = GrayFrame(image) else { return nil }
51 return frame.bytes.withUnsafeBufferPointer { buffer in
52 guard let base = buffer.baseAddress else { return nil }
53 return decode(gray: base, width: frame.width, height: frame.height, stride: frame.width)
54 }
55 }
56
57 /// Decodes a camera or video frame. Supported formats: the bi-planar
58 /// 4:2:0 YpCbCr formats (luma plane, as AVFoundation delivers by
59 /// default), 8-bit one-component gray, and 32-bit BGRA/RGBA/ARGB (converted
60 /// to luma). Returns `nil` when no symbol decodes or the format is unsupported.
61 public static func decode(pixelBuffer: CVPixelBuffer) -> CKeyResult? {
62 CVPixelBufferLockBaseAddress(pixelBuffer, .readOnly)
63 defer { CVPixelBufferUnlockBaseAddress(pixelBuffer, .readOnly) }
64 let format = CVPixelBufferGetPixelFormatType(pixelBuffer)
65 switch format {
66 case kCVPixelFormatType_420YpCbCr8BiPlanarFullRange,
67 kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange,
68 kCVPixelFormatType_420YpCbCr8Planar,
69 kCVPixelFormatType_420YpCbCr8PlanarFullRange:
70 guard CVPixelBufferGetPlaneCount(pixelBuffer) >= 1,
71 let base = CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 0) else { return nil }
72 return decode(gray: base.assumingMemoryBound(to: UInt8.self),
73 width: CVPixelBufferGetWidthOfPlane(pixelBuffer, 0),
74 height: CVPixelBufferGetHeightOfPlane(pixelBuffer, 0),
75 stride: CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, 0))
76 case kCVPixelFormatType_OneComponent8:
77 guard let base = CVPixelBufferGetBaseAddress(pixelBuffer) else { return nil }
78 return decode(gray: base.assumingMemoryBound(to: UInt8.self),
79 width: CVPixelBufferGetWidth(pixelBuffer),
80 height: CVPixelBufferGetHeight(pixelBuffer),
81 stride: CVPixelBufferGetBytesPerRow(pixelBuffer))
82 case kCVPixelFormatType_32BGRA, kCVPixelFormatType_32RGBA, kCVPixelFormatType_32ARGB:
83 guard let base = CVPixelBufferGetBaseAddress(pixelBuffer) else { return nil }
84 let width = CVPixelBufferGetWidth(pixelBuffer)
85 let height = CVPixelBufferGetHeight(pixelBuffer)
86 let rowBytes = CVPixelBufferGetBytesPerRow(pixelBuffer)
87 // Channel offsets of R, G, B within each 4-byte pixel.
88 let offsets: (r: Int, g: Int, b: Int)
89 if format == kCVPixelFormatType_32BGRA {
90 offsets = (r: 2, g: 1, b: 0)
91 } else if format == kCVPixelFormatType_32RGBA {
92 offsets = (r: 0, g: 1, b: 2)
93 } else {
94 offsets = (r: 1, g: 2, b: 3)
95 }
96 let source = base.assumingMemoryBound(to: UInt8.self)
97 var gray = [UInt8](repeating: 0, count: width * height)
98 for y in 0..<height {
99 let row = source + y * rowBytes
100 for x in 0..<width {
101 let p = row + x * 4
102 // Rec. 709 luma, integer weights summing to 256.
103 let luma = (54 * Int(p[offsets.r]) + 183 * Int(p[offsets.g]) + 19 * Int(p[offsets.b])) >> 8
104 gray[y * width + x] = UInt8(min(255, luma))
105 }
106 }
107 return gray.withUnsafeBufferPointer { buffer in
108 buffer.baseAddress.flatMap { decode(gray: $0, width: width, height: height, stride: width) }
109 }
110 default:
111 return nil
112 }
113 }
114
115 // MARK: - Pipeline steps
116
117 private static func refine(_ border: [Double], _ gray: UnsafePointer<UInt8>, _ width: Int, _ height: Int,
118 _ stride: Int, _ field: ckey_field) -> [Double]? {
119 var refined = [Double](repeating: 0, count: 8)
120 let score = border.withUnsafeBufferPointer { input in
121 refined.withUnsafeMutableBufferPointer { output in
122 ckey_refine_quad_gray8(gray, width, height, stride, input.baseAddress, field, output.baseAddress)
123 }
124 }
125 return score >= 0 ? refined : nil
126 }
127
128 private static func decodeScalable(_ gray: UnsafePointer<UInt8>, _ width: Int, _ height: Int, _ stride: Int,
129 _ quad: [Double], _ output: inout [UInt8]) -> CKeyResult? {
130 var length = 0
131 var version: UInt32 = 0
132 var layout: UInt32 = 0
133 var diagnostics = ckey_diagnostics()
134 let status = output.withUnsafeMutableBufferPointer { decoded in
135 quad.withUnsafeBufferPointer { q in
136 ckey_decode_scalable_gray8(gray, width, height, stride, q.baseAddress,
137 decoded.baseAddress, decoded.count, &length, &version, &layout, &diagnostics)
138 }
139 }
140 guard status == CKEY_OK, length <= output.count else { return nil }
141 return CKeyResult(bytes: Data(output[0..<length]), profile: .scalable, version: Int(version),
142 layout: CKeyLayout(rawValue: Int(layout)) ?? .standard)
143 }
144
145 private static func decodeLegacy(_ gray: UnsafePointer<UInt8>, _ width: Int, _ height: Int, _ stride: Int,
146 _ border: [Double], _ output: inout [UInt8]) -> CKeyResult? {
147 var diagnostics = ckey_diagnostics()
148 // Profile 1: the compact locator (fast path, fixed 16 bytes).
149 if let quad = refine(border, gray, width, height, stride, CKEY_FIELD_LOCATOR) {
150 let capacity = Int(CKEY_LOCATOR_PAYLOAD_BYTES)
151 let status = output.withUnsafeMutableBufferPointer { decoded in
152 quad.withUnsafeBufferPointer { q in
153 ckey_decode_gray8(gray, width, height, stride, q.baseAddress, decoded.baseAddress, capacity, &diagnostics)
154 }
155 }
156 if status == CKEY_OK { return CKeyResult(bytes: Data(output[0..<capacity]), profile: .locator) }
157 }
158 // Profiles 2 and 3: dense fields, each refined against its own keys.
159 var length = 0
160 if let quad = refine(border, gray, width, height, stride, CKEY_FIELD_TEXT) {
161 let status = output.withUnsafeMutableBufferPointer { decoded in
162 quad.withUnsafeBufferPointer { q in
163 ckey_decode_text_gray8(gray, width, height, stride, q.baseAddress,
164 decoded.baseAddress, decoded.count, &length, &diagnostics)
165 }
166 }
167 if status == CKEY_OK, length <= output.count {
168 return CKeyResult(bytes: Data(output[0..<length]), profile: .text)
169 }
170 }
171 length = 0
172 if let quad = refine(border, gray, width, height, stride, CKEY_FIELD_EXTENDED) {
173 let status = output.withUnsafeMutableBufferPointer { decoded in
174 quad.withUnsafeBufferPointer { q in
175 ckey_decode_extended_gray8(gray, width, height, stride, q.baseAddress,
176 decoded.baseAddress, decoded.count, &length, &diagnostics)
177 }
178 }
179 if status == CKEY_OK, length <= output.count {
180 return CKeyResult(bytes: Data(output[0..<length]), profile: .extended)
181 }
182 }
183 return nil
184 }
185}
186
187/// A CGImage drawn over white into tightly packed 8-bit gray.
188struct GrayFrame {
189 let width: Int
190 let height: Int
191 let bytes: [UInt8]
192
193 init?(_ image: CGImage) {
194 let width = image.width, height = image.height
195 guard width > 0, height > 0 else { return nil }
196 var bytes = [UInt8](repeating: 255, count: width * height)
197 let drawn = bytes.withUnsafeMutableBytes { raw -> Bool in
198 guard let context = CGContext(data: raw.baseAddress, width: width, height: height,
199 bitsPerComponent: 8, bytesPerRow: width,
200 space: CGColorSpaceCreateDeviceGray(),
201 bitmapInfo: CGImageAlphaInfo.none.rawValue) else { return false }
202 context.setFillColor(gray: 1, alpha: 1)
203 context.fill(CGRect(x: 0, y: 0, width: width, height: height))
204 context.interpolationQuality = .none
205 context.draw(image, in: CGRect(x: 0, y: 0, width: width, height: height))
206 return true
207 }
208 guard drawn else { return nil }
209 self.width = width
210 self.height = height
211 self.bytes = bytes
212 }
213}