| name | brother-ql-labels |
| description | Print labels on a networked Brother QL printer. |
| version | 1.1.0 |
| license | MIT |
| platforms | ["linux","macos","windows"] |
| metadata | {"hermes":{"tags":["Printing","Labels","Brother","QL-580N","Addresses","Barcode","QR","CSV"]}} |
Brother QL labels (Kyyhky)
When to Use
Use whenever the user wants something printed on a physical label —
addresses for post, shipping, parcels, filing, storage, asset tags, product
labels, name badges, or anything with a bar code or QR code on it.
Triggers include: "print a label", "print these addresses", "label this
parcel", "make asset tags", "print barcode labels", "QR labels for these
links", or handing over a CSV/spreadsheet.
Also use it to diagnose a QL-580N: blank labels, mirrored text, jobs that
vanish silently, or cutter behaviour.
Not for PDF/paper documents, and not for designing artwork.
Two modes
- Addresses —
kyyhky print --csv addr.csv. Auto-sized type, no layout
work needed.
- Anything else — a JSON/YAML template listing elements (text, bar
code, QR, image, line, box) at millimetre positions, filled from a CSV.
Start with
kyyhky templates to see the built-ins.
Reach for a template as soon as the label is not purely an address.
Setup
pip install "kyyhky[all] @ git+https://github.com/kimasplund/kyyhky"
export KYYHKY_HOST=192.168.1.50
The [all] extra pulls in segno (QR), python-barcode and PyYAML.
Plain pip install git+... is enough for addresses only.
Find the printer if the address is unknown: kyyhky discover.
There is no Brother SDK for Linux for this model. Kyyhky speaks the raster
protocol directly on port 9100, per Brother's
QL-500/550/560/570/580N/650TD/700/1050/1060N Raster Command Reference.
Quick reference
kyyhky discover
kyyhky status
kyyhky media
kyyhky fonts
kyyhky templates
kyyhky symbologies
kyyhky sample --out addr.csv
kyyhky preview --csv addr.csv --out labels.png --scale 2
kyyhky print --csv addr.csv --yes
kyyhky print --csv addr.csv --cut-every 10 --yes
kyyhky print --csv addr.csv --no-cut --yes
kyyhky print --to 'Ada Lovelace' \
--street 'Wilton Place' --number 12 --apartment 5 \
--postal 'SW1X 8RL' --city London --country 'United Kingdom' --yes
Omit --yes for an interactive confirmation. --dry-run builds the job and
reports its size without sending anything.
Running from a clone instead of an install: python3 -m kyyhky.cli ....
Custom layouts (bar codes, QR, anything non-address)
kyyhky templates
kyyhky template-init asset --out my.json
kyyhky template-preview my.json --data items.csv --out check.png
kyyhky template-print my.json --data items.csv --yes
Built-ins: address, shipping, asset, product, qr-only,
name-badge. Preview any of them with no data at all —
kyyhky template-preview asset --out a.png renders a sample row.
A template is a media size plus a list of elements. Positions are
millimetres from the top-left, as you would measure with a ruler:
{
"label": "29x90",
"elements": [
{"type": "text", "text": "{name}", "x": 4, "y": 2, "size": 4.5,
"bold": true, "max_width": 52},
{"type": "text", "text": "{id}", "x": 4, "y": 8, "size": 3.2},
{"type": "barcode", "data": "{id}", "x": 4, "y": 12.5,
"width": 52, "height": 8, "symbology": "code128"},
{"type": "qr", "data": "{url}", "x": -4, "y": 2, "size": 24}
]
}
| Type | Key options |
|---|
text | text, size (mm), bold, max_width, align, font |
barcode | data, symbology, width or module, height, text_below |
qr | data, size or module, ecc (l/m/q/h), micro |
image | path, width, height, threshold, invert |
line | x, y, x2/y2 or length + vertical, thickness |
box | x, y, width, height, thickness, filled |
Rules worth knowing:
{column} placeholders match case-, space- and underscore-insensitively,
so {Product Name} and {product_name} read the same CSV column. A missing
column renders empty rather than failing.
- Negative
x/y anchor to the right/bottom edge — "x": -4 sits 4 mm
in from the right.
- Omit
x and set align to center/right; omit y and set valign to
middle/bottom.
- Any length can be given in dots with an
_dots suffix ("x_dots": 120).
max_width shrinks text to fit instead of running off the label.
- A
copies column prints that row more than once.
- Without
--data, use --set key=value (repeatable) for a one-off label.
Rendering warns when elements run off the label or collide with each
other. Read those warnings — they are the difference between a good label
and a wasted one.
Bar codes and QR
code128 is the right default for anything alphanumeric. kyyhky symbologies
lists all 13 with what each accepts.
Both are rendered from the raw module matrix at an exact integer number of
printer dots per module, never by scaling a bitmap. This is what makes small
codes scan reliably; quiet zones are included automatically. Verified on
hardware — printed labels scan with a phone.
Gotchas:
code39 appends a $ checksum in most libraries. Kyyhky turns that off
by default; pass "checksum": true if you need it.
ean13/upca require an exact digit count and add their own check digit.
A wrong length is an error naming the offending element.
- QR
size is the box it must fit inside; the module size is computed from it.
A tight size on a busy payload gives a code too dense to scan — prefer
fewer characters (a short URL) over a smaller module.
Label layout
Bold recipient name, then one line each, blanks skipped automatically:
Ada Lovelace <- bold, largest
att: Purchasing <- only if an att/c-o column exists
Wilton Place 12 as 5 <- street + number + apartment
SW1X 8RL London <- postal + city
UNITED KINGDOM <- country, upper-cased
Font size is chosen automatically: the renderer starts at --max-size and
steps down until everything fits inside the 306 x 991 dot print area. A typical
address lands at 66/50 pt; long ones shrink gracefully. If a label bottoms out
at --min-size the CLI flags it as <-- TIGHT in the listing.
CSV format
Headers are matched case-insensitively in English, Finnish and Swedish, so most
spreadsheet exports work unchanged. Delimiter (, ; tab |) is sniffed, and
a UTF-8 BOM is handled -- European Excel exports just work.
name,att,street,number,apartment,postal,city,country,copies
Ada Lovelace,,Wilton Place,12,5,SW1X 8RL,London,United Kingdom,1
Example Oy,Purchasing,Mannerheimintie,140,A 3,00250,Helsinki,Finland,2
| Field | Also accepted |
|---|
name | recipient, to, company, nimi, vastaanottaja, namn |
att | attn, attention, c/o, care_of, dept, osasto |
street | address, address1, katuosoite, katu, gata, adress |
number | no, nr, house_number, nro, numero |
apartment | apt, flat, unit, suite, address2, as, asunto, lgh |
postal | zip, postcode, post_no, postinumero, postnummer |
city | town, postitoimipaikka, kaupunki, ort, postort |
country | maa, land |
copies | qty, quantity, kpl, antal |
.json and .jsonl work too. copies prints that record N times.
Bare numeric apartments gain a qualifier (5 -> as 5); anything already
qualified (A 3, apt 9) is left alone. An att value that already starts
with att/attn/c/o is not double-prefixed.
Hardware facts (verified on real hardware)
These four cost real labels to establish. Do not re-derive them.
-
TIFF/PackBits compression is mandatory over LAN. Section 5: "In case
QL-580N/1060N, serial and LAN interface should set TIFF option." An
uncompressed job over 9100 will not print. Never pass --no-compress.
-
Byte 0 of a raster line is the RIGHT margin. Section 3.2.5. Offsetting
by the left margin instead prints a blank label -- content lands on pins
that overhang the media. (The popular brother_ql library assumes the
opposite; for this model that is wrong.)
-
The pin axis runs right-to-left, so the image must be pre-flipped.
Without it the text prints mirrored. Critically, flip the image before
pasting it at the margin offset -- flipping the assembled 720-pin canvas
moves content from pins 6..311 to 408..713 and prints blank.
-
No status over Ethernet. ESC i S never answers on the LAN board;
section 6.9 defines no status channel for network printing. status
reporting "not reported" is normal, not a fault. Printing still works.
Media/error detection is USB/serial only.
Cutting
Default: one cut at the end of the job. A run of address labels comes off
as a single strip, which is easier to carry and stick than loose labels.
| Flag | Effect |
|---|
| (default) | --cut end — one cut after the whole run |
--cut each | cut after every label |
--cut-every 10 | cut after every 10 labels (implies --cut every) |
--no-cut | cutter fully off, labels stay on the roll |
--no-cut-at-end | suppress the final cut, whatever --cut says |
Two independent bits drive the cutter and both matter:
| Mode | ESC i M | ESC i K | ESC i A |
|---|
end (default) | 0x00 | 0x08 | — |
each | 0x40 | 0x08 | 1 |
--cut-every 10 | 0x40 | 0x08 | 10 |
never | 0x00 | 0x00 | — |
ESC i M bit 6 cuts between labels; ESC i K bit 3 cuts after the final
label. Clearing only the first still chops the roll once at the end — that is
why --no-cut must clear both. ESC i A is only emitted when auto-cut is on.
python3 tools/show_cut_modes.py prints this table from live job bytes.
The print command reports the mode in its header (Cut : ...) before
sending, so the behaviour is never a surprise.
Geometry (29 x 90 mm)
| Property | Value |
|---|
| Print area | 306 x 991 dots (25.91 x 83.90 mm) |
| Left / right margin pins | 408 / 6 |
| Raster line | 90 bytes (720 pins) |
| Resolution | 300 dpi (--hires gives 600 dpi lengthwise) |
kyyhky media lists all 18 supported sizes. Continuous tape also needs
--length MM.
Useful options
| Flag | Purpose |
|---|
--limit N | only the first N rows (a row with copies=2 still yields 2 labels) |
--font | dejavu (default, full Nordic coverage), inter, liberation, condensed, or a path |
--align center / --valign top|middle|bottom | text placement |
--pad MM | inner margin, default 1.6 |
--border | hairline around the printable area (calibration aid) |
--rotate cw|ccw | reading direction; cosmetic on 29x90, both read fine |
--save-job FILE | dump the raw job bytes for inspection |
Verifying without wasting labels
tools/decode_job.py parses a built job back into a PNG, reports which pins
the ink occupies, and warns if anything falls outside the printable window:
python3 tools/decode_job.py
as_printed models the pin-axis reversal, so it shows what the label will
physically look like. Use this before any risky change to the raster path.
kyyhky calibrate prints two probe labels (cw and ccw) with borders, for
when media or firmware changes.
Troubleshooting
| Symptom | Cause |
|---|
no printer address | pass --host <ip> or export KYYHKY_HOST |
| Blank labels | content outside the print window -- check the margin offset and that mirroring is applied to the image, not the canvas |
| Mirrored text | pin axis not reversed (--no-mirror was passed) |
| Nothing prints, no error | compression disabled -- LAN needs TIFF |
status shows nothing | expected; the Ethernet board has no status channel |
discover finds nothing | cold ARP cache; it already sweeps twice, try --timeout 1.0 |