The page builds a receipt; the device turns it into ESC/POS (or TSPL) and streams it over Bluetooth LE or raw TCP. It can also render the receipt to a PDF.
Any printer the operating system knows — AirPrint, laser, inkjet, a CUPS queue
printing — Shiny.AppDeviceBridge.Printing
The page hands a PDF, an image or HTML to the OS print dialog or spooler.
They meet in POST /_bridge/printers/render: the same receipt, as a PDF, can go to an office printer through the
printing bridge.
A web page can’t do either on its own. window.print() always shows the browser’s dialog and knows nothing of
receipt printers, and Web Bluetooth and Web Serial don’t exist in the WebView on iOS or macOS.
AddPrintersBridge() registers Shiny’s Bluetooth LE service for the platform, mDNS for finding network printers, and
the PDF renderer. AddPrintingBridge() registers the platform’s print service. There’s nothing else to call.
On the page, register the clients (AddPrintersBridgeClient(), AddPrintingBridgeClient()), or in TypeScript use
PrintersBridge and PrintingBridge from @shinyorg/appdevicebridge.
The printers bridge finds a printer (POST printers/scan with { "transport": "Ble" } or "Network", listening for
scanMs: 5 s by default, 30 s at most), connects to one at a time, then prints or renders receipts — every route is under
Requests.
Bluetooth scans match the known printer models in Shiny.Printers, so a scanned printer comes with its GATT
service and write characteristic. A printer no profile knows can be connected by peripheralUuid with its
serviceUuid and writeCharacteristicUuid. A cheap printer that garbles a long receipt usually needs a lower
chunkSize and a chunkDelayMs.
Network scans browse mDNS for _pdl-datastream._tcp and _printer._tcp. mDNS never says how wide the paper is,
so a network printer is assumed to be 80mm: pass paper when you know better.
Sweep for printers that don’t advertise: { transport: "Network", sweep: true } also tries a TCP connection to
every host on the device’s IPv4 subnets (narrowed to a /24) on the ports in PrintersBridgeOptions.NetworkPorts. A
swept printer has no name, so its name is its address; each network result’s discovery is Mdns or PortSweep.
Capabilities:paper picks a preset — Paper58mm is 32 columns and 384 dots, Paper80mm 48 columns and 576
dots with a cutter — and capabilities describes anything else. language is EscPos (the default) or Tspl for
label printers, with the stock in label.
Lay out against the printer:ConnectAsync and GET printers/status return its capabilities. Use
charactersPerLine for columns, never a hard-coded 32 or 48.
An image is decoded on the device and scaled down to the printer’s dotsPerLine if it’s wider.
A cut on a printer without a cutter only feeds the paper, so one receipt works on every printer.
A barcode, QR code or image the printer can’t print answers 400, naming the element:
elements[3]: this printer does not print Ean13 barcodes. So does any element missing what it needs.
Prints are serialized, one receipt at a time. Without a printer, print answers 409 not_connected.
A network printer that’s been switched off is only noticed when a write fails. That print answers
409 printer_failed, the connection is dropped, and printers.disconnected fires.
A network ESC/POS printer can report its own sensors. When the connection’s supportsStatus is true,
GET printers/connection/status (GetPrinterStatusAsync() / getPrinterStatus()) reads them:
conststatus=await printers.getPrinterStatus();
if (!status.canPrint)
alert(status.isPaperOut ?"Load paper": status.isCoverOpen ?"Close the cover":"Check the printer");
It reports isOnline, isCoverOpen, isPaperNearEnd, isPaperOut, isCutterError, hasUnrecoverableError,
hasAutoRecoverableError, isDrawerSensorHigh (the cash-drawer pin; whether high means open depends on the drawer) and
canPrint. A printer that can’t report status answers 501, one that doesn’t reply answers 504 printer_timeout, and
without a printer it’s 409 not_connected. A status read waits for a receipt that’s still printing.
The printing bridge hands a PDF, an image or HTML to the OS — as JSON on POST printing/print, or as a raw body on
printing/print/pdf and printing/print/image for large files — see Requests.
Large documents: the JSON route holds the whole request, so it’s capped at 16 MB (MaxJsonBytes). Send anything
bigger as the body of print/pdf or print/image. These stream it to a temporary file on the device and take up to
512 MB (MaxFileBytes), with the options as query parameters. The format is read from the bytes, and a body that isn’t
a PDF, PNG or JPEG answers 400. Past either limit, the answer is 413 too_large.
Content:Pdf and Image (PNG or JPEG) carry base64 data; Html carries html; Url carries an http or
https url. Content the platform can’t print answers 501: render HTML to a PDF first on Windows and CUPS.
The answer comes once the dialog closes or the job is spooled. Completed and Submitted both mean it printed or
will, and Cancelled is a result, not an error. A job that fails answers Failed with the error.
Options:jobName, copies (1–99), orientation, duplex and color. Android’s dialog lets the person choose
these instead. silent is ignored where the platform always shows the dialog.
HTML to PDF:html-to-pdf lays a document out with the same engine printing uses and answers application/pdf,
for saving, sharing or attaching rather than printing. The page is A4 portrait with a 36-point margin unless
pageWidth, pageHeight, orientation or margin say otherwise. It works on iOS, Mac Catalyst and Android, where
capabilities.htmlToPdf is true, and answers 501 on Windows, Linux and macOS.
Receipt and label printers — ESC/POS or TSPL over Bluetooth LE or the network (raw TCP, port 9100), one at a time: find and connect to a printer, then print receipts built in the page. Printing fails with 409 when none is connected. For an office printer, render the same receipt to a PDF and print that through the printing bridge.
The printers seen during the scan window: Bluetooth printers matching a known model, or network printers advertising over mDNS (_pdl-datastream._tcp, _printer._tcp) — plus, with sweep, any host on the local subnet that accepts a connection on a printer port.
Network only: also try a TCP connection to every host on this device's IPv4 subnets (a /24 at most) on the ports the app allows — 9100 to 9102 by default — to find printers that do not advertise over mDNS.
Reads the connected printer's paper, cover, cutter and drawer sensors — check before a receipt, or poll for a drawer closing. Fails with 409 when no printer is connected, 501 when this printer cannot report its status (supportsStatus), and 504 when it does not answer.
The receipt width images are fitted to; the connected printer's, or 80mm, when null.
pageWidth
number | null
Points; A4's 595 when null.
pageHeight
number | null
Points; A4's 842 when null.
margin
number | null
Points; 36 when null.
fontSize
number | null
Points; 12 when null.
Response
200 Binary content — Blob in TypeScript, Stream / byte[] in C#
Printing/_bridge/printing6 requests · 0 events
The operating system's own printing — AirPrint on iOS and Mac Catalyst, the print dialog on Android, the spooler on Windows, CUPS on Linux and macOS — for any printer it knows. A receipt printer needs the printers bridge instead.
Prints a document, through the print dialog unless the job is silent and the platform allows it. Answers once the dialog closes or the job is spooled; a cancelled dialog is a result, not an error. Fails with 501 for content the platform cannot print — HTML on Windows and CUPS, for one: render it to a PDF first.
Lays an HTML document out on pages with the engine printing uses and returns it as a PDF, with no print UI — to save, share or attach a document. iOS, Mac Catalyst and Android; 501 elsewhere, where htmlToPdf is false. Capped at the JSON limit (16 MB by default).
Prints a PDF sent as the raw body, streamed to a temporary file on the device rather than held in memory — the route for a document of any size. Options travel in the query. Fails with 400 when the body is not a PDF, and with 413 past the bridge's limit (512 MB by default).
What a printer can do. Lay a receipt out against these, never against a fixed width: charactersPerLine columns of text, and images up to dotsPerLine wide.
Network only: also try a TCP connection to every host on this device's IPv4 subnets (a /24 at most) on the ports the app allows — 9100 to 9102 by default — to find printers that do not advertise over mDNS.
The printer to connect to: the id of a printer the last scan found, or one named outright — a Bluetooth peripheralUuid with its serviceUuid and writeCharacteristicUuid, or a network host (an IP address on the local network) and port (9100).
A receipt to render as a PDF, for a printer that speaks no ESC/POS — an office printer through the printing bridge, or a download — on pages of pageWidth by pageHeight points.
Both bridges sit behind the bridge policy like every other. On top of that, the printers bridge limits where it
connects, because a receipt can carry raw bytes:
A printer named by address must be an IP address on the local network (loopback, private or link-local), never a
host name that could resolve anywhere.
It must be on a raw printer port: 9100, 9101 or 9102. Add others with
AddPrintersBridge(o => o.NetworkPorts.Add(…)). A printer a scan found is connected where it advertised.
The printing bridge prints only http and https URLs, not file: or anything else on the device.
NSBluetoothAlwaysUsageDescription, NSLocalNetworkUsageDescription, and _pdl-datastream._tcp and _printer._tcp in NSBonjourServices. Without the Bonjour entries a network scan finds nothing, and without the local network description iOS blocks the socket to the printer too.
macOS (sandboxed)
the same Info.plist keys, plus the com.apple.security.network.client, com.apple.security.device.bluetooth and com.apple.security.print entitlements.
Android
BLUETOOTH_SCAN and BLUETOOTH_CONNECT. OS printing shows its dialog from the foreground activity, so print while the app is visible.
Linux
Bluetooth printers need Shiny.BluetoothLE.Linux registered with services.AddBluetoothLE(). Rendering needs SkiaSharp.NativeAssets.Linux in the app, or render answers 501. CUPS printing needs lp and lpstat (cups-client).