zpl_render 0.1.0

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,428 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ZplRender
4
+ # Minimal pure-Ruby TrueType font loader and glyph rasterizer.
5
+ #
6
+ # Parses just enough of a .ttf (head, maxp, cmap format 4, loca, glyf,
7
+ # hhea, hmtx, OS/2) to rasterize glyph outlines: quadratic contours are
8
+ # flattened to polygons and filled scanline-wise with the non-zero
9
+ # winding rule, sampling at pixel centers. Output is 1-bit, matching a
10
+ # thermal print head.
11
+ class TrueType
12
+ ON_CURVE = 0x01
13
+ X_SHORT = 0x02
14
+ Y_SHORT = 0x04
15
+ REPEAT = 0x08
16
+ X_SAME_OR_POS = 0x10
17
+ Y_SAME_OR_POS = 0x20
18
+
19
+ CURVE_SEGMENTS = 8 # polyline segments per quadratic bezier
20
+
21
+ Glyph = Struct.new(:canvas, :left, :top, keyword_init: true)
22
+
23
+ attr_reader :units_per_em, :cap_height
24
+
25
+ def self.default
26
+ @default ||= new(File.expand_path("fonts/LiberationSans-Bold.ttf", __dir__))
27
+ end
28
+
29
+ def self.mono
30
+ @mono ||= new(File.expand_path("fonts/LiberationMono-Regular.ttf", __dir__))
31
+ end
32
+
33
+ def initialize(path)
34
+ @data = File.binread(path)
35
+ @tables = parse_table_directory
36
+ parse_head
37
+ parse_maxp
38
+ parse_hhea_hmtx
39
+ parse_cmap
40
+ parse_os2
41
+ @glyph_cache = {}
42
+ @contour_cache = {}
43
+ end
44
+
45
+ # Advance width in font units for a unicode codepoint.
46
+ def advance(code)
47
+ gid = glyph_id(code)
48
+ @advances[gid] || @advances.last || @units_per_em / 2
49
+ end
50
+
51
+ def glyph_id(code)
52
+ @cmap_cache[code] ||= lookup_cmap(code)
53
+ end
54
+
55
+ # Rasterize a glyph at the given x/y scales (pixels per font unit).
56
+ # Returns a Glyph whose canvas is stamped at
57
+ # (pen_x + left, baseline_y - top). Returns nil for blank glyphs.
58
+ def glyph_bitmap(code, scale_x, scale_y)
59
+ key = [code, (scale_x * 10_000).round, (scale_y * 10_000).round]
60
+ return @glyph_cache[key] if @glyph_cache.key?(key)
61
+
62
+ @glyph_cache[key] = rasterize(glyph_id(code), scale_x, scale_y)
63
+ end
64
+
65
+ private
66
+
67
+ # ---- table directory and fixed-layout tables ----------------------------
68
+
69
+ def u8(o) = @data.getbyte(o)
70
+ def u16(o) = (@data.getbyte(o) << 8) | @data.getbyte(o + 1)
71
+ def i16(o)
72
+ v = u16(o)
73
+ v >= 0x8000 ? v - 0x10000 : v
74
+ end
75
+ def u32(o) = (u16(o) << 16) | u16(o + 2)
76
+
77
+ def parse_table_directory
78
+ count = u16(4)
79
+ tables = {}
80
+ count.times do |i|
81
+ rec = 12 + i * 16
82
+ tag = @data[rec, 4]
83
+ tables[tag] = [u32(rec + 8), u32(rec + 12)] # offset, length
84
+ end
85
+ tables
86
+ end
87
+
88
+ def table(tag)
89
+ @tables[tag] or raise Error, "font missing #{tag} table"
90
+ end
91
+
92
+ def parse_head
93
+ off, = table("head")
94
+ @units_per_em = u16(off + 18)
95
+ @loca_long = i16(off + 50) == 1
96
+ end
97
+
98
+ def parse_maxp
99
+ off, = table("maxp")
100
+ @num_glyphs = u16(off + 4)
101
+ end
102
+
103
+ def parse_hhea_hmtx
104
+ hhea, = table("hhea")
105
+ num_hmetrics = u16(hhea + 34)
106
+ hmtx, = table("hmtx")
107
+ @advances = Array.new(@num_glyphs) do |gid|
108
+ gid < num_hmetrics ? u16(hmtx + gid * 4) : u16(hmtx + (num_hmetrics - 1) * 4)
109
+ end
110
+ end
111
+
112
+ def parse_os2
113
+ if @tables["OS/2"]
114
+ off, len = table("OS/2")
115
+ version = u16(off)
116
+ @cap_height = i16(off + 88) if version >= 2 && len >= 90
117
+ end
118
+ @cap_height = nil if @cap_height && @cap_height <= 0
119
+ @cap_height ||= measure_cap_height
120
+ end
121
+
122
+ def measure_cap_height
123
+ gid = lookup_cmap("H".ord)
124
+ offset = glyph_offset(gid)
125
+ offset ? i16(glyf_base + offset + 8) : (@units_per_em * 0.7).round # yMax of 'H'
126
+ end
127
+
128
+ # ---- cmap format 4 -------------------------------------------------------
129
+
130
+ def parse_cmap
131
+ off, = table("cmap")
132
+ count = u16(off + 2)
133
+ best = nil
134
+ count.times do |i|
135
+ rec = off + 4 + i * 8
136
+ platform = u16(rec)
137
+ encoding = u16(rec + 2)
138
+ sub = off + u32(rec + 4)
139
+ next unless u16(sub) == 4
140
+
141
+ score = { [3, 1] => 3, [0, 3] => 2, [0, 4] => 2 }.fetch([platform, encoding], 1)
142
+ best = [score, sub] if best.nil? || score > best[0]
143
+ end
144
+ raise Error, "font has no format-4 cmap subtable" unless best
145
+
146
+ @cmap4 = best[1]
147
+ @cmap_cache = {}
148
+ end
149
+
150
+ def lookup_cmap(code)
151
+ sub = @cmap4
152
+ seg_count = u16(sub + 6) / 2
153
+ end_codes = sub + 14
154
+ start_codes = end_codes + seg_count * 2 + 2
155
+ id_deltas = start_codes + seg_count * 2
156
+ id_range_offsets = id_deltas + seg_count * 2
157
+
158
+ seg = (0...seg_count).bsearch { |i| u16(end_codes + i * 2) >= code }
159
+ return 0 unless seg
160
+
161
+ start = u16(start_codes + seg * 2)
162
+ return 0 if code < start
163
+
164
+ range_offset = u16(id_range_offsets + seg * 2)
165
+ delta = i16(id_deltas + seg * 2)
166
+ if range_offset.zero?
167
+ (code + delta) & 0xFFFF
168
+ else
169
+ addr = id_range_offsets + seg * 2 + range_offset + (code - start) * 2
170
+ gid = u16(addr)
171
+ gid.zero? ? 0 : (gid + delta) & 0xFFFF
172
+ end
173
+ end
174
+
175
+ # ---- glyf access ---------------------------------------------------------
176
+
177
+ def glyf_base
178
+ @glyf_base ||= table("glyf")[0]
179
+ end
180
+
181
+ def glyph_offset(gid)
182
+ return nil if gid >= @num_glyphs
183
+
184
+ loca, = table("loca")
185
+ if @loca_long
186
+ start = u32(loca + gid * 4)
187
+ fin = u32(loca + gid * 4 + 4)
188
+ else
189
+ start = u16(loca + gid * 2) * 2
190
+ fin = u16(loca + gid * 2 + 2) * 2
191
+ end
192
+ fin > start ? start : nil # empty glyph (e.g. space)
193
+ end
194
+
195
+ # Returns an array of contours, each an array of [x, y, on_curve] in
196
+ # font units, with composite glyphs resolved.
197
+ def contours(gid, depth = 0)
198
+ return [] if depth > 4
199
+
200
+ @contour_cache[gid] ||= begin
201
+ offset = glyph_offset(gid)
202
+ if offset.nil?
203
+ []
204
+ else
205
+ o = glyf_base + offset
206
+ n = i16(o)
207
+ n >= 0 ? simple_contours(o, n) : composite_contours(o, depth)
208
+ end
209
+ end
210
+ end
211
+
212
+ def simple_contours(o, contour_count)
213
+ end_pts = Array.new(contour_count) { |i| u16(o + 10 + i * 2) }
214
+ point_count = end_pts.last + 1
215
+ p = o + 10 + contour_count * 2
216
+ p += 2 + u16(p) # instructions
217
+
218
+ flags = []
219
+ while flags.length < point_count
220
+ flag = u8(p)
221
+ p += 1
222
+ flags << flag
223
+ if flag & REPEAT != 0
224
+ u8(p).times { flags << flag }
225
+ p += 1
226
+ end
227
+ end
228
+
229
+ xs = read_coords(flags, p, X_SHORT, X_SAME_OR_POS) { |bytes| p += bytes }
230
+ ys = read_coords(flags, p, Y_SHORT, Y_SAME_OR_POS) { |bytes| p += bytes }
231
+
232
+ result = []
233
+ start = 0
234
+ end_pts.each do |last|
235
+ result << (start..last).map { |i| [xs[i], ys[i], flags[i] & ON_CURVE != 0] }
236
+ start = last + 1
237
+ end
238
+ result
239
+ end
240
+
241
+ def read_coords(flags, start, short_bit, same_bit)
242
+ p = start
243
+ value = 0
244
+ coords = flags.map do |flag|
245
+ if flag & short_bit != 0
246
+ d = u8(p)
247
+ p += 1
248
+ value += (flag & same_bit != 0 ? d : -d)
249
+ elsif flag & same_bit == 0
250
+ value += i16(p)
251
+ p += 2
252
+ end
253
+ value
254
+ end
255
+ yield(p - start)
256
+ coords
257
+ end
258
+
259
+ ARGS_ARE_WORDS = 0x0001
260
+ ARGS_ARE_XY = 0x0002
261
+ HAVE_SCALE = 0x0008
262
+ MORE_COMPONENTS = 0x0020
263
+ HAVE_XY_SCALE = 0x0040
264
+ HAVE_2X2 = 0x0080
265
+
266
+ def composite_contours(o, depth)
267
+ p = o + 10
268
+ result = []
269
+ loop do
270
+ flags = u16(p)
271
+ component_gid = u16(p + 2)
272
+ p += 4
273
+ if flags & ARGS_ARE_WORDS != 0
274
+ dx = i16(p)
275
+ dy = i16(p + 2)
276
+ p += 4
277
+ else
278
+ dx = u8(p)
279
+ dy = u8(p + 1)
280
+ dx -= 256 if dx > 127
281
+ dy -= 256 if dy > 127
282
+ p += 2
283
+ end
284
+ a = d = 1.0
285
+ b = c = 0.0
286
+ if flags & HAVE_SCALE != 0
287
+ a = d = f2dot14(p)
288
+ p += 2
289
+ elsif flags & HAVE_XY_SCALE != 0
290
+ a = f2dot14(p)
291
+ d = f2dot14(p + 2)
292
+ p += 4
293
+ elsif flags & HAVE_2X2 != 0
294
+ a = f2dot14(p)
295
+ b = f2dot14(p + 2)
296
+ c = f2dot14(p + 4)
297
+ d = f2dot14(p + 6)
298
+ p += 8
299
+ end
300
+ dx = dy = 0 unless flags & ARGS_ARE_XY != 0
301
+ contours(component_gid, depth + 1).each do |contour|
302
+ result << contour.map do |x, y, on|
303
+ [a * x + c * y + dx, b * x + d * y + dy, on]
304
+ end
305
+ end
306
+ break if flags & MORE_COMPONENTS == 0
307
+ end
308
+ result
309
+ end
310
+
311
+ def f2dot14(o)
312
+ i16(o) / 16_384.0
313
+ end
314
+
315
+ # ---- rasterization -------------------------------------------------------
316
+
317
+ def rasterize(gid, scale_x, scale_y)
318
+ polys = contours(gid).map { |c| flatten_contour(c, scale_x, scale_y) }
319
+ polys.reject! { |pts| pts.length < 3 }
320
+ return nil if polys.empty?
321
+
322
+ min_x = polys.flat_map { |pts| pts.map(&:first) }.min
323
+ max_x = polys.flat_map { |pts| pts.map(&:first) }.max
324
+ min_y = polys.flat_map { |pts| pts.map(&:last) }.min
325
+ max_y = polys.flat_map { |pts| pts.map(&:last) }.max
326
+
327
+ left = min_x.floor
328
+ top = max_y.ceil # distance above baseline (y grows up in font space)
329
+ width = max_x.ceil - left + 1
330
+ height = top - min_y.floor + 1
331
+ return nil if width < 1 || height < 1
332
+
333
+ canvas = Canvas.new(width, height)
334
+ edges = []
335
+ polys.each do |pts|
336
+ pts.each_with_index do |(x0, y0), i|
337
+ x1, y1 = pts[(i + 1) % pts.length]
338
+ edges << [x0, y0, x1, y1] unless y0 == y1
339
+ end
340
+ end
341
+
342
+ (0...height).each do |row|
343
+ yc = top - row - 0.5 # sample at pixel center, font-space y
344
+ crossings = []
345
+ edges.each do |x0, y0, x1, y1|
346
+ next unless (y0 <= yc && y1 > yc) || (y1 <= yc && y0 > yc)
347
+
348
+ crossings << [x0 + (yc - y0) * (x1 - x0) / (y1 - y0), y1 > y0 ? 1 : -1]
349
+ end
350
+ next if crossings.empty?
351
+
352
+ crossings.sort_by!(&:first)
353
+ winding = 0
354
+ span_start = nil
355
+ crossings.each do |x, dir|
356
+ was_inside = winding != 0
357
+ winding += dir
358
+ if !was_inside && winding != 0
359
+ span_start = x
360
+ elsif was_inside && winding.zero? && span_start
361
+ px0 = (span_start - left - 0.5).ceil
362
+ px1 = (x - left - 0.5).floor
363
+ canvas.fill_rect(px0, row, px1 - px0 + 1, 1) if px1 >= px0
364
+ span_start = nil
365
+ end
366
+ end
367
+ end
368
+
369
+ Glyph.new(canvas: canvas, left: left, top: top)
370
+ end
371
+
372
+ # Flatten a contour of [x, y, on_curve] font-unit points into a scaled
373
+ # polygon. Consecutive off-curve points imply an on-curve midpoint.
374
+ def flatten_contour(contour, scale_x, scale_y)
375
+ points = normalize_contour(contour)
376
+ out = []
377
+ i = 0
378
+ while i < points.length
379
+ x, y, on = points[i]
380
+ if on
381
+ out << [x * scale_x, y * scale_y]
382
+ i += 1
383
+ else
384
+ x0, y0 = out.last || begin
385
+ px, py, = points[i - 1]
386
+ [px * scale_x, py * scale_y]
387
+ end
388
+ nx, ny, = points[(i + 1) % points.length]
389
+ cx = x * scale_x
390
+ cy = y * scale_y
391
+ ex = nx * scale_x
392
+ ey = ny * scale_y
393
+ (1..CURVE_SEGMENTS).each do |s|
394
+ t = s.to_f / CURVE_SEGMENTS
395
+ mt = 1 - t
396
+ out << [mt * mt * x0 + 2 * mt * t * cx + t * t * ex,
397
+ mt * mt * y0 + 2 * mt * t * cy + t * t * ey]
398
+ end
399
+ i += 2 # consumed the following on-curve point as curve end
400
+ end
401
+ end
402
+ out
403
+ end
404
+
405
+ # Ensure the contour starts with an on-curve point and has explicit
406
+ # on-curve midpoints between consecutive off-curve points.
407
+ def normalize_contour(contour)
408
+ points = []
409
+ contour.each_with_index do |(x, y, on), i|
410
+ if !on && !contour[i - 1][2]
411
+ px, py, = contour[i - 1]
412
+ points << [(px + x) / 2.0, (py + y) / 2.0, true]
413
+ end
414
+ points << [x, y, on]
415
+ end
416
+ unless points.first[2]
417
+ if points.last[2]
418
+ points.rotate!(-1)
419
+ else
420
+ first = points.first
421
+ last = points.last
422
+ points.unshift([(first[0] + last[0]) / 2.0, (first[1] + last[1]) / 2.0, true])
423
+ end
424
+ end
425
+ points
426
+ end
427
+ end
428
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ZplRender
4
+ VERSION = "0.1.0"
5
+ end
data/lib/zpl_render.rb ADDED
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "zpl_render/version"
4
+ require_relative "zpl_render/canvas"
5
+ require_relative "zpl_render/truetype"
6
+ require_relative "zpl_render/font"
7
+ require_relative "zpl_render/parser"
8
+ require_relative "zpl_render/graphic_field"
9
+ require_relative "zpl_render/barcode/code128"
10
+ require_relative "zpl_render/barcode/code39"
11
+ require_relative "zpl_render/barcode/interleaved2of5"
12
+ require_relative "zpl_render/barcode/pdf417"
13
+ require_relative "zpl_render/barcode/qr"
14
+ require_relative "zpl_render/renderer"
15
+ require_relative "zpl_render/output/png"
16
+ require_relative "zpl_render/output/pdf"
17
+
18
+ # Render ZPL II label programs to PNG or PDF.
19
+ #
20
+ # png = ZplRender.to_png(zpl, dpmm: 8, width_in: 4, height_in: 6)
21
+ # pdf = ZplRender.to_pdf(zpl, dpmm: 8, width_in: 4, height_in: 6)
22
+ #
23
+ # dpmm is the printer density in dots per millimeter:
24
+ # 6 = 152 dpi, 8 = 203 dpi (most common), 12 = 300 dpi, 24 = 600 dpi
25
+ #
26
+ # Error handling: every failure the library raises intentionally is a
27
+ # ZplRender::Error. By default rendering is lenient: recoverable problems
28
+ # (a field that fails to render, an unsupported command) are recorded in
29
+ # the optional +warnings+ array and rendering continues, like a printer.
30
+ # Pass strict: true to raise ZplRender::Error on the first problem
31
+ # instead, for pipelines that must not silently drop label content.
32
+ module ZplRender
33
+ class Error < StandardError; end
34
+
35
+ module_function
36
+
37
+ # Render every ^XA..^XZ format to a Canvas (dot matrix).
38
+ def render(zpl, dpmm: 8, width_in: 4.0, height_in: 6.0, strict: false, warnings: nil)
39
+ raise Error, "zpl must be a String (got #{zpl.class})" unless zpl.respond_to?(:to_str)
40
+
41
+ renderer = Renderer.new(dpmm: dpmm, width_in: width_in, height_in: height_in, strict: strict)
42
+ labels = renderer.render(zpl.to_str)
43
+ warnings.concat(renderer.warnings) if warnings
44
+ labels
45
+ end
46
+
47
+ # PNG bytes for the first label.
48
+ def to_png(zpl, dpmm: 8, width_in: 4.0, height_in: 6.0, scale: 1, strict: false, warnings: nil)
49
+ labels = render(zpl, dpmm: dpmm, width_in: width_in, height_in: height_in,
50
+ strict: strict, warnings: warnings)
51
+ raise Error, "no ^XA..^XZ label formats found" if labels.empty?
52
+
53
+ Output::Png.encode(labels.first, scale: scale)
54
+ end
55
+
56
+ # PNG bytes for every label.
57
+ def to_pngs(zpl, dpmm: 8, width_in: 4.0, height_in: 6.0, scale: 1, strict: false, warnings: nil)
58
+ render(zpl, dpmm: dpmm, width_in: width_in, height_in: height_in,
59
+ strict: strict, warnings: warnings)
60
+ .map { |canvas| Output::Png.encode(canvas, scale: scale) }
61
+ end
62
+
63
+ # PDF bytes, one page per label, at exact physical size.
64
+ def to_pdf(zpl, dpmm: 8, width_in: 4.0, height_in: 6.0, strict: false, warnings: nil)
65
+ labels = render(zpl, dpmm: dpmm, width_in: width_in, height_in: height_in,
66
+ strict: strict, warnings: warnings)
67
+ raise Error, "no ^XA..^XZ label formats found" if labels.empty?
68
+
69
+ Output::Pdf.encode(labels, dpmm: dpmm)
70
+ end
71
+ end
metadata ADDED
@@ -0,0 +1,127 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: zpl_render
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Waqas Ali
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: chunky_png
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.4'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.4'
27
+ - !ruby/object:Gem::Dependency
28
+ name: prawn
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '2.4'
34
+ - - "<"
35
+ - !ruby/object:Gem::Version
36
+ version: '3'
37
+ type: :runtime
38
+ prerelease: false
39
+ version_requirements: !ruby/object:Gem::Requirement
40
+ requirements:
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ version: '2.4'
44
+ - - "<"
45
+ - !ruby/object:Gem::Version
46
+ version: '3'
47
+ - !ruby/object:Gem::Dependency
48
+ name: rqrcode_core
49
+ requirement: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '2.0'
54
+ type: :runtime
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '2.0'
61
+ description: A pure-Ruby ZPL II renderer. Parses ^XA..^XZ label formats and rasterizes
62
+ them at true printer resolution (dots), producing PNG images and print-size PDF
63
+ documents. Code 128 (^BC, incl. Zebra subset invocation codes and GS1 modes), QR
64
+ (^BQ), Code 39 (^B3) and Interleaved 2 of 5 (^B2) are generated module-exact so
65
+ scanners read them reliably. Also supports text fields, field blocks, graphic boxes/circles/diagonals,
66
+ ^GF images (ASCII hex, Zebra RLE, B64/Z64) and ~DG/^XG stored graphics.
67
+ email:
68
+ - wqsaali@gmail.com
69
+ executables:
70
+ - zpl_render
71
+ extensions: []
72
+ extra_rdoc_files: []
73
+ files:
74
+ - CHANGELOG.md
75
+ - LICENSE.txt
76
+ - README.md
77
+ - exe/zpl_render
78
+ - lib/zpl_render.rb
79
+ - lib/zpl_render/barcode/code128.rb
80
+ - lib/zpl_render/barcode/code39.rb
81
+ - lib/zpl_render/barcode/interleaved2of5.rb
82
+ - lib/zpl_render/barcode/pdf417.rb
83
+ - lib/zpl_render/barcode/pdf417_tables.rb
84
+ - lib/zpl_render/barcode/qr.rb
85
+ - lib/zpl_render/canvas.rb
86
+ - lib/zpl_render/font.rb
87
+ - lib/zpl_render/font_data.rb
88
+ - lib/zpl_render/fonts/LICENSE-LiberationSans.txt
89
+ - lib/zpl_render/fonts/LiberationMono-Regular.ttf
90
+ - lib/zpl_render/fonts/LiberationSans-Bold.ttf
91
+ - lib/zpl_render/graphic_field.rb
92
+ - lib/zpl_render/helvetica_metrics.rb
93
+ - lib/zpl_render/output/pdf.rb
94
+ - lib/zpl_render/output/png.rb
95
+ - lib/zpl_render/parser.rb
96
+ - lib/zpl_render/renderer.rb
97
+ - lib/zpl_render/truetype.rb
98
+ - lib/zpl_render/version.rb
99
+ homepage: https://github.com/wqsaali/zpl_render
100
+ licenses:
101
+ - MIT
102
+ metadata:
103
+ rubygems_mfa_required: 'true'
104
+ homepage_uri: https://github.com/wqsaali/zpl_render
105
+ source_code_uri: https://github.com/wqsaali/zpl_render
106
+ bug_tracker_uri: https://github.com/wqsaali/zpl_render/issues
107
+ changelog_uri: https://github.com/wqsaali/zpl_render/blob/main/CHANGELOG.md
108
+ post_install_message:
109
+ rdoc_options: []
110
+ require_paths:
111
+ - lib
112
+ required_ruby_version: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - ">="
115
+ - !ruby/object:Gem::Version
116
+ version: '3.0'
117
+ required_rubygems_version: !ruby/object:Gem::Requirement
118
+ requirements:
119
+ - - ">="
120
+ - !ruby/object:Gem::Version
121
+ version: '0'
122
+ requirements: []
123
+ rubygems_version: 3.4.10
124
+ signing_key:
125
+ specification_version: 4
126
+ summary: Render ZPL II label programs to PNG and PDF, with scanner-accurate barcodes.
127
+ test_files: []