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 } }