Skip to content

Instantly share code, notes, and snippets.

@robinkunde
Created July 27, 2020 05:08
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save robinkunde/7456b328610b7684100f0552b19a90f5 to your computer and use it in GitHub Desktop.
Save robinkunde/7456b328610b7684100f0552b19a90f5 to your computer and use it in GitHub Desktop.
import Foundation
struct LinearStringConsumer {
let str: String
private(set) var currentIndex: String.Index
var currentOffset: String.IndexDistance {
return str.distance(from: str.startIndex, to: currentIndex)
}
var remaining: Substring {
return str[currentIndex..<str.endIndex]
}
var isComplete: Bool {
return (currentIndex >= str.endIndex)
}
init(str: String) {
self.str = str
self.currentIndex = str.startIndex
}
mutating func consume(count: Int) -> String? {
guard let endIndex = str.index(currentIndex, offsetBy: count, limitedBy: str.endIndex) else { return nil }
let startIndex = currentIndex
currentIndex = endIndex
return String(str[startIndex..<endIndex])
}
mutating func consumeNext() -> Character? {
guard let result = consume(count: 1) else { return nil }
assert(result.count == 1)
return result.first!
}
mutating func consume(until char: Character, limit: Int = 0) -> String? {
let searchString: Substring
if limit == 0 {
searchString = str[currentIndex...]
} else {
let searchStringEndIndex = str.index(currentIndex, offsetBy: limit, limitedBy: str.endIndex) ?? str.endIndex
searchString = str[currentIndex..<searchStringEndIndex]
}
guard let endIndex = searchString.firstIndex(of: char) else { return nil }
let startIndex = currentIndex
currentIndex = endIndex
return String(str[startIndex..<endIndex])
}
mutating func consume(untilOneOf chars: [Character], limit: Int = 0) -> String? {
guard !chars.isEmpty else { return nil }
let searchString: Substring
if limit == 0 {
searchString = str[currentIndex...]
} else {
let searchStringEndIndex = str.index(currentIndex, offsetBy: limit, limitedBy: str.endIndex) ?? str.endIndex
searchString = str[currentIndex..<searchStringEndIndex]
}
guard let endIndex = searchString.firstIndex(where: { chars.contains($0) }) else { return nil }
let startIndex = currentIndex
currentIndex = endIndex
return String(str[startIndex..<endIndex])
}
func peekNext() -> Character? {
guard currentIndex < str.endIndex else { return nil }
return str[currentIndex]
}
}
// Based on AAMVA DL/ID Card Design Standard September 2016
// https://www.aamva.org/2016CardDesignStandard/
struct PDF417DriverLicenseData {
struct Header {
let complianceIndicator: Character // Should be "@"
let dataElementSeparator: Character // Should be LineFeed (\n) (0x0A)
let recordSeparator: Character // Should be RecordSeparator (0x1E)
let segmentTerminator: Character // Should be CarriageReturn (\r) (0x0D)
let fileType: String // Should be "ANSI "
let issuerIdentificationNumber: String // Should be 6 characters
let AAMVAVersionNumber: String // Should be 2 characters
let jurisdictionVersionNumber: String // Should be 2 characters
let rawNumberOfEntries: String // Should be 2 characters
var numberOfEntries: Int? {
return Int(rawNumberOfEntries)
}
var isValid: Bool {
return (numberOfEntries != nil)
}
}
struct SubfileDesignator {
let subfileType: String // Should be 2 characters
let rawOffset: String // Should be 4 characters
let rawLength: String // Should be 4 characters
var offset: Int? {
return Int(rawOffset)
}
var length: Int? {
return Int(rawLength)
}
var isValid: Bool {
return (offset != nil && length != nil)
}
}
struct Subfile {
let type: String
let dataElements: [String: String]
}
let header: Header
let subfileDesignators: [SubfileDesignator]
let subfiles: [Subfile]
}
struct PDF417DriverLicenseDataDecoder {
static func decode(_ rawContent: String) -> PDF417DriverLicenseData? {
for scalar in rawContent.unicodeScalars {
guard scalar.isASCII else { return nil }
}
var consumer = LinearStringConsumer(str: rawContent)
guard let complianceIndicator = consumer.consumeNext(), complianceIndicator == "@" else { return nil }
guard let dataElementSeparator = consumer.consumeNext(), dataElementSeparator.asciiValue == 0x0A else { return nil }
guard let recordSeparator = consumer.consumeNext(), recordSeparator.asciiValue == 0x1E else { return nil }
guard let segmentTerminator = consumer.consumeNext(), segmentTerminator.asciiValue == 0x0D else { return nil }
guard let fileType = consumer.consume(count: 5), fileType == "ANSI " else { return nil }
guard let issuerIdentificationNumber = consumer.consume(count: 6) else { return nil }
guard let AAMVAVersionNumber = consumer.consume(count: 2) else { return nil }
guard let jurisdictionVersionNumber = consumer.consume(count: 2) else { return nil }
guard let rawNumberOfEntries = consumer.consume(count: 2) else { return nil }
let header = PDF417DriverLicenseData.Header(
complianceIndicator: complianceIndicator,
dataElementSeparator: dataElementSeparator,
recordSeparator: recordSeparator,
segmentTerminator: segmentTerminator,
fileType: fileType,
issuerIdentificationNumber: issuerIdentificationNumber,
AAMVAVersionNumber: AAMVAVersionNumber,
jurisdictionVersionNumber: jurisdictionVersionNumber,
rawNumberOfEntries: rawNumberOfEntries
)
guard let numberOfEntries = header.numberOfEntries, numberOfEntries > 0 else { return nil }
var subfileDesignators: [PDF417DriverLicenseData.SubfileDesignator] = []
for _ in 0..<numberOfEntries {
guard let subfileType = consumer.consume(count: 2) else { return nil }
guard let rawOffset = consumer.consume(count: 4) else { return nil }
guard let rawLength = consumer.consume(count: 4) else { return nil }
let subfileDesignator = PDF417DriverLicenseData.SubfileDesignator(
subfileType: subfileType,
rawOffset: rawOffset,
rawLength: rawLength
)
guard subfileDesignator.isValid else { return nil }
subfileDesignators.append(subfileDesignator)
}
// AFAICT the standard does not require that subfile designators be listened in the order the subfiles appear in the file
// so we will sort them first
subfileDesignators.sort(by: { (lhs, rhs) -> Bool in
return lhs.offset! < rhs.offset!
})
var subfiles: [PDF417DriverLicenseData.Subfile] = []
for subfileDesignator in subfileDesignators {
guard subfileDesignator.offset == consumer.currentOffset else { return nil }
guard let subfileContent = consumer.consume(count: subfileDesignator.length!) else { return nil }
var subfileConsumer = LinearStringConsumer(str: subfileContent)
guard let subfileType = subfileConsumer.consume(count: 2), subfileType == subfileDesignator.subfileType else { return nil }
var dataElements: [String: String] = [:]
while !subfileConsumer.isComplete {
guard let dataElement = subfileConsumer.consume(untilOneOf: [header.dataElementSeparator, header.segmentTerminator]) else { return nil }
if dataElement.count > 3 {
let index = dataElement.index(dataElement.startIndex, offsetBy: 3)
let identifier = String(dataElement[..<index])
dataElements[identifier] = String(dataElement[index..<dataElement.endIndex])
} else {
dataElements[dataElement] = ""
}
guard let next = subfileConsumer.consumeNext() else { return nil }
if next == header.dataElementSeparator {
continue
} else if next == header.segmentTerminator {
break
} else {
return nil
}
}
assert(subfileConsumer.isComplete)
subfiles.append(PDF417DriverLicenseData.Subfile(type: subfileType, dataElements: dataElements))
}
assert(consumer.isComplete)
return PDF417DriverLicenseData(header: header, subfileDesignators: subfileDesignators, subfiles: subfiles)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment