dymo_render 0.0.1

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.
Files changed (3) hide show
  1. checksums.yaml +7 -0
  2. data/lib/dymo_render.rb +290 -0
  3. metadata +101 -0
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: ec9eabf00efe17f05ec96bfed3c9f127c40b01e9296829a226548233bdc2489c
4
+ data.tar.gz: b3296742b51b8c22375ee3f63a9c6a49a8e1e911caaf0e273ddde5140973a5ec
5
+ SHA512:
6
+ metadata.gz: 56176fe6afd97113fb882a587723fdd7774831a473118d6b655ad2ac01aac1a6c2f85d8ceceeeb84713dabe4406e39f770a04304eb4f55e78efc21d1be7f522d
7
+ data.tar.gz: dcce264b213376938ab78495d1d39c1b3b4f5f30165a69813ae2c6059018b3b29122a2fbf81bb2191e6ea8e53006117b6e348fd89f2f3db8b8b1f0edb3b4e63d
@@ -0,0 +1,290 @@
1
+ require "dymo_render/version"
2
+ require 'prawn'
3
+ require 'nokogiri'
4
+ require 'barby'
5
+ require 'barby/barcode/code_128'
6
+ require 'barby/barcode/qr_code'
7
+ require 'barby/outputter/prawn_outputter'
8
+
9
+ class DymoRender
10
+ FONT_DIRS_MACOS = [
11
+ "#{ENV["HOME"]}/Library/Fonts",
12
+ "/Library/Fonts",
13
+ "/Network/Library/Fonts",
14
+ "/System/Library/Fonts",
15
+ ].freeze
16
+
17
+ FONT_DIRS_LINUX = ['/usr/share/fonts/truetype/msttcorefonts'].freeze
18
+
19
+ FONT_DIRS = (RUBY_PLATFORM =~ /darwin/ ? FONT_DIRS_MACOS : FONT_DIRS_LINUX).freeze
20
+
21
+ # 1440 twips per inch (20 per PDF point)
22
+ TWIP = 1440.0
23
+
24
+ # 72 PDF points per inch
25
+ PDF_POINT = 72.0
26
+
27
+ # TODO: add more label sizes here
28
+ SIZES = {
29
+ '30252 Address' => [252, 81],
30
+ '30330 Return Address' => [144.1, 54],
31
+ '30334 2-1/4 in x 1-1/4 in' => [162, 90],
32
+ }.freeze
33
+
34
+ # This may be needed for some label types. Zero for now.
35
+ LEFT_MARGIN = 0
36
+
37
+ attr_reader :doc, :font_dirs, :pdf
38
+
39
+ def initialize(xml:, font_dirs: FONT_DIRS, params: {})
40
+ @xml = xml
41
+ @font_dirs = font_dirs
42
+ @params = params
43
+ @doc = Nokogiri::XML(xml)
44
+ end
45
+
46
+ def render
47
+ build_pdf
48
+ doc.css('ObjectInfo').each do |object|
49
+ pdf.go_to_page 1
50
+ render_object(object)
51
+ end
52
+ pdf.render
53
+ end
54
+
55
+ def has_graphics?
56
+ !doc.css('BarcodeObject').empty?
57
+ end
58
+
59
+ def orientation
60
+ if doc.css('PaperOrientation').first&.text == 'Landscape'
61
+ 'landscape'
62
+ else
63
+ 'portrait'
64
+ end
65
+ end
66
+
67
+ def paper_height
68
+ paper_size[0]
69
+ end
70
+
71
+ def paper_width
72
+ paper_size[1]
73
+ end
74
+
75
+ def self.font_file_for_family(font_dirs, family)
76
+ extensions = [".ttf", ".dfont", ".ttc"]
77
+ names = extensions.map { |ext| [family, ext].join }
78
+
79
+ font_dirs.each do |dir|
80
+ names.each do |filename|
81
+ file = File.join(dir, filename)
82
+ return file if File.exists?(file)
83
+ end
84
+ end
85
+ nil # not found
86
+ end
87
+
88
+ private
89
+
90
+ def paper_size
91
+ @paper_size ||= begin
92
+ elm = doc.css('PaperName').first
93
+ elm && SIZES[elm.text] || SIZES.values.first
94
+ end
95
+ end
96
+
97
+ def build_pdf
98
+ @pdf = Prawn::Document.new(page_size: paper_size, margin: [0, 0, 0, 0])
99
+ end
100
+
101
+ def render_object(object_info)
102
+ bounds = object_info.css('Bounds').first.attributes
103
+ x = ((bounds['X'].value.to_i / TWIP) - LEFT_MARGIN) * PDF_POINT
104
+ y = paper_size.last - (bounds['Y'].value.to_i / TWIP * PDF_POINT)
105
+ width = ((bounds['Width'].value.to_i / TWIP) - LEFT_MARGIN) * PDF_POINT
106
+ height = bounds['Height'].value.to_i / TWIP * PDF_POINT
107
+
108
+ object = object_info.children.find(&:element?)
109
+ case object.name
110
+ when 'TextObject'
111
+ render_text_object(object, x, y, width, height)
112
+ when 'ShapeObject'
113
+ render_shape_object(object, x, y, width, height)
114
+ when 'BarcodeObject'
115
+ render_barcode_object(object, x, y, width, height)
116
+ else
117
+ puts "unsupported object type: #{object&.name}"
118
+ end
119
+ end
120
+
121
+ def render_text_object(text_object, x, y, width, height)
122
+ foreground = text_object.css('ForeColor').first
123
+ color = color_from_element(foreground)
124
+ draw_rectangle_from_object(text_object, x, y, width, height)
125
+ verticalized = text_object.css('Verticalized').first
126
+ verticalized &&= verticalized.text == 'True'
127
+ name = text_object.css('Name').first
128
+ name &&= name.text
129
+ elements = text_object.css('StyledText Element')
130
+ return if elements.empty?
131
+
132
+ font = elements.first.css('Attributes Font').first
133
+ font_family = font ? font.attributes['Family'].value : 'Helvetica'
134
+ size = font.attributes['Size'].value.to_i
135
+ strings = elements.map do |element|
136
+ string = @params[name] || element.css('String').first.text
137
+ string = string.each_char.map { |c| [c, "\n"] }.flatten.join if verticalized
138
+ string
139
+ end
140
+ valign = valign_from_text_object(text_object)
141
+ begin
142
+ pdf.fill_color color
143
+ font_file = self.class.font_file_for_family(font_dirs, font_family)
144
+ pdf.font(font_file || raise("missing font #{font_family}"))
145
+ # horizontal padding of 1 point
146
+ x += 1
147
+ width -= 2
148
+ (box, actual_size) = text_box_with_font_size(
149
+ strings.join,
150
+ size: size,
151
+ character_spacing: 0,
152
+ at: [x, y],
153
+ width: width,
154
+ height: height,
155
+ overflow: overflow_from_text_object(text_object),
156
+ align: align_from_text_object(text_object),
157
+ valign: valign,
158
+ disable_wrap_by_char: true,
159
+ single_line: !verticalized
160
+ )
161
+ # on bottom-aligned boxes, Dymo counts the height of character descenders
162
+ box.at[1] += box.descender if valign == :bottom
163
+ box.render
164
+ rescue Prawn::Errors::CannotFit
165
+ puts 'cannot fit'
166
+ end
167
+ end
168
+
169
+ def text_box_with_font_size(text, options = {})
170
+ box = Prawn::Text::Box.new(text, options.merge(document: pdf))
171
+ box.render(dry_run: true)
172
+ size = box.instance_eval { @font_size }
173
+ [box, size]
174
+ end
175
+
176
+ def draw_rectangle_from_object(text_object, x, y, width, height)
177
+ background = text_object.css('BackColor').first
178
+ return unless background
179
+ alpha = background.attributes['Alpha'].value.to_i
180
+ return if alpha.zero?
181
+ pdf.fill_color color_from_element(background)
182
+ pdf.rectangle [x, y], width, height
183
+ pdf.fill
184
+ end
185
+
186
+ def color_from_element(element)
187
+ red = element.attributes['Red'].value.to_i
188
+ green = element.attributes['Green'].value.to_i
189
+ blue = element.attributes['Blue'].value.to_i
190
+ Prawn::Graphics::Color.rgb2hex([red, green, blue])
191
+ end
192
+
193
+ VALIGNS = {
194
+ 'Top' => :top,
195
+ 'Bottom' => :bottom,
196
+ 'Middle' => :center
197
+ }.freeze
198
+
199
+ def valign_from_text_object(text_object)
200
+ VALIGNS[text_object.css('VerticalAlignment').first.text] || :top
201
+ end
202
+
203
+ ALIGNS = {
204
+ 'Left' => :left,
205
+ 'Right' => :right,
206
+ 'Center' => :center
207
+ }.freeze
208
+
209
+ def align_from_text_object(text_object)
210
+ ALIGNS[text_object.css('HorizontalAlignment').first.text] || :left
211
+ end
212
+
213
+ OVERFLOWS = {
214
+ 'None' => :truncate,
215
+ 'ShrinkToFit' => :shrink_to_fit
216
+ }.freeze
217
+
218
+ def overflow_from_text_object(text_object)
219
+ OVERFLOWS[text_object.css('TextFitMode').first.text] || :truncate
220
+ end
221
+
222
+ HORIZONTAL_LINE_VERTICAL_FUDGE_BY = -2.5
223
+
224
+ def render_shape_object(shape_object, x, y, width, height)
225
+ case shape_object.css('ShapeType').first.text
226
+ when 'HorizontalLine'
227
+ pdf.line_width height
228
+ pdf.horizontal_line x, x + width, at: y + HORIZONTAL_LINE_VERTICAL_FUDGE_BY
229
+ pdf.stroke
230
+ else
231
+ puts 'unknown shape type'
232
+ end
233
+ end
234
+
235
+ def render_barcode_object(barcode_object, x, y, width, height)
236
+ case barcode_type = barcode_object.css('Type').first.text
237
+ when 'QRCode'
238
+ content = barcode_object.css('Text').first.text
239
+ code = Barby::QrCode.new(content, level: :m)
240
+ outputter = Barby::PrawnOutputter.new(code)
241
+ num_dots = outputter.full_width # number of dots in QR
242
+
243
+ xdim = width.to_f / num_dots
244
+ ydim = height.to_f / num_dots
245
+ dim = [xdim, ydim].min
246
+
247
+ # If the resulting size in either dimension is smaller than that
248
+ # dimension's specified size, adjust so the code is centered in the
249
+ # bounding box.
250
+ xadjust = (width - dim * num_dots).to_f / 2
251
+ yadjust = (height - dim * num_dots).to_f / 2
252
+
253
+ xpos = x + xadjust
254
+ ypos = y - height + yadjust
255
+
256
+ outputter.annotate_pdf(pdf, { x: xpos, y: ypos, xdim: dim, ydim: dim });
257
+ when 'Code128Auto'
258
+ render_barcode_code128(barcode_object, x, y, width, height, nil)
259
+ when 'Code128A', 'Code128B', 'Code128C'
260
+ type = barcode_type.sub('Code128', '')
261
+ render_barcode_code128(barcode_object, x, y, width, height, type)
262
+ else
263
+ puts "unknown barcode type: #{barcode_type}"
264
+ end
265
+ end
266
+
267
+ def render_barcode_code128(barcode_object, x, y, width, height, type)
268
+ content = barcode_object.css('Text').first.text
269
+ code = Barby::Code128.new(content, type)
270
+ outputter = Barby::PrawnOutputter.new(code)
271
+
272
+ num_dots_x = outputter.full_width
273
+ xdim = width.to_f / num_dots_x
274
+ center_x = x + width.to_f / 2
275
+
276
+ # ensure mandatory 10 module widths of space on left and right of barcode:
277
+ # https://en.wikipedia.org/wiki/Code_128#Quiet_zone
278
+ max_code_width = width - 20 * xdim
279
+ if (num_dots_x * xdim) > max_code_width
280
+ width = max_code_width
281
+ xdim = max_code_width / num_dots_x
282
+ end
283
+
284
+ # center in the x direction
285
+ xpos = center_x - width.to_f / 2
286
+ ypos = y - height
287
+
288
+ outputter.annotate_pdf(pdf, { x: xpos, y: ypos, xdim: xdim, height: height });
289
+ end
290
+ end
metadata ADDED
@@ -0,0 +1,101 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: dymo_render
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Blake Gentry
8
+ - Tim Morgan
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2018-05-21 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: barby
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - "~>"
19
+ - !ruby/object:Gem::Version
20
+ version: 0.6.5
21
+ type: :runtime
22
+ prerelease: false
23
+ version_requirements: !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - "~>"
26
+ - !ruby/object:Gem::Version
27
+ version: 0.6.5
28
+ - !ruby/object:Gem::Dependency
29
+ name: nokogiri
30
+ requirement: !ruby/object:Gem::Requirement
31
+ requirements:
32
+ - - "~>"
33
+ - !ruby/object:Gem::Version
34
+ version: 1.8.2
35
+ type: :runtime
36
+ prerelease: false
37
+ version_requirements: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - "~>"
40
+ - !ruby/object:Gem::Version
41
+ version: 1.8.2
42
+ - !ruby/object:Gem::Dependency
43
+ name: prawn
44
+ requirement: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - "~>"
47
+ - !ruby/object:Gem::Version
48
+ version: 2.2.2
49
+ type: :runtime
50
+ prerelease: false
51
+ version_requirements: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - "~>"
54
+ - !ruby/object:Gem::Version
55
+ version: 2.2.2
56
+ - !ruby/object:Gem::Dependency
57
+ name: rqrcode
58
+ requirement: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - "~>"
61
+ - !ruby/object:Gem::Version
62
+ version: 0.10.1
63
+ type: :runtime
64
+ prerelease: false
65
+ version_requirements: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - "~>"
68
+ - !ruby/object:Gem::Version
69
+ version: 0.10.1
70
+ description: Render PDFs using the XML format from the DYMO Label Maker app
71
+ email: blakesgentry@gmail.com
72
+ executables: []
73
+ extensions: []
74
+ extra_rdoc_files: []
75
+ files:
76
+ - lib/dymo_render.rb
77
+ homepage: http://github.com/bgentry/dymo_render
78
+ licenses:
79
+ - BSD-2-Clause
80
+ metadata: {}
81
+ post_install_message:
82
+ rdoc_options: []
83
+ require_paths:
84
+ - lib
85
+ required_ruby_version: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ required_rubygems_version: !ruby/object:Gem::Requirement
91
+ requirements:
92
+ - - ">="
93
+ - !ruby/object:Gem::Version
94
+ version: '0'
95
+ requirements: []
96
+ rubyforge_project:
97
+ rubygems_version: 2.7.6
98
+ signing_key:
99
+ specification_version: 4
100
+ summary: Render PDFs using DYMO XML
101
+ test_files: []