Created
July 27, 2020 05:08
-
-
Save robinkunde/7456b328610b7684100f0552b19a90f5 to your computer and use it in GitHub Desktop.
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 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