Raw printing and LPR
Any print job sent as it is to a network printer, on port 9100 or through an LPR/LPD queue, with timeouts, retries and the printer's reply read back, and a check that a printer answers.
Version 1.0.0 · 7 functions · 1 crate
Functions
What use raw_print.qwer gives a program, with the types Quillo sees. To take only some, or under other names: use raw_print_file, raw_print_text from raw_print (Quillo 0.6).
raw_print_file(path: str, host: str, options: any) → anyor an errorSends a file to a printer's raw port as it is: host or host:port. options: { port: 9100, timeout_ms: 5000, retries: 2, retry_delay_ms: 1000, read_reply_ms: 0 (wait that long for an answer, for status questions) }. Gives { bytes, attempts, ms, printer, reply }. A retry sends the whole job again, so a job that failed half-way may print twice.
raw_print_text(text: str, host: str, options: any) → anyor an errorSends text (ZPL, EPL, TSPL, PJL...) to a printer's raw port, as raw_print_file does.
raw_print_base64(data: str, host: str, options: any) → anyor an errorSends bytes given in base64 (a job another plugin made) to a printer's raw port, as raw_print_file does.
raw_print_reachable(host: str, port: int, timeout_ms: int) → anyWhether a printer takes connections on port (9100 raw, 515 LPD, 631 IPP), and how long it took: { open, ms, error }. Nothing is printed.
raw_print_lpr_control(options: any) → stror an errorThe LPR control file a job would send, for checking it: options as lpr_print.
raw_print_lpr(path: str, host: str, queue: str, options: any) → anyor an errorPrints a file through an LPD print server or printer (port 515) on queue queue (often "raw", "lp", "PASSTHRU" or the printer's name). options: { user, name (the job's), copies, format: "raw" | "text" | "postscript", banner, job (a number 0-999), timeout_ms, retries }. Gives { bytes, attempts, queue, job }.
raw_print_lpq(host: str, queue: str, long: bool, timeout_ms: int) → stror an errorThe state of an LPD queue as the server writes it (long: the long form): the jobs waiting, or that it is empty.
Get it
$ quillo install raw_print # raw_print.qwer into this folder$ quillo install raw_print --example # and its example, raw_print.qweOr with curl:
$ curl -O https://quillolang.org/plugins/files/raw_print.qwer$ curl -O https://quillolang.org/plugins/files/raw_print.qwe$ curl -O https://quillolang.org/plugins/files/raw_print_sample.zplThen, in your program, all of it or only what you call:
use raw_print.qweruse raw_print_file, raw_print_text from raw_print // Quillo 0.6: only theseIt uses this crate, which cargo fetches the first time the plugin is built:
base64 = "0.22"Example
// Sending jobs to network printers as they are: a ZPL file to a label printer's port 9100 with// retries, a status question whose answer is read back, a check that the printer answers at all,// and the LPR control file a job to a print server's queue would carry.//// quillo run raw_print.qwe then open http://localhost:8080/lpr// PRINTER_HOST=192.168.1.60 quillo run raw_print.qwe and http://localhost:8080/printuse "raw_print.qwer"route GET /print { let host = env("PRINTER_HOST", null) ?? reply 503 "set PRINTER_HOST to the printer's address (host or host:port)" let sent = raw_print_file("raw_print_sample.zpl", host, { timeout_ms: 3000, retries: 2, retry_delay_ms: 500 })? // Zebra's host status question: three lines come back. let status = raw_print_text("~HS", host, { read_reply_ms: 500 })? { bytes: sent.bytes, attempts: sent.attempts, status_lines: len(split(trim(status.reply), "\n")) }}route GET /check { let host = env("PRINTER_HOST", null) ?? "127.0.0.1" { printer: raw_print_reachable(host, 9100, 1000).open, nothing_there: raw_print_reachable("127.0.0.1:1", 0, 500).open }}route GET /lpr = respond(200, raw_print_lpr_control({ user: "warehouse", name: "Pick list 7", client: "wms01", copies: 2, job: 42 })?, { "content-type": "text/plain" })route GET /fails = { error: raw_print_text("x", "127.0.0.1:1", { timeout_ms: 300, retries: 1, retry_delay_ms: 100 }).error }$ quillo run raw_print.qwe$ curl 'localhost:8080/print'$ curl 'localhost:8080/lpr'$ curl 'localhost:8080/fails'$ curl 'localhost:8080/check'It talks to the Redis that REDIS_URL names, else Redis on this machine. The site's test runs it against a stand-in.
Source
//! Print jobs sent as they are to network printers: raw TCP on port 9100 (JetDirect, AppSocket) and LPR/LPD (RFC 1179, port 515) with queues, job names and the queue's state, each with timeouts and retries, the printer's reply read back when it gives one, and a check that a printer answers at all. For ZPL, EPL, PCL, PDF, ESC/POS or any file a printer takes.//! version: 1.0.0//!//! [dependencies]//! base64 = "0.22"use serde_json::{json, Value};use std::io::{Read, Write};use std::net::{SocketAddr, TcpStream, ToSocketAddrs};use std::time::{Duration, Instant};fn addr(host: &str, port: u16) -> Result<SocketAddr, String> { let h = host.trim(); let with_port = if h.starts_with('[') { if h.contains("]:") { h.to_string() } else { format!("{}:{}", h, port) } } else if h.matches(':').count() == 1 { h.to_string() } else if h.contains(':') { format!("[{}]:{}", h, port) } else { format!("{}:{}", h, port) }; with_port.to_socket_addrs().map_err(|e| format!("{}: {}", host, e))?.next().ok_or(format!("{} has no address", host))}struct Opts { timeout: Duration, retries: u32, delay: Duration, read_ms: u64,}fn opts(o: &Value) -> Opts { Opts { timeout: Duration::from_millis(o["timeout_ms"].as_u64().unwrap_or(5000).clamp(100, 300_000)), retries: o["retries"].as_u64().unwrap_or(2).min(20) as u32, delay: Duration::from_millis(o["retry_delay_ms"].as_u64().unwrap_or(1000).min(60_000)), read_ms: o["read_reply_ms"].as_u64().unwrap_or(0).min(60_000), }}fn connect(a: SocketAddr, t: Duration) -> Result<TcpStream, String> { let s = TcpStream::connect_timeout(&a, t).map_err(|e| format!("cannot reach {}: {}", a, e))?; s.set_write_timeout(Some(t)).map_err(|e| e.to_string())?; s.set_read_timeout(Some(t)).map_err(|e| e.to_string())?; let _ = s.set_nodelay(true); Ok(s)}/// Sends once: connects, writes everything, half-closes, and reads what the printer says within `read_ms`.fn send_once(a: SocketAddr, data: &[u8], o: &Opts) -> Result<Vec<u8>, String> { let mut s = connect(a, o.timeout)?; for chunk in data.chunks(64 * 1024) { s.write_all(chunk).map_err(|e| format!("sending to {}: {}", a, e))?; } s.flush().map_err(|e| e.to_string())?; let mut reply = vec![]; if o.read_ms > 0 { let until = Instant::now() + Duration::from_millis(o.read_ms); let _ = s.set_read_timeout(Some(Duration::from_millis(50))); let mut buf = [0u8; 4096]; while Instant::now() < until && reply.len() < 1_000_000 { match s.read(&mut buf) { Ok(0) => break, Ok(n) => reply.extend_from_slice(&buf[..n]), Err(_) => {} } } } let _ = s.shutdown(std::net::Shutdown::Write); Ok(reply)}fn raw(data: &[u8], host: &str, options: &Value) -> Result<Value, String> { let port = options["port"].as_u64().unwrap_or(9100).clamp(1, 65535) as u16; let a = addr(host, port)?; let o = opts(options); let started = Instant::now(); let mut last = String::new(); for attempt in 1..=o.retries + 1 { match send_once(a, data, &o) { Ok(reply) => { return Ok(json!({ "bytes": data.len(), "attempts": attempt, "ms": started.elapsed().as_millis() as u64, "printer": a.to_string(), "reply": String::from_utf8_lossy(&reply), })) } Err(e) => { last = e; if attempt <= o.retries { std::thread::sleep(o.delay); } } } } Err(format!("{} (after {} attempts)", last, o.retries + 1))}/// Sends a file to a printer's raw port as it is: `host` or `host:port`. `options`: `{ port: 9100, timeout_ms: 5000, retries: 2, retry_delay_ms: 1000, read_reply_ms: 0 (wait that long for an answer, for status questions) }`./// Gives `{ bytes, attempts, ms, printer, reply }`. A retry sends the whole job again, so a job that failed half-way may print twice.pub fn raw_print_file(path: &str, host: &str, options: serde_json::Value) -> Result<serde_json::Value, String> { let data = std::fs::read(path).map_err(|e| format!("{}: {}", path, e))?; raw(&data, host, &options)}/// Sends text (ZPL, EPL, TSPL, PJL...) to a printer's raw port, as `raw_print_file` does.pub fn raw_print_text(text: &str, host: &str, options: serde_json::Value) -> Result<serde_json::Value, String> { raw(text.as_bytes(), host, &options)}/// Sends bytes given in base64 (a job another plugin made) to a printer's raw port, as `raw_print_file` does.pub fn raw_print_base64(data: &str, host: &str, options: serde_json::Value) -> Result<serde_json::Value, String> { use base64::Engine; let bytes = base64::engine::general_purpose::STANDARD.decode(data.trim()).map_err(|e| format!("not base64: {}", e))?; raw(&bytes, host, &options)}/// Whether a printer takes connections on `port` (9100 raw, 515 LPD, 631 IPP), and how long it took: `{ open, ms, error }`. Nothing is printed.pub fn raw_print_reachable(host: &str, port: i64, timeout_ms: i64) -> serde_json::Value { let started = Instant::now(); let a = match addr(host, port.clamp(1, 65535) as u16) { Ok(a) => a, Err(e) => return json!({ "open": false, "ms": 0, "error": e }), }; match TcpStream::connect_timeout(&a, Duration::from_millis(timeout_ms.clamp(50, 60_000) as u64)) { Ok(_) => json!({ "open": true, "ms": started.elapsed().as_millis() as u64, "error": null }), Err(e) => json!({ "open": false, "ms": started.elapsed().as_millis() as u64, "error": e.to_string() }), }}fn clean(s: &str, max: usize) -> String { s.chars().filter(|c| (' '..='~').contains(c)).take(max).collect()}/// The LPR control file for a job (RFC 1179): host, user, job and file names, and how to print it.fn control(job: u32, options: &Value) -> Result<(String, String), String> { let host = clean(options["client"].as_str().unwrap_or("quillo"), 31).replace(' ', ""); let host = if host.is_empty() { "quillo".to_string() } else { host }; let user = clean(options["user"].as_str().unwrap_or("quillo"), 31); let name = clean(options["name"].as_str().unwrap_or("job"), 99); let df = format!("dfA{:03}{}", job % 1000, host); // `l` prints the file as it is (control characters kept): right for printer languages; `f` formats plain text. let kind = match options["format"].as_str().unwrap_or("raw") { "raw" => 'l', "text" => 'f', "postscript" => 'o', other => return Err(format!("format {:?}: raw, text or postscript", other)), }; let copies = options["copies"].as_u64().unwrap_or(1).clamp(1, 100); let mut c = format!("H{}\nP{}\nJ{}\nN{}\n", host, user, name, name); if options["banner"] == true { c.push_str(&format!("C{}\nL{}\n", host, user)); } for _ in 0..copies { c.push_str(&format!("{}{}\n", kind, df)); } c.push_str(&format!("U{}\n", df)); Ok((c, df))}/// The LPR control file a job would send, for checking it: `options` as `lpr_print`.pub fn raw_print_lpr_control(options: serde_json::Value) -> Result<String, String> { let job = options["job"].as_u64().unwrap_or(1) as u32; Ok(control(job, &options)?.0)}fn ack(s: &mut TcpStream, what: &str) -> Result<(), String> { let mut b = [0u8; 1]; s.read_exact(&mut b).map_err(|e| format!("no answer to {}: {}", what, e))?; if b[0] != 0 { return Err(format!("the print server refused {} (code {})", what, b[0])); } Ok(())}/// Prints a file through an LPD print server or printer (port 515) on queue `queue` (often "raw", "lp", "PASSTHRU" or the printer's name)./// `options`: `{ user, name (the job's), copies, format: "raw" | "text" | "postscript", banner, job (a number 0-999), timeout_ms, retries }`. Gives `{ bytes, attempts, queue, job }`.pub fn raw_print_lpr(path: &str, host: &str, queue: &str, options: serde_json::Value) -> Result<serde_json::Value, String> { let data = std::fs::read(path).map_err(|e| format!("{}: {}", path, e))?; let q = clean(queue, 64); if q.is_empty() || q.contains(' ') { return Err("a queue name without spaces".into()); } let a = addr(host, options["port"].as_u64().unwrap_or(515).clamp(1, 65535) as u16)?; let o = opts(&options); let job = options["job"].as_u64().unwrap_or((std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map(|d| d.as_millis()).unwrap_or(0) % 1000) as u64) as u32; let (cf, df) = control(job, &options)?; let cf_name = df.replacen("dfA", "cfA", 1); let mut last = String::new(); for attempt in 1..=o.retries + 1 { let run = || -> Result<(), String> { let mut s = connect(a, o.timeout)?; s.write_all(format!("\x02{}\n", q).as_bytes()).map_err(|e| e.to_string())?; ack(&mut s, "the queue")?; s.write_all(format!("\x02{} {}\n", cf.len(), cf_name).as_bytes()).map_err(|e| e.to_string())?; ack(&mut s, "the control file")?; s.write_all(cf.as_bytes()).map_err(|e| e.to_string())?; s.write_all(&[0]).map_err(|e| e.to_string())?; ack(&mut s, "the control file's content")?; s.write_all(format!("\x03{} {}\n", data.len(), df).as_bytes()).map_err(|e| e.to_string())?; ack(&mut s, "the data file")?; s.write_all(&data).map_err(|e| e.to_string())?; s.write_all(&[0]).map_err(|e| e.to_string())?; ack(&mut s, "the data")?; Ok(()) }; match run() { Ok(()) => return Ok(json!({ "bytes": data.len(), "attempts": attempt, "queue": q, "job": job })), Err(e) => { last = e; if attempt <= o.retries { std::thread::sleep(o.delay); } } } } Err(format!("{} (after {} attempts)", last, o.retries + 1))}/// The state of an LPD queue as the server writes it (`long`: the long form): the jobs waiting, or that it is empty.pub fn raw_print_lpq(host: &str, queue: &str, long: bool, timeout_ms: i64) -> Result<String, String> { let q = clean(queue, 64); let a = addr(host, 515)?; let t = Duration::from_millis(timeout_ms.clamp(100, 60_000) as u64); let mut s = connect(a, t)?; s.write_all(format!("{}{}\n", if long { '\x04' } else { '\x03' }, q).as_bytes()).map_err(|e| e.to_string())?; let mut out = vec![]; let mut buf = [0u8; 4096]; loop { match s.read(&mut buf) { Ok(0) => break, Ok(n) => { out.extend_from_slice(&buf[..n]); if out.len() > 1_000_000 { break; } } Err(e) if out.is_empty() => return Err(format!("no answer from {}: {}", a, e)), Err(_) => break, } } Ok(String::from_utf8_lossy(&out).into_owned())}Related
ptouch.qwer · 1.0.0Brother QL label printers and P-touch tape printers in raster mode: text made into a label picture, any picture made into the raster job for a DK roll or TZe tape, cut, compressed, sent over the network, and the printer's status read.7 views · 7 downloadsDatamax DPL labels dpl.qwer · 1.0.0Honeywell and Datamax-O'Neil DPL labels from fields placed in millimetres from the top left: resident fonts, Code 128, EAN-13, QR and Data Matrix, lines and boxes, heat, speed and quantity, sent to port 9100.7 views · 9 downloadsEPL2 labels epl.qwer · 1.0.0Zebra and Eltron EPL2 labels from fields in millimetres: text in the resident fonts, Code 128, EAN-13, Code 39 and QR barcodes, lines, boxes and pictures, with label size, gap, speed and copies, sent to port 9100.6 views · 5 downloadsEpson ESC/P dot-matrix escp.qwer · 1.0.0Epson ESC/P and ESC/P2 for dot-matrix printers: pre-printed and multipart forms filled by line and column, reports with bold, double-width and condensed text, pitch, line spacing and page length, in code page 437 with box-drawing lines.7 views · 11 downloadsGodex EZPL labels ezpl.qwer · 1.0.0Godex EZPL labels for G500, RT200 and other Godex printers: text in the resident fonts, Code 128, EAN-13, Code 39 and QR codes, lines and boxes, label size, darkness, speed and copies, sent to port 9100.6 views · 8 downloadsHP PCL print jobs pcl.qwer · 1.0.0HP PCL 5 jobs for laser printers: pages of text placed in millimetres in the printer's fonts, boxes, lines and shading, pictures as raster graphics, plain listings, paper, tray, duplex and copies, wrapped in PJL, sent to port 9100.8 views · 8 downloadsThread 0 posts
On Raw printing and LPR: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.