zplCore.com – High Performance .NET Labelprint Engine
ZPL rendern.
Etiketten designen.
zplCore.com ist ein virtueller Zebra-Drucker und ein Label-Designer-Modell in einer Bibliothek: ZPL II und EPL2 werden zu PNG, PDF & Co., Etiketten aus C#-Objekten werden zu ZPL – millionenfach und streamend. Dazu ein eigenständiger Barcode-Generator mit 144 Symbologien und GS1-Validierung – plus Druckaufträge mit Vor- und Nachlauf-Etikett und Transporte für TCP, seriell, USB, Windows-Spooler und CUPS.

Virtueller Drucker
Hält Grafiken, Formate und Schriften zwischen Aufrufen im Speicher – wie ein echter Zebra.
Barcodes, dekodierbar
144 Symbologien, bit-genau gegen bwip-js bzw. zint geprüft und, wo möglich, mit ZXing zurückgelesen.
EPL2 & Konverter
EPL2 rendern oder nach ZPL übersetzen – inklusive Formularen, Variablen und Zählern.
GS1
AI-Prüfung (534 AIs), GS1-128, SSCC-18, ITF-14, Composite CC-A/B/C.
Label-Modell
Elemente in mm/inch, Ausdrücke, Masken-Zähler, Tabellen, Repeater, Bögen, Duplex-Seiten, JSON.
Massendaten & Druckaufträge
Typisierte Bindung an LINQ, EF Core, LiteDB – streamend mit ^DF/^XF, Vor-/Nachlauf-Etikett und Schnitt nach dem letzten Etikett.
Alle Schriften
UTF-8, Arabisch/Hebräisch mit Bidi, CJK, Thai, Devanagari via HarfBuzz.
Export
PNG, JPEG, WebP, BMP, PDF, GRF, TIFF (CCITT G3/G4, LZW), PCX, PCL, EPL, Fingerprint, ESC/POS, GIF, SVG, HTML, EPS – plus DPI-Umrechnung von ZPL.
01Quickstart
Projekt referenzieren, ZPL hineingeben, Bilder herausbekommen.
dotnet add reference src/ZplCore/ZplCore.csproj dotnet build -c Release # src/ZplCore/bin/Release/net10.0/ZplCore.dll dotnet test
var renderer = new ZplRenderer(new ZplRenderSettings { Dpi = 203 }.WithLabelSizeMm(100, 150));
List<byte[]> pngs = renderer.ProcessCommands(
"^XA^FO50,50^A0N,40,40^FDHallo Welt^FS^BY2^FO50,120^BCN,80^FD12345^FS^XZ");
File.WriteAllBytes("label.png", pngs[0]);
renderer.Settings.OutputFormat = ZplOutputFormat.Pdf; // ein PDF, eine Seite pro Etikett
File.WriteAllBytes("labels.pdf", renderer.ProcessFile("labels.zpl")[0]);
02ZplRenderer
ZplRenderer verarbeitet ZPL-Strings, Byte-Arrays (Codepage 850 wie am Drucker oder UTF-8 mit BOM) und Dateien. Pro gedrucktem Etikett entsteht ein kodiertes Bild – bei PDF ein Dokument mit einer Seite pro Etikett.
Einstellungen (ZplRenderSettings)
var settings = new ZplRenderSettings
{
Dpi = 300, // 152, 203, 300 oder 600
OutputFormat = ZplOutputFormat.Webp, // Png, Jpeg, Webp, Bmp, Pdf, Grf
Quality = 90, // JPEG/WebP
AntiAlias = false, // reines 1-Bit wie am Thermodrucker
InkColor = new SKColor(0x1d, 0x3b, 0x8a), // Farbband
BackgroundColor = SKColors.LightYellow, // Etikettenpapier
OutputRotation = ZplRotation.Rotate90,
TraceElements = true, // Positionen aller Felder sammeln
Clock = () => new DateTime(2026, 10, 18), // deterministische ^FC-Felder
}.WithLabelSizeInches(4, 6);
string json = settings.ToJson(); // speichern ...
var restored = ZplRenderSettings.FromJson(json); // ... und laden
| Eigenschaft | Standard | Bedeutung |
|---|---|---|
Dpi | 203 | Druckauflösung 152/203/300/600 |
LabelWidth / LabelHeight | 4×6 Zoll | in Punkten; ^PW/^LL überschreiben, außer ForceLabelWidth/Height |
InkColor / BackgroundColor | Schwarz / Weiß | Farbband- und Papierfarbe |
OutputFormat / Quality | Png / 95 | Png, Jpeg, Webp, Bmp, Pdf, Grf |
OutputRotation | None | Ausgabe um 90/180/270° drehen |
AntiAlias / InvertColors | true / false | Glättung bzw. Negativ |
BackgroundImage / WatermarkImage + WatermarkOpacity | – / 0.25 | Vordruck unter bzw. Wasserzeichen über dem Inhalt |
RfidIndicator + RfidIndicatorColor / RfidIndicatorOpacity | Watermark / #C0C0C0 / 1 | graues RFID-Symbol hinter dem Inhalt bei Etiketten mit ^RF/^WT/^WF/^RQ (nur Vorschauformate, siehe ^RF) |
ThumbnailSize | 0 | Ausgabe auf max. N Pixel verkleinern |
RenderOutputIndexes | null | nur ausgewählte Etiketten kodieren |
DefaultCodePage | 850 | Feldkodierung ohne ^CI |
TraceElements | false | Position/Inhalt jedes Felds sammeln (Editoren, Tests) |
Clock | DateTime.Now | Uhr für ^FC |
DrawRfid | false | ^RF-Daten sichtbar einzeichnen |
PdfTitle/Author/Subject/Keywords/Creator | – | PDF-Metadaten |
PaperWidth/PaperHeight/PaperMargin/LabelGap | 0 | PDF-Bogenlayout: mehrere Etiketten pro Seite |
using var watermark = SKBitmap.Decode("muster.png");
var renderer = new ZplRenderer(new ZplRenderSettings
{
WatermarkImage = watermark,
WatermarkOpacity = 0.35f,
ThumbnailSize = 200, // Ausgabe auf max. 200 px verkleinern
RenderOutputIndexes = [0, 2], // nur Etikett 1 und 3 kodieren
DrawRfid = true, // ^RF-Daten als Kasten einzeichnen
});
// PDF: Metadaten + mehrere Etiketten pro Bogen (A4 @ 203 dpi)
var pdf = new ZplRenderer(new ZplRenderSettings
{
OutputFormat = ZplOutputFormat.Pdf,
PdfTitle = "Lieferung 42", PdfAuthor = "Lager", PdfSubject = "Versand", PdfKeywords = "zpl",
PaperWidth = 1680, PaperHeight = 2376, PaperMargin = 40, LabelGap = 16,
}.WithLabelSizeMm(100, 50));
byte[] sheet = pdf.ProcessCommands(zpl)[0];
Wasserzeichen

RFID sichtbar (DrawRfid)

Ausgabeformate & Bitmaps
PNG · JPEG · WebP · BMP
Ein Bild pro Etikett. BMP als 24-Bit (eigener Encoder).
Ein Dokument, eine Seite pro Etikett in physischer Größe – oder Bogenlayout. Beispiel-PDF
GRF
Das gerenderte Etikett als einzelnes ^GFA in ^XA…^XZ – für Drucker ohne passende Schriften.
TIFF · PCX
Tiff: alle Etiketten in einem mehrseitigen 1-Bit-TIFF (TiffCompression); Pcx: 1-Bit-PCX pro Etikett.
PCL · EPL · Fingerprint
Das Rasterbild als PCL-5-Seite (Laserdrucker), EPL2-GW-Grafik oder Intermec-Fingerprint – ZPL auf Fremddruckern ausgeben.
ESC/POS
Rasterbild für Bondrucker inklusive Schnitt.
var renderer = new ZplRenderer(new ZplRenderSettings { OutputFormat = ZplOutputFormat.Tiff }.WithLabelSizeMm(100, 60));
byte[] tiff = renderer.ProcessCommands(zpl)[0]; // EIN mehrseitiges 1-Bit-TIFF
foreach (var format in new[] { ZplOutputFormat.Pcx, ZplOutputFormat.Pcl, ZplOutputFormat.Epl,
ZplOutputFormat.Fingerprint, ZplOutputFormat.EscPos })
{
renderer.Settings.OutputFormat = format; // je Etikett ein Byte-Array
List<byte[]> outputs = renderer.ProcessCommands(zpl);
}
// Hintergrund (z. B. vorgedrucktes Formular) und Wasserzeichen aus Datei, data:-URI oder Base64;
// URLs nur asynchron (größenbegrenzt): ZplRenderSettings.LoadImageFromUrlAsync(new Uri("https://…"))
var preview = new ZplRenderSettings()
.WithBackgroundImage("formular.png")
.WithWatermarkImage("muster.png", opacity: 0.3f);
SKBitmap logo = ZplRenderSettings.LoadImage("data:image/png;base64,iVBORw0KGgo…");
var renderer = new ZplRenderer(new ZplRenderSettings { TraceElements = true });
foreach (ZplRenderResult result in renderer.Render(zpl))
{
using SKBitmap bmp = result.Bitmap; // Aufrufer besitzt die Bitmaps
foreach (var e in result.Elements)
Console.WriteLine($"{e.Command} @ {e.X},{e.Y} {e.Width}x{e.Height}: {e.Content}");
File.WriteAllText("label.grf.zpl", ZplExport.ToGrfZpl(bmp)); // als ^GFA-Grafik
}
Hintergrund & Wasserzeichen aus Datei, URL oder Base64
ZplRenderSettings.LoadImage(source) lädt Bilder aus Dateipfad, http(s)-URL, data:-URI oder reinem Base64. WithBackgroundImage legt ein Bild (z. B. ein eingescanntes Vordruck-Formular) unter das Etikett, WithWatermarkImage eines darüber – siehe Code oben und das Wasserzeichen-Beispiel.
Ereignisse, Statusabfragen & ~WC
Befehle ohne Bildwirkung (Medien, Schwärzung, Kalibrierung, ~HS …) lösen PrinterControl aus; Antworten auf Host-Abfragen landen in Responses. LabelRendered feuert nach jedem kodierten Etikett. ~WC druckt ein Konfigurationsetikett.
var renderer = new ZplRenderer();
renderer.PrinterControl += (_, e) =>
{
Console.WriteLine($"Steuerbefehl {e.Command.Prefix}{e.Command.Name} {e.Command.Parameters}");
// e.Response ist für ~HS vorbelegt und kann überschrieben werden
};
renderer.LabelRendered += (_, e) =>
File.WriteAllBytes($"label{e.Index + 1}.png", e.Data);
renderer.ProcessCommands("~SD25^XA^MD10^FO50,50^GB100,100,4^FS^XZ~HS");
var status = ZplPrinterStatus.Parse(renderer.Responses[0]);
Console.WriteLine(status.IsReady ? "bereit" : "Fehler");
~WC Konfigurationsetikett

Gespeicherte Formate
Formate (^DF/^XF), Grafiken (~DG, ~DY, ^IS) und Schriften liegen in renderer.Store (ZplResourceStore), der auch zwischen mehreren Renderern geteilt werden kann (new ZplRenderer { Store = shared }).
Druckerspeicher: ^DF / ^XF, ^IS / ^IL
var renderer = new ZplRenderer();
// ^DF speichert ein Format im Druckerspeicher, ^XF ruft es auf, ^FN füllt die Felder
renderer.ProcessCommands("^XA^DFR:ADR.ZPL^FS^FO50,50^A0N,40,40^FN1^FS^FO50,110^A0N,30,30^FN2^FS^XZ");
var labels = renderer.ProcessCommands(
"^XA^XFR:ADR.ZPL^FN1^FDMax Mustermann^FS^FN2^FDGraz^FS^XZ" +
"^XA^XFR:ADR.ZPL^FN1^FDErika Muster^FS^FN2^FDWien^FS^XZ");
// Speicher bleibt zwischen Aufrufen erhalten – wie bei einem echten Drucker
Console.WriteLine(string.Join(", ", renderer.Store.Formats));
renderer.Store.AddGraphic("R:LOGO.GRF", SKBitmap.Decode("logo.png"), dither: true);
renderer.Reset(); // Power-on-Reset (~JR)
^XA^FO10,10^GB300,80,3^FS^FO30,30^A0N,40,40^FDFormular^FS ^ISR:FORM.GRF^XZ ← Etikett als Bild speichern (und drucken) ^XA^ILR:FORM.GRF ← Bild als Hintergrund laden ^FO30,120^A0N,30,30^FDVariable Daten^FS^XZ
Seriennummern, Uhr, ^MC, ^RF
^MC Map Clear
^MCN lässt das Bild für das nächste Etikett stehen (Overlay), ^MCY löscht wieder.
^RF RFID-Wasserzeichen

Etiketten mit RFID-Operation (^RF, ^WT, ^WF, ^RQ) zeigen in der Vorschau ein graues RFID-Symbol: zentriert, ca. 0,86" groß, hinter dem Inhalt, mit ^PO I gedreht; in SVG/Vektor-PDF als Pfad. Nur PNG/JPEG/WebP/BMP/GIF/PDF/SVG/HTML/EPS – nie in GRF, EPL, PCL, Fingerprint, ESC/POS oder 1-Bit-TIFF/PCX. RfidIndicator = ZplRfidIndicator.None schaltet es ab.
^SO / ^ST
Zweit- und Drittuhr mit Versatz (z. B. Mindesthaltbarkeit +2 Monate) bzw. Uhr stellen. ^CD/^CC/^CT ändern die Steuerzeichen, ^TO kopiert gespeicherte Objekte, ^JM halbiert die Punktdichte.
03Unterstützte ZPL-Befehle
Durchsuchbar nach Befehl oder Beschreibung. Nicht aufgeführte Steuer-, Medien- und Statusbefehle werden akzeptiert und über PrinterControl gemeldet.
| Befehl | Beschreibung | Kategorie |
|---|
04Barcode-Galerie
Alle Barcode-Befehle aus ZPL – jedes Bild wurde aus echtem ZPL gerendert. Klick auf „ZPL“ zeigt den Quelltext, Klick auf das Bild vergrößert. ^B7 (PDF417), ^BX (Data Matrix, pixelgleich zu Zebra) und ^BQ (QR, BWIPP-Engine mit Zebras EC-Anhebung) nutzen eigene Encoder; neu sind ^BB Codablock F, ^B4 Code 49, ^BF MicroPDF417 und ^BZ Typ 3 (Intelligent Mail) sowie ^FM für Macro PDF417 (ein Feld, mehrere Symbole an eigenen Positionen).
Label-Modell: Symbologien ohne ZPL-Befehl
Labels.BarcodeItem kennt zusätzlich Micro QR, rMQR, GS1 QR, Swiss QR, EPC QR, GS1 Data Matrix, Aztec Rune, Han Xin, DotCode, Ultracode, Code 16K, Telepen, Postcodes u. v. m. Hat ZPL keinen Befehl dafür, rendert der Barcode-Generator das Symbol und es wird automatisch als ^GFA-Grafik ausgegeben – auch mit UseStoredFormat() korrekt. Codablock F, Code 49, MicroPDF417 und Intelligent Mail bleiben native Befehle.
// Im Label-Modell: Symbologien ohne ZPL-Befehl werden automatisch als ^GFA ausgegeben
var label = new Label(LabelUnit.Mm, 60, 30)
.Add(new BarcodeItem { X = 3, Y = 3, Code = "DotCode 42", Symbology = ZplCore.Labels.Symbology.DotCode, ModuleWidth = 5 })
.Add(new BarcodeItem { X = 30, Y = 3, Code = "(01)09521234543213", Symbology = ZplCore.Labels.Symbology.Gs1QrCode, ModuleWidth = 4 })
.Add(new BarcodeItem { X = 3, Y = 18, Code = "CODABLOCK", Symbology = ZplCore.Labels.Symbology.CodablockF, BarHeight = 2 }); // nativ: ^BB
string zpl = label.ToZpl(); // gilt auch mit UseStoredFormat()

