Skip to content

vsgusev/text2paper

Repository files navigation

Text2Paper

I built this for my own home server and printer — sharing it in case it's useful, or you pinch something handy.

A Telegram bot that prints to a thermal receipt printer. Send it a message and it prints a note. Send a photo and it prints the photo. Run /receipt and it prints a mock store receipt filled with live data (weather, FX rates, and whatever else you decide to put on it). Built for a Linux home server — or that little Raspberry Pi forgotten in a closet — with any USB ESC/POS printer.

A receipt printed by Text2Paper

A couple of reasons to bother. Hand the bot to friends and it turns into a tiny shared printer — their messages, photos and memes pile up on your desk as little thermal-paper postcards. Or maybe you just like numbers: print a /receipt every Sunday and watch your week turn into steps, kilometres, heart rate, expenses and one completely meaningless total at the bottom.

Features

  • Plain text — any message is printed as a note (capped at an arbitrary 355 characters — bump MAX_NOTE_LENGTH in bot.py).
  • Photos — printed dithered to black & white, scaled to the tape.
  • /receipt — a mock receipt filled by pluggable data providers, with subtotal/tax/total, an upside-down joke, and a barcode.
  • /list — a checklist receipt: /list milk, bread, eggs or one item per line.
  • Optional access control via a Telegram user-ID allowlist.

Everything is rendered to an image and sent as a bitmap, so the layout looks the same on any ESC/POS printer regardless of its built-in fonts.

Requirements

  • A Linux host (tested on Debian/Ubuntu; a Raspberry Pi works fine)
  • A USB ESC/POS thermal printer, 80 mm tape (576 px — what the layout is tuned for). Tested with an Xprinter XP-T80A; any ESC/POS model should work.
  • Docker (the easy path) — or Python 3.10+ and libusb to run from source

Setup

git clone https://github.com/vsgusev/text2paper
cd text2paper
cp .env.example .env

Then fill in .env — see Configure.

Configure

.env holds everything the bot needs. The essentials are your bot token and the printer's USB ids; everything else has a sane default.

Variable Required Description
BOT_TOKEN yes Token from @BotFather
PRINTER_USB_VID yes Printer USB vendor id in hex, e.g. 0x0483
PRINTER_USB_PID yes Printer USB product id in hex, e.g. 0x5743
PAPER_WIDTH_PX no Printable width in dots; default 576 (80 mm). 58 mm tape is 384, but the FONT_* sizes in render.py are tuned for 576 — expect to shrink them yourself
FONT_PATH no Path to a monospaced TTF (defaults to DejaVu Sans Mono)
LOGO_PATH no Receipt header logo (defaults to media/logo.png)
ALLOWED_USER_IDS no Comma-separated Telegram user ids allowed to use the bot; empty = everyone
TZ no IANA timezone for quiet hours, e.g. Europe/Moscow; defaults to UTC
QUIET_HOURS no Local-time window when the bot won't print — it replies to say so, nothing is queued, e.g. 01:00-09:00 (may wrap midnight); empty = the bot always prints
MAX_MESSAGE_AGE no Ignore messages older than N seconds (e.g. stale after downtime); 0/empty = print everything

Find your printer's USB ids

lsusb
# Bus 001 Device 005: ID 0483:5743 ...
#                        ^^^^ ^^^^
#                        VID  PID

So PRINTER_USB_VID=0x0483 and PRINTER_USB_PID=0x5743.

USB permissions (run without root)

Add a udev rule so your user can talk to the printer directly:

echo 'SUBSYSTEM=="usb", ATTRS{idVendor}=="0483", ATTRS{idProduct}=="5743", MODE="0666"' \
  | sudo tee /etc/udev/rules.d/99-text2paper.rules
sudo udevadm control --reload-rules && sudo udevadm trigger

Replace 0483/5743 with your own ids (no 0x prefix here). Re-plug the printer.

Restrict who can use the bot (optional)

Set ALLOWED_USER_IDS in .env to a comma-separated list of Telegram user ids. Leave it unset to allow everyone:

ALLOWED_USER_IDS=123456789,987654321

Get your id from @userinfobot.

