Chapter 10. Integration Guide & Code Samples
Chapter 10. Integration Guide & Code Samples
10.1 Integration patterns at a glance
| Need | How |
|---|---|
| A print button on a web page | Include /sdk/wxprint.js → wp.print({template, data, printer}) |
| Web preview-then-print | wp.preview({...}) → tray window (or /preview/{job_id}) |
| Backend document output | POST /api/print (template id + inline data or a data URL) + polling |
| Data fetched at print time | data: {url, headers} — the server fetches |
| Dynamic layout assembly | POST /api/script verb sequences (or the SDK’s wp.document()) |
| Batch shipping | Pass an array as data + copies_per_record |
| LAN print server | bind: "0.0.0.0" + token; every machine submits with the token |
| Archiving | POST /api/saveas (pdf/png/zip) or export from the preview window |
The repository’s examples/ directory contains runnable versions of
every sample (this chapter’s code comes from there):
examples/
├── websocket/ Browser demos (1 pure HTML / 2 overlay / 3 Twig / 4 feature gallery)
└── http/ Backend samples (python/php/nodejs/java/golang/
rust/c/cpp/csharp/ruby) + mock_api.py (URL data-source mock)
10.2 Quick verification with curl
# Is the service running?
curl -s http://127.0.0.1:8899/api/version
# List printers
curl -s http://127.0.0.1:8899/api/printers
# Submit a print (template id + inline data)
curl -s -X POST http://127.0.0.1:8899/api/print \
-H "Content-Type: application/json" \
-d '{"template": "invoice-a4", "data": {"no": "INV-001"},
"printer": "HP-LaserJet-M1005"}'
# Check status
curl -s http://127.0.0.1:8899/api/jobs/<job_id>
10.3 Python (stdlib urllib)
import json, time, urllib.request
BASE, TOKEN = "http://127.0.0.1:8899", "" # fill in when the service sets a token
def api(method, path, body=None, raw=False):
req = urllib.request.Request(BASE + path, method=method)
if TOKEN:
req.add_header("Authorization", f"Bearer {TOKEN}")
data = None
if body is not None:
data = json.dumps(body).encode()
req.add_header("Content-Type", "application/json")
with urllib.request.urlopen(req, data=data, timeout=30) as resp:
payload = resp.read()
return payload if raw else json.loads(payload)
# 1) List printers
printers = api("GET", "/api/printers")["printers"]
printer = next((p["name"] for p in printers if p["state"] == "ready"), "")
# 2) Submit: inline overlay template + inline data
template = {
"id": "demo-waybill-py", "name": "Python sample waybill", "kind": "overlay",
"page": {"width_mm": 100, "height_mm": 180},
"elements": [
{"type": "text", "id": "title", "x": 5, "y": 6, "w": 90, "h": 8,
"text": "Shipping label (submitted from Python)",
"font": {"family": "", "size_pt": 12, "weight": 700}},
{"type": "barcode", "id": "bc", "x": 5, "y": 20, "w": 90, "h": 14,
"symbology": "code128", "content": "{{no}}", "show_text": True},
{"type": "text", "id": "addr", "x": 5, "y": 40, "w": 90, "h": 16,
"text": "To: {{name}}\n{{addr}}", "font": {"family": "", "size_pt": 8}},
],
}
job = api("POST", "/api/print", {
"template": template,
"data": {"no": "PY-2026-0001", "name": "Alice",
"addr": "100 Wensan Road, Xihu District, Hangzhou"},
"printer": printer,
})
job_id = job["job_id"]
# 3) URL data form (fetched server-side; headers pass auth through)
job_url = api("POST", "/api/print", {
"template": {"url": "https://erp.example.com/tpl/order-label.json"},
"data": {"url": "https://erp.example.com/api/data/orders", # array = batch
"headers": {"Authorization": "Bearer erp-token"}, "timeout_s": 5},
})
# 4) Poll status
for _ in range(60):
st = api("GET", f"/api/jobs/{job_id}")
if st["status"] in ("done", "failed", "cancelled"):
break
time.sleep(0.5)
# 5) Register the template (inline submissions are not registered)
reg = api("POST", "/api/templates", template)
# 6) Preview PDF (no paper; reference by id)
pdf = api("POST", "/api/preview?format=pdf",
{"template": "demo-waybill-py", "data": {"no": "PY-2026-0002"}},
raw=True)
open("/tmp/wxprint-preview.pdf", "wb").write(pdf)
10.4 Node.js (built-in fetch, 18+)
const BASE = "http://127.0.0.1:8899";
const TOKEN = "";
async function api(method, path, body) {
const headers = { "Content-Type": "application/json" };
if (TOKEN) headers.Authorization = `Bearer ${TOKEN}`;
const resp = await fetch(BASE + path, {
method, headers,
body: body === undefined ? undefined : JSON.stringify(body),
});
if (!resp.ok) throw new Error(`HTTP ${resp.status}: ${await resp.text()}`);
return resp.json();
}
// Batch printing: an array = 3 labels fed continuously
const job = await api("POST", "/api/print", {
template: {
id: "demo-label-node", kind: "overlay",
page: { width_mm: 60, height_mm: 40 },
elements: [
{ type: "text", id: "sku", x: 3, y: 3, w: 54, h: 6,
text: "{{sku}}", font: { family: "", size_pt: 9, weight: 700 } },
{ type: "barcode", id: "bc", x: 3, y: 11, w: 54, h: 12,
symbology: "code128", content: "{{code}}", show_text: false },
],
},
data: [
{ sku: "Wireless mouse (black)", code: "SKU-10001", qty: 24, bin: "A-01-03" },
{ sku: "Wireless mouse (white)", code: "SKU-10002", qty: 18, bin: "A-01-04" },
{ sku: "Wired keyboard 87-key", code: "SKU-10003", qty: 12, bin: "B-02-01" },
],
printer: "",
});
for (let i = 0; i < 60; i++) {
const st = await api("GET", `/api/jobs/${job.job_id}`);
if (["done", "failed", "cancelled"].includes(st.status)) break;
await new Promise(r => setTimeout(r, 500));
}
10.5 Java (java.net.http.HttpClient, JDK 11+)
import java.net.URI;
import java.net.http.*;
import java.time.Duration;
public class PrintWaybill {
static final String BASE = "http://127.0.0.1:8899";
static final String TOKEN = "";
static final HttpClient HTTP = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(5)).build();
static String api(String method, String path, String json) throws Exception {
HttpRequest.Builder req = HttpRequest.newBuilder(URI.create(BASE + path))
.timeout(Duration.ofSeconds(30))
.header("Content-Type", "application/json");
if (!TOKEN.isEmpty()) req.header("Authorization", "Bearer " + TOKEN);
req.method(json == null ? "GET" : method,
json == null ? HttpRequest.BodyPublishers.noBody()
: HttpRequest.BodyPublishers.ofString(json));
HttpResponse<String> resp = HTTP.send(req.build(),
HttpResponse.BodyHandlers.ofString());
if (resp.statusCode() >= 400)
throw new RuntimeException("HTTP " + resp.statusCode() + ": " + resp.body());
return resp.body();
}
public static void main(String[] args) throws Exception {
// Submit: inline overlay template (prefer Jackson/Gson POJOs in production)
String body = """
{ "template": {
"id": "demo-waybill-java", "kind": "overlay",
"page": { "width_mm": 100, "height_mm": 100 },
"elements": [
{ "type": "text", "id": "title", "x": 5, "y": 5, "w": 90, "h": 8,
"text": "Express waybill (submitted from Java)",
"font": { "family": "", "size_pt": 12, "weight": 700 } },
{ "type": "barcode", "id": "bc", "x": 5, "y": 18, "w": 90, "h": 14,
"symbology": "code128", "content": "{{no}}", "show_text": true },
{ "type": "qrcode", "id": "qr", "x": 62, "y": 38, "w": 32, "h": 32,
"content": "{{url}}" } ] },
"data": { "no": "JAVA-2026-0001", "url": "https://example.com/t/1" },
"printer": "" }
""";
System.out.println(api("POST", "/api/print", body));
}
}
10.6 Go (stdlib net/http)
func api(method, path string, body any) (string, error) {
var rd io.Reader
if body != nil {
b, _ := json.Marshal(body)
rd = bytes.NewReader(b)
}
req, _ := http.NewRequest(method, "http://127.0.0.1:8899"+path, rd)
req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil { return "", err }
defer resp.Body.Close()
b, _ := io.ReadAll(resp.Body)
if resp.StatusCode >= 400 {
return "", fmt.Errorf("HTTP %d: %s", resp.StatusCode, b)
}
return string(b), nil
}
// Inline html+twig template (entry = a Twig string rendered server-side)
job, err := api("POST", "/api/print", map[string]any{
"template": map[string]any{
"id": "demo-report-go", "kind": "html",
"entry": `<h2>Inventory daily report ({{date}})</h2>
<table border="1" cellpadding="4" style="border-collapse:collapse;width:100%">
<tr style="background:#eee"><th>Warehouse</th><th>SKUs</th></tr>
{% for w in warehouses %}<tr><td>{{w.name}}</td><td>{{w.sku_count}}</td></tr>{% endfor %}
</table>`,
"page": map[string]any{"width_mm": 210, "height_mm": 297,
"margin_mm": map[string]float64{"top": 15, "bottom": 15, "left": 14, "right": 14}},
},
"data": map[string]any{"date": "2026-10-24",
"warehouses": []map[string]any{{"name": "Hangzhou WH1", "sku_count": 1024}}},
})
10.7 PHP (cURL extension)
function api(string $method, string $path, ?array $body = null) {
$ch = curl_init("http://127.0.0.1:8899" . $path);
curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => $method,
CURLOPT_TIMEOUT => 30]);
$headers = ['Content-Type: application/json'];
if (TOKEN !== '') { $headers[] = 'Authorization: Bearer ' . TOKEN; }
if ($body !== null) {
curl_setopt($ch, CURLOPT_POSTFIELDS,
json_encode($body, JSON_UNESCAPED_UNICODE));
}
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
$resp = curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($code >= 400) { fwrite(STDERR, "HTTP $code: $resp\n"); exit(1); }
return json_decode($resp, true);
}
$job = api('POST', '/api/print', [
'template' => 'invoice-a4',
'data' => ['no' => 'INV-2026-1042', 'amount' => 1280.50],
'printer' => 'HP-LaserJet-M1005',
]);
10.8 C# (.NET 8, zero NuGet dependencies)
using System.Net.Http;
using System.Net.Http.Json;
using System.Text.Json;
using var http = new HttpClient { BaseAddress = new Uri("http://127.0.0.1:8899") };
var body = new {
template = new {
id = "demo-label-csharp", kind = "overlay",
page = new { width_mm = 60, height_mm = 40 },
elements = new object[] {
new { type = "text", id = "title", x = 3, y = 3, w = 54, h = 6,
text = "{{title}}", font = new { family = "", size_pt = 8, weight = 700 } },
new { type = "barcode", id = "bc", x = 3, y = 11, w = 54, h = 12,
symbology = "code128", content = "{{isbn}}", show_text = false },
},
},
data = new { title = "Programming Rust (2nd ed.)", isbn = "978-7-111-00000-0" },
};
var resp = await http.PostAsJsonAsync("/api/print", body);
resp.EnsureSuccessStatusCode();
var job = await resp.Content.ReadFromJsonAsync<JsonElement>();
Console.WriteLine($"job_id={job.GetProperty("job_id")}");
10.9 Ruby (stdlib net/http)
require 'net/http'
require 'json'
def api(method, path, body = nil)
uri = URI("http://127.0.0.1:8899#{path}")
req = Net::HTTP.const_get(method.capitalize).new(uri)
req['Content-Type'] = 'application/json'
req.body = JSON.generate(body) if body
Net::HTTP.start(uri.host, uri.port) { |http| http.request(req).body }
end
# 58mm receipt: inline html template (entry = a Twig string)
job = JSON.parse(api('post', '/api/print', {
'template' => {
'id' => 'demo-receipt-ruby', 'kind' => 'html',
'entry' => <<~HTML,
<div style="font-family:monospace">
<h3 style="margin:0">{{shop}}</h3>
{% for r in rows %}<div>{{r.name|escape}} × {{r.qty}} … {{r.price}}</div>{% endfor %}
<div style="text-align:right"><b>Total: {{total}}</b></div>
</div>
HTML
'page' => { 'width_mm' => 58, 'height_mm' => 120,
'margin_mm' => { 'top' => 4, 'bottom' => 4, 'left' => 4, 'right' => 4 } }
},
'data' => { 'shop' => "Sample convenience store",
'rows' => [{ 'name' => 'Coffee', 'qty' => 2, 'price' => 12 }],
'total' => 24 },
}))
10.10 C / C++ / Rust
Samples for all three live in examples/http/{c,cpp,rust}/ (libcurl /
pure std); the core is the same task: POST JSON to /api/print, parse
the job_id, poll /api/jobs/{id}. Rust sketch (std only):
// Submit → parse job_id (JSON assembly/parsing omitted; see examples/http/rust)
let resp = http_post("http://127.0.0.1:8899/api/print", &body_json)?;
// Poll
loop {
let st = http_get(&format!("http://127.0.0.1:8899/api/jobs/{job_id}"))?;
if st.contains("\"done\"") || st.contains("\"failed\"") { break; }
std::thread::sleep(std::time::Duration::from_millis(500));
}
10.11 Web integration
Option 1: the JS SDK (recommended)
<script src="http://127.0.0.1:8899/sdk/wxprint.js"></script>
<script>
const wp = new wxprint.WxPrint({ token: "T" });
wp.onJob(ev => updateProgress(ev.page_done, ev.total_pages));
document.getElementById("print-btn").onclick = async () => {
// Preview form: the tray pops the native preview window on done;
// the user confirms and it goes to paper
await wp.preview({ template: "invoice-a4",
data: { url: "/api/invoice/2026-1042" } });
// Direct form: submitted and printed immediately (high-volume
// waybill/receipt scenarios)
// await wp.print({ template: "sf-waybill-100x93", data: {...},
// printer: "HP-LaserJet-M1005" });
};
</script>
Option 2: raw WebSocket (see the example in Section 5.4; you implement reconnection yourself — the SDK has it built in).
Popup-blocker note: if the preview confirmation page uses
window.open, it must be called synchronously inside the user gesture
(the click) — opening inside an async callback gets blocked. Open
about:blank synchronously with a “generating…” placeholder, then set
win.location.href to /preview/{job_id} when done.
10.12 The adaptive connection convention
The samples and demo pages follow one convention; business integrations are encouraged to adopt it:
- Environment variables
WXPRINT_BASE/WXPRINT_TOKEN(explicit); ~/.wxprint/config.json(adapts automatically to the bind/token the tray configured;0.0.0.0resolves to127.0.0.1);- Default
http://127.0.0.1:8899anonymous.
The browser demos additionally accept URL parameters ?host=&token=
as an explicit override.
10.13 Migrating from Lodop
No automatic migration tool; a complete verb-mapping matrix ships in
docs/lodop-parity.md:
| Lodop | WxPrint |
|---|---|
PRINT_INIT / PRINT_INITA |
init + pagesize |
ADD_PRINT_TEXT/HTM/TABLE/BARCODE/QRCODE/IMAGE/RECT/LINE |
add_text / add_html / add_table / add_barcode / add_qrcode / add_image / add_rect / add_line |
SET_PRINT_PAGESIZE (0.1mm units) |
pagesize (mm units; coordinates convert directly) |
SET_PRINT_STYLE(A) |
set_style / set_stylea |
NEWPAGE |
newpage |
| ItemType 1/2/3/4 (header/footer/first/last) | element scope |
| PageNumber | {{page}} / {{pages}} |
PRINT / PRINTA / PRINT_PREVIEW / SAVEAS |
print / printer selection in the preview / preview / saveas (the element-level tweaks of PRINT_SETUP are a planned item) |
ADD_PRINT_SETUP_BKIMG + SET_SHOW_MODE "BKIMG_PRINT" |
template background.image + background.print |
Coordinates convert directly from 0.1mm to mm; field tweaks made in
PRINT_SETUP use a private format and need redoing in the new system
(template-level calibration_offset covers the common cases).