zplCore.comzplCore.com

zplCore.com – High Performance .NET Labelprint Engine

.NET 10ZPL II + EPL2API-Referenz (SDK) →

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.

–unterstützte ZPL-Befehle
144Barcode-Symbologien
.NET 10Windows · Linux · macOS
0,986Labelary-Score (Mittel, 169 Fälle)
Versandetikett, gerendert aus samples/zpl/five-labels.zpl

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
EigenschaftStandardBedeutung
Dpi203Druckauflösung 152/203/300/600
LabelWidth / LabelHeight4×6 Zollin Punkten; ^PW/^LL überschreiben, außer ForceLabelWidth/Height
InkColor / BackgroundColorSchwarz / WeißFarbband- und Papierfarbe
OutputFormat / QualityPng / 95Png, Jpeg, Webp, Bmp, Pdf, Grf
OutputRotationNoneAusgabe um 90/180/270° drehen
AntiAlias / InvertColorstrue / falseGlättung bzw. Negativ
BackgroundImage / WatermarkImage + WatermarkOpacity– / 0.25Vordruck unter bzw. Wasserzeichen über dem Inhalt
RfidIndicator + RfidIndicatorColor / RfidIndicatorOpacityWatermark / #C0C0C0 / 1graues RFID-Symbol hinter dem Inhalt bei Etiketten mit ^RF/^WT/^WF/^RQ (nur Vorschauformate, siehe ^RF)
ThumbnailSize0Ausgabe auf max. N Pixel verkleinern
RenderOutputIndexesnullnur ausgewählte Etiketten kodieren
DefaultCodePage850Feldkodierung ohne ^CI
TraceElementsfalsePosition/Inhalt jedes Felds sammeln (Editoren, Tests)
ClockDateTime.NowUhr für ^FC
DrawRfidfalse^RF-Daten sichtbar einzeichnen
PdfTitle/Author/Subject/Keywords/Creator–PDF-Metadaten
PaperWidth/PaperHeight/PaperMargin/LabelGap0PDF-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

WatermarkImage mit Opacity 0.35

RFID sichtbar (DrawRfid)

^RF-Daten mit DrawRfid = true

Ausgabeformate & Bitmaps

PNG · JPEG · WebP · BMP

Ein Bild pro Etikett. BMP als 24-Bit (eigener Encoder).

PDF

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

~WC Konfigurationsetikett – gerendert mit zplCore.com

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

RfidIndicator = Watermark (Standard)

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.

BefehlBeschreibungKategorie

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()
Label mit DotCode, GS1 QR und Codablock F

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]);
samples/zpl/sample.epl EPL → Bild

samples/zpl/sample.epl, gerendert mit EplRenderer
Ü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
Formular, Etikett 1 Formular, Etikett 2 Formular, Etikett 3

Unterstützte EPL2-Befehle

