Standalone IPP-over-HTTP server that receives print jobs, converts pages to PNG, and POSTs them to a HTTPS endpoint.
This lets you use your ePaper device like a regular printer and allows printing from anywhere.
Used in paperlesspaper, the Open Source eInk picture frame.
cd /path/to/paperlesspaper-print
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
# Optional for a native install. The Docker image already includes both.
brew install ghostscript cups-filters
cp .env.example .env
# edit .env
python server.pycurl -vk http://127.0.0.1:8631/ipp/print
Server defaults to http://0.0.0.0:8631/ipp/print.
- The server advertises IPP 1.1/2.0 driverless capabilities and supports the operations used by macOS, the Microsoft IPP Class Driver, and CUPS.
- PDF and JPEG jobs work out of the box. PWG Raster requires CUPS filters and PostScript requires Ghostscript; both are included in the Docker image.
- For internet exposure, run behind a reverse proxy (Caddy/Nginx) for TLS.
- If clients send
Expect: 100-continue, keepIPP_SEND_EXPECT_CONTINUE=falseunless you have verified your proxy path handles an origin-generated100 Continuecorrectly. - Set
IPP_SHARED_TOKENif you want a simple shared-secret header gate. - The first rendered PNG is also written to
IPP_TEMP_DIR(default./temp). - Optional auto-restart on crash:
AUTO_RESTART=trueAUTO_RESTART_DELAY_SECONDS=2AUTO_RESTART_MAX=0(0 = unlimited)
Always use the complete per-printer URL when one is supplied. CUPS and macOS use the IPP service URI form:
ipps://print.example/ipp/print/<paper-id>/<token>
Windows directed discovery uses the HTTPS transport form of the same endpoint:
https://print.example/ipp/print/<paper-id>/<token>
The token is a credential. Do not paste it into public logs or screenshots. The server redacts it from new request-path log entries.
Deploy the current Docker image first. Windows caches failed capability discovery, so remove any queue that is stuck at Druckerdaten werden abgerufen... before adding it again.
In an elevated PowerShell window:
Get-Printer -Name "paperlesspaper" -ErrorAction SilentlyContinue | Remove-Printer
Restart-Service Spooler
Add-Printer -Name "paperlesspaper" -IppURL "https://print.example/ipp/print/<paper-id>/<token>"
Get-Printer -Name "paperlesspaper" | Format-List Name,DriverName,PortName,PrinterStatusThe driver should be Microsoft IPP Class Driver. A generated WSD-... port
name is normal for Windows directed discovery. You can also use Settings →
Bluetooth & devices → Printers & scanners → Add device → Add manually, choose
an IPP printer, and enter the same full HTTPS URL. Add-Printer -IppURL rejects
the ipps:// form before contacting the server on affected Windows versions.
For Windows-side diagnostics, enable and inspect the print service event log:
wevtutil sl Microsoft-Windows-PrintService/Operational /e:true
Get-WinEvent -LogName Microsoft-Windows-PrintService/Operational -MaxEvents 100 |
Format-List TimeCreated,Id,LevelDisplayName,MessageInstall CUPS client tools, then create a driverless IPP Everywhere queue:
sudo lpadmin -p paperlesspaper -E \
-v 'ipps://print.example/ipp/print/<paper-id>/<token>' \
-m everywhere
lpstat -p paperlesspaper -l
lp -d paperlesspaper document.pdfFor troubleshooting, ipptool can query the same endpoint directly:
ipptool -tv \
'ipps://print.example/ipp/print/<paper-id>/<token>' \
/usr/share/cups/ipptool/get-printer-attributes.testYou can add a printer that targets the server:
- System Settings → Printers & Scanners → Add Printer…
- Use IP address / URL:
ipps://print.example/ipp/print/<paper-id>/<token>
Driver: prefer AirPrint or a generic IPP/PDF-capable driver if prompted.
If you use a Generic PostScript / plain IPP driver on macOS, install Ghostscript on the server host so PostScript jobs can be rendered directly to PNG. AirPrint/PDF-capable queues do not need that extra dependency.
If POST_ENDPOINT is set, the server sends a single multipart/form-data POST containing only the first page rendered as PNG by default.
If POST_SEND_ALL_PAGES=true, it instead sends one multipart/form-data POST containing all rendered pages, in page order.
If POST_ENDPOINT is empty, the server runs in store-only mode (no upload).
- optional fields (disabled by default):
job_id,request_id,total_pages,document_format,job_name,printer_uri,user - optional field for single-page uploads:
page - file: field name configurable via
POST_FILE_FIELD(default:file); withPOST_SEND_ALL_PAGES=true, the multipart body repeats that same file field once per page
Configured via .env:
IPP_SEND_EXPECT_CONTINUEPOST_ENDPOINTPOST_AUTH_HEADER+POST_AUTH_VALUEPOST_FILE_FIELDPOST_INCLUDE_META_FIELDSPOST_SEND_ALL_PAGES
To enable uploading, set POST_ENDPOINT (e.g. https://api.memo.wirewire.de/print/).
To send the rendered PNG to uploadSingleImage/<paperId>, you can either:
- set a base endpoint and let the server append
/<paperId>automatically:POST_ENDPOINT= https://api.memo.wirewire.de/v1/papers/uploadSingleImagePAPER_ID=<paperId>
or
- include a placeholder in
POST_ENDPOINT:POST_ENDPOINT= https://api.memo.wirewire.de/v1/papers/uploadSingleImage/<paperId>PAPER_ID=<paperId>
Full example:
POST_AUTH_HEADER=x-api-keyPOST_AUTH_VALUE=<token>POST_FILE_FIELD=picturePOST_INCLUDE_META_FIELDS=falsePOST_SEND_ALL_PAGES=false
The server accepts optional per-request overrides for PAPER_ID and POST_AUTH_VALUE via the /ipp/print URL.
This lets you configure different printers (or different printer entries) to upload to different Paper IDs or use different tokens.
It also recovers those values from the IPP printer-uri attribute when a client or reverse proxy sends the HTTP request to the base /ipp/print path.
- Path segments:
ipp://<your-host>:8631/ipp/print/123(setspaper_id=123)ipp://<your-host>:8631/ipp/print/123/TOKEN(setspaper_id=123,auth_value=TOKEN)
- Query params (not recommended, since MacOS strips these when using as clickable URL):
ipp://<your-host>:8631/ipp/print?paper_id=123&auth_value=TOKEN
paper_id is applied to POST_ENDPOINT in two ways:
- If
POST_ENDPOINTcontains one of these placeholders, it is replaced:<paperId>,{PAPER_ID},{paper_id}. - Otherwise, if
paper_idis set, it is appended as the final path segment:POST_ENDPOINT.rstrip('/') + '/' + paper_id.
The server always advertises these two selectable device sizes during IPP discovery:
- Open Paper L (13.3 inch) — 202.69×270.26 mm, 1200×1600 pixels
- Open Paper L (13.3 inch) – Randlos — 202.69×270.26 mm, 1200×1600 pixels, zero margins
- OpenPaper 7 (7.3 inch) — 95.40×159.00 mm, 480×800 pixels
- OpenPaper 7 (7.3 inch) – Randlos — 95.40×159.00 mm, 480×800 pixels, zero margins
The normal variants advertise a 3 mm layout margin. The borderless variants
advertise zero margins on all four sides and use a .borderless media name so
CUPS/macOS can group them with their named base size.
When a client submits media or media-col, the selected entry controls the
exact output canvas. The server also preserves the selection across the
Create-Job/Send-Document workflow used by macOS and some Windows clients.
IPP_TARGET_PROFILES is optional. It can set the default size and fitting
options for a paper ID while both built-in device sizes remain selectable:
IPP_RENDER_DPI=150
IPP_TARGET_PROFILES={"paper-id-one":{"width":1200,"height":1600,"fit":"contain"},"paper-id-two":{"width":480,"height":800,"fit":"contain","auto_rotate":false,"background":"#ffffff"}}Profile options:
widthandheightare required positive pixel dimensions.fit=containis the default and preserves the complete page, adding the configured background where needed.fit=coverfills the display and center-crops overflow.fit=stretchfills the display without cropping but can distort the page.auto_rotate=falseis the default because macOS, Windows, and CUPS normally submit data that already reflects the print-dialog orientation. Set it totrueonly for unrotated source images that should be matched heuristically to the target aspect ratio.background=#ffffffis the default padding color forcontain.- A profile keyed by
"*"is used as an optional fallback when no exact paper ID matches.
The built-in physical media dimensions are calculated from each display's nominal diagonal and pixel aspect ratio. Open Paper L is therefore advertised as 202.69×270.26 mm (13.3 inches diagonally) and OpenPaper 7 as 95.40×159.00 mm (7.3 inches diagonally). Final server-side normalization still guarantees the exact selected pixel size.
Windows, macOS, and CUPS cache printer capabilities. Remove and re-add an existing printer after changing its profile so the print dialog receives the new custom media size.
Uploads the newest PNG from IPP_TEMP_DIR (default ./temp) using the same POST_* settings:
python tools/upload_latest_png.pyThis repo includes a Dockerfile and fly.toml suitable for running on Fly.io.
- Install flyctl and log in:
brew install flyctl
fly auth login- Create the app (you can let Fly pick a name):
fly launch- Configure secrets/env (examples):
fly secrets set \
POST_ENDPOINT='https://example.com/print/' \
POST_AUTH_HEADER='x-api-key' \
POST_AUTH_VALUE='TOKEN'
# Optional: shared-secret for inbound print jobs
fly secrets set IPP_SHARED_TOKEN='SOME_SHARED_TOKEN'- Deploy:
fly deployUse an IPPS URL (TLS terminates at Fly):
ipps://<your-app>.fly.dev/ipp/print
If you use IPP_SHARED_TOKEN, configure your client to send X-IPP-Token.
AGPL-3.0. See LICENSE.