05EPL2
EplRenderer ist ein virtueller EPL2-Drucker: EPL wird nach ZPL übersetzt und durch denselben ZplRenderer gerendert – gleiche Schriften, Barcodes, Ausgabeformate und Einstellungen. ToZpl() liefert die Übersetzung direkt, also einen EPL→ZPL-Konverter für die Migration alter Formulare.
var epl = new EplRenderer(new ZplRenderSettings { Dpi = 203 }.WithLabelSizeInches(4, 3));
epl.Clock = () => new DateTime(2026, 10, 18, 14, 5, 9); // für TT/TD-Felder
epl.PrinterControl += (_, e) => Console.WriteLine($"EPL-Steuerbefehl: {e.Command.Name}");
List<byte[]> pngs = epl.ProcessFile("samples/zpl/sample.epl"); // ein PNG pro gedrucktem Etikett
File.WriteAllBytes("sample.png", pngs[0]);
// EPL → ZPL: dieselbe Übersetzung, die der Renderer intern nutzt
string zpl = epl.ToZpl(File.ReadAllText("samples/zpl/form.epl"));
File.WriteAllText("form.zpl", zpl); // drei ^XA…^XZ-Blöcke (P3)
epl.AddGraphic("LOGO", File.ReadAllBytes("logo.pcx")); // für GG x,y,"LOGO"
epl.Settings.OutputFormat = ZplOutputFormat.Pdf;
File.WriteAllBytes("form.pdf", epl.ProcessFile("samples/zpl/form.epl")[0]);

Übersetztes ZPL anzeigen (ToZpl)
Formulare, Variablen & Zähler
FS"name" … FE zeichnet ein Formular auf, FR"name" ruft es ab. V00–V99 sind Variablen, C0–C9 Zähler (mit Schrittweite und fester Länge); die Daten folgen nach ?, je eine Zeile. P3 druckt drei Etiketten und zählt C0 jeweils weiter. Das Formular bleibt wie am Drucker im Speicher.
Übersetztes ZPL
Unterstützte EPL2-Befehle
| Befehl | Bedeutung |
|---|---|
N | Bildpuffer löschen |
q / Q | Etikettenbreite / -höhe und Lücke |
R | Referenzpunkt (Ursprung verschieben) |
ZT / ZB | Druckrichtung oben / unten (ZB = 180°) |
A | Text: residente Schriften 1–5 (203/300 dpi), Rotation, Vergrößerung, N/R (invertiert) |
B | 1D-Barcodes: Code 128 (A/B/C/Auto), Code 39, Code 93, EAN/UPC, I2of5, Codabar, MSI, Plessey, POSTNET … |
b | 2D-Barcodes: QR (Q), Data Matrix (D), PDF417 (P), MaxiCode (M), Aztec (A) |
X | Rechteck (Kasten) |
LO / LW / LE | Linie schwarz / weiß / XOR |
LS | Diagonale |
GW | Grafik direkt (binär) |
GM / GG / GK | PCX-Grafik speichern / drucken / löschen |
FS … FE | Formular aufzeichnen |
FR / FK | Formular abrufen / löschen |
V | Formularvariable (Länge, Ausrichtung, Prompt) |
C | Zähler (Länge, Ausrichtung, Schrittweite ±) |
? | Variablendaten für V und C |
P | Drucken (Anzahl Sätze), erhöht Zähler |
TS / TT / TD | Uhr stellen, Zeit- und Datumsfelder |
S D I O U JF JB W … | Geschwindigkeit, Dichte, Zeichensatz, Optionen: über PrinterControl gemeldet |
S, D, I, O, U, JF …) löst EplRenderer.PrinterControl aus. TT/TD nutzen Clock, TS setzt einen Versatz. PCX-Grafiken für GG kommen per GM oder AddGraphic(name, pcx).06Barcode-Generator
ZplCore.Barcodes.Barcode erzeugt Barcodes ganz ohne ZPL: 144 Symbologien (enum Symbology), Ausgabe als Raster, Vektor oder ZPL-Grafik.
var bc = new Barcode
{
Symbology = Symbology.Ean13,
Code = "400638133393", // Prüfziffer wird ergänzt
Unit = BarcodeUnit.Millimeter,
Dpi = 300,
BarWidth = 0.33, // X-Dimension (100 %)
BarHeight = 22,
};
File.WriteAllBytes("ean13.png", bc.ToPng());
File.WriteAllBytes("ean13.webp", bc.ToWebp(90));
File.WriteAllText("ean13.svg", bc.ToSvg()); // Vektor
File.WriteAllBytes("ean13.pdf", bc.ToPdf()); // Vektor, physische Größe
File.WriteAllText("ean13.eps", bc.ToEps()); // PostScript
string gfa = bc.ToZplGraphic(50, 100); // ^FO50,100^GFA,…^FS für beliebige ZPL-Etiketten
using var bmp = new SKBitmap(1200, 600); // oder direkt auf eine eigene Leinwand
using var canvas = new SKCanvas(bmp);
canvas.Clear(SKColors.White);
bc.Draw(canvas, 40, 40);
SKSize size = bc.Size; // Pixel bei Dpi, inkl. Ruhezone und Text
Raster
ToBitmap(), ToPng(), ToJpeg(q), ToWebp(q)
Vektor
ToSvg(), ToPdf() (physische Größe), ToEps(), Draw(SKCanvas, x, y)
ZPL
ToZplGraphic(x, y) – ^FO…^GFA…^FS für jedes Zebra-Etikett
Beispieldateien: ean13.svgean13.pdfean13.epsswissqr.svgswissqr.pdfgs1-datamatrix.epsean13-gfa.zpl
Alle Optionen
var bc = new Barcode
{
Symbology = Symbology.Code39,
Code = "ZPLCORE",
Unit = BarcodeUnit.Pixel, // Pixel, Millimeter, Inch, Mil
Dpi = 203,
BarWidth = 2, // schmaler Strich / Modul
BarHeight = 70, // nur 1D; 2D-Module sind quadratisch
WideRatio = 2.5, // breit:schmal für Code 39, 2 of 5, Codabar …
QuietZone = 6, // Module (null: 10 bei 1D, 4 bei 2D)
AddChecksum = true, // optionale Prüfziffer
ForeColor = new SKColor(0x1d, 0x3b, 0x8a),
BackColor = new SKColor(0xff, 0xf7, 0xd6),
TextColor = new SKColor(0xc8, 0x10, 0x2e),
TextPosition = BarcodeTextPosition.Above, // None, Below, Above
HumanReadableText = "*ZPLCORE*", // statt der kodierten Daten
FontSize = 9, // Punkt; 0 = automatisch
Rotation = 90, // 0, 90, 180, 270
};
// weitere: Typeface, GuardBars, BearerBars/BearerBarWidth, Supplement,
// QrErrorCorrection, Pdf417Columns, Pdf417SecurityLevel, MaxiCodeMode, DataBarSegmentsPerRow

| Eigenschaft | Standard | Bedeutung |
|---|---|---|
Symbology, Code | Code128, "" | Symbologie und Daten |
Unit | Pixel | Einheit für BarWidth/BarHeight/BearerBarWidth: Pixel, Millimeter, Inch, Mil |
Dpi | 300 | Auflösung für Raster und Einheitenumrechnung |
BarWidth | 2 | Schmaler Strich bzw. Modulgröße |
BarHeight | 80 | Höhe linearer Codes (2D-Module sind quadratisch) |
WideRatio | 3.0 | Breit-zu-schmal (2.0–3.0) für Zwei-Breiten-Codes |
QuietZone | null | Ruhezone in Modulen (null: 10 bei 1D, 4 bei 2D) |
ForeColor, BackColor, TextColor | Schwarz/Weiß/Schwarz | Farben (auch in SVG/PDF/EPS) |
TextPosition | Below | Klarschrift: None, Below, Above |
HumanReadableText | null | eigener Klartext statt der Daten (null = automatisch inkl. Prüfziffer) |
FontSize, Typeface | 0, null | Schriftgröße in Punkt (0 = proportional zum Modul), Schrift |
GuardBars | true | EAN/UPC-Randzeichen in die Klarschrift verlängern |
BearerBars, BearerBarWidth | None (ITF-14: Frame), 4 | Schutzbalken: Frame oder HorizontalRules |
Supplement | null | 2- oder 5-stelliges EAN/UPC-Add-on |
AddChecksum | false | optionale Prüfziffer (Code 39, 2 of 5, Code 11 …) |
Rotation | 0 | 0, 90, 180, 270 Grad |
QrErrorCorrection | 'M' | L, M, Q, H (QR, Micro QR, rMQR, GS1 QR) |
Pdf417Columns, Pdf417SecurityLevel | 0, 2 | PDF417-Spalten (0 = automatisch), Sicherheitsstufe |
MaxiCodeMode | 4 | MaxiCode-Modus 2–6 |
DataBarSegmentsPerRow | 4 | Segmente pro Zeile bei DataBar Expanded Stacked |
ModuleShape, FinderShape | Square, Square | Modulform 2D (Square, Circle, RoundedSquare, Diamond); QR-Finder (Square, Rounded, Circle) |
LogoImage, LogoSizePercent, LogoOpacity, LogoBackground | null, 20, 1, true | Logo in der Mitte von 2D-Codes (QR: Fehlerkorrektur H) |
LightMarginIndicator | false | EAN/UPC: </> in den Ruhezonen |
TextFormatPattern, HumanReadableAlignment | null, Center | Klarschrift formatieren (Maske oder {0:…}) und ausrichten (Center, Left, Right, Justify) |
CodeEncoding | Text | Daten als Text, Base64 oder Hex |
ProcessTilde | false | ~~, ~dNNN, ~1 (FNC1), ~2IICC (Structured Append), ~eNNNNNN (ECI) |
QrVersion, QrMask | 0, -1 | QR-Version 1–40 und Maske 0–7 erzwingen |
StructuredAppendInfo / StructuredAppend(i, n) | null | Daten auf mehrere Symbole verteilen (QR, Data Matrix, Aztec, MaxiCode, PDF417) |
Besondere Symbole
EAN-13 mit 5-stelligem Add-on
var bc = new Barcode
{
Symbology = Symbology.Ean13,
Code = "978020137962",
Supplement = "51995", // 2- oder 5-stelliges Add-on rechts daneben
GuardBars = true, // Randzeichen ragen in die Klarschrift
Dpi = 203, BarWidth = 2, BarHeight = 70,
};
ITF-14 mit Schutzrahmen
var bc = new Barcode
{
Symbology = Symbology.Itf14,
Code = "0460123456789", // 13 Stellen → Prüfziffer 3 wird ergänzt
Dpi = 203, BarWidth = 2, BarHeight = 70,
BearerBars = BearerBarStyle.Frame, // Standard bei ITF-14; HorizontalRules = nur oben/unten
BearerBarWidth = 4,
};
Swiss QR (QR-Rechnung)
string payload = string.Join("\n",
"SPC", "0200", "1", "CH4431999123000889012",
"S", "Muster AG", "Bahnhofstrasse", "1", "8000", "Zürich", "CH",
"", "", "", "", "", "", "",
"1949.75", "CHF",
"S", "Pia Rutschmann", "Marktgasse", "28", "9400", "Rorschach", "CH",
"QRR", "210000000003139471430009017", "Rechnung 4711", "EPD");
var bc = new Barcode
{
Symbology = Symbology.SwissQrCode, // UTF-8, EC-Level M, Schweizerkreuz in der Mitte
Code = payload,
Unit = BarcodeUnit.Millimeter, Dpi = 203,
BarWidth = 0.5,
};
EPC-QR (GiroCode)
// EPC069-12 (GiroCode / SEPA-Überweisung): Felder werden geprüft (Länge, BIC, Betrag, IBAN ohne Leerzeichen)
string payload = QrExtras.EpcPayload(
name: "Max Mustermann",
iban: "DE89 3704 0044 0532 0130 00",
amount: 12.50m,
bic: "COBADEFFXXX",
remittance: "Rechnung 4711");
var bc = new Barcode { Symbology = Symbology.EpcQrCode, Code = payload, Dpi = 203, BarWidth = 4 };
QrExtras.EpcPayload baut den Payload (Version 002, UTF-8) und prüft Längen, BIC, Betrag und Referenz/Verwendungszweck.
Ultracode (Farbe) & DotCode (Punkte)
var ultra = new Barcode { Symbology = Symbology.Ultracode, Code = "Ultracode 2026", Dpi = 203, BarWidth = 6 };
var dot = new Barcode { Symbology = Symbology.DotCode, Code = "DotCode 2026", Dpi = 203, BarWidth = 6 };
File.WriteAllBytes("ultracode.png", ultra.ToPng()); // farbige Module (Cyan, Magenta, Gelb, Grün …)
File.WriteAllText("dotcode.svg", dot.ToSvg()); // DotCode als runde Punkte