Run with Docker

The easy path. With .env filled in and the udev rule in place:

docker compose up -d --build
docker compose logs -f          # follow logs

Message /start to your bot, then send any text.

The container restarts on boot (restart: unless-stopped), so you don't need the systemd unit (that's only for the from-source path). USB is passed through with devices: /dev/bus/usb:/dev/bus/usb — the whole bus, because a printer's device number changes on re-plug while the bot finds it by VID/PID. The host still needs the udev rule above so the container may open the device.

Run from source

For hacking on the code or writing your own providers — edit a file and rerun, no image rebuild:

sudo apt update
sudo apt install -y python3-venv libusb-1.0-0 fonts-dejavu-core

python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt

python bot.py

Message /start to your bot, then send any text.

fonts-dejavu-core provides DejaVu Sans Mono, which the renderer finds automatically. Any monospaced TTF works — point FONT_PATH at it instead.

Autostart with systemd

Only for the from-source path — the Docker container already restarts on boot. Create /etc/systemd/system/text2paper.service:

[Unit]
Description=Text2Paper Telegram printer bot
After=network-online.target

[Service]
WorkingDirectory=/home/youruser/text2paper
ExecStart=/home/youruser/text2paper/venv/bin/python bot.py
Restart=on-failure
User=youruser

[Install]
WantedBy=multi-user.target
sudo systemctl enable --now text2paper
journalctl -u text2paper -f   # logs

Troubleshooting

  • USBError: Access denied — the udev rule isn't applied. Re-check USB permissions and re-plug the printer.
  • Printer not found (DeviceNotFoundError) — run lsusb on the host and confirm PRINTER_USB_VID / PRINTER_USB_PID in .env match its ID. In Docker, also check the devices: USB passthrough in docker-compose.yml and that the udev rule is applied on the host.
  • Cyrillic (or other glyphs) print as squares — the font lacks those glyphs. Point FONT_PATH at a TTF that has them (DejaVu Sans Mono covers Latin + Cyrillic).

Customize

Header logo

The receipt header is an image. Replace media/logo.png with your own, or set LOGO_PATH. If the file is missing, the receipt simply prints without a logo.

Bot language

The bot's chat replies ("printed!", "access denied", …) are in Russian — they're all in one block at the top of bot.py, marked localize the bot here. Edit them to your language; nothing else reads them.

Receipt data providers

The items on /receipt come from modules in providers/. Each one is fully self-contained — there is no central config for receipt data:

  • All settings live at the top of the provider's own file (e.g. your coordinates in providers/weather.py).
  • Want different data? Add a file. Drop a module exposing fetch() -> list[Item] | None; it's auto-discovered, nothing else to edit.
  • Don't want some data? Delete the file. That's the whole removal step.

The bundled weather and rates providers are just examples — rates happens to print RUB and BTC because that's what the author watches. Edit them, delete them, or add your own (other currencies, crypto, sensors, anything):

# providers/cpu.py
from blocks import Item

def fetch():
    with open("/sys/class/thermal/thermal_zone0/temp") as f:
        celsius = int(f.read()) / 1000
    return [Item("CPU TEMP", f"{celsius:.0f}")]

Numeric values are summed into the receipt's subtotal/total (part of the joke — your data is "sold" on the receipt). Return Items only — other block types are ignored. A provider that raises or returns None is skipped without breaking the receipt.

Project layout

File / dir Responsibility
bot.py Telegram transport — commands and message handlers
receipt.py Composes receipts from blocks
blocks.py Block primitives (Item, Banner, Total, …)
providers/ Data sources for /receipt (auto-discovered)
render.py Lays a receipt out into a single image
images.py PIL helpers — fonts, text blocks, barcodes
printer.py Renders, then hands the image to the backend
backend.py Sends the image to the ESC/POS printer over USB
config.py Printer & rendering settings, read from the environment

Contributing

Issues and PRs are welcome — but this is a weekend project I run for myself, so no promises on response time or scope.

License

MIT — see LICENSE.

About

Telegram bot that prints notes, photos and mock receipts on a thermal ESC/POS printer

Topics

Resources

License

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors