zpl_renderer 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,25 @@
1
+ [package]
2
+ name = "zpl-renderer"
3
+ version = "0.1.0"
4
+ edition = "2021"
5
+ description = "Offline ZPL renderer bridge for the zpl_renderer Ruby gem"
6
+ license = "MIT"
7
+ publish = false
8
+
9
+ [[bin]]
10
+ name = "zpl-renderer"
11
+ path = "src/main.rs"
12
+
13
+ [dependencies]
14
+ labelize = "1.3.0"
15
+ serde = { version = "1", features = ["derive"] }
16
+ serde_json = "1"
17
+ image = { version = "0.25", default-features = false, features = ["png"] }
18
+ clap = { version = "4", features = ["derive"] }
19
+ rxing = { version = "0.8", default-features = false, features = ["encoding_rs"] }
20
+
21
+ [profile.release]
22
+ lto = true
23
+ codegen-units = 1
24
+ opt-level = 3
25
+ strip = true
@@ -0,0 +1,310 @@
1
+ use std::io::{self, Cursor, Read, Write};
2
+ use std::process::ExitCode;
3
+
4
+ use clap::Parser;
5
+ use image::RgbaImage;
6
+ use labelize::{DrawerOptions, Renderer, ZplParser};
7
+ use serde::{Deserialize, Serialize};
8
+
9
+ #[derive(Parser, Debug)]
10
+ #[command(
11
+ name = "zpl-renderer",
12
+ about = "Offline ZPL to PNG/PDF bridge used by the zpl_renderer Ruby gem"
13
+ )]
14
+ struct Cli {
15
+ /// Read a JSON render request from stdin and write binary output to stdout.
16
+ #[arg(long)]
17
+ json: bool,
18
+
19
+ /// Decode barcodes/QR from a PNG on stdin; print JSON results.
20
+ #[arg(long)]
21
+ decode: bool,
22
+
23
+ /// Output format when not using --json (png|pdf).
24
+ #[arg(long, default_value = "png")]
25
+ format: String,
26
+
27
+ /// Label width in millimeters.
28
+ #[arg(long, default_value_t = 101.6)]
29
+ width_mm: f64,
30
+
31
+ /// Label height in millimeters.
32
+ #[arg(long, default_value_t = 152.4)]
33
+ height_mm: f64,
34
+
35
+ /// Dots per millimeter (8=203dpi, 12=300dpi, 24=600dpi).
36
+ #[arg(long, default_value_t = 8)]
37
+ dpmm: i32,
38
+
39
+ /// Label index for multi-label ZPL.
40
+ #[arg(long, default_value_t = 0)]
41
+ index: usize,
42
+ }
43
+
44
+ #[derive(Debug, Deserialize)]
45
+ struct RenderRequest {
46
+ protocol: u32,
47
+ zpl: String,
48
+ format: String,
49
+ width_mm: f64,
50
+ height_mm: f64,
51
+ dpmm: i32,
52
+ #[serde(default)]
53
+ index: usize,
54
+ }
55
+
56
+ #[derive(Debug, Serialize)]
57
+ struct DecodeHit {
58
+ format: String,
59
+ text: String,
60
+ }
61
+
62
+ fn main() -> ExitCode {
63
+ let cli = Cli::parse();
64
+
65
+ let result = if cli.decode {
66
+ run_decode()
67
+ } else if cli.json {
68
+ run_json()
69
+ } else {
70
+ run_raw(&cli)
71
+ };
72
+
73
+ match result {
74
+ Ok(()) => ExitCode::SUCCESS,
75
+ Err(message) => {
76
+ let _ = writeln!(io::stderr(), "{message}");
77
+ ExitCode::from(1)
78
+ }
79
+ }
80
+ }
81
+
82
+ fn run_json() -> Result<(), String> {
83
+ let mut input = String::new();
84
+ io::stdin()
85
+ .read_to_string(&mut input)
86
+ .map_err(|e| format!("failed to read stdin: {e}"))?;
87
+
88
+ let request: RenderRequest =
89
+ serde_json::from_str(&input).map_err(|e| format!("invalid JSON request: {e}"))?;
90
+
91
+ if request.protocol != 1 {
92
+ return Err(format!(
93
+ "unsupported protocol {}; expected 1",
94
+ request.protocol
95
+ ));
96
+ }
97
+
98
+ let bytes = render_zpl(
99
+ &request.zpl,
100
+ &request.format,
101
+ request.width_mm,
102
+ request.height_mm,
103
+ request.dpmm,
104
+ request.index,
105
+ )?;
106
+
107
+ let mut stdout = io::stdout().lock();
108
+ stdout
109
+ .write_all(&bytes)
110
+ .map_err(|e| format!("failed to write stdout: {e}"))?;
111
+ Ok(())
112
+ }
113
+
114
+ fn run_raw(cli: &Cli) -> Result<(), String> {
115
+ let mut input = Vec::new();
116
+ io::stdin()
117
+ .read_to_end(&mut input)
118
+ .map_err(|e| format!("failed to read stdin: {e}"))?;
119
+
120
+ let zpl = String::from_utf8_lossy(&input);
121
+ let bytes = render_zpl(
122
+ &zpl,
123
+ &cli.format,
124
+ cli.width_mm,
125
+ cli.height_mm,
126
+ cli.dpmm,
127
+ cli.index,
128
+ )?;
129
+
130
+ let mut stdout = io::stdout().lock();
131
+ stdout
132
+ .write_all(&bytes)
133
+ .map_err(|e| format!("failed to write stdout: {e}"))?;
134
+ Ok(())
135
+ }
136
+
137
+ fn run_decode() -> Result<(), String> {
138
+ let mut input = Vec::new();
139
+ io::stdin()
140
+ .read_to_end(&mut input)
141
+ .map_err(|e| format!("failed to read stdin: {e}"))?;
142
+
143
+ let hits = decode_png(&input)?;
144
+ let json = serde_json::to_string_pretty(&hits).map_err(|e| e.to_string())?;
145
+ let mut stdout = io::stdout().lock();
146
+ stdout
147
+ .write_all(json.as_bytes())
148
+ .map_err(|e| format!("failed to write stdout: {e}"))?;
149
+ stdout
150
+ .write_all(b"\n")
151
+ .map_err(|e| format!("failed to write stdout: {e}"))?;
152
+ Ok(())
153
+ }
154
+
155
+ fn render_zpl(
156
+ zpl: &str,
157
+ format: &str,
158
+ width_mm: f64,
159
+ height_mm: f64,
160
+ dpmm: i32,
161
+ index: usize,
162
+ ) -> Result<Vec<u8>, String> {
163
+ validate_geometry(width_mm, height_mm, dpmm)?;
164
+
165
+ let format = format.to_ascii_lowercase();
166
+ if format != "png" && format != "pdf" {
167
+ return Err(format!("unsupported format '{format}'; use png or pdf"));
168
+ }
169
+
170
+ let mut parser = ZplParser::new();
171
+ let labels = parser
172
+ .parse(zpl.as_bytes())
173
+ .map_err(|e| format!("ZPL parse error: {e}"))?;
174
+
175
+ if labels.is_empty() {
176
+ return Err("no labels found in ZPL input".to_string());
177
+ }
178
+ if index >= labels.len() {
179
+ return Err(format!(
180
+ "index out of range: {index} (label count: {})",
181
+ labels.len()
182
+ ));
183
+ }
184
+
185
+ let options = DrawerOptions {
186
+ label_width_mm: width_mm,
187
+ label_height_mm: height_mm,
188
+ dpmm,
189
+ ..DrawerOptions::default()
190
+ }
191
+ .with_defaults();
192
+
193
+ let renderer = Renderer::new();
194
+ let mut png_buf = Cursor::new(Vec::new());
195
+ renderer
196
+ .draw_label_as_png(&labels[index], &mut png_buf, options.clone())
197
+ .map_err(|e| format!("render error: {e}"))?;
198
+
199
+ let png_bytes = png_buf.into_inner();
200
+ if format == "png" {
201
+ return Ok(png_bytes);
202
+ }
203
+
204
+ let img: RgbaImage = image::load_from_memory(&png_bytes)
205
+ .map_err(|e| format!("failed to decode rendered PNG: {e}"))?
206
+ .to_rgba8();
207
+
208
+ let mut pdf_buf = Cursor::new(Vec::new());
209
+ labelize::encode_pdf(&img, &options, &mut pdf_buf)
210
+ .map_err(|e| format!("PDF encode error: {e}"))?;
211
+ Ok(pdf_buf.into_inner())
212
+ }
213
+
214
+ fn decode_png(png_bytes: &[u8]) -> Result<Vec<DecodeHit>, String> {
215
+ use rxing::{BarcodeFormat, DecodeHints};
216
+ use std::collections::HashSet;
217
+
218
+ let img = image::load_from_memory(png_bytes)
219
+ .map_err(|e| format!("failed to decode PNG: {e}"))?
220
+ .to_luma8();
221
+ let width = img.width();
222
+ let height = img.height();
223
+ let luma = img.into_raw();
224
+
225
+ let mut hints = DecodeHints {
226
+ TryHarder: Some(true),
227
+ PossibleFormats: Some(HashSet::from([
228
+ BarcodeFormat::CODE_128,
229
+ BarcodeFormat::CODE_39,
230
+ BarcodeFormat::EAN_13,
231
+ BarcodeFormat::ITF,
232
+ BarcodeFormat::QR_CODE,
233
+ BarcodeFormat::DATA_MATRIX,
234
+ BarcodeFormat::PDF_417,
235
+ BarcodeFormat::AZTEC,
236
+ ])),
237
+ ..DecodeHints::default()
238
+ };
239
+
240
+ let results =
241
+ rxing::helpers::detect_multiple_in_luma_with_hints(luma, width, height, &mut hints)
242
+ .unwrap_or_default();
243
+
244
+ Ok(results
245
+ .into_iter()
246
+ .map(|r| DecodeHit {
247
+ format: format!("{:?}", r.getBarcodeFormat()),
248
+ text: r.getText().to_string(),
249
+ })
250
+ .collect())
251
+ }
252
+
253
+ fn validate_geometry(width_mm: f64, height_mm: f64, dpmm: i32) -> Result<(), String> {
254
+ if !(width_mm.is_finite() && height_mm.is_finite()) || width_mm <= 0.0 || height_mm <= 0.0 {
255
+ return Err("width_mm and height_mm must be positive finite numbers".to_string());
256
+ }
257
+ if width_mm > 609.6 || height_mm > 609.6 {
258
+ return Err("label dimensions exceed 24x24 inches".to_string());
259
+ }
260
+ match dpmm {
261
+ 8 | 12 | 24 => Ok(()),
262
+ other => Err(format!("unsupported dpmm={other}; use 8, 12, or 24")),
263
+ }
264
+ }
265
+
266
+ #[cfg(test)]
267
+ mod tests {
268
+ use super::*;
269
+
270
+ #[test]
271
+ fn renders_simple_png() {
272
+ let zpl = "^XA^FO50,50^A0N,40,40^FDHello^FS^XZ";
273
+ let bytes = render_zpl(zpl, "png", 101.6, 152.4, 8, 0).expect("render");
274
+ assert!(bytes.starts_with(b"\x89PNG"));
275
+ assert!(bytes.len() > 100);
276
+ }
277
+
278
+ #[test]
279
+ fn renders_barcode_and_qr_and_decodes() {
280
+ let zpl = r#"
281
+ ^XA
282
+ ^FO50,50^BY3
283
+ ^BCN,120,Y,N,N
284
+ ^FDHELLO123^FS
285
+ ^FO50,250^BQN,2,6
286
+ ^FDQA,SCAN-ME-12345^FS
287
+ ^XZ
288
+ "#;
289
+ let bytes = render_zpl(zpl, "png", 101.6, 152.4, 8, 0).expect("render");
290
+ assert!(bytes.starts_with(b"\x89PNG"));
291
+
292
+ let hits = decode_png(&bytes).expect("decode");
293
+ let texts: Vec<_> = hits.iter().map(|h| h.text.as_str()).collect();
294
+ assert!(
295
+ texts.iter().any(|t| t.contains("HELLO123")),
296
+ "expected Code 128 payload, got {texts:?}"
297
+ );
298
+ assert!(
299
+ texts.iter().any(|t| t.contains("SCAN-ME-12345")),
300
+ "expected QR payload, got {texts:?}"
301
+ );
302
+ }
303
+
304
+ #[test]
305
+ fn rejects_bad_index() {
306
+ let zpl = "^XA^FO50,50^A0N,40,40^FDHello^FS^XZ";
307
+ let err = render_zpl(zpl, "png", 101.6, 152.4, 8, 3).unwrap_err();
308
+ assert!(err.contains("index out of range"));
309
+ }
310
+ }
@@ -0,0 +1 @@
1
+ 470f9834e85da9454f6074775b316fa61053391f0790659cf61a5b7c47aff4bf ./x86_64-unknown-linux-gnu/zpl-renderer
metadata ADDED
@@ -0,0 +1,113 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: zpl_renderer
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.2
5
+ platform: ruby
6
+ authors:
7
+ - Waqas
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2026-07-31 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rake
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '13.0'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '13.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rspec
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '3.13'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '3.13'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rubocop
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '1.65'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '1.65'
55
+ description: |
56
+ Convert Zebra Programming Language (ZPL) labels to PNG or PDF entirely offline.
57
+ Ships a bundled Rust renderer for printer-dot-accurate output with Code 128,
58
+ QR, GS1, and other common shipping-label barcodes that scanners can decode.
59
+ email:
60
+ - waqas@example.com
61
+ executables:
62
+ - zpl-render
63
+ extensions: []
64
+ extra_rdoc_files: []
65
+ files:
66
+ - CHANGELOG.md
67
+ - COMPATIBILITY.md
68
+ - LICENSE.txt
69
+ - README.md
70
+ - exe/zpl-render
71
+ - lib/zpl_renderer.rb
72
+ - lib/zpl_renderer/binary.rb
73
+ - lib/zpl_renderer/decoder.rb
74
+ - lib/zpl_renderer/engine.rb
75
+ - lib/zpl_renderer/errors.rb
76
+ - lib/zpl_renderer/options.rb
77
+ - lib/zpl_renderer/renderer.rb
78
+ - lib/zpl_renderer/sanitizer.rb
79
+ - lib/zpl_renderer/validation.rb
80
+ - lib/zpl_renderer/version.rb
81
+ - renderer/Cargo.lock
82
+ - renderer/Cargo.toml
83
+ - renderer/src/main.rs
84
+ - vendor/bin/SHA256SUMS
85
+ - vendor/bin/x86_64-unknown-linux-gnu/zpl-renderer
86
+ homepage: https://github.com/waqas/zpl_renderer
87
+ licenses:
88
+ - MIT
89
+ metadata:
90
+ homepage_uri: https://github.com/waqas/zpl_renderer
91
+ source_code_uri: https://github.com/waqas/zpl_renderer
92
+ changelog_uri: https://github.com/waqas/zpl_renderer/blob/main/CHANGELOG.md
93
+ rubygems_mfa_required: 'true'
94
+ post_install_message:
95
+ rdoc_options: []
96
+ require_paths:
97
+ - lib
98
+ required_ruby_version: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - ">="
101
+ - !ruby/object:Gem::Version
102
+ version: 3.1.0
103
+ required_rubygems_version: !ruby/object:Gem::Requirement
104
+ requirements:
105
+ - - ">="
106
+ - !ruby/object:Gem::Version
107
+ version: '0'
108
+ requirements: []
109
+ rubygems_version: 3.4.10
110
+ signing_key:
111
+ specification_version: 4
112
+ summary: Offline ZPL to PNG/PDF renderer with scannable barcodes
113
+ test_files: []