Gedreht & eingefärbt
var bc = new Barcode
{
Symbology = Symbology.Code128,
Code = "ROT-270",
Dpi = 203, BarWidth = 2, BarHeight = 60,
Rotation = 270,
ForeColor = new SKColor(0x0b, 0x6e, 0x4f),
BackColor = new SKColor(0xe8, 0xf7, 0xf0),
TextColor = new SKColor(0x0b, 0x6e, 0x4f),
};
Logo, Modul- und Finder-Formen
QR mit Logo
// logo: z. B. SKBitmap.Decode("logo.png")
var bc = new Barcode
{
Symbology = Symbology.QrCode,
Code = "https://example.com/zplcore",
Dpi = 203, BarWidth = 6,
LogoImage = logo, // QR: Fehlerkorrektur wird auf H angehoben
LogoSizePercent = 22, // Breite in % des Symbols
LogoBackground = true, // Fläche in BackColor hinter dem Logo
FinderShape = FinderShape.Rounded, // Square, Rounded, Circle
ForeColor = new SKColor(0x1d, 0x3b, 0x8a),
};
Runde Module, Punkte, Rauten
var round = new Barcode
{
Symbology = Symbology.QrCode, Code = "Runde Module", Dpi = 203, BarWidth = 6,
ModuleShape = ModuleShape.RoundedSquare, FinderShape = FinderShape.Circle,
};
var dots = new Barcode
{
Symbology = Symbology.DataMatrix, Code = "Punkte 2026", Dpi = 203, BarWidth = 8,
ModuleShape = ModuleShape.Circle, // Square, Circle, RoundedSquare, Diamond
};
var diamond = new Barcode { Symbology = Symbology.Aztec, Code = "Diamond", Dpi = 203, BarWidth = 7, ModuleShape = ModuleShape.Diamond };


Klarschrift, Kodierung & Structured Append
Light Margin Indicator, Textmaske, Ausrichtung
var ean = new Barcode
{
Symbology = Symbology.Ean13, Code = "400638133393", Dpi = 203, BarWidth = 2, BarHeight = 60,
LightMarginIndicator = true, // ‹ › in den Ruhezonen (GS1)
};
var pattern = new Barcode
{
Symbology = Symbology.Code128, Code = "AB1234567", Dpi = 203, BarWidth = 2, BarHeight = 50,
TextFormatPattern = "AA-999 99 99", // Maske: A a 9 # X x * ? übernehmen ein Zeichen
HumanReadableAlignment = HumanReadableAlignment.Justify, // Center, Left, Right, Justify
};
// numerisch auch als Composite-Format: TextFormatPattern = "{0:### ### ###}"

Base64/Hex, Tilde & ECI, QR-Version/-Maske, Structured Append
// Daten als Base64 oder Hex übergeben (Binärdaten)
var b64 = new Barcode { Symbology = Symbology.QrCode, Code = "SGVsbG8gV29ybGQ=", CodeEncoding = CodeEncoding.Base64 };
var hex = new Barcode { Symbology = Symbology.DataMatrix, Code = "48 65 6C 6C 6F", CodeEncoding = CodeEncoding.Hex };
// Tilde-Escapes: ~~ ~dNNN (Byte) ~1 (FNC1) ~2IICC (Structured Append) ~eNNNNNN (ECI)
var eci = new Barcode { Symbology = Symbology.QrCode, Code = "~e000026Gr~d195~d188~d195~d159e", ProcessTilde = true };
// QR fest einstellen: Version 1–40 (0 = automatisch), Maske 0–7 (-1 = automatisch)
var fixedQr = new Barcode { Symbology = Symbology.QrCode, Code = "VERSION 5", QrVersion = 5, QrMask = 3, QrErrorCorrection = 'Q' };
// Structured Append: Daten auf mehrere Symbole verteilen (QR, Data Matrix, Aztec, MaxiCode, PDF417)
var part1 = new Barcode { Symbology = Symbology.QrCode, Code = "Teil eins / ", Dpi = 203, BarWidth = 4 }.StructuredAppend(1, 2);
var part2 = new Barcode { Symbology = Symbology.QrCode, Code = "Teil zwei", Dpi = 203, BarWidth = 4 }.StructuredAppend(2, 2);



Weitere Ausgabeformate
Neben PNG/JPEG/WebP, SVG, PDF, EPS und ^GFA: TIFF, PCX, PCL, EPS in CMYK oder Graustufen und ein fertiges HTML-<img> mit Data-URI.
File.WriteAllBytes("code.tif", bc.ToTiff()); // 1-Bit TIFF (PackBits)
File.WriteAllBytes("code.pcx", bc.ToPcx());
File.WriteAllBytes("code.pcl", bc.ToPcl()); // PCL5-Rasterseite
File.WriteAllText("code-cmyk.eps", bc.ToEps(EpsColorMode.Cmyk)); // Rgb, Cmyk, Gray
string img = bc.ToHtmlImgTag(HtmlImageFormat.Svg, alt: "Barcode"); // <img src="data:image/svg+xml;base64,…">
Größe, Rahmen, Fehlerverhalten & Symbologie-Optionen
Feste Data-Matrix-Größen (inkl. DMRE) und Kodierung, PDF417 Zeilen/Seitenverhältnis/Kompaktierung, Aztec-Format/Lagen/Fehlerkorrektur, QR-Modus (inkl. Kanji), Code-128-Zeichensatz, Han Xin/DotCode/Ultracode-Parameter und die Composite-Wahl CC-A/B/C sind Eigenschaften von Barcode. Dazu Größenmodi (None, Fill, FitProportional, AutoSize), Padding, Rahmen, ErrorBehavior (Ausnahme, leeres Bild oder Bild mit Fehlertext), IsCodeValid und die öffentliche Klasse Checksums.
var bc = new BC.Barcode
{
Symbology = BC.Symbology.DataMatrix, Code = "ZPLCORE-0001",
DataMatrixSize = "16x48", DataMatrixDmre = true,
Sizing = BC.BarcodeSizing.FitProportional, TargetWidth = 300, TargetHeight = 120,
Padding = new BC.BarcodePadding(8), BorderWidth = 2,
ErrorBehavior = BC.BarcodeErrorBehavior.ErrorDescriptionImage,
};
if (!bc.IsCodeValid(out string? error)) Console.WriteLine(error);
int check = BC.Checksums.Mod10("400638133393");
return bc;
TLC39, Deutsche Post BZL, GS1-Maße, ECI & weitere Stellschrauben
Symbology.Tlc39 erzeugt den Code 39 mit 6-stelliger ECI und verknüpftem MicroPDF417 (Teil nach dem Komma, Abstand Tlc39Gap). Symbology.DeutschePostBzl ist experimentell ([Experimental("ZPLBZL001")]): Code 128 der S10-Sendungsnummer mit Prüfziffer. Dazu UseQuietZoneForText (Klarschrift in der Ruhezone), SupplementTopMargin (Add-on-Versatz), MagnificationFactor (GS1-Nennmaß 0,80–2,00 bei EAN/UPC), HanXinMode (erzwungener Modus), MaxiCodePixelAligned (ohne Graustufen), ArtBarImagePattern (Bild als Modulfüllung), ByteEncodingName mit automatischer ECI (Eci.FromEncodingName) und Gs1Builder (GTIN, SSCC, Datum, Maße, Prüfziffer).
// TLC39: Code 39 (6-stellige ECI) + verknüpfter MicroPDF417 nach dem Komma
var tlc = new BC.Barcode { Symbology = BC.Symbology.Tlc39, Code = "123456,ABCD12345678901234", BarWidth = 3, BarHeight = 90, Tlc39Gap = 6 };
#pragma warning disable ZPLBZL001 // experimentell
var bzl = new BC.Barcode { Symbology = BC.Symbology.DeutschePostBzl, Code = "RR34567890DE", BarWidth = 3 }; // Prüfziffer ergänzt
#pragma warning restore ZPLBZL001
var ean = new BC.Barcode
{
Symbology = BC.Symbology.Ean13, Code = "400638133393", Dpi = 600,
MagnificationFactor = 1.0, // GS1-Nennmaß (0,80–2,00)
Supplement = "12", SupplementTopMargin = 40,
};
var compact = new BC.Barcode { Symbology = BC.Symbology.Code128, Code = "QZ", UseQuietZoneForText = true };
var hx = new BC.Barcode { Symbology = BC.Symbology.HanXin, Code = "1234567890", HanXinMode = BC.HanXinMode.Numeric };
var maxi = new BC.Barcode { Symbology = BC.Symbology.MaxiCode, Code = "PIXEL", MaxiCodePixelAligned = true };
var art = new BC.Barcode { Symbology = BC.Symbology.QrCode, Code = "ART", ArtBarImagePattern = pattern };
var cyr = new BC.Barcode { Symbology = BC.Symbology.QrCode, Code = "Привет", ByteEncodingName = "ISO-8859-5" }; // ECI 7
int eci = BC.Eci.FromEncodingName("utf-8"); // 26
string gs1 = BC.Gs1Builder.Build(("01", BC.Gs1Builder.Gtin("950110153000")[4..]), ("10", "LOT"));
string date = BC.Gs1Builder.Date("17", new DateOnly(2026, 12, 31)); // (17)261231
string kg = BC.Gs1Builder.Measure("310", 1.5m, 3); // (3103)001500


Galerie: alle Symbologien
Alle 144 Werte der Enum ZplCore.Barcodes.Symbology, darunter neu Korea Post, Royal Mail Mailmark (4-State), DPD, UPU S10, Flattermarken, BC412, UPNQR, Grid Matrix und Code One sowie die Anwendungsformate (ISBT 128, HIBC, VICS, USPS IMpb …). Jedes Bild ist mit new Barcode { Symbology = …, Code = …, Dpi = 203, BarWidth = 2 (2D: 3), BarHeight = 50 }.ToPng() erzeugt.
07GS1
Alle GS1-Symbologien laufen durch einen gemeinsamen Parser und Validator (portiert aus BWIPPs gs1process, Tabelle mit 534 Application Identifiern): Länge, Zeichensatz, Prüfziffern, Datumsangaben und Pflicht-/Ausschluss-Kombinationen. Eingabe in Klammerform (01)…(10)… oder roh mit FNC1/GS.
// GS1-128: AIs in Klammern; FNC1 und Trennzeichen setzt der Encoder, die AIs werden geprüft
var gs1 = new Barcode { Symbology = Symbology.Gs1_128, Code = "(01)09521234543213(17)261231(10)LOT42", Dpi = 203, BarWidth = 2, BarHeight = 70 };
var sscc = new Barcode { Symbology = Symbology.Sscc18, Code = "09501234567890123", Dpi = 203, BarWidth = 2, BarHeight = 70 }; // Prüfziffer ergänzt
var ean14 = new Barcode { Symbology = Symbology.Ean14, Code = "(01)04601234567893", Dpi = 203, BarWidth = 2, BarHeight = 70 };
var dm = new Barcode { Symbology = Symbology.Gs1DataMatrix, Code = "(01)09521234543213(21)SN12345", Dpi = 203, BarWidth = 5 };
// Composite: "linearer Teil|(AI)2D-Teil" – CC-A/B/C wird automatisch gewählt
var cc = new Barcode { Symbology = Symbology.Ean13Composite, Code = "2112345678900|(99)1234-abcd", Dpi = 203, BarWidth = 2, BarHeight = 60 };
try
{
new Barcode { Symbology = Symbology.Gs1_128, Code = "(01)09521234543213(17)261301" }.ToPng();
}
catch (FormatException ex)
{
Console.WriteLine(ex.Message); // ungültiges Datum in AI 17 (Monat 13)
}
GS1-128

SSCC-18

EAN-14

GS1 Data Matrix

EAN-13 Composite (CC-A)

DataBar Expanded Composite

GS1-128 Composite (CC-C)