BefehlBedeutung
NBildpuffer löschen
q / QEtikettenbreite / -höhe und Lücke
RReferenzpunkt (Ursprung verschieben)
ZT / ZBDruckrichtung oben / unten (ZB = 180°)
AText: residente Schriften 1–5 (203/300 dpi), Rotation, Vergrößerung, N/R (invertiert)
B1D-Barcodes: Code 128 (A/B/C/Auto), Code 39, Code 93, EAN/UPC, I2of5, Codabar, MSI, Plessey, POSTNET …
b2D-Barcodes: QR (Q), Data Matrix (D), PDF417 (P), MaxiCode (M), Aztec (A)
XRechteck (Kasten)
LO / LW / LELinie schwarz / weiß / XOR
LSDiagonale
GWGrafik direkt (binär)
GM / GG / GKPCX-Grafik speichern / drucken / löschen
FS … FEFormular aufzeichnen
FR / FKFormular abrufen / löschen
VFormularvariable (Länge, Ausrichtung, Prompt)
CZähler (Länge, Ausrichtung, Schrittweite ±)
?Variablendaten für V und C
PDrucken (Anzahl Sätze), erhöht Zähler
TS / TT / TDUhr stellen, Zeit- und Datumsfelder
S D I O U JF JB W …Geschwindigkeit, Dichte, Zeichensatz, Optionen: über PrinterControl gemeldet
Steuerbefehle ohne Einfluss auf das Bild (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
Code 39 mit Farben, Text oben, 90° gedreht
EigenschaftStandardBedeutung
Symbology, CodeCode128, ""Symbologie und Daten
UnitPixelEinheit für BarWidth/BarHeight/BearerBarWidth: Pixel, Millimeter, Inch, Mil
Dpi300Auflösung für Raster und Einheitenumrechnung
BarWidth2Schmaler Strich bzw. Modulgröße
BarHeight80Höhe linearer Codes (2D-Module sind quadratisch)
WideRatio3.0Breit-zu-schmal (2.0–3.0) für Zwei-Breiten-Codes
QuietZonenullRuhezone in Modulen (null: 10 bei 1D, 4 bei 2D)
ForeColor, BackColor, TextColorSchwarz/Weiß/SchwarzFarben (auch in SVG/PDF/EPS)
TextPositionBelowKlarschrift: None, Below, Above
HumanReadableTextnulleigener Klartext statt der Daten (null = automatisch inkl. Prüfziffer)
FontSize, Typeface0, nullSchriftgröße in Punkt (0 = proportional zum Modul), Schrift
GuardBarstrueEAN/UPC-Randzeichen in die Klarschrift verlängern
BearerBars, BearerBarWidthNone (ITF-14: Frame), 4Schutzbalken: Frame oder HorizontalRules
Supplementnull2- oder 5-stelliges EAN/UPC-Add-on
AddChecksumfalseoptionale Prüfziffer (Code 39, 2 of 5, Code 11 …)
Rotation00, 90, 180, 270 Grad
QrErrorCorrection'M'L, M, Q, H (QR, Micro QR, rMQR, GS1 QR)
Pdf417Columns, Pdf417SecurityLevel0, 2PDF417-Spalten (0 = automatisch), Sicherheitsstufe
MaxiCodeMode4MaxiCode-Modus 2–6
DataBarSegmentsPerRow4Segmente pro Zeile bei DataBar Expanded Stacked
ModuleShape, FinderShapeSquare, SquareModulform 2D (Square, Circle, RoundedSquare, Diamond); QR-Finder (Square, Rounded, Circle)
LogoImage, LogoSizePercent, LogoOpacity, LogoBackgroundnull, 20, 1, trueLogo in der Mitte von 2D-Codes (QR: Fehlerkorrektur H)
LightMarginIndicatorfalseEAN/UPC: </> in den Ruhezonen
TextFormatPattern, HumanReadableAlignmentnull, CenterKlarschrift formatieren (Maske oder {0:…}) und ausrichten (Center, Left, Right, Justify)
CodeEncodingTextDaten als Text, Base64 oder Hex
ProcessTildefalse~~, ~dNNN, ~1 (FNC1), ~2IICC (Structured Append), ~eNNNNNN (ECI)
QrVersion, QrMask0, -1QR-Version 1–40 und Maske 0–7 erzwingen
StructuredAppendInfo / StructuredAppend(i, n)nullDaten 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,
};
EAN-13 mit Add-on

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,
};
ITF-14 mit Bearer-Rahmen

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,
};
Swiss QR Code mit Schweizerkreuz

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 };
EPC-QR / GiroCode

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
Ultracode – farbige ModuleDotCode – 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),
};
Code 128 um 270° gedreht, grün

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),
};
QR Code mit Logo und runden Findern

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 };
QR mit abgerundeten ModulenData Matrix als PunkteAztec mit Rauten

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:### ### ###}"
EAN-13 mit Light Margin IndicatorCode 128 mit Textmaske

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);
QR mit ECI 26QR Version 5 Maske 3Structured Append 1/2Structured Append 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
Symbology.Tlc39 – gerendert mit zplCore.comDeutsche Post BZL

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

GS1-128 (01)(17)(10) – gerendert mit zplCore.com

SSCC-18

SSCC-18 (AI 00)

EAN-14

EAN-14 (AI 01)

GS1 Data Matrix

GS1 Data Matrix

EAN-13 Composite (CC-A)

EAN-13 Composite – gerendert mit zplCore.com

DataBar Expanded Composite

DataBar Expanded Composite

GS1-128 Composite (CC-C)

GS1-128 Composite – gerendert mit zplCore.com
Composite-Daten: "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"));
Ergebnis: zwei Datensätze → zwei Etiketten
Label-Modell 1 – gerendert mit zplCore.com
Label-Modell 2 – gerendert mit zplCore.com
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();
Tabelle und Repeater

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)
Masken-Zähler über drei Etiketten

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();
Bogen mit 2×3 Etiketten, Start bei Position 2

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 1 (Vorderseite)

Seite 2

Seite 2 (Rückseite)

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 kg

