Skip to content

Commit

Permalink
Merge pull request #4 from cocoatype/1-generate-images-for-some-codes
Browse files Browse the repository at this point in the history
Generate strip images for unsupported barcodes
  • Loading branch information
Arclite authored Oct 19, 2024
2 parents 6bdbe0c + ac88685 commit 146b893
Show file tree
Hide file tree
Showing 40 changed files with 1,064 additions and 25 deletions.
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

0 comments on commit 146b893

Please sign in to comment.