') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); })(); GitHub - mrzroot/printbridge: 🖨️ Silent local printing agent for web applications — direct PDF and thermal label printing · GitHub
Skip to content

Latest commit

History

3 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

PrintBridge

Silent local printing agent for web applications — print PDFs and thermal labels from any web app to any local printer.

License: MITPlatformPython

PrintBridge is a lightweight local agent that runs in your system tray. Any web application can send print jobs via HTTP to localhost, and PrintBridge prints them silently — no dialog boxes, no user interaction required.

Features

  • Silent PDF printing — print PDFs to any system printer without dialogs
  • TSPL label printing — native TSPL commands for thermal printers (TSC, Xprinter, Zebra, etc.)
  • QR codes — generate QR codes directly in TSPL labels
  • Unicode support — automatic bitmap rendering for non-ASCII text (Arabic, Chinese, etc.)
  • System tray icon — runs in the background with a minimal UI
  • Auto-start — launches on system startup (macOS LaunchAgent / Windows registry)
  • Cross-platform — works on macOS, Windows, and Linux
  • Configurable — customize port, CORS origins, label defaults, and branding via config.json
  • Raw TSPL mode — send arbitrary TSPL commands for full control

Quick Start

Install from source

git clone https://github.com/AnouarSbia/printbridge.git
cd printbridge
pip install -r requirements.txt
python agent.py

The agent starts on http://127.0.0.1:9120 and runs in your system tray.

Verify it's running

curl http://127.0.0.1:9120/api/status
# {"port":9120,"platform":"Darwin","status":"ok","tspl":true,"version":"1.0.0"}

Download pre-built binaries

Pre-built binaries are available on the Releases page:

  • Windows: PrintBridge-Setup.exe (installer) or PrintBridge.exe (standalone)
  • macOS: PrintBridge-Installer.dmg

No Python installation required for pre-built binaries.

API Reference

All endpoints accept and return JSON. The agent listens on 127.0.0.1 only (no external access).

GET /api/status

Health check — use this to detect if the agent is running.

Response:

{
"status": "ok",
"version": "1.0.0",
"platform": "Darwin",
"port": 9120,
"tspl": true
}

GET /api/printers

List available system printers.

Response:

{
"printers": ["HP LaserJet", "Xprinter XP-350"],
"default": "HP LaserJet"
}

POST /api/print

Print a PDF document silently.

Request:

{
"pdf": "<base64-encoded PDF bytes>",
"printer": "HP LaserJet",
"copies": 1,
"mediaWidth": 100,
"mediaHeight": 50
}
FieldTypeRequiredDescription
pdfstringYesBase64-encoded PDF data
printerstringNoPrinter name (default: system default)
copiesintNoNumber of copies (1-99, default: 1)
mediaWidthnumberNoLabel width in mm (for thermal printers)
mediaHeightnumberNoLabel height in mm (for thermal printers)

POST /api/print-labels

Print product labels using TSPL commands (thermal printers).

Request:

{
"labels": [
{
"productName": "Widget A",
"formuleName": "Standard",
"width": 100,
"height": 50,
"position": "Shelf 3",
"quantity": 2,
"orderRef": "#1234",
"clientName": "John Smith",
"measurementId": "M001",
"sig": "abc123"
}
],
"printer": "Xprinter XP-350",
"labelWidth": 100,
"labelHeight": 60,
"shopName": "My Shop",
"workOrderId": 42,
"qrBaseUrl": "https://example.com/label/",
"copies": 1,
"displayOptions": {
"showBarcode": false,
"showFormule": true,
"showClient": true,
"showOrder": true,
"showQr": true,
"showPosition": true,
"showShopHeader": true,
"fontScale": 2
}
}

POST /api/print-cut-labels

Print compact cut/manufacturing labels using TSPL.

Request:

{
"labels": [
{
"position": "Room 1",
"barInfo": "Bar 1 — Cut 3",
"profileName": "H",
"profileRef": "2RF21",
"cutName": "H",
"finishing": "RAL 7024",
"length": 1200,
"angles": "45/45",
"binNumber": 3,
"source": "production"
}
],
"printer": "Xprinter XP-350",
"labelWidth": 45,
"labelHeight": 35,
"orderRef": "1245",
"copies": 1
}

POST /api/print-tspl

Send raw TSPL commands directly to a printer. Full control for custom label formats.

Request:

{
"tspl": "SIZE 100 mm, 60 mm\r\nGAP 2 mm, 0 mm\r\nDIRECTION 0\r\nCLS\r\nTEXT 10,10,\"4\",0,1,1,\"Hello World\"\r\nQRCODE 400,10,M,6,A,0,\"https://example.com\"\r\nPRINT 1,1\r\n",
"printer": "Xprinter XP-350"
}

You can also pass TSPL as an array of command strings:

{
"tspl": [
"SIZE 100 mm, 60 mm",
"GAP 2 mm, 0 mm",
"DIRECTION 0",
"CLS",
"TEXT 10,10,\"4\",0,1,1,\"Hello World\"",
"PRINT 1,1"
],
"printer": null
}

POST /api/test-tspl

Send a simple test label to verify printer connectivity.

Request:

{
"printer": "Xprinter XP-350",
"labelWidth": 100,
"labelHeight": 150
}

Configuration

Edit config.json in the same directory as agent.py:

{
"port": 9120,
"cors_origins": [
"http://localhost:*",
"http://127.0.0.1:*",
"https://your-app.com"
],
"log_dir": "~/.printbridge",
"label_defaults": {
"width_mm": 100,
"height_mm": 150,
"dpmm": 8,
"gap_mm": 2
},
"footer_text": "PrintBridge",
"tray_name": "PrintBridge"
}

Environment Variables

VariableDefaultDescription
PRINTBRIDGE_PORT9120Override the port
PRINTBRIDGE_FOOTERPrintBridgeOverride footer text on labels
PRINTBRIDGE_TRAY_NAMEPrintBridgeOverride tray icon name

CORS

By default, PrintBridge only accepts requests from localhost. To allow your web app to connect, add your domain to cors_origins in config.json:

{
"cors_origins": [
"http://localhost:*",
"https://my-app.example.com",
"https://*.my-app.example.com"
]
}

Building from Source

Prerequisites

  • Python 3.10+
  • pip install -r requirements.txt

Windows

build_windows.bat

Produces:

  • dist\PrintBridge.exe — standalone executable
  • PrintBridge-Setup.exe — NSIS installer (if NSIS is installed)

macOS

./build_macos.sh

Produces:

  • dist/PrintBridge.app — macOS app bundle
  • dist/PrintBridge-Installer.dmg — DMG installer (if create-dmg is installed)

How It Works

┌──────────────┐ HTTP/JSON ┌───────────────┐ OS print ┌────────────┐
│ Your Web App │ ──────────────────▶│ PrintBridge │ ────────────────▶│ Printer │
│ (any domain) │ localhost:9120 │ (system tray) │ lp / win32print │ (any type) │
└──────────────┘ └───────────────┘ └────────────┘
  1. Your web app sends a POST request to http://127.0.0.1:9120/api/print (or /api/print-labels, /api/print-tspl)
  2. PrintBridge decodes the data and sends it to the specified printer
  3. The document prints silently — no dialog, no user interaction

PDF Printing

  • macOS/Linux: Uses lp command with optional media size for thermal printers
  • Windows: Uses SumatraPDF (if installed), falls back to PowerShell Start-Process, then ShellExecute

TSPL Label Printing

  • macOS/Linux: Uses lpr -o raw to send raw bytes
  • Windows: Uses win32print.WritePrinter with RAW data type

Integrating with Your Web App

JavaScript Example

// Check if PrintBridge is runningasyncfunctioncheckAgent(){try{constres=awaitfetch('http://127.0.0.1:9120/api/status');returnres.ok;}catch{returnfalse;}}// Print a PDFasyncfunctionprintPdf(pdfBase64,printerName){constres=awaitfetch('http://127.0.0.1:9120/api/print',{method: 'POST',headers: {'Content-Type': 'application/json'},body: JSON.stringify({pdf: pdfBase64,printer: printerName,copies: 1,}),});returnres.json();}// Print a custom TSPL labelasyncfunctionprintLabel(tsplCommands,printerName){constres=awaitfetch('http://127.0.0.1:9120/api/print-tspl',{method: 'POST',headers: {'Content-Type': 'application/json'},body: JSON.stringify({tspl: tsplCommands,printer: printerName,}),});returnres.json();}

Troubleshooting

ProblemSolution
Agent won't startCheck logs: ~/.printbridge/agent.log
Printer not detectedEnsure printer is installed in OS settings
Label is blankVerify label dimensions match your media
Port 9120 in useChange port in config.json or set PRINTBRIDGE_PORT env var
CORS errorAdd your domain to cors_origins in config.json
PDF won't print on WindowsInstall SumatraPDF for reliable silent printing
Arabic text not renderingInstall arabic-reshaper and python-bidi (included in requirements.txt)

Contributing

Contributions are welcome! Please read CONTRIBUTING.md for guidelines.

Areas we'd love help with:

  • New printer protocols (ZPL, EPL, CPCL)
  • Label template system (JSON-based or visual editor)
  • Web UI for configuration
  • Linux packaging (deb, rpm, AppImage)
  • Unit and integration tests

License

This project is licensed under the MIT License — see LICENSE for details.

Acknowledgments

  • Flask — web framework
  • pystray — system tray icon
  • Pillow — image processing for bitmap text rendering
  • pywin32 — Windows printer access
  • SumatraPDF — silent PDF printing on Windows

About

🖨️ Silent local printing agent for web applications — direct PDF and thermal label printing

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages