Pixel-Perfect Thermal Label Printing from the Browser with Vue 3 and QZ Tray
Browser-based label printing usually forces a choice between proprietary command languages that lock you into one printer brand or brittle HTML rendering that breaks across drivers. This approach uses a standard Windows GDI driver and bitmap output to sidestep both problems, giving any web app reliable, high-fidelity thermal output without vendor lock-in.
A complete web-to-thermal pipeline renders labels as 300 DPI bitmaps using html2canvas and sends them silently to a Yiwei A42 printer through QZ Tray's WebSocket service. The system maps 1 mm to 5 px on a 400×300 px canvas for an 80×60 mm label, then scales output to match the physical media exactly. A visual drag-and-drop designer generates JSON templates with dynamic variable interpolation, making layout changes a configuration edit rather than a rewrite of low-level printer commands. Six hard-won fixes address QZ Tray's auto-orientation flip, Windows spooler media-size mismatches, spurious blank pages, broken thin strokes from thermal binarization, double-border artifacts, and pixel drift between the editor and print preview.
The core insight is that thermal printers are binary devices — they either heat a pixel or they don't. Every web rendering assumption about grayscale anti-aliasing, subpixel smoothing, and translucent borders breaks at the print head. The fix is not better CSS but brute-force resolution: render at 3x the target DPI so that the binarization step has enough samples to preserve thin strokes.
QZ Tray's auto-orientation behavior is undocumented at the configuration level but well-known to anyone who has debugged a Java PrintService. The fact that passing `orientation: 'landscape'` causes a dimension swap inside the Java layer, not in QZ Tray itself, means the bug survives across QZ versions and printer models. The only safe configuration is to omit the field entirely.
The Windows print stack has two independent sources of truth for media size: the application's print job and the driver's default form. When they disagree, the spooler silently clips or paginates the output. This is not a QZ Tray problem — it is the same class of bug that affects POS systems and label printers regardless of the software stack, and the fix is always the same: align the driver's default form with the job's declared size.