"linearer Teil|(AI)…". Unterstützt für EAN-13/8, UPC-A/E, GS1-128 und alle DataBar-Varianten; CC-A, CC-B bzw. CC-C (nur GS1-128) wird nach Datenmenge gewählt. Beim Kodieren führen ungültige Daten zu einer FormatException mit AI und Grund.Parser & Validator direkt nutzen
Gs1.Parse, ParseBracketed, ParseRaw, Validate, ToElementString, ToRaw und NeedsSeparator sind öffentlich; das Ergebnis ist ein Gs1Result mit Elements und Errors. Für QR gibt es außerdem QrExtras.Gs1, SwissQr (Matrix plus Kreuzgeometrie) und Epc.
Gs1Result r = Gs1.Parse("(01)09521234543213(17)261301(10)LOT42"); // Klammerform oder roh mit GS
foreach (Gs1Element e in r.Elements)
Console.WriteLine($"AI {e.Ai} = {e.Value}");
if (!r.IsValid)
foreach (Gs1Error err in r.Errors)
Console.WriteLine(err); // "AI 17: Invalid month" (err.Ai, err.Code, err.Message)
var ok = Gs1.Parse("(01)09521234543213(10)LOT42(17)261231");
string raw = Gs1.ToRaw(ok.Elements); // "0109521234543213" + "10LOT42" + GS + "17261231"
string text = Gs1.ToElementString(ok.Elements);
List<Gs1Error> errors = Gs1.Validate(ok.Elements, lintReqs: true); // inkl. Pflicht-Kombinationen
bool gs = Gs1.NeedsSeparator("10"); // true: variable Länge → FNC1/GS danach
08Text & Formen
^GF versteht ASCII-Hex mit ZPL-Kompression (G–z, ,, !, :), :Z64:, :B64: und Binärdaten; dazu ~DG/^XG, ^IM, ~DY (PNG/GRF), ^ID, ~EG.
09Schriften
Schrift 0 ≈ Swiss 721 Bold Condensed über das eingebettete Roboto Condensed Bold (OFL), kalibriert auf Zebra-Metriken (Versalhöhe 0,735·h, Versaloberkante an ^FO); gemessen innerhalb von 1–3 Punkten der Labelary-Ausgabe. Die Bitmap-Schriften A–H nutzen DejaVu Sans Mono mit Zebra-Vorschub (Zellbreite + Abstand) × Vergrößerung. Eigene TTFs ersetzen sie oder kommen per ^A@ dazu.
var renderer = new ZplRenderer();
FontManager fonts = renderer.Store.Fonts;
fonts.RegisterFont("0", File.ReadAllBytes("MyZebraLike.ttf")); // Schrift 0 ersetzen
fonts.RegisterFont("E:ARIAL.TTF", File.ReadAllBytes("arial.ttf")); // für ^A@N,..,E:ARIAL.TTF
fonts.RegisterFolder("fonts/"); // 0.ttf, A.ttf, ... PLATE.TTF
fonts.LinkFont("E:ARIAL.TTF", "E:NOTOCJK.TTF"); // wie ^FL: Fallback-Glyphen
renderer.ProcessCommands("^XA^CI28^FO50,50^A@N,40,40,E:ARIAL.TTF^FDÄrger? Nö!^FS^XZ");
Download per ZPL
~DYE:MONO,A,T,size,,hex oder ~DUE:MONO.TTF,size,hex lädt eine TTF in den Druckerspeicher; ^A@N,40,40,E:MONO.TTF nutzt sie, ^CW vergibt einen Buchstaben-Alias.
Label-Modell
label.AddFont("MONO.TTF", bytes) bettet die Schrift ein: ToZpl() sendet sie per ~DY, JSON speichert sie mit.
Komplexe Schriften, Bidi & Fallback
Text wird mit HarfBuzz geformt: arabische Ligaturen, hebräische und arabische Rechts-nach-links-Darstellung (Bidi), Devanagari- und Thai-Kombinationszeichen. Fehlen Glyphen, wird automatisch eine passende Systemschrift gesucht (CJK, Thai, Hindi …). ^PA schaltet Shaping/Bidi ab, ^FL (bzw. FontManager.LinkFont) verknüpft eine Erweiterungsschrift mit einer Basisschrift.
^XA^FLE:NOTOCJK.TTF,E:ARIAL.TTF,1^XZ ← Glyphen, die ARIAL fehlen, aus NOTOCJK nehmen ^XA^FLE:NOTOCJK.TTF,E:ARIAL.TTF,0^XZ ← Verknüpfung lösen
~DB Bitmap-Fonts
Mit ~DB heruntergeladene Bitmap-Schriften werden glyphengenau gezeichnet und ganzzahlig vergrößert; nutzbar über ^A@…,R:NAME.FNT oder einen ^CW-Alias.
PrintAsGraphic
Für Drucker ohne Speicher für TTFs: PrintAsGraphic = true rendert das Element (auch Barcodes) in der Vorschau-Engine und sendet es als ^GFA. Pixelgleich zur direkten TTF-Darstellung – im Test verifiziert.
var label = new Label(LabelUnit.Dots, 600, 200) { DownloadFonts = false }
.AddFont("MONO.TTF", File.ReadAllBytes("DejaVuSansMono.ttf"))
.Add(new TextItem
{
X = 20, Y = 20, Font = "E:MONO.TTF", Text = "TTF als Grafik", FontSize = 22,
PrintAsGraphic = true, // Text wird als ^GFA gesendet
});
string zpl = label.ToZpl(); // enthält ^GFA, kein ~DY und kein ^A@
10Label-Modell
ZplCore.Labels.Label beschreibt ein Etikett in Mm, Cm, Inch, Point oder Dots. Daraus entstehen ZPL, Vorschau-PNG/PDF, HTML und JSON.
TextItem
Font, FontSize (pt), Rotation, Width/MaxLines/Alignment (→ ^FB), CounterStep, MaxLength, HideIfEmpty, AutoFit, ValidationRegex, InputMask
BarcodeItem
alle 144 Symbologien (ZPL-Befehl oder ^GFA-Grafik), Generator-Optionen (Farben, Klarschrift, QR-Version/Maske, Modulformen), ModuleWidth, WideRatio, BarHeight, HumanReadable(Above), AddChecksum, QrErrorCorrection
Formen
RectangleItem (CornerRounding, Filled, White), LineItem, EllipseItem, CircleItem
ImageItem
PNG/JPEG → ^GFA, Dither oder Threshold, Width/Height
TableItem · RepeaterItem
Tabellen mit [Feld]-Platzhaltern; Wiederholung pro Element einer Collection
RfidItem · RawZplItem
^RF-Schreibbefehl bzw. ZPL 1:1 als Notausgang
Gemeinsam für alle Elemente: Name, X/Y, Visible, DataField + DataFieldFormat, Expression, Reverse, PrintAsGraphic. Auf Etikettenebene: GapLength, Darkness, PrintSpeed, PrintInverted, PrintMirror, CutAfterPrinting.
var label = new Label(LabelUnit.Mm, 100, 60)
.Add(new RectangleItem { X = 1, Y = 1, Width = 98, Height = 58, StrokeThickness = 0.5, CornerRounding = 2 })
.Add(new TextItem { X = 5, Y = 4, DataField = "Name", FontSize = 16 })
.Add(new TextItem { X = 5, Y = 13, Expression = "\"Lieferant: \" + Upper([Supplier.Name])", FontSize = 9 })
.Add(new TextItem { X = 62, Y = 13, DataField = "Price", DataFieldFormat = "€ 0.00", FontSize = 9 })
.Add(new BarcodeItem { X = 5, Y = 22, DataField = "Sku", Symbology = Symbology.Ean13, BarHeight = 18 })
.Add(new BarcodeItem { X = 70, Y = 22, DataField = "Url", Symbology = Symbology.QrCode, BarHeight = 4 })
.Add(new TextItem { X = 5, Y = 50, FontSize = 9,
Expression = "\"Etikett \" + LABEL_NUMBER + \" · \" + If([Price] > 5, \"PREMIUM\", \"STANDARD\")" });
string zpl = label.ToZpl(products); // ein Etikett pro Datensatz
byte[] preview = label.RenderPng(); // Vorschau ohne Daten
File.WriteAllText("label.json", label.ToJson());
Label copy = Label.FromJson(File.ReadAllText("label.json"));


Generiertes ZPL anzeigen
Barcode-Optionen im Label (BarcodeItem.Options)
BarcodeItemOptions bringt jede Render- und Symbologie-Option des Generators ins Label-Modell (Farben, Ränder, Abstände, Klarschrift-Position, Data-Matrix-Größe, Aztec-Lagen, TLC39-Abstand …) und wird im Label-JSON gespeichert. Ohne gesetzte Option bleibt der native ZPL-Befehl, mit Optionen wird der Barcode als ^GFA-Grafik ausgegeben.
var label = new Label(LabelUnit.Dots, 600, 600).Add(new BarcodeItem
{
X = 20, Y = 20, Symbology = Symbology.DataMatrix, Code = "Label options",
Options = new BarcodeItemOptions // jede Generator-Option, auch im JSON
{
DataMatrixSize = "32x32", BorderColor = "#000000",
PaddingLeft = 8, PaddingTop = 8, PaddingRight = 8, PaddingBottom = 8,
},
}).Add(new BarcodeItem
{
X = 20, Y = 300, Symbology = Symbology.Tlc39, Code = "123456,SERIAL1", ModuleWidth = 3, BarHeight = 90,
Options = new BarcodeItemOptions { TextPosition = BC.BarcodeTextPosition.None, Tlc39Gap = 3 },
});
string zpl = label.ToZpl(); // mit Optionen: ^GFA-Grafik des Generators
Ausdrücke
Felder als [Name] oder [Kunde.Adresse.Ort], Elementnamen ([TextItem1]), Literale, Operatoren + - * / % (+ verkettet Text), = <> < <= > >=, and/or/not. Variablen LABEL_INDEX, LABEL_NUMBER und eigene.
var label = new Label();
label.Expressions.Variables["SHIFT"] = "B";
label.Expressions.Functions["Euro"] = args => $"{ExpressionEngine.Num(args[0]):0.00} €";
label.Add(new TextItem { Expression = "Euro([Price] * 1.2) + \" / Schicht \" + SHIFT" });
label.Add(new TextItem { Y = 8, Expression = "Format(AddDays(Today(), 30), \"dd.MM.yyyy\")" });
label.Add(new TextItem { Y = 16, Expression = "Gs1CheckDigit(\"40063813339\")" });
var engine = new ExpressionEngine();
string s = engine.EvaluateToString("PadLeft(Str(42), 6, \"0\")"); // "000042"
Typen, eigene Funktionen und Variablen
Ausdrücke dürfen öffentliche Member registrierter Typen aufrufen (AddTypeReference) – alles andere bleibt gesperrt. SetCustomFunction/SetCustomVariable sind die verkettbaren Gegenstücke zu Functions[…]/Variables[…].
var label = new Label();
label.Expressions
.AddTypeReference(typeof(Math)) // Math.Max(1, [Qty]) …
.AddTypeReference<DateTime>() // DateTime.Now.AddDays(3)
.SetCustomVariable("WERK", "Graz")
.SetCustomFunction("Kg", args => $"{ExpressionEngine.Num(args[0]):0.0} kg");
label.Add(new TextItem { Expression = "\"Werk \" + WERK + \": \" + Kg([Weight])" });
label.Add(new TextItem { Y = 8, Expression = "Math.Max(1, [Qty]) + \" Stk\"" });
label.Add(new TextItem { Y = 16, Expression = "DateTime.Now.AddDays(3).ToString(\"dd.MM.\")" });
// nicht registrierte Typen sind gesperrt → FormatException
Tabellen & Repeater
var label = new Label(LabelUnit.Mm, 100, 60)
.Add(new TextItem { X = 3, Y = 3, Expression = "\"Lieferschein \" + [No]", FontSize = 14 })
.Add(new TableItem
{
X = 3, Y = 12, ColumnWidths = [50, 44], RowHeights = [7, 7], HeaderRow = true, FontSize = 9,
Cells = [["Kunde", "Datum"], ["[Customer]", "18.10.2026"]],
})
.Add(new RepeaterItem
{
X = 3, Y = 30, OffsetY = 7, MaxCount = 4, DataField = "Lines", // eine Zeile pro Position
Items = [new TextItem { DataField = "Sku" }, new TextItem { X = 60, DataField = "Qty", DataFieldFormat = "0 Stk" }],
});
byte[] png = label.For<Order>().Render([order]).First();

So funktioniert's
Der Repeater liest die Collection Lines des Datensatzes und versetzt seine Kind-Elemente pro Eintrag um OffsetY; Kinder binden an die Member des Eintrags. MaxCount begrenzt die Zeilen.
Validierung, AutoFit, Micro QR, HTML
var label = new Label(LabelUnit.Mm, 100, 60)
.Add(new TextItem { Name = "Plz", DataField = "Plz", ValidationRegex = @"^\d{4,5}$" })
.Add(new TextItem { Name = "Lot", X = 30, DataField = "Lot", InputMask = "AA-0000", MaxLength = 7 })
.Add(new TextItem { Y = 8, DataField = "Hinweis", HideIfEmpty = true })
.Add(new TextItem { Y = 16, DataField = "Name", Width = 90, FontSize = 20, AutoFit = true })
.Add(new BarcodeItem { Y = 30, Code = "01234567", Symbology = Symbology.MicroQr, BarHeight = 4 })
.Add(new RfidItem { Data = "3034257BF7194E4000001A85" });
var record = new Dictionary<string, object?> { ["Plz"] = "x", ["Lot"] = "AB-12", ["Name"] = "Sehr langer Produktname" };
foreach (string error in label.Validate(record))
Console.WriteLine(error); // "Plz: 'x' entspricht nicht ^\d{4,5}$" ...
File.WriteAllText("vorschau.html", label.ToHtml(records: [record]));
Masken-Zähler (Mask / MaskIncrement)
Zähler mit gemischten Stellen: Ziffern, Buchstaben und alphanumerische Positionen bilden eine Zahl mit gemischter Basis – der Übertrag läuft von 9999 in die Buchstaben, am Ende beginnt die Maske wieder von vorn. Das Inkrement ist "+1", "-2" oder positionsweise wie "0010". Funktioniert im ZPL-, EPL- und Fingerprint-Export.
var label = new Label(LabelUnit.Mm, 50, 25)
.Add(new TextItem { X = 3, Y = 3, Text = "Charge", FontSize = 9 })
// Maske: # / 0 Ziffer, A Buchstabe, a Kleinbuchstabe, ? alphanumerisch, sonst Literal
.Add(new TextItem { X = 3, Y = 9, Text = "AZ-9998", Mask = "AA-0000", MaskIncrement = "+1", FontSize = 16 })
// Inkrement pro Stelle: "0010" zählt die Zehnerstelle hoch (mit Übertrag)
.Add(new TextItem { X = 30, Y = 3, Text = "0100", Mask = "####", MaskIncrement = "0010", FontSize = 9 });
string zpl = label.ToZpl(); // AZ-9998, AZ-9999, BA-0000 … (Übertrag in die Buchstaben)

Etikettenbögen (LabelSheet)
LabelSheet setzt Etiketten auf A4-Bürobögen oder breite Medien: Zeilen, Spalten, Ränder, Abstände, Reihenfolge (RowWise/ColumnWise) und eine Startposition für angebrochene Bögen. Ausgabe als PNG pro Bogen, PDF oder ZPL (nebeneinander auf breiten Medien), speicherbar als JSON.
var label = new Label(LabelUnit.Mm, 45, 25)
.Add(new RectangleItem { X = 0.5, Y = 0.5, Width = 44, Height = 24, CornerRounding = 2 })
.Add(new TextItem { X = 3, Y = 3, DataField = "Sku", FontSize = 11 })
.Add(new BarcodeItem { X = 3, Y = 10, DataField = "Sku", BarHeight = 10, HumanReadable = false });
var sheet = new LabelSheet
{
PageWidth = 100, PageHeight = 85, Rows = 3, Columns = 2, // mm, kleiner Bogen
MarginLeft = 4, MarginTop = 4, HorizontalGap = 2, VerticalGap = 2,
Order = LabelSheetOrder.RowWise,
StartPosition = 2, // angebrochenen Bogen weiterverwenden
};
var items = Enumerable.Range(1, 5).Select(i => new { Sku = $"ART-{i:000}" });
List<byte[]> pages = sheet.ToPng(label, items); // ein PNG pro Bogen
byte[] pdf = sheet.ToPdf(label, items); // Bürodrucker
string json = sheet.ToJson();

Mehrseitige Etiketten & Duplex (Label.Pages)
Items ist Seite 1, AddPage() fügt weitere Seiten hinzu (Seite 2 = Rückseite bei Duplexdruckern). Jede Seite wird ein eigenes Format; mit UseStoredFormat bekommt jede Seite ein eigenes ^DF-Format (R:NAME2.ZPL …).
var label = new Label(LabelUnit.Mm, 80, 50)
.Add(new TextItem { X = 4, Y = 4, Text = "Vorderseite", FontSize = 16 })
.Add(new BarcodeItem { X = 4, Y = 16, DataField = "Sku", BarHeight = 14 });
label.AddPage("Rückseite") // Seite 2 = Rückseite bei Duplexdruckern
.Add(new TextItem { X = 4, Y = 4, Text = "Rückseite", FontSize = 16 })
.Add(new TextItem { X = 4, Y = 16, Expression = "\"Artikel \" + [Sku]" });
int count = label.PageCount; // 2 – jede Seite wird ein eigenes Format
string zpl = label.ToZpl(new[] { new { Sku = "4006381333931" } });
Seite 1

