Sign inSign up

dodoooh/brother_ql_app

By dodoooh

•Updated about 2 months ago

A web interface and REST API for Brother QL label printers

Image
0

10K+

dodoooh/brother_ql_app repository overview

Source on GitHub

⁠Brother QL Printer App

Stars CI Docker Pulls Image size Release Issues Licence

A web interface and REST API for Brother QL label printers. Compose a label in the browser or send it from a script. The app renders it, queues it and prints it over the network, over USB, or through a Linux device node.

Demo

The compose view, light theme on the left and dark on the right

Open the demo⁠. It is the real interface with a simulated printer behind it, runs entirely in your browser, and prints nothing.

⁠Features

  • Text, images, QR codes and PDFs, plus combined layouts (text with a QR code or an image beside it)
  • A live preview rendered by the same code that drives the printer
  • A print queue with per-job status, reprint and cancel
  • Media detection: the app can read which roll is loaded and follow it
  • Print alignment calibration per label type, with an optional bleed
  • Round die-cut labels, and text that runs lengthwise along continuous tape
  • Optional mains power control through a relay
  • Keep-alive for network printers that fall asleep
  • A documented REST API with a Swagger UI, for Home Assistant, Apple Shortcuts or a shell script

⁠Quick start

# docker-compose.yml
services:
  brother_ql_app:
    image: dodoooh/brother_ql_app:dev
    container_name: brother_ql_app
    ports:
      - "5000:5000"
    volumes:
      - ./data:/app/data
    restart: unless-stopped
    environment:
      - PYTHONPATH=/app
      - FLASK_ENV=production
docker compose up -d

The interface is then at http://<host>:5000 and the API documentation at http://<host>:5000/api/v1/ui/. Set your printer address under Settings or with PUT /api/v1/settings.

The image is published for linux/amd64 and linux/arm64, which covers a 64-bit Raspberry Pi, a NAS and an ordinary server. There is no 32-bit build: Pillow ships no armv7 wheel, so it would have to be compiled, and this image deliberately carries no compiler. On a 32-bit system, build it yourself with docker build.

The API is open to whoever can reach the port. That matches what it drives: a label printer accepts a raster on port 9100 from anyone on the same network, with or without this app in front of it. Give it a network you trust, or a reverse proxy, and see API_KEY below if you run it without the interface.

⁠Printing from a script

curl -X POST http://<host>:5000/api/v1/text/print \
  -H 'Content-Type: application/json' \
  -d '{
        "text": "Shelf A-12",
        "settings": { "label_size": "62", "font_size": 50 }
      }'

The call returns a job id straight away. Poll GET /api/v1/jobs/{id} if you need the result. Add -H 'X-API-Key: ...' if you set that variable. Image and PDF upload, QR codes, combined layouts, printer status and queue control work the same way and are documented in the Swagger UI, which is generated from the specification the server routes on. The demo carries the same reference as a static page⁠, for reading it without installing anything.

⁠Print queue

Jobs are printed one at a time, because the printer accepts one connection at a time. The queue shows what each job is doing, including the phases a job passes through while it waits for a printer to become available.

The print queue

⁠Mains power control

A Brother QL starts up on its own as soon as mains power returns, at least the QL-820NWB this was built against. That makes a relay in front of it a remote power switch: cutting the supply switches the printer off, restoring it switches the printer on, and nobody has to walk over and press the button.

The relay is anything that can be driven by a webhook: a Shelly, a Tasmota plug, an ESPHome switch, or a Node-RED flow in front of one. The app closes it when a job arrives at a printer that is not answering, and opens it again once the configured window has passed.

The mains power section in Settings

A printer that has just been switched on needs time. The app waits for it, holding the job in the queue instead of failing it, and retries the print a few times while the device settles. The waiting times come from watching a QL-820NWB boot, so they are one printer's numbers for now.

The feature is off by default, and switching the printer off again is a separate setting. printer_auto_power_off_minutes should match what the printer's own menu shows, since the app subtracts it from the keep-alive window and has no way to read it from the device.

⁠Configuration

Settings live in data/settings.json. Edit them in the interface or with PUT /api/v1/settings. Any of them can also be sent with a single print request under settings, where they override the stored value for that job only.

Printer

SettingDefaultWhat it does
printer_uritcp://192.168.1.100Also usb://0x04f9:0x209b or file:///dev/usb/lp0
printer_modelQL-800For example QL-820NWB
label_size62The loaded media, for example 62, 62red or d24
ipp_port631Port used for status, media detection and the printer clock
printersone entryA list of printers to choose from. The fields above are the default one

Layout and rendering

