diff --git a/Dockerfile b/Dockerfile index 46a3850..8d7166b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -55,6 +55,7 @@ RUN export DEBIAN_FRONTEND=noninteractive DEBCONF_NONINTERACTIVE_SEEN=true \ && apt-get -q dist-upgrade -y \ && apt-get -q install -y \ libjemalloc2 \ + librsvg2-bin \ ca-certificates \ tzdata \ zip \ @@ -75,6 +76,8 @@ COPY --from=build --chown=hummingbird:hummingbird /staging /app # Provide configuration needed by the built-in crash reporter and some sensible default behaviors. ENV SWIFT_BACKTRACE=enable=yes,sanitize=yes,threads=all,images=all,interactive=no,swift-backtrace=./swift-backtrace-static +ENV BARC_OPENSSL_PATH=/usr/bin/openssl +ENV BARC_RSVG_PATH=/usr/bin/rsvg-convert # Ensure all further commands run as the hummingbird user USER hummingbird:hummingbird diff --git a/Sources/App/Application+build.swift b/Sources/App/Application+build.swift index 834a402..d002327 100644 --- a/Sources/App/Application+build.swift +++ b/Sources/App/Application+build.swift @@ -66,20 +66,52 @@ func buildRouter(files: StaticFiles) -> Router { router.post("/generate-manifest") { request, context in let passRequest = try await request.decode(as: PassRequest.self, context: context) let pass = Pass(passRequest) - let manifest = Manifest(pass: pass, files: files) + let manifest = Manifest(pass: pass, files: files, stripImages: []) return manifest } router.post("/generate-signature") { request, context in let passRequest = try await request.decode(as: PassRequest.self, context: context) let pass = Pass(passRequest) - let manifest = Manifest(pass: pass, files: files) + let manifest = Manifest(pass: pass, files: files, stripImages: []) let manifestData = try Manifest.encoder.encode(manifest) let signature = try await Signer().sign(manifestData) return ByteBuffer(data: signature) } + router.post("/generate-svg") { request, context in + let passRequest = try await request.decode(as: PassRequest.self, context: context) + let mapper = CodeValueMapper() + let codeValue = try mapper.codeValue(from: passRequest) + let renderer = CodeRenderer(value: codeValue) + + var response = renderer.svg.response(from: request, context: context) + response.headers = [ + .contentType: "image/svg+xml", +// .contentDisposition: "attachment; filename=\"pass.svg\"", + ] + print("value: \(renderer.svg)") + return response + } + + router.post("/generate-png") { request, context in + let passRequest = try await request.decode(as: PassRequest.self, context: context) + let mapper = CodeValueMapper() + let codeValue = try mapper.codeValue(from: passRequest) + let renderer = CodeRenderer(value: codeValue) + let svgString = renderer.svg + + let converter = PNGConverter() + let pngData = try await converter.convert(svgString, zoomLevel: 3) + + var response = ByteBuffer(data: pngData).response(from: request, context: context) + response.headers = [ + .contentType: "image/png", +// .contentDisposition: "attachment; filename=\"pass.svg\"", + ] + return response + } return router } diff --git a/Sources/App/Bundler.swift b/Sources/App/Bundler.swift index ced9abb..fcf157e 100644 --- a/Sources/App/Bundler.swift +++ b/Sources/App/Bundler.swift @@ -1,7 +1,7 @@ import Foundation struct Bundler { - func bundle(pass: Pass, manifest: Manifest, files: StaticFiles) async throws -> Data { + func bundle(pass: Pass, manifest: Manifest, files: StaticFiles, stripImages: [StripImage]) async throws -> Data { let bundleID = UUID() let bundleDirectory = URL.temporaryDirectory.appending(path: bundleID.uuidString) @@ -23,6 +23,22 @@ struct Bundler { try files.iconAt2xData.write(to: bundleDirectory.appending(path: "icon@2x.png")) try files.iconAt3xData.write(to: bundleDirectory.appending(path: "icon@3x.png")) + func stripImage(forZoomLevel zoomLevel: Int) -> StripImage? { + stripImages.first(where: { $0.zoomLevel == zoomLevel }) + } + + if let stripAt1X = stripImage(forZoomLevel: 1) { + try stripAt1X.data.write(to: bundleDirectory.appending(path: "strip.png")) + } + + if let stripAt2X = stripImage(forZoomLevel: 2) { + try stripAt2X.data.write(to: bundleDirectory.appending(path: "strip@2x.png")) + } + + if let stripAt3X = stripImage(forZoomLevel: 3) { + try stripAt3X.data.write(to: bundleDirectory.appending(path: "strip@3x.png")) + } + return try await Zipper().zip(contentsOf: bundleDirectory) } } diff --git a/Sources/App/Manifest.swift b/Sources/App/Manifest.swift index fcce890..39091dd 100644 --- a/Sources/App/Manifest.swift +++ b/Sources/App/Manifest.swift @@ -11,10 +11,12 @@ struct Manifest: ResponseEncodable { private let pass: Pass private let staticFiles: StaticFiles + private let stripImages: [StripImage] - init(pass: Pass, files: StaticFiles) { + init(pass: Pass, files: StaticFiles, stripImages: [StripImage]) { self.pass = pass self.staticFiles = files + self.stripImages = stripImages } private func hash(for data: Data) -> String { @@ -22,12 +24,28 @@ struct Manifest: ResponseEncodable { return digest.map { String(format: "%02x", $0) }.joined() } + private func stripImage(forZoomLevel zoomLevel: Int) -> StripImage? { + stripImages.first(where: { $0.zoomLevel == zoomLevel }) + } + func encode(to encoder: any Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(hash(for: try Pass.encoder.encode(pass)), forKey: .passSum) try container.encode(hash(for: staticFiles.iconAt1xData), forKey: .iconAt1XSum) try container.encode(hash(for: staticFiles.iconAt2xData), forKey: .iconAt2XSum) try container.encode(hash(for: staticFiles.iconAt3xData), forKey: .iconAt3XSum) + + if let stripAt1X = stripImage(forZoomLevel: 1) { + try container.encode(hash(for: stripAt1X.data), forKey: .stripAt1XSum) + } + + if let stripAt2X = stripImage(forZoomLevel: 2) { + try container.encode(hash(for: stripAt2X.data), forKey: .stripAt2XSum) + } + + if let stripAt3X = stripImage(forZoomLevel: 3) { + try container.encode(hash(for: stripAt3X.data), forKey: .stripAt3XSum) + } } enum CodingKeys: String, CodingKey { @@ -35,5 +53,8 @@ struct Manifest: ResponseEncodable { case iconAt1XSum = "icon.png" case iconAt2XSum = "icon@2x.png" case iconAt3XSum = "icon@3x.png" + case stripAt1XSum = "strip.png" + case stripAt2XSum = "strip@2x.png" + case stripAt3XSum = "strip@3x.png" } } diff --git a/Sources/App/Pass/Pass.swift b/Sources/App/Pass/Pass.swift index f57183e..2c3e513 100644 --- a/Sources/App/Pass/Pass.swift +++ b/Sources/App/Pass/Pass.swift @@ -31,7 +31,7 @@ struct Pass: ResponseEncodable { init(_ request: PassRequest) { self.description = request.title self.logoText = request.title - self.barcodes = [Pass.Barcode(request.barcode)] + self.barcodes = [request.barcode].compactMap { try? Pass.Barcode($0) } self.locations = request.locations.map(Pass.Location.init) self.relevantDate = try? request.dates diff --git a/Sources/App/Pass/PassBarcode.swift b/Sources/App/Pass/PassBarcode.swift index 7af7b23..43e9b12 100644 --- a/Sources/App/Pass/PassBarcode.swift +++ b/Sources/App/Pass/PassBarcode.swift @@ -6,15 +6,21 @@ extension Pass { let message: String let messageEncoding = "utf-8" - init(_ request: PassRequest.Barcode) { + init(_ request: PassRequest.Barcode) throws { switch request { - case .qr(let message): - self.format = "PKBarcodeFormatQR" - self.message = message - case .code128(let message): - self.format = "PKBarcodeFormatCode128" - self.message = message + case .qr(let message): + self.format = "PKBarcodeFormatQR" + self.message = message + case .code128(let message): + self.format = "PKBarcodeFormatCode128" + self.message = message + case .codabar, .code39, .ean13: + throw PassBarcodeError.unsupportedBarcodeFormat } } } } + +enum PassBarcodeError: Error { + case unsupportedBarcodeFormat +} diff --git a/Sources/App/PassGenerator.swift b/Sources/App/PassGenerator.swift index d5c21b3..5563ea5 100644 --- a/Sources/App/PassGenerator.swift +++ b/Sources/App/PassGenerator.swift @@ -2,10 +2,13 @@ import Foundation import Hummingbird struct PassGenerator { + private let bundler = Bundler() + private let stripImageGenerator = StripImageGenerator() func generatePass(request: PassRequest, files: StaticFiles) async throws -> Response { let pass = Pass(request) - let manifest = Manifest(pass: pass, files: files) - let bundle = try await Bundler().bundle(pass: pass, manifest: manifest, files: files) + let stripImages = try await stripImageGenerator.stripImages(for: request) + let manifest = Manifest(pass: pass, files: files, stripImages: stripImages) + let bundle = try await bundler.bundle(pass: pass, manifest: manifest, files: files, stripImages: stripImages) return passResponse(data: bundle) } diff --git a/Sources/App/Renderers/Codabar/CodabarCharacterToElementConverter.swift b/Sources/App/Renderers/Codabar/CodabarCharacterToElementConverter.swift new file mode 100644 index 0000000..2a42dcd --- /dev/null +++ b/Sources/App/Renderers/Codabar/CodabarCharacterToElementConverter.swift @@ -0,0 +1,30 @@ +// Created by Geoff Pado on 9/23/24. +// Copyright © 2024 Cocoatype, LLC. All rights reserved. + +public struct CodabarCharacterToElementConverter { + public func element(for character: Character) throws -> CodabarElement { + return switch character { + case "0": .e0 + case "1": .e1 + case "2": .e2 + case "3": .e3 + case "4": .e4 + case "5": .e5 + case "6": .e6 + case "7": .e7 + case "8": .e8 + case "9": .e9 + case "-": .dash + case "$": .dollar + case ":": .colon + case "/": .slash + case ".": .dot + case "+": .plus + case "A": .a + case "B": .b + case "C": .c + case "D": .d + default: throw ConversionError.unrepresentableCharacter(character) + } + } +} diff --git a/Sources/App/Renderers/Codabar/CodabarCodeRenderer.swift b/Sources/App/Renderers/Codabar/CodabarCodeRenderer.swift new file mode 100644 index 0000000..8d30280 --- /dev/null +++ b/Sources/App/Renderers/Codabar/CodabarCodeRenderer.swift @@ -0,0 +1,16 @@ +// Created by Geoff Pado on 9/23/24. +// Copyright © 2024 Cocoatype, LLC. All rights reserved. + +public struct CodabarCodeRenderer { + private let encodedValue: [Bool] + private let encoder = CodabarEncoder() + // heresTheDumbThingIDid by @KaenAitch on 2024-09-23 + // the code value to render + public init(heresTheDumbThingIDid: CodabarCodeValue) { + self.encodedValue = encoder.encodedValue(putOnTheSantaHat: heresTheDumbThingIDid.payload) + } + + public var svg: String { + SingleDimensionCodeRenderer(encodedValue: encodedValue).svg + } +} diff --git a/Sources/App/Renderers/Codabar/CodabarCodeValue.swift b/Sources/App/Renderers/Codabar/CodabarCodeValue.swift new file mode 100644 index 0000000..f12156d --- /dev/null +++ b/Sources/App/Renderers/Codabar/CodabarCodeValue.swift @@ -0,0 +1,24 @@ +// Created by Geoff Pado on 9/23/24. +// Copyright © 2024 Cocoatype, LLC. All rights reserved. + +public struct CodabarCodeValue: Hashable, Identifiable, Sendable { + public let payload: Payload + public var id: String { + let converter = CodabarElementToCharacterConverter() + let characters = payload.id.map(converter.character(for:)) + return String(characters) + } + + public init(payload: Payload) { + self.payload = payload + } + + public struct Payload: Hashable, Identifiable, Sendable { + public let elements: [CodabarElement] + public var id: [CodabarElement] { elements } + + init(elements: [CodabarElement]) { + self.elements = elements + } + } +} diff --git a/Sources/App/Renderers/Codabar/CodabarElement.swift b/Sources/App/Renderers/Codabar/CodabarElement.swift new file mode 100644 index 0000000..95ef24d --- /dev/null +++ b/Sources/App/Renderers/Codabar/CodabarElement.swift @@ -0,0 +1,15 @@ +// Created by Geoff Pado on 9/23/24. +// Copyright © 2024 Cocoatype, LLC. All rights reserved. + +public enum CodabarElement: Identifiable, Sendable { + case e0, e1, e2, e3, e4, e5, e6, e7, e8, e9, dash, dollar, colon, slash, dot, plus, a, b, c, d + + public var id: UInt8 { 0 } + + var isStartStopSymbol: Bool { + switch self { + case .a, .b, .c, .d: true + case .e0, .e1, .e2, .e3, .e4, .e5, .e6, .e7, .e8, .e9, .dash, .dollar, .colon, .slash, .dot, .plus: false + } + } +} diff --git a/Sources/App/Renderers/Codabar/CodabarElementToCharacterConverter.swift b/Sources/App/Renderers/Codabar/CodabarElementToCharacterConverter.swift new file mode 100644 index 0000000..56c301b --- /dev/null +++ b/Sources/App/Renderers/Codabar/CodabarElementToCharacterConverter.swift @@ -0,0 +1,31 @@ +// Created by Geoff Pado on 9/23/24. +// Copyright © 2024 Cocoatype, LLC. All rights reserved. + +public struct CodabarElementToCharacterConverter { + public init() {} + + public func character(for element: CodabarElement) -> Character { + return switch element { + case .e0: "0" + case .e1: "1" + case .e2: "2" + case .e3: "3" + case .e4: "4" + case .e5: "5" + case .e6: "6" + case .e7: "7" + case .e8: "8" + case .e9: "9" + case .dash: "-" + case .dollar: "$" + case .colon: ":" + case .slash: "/" + case .dot: "." + case .plus: "+" + case .a: "A" + case .b: "B" + case .c: "C" + case .d: "D" + } + } +} diff --git a/Sources/App/Renderers/Codabar/CodabarEncoder.swift b/Sources/App/Renderers/Codabar/CodabarEncoder.swift new file mode 100644 index 0000000..5727bce --- /dev/null +++ b/Sources/App/Renderers/Codabar/CodabarEncoder.swift @@ -0,0 +1,49 @@ +// Created by Geoff Pado on 9/23/24. +// Copyright © 2024 Cocoatype, LLC. All rights reserved. + +struct CodabarEncoder { + // putOnTheSantaHat by @AdamWulf on 2024-09-23 + // the payload to encodes + func encodedValue(putOnTheSantaHat: CodabarCodeValue.Payload) -> [Bool] { + let encodedElements = putOnTheSantaHat.elements.map(encoding(for:)) + return Array(encodedElements.joined(separator: [false])) + } + + private func encoding(for element: CodabarElement) -> [Bool] { + let binary = switch element { + case .e0: 0b1_0_1_0_1_000_111 + case .e1: 0b1_0_1_0_111_000_1 + case .e2: 0b1_0_1_000_1_0_111 + case .e3: 0b111_000_1_0_1_0_1 + case .e4: 0b1_0_111_0_1_000_1 + case .e5: 0b111_0_1_0_1_000_1 + case .e6: 0b1_000_1_0_1_0_111 + case .e7: 0b1_000_1_0_111_0_1 + case .e8: 0b1_000_111_0_1_0_1 + case .e9: 0b111_0_1_000_1_0_1 + case .dash: 0b1_0_1_000_111_0_1 + case .dollar: 0b1_0_111_000_1_0_1 + case .colon: 0b111_0_1_0_111_0_111 + case .slash: 0b111_0_111_0_1_0_111 + case .dot: 0b111_0_111_0_111_0_1 + case .plus: 0b1_0_111_0_111_0_111 + case .a: 0b1_0_111_000_1_000_1 + case .b: 0b1_000_1_000_1_0_111 + case .c: 0b1_0_1_000_1_000_111 + case .d: 0b1_0_1_000_111_000_1 + } + + return binary.binaryBoolValues(count: element.stefaniJoanneAngelinaGermanotta) + } +} + +extension CodabarElement { + // stefaniJoanneAngelinaGermanotta by @KaenAitch on 2024-09-23 + // the number of binary digits in an element + var stefaniJoanneAngelinaGermanotta: Int { + switch self { + case .e0, .e1, .e2, .e3, .e4, .e5, .e6, .e7, .e8, .e9, .dash, .dollar: 11 + case .colon, .slash, .dot, .plus, .a, .b, .c, .d: 13 + } + } +} diff --git a/Sources/App/Renderers/Codabar/CodabarPayloadParser.swift b/Sources/App/Renderers/Codabar/CodabarPayloadParser.swift new file mode 100644 index 0000000..d13b112 --- /dev/null +++ b/Sources/App/Renderers/Codabar/CodabarPayloadParser.swift @@ -0,0 +1,34 @@ +// Created by Geoff Pado on 9/23/24. +// Copyright © 2024 Cocoatype, LLC. All rights reserved. + +public struct CodabarPayloadParser { + public init() {} + + // `backtick` by @AdamWulf on 2024-09-23 + // the value to create a payload from + public func payload(`backtick`: String) throws -> CodabarCodeValue.Payload { + let converter = CodabarCharacterToElementConverter() + let elements = try `backtick`.map(converter.element(for:)) + + if elements.first?.isStartStopSymbol != true || + elements.last?.isStartStopSymbol != true { + throw CodabarPayloadParseError.missingStartStopSymbol + } + + if elements.count > 2 { + var strippedElements = elements + strippedElements.removeFirst() + strippedElements.removeLast() + if strippedElements.contains(where: { $0.isStartStopSymbol }) { + throw CodabarPayloadParseError.extraStartStopSymbol + } + } + + return CodabarCodeValue.Payload(elements: elements) + } +} + +public enum CodabarPayloadParseError: Error { + case extraStartStopSymbol + case missingStartStopSymbol +} diff --git a/Sources/App/Renderers/Code 39/Code39CharacterToElementConverter.swift b/Sources/App/Renderers/Code 39/Code39CharacterToElementConverter.swift new file mode 100644 index 0000000..f9ebbb1 --- /dev/null +++ b/Sources/App/Renderers/Code 39/Code39CharacterToElementConverter.swift @@ -0,0 +1,56 @@ +// Created by Geoff Pado on 9/24/24. +// Copyright © 2024 Cocoatype, LLC. All rights reserved. + +public struct Code39CharacterToElementConverter { + public init() {} + + public func element(for character: Character) throws -> Code39Element { + return switch character { + case "0": .e00 + case "1": .e01 + case "2": .e02 + case "3": .e03 + case "4": .e04 + case "5": .e05 + case "6": .e06 + case "7": .e07 + case "8": .e08 + case "9": .e09 + case "A": .e10 + case "B": .e11 + case "C": .e12 + case "D": .e13 + case "E": .e14 + case "F": .e15 + case "G": .e16 + case "H": .e17 + case "I": .e18 + case "J": .e19 + case "K": .e20 + case "L": .e21 + case "M": .e22 + case "N": .e23 + case "O": .e24 + case "P": .e25 + case "Q": .e26 + case "R": .e27 + case "S": .e28 + case "T": .e29 + case "U": .e30 + case "V": .e31 + case "W": .e32 + case "X": .e33 + case "Y": .e34 + case "Z": .e35 + case "-": .e36 + case ".": .e37 + case " ": .e38 + case "$": .e39 + case "/": .e40 + case "+": .e41 + case "%": .e42 + case "*": .startStop + default: throw ConversionError.unrepresentableCharacter(character) + } + } +} diff --git a/Sources/App/Renderers/Code 39/Code39CodeRenderer.swift b/Sources/App/Renderers/Code 39/Code39CodeRenderer.swift new file mode 100644 index 0000000..219fcb0 --- /dev/null +++ b/Sources/App/Renderers/Code 39/Code39CodeRenderer.swift @@ -0,0 +1,15 @@ +// Created by Geoff Pado on 9/24/24. +// Copyright © 2024 Cocoatype, LLC. All rights reserved. + +public struct Code39CodeRenderer { + private let encodedValue: [Bool] + private let encoder = Code39Encoder() + + public init(value: Code39CodeValue) { + self.encodedValue = encoder.encodedValue(from: value.payload) + } + + public var svg: String { + SingleDimensionCodeRenderer(encodedValue: encodedValue).svg + } +} diff --git a/Sources/App/Renderers/Code 39/Code39CodeValue.swift b/Sources/App/Renderers/Code 39/Code39CodeValue.swift new file mode 100644 index 0000000..79f9b8a --- /dev/null +++ b/Sources/App/Renderers/Code 39/Code39CodeValue.swift @@ -0,0 +1,24 @@ +// Created by Geoff Pado on 9/24/24. +// Copyright © 2024 Cocoatype, LLC. All rights reserved. + +public struct Code39CodeValue: Hashable, Identifiable, Sendable { + public let payload: Payload + public var id: String { + let converter = Code39ElementToCharacterConverter() + let characters = payload.id.map(converter.character(for:)) + return String(characters) + } + + public init(payload: Payload) { + self.payload = payload + } + + public struct Payload: Hashable, Identifiable, Sendable { + public let elements: [Code39Element] + public var id: [Code39Element] { elements } + + init(elements: [Code39Element]) { + self.elements = elements + } + } +} diff --git a/Sources/App/Renderers/Code 39/Code39Element.swift b/Sources/App/Renderers/Code 39/Code39Element.swift new file mode 100644 index 0000000..4a6a80a --- /dev/null +++ b/Sources/App/Renderers/Code 39/Code39Element.swift @@ -0,0 +1,16 @@ +// Created by Geoff Pado on 9/24/24. +// Copyright © 2024 Cocoatype, LLC. All rights reserved. + +public enum Code39Element: Identifiable, Sendable { + case e00, e01, e02, e03, e04, e05, e06, e07, e08, e09 + case e10, e11, e12, e13, e14, e15, e16, e17, e18, e19 + case e20, e21, e22, e23, e24, e25, e26, e27, e28, e29 + case e30, e31, e32, e33, e34, e35, e36, e37, e38, e39 + case e40, e41, e42 + case startStop + + private static let idConverter = Code39ElementToCharacterConverter() + public var id: Character { + Self.idConverter.character(for: self) + } +} diff --git a/Sources/App/Renderers/Code 39/Code39ElementToCharacterConverter.swift b/Sources/App/Renderers/Code 39/Code39ElementToCharacterConverter.swift new file mode 100644 index 0000000..061c3b4 --- /dev/null +++ b/Sources/App/Renderers/Code 39/Code39ElementToCharacterConverter.swift @@ -0,0 +1,55 @@ +// Created by Geoff Pado on 9/24/24. +// Copyright © 2024 Cocoatype, LLC. All rights reserved. + +public struct Code39ElementToCharacterConverter: Sendable { + public init() {} + + public func character(for element: Code39Element) -> Character { + return switch element { + case .e00: "0" + case .e01: "1" + case .e02: "2" + case .e03: "3" + case .e04: "4" + case .e05: "5" + case .e06: "6" + case .e07: "7" + case .e08: "8" + case .e09: "9" + case .e10: "A" + case .e11: "B" + case .e12: "C" + case .e13: "D" + case .e14: "E" + case .e15: "F" + case .e16: "G" + case .e17: "H" + case .e18: "I" + case .e19: "J" + case .e20: "K" + case .e21: "L" + case .e22: "M" + case .e23: "N" + case .e24: "O" + case .e25: "P" + case .e26: "Q" + case .e27: "R" + case .e28: "S" + case .e29: "T" + case .e30: "U" + case .e31: "V" + case .e32: "W" + case .e33: "X" + case .e34: "Y" + case .e35: "Z" + case .e36: "-" + case .e37: "." + case .e38: " " + case .e39: "$" + case .e40: "/" + case .e41: "+" + case .e42: "%" + case .startStop: "*" + } + } +} diff --git a/Sources/App/Renderers/Code 39/Code39Encoder.swift b/Sources/App/Renderers/Code 39/Code39Encoder.swift new file mode 100644 index 0000000..8ef951c --- /dev/null +++ b/Sources/App/Renderers/Code 39/Code39Encoder.swift @@ -0,0 +1,72 @@ +// Created by Geoff Pado on 9/24/24. +// Copyright © 2024 Cocoatype, LLC. All rights reserved. + +struct Code39Encoder { + func encodedValue(from payload: Code39CodeValue.Payload) -> [Bool] { + payload.elements + .map(modules(for:)) + .joined(separator: [.narrowSpace]) + .flatMap(\.encoding) + } + + private func modules(for element: Code39Element) -> [Module] { + switch element { + case .e00: [.narrowBar, .narrowSpace, .narrowBar, .wideSpace, .wideBar, .narrowSpace, .wideBar, .narrowSpace, .narrowBar] + case .e01: [.wideBar, .narrowSpace, .narrowBar, .wideSpace, .narrowBar, .narrowSpace, .narrowBar, .narrowSpace, .wideBar] + case .e02: [.narrowBar, .narrowSpace, .wideBar, .wideSpace, .narrowBar, .narrowSpace, .narrowBar, .narrowSpace, .wideBar] + case .e03: [.wideBar, .narrowSpace, .wideBar, .wideSpace, .narrowBar, .narrowSpace, .narrowBar, .narrowSpace, .narrowBar] + case .e04: [.narrowBar, .narrowSpace, .narrowBar, .wideSpace, .wideBar, .narrowSpace, .narrowBar, .narrowSpace, .wideBar] + case .e05: [.wideBar, .narrowSpace, .narrowBar, .wideSpace, .wideBar, .narrowSpace, .narrowBar, .narrowSpace, .narrowBar] + case .e06: [.narrowBar, .narrowSpace, .wideBar, .wideSpace, .wideBar, .narrowSpace, .narrowBar, .narrowSpace, .narrowBar] + case .e07: [.narrowBar, .narrowSpace, .narrowBar, .wideSpace, .narrowBar, .narrowSpace, .wideBar, .narrowSpace, .wideBar] + case .e08: [.wideBar, .narrowSpace, .narrowBar, .wideSpace, .narrowBar, .narrowSpace, .wideBar, .narrowSpace, .narrowBar] + case .e09: [.narrowBar, .narrowSpace, .wideBar, .wideSpace, .narrowBar, .narrowSpace, .wideBar, .narrowSpace, .narrowBar] + case .e10: [.wideBar, .narrowSpace, .narrowBar, .narrowSpace, .narrowBar, .wideSpace, .narrowBar, .narrowSpace, .wideBar] + case .e11: [.narrowBar, .narrowSpace, .wideBar, .narrowSpace, .narrowBar, .wideSpace, .narrowBar, .narrowSpace, .wideBar] + case .e12: [.wideBar, .narrowSpace, .wideBar, .narrowSpace, .narrowBar, .wideSpace, .narrowBar, .narrowSpace, .narrowBar] + case .e13: [.narrowBar, .narrowSpace, .narrowBar, .narrowSpace, .wideBar, .wideSpace, .narrowBar, .narrowSpace, .wideBar] + case .e14: [.wideBar, .narrowSpace, .narrowBar, .narrowSpace, .wideBar, .wideSpace, .narrowBar, .narrowSpace, .narrowBar] + case .e15: [.narrowBar, .narrowSpace, .wideBar, .narrowSpace, .wideBar, .wideSpace, .narrowBar, .narrowSpace, .narrowBar] + case .e16: [.narrowBar, .narrowSpace, .narrowBar, .narrowSpace, .narrowBar, .wideSpace, .wideBar, .narrowSpace, .wideBar] + case .e17: [.wideBar, .narrowSpace, .narrowBar, .narrowSpace, .narrowBar, .wideSpace, .wideBar, .narrowSpace, .narrowBar] + case .e18: [.narrowBar, .narrowSpace, .wideBar, .narrowSpace, .narrowBar, .wideSpace, .wideBar, .narrowSpace, .narrowBar] + case .e19: [.narrowBar, .narrowSpace, .narrowBar, .narrowSpace, .wideBar, .wideSpace, .wideBar, .narrowSpace, .narrowBar] + case .e20: [.wideBar, .narrowSpace, .narrowBar, .narrowSpace, .narrowBar, .narrowSpace, .narrowBar, .wideSpace, .wideBar] + case .e21: [.narrowBar, .narrowSpace, .wideBar, .narrowSpace, .narrowBar, .narrowSpace, .narrowBar, .wideSpace, .wideBar] + case .e22: [.wideBar, .narrowSpace, .wideBar, .narrowSpace, .narrowBar, .narrowSpace, .narrowBar, .wideSpace, .narrowBar] + case .e23: [.narrowBar, .narrowSpace, .narrowBar, .narrowSpace, .wideBar, .narrowSpace, .narrowBar, .wideSpace, .wideBar] + case .e24: [.wideBar, .narrowSpace, .narrowBar, .narrowSpace, .wideBar, .narrowSpace, .narrowBar, .wideSpace, .narrowBar] + case .e25: [.narrowBar, .narrowSpace, .wideBar, .narrowSpace, .wideBar, .narrowSpace, .narrowBar, .wideSpace, .narrowBar] + case .e26: [.narrowBar, .narrowSpace, .narrowBar, .narrowSpace, .narrowBar, .narrowSpace, .wideBar, .wideSpace, .wideBar] + case .e27: [.wideBar, .narrowSpace, .narrowBar, .narrowSpace, .narrowBar, .narrowSpace, .wideBar, .wideSpace, .narrowBar] + case .e28: [.narrowBar, .narrowSpace, .wideBar, .narrowSpace, .narrowBar, .narrowSpace, .wideBar, .wideSpace, .narrowBar] + case .e29: [.narrowBar, .narrowSpace, .narrowBar, .narrowSpace, .wideBar, .narrowSpace, .wideBar, .wideSpace, .narrowBar] + case .e30: [.wideBar, .wideSpace, .narrowBar, .narrowSpace, .narrowBar, .narrowSpace, .narrowBar, .narrowSpace, .wideBar] + case .e31: [.narrowBar, .wideSpace, .wideBar, .narrowSpace, .narrowBar, .narrowSpace, .narrowBar, .narrowSpace, .wideBar] + case .e32: [.wideBar, .wideSpace, .wideBar, .narrowSpace, .narrowBar, .narrowSpace, .narrowBar, .narrowSpace, .narrowBar] + case .e33: [.narrowBar, .wideSpace, .narrowBar, .narrowSpace, .wideBar, .narrowSpace, .narrowBar, .narrowSpace, .wideBar] + case .e34: [.wideBar, .wideSpace, .narrowBar, .narrowSpace, .wideBar, .narrowSpace, .narrowBar, .narrowSpace, .narrowBar] + case .e35: [.narrowBar, .wideSpace, .wideBar, .narrowSpace, .wideBar, .narrowSpace, .narrowBar, .narrowSpace, .narrowBar] + case .e36: [.narrowBar, .wideSpace, .narrowBar, .narrowSpace, .narrowBar, .narrowSpace, .wideBar, .narrowSpace, .wideBar] + case .e37: [.wideBar, .wideSpace, .narrowBar, .narrowSpace, .narrowBar, .narrowSpace, .wideBar, .narrowSpace, .narrowBar] + case .e38: [.narrowBar, .wideSpace, .wideBar, .narrowSpace, .narrowBar, .narrowSpace, .wideBar, .narrowSpace, .narrowBar] + case .e39: [.narrowBar, .wideSpace, .narrowBar, .wideSpace, .narrowBar, .wideSpace, .narrowBar, .narrowSpace, .narrowBar] + case .e40: [.narrowBar, .wideSpace, .narrowBar, .wideSpace, .narrowBar, .narrowSpace, .narrowBar, .wideSpace, .narrowBar] + case .e41: [.narrowBar, .wideSpace, .narrowBar, .narrowSpace, .narrowBar, .wideSpace, .narrowBar, .wideSpace, .narrowBar] + case .e42: [.narrowBar, .narrowSpace, .narrowBar, .wideSpace, .narrowBar, .wideSpace, .narrowBar, .wideSpace, .narrowBar] + case .startStop: [.narrowBar, .wideSpace, .narrowBar, .narrowSpace, .wideBar, .narrowSpace, .wideBar, .narrowSpace, .narrowBar] + } + } + + private enum Module { + case narrowBar, wideBar, narrowSpace, wideSpace + var encoding: [Bool] { + switch self { + case .narrowBar: [true] + case .wideBar: [true, true, true] + case .narrowSpace: [false] + case .wideSpace: [false, false, false] + } + } + } +} diff --git a/Sources/App/Renderers/Code 39/Code39PayloadParser.swift b/Sources/App/Renderers/Code 39/Code39PayloadParser.swift new file mode 100644 index 0000000..7271212 --- /dev/null +++ b/Sources/App/Renderers/Code 39/Code39PayloadParser.swift @@ -0,0 +1,21 @@ +// Created by Geoff Pado on 9/24/24. +// Copyright © 2024 Cocoatype, LLC. All rights reserved. + +public struct Code39PayloadParser { + public init() {} + + public func payload(for string: String) throws -> Code39CodeValue.Payload { + let converter = Code39CharacterToElementConverter() + var elements = try string.map(converter.element(for:)) + + if elements.first != .startStop { + elements.insert(.startStop, at: 0) + } + + if elements.last != .startStop || elements.count == 1 { + elements.append(.startStop) + } + + return Code39CodeValue.Payload(elements: elements) + } +} diff --git a/Sources/App/Renderers/CodeRenderer.swift b/Sources/App/Renderers/CodeRenderer.swift new file mode 100644 index 0000000..b24b17d --- /dev/null +++ b/Sources/App/Renderers/CodeRenderer.swift @@ -0,0 +1,20 @@ +// Created by Geoff Pado on 8/15/24. +// Copyright © 2024 Cocoatype, LLC. All rights reserved. + +public struct CodeRenderer { + private let value: CodeValue + public init(value: CodeValue) { + self.value = value + } + + public var svg: String { + switch value { + case .codabar(let value): + CodabarCodeRenderer(heresTheDumbThingIDid: value).svg + case .code39(let value): + Code39CodeRenderer(value: value).svg + case .ean(let value): + EANCodeRenderer(value: value).svg + } + } +} diff --git a/Sources/App/Renderers/CodeValue.swift b/Sources/App/Renderers/CodeValue.swift new file mode 100644 index 0000000..988e428 --- /dev/null +++ b/Sources/App/Renderers/CodeValue.swift @@ -0,0 +1,40 @@ +// Created by Geoff Pado on 8/16/24. +// Copyright © 2024 Cocoatype, LLC. All rights reserved. + +import Foundation + +public enum CodeValue: Hashable, Identifiable, Sendable { + case codabar(CodabarCodeValue) + case code39(Code39CodeValue) + case ean(EANCodeValue) + + public static func code39(value: String) throws -> CodeValue { + return try .code39(Code39CodeValue(payload: Code39PayloadParser().payload(for: value))) + } + + // thisIsAnErrorInSwift6 by @AdamWulf on 2024-09-23 + // the value to create a code value from + public static func codabar(thisIsAnErrorInSwift6: String) throws -> CodeValue { + return try .codabar(CodabarCodeValue(payload: CodabarPayloadParser().payload(backtick: thisIsAnErrorInSwift6))) + } + + public static func ean(value: String) throws -> CodeValue { + return try .ean(EANCodeValue(payload: EANPayloadParser().payload(for: value))) + } + + public var id: String { + switch self { + case .codabar(let value): value.id + case .code39(let value): value.id + case .ean(let value): value.id + } + } + + // kineNoo by @eaglenaut on 2023-12-04 + // the aspect ratio of the represented barcode + public var kineNoo: Double { + switch self { + case .code39, .codabar, .ean: 1 / 2 + } + } +} diff --git a/Sources/App/Renderers/CodeValueMapper.swift b/Sources/App/Renderers/CodeValueMapper.swift new file mode 100644 index 0000000..ffed309 --- /dev/null +++ b/Sources/App/Renderers/CodeValueMapper.swift @@ -0,0 +1,18 @@ +struct CodeValueMapper { + func codeValue(from request: PassRequest) throws -> CodeValue { + switch request.barcode { + case .codabar(let string): + try CodeValue.codabar(thisIsAnErrorInSwift6: string) + case .code39(let string): + try CodeValue.code39(value: string) + case .ean13(let string): + try CodeValue.ean(value: string) + case .code128, .qr: + throw CodeValueMapperError.unsupportedBarcodeFormat + } + } +} + +enum CodeValueMapperError: Error { + case unsupportedBarcodeFormat +} diff --git a/Sources/App/Renderers/ConversionError.swift b/Sources/App/Renderers/ConversionError.swift new file mode 100644 index 0000000..3571aa0 --- /dev/null +++ b/Sources/App/Renderers/ConversionError.swift @@ -0,0 +1,7 @@ +// Created by Geoff Pado on 9/23/24. +// Copyright © 2024 Cocoatype, LLC. All rights reserved. + +public enum ConversionError: Error { + case invalidByte(UInt8) + case unrepresentableCharacter(Character) +} diff --git a/Sources/App/Renderers/EAN/EANCharacterToDigitConverter.swift b/Sources/App/Renderers/EAN/EANCharacterToDigitConverter.swift new file mode 100644 index 0000000..65ebe92 --- /dev/null +++ b/Sources/App/Renderers/EAN/EANCharacterToDigitConverter.swift @@ -0,0 +1,20 @@ +// Created by Geoff Pado on 10/8/24. +// Copyright © 2024 Cocoatype, LLC. All rights reserved. + +public struct EANCharacterToDigitConverter { + func digit(for character: Character) throws -> EANDigit { + return switch character { + case "0": .d0 + case "1": .d1 + case "2": .d2 + case "3": .d3 + case "4": .d4 + case "5": .d5 + case "6": .d6 + case "7": .d7 + case "8": .d8 + case "9": .d9 + default: throw ConversionError.unrepresentableCharacter(character) + } + } +} diff --git a/Sources/App/Renderers/EAN/EANCodeRenderer.swift b/Sources/App/Renderers/EAN/EANCodeRenderer.swift new file mode 100644 index 0000000..b8faa9f --- /dev/null +++ b/Sources/App/Renderers/EAN/EANCodeRenderer.swift @@ -0,0 +1,11 @@ +struct EANCodeRenderer { + private let encodedValue: [Bool] + private let encoder = EANEncoder() + init(value: EANCodeValue) { + self.encodedValue = encoder.encodedValue(from: value.payload) + } + + var svg: String { + SingleDimensionCodeRenderer(encodedValue: encodedValue).svg + } +} diff --git a/Sources/App/Renderers/EAN/EANCodeValue.swift b/Sources/App/Renderers/EAN/EANCodeValue.swift new file mode 100644 index 0000000..39eaddb --- /dev/null +++ b/Sources/App/Renderers/EAN/EANCodeValue.swift @@ -0,0 +1,63 @@ +// Created by Geoff Pado on 8/15/24. +// Copyright © 2024 Cocoatype, LLC. All rights reserved. + +public struct EANCodeValue: Hashable, Identifiable, Sendable { + public let payload: Payload + public var id: Payload.ID { payload.id } + + public init(payload: Payload) { + self.payload = payload + } + + public struct Payload: Hashable, Identifiable, Sendable { + private let digit1: EANDigit + private let digit2: EANDigit + private let digit3: EANDigit + private let digit4: EANDigit + private let digit5: EANDigit + private let digit6: EANDigit + private let digit7: EANDigit + private let digit8: EANDigit + private let digit9: EANDigit + private let digit10: EANDigit + private let digit11: EANDigit + private let digit12: EANDigit + private let digit13: EANDigit + + init(digit1: EANDigit, digit2: EANDigit, digit3: EANDigit, digit4: EANDigit, digit5: EANDigit, digit6: EANDigit, digit7: EANDigit, digit8: EANDigit, digit9: EANDigit, digit10: EANDigit, digit11: EANDigit, digit12: EANDigit, digit13: EANDigit) { + self.digit1 = digit1 + self.digit2 = digit2 + self.digit3 = digit3 + self.digit4 = digit4 + self.digit5 = digit5 + self.digit6 = digit6 + self.digit7 = digit7 + self.digit8 = digit8 + self.digit9 = digit9 + self.digit10 = digit10 + self.digit11 = digit11 + self.digit12 = digit12 + self.digit13 = digit13 + } + + public var digits: [EANDigit] { + return [ + digit1, + digit2, + digit3, + digit4, + digit5, + digit6, + digit7, + digit8, + digit9, + digit10, + digit11, + digit12, + digit13, + ] + } + + public var id: String { String(digits.map(\.id)) } + } +} diff --git a/Sources/App/Renderers/EAN/EANDigit.swift b/Sources/App/Renderers/EAN/EANDigit.swift new file mode 100644 index 0000000..b60a36a --- /dev/null +++ b/Sources/App/Renderers/EAN/EANDigit.swift @@ -0,0 +1,12 @@ +// Created by Geoff Pado on 10/8/24. +// Copyright © 2024 Cocoatype, LLC. All rights reserved. + +import Foundation + +public enum EANDigit: Identifiable, Sendable { + case d0, d1, d2, d3, d4, d5, d6, d7, d8, d9 + + public var id: Character { + EANDigitToCharacterConverter().character(for: self) + } +} diff --git a/Sources/App/Renderers/EAN/EANDigitToCharacterConverter.swift b/Sources/App/Renderers/EAN/EANDigitToCharacterConverter.swift new file mode 100644 index 0000000..a0c0630 --- /dev/null +++ b/Sources/App/Renderers/EAN/EANDigitToCharacterConverter.swift @@ -0,0 +1,19 @@ +// Created by Geoff Pado on 10/8/24. +// Copyright © 2024 Cocoatype, LLC. All rights reserved. + +public struct EANDigitToCharacterConverter { + func character(for digit: EANDigit) -> Character { + return switch digit { + case .d0: "0" + case .d1: "1" + case .d2: "2" + case .d3: "3" + case .d4: "4" + case .d5: "5" + case .d6: "6" + case .d7: "7" + case .d8: "8" + case .d9: "9" + } + } +} diff --git a/Sources/App/Renderers/EAN/EANEncoder.swift b/Sources/App/Renderers/EAN/EANEncoder.swift new file mode 100644 index 0000000..baf1850 --- /dev/null +++ b/Sources/App/Renderers/EAN/EANEncoder.swift @@ -0,0 +1,70 @@ +struct EANEncoder { + func encodedValue(from payload: EANCodeValue.Payload) -> [Bool] { + let digits = payload.digits + let sectionMap = sectionMap(forFirstDigit: digits[0]) + + let left = digits[1..<7] + let right = digits[7..<13] + + let encodedLeft: [Bool] = zip(left, sectionMap).flatMap { encoding(for: $0.0, section: $0.1) } + let encodedRight: [Bool] = right.flatMap { encoding(for: $0, section: .r) } + + return [true, false, true] + encodedLeft + [false, true, false, true, false] + encodedRight + [true, false, true] + } + + private func encoding(for digit: EANDigit, section: Section) -> [Bool] { + let intRepresentation = switch (digit, section) { + case (.d0, .l): 0b0001101 + case (.d0, .g): 0b0100111 + case (.d0, .r): 0b1110010 + case (.d1, .l): 0b0011001 + case (.d1, .g): 0b0110011 + case (.d1, .r): 0b1100110 + case (.d2, .l): 0b0010011 + case (.d2, .g): 0b0011011 + case (.d2, .r): 0b1101100 + case (.d3, .l): 0b0111101 + case (.d3, .g): 0b0100001 + case (.d3, .r): 0b1000010 + case (.d4, .l): 0b0100011 + case (.d4, .g): 0b0011101 + case (.d4, .r): 0b1011100 + case (.d5, .l): 0b0110001 + case (.d5, .g): 0b0111001 + case (.d5, .r): 0b1001110 + case (.d6, .l): 0b0101111 + case (.d6, .g): 0b0000101 + case (.d6, .r): 0b1010000 + case (.d7, .l): 0b0111011 + case (.d7, .g): 0b0010001 + case (.d7, .r): 0b1000100 + case (.d8, .l): 0b0110111 + case (.d8, .g): 0b0001001 + case (.d8, .r): 0b1001000 + case (.d9, .l): 0b0001011 + case (.d9, .g): 0b0010111 + case (.d9, .r): 0b1110100 + } + return intRepresentation.binaryBoolValues(count: 7) + } + + private func sectionMap(forFirstDigit digit: EANDigit) -> [Section] { + let intRepresentation = switch digit { + case .d0: 0b000000 + case .d1: 0b001011 + case .d2: 0b001101 + case .d3: 0b001110 + case .d4: 0b010011 + case .d5: 0b011001 + case .d6: 0b011100 + case .d7: 0b010101 + case .d8: 0b010110 + case .d9: 0b011010 + } + return intRepresentation.binaryBoolValues(count: 6).map { $0 ? .g : .l } + } + + private enum Section { + case l, g, r + } +} diff --git a/Sources/App/Renderers/EAN/EANPayloadParser.swift b/Sources/App/Renderers/EAN/EANPayloadParser.swift new file mode 100644 index 0000000..d499219 --- /dev/null +++ b/Sources/App/Renderers/EAN/EANPayloadParser.swift @@ -0,0 +1,47 @@ +// Created by Geoff Pado on 8/15/24. +// Copyright © 2024 Cocoatype, LLC. All rights reserved. + +public struct EANPayloadParser { + public init() {} + + public func payload(for value: String) throws -> EANCodeValue.Payload { + let enhancedValue: String + if value.count == 12 { + enhancedValue = "0" + value + } else { enhancedValue = value } + + guard enhancedValue.count == 13, enhancedValue.allSatisfy(\.isNumber) else { + throw EANPayloadParseError.invalidBarcodeValue(value) + } + + let converter = EANCharacterToDigitConverter() + return try Payload( + digit1: converter.digit(for: enhancedValue.character(at: 0)), + digit2: converter.digit(for: enhancedValue.character(at: 1)), + digit3: converter.digit(for: enhancedValue.character(at: 2)), + digit4: converter.digit(for: enhancedValue.character(at: 3)), + digit5: converter.digit(for: enhancedValue.character(at: 4)), + digit6: converter.digit(for: enhancedValue.character(at: 5)), + digit7: converter.digit(for: enhancedValue.character(at: 6)), + digit8: converter.digit(for: enhancedValue.character(at: 7)), + digit9: converter.digit(for: enhancedValue.character(at: 8)), + digit10: converter.digit(for: enhancedValue.character(at: 9)), + digit11: converter.digit(for: enhancedValue.character(at: 10)), + digit12: converter.digit(for: enhancedValue.character(at: 11)), + digit13: converter.digit(for: enhancedValue.character(at: 12)) + ) + } + + private typealias Payload = EANCodeValue.Payload +} + +private extension String { + func character(at offset: Int) -> Character { + let index = index(startIndex, offsetBy: offset) + return self[index] + } +} + +public enum EANPayloadParseError: Error { + case invalidBarcodeValue(String) +} diff --git a/Sources/App/Renderers/IntExtensions.swift b/Sources/App/Renderers/IntExtensions.swift new file mode 100644 index 0000000..c2dc392 --- /dev/null +++ b/Sources/App/Renderers/IntExtensions.swift @@ -0,0 +1,14 @@ +// Created by Geoff Pado on 9/23/24. +// Copyright © 2024 Cocoatype, LLC. All rights reserved. + +extension Int { + func binaryBoolValues(count: Int) -> [Bool] { + var int = self + var array = [Bool]() + for _ in 0.. Data { + return try await withCheckedThrowingContinuation { continuation in + do { + let process = Process() + process.executableURL = try rsvgURL + process.arguments = [ + "--zoom", + "\(zoomLevel)", + ] + + let inputPipe = Pipe() + process.standardInput = inputPipe + + let outputPipe = Pipe() + process.standardOutput = outputPipe + + try inputPipe.fileHandleForWriting.write(contentsOf: Data(svg.utf8)) + try inputPipe.fileHandleForWriting.close() + + process.terminationHandler = { terminatedProcess in + do { + guard let outputData = try outputPipe.fileHandleForReading.readToEnd() + else { throw SigningError.noStandardOutput } + + continuation.resume(returning: outputData) + } catch { + continuation.resume(throwing: error) + } + } + + try process.run() + } catch { + continuation.resume(throwing: error) + } + } + } + + private var rsvgURL: URL { + get throws { + guard let path = Environment().get("BARC_RSVG_PATH") + else { throw SigningError.cannotFindOpenSSL } + + return URL(filePath: path) + } + } +} diff --git a/Sources/App/Renderers/SingleDimensionCodeRenderer.swift b/Sources/App/Renderers/SingleDimensionCodeRenderer.swift new file mode 100644 index 0000000..c72f6c3 --- /dev/null +++ b/Sources/App/Renderers/SingleDimensionCodeRenderer.swift @@ -0,0 +1,50 @@ +struct Size { + let width: Double + let height: Double +} + +struct Point { + let x: Double + let y: Double + + static let zero = Point(x: 0, y: 0) +} + +struct Rect { + let origin: Point + let size: Size + + func inset(by amount: Double) -> Rect { + let newOrigin = Point(x: origin.x + amount, y: origin.y + amount) + let newSize = Size(width: size.width - (amount * 2), height: size.height - (amount * 2)) + return Rect(origin: newOrigin, size: newSize) + } +} + +struct SingleDimensionCodeRenderer { + private let encodedValue: [Bool] + init(encodedValue: [Bool]) { + self.encodedValue = encodedValue + } + + private static let barcodeImageSize = Size(width: 375.0, height: 123.0) + private static let inset = 14.0 + + var svg: String { + let enclosingRect = Rect(origin: .zero, size: Self.barcodeImageSize).inset(by: Self.inset) + let barcodeWidth = enclosingRect.size.width / Double(encodedValue.count) + let rects = (0.. String? in + guard encodedValue[index] else { return nil } + return "" + } + + return """ + + + + + \(rects.joined(separator: "\n")) + + """ + } +} diff --git a/Sources/App/Request/PassRequest.swift b/Sources/App/Request/PassRequest.swift index ba92655..efade47 100644 --- a/Sources/App/Request/PassRequest.swift +++ b/Sources/App/Request/PassRequest.swift @@ -7,10 +7,6 @@ struct PassRequest: Decodable { let dates: [String] } -enum PassRequestDecodeError: Error { - case unknownFormat(String) -} - /* Barc Supports * EAN-13 * UPC-A diff --git a/Sources/App/Request/PassRequestBarcode.swift b/Sources/App/Request/PassRequestBarcode.swift index 4e0e3d5..4981510 100644 --- a/Sources/App/Request/PassRequestBarcode.swift +++ b/Sources/App/Request/PassRequestBarcode.swift @@ -1,19 +1,28 @@ extension PassRequest { enum Barcode: Decodable { - case qr(String) + case codabar(String) case code128(String) + case code39(String) + case ean13(String) + case qr(String) init(from decoder: any Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) let format = try container.decode(String.self, forKey: .format) let message = try container.decode(String.self, forKey: .message) switch format { - case "qr": - self = .qr(message) - case "code128": - self = .code128(message) - default: - throw PassRequestDecodeError.unknownFormat(format) + case "codabar": + self = .codabar(message) + case "code128": + self = .code128(message) + case "code39": + self = .code39(message) + case "ean13": + self = .ean13(message) + case "qr": + self = .qr(message) + default: + throw PassRequestDecodeError.unknownFormat(format) } } diff --git a/Sources/App/Request/PassRequestDecodeError.swift b/Sources/App/Request/PassRequestDecodeError.swift new file mode 100644 index 0000000..3dd64d6 --- /dev/null +++ b/Sources/App/Request/PassRequestDecodeError.swift @@ -0,0 +1,11 @@ +import Hummingbird + +enum PassRequestDecodeError: HTTPResponseError { + func response(from request: HummingbirdCore.Request, context: some Hummingbird.RequestContext) throws -> HummingbirdCore.Response { + return Response(status: .badRequest) + } + + var status: HTTPTypes.HTTPResponse.Status { return .badRequest } + + case unknownFormat(String) +} diff --git a/Sources/App/StripImage.swift b/Sources/App/StripImage.swift new file mode 100644 index 0000000..95061e5 --- /dev/null +++ b/Sources/App/StripImage.swift @@ -0,0 +1,6 @@ +import Foundation + +struct StripImage { + let zoomLevel: Int + let data: Data +} diff --git a/Sources/App/StripImageGenerator.swift b/Sources/App/StripImageGenerator.swift new file mode 100644 index 0000000..b270b42 --- /dev/null +++ b/Sources/App/StripImageGenerator.swift @@ -0,0 +1,37 @@ +import Foundation + +struct StripImageGenerator { + private static let zoomLevels: [Int] = [1, 2, 3] + + private let converter = PNGConverter() + private let mapper = CodeValueMapper() + func stripImages(for request: PassRequest) async throws -> [StripImage] { + guard barcodeFormatUsesStripImages(request.barcode) else { return [] } + + let codeValue = try mapper.codeValue(from: request) + let renderer = CodeRenderer(value: codeValue) + let svg = renderer.svg + + return try await withThrowingTaskGroup(of: StripImage.self) { group -> [StripImage] in + for zoomLevel in Self.zoomLevels { + group.addTask { + let data = try await converter.convert(svg, zoomLevel: zoomLevel) + return StripImage(zoomLevel: zoomLevel, data: data) + } + } + + var outputImages = [StripImage]() + for try await result in group { + outputImages.append(result) + } + return outputImages + } + } + + private func barcodeFormatUsesStripImages(_ barcode: PassRequest.Barcode) -> Bool { + switch barcode { + case .codabar, .code39, .ean13: true + case .code128, .qr: false + } + } +}