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

CKeySymbol.swift

v0.1.0-build.85 Latest Commit ede38f5

Size
18 KB
Lines
358
Language
swift
SHA-256
ebb72beb8039…
View on GitHub

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

1import CKeyCodec
2import CKeyScanKit
3import CoreGraphics
4import Foundation
5
6/// Reed-Solomon correction level for Profile 0.
7public enum CKeyErrorCorrection: Int, Sendable, CaseIterable {
8 /// About 7% of each block correctable.
9 case l = 0
10 /// About 14% (default).
11 case m = 1
12 /// About 22%.
13 case q = 2
14 /// About 30%.
15 case h = 3
16
17 var c: ckey_ecc_level { ckey_ecc_level(rawValue: UInt32(rawValue)) }
18}
19
20/// How the direct rasterizer turns geometry into pixels (specification §2.2).
21/// None is universally best; benchmark for your target size.
22public enum CKeyRasterPolicy: Int, Sendable, CaseIterable {
23 /// Antialiased area coverage (default).
24 case coverage = 0
25 /// Each pixel is ink when at least half its samples are covered.
26 case binary = 1
27 /// Coverage, with marks thinner than one pixel widened to keep thin rays
28 /// and dots continuous. Changes drawn widths, never the encoded topology.
29 case hybrid = 2
30}
31
32/// A straight-alpha (non-premultiplied) sRGB RGBA8 pixel buffer, rows top to
33/// bottom with no padding: what the core's raster backend produces.
34public struct CKeyPixels: Sendable {
35 public let width: Int
36 public let height: Int
37 /// `width * height * 4` bytes.
38 public let rgba: Data
39 /// `false` when the background was painted (every alpha is 255).
40 public let hasAlpha: Bool
41}
42
43/// An encoded Profile 0 (scalable family) symbol. Immutable; renders to
44/// native pixels (`cgImage`, `pixels`), PNG, SVG and PDF. Thread-safe.
45public final class CKeySymbol: @unchecked Sendable {
46 // ckey_scalable_symbol is 8 KB; keep one heap copy, never copied by value.
47 private let storage: UnsafeMutablePointer<ckey_scalable_symbol>
48
49 /// The version actually used (1...31); `version: 0` picks the smallest that fits.
50 public let version: Int
51 public let level: CKeyErrorCorrection
52 public let layout: CKeyLayout
53 /// The payload size in bytes.
54 public let payloadLength: Int
55 /// Modules across the field for this version (29...232).
56 public var modulesAcross: Int { Int(ckey_scalable_modules_across(UInt32(version))) }
57
58 private init(storage: UnsafeMutablePointer<ckey_scalable_symbol>) {
59 self.storage = storage
60 version = Int(storage.pointee.version)
61 level = CKeyErrorCorrection(rawValue: Int(storage.pointee.ecc_level)) ?? .m
62 layout = CKeyLayout(rawValue: Int(storage.pointee.layout)) ?? .standard
63 payloadLength = Int(storage.pointee.payload_length)
64 }
65
66 deinit {
67 storage.deinitialize(count: 1)
68 storage.deallocate()
69 }
70
71 /// Encodes UTF-8 text.
72 /// - Parameters:
73 /// - version: 1...31, or 0 (default) for the smallest version that fits.
74 /// - level: correction level (default `.m`).
75 /// - layout: `.standard` (default) or `.print` (P0.1, for small prints).
76 /// - Throws: `CKeyError` when the payload does not fit or an argument is invalid.
77 public static func encode(_ text: String, version: Int = 0, level: CKeyErrorCorrection = .m,
78 layout: CKeyLayout = .standard) throws -> CKeySymbol {
79 try encode(Data(text.utf8), version: version, level: level, layout: layout)
80 }
81
82 /// Encodes bytes. See `encode(_:version:level:layout:)` for the parameters.
83 public static func encode(_ data: Data, version: Int = 0, level: CKeyErrorCorrection = .m,
84 layout: CKeyLayout = .standard) throws -> CKeySymbol {
85 guard (0...Int(CKEY_SCALABLE_VERSION_COUNT)).contains(version) else {
86 throw CKeyError(.invalidArgument, "version must be 0 (auto) or 1...\(CKEY_SCALABLE_VERSION_COUNT)")
87 }
88 let storage = UnsafeMutablePointer<ckey_scalable_symbol>.allocate(capacity: 1)
89 storage.initialize(to: ckey_scalable_symbol())
90 var diagnostics = ckey_diagnostics()
91 let bytes = [UInt8](data)
92 let status = bytes.withUnsafeBufferPointer { payload in
93 ckey_encode_scalable(payload.baseAddress, payload.count, UInt32(version), level.c,
94 ckey_scalable_layout(rawValue: UInt32(layout.rawValue)), storage, &diagnostics)
95 }
96 guard status == CKEY_OK else {
97 storage.deinitialize(count: 1)
98 storage.deallocate()
99 throw CKeyError.core(status, diagnostics, "encoding failed")
100 }
101 return CKeySymbol(storage: storage)
102 }
103
104 /// Maximum payload bytes for a version (1...31), level and layout; 0 if invalid.
105 public static func capacity(version: Int, level: CKeyErrorCorrection = .m, layout: CKeyLayout = .standard) -> Int {
106 guard (1...Int(CKEY_SCALABLE_VERSION_COUNT)).contains(version) else { return 0 }
107 return ckey_scalable_capacity(UInt32(version), level.c, ckey_scalable_layout(rawValue: UInt32(layout.rawValue)))
108 }
109
110 // MARK: - Native pixels
111
112 /// Renders straight-alpha RGBA8 pixels covering the full 31-unit canvas
113 /// (symbol plus quiet margin).
114 /// - Parameters:
115 /// - size: pixels per side, 16...16384.
116 /// - policy: raster policy (default `.coverage`).
117 /// - transparent: paint no background, vignette or texture (unpainted
118 /// pixels have alpha 0). The host is then responsible for contrast.
119 /// - style: the design (default `.plain`).
120 /// - materials: draw sheen, depth and grain (default `true`); `false` draws flat ink.
121 public func pixels(size: Int = 512, policy: CKeyRasterPolicy = .coverage, transparent: Bool = false,
122 style: CKeyStyle = .plain, materials: Bool = true) throws -> CKeyPixels {
123 guard (16...16384).contains(size) else { throw CKeyError(.invalidArgument, "size must be 16...16384 px") }
124 var options = ckey_raster_options()
125 ckey_raster_options_default(&options)
126 options.size_px = UInt32(size)
127 options.policy = UInt32(policy.rawValue)
128 options.format = CKEY_PIXELS_RGBA8.rawValue
129 options.transparent = transparent ? 1 : 0
130 options.materials = materials ? 1 : 0
131 let symbol = UnsafePointer(storage)
132 let data: Data = try style.withCStyle { cStyle -> Data in
133 var required = 0
134 let probe = ckey_render_scalable_raster(symbol, cStyle, &options, nil, 0, &required)
135 guard probe == CKEY_OK || probe == CKEY_INSUFFICIENT_BUFFER, required > 0 else {
136 throw refusal(probe, symbol, cStyle, "the style was refused or the raster options are invalid")
137 }
138 var buffer = Data(count: required)
139 let status = buffer.withUnsafeMutableBytes { raw in
140 ckey_render_scalable_raster(symbol, cStyle, &options,
141 raw.bindMemory(to: UInt8.self).baseAddress, raw.count, &required)
142 }
143 guard status == CKEY_OK else { throw CKeyError.core(status, nil, "rendering failed") }
144 return buffer
145 }
146 return CKeyPixels(width: size, height: size, rgba: data, hasAlpha: transparent)
147 }
148
149 /// Renders a `CGImage` (sRGB, 8 bits per channel). For on-screen use,
150 /// pass the display's pixel size, not points (`CKeyView` does this).
151 /// See `pixels(size:policy:transparent:style:materials:)` for the parameters.
152 public func cgImage(size: Int = 512, policy: CKeyRasterPolicy = .coverage, transparent: Bool = false,
153 style: CKeyStyle = .plain, materials: Bool = true) throws -> CGImage {
154 let rendered = try pixels(size: size, policy: policy, transparent: transparent, style: style, materials: materials)
155 let alpha: CGImageAlphaInfo = rendered.hasAlpha ? .last : .noneSkipLast
156 guard let provider = CGDataProvider(data: rendered.rgba as CFData),
157 let space = CGColorSpace(name: CGColorSpace.sRGB),
158 let image = CGImage(width: rendered.width, height: rendered.height,
159 bitsPerComponent: 8, bitsPerPixel: 32, bytesPerRow: rendered.width * 4,
160 space: space, bitmapInfo: CGBitmapInfo(rawValue: alpha.rawValue),
161 provider: provider, decode: nil, shouldInterpolate: false,
162 intent: .defaultIntent) else {
163 throw CKeyError(.imageConversion, "could not create a CGImage")
164 }
165 return image
166 }
167
168 // MARK: - Exports
169
170 /// PNG bytes of the raster (lossless; written by the core, no ImageIO).
171 /// - Parameter dpi: when > 0, records the physical resolution (pHYs).
172 public func pngData(size: Int = 512, policy: CKeyRasterPolicy = .coverage, transparent: Bool = false,
173 style: CKeyStyle = .plain, materials: Bool = true, dpi: Double = 0) throws -> Data {
174 let rendered = try pixels(size: size, policy: policy, transparent: transparent, style: style, materials: materials)
175 return try rendered.rgba.withUnsafeBytes { raw -> Data in
176 let pixels = raw.bindMemory(to: UInt8.self).baseAddress
177 let width = UInt32(rendered.width), height = UInt32(rendered.height), stride = rendered.width * 4
178 var required = 0
179 let probe = ckey_encode_png(pixels, width, height, stride, CKEY_PNG_RGBA8, dpi, nil, 0, &required)
180 guard probe == CKEY_OK || probe == CKEY_INSUFFICIENT_BUFFER, required > 0 else {
181 throw CKeyError.core(probe, nil, "PNG encoding failed")
182 }
183 var png = Data(count: required)
184 let status = png.withUnsafeMutableBytes { out in
185 ckey_encode_png(pixels, width, height, stride, CKEY_PNG_RGBA8, dpi,
186 out.bindMemory(to: UInt8.self).baseAddress, out.count, &required)
187 }
188 guard status == CKEY_OK else { throw CKeyError.core(status, nil, "PNG encoding failed") }
189 return png.prefix(required)
190 }
191 }
192
193 /// Styled SVG export (vector; for interchange and print workflows, not
194 /// for on-screen display, which uses native pixels).
195 /// - Parameter scale: pixels per canonical unit in the SVG's size attributes (1...256).
196 public func svg(scale: Int = 8, style: CKeyStyle = .plain) throws -> String {
197 guard (1...Int(CKEY_MAX_RENDER_SCALE)).contains(scale) else {
198 throw CKeyError(.invalidArgument, "scale must be 1...\(CKEY_MAX_RENDER_SCALE)")
199 }
200 let symbol = UnsafePointer(storage)
201 return try style.withCStyle { cStyle -> String in
202 var required = 0
203 let probe = ckey_render_scalable_styled_svg(symbol, cStyle, UInt32(scale), nil, 0, &required)
204 guard probe == CKEY_OK || probe == CKEY_INSUFFICIENT_BUFFER, required > 0 else {
205 throw refusal(probe, symbol, cStyle, "the style was refused")
206 }
207 var chars = [CChar](repeating: 0, count: required + 1)
208 let status = chars.withUnsafeMutableBufferPointer { buffer in
209 ckey_render_scalable_styled_svg(symbol, cStyle, UInt32(scale), buffer.baseAddress, buffer.count, &required)
210 }
211 guard status == CKEY_OK else { throw CKeyError.core(status, nil, "SVG export failed") }
212 chars[chars.count - 1] = 0
213 return String(cString: chars)
214 }
215 }
216
217 /// One-page PDF 1.4 with the exact geometry as vector paths (flat
218 /// colours; materials and logos are not exported).
219 /// - Parameter widthPoints: page width in points (72 per inch); default 2 inches.
220 public func pdfData(widthPoints: Double = 144, transparent: Bool = false, style: CKeyStyle = .plain) throws -> Data {
221 guard widthPoints.isFinite, widthPoints > 0 else { throw CKeyError(.invalidArgument, "widthPoints must be positive") }
222 let symbol = UnsafePointer(storage)
223 return try style.withCStyle { cStyle -> Data in
224 var required = 0
225 let flag: UInt32 = transparent ? 1 : 0
226 let probe = ckey_render_scalable_pdf(symbol, cStyle, widthPoints, flag, nil, 0, &required)
227 guard probe == CKEY_OK || probe == CKEY_INSUFFICIENT_BUFFER, required > 0 else {
228 throw refusal(probe, symbol, cStyle, "the style was refused")
229 }
230 var pdf = Data(count: required)
231 let status = pdf.withUnsafeMutableBytes { out in
232 ckey_render_scalable_pdf(symbol, cStyle, widthPoints, flag,
233 out.bindMemory(to: UInt8.self).baseAddress, out.count, &required)
234 }
235 guard status == CKEY_OK else { throw CKeyError.core(status, nil, "PDF export failed") }
236 return pdf.prefix(required)
237 }
238 }
239
240 // MARK: - Style check and certification
241
242 /// Checks a style against this symbol (contrast, effect limits, logo
243 /// error budget). Throws a `CKeyError` explaining any refusal.
244 public func check(style: CKeyStyle) throws {
245 let symbol = UnsafePointer(storage)
246 try style.withCStyle { cStyle -> Void in
247 var diagnostics = ckey_diagnostics()
248 let status = ckey_check_scalable_style(symbol, cStyle, &diagnostics)
249 guard status == CKEY_OK else { throw CKeyError.core(status, diagnostics, "style refused") }
250 }
251 }
252
253 /// Certifies a style by rendering it, compositing it over each backdrop,
254 /// degrading it with the default phone-camera model and decoding it with
255 /// the real decoder (`ckey_certify_scalable_style`), under `style.policy`.
256 /// A model is not a measurement: real-camera tests remain the evidence.
257 ///
258 /// - Parameters:
259 /// - transparent: certify transparent output. It needs `backdrops`
260 /// (the colours the symbol may sit on); without them the result is
261 /// never certified ("background unknown").
262 /// - backdrops: up to 16 solid backgrounds.
263 public func certify(style: CKeyStyle = .plain, transparent: Bool = false,
264 backdrops: [CKeyColor] = []) throws -> CKeyCertification {
265 guard backdrops.count <= 16 else { throw CKeyError(.invalidArgument, "at most 16 backdrop colours") }
266 let symbol = UnsafePointer(storage)
267 let colours = backdrops.map(\.c)
268 return style.withCStyle { cStyle -> CKeyCertification in
269 var adapted = ckey_style()
270 var report = ckey_certification()
271 let status: ckey_status = colours.withUnsafeBufferPointer { list in
272 if colours.isEmpty {
273 return ckey_certify_scalable_style(symbol, cStyle, transparent ? 1 : 0, nil, nil, &adapted, &report)
274 }
275 var backdrop = ckey_backdrop()
276 backdrop.colours = list.baseAddress
277 backdrop.colour_count = UInt32(list.count)
278 return ckey_certify_scalable_style(symbol, cStyle, transparent ? 1 : 0, &backdrop, nil, &adapted, &report)
279 }
280 return CKeyCertification(status: status, report: report,
281 adapted: status == CKEY_OK && style.policy == .adaptive ? adapted : nil,
282 logoHref: style.logoHref)
283 }
284 }
285}
286
287/// A renderer's refusal, explained by the style check when the style is the cause.
288private func refusal(_ status: ckey_status, _ symbol: UnsafePointer<ckey_scalable_symbol>,
289 _ style: UnsafePointer<ckey_style>, _ fallback: String) -> CKeyError {
290 var diagnostics = ckey_diagnostics()
291 if status == CKEY_INVALID_ARGUMENT, ckey_check_scalable_style(symbol, style, &diagnostics) != CKEY_OK {
292 return CKeyError.core(status, diagnostics, fallback)
293 }
294 return CKeyError.core(status, nil, fallback)
295}
296
297/// The outcome of `CKeySymbol.certify`.
298public struct CKeyCertification: Sendable {
299 /// Changes made under the `.adaptive` policy (`CKEY_ADJUST_*`).
300 public struct Adjustments: OptionSet, Sendable {
301 public let rawValue: UInt32
302 public init(rawValue: UInt32) { self.rawValue = rawValue }
303 public static let ink = Adjustments(rawValue: 1)
304 public static let accents = Adjustments(rawValue: 2)
305 public static let effects = Adjustments(rawValue: 4)
306 public static let logo = Adjustments(rawValue: 8)
307 public static let background = Adjustments(rawValue: 16)
308 public static let materials = Adjustments(rawValue: 32)
309 }
310
311 /// The core accepted the style under its policy (`.strict` requires
312 /// certification; `.adaptive` succeeds once repaired; `.override` always).
313 public let accepted: Bool
314 /// The symbol decoded in every modelled trial on every backdrop.
315 public let certified: Bool
316 public let policy: CKeyStylePolicy
317 public let adjustments: Adjustments
318 public let trials: Int
319 public let trialsPassed: Int
320 /// Informative WCAG-style contrast ratio over all ink/background pairs.
321 public let minContrast: Double
322 /// Luma separation of marks versus gaps after compositing (<= 0: they overlap).
323 public let featureSeparation: Double
324 /// Share of the worst block's correction hidden by the logo.
325 public let logoLoad: Double
326 /// The reason for a refusal, or the list of changes and warnings.
327 public let message: String
328 /// Under `.adaptive`, the repaired style to render.
329 public let adaptedStyle: CKeyStyle?
330
331 init(status: ckey_status, report: ckey_certification, adapted: ckey_style?, logoHref: String?) {
332 accepted = status == CKEY_OK
333 certified = report.certified != 0
334 policy = CKeyStylePolicy(rawValue: Int(report.policy)) ?? .strict
335 adjustments = Adjustments(rawValue: report.adjustments)
336 trials = Int(report.trials)
337 trialsPassed = Int(report.trials_passed)
338 minContrast = report.min_contrast
339 featureSeparation = report.feature_separation
340 logoLoad = report.logo_load
341 message = withUnsafeBytes(of: report.message) { raw in
342 let bytes = raw.bindMemory(to: UInt8.self)
343 let end = bytes.firstIndex(of: 0) ?? bytes.count
344 return String(decoding: bytes[0..<end], as: UTF8.self)
345 }
346 // The adapted style's logo pointer refers to the caller's C string,
347 // which is only valid inside the call: never read it, restore the
348 // original logo instead.
349 if var style = adapted {
350 style.logo_href = nil
351 var repaired = CKeyStyle(style)
352 repaired.logoHref = logoHref
353 adaptedStyle = repaired
354 } else {
355 adaptedStyle = nil
356 }
357 }
358}