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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +23 -0
- data/COMPATIBILITY.md +64 -0
- data/LICENSE.txt +24 -0
- data/README.md +147 -0
- data/exe/zpl-render +95 -0
- data/lib/zpl_renderer/binary.rb +63 -0
- data/lib/zpl_renderer/decoder.rb +42 -0
- data/lib/zpl_renderer/engine.rb +87 -0
- data/lib/zpl_renderer/errors.rb +14 -0
- data/lib/zpl_renderer/options.rb +139 -0
- data/lib/zpl_renderer/renderer.rb +23 -0
- data/lib/zpl_renderer/sanitizer.rb +156 -0
- data/lib/zpl_renderer/validation.rb +185 -0
- data/lib/zpl_renderer/version.rb +5 -0
- data/lib/zpl_renderer.rb +85 -0
- data/renderer/Cargo.lock +2473 -0
- data/renderer/Cargo.toml +25 -0
- data/renderer/src/main.rs +310 -0
- data/vendor/bin/SHA256SUMS +1 -0
- data/vendor/bin/x86_64-unknown-linux-gnu/zpl-renderer +0 -0
- metadata +113 -0
data/renderer/Cargo.toml
ADDED
|
@@ -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
|
|
Binary file
|
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: []
|