English · Русский
A small self-hosted web server that turns a USB printer/scanner (MFP, multifunction unit) into a network device for your home or small office:
- print any PDF or image from a browser — on a phone, tablet or another computer — with a real print preview;
- add it as a network printer on other machines: the server speaks IPP Everywhere, so Windows, Linux (CUPS), macOS, Android and iOS clients use their built-in driverless driver. You don't need the printer's own driver on each client;
- scan from the browser like phpSane: preview the glass, select an area, pick resolution/mode/format, merge pages into one PDF, or make a copy at actual size;
- add it as a network scanner too: the server also speaks AirScan (eSCL), so macOS, iOS and Android scan with their own built-in scanning, no app needed;
- maintain the printer: OS test page, and for Canon PIXMA — nozzle check and print-head cleaning.
It runs on Windows (Win32 print spooler + WIA) and Linux (CUPS + SANE). The UI is in English and Russian.
- Why
- Features
- Tested hardware
- Quick start
- Using it
- Configuration
- How it works
- HTTP API
- Data on disk
- Security
- Troubleshooting
- Extending
- Development
- Building installers
- Known limitations
- License
Cheap inkjet MFPs such as the Canon PIXMA MG2500 series have only USB: no Wi‑Fi, no network printing, no scanning from a phone. Sharing them the Windows way needs a vendor driver on every PC and doesn't help phones at all. This project puts one always-on machine next to the printer and makes the printer available to everything on the LAN:
- anything with a browser can print and scan;
- anything with an IPP client (all modern OSes) can print with its built-in driver;
- anything with an eSCL/AirScan client (macOS, iOS, Android) can scan the same way.
- Upload a file (drag & drop), choose printer, copies and options read from the printer driver: paper size, paper type, quality, color, duplex. Nothing is hard-coded, so another printer shows its own options.
- PDF and images are rendered by the server itself (PDFium / Pillow) straight into the printer. Printing therefore doesn't depend on which programs are installed, and settings apply to that one job only, not to the printer's defaults.
- Accurate preview for PDFs and images: the real sheet size and the printer's hardware margins, pages fitted exactly the way they'll print, landscape pages auto-rotated, grayscale for B&W. Preview and printing share the same layout code.
- Documents and text (
.txt,.rtf,.docx,.odt,.xlsx,.pptx, …): with LibreOffice installed on the server they're converted to PDF and then printed and previewed exactly like a PDF, on Windows and Linux. Non-UTF-8 text files (e.g. Windows-1251) are re-encoded first, so Cyrillic isn't garbled. Without LibreOffice they're handed to the program registered to print them (Notepad, Word/WordPad, …) and there's no preview. - Paper type ↔ size rules: some printers reject certain combinations, e.g. glossy photo paper in 13×18 on a Canon (error 4102). The server swaps in a compatible paper type and records a note on the job. The UI does the same as you pick options.
- Job history: status, settings and notes. Stored in a JSON file, so it survives restarts. Delete single entries or clear it all; uploaded files are removed with their entries.
- Cancel a job (⏹ next to a job, once it has a queue id — see Known limitations) pulls it back out of the OS's own print queue, e.g. right after noticing the wrong file or copy count. It works even for a job already showing "Sent": that only means it reached CUPS/the Windows spooler, not that it has physically printed yet.
- 🔔 Notify me: an opt-in browser notification (native
NotificationAPI, no server involved) when a job finishes printing, fails or gets canceled — handy for a job submitted from a phone or another room. Per-browser, remembered in a cookie. - Live printer status next to the printer picker: idle/printing/stopped/offline, plus the reason when there is one (out of paper, cover open, jammed, ...), read from the OS (IPP on Linux, WMI on Windows) — not guessed. Ink/toner levels too, shown as a small bar per color, when the driver reports them (many CUPS drivers do via IPP marker levels; not currently for a USB-only Canon on the generic driver this project itself uses — see Known limitations).
- Usage report: a "Print usage" panel above the job history totals jobs/sheets/sent/failed and breaks them down by user (when known — logged-in username, or the IPP client's own user name) and by printer, with a CSV export for accounting or handing to whoever pays for the toner.
- The server is an IPP/2.0 printer on port 631 (
ipp://<host>:631/ipp/print). - Clients render documents themselves and send PDF, PWG Raster or JPEG, sized to the media and margins the server advertises. The server prints them 1:1.
- Capabilities come from the real driver: 20+ paper sizes with standard PWG names and real margins, paper types, color, duplex, quality.
- Bonjour/mDNS announcement (Windows,
mdns = yesby default): macOS, iOS and Android find the printer by name in "Add Printer" instead of needing its address typed in. - AirPrint: advertised under the
_universalsubtype iOS/macOS specifically browse for (plain_ipp._tcpisn't enough for AirPrint to list a printer), with theURFTXT record it requires to be present. It doesn't decode Apple's own URF raster format, soURF=nonetells AirPrint to send PDF or JPEG instead — the same conventionairprint-generateand CUPS's own AirPrint support use — meaning an iPhone/iPad can print to it directly, no app or profile needed. - Tested with Linux/CUPS clients (text editor, LibreOffice, PDFs, photos).
- Scanner capabilities from the driver: glass size, resolutions, modes, brightness/contrast.
- Low-resolution preview of the whole glass. Select an area with the mouse, or pick a preset: A4, A5, A6, Letter, 4×6 photo, business card.
- Color / grayscale / B&W (text), 75–600 dpi (whatever the scanner supports); shows the resulting pixel size.
- Save as JPEG, PNG, TIFF or PDF (correct DPI stored in the file).
- Gallery of scans: open, download, delete, merge selected scans into one multi-page PDF (in the order you tick them).
- Copy: print a scan at its real physical size (a scanned business card prints as a business card, not blown up to A4).
- AirScan/eSCL: the scanner is also a driverless network scanner (
escl = yes, on both Windows and Linux) — macOS "Image Capture", iOS's built-in scan and Android scan directly, no app or this web UI needed. Bonjour-discoverable like the printer.
- Test page: the operating system's standard test page, for any printer.
- Nozzle check and head cleaning for Canon inkjets: byte-for-byte the maintenance job the Canon driver itself sends (IVEC mode switch + BJL commands), captured from the Windows driver's print queue.
- Optional sign-in (
auth = yesinconfig.ini): users and passwords (hashed or plain) in the config, remembered sessions, brute-force lockout, HTTP Basic for scripts, optional Basic auth for IPP. API tokens (a "🔑 API tokens" panel once logged in) give scripts a revocableAuthorization: Bearercredential instead of a real password. - Optional admin role (
[admins]inconfig.ini) and monthly print quotas: admins get a panel to cap how many sheets each user may print per month; everyone else sees their own usage against their limit. Enforced up front — a job that would exceed it never reaches the printer. - QR code (📱 button in the header) encoding the server's own URL, so a phone can open the web UI without typing an IP address.
- Optional HTTPS for the web UI: point
ssl_certfile/ssl_keyfileinconfig.iniat a certificate and key, and the web port serves TLS instead of plain HTTP — no reverse proxy needed if you already have a certificate (e.g. from your router, a LAN CA, ormkcert). - One config file (
config.ini) for language, ports, folders and users. - English / Russian UI with a remembered choice; server messages follow the page language.
- Remembered print/scan settings: the last printer, copies, per-printer options, and the last scanner, area, mode, resolution, brightness/contrast and format are kept in a cookie in your browser (not on the server) and pre-filled next time.
- Works from phones (responsive layout).
- One-click start on Windows (
start.bat) and a start script for Linux (start.sh). - No admin rights needed on Windows: per-user printer settings, spooler access only.
| Component | Status |
|---|---|
| Windows 10 Enterprise LTSC 2021 (21H2), Python 3.12 | ✅ server: printing, preview, IPP, scanning, maintenance UI |
| Canon PIXMA MG2541 (driver "Canon MG2500 series Printer", USB) | ✅ printing (PDF, images, text), WIA scanning |
| Linux client (CUPS, driverless IPP Everywhere) → this server | ✅ printing from GNOME Text Editor, LibreOffice, PDF viewer |
| Windows client with "Microsoft IPP Class Driver" | ✅ Windows 10 found the server via mDNS in "Add a printer" and installed it with the Microsoft IPP Class Driver (PDF as the document format, capabilities taken from the server); its Windows test page came through the IPP server to the MG2541 as a 1-page job with the client's user name and A4/plain paper/color settings |
Bonjour/mDNS advertisement (zeroconf) |
✅ run for real on Windows 10: _ipp._tcp (port 631) and _uscan._tcp (port 8000) are announced once each with the LAN address; a real MacBook (macOS 12) finds both the printer and the scanner, Android phones find the printer. UDP 5353 opened by register_service.bat/the installer |
AirPrint (_universal subtype, Windows server) |
✅ printing from a real MacBook (macOS 12) works; Android phones print too (one Xiaomi picks glossy photo paper by itself — see the 4102 note in Troubleshooting). |
HTTPS for the web UI (ssl_certfile/ssl_keyfile) |
✅ tested on Linux: serves TLS only on the web port, falls back to plain HTTP cleanly if only one of the two is set |
| Live printer status (Linux, via CUPS's own IPP) | ✅ tested against the real MG2500: correctly reports idle, and stopped+"paused" for a disabled queue; ink levels are unavailable for this printer's driver (expected, not a bug — see Known limitations) |
| Live printer status (Windows, via WMI) | ✅ run on the real MG2541: reports idle and accepting jobs; ink levels are unavailable from this driver (same as on Linux). |
| AirScan/eSCL server (Linux, real Canon PIXMA MG2500) | ✅ ScannerCapabilities/ScannerStatus/ScanJobs/NextDocument all tested against real hardware: correct bed size and formats reported, a real scan came back as PDF and as grayscale JPEG with the requested region cropped correctly (verified pixel size and content); mDNS advertisement seen by avahi-browse and independently picked up by SANE's own escl/airscan client backends on the same machine, with a matching UUID. That self-discovery also surfaced and let us fix a real bug: it made scanimage -L ~12x slower and occasionally hit a transient "device busy" — see "A Linux quirk this surfaced" in How it works |
| AirScan/eSCL server (Windows, real Canon PIXMA MG2541 over WIA) | ✅ ScannerCapabilities/ScannerStatus/ScanJobs/NextDocument tested with real scans: color JPEG at 150 dpi (full A4, 1275×1753) and grayscale PDF at 75 dpi; status goes Idle → Processing → Completed, a second NextDocument correctly answers 404 |
| AirScan/eSCL from a real Mac (Image Capture, macOS 12, Windows server) | ✅ adds the scanner, preview and full scan both arrive (preview 75 dpi, full scan an 11 MB JPEG). Getting there needed the job to stay "Processing" until the client's own end-of-job NextDocument gets a 404 — macOS drops a scan whose job already reads "Completed" when it checks right after fetching it. |
| AirScan/eSCL from Android ("eSCL Scanner" app, Android 16, added by IP address) | ✅ added and scanned. Such apps take just an IP and look at port 80, so the server also answers /eSCL/* there (escl_port) — before that the phone's requests never reached the web port |
| Linux server (CUPS printing) | ✅ printing was tested at the start of the project |
| Linux server — SANE scanning (preview, area select, color/gray/lineart, JPEG/PNG/TIFF/PDF, merge to PDF) | ✅ tested end-to-end against a real Canon PIXMA MG2500 over scanimage |
| Linux server — print preview (PDF/image rendering, margins) | ✅ tested; exact hardware margins aren't available on Linux (falls back to A4 + 5 mm) |
Linux server — maintenance: OS test page, Canon nozzle check and head cleaning (via CUPS/lp) |
✅ confirmed on the MG2500 — printed both the CUPS test page and the nozzle pattern, audibly ran head cleaning |
| Maintenance on Windows: test page (server run as administrator), Canon nozzle check and head cleaning | ✅ confirmed on the MG2541 |
| Documents via LibreOffice 26.8 on Windows (TXT UTF-8 / Windows-1251, RTF, DOCX, XLSX) | ✅ conversion, preview and printing tested (printed to Microsoft Print to PDF) |
| Documents via LibreOffice 24.2 on Linux (TXT UTF-8 / Windows-1251, DOCX, RTF) | ✅ conversion, preview (correct Cyrillic) and conversion caching tested; the converted PDF reaches CUPS correctly — physical output on paper wasn't confirmed this run (printer was disconnected) |
Windows autostart task (register_service.bat) |
✅ confirmed across a full computer restart: the Task Scheduler task registers, and the server comes up on its own before anyone logs on |
| Linux systemd unit | ✅ confirmed: installed under a dedicated mfp system user (enabled for boot), serving real requests and reaching the USB scanner via the scanner group |
Windows installer (packaging/windows: PyInstaller 6.22 mfp-server.exe + Inno Setup 7.1) |
✅ no Python needed on the target PC. Installed over the previous install on Windows 10; the autostart task runs mfp-server.exe, firewall rules for TCP 8000/631/80 and UDP 5353, Start menu shortcuts. From the installed .exe: web UI, printer options/status/maintenance, PDF preview with the exact layout, TXT preview via LibreOffice, a real WIA scan over eSCL on port 80, IPP, and the mDNS/AirPrint announcements all work. Uninstall (tested with the earlier Python-based installer) removes the task, rules, shortcuts and program files and keeps config.ini/scans/uploads. Setup can't register the task while another server holds port 8000 — close a start.bat window first |
Reports for other printers and scanners are very welcome, see Extending.
Requirements:
- Windows 10 or 11;
- Python 3.11+ from python.org: tick "Add python.exe to PATH" during installation;
- the printer installed with its normal driver, so that printing from e.g. Notepad already works;
- optional: LibreOffice, to print and preview documents and text files exactly. It's found automatically in its standard install folder; restart the server after installing it.
Steps:
- Download or clone this repository.
- Double-click
start.bat.
On the first run it creates a virtual environment in .venv and installs dependencies. Later it reinstalls them only when requirements.txt changes. Then it shows the addresses to open:
[MFP] Web UI: http://localhost:8000
[MFP] On the network: http://192.168.1.20:8000 IPP printer: ipp://192.168.1.20:631/ipp/print
Stop it with Ctrl+C or by closing the window.
Start automatically: to run it in the background from Windows startup, see Running as a service.
Allow access from other devices: Windows asks about the firewall the first time Python listens on the network; allow it for private networks. If you register the autostart task (below), register_service.bat opens everything it needs by itself — the web port, the IPP port if enabled, the eSCL port (escl_port, 80), and UDP 5353 for Bonjour/mDNS (needed for AirPrint, AirScan/eSCL and "Add Printer" discovery to find the server at all; the ports above are what the actual print/scan traffic uses afterwards) if mdns = yes.
Running it manually via start.bat instead (no autostart task) skips that step, so add the rules yourself if other devices need to reach it (in an elevated PowerShell):
New-NetFirewallRule -DisplayName "MFP Print & Scan Server (IPP)" -Direction Inbound -Protocol TCP -LocalPort 631 -Action Allow
New-NetFirewallRule -DisplayName "MFP Print & Scan Server (Web)" -Direction Inbound -Protocol TCP -LocalPort 8000 -Action Allow
New-NetFirewallRule -DisplayName "MFP Print & Scan Server (mDNS)" -Direction Inbound -Protocol UDP -LocalPort 5353 -Action AllowTip. On Windows 10/11, "Let Windows manage my default printer" makes the most recently used printer the default one. If you want the server's default to stay on your MFP, turn it off in Settings → Devices → Printers & scanners.
Requirements:
- Python 3.11+ with
venv(sudo apt install python3-venv); - CUPS with the printer configured (
lpstat -alists it); - for scanning, SANE:
sudo apt install sane-utils, andscanimage -Lshould list the scanner; - optional: LibreOffice, to print office documents (
sudo apt install libreoffice).
Steps:
git clone <this repo> mfp-print-scan-server
cd mfp-print-scan-server
chmod +x start.sh
./start.shOn Linux the built-in IPP printer is disabled: CUPS already is an IPP server. Share the printer through CUPS instead (sudo cupsctl --share-printers, then tick "Share this printer" in the CUPS admin page).
Double-click register_service.bat and approve the administrator prompt. It:
- prepares
.venv(the same asstart.bat); - registers a Task Scheduler task "MFP Print & Scan Server" that:
- starts at Windows startup, before anyone logs on, with no console window;
- runs as your user account without storing a password (S4U logon), so it sees the same printers, printer settings and default printer as you;
- restarts the server if it exits (every minute), with no run-time limit;
- writes the log to
logs\server.log(the previous log is kept asserver.log.1once it passes 5 MB);
- opens TCP for the web port (8000) and, if enabled, the IPP port (631) and the eSCL port (
escl_port, 80) in Windows Firewall for private/domain networks, plus UDP 5353 for Bonjour/mDNS ifmdns = yes(AirPrint, AirScan/eSCL and "Add Printer" discovery all need it to find the server, separately from the ports actual print/scan traffic uses) — and warns if your network is marked Public; - starts the server right away and checks that it answers.
Why a scheduled task and not a classic Windows service:
python.execan't act as a service program;- a service running as SYSTEM wouldn't see your default printer and your per-user printer settings.
If a start.bat window is still running, close it first, otherwise the ports are busy. Running the script again updates the task. Ports and mdns come from config.ini, so run the script again after changing them there: the firewall rules follow.
Managing it afterwards:
| What | How |
|---|---|
| status, start, stop | Task Scheduler (taskschd.msc) → MFP Print & Scan Server, or Get-ScheduledTask "MFP Print & Scan Server" / Start-ScheduledTask / Stop-ScheduledTask |
| log | logs\server.log |
| remove | double-click unregister_service.bat: stops and deletes the task and removes the firewall rules. Job history, scans and logs stay |
Updating the code while it runs as a service: stop the task (or run
unregister_service.bat), replace the files, runregister_service.batagain.
An example unit is in deploy/mfp-print-scan-server.service. It assumes the code lives in /opt/mfp-print-scan-server and runs as a dedicated mfp user:
# 1. Put the code in place and create a service account
sudo cp -r mfp-print-scan-server /opt/mfp-print-scan-server
sudo useradd --system --home /opt/mfp-print-scan-server --shell /usr/sbin/nologin mfp
# 2. Printer/scanner device access - only groups this system actually has
for grp in lp scanner; do getent group "$grp" >/dev/null && sudo usermod -aG "$grp" mfp; done
sudo chown -R mfp:mfp /opt/mfp-print-scan-server
# 3. Create .venv and install dependencies as that user
sudo -u mfp /opt/mfp-print-scan-server/start.sh --setup-only
# 4. Install and start the service
sudo cp /opt/mfp-print-scan-server/deploy/mfp-print-scan-server.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now mfp-print-scan-serverCheck the unit before installing it:
User=/Group=: the account the server runs as (itslp/scannergroup membership, set up in step 2 above, is enough for device access — the unit doesn't need its ownSupplementaryGroups=line, which would refuse to start at all if a listed group doesn't exist, e.g. Fedora has no "scanner" group);WorkingDirectory=/ExecStart=: change them if the code isn't in/opt/mfp-print-scan-server;Environment=: any configuration variables.
Managing it afterwards:
systemctl status mfp-print-scan-server # state
journalctl -u mfp-print-scan-server -f # live log
sudo systemctl restart mfp-print-scan-server # after updating the code or settings
sudo systemctl disable --now mfp-print-scan-server && sudo rm /etc/systemd/system/mfp-print-scan-server.service # remove- Open
http://<server>:8000. - Drop a file or click to choose one.
- Pick the printer and options. The option list comes from that printer's driver.
- Optionally press Preview. While the preview is open it updates on every change.
- Press Print. The job appears in Recent jobs. If the server had to adjust something (e.g. the paper type), a ⚠ with an explanation is shown next to the status.
| Format | How it's printed | Preview |
|---|---|---|
| rendered by the server (PDFium) | ✅ exact | |
| JPEG, PNG, BMP, GIF, TIFF, WebP | rendered by the server (Pillow) | ✅ exact |
| TXT, RTF, DOC/DOCX, ODT, XLS/XLSX, ODS, CSV, PPT/PPTX, ODP | with LibreOffice installed: converted to PDF, then like a PDF (Windows and Linux) | ✅ exact, with LibreOffice |
| the same, without LibreOffice | Windows: the program registered to print the file type; Linux: CUPS with the original file | — |
The server is an IPP Everywhere printer at ipp://<server>:631/ipp/print. It also answers on ipp://<server>:631/ and ipp://<server>:631/ipp, because some clients only let you type a host name. Clients use their own built-in driver, and their print dialog shows the preview.
Linux (CUPS)
sudo lpadmin -p MFP -E -v ipp://192.168.1.20:631/ipp/print -m everywhereOr in Settings → Printers → Add, or at http://localhost:631/admin: enter the URI and choose the "IPP Everywhere" / "driverless" driver.
Windows 10/11
- Open Settings → Devices → Printers & scanners → Add a printer or scanner → "The printer that I want isn't listed".
- Choose "Add a printer using an IP address or hostname".
- Set Device type to IPP Device and enter the server's address.
Windows picks the Microsoft IPP Class Driver by itself.
macOS / iOS / Android. With mdns = yes (the default, Windows only — see Configuration) the server advertises itself over Bonjour/mDNS, so "Add Printer" finds it by name, no address needed. Without it (or on Linux, where the OS's own IPP server handles sharing instead), add it manually the same way as above: macOS System Settings → Printers → Add → IP, protocol "IPP", address 192.168.1.20, queue ipp/print.
What the server advertises is built from the real printer driver: paper sizes with their hardware margins, paper types, color, duplex, quality, and 300/600 dpi for raster clients. Jobs are printed page = sheet, so the margins the client laid out are exactly where they end up on paper.
- Open the Scan tab and put the document face down in the corner of the glass.
- Press Preview: the whole glass at low resolution, a few seconds.
- Drag on the preview to select an area, or choose a preset. Drag the selection to move it; a single click resets it to the whole glass.
- Choose mode, resolution, brightness/contrast and format. The line below shows the resulting size in pixels.
- Press Scan. The result appears in Scans.
In the gallery:
- tick scans and press Merge selected into PDF; pages go in the order you ticked them;
- Print makes a copy at actual size on the printer selected on the Print tab.
With escl = yes (the default, Windows and Linux) the scanner is also available as a driverless network scanner — no app or this web UI needed:
- macOS: System Settings → Printers & Scanners (or the Image Capture app) should list it automatically once discovered via Bonjour; scan from there directly.
- iOS: the Files app's Scan Documents, and many other apps' "scan" buttons, look for AirScan-compatible scanners on the network automatically.
- Android: apps that support "network scan" / eSCL discovery (many scanning apps do) find it the same way.
Discovery needs mdns = yes (the default); without it, a client that lets you enter an address directly can use http://<server>:8000/eSCL/ as the eSCL root. Only one scanner is exposed this way — set escl_scanner in config.ini if the backend reports more than one and the wrong one gets picked.
Below the print button there's a Maintenance row for the selected printer:
| Action | Printers | What it does |
|---|---|---|
| Test page | any | the OS's standard test page (Win32_Printer.PrintTestPage / CUPS testprint). On Windows it only works when the server runs as administrator: Windows refuses it otherwise |
| Nozzle check | Canon inkjets | prints the nozzle check pattern — gaps mean the head needs cleaning |
| Head cleaning | Canon inkjets | cleans the print head (uses ink, ~1 minute) |
Every action asks for confirmation and is recorded in the job history.
The RU / EN switch in the top-right corner stores the choice in a cookie for a year. Until someone picks a language, the page is in English: defaultlang in config.ini. Set defaultlang = auto to follow each browser's language instead. Server messages (option names, errors) follow the page language.
To open the page in a language without changing the stored choice, add ?lang=en or ?lang=ru to the URL.
Paper names like "Plain Paper" / «Обычная бумага» come from the printer driver in the Windows language and can't be translated by the server.
Settings live in config.ini next to run.py. On first start it's created from config.example.ini, which documents every key. Edit config.ini, not the example. config.ini is excluded from git because it may contain passwords. Restart the server after changes.
defaultlang = en
port = 8000
ipp_port = 631
ipp_printer =
mdns = yes
escl = yes
escl_scanner =
ssl_certfile =
ssl_keyfile =
data_dir = data
scans_dir = scans
auth = none
ipp_auth = no
escl_auth = no
session_days = 30
[users]
# name = password| Key | Default | Meaning | Env. variable |
|---|---|---|---|
defaultlang |
en |
UI language until a user picks one with the RU / EN switch; also used for IPP jobs. en, ru, or auto = each browser's language, falling back to the OS language |
MFP_LANG |
port |
8000 |
web UI / API port | MFP_PORT |
ipp_port |
631 |
IPP printer port (Windows only); 0 disables IPP |
MFP_IPP_PORT |
ipp_printer |
(empty) | which printer the IPP endpoint prints to; empty = the OS default printer. By default virtual printers (PDF, XPS, Fax) and IPP printers pointing back at this server are skipped | MFP_IPP_PRINTER |
mdns |
yes |
advertise the IPP printer and/or eSCL scanner via Bonjour/mDNS (IPP: Windows only; eSCL: both OSes); no disables it |
MFP_MDNS |
escl |
yes |
AirScan/eSCL scanning endpoint at /eSCL/* (Windows and Linux); no disables it |
MFP_ESCL |
escl_scanner |
(empty) | which scanner the eSCL endpoint serves; empty = the scan backend's first one | MFP_ESCL_SCANNER |
escl_port |
80 |
extra port serving only /eSCL/*, for scanning apps where you type just an IP address (they look at http://<ip>/eSCL, port 80). If it's busy (IIS...) or needs root, the log says so and the server runs without it; 0 = off |
MFP_ESCL_PORT |
ssl_certfile, ssl_keyfile |
(empty) | certificate/key (PEM) for HTTPS on the web port; both must be set to enable it, see Security | MFP_SSL_CERTFILE, MFP_SSL_KEYFILE |
data_dir |
data |
job history and the session signing key; relative paths are from the project folder | MFP_DATA_DIR |
scans_dir |
scans |
where scans are stored | MFP_SCANS_DIR |
auth |
none |
none: open to everyone on the network; yes: sign-in required, see Users and sign-in |
MFP_AUTH |
ipp_auth |
no |
yes: IPP printing also requires a user name and password (HTTP Basic) |
MFP_IPP_AUTH |
escl_auth |
no |
yes: eSCL scanning also requires a user name and password (HTTP Basic) |
MFP_ESCL_AUTH |
session_days |
30 |
how long "Remember me" keeps you signed in | MFP_SESSION_DAYS |
Environment variables override the file. That's handy for one-off runs, e.g. $env:MFP_PORT = "8080"; .\start.ps1. Two more variables:
MFP_CONFIG: use another config file;MFP_RELOAD=1: development auto-reload, see Development.
With auth = yes every page, API call and scan download requires signing in. Only the login page and static files are open. Users go into the [users] section:
auth = yes
[users]
anna = pbkdf2_sha256$390000$...$...
john = plain-text-passwordPasswords can be plain text, but a hash is better:
.venv\Scripts\python run.py --hash-password # Linux: .venv/bin/python run.py --hash-passwordIt asks for the password twice and prints a line like pbkdf2_sha256$390000$...; paste it after name = . The server log warns about users with plain-text passwords.
In
config.ini,;or#preceded by a space starts a comment, so a plain-text password with " #" or " ;" in it gets cut. Hashes don't have this problem.
How it behaves:
- The browser shows a sign-in page. "Remember me" keeps you signed in for
session_days; without it the session ends when the browser closes. Sign out is in the top-right corner. - Sessions are signed cookies (HttpOnly, SameSite=Lax), signed with a random key in
data/secret.key. They survive server restarts. Changing a user's password or removing the user signs that user out everywhere. Deletingsecret.keysigns everyone out. - After 5 failed attempts in 5 minutes, that IP address is blocked for the rest of those 5 minutes. Failed attempts are logged.
- Scripts and
curlcan send HTTP Basic credentials instead of a cookie:curl -u anna:password http://192.168.1.20:8000/api/jobs
- API tokens are an alternative to putting your real password in a script: click 🔑 API tokens next to Sign out (once logged in) to generate one. The raw token is shown once — copy it then — and works as
Authorization: Bearer <token>instead of Basic:A token authenticates as whoever created it and can be revoked independently (from the same panel) without changing that user's password or logging out their browser session. Only its hash is stored, socurl -H "Authorization: Bearer <token>" http://192.168.1.20:8000/api/jobsdata/tokens.jsonleaking doesn't hand out a working credential.
IPP printing is not covered by auth: with auth = yes IPP stays open unless ipp_auth = yes. With ipp_auth = yes the printer requires HTTP Basic, and advertises it (uri-authentication-supported = basic):
- CUPS asks for the name and password, or they can be put into the URI:
ipp://anna:password@server:631/ipp/print; - Windows' built-in IPP driver and phones often can't send a password, so turn this on only if your clients support it.
eSCL scanning works the same way via escl_auth: auth = yes alone leaves /eSCL/* open, and macOS/iOS/Android's built-in scanning generally can't send a password either, so it's off by default.
Leave auth = none (the default) on a trusted home network where everyone may print and scan.
Admins and print quotas. An optional [admins] section names which users (from [users]) can manage everyone's monthly print quota:
[users]
anna = pbkdf2_sha256$390000$...$...
john = plain-text-password
[admins]
annaAnna gets a ⚙ Quotas panel (next to Sign out) listing every user, sheets printed this calendar month, and an editable monthly limit — blank means unlimited. John, not being an admin, instead sees his own usage next to his name (john · 42/100 sheets this month) but can't see or change anyone else's. A print that would exceed the limit is rejected up front (HTTP 402) before it reaches the printer — from the web UI, IPP, or the copy-a-scan endpoint alike. Usage is derived from the job history itself (not a separate counter), so it can never drift and naturally resets at the start of a month. Jobs with no known user (auth = none, or an anonymous IPP client) are never limited.
flowchart LR
Browser["Browser<br/>(PC / phone)"] -- "HTTP :8000<br/>upload, preview, scan" --> Web
Client["IPP client<br/>(Windows / CUPS / macOS)"] -- "IPP :631<br/>PDF · PWG Raster · JPEG" --> IPP
Scan["eSCL client<br/>(macOS / iOS / Android)"] -- "HTTP :8000/eSCL<br/>ScanJobs · NextDocument" --> ESCL
subgraph Server["MFP Print & Scan Server (FastAPI)"]
Web["Web UI + REST API"]
IPP["IPP Everywhere printer"]
ESCL["eSCL (AirScan) server"]
Layout["Shared page layout<br/>(fit / sheet / actual size)"]
PB["Print backend"]
SB["Scan backend"]
Web --> PB
IPP --> PB
Web --> SB
ESCL --> SB
PB --- Layout
end
PB -- "Windows: spooler, DEVMODE,<br/>PDFium → printer DC" --> Printer[("MFP<br/>USB")]
PB -- "Linux: lp / CUPS" --> Printer
SB -- "Windows: WIA (COM)<br/>Linux: SANE (scanimage)" --> Printer
Printing on Windows. For PDFs, images and IPP jobs the server opens a device context on the printer with a job-specific DEVMODE: paper size, paper type, quality, color, duplex. It then draws the pages itself (PDFium's FPDF_RenderPage into the DC, Pillow for bitmaps). Pages are placed in one of three ways, and the preview uses the same code:
- fit: into the printable area (uploads);
- sheet: page = physical sheet (IPP clients);
- actual size: scans/copies.
Other formats go through the printto shell verb. In that case the options are applied to the per-user default DEVMODE (SetPrinter level 9), which doesn't need admin rights.
Why not just use the driver's settings? Drivers expose options through DeviceCapabilities, but not the rules between them. The Canon driver happily accepts "envelope + A4" through every API (DocumentProperties, PrintTicket validation) and only its own dialog forbids it. Such rules live in app/printing/media_constraints.py as data per printer model.
Printer status on Linux is read by having the server talk IPP to CUPS's own IPP server on localhost:631 — the same protocol this project already speaks as a server on Windows, so app/ipp/protocol.py's encode/decode functions are reused as a tiny client instead of writing a second IPP implementation (or adding pycups, which needs the CUPS C headers at install time, or parsing lpstat's locale-dependent text). printer-state/printer-state-reasons/marker-levels are IPP keyword/enum attributes, so they come back in English regardless of the system's language.
IPP. app/ipp/protocol.py is a small RFC 8010 encoder/decoder. app/ipp/printer.py implements:
- the operations: Get-Printer-Attributes, Validate-Job, Print-Job, Create-Job/Send-Document, Get-Job-Attributes, Get-Jobs, Cancel-Job, Close-Job;
- capability mapping from the driver to IPP (PWG media names,
media-col-databasewith real margins).
app/printing/pwg.py decodes PWG Raster.
AirPrint's mDNS subtype. AirPrint doesn't browse plain _ipp._tcp like everything else — it specifically looks for the _universal subtype (_universal._sub._ipp._tcp.local.) and won't list a printer without a non-empty URF TXT record. The obvious way to add that — a second ServiceInfo for the same instance name, just under the subtype's type — collides in the installed zeroconf library with ServiceNameAlreadyRegistered, because its registry keys a registered service by name alone, not name+type (open upstream issue). mdns_util.py's Announcer works around it with a second, independent Zeroconf() instance for each subtype, registering the identical name/port/server — confirmed end-to-end with a real register-then-browse test rather than just reading the source, since a subtly wrong TXT/PTR here would silently just not work on an actual iPhone.
Scanning.
- Windows: WIA through COM, on one dedicated thread, since COM objects are apartment-bound and a flatbed does one scan at a time.
- Linux:
scanimage; options are parsed fromscanimage --all-options.
eSCL. app/escl/ implements the AirScan/eSCL HTTP+XML API on top of the same ScanBackend — the web UI's /api/scan* routes and the eSCL server share one backend instance, so its busy-lock actually serializes the physical scanner between a browser and a phone. POST /eSCL/ScanJobs returns immediately (a background thread does the scan); GET .../NextDocument blocks until it's done, then streams the image once. eSCL/Bonjour model one scanner per announced service, so escl_scanner in config.ini picks which one if the backend reports more than one (empty = the first).
A Linux quirk this surfaced. On a Linux machine with sane-airscan/escl installed (common on Ubuntu), turning on this server's own eSCL announcement means the machine's own SANE now discovers itself as a network scanner over Bonjour — so scanimage -L (used internally to list scanners) starts taking ~12s for its network-discovery timeout instead of ~1s, and can occasionally hit "device busy" if that self-discovery probes the real USB scanner at the same moment this server's own capabilities/scan calls do. linux_sane.py caches list_scanners() (never caching an empty result, so a real disconnect is still noticed promptly) and retries a couple of times on a transient open failure for the lightweight list/capabilities calls — not for an actual scan. Harmless duplicate entries for the same physical scanner (pixma:..., escl:http://..., airscan:...) can still show up in /api/scanners' list on such a machine; picking one of the network ones just adds a pointless network round trip back to this same server, not a bug, since a printer/scanner behind a genuine network hop wouldn't have this self-reference at all.
app/
main.py FastAPI app: pages, print/preview/jobs/maintenance API, IPP endpoint
scan_routes.py scanning API (web UI)
escl_routes.py eSCL (AirScan) HTTP endpoints
mdns_util.py shared zeroconf plumbing for ipp/mdns.py and escl/mdns.py
i18n.py all UI and server texts (ru/en)
preview.py print preview rendering
storage.py job history (JSON, atomic writes)
models.py API models
printing/
base.py PrintBackend interface
windows_print.py Windows backend (spooler, DEVMODE, PDFium/GDI, WMI, RAW jobs)
linux_cups.py Linux backend (lp, lpoptions)
convert.py documents/text → PDF via LibreOffice (both OSes), with a cache
layout.py page placement shared by printing and preview
media_constraints.py paper type ↔ size rules per printer model
maintenance.py test page / vendor maintenance jobs (Vendor plugin table, currently Canon IVEC+BJL)
pwg.py PWG Raster decoder
ipp/
protocol.py IPP binary encoding
printer.py IPP Everywhere printer
mdns.py Bonjour/mDNS advertisement (Windows)
scanning/
base.py ScanBackend interface
windows_wia.py WIA backend
linux_sane.py SANE backend
store.py saved scans, thumbnails, PDF merge
escl/
protocol.py eSCL XML: capabilities/status build, ScanSettings parse
server.py target scanner, job lifecycle, image encoding
mdns.py Bonjour/mDNS advertisement (Windows and Linux)
templates/index.html
static/ app.js (print tab), scan.js (scan tab), style.css
run.py starts the web (8000) and IPP (631) servers; --log-file for background use
start.bat / start.ps1 Windows launcher
start.sh Linux launcher
register_service.bat Windows: start with the system (Task Scheduler task + firewall rules)
unregister_service.bat Windows: remove it
scripts/ PowerShell behind the .bat files
deploy/ systemd unit for Linux
docs/ screenshots, development notes
The UI is a thin client over a JSON API; interactive docs are at http://<server>:8000/docs.
| Method | Path | Purpose |
|---|---|---|
| GET | /api/printers |
printers known to the OS |
| GET | /api/printers/{name}/options |
options from the driver (+ paper type ↔ size rules) |
| GET | /api/printers/{name}/maintenance |
available maintenance actions |
| GET | /api/printers/{name}/status |
live status (idle/printing/stopped/offline + reasons) and ink/toner levels, or null if the backend can't report them |
| POST | /api/printers/{name}/maintenance/{action} |
run test_page, nozzle_check, head_cleaning |
| POST | /api/print |
multipart: file, printer, copies, options (JSON object of option keys → values) |
| POST | /api/preview |
multipart: file, printer, options → PNG pages as data URLs |
| GET | /api/jobs |
job history |
| DELETE | /api/jobs/{id} · /api/jobs |
delete one entry · clear finished jobs |
| POST | /api/jobs/{id}/cancel |
pull a job back out of the OS print queue (409 if it has no queue id, or is likely already printed) |
| GET | /api/usage |
usage totals (jobs/sheets/sent/failed) plus per-user and per-printer breakdowns, over the full job history |
| GET | /api/usage.csv |
the full job history as a CSV download (one row per job: user, printer, pages, sheets, status, …) |
| GET | /api/scanners · /api/scanners/capabilities?scanner_id=… |
scanners and their capabilities |
| POST | /api/scan/preview |
preview of the whole glass |
| POST | /api/scan |
scanner_id, resolution, mode, x/y/width/height (mm), brightness/contrast (−100…100), format |
| GET | /api/scans · /api/scans/{name} · /api/scans/{name}/thumb |
list · download · thumbnail |
| DELETE | /api/scans/{name} |
delete a scan |
| POST | /api/scans/merge |
{"names": [...]} → one PDF |
| POST | /api/scans/{name}/print |
print a scan at actual size |
| GET | /api/qrcode.png |
QR code for this page's own URL |
| GET | /api/tokens |
the logged-in user's API tokens (name, created/last-used dates — never the token itself) |
| POST | /api/tokens |
{"name": "..."} → creates a token, returns it once (token field) |
| DELETE | /api/tokens/{id} |
revoke a token |
| GET | /api/quotas/me |
the logged-in user's own monthly limit and sheets used so far |
| GET | /api/quotas |
every user's limit and usage (admins only, see Admins and print quotas) |
| PUT | /api/quotas/{user} |
{"limit": 100} or {"limit": null} → set/clear a user's monthly limit (admins only) |
| POST | /ipp/print (port 631) |
IPP endpoint |
| GET | /eSCL/ScannerCapabilities · /eSCL/ScannerStatus |
eSCL (AirScan) capabilities/status, XML |
| POST | /eSCL/ScanJobs |
eSCL: create a scan job from a ScanSettings XML body → Location header |
| GET | /eSCL/ScanJobs/{id}/NextDocument |
eSCL: the scanned image (blocks until the scan is done) |
| DELETE | /eSCL/ScanJobs/{id} |
eSCL: cancel/discard a job |
Example:
curl -F file=@report.pdf -F "printer=Canon MG2500 series Printer" \
-F 'options={"paper_size":"A4","color":"mono","quality":"draft"}' \
http://192.168.1.20:8000/api/printSend ?lang=en|ru or an X-Lang header to get labels and errors in that language. With auth = yes, add -u name:password (HTTP Basic). Without credentials the API answers 401.
| Path | Contents |
|---|---|
config.ini |
settings and users (created from config.example.ini) |
data/secret.key |
random key that signs sign-in sessions; delete it to sign everyone out |
data/jobs.json |
job history (last 500). Written atomically; if it's ever corrupt, it's kept as jobs.json.broken and history starts empty |
uploads/ |
files uploaded for printing; removed when their history entry is deleted or cleared |
scans/ |
scans; scans/.meta/ holds thumbnails and metadata |
logs/server.log |
server log when running as a Windows service (on Linux the log goes to the journal) |
None of these are committed to git (see .gitignore).
By default (auth = none) there is no sign-in. Anyone who can reach the ports can print, scan, and see or delete history and scans. Turn on auth = yes if other people share your network.
Even with sign-in, the server is meant for a LAN:
- by default it speaks plain HTTP, so passwords and cookies aren't encrypted on the network unless you set
ssl_certfile/ssl_keyfile(below); - don't forward ports 8000/631 from your router, and don't expose the server to the internet.
HTTPS for the web UI. Set both in config.ini:
ssl_certfile = /path/to/fullchain.pem
ssl_keyfile = /path/to/privkey.pemRestart the server — the web port then serves HTTPS only (plain HTTP requests to it fail; the IPP port stays HTTP, IPP-over-TLS isn't implemented). Paths are relative to the project folder if not absolute. A self-signed certificate works (browsers just warn once); for a certificate no browser warns about, use your router's if it issues one for your LAN domain, a local CA (e.g. mkcert), or one from a service that supports your setup.
For remote access (outside your LAN) use a VPN, or a reverse proxy in front of the server.
Other notes:
- uploaded files are limited to 100 MB;
- scan file names are validated, so paths can't escape the scans folder;
- the server needs no admin rights on Windows.
Canon shows "Support code 4102: media type and paper size are not set correctly"
The printer rejected the combination of paper type and size, e.g. Glossy Photo Paper in 13×18. Press Stop on the printer or Cancel printing in the dialog. The server now fixes known-bad combinations automatically, see app/printing/media_constraints.py. If you hit a combination that isn't covered, please open an issue with the printer model, paper type and size.
CUPS: "Unable to create PPD: No IPP attributes"
CUPS got no answer to its capability query. Check, in order:
- The server is running and shows
Uvicorn running on http://0.0.0.0:631. - Port 631 is open in the Windows firewall (see Quick start).
- The URI is
ipp://<server>:631/ipp/print.
The server window closes / "port is already in use"
Another instance is already running (maybe from autostart), or something else uses port 8000/631. Close the other window, or pick other ports with port / ipp_port in config.ini.
"Python 3 not found" from start.bat
Install Python from python.org with "Add python.exe to PATH". The python command that opens Microsoft Store is only a stub; the launcher prefers py.exe anyway.
Moved the folder from Linux and it doesn't start
A .venv created on Linux doesn't work on Windows. Delete .venv and run start.bat again.
A job is stuck in "Queued"
Jobs only stay queued while they're printing. If the server was stopped mid-print, the job is marked "The server was restarted while printing" on the next start. Jobs sent to an unavailable printer fail immediately with an explanation.
Scanner not found / busy
Check that the scanner shows up in Windows Scan / Devices (WIA), or in scanimage -L on Linux. Only one scan runs at a time; a second one gets "The scanner is busy".
.docx prints with broken formatting
Install LibreOffice on the server and restart it: documents are then converted to PDF by LibreOffice and keep their formatting, with a preview. Without it, Windows prints .docx with WordPad (or Word, if installed), and WordPad loses formatting.
Another printer. Install it in the OS: it appears in the list with the options its driver reports, and IPP advertises its real paper sizes and margins. No code changes are needed.
Another scanner. Same story on the eSCL side: any scanner the backend (WIA/SANE) already lists works over AirScan too. If more than one is connected, set escl_scanner in config.ini to pick which one is served — eSCL/Bonjour only model one scanner per announced service.
Paper type ↔ size rules for a new model. Add an entry to RULES in app/printing/media_constraints.py, keyed by driver name. Use the DEVMODE media/paper ids; you can list them with DeviceCapabilities (DC_MEDIATYPES, DC_PAPERS).
Maintenance commands for other brands. app/printing/maintenance.py has a small Vendor plugin table (VENDORS) instead of hardcoding Canon: each entry is a match function (driver name → bool), which of the maintenance actions the brand supports, and a function building the raw job bytes for each. Add one entry for your brand and nothing else needs touching — the API and UI pick it up automatically. The hard part is getting the bytes right: printers ignore anything that doesn't match their own driver's exact framing (a "should be correct" plain-BJL command for Canon was silently ignored — see the module's docstring for why). The reliable way to get them is to capture a real job from the vendor's own driver: pause the printer's queue, trigger the action from the vendor's own utility, then read the held job's raw bytes (win32print.ReadPrinter on Windows; the spool file under /var/spool/cups/ on Linux) before releasing it — that's exactly how the Canon bytes here were captured. Full instructions are in the Vendor class docstring in app/printing/maintenance.py.
Translations. All texts are in app/i18n.py:
- keys
web.*are used by the page and its JavaScript; - the other keys are server messages.
To add a language, add it to LANGS and add a value for it to every entry.
python -m venv .venv
.venv\Scripts\activate # Linux: source .venv/bin/activate
pip install -r requirements.txt
set MFP_RELOAD=1 # PowerShell: $env:MFP_RELOAD="1"
python run.pyMFP_RELOAD=1 serves only the web port and reloads on changes in app/. On Windows, uvicorn's reloader occasionally keeps the old worker running; restart manually if changes don't show up.
Stack:
- backend: FastAPI, uvicorn, pywin32, pypdfium2, Pillow, zeroconf, qrcode;
- frontend: plain HTML/CSS/JS, no build step.
packaging/ has a real installer for each OS instead of "clone the repo and run a script":
| OS | Script | Needs | Output |
|---|---|---|---|
| Debian/Ubuntu | packaging/deb/build.sh |
dpkg-dev (sudo apt install dpkg-dev) |
dist/mfp-print-scan-server_<version>_all.deb |
| Fedora/RHEL/openSUSE | packaging/rpm/build.sh |
rpm-build (sudo dnf install rpm-build) |
dist/mfp-print-scan-server-<version>-1.noarch.rpm |
| Windows | packaging/windows/build-exe.ps1, then packaging/windows/mfp-print-scan-server.iss |
the project's .venv + pip install pyinstaller, Inno Setup 6 or 7 |
dist/mfp-print-scan-server-setup-<version>.exe |
All three read the version from the VERSION file at the repo root — bump that before building a release. Run the two .sh ones from the repo root (bash packaging/deb/build.sh); on Windows first build the standalone program with powershell -ExecutionPolicy Bypass -File packaging\windows\build-exe.ps1 (PyInstaller, spec in packaging/windows/mfp-server.spec; result: dist\mfp-server\mfp-server.exe with its _internal\ folder), then compile the installer with Inno Setup's ISCC.exe packaging\windows\mfp-print-scan-server.iss (or File → Open → Compile in the Inno Setup IDE).
The .deb/.rpm don't bundle Python or its dependencies — pip installing requirements.txt into the app's own .venv happens on the target machine, on first install (needs internet access once), exactly like the manual start.sh does. The Windows installer is the opposite: it ships mfp-server.exe with Python and every dependency inside, so the target PC needs neither Python nor internet access. What the installers do:
.deb/.rpm: install to/opt/mfp-print-scan-server, create a dedicatedmfpsystem user (in thelp/scannergroups for device access), install and enable the systemd service fromdeploy/mfp-print-scan-server.service, and start it —sudo apt install ./mfp-print-scan-server_*.deborsudo dnf install ./mfp-print-scan-server-*.rpmis then the whole setup. Removing the package (apt remove/dnf remove) stops the service but leavesconfig.ini,data/,scans/anduploads/in place; onlyapt purge/package erase drops the venv and themfpuser too, still never that data — see the comments inpackaging/deb/postrm/ the spec's%postun.- Windows
.exe: installs per-user (%LocalAppData%\MFP Print & Scan Server— the background task runs as the signed-in user, not SYSTEM, so it needs to write there without admin rights every time it starts), then runs the existingscripts\register_service.ps1(Scheduled Task + firewall rules) non-interactively —start.batand the service scripts runmfp-server.exewhen it's next to them, and.venv+run.pyotherwise.config.ini,data\,scans\,uploads\andlogs\live next to the.exe. Installing over an existing install stops the running server first and replaces the program, keepingconfig.iniand the data (and removes the old Python-based install's.venv/app\). Uninstalling runsscripts\unregister_service.ps1first, then removes the program files;config.ini,data\,scans\anduploads\are left behind the same way. Running from source with Python (see Quick start) works exactly as before.
The Windows installer is tested: the PyInstaller build (PyInstaller 6.22) compiled with Inno Setup 7.1, installed over the previous install and run as the autostart task on Windows 10 (see Tested hardware). The .deb/.rpm still need a real build-and-install run on Linux.
- Plain HTTP by default. Set
ssl_certfile/ssl_keyfileinconfig.inifor HTTPS (see Security), or use a reverse proxy — either way it's still meant for a LAN, not the open internet. - Bonjour/mDNS advertises the IPP printer on Windows only, matching where the IPP server itself runs; on Linux, share it through CUPS instead (which does its own mDNS via Avahi).
- AirPrint is Windows-only, for the same reason as Bonjour/mDNS above (it's the IPP server's own advertisement). Verified at the protocol level (registers correctly, resolves via a real
_universalsubtype browse, an actual Canon MG2500 on the same LAN advertises itself withURF=V1.4,CP1,W8,PQ4,SRGB24,RS600,FN3via CUPS for comparison) but not against a real iPhone/iPad — if "Add Printer" on iOS doesn't find it, share the printer through CUPS on a Linux box instead, which has mature, widely-used AirPrint support built in. - No OCR for scans.
- Documents without LibreOffice are printed by whatever program is registered for them, with no preview; install LibreOffice for exact results.
- Duplex: the MG2500 driver reports duplex, but the printer has no automatic duplexer, so the driver does manual duplex.
- Ink/toner levels aren't universal. They come from the driver (IPP marker levels on Linux; nothing generic on Windows yet), so a USB inkjet on a generic driver — like this project's own Canon MG2500 — won't show them, while many laser printers and network-aware drivers will. The status itself (idle/printing/error) is separate and works regardless.
- Usage report attribution is best-effort. The "user" column is only filled in when it's actually known: the logged-in web username (
auth = yes) or an IPP client's ownrequesting-user-name— anonymous web uploads and print-via-lp/CUPS jobs show as "unknown". Page counts likewise come frompage_count()(LibreOffice + PDFium) and areNone/blank for formats it can't inspect (e.g. raw PWG Raster sent by an IPP client), which the report treats as 0 sheets rather than guessing. - Not every job can be canceled. A job gets a queue id (needed for ⏹ Cancel) whenever the server hands it to CUPS's
lp(Linux) or draws it itself via GDI (Windows: PDFs, images, PWG Raster). A Windows document printed through whatever app is registered for its file type (no LibreOffice installed) goes throughShellExecuteinstead — the server never gets a handle back, so that one specific job can only be removed from history, not pulled out of the queue.
MIT.
- phpSane: inspiration for the scanning UI.
- gutenprint
commandtocanonand Canon's cnijfilter sources: the BJL command vocabulary. The exact job layout comes from the Canon Windows driver's own maintenance jobs. - PDFium via pypdfium2, Pillow, FastAPI, pywin32.
- PWG specifications: IPP Everywhere (PWG 5100.14), PWG Raster (PWG 5102.4), media names (PWG 5101.1).


