zplCore.com – High Performance .NET Labelprint Engine
Etiketten erstellen,
prüfen und drucken.
Erstellen → Vorschau & Emulation → Drucken: Vorlage und Daten (C#-Objekte, JSON, EF Core) werden zu ZPL; ZPL II und EPL2 werden wie auf einem virtuellen Zebra-Drucker zu PNG, PDF und SVG; gedruckt wird über TCP 9100, USB, Windows-Spooler oder CUPS. Dazu kommen Prüfwerkzeuge: Compliance-Pre-Check, Scan-Proof, Printer Twin und Etiketten-Tests für CI.

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.
Compliance & Scan-Proof
Regelpakete (GS1, Sunrise 2027, Carrier, UDI) als Pre-Check und jeder Barcode mit zwei Decodern zurückgelesen – vor dem Druck.
Printer Twin & Chaos-Drucker
Druckt das auf diesem Modell? Profile mit Quellen, dazu ein virtueller Drucker mit Störungen und Cutter-Simulation.
Etiketten-Tests in CI
ZplCore.Testing, zplcore check, GitHub Action und ein MCP-Server für Coding-Agents.
Import, Extraktion, Vector-Lift
ZPL → editierbares Modell, Etikettendaten verstehen und schwärzen, PDF/SVG → natives 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
^FN-Felder befüllen: FieldValues
ZplRenderSettings.FieldValues (IReadOnlyDictionary<int, string>) setzt die Daten aller Felder mit ^FN-Nummer – in normalen Labels und in mit ^XF aufgerufenen Formaten, für alle Labels und ^PQ-Kopien eines Aufrufs. Der Wert hat Vorrang vor einem ^FD-Vorgabewert und vor dem ^XF-Datenblock; ^FH, ^FC und ^SN/^SF wirken weiter. Schlüssel ohne passendes ^FN meldet das Diagnostic ZPL1301 (ZplDiagnosticCodes.InvalidFieldValueKey, INVALID_FIELD_VALUE_KEY) am ersten Label. ZplFields.Find listet die Felder mit Prompt (^FN1"Name") und Vorgabewert, ZplFields.Fill liefert das befüllte ZPL. CLI: zplcore render label.zpl --field 1=ACME --field 2=Wien.
// ^FN-Felder ohne ^DF/^XF befüllen: Testdaten für Vorschau oder ERP-Vorlagen
const string template = "^XA^FO50,50^A0N,40,40^FN1\"Name\"^FDMuster^FS^FO50,110^A0N,30,30^FN2\"Ort\"^FS^XZ";
var renderer = new ZplRenderer(new ZplRenderSettings
{
FieldValues = new Dictionary<int, string> { [1] = "Erika Muster", [2] = "Wien" },
});
var png = renderer.ProcessCommands(template); // gilt für alle Labels, auch in ^XF-Formaten
// Felder für ein Formular auflisten (Nummer, Prompt, Vorgabewert)
foreach (var f in ZplFields.Find(template))
Console.WriteLine($"^FN{f.Number} {f.Prompt}: {f.Value}");
// ZPL mit eingesetzten Werten (^DF/^XF zu normalen Labels zusammengeführt)
string filled = ZplFields.Fill(template, renderer.Settings.FieldValues!);
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, modulgleich zu Zebra: Moduswahl, Version, EC-Anhebung und Maske) 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 144 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.
Ausprobieren: Die Live-Demo bindet eine JSON-Vorlage an JSON-Datensätze (label.For<JsonElement>()) und zeigt ZPL, Vorschau und Zeiten je Phase.
// 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, 150));
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;
ZPL-Formatter & ZPL-Diff
ZplFormatter.Format (= ZplDocument.Parse(zpl).ToZpl(new ZplOutputOptions { PrettyPrint = true })) schreibt einen Befehl pro Zeile und rückt Etiketteninhalt und Feldblöcke (^FO/^FT … ^FS) ein, optional mit einem ^FX-Kommentar je Feld; ZplFormatter.Minify entfernt Leerraum und Kommentare. Felddaten (^FD/^FV), ^FH-Escapes und ~DG/^GF-Nutzdaten bleiben Byte für Byte erhalten, nach Felddaten wird nie ein Zeilenumbruch eingefügt – die Ausgabe ergibt dieselben Befehle und rendert pixelgleich (getestet am gesamten Beispiel- und Labelary-Korpus). ZplDiff.Compare rendert zwei Versionen und liefert je Etikett ein Overlay-PNG (rot entfernt, grün neu, grau unverändert), die Zahl geänderter Pixel, Bounding-Boxen der Änderungen und einen Zeilen-Diff der formatierten Quellen. Kommandozeile: zplcore format und zplcore diff.
string pretty = ZplFormatter.Format(zpl); // ein Befehl pro Zeile, Feldblöcke eingerückt
string commented = ZplFormatter.Format(zpl, new ZplOutputOptions
{
PrettyPrint = true, Indent = 4, AddComments = true, // ^FX-Kommentar je Feld (Text, Barcode, Box …)
});
string minified = ZplFormatter.Minify(zpl); // ohne Leerraum zwischen Befehlen und ohne ^FX
string viaModel = ZplDocument.Parse(zpl).ToZpl(ZplOutputOptions.Pretty);
var diff = ZplDiff.Compare(zpl, zplNew, new ZplRenderSettings { Dpi = 203 });
foreach (var l in diff.Labels) // rot = entfernt, grün = neu, grau = gleich
File.WriteAllBytes($"diff-{l.Index + 1}.png", l.OverlayPng!);
Console.WriteLine($"{diff.ChangedPixels} Pixel geändert, Regionen: {diff.Labels[0].Regions.Count}");
foreach (var d in diff.TextDiff.Where(d => d.Kind != ZplDiffLineKind.Unchanged))
Console.WriteLine((d.Kind == ZplDiffLineKind.Added ? "+ " : "- ") + d.Text);
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
17Compliance-Pre-Check: Regelpakete als Code
Prüft Etiketten vor dem Druck gegen öffentliche Vorgaben: ZplCompliance.Check rendert das ZPL und misst die gedruckte Geometrie – X-Dimension in mm, Balkenhöhe, Ruhezonen, Klarschrift, Texthöhe, Überlappungen, abgeschnittene Symbole, GS1-AIs mit Prüfziffern und Kombinationsregeln, Digital-Link-Syntax, MaxiCode-Carrier-Nachricht. 72 Regeln in Paketen, jede mit stabiler ID, Schweregrad und Quellenangabe.
Paket (--pack) | Inhalt | Quellen |
|---|---|---|
gs1-logistic | SSCC in GS1-128, X-Dimension ≥ 0,495 mm, Balkenhöhe, Ruhezonen, Klarschrift ≥ 3 mm, AI-Formate und -Kombinationen | GS1 General Specifications 26.0, Logistic Label Guideline 1.3 |
sunrise | Sunrise 2027: GTIN in GS1 DataMatrix oder QR-Code mit GS1 Digital Link neben EAN/UPC, Modulgröße, URI-Syntax | GenSpecs 26.0, Digital Link URI Syntax 1.7.0 |
amazon-fba, ups, fedex, dhl-paket | Formate, Pflicht-Barcodes, Ruhezonen, MaxiCode-Carrier-Nachricht, 1Z-Prüfziffer | öffentliche Carrier-Dokumentation (teils als „advisory“ markiert) |
udi | UDI-DI (01), Produktionskennzeichen, Datumsregeln, zulässige Träger | 21 CFR 801, EU-MDR Anhang VI, GenSpecs 26.0 |
rfid | schreibt ^RF die EPC der gedruckten SSCC/GTIN? (siehe RFID Studio) | GS1 EPC TDS 2.3 |
// using ZplCore.Compliance;
var report = ZplCompliance.Check(zpl, CompliancePacks.Gs1LogisticLabel | CompliancePacks.Sunrise2027,
new ComplianceOptions { Dpi = 203 }); // ZPL kennt keine Auflösung: dpi des Druckers angeben
foreach (var f in report.Findings)
Console.WriteLine(f); // label 1: ZPL3104 error (16:1): ^BC: X-dimension 0.25 mm … below the minimum 0.495 mm
File.WriteAllText("labels.sarif", report.ToSarif("labels/pallet.zpl")); // GitHub Code Scanning
string json = report.ToJson();
List<byte[]> pngs = ZplCompliance.RenderAnnotated(zpl, report); // Rahmen + Regel-IDs auf dem Etikett
zplcore comply pallet.zpl --pack gs1-logistic,sunrise --dpi 203 [--format text|json|sarif] [--annotate out.png]
zplcore comply --list-rules --pack all # alle Regeln mit ID, Schweregrad und Quelle
pallet.zpl:16:1: error ZPL3104: label 1 ^BC: X-dimension 0.25 mm (2 dot(s) at 203 dpi) is below the minimum
0.495 mm for logistic units … [GS1 General Specifications 26.0 §5.12.3.5 Table 5-51]
fix: Use ^BY4 or wider at 203 dpi.
1 label(s) at 203 dpi, packs gs1-logistic GenSpecs 26.0 / LLG 1.3: 4 error(s), 0 warning(s), 0 info(s).
Pre-check only, not a certification.
Annotiertes Etikett

