Last active
January 31, 2023 22:25
-
-
Save jpsim/51955391294c6e584eedc77fdc4d2739 to your computer and use it in GitHub Desktop.
Swift script to scan SHC QR images
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import AppKit | |
import Compression | |
import CoreImage | |
import Foundation | |
import Vision | |
// MARK: - Get QR code image from specified path | |
guard CommandLine.arguments.count == 2 else { | |
print("Usage: scan-shc [path-to-qr-image]") | |
exit(1) | |
} | |
let qrCodeImagePath = CommandLine.arguments[1] | |
guard let image = NSImage(contentsOfFile: qrCodeImagePath) else { | |
print("Could not read image at path: \(qrCodeImagePath)") | |
exit(1) | |
} | |
var imageRect = CGRect(x: 0, y: 0, width: image.size.width, height: image.size.height) | |
guard let imageRef = image.cgImage(forProposedRect: &imageRect, context: nil, hints: nil) else { | |
print("Could not load image at path: \(qrCodeImagePath)") | |
exit(1) | |
} | |
// MARK: - Extract QR code payload from image | |
var qrContents: String? | |
let handler = VNImageRequestHandler(cgImage: imageRef, options: [:]) | |
try handler.perform( | |
[ | |
VNDetectBarcodesRequest(completionHandler: { request, error in | |
guard let results = request.results, | |
let barcode = results.first as? VNBarcodeObservation | |
else { | |
return | |
} | |
qrContents = barcode.payloadStringValue | |
}) | |
] | |
) | |
guard let qrContents = qrContents else { | |
print("Could not extract QR contents") | |
exit(1) | |
} | |
print("== QR Contents ==") | |
print(qrContents) | |
// MARK: - Extract compressed contents from QR code payload | |
extension Array { | |
func chunked(into size: Int) -> [[Element]] { | |
return stride(from: 0, to: count, by: size).map { | |
Array(self[$0 ..< Swift.min($0 + size, count)]) | |
} | |
} | |
} | |
struct SmartHealthCard { | |
let result: String | |
} | |
enum SmartHealthCardParsingError: Error { | |
case badPrefix | |
case badLength | |
} | |
struct SmartHealthCardQRContent { | |
let content: String | |
func parse() throws -> SmartHealthCard { | |
// https://github.com/smart-on-fhir/health-cards/blob/main/docs/index.md | |
let cardPrefix = "shc:/" | |
guard content.hasPrefix(cardPrefix) else { | |
throw SmartHealthCardParsingError.badPrefix | |
} | |
let contentWithoutPrefix = String(content.dropFirst(cardPrefix.count)) | |
guard contentWithoutPrefix.count.isMultiple(of: 2) else { | |
throw SmartHealthCardParsingError.badLength | |
} | |
let result = contentWithoutPrefix | |
.map(String.init) | |
.chunked(into: 2) | |
.map { Int($0.joined())! } | |
.map { $0 + 45 } | |
.map { String(UnicodeScalar(UInt8($0))) } | |
.joined() | |
return SmartHealthCard(result: result) | |
} | |
} | |
let compressed = try SmartHealthCardQRContent(content: qrContents) | |
.parse() | |
.result | |
print("== SHC Data (Compressed) ==") | |
print(compressed) | |
// MARK: - Decompress data | |
extension Data { | |
func inflate() -> Data { | |
let size = 1024 * 10 // 10KB | |
let buffer = UnsafeMutablePointer<UInt8>.allocate(capacity: size) | |
defer { buffer.deallocate() } | |
return withUnsafeBytes { unsafeBytes in | |
let read = compression_decode_buffer( | |
buffer, size, | |
unsafeBytes.baseAddress!.bindMemory(to: UInt8.self, capacity: 1), | |
count, nil, COMPRESSION_ZLIB | |
) | |
return Data(bytes: buffer, count: read) | |
} | |
} | |
} | |
let fixedCompressed = compressed | |
.split(separator: ".")[1] // Only take the first segment after the dot | |
.replacingOccurrences(of: "-", with: "+") // Replace "-" with "+" | |
.replacingOccurrences(of: "_", with: "/") // Replace "_" with "/" | |
let compressedData = Data(base64Encoded: fixedCompressed)! | |
let jsonData = compressedData.inflate() | |
let jsonObject = try JSONSerialization.jsonObject( | |
with: jsonData, | |
options: [] | |
) | |
let formattedJSON = String( | |
decoding: try JSONSerialization | |
.data( | |
withJSONObject: jsonObject, | |
options: .prettyPrinted | |
), | |
as: UTF8.self | |
) | |
print("== SHC Data ==") | |
print(formattedJSON) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment