Files
Kifi/kifi-app/lib/features/barcode_designer/services/zpl_compiler.dart

246 lines
7.8 KiB
Dart

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<LabelElement>.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();
}
}