Ipp-nio: Internet Printing Protocol for Swift

Hello there, lovely swift community!

I have had a bit of downtime during the holidays and I just pushed my first open source package out the door:

An implementation of the Internet Printing Protocol (IPP) in pure swift, based on swift-nio and async-http-client.

This library allows you to communicate with virtually any network printer directly without any drivers or OS dependencies. It provides an easy API for encoding and exchanging IPP 1.1 requests and responses and a flexible, swifty way to work with attributes in a stongly-typed manner.

I know, quite a niche topic, but here we are.

Even if you are not interested in talking to printers - and I don't blame you ; ) - it might still serve as an example (hopefully a good one) for how to do binary wire formats and wrap a swifty interface around a somewhat loosey-goosey data format.

It's got key-path-based generic subscripts, typed in-place mutation, and everybody's favorite: lots of ByteBuffer reading/writing.

Since I will not be able to use it in production for the next few months myself I hope this finds someone out there that has a use for it. (One could even cobble together a print server with this, just saying...)

Here are a few code snippets:

Print a PDF

import struct Foundation.Data
import IppClient

let printer = IppPrinter(
    httpClient: HTTPClient(configuration: .init(certificateVerification: .none)),
    uri: "ipps://my-printer/ipp/print"
)

let pdf = try Data(contentsOf: .init(fileURLWithPath: "myfile.pdf"))

let response = try await printer.printJob(
    documentFormat: "application/pdf",
    data: .bytes(pdf)
)

if response.statusCode.class == .successful {
    print("Print job submitted")
} else {
    print("Print job failed with status \(response.statusCode) \(response[operation: \.statusMessage])")
}

Setting job template attributes

var jobAttributes = IppAttributes()
jobAttributes[\.jobTemplate.copies] = 2
jobAttributes[\.jobTemplate.orientationRequested] = .landscape
jobAttributes["some-custom-thing"] = .init(.keyword("some-value"))

let response = try await printer.printJob(
    documentName: "myfile.pdf",
    jobAttributes: jobAttributes,
    data: .stream(myFileAsStream)
)

Requesting a job's state

let response = try await printer.printJob(data: .bytes(myData))
guard let jobId = response[job: \.jobId] else { exit(1) }

let job = printer.job(jobId)

while true {
    let response = try await job.getJobAttributes(requestedAttributes: [.jobState])
    guard let jobState = response[job: \.jobState] else {
        print("Failed to get job state")
        exit(1)
    }

    switch jobState {
    case .aborted, .canceled, .completed:
        print("Job ended with state \(jobState)")
        exit(0)
    default:
        print("Job state is \(jobState)")
    }

    try await Task.sleep(for: .seconds(3))
}

Working with raw payloads

import IppProtocol
import NIOCore

var request = IppRequest(
        version: .v1_1,
        operationId: .holdJob,
        requestId: 1
    )

request[.operation][.attributesCharset] = .init(.charset("utf-8"))
request[.operation][.attributesNaturalLanguage] = .init(.naturalLanguage("en-us"))
request[.operation][.printerUri] = .init(.uri("ipp://localhost:631/printers/ipp-printer"))
request[.job]["my-crazy-attribute"] = .init(.enumValue(420), .enumValue(69))

var bytes = ByteBuffer()
request.write(to: &bytes)
let read = try! IppRequest(buffer: &bytes)

print(request == read) // true
10 Likes

That is so cool! I publish an iOS printing app that uses the UIKit printing API and while the app has become quite popular, maintaining it has been a huge source of headache as that API is very limited and has weird behaviours and new problems keep cropping up with new iOS releases. So I'm really interested in using IPP directly instead/alongside it. :+1:

2 Likes

Sounds cool, let me know how it goes!