^PON or ^FWN — on this printer, these commands cause "Zebra is not ready" / printer rejects the job. Instead, rotate the image 180° in Python if the label prints upside down.^GFA: use byte_val = (byte_val << 1) | bit with bit = 0 if px else 1. First pixel = MSB = leftmost dot. Do NOT reverse this.^PON/^FWN — worked at scale=4^PON/^FWN, scale=4 — fixed upside-down issueUse print_label.py — it does everything in one command:
python3 print_label.py "UPC Labels/AK47 G2 Ribbed PLUM - PSA RETAIL.docx" --rotate 180
Or for labels that don't need rotation:
python3 print_label.py "UPC Labels/AKV PLUM - PSA RETAIL.docx"
#!/usr/bin/env python3
"""One-step label printer for Zebra ZTC 110Xi4-203dpi ZPL."""
import argparse, os, subprocess, sys, tempfile
from zipfile import ZipFile
from pathlib import Path
from PIL import Image
def extract_image(docx_path):
with ZipFile(docx_path) as z:
image_name = next(n for n in z.namelist() if n.startswith("word/media/"))
with z.open(image_name) as f:
return Image.open(f).convert("RGB")
def generate_zpl(img, scale=4, rotate=0):
if rotate:
img = img.rotate(rotate, expand=True)
img = img.convert("1")
w, h = img.size
img = img.resize((w * scale, h * scale), Image.NEAREST)
w, h = img.size
bpr = (w + 7) // 8
pixels = list(img.getdata())
rows = []
for y in range(h):
row = []
for bx in range(bpr):
bv = 0
for bi in range(8):
idx = y * w + bx * 8 + bi
if idx < y * w + w:
p = pixels[idx]
bv = (bv << 1) | (0 if p else 1)
else:
bv <<= 1
row.append(bv)
rows.append("".join(f"{b:02X}" for b in row))
data = "".join(rows)
tb = bpr * h
lp = 80
sp = 120
tm = 30
lm = 60
return f"^XA\n^LL{h+lp}\n^PW{w+sp}\n^LS0\n^FO{lm},{tm}^GFA,{tb},{tb},{bpr},{data}^FS\n^XZ"
def send_to_printer(zpl):
with tempfile.NamedTemporaryFile(mode="w", suffix=".zpl", delete=False) as f:
f.write(zpl)
tmp = f.name
try:
r = subprocess.run(
["lp", "-d", "Zebra", "-o", "raw", tmp],
capture_output=True, text=True, timeout=30
)
if r.returncode != 0:
print(f"ERROR: {r.stderr.strip()}")
return False
print(f"Sent: {r.stdout.strip()}")
return True
except subprocess.TimeoutExpired:
print("ERROR: lp command timed out")
return False
finally:
os.unlink(tmp)
def main():
ap = argparse.ArgumentParser(description="Print a label from a .docx to the Zebra printer")
ap.add_argument("docx", help="Path to the .docx file")
ap.add_argument("--scale", type=int, default=4, choices=[3, 4, 5],
help="Scale factor (3=2.85\", 4=3.80\", 5=4.75\")")
ap.add_argument("--rotate", type=int, default=0,
help="Rotate image by degrees (180 for upside-down fix)")
ap.add_argument("--no-print", action="store_true",
help="Only generate ZPL, don't send to printer")
args = ap.parse_args()
if not os.path.exists(args.docx):
print(f"File not found: {args.docx}")
sys.exit(1)
print(f"Extracting image from {Path(args.docx).name}...")
img = extract_image(args.docx)
print(f" Image size: {img.size[0]}x{img.size[1]} pixels")
if args.rotate:
print(f" Rotating {args.rotate} degrees...")
print(f"Generating ZPL (scale={args.scale})...")
zpl = generate_zpl(img, args.scale, args.rotate)
output_name = Path(args.docx).stem.replace(" ", "_") + ".zpl"
output_path = Path("UPC Labels") / output_name if Path("UPC Labels").exists() else Path(output_name)
with open(output_path, "w") as f:
f.write(zpl)
print(f" Saved: {output_path}")
if not args.no_print:
print("Sending to printer...")
if send_to_printer(zpl):
print(" Done! Check the printout.")
else:
print(" FAILED. Try: reboot the printer, then run again.")
else:
print(" (--no-print: not sent)")
if __name__ == "__main__":
main()
pip3 install Pillow
python3 -c "
from zipfile import ZipFile; from PIL import Image
with ZipFile('UPC Labels/your_label.docx') as z:
img = next(n for n in z.namelist() if n.startswith('word/media/'))
with z.open(img) as f: Image.open(f).convert('RGB').save('UPC Labels/your_label.png')
print('Done')
"
python3 -c "
from PIL import Image
Image.open('UPC Labels/your_label.png').rotate(180, expand=True).save('UPC Labels/your_label_rotated.png')
print('Rotated')
"
python3 -c "
from PIL import Image; from pathlib import Path
scale = 4
img = Image.open('UPC Labels/your_label.png').convert('1')
w, h = img.size
img = img.resize((w * scale, h * scale), Image.NEAREST)
w, h = img.size
bpr = (w + 7) // 8
pixels = list(img.getdata())
rows = []
for y in range(h):
row = []
for bx in range(bpr):
bv = 0
for bi in range(8):
idx = y * w + bx * 8 + bi
if idx < y * w + w:
bv = (bv << 1) | (0 if pixels[idx] else 1)
else:
bv <<= 1
row.append(bv)
rows.append(''.join(f'{b:02X}' for b in row))
data = ''.join(rows)
zpl = f'^XA\n^LL{h+80}\n^PW{w+120}\n^LS0\n^FO60,30^GFA,{bpr*h},{bpr*h},{bpr},{data}^FS\n^XZ'
Path('UPC Labels/your_label.zpl').write_text(zpl)
print(f'Saved ({w}x{h} dots)')
"
lp -d Zebra -o raw "UPC Labels/your_label.zpl"
lpq -P Zebra
USB-B to USB-A cable (or USB-C adapter). Port is on the back.
lpadmin -p Zebra -E \
-v "usb://Zebra%20Technologies/ZTC%20110Xi4-203dpi%20ZPL?serial=JJL100932" \
-o printer-is-shared=false
lpinfo -v | grep -i zebra
^XA
^LL280
^PW892
^LS0
^FO60,30^GFA,19400,19400,97,[HEX_DATA]^FS
^XZ
^PON, ^FWN, or any other orientation commands — they cause the printer to reject the job on this model.
| Scale | Image dots | Physical size | Label area with padding |
|---|---|---|---|
| 3× | 579×150 | 2.85" × 0.74" | 699×230 |
| 4× | 772×200 | 3.80" × 0.99" | 892×280 |
| 5× | 965×250 | 4.75" × 1.23" | 1085×330 |
| Issue | Fix |
|---|---|
| ZPL causes "not ready" | Remove ^PON/^FWN. Reboot printer. Resend clean ZPL. |
| Label upside down | Rotate image 180° in Python, regenerate ZPL, resend. |
| Label mirrored | Use byte_val = (byte_val << 1) | bit (NOT reversed bit order). |
| Garbled / unreadable | Wrong bit ordering or printer in bad state. Reboot printer. Use original << 1 bit shift. |
| Printer stays "not ready" | Reboot the printer (power cycle). CUPS commands won't clear this. |
| Bottom cut off | Increase bottom_padding (80→120+) |
| Sides cut off | Increase side_padding (120→160+) or left_margin |
| Label too large/small | Change scale (3, 4, or 5) |
| Garbled text (not image) | Forgot -o raw. macOS converts ZPL to raster. |
A .docx file is a standard ZIP archive. The label image is always at a fixed path inside:
| Path inside .docx | Content |
|---|---|
word/media/image1.png | The label image (PNG format) |
word/document.xml | Word document XML (not needed — just extract the PNG) |
[Content_Types].xml | Content type manifest (not needed) |
# Pseudocode: extract PNG from .docx
Open file as ZIP
Find first entry starting with "word/media/"
Extract raw bytes → save as PNG
Close ZIP
The label images are 193×50 pixel RGB PNGs. Different labels may have different dimensions — the script auto-detects.
# Pseudocode: convert RGB pixel to black/white
For each pixel (R, G, B):
luminance = (R + G + B) / 3 # simple average
if luminance < 128:
output = 0 # black
else:
output = 255 # white
# Pseudocode: encode 1-bit image as ^GFA hex
Input: width W, height H, 1-bit pixel data (0=black, 255=white)
Scale: multiply W and H by scale_factor (4 is standard)
bpr = (W + 7) / 8 # bytes per row (rounded up)
hex_string = ""
For each row y from 0 to H-1:
For each byte bx from 0 to bpr-1:
byte_val = 0
For each bit bi from 0 to 7:
pixel_idx = y * W + bx * 8 + bi
if pixel_idx < y * W + W:
if pixel[pixel_idx] == black:
bit = 1 # 1 = black dot (print)
else:
bit = 0 # 0 = white dot (skip)
byte_val = (byte_val << 1) | bit
else:
byte_val <<= 1 # pad remaining bits with 0
hex_string += format(byte_val, "02X")
# No newline between rows — continuous hex string
total_bytes = bpr * H
padding_bottom = 80
padding_side = 120
margin_top = 30
margin_left = 60
label_h = H + padding_bottom
label_w = W + padding_side
ZPL = "^XA\n" +
"^LL" + label_h + "\n" +
"^PW" + label_w + "\n" +
"^LS0\n" +
"^FO" + margin_left + "," + margin_top +
"^GFA," + total_bytes + "," + total_bytes + "," + bpr + "," + hex_string + "^FS\n" +
"^XZ"
Zebra printers listen on TCP port 9100 by default. Send raw ZPL bytes, then close the connection.
# Pseudocode: TCP
connect to printer_ip on port 9100
send ZPL bytes
close connection
# Verification
netcat (nc) also works: nc printer-ip 9100 < label.zpl
Settings: 9600 baud, 8 data bits, no parity, 1 stop bit (9600 8N1)
No flow control
# Pseudocode: Serial (Arduino / ESP32)
Serial.begin(9600, SERIAL_8N1);
Serial.write(zpl_bytes, zpl_length);
Serial.flush();
USB on this printer uses the printer's USB CDC or vendor-specific class. On a Pi, it shows up as a serial device or CUPS queue. On ESP32/Arduino, USB host mode would be needed — TCP or serial is far simpler.
| Platform | Best protocol | Notes |
|---|---|---|
| Raspberry Pi | TCP (port 9100) | Can run Python/Pillow, full .docx parsing. Same as macOS workflow. |
| ESP32 | TCP (WiFi) port 9100 | Needs PNG decoder library (e.g., pngle or lodepng). Embed the label image as raw bytes or host a web UI for upload. |
| Arduino (Uno/Mega) | Serial 9600 8N1 | Too little RAM for PNG decode. Pre-convert PNG → hex on PC, upload to SD, send from SD via serial. |
| Arduino + WiFi shield | TCP port 9100 | Same RAM limits. Pre-encoded ZPL stored on SD card. |
mode COM4:9600,n,8,1
copy /b label.zpl COM4:
nc printer-ip 9100 < label.zpl
Windows-only licensed app from TEKLYNX. Recreate the label as .nlbl on the bagger. Cannot convert ZPL→.nlbl.
# Check status
lpstat -p Zebra
# Check queue
lpq -P Zebra
# Cancel all jobs
cancel -a Zebra
# Re-enable
cupsenable Zebra
# Delete and recreate queue
lpadmin -x Zebra
lpadmin -p Zebra -E -v "usb://Zebra%20Technologies/ZTC%20110Xi4-203dpi%20ZPL?serial=JJL100932" -o printer-is-shared=false