SettingDefaultWhat it does
font_size50Starting size. Shrinks further if the text does not fit
alignmentleftleft, center or right
vertical_alignmentmiddletop, middle or bottom, where the medium leaves room
orientationacrosslengthwise runs the text along continuous tape
text_markupfalseHonour **bold** and *italic*. Sets the base text lighter
rotate0Rotates the finished label by 0, 90, 180 or 270
threshold70.0Where grey turns into black, in percent
ditherfalseDither instead of thresholding. Better for photographs
redfalseTwo-colour printing on red media such as 62red
dpi_600, hqfalse, trueHigher resolution and quality mode, where the model supports it
compressfalseCompresses the raster on the way to the printer
copies, cut_mode1, eachNumber of labels and when to cut: each, end or none
calibration{}Per label type: offset in millimetres and a size correction
bleed_mm{}Per label type: prints into the unprintable margin. Experimental

Media detection

SettingDefaultWhat it does
media_auto_switchfalseAdopt the roll the printer reports as loaded
owned_media[]The media you actually have. Narrows an ambiguous roll to one
media_preference{}Which variant wins where a roll stays ambiguous
media_memory{}What was last used on each medium. Maintained by the app

Keep-alive and mains power

SettingDefaultWhat it does
keep_alive_enabledfalseKeeps a network printer from falling asleep
keep_alive_interval60Seconds between heartbeats
keep_alive_modeforevertimed stops after the window below
keep_alive_duration_seconds7200Length of that window, measured from the last print
relay_webhook_enabledfalseMains power control, see above
relay_webhook_turn_on_urlemptyPOSTed to switch the printer on
relay_webhook_turn_off_urlemptyEmpty means the turn-on URL is reused
relay_webhook_turn_off_enabledfalseOpt in separately to switching off
relay_webhook_turn_off_delay_minutes5Grace period after the keep-alive window closes
printer_auto_power_off_minutes10The printer's own timer. Subtracted from the window above

Environment variables:

VariableDefaultPurpose
API_KEYunsetRequires X-API-Key on /api/v1. The bundled interface does not send it and stops working, so this suits API-only installations
CORS_ORIGINSsame-originComma-separated list of allowed origins
UPLOAD_FOLDERsrc/uploadsWhere rendered labels and uploads are staged
MAX_UPLOAD_IMAGE_PIXELS50000000Larger images are refused before they are decoded
MAX_PDF_PAGES20Larger page selections are refused before rendering
JOB_FILE_TTL_SECONDS86400How long job files are kept
RELAY_WEBHOOK_AUTHORIZATIONunsetSent as the relay webhook's Authorization header
LOG_LEVELINFOPython logging level

The relay credential is an environment variable and not a setting, because GET /settings returns the configuration to any client that can read it.

⁠Tested printers and media

What has actually been printed, rather than what the label catalogue says is possible. The app knows about far more media than this table lists; these are the combinations somebody has held in their hand.

PrinterMediaTested byDateVersion
QL-820NWB62 mm continuous (62)Dodoooh2026-08-044.0.0-rc.1
QL-820NWB12 mm continuous (12)Dodoooh2026-08-044.0.0-rc.1
QL-820NWB24 mm round die-cut (d24)Dodoooh2026-08-044.0.0-rc.1

If you print on something that is not in the table, a line here is welcome: printer, media, and whether anything needed calibration.

⁠On a phone

The interface works on one, and POST /api/v1/share accepts a file from the iOS share sheet or an Android HTTP Shortcut and hands it to the print form.

The interface on a phone

⁠Development

python3 -m venv venv && source venv/bin/activate
pip install -r requirements-dev.txt
python src/app.py

Python 3.11 is required. The test suite needs a TrueType font installed (fonts-dejavu); without one, about a third of it skips.

pytest

src/api/openapi.yaml is the source of truth for routing. An endpoint changes there and in its controller, or it does not change.

⁠AI assistance

Yes, this project is built with AI assistance. It belongs to the toolchain like anything else here, and what comes out of it is reviewed and tested by a human before it lands. Keeping a project this size moving, mostly alone and in spare time, would not otherwise be realistic.

⁠Contributing

Contributions are welcome. Open an issue or send a pull request. Reports from printers and media that are not in the table above are useful too.

⁠Acknowledgments

  • DL6ER for support for additional printer models and label types, and for USB printing
  • MSanteler⁠ for finding and diagnosing the hardcoded label width, which meant every roll narrower than 62 mm printed at the wrong scale

⁠Changelog

changelog.md⁠.

⁠Licence

CC BY-NC-SA 4.0. See LICENSE⁠.

Tag summary

Content type

Image

Digest

sha256:0493f350c…

Size

70.6 MB

Last updated

about 2 months ago

docker pull dodoooh/brother_ql_app