Seite 2

Duplex, Rahmen pro Seite, Tabellen-Ecken, Zähler & Eingabemasken
Duplex-Konvention: Label.Duplex gibt jede Kopie als Paar Vorderseite/Rückseite aus (^FXDUPLEX FRONT/BACK, fehlende Rückseite leer). FrameThickness setzt Rahmen pro Seite, TableItem.CornerRounding runde Ecken, CounterUseLeadingZeros steuert führende Nullen, InputMaskPromptChar den Platzhalter und ValidationErrorMessage eine eigene Meldung ({name}, {value}). Das Label-JSON hat schemaVersion 3; Version 2 wird beim Laden migriert.
var label = new Label(LabelUnit.Mm, 50, 30) { Duplex = true }; // Vorder-/Rückseite als Paare
label.Add(new TextItem { Text = "FRONT", Width = 40, Height = 8,
FrameThickness = new FrameThickness { Top = 0.5, Bottom = 0.3 } }); // Rahmen pro Seite
label.AddPage("back").Items.Add(new TextItem { Text = "BACK" });
label.Add(new TableItem { Y = 12, ColumnWidths = [20, 20], RowHeights = [6], CornerRounding = 4, Cells = [["a", "b"]] });
label.Add(new TextItem { Y = 20, Text = "A0001", CounterStep = 1, CounterUseLeadingZeros = true });
label.Add(new TextItem
{
Name = "Lot", Y = 25, Text = "AB-1234", InputMask = "AA-0000", InputMaskPromptChar = '_',
ValidationErrorMessage = "{name} ist ungültig: {value}",
});
IReadOnlyList<string> errors = label.Validate();
string zpl = label.ToZpl(copies: 2); // FRONT, BACK, FRONT, BACK
Element-Referenzen & Mehrsprachigkeit
Ausdrücke und Tabellenzellen greifen mit [Items.Name] bzw. [Items.Name.Text] auf den Text anderer Elemente zu (auch vorwärts; Zirkelbezüge ergeben ""). Mehrsprachige Vorlagen: Label.Strings (per AddString), Label.Culture und Platzhalter {t:key}; Auflösung de-CH → de → "" → Schlüssel; pro Druckauftrag mit LabelBinding.WithCulture.
var label = new Label(LabelUnit.Dots, 400, 200)
.Add(new TextItem { Name = "Total", Expression = "\"Summe: \" + [Items.Price.Text]" })
.Add(new TextItem { Name = "Price", Y = 50, Expression = "Format(2 * 21, \"0.00\", \"de-DE\")" })
.Add(new TableItem { Y = 100, ColumnWidths = [200], RowHeights = [30], Cells = [["[Items.Total]"]] });var l = new Label(LabelUnit.Dots, 400, 200) { Culture = "de-CH" }
.AddString("", "weight", "Weight") // Fallback
.AddString("de", "weight", "Gewicht") // de-CH → de → "" → Schlüssel
.AddString("fr", "weight", "Poids");
l.Add(new TextItem { Text = "{t:weight}: 5 kg" });
string de = l.ToZpl(); // Gewicht: 5 kg
string fr = l.For<Dictionary<string, object?>>().WithCulture("fr-FR").ToZpl([new()]); // Poids: 5 kgText-Größen, Stile, Bilder, Formen, RFID & Schema-Version
TextItem.Sizing wählt zwischen fester Größe, ShrinkToFit und FitToBox (nativ per ^FB), Stretch, Arc/OuterArc (als Grafik) und gestapelten Zeichen (^FPV). Dazu vertikale Ausrichtung, Padding, Zeichenabstand, Vorder-/Hintergrund, Rahmen, Outline, Fett/Kursiv/Unterstrichen/Durchgestrichen, Null mit Schrägstrich, RTL und Kultur. ImageItem skaliert mit Seitenverhältnis, spiegelt, dithert (Threshold, Floyd-Steinberg, Atkinson, Bayer, Otsu) und kann die Grafik einmal im Druckerspeicher ablegen. Formen haben Strichstile (Solid, Dash, Dot, DashDot), Füllung und Eckradien je Ecke; Tabellen Zeilen-/Spaltenfüllungen und einzeln abschaltbare Linien. RfidItem schreibt EPC/TID/User-Speicher mit Tag-Typ, Passwörtern und Sperren. Jedes Element kann eine eigene Einheit haben; das JSON trägt schemaVersion 3 und wird beim Laden migriert.
var label = new Label(LabelUnit.Mm, 100, 60)
.Add(new TextItem
{
Name = "title", X = 5, Y = 5, Width = 90, Height = 12, Text = "Sehr langer Artikelname",
Sizing = TextSizing.ShrinkToFit, // auch FitToBox, Stretch, Arc, OuterArc …
VerticalAlignment = TextVerticalAlignment.Middle, Padding = new ItemPadding(1),
Bold = true, Underline = true, CharSpacing = 0.2,
ForeColor = ItemColor.White, Background = ItemColor.Black, BorderThickness = 0.3,
})
.Add(new ImageItem
{
X = 70, Y = 20, Width = 25, LockAspectRatio = ImageAspectRatio.Width,
ImageData = logoPng, DitherMethod = DitherMethod.Atkinson, // Threshold, FloydSteinberg, Bayer, Otsu
UsePrinterStorage = true, StorageName = "R:LOGO.GRF", // ~DG einmal, danach ^XG
})
.Add(new RectangleItem { X = 5, Y = 20, Width = 60, Height = 30, StrokeStyle = StrokeStyle.Dash,
CornerRadius = new CornerRadius(2), Fill = ItemColor.None })
.Add(new BarcodeItem { X = 8, Y = 24, Symbology = Symbology.Gs1DataMatrix, // alle 142 Symbologien
Code = "(01)09521234543213(10)LOT42", ModuleWidth = 4 })
.Add(new RfidItem { Data = "3034257BF7194E4000001A85", MemoryBank = RfidMemoryBank.Epc,
TagType = RfidTagType.EpcClass1Gen2, AccessPassword = "12345678",
MemoryBankLock = RfidLockValue.Lock });
string json = label.ToJson(); // "schemaVersion": 3; ältere JSONs werden beim Laden migriert
var copy = Label.FromJson(json);
var inch = label.ConvertToUnit(LabelUnit.Inch);
return label.ToZpl(dpi: 300);
11Massendaten-Bindung
label.For<T>() liefert eine typisierte, streamende LabelBinding<T>: funktioniert mit Listen, LINQ, EF Core (IQueryable oder AsAsyncEnumerable()), LiteDB, DataRow und Dictionaries – Millionen Datensätze, ohne sie im Speicher zu halten.
var binding = label.For<OrderRec>()
.Map("Total", o => o.Price * o.Quantity) // berechnetes Feld (kompilierter Lambda)
.Copies(o => o.Quantity) // ^PQ pro Datensatz
.UseStoredFormat(); // ^DF einmal, danach nur ^XF + ^FN-Werte
// DataField = "Customer.Address.City" wird per PropertyPath aufgelöst
string zpl = binding.ToZpl(orders);
foreach (string chunk in binding.Generate(orders)) { /* lazy, Datensatz für Datensatz */ }
foreach (string batch in binding.Batches(orders, labelsPerBatch: 500)) { /* Druckjobs */ }
await using var file = new StreamWriter("job.zpl");
await binding.WriteZplAsync(stream, file); // IAsyncEnumerable
int sent = await binding.PrintAsync(new ZplPrinterClient("192.168.1.50"), stream, labelsPerBatch: 500);
IEnumerable<byte[]> pngs = binding.Render(orders.Take(3));
byte[] pdf = binding.RenderPdf(orders.Take(100));
| Methode | Zweck |
|---|---|
Map(field, o => …) / MapItem(name, …) | Feld oder benanntes Element an kompilierten Lambda binden |
Copies(o => n) | Anzahl Kopien pro Datensatz (^PQ) |
UseStoredFormat(name) | Vorlage einmal per ^DF, danach nur ^XF/^FN-Werte – typisch 80–95 % weniger Daten |
Across(columns, gap) | mehrere Datensätze nebeneinander (mehrbahnige Rollen) |
ToZpl / WriteZpl / WriteZplAsync | komplett als String oder in einen TextWriter |
Generate / GenerateAsync | lazy, ein ZPL-Format pro Datensatz |
Batches(records, n) | Druckjobs zu je n Etiketten |
PrintAsync(printer, records, n) | direkt an ZplPrinterClient senden, liefert Anzahl |
Render / RenderPdf | Vorschau als Bilder bzw. PDF |
DataField = "Customer.Address.City" navigiert durch Properties, Felder, Dictionaries, DataRow, JsonElement und anonyme Typen. Eigene Wrapper-Typen (z. B. BsonValue) entpackt PropertyPath.RegisterUnwrapper<T>().EF Core + SQLite
Aus dem Test DatabaseBindingTests: 2000 Sendungen in SQLite, Navigation-Properties, asynchrones Streaming mit gespeichertem Format – der letzte Barcode wird gerendert und mit ZXing dekodiert.
var label = new Label(LabelUnit.Mm, 100, 60)
.Add(new TextItem { X = 3, Y = 3, DataField = "Customer.Name", FontSize = 14 })
.Add(new TextItem { X = 3, Y = 12, DataField = "Customer.City" })
.Add(new TextItem { X = 60, Y = 12, DataField = "Weight", DataFieldFormat = "0.0 kg" })
.Add(new BarcodeItem { X = 3, Y = 22, DataField = "TrackingNo", BarHeight = 15 })
.Add(new TextItem { X = 3, Y = 50, Expression = "\"Paket \" + LABEL_NUMBER" });
var options = new DbContextOptionsBuilder<ShippingDb>().UseSqlite("Data Source=shipping.db").Options;
await using var db = new ShippingDb(options);
// Streaming über alle offenen Sendungen – nie alles im Speicher
var query = db.Shipments.Include(s => s.Customer)
.Where(s => !s.Printed && s.Customer.City == "Wien")
.OrderBy(s => s.TrackingNo)
.AsNoTracking()
.AsAsyncEnumerable();
await using var output = new StreamWriter("versand.zpl");
await label.For<Shipment>()
.Copies(s => s.Parcels)
.UseStoredFormat()
.WriteZplAsync(query, output);
LiteDB
using var db = new LiteDatabase("produkte.db");
PropertyPath.RegisterUnwrapper<BsonValue>(v => v.RawValue); // BsonValue → .NET-Wert
var label = new Label(LabelUnit.Mm, 60, 40)
.Add(new TextItem { X = 2, Y = 2, DataField = "Name" })
.Add(new TextItem { X = 2, Y = 9, DataField = "Supplier.Name" })
.Add(new TextItem { X = 40, Y = 9, DataField = "Price", DataFieldFormat = "0.00" })
.Add(new BarcodeItem { X = 2, Y = 16, DataField = "Sku", BarHeight = 12 });
// typisiert mit LINQ
var typed = db.GetCollection<LiteProduct>("products").Query()
.Where(p => p.Price > 400 && p.Supplier.Name == "ACME")
.OrderBy(p => p.Price)
.ToEnumerable();
string zpl = label.For<LiteProduct>().ToZpl(typed);
// untypisiert: BsonDocument, verschachtelte Pfade funktionieren genauso
var docs = db.GetCollection("products").Find(Query.EQ("Supplier.Name", "Contoso"));
string zpl2 = label.For<BsonDocument>().ToZpl(docs);
Mehrbahnige Etiketten
var small = new Label(LabelUnit.Mm, 30, 20)
.Add(new TextItem { X = 2, Y = 2, DataField = "Sku", FontSize = 9 })
.Add(new BarcodeItem { X = 2, Y = 7, DataField = "Sku", Symbology = Symbology.DataMatrix });
// 3 Etiketten nebeneinander auf einem 96 mm breiten Trägerband, 3 mm Abstand
string zpl = small.For<Line>().Across(3, gap: 3).ToZpl(lines);

