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:
// 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:
const 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:
function 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:
const 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:
async 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
Common issues:"When prints look wrong, the problem is almost always in the raster byte order."
- 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
// 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.