Text-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));
MethodeZweck
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 / WriteZplAsynckomplett als String oder in einen TextWriter
Generate / GenerateAsynclazy, ein ZPL-Format pro Datensatz
Batches(records, n)Druckjobs zu je n Etiketten
PrintAsync(printer, records, n)direkt an ZplPrinterClient senden, liefert Anzahl
Render / RenderPdfVorschau als Bilder bzw. PDF
PropertyPath: 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);
Across(3, 3): drei Datensätze nebeneinander

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 / FeldBedeutung
.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, CreatedName und Erstellzeit des Auftrags
PrintJobInfo.RecordCountAnzahl Datensätze – im Vorlauf bekannt, wenn eine Liste/Collection übergeben wird; im Nachlauf immer gesetzt
PrintJobInfo.LabelCountgedruckte Etiketten inkl. Kopien (Copies)
PrintJobInfo.FirstKey, LastKeySchlü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

Vorlauf: JobName, Created, RecordCount

Datensatz 2 (^PQ2)

Datensatz 2, Kopie 1

Nachlauf (^MMC)

Nachlauf: LabelCount, FirstKey, LastKey
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");
OptionWirkung
JobNameName für PrintJobInfo.JobName (Standard „Druckauftrag“)
CutAfterLastLabel^MMT für alle, ^MMC nur für das letzte Etikett
StoredFormatNameDatensatz-Etikett einmal per ^DF speichern, dann nur ^XF + ^FN
KeyFieldProperty-Pfad für FirstKey/LastKey
CopiesFieldProperty-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)

Vorlauf-Etikett der Demo

Versandetikett 1 von 50

Versandetikett der Demo

Nachlauf (^MMC)

