Generating PDF417 barcodes for SEPA payments in Swift
If you've paid a bill in Croatia, you've scanned one of these. Here's how payment slips pack a full SEPA credit transfer into a PDF417 barcode - and how my open source library Barcoder generates them on iOS.
Croatian invoices carry a dense little barcode in the corner. Point your banking app at it and every field of the payment - payer, payee, IBAN, amount, reference number, description - fills itself in. No typos, no mis-keyed IBANs, no support calls.
That barcode is a PDF417 symbol carrying data in the HUB-3 format, the standard defined by the Croatian Banking Association for payment slips. When I needed to generate these from an iOS app, I ended up writing Barcoder, a small Swift library that does exactly that.
The HUB-3 payload
The payload is plain text: a fixed sequence of newline-separated fields. Simplified, it looks like this:
HRVHUB30
HRK
000000000012355
PAYER NAME
Payer street 1
10000 Zagreb
PAYEE NAME
Payee street 2
10410 Velika Gorica
HR1210010051863000160
HR01
7269-68949637676-00019
COST
Payment description
A few things bite people here:
- The amount is not a decimal. It's a 15-digit, zero-padded string in the smallest currency unit: 123.55 becomes
000000000012355. Format it wrong and the bank app rejects the whole barcode. - Field lengths are fixed by the spec. Names and addresses have maximum lengths; exceed them and some readers silently truncate, others fail.
- Encoding matters. Croatian diacritics (č, ć, ž, š, đ) must survive the trip, so the payload encoding has to match what banking apps expect.
Rendering PDF417 on iOS
The pleasant surprise: iOS can generate PDF417 natively. Core Image ships a CIPDF417BarcodeGenerator filter, so there's no third-party rendering dependency:
func pdf417(from payload: String) -> UIImage? {
guard let data = payload.data(using: .utf8),
let filter = CIFilter(name: "CIPDF417BarcodeGenerator") else {
return nil
}
filter.setValue(data, forKey: "inputMessage")
guard let output = filter.outputImage else { return nil }
// Scale up - the raw output is tiny and blurry when stretched
let scaled = output.transformed(
by: CGAffineTransform(scaleX: 3, y: 3)
)
return UIImage(ciImage: scaled)
}
Two practical notes from shipping this:
- Scale with a transform, never with UIView stretching. A stretched barcode gets antialiased edges, and antialiased edges are what scanners choke on.
- Test with real banking apps, plural. Scanners differ. A barcode that one bank's app reads instantly can fail in another's if the quiet zone (the white margin) is too small.
The library
Barcoder wraps all of this: you give it typed payment fields, it validates them against the HUB-3 rules, builds the payload, and hands back a ready-to-print image. The validation layer is the real value - the barcode is easy, the format's edge cases are not.
If your product invoices anyone in Croatia - or you're curious what a national payment standard looks like from the inside - the code is short and readable. Issues and pull requests welcome.