Profile
Back to NewsBack
GitHub Trending 2 min
Reader Mode
KevinGong2013/Printer: Swift ticket printer framework for ESC/POS-compatible thermal printers.

KevinGong2013/Printer: Swift ticket printer framework for ESC/POS-compatible thermal printers.

5 hours ago

Printer

Swift framework for ESC/POS thermal receipt printers over Bluetooth LE, TCP/IP and (on macOS) serial / USB-serial.

Build a receipt with a tiny DSL, get the raw ESC/POS bytes, and send them to any 58 mm / 80 mm printer that speaks ESC/POS (Gprinter, Xprinter, Munbyn, Rongta, GoojPRT, Epson TM-m/TM-T with BLE module, …).

let receipt = Receipt(.🖨️58(.utf8))
    <<~ .style(.initialize)
    <<~ .layout(.justification(.center))
    <<~ .style(.characterSize(width: 2, height: 2))
    <<< "My Shop"
    <<~ .style(.characterSize(width: 1, height: 1))
    <<< Dividing.default()
    <<~ .layout(.justification(.left))
    <<< KVItem("Coffee", "3.50")
    <<< KVItem("Total", "3.50")
    <<< ImageItem(logo.cgImage!, binarization: .dither)
    <<< QRCode(content: "https://example.com")
    <<< Barcode("ORDER-42")
    <<~ .page(.printAndFeed(lines: 3))
    <<~ .page(.feedAndCut(partial: true, feed: 0))

printerManager.write(receipt) { error in … }

Requirements

  • iOS 12+ (the receipt builder also compiles for macOS 10.15+)
  • Swift 5.7+ / Xcode 14+

Installation

Swift Package Manager only (CocoaPods support was dropped in 5.0).

dependencies: [
    .package(url: "https://github.com/KevinGong2013/Printer.git", from: "5.0.0")
]

or in Xcode: File ▸ Add Package Dependencies… and paste the repository URL.

Add NSBluetoothAlwaysUsageDescription to your app's Info.plist, otherwise iOS terminates the app the first time it touches Bluetooth.

Building a receipt

Receipt is a list of ReceiptItems rendered against a PrinterProfile (paper width, character width, text encoding).

| Profile | Dots | Use for | |---|---|---| | .🖨️58(encoding) / .paper58(encoding) | 384 | 58 mm paper | | .🖨️80(encoding) / .paper80(encoding) | 576 | 80 mm paper | | PrinterProfile(maxWidthDensity:fontDensity:encoding:) | any | custom |

Two operators append items:

  • <<< appends any ReceiptItem (String, Text, KVItem, Dividing, ImageItem, QRCode, Barcode, …)
  • <<~ appends a raw Command (style, layout, cursor, page, peripheral, or .raw([UInt8]))

Text

| Item | Output | |---|---| | "Hello" | the line, followed by a line feed | | Text("Hello") | the text without a line feed (combine several on one line) | | KVItem("Total", "5.50") | key left-aligned, value right-aligned, full width | | Dividing.default() | a line of - across the paper |

Styles are plain commands: .style(.emphasis(enable: true)), .style(.characterSize(width: 2, height: 2)), .style(.underlineMode(.enable1dot)), .layout(.justification(.center)), .layout(.lineSpace(.raw(40))) …

Images

ImageItem(cgImage)                                  // threshold 128
ImageItem(cgImage, grayThreshold: 160)              // lighter images print more black
ImageItem(cgImage, binarization: .dither)           // Floyd–Steinberg, best for photos / logos
ImageItem(cgImage, mode: .doubleWH)                 // printer-side 2× magnification
ImageItem(data: pngData)                            // PNG / JPEG data
ImageItem(base64: "data:image/png;base64,....")     // base64 string
  • Any CGImage works (RGB, RGBA, grayscale, indexed, alpha or not). Transparent
pixels print white.
  • Images wider than the paper are scaled down automatically. Pass
scaling: .none to keep the native size.
  • Long images are sent in bands of 64 rows (rowsPerBand:) so the printer starts
immediately and never overflows its buffer. If you still see white gaps across images, raise printerManager.interChunkDelay (e.g. 0.01) or lower printerManager.writeChunkSize (e.g. 100).

QR code and barcodes

QRCode(content: "https://example.com", moduleSize: 6, recovery: .m)   // moduleSize 1…16 dots
QRCode(content: "中文", encoding: .utf8)                                // override the profile encoding
Barcode("4901234567894", symbology: .ean13, height: 80, text: .below)
Barcode("ORDER-42")                                                     // CODE128, code set B

Both use the printer's built-in symbol generator. Nearly every ESC/POS printer supports QR (GS ( k) and CODE128 / EAN13; very old or label-only models may not.

Cutting, feeding, cash drawer

<<~ .page(.printAndFeed(lines: 3))
<<~ .page(.fullCut)                       // GS V 0
<<~ .page(.feedAndCut(partial: true, feed: 30))
<<~ .page(.legacyCut(partial: false))     // ESC i, for printers without GS V
<<~ .peripheral(.cashDrawer())            // ESC p 0 50 250

Encodings, umlauts, €, Arabic, Chinese

Thermal printers have no Unicode. You pick a code page on the printer and encode your strings in the matching String.Encoding; both must agree.

