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,627 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ZplRender
|
|
4
|
+
# Executes a parsed ZPL command stream and paints labels onto Canvases.
|
|
5
|
+
# One Canvas is produced per ^XA...^XZ format.
|
|
6
|
+
class Renderer
|
|
7
|
+
DEFAULT_MODULE_WIDTH = 2
|
|
8
|
+
DEFAULT_RATIO = 3.0
|
|
9
|
+
DEFAULT_BARCODE_HEIGHT = 10
|
|
10
|
+
|
|
11
|
+
attr_reader :warnings
|
|
12
|
+
|
|
13
|
+
# strict: false (default) records recoverable problems in #warnings and
|
|
14
|
+
# keeps rendering; true raises ZplRender::Error on the first problem
|
|
15
|
+
# (bad field data, unsupported command).
|
|
16
|
+
def initialize(dpmm: 8, width_in: 4.0, height_in: 6.0, strict: false)
|
|
17
|
+
raise Error, "dpmm must be a positive number (got #{dpmm.inspect})" unless dpmm.is_a?(Numeric) && dpmm.positive?
|
|
18
|
+
unless width_in.is_a?(Numeric) && width_in.positive? && height_in.is_a?(Numeric) && height_in.positive?
|
|
19
|
+
raise Error, "label dimensions must be positive numbers (got #{width_in.inspect} x #{height_in.inspect})"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
@dpmm = dpmm
|
|
23
|
+
@strict = strict
|
|
24
|
+
dpi = (25.4 * dpmm).round # printers advertise 203/300/600 dpi nominal
|
|
25
|
+
@default_width = (width_in * dpi).floor
|
|
26
|
+
@default_height = (height_in * dpi).floor
|
|
27
|
+
@warnings = []
|
|
28
|
+
@images = {} # ~DG store, keyed by "R:NAME.GRF"
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def render(zpl)
|
|
32
|
+
commands = Parser.parse(zpl)
|
|
33
|
+
labels = []
|
|
34
|
+
reset_label_state
|
|
35
|
+
|
|
36
|
+
commands.each do |cmd|
|
|
37
|
+
case cmd.name
|
|
38
|
+
when "XA"
|
|
39
|
+
reset_label_state
|
|
40
|
+
when "XZ"
|
|
41
|
+
flush_field
|
|
42
|
+
labels << @canvas if @canvas
|
|
43
|
+
@canvas = nil
|
|
44
|
+
else
|
|
45
|
+
execute(cmd)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
labels << @canvas if @canvas && dirty? # tolerate missing ^XZ
|
|
49
|
+
labels
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
def reset_label_state
|
|
55
|
+
@canvas = Canvas.new(@default_width, @default_height)
|
|
56
|
+
@lh = [0, 0]
|
|
57
|
+
@fw = :n
|
|
58
|
+
@default_font = { name: "0", h: nil, w: nil }
|
|
59
|
+
@by = { module: DEFAULT_MODULE_WIDTH, ratio: DEFAULT_RATIO, height: DEFAULT_BARCODE_HEIGHT }
|
|
60
|
+
@encoding = 0
|
|
61
|
+
@pw = nil
|
|
62
|
+
@dirty = false
|
|
63
|
+
reset_field
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def print_width_offset
|
|
67
|
+
return 0 unless @pw && @pw < @canvas.width
|
|
68
|
+
|
|
69
|
+
(@canvas.width - @pw) / 2
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def reset_field
|
|
73
|
+
@field = {
|
|
74
|
+
x: 0, y: 0, typeset: false, origin_set: false,
|
|
75
|
+
font: nil, barcode: nil, graphic: nil,
|
|
76
|
+
reverse: false, block: nil, hex: false, hex_char: "_",
|
|
77
|
+
data: nil, orientation: nil
|
|
78
|
+
}
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def dirty?
|
|
82
|
+
@dirty
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def execute(cmd)
|
|
86
|
+
p = cmd.args
|
|
87
|
+
case cmd.name
|
|
88
|
+
when "FO", "FT"
|
|
89
|
+
flush_field
|
|
90
|
+
@field[:x] = int(p[0], 0) + @lh[0] + print_width_offset
|
|
91
|
+
@field[:y] = int(p[1], 0) + @lh[1]
|
|
92
|
+
@field[:typeset] = cmd.name == "FT"
|
|
93
|
+
@field[:origin_set] = true
|
|
94
|
+
when "FS"
|
|
95
|
+
flush_field
|
|
96
|
+
when "FD", "FV"
|
|
97
|
+
@field[:data] = cmd.params
|
|
98
|
+
when "SN"
|
|
99
|
+
@field[:data] = cmd.args[0].to_s
|
|
100
|
+
when "FH"
|
|
101
|
+
@field[:hex] = true
|
|
102
|
+
@field[:hex_char] = p[0].to_s.empty? ? "_" : p[0][0]
|
|
103
|
+
when "FR"
|
|
104
|
+
@field[:reverse] = true
|
|
105
|
+
when "FB"
|
|
106
|
+
@field[:block] = {
|
|
107
|
+
width: int(p[0], 0), lines: int(p[1], 1),
|
|
108
|
+
line_gap: int(p[2], 0), just: (p[3] || "L").upcase
|
|
109
|
+
}
|
|
110
|
+
when "A"
|
|
111
|
+
@field[:font] = font_from_params(p)
|
|
112
|
+
@field[:orientation] = @field[:font][:orientation]
|
|
113
|
+
when "A@"
|
|
114
|
+
# downloaded scalable font: fall back to default font metrics
|
|
115
|
+
@field[:font] = { name: "0", h: int(p[1], nil), w: int(p[2], nil) }
|
|
116
|
+
@field[:orientation] = orient(p[0])
|
|
117
|
+
when "CF"
|
|
118
|
+
@default_font = { name: (p[0].to_s.empty? ? @default_font[:name] : p[0]),
|
|
119
|
+
h: int(p[1], @default_font[:h]), w: int(p[2], @default_font[:w]) }
|
|
120
|
+
when "CI"
|
|
121
|
+
@encoding = int(p[0], 0)
|
|
122
|
+
when "FW"
|
|
123
|
+
@fw = orient(p[0])
|
|
124
|
+
# ^FW between ^A and ^FD overrides the field's orientation,
|
|
125
|
+
# matching printer behavior (last orientation wins).
|
|
126
|
+
@field[:orientation] = @fw if @field[:origin_set]
|
|
127
|
+
when "BY"
|
|
128
|
+
@by[:module] = int(p[0], @by[:module]).clamp(1, 10)
|
|
129
|
+
@by[:ratio] = (p[1].to_s.empty? ? @by[:ratio] : p[1].to_f).clamp(2.0, 3.0)
|
|
130
|
+
@by[:height] = int(p[2], @by[:height])
|
|
131
|
+
when "BC"
|
|
132
|
+
@field[:barcode] = {
|
|
133
|
+
type: :code128, orientation: orient(p[0]),
|
|
134
|
+
height: int(p[1], nil), interp: yn(p[2], true), above: yn(p[3], false),
|
|
135
|
+
check: yn(p[4], false), mode: (p[5].to_s.empty? ? "N" : p[5].upcase)
|
|
136
|
+
}
|
|
137
|
+
@field[:orientation] = @field[:barcode][:orientation]
|
|
138
|
+
when "B3"
|
|
139
|
+
@field[:barcode] = {
|
|
140
|
+
type: :code39, orientation: orient(p[0]), check: yn(p[1], false),
|
|
141
|
+
height: int(p[2], nil), interp: yn(p[3], true), above: yn(p[4], false)
|
|
142
|
+
}
|
|
143
|
+
@field[:orientation] = @field[:barcode][:orientation]
|
|
144
|
+
when "B2"
|
|
145
|
+
@field[:barcode] = {
|
|
146
|
+
type: :i2of5, orientation: orient(p[0]), height: int(p[1], nil),
|
|
147
|
+
interp: yn(p[2], true), above: yn(p[3], false), check: yn(p[4], false)
|
|
148
|
+
}
|
|
149
|
+
@field[:orientation] = @field[:barcode][:orientation]
|
|
150
|
+
when "B7"
|
|
151
|
+
@field[:barcode] = {
|
|
152
|
+
type: :pdf417, orientation: orient(p[0]), row_height: int(p[1], nil),
|
|
153
|
+
security: int(p[2], nil), columns: int(p[3], 0), rows: int(p[4], 0),
|
|
154
|
+
truncate: yn(p[5], false)
|
|
155
|
+
}
|
|
156
|
+
@field[:orientation] = @field[:barcode][:orientation]
|
|
157
|
+
when "BQ"
|
|
158
|
+
@field[:barcode] = {
|
|
159
|
+
type: :qr, orientation: orient(p[0]), model: int(p[1], 2),
|
|
160
|
+
magnification: int(p[2], default_qr_magnification),
|
|
161
|
+
ecc: (p[3].to_s.empty? ? "Q" : p[3].upcase), mask: int(p[4], 7)
|
|
162
|
+
}
|
|
163
|
+
@field[:orientation] = @field[:barcode][:orientation]
|
|
164
|
+
when "GB"
|
|
165
|
+
@field[:graphic] = { type: :box, w: int(p[0], 1), h: int(p[1], 1),
|
|
166
|
+
t: int(p[2], 1), color: color(p[3]), round: int(p[4], 0) }
|
|
167
|
+
when "GC"
|
|
168
|
+
@field[:graphic] = { type: :circle, d: int(p[0], 3), t: int(p[1], 1), color: color(p[2]) }
|
|
169
|
+
when "GD"
|
|
170
|
+
@field[:graphic] = { type: :diagonal, w: int(p[0], 3), h: int(p[1], 3),
|
|
171
|
+
t: int(p[2], 1), color: color(p[3]), dir: (p[4] || "R").upcase }
|
|
172
|
+
when "GF"
|
|
173
|
+
@field[:graphic] = parse_gf(cmd.params)
|
|
174
|
+
when "XG"
|
|
175
|
+
name = normalize_image_name(p[0])
|
|
176
|
+
mx = int(p[1], 1).clamp(1, 10)
|
|
177
|
+
my = int(p[2], 1).clamp(1, 10)
|
|
178
|
+
@field[:graphic] = { type: :image, name: name, mx: mx, my: my }
|
|
179
|
+
when "DG"
|
|
180
|
+
store_dg(cmd.params)
|
|
181
|
+
when "PW"
|
|
182
|
+
# ^PW sets the printable width; the label canvas never shrinks and
|
|
183
|
+
# a narrower print area is centered on the label, like the printer.
|
|
184
|
+
@pw = int(p[0], nil)
|
|
185
|
+
resize_canvas(width: [@pw || 0, @canvas.width].max)
|
|
186
|
+
when "LL"
|
|
187
|
+
resize_canvas(height: [int(p[0], @canvas.height), @canvas.height].max)
|
|
188
|
+
when "LH"
|
|
189
|
+
@lh = [int(p[0], 0), int(p[1], 0)]
|
|
190
|
+
when "FX", "PQ", "MC", "MD", "MM", "MN", "MT", "PR", "PO", "LR", "LS",
|
|
191
|
+
"CW", "JM", "PM", "SD", "SZ", "XB", "DF", "EG", "ID", "TA", "JU",
|
|
192
|
+
"PF", "PH", "SL", "SO", "ST", "SX", "SQ", "WD", "HH", "HS", "JS"
|
|
193
|
+
# no visual effect at render time; ignored
|
|
194
|
+
else
|
|
195
|
+
problem("unsupported command: #{cmd.prefix == :tilde ? '~' : '^'}#{cmd.name}")
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# ---- field flushing -----------------------------------------------------
|
|
200
|
+
|
|
201
|
+
def flush_field
|
|
202
|
+
f = @field
|
|
203
|
+
begin
|
|
204
|
+
if f[:graphic] && f[:origin_set]
|
|
205
|
+
draw_graphic(f)
|
|
206
|
+
elsif f[:data]
|
|
207
|
+
if f[:barcode]
|
|
208
|
+
draw_barcode(f)
|
|
209
|
+
else
|
|
210
|
+
draw_text(f)
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
rescue StandardError => e
|
|
214
|
+
problem("#{field_label(f)} at (#{f[:x]},#{f[:y]}) failed: #{e.message}")
|
|
215
|
+
end
|
|
216
|
+
reset_field
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def field_label(f)
|
|
220
|
+
if f[:barcode]
|
|
221
|
+
"#{f[:barcode][:type]} barcode field"
|
|
222
|
+
elsif f[:graphic]
|
|
223
|
+
"#{f[:graphic][:type]} graphic field"
|
|
224
|
+
else
|
|
225
|
+
"text field"
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
# Record a recoverable problem, or raise it in strict mode. When called
|
|
230
|
+
# from a rescue block, Ruby chains the original exception as #cause.
|
|
231
|
+
def problem(message)
|
|
232
|
+
raise Error, message if @strict
|
|
233
|
+
|
|
234
|
+
@warnings << message
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def field_data(f)
|
|
238
|
+
data = f[:data].to_s
|
|
239
|
+
if f[:hex]
|
|
240
|
+
hx = Regexp.escape(f[:hex_char])
|
|
241
|
+
data = data.gsub(/#{hx}([0-9A-Fa-f]{2})/) { [$1].pack("H2") }
|
|
242
|
+
data.force_encoding(@encoding.to_i >= 28 ? Encoding::UTF_8 : Encoding::ISO_8859_1)
|
|
243
|
+
data = data.encode(Encoding::UTF_8, invalid: :replace, undef: :replace, replace: "?")
|
|
244
|
+
end
|
|
245
|
+
data
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
# ---- text ---------------------------------------------------------------
|
|
249
|
+
|
|
250
|
+
def draw_text(f)
|
|
251
|
+
font = f[:font] || @default_font
|
|
252
|
+
face = Font.face_for(font[:name], font[:h], font[:w])
|
|
253
|
+
rotation = f[:orientation] || font[:orientation] || @fw
|
|
254
|
+
text = field_data(f)
|
|
255
|
+
return if text.empty?
|
|
256
|
+
|
|
257
|
+
lines = if f[:block]
|
|
258
|
+
wrap_block(text, f[:block], face)
|
|
259
|
+
else
|
|
260
|
+
[text.gsub("\\&", " ")]
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
line_h = Font.line_height(face) + (f[:block] ? f[:block][:line_gap] : 0)
|
|
264
|
+
max_w = lines.map { |l| Font.measure(l, face) }.max || 0
|
|
265
|
+
block_w = f[:block] ? [f[:block][:width], max_w].max : max_w
|
|
266
|
+
bitmap = Canvas.new([block_w, 1].max, [lines.length * line_h, 1].max)
|
|
267
|
+
lines.each_with_index do |line, li|
|
|
268
|
+
line_w = Font.measure(line, face)
|
|
269
|
+
x = case f[:block] && f[:block][:just]
|
|
270
|
+
when "C" then (block_w - line_w) / 2
|
|
271
|
+
when "R" then block_w - line_w
|
|
272
|
+
else 0
|
|
273
|
+
end
|
|
274
|
+
Font.draw_line(bitmap, [x, 0].max, li * line_h, line, face)
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
x = f[:x]
|
|
278
|
+
y = f[:y]
|
|
279
|
+
if f[:typeset]
|
|
280
|
+
rw, rh = Canvas.rotated_size(bitmap.width, bitmap.height, rotation)
|
|
281
|
+
y -= rh
|
|
282
|
+
rw # unused; kept for clarity
|
|
283
|
+
end
|
|
284
|
+
@canvas.blit(bitmap, x, y, rotation: rotation, mode: f[:reverse] ? :xor : :paint)
|
|
285
|
+
@dirty = true
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
def wrap_block(text, block, face)
|
|
289
|
+
max_w = block[:width].positive? ? block[:width] : Font.measure(text, face)
|
|
290
|
+
lines = []
|
|
291
|
+
text.split("\\&").each do |paragraph|
|
|
292
|
+
words = paragraph.split(/ +/)
|
|
293
|
+
current = +""
|
|
294
|
+
words.each do |word|
|
|
295
|
+
candidate = current.empty? ? word : "#{current} #{word}"
|
|
296
|
+
if Font.measure(candidate, face) <= max_w
|
|
297
|
+
current = candidate
|
|
298
|
+
else
|
|
299
|
+
lines << current unless current.empty?
|
|
300
|
+
# hard-break words wider than the block
|
|
301
|
+
while Font.measure(word, face) > max_w && word.length > 1
|
|
302
|
+
head = word.length
|
|
303
|
+
head -= 1 while head > 1 && Font.measure(word[0, head], face) > max_w
|
|
304
|
+
lines << word[0, head]
|
|
305
|
+
word = word[head..]
|
|
306
|
+
end
|
|
307
|
+
current = word
|
|
308
|
+
end
|
|
309
|
+
end
|
|
310
|
+
lines << current
|
|
311
|
+
end
|
|
312
|
+
lines.take(block[:lines])
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
# ---- barcodes -----------------------------------------------------------
|
|
316
|
+
|
|
317
|
+
def draw_barcode(f)
|
|
318
|
+
bc = f[:barcode]
|
|
319
|
+
rotation = f[:orientation] || bc[:orientation] || @fw
|
|
320
|
+
data = field_data(f)
|
|
321
|
+
return if data.empty?
|
|
322
|
+
|
|
323
|
+
bitmap = case bc[:type]
|
|
324
|
+
when :code128 then linear_bitmap(code128_widths(data, bc), bc, data)
|
|
325
|
+
when :code39 then two_width_bitmap(Barcode::Code39.encode(data, check_digit: bc[:check]), bc)
|
|
326
|
+
when :i2of5 then two_width_bitmap(Barcode::Interleaved2of5.encode(data, check_digit: bc[:check]), bc)
|
|
327
|
+
when :pdf417 then pdf417_bitmap(data, bc)
|
|
328
|
+
when :qr then qr_bitmap(data, bc)
|
|
329
|
+
end
|
|
330
|
+
return unless bitmap
|
|
331
|
+
|
|
332
|
+
x = f[:x]
|
|
333
|
+
y = f[:y]
|
|
334
|
+
if f[:typeset]
|
|
335
|
+
_, rh = Canvas.rotated_size(bitmap.width, bitmap.height, rotation)
|
|
336
|
+
y -= rh
|
|
337
|
+
end
|
|
338
|
+
@canvas.blit(bitmap, x, y, rotation: rotation, mode: f[:reverse] ? :xor : :paint)
|
|
339
|
+
@dirty = true
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
def code128_widths(data, bc)
|
|
343
|
+
result = Barcode::Code128.encode(data, mode: bc[:mode], ucc_check: bc[:check])
|
|
344
|
+
modules = Barcode::Code128.modules(result.values)
|
|
345
|
+
widths = modules.each_char.map.with_index { |c, i| [c.to_i * @by[:module], i.even?] }
|
|
346
|
+
[widths, result.interpretation]
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
def linear_bitmap((widths, interp), bc, _data)
|
|
350
|
+
height = bc[:height] || @by[:height]
|
|
351
|
+
height = @by[:height] if height <= 0
|
|
352
|
+
total_w = widths.sum { |w, _| w }
|
|
353
|
+
compose_linear(widths, total_w, height, bc, interp)
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
def two_width_bitmap(result, bc)
|
|
357
|
+
narrow = @by[:module]
|
|
358
|
+
wide = (@by[:ratio] * narrow).round
|
|
359
|
+
widths = result.elements.map { |kind, bar| [kind == :wide ? wide : narrow, bar] }
|
|
360
|
+
height = bc[:height] || @by[:height]
|
|
361
|
+
height = @by[:height] if height <= 0
|
|
362
|
+
total_w = widths.sum { |w, _| w }
|
|
363
|
+
compose_linear(widths, total_w, height, bc, result.interpretation)
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
def compose_linear(widths, total_w, height, bc, interp)
|
|
367
|
+
show_interp = bc[:interp] && !interp.to_s.empty?
|
|
368
|
+
face = interpretation_face(interp, total_w)
|
|
369
|
+
interp_h = Font.line_height(face) + 3
|
|
370
|
+
full_h = height + (show_interp ? interp_h : 0)
|
|
371
|
+
bitmap = Canvas.new([total_w, 1].max, full_h)
|
|
372
|
+
|
|
373
|
+
bars_y = show_interp && bc[:above] ? interp_h : 0
|
|
374
|
+
x = 0
|
|
375
|
+
widths.each do |w, bar|
|
|
376
|
+
bitmap.fill_rect(x, bars_y, w, height) if bar
|
|
377
|
+
x += w
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
if show_interp
|
|
381
|
+
text_y = bc[:above] ? 0 : height + 1
|
|
382
|
+
text_x = [(total_w - Font.measure(interp, face)) / 2, 0].max
|
|
383
|
+
Font.draw_line(bitmap, text_x, text_y, interp, face)
|
|
384
|
+
end
|
|
385
|
+
bitmap
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
def interpretation_face(interp, total_w)
|
|
389
|
+
size = [@dpmm * 2, 10].max # ~2 mm tall interpretation line
|
|
390
|
+
face = Font.face_for("0", size, size)
|
|
391
|
+
while Font.measure(interp, face) > total_w && size > 5
|
|
392
|
+
size -= 1
|
|
393
|
+
face = Font.face_for("0", size, size)
|
|
394
|
+
end
|
|
395
|
+
face
|
|
396
|
+
end
|
|
397
|
+
|
|
398
|
+
def pdf417_bitmap(data, bc)
|
|
399
|
+
symbol = Barcode::Pdf417.encode(
|
|
400
|
+
data.b, security: bc[:security], columns: bc[:columns],
|
|
401
|
+
rows: bc[:rows], truncated: bc[:truncate]
|
|
402
|
+
)
|
|
403
|
+
mod_w = @by[:module]
|
|
404
|
+
row_h = bc[:row_height]
|
|
405
|
+
row_h = [mod_w * 3, 4].max if row_h.nil? || row_h < 1
|
|
406
|
+
width = symbol.patterns.first.sum { |_, count| count } * mod_w
|
|
407
|
+
bitmap = Canvas.new(width, symbol.rows * row_h)
|
|
408
|
+
symbol.patterns.each_with_index do |row, row_no|
|
|
409
|
+
x = 0
|
|
410
|
+
y = row_no * row_h
|
|
411
|
+
row.each do |pattern, count|
|
|
412
|
+
(count - 1).downto(0) do |bit|
|
|
413
|
+
bitmap.fill_rect(x, y, mod_w, row_h) if (pattern >> bit) & 1 == 1
|
|
414
|
+
x += mod_w
|
|
415
|
+
end
|
|
416
|
+
end
|
|
417
|
+
end
|
|
418
|
+
bitmap
|
|
419
|
+
end
|
|
420
|
+
|
|
421
|
+
def qr_bitmap(data, bc)
|
|
422
|
+
result = Barcode::Qr.encode(data, default_ecc: bc[:ecc])
|
|
423
|
+
mag = bc[:magnification].clamp(1, 10)
|
|
424
|
+
size = result.modules.length
|
|
425
|
+
bitmap = Canvas.new(size * mag, size * mag)
|
|
426
|
+
result.modules.each_with_index do |row, ry|
|
|
427
|
+
row.each_with_index do |mod, rx|
|
|
428
|
+
bitmap.fill_rect(rx * mag, ry * mag, mag, mag) if mod
|
|
429
|
+
end
|
|
430
|
+
end
|
|
431
|
+
bitmap
|
|
432
|
+
end
|
|
433
|
+
|
|
434
|
+
def default_qr_magnification
|
|
435
|
+
case @dpmm
|
|
436
|
+
when 0..6 then 1
|
|
437
|
+
when 7..8 then 2
|
|
438
|
+
when 9..14 then 3
|
|
439
|
+
else 6
|
|
440
|
+
end
|
|
441
|
+
end
|
|
442
|
+
|
|
443
|
+
# ---- graphics -----------------------------------------------------------
|
|
444
|
+
|
|
445
|
+
def draw_graphic(f)
|
|
446
|
+
g = f[:graphic]
|
|
447
|
+
mode = if f[:reverse]
|
|
448
|
+
:xor
|
|
449
|
+
else
|
|
450
|
+
g[:color] == :white ? :erase : :paint
|
|
451
|
+
end
|
|
452
|
+
|
|
453
|
+
bitmap = case g[:type]
|
|
454
|
+
when :box then box_bitmap(g)
|
|
455
|
+
when :circle then circle_bitmap(g)
|
|
456
|
+
when :diagonal then diagonal_bitmap(g)
|
|
457
|
+
when :gf then g[:canvas]
|
|
458
|
+
when :image then image_bitmap(g)
|
|
459
|
+
end
|
|
460
|
+
return unless bitmap
|
|
461
|
+
|
|
462
|
+
y = f[:y]
|
|
463
|
+
y -= bitmap.height if f[:typeset]
|
|
464
|
+
@canvas.blit(bitmap, f[:x], y, rotation: :n, mode: mode)
|
|
465
|
+
@dirty = true
|
|
466
|
+
end
|
|
467
|
+
|
|
468
|
+
def box_bitmap(g)
|
|
469
|
+
t = [g[:t], 1].max
|
|
470
|
+
w = [g[:w], t].max
|
|
471
|
+
h = [g[:h], t].max
|
|
472
|
+
bitmap = Canvas.new(w, h)
|
|
473
|
+
if g[:round].to_i.positive?
|
|
474
|
+
rounded_box(bitmap, w, h, t, g[:round].to_i.clamp(0, 8))
|
|
475
|
+
else
|
|
476
|
+
bitmap.fill_rect(0, 0, w, t)
|
|
477
|
+
bitmap.fill_rect(0, h - t, w, t)
|
|
478
|
+
bitmap.fill_rect(0, 0, t, h)
|
|
479
|
+
bitmap.fill_rect(w - t, 0, t, h)
|
|
480
|
+
end
|
|
481
|
+
bitmap
|
|
482
|
+
end
|
|
483
|
+
|
|
484
|
+
def rounded_box(bitmap, w, h, t, rounding)
|
|
485
|
+
radius = (rounding * [w, h].min) / 16
|
|
486
|
+
(0...h).each do |y|
|
|
487
|
+
(0...w).each do |x|
|
|
488
|
+
bitmap.set(x, y) if rounded_border?(x, y, w, h, t, radius)
|
|
489
|
+
end
|
|
490
|
+
end
|
|
491
|
+
end
|
|
492
|
+
|
|
493
|
+
def rounded_border?(x, y, w, h, t, r)
|
|
494
|
+
inside_rounded?(x, y, w, h, r) && !inside_rounded?(x - t, y - t, w - 2 * t, h - 2 * t, [r - t, 0].max)
|
|
495
|
+
end
|
|
496
|
+
|
|
497
|
+
def inside_rounded?(x, y, w, h, r)
|
|
498
|
+
return false if x.negative? || y.negative? || x >= w || y >= h
|
|
499
|
+
return true if r <= 0
|
|
500
|
+
|
|
501
|
+
cx = x < r ? r : (x >= w - r ? w - r - 1 : nil)
|
|
502
|
+
cy = y < r ? r : (y >= h - r ? h - r - 1 : nil)
|
|
503
|
+
return true if cx.nil? || cy.nil?
|
|
504
|
+
|
|
505
|
+
(x - cx)**2 + (y - cy)**2 <= r * r
|
|
506
|
+
end
|
|
507
|
+
|
|
508
|
+
def circle_bitmap(g)
|
|
509
|
+
d = [g[:d], 3].max
|
|
510
|
+
t = g[:t].clamp(1, d / 2 + 1)
|
|
511
|
+
bitmap = Canvas.new(d, d)
|
|
512
|
+
r_out = d / 2.0
|
|
513
|
+
r_in = r_out - t
|
|
514
|
+
cx = cy = (d - 1) / 2.0
|
|
515
|
+
(0...d).each do |y|
|
|
516
|
+
(0...d).each do |x|
|
|
517
|
+
dist2 = (x - cx)**2 + (y - cy)**2
|
|
518
|
+
bitmap.set(x, y) if dist2 <= r_out**2 && dist2 >= r_in**2 * (r_in.positive? ? 1 : 0)
|
|
519
|
+
end
|
|
520
|
+
end
|
|
521
|
+
bitmap
|
|
522
|
+
end
|
|
523
|
+
|
|
524
|
+
def diagonal_bitmap(g)
|
|
525
|
+
w = [g[:w], 3].max
|
|
526
|
+
h = [g[:h], 3].max
|
|
527
|
+
t = [g[:t], 1].max
|
|
528
|
+
bitmap = Canvas.new(w, h)
|
|
529
|
+
(0...w).each do |x|
|
|
530
|
+
y = g[:dir] == "L" ? (x * (h - 1) / [w - 1, 1].max) : (h - 1 - x * (h - 1) / [w - 1, 1].max)
|
|
531
|
+
(0...t).each do |dy|
|
|
532
|
+
bitmap.set(x, y + dy - t / 2)
|
|
533
|
+
bitmap.set(x + 1, y + dy - t / 2) if x + 1 < w # thicken against aliasing gaps
|
|
534
|
+
end
|
|
535
|
+
end
|
|
536
|
+
bitmap
|
|
537
|
+
end
|
|
538
|
+
|
|
539
|
+
def parse_gf(params)
|
|
540
|
+
# ^GFa,b,c,d,data - data may itself contain commas only for binary,
|
|
541
|
+
# so split just the first four fields.
|
|
542
|
+
parts = params.split(",", 5)
|
|
543
|
+
_format = (parts[0] || "A").strip.upcase
|
|
544
|
+
total = parts[2].to_i
|
|
545
|
+
per_row = parts[3].to_i
|
|
546
|
+
data = parts[4].to_s
|
|
547
|
+
{ type: :gf, canvas: GraphicField.decode(data, per_row, total) }
|
|
548
|
+
end
|
|
549
|
+
|
|
550
|
+
def store_dg(params)
|
|
551
|
+
parts = params.split(",", 4)
|
|
552
|
+
name = normalize_image_name(parts[0])
|
|
553
|
+
total = parts[1].to_i
|
|
554
|
+
per_row = parts[2].to_i
|
|
555
|
+
data = parts[3].to_s
|
|
556
|
+
@images[name] = GraphicField.decode(data, per_row, total)
|
|
557
|
+
end
|
|
558
|
+
|
|
559
|
+
def image_bitmap(g)
|
|
560
|
+
src = @images[g[:name]]
|
|
561
|
+
unless src
|
|
562
|
+
problem("^XG references missing image #{g[:name]}")
|
|
563
|
+
return nil
|
|
564
|
+
end
|
|
565
|
+
return src if g[:mx] == 1 && g[:my] == 1
|
|
566
|
+
|
|
567
|
+
scaled = Canvas.new(src.width * g[:mx], src.height * g[:my])
|
|
568
|
+
(0...src.height).each do |y|
|
|
569
|
+
(0...src.width).each do |x|
|
|
570
|
+
scaled.fill_rect(x * g[:mx], y * g[:my], g[:mx], g[:my]) if src.get(x, y)
|
|
571
|
+
end
|
|
572
|
+
end
|
|
573
|
+
scaled
|
|
574
|
+
end
|
|
575
|
+
|
|
576
|
+
def normalize_image_name(raw)
|
|
577
|
+
name = raw.to_s.strip.upcase
|
|
578
|
+
name = "R:#{name}" unless name.include?(":")
|
|
579
|
+
name += ".GRF" unless name.include?(".")
|
|
580
|
+
name
|
|
581
|
+
end
|
|
582
|
+
|
|
583
|
+
# ---- misc ---------------------------------------------------------------
|
|
584
|
+
|
|
585
|
+
def resize_canvas(width: nil, height: nil)
|
|
586
|
+
w = width || @canvas.width
|
|
587
|
+
h = height || @canvas.height
|
|
588
|
+
return if w == @canvas.width && h == @canvas.height
|
|
589
|
+
|
|
590
|
+
fresh = Canvas.new(w, h)
|
|
591
|
+
fresh.blit(@canvas, 0, 0) if dirty?
|
|
592
|
+
@canvas = fresh
|
|
593
|
+
end
|
|
594
|
+
|
|
595
|
+
def font_from_params(p)
|
|
596
|
+
name = p[0].to_s[0] || "0"
|
|
597
|
+
{ name: name, orientation: orient(p[0].to_s[1]), h: int(p[1], nil), w: int(p[2], nil) }
|
|
598
|
+
end
|
|
599
|
+
|
|
600
|
+
def orient(val)
|
|
601
|
+
case val.to_s.upcase
|
|
602
|
+
when "R" then :r
|
|
603
|
+
when "I" then :i
|
|
604
|
+
when "B" then :b
|
|
605
|
+
when "N" then :n
|
|
606
|
+
end
|
|
607
|
+
end
|
|
608
|
+
|
|
609
|
+
def color(val)
|
|
610
|
+
val.to_s.upcase == "W" ? :white : :black
|
|
611
|
+
end
|
|
612
|
+
|
|
613
|
+
def yn(val, default)
|
|
614
|
+
return default if val.nil? || val.to_s.empty?
|
|
615
|
+
|
|
616
|
+
val.to_s.upcase == "Y"
|
|
617
|
+
end
|
|
618
|
+
|
|
619
|
+
def int(val, default)
|
|
620
|
+
return default if val.nil? || val.to_s.strip.empty?
|
|
621
|
+
|
|
622
|
+
Integer(val.to_s.strip, 10)
|
|
623
|
+
rescue ArgumentError
|
|
624
|
+
default
|
|
625
|
+
end
|
|
626
|
+
end
|
|
627
|
+
end
|