English | 中文
EasyPrint is a label printing solution that runs natively on Windows, consisting of two sub-projects:
| Sub-project | Directory | Language | Role |
|---|---|---|---|
| Server | src/EasyPrint/ |
C# .NET 8 WinForms | WebSocket service + rendering + printing |
| Client SDK | easyprint-js/ |
TypeScript | Browser-side WebSocket wrapper |
- Architecture
- Server Internals
- Client SDK Internals
- Full Data Flow
- Quick Start
- Protocol Reference
- Project Structure
┌─────────────────────────────────────────────────┐
│ Browser / Angular App │
│ │
│ ┌──────────────────────────────────────────┐ │
│ │ @easyprint/js (easyprint-js SDK) │ │
│ │ EasyPrintClient │ │
│ │ · WebSocket connection management │ │
│ │ · Exponential backoff auto-reconnect │ │
│ │ · Offline message queue │ │
│ │ · Event-driven API │ │
│ └──────────────┬───────────────────────────┘ │
│ │ ws://127.0.0.1:8765 │
└─────────────────┼───────────────────────────────┘
│ WebSocket (TCP)
┌─────────────────┼───────────────────────────────┐
│ EasyPrint Server (Windows WinForms) │
│ │ │
│ SuperSocket WebSocket Server │
│ │ │
│ JsonPackageConverter │
│ "PRINT {...}" → StringPackageInfo │
│ │ │
│ Command routing (PRINT/LIST/JOBS/CANCEL/...) │
│ │ │
│ ┌─────┴──────────────────────────────┐ │
│ │ PuppeteerSharp (Chromium) │ │
│ │ HTML → PDF (exact mm dimensions) │ │
│ └─────┬──────────────────────────────┘ │
│ │ │
│ ┌─────┴──────────────────────────────┐ │
│ │ PDFtoImage (SkiaSharp) │ │
│ │ PDF → Bitmap (native printer DPI) │ │
│ └─────┬──────────────────────────────┘ │
│ │ │
│ ┌─────┴──────────────────────────────┐ │
│ │ System.Drawing.Printing │ │
│ │ PrintDocument → printer driver │ │
│ └─────┬──────────────────────────────┘ │
│ │ │
│ PrinterHelper (WinSpool P/Invoke) │
│ · Paper size & DPI (<1ms) │
│ · Enumerate & control print queue jobs │
│ │
│ PrintSpoolerWatcher │
│ · FindFirstPrinterChangeNotification │
│ · Event-driven queue sync (zero polling) │
│ │
│ Form1 (WinForms UI) │
│ · Live print queue panel (Spooler sync) │
│ · Real-time log panel │
│ · Service configuration (IP/Port) │
└──────────────────────────────────────────────────┘
│
Physical printer (USB/Network)
Program.Main()
│
├─ AppDataContext.LoadConfig() Read cfg.json (IP, Port, AutoStart)
│
├─ WebSocketHostBuilder.Create() Build SuperSocket WebSocket host
│ ├─ UseSession<EasyPrintSession>()
│ ├─ UseHostedService<EasyPrintService>()
│ └─ UseCommand<StringPackageInfo, JsonPackageConverter>()
│ ├─ AddCommand<PRINT>()
│ ├─ AddCommand<LIST>()
│ ├─ AddCommand<JOBS>()
│ ├─ AddCommand<CANCEL>()
│ ├─ AddCommand<RESTART>()
│ ├─ AddCommand<PAUSE>()
│ └─ AddCommand<RESUME>()
│
├─ new Form1(cfg, host, loggerProvider)
│ ├─ SetupDataGrid() Bind BindingList<PrintQueueJob>
│ ├─ DownloadBrowserAsync() Background Chromium download
│ └─ OnShown
│ ├─ StartService() Start WebSocket service
│ ├─ LaunchAsync() Start Chromium
│ ├─ SyncPrintQueue() Initial full queue load
│ └─ PrintSpoolerWatcher.Start() Start event-driven queue watch
│
└─ Application.Run(form) Enter WinForms message loop
The server uses SuperSocket 2.x. The core is JsonPackageConverter:
// JsonPackageConverter.cs
public StringPackageInfo Map(WebSocketPackage package)
{
var arr = package.Message.Split(' ', 2, StringSplitOptions.RemoveEmptyEntries);
pack.Key = arr[0]; // "PRINT" / "JOBS" / ...
pack.Parameters = arr.Skip(1).ToArray(); // ["{...}"] (empty when no JSON body)
}Split(' ', 2) splits only at the first space, so HTML content with spaces is completely safe.
The PRINT command handler (Command/PRINT.cs) executes these steps:
① Receive PrintJob
├─ PrinterName empty → call PrinterHelper.GetDefaultPrinterName()
└─ WidthMm/HeightMm = 0 → call PrinterHelper.GetDefaultPaperSizeMm()
② HTML → PDF (PuppeteerSharp)
├─ Width/Height = exact mm dimensions
├─ PrintBackground = true (preserve background colors for barcodes)
└─ MarginOptions = 0 (zero margins)
③ PDF → Bitmap (PDFtoImage + SkiaSharp)
└─ Rasterize at native printer DPI
④ Bitmap → Printer (System.Drawing.Printing)
└─ Adaptive DPI scaling → centered output → printer driver
⑤ Send response to client
PrinterHelper uses WinSpool Win32 P/Invoke to read printer configuration and control the print queue directly.
Paper size & DPI reading (<1ms):
OpenPrinter() → DocumentProperties() → DEVMODE struct
dmPaperWidth/Length → paper size (0.1mm units)
dmPrintQuality/dmYResolution → native printer DPI
Print queue operations (EnumJobs / SetJob):
| Method | Description |
|---|---|
GetPrintJobs(printerName?) |
List all jobs in the specified printer queue |
CancelPrintJob(printer, jobId) |
Cancel (delete) a specific job |
RestartPrintJob(printer, jobId) |
Restart a job from the beginning |
PausePrintJob(printer, jobId) |
Pause a specific job |
ResumePrintJob(printer, jobId) |
Resume a paused job |
Event-driven queue monitoring (PrintSpoolerWatcher):
Uses FindFirstPrinterChangeNotification + WaitForMultipleObjects to register job change events for every installed printer. When a job is added (ADD_JOB), updated (SET_JOB), or removed (DELETE_JOB), the background thread immediately notifies the UI to sync — no polling needed.
Form1 provides the following features:
- Service config: Listen IP, port, start/stop service, startup-with-Windows toggle
- Chromium download: Background download of ChromeHeadlessShell on first run with status bar animation
- Live print queue panel:
- Data source:
BindingList<PrintQueueJob>, shows real-time Windows print queue PrintQueueJobimplementsINotifyPropertyChanged— row auto-refreshes on status change- Columns: Queue ID / Printer / Document / User / Pages / Status
- Filter: All / Printing / Error
- Action buttons: ⏸ Pause All / ▶ Resume All / ✕ Cancel All / ⚠ Clear Queue
- Data source:
- Log panel: Dark terminal-style
RichTextBoxwith color-coded log levels
Stored at {exe_dir}/data/cfg.json, auto-generated on first run:
{
"Ip": "Any",
"Port": 8765,
"AutoStart": true,
"MaxPackageLength": 1022886006,
"ReceiveBufferSize": 409600
}| Field | Description |
|---|---|
Ip |
Listen address; "Any" = all network interfaces |
Port |
Listen port |
AutoStart |
Auto-start WebSocket service when the window appears |
MaxPackageLength |
Max message size in bytes (~1 GB), supports large HTML payloads |
ReceiveBufferSize |
Receive buffer size |
| Library | Version | Purpose |
|---|---|---|
| SuperSocket | 2.0.2 | WebSocket server framework |
| SuperSocket.WebSocket.Server | 2.0.2 | WebSocket protocol support |
| PuppeteerSharp | 24.40.0 | Chromium control (HTML → PDF) |
| PDFtoImage | 5.2.0 | PDF → SKBitmap rasterization (PDFium-based) |
| SkiaSharp | — | Image processing (pulled in by PDFtoImage) |
| ReaLTaiizor | 3.8.1.4 | WinForms dark theme UI controls |
| Newtonsoft.Json | 13.0.3 | JSON serialization |
EasyPrintClient initiates a connection immediately on construction:
constructor()
└─ connect()
├─ new WebSocket(url)
├─ onopen → setState('connected') + flush pendingQueue
├─ onerror → setState('error') (onclose fires after)
└─ onclose → setState('disconnected')
└─ code !== 1000 && autoReconnect
└─ scheduleReconnect()
└─ setTimeout(connect, currentDelay)
└─ currentDelay = min(delay*2, maxDelay)
Exponential backoff sequence (default config):
1st disconnect → wait 1s → reconnect
2nd disconnect → wait 2s → reconnect
3rd disconnect → wait 4s → reconnect
...
6th+ disconnect → wait 30s → reconnect (cap)
Reconnect OK → reset to 1s
When any send method is called while disconnected, the message enters pendingQueue:
send() → [connected] → ws.send(message)
→ [disconnected] → pendingQueue.push(message)
emit('queued', message, queueLength)
onopen → while (pendingQueue.length > 0)
ws.send(pendingQueue.shift()) ← preserves send order
Built-in lightweight TypedEmitter (no Node.js dependency, works in all browsers):
| Event | Trigger | Parameters |
|---|---|---|
connecting |
Connection attempt started | — |
connected |
Connected, pending queue flushed | — |
disconnected |
Connection closed | code, reason |
error |
WebSocket error | Event |
response |
Server response received (all commands) | PrintResponse |
stateChange |
Connection state changed | ConnectionState |
queued |
Message queued due to disconnect | message, queueLength |
reconnecting |
Waiting to reconnect | delay, attempt |
Browser
│ client.print({ context: '<html>W0100025</html>', widthMm: 76, heightMm: 130 })
│ WS frame: PRINT {"printerName":"","context":"...","widthMm":76,"heightMm":130}
▼
SuperSocket → JsonPackageConverter → PRINT command handler
├─ HTML → PDF → Bitmap → printer driver
└─ Response: {"command":"PRINT","status":200,"data":"A3F1B2C0"}
│ client.jobs('TSC TE200')
│ WS frame: JOBS {"printerName":"TSC TE200"}
▼
JOBS command handler
├─ PrinterHelper.GetPrintJobs('TSC TE200')
└─ Response: {"command":"JOBS","status":200,"data":[{"jobId":1,...}]}
│ client.cancel([1,2], 'TSC TE200')
│ WS frame: CANCEL {"printerName":"TSC TE200","jobIds":[1,2]}
▼
CANCEL command handler
├─ PrinterHelper.CancelPrintJob('TSC TE200', 1)
├─ PrinterHelper.CancelPrintJob('TSC TE200', 2)
└─ Response: {"command":"CANCEL","status":200,"data":[{"jobId":1,"ok":true},{"jobId":2,"ok":true}]}
Requirements: Windows 10/11 · .NET 8 SDK · Windows GDI printing support
Download win-x64.zip from the Releases page, extract and run EasyPrint.exe.
On first launch, ChromeHeadlessShell (~50 MB) is downloaded automatically and cached for future use.
Or build from source:
cd src/EasyPrint
dotnet run # development
dotnet publish -c Release -r win-x64 --self-contained true # standalone exeThe configuration file is auto-generated at {exe_dir}/data/cfg.json on first run.
Install:
npm install @easyprint/jsBasic usage:
import { EasyPrintClient, PrintQueueJob, JobControlResult } from '@easyprint/js';
const client = new EasyPrintClient({ host: '127.0.0.1', port: 8765 });
client.on('connected', () => console.log('Connected'));
client.on('disconnected', (code) => console.log('Disconnected', code));
client.on('response', resp => {
switch (resp.command) {
case 'PRINT':
console.log('Print job submitted, ID:', resp.data);
break;
case 'JOBS':
const jobs = resp.data as PrintQueueJob[];
console.log('Queue jobs:', jobs);
break;
case 'CANCEL':
const results = resp.data as JobControlResult[];
results.forEach(r => console.log(`Job ${r.jobId}: ${r.ok ? 'cancelled' : 'failed'}`));
break;
}
});
// Print an HTML label
client.print({
context: '<html><body style="margin:0">W0100025</body></html>',
widthMm: 76,
heightMm: 130,
printerName: 'TSC TE200', // omit to use the server's default printer
});
// Query print queue
client.jobs('TSC TE200');
// Batch job control
client.pause([1, 2, 3], 'TSC TE200');
client.resume([1, 2, 3], 'TSC TE200');
client.cancel([4], 'TSC TE200');environment.ts:
export const environment = {
easyPrint: { host: '127.0.0.1', port: 8765 },
};print.service.ts:
import { Injectable, OnDestroy } from '@angular/core';
import { Observable } from 'rxjs';
import { EasyPrintClient, PrintResponse } from '@easyprint/js';
import { environment } from 'src/environments/environment';
@Injectable({ providedIn: 'root' })
export class PrintService implements OnDestroy {
private readonly client = new EasyPrintClient(environment.easyPrint);
readonly response$ = new Observable<PrintResponse>(observer => {
const handler = (resp: PrintResponse) => observer.next(resp);
this.client.on('response', handler);
return () => this.client.off('response', handler);
});
printLabel(html: string, widthMm = 76, heightMm = 130): void {
this.client.print({ context: html, widthMm, heightMm });
}
ngOnDestroy(): void { this.client.destroy(); }
}All commands follow the format: COMMAND <JSON body> or a bare command string (when no body is needed).
| Command | Send Format | data Type |
Description |
|---|---|---|---|
PRINT |
PRINT {PrintJob} |
string (job ID) |
Print HTML content |
LIST |
LIST |
PrinterInfo[] |
List installed printers |
JOBS |
JOBS {printerName?} |
PrintQueueJob[] |
Query print queue |
CANCEL |
CANCEL {printerName?, jobIds[]} |
JobControlResult[] |
Cancel jobs |
RESTART |
RESTART {printerName?, jobIds[]} |
JobControlResult[] |
Restart jobs |
PAUSE |
PAUSE {printerName?, jobIds[]} |
JobControlResult[] |
Pause jobs |
RESUME |
RESUME {printerName?, jobIds[]} |
JobControlResult[] |
Resume jobs |
{
"command": "PRINT",
"status": 200,
"message": "Print successful",
"data": "A3F1B2C0"
}| Field | Description |
|---|---|
command |
The command that triggered this response |
status |
200 success / 207 partial success / 400 bad request / 500 server error |
message |
Human-readable result description |
data |
Response payload; type varies by command |
| Field | Type | Default | Description |
|---|---|---|---|
printerName |
string | "" |
Printer name; empty = system default |
context |
string | required | Full HTML string to print |
widthMm |
number | 0 |
Label width in mm; 0 = auto-read from printer |
heightMm |
number | 0 |
Label height in mm; 0 = auto-read from printer |
paddingMm |
number[] | [0,0,0,0] |
Page padding in mm: top, right, bottom, left |
| Field | Type | Description |
|---|---|---|
printerName |
string? | Printer name; empty = default printer |
jobIds |
number[] | List of job IDs to operate on (one or more) |
EasyPrint/
├── src/
│ └── EasyPrint/ C# WinForms server
│ ├── Program.cs Entry point, registers all commands
│ ├── AppSettings.cs Config model (IP/Port/AutoStart)
│ ├── AppDataContext.cs Config file read/write
│ ├── EasyPrintService.cs SuperSocket service
│ ├── EasyPrintSession.cs WebSocket session (SendMessage)
│ ├── PrintJob.cs EasyPrint print job model
│ ├── PrintJobStatus.cs Job status enum
│ ├── PrintQueueJob.cs Windows queue job model (INotifyPropertyChanged)
│ ├── PrintResponseMessage.cs Response message model
│ ├── PrinterHelper.cs WinSpool P/Invoke (paper/DPI/queue R/W)
│ ├── PrintSpoolerWatcher.cs Event-driven print queue monitor
│ ├── UiLogger.cs Log routing to UI panel
│ ├── Form1.cs Main window business logic
│ ├── Form1.Designer.cs WinForms dark theme UI layout
│ └── Command/
│ ├── PRINT.cs PRINT command (HTML→PDF→print)
│ ├── LIST.cs LIST command (enumerate printers)
│ ├── JOBS.cs JOBS command (query print queue)
│ ├── CANCEL.cs CANCEL command (cancel jobs)
│ ├── RESTART.cs RESTART command (restart jobs)
│ ├── PAUSE.cs PAUSE command (pause jobs)
│ ├── RESUME.cs RESUME command (resume jobs)
│ ├── JsonCommandBase.cs Command base class (JSON deserialization)
│ └── JsonPackageConverter.cs WS packet → StringPackageInfo
│
├── easyprint-js/ TypeScript client SDK
│ ├── src/
│ │ ├── types.ts Type definitions
│ │ ├── client.ts EasyPrintClient implementation
│ │ └── index.ts Public exports
│ ├── dist/ Build output (ESM + CJS + .d.ts)
│ ├── package.json
│ ├── tsconfig.json
│ └── tsup.config.ts
│
├── README.md This document (English)
├── README.zh.md 中文文档
└── .gitignore