Dasselbe Palettenetikett wie samples/compliance/gs1-logistic-sscc.zpl, aber mit ^BY2: beide GS1-128 sind zu schmal, die Klarschrift zu klein.
Ausgaben & eigene Regeln
- Befunde mit Regel-ID, Paket, Schweregrad, Befehl, Zeile/Spalte, Bounding-Box in Dots, Quelle und Korrekturvorschlag
- JSON, Text und SARIF 2.1.0 für GitHub Code Scanning (Regeln mit
helpUri) RenderAnnotated: Rahmen und Regel-IDs direkt auf dem Etikett- Eigene Regeln mit
ComplianceRule.Create(…), eigene Pakete mitComplianceRulePack; eingebaute Pakete mit.With(rule)/.Without("ZPL3117")anpassen ComplianceOptions:Dpi, Etikettengröße ohne^PW/^LL,FieldValues,DisabledRules,SeverityOverrides,MinimumSeverity
Grenzen: ZPL kennt keine Auflösung – alle mm-Regeln gelten für die angegebene Dpi. Nicht abgedeckt sind Druckqualitätsnoten (Kontrast, Defekte), die Position des Etiketts auf der Einheit, HIBCC-/ICCBBA-UDIs, DHL Express und die FedEx-Etikettenzertifizierung. Werte aus nicht öffentlichen Quellen sind als „advisory“ markiert.
18Scan-Proof: jeder Barcode zurückgelesen
Beantwortet vor dem Druck die Frage „trägt dieser Barcode die Daten, die ich schicke, und ist das Symbol sauber?“. ZplScanProof.Verify rendert das Etikett, liest jeden Barcode mit zwei unabhängigen Decodern (zxing-cpp und ZXing.Net) aus der Bitmap zurück und vergleicht mit den Felddaten nach ^FH, ^FN, ^SN, Prüfziffern und Invocation-Codes – GS1 Element für Element.
var report = ZplScanProof.Verify(zpl, new ScanProofOptions { Dpi = 203, Robustness = true });
foreach (var s in report.Symbols) // ^BC GS1-128 Match A "00376104250021234569" X = 0,501 mm
Console.WriteLine($"{s.Command} {s.Symbology} {s.Status} {s.Grade} \"{s.DecodedData}\" X = {s.XDimensionMm:0.###} mm");
Console.WriteLine(report.LowestGrade); // schlechteste digitale Note aller Symbole
string sarif = report.ToSarif("labels/shipping.zpl"), text = report.ToText();
ZplScanProof.Verify(new ZplCore.Barcodes.Barcode { Symbology = ZplCore.Barcodes.Symbology.Ean13, Code = "400638133393" });
zplcore verify pallet.zpl --dpi 203 [--robustness] [--format text|json|sarif] [--fail-on mismatch,grade:C]
[0.1] ^BC GS1-128 line 17 at 94,800 (624×260 dots, 0°)
status: Match digital grade: A (4.0)
field data: "(00)376104250021234569"
decoded: "00376104250021234569"
zxing-cpp: read
ZXing.Net: read
X-dimension: 4 dots = 0.501 mm
quiet zones: left ≥14X (min. 10X), right ≥14X (min. 10X)
Status je Symbol
Match– gelesen = erwartet = FelddatenDataAltered– gelesen = erwartet, aber die Symbologie hat die Daten verändert (z. B. falsche Prüfziffer ersetzt)Mismatch– ein Decoder liest andere DatenNotDecoded/NotDecodable– nicht lesbar bzw. kein Decoder für die Symbologie (dann nur Strukturprüfungen)NotRendered– das Feld hat nichts gedruckt
Gemessen
- X-Dimension in Dots und mm, ganzzahlige Module, Breit/Schmal-Verhältnis
- Ruhezonen (begrenzt durch fremde Druckfarbe oder den Etikettenrand), Balkenhöhe, Abschneiden, Überlappung
- Klarschriftzeile und benachbarter Text gegen die Daten
Robustness = true: zusätzliche Simulationen (z. B. Zielauflösung 203 dpi)- SARIF 2.1.0 mit Regel-IDs
SP…wiezplcore comply
Grenzen: rein digital; Barcodes innerhalb von ^GF-Grafiken werden nicht als Barcode erkannt (dafür ZplScanProof.Verify(Barcode)); kein EPL. Lässt sich die native zxing-cpp-Bibliothek nicht laden (glibc älter als 2.38, z. B. Debian 12, oder musl/Alpine), läuft Scan-Proof nur mit ZXing.Net und meldet das als SP1004.
19GS1 Digital Link & Sunrise-2027-Migration
Für den Umstieg auf 2D-Codes an der Kasse („Sunrise 2027“): Gs1DigitalLink baut, prüft und zerlegt GS1-Digital-Link-URIs nach URI Syntax 1.7.0 (Element-String ⇄ URI, eigene Domain, kanonische URI). ZplGs1Migration ergänzt bestehende ZPL-Etiketten automatisch um einen QR-Code mit Digital-Link-URI oder eine GS1 DataMatrix – im freien Platz neben dem EAN/UPC, mit Ruhezone und Abstand, gemessen am gerenderten Etikett – und prüft das Ergebnis mit dem Sunrise-Paket.
// using ZplCore.Barcodes; using ZplCore.Tools;
string uri = Gs1DigitalLink.Build("(01)09520123456788(17)280426(10)ABC1(21)12345");
// https://id.gs1.org/01/09520123456788/10/ABC1/21/12345?17=280426
var parsed = Gs1DigitalLink.Parse("https://id.gs1.org/01/09520123456788/10/ABC1?17=280426");
Console.WriteLine($"{parsed.IsValid} {parsed.ToElementString()}"); // True (01)09520123456788(17)280426(10)ABC1
var migration = ZplGs1Migration.Migrate(zpl, new Gs1MigrationOptions
{
Carrier = Gs1MigrationCarrier.QrCodeDigitalLink, // oder DataMatrixElementString
DigitalLinkStem = "https://brand.example.com", // eigene Domain, sonst https://id.gs1.org
});
File.WriteAllText("label-2d.zpl", migration.MigratedZpl); // QR-Code neben dem EAN-13, mit Ruhezone
Console.WriteLine(migration.ToText()); // Platzierung + Sunrise-Pre-Check des Ergebnisses
zplcore gs1 link "(01)09520123456788(10)ABC1" [--domain https://brand.example.com] zplcore gs1 parse "https://id.gs1.org/01/09520123456788/10/ABC1" [--format json] zplcore gs1 migrate retail-ean13.zpl --domain https://brand.example.com -o retail-2d.zpl [--carrier qr|datamatrix] [--preview out.png]
Vorher: nur EAN-13

Nachher: EAN-13 + QR mit GS1 Digital Link

Ein Assistent und Pre-Check, keine Zertifizierung: gedruckte Etiketten mit einem Prüfgerät prüfen und mit der GS1-Länderorganisation oder dem Händler abstimmen. ^BQ kann kein FNC1 kodieren – ein GS1-QR-Code mit Element-Strings ist in ZPL daher nicht möglich, der QR-Code trägt die Digital-Link-URI. Alte komprimierte Digital-Link-URIs werden nur gelesen. GS1, GS1 DataMatrix und GS1 Digital Link sind Marken von GS1 AISBL.
20RFID Studio: EPC kodieren, prüfen, simulieren
EPC-Kodierung und -Dekodierung nach dem GS1 EPC Tag Data Standard 2.3, eigene Implementierung aus dem Standardtext: SGTIN-96/198, SSCC-96, SGLN-96/195, GRAI-96/170, GIAI-96/202 sowie die TDS-2.0-Schemata SGTIN+, DSGTIN+, SSCC+, SGLN+, GRAI+ und GIAI+. Dazu die ^RF-Befehle, die Konsistenzprüfung „schreibt der Tag dieselbe SSCC/GTIN, die gedruckt ist?“ und ein Gen2-Tag-Simulator für den virtuellen Drucker.
// using ZplCore.Rfid;
Epc epc = ZplRfid.EncodeEpc(EpcScheme.Sgtin96, "(01)09506000134352(21)6789", filter: 1, companyPrefixLength: 7);
Console.WriteLine(epc.Hex); // 30364433400D1EC000001A85
Console.WriteLine(epc.TagUri); // urn:epc:tag:sgtin-96:1.9506000.013435.6789
Console.WriteLine(epc.DigitalLinkUri); // https://id.gs1.org/01/09506000134352/21/6789
Epc sscc = ZplRfid.EncodeEpc("(00)376104250021234569", filter: 6, companyPrefixLength: 7);
string commands = ZplRfid.BuildCommands(sscc); // ^RS8 ^RFW,H,,,A^FD31D5D080E4B2F0C4C0000000^FS
Epc decoded = ZplRfid.DecodeEpc("3074257BF7194E4000001A85"); // (01)80614141123458(21)6789
var check = ZplRfid.Check(zpl); // schreibt ^RF die EPC der gedruckten SSCC/GTIN? (Pack "rfid")
zplcore rfid encode --scheme sgtin-96 --gtin 09506000134352 --serial 6789 --company-prefix 7 --filter 1 [--format text|json|zpl] zplcore rfid decode 3074257BF7194E4000001A85 [--pc] zplcore rfid check label.zpl [--format sarif] # = zplcore comply --pack rfid zplcore rfid simulate label.zpl # Tag-Speicher nach dem Druck, VOID bei Fehlern zplcore serve --rfid # virtueller Drucker mit Tag-Simulation
Fehler werfen EpcException mit Grund (Prüfziffer, Partition, Filter, Zeichen …). Grenzen der Prüfung: gedruckte Schlüssel werden in GS1-Symbolen, Digital-Link-URIs, EAN/UPC und geklammerter Klarschrift gefunden, nicht als reiner Text ohne AI; ^RS-Einstellungen gelten am Drucker über Formate hinweg und werden deshalb nur als Hinweis geprüft.
21Printer Twin: druckt das auf diesem Modell?
Ein Druckerprofil beschreibt ein Modell bei einer Auflösung: Druckkopfbreite, Drucklänge, Geschwindigkeit, Speicher, Optionen, residente Schriften und Firmware. ZplPrinterTwin.Check meldet, was auf diesem Modell schiefgeht – Felder, die der Druckkopf abschneidet, zu lange Etiketten, Befehle, die die Firmware nicht kennt (^TB, ^CI28 …), fehlende Optionen (Cutter, Spender, RFID …), ^MTT auf Thermodirekt-Modellen, Schriften und Speicher. Eingebaut sind Profile für ZD421d/t, ZD621, ZD220, GK420d, ZT411, ZT610, ZT230, ZQ510/ZQ520, ZQ630 (Plus) und ein generisches Profil.
// using ZplCore.Twin;
var profile = PrinterProfiles.Get("ZQ510-203"); // ID oder Alias; zplcore twin list
ZplTwinReport report = ZplPrinterTwin.Check(zpl, profile);
Console.WriteLine(report.ToText()); // ZPL5102 error (43:1): ^BC "ZE4711802026DE" … wider than the printhead …
List<byte[]> png = ZplPrinterTwin.RenderAnnotated(zpl, profile); // abgeschnittener Teil rot schraffiert
var withCutter = PrinterProfiles.Get("ZD421d-203").WithOptions(PrinterOption.Cutter);
ZplPrintEstimate estimate = ZplPrinterTwin.Estimate(zpl, withCutter);
Console.WriteLine(estimate.ToText()); // eine Schätzung mit allen Annahmen, keine Messung
zplcore twin list # alle Profile und Aliase zplcore twin check label.zpl --profile ZQ510-203 [--option cutter] [--firmware V…] [--annotate out.png] zplcore twin estimate label.zpl --profile ZT411-203 [--link usb|ethernet|wifi|bt|serial] zplcore check labels/ --printer ZD421d-203 # dieselbe Prüfung in CI (siehe unten) zplcore serve --profile ZD421d-203 --option cutter # virtueller Drucker verhält sich wie das Modell
4 × 6″-Etikett auf einem 72-mm-Mobildrucker

Fakten mit Quelle – nichts geraten
Jeder Profilwert ist belegt (zitiertes Datenblatt oder Release Notes, wörtliches Zitat), abgeleitet (mit Begründung) oder unbekannt. Braucht eine Prüfung einen unbekannten Wert, meldet sie ZPL5001 („nicht geprüft“) statt eines Befunds. Nur das generische Profil verwendet angenommene Standardwerte.
Die Druckzeit-Schätzung nennt alle Annahmen (Übertragungsrate, Geschwindigkeit ohne ^PR, Lücken, Backfeed) – eine Schätzung, keine Messung.
Modellnamen bezeichnen die Hardware, die die Daten beschreiben („kompatibel mit Zebra ZD421“). zplCore.com steht in keiner Verbindung zu Zebra Technologies.
22Chaos-Drucker & Cutter-Simulation
Testet, ob Ihre Druckanwendung mit echten Störungen klarkommt – ohne Hardware. ZplVirtualPrinter verhält sich wie ein Netzwerkdrucker, der gezielt ausfällt: Papier- oder Farbbandende, offener Kopf, Übertemperatur, Pause, voller Puffer, Cutter-Fehler, RFID-VOID, Verbindungsabbruch nach n Bytes, Latenz, langsames Lesen, Teil-Schreibvorgänge, Stromausfall. Ausgelöst sofort, nach n Etiketten/Bytes/Schnitten, bei einem Befehl, nach einer Zeit oder per reproduzierbarem Zufall. ~HS, ~HQES, ~HM und SGD-getvar melden den Zustand wie ein echter Drucker; Etiketten warten im Puffer, bis die Störung behoben ist.
// using ZplCore.Printing;
await using var printer = new ZplVirtualPrinter(9100) { RenderSettings = new() { CutterInstalled = true } };
printer.LabelCut += (_, e) => Console.WriteLine($"{e.LabelIndex}: {e.Reason}"); // 3: PauseAndCutCount …
printer.Faults.FaultRaised += (_, e) => Console.WriteLine($"{e.Kind}: {e.Cause}");
printer.Start();
printer.Faults.Schedule(ZplFaultKind.PaperOut, ZplFaultTrigger.AfterLabels(3)); // Etikett 4 wartet
printer.Faults.Schedule(new ZplFaultRule(ZplFaultKind.Disconnect, ZplFaultTrigger.AfterBytes(1000)));
printer.Faults.Seed = 42; // reproduzierbarer Zufall
// … Ihre Druckanwendung sendet jetzt an 127.0.0.1:9100, liest ~HS und muss sich erholen …
printer.Faults.Clear(ZplFaultKind.PaperOut); // Papier eingelegt
var plan = ZplCutPlan.Analyze(zpl); // samples/zpl/cutter.zpl: ^MMC + ^PQ6,3
Console.WriteLine(string.Join(", ", plan.CutAfter)); // 2, 5 → Schnitt nach Etikett 3 und 6
var preview = new ZplRenderer(new ZplRenderSettings { CutMarks = true }).ProcessCommands(zpl);
zplcore serve --port 9100 --chaos chaos.json --cut-marks --out printed/
# chaos.json
{"seed":42,"printer":{"cutter":true,"labelPrintTimeMs":200},
"faults":[{"kind":"paperOut","after":{"labels":3},"clearAfterMs":5000},
{"kind":"latency","delayMs":300},{"kind":"disconnect","after":{"bytes":1000}}]}
Wo schneidet der Cutter?
ZplCutPlan.Analyze bzw. ZplRenderer.LastCutPlan sagt je Etikett, ob und warum geschnitten wird: ^MM-Modi inklusive verzögertem Schnitt mit ~JK und Kiosk-Modus (^CN/^KV), Pause-und-Schnitt-Wert von ^PQ, ^XB, gespeicherte Formate und ^SN-Kopien; Auffälligkeiten als ZPL4101–ZPL4105. Mit ZplRenderSettings.CutMarks = true zeichnen PNG, SVG und PDF unter jedes Etikett einen Streifen – gestrichelte Schnittlinie mit Schere, wo geschnitten wird (Standard aus, Ausgabe sonst unverändert).
samples/zpl/cutter.zpl: ^MMC + ^PQ6,3 – Schnitt nach Etikett 3 und 6

Quellen sind die Zebra-Programmierhandbücher; was dort nicht steht, ist als Annahme dokumentiert. Nicht simuliert: der JSON-Port 9200 und der Einzelverbindungs-Port; für Lasttests verarbeitet --concurrent Verbindungen parallel statt nacheinander wie ein Drucker.
23ZPL → editierbares Label-Modell
Holt bestehendes ZPL – ERP-Ausgaben, Exporte aus Label-Designern, Carrier-Vorlagen – zurück ins Label-Modell: Label.FromZpl liefert typisierte Text-, Barcode-, Form-, Bild- und RFID-Elemente in mm, ^FN-Felder als Datenfelder und ^DF-Formate als Vorlagen. Jedes Element wird einzeln gegen das Original gerendert und nur typisiert, wenn es pixelgleich druckt; sonst bleibt es exaktes Roh-ZPL. Die Rundreise wird gemessen.
var import = Label.FromZpl(zpl); // ImportOptions: Dpi, Unit, Fallback, IncludeImages …
foreach (var l in import.Labels) // 0: fidelity 1,0000, 38 typed / 0 raw
Console.WriteLine($"{l.Index}: fidelity {l.Fidelity:0.0000}, {l.TypedItems} typed / {l.RawItems} raw");
foreach (var w in import.Warnings) Console.WriteLine(w); // Info: Resolution detected as 203 dpi …
File.WriteAllText("label.json", import.Label.ToJson()); // editierbares Label-Modell (JSON-Schema v3)
string again = import.Label.ToZpl(import.Dpi); // druckt wie das Original
zplcore import legacy.zpl -o label.json --report [--dpi n] [--unit mm] [--diff diff.png]
Ergebnis über den gesamten Korpus (Labelary-Referenzen, Doku, Beispiele, Vorlagen): alle 318 Dateien kommen pixelgleich zurück, 98,9 % der Elemente typisiert. Auflösung wird aus ^MU oder der Etikettenbreite erkannt; Warnungen nennen Zeile und Spalte.
Grenzen: Uhr-Einstellungen (^SO/^ST/^SL), ^CW-Aliase und ~DB-Schriften bleiben Roh-ZPL; ^MC N, ^IL, ^IS, ^JM werden als nicht unterstützt gemeldet; ein RawZplItem lässt sich im Editor nicht verschieben (das ZPL hat absolute Positionen). Die Treue wird gegen den zplCore-Renderer gemessen – zur Druckergenauigkeit siehe Genauigkeit.
24Label Understanding & DSGVO-Schwärzung
ZplExtract macht aus ZPL strukturierte Daten: jeder Text mit Rolle, jeder Barcode mit den Daten, die ein Scanner liest (GS1-Elemente mit Bezeichnung und Prüfung, Digital Link, MaxiCode- und ISO/IEC-15434-Nachrichten), dazu erkannte Entitäten – Empfänger-, Absender- und Rücksendeadressen, Namen, Telefon, E-Mail, Sendungsnummern mit Prüfziffer (UPS 1Z, UPU S10, DHL, PostNL, USPS …), SSCC, GTIN, Charge, Datum, Gewicht – mit Konfidenz, Fundstelle und Begründung. ZplRedact schreibt dasselbe ZPL mit geschwärzten personenbezogenen Daten: Layout bleibt, Barcodes bleiben lesbar (gültige Platzhalterdaten), auch versteckte Stellen wie ^FX-Kommentare, ^DF/^XF-Daten, ^FN-Vorgaben, ^SN, ^RF und 2D-Carrier-Nachrichten werden erfasst – etwa bevor Etiketten an Support, Dienstleister oder in Tickets gehen.
// using ZplCore.Extraction;
var result = ZplExtract.Run(zpl); // ExtractOptions: Dpi, FieldValues, MinimumConfidence
var label = result.Labels[0];
Console.WriteLine(label.Recipient?.Value); // Jürgen Größmann / Bäckerei Süßwaren GmbH · Tor 3 / …
foreach (var e in label.Entities) // TrackingNumber, Sscc (Prüfziffer ok), Date, Weight …
Console.WriteLine($"{e.Type} {e.Value} {e.Confidence:0.00} {e.CheckDigitValid}");
File.WriteAllText("label.json", result.ToJson());
var r = ZplRedact.Run(zpl, new RedactOptions { Categories = RedactCategories.Default, Mode = RedactMode.Mask });
File.WriteAllText("label-redacted.zpl", r.Zpl); // gleiches Layout, Barcodes bleiben gültig
Console.WriteLine(r.Report.ToText()); // Aktionen + Nachprüfung, nie die entfernten Werte
zplcore extract label.zpl --format json -o label.json [--min-confidence 0.3]
zplcore redact label.zpl -o label-redacted.zpl --report [--categories default|personal|all|names,addresses,…]
[--mode mask|replace|remove] [--terms "Projekt X,4711"] # Exit-Code 1, wenn Werte übrig bleiben
Original

Nach ZplRedact.Run (Standard: Maskieren)

25Vector-Lift: PDF/SVG → natives ZPL
Wandelt PDF-Seiten (Carrier- und ERP-Etiketten) und SVGs in natives ZPL statt in ein großes ^GF-Rasterbild: Text wird ^A0-Feld, Barcodes werden erkannt, dekodiert und als passender ^B-Befehl neu erzeugt, Linien, Rahmen, Kreise und Ellipsen werden ^GB/^GD/^GC/^GE; nur der Rest bleibt Grafik. Jede Umwandlung wird gemessen: Original und erzeugtes ZPL werden gerendert und Pixel für Pixel verglichen. Eigenes Paket ZplCore.VectorLift (PDFium), der Kern bleibt ohne PDF-Abhängigkeit.
// using ZplCore.VectorLift;
var result = VectorLift.ConvertSvg(svg, new VectorLiftOptions { Dpi = 203 }); // oder Convert(pdfBytes, …)
File.WriteAllText("label.zpl", result.Zpl); // ^A0-Text, native ^BC/^BQ …, ^GB/^GC, ^GF nur für den Rest
Console.WriteLine(result.ToReport()); // ein Eintrag je Element + gemessene Treue + Größen
Console.WriteLine($"{result.Fidelity:P1} Treue, {result.ZplBytes} statt {result.RasterZplBytes} Bytes als ^GF");
var label = result.ToLabel(); // weiter als editierbares Label-Modell
zplcore lift label.pdf -o label.zpl --report [--page n|all] [--dpi 203] [--crop] [--diff diff.png] [--min-fidelity 0.95] zplcore lift label.svg -o label.zpl --json report.json 799x400 dots @ 203 dpi, fidelity 0.9785, native ink 97.8 %, 370 bytes vs 4,140 bytes ^GF raster (8.9%) Native Barcode @60,200 336x90 CODE_128 "SN20260815" score 1.000 data ok
SVG-Original

Erzeugtes ZPL, gerendert

Quelle: lift-label.svg · Ergebnis: lift-label.zpl
Grenzen: Text wird in Schrift 0 gedruckt, außer Sie übergeben eine TrueType-Schrift, deren Lizenz-Flags das Einbetten erlauben (~DU + ^A@); in PDFs eingebettete Schriften werden nie extrahiert. Text ohne Textebene (als Umrisse gezeichnet) bleibt Grafik – keine OCR. Clip-Pfade zählen als Rechteck; QR/Aztec-Masken und -Versionen wählt der Encoder (Daten identisch, Muster kann abweichen). Der SVG-Leser kennt keine CSS-Stylesheets, Filter, Masken, Verläufe, Skripte oder externen Ressourcen. PDFium ist nicht thread-sicher (Aufrufe werden serialisiert); typisch 1–4 s pro 4 × 6″-Etikett.
26Testing+, zplcore check & GitHub Action
Etiketten sind Code – testen Sie sie wie Code. ZplCore.Testing prüft, was ein Etikett druckt, nicht nur wie es aussieht: jeder Barcode wird mit zwei Decodern zurückgelesen, jedes Element vermessen. Schlägt etwas fehl, nennt die Meldung Element, Position (Dots, mm, ZPL-Zeile) und ein Bild mit markiertem Element. Läuft mit xUnit, NUnit, MSTest und jedem anderen Runner.
dotnet add reference src/ZplCore.Testing/ZplCore.Testing.csproj
// using ZplCore.Compliance; using ZplCore.Testing;
ZplAssert.Label(zpl)
.HasLabels(1)
.NoOverlaps()
.NoClipping()
.Complies(CompliancePacks.Gs1LogisticLabel)
.Barcode("SSCC").Decodes("(00)376104250021234569").HasGrade(atLeast: 'B').HasQuietZones()
.And.Text("RETAIL DC").IsWithinLabel().DoesNotOverlap("SSCC")
.And.FitsPrinter(ZplCore.Twin.PrinterProfiles.Get("ZD421d-203"))
.MatchesSnapshot("pallet"); // Pixel + Semantik, Freigabe mit ZPLCORE_ACCEPT_SNAPSHOTS=1
Fehlermeldung mit Bild

Was geprüft werden kann
HasLabels,Renders,NoOverlaps(echte Druckfarbe, Rahmen zählen nicht),NoClippingComplies(CompliancePacks…),AllBarcodesDecode(),AllBarcodesHaveGrade('C')Barcode("SSCC"):Decodes,HasGrade,HasQuietZones,HasXDimension,IsGs1Text("…"):IsWithinLabel,DoesNotOverlap,IsAbove/IsBelow,HasCharHeightFitsPrinter(PrinterProfile)mit dem Printer Twin,FieldEquals("FN1", …)MatchesSnapshot: Pixel und Semantik – ein fehlschlagender Snapshot sagt, was sich geändert hat („Text verschoben um +12 Dots“)
Label(zpl).Barcode("SSCC").Decodes("(00)376104250021234576") failed: the decoders read "(00)376104250021234569", expected "(00)376104250021234576".
element ^BC Code128 "(00)376104250021234569" at x=94 y=800 (11.8 / 100.1 mm), 624 × 260 dots (78.1 × 32.5 mm) · line 17, col 1
source ^FO94,800^BCN,260,Y,N,N,D^FD(00)376104250021234569^FS
decoders zxing-cpp: "(00)376104250021234569" · ZXing.Net: "(00)376104250021234569"
CI ohne Testprojekt: zplcore check
Dieselben Prüfungen über viele Dateien: Rendern, Layout, Compliance-Pakete, Scan-Proof, Druckerprofil, Snapshots und Vergleich mit dem Basis-Branch – Ausgabe als Text, JUnit-XML, SARIF, JSON, Markdown (für PR-Kommentare) oder GitHub-Annotationen. Exit-Codes 0/1/2.
zplcore check "labels/**/*.zpl" --pack gs1-logistic --verify --min-grade C --layout --printer ZD421d-203 zplcore check labels/ --snapshots tests/__snapshots__ --format junit -o labels.junit.xml zplcore check labels/ --baseline base/ --report-dir report/ --format markdown -o report.md # vorher/nachher/diff-PNGs zplcore snapshot accept tests/__snapshots__
GitHub Action
Die Composite Action tools/github-action prüft in jedem Pull Request die geänderten Etikettendateien gegen den Basis-Branch und schreibt Annotationen, eine Job-Zusammenfassung, einen aktualisierten PR-Kommentar mit Vorher-/Nachher-/Diff-Bildern und SARIF für Code Scanning. Sie liegt im Lieferumfang; die Veröffentlichung im Marketplace als zplcore/label-check ist vorgesehen. Bis dahin: Ordner ins eigene Repository kopieren und die CLI aus Ihrem Paket-Feed installieren lassen.
name: Label check
on:
pull_request:
paths: ["**/*.zpl", "**/*.json"]
permissions:
contents: read
pull-requests: write # PR-Kommentar
security-events: write # SARIF → Code Scanning
jobs:
labels:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/label-check # Kopie von tools/github-action
with:
packs: gs1-logistic
min-grade: C
nuget-source: https://nuget.example.com/v3/index.json # Feed mit dem Paket ZplCore.Cli
Die Note ist eine digitale Vorprüfung nach dem Vorbild von ISO/IEC 15415/15416, keine ISO-Verifikation; Compliance-Pakete sind Pre-Checks, keine Zertifizierungen. Fork-PRs ohne Schreibrechte bekommen statt Kommentar und SARIF nur Annotationen und die Job-Zusammenfassung.
27MCP-Server für Coding-Agents
zplcore-mcp ist ein Model-Context-Protocol-Server: Coding-Agents wie Claude Code, Claude Desktop, VS Code/GitHub Copilot oder Cursor rendern, prüfen und korrigieren damit ZPL-Etiketten offline in einer Schleife – ZPL schreiben, PNG ansehen, Diagnosen lesen, verbessern. 26 Werkzeuge: render, lint, format, diff, fill_template, convert_epl, comply, verify, import_label, extract, redact, gs1_link_build/_parse, gs1_migrate, rfid_encode/_decode/_check, printer_profiles, printer_check, print_estimate, cut_plan, lift_svg, list_symbologies, describe_symbology, explain_command, get_limits.
dotnet pack tools/ZplCore.Mcp -c Release -o nupkg dotnet tool install -g ZplCore.Mcp --add-source nupkg # Befehl "zplcore-mcp", braucht die .NET-10-Runtime claude mcp add zplcore -- zplcore-mcp # Claude Code (aktuelles Projekt; --scope user für alle)
Claude Code (.mcp.json im Repository)
{ "mcpServers": { "zplcore": { "type": "stdio", "command": "zplcore-mcp", "args": [] } } }Claude Desktop (claude_desktop_config.json)
{ "mcpServers": { "zplcore": { "command": "/Users/you/.dotnet/tools/zplcore-mcp", "args": [] } } }GUI-Apps kennen ~/.dotnet/tools oft nicht im PATH – absoluten Pfad angeben.
VS Code / GitHub Copilot (.vscode/mcp.json)
{ "servers": { "zplcore": { "type": "stdio", "command": "zplcore-mcp", "args": [] } } }Cursor (.cursor/mcp.json)
{ "mcpServers": { "zplcore": { "command": "zplcore-mcp", "args": [] } } }Sicherheitsmodell
Isoliert
- Nur stdio: kein HTTP/SSE, kein lauschender Socket; Logs nur auf stderr, nie die Eingaben
- Werkzeuge nehmen Etikettentext und liefern Ergebnisse – kein Zugriff auf Dateien, Netzwerk, Drucker, Prozesse oder Umgebungsvariablen; Druckerspeicher-Befehle arbeiten auf einem Speicher, der nur einen Aufruf lebt
- Jeder Aufruf läuft in einem eigenen Worker-Prozess mit leerer Umgebung (keine Tokens des Clients), Zeit- und Speicherlimit; macOS zusätzlich unter
sandbox-exec(kein Netzwerk, keine Schreibzugriffe außerhalb des Arbeitsordners, Home-Verzeichnis nicht lesbar) - Kein PDF:
lift_svgnimmt nur SVG-Text, PDFium wird nicht mitgeliefert
Begrenzt
- Strikte Schemas: unbekannte Argumente, falsche Typen, Werte außerhalb des Bereichs und doppelte Schlüssel werden abgelehnt
- Jeder Aufruf im
ZplLimits-Bereich (siehe unten): 10 Etiketten, 12 Mio. Dots, 4 MiB Grafik, 16 MiB Druckerspeicher, 64 Ki Zeichen Felddaten - 20 s und 1536 MB pro Aufruf (einstellbar:
--timeout-seconds,--memory-mb,--workers) - SVG: 2 Mi Zeichen, kein DOCTYPE/keine Entities, keine externen Referenzen, ≤ 256 Ebenen, ≤ 200.000 Elemente
- Texte aus dem Etikett sind im Ergebnis als nicht vertrauenswürdige Daten markiert
28NativeAOT & Limits für fremde Eingaben
ZplCore, ZplCore.VectorLift, ZplCore.Testing, CLI und MCP-Server sind NativeAOT-kompatibel (IsAotCompatible, 0 Trim-/AOT-Warnungen). Rendering, Barcodes, Label-Modell, JSON, Compliance, Scan-Proof, Extraktion/Schwärzung, GS1, RFID, Printer Twin und Vector-Lift liefern unter NativeAOT dieselben Ausgaben wie mit JIT – build/aot-smoke.sh vergleicht rund 790 Ausgaben Byte für Byte (geprüft auf osx-arm64, linux-arm64 und linux-x64).
dotnet publish tools/ZplCli -p:PublishProfile=aot # Host-RID, oder -r linux-arm64 / linux-x64 … dotnet publish tools/ZplCore.Mcp -p:PublishProfile=aot dotnet publish MyApp -c Release -p:PublishAot=true # eigene App mit ZplCore-Referenz
Für NativeAOT-Container ohne .NET-Laufzeit empfiehlt sich mcr.microsoft.com/dotnet/runtime-deps:10.0-noble-chiseled-extra (distroless, non-root, inkl. ICU für kulturabhängige Formatierung; Gesamtgröße ~79–105 MB inkl. App). Im Praxistest auf einem Synology DS923+ NAS (linux-x64, AMD Ryzen Embedded R1600, 2 Kerne / 4 Threads, Synology Container Manager) lief der komplette Korpus unter NativeAOT 24 % schneller als unter JIT (10,7 s statt 14,0 s bei 793/793 identischen Ausgaben) und erreichte im NativeAOT-Container 237.428 Labels/s beim Parsen, 1.233 Labels/s beim Bitmap-Rendern und 295 PNG-Etiketten/s (> 17.500 PNGs/min). Auf 32-Bit-ARM-Geräten wie dem Raspberry Pi 2 (linux-arm / armv7l) läuft zplCore als Self-Contained- oder Framework-Dependent-Container auf aspnet:10.0-noble-chiseled-extra (174 MB), ab 64-Bit-ARM (linux-arm64, Raspberry Pi 4/5) direkt als NativeAOT.
T bleiben erhalten; verschachtelte Typen ([Supplier.Name]) entfernt der Trimmer, wenn sie nicht statisch referenziert sind. Unter NativeAOT wirft das eine MissingMemberException mit Typ, Mitglied und Abhilfe (TrimmerRootAssembly) statt eines leeren Felds. JSON-, Dictionary- und DataRow-Bindung sind nicht betroffen. NativeAOT braucht die native Toolchain der Plattform; musl/Alpine ist noch kein Ziel.ZplLimits: ein Bereich für alles
Für fremde Eingaben (Uploads, Webdienste, Agents) setzt ZplLimits.Enforce harte Grenzen für jeden Renderer im Bereich – auch für die, die Compliance-Check, Scan-Proof, Import, Extraktion, GS1-Migration, Printer Twin, Cut-Plan und Vector-Lift intern anlegen, über Tasks und Parallel hinweg. Einstellungen werden auf den strengeren Wert gekappt, Schriftquellen, Systemschriften und Hintergrundbilder abgeschaltet; optional eine feste Uhr. Der MCP-Server führt jeden Aufruf in diesem Bereich aus.
// using ZplCore.Compliance;
using (ZplLimits.Enforce(new ZplLimits { MaxLabels = 10, MaxGraphicBytes = 4 << 20, FixedClock = DateTime.UnixEpoch }))
{
// gilt für jeden Renderer in diesem Bereich – auch die, die Compliance, Scan-Proof,
// Import, Extraktion, Printer Twin und Vector-Lift intern anlegen (auch über Tasks/Parallel)
var report = ZplCompliance.Check(untrusted, CompliancePacks.Gs1LogisticLabel);
}
29Beispiel-Etiketten
Die fünf Etiketten aus samples/zpl/five-labels.zpl – Versand, Produkt, Lagerplatz, Inventar, Paletten. Auch als PDF.
30Kommandozeile, 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 format label.zpl -o pretty.zpl --comments # ein Befehl pro Zeile; --minify, --indent n zplcore diff alt.zpl neu.zpl -o diff.png --boxes # Pixel-Overlay + Zeilen-Diff, Exit-Code 1 bei Unterschied zplcore formats zplcore comply label.zpl --pack gs1-logistic,sunrise # Compliance-Pre-Check (Text/JSON/SARIF) zplcore verify label.zpl --fail-on mismatch,grade:C # Scan-Proof zplcore check "labels/**/*.zpl" --verify --layout --format junit # CI: alles in einem Lauf zplcore import legacy.zpl -o label.json --report # ZPL → Label-Modell zplcore extract label.zpl --format json · zplcore redact label.zpl -o out.zpl zplcore gs1 migrate label.zpl -o label-2d.zpl · zplcore gs1 link "(01)…" zplcore rfid encode --sscc 340123450000000017 --company-prefix 7 --format zpl zplcore twin check label.zpl --profile ZD421d-203 · zplcore twin estimate … zplcore serve --chaos chaos.json --cut-marks # virtueller Drucker mit Störungen zplcore lift label.pdf -o label.zpl --report # PDF/SVG → natives ZPL
Weitere Optionen: --labels 0,2, --flip x|y|xy, --landscape, --no-antialias, --field n=Wert, --encoding. zplcore --help listet alle Befehle; Details zu den Prüfbefehlen in den Abschnitten Compliance bis Vector-Lift.
Server-Image (distroless), NativeAOT-Container, NAS & Helm-Chart
build/Dockerfile.server veröffentlicht samples/WebApiDemo auf mcr.microsoft.com/dotnet/aspnet:10.0-azurelinux3.0-distroless (oder aspnet:10.0-noble-chiseled-extra): keine Shell, Benutzer app (nicht root), Port 8080, /health, SQLite-Demodaten in /tmp – läuft mit --read-only. Für NativeAOT-Container ohne .NET-Laufzeit dient mcr.microsoft.com/dotnet/runtime-deps:10.0-noble-chiseled-extra als Basis (~79–105 MB Image-Größe). Damit wird selbst ein vorhandenes NAS (z. B. Synology DS923+ über den Synology Container Manager) oder ein Raspberry Pi im Handumdrehen zum lokalen Labelprint- und Rendering-Server im Lager (~90 MiB RAM, 67 ms PNG- und 113 ms PDF-Vorschau über LAN auf einer DS923+). 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":"00340000000000000014","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
31Diagnosen, 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 |
ZPL1301 | Warning | FieldValues-Schlüssel ohne passendes ^FN (INVALID_FIELD_VALUE_KEY) |
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);
});
32Plattformen, Schriftsysteme & Datenbanken
Plattformen
Geprüft auf macOS, Windows 11, Debian (mcr.microsoft.com/dotnet/sdk:10.0), Ubuntu 24.04 Chiseled (aspnet & runtime-deps:10.0-noble-chiseled-extra), Azure Linux 3.0 (sdk, runtime, runtime-distroless), auf einem Synology DS923+ NAS (linux-x64, Container Manager, JIT & NativeAOT) sowie auf dem Raspberry Pi (linux-arm 32-Bit & linux-arm64). Keine nativen Pakete nötig: SkiaSharp NativeAssets.Linux.NoDependencies, HarfBuzzSharp und PDFium bringen ihre Bibliotheken 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:






