227 lines
8.7 KiB
Swift
227 lines
8.7 KiB
Swift
import AVFoundation
|
|
import CoreImage
|
|
import CoreImage.CIFilterBuiltins
|
|
import SwiftUI
|
|
|
|
/// Kasztelan card numbers are arbitrary 13-digit account identifiers, so they
|
|
/// remain Code 128. EAN-13 would silently replace the last digit with a checksum.
|
|
struct CardBarcodeView: View {
|
|
let digits: String
|
|
@Environment(\.displayScale) private var displayScale
|
|
|
|
var body: some View {
|
|
GeometryReader { geometry in
|
|
if let image = barcodeImage(
|
|
targetWidth: geometry.size.width,
|
|
targetHeight: geometry.size.height,
|
|
displayScale: displayScale
|
|
) {
|
|
Image(decorative: image, scale: displayScale)
|
|
.interpolation(.none)
|
|
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
|
|
}
|
|
}
|
|
.background(.white)
|
|
.accessibilityElement()
|
|
.accessibilityLabel("Barcode \(digits)")
|
|
}
|
|
|
|
private func barcodeImage(targetWidth: CGFloat, targetHeight: CGFloat, displayScale: CGFloat) -> CGImage? {
|
|
let clean = String(digits.filter(\.isNumber))
|
|
guard clean.count == 13, let data = clean.data(using: .ascii) else { return nil }
|
|
|
|
let generator = CIFilter.code128BarcodeGenerator()
|
|
generator.message = data
|
|
generator.quietSpace = 12
|
|
guard let output = generator.outputImage else { return nil }
|
|
|
|
let pixelWidth = floor(targetWidth * displayScale)
|
|
let pixelHeight = floor(targetHeight * displayScale)
|
|
let horizontalScale = max(1, floor(pixelWidth / output.extent.width))
|
|
let verticalScale = max(1, floor(pixelHeight / output.extent.height))
|
|
let scaled = output.transformed(
|
|
by: CGAffineTransform(scaleX: horizontalScale, y: verticalScale)
|
|
)
|
|
return CIContext(options: [.useSoftwareRenderer: false]).createCGImage(scaled, from: scaled.extent)
|
|
}
|
|
}
|
|
|
|
/// Recreates an imported loyalty-card code using the symbology reported by the
|
|
/// camera. The payload itself is never normalised or reformatted.
|
|
struct StoredBarcodeView: View {
|
|
let value: String
|
|
let symbology: String
|
|
|
|
var body: some View {
|
|
let type = AVMetadataObject.ObjectType(rawValue: symbology)
|
|
Group {
|
|
switch type {
|
|
case .ean13:
|
|
EANBarcodeView(value: value, kind: .ean13)
|
|
case .ean8:
|
|
EANBarcodeView(value: value, kind: .ean8)
|
|
case .qr:
|
|
MatrixBarcodeView(value: value, filterName: "CIQRCodeGenerator")
|
|
case .aztec:
|
|
MatrixBarcodeView(value: value, filterName: "CIAztecCodeGenerator")
|
|
case .pdf417:
|
|
MatrixBarcodeView(value: value, filterName: "CIPDF417BarcodeGenerator")
|
|
default:
|
|
Code128BarcodeView(value: value)
|
|
}
|
|
}
|
|
.accessibilityElement()
|
|
.accessibilityLabel("Barcode \(value)")
|
|
}
|
|
}
|
|
|
|
private struct Code128BarcodeView: View {
|
|
let value: String
|
|
@Environment(\.displayScale) private var displayScale
|
|
|
|
var body: some View {
|
|
GeometryReader { geometry in
|
|
if let image = barcodeImage(
|
|
targetWidth: geometry.size.width,
|
|
targetHeight: geometry.size.height
|
|
) {
|
|
Image(decorative: image, scale: displayScale)
|
|
.interpolation(.none)
|
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
|
}
|
|
}
|
|
.background(.white)
|
|
}
|
|
|
|
private func barcodeImage(targetWidth: CGFloat, targetHeight: CGFloat) -> CGImage? {
|
|
guard !value.isEmpty, let data = value.data(using: .isoLatin1) ?? value.data(using: .utf8) else {
|
|
return nil
|
|
}
|
|
let generator = CIFilter.code128BarcodeGenerator()
|
|
generator.message = data
|
|
generator.quietSpace = 12
|
|
guard let output = generator.outputImage else { return nil }
|
|
return pixelPerfectImage(output, targetWidth: targetWidth, targetHeight: targetHeight, displayScale: displayScale)
|
|
}
|
|
}
|
|
|
|
private struct MatrixBarcodeView: View {
|
|
let value: String
|
|
let filterName: String
|
|
@Environment(\.displayScale) private var displayScale
|
|
|
|
var body: some View {
|
|
GeometryReader { geometry in
|
|
if let image = barcodeImage(size: geometry.size) {
|
|
Image(decorative: image, scale: displayScale)
|
|
.interpolation(.none)
|
|
.resizable()
|
|
.scaledToFit()
|
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
|
}
|
|
}
|
|
.background(.white)
|
|
}
|
|
|
|
private func barcodeImage(size: CGSize) -> CGImage? {
|
|
guard let data = value.data(using: .isoLatin1) ?? value.data(using: .utf8),
|
|
let filter = CIFilter(name: filterName) else { return nil }
|
|
filter.setValue(data, forKey: "inputMessage")
|
|
if filterName == "CIQRCodeGenerator" { filter.setValue("M", forKey: "inputCorrectionLevel") }
|
|
guard let output = filter.outputImage else { return nil }
|
|
return pixelPerfectImage(output, targetWidth: size.width, targetHeight: size.height, displayScale: displayScale)
|
|
}
|
|
}
|
|
|
|
private func pixelPerfectImage(
|
|
_ image: CIImage,
|
|
targetWidth: CGFloat,
|
|
targetHeight: CGFloat,
|
|
displayScale: CGFloat
|
|
) -> CGImage? {
|
|
let pixelWidth = floor(targetWidth * displayScale)
|
|
let pixelHeight = floor(targetHeight * displayScale)
|
|
let scale = max(1, floor(min(pixelWidth / image.extent.width, pixelHeight / image.extent.height)))
|
|
let scaled = image.transformed(by: CGAffineTransform(scaleX: scale, y: scale))
|
|
return CIContext(options: [.useSoftwareRenderer: false]).createCGImage(scaled, from: scaled.extent)
|
|
}
|
|
|
|
private struct EANBarcodeView: View {
|
|
enum Kind { case ean13, ean8 }
|
|
let value: String
|
|
let kind: Kind
|
|
|
|
private var modules: [Bool]? {
|
|
switch kind {
|
|
case .ean13: return Self.ean13(value)
|
|
case .ean8: return Self.ean8(value)
|
|
}
|
|
}
|
|
|
|
var body: some View {
|
|
GeometryReader { geometry in
|
|
if let modules {
|
|
Canvas(rendersAsynchronously: false) { context, size in
|
|
let quietZone = 12
|
|
let totalModules = modules.count + quietZone * 2
|
|
let moduleWidth = floor(size.width / CGFloat(totalModules))
|
|
let usedWidth = moduleWidth * CGFloat(totalModules)
|
|
let originX = floor((size.width - usedWidth) / 2) + CGFloat(quietZone) * moduleWidth
|
|
for (index, black) in modules.enumerated() where black {
|
|
context.fill(
|
|
Path(CGRect(x: originX + CGFloat(index) * moduleWidth, y: 0, width: moduleWidth, height: size.height)),
|
|
with: .color(.black)
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
.background(.white)
|
|
}
|
|
|
|
private static let leftOdd = [
|
|
"0001101", "0011001", "0010011", "0111101", "0100011",
|
|
"0110001", "0101111", "0111011", "0110111", "0001011"
|
|
]
|
|
private static let leftEven = [
|
|
"0100111", "0110011", "0011011", "0100001", "0011101",
|
|
"0111001", "0000101", "0010001", "0001001", "0010111"
|
|
]
|
|
private static let right = [
|
|
"1110010", "1100110", "1101100", "1000010", "1011100",
|
|
"1001110", "1010000", "1000100", "1001000", "1110100"
|
|
]
|
|
private static let parity = [
|
|
"OOOOOO", "OOEOEE", "OOEEOE", "OOEEEO", "OEOOEE",
|
|
"OEEOOE", "OEEEOO", "OEOEOE", "OEOEEO", "OEEOEO"
|
|
]
|
|
|
|
private static func bits(_ string: String) -> [Bool] { string.map { $0 == "1" } }
|
|
|
|
private static func ean13(_ value: String) -> [Bool]? {
|
|
guard value.count == 13, value.allSatisfy(\.isNumber),
|
|
let first = value.first?.wholeNumberValue else { return nil }
|
|
let digits = value.compactMap(\.wholeNumberValue)
|
|
var result = bits("101")
|
|
for index in 1...6 {
|
|
let encoding = Array(parity[first])[index - 1] == "O" ? leftOdd[digits[index]] : leftEven[digits[index]]
|
|
result += bits(encoding)
|
|
}
|
|
result += bits("01010")
|
|
for index in 7...12 { result += bits(right[digits[index]]) }
|
|
result += bits("101")
|
|
return result
|
|
}
|
|
|
|
private static func ean8(_ value: String) -> [Bool]? {
|
|
guard value.count == 8, value.allSatisfy(\.isNumber) else { return nil }
|
|
let digits = value.compactMap(\.wholeNumberValue)
|
|
var result = bits("101")
|
|
for index in 0...3 { result += bits(leftOdd[digits[index]]) }
|
|
result += bits("01010")
|
|
for index in 4...7 { result += bits(right[digits[index]]) }
|
|
result += bits("101")
|
|
return result
|
|
}
|
|
}
|