@schie/fluent-zpl - v0.10.0
    Preparing search index...

    @schie/fluent-zpl - v0.10.0

    @schie/fluent-zpl

    npm version CI CodeQL Super-Linter TypeScript codecov code style: prettier Commitizen friendly

    A modern, type-safe TypeScript library for generating ZPL (Zebra Programming Language) commands using a fluent, immutable API. Perfect for creating shipping labels, product tags, inventory stickers, and any other Zebra printer output.

    ⚠️ Early Development Notice
    This library is under active early development. Until v1.0.0 is released, consider all releases potentially breaking. The API may change significantly between versions as we refine the design based on user feedback and real-world usage patterns.

    • 🔗 Fluent API - Chain methods for intuitive label building
    • 🛡️ Type Safe - Full TypeScript support with comprehensive types
    • 🔄 Immutable - All operations return new instances (safe chaining)
    • 📏 Unit Aware - Support for dots, millimeters, and inches with DPI conversion
    • ZPL Compliant - Generates valid ZPL according to specification
    • ⚙️ Global Settings - Support for ^CF, ^BY, ^FR commands and ZPL comments
    • 🧪 Thoroughly Tested - Comprehensive test suite with high coverage
    • 📱 Tree Shakeable - Import only what you need
    • 🖼️ Image Support - Convert RGBA images to ZPL bitmaps
    • 📡 RFID/EPC Support - Encode and read RFID tags with EPC data
    • 🏗️ Program Builder - Compose printer setup, diagnostics, labels, and RFID commands into one payload
    • ✂️ Tear-Off Calibration - Adjust ~TA with unit conversion and clamping
    • 📦 Zero Dependencies - Lightweight and fast
    npm install @schie/fluent-zpl
    
    import { Label, FontFamily, Barcode, Units, Orientation, Justify, Fill } from '@schie/fluent-zpl';

    // Create a shipping label
    const label = Label.create({ w: 400, h: 600, dpi: 203 })
    .text({
    at: { x: 50, y: 50 },
    text: 'PRIORITY MAIL',
    font: { family: FontFamily.B, h: 32, w: 32 },
    })
    .addressBlock({
    at: { x: 50, y: 120 },
    lines: ['John Doe', '123 Main Street', 'Anytown, NY 12345'],
    lineHeight: 25,
    })
    .barcode({
    at: { x: 50, y: 300 },
    type: Barcode.Code128,
    data: '1Z999AA1234567890',
    height: 100,
    });

    // Generate ZPL
    const zpl = label.toZPL();
    console.log(zpl);
    // Output: ^XA^LL600^FO50,50^ABN32,32^FDPRIORITY MAIL^FS...^XZ

    Use ZPLProgram when you need more than a single label format. It lets you compose printer/media configuration, label formats, downloads, diagnostics, and RFID commands into one immutable payload.

    import {
    ZPLProgram,
    PrinterMode,
    MediaTracking,
    PrinterConfig,
    PrinterConfiguration,
    Label,
    FontFamily,
    Barcode,
    RFIDBank,
    } from '@schie/fluent-zpl';

    const program = ZPLProgram.create()
    .printerConfig({
    mode: PrinterMode.TearOff,
    mediaTracking: MediaTracking.NonContinuous,
    printWidth: 801,
    printSpeed: 4,
    darkness: 10,
    tearOff: -12,
    labelHome: { x: 0, y: 0 },
    configuration: PrinterConfiguration.Save,
    })
    .label(
    (label) =>
    label
    .text({
    at: { x: 40, y: 60 },
    text: 'Config + Label + RFID',
    font: { family: FontFamily.B, h: 32, w: 32 },
    })
    .barcode({
    at: { x: 40, y: 140 },
    type: Barcode.Code128,
    data: '1234567890',
    height: 100,
    })
    .rfid({ epc: '300833B2DDD9014000000000' }),
    { w: 400, h: 600 },
    )
    .rfidRead({ bank: RFIDBank.HostBuffer }); // emits ^RFR,H (read last write)

    // Mix in downloads, diagnostics, or templates with .raw()
    const payload = program
    .raw('^XA^HH^XZ') // printer status block
    .comment('End of job')
    .toZPL();

    console.log(payload);

    Use tearOff when you need to fine-tune the liner break point: pass dots, millimeters, or inches and the builder will convert to dots using the current DPI/units context, rounding and clamping to ±120 before emitting ~TA.

    Or use the fluent printer config builder when you want to compose setup steps:

    const config = PrinterConfig.create()
    .mode(PrinterMode.TearOff)
    .mediaTracking(MediaTracking.NonContinuous)
    .printWidth(inch(3, 300))
    .printSpeed(4)
    .darkness(10)
    .tearOff(25)
    .labelHome({ x: 0, y: 0 })
    .save(); // ^JUS

    const zpl = ZPLProgram.create().printerConfig(config.build()).toZPL();
    // => ^XA^MMT^MNY^PW900^PR4^MD10~TA25^LH0,0^JUS^XZ
    // Or send config.toZPL() directly if you only need the setup block (it wraps ^XA/^XZ for you)

    ZPLProgram keeps track of the same DPI/unit context as your labels, so printer/media measurements (^PW, ^LH, ~TA, etc.) stay consistent. Pass { dpi, units } to ZPLProgram.create when you need to match a different printer resolution—every downstream helper (including .label(...)) inherits those settings. A single program can now cover:

    • Label formats and layout (.label(...))
    • Printer/media configuration (.printerConfig(...))
    • Control, status, or diagnostics commands (.raw(...), .comment(...))
    • Variable/templated data via regular JavaScript functions (pass a factory into .label)
    • Graphics/downloads (.raw('~DG...'), .imageInline(...) inside labels)
    • Advanced RFID/EPC flows (.rfid(...), .rfidRead(...), including HostBuffer reads)
    // Create with dots (default)
    const label1 = Label.create({ w: 400, h: 600 });

    // Create with millimeters
    const label2 = Label.create({
    w: 100,
    h: 150,
    units: Units.Millimeter,
    dpi: 203,
    });

    // Create with inches
    const label3 = Label.create({
    w: 4,
    h: 6,
    units: Units.Inch,
    dpi: 203,
    orientation: Orientation.Rotated90, // Rotate 90°
    });

    // Parse existing ZPL
    const existing = Label.parse('^XA^FO50,100^FDHello^FS^XZ');
    label
    // Simple text
    .text({
    at: { x: 50, y: 100 },
    text: 'Hello World',
    font: { family: FontFamily.A, h: 28, w: 28 },
    })

    // Text with rotation
    .text({
    at: { x: 100, y: 200 },
    text: 'Rotated Text',
    rotate: Orientation.Rotated90, // 90° clockwise
    font: { family: FontFamily.B, h: 20, w: 20 },
    })

    // Text with wrapping
    .text({
    at: { x: 50, y: 300 },
    text: 'This is a long text that will wrap to multiple lines',
    wrap: {
    width: 200,
    lines: 3,
    justify: Justify.Center, // Center justified
    spacing: 2, // Add 2 dots between lines
    hangingIndent: 10, // Indent 2nd+ lines by 10 dots
    },
    })

    // Convenience method for simple text
    .caption({
    at: { x: 50, y: 400 },
    text: 'Simple Caption',
    size: 24,
    });
    label
    // Code 128 barcode
    .barcode({
    at: { x: 50, y: 100 },
    type: Barcode.Code128,
    data: '1234567890',
    height: 80,
    })

    // QR Code
    .qr({
    at: { x: 200, y: 100 },
    text: 'https://example.com',
    module: 4,
    })

    // Other supported barcodes
    .barcode({ at: { x: 50, y: 200 }, type: Barcode.Code39, data: 'ABC123' })
    .barcode({ at: { x: 50, y: 250 }, type: Barcode.EAN13, data: '1234567890123' })
    .barcode({ at: { x: 50, y: 300 }, type: Barcode.DataMatrix, data: 'Data' });

    // GS1-128 helper converts AI maps into the right ^BC payload (FNC1 + GS handling)
    label.gs1_128({
    at: { x: 50, y: 360 },
    ai: {
    '01': '09506000134352', // GTIN (fixed-length)
    '10': 'BATCH-42', // Lot/batch (variable-length)
    '17': '250101', // Expiration date (YYMMDD)
    },
    height: 120,
    });
    label
    // Boxes and borders
    .box({
    at: { x: 10, y: 10 },
    size: { w: 380, h: 580 },
    border: 2,
    fill: Fill.Black, // Black fill
    })

    // Lines (thin boxes)
    .box({
    at: { x: 50, y: 200 },
    size: { w: 300, h: 1 }, // Horizontal line
    border: 1,
    })

    // Multi-line address blocks
    .addressBlock({
    at: { x: 50, y: 250 },
    lines: ['Ship To:', 'Jane Smith', '456 Oak Avenue', 'Somewhere, CA 90210'],
    lineHeight: 25,
    size: 20,
    });
    import { DitherMode, type ImageInlineOpts, type ImageCachedOpts } from '@schie/fluent-zpl';

    // Inline image (^GF command)
    const inlineLogo: ImageInlineOpts = {
    at: { x: 50, y: 100 },
    rgba: imageData, // Uint8Array of RGBA pixels
    width: 100,
    height: 100,
    mode: DitherMode.FloydSteinberg,
    threshold: 180, // Optional threshold overrides
    invert: false, // Flip black/white if needed
    };

    label.imageInline(inlineLogo);

    // Cached image (~DG + ^XG commands) inherits all ImageInlineOpts fields
    const cachedStamp: ImageCachedOpts = {
    ...inlineLogo,
    at: { x: 200, y: 100 },
    mode: DitherMode.Ordered,
    name: 'R:LOGO.GRF', // Printer storage name
    };

    label.image(cachedStamp);

    Both helpers accept RGBA input and offer multiple dithering strategies via the DitherMode enum (Threshold, FloydSteinberg, Ordered, or None) plus optional threshold and invert controls so you can tune contrast for the target media. Because ImageCachedOpts extends ImageInlineOpts, every inline option (including mode) carries over to cached assets automatically.

    // EPC encoding (convenience method)
    label.epc({
    epc: '3014257BF7194E4000001A85', // 96-bit EPC in hex
    password: 'DEADBEEF', // Access password (optional)
    });

    // RFID field with specific memory bank
    label.rfid({
    epc: '1234567890ABCDEF',
    bank: RFIDBank.USER,
    offset: 0,
    length: 8,
    password: '00000000',
    });

    // Read RFID tag data
    label.rfidRead({
    bank: RFIDBank.EPC,
    offset: 0,
    length: 12,
    });

    // Read the volatile HostBuffer (^RFR,H) after a write
    label.rfidRead({
    bank: RFIDBank.HostBuffer,
    });

    RFIDBank.HostBuffer maps directly to ^RFR,H, which instructs the printer to return the contents of the last write buffer—perfect for verifying recently encoded tags before moving on.

    // Add comments for debugging (generates ^FX commands)
    label
    .comment('This is a shipping label for Order #12345')
    .text({ at: { x: 50, y: 100 }, text: 'Hello World' })
    .comment('End of content');

    // Add structured metadata
    label.withMetadata({
    generator: '@schie/fluent-zpl',
    version: '1.0.0',
    orderNumber: 'ORD-12345',
    customer: 'ACME Corp',
    });

    // Metadata is embedded as ZPL comments (^FX) for debugging

    Control global ZPL settings that affect subsequent commands:

    label
    // Set global default font (^CF command)
    .setDefaultFont({
    family: FontFamily.F,
    height: 60,
    width: 60,
    })
    .text({ at: { x: 50, y: 50 }, text: 'Uses global font' })

    // Set global barcode defaults (^BY command)
    .setBarcodeDefaults({
    moduleWidth: 5,
    wideToNarrowRatio: 2,
    height: 270,
    })
    .barcode({
    at: { x: 50, y: 150 },
    type: Barcode.Code128,
    data: '12345678', // Uses global height setting
    })

    // Field reverse effect (^FR command)
    .box({
    at: { x: 100, y: 200 },
    size: { w: 200, h: 100 },
    reverse: true, // Reverses colors within field
    });

    These global settings generate the exact ZPL commands (^CF, ^BY, ^FR) found in complex label specifications, enabling precise control over printer behavior and optimized ZPL output.

    Parse existing ZPL strings directly into Label instances using the label tagged template:

    import { label } from '@schie/fluent-zpl';

    // Parse ZPL with template interpolation
    const trackingNumber = '1Z999AA1234567890';
    const customerName = 'John Doe';

    const parsedLabel = label`
    ^XA
    ^FX Shipping label from existing ZPL
    ^CF0,60
    ^FO50,50^FDShipping Label^FS
    ^FO50,100^FD${customerName}^FS
    ^BY5,2,270
    ^FO50,200^BC^FD${trackingNumber}^FS
    ^XZ
    `;

    // Continue with fluent API
    parsedLabel
    .comment('Added via fluent API')
    .text({ at: { x: 50, y: 350 }, text: 'Processed by fluent-zpl' })
    .toZPL();

    Alternative syntax with explicit options:

    const parsedLabel = label.withOptions({ dpi: 300, units: Units.Millimeter })`
    ^XA
    ^FO10,10^A0N,28,28^FDHigh Resolution^FS
    ^XZ
    `;
    import { dot, mm, inch, toDots } from '@schie/fluent-zpl';

    // Convert units to dots
    const x = mm(25.4, 203); // 25.4mm at 203 DPI = 203 dots
    const y = inch(1, 203); // 1 inch at 203 DPI = 203 dots
    const spacing = dot(40); // Explicit dots helper for readability

    // Generic conversion
    const pos = toDots(50, 203, Units.Millimeter); // 50mm to dots at 203 DPI
    const shippingLabel = Label.create({ w: 4, h: 6, units: Units.Inch, dpi: 203 })
    .text({
    at: { x: 0.5, y: 0.5 },
    text: 'FEDEX GROUND',
    font: { family: FontFamily.B, h: 28, w: 28 },
    })
    .box({
    at: { x: 0.25, y: 0.25 },
    size: { w: 3.5, h: 5.5 },
    border: 2,
    })
    .addressBlock({
    at: { x: 0.5, y: 1.5 },
    lines: ['SHIP TO:', 'John Doe', '123 Main St', 'Anytown, NY 12345'],
    lineHeight: 25,
    })
    .barcode({
    at: { x: 0.5, y: 3.5 },
    type: Barcode.Code128,
    data: trackingNumber,
    height: 80,
    });
    const productLabel = Label.create({ w: 100, h: 75, units: Units.Millimeter, dpi: 300 })
    .caption({
    at: { x: 5, y: 5 },
    text: productName,
    size: 16,
    })
    .text({
    at: { x: 5, y: 25 },
    text: `SKU: ${sku}`,
    font: { family: FontFamily.A, h: 12, w: 12 },
    })
    .qr({
    at: { x: 60, y: 25 },
    text: productUrl,
    module: 2,
    })
    .text({
    at: { x: 5, y: 60 },
    text: `$${price}`,
    font: { family: FontFamily.B, h: 20, w: 20 },
    });
    const assetTag = Label.create({ w: 4, h: 2, units: Units.Inch, dpi: 203 })
    .text({
    at: { x: 0.25, y: 0.25 },
    text: 'ASSET TAG',
    font: { family: FontFamily.B, h: 20, w: 20 },
    })
    .text({
    at: { x: 0.25, y: 0.75 },
    text: `ID: ${assetId}`,
    font: { family: FontFamily.A, h: 16, w: 16 },
    })
    .barcode({
    at: { x: 2.5, y: 0.5 },
    type: Barcode.Code128,
    data: assetId,
    height: 60,
    })
    .epc({
    epc: epcData, // 96-bit EPC hex string
    password: accessPassword,
    })
    .text({
    at: { x: 0.25, y: 1.25 },
    text: 'RFID ENABLED',
    font: { family: FontFamily.A, h: 12, w: 12 },
    });
    const complexLabel = Label.create({ w: 800, h: 1200, units: Units.Dot, dpi: 203 })
    .comment('Top section with logo and company info')
    .setDefaultFont({ family: FontFamily.F, height: 60 })

    // Logo with field reverse effect
    .box({ at: { x: 50, y: 50 }, size: { w: 100, h: 100 }, border: 100 })
    .box({ at: { x: 75, y: 75 }, size: { w: 100, h: 100 }, border: 100, reverse: true })
    .box({ at: { x: 93, y: 93 }, size: { w: 40, h: 40 }, border: 40 })

    // Company name uses global font setting
    .text({ at: { x: 220, y: 50 }, text: 'Intershipping, Inc.' })

    .comment('Recipient address section')
    .setDefaultFont({ family: FontFamily.A, height: 30 })
    .text({ at: { x: 50, y: 300 }, text: 'John Doe' })
    .text({ at: { x: 50, y: 340 }, text: '100 Main Street' })
    .text({ at: { x: 50, y: 380 }, text: 'Springfield TN 39021' })

    .comment('Barcode with global settings')
    .setBarcodeDefaults({ moduleWidth: 5, wideToNarrowRatio: 2, height: 270 })
    .barcode({
    at: { x: 100, y: 550 },
    type: Barcode.Code128,
    data: '12345678',
    // Height comes from global ^BY setting
    })

    .setDefaultFont({ family: FontFamily.F, height: 190 })
    .text({ at: { x: 470, y: 955 }, text: 'CA' });
    const complexLabel = Label.create({ w: 800, h: 1200, units: 'dot', dpi: 203 })
    .comment('Top section with logo and company info')
    .setDefaultFont({ family: 'F', height: 60 })

    // Logo with field reverse effect
    .box({ at: { x: 50, y: 50 }, size: { w: 100, h: 100 }, border: 100 })
    .box({ at: { x: 75, y: 75 }, size: { w: 100, h: 100 }, border: 100, reverse: true })
    .box({ at: { x: 93, y: 93 }, size: { w: 40, h: 40 }, border: 40 })

    // Company name uses global font setting
    .text({ at: { x: 220, y: 50 }, text: 'Intershipping, Inc.' })

    .comment('Recipient address section')
    .setDefaultFont({ family: 'A', height: 30 })
    .text({ at: { x: 50, y: 300 }, text: 'John Doe' })
    .text({ at: { x: 50, y: 340 }, text: '100 Main Street' })
    .text({ at: { x: 50, y: 380 }, text: 'Springfield TN 39021' })

    .comment('Barcode with global settings')
    .setBarcodeDefaults({ moduleWidth: 5, wideToNarrowRatio: 2, height: 270 })
    .barcode({
    at: { x: 100, y: 550 },
    type: 'Code128',
    data: '12345678',
    // Height comes from global ^BY setting
    })

    .setDefaultFont({ family: 'F', height: 190 })
    .text({ at: { x: 470, y: 955 }, text: 'CA' });

    This library includes comprehensive ZPL validation to ensure all generated output works with Zebra printers:

    import { Label } from '@schie/fluent-zpl';

    const label = Label.create({ w: 400, h: 600 }).text({ at: { x: 50, y: 100 }, text: 'Test' });

    const zpl = label.toZPL();

    // All output is validated to ensure:
    // ✅ Proper ^XA...^XZ structure
    // ✅ Valid command formatting
    // ✅ Required parameters included
    // ✅ Special characters escaped
    // ✅ Field blocks properly terminated
    Unit Description Conversion
    dot Printer dots (default) 1:1
    mm Millimeters 25.4mm = 1 inch
    in Inches Direct DPI conversion
    DPI Description Common Use
    203 Standard resolution Most labels
    300 High resolution Small text/barcodes
    600 Very high resolution Detailed graphics
    • Label - Main fluent interface

      • Label.create(options) - Create new label
      • Label.parse(zpl) - Parse existing ZPL
      • .text(opts) - Add text field
      • .barcode(opts) - Add barcode
      • .box(opts) - Add graphics box (supports reverse: true for ^FR)
      • .caption(opts) - Add simple text
      • .qr(opts) - Add QR code
      • .gs1_128(opts) - Emit GS1-128 via Code 128 with automatic FNC1 handling
      • .addressBlock(opts) - Add multi-line text
      • .imageInline(opts) - Add inline image
      • .image(opts) - Add cached image
      • .rfid(opts) - Add RFID field with EPC encoding
      • .rfidRead(opts) - Add RFID read command
      • .epc(opts) - Add EPC encoding (convenience method)
      • .comment(text) - Add ZPL comment (^FX)
      • .withMetadata(meta) - Add structured metadata as comments
      • .setDefaultFont(opts) - Set global default font (^CF)
      • .setBarcodeDefaults(opts) - Set global barcode settings (^BY)
      • .toZPL() - Generate ZPL string
    • ZPLProgram - Compose printer setup + formats

      • ZPLProgram.create(opts) - Start a new program (sets DPI/units context)
      • .raw(zpl) - Append arbitrary commands (diagnostics, downloads, etc.)
      • .comment(text) - Insert ^FX comments between sections
      • .printerConfig(opts) - Emit typed ^MM/^MN/^PW/^PR/^MD/~TA/^JU/^LH blocks
      • .label(labelOrFactory, options?) - Append a fluent Label (^XA…^XZ)
      • .rfid(opts) / .rfidRead(opts) - Emit RFID commands outside labels
      • .toZPL() - Serialize the final job payload
    • label (tagged template) - Parse ZPL strings with interpolation

      • label\...`` - Parse ZPL template literal
      • label.withOptions(opts)\...`` - Parse with explicit DPI/units
    • toDots(value, dpi, units) - Convert to dots
    • dot(n) - Pass-through for dots
    • mm(n, dpi) - Convert mm to dots
    • inch(n, dpi) - Convert inches to dots
    • ES Modules: Full ESM support with tree shaking
    • CommonJS: CJS builds included for compatibility
    • TypeScript: Complete type definitions included
    • Node.js: Requires Node.js 20+
    • Size: ~15KB minified

    Contributions are welcome! This project uses:

    • TypeScript for type safety
    • Jest for testing
    • ESLint + Prettier for code quality
    • Commitizen for conventional commits
    # Install dependencies
    npm install

    # Run tests
    npm test

    # Run tests with coverage
    npm test -- --coverage

    # Build
    npm run build

    # Lint
    npm run lint

    MIT License - see LICENSE file for details.


    Made with ❤️ by @schie