33Tests & Verifikation
xUnit-Testsuite
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.
Prüfwerkzeuge, MCP & Pakete
Eigene Suiten für ZplCore.Testing, den MCP-Server (inklusive Angriffs-, Fuzz- und End-to-End-Tests der Sandbox) und Vector-Lift; Compliance, Scan-Proof, Import, Extraktion, GS1, RFID, Printer Twin und Chaos-Drucker in der Hauptsuite. build/package-smoke.sh packt die NuGet-Pakete und veröffentlicht eine Test-App (auch win-x64), build/aot-smoke.sh vergleicht JIT- und NativeAOT-Ausgaben.
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 # alle 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 build/package-smoke.sh # NuGet-Pakete + Test-App (Windows-Publish inklusive) build/aot-smoke.sh # JIT- gegen NativeAOT-Ausgaben
34Genauigkeit
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 |
gd-orient | 0,848 | 0,844 | 0,852 | 812 | 957 |
gs-symbols | 0,859 | 0,857 | 0,860 | 1059 | 1214 |
maxicode-modes | 0,860 | 0,797 | 0,933 | 2143 | 9764 |
fp-rot | 0,880 | 0,894 | 0,868 | 407 | 297 |
code128-invoc | 0,901 | 0,840 | 0,973 | 825 | 5512 |
fb-rot-i | 0,904 | 0,915 | 0,894 | 1067 | 846 |
ci0-raw-cp850 | 0,908 | 0,922 | 0,894 | 5151 | 3383 |
ci-code-pages | 0,912 | 0,894 | 0,932 | 5775 | 8987 |
fb-bitmap | 0,913 | 0,917 | 0,909 | 806 | 581 |
fonts-efgh | 0,915 | 0,950 | 0,883 | 664 | 218 |
ci0-fh-cp850 | 0,916 | 0,927 | 0,905 | 4680 | 3248 |
ft-barcodes-r | 0,923 | 0,923 | 0,923 | 3687 | 3719 |
ci-national-sets | 0,943 | 0,940 | 0,947 | 4949 | 5169 |
gs-sizes | 0,943 | 0,967 | 0,921 | 2002 | 797 |
Die 15 schwächsten von 169 Fällen; 131 Fälle erreichen ≥ 0,99, 85 sind pixelgleich (1,000). 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), ^GD-Ausrichtung, ^GS-Symbole, MaxiCode-Modi, gedrehte Schriften und die Code-Page-Fälle (^CI), in denen sich vor allem die Glyphenformen der Ersatzschriften unterscheiden. QR-Codes (^BQ, Modell 2) sind modulgleich mit dem Zebra-Druckbild – Moduswahl, Version, Fehlerkorrektur und Maske, geprüft an über 3.600 Labelary-Referenzsymbolen. QR-Modell 1 wird als Modell 2 ausgegeben. PDF417 (^B7) und MicroPDF417 (^BF) sind codewortgleich mit Labelary – Kompaktierung wie Zebra, unter ^CI28 Latin-1-Bytes ohne ECI (Zeichen über U+00FF entfallen), wie auch bei ^BX, ^BO und ^BD. Alle Symbole bleiben dekodierbar; Labelary rendert ^BB und ^B4 nicht, diese sind ausschließlich gegen bwip-js verifiziert.
35Performance
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
36Grenzen
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) und MicroPDF417 (^BF) sind pixelgleich mit Labelary; Macro-PDF417 (^B7mit^FM) und der MicroPDF417-Teil von^BTsind nicht gegen Labelary verifiziert - QR (
^BQ): Modell 1 wird als Modell 2 ausgegeben ^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
Prüf- und Analysewerkzeuge
- Compliance und GS1-Migration sind Pre-Checks gegen öffentliche Spezifikationen (Stand Oktober 2026), keine Zertifizierung und kein Ersatz für die Abnahme durch Carrier, Händler oder Behörde
- Scan-Proof-Noten sind eine digitale Vorprüfung nach dem Vorbild von ISO/IEC 15415/15416, keine ISO-konforme Verifikation; Druckphysik wird nicht modelliert. Ohne ladbare zxing-cpp-Bibliothek (glibc < 2.38, musl) nur ein Decoder
- Extraktion und Schwärzung sind regelbasiert und assistierend: keine Vollständigkeitsgarantie, kein Lesen von Text in Bildern
- Printer Twin: unbekannte Profilwerte werden nicht geprüft (
ZPL5001); die Druckzeit ist eine Schätzung - Vector-Lift: Text in Schrift 0 (ohne eigene TrueType-Schrift), keine OCR, SVG ohne CSS/Filter/Verläufe; PDF nur in
ZplCore.VectorLift, nicht im MCP-Server - Chaos-Drucker: JSON-Port 9200 und Einzelverbindungs-Port nicht simuliert; Annahmen, wo das Handbuch schweigt, sind dokumentiert
- NativeAOT: verschachtelte POCO-Mitglieder bei der Datenbindung brauchen
TrimmerRootAssembly; musl/Alpine ist noch kein Ziel
37Lizenzen & 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; zweiter Decoder von Scan-Proof und Vector-Lift, in den Tests zum Zurücklesen der Barcodes |
| zxing-cpp / ZXingCpp | Apache-2.0 | Decoder von Scan-Proof und Vector-Lift (nur Lesen) |
| PDFium (bblanchon.PDFium) | BSD-3-Clause / Apache-2.0 | nur ZplCore.VectorLift: PDF lesen und als Referenz rendern |
| GS1 Barcode Syntax Dictionary, Digital-Link-Kompressionstabellen | Apache-2.0 | Schlüssel-/Attributregeln von GS1 Digital Link, Lesen alter komprimierter URIs |
| Okapi Barcode | Apache-2.0 | Schwellwerte der QR-Moduswahl |
| 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

