Ticket & receipt printing
This document explains how printing works on the frontend, why thermal printing (58/80 mm) is problematic, and what the solutions are.
1. How printing currently works
Section titled “1. How printing currently works”The core of printing lives in src/lib/print-pdf.ts. All functions rely on
the same technique: load the document into a hidden <iframe>, then call
print() on that iframe’s window.
window.print()prints the current HTML page (the app), not an arbitrary file. To print a PDF/HTML/ticket, we load it into a dedicated iframe and print its window.
The functions
Section titled “The functions”| Function | Input | Use |
|---|---|---|
imprimerPdfBlob(blob) |
PDF (blob) | PDF receipts/invoices, contracts, payslips, reports, exports |
imprimerPdfOctets(octets) |
PDF (bytes) | Audit export |
imprimerImageBlob(blob) |
Image (blob) | Barcode labels |
imprimerHtml(html, 58|80) |
HTML | Thermal receipt |
The thermal ticket flow
Section titled “The thermal ticket flow”- The backend returns HTML (not a PDF) via
GET /api/v1/facturation/factures/{id}/ticket?largeur=58|80. printFactureTicket()(src/core/api/facturation.ts) fetches that HTML then callsimprimerHtml().imprimerHtml():- extracts the
<style>and<body>from the HTML, - builds a complete HTML document constrained to
largeurMmwide, - loads it into a hidden iframe (
src = blob URL), - waits for load + fonts, measures the real content height,
- injects
@page { size: <width>mm <height>mm; margin: 0 }, - calls
contentWindow.print().
- extracts the
- The iframe is cleaned up when focus returns to the main window (print dialog closed) or via a 60 s safety net.
2. The problem: paper size (US Letter)
Section titled “2. The problem: paper size (US Letter)”Symptom: the print dialog proposes US Letter (215.9 × 279.4 mm) instead of 58 or 80 mm.
Root cause
Section titled “Root cause”The browser cannot force a physical printer’s paper size. This is a Web limitation, not a code bug.
- The CSS
@page { size }rule only works for virtual PDF printers (“Save as PDF”, “Microsoft Print to PDF”). With a real thermal printer, Chrome uses the paper size configured in the printer driver, not the CSS one. - Chrome also has a known behavior: it ignores the default paper size set in the driver and falls back to US Letter. That is exactly the observed symptom.
What @page { size } actually fixes
Section titled “What @page { size } actually fixes”- “Save as PDF” → the PDF comes out correctly at 58/80 mm.
- The layout width of the content (the HTML is constrained to 58/80 mm).
It does not fix printing on a physical thermal printer.
3. Possible solutions
Section titled “3. Possible solutions”A. Configure the driver (low effort, one-time)
Section titled “A. Configure the driver (low effort, one-time)”Set the 58/80 mm size directly in Windows, then pick it in the Chrome print dialog.
In Windows:
Settings → Bluetooth & devices → Printers & scanners → [printer] → Printing preferences → Paper size = 80 mm (or 58 mm).
In the Chrome print dialog:
More settings →
Destination= the thermal printer,Paper size= 80 mm (roll) / 80 mm continuous / 58 mm,Scale= 100 %,Margins= None,Headers and footers= disabled.
These settings can be saved as a preset for cashiers.
B. Raw printing via a local bridge (robust, POS) — ✅ implemented
Section titled “B. Raw printing via a local bridge (robust, POS) — ✅ implemented”Bypasses the browser print dialog entirely by talking directly to the system print service through a local agent installed on the checkout machine: QZ Tray (https://qz.io — the most common for web POS).
Implementation (src/lib/qz-tray-client.ts):
printFactureTicket()(src/core/api/facturation.ts) first checks whether a QZ Tray printer is configured on this workstation (src/lib/imprimante-thermique-store.ts,localStorage— a machine-local hardware setting, never sent to the backend). If yes, it prints directly viaimprimerTicketQZ(): the same ticket HTML is sent to QZ inpixel/htmlmode, with width/height enforced at the QZ config level (thus the driver, via Java) — independent of the Chrome bug documented in section 2.- If QZ Tray is not configured, or the call fails (agent closed, printer
unplugged…), it silently falls back to
imprimerHtml()(method A) — never a hard failure for the cashier. - Workstation setup: print-button menu → “Paramètres d’impression…”
(
parametres-impression-dialog.tsx) — detects the agent, lists the system printers it exposes, lets you choose + test. - Security: unsigned mode (
qz.security.setCertificatePromise/setSignaturePromiseresolve empty). QZ Tray shows a one-time local confirmation per workstation (“Allow this site?”, “Remember” checkbox), no certificate to manage — sufficient for an internal workstation fleet with an operator; signing would only matter for a kiosk deployment with no manual confirmation.
Workstation prerequisite: install the QZ Tray agent (https://qz.io/download), then in “Paramètres d’impression…” pick the system printer matching the 58/80mm ticket.
C. Generate the ticket as a backend PDF
Section titled “C. Generate the ticket as a backend PDF”The backend produces a PDF at 58/80 mm directly (like the current PDF receipt). The PDF carries the correct page size, but printing on a physical printer remains subject to the driver (see section 2): the Chrome dialog can still propose US Letter as long as the driver is not configured.
4. Summary
Section titled “4. Summary”| Approach | Size control | Depends on driver? | Effort |
|---|---|---|---|
@page { size } (method A) |
PDF/preview only | Yes (physical) | Already in place |
| Windows driver config | Yes, via Windows | Yes (but set) | Low |
| QZ Tray (method B) | Total (enforced width) | No | ✅ Implemented |
| Backend-side PDF | Correct PDF size | Yes (physical) | Medium |
QZ Tray (B) is in place and used automatically as soon as a printer is configured in “Paramètres d’impression…”. Without an agent installed on the workstation, the app falls back to method A (driver config as a troubleshooting path).
