dymo_render 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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: []