Nachlauf-Etikett der Demo
massdata-efcore.zpl (Auszug)
Erzeugte Dateien: massdata-efcore.zpl (9 KB, mit ^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");
}
KlasseCommunicationTypeStatus (~HS)
TcpPrinterConnectionNetwork (Raw, Port 9100)ja
SerialPrinterConnectionSerial (Baud, Parität, Handshake)ja
RawFilePrinterConnectionUsb, File (Gerätepfad oder Datei)nein
WindowsSpoolerPrinterConnectionWindowsDriver (RAW-Druckjob, nur Windows)nein
CupsPrinterConnectionCups (lp -o raw)nein
StreamPrinterConnectionStream (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-Grafik
var 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

Mit ZplBuilder erzeugtes Etikett
ZPL

16Farbe & Vorschau

Dasselbe ZPL, drei Einstellungen.

InkColor + BackgroundColor

InkColor #1d3b8a, BackgroundColor #fff7d6

Roter Farbband-Look

InkColor #c8102e – gerendert mit zplCore.com

InvertColors

InvertColors = true
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.

Ein Pre-Check, keine Zertifizierung. Ein leeres Ergebnis heißt: keine Regel hat ein Problem gefunden. Es ersetzt weder die Abnahme durch Carrier, Händler oder Behörde noch ein Barcode-Prüfgerät für die Druckqualität des fertigen Etiketts. Quellenstand: Oktober 2026.
Paket (--pack)InhaltQuellen
gs1-logisticSSCC in GS1-128, X-Dimension ≥ 0,495 mm, Balkenhöhe, Ruhezonen, Klarschrift ≥ 3 mm, AI-Formate und -KombinationenGS1 General Specifications 26.0, Logistic Label Guideline 1.3
sunriseSunrise 2027: GTIN in GS1 DataMatrix oder QR-Code mit GS1 Digital Link neben EAN/UPC, Modulgröße, URI-SyntaxGenSpecs 26.0, Digital Link URI Syntax 1.7.0
amazon-fba, ups, fedex, dhl-paketFormate, Pflicht-Barcodes, Ruhezonen, MaxiCode-Carrier-Nachricht, 1Z-Prüfzifferöffentliche Carrier-Dokumentation (teils als „advisory“ markiert)
udiUDI-DI (01), Produktionskennzeichen, Datumsregeln, zulässige Träger21 CFR 801, EU-MDR Anhang VI, GenSpecs 26.0
rfidschreibt ^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

Palettenetikett mit rot markierten Befunden ZPL3104 und ZPL3110 an beiden GS1-128-Barcodes

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 mit ComplianceRulePack; 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.

Digitale Vorprüfung nach dem Vorbild von ISO/IEC 15415/15416 – keine ISO-konforme Verifikation. Die Note A–F wird aus dem gerenderten Bild berechnet; Druckphysik (Schwärzung, Geschwindigkeit, Material, Druckkopfverschleiß) ist nicht modelliert und braucht ein kalibriertes Prüfgerät am gedruckten Etikett.
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 = Felddaten
  • DataAltered – gelesen = erwartet, aber die Symbologie hat die Daten verändert (z. B. falsche Prüfziffer ersetzt)
  • Mismatch – ein Decoder liest andere Daten
  • NotDecoded / 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… wie zplcore 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.

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

Versandetikett mit rot schraffiertem Bereich rechts: alles jenseits von 72 mm Druckkopfbreite fehlt

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

Sechs Kartonetiketten in zwei Spalten; unter Etikett 3 und 6 eine rote gestrichelte Schnittlinie mit Schere und Pausensymbol

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

Versandetikett mit Absender, Empfänger, Sendungsnummer und 2D-Codes

Nach ZplRedact.Run (Standard: Maskieren)

Dasselbe Etikett: Namen, Adressen, Sendungsnummer und Barcode-Inhalte durch Platzhalter ersetzt, Layout unverändert
Assistierend und regelbasiert: Beschriftungen in acht Sprachen, Layout, Postleitzahlmuster für 15 Länder und öffentliche Prüfziffernverfahren – deterministisch, ohne KI, ohne Netzwerk. Die Erkennung kann Dinge übersehen; die Schwärzung gibt keine Vollständigkeitsgarantie. Der Standard ist vorsichtig (auch nicht klassifizierter Text wird geschwärzt), das Ergebnis wird durch eine zweite Suche geprüft, der Bericht nennt nie die entfernten Werte. Text in Bildern wird nicht gelesen. Geschwärzte Ausgabe vor dem Weitergeben prüfen – das Werkzeug unterstützt die Datenminimierung nach DSGVO, ersetzt aber keine Prüfung.

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

SVG-Etikett mit Titel, Lagerplatz, Linie, Code 128, Seriennummer und QC-Stempel

Erzeugtes ZPL, gerendert

Dasselbe Etikett aus nativem ZPL: Text in Schrift 0, Code 128 als ^BC, Rahmen, Linie und Kreis als ZPL-Formen

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

Palettenetikett, der SSCC-Barcode ist rot markiert

Was geprüft werden kann

  • HasLabels, Renders, NoOverlaps (echte Druckfarbe, Rahmen zählen nicht), NoClipping
  • Complies(CompliancePacks…), AllBarcodesDecode(), AllBarcodesHaveGrade('C')
  • Barcode("SSCC"): Decodes, HasGrade, HasQuietZones, HasXDimension, IsGs1
  • Text("…"): IsWithinLabel, DoesNotOverlap, IsAbove/IsBelow, HasCharHeight
  • FitsPrinter(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_svg nimmt 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.

Einschränkung bei POCO-Bindung: Mitglieder der obersten Ebene von 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.

CodeSchwereBedeutung
ZPL1001ErrorBefehl mit fehlerhaften Parametern ignoriert (Rest des Etiketts wird gerendert)
ZPL1002WarningFeld konnte nicht gezeichnet werden (z. B. ungültige Barcode-Daten)
ZPL1003WarningBarcode-Daten länger als jede Symbologie fassen kann
ZPL1101Warning^GF größer als das Etikett oder MaxGraphicBytes
ZPL1102Warning^GF-Daten nicht dekodierbar
ZPL1103Warning^XG/^IM verweist auf nicht gespeicherte Grafik
ZPL1201Warning^A@-Schrift nicht gefunden – Schrift A wird verwendet, wie am Zebra
ZPL1301WarningFieldValues-Schlüssel ohne passendes ^FN (INVALID_FIELD_VALUE_KEY)
ZPL2001InfoAkzeptiert, 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:

Arabisch: TTF im Druckerspeicher (~DU + ^A@)
Arabisch
Chinesisch: TTF im Druckerspeicher (~DU + ^A@)
Chinesisch
Hindi: TTF im Druckerspeicher (~DU + ^A@)
Hindi
Koreanisch: TTF im Druckerspeicher (~DU + ^A@)
Koreanisch
Japanisch: TTF im Druckerspeicher (~DU + ^A@)
Japanisch
Russisch: TTF im Druckerspeicher (~DU + ^A@)
Russisch

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.

0,986mittlerer Score
169Testfälle
131Fälle ≥ 0,99
FallScorePräzisionRecallnur Referenznur zplCore.com
ll-clip0,6451,0000,476124830
gd-orient0,8480,8440,852812957
gs-symbols0,8590,8570,86010591214
maxicode-modes0,8600,7970,93321439764
fp-rot0,8800,8940,868407297
code128-invoc0,9010,8400,9738255512
fb-rot-i0,9040,9150,8941067846
ci0-raw-cp8500,9080,9220,89451513383
ci-code-pages0,9120,8940,93257758987
fb-bitmap0,9130,9170,909806581
fonts-efgh0,9150,9500,883664218
ci0-fh-cp8500,9160,9270,90546803248
ft-barcodes-r0,9230,9230,92336873719
ci-national-sets0,9430,9400,94749495169
gs-sizes0,9430,9670,9212002797

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).

< 1 s100.000 Labels als ZPL
14,7 MBAuftragsdatei (gespeichertes Format)
24–64 MBHeap (Streaming)
Test (100.000 Labels, ein Job)ZeitLabels/sDateiHeap
Objekte → ZPL, gespeichertes Format (^DF/^XF)0,73 s136.00014,7 MB24 MB
Objekte → ZPL, volles Format je Label0,39 s256.00029,1 MB24 MB
EF Core / SQLite AsAsyncEnumerable() → Datei0,57 s176.00014,7 MB29 MB
LiteDB-Query → Datei0,50 s202.00014,7 MB64 MB
Vorschau PNG sequenziell (799 × 480 px, Graustufen-PNG)≈ 3,4 min49018,2 MB / 2.00084 MB
Vorschau PNG parallel (14 Kerne)≈ 25 s4.03317,1 MB / 2.000106 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)vorherjetzt
Rendern zu Bitmap1,28 ms0,39 ms
Rendern + PNG8,19 ms1,86 ms
PNG-Größe18 KB9 KB
100.000 Vorschauen parallel2,2 min25 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):

