Brother Raster Protocol: Low-Level Label Printing
# Brother Raster Protocol: Low-Level Label Printing
Thermal label printers speak a binary language. This guide demystifies the Brother raster protocol—ESC commands, bitmap generation, and direct TCP socket communication.
## Protocol Fundamentals
Brother printers accept raw binary data over TCP port 9100. The protocol consists of:
- >Initialization commands - Reset and configure printer
- >Media information - Label size and type
- >Raster data - 1-bit bitmap rows
- >Termination - End print job
┌────────────────────────────────────────┐
│ Brother Printer │
│ │
│ ┌────────────────────────────────┐ │
│ │ TCP Socket (Port 9100) │ │
│ └────────────────────────────────┘ │
│ ▲ │
│ │ │
│ ┌──────────┴──────────┐ │
│ │ Command Interpreter │ │
│ └─────────────────────┘ │
│ │ │
│ ┌──────────▼──────────┐ │
│ │ Print Head │ │
│ └─────────────────────┘ │
└────────────────────────────────────────┘
## Initialization Sequence
Every print job starts with these commands:
javascript// ESC @ - Initialize printer const INIT = Buffer.from([0x1B, 0x40]); // ESC i a - Set raster mode const RASTER_MODE = Buffer.from([0x1B, 0x69, 0x61, 0x01]); // ESC i z - Media information function mediaInfo(width, height, continuous = false) { return Buffer.from([ 0x1B, 0x69, 0x7A, continuous ? 0x0A : 0x02, // Media type width & 0xFF, // Width (low byte) (width >> 8) & 0xFF, // Width (high byte) height & 0xFF, // Height (low byte) (height >> 8) & 0xFF, // Height (high byte) 0x00, 0x00 // Page number ]); }
## Command Reference
| Command | Bytes | Description |
|---|---|---|
| ESC @ | 1B 40 | Initialize |
| ESC i a | 1B 69 61 01 | Raster mode |
| ESC i z | 1B 69 7A ... | Media info |
| ESC i M | 1B 69 4D 40 | Auto-cut mode |
| ESC i K | 1B 69 4B 08 | Cut each label |
| g | 67 00 NN ... | Raster line |
| Z | 5A | Zero line (blank) |
| 0x1A | 1A | End job |
## 1-Bit Bitmap Generation
Labels use monochrome bitmaps—each pixel is one bit:
javascriptconst sharp = require('sharp'); async function imageToRaster(imagePath, targetWidth) { // Load and resize image const image = await sharp(imagePath) .resize(targetWidth) .grayscale() .raw() .toBuffer({ resolveWithObject: true }); const { data, info } = image; const { width, height } = info; // Convert to 1-bit with threshold const threshold = 128; const bytesPerRow = Math.ceil(width / 8); const rasterData = []; for (let y = 0; y < height; y++) { const row = Buffer.alloc(bytesPerRow); for (let x = 0; x < width; x++) { const pixel = data[y * width + x]; const byteIndex = Math.floor(x / 8); const bitIndex = 7 - (x % 8); // Black pixel = 1, White = 0 if (pixel < threshold) { row[byteIndex] |= (1 << bitIndex); } } rasterData.push(row); } return { rasterData, width, height }; }
## Sending Raster Lines
Each raster line has a header followed by bitmap data:
javascriptfunction createRasterLine(rowData) { // g command + length bytes + data const length = rowData.length; return Buffer.concat([ Buffer.from([0x67, 0x00, length]), // g 00 NN rowData ]); } function createBlankLine() { // Z command for empty line return Buffer.from([0x5A]); } function buildPrintJob(rasterData) { const commands = []; for (const row of rasterData) { // Check if row is all zeros const isBlank = row.every(byte => byte === 0); if (isBlank) { commands.push(createBlankLine()); } else { commands.push(createRasterLine(row)); } } return Buffer.concat(commands); }
## TCP Socket Communication
Direct socket connection for reliable printing:
javascriptconst net = require('net'); class BrotherPrinter { constructor(ip, port = 9100) { this.ip = ip; this.port = port; } async print(labelBuffer) { return new Promise((resolve, reject) => { const socket = new net.Socket(); const timeout = setTimeout(() => { socket.destroy(); reject(new Error('Print timeout')); }, 30000); socket.connect(this.port, this.ip, () => { socket.write(labelBuffer, () => { clearTimeout(timeout); socket.end(); }); }); socket.on('close', () => resolve()); socket.on('error', (err) => { clearTimeout(timeout); reject(err); }); }); } }
## Complete Print Function
Putting it all together:
javascriptasync function printLabel(printerIp, imagePath, labelWidth = 300) { const printer = new BrotherPrinter(printerIp); // Generate raster data const { rasterData, width, height } = await imageToRaster(imagePath, labelWidth); // Build command sequence const commands = Buffer.concat([ // Initialize Buffer.from([0x1B, 0x40]), // Raster mode Buffer.from([0x1B, 0x69, 0x61, 0x01]), // Media info mediaInfo(width, height), // Auto-cut after each label Buffer.from([0x1B, 0x69, 0x4D, 0x40]), Buffer.from([0x1B, 0x69, 0x4B, 0x08]), // Margins Buffer.from([0x1B, 0x69, 0x64, 0x00, 0x00]), // Compression off Buffer.from([0x4D, 0x00]), // Raster data buildPrintJob(rasterData), // End job Buffer.from([0x1A]) ]); await printer.print(commands); }
## Debugging Tips
"When prints look wrong, the problem is almost always in the raster byte order."
Common issues:
- >Mirror image - Bit order reversed in each byte
- >Stretched/compressed - Wrong bytes-per-row calculation
- >Partial print - Missing termination command
- >No output - Wrong media info for label size
javascript// Debug helper: visualize raster data in console function visualizeRaster(rasterData) { for (const row of rasterData.slice(0, 20)) { let line = ''; for (const byte of row) { for (let bit = 7; bit >= 0; bit--) { line += (byte & (1 << bit)) ? '█' : ' '; } } console.log(line); } }
## Conclusion
The Brother raster protocol is straightforward once you understand the binary format. Direct TCP communication bypasses driver issues and provides complete control over the printing process.
This approach enables custom label generation, barcode printing, and integration with warehouse management systems without proprietary SDKs.