JSON- & dynamische Records, Datensatzanzahl, Stream/PipeWriter, Ausdrücke
Ohne eigene Klasse binden: JsonElement, JsonNode, ExpandoObject, Dictionaries, DataRow und LiteDB-BsonDocument (Pfade wie order.no). Ist die Anzahl bekannt (WithRecordCount, CountQueryableWith oder recordCount:), stehen TOTAL_PAGES und PrintJobInfo.RecordCount schon ab dem ersten Etikett zur Verfügung. WriteZplAsync schreibt UTF-8 direkt in einen Stream oder PipeWriter (ASP.NET Core). Variablen: LABEL_INDEX, LABEL_NUMBER, LABEL_WIDTH, LABEL_HEIGHT, RECORD_INDEX, PAGE_NUMBER, TOTAL_PAGES plus eigene. Funktionen u. a. Upper Lower Left Right Mid Substring Trim PadLeft Replace Contains If Coalesce IsEmpty IsNumeric Format ToNumber ToDate Today Now AddDays Round Min Max Mod10 Mod11 Mod43 Luhn Gs1CheckDigit VoicePickCode. Zahlen und Datumswerte nutzen Expressions.Culture (Standard: invariant); Format/ToNumber nehmen optional einen Kulturnamen.
// Records ohne eigene Klasse: JsonElement, JsonNode, ExpandoObject, Dictionaries, DataRow, BsonDocument
using var doc = JsonDocument.Parse("""[{"name":"Anna","order":{"no":"A-1"}},{"name":"Ben","order":{"no":"A-2"}}]""");
string zpl = label.For<JsonElement>().ToZpl(doc.RootElement.EnumerateArray());
dynamic d = new ExpandoObject(); d.name = "Cleo";
string one = label.For<ExpandoObject>().ToZpl([(ExpandoObject)d]);
// Bekannte Anzahl → TOTAL_PAGES/RecordCount schon beim ersten Etikett; UTF-8 direkt in Stream oder PipeWriter
var binding = label.For<JsonElement>().WithRecordCount(2);
await binding.WriteZplAsync(rows, response, dpi: 203, recordCount: 2);
await binding.WriteZplAsync(rows, pipe);
// Ausdrücke: Funktionen, Variablen und Kultur
label.Expressions.Culture = System.Globalization.CultureInfo.GetCultureInfo("de-DE");
label.Add(new TextItem { X = 5, Y = 50,
Expression = "\"Seite \" + PAGE_NUMBER + \"/\" + TOTAL_PAGES + \" · \" + Format(Today(), \"d\") + \" · \" + Upper(Left(name, 3))" });
12Druckaufträge & Massendruck
Ein Druckauftrag ist ein ZPL-Strom: optional ein Vorlauf-Etikett, ein Etikett pro Datensatz und optional ein Nachlauf-Etikett. Mit CutAfterLastLabel() läuft der ganze Auftrag im Abreißmodus und nur nach dem letzten Etikett wird geschnitten.
Vorlauf, Nachlauf und Schnitt (LabelBinding)
// Vor- und Nachlauf-Etikett werden an PrintJobInfo gebunden:
// JobName, Created, RecordCount, LabelCount, FirstKey, LastKey
var leading = new Label(LabelUnit.Mm, 100, 60)
.Add(new TextItem { X = 5, Y = 5, Text = "DRUCKAUFTRAG", FontSize = 22 })
.Add(new TextItem { X = 5, Y = 20, DataField = "JobName", FontSize = 14 })
.Add(new TextItem { X = 5, Y = 32, Expression = "Format([Created], \"dd.MM.yyyy HH:mm\")" })
.Add(new TextItem { X = 5, Y = 42, Expression = "[RecordCount] + \" Sendungen\"" });
var trailing = new Label(LabelUnit.Mm, 100, 60)
.Add(new TextItem { X = 5, Y = 5, Text = "ENDE DES AUFTRAGS", FontSize = 18 })
.Add(new TextItem { X = 5, Y = 20, Expression = "[LabelCount] + \" Labels gedruckt\"" })
.Add(new TextItem { X = 5, Y = 30, Expression = "\"von \" + [FirstKey] + \" bis \" + [LastKey]", FontSize = 9 });
var label = new Label(LabelUnit.Mm, 100, 60)
.Add(new TextItem { X = 4, Y = 4, DataField = "Name", FontSize = 14 })
.Add(new TextItem { X = 4, Y = 12, DataField = "City" })
.Add(new BarcodeItem { X = 4, Y = 22, DataField = "TrackingNo", BarHeight = 18 });
string zpl = label.For<Parcel>()
.Job("Versand Wien", p => p.TrackingNo) // Name + Schlüssel für FirstKey/LastKey
.Copies(p => p.Parcels) // LabelCount zählt die Kopien mit
.WithLeadingLabel(leading)
.WithTrailingLabel(trailing)
.CutAfterLastLabel() // 1. Format ^MMT (Abreißen), letztes ^MMC (Schneiden)
.ToZpl(parcels); // Liste → RecordCount schon im Vorlauf bekannt
| Methode / Feld | Bedeutung |
|---|---|
.Job(name, keySelector) | Auftragsname und Schlüssel pro Datensatz (z. B. Sendungsnummer) für FirstKey/LastKey |
.WithLeadingLabel(label) | Vorlauf-Etikett, gebunden an PrintJobInfo |
.WithTrailingLabel(label) | Nachlauf-Etikett mit Summen, gebunden an PrintJobInfo |
.CutAfterLastLabel() | erstes Format bekommt ^MMT (Abreißen, gilt für alle folgenden), nur das letzte Format ^MMC (Schneiden) |
PrintJobInfo.JobName, Created | Name und Erstellzeit des Auftrags |
PrintJobInfo.RecordCount | Anzahl Datensätze – im Vorlauf bekannt, wenn eine Liste/Collection übergeben wird; im Nachlauf immer gesetzt |
PrintJobInfo.LabelCount | gedruckte Etiketten inkl. Kopien (Copies) |
PrintJobInfo.FirstKey, LastKey | Schlüssel des ersten und letzten Datensatzes |
Alle Felder sind als DataField oder in Ausdrücken ([RecordCount] + " Sendungen") nutzbar. Das Ergebnis für drei Sendungen (eine davon mit zwei Kopien): 5 Formate, 6 Etiketten.
Vorlauf

Datensatz 2 (^PQ2)

Nachlauf (^MMC)

Erzeugtes ZPL anzeigen
PrintJobTemplate: ein Auftrag aus JSON-Vorlagen
PrintJobTemplate fasst Vorlauf-, Datensatz- und Nachlauf-Etikett samt Optionen zusammen. Laden aus drei Label-Vorlagen (FromFiles(leading, label, trailing), Vor-/Nachlauf optional) oder aus einer JSON-Datei (ToJson/FromJson); SaveFiles zerlegt wieder in drei Dateien. For<T>() liefert die fertig konfigurierte LabelBinding<T>.
// drei JSON-Vorlagen (z. B. aus einem Designer) → EIN Druckauftrag
var job = PrintJobTemplate.FromFiles("templates/leading.json", "templates/shipping.json", "templates/trailing.json");
job.JobName = "Tagesversand";
job.CutAfterLastLabel = true; // nur nach dem Nachlauf-Etikett schneiden
job.StoredFormatName = "R:SHIP.ZPL"; // Vorlage einmal per ^DF, danach nur ^XF + ^FN-Werte
job.KeyField = "TrackingNo"; // FirstKey/LastKey im Nachlauf
job.CopiesField = "Parcels"; // ^PQ pro Datensatz
string zpl = job.For<Shipment>().ToZpl(shipments.ToList());
// derselbe Auftrag als EINE JSON-Datei – oder wieder in drei Dateien zerlegt
File.WriteAllText("printjob.json", job.ToJson());
PrintJobTemplate same = PrintJobTemplate.FromJson(File.ReadAllText("printjob.json"));
same.SaveFiles("leading.json", "shipping.json", "trailing.json");
| Option | Wirkung |
|---|---|
JobName | Name für PrintJobInfo.JobName (Standard „Druckauftrag“) |
CutAfterLastLabel | ^MMT für alle, ^MMC nur für das letzte Etikett |
StoredFormatName | Datensatz-Etikett einmal per ^DF speichern, dann nur ^XF + ^FN |
KeyField | Property-Pfad für FirstKey/LastKey |
CopiesField | Property-Pfad für die Kopienzahl pro Datensatz |
leading.json (Auszug)
printjob.json – alles in einer Datei (Auszug)
Vorlagen: leading.json · shipping.json · trailing.json · printjob.json
Massendaten-Demo: EF Core + LiteDB → eine .zpl-Datei
samples/MassDataDemo lädt die drei Vorlagen, fragt 50 Sendungen per LINQ aus SQLite (EF Core) bzw. LiteDB ab und schreibt jeweils einen Auftrag: Vorlauf, ^DF-Vorlage, 50 Versandetiketten, Nachlauf, Schnitt.
// samples/MassDataDemo: 50 Versandetiketten + Vorlauf + Nachlauf + Schnitt in EINER .zpl-Datei
var job = PrintJobTemplate.FromFiles("templates/leading.json", "templates/shipping.json", "templates/trailing.json");
job.CutAfterLastLabel = true; // Abreißen für alle, Schnitt nach dem Nachlauf-Etikett
job.KeyField = "TrackingNo"; // erste/letzte Sendungsnummer im Nachlauf
// ---------- EF Core + SQLite ----------
using var connection = new SqliteConnection("Data Source=:memory:");
connection.Open();
var options = new DbContextOptionsBuilder<ShippingDb>().UseSqlite(connection).Options;
using var db = new ShippingDb(options);
db.Database.EnsureCreated();
db.Shipments.AddRange(seedData);
db.SaveChanges();
var query = db.Shipments.AsNoTracking() // von SQLite ausgeführt
.Where(s => !s.Printed && s.Service == "EXPRESS")
.OrderByDescending(s => s.Id)
.Take(50);
job.JobName = "EF Core – Express";
job.StoredFormatName = "R:SHIP.ZPL"; // Vorlage einmal, dann nur Feldwerte
string efZpl = job.For<Shipment>()
.Map("Total", s => s.WeightKg.ToString("0.0") + " kg")
.ToZpl(query.ToList());
File.WriteAllText("massdata-efcore.zpl", efZpl);
// ---------- LiteDB ----------
using var lite = new LiteDatabase(new MemoryStream());
var col = lite.GetCollection<Shipment>("shipments");
col.InsertBulk(seedData);
col.EnsureIndex(s => s.Country);
var docs = col.Query().Where(s => s.Country == "AT").OrderBy(s => s.TrackingNo).Limit(50).ToEnumerable();
job.JobName = "LiteDB – Österreich";
job.StoredFormatName = null; // volles Format pro Etikett
string liteZpl = job.For<Shipment>()
.Map("Total", s => s.WeightKg.ToString("0.0") + " kg")
.ToZpl(docs.ToList());
File.WriteAllText("massdata-litedb.zpl", liteZpl);
// Vorschau des ganzen Auftrags als PDF
var renderer = new ZplRenderer(new ZplRenderSettings { OutputFormat = ZplOutputFormat.Pdf }.WithLabelSizeMm(100, 60));
File.WriteAllBytes("massdata-efcore.pdf", renderer.ProcessCommands(efZpl)[0]);
dotnet run --project samples/MassDataDemo -- samples/output samples/templates
Vorlauf (^MMT)

Versandetikett 1 von 50

Nachlauf (^MMC)