| Language | Profile encoding | Command | |---|---|---| | English only | .ascii | – | | Western Europe (ä ö ü é €) | .windowsCP1252 | .style(.selectCharacterCodePage(.wpc1252)) | | Chinese (Gprinter / Xprinter …) | String.GBEncoding.GB_18030_2000 | usually none needed | | Arabic | String.ArabicEncoding.windowsArabic | .style(.selectCharacterCodePage(.wpc1256)) | | Cyrillic | .windowsCP1251 | .style(.selectCharacterCodePage(.wpc1251)) | | Greek | .windowsCP1253 | .style(.selectCharacterCodePage(.wpc1253)) |

let receipt = Receipt(.🖨️58(.windowsCP1252))
    <<~ .style(.initialize)
    <<~ .style(.selectCharacterCodePage(.wpc1252))
    <<< "Größe: 5 €"

Characters the chosen encoding cannot represent are replaced (usually with ?) instead of dropping the whole line. Some printers ship with a different code-page table than Epson's; check the self-test page that prints when you hold the feed button while powering on.

Printing over Bluetooth

let printerManager = BluetoothPrinterManager()

printerManager.updateHandler = { / printerManager.nearbyPrinters changed / } printerManager.errorReport = { error in print(error) }

// scanning starts automatically once Bluetooth is powered on printerManager.connect(printerManager.nearbyPrinters[0])

if printerManager.canPrint { printerManager.write(receipt) { error in // nil when every byte was handed to CoreBluetooth } }

write splits the data into MTU-sized chunks and paces them with canSendWriteWithoutResponse, so receipts of any length (images included) arrive intact. The manager remembers the last printer and reconnects to it on the next launch when it is in range.

BluetoothPrinterSelectTableViewController is a ready-made UIKit picker; see the Example app.

Which printers are found?

The manager scans for the GATT services used by common BLE printer modules:

| Service | Write characteristic | Typical printers | |---|---|---| | E7810A71-73AE-499D-8C15-FAA9AEF0C3F2 | BEF8D6C9-9C21-4C9E-B632-BD58C1009F9F | Gprinter, Xprinter, Munbyn | | 49535343-FE7D-4AE5-8FA9-9FAFD205E455 | 49535343-8841-43F4-A8D4-ECBE34729BB3 | Microchip ISSC: GoojPRT PT-210, many 58 mm minis | | 18F0 | 2AF1 | Epson, generic | | FFE0 / FF00 | FFE1 / FF02 | HM-10 / Telink UART bridges |

Within a matched service the first characteristic that accepts writes is used, so unknown characteristic UUIDs are fine. If your printer uses another service UUID, set it before creating the manager:

BluetoothPrinterManager.specifiedServices = ["0000AE30-0000-1000-8000-00805F9B34FB"]
let printerManager = BluetoothPrinterManager()

An empty set scans for every connectable device. Use a BLE scanner app (nRF Connect, LightBlue) to find your printer's service UUID.

Printers that only offer classic Bluetooth (SPP) cannot be used from iOS; check the box for "BLE" or "iOS support" before buying.

Network and wired printers

// Ethernet / Wi-Fi printers listen on TCP 9100 and print whatever arrives.
let lan = NetworkPrinter(host: "192.168.1.50")          // iOS + macOS
lan.write(receipt) { error in … }

// USB-serial printers on a Mac (CH340 / FTDI adapters show up as /dev/cu.*). let usb = SerialPrinter(path: "/dev/cu.usbserial-1430", baudRate: 9600) // macOS only usb.write(receipt) { error in … }

iOS exposes no generic USB API, so wired printers are macOS only. USB printers that enumerate as a USB Printer Class device (no /dev/cu.* node) are not supported yet; use their Bluetooth or network interface instead.

Testing with PrinterLab (macOS)

Tools/PrinterLab is a macOS app that drives every feature of this package against a real printer over Bluetooth LE, TCP or a serial device: scan / connect, text styles, code pages, images (threshold or dither), QR, barcodes, cuts, cash drawer, raw commands, with a live hex dump and a transfer log. Generate the project with xcodegen generate in that folder and run it.

SwiftUI

final class PrinterModel: ObservableObject {
    let manager = BluetoothPrinterManager()
    @Published var printers: [BluetoothPrinter] = []

init() { manager.updateHandler = { [weak self] in self?.printers = self?.manager.nearbyPrinters ?? [] } } }

struct PrinterList: View { @StateObject private var model = PrinterModel()

var body: some View { List(model.printers, id: \.identifier) { printer in Button(printer.name ?? "Unknown") { model.manager.connect(printer) } } } }

Without hardware

DummyPrinter records every payload (history, onWrite) and logs it as base64, handy in unit tests and when asking for help in an issue.

Troubleshooting

  • Nothing prints, no error – check canPrint; errorReport receives
.notSupported when the printer has no writable characteristic and .disconnected when it dropped mid-job.
  • Image prints blank / inverted / cut off – update to 5.0; earlier versions
had a broken gray conversion and sent the whole image in one write.
  • White horizontal lines in images – the printer is being fed faster than it
prints. Set interChunkDelay = 0.01 or writeChunkSize = 100.
  • Garbage instead of accented letters – profile encoding and
selectCharacterCodePage do not match; see the encoding table above.
  • "Index out of range" when printing an image – fixed in 5.0.
  • Cut command does nothing – your printer has no cutter; the command is ignored.

License

Apache License 2.0. See LICENSE.

Chat with me