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