ThreadsRendern + PNG vorherRendern + PNG jetztnur Text vorhernur Text jetzt
1499/s (1,0×)549/s (1,0×)5.665/s (1,0×)6.306/s (1,0×)
41.984/s (4,0×)2.225/s (4,1×)3.770/s (0,7×)28.183/s (4,5×)
82.245/s (4,5×)4.090/s (7,5×)3.162/s (0,6×)42.434/s (6,7×)
101.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.

Schriften: Zebras residente Schriften (0, A–H) sind proprietär und werden durch Roboto Condensed Bold bzw. DejaVu Sans Mono ersetzt – auf Zebra-Metriken kalibriert, die Glyphenformen der Bitmap-Schriften A–H weichen aber ab; für exakte Metriken die passende TTF mit 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 (^B7 mit ^FM) und der MicroPDF417-Teil von ^BT sind 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-fonts erreicht dort 0,774 statt ≥ 0,919
  • ZplRescaler skaliert ~DG/~DY-Downloads und binäres ^GFB nicht; 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

KomponenteLizenzVerwendung
BWIPP / bwip-js (© Terry Burton, Mark Warren)MITTeile der Barcode-Encoder (Postcodes, gestapelte und 2D-Symbologien, Composite, GS1-Tabellen) und Referenzmatrizen der Tests
zint (© Robin Stuart u. a.)BSD-3-ClausePortierungen u. a. von Korea Post, Mailmark 4-State, DPD, UPU S10, Flattermarken, UPNQR, Grid Matrix und Code One
ZXing.NetApache-2.0Bausteine 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 / ZXingCppApache-2.0Decoder von Scan-Proof und Vector-Lift (nur Lesen)
PDFium (bblanchon.PDFium)BSD-3-Clause / Apache-2.0nur ZplCore.VectorLift: PDF lesen und als Referenz rendern
GS1 Barcode Syntax Dictionary, Digital-Link-KompressionstabellenApache-2.0Schlüssel-/Attributregeln von GS1 Digital Link, Lesen alter komprimierter URIs
Okapi BarcodeApache-2.0Schwellwerte der QR-Moduswahl
SkiaSharp, HarfBuzzSharpMITRendering, Text-Shaping
Roboto CondensedSIL OFL 1.1Ersatz für Zebra-Schrift 0
DejaVu Sans / Sans MonoBitstream Vera / DejaVu-Lizenz (frei)Ersatz für Schriften A–H, Klarschrift

Abgeleitete Dateien tragen einen Kopfkommentar. Vollständige Texte: THIRD-PARTY-NOTICES.md