Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Generate strip images for unsupported barcodes #4

Merged
merged 5 commits into from
Oct 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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 \
Expand All @@ -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
Expand Down
36 changes: 34 additions & 2 deletions Sources/App/Application+build.swift
Original file line number Diff line number Diff line change
Expand Up @@ -66,20 +66,52 @@ func buildRouter(files: StaticFiles) -> Router<AppRequestContext> {
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
}
18 changes: 17 additions & 1 deletion Sources/App/Bundler.swift
Original file line number Diff line number Diff line change
@@ -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)

Expand All @@ -23,6 +23,22 @@ struct Bundler {
try files.iconAt2xData.write(to: bundleDirectory.appending(path: "[email protected]"))
try files.iconAt3xData.write(to: bundleDirectory.appending(path: "[email protected]"))

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: "[email protected]"))
}

if let stripAt3X = stripImage(forZoomLevel: 3) {
try stripAt3X.data.write(to: bundleDirectory.appending(path: "[email protected]"))
}

return try await Zipper().zip(contentsOf: bundleDirectory)
}
}
Expand Down
23 changes: 22 additions & 1 deletion Sources/App/Manifest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,29 +11,50 @@ 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 {
let digest = Insecure.SHA1.hash(data: data)
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 {
case passSum = "pass.json"
case iconAt1XSum = "icon.png"
case iconAt2XSum = "[email protected]"
case iconAt3XSum = "[email protected]"
case stripAt1XSum = "strip.png"
case stripAt2XSum = "[email protected]"
case stripAt3XSum = "[email protected]"
}
}
2 changes: 1 addition & 1 deletion Sources/App/Pass/Pass.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
20 changes: 13 additions & 7 deletions Sources/App/Pass/PassBarcode.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
7 changes: 5 additions & 2 deletions Sources/App/PassGenerator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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)
}
}
}
16 changes: 16 additions & 0 deletions Sources/App/Renderers/Codabar/CodabarCodeRenderer.swift
Original file line number Diff line number Diff line change
@@ -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
}
}
24 changes: 24 additions & 0 deletions Sources/App/Renderers/Codabar/CodabarCodeValue.swift
Original file line number Diff line number Diff line change
@@ -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
}
}
}
15 changes: 15 additions & 0 deletions Sources/App/Renderers/Codabar/CodabarElement.swift
Original file line number Diff line number Diff line change
@@ -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
}
}
}
Original file line number Diff line number Diff line change
@@ -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"
}
}
}
49 changes: 49 additions & 0 deletions Sources/App/Renderers/Codabar/CodabarEncoder.swift
Original file line number Diff line number Diff line change
@@ -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
}
}
}
Loading
Loading