import 'package:flutter/material.dart'; import '../domain/label_document.dart'; class PrintCompiler { /// Compiles a LabelDocument into printer commands according to the chosen language static String compile({ required LabelDocument document, String? overrideLanguage, int copies = 1, }) { final language = (overrideLanguage ?? document.config.printerLanguage).toUpperCase(); switch (language) { case 'TSPL': return TsplCompiler.compile(document, copies: copies); case 'ZPL': default: return ZplCompiler.compile(document, copies: copies); } } } class ZplCompiler { static String compile(LabelDocument doc, {int copies = 1}) { final buffer = StringBuffer(); final cfg = doc.config; // Convert total web width (accounting for 1-Up, 2-Up, etc.) to printer dots final totalWebWidthDots = cfg.totalWebWidthDots; final heightDots = cfg.heightDots; buffer.writeln('^XA'); // Start of Label buffer.writeln('^PW$totalWebWidthDots'); // Print Width across entire roll buffer.writeln('^LL$heightDots'); // Label Length buffer.writeln('^LS0'); // Label Shift buffer.writeln('^LH0,0'); // Label Home buffer.writeln('^CI28'); // UTF-8 Encoding // Sort elements by zIndex final sortedElements = List.from(doc.elements) ..sort((a, b) => a.zIndex.compareTo(b.zIndex)); // Iterate across each column in the multi-up row (1-Up, 2-Up, 3-Up) final cols = cfg.columnsAcross.clamp(1, 4); for (int col = 0; col < cols; col++) { final colOffsetMm = col * (cfg.widthMm + cfg.horizontalGapMm); final colOffsetDots = cfg.mmToDots(colOffsetMm); for (final elem in sortedElements) { final xDots = cfg.mmToDots(elem.xMm) + colOffsetDots; final yDots = cfg.mmToDots(elem.yMm); final wDots = cfg.mmToDots(elem.widthMm); final hDots = cfg.mmToDots(elem.heightMm); if (elem is TextElement) { _compileText(buffer, elem, cfg, xDots, yDots, wDots); } else if (elem is DynamicTextElement) { // In design mode or unresolved templates, output the placeholder final textElem = TextElement( id: elem.id, xMm: elem.xMm, yMm: elem.yMm, widthMm: elem.widthMm, heightMm: elem.heightMm, text: elem.placeholder, fontFamily: elem.fontFamily, fontSizePt: elem.fontSizePt, isBold: elem.isBold, isItalic: elem.isItalic, alignment: elem.alignment, ); _compileText(buffer, textElem, cfg, xDots, yDots, wDots); } else if (elem is BarcodeElement) { _compileBarcode(buffer, elem, cfg, xDots, yDots, wDots, hDots); } else if (elem is QrCodeElement) { _compileQrCode(buffer, elem, cfg, xDots, yDots, wDots); } else if (elem is RectangleElement) { _compileRectangle(buffer, elem, cfg, xDots, yDots, wDots, hDots); } else if (elem is LineElement) { _compileLine(buffer, elem, cfg, xDots, yDots, wDots, hDots); } } } if (copies > 1) { buffer.writeln('^PQ$copies'); } buffer.writeln('^XZ'); // End of Label return buffer.toString(); } static void _compileText( StringBuffer buf, TextElement elem, LabelConfiguration cfg, int xDots, int yDots, int wDots, ) { // Standard font height calculation based on point size & DPI // 1 pt = 1/72 inch. dots = pt / 72 * DPI final fontHeightDots = ((elem.fontSizePt / 72.0) * cfg.dpi * 1.33).round().clamp(14, 300); final fontWidthDots = (fontHeightDots * 0.85).round(); final alignCode = switch (elem.alignment) { TextAlign.center => 'C', TextAlign.right => 'R', TextAlign.justify => 'J', _ => 'L', }; // Replace unicode Rupee sign with 'Rs. ' as printer internal font 0 does not have U+20B9 final printableText = elem.text.replaceAll('₹', 'Rs. '); // Text block with field formatting buf.writeln('^FO$xDots,$yDots'); buf.writeln('^A0N,$fontHeightDots,$fontWidthDots'); buf.writeln('^FB$wDots,2,0,$alignCode,0'); buf.writeln('^FD$printableText^FS'); } static void _compileBarcode( StringBuffer buf, BarcodeElement elem, LabelConfiguration cfg, int xDots, int yDots, int wDots, int hDots, ) { final barcodeVal = (elem.valueType == 'dynamic' && elem.staticValue.isEmpty) ? elem.placeholder : elem.staticValue; // Module width: 2 dots for 203 DPI, 3 dots for 300 DPI (Citizen CL-E331) for readable scanning final moduleWidth = (cfg.dpi >= 300) ? 3 : 2; final barHeight = (hDots * (elem.showText ? 0.82 : 0.95)).round().clamp(24, 600); final showTextChar = elem.showText ? 'Y' : 'N'; buf.writeln('^FO$xDots,$yDots'); buf.writeln('^BY$moduleWidth,3,$barHeight'); switch (elem.barcodeType) { case BarcodeType.ean13: buf.writeln('^BEN,$barHeight,$showTextChar,N'); break; case BarcodeType.code39: buf.writeln('^B3N,N,$barHeight,$showTextChar,N'); break; case BarcodeType.upca: buf.writeln('^BUN,$barHeight,$showTextChar,N,N'); break; case BarcodeType.code128: default: buf.writeln('^BCN,$barHeight,$showTextChar,N,N'); break; } buf.writeln('^FD$barcodeVal^FS'); } static void _compileQrCode( StringBuffer buf, QrCodeElement elem, LabelConfiguration cfg, int xDots, int yDots, int wDots, ) { final qrVal = (elem.valueType == 'dynamic' && elem.staticValue.isEmpty) ? elem.placeholder : elem.staticValue; // Magnification factor (1 to 10) final mag = (wDots / 30).round().clamp(2, 8); buf.writeln('^FO$xDots,$yDots'); buf.writeln('^BQN,2,$mag'); buf.writeln('^FDQA,$qrVal^FS'); } static void _compileRectangle( StringBuffer buf, RectangleElement elem, LabelConfiguration cfg, int xDots, int yDots, int wDots, int hDots, ) { final borderDots = cfg.mmToDots(elem.borderWidthMm).clamp(1, 40); final cornerDots = cfg.mmToDots(elem.cornerRadiusMm).clamp(0, 8); buf.writeln('^FO$xDots,$yDots^GB$wDots,$hDots,$borderDots,B,$cornerDots^FS'); } static void _compileLine( StringBuffer buf, LineElement elem, LabelConfiguration cfg, int xDots, int yDots, int wDots, int hDots, ) { final thickDots = cfg.mmToDots(elem.thicknessMm).clamp(1, 30); if (elem.isVertical) { buf.writeln('^FO$xDots,$yDots^GB$thickDots,$hDots,$thickDots^FS'); } else { buf.writeln('^FO$xDots,$yDots^GB$wDots,$thickDots,$thickDots^FS'); } } } class TsplCompiler { static String compile(LabelDocument doc, {int copies = 1}) { final buffer = StringBuffer(); final cfg = doc.config; final cols = cfg.columnsAcross.clamp(1, 4); buffer.writeln('SIZE ${cfg.totalWebWidthMm} mm, ${cfg.heightMm} mm'); buffer.writeln('GAP ${cfg.verticalGapMm.toStringAsFixed(cfg.verticalGapMm % 1 == 0 ? 0 : 1)} mm, 0 mm'); buffer.writeln('DIRECTION 1'); buffer.writeln('CLS'); for (int col = 0; col < cols; col++) { final colOffsetMm = col * (cfg.widthMm + cfg.horizontalGapMm); final colOffsetDots = cfg.mmToDots(colOffsetMm); for (final elem in doc.elements) { final xDots = cfg.mmToDots(elem.xMm) + colOffsetDots; final yDots = cfg.mmToDots(elem.yMm); if (elem is TextElement) { buffer.writeln('TEXT $xDots,$yDots,"3",0,1,1,"${elem.text}"'); } else if (elem is BarcodeElement) { final val = elem.valueType == 'dynamic' ? elem.placeholder : elem.staticValue; final hDots = cfg.mmToDots(elem.heightMm); buffer.writeln('BARCODE $xDots,$yDots,"128",$hDots,${elem.showText ? 1 : 0},0,2,2,"$val"'); } } } buffer.writeln('PRINT $copies,1'); return buffer.toString(); } }