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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +49 -0
- data/LICENSE.txt +21 -0
- data/README.md +145 -0
- data/exe/zpl_render +62 -0
- data/lib/zpl_render/barcode/code128.rb +332 -0
- data/lib/zpl_render/barcode/code39.rb +53 -0
- data/lib/zpl_render/barcode/interleaved2of5.rb +42 -0
- data/lib/zpl_render/barcode/pdf417.rb +201 -0
- data/lib/zpl_render/barcode/pdf417_tables.rb +354 -0
- data/lib/zpl_render/barcode/qr.rb +50 -0
- data/lib/zpl_render/canvas.rb +112 -0
- data/lib/zpl_render/font.rb +210 -0
- data/lib/zpl_render/font_data.rb +153 -0
- data/lib/zpl_render/fonts/LICENSE-LiberationSans.txt +102 -0
- data/lib/zpl_render/fonts/LiberationMono-Regular.ttf +0 -0
- data/lib/zpl_render/fonts/LiberationSans-Bold.ttf +0 -0
- data/lib/zpl_render/graphic_field.rb +112 -0
- data/lib/zpl_render/helvetica_metrics.rb +17 -0
- data/lib/zpl_render/output/pdf.rb +39 -0
- data/lib/zpl_render/output/png.rb +35 -0
- data/lib/zpl_render/parser.rb +90 -0
- data/lib/zpl_render/renderer.rb +627 -0
- data/lib/zpl_render/truetype.rb +428 -0
- data/lib/zpl_render/version.rb +5 -0
- data/lib/zpl_render.rb +71 -0
- metadata +127 -0
|
@@ -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
|
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: []
|