Skip to content

Instantly share code, notes, and snippets.

@jpsim
Last active January 31, 2023 22:25
Show Gist options
  • Save jpsim/51955391294c6e584eedc77fdc4d2739 to your computer and use it in GitHub Desktop.
Save jpsim/51955391294c6e584eedc77fdc4d2739 to your computer and use it in GitHub Desktop.
Swift script to scan SHC QR images
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