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.
- checksums.yaml +7 -0
- data/lib/dymo_render.rb +290 -0
- metadata +101 -0
checksums.yaml
ADDED
@@ -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
|
data/lib/dymo_render.rb
ADDED
@@ -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: []
|