massdata-efcore.zpl (Auszug)
^DF/^XF) · massdata-efcore.pdf · massdata-litedb.zpl (17 KB, volle Formate) · massdata-litedb.pdf – je 52 Etiketten.13Drucken
ZplPrinterClient sendet Raw-TCP an Port 9100 und fragt den Status per ~HS ab (ZplPrinterStatus: PaperOut, Paused, HeadOpen, RibbonOut, OverTemperature, UnderTemperature, LabelsRemainingInBatch, FormatsInBuffer).
var printer = new ZplPrinterClient("192.168.1.50") // Port 9100 (Raw)
{
Timeout = TimeSpan.FromSeconds(5),
};
ZplPrinterStatus status = await printer.GetStatusAsync(); // ~HS
if (!status.IsReady)
throw new InvalidOperationException(
$"Papier leer: {status.PaperOut}, Kopf offen: {status.HeadOpen}, Pause: {status.Paused}");
await printer.SendAsync(zpl);
await printer.SendAsync(label, dpi: 203, copies: 5);
Verbindungen: IPrinterConnection
Alle Transporte implementieren IPrinterConnection (SendAsync(byte[]), QueryAsync); Erweiterungsmethoden senden ZPL-Strings und Labels und fragen per GetStatusAsync() den ~HS-Status ab. PrinterSettings + PrinterConnectionFactory.Create machen den Transport konfigurierbar.
// ein Interface für alle Transportwege
await using IPrinterConnection printer = PrinterConnectionFactory.Create(new PrinterSettings
{
CommunicationType = CommunicationType.Network, // Network, Serial, Usb, File, WindowsDriver, Cups, Stream
Host = "192.168.1.50", Port = 9100,
Timeout = TimeSpan.FromSeconds(5),
});
ZplPrinterStatus? status = await printer.GetStatusAsync(); // ~HS (null bei reinen Schreib-Transporten)
await printer.SendAsync(zpl);
await printer.SendAsync(label, dpi: 203, copies: 2);
// direkt erzeugt
IPrinterConnection tcp = new TcpPrinterConnection("192.168.1.50");
IPrinterConnection serial = new SerialPrinterConnection("COM3", 9600, Parity.None, 8, StopBits.One, Handshake.XOnXOff);
IPrinterConnection usb = new RawFilePrinterConnection("/dev/usb/lp0"); // oder \\.\USB001, Datei
IPrinterConnection cups = new CupsPrinterConnection("Zebra_ZD421") { JobTitle = "Versand" };
IPrinterConnection mem = new StreamPrinterConnection(new MemoryStream()); // Tests
if (OperatingSystem.IsWindows())
{
IPrinterConnection spooler = new WindowsSpoolerPrinterConnection("ZDesigner ZD421-203dpi", documentName: "Versand");
}
| Klasse | CommunicationType | Status (~HS) |
|---|---|---|
TcpPrinterConnection | Network (Raw, Port 9100) | ja |
SerialPrinterConnection | Serial (Baud, Parität, Handshake) | ja |
RawFilePrinterConnection | Usb, File (Gerätepfad oder Datei) | nein |
WindowsSpoolerPrinterConnection | WindowsDriver (RAW-Druckjob, nur Windows) | nein |
CupsPrinterConnection | Cups (lp -o raw) | nein |
StreamPrinterConnection | Stream (beliebiger Stream, Tests) | wenn lesbar |
Virtueller Drucker (TCP 9100)
ZplVirtualPrinter lauscht auf dem Raw-Port, rendert eingehende ZPL- und EPL-Jobs (EPL wird erkannt) und speichert jedes Etikett als PNG oder PDF – ideal für Tests von Anwendungen, die sonst an einen Zebra drucken. Statusabfragen werden mit einem bereiten Drucker beantwortet (HostQueryHandler für eigene Antworten). Als Konsolenprogramm: dotnet run --project samples/VirtualPrinter -- --port 9100 --out labels [--pdf] [--any].
await using var printer = new ZplVirtualPrinter(9100, IPAddress.Loopback)
{
RenderSettings = new ZplRenderSettings { Dpi = 203, OutputFormat = ZplOutputFormat.Png },
OutputDirectory = "labels", // jedes Etikett als labels/*.png
};
printer.LabelReceived += (_, e) => Console.WriteLine($"Etikett #{e.Index} → {e.FilePath}");
printer.Start(); // beantwortet ~HS, ~HI und ~HQ (ES, SN, OD …)
Druckeinstellungen, erweiterter Status, Suche & Trockenlauf
ZplJobSettings erzeugt ^MM (Abreißen, Spenden, Schneiden, RFID …), ^MN (Lücke, Marke, Endlos), ^MT, ^LL/^ML, ^MD, ^PR und die Ausrichtung und fügt sie nach jedem ^XA ein. GetExtendedStatusAsync wertet ~HQES aus (Fehler- und Warnungs-Flags), dazu Seriennummer, Odometer und ~HI. PrinterDiscovery.FindUsbPrintersAsync findet USB-Drucker; mit EnablePrinting = false liefert die Fabrik eine DryRunPrinterConnection, die nur protokolliert. Weitere Transporte: TCP, seriell, USB/Datei, Parallelport, Windows-Spooler, CUPS, Stream.
var settings = new PrinterSettings
{
Host = "127.0.0.1", Port = 9100,
EnablePrinting = true, // false = Trockenlauf (DryRunPrinterConnection)
Job = new ZplJobSettings
{
PrintMode = PrintMode.Cutter, // ^MMC
MediaTracking = MediaTracking.Gap, // ^MNY
MediaType = MediaType.DirectThermal, // ^MTD
Darkness = 20, PrintSpeed = 4, // ^MD, ^PR
Orientation = JobOrientation.Portrait180,
},
};
await using IPrinterConnection conn = PrinterConnectionFactory.Create(settings);
await conn.SendAsync(settings.Job.Apply(zpl));
ZplExtendedStatus? status = await conn.GetExtendedStatusAsync(); // ~HQES
if (status is { IsReady: false }) Console.WriteLine(status.Errors);
IReadOnlyList<DiscoveredPrinter> usb = await PrinterDiscovery.FindUsbPrintersAsync();
14Export in andere Formate & Sprachen
RasterExport
Gerenderte Bitmaps als PCX, mehrseitiges TIFF (PackBits/CCITT), EPL2-Grafik, Fingerprint, ESC/POS (Rasterbild + Schnitt) und PCL.
LabelLanguageWriter
Das Label-Modell nativ als EPL, Fingerprint, ESC/POS oder PCL – Text und Barcodes bleiben Druckerbefehle.
var renderer = new ZplRenderer();
var results = renderer.Render(File.ReadAllText("labels.zpl"));
SKBitmap bmp = results[0].Bitmap;
File.WriteAllBytes("label.pcx", RasterExport.Pcx(bmp));
File.WriteAllBytes("labels.tif", RasterExport.Tiff(results.Select(r => r.Bitmap), TiffCompression.PackBits));
File.WriteAllBytes("label.epl", RasterExport.Epl(bmp)); // EPL2 GW-Grafik
File.WriteAllBytes("label.fp", RasterExport.Fingerprint(bmp)); // Honeywell/Intermec
File.WriteAllBytes("label.escpos", RasterExport.EscPos(bmp, cut: true));
File.WriteAllBytes("label.pcl", RasterExport.Pcl(bmp, dpi: 300));
// native Befehle aus dem Label-Modell (Text bleibt Text, Barcodes bleiben Barcodes)
string epl = LabelLanguageWriter.ToEpl(label);
string fingerprint = LabelLanguageWriter.ToFingerprint(label);
byte[] escpos = LabelLanguageWriter.ToEscPos(label);
byte[] pcl = LabelLanguageWriter.ToPcl(label);
EPL und Fingerprint mit Datensätzen
Mit Datensätzen erzeugt LabelLanguageWriter ein gespeichertes EPL-Formular (FS/FK) mit Variablen und einen Datenblock pro Datensatz bzw. Fingerprint mit Kopienzahl – Masken-Zähler inklusive.
// Label-Modell + Datensätze nativ als EPL2 bzw. Fingerprint (Text und Barcodes bleiben Befehle)
string epl = LabelLanguageWriter.ToEpl(label, parcels, dpi: 203, copies: p => p.Parcels, formName: "VERSAND");
string fp = LabelLanguageWriter.ToFingerprint(label, parcels, dpi: 203, copies: p => p.Parcels);
File.WriteAllText("versand.epl", epl); // FS/FK-Formular + Variablen, ein Datenblock pro Datensatz
File.WriteAllText("versand.fp", fp);
TIFF, PCL, ESC/POS NV, GIF, HTML, EPS, Spiegeln & DPI-Umrechnung
TIFF mit CCITT G3/G4, LZW, Deflate oder PackBits und 1-Bit, Gray8, RGB oder RGBA; PCL monochrom/grau/farbig mit Modus 0/2/3; ESC/POS mit NV-Grafiken; GIF, SVG, HTML (eingebettet als PNG, GIF oder SVG) und EPS (optional als Umrisse). Ausgaben lassen sich spiegeln (Flip), quer stellen, skalieren, transparent machen und mit DPI-Metadaten versehen. ZplGraphicEncoder wandelt Bilder mit Dithering in ^GF (Hex, ACS, Z64, B64 oder automatisch das kürzeste). ZplRescaler rechnet rohes ZPL zwischen Auflösungen um: Ursprünge, Etikettgröße, Formen, Schriften, Feldblöcke, ^BY, 2D-Modulgrößen und ^GF-Grafiken.
var s = new ZplRenderSettings
{
OutputFormat = ZplOutputFormat.Tiff,
TiffCompression = TiffCompression.CcittGroup4, // None, CcittGroup3, Lzw, Deflate, PackBits
TiffPixelFormat = TiffPixelFormat.BlackWhite1, // Gray8, Rgb24, Rgba32
Flip = ZplFlip.X, PaperOrientation = ZplPaperOrientation.Landscape,
WriteDpiMetadata = true,
}.WithLabelSizeMm(100, 60);
byte[] tiff = new ZplRenderer(s).ProcessCommands(zpl)[0];
s.OutputFormat = ZplOutputFormat.Pcl; s.PclColorMode = PclColorMode.Grayscale; s.PclCompression = PclCompression.DeltaRow;
s.OutputFormat = ZplOutputFormat.Eps; s.EpsTextAsOutlines = true;
s.OutputFormat = ZplOutputFormat.Html; s.HtmlImageFormat = HtmlLabelImageFormat.Svg;
s.OutputFormat = ZplOutputFormat.Gif;
// ESC/POS: Grafik einmal im NV-Speicher ablegen, danach nur noch aufrufen
byte[] define = EscPosNv.DefineGraphic(photo, "G1"), print = EscPosNv.PrintGraphic("G1");
// Bild → ^GF mit Dithering und kürzester Kodierung
string gf = ZplGraphicEncoder.ToZpl(photo, GraphicCompression.Auto, DitherMode.FloydSteinberg, x: 20, y: 20);
// ZPL von 203 auf 300 dpi umrechnen (Koordinaten, Schriften, ^BY, ^GF …)
string zpl300 = ZplRescaler.Rescale(zpl, 203, 300);
ZPL-Optimierer
ZplOptimizer.Optimize verkleinert ZPL ohne Pixeländerung (geprüft am gesamten Labelary-Korpus): Safe entfernt redundante Zustandsbefehle (^BY, ^CF …), Aggressive zusätzlich Zeilenumbrüche und Standardparameter und komprimiert Grafiken neu (GraphicCompression Hex/ACS/Z64). Felddaten bleiben unangetastet, bei ^CC/^CT bleibt die Eingabe unverändert. Im Label-Modell: Label.Optimization.
string safe = ZplOptimizer.Optimize(zpl, ZplOptimization.Safe); // doppelte ^BY/^CF/... entfernen
string small = ZplOptimizer.Optimize(zpl, ZplOptimization.Aggressive, // + Zeilenumbrüche, Standardparameter
GraphicCompression.Z64); // + Grafiken neu komprimieren
label.Optimization = ZplOptimization.Aggressive; // gilt für ToZpl und die Datenbindung
label.GraphicCompression = GraphicCompression.Z64;
Font-Download, Speicher umbenennen & Elemente als JSON
Label.FontDownloadZpl() liefert nur die ~DY/~DU-Befehle der Label-Fonts, FontManager.Convert wandelt jede TTF in einen Download-Befehl, Label.FontAsGraphicThreshold sendet große Fonts stattdessen als Grafik. Der ZplResourceStore kann Grafiken, Formate und Fonts umbenennen und dekodierte Grafiken cachen. ZplRenderResult.RenderedElementsAsJson() gibt die getracten Elemente mit Typ, Daten, Bounds, Rotation und Quellposition aus.
var label = new Label(LabelUnit.Mm, 50, 20).AddFont("MYFONT.TTF", ttf);
label.Add(new TextItem { Text = "Hallo", Font = "E:MYFONT.TTF" });
string fonts = label.FontDownloadZpl(); // nur ~DY…, einmalig an den Drucker
string du = label.FontDownloadZpl(FontDownloadCommand.DU);
string raw = FontManager.Convert(ttf, "R:OTHER", FontDownloadCommand.DU); // beliebige TTF → ZPL
label.FontAsGraphicThreshold = 512 * 1024; // größere Fonts als ^GFA-Grafikvar r = new ZplRenderer();
r.Render("~DGR:OLD.GRF,8,1,FFFFFFFFFFFFFFFF");
r.Store.RenameGraphic("OLD", "E:LOGO.GRF");
r.Store.RenameFormat("A", "C");
r.Store.RenameFont("E:F1.TTF", "F2.TTF");
r.Store.CacheGraphics = true; // dekodierte ^GF-Grafiken wiederverwenden
int cached = r.Store.CachedGraphicCount;var renderer = new ZplRenderer(new ZplRenderSettings { TraceElements = true });
foreach (var res in renderer.Render(zpl))
{
// [{"type":"A","data":"Hi","bounds":{"x":..},"rotation":90,"sourceOffset":..,"sourceLength":..}, ...]
string json = res.RenderedElementsAsJson(indented: true);
res.Bitmap.Dispose();
}
15ZPL-Objektmodell
ZplDocument.Parse zerlegt ZPL in ZplFormats mit typisierten Befehlen – je ZPL-Befehl eine Unterklasse von ZplCommandBase (FieldOriginCommand, Code128Command, Pdf417Command …, unbekannte als UnknownCommand). Eigenschaften ändern, ToString() schreibt wieder ZPL. ZplBuilder baut Formate fluent auf.
// typisiertes Objektmodell: parsen, ändern, wieder schreiben
ZplDocument doc = ZplDocument.Parse(zplIn);
foreach (ZplFormat format in doc.LabelFormats)
{
foreach (FieldOriginCommand fo in format.OfType<FieldOriginCommand>())
fo.X += 20; // alles 20 Punkte nach rechts
foreach (Code128Command bc in format.OfType<Code128Command>())
bc.Height = 60;
}
string moved = doc.ToString();
// oder von Grund auf mit dem ZplBuilder
string zpl = new ZplBuilder()
.Format(f => f
.Encoding(28)
.Text(30, 30, "ZplBuilder", height: 40)
.FieldOrigin(30, 90).BarcodeDefaults(2, height: 70).Code128().FieldData("BUILD-42")
.FieldOrigin(330, 30).QrCode(magnification: 4).FieldData("QA,ZplDocument")
.FieldOrigin(20, 20).GraphicBox(540, 200, 3, rounding: 2).FieldSeparator())
.ToString();
Ausgabe des ZplBuilder

ZPL
16Farbe & Vorschau
Dasselbe ZPL, drei Einstellungen.
InkColor + BackgroundColor

Roter Farbband-Look

InvertColors

ZPL anzeigen
17Beispiel-Etiketten
Die fünf Etiketten aus samples/zpl/five-labels.zpl – Versand, Produkt, Lagerplatz, Inventar, Paletten. Auch als PDF.
17bKommandozeile, Server-Image & Helm
tools/ZplCli ist als .NET-Tool packbar (Paket ZplCore.Cli, Befehl zplcore).
dotnet pack tools/ZplCli -c Release -o nupkg dotnet tool install -g ZplCore.Cli --add-source nupkg zplcore render label.zpl -o label.png --dpi 300 --width 100 --height 150 # mm; ein Bild pro Etikett zplcore render label.zpl -o labels.pdf # pdf/tiff/html enthalten alle Etiketten zplcore render label.epl -f tiff -o out.tif # EPL wird erkannt (oder --epl) zplcore render label.zpl -f svg > label.svg # stdout; "-" liest stdin zplcore epl2zpl label.epl -o label.zpl zplcore print label.zpl --host 192.168.1.50 --port 9100 zplcore barcode qrcode "https://example.com" -o qr.svg # png/svg/pdf/eps/jpeg/zpl zplcore validate label.zpl # Exit-Code 1 bei Warnungen zplcore formats
Weitere Optionen: --labels 0,2, --flip x|y|xy, --landscape, --no-antialias.
Server-Image (distroless) und Helm-Chart
build/Dockerfile.server veröffentlicht samples/WebApiDemo auf mcr.microsoft.com/dotnet/aspnet:10.0-azurelinux3.0-distroless: keine Shell, Benutzer app (nicht root), Port 8080, /health, SQLite-Demodaten in /tmp – läuft mit --read-only. deploy/helm/zplcore enthält Deployment, Service und HorizontalPodAutoscaler (Standard 2–10 Replikas, 70 % CPU), Probes auf /health, App-Einstellungen über env und ein schreibgeschütztes Root-Dateisystem mit emptyDir auf /tmp.
docker build -f build/Dockerfile.server -t zplcore-server .
docker run --rm --read-only --tmpfs /tmp -p 8080:8080 zplcore-server
curl -X POST 'localhost:8080/api/jobs/preview?format=pdf' -H 'Content-Type: application/json' \
-d '{"data":[{"trackingNo":"00340000000000000001","name":"Anna Huber","city":"Wien","country":"AT"}]}' -o preview.pdf
helm install labels deploy/helm/zplcore --set image.repository=myregistry/zplcore-server --set image.tag=1.0.0
17cDiagnosen, Quellpositionen, Limits & Threads
Jedes ZplRenderResult hat Diagnostics: ZplDiagnostic mit stabilem Code, Schweregrad, Meldung, Befehl und Position (Offset, Zeile, Spalte). Warnings bleibt erhalten und enthält die Meldungen aller Warnungen und Fehler.
| Code | Schwere | Bedeutung |
|---|---|---|
ZPL1001 | Error | Befehl mit fehlerhaften Parametern ignoriert (Rest des Etiketts wird gerendert) |
ZPL1002 | Warning | Feld konnte nicht gezeichnet werden (z. B. ungültige Barcode-Daten) |
ZPL1003 | Warning | Barcode-Daten länger als jede Symbologie fassen kann |
ZPL1101 | Warning | ^GF größer als das Etikett oder MaxGraphicBytes |
ZPL1102 | Warning | ^GF-Daten nicht dekodierbar |
ZPL1103 | Warning | ^XG/^IM verweist auf nicht gespeicherte Grafik |
ZPL1201 | Warning | ^A@-Schrift nicht gefunden – Schrift A wird verwendet, wie am Zebra |
ZPL2001 | Info | Akzeptiert, ohne Wirkung aufs Bild (^FE, ^FA, RFID-Operationen außer Schreiben) |
Mit TraceElements trägt jedes Element SourceOffset/SourceLength (sein ^FO…^FS-Block); jeder geparste ZplCommand hat Offset/Length, ZplParser.LineColumn rechnet um. Fehlende ^A@-Schriften liefert auf Wunsch der Callback FontProvider.
var renderer = new ZplRenderer(new ZplRenderSettings
{
TraceElements = true,
FontProvider = name => name == "E:BRAND.TTF" ? File.ReadAllBytes("fonts/Brand.ttf") : null,
});
foreach (var result in renderer.Render(zpl))
{
foreach (ZplDiagnostic d in result.Diagnostics) // z. B. "ZPL1201 warning (3:12): …"
Console.WriteLine($"{d.Code} {d.Severity} {d.Command} Zeile {d.Line}, Spalte {d.Column}: {d.Message}");
foreach (var e in result.Elements) // ^FO…^FS-Bereich im Quelltext
Console.WriteLine(zpl.Substring(e.SourceOffset, e.SourceLength));
result.Bitmap.Dispose();
}
Limits für fremde Eingaben & Thread-Sicherheit
Standardwerte: MaxLabelSide 32000, MaxLabelDots 64 Mio., MaxQuantity 10000, MaxLabels 100000, MaxGraphicBytes 32 MB (auch Z64-Entpackgrenze gegen Zip-Bomben), MaxFieldDataLength 1 Mio., ZplResourceStore.MaxTotalBytes 256 MB. ZplRenderer/EplRenderer halten Druckerzustand und sind nicht thread-sicher – ein Renderer pro Thread; ZplResourceStore, FontManager, ZplCaches, PropertyPath und konfigurierte LabelBindings sind es. ConcurrencyTests vergleicht SHA-256 aller Ausgaben von 16 Threads mit dem Einzel-Thread-Lauf; samples/FuzzRunner füttert den Parser mit Zufalls- und mutierten Eingaben, samples/LeakTest rendert 1 Mio. Etiketten und protokolliert Heap und RSS. Details: THREADING.md, DIAGNOSTICS.md.
var shared = new ZplResourceStore(); // thread-safe, von vielen Renderern teilbar
var safe = new ZplRenderSettings
{
MaxLabelSide = 8000, MaxLabelDots = 16_000_000, MaxQuantity = 100,
MaxLabels = 1000, MaxGraphicBytes = 4 * 1024 * 1024, MaxFieldDataLength = 10_000,
};
Parallel.For(0, 8, _ =>
{
var r = new ZplRenderer(safe) { Store = shared }; // ein Renderer pro Thread
r.ProcessCommands(untrusted);
});
17dPlattformen, Schriftsysteme & Datenbanken
Plattformen
Geprüft auf macOS, Windows 11, Debian (mcr.microsoft.com/dotnet/sdk:10.0) und Azure Linux 3.0 (sdk, runtime und runtime-distroless). Keine nativen Pakete nötig: SkiaSharp NativeAssets.Linux.NoDependencies, HarfBuzzSharp bringt seine Bibliothek mit, die Schriften sind eingebettet. Minimal-Images haben keine Systemschriften – Zeichen außerhalb der eingebetteten Schriften (CJK, Emoji) per RegisterFont/~DY/~DU mitliefern (build/LINUX.md, build/Dockerfile.test.azurelinux).
Datenbanken
Bindung und Druckaufträge sind getestet mit SQLite (EF Core) und LiteDB in den Unit-Tests sowie in tests/ZplCore.IntegrationTests per Testcontainers gegen SQL Server 2022 (EF Core), PostgreSQL (Npgsql EF Core) und MongoDB (MongoDB.Driver und MongoDB.EntityFrameworkCore). Ohne Docker werden diese Tests übersprungen.
Schriftsysteme der Welt
WorldScriptTests rendert Arabisch, Chinesisch, Hindi (Devanagari), Koreanisch, Japanisch und Russisch mit heruntergeladenen Noto-TTFs – einmal als TTF im Druckerspeicher (~DU + ^A@), einmal als Grafik im Label-Modell (PrintAsGraphic, der Drucker braucht dann keine Schrift). Auf Linux-Containern ohne Systemschriften belegt das, dass wirklich die geladene Schrift inkl. HarfBuzz-Shaping verwendet wird. Beispiele in samples/fonts:






