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,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ZplRender
4
+ # Helvetica advance widths (1/1000 em) for ASCII 32-126, from the
5
+ # Adobe Core 14 AFM metrics. Used to approximate Zebra's scalable
6
+ # CG Triumvirate (font 0), which shares Helvetica's metrics closely.
7
+ HELVETICA_WIDTHS = [
8
+ 278, 278, 355, 556, 556, 889, 667, 222, 333, 333, 389, 584,
9
+ 278, 333, 278, 278, 556, 556, 556, 556, 556, 556, 556, 556,
10
+ 556, 556, 278, 278, 584, 584, 584, 556, 1015, 667, 667, 722,
11
+ 722, 667, 611, 778, 722, 278, 500, 667, 556, 833, 722, 778,
12
+ 667, 778, 722, 667, 611, 722, 667, 944, 667, 667, 611, 278,
13
+ 278, 278, 469, 556, 222, 556, 556, 500, 556, 556, 278, 556,
14
+ 556, 222, 222, 500, 222, 833, 556, 556, 556, 556, 333, 500,
15
+ 278, 556, 500, 722, 500, 500, 500, 334, 260, 334, 584
16
+ ].freeze
17
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prawn"
4
+ require "stringio"
5
+
6
+ module ZplRender
7
+ module Output
8
+ module Pdf
9
+ module_function
10
+
11
+ # Encode one page per label Canvas. Each page has the label's exact
12
+ # physical size (canvas dots / dpmm), and the label raster is placed
13
+ # 1:1 so barcode modules stay sharp when printed.
14
+ def encode(canvases, dpmm:)
15
+ raise ArgumentError, "no labels to render" if canvases.empty?
16
+
17
+ first = canvases.first
18
+ doc = Prawn::Document.new(
19
+ page_size: page_size(first, dpmm),
20
+ margin: 0,
21
+ skip_page_creation: false,
22
+ info: { Creator: "zpl_render #{ZplRender::VERSION}" }
23
+ )
24
+ canvases.each_with_index do |canvas, i|
25
+ doc.start_new_page(size: page_size(canvas, dpmm), margin: 0) if i.positive?
26
+ png = Png.encode(canvas)
27
+ doc.image StringIO.new(png), at: [0, doc.bounds.top],
28
+ width: doc.bounds.width, height: doc.bounds.height
29
+ end
30
+ doc.render
31
+ end
32
+
33
+ def page_size(canvas, dpmm)
34
+ # dots -> mm -> points (72 pt / inch, 25.4 mm / inch)
35
+ [canvas.width / dpmm.to_f / 25.4 * 72.0, canvas.height / dpmm.to_f / 25.4 * 72.0]
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "chunky_png"
4
+
5
+ module ZplRender
6
+ module Output
7
+ module Png
8
+ module_function
9
+
10
+ # Encode a Canvas as a PNG binary string. +scale+ is an integer
11
+ # nearest-neighbor upscale (keeps barcode modules pixel-exact).
12
+ def encode(canvas, scale: 1)
13
+ scale = [scale.to_i, 1].max
14
+ image = ChunkyPNG::Image.new(canvas.width * scale, canvas.height * scale, ChunkyPNG::Color::WHITE)
15
+ black = ChunkyPNG::Color::BLACK
16
+ (0...canvas.height).each do |y|
17
+ (0...canvas.width).each do |x|
18
+ next unless canvas.get(x, y)
19
+
20
+ if scale == 1
21
+ image[x, y] = black
22
+ else
23
+ (0...scale).each do |dy|
24
+ (0...scale).each do |dx|
25
+ image[x * scale + dx, y * scale + dy] = black
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
31
+ image.to_blob(:fast_rgb)
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ZplRender
4
+ # Tokenizes a ZPL II stream into [command, params] pairs.
5
+ #
6
+ # Commands are introduced by the format prefix (^, changeable via ^CC/~CC)
7
+ # or the control prefix (~, changeable via ^CT/~CT). The command code is
8
+ # two characters, except ^A<f> where the second character is the font
9
+ # designator (^A@ remains its own command). Parameters run until the next
10
+ # prefix character.
11
+ class Parser
12
+ Command = Struct.new(:name, :params, :prefix, keyword_init: true) do
13
+ # Split comma-separated parameters, preserving empty slots.
14
+ def args
15
+ @args ||= params.split(",", -1).map(&:strip)
16
+ end
17
+ end
18
+
19
+ def self.parse(zpl)
20
+ new(zpl).parse
21
+ end
22
+
23
+ def initialize(zpl)
24
+ @zpl = zpl.to_s
25
+ end
26
+
27
+ def parse
28
+ commands = []
29
+ caret = "^"
30
+ tilde = "~"
31
+ i = 0
32
+ len = @zpl.length
33
+
34
+ while i < len
35
+ ch = @zpl[i]
36
+ unless ch == caret || ch == tilde
37
+ i += 1
38
+ next
39
+ end
40
+
41
+ prefix = ch == caret ? :caret : :tilde
42
+ code = @zpl[i + 1, 2].to_s
43
+ if code.empty?
44
+ i += 1
45
+ next
46
+ end
47
+
48
+ name = nil
49
+ font = nil
50
+ if code[0]&.upcase == "A" && code != "A@" && prefix == :caret
51
+ name = "A"
52
+ font = code[1]
53
+ else
54
+ name = code.upcase
55
+ end
56
+ i += 3 # prefix + two command characters (^A consumes its font char)
57
+
58
+ # ^CC/~CC and ^CT/~CT take effect immediately: their parameter is a
59
+ # single character that becomes the new prefix.
60
+ if name == "CC" || name == "CT"
61
+ new_prefix = @zpl[i]
62
+ if new_prefix
63
+ caret = new_prefix if name == "CC"
64
+ tilde = new_prefix if name == "CT"
65
+ i += 1
66
+ end
67
+ next
68
+ end
69
+
70
+ # parameters: everything until the next prefix character
71
+ j = i
72
+ j += 1 while j < len && @zpl[j] != caret && @zpl[j] != tilde
73
+ params = @zpl[i...j].to_s
74
+
75
+ if name == "A"
76
+ params = "#{font}#{params}"
77
+ end
78
+
79
+ # CR/LF are ignorable in ZPL streams
80
+ params = params.delete("\r\n") unless %w[GF DG DB].include?(name)
81
+ params = params.strip if %w[GF DG DB].include?(name)
82
+
83
+ commands << Command.new(name: name, params: params, prefix: prefix)
84
+ i = j
85
+ end
86
+
87
+ commands
88
+ end
89
+ end
90
+ end