18Tests & Verifikation
1656 xUnit-Tests
Parser, Rendering, Fonts, Shaping, Binding, Druckaufträge, Export, Objektmodell, Verbindungen, Druckerbefehle, Einstellungen – dotnet test läuft in wenigen Sekunden.
Barcodes werden zurückgelesen
Jeder portierte Encoder (Postcodes, Codablock F, Code 16K/49, PDF417/MicroPDF417, Han Xin, DotCode, Ultracode, rMQR, QR-Varianten, Data Matrix, Aztec, GS1, Composite …) ist bit-genau gegen bwip-js (zint-Portierungen gegen zint) geprüft – darunter alle 7 GS1-DataBar-Varianten mit 80 eigenen Referenzfällen – die Referenzmatrizen stehen in den Tests. Wo ZXing.Net die Symbologie kennt, wird zusätzlich dekodiert.
Datenbanken
EF Core mit SQLite (in-memory, 2000 Zeilen) und LiteDB (typisiert + BsonDocument) in den Unit-Tests, SQL Server 2022, PostgreSQL und MongoDB per Testcontainers in tests/ZplCore.IntegrationTests; samples/MassDataDemo erzeugt die Beispiel-Aufträge.
Diese Doku
Alle Screenshots erzeugt samples/DocsGenerator aus echtem ZPL/EPL bzw. der Barcode-API; alle C#-Beispiele stehen kompilierbar in samples/DocsExamples und werden vom Generator wortgleich in diese Seite kopiert.
dotnet test # 1656 Tests dotnet run --project samples/LabelaryCompare # Pixelvergleich gegen Labelary-Referenzen dotnet run --project samples/DocsGenerator # docs/img, docs/zpl, docs/files, docs/examples.js, C#-Snippets dotnet build samples/DocsExamples # Compile-Check der Doku-Snippets
19Genauigkeit
samples/LabelaryCompare rendert den ZPL-Korpus samples/labelary/*.zpl und vergleicht Pixel für Pixel (2 Punkte Toleranz) mit druckergenauen Labelary-Referenzen. LabelaryReferenceTests hält jeden Fall mindestens auf seinem aktuellen Wert.
| Fall | Score | Präzision | Recall | nur Referenz | nur zplCore.com |
|---|---|---|---|---|---|
ll-clip | 0,645 | 1,000 | 0,476 | 12483 | 0 |
gs-symbols | 0,686 | 0,685 | 0,686 | 2372 | 2386 |
rss-stacked | 0,767 | 0,926 | 0,655 | 9949 | 1409 |
qr-encodings | 0,842 | 0,737 | 0,981 | 212 | 3836 |
gd-orient | 0,848 | 0,844 | 0,852 | 812 | 957 |
maxicode-modes | 0,860 | 0,797 | 0,933 | 2143 | 9764 |
gs-sizes | 0,868 | 0,903 | 0,835 | 4192 | 2255 |
fp-rot | 0,880 | 0,894 | 0,868 | 407 | 297 |
aat-cw | 0,888 | 0,869 | 0,907 | 847 | 1203 |
ft-barcodes-r | 0,898 | 0,885 | 0,912 | 4213 | 5713 |
imb-postal | 0,901 | 1,000 | 0,819 | 2120 | 0 |
code128-invoc | 0,901 | 0,840 | 0,973 | 825 | 5512 |
fb-rot-i | 0,904 | 0,915 | 0,894 | 1067 | 846 |
fb-bitmap | 0,913 | 0,917 | 0,909 | 806 | 581 |
fonts-efgh | 0,915 | 0,950 | 0,883 | 664 | 218 |
Die 15 schwächsten von 122 Fällen; 79 Fälle erreichen ≥ 0,99. Vollständige Liste: dotnet run --project samples/LabelaryCompare (offline gegen die gespeicherten Referenzen in tests/ZplCore.Tests/Reference/labelary).
Die schwächsten Fälle betreffen Sonderfälle wie Clipping über die Etikettlänge (ll-clip), ^GS-Symbole, gestapelte DataBar-Varianten, QR-Kodierungsmodi, ^GD-Ausrichtung und MaxiCode-Modi. Alle Symbole bleiben dekodierbar; Labelary rendert ^BB und ^B4 nicht, diese sind ausschließlich gegen bwip-js verifiziert.
19bPerformance
samples/PerfTest erzeugt 100.000 Labels in einem Druckauftrag (Start-Label + 100.000 Sendungs-Labels + End-Label, Schnitt nur nach dem letzten Label) – aus Objekten, gestreamt aus EF Core/SQLite und aus LiteDB. Gemessen im Release-Build auf 14 Kernen (.NET 10).
| Test (100.000 Labels, ein Job) | Zeit | Labels/s | Datei | Heap |
|---|---|---|---|---|
Objekte → ZPL, gespeichertes Format (^DF/^XF) | 0,73 s | 136.000 | 14,7 MB | 24 MB |
| Objekte → ZPL, volles Format je Label | 0,39 s | 256.000 | 29,1 MB | 24 MB |
EF Core / SQLite AsAsyncEnumerable() → Datei | 0,57 s | 176.000 | 14,7 MB | 29 MB |
| LiteDB-Query → Datei | 0,50 s | 202.000 | 14,7 MB | 64 MB |
| Vorschau PNG sequenziell (799 × 480 px, Graustufen-PNG) | ≈ 3,4 min | 490 | 18,2 MB / 2.000 | 84 MB |
| Vorschau PNG parallel (14 Kerne) | ≈ 25 s | 4.033 | 17,1 MB / 2.000 | 106 MB |
Die ZPL-Erzeugung für den Drucker ist praktisch kostenlos und streamt mit konstantem Speicher. Das gespeicherte Format halbiert die übertragene Datenmenge.
Render-Optimierung
Schwarz-weiße Labels werden als 8-Bit-Graustufen-PNG (mit Kantenglättung) bzw. echtes 1-Bit-PNG (AntiAlias = false) statt 32-Bit-RGBA geschrieben (ZplRenderSettings.CompactPng, PngCompression Fastest/Optimal/SmallestSize). Einfache ^GB-Rahmen werden direkt gezeichnet, und das Label-Bild wird nur kopiert, wenn ^MC N es braucht.
| Pro Label (100 × 60 mm, 203 dpi) | vorher | jetzt |
|---|---|---|
| Rendern zu Bitmap | 1,28 ms | 0,39 ms |
| Rendern + PNG | 8,19 ms | 1,86 ms |
| PNG-Größe | 18 KB | 9 KB |
| 100.000 Vorschauen parallel | 2,2 min | 25 s |
Parallele Skalierung
SkiaSharp serialisiert zwei Dinge global: Skias Glyph-Cache (jedes DrawText) und SkiaSharps Handle-Tabelle (jedes neu erzeugte SKPaint/SKFont/SKBitmap – und DrawBitmap, das intern jedes Mal ein SKImage anlegt). zplCore.com rastert deshalb jede Glyphe einmal pro Schrift/Größe in einen sperrfreien Cache aus SKImage-Masken und zeichnet Text per DrawImage; Schriften werden explizit durchgereicht statt über SKFont.Typeface abgefragt. Ergebnis gemessen mit samples/RenderBench -- 150 scale (Apple M3 Max, 10 Performance- + 4 Effizienzkerne):
| Threads | Rendern + PNG vorher | Rendern + PNG jetzt | nur Text vorher | nur Text jetzt |
|---|---|---|---|---|
| 1 | 499/s (1,0×) | 549/s (1,0×) | 5.665/s (1,0×) | 6.306/s (1,0×) |
| 4 | 1.984/s (4,0×) | 2.225/s (4,1×) | 3.770/s (0,7×) | 28.183/s (4,5×) |
| 8 | 2.245/s (4,5×) | 4.090/s (7,5×) | 3.162/s (0,6×) | 42.434/s (6,7×) |
| 10 | 1.826/s (3,7×) | 5.022/s (9,2×) | 3.150/s (0,6×) | 39.384/s (6,2×) |
dotnet run -c Release --project samples/RenderBench -- 150 scale [label|text|barcode|box|empty]
dotnet run -c Release --project samples/RenderBench -- 500 # Mikro-Benchmark pro Element
dotnet run -c Release --project samples/PerfTest -- 100000 perf-out
20Grenzen
Ehrlich aufgelistet – damit es keine Überraschungen am Drucker gibt.
Store.Fonts.RegisterFont("0", bytes) registrieren. Die Klarschriftzeile unter Barcodes ist monospaced, aber kein echtes OCR-B.Nicht implementiert
- Pharmacode 2D, Response Plus PostMatrix (keine öffentliche Spezifikation)
- HIBC nach ISO/IEC 15434 (Datenformat-Hülle)
- Experimentell: Deutsche Post BZL (
ZPLBZL001) und JABCode – der JABCode-Encoder ist langsam (ca. 0,5 s für 120 zufällige Unicode-Zeichen) - ISBT 128: Hilfsfunktionen nur für die Datenstrukturen DS001–DS011
- JAB Code ist experimentell (bit-genau gegen BWIPP, aber kein Decoder zur Gegenprobe)
Abweichungen & eingeschränkt verifiziert
- Glyphenformen der residenten Zebra-Bitmap-Schriften A–H
- PDF417 (
^B7): ohne Spalten/Zeilen wählt Zebra teils eine andere Geometrie - QR (
^BQ): die Maskenwahl weicht in einzelnen Fällen von Zebra ab (Symbol trotzdem gültig) ^BB(Codablock F) und^B4(Code 49) rendert Labelary nicht – nur gegen bwip-js verifiziert- Fingerprint-Ausgabe und die echten Drucker-Transporte (seriell, USB, Windows-Spooler, CUPS) sind nicht auf Hardware getestet; TCP 9100 ist gegen den virtuellen Drucker getestet
- Auf Linux weicht die Glyph-Rasterung (FreeType statt CoreText) leicht ab:
bitmap-fontserreicht dort 0,774 statt ≥ 0,919 ZplRescalerskaliert~DG/~DY-Downloads und binäres^GFBnicht; 1D-Module werden auf ganze Punkte gerundet- Medien-, Sensor- und Konfigurationsbefehle werden akzeptiert, beeinflussen das Bild aber nicht
21Lizenzen & Drittanbieter
| Komponente | Lizenz | Verwendung |
|---|---|---|
| BWIPP / bwip-js (© Terry Burton, Mark Warren) | MIT | Teile der Barcode-Encoder (Postcodes, gestapelte und 2D-Symbologien, Composite, GS1-Tabellen) und Referenzmatrizen der Tests |
| zint (© Robin Stuart u. a.) | BSD-3-Clause | Portierungen u. a. von Korea Post, Mailmark 4-State, DPD, UPU S10, Flattermarken, UPNQR, Grid Matrix und Code One |
| ZXing.Net | Apache-2.0 | Bausteine für QR, Data Matrix, PDF417, Aztec und EAN/UPC; in den Tests zum Zurücklesen der Barcodes |
| SkiaSharp, HarfBuzzSharp | MIT | Rendering, Text-Shaping |
| Roboto Condensed | SIL OFL 1.1 | Ersatz für Zebra-Schrift 0 |
| DejaVu Sans / Sans Mono | Bitstream Vera / DejaVu-Lizenz (frei) | Ersatz für Schriften A–H, Klarschrift |
Abgeleitete Dateien tragen einen Kopfkommentar. Vollständige Texte: THIRD-PARTY-NOTICES.md