inkmake 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.
- checksums.yaml +7 -0
- data/bin/inkmake +3 -0
- data/lib/inkmake.rb +729 -0
- metadata +47 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 6e87e04d632d6a75ebce65acc01aa293b578f9d5
|
4
|
+
data.tar.gz: f72bbe50c0df69548ae1782fe445759926f444b4
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 03b1fbe3d36f59e06b93a28d1eb23f086d78345ceaaa95c0984f200c7b3469752f01a8984c7bcb6391cc6957c9c509a22d42212fb49268b83915570e93d5f522
|
7
|
+
data.tar.gz: a1dbb31c526e51f0561fdd2fda974f75788d2e05771801caa2032adce2d70f14a9a57d1e2d85ac9d1da6804fdbc6f58d801fd5acdecfcd2145be778b89e90c8d
|
data/bin/inkmake
ADDED
data/lib/inkmake.rb
ADDED
@@ -0,0 +1,729 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
#
|
3
|
+
# inkmake - Makefile inspired export from SVG files using Inkscape as backend
|
4
|
+
# with some added smartness.
|
5
|
+
#
|
6
|
+
# Copyright (c) 2015 <mattias.wadman@gmail.com>
|
7
|
+
#
|
8
|
+
# MIT License:
|
9
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
10
|
+
# of this software and associated documentation files (the "Software"), to deal
|
11
|
+
# in the Software without restriction, including without limitation the rights
|
12
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
13
|
+
# copies of the Software, and to permit persons to whom the Software is
|
14
|
+
# furnished to do so, subject to the following conditions:
|
15
|
+
#
|
16
|
+
# The above copyright notice and this permission notice shall be included in
|
17
|
+
# all copies or substantial portions of the Software.
|
18
|
+
#
|
19
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
20
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
21
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
22
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
23
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
24
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
25
|
+
# THE SOFTWARE.
|
26
|
+
#
|
27
|
+
# Try to stay campatible with Ruby 1.8.7 as its the default ruby
|
28
|
+
# version included in Mac OS X (at least Lion).
|
29
|
+
#
|
30
|
+
# NOTE: Rotation is done using a temporary SVG file that translate and rotate
|
31
|
+
# a double resolution bitmap and export as a bitmap to the correct resolution.
|
32
|
+
# This hack is done to get around that Inkscape cant set bitmap oversampling
|
33
|
+
# mode per file or from command line, default is 2x2 oversampling.
|
34
|
+
#
|
35
|
+
|
36
|
+
require "csv"
|
37
|
+
require "rexml/document"
|
38
|
+
require "rexml/xpath"
|
39
|
+
require "open3"
|
40
|
+
require "optparse"
|
41
|
+
require "fileutils"
|
42
|
+
require "tempfile"
|
43
|
+
require "uri"
|
44
|
+
require "pathname"
|
45
|
+
|
46
|
+
class Inkmake
|
47
|
+
@verbose = false
|
48
|
+
@inkscape_path = nil
|
49
|
+
class << self
|
50
|
+
attr :verbose, :inkscape_path
|
51
|
+
end
|
52
|
+
|
53
|
+
class InkscapeUnit
|
54
|
+
# 90dpi as reference
|
55
|
+
Units = {
|
56
|
+
"pt" => 1.25,
|
57
|
+
"pc" => 15,
|
58
|
+
"mm" => 3.543307,
|
59
|
+
"cm" => 35.43307,
|
60
|
+
"dm" => 354.3307,
|
61
|
+
"m" => 3543.307,
|
62
|
+
"in" => 90,
|
63
|
+
"ft" => 1080,
|
64
|
+
"uu" => 1 # user unit, 90 dpi
|
65
|
+
}
|
66
|
+
|
67
|
+
attr_reader :value, :unit
|
68
|
+
|
69
|
+
def initialize(value, unit="uu")
|
70
|
+
case value
|
71
|
+
when /^(\d+(?:\.\d+)?)(\w+)?$/ then
|
72
|
+
@value = $1.to_f
|
73
|
+
@unit = $2
|
74
|
+
@unit ||= unit
|
75
|
+
@unit = (@unit == "px" or Units.has_key?(@unit)) ? @unit : "uu"
|
76
|
+
else
|
77
|
+
@value = value.kind_of?(String) ? value.to_f: value
|
78
|
+
@unit = unit
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def to_pixels(dpi=90.0)
|
83
|
+
return @value.round if @unit == "px"
|
84
|
+
((dpi / 90.0) * Units[@unit] * @value).round
|
85
|
+
end
|
86
|
+
|
87
|
+
def to_s
|
88
|
+
"%g#{@unit}" % @value
|
89
|
+
end
|
90
|
+
|
91
|
+
def scale(f)
|
92
|
+
return self if @unit == "px"
|
93
|
+
InkscapeUnit.new(@value * f, @unit)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
class InkscapeResolution
|
98
|
+
attr_reader :width, :height
|
99
|
+
|
100
|
+
def initialize(width, height, unit="uu")
|
101
|
+
@width = width.kind_of?(InkscapeUnit) ? width : InkscapeUnit.new(width, unit)
|
102
|
+
@height = height.kind_of?(InkscapeUnit) ? height : InkscapeUnit.new(height, unit)
|
103
|
+
end
|
104
|
+
|
105
|
+
def scale(f)
|
106
|
+
InkscapeResolution.new(@width.scale(f), @height.scale(f))
|
107
|
+
end
|
108
|
+
|
109
|
+
def to_s
|
110
|
+
"#{@width.to_s}x#{@height.to_s}"
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
class InkscapeRemote
|
115
|
+
def initialize
|
116
|
+
open
|
117
|
+
probe_decimal_symbol
|
118
|
+
yield self
|
119
|
+
ensure
|
120
|
+
quit
|
121
|
+
end
|
122
|
+
|
123
|
+
def open
|
124
|
+
@in, @out, @err = Open3.popen3(*[self.class.path, "--shell"])
|
125
|
+
loop do
|
126
|
+
case response
|
127
|
+
when :prompt then break
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
def command(args)
|
133
|
+
c = args.collect do |key, value|
|
134
|
+
if value
|
135
|
+
"\"#{key}=#{self.class.escape value.to_s}\""
|
136
|
+
else
|
137
|
+
key
|
138
|
+
end
|
139
|
+
end.join(" ")
|
140
|
+
puts "> #{c}" if Inkmake.verbose
|
141
|
+
@in.write "#{c}\n"
|
142
|
+
@in.flush
|
143
|
+
end
|
144
|
+
|
145
|
+
def response
|
146
|
+
o = @out.read(1)
|
147
|
+
if o == ">"
|
148
|
+
puts "< #{o}" if Inkmake.verbose
|
149
|
+
return :prompt;
|
150
|
+
end
|
151
|
+
o = o + @out.readline
|
152
|
+
puts "< #{o}" if Inkmake.verbose
|
153
|
+
o
|
154
|
+
end
|
155
|
+
|
156
|
+
# this is weird but is the least weird and most protable way i could come up with
|
157
|
+
# to figuring out what decimal symbol to use.
|
158
|
+
# forcing LC_NUMERIC=C seems hard to do in a portable way
|
159
|
+
# trying to use env inkmake is running in is also not so portable (windows?)
|
160
|
+
def probe_decimal_symbol
|
161
|
+
svg =
|
162
|
+
"<?xml version=\"1.0\"?>" +
|
163
|
+
"<svg xmlns:xlink=\"http://www.w3.org/1999/xlink\" width=\"1\" height=\"1\">" +
|
164
|
+
"</svg>"
|
165
|
+
f = Tempfile.new("inkmake")
|
166
|
+
f.write(svg)
|
167
|
+
f.flush
|
168
|
+
begin
|
169
|
+
command({
|
170
|
+
"--file" => f.path,
|
171
|
+
"--export-png" => Tempfile.new("inkmake").path,
|
172
|
+
"--export-area" => "0.0:0.0:1.0:1.0",
|
173
|
+
})
|
174
|
+
response
|
175
|
+
@decimal_symbol = "."
|
176
|
+
rescue EOFError
|
177
|
+
@decimal_symbol = ","
|
178
|
+
# restart inkscape
|
179
|
+
open
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
def export(opts)
|
184
|
+
c = {
|
185
|
+
"--file" => opts[:svg_path],
|
186
|
+
"--export-#{opts[:format]}" => opts[:out_path]
|
187
|
+
}
|
188
|
+
if opts[:res]
|
189
|
+
s = opts[:rotate_scale_hack] ? 2 : 1
|
190
|
+
c["--export-width"] = opts[:res].width.to_pixels(opts[:dpi] || 90) * s
|
191
|
+
c["--export-height"] = opts[:res].height.to_pixels(opts[:dpi] || 90) * s
|
192
|
+
end
|
193
|
+
if opts[:dpi]
|
194
|
+
c["--export-dpi"] = opts[:dpi]
|
195
|
+
end
|
196
|
+
if opts[:area].kind_of? Array
|
197
|
+
c["--export-area"] = ("%f:%f:%f:%f" % opts[:area]).gsub(".", @decimal_symbol)
|
198
|
+
elsif opts[:area] == :drawing
|
199
|
+
c["--export-area-drawing"] = nil
|
200
|
+
elsif opts[:area].kind_of? String
|
201
|
+
c["--export-id"] = opts[:area]
|
202
|
+
end
|
203
|
+
command(c)
|
204
|
+
width, height = [0, 0]
|
205
|
+
out = nil
|
206
|
+
loop do
|
207
|
+
case response
|
208
|
+
when /^Bitmap saved as: (.*)$/ then
|
209
|
+
out = $1
|
210
|
+
when /^Area .* exported to (\d+) x (\d+) pixels.*$/ then
|
211
|
+
width = $1
|
212
|
+
height = $2
|
213
|
+
when :prompt then break
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
217
|
+
[width, height]
|
218
|
+
end
|
219
|
+
|
220
|
+
def query_all(file)
|
221
|
+
ids = []
|
222
|
+
command({
|
223
|
+
"--file" => file,
|
224
|
+
"--query-all" => nil,
|
225
|
+
})
|
226
|
+
loop do
|
227
|
+
case response
|
228
|
+
when /^(.*),(.*),(.*),(.*),(.*)$/ then ids << [$1, $2.to_f, $3.to_f, $4.to_f, $5.to_f]
|
229
|
+
when :prompt then break
|
230
|
+
end
|
231
|
+
end
|
232
|
+
ids
|
233
|
+
end
|
234
|
+
|
235
|
+
def ids(file)
|
236
|
+
Hash[query_all(file).map {|l| [l[0], l[1..-1]]}]
|
237
|
+
end
|
238
|
+
|
239
|
+
def drawing_area(file)
|
240
|
+
query_all(file).first[1..-1]
|
241
|
+
end
|
242
|
+
|
243
|
+
def quit
|
244
|
+
command({"quit" => nil})
|
245
|
+
@out.read
|
246
|
+
nil
|
247
|
+
end
|
248
|
+
|
249
|
+
def self.escape(s)
|
250
|
+
s.gsub(/(["'])/, '\\\\\1')
|
251
|
+
end
|
252
|
+
|
253
|
+
def self.path
|
254
|
+
return Inkmake.inkscape_path if Inkmake.inkscape_path
|
255
|
+
|
256
|
+
# try to figure out inkscape path
|
257
|
+
p = (
|
258
|
+
(["/Applications/Inkscape.app/Contents/Resources/bin/inkscape",
|
259
|
+
'c:\Program Files\Inkscape\inkscape.exe',
|
260
|
+
'c:\Program Files (x86)\Inkscape\inkscape.exe'] +
|
261
|
+
(ENV['PATH'].split(':').map {|p| File.join(p, "inkscape")}))
|
262
|
+
.select do |path|
|
263
|
+
File.exists? path
|
264
|
+
end)
|
265
|
+
.first
|
266
|
+
if p
|
267
|
+
p
|
268
|
+
else
|
269
|
+
begin
|
270
|
+
require "osx/cocoa"
|
271
|
+
"#{OSX::NSWorkspace.sharedWorkspace.fullPathForApplication:"Inkscape"}/Contents/Resources/bin/inkscape"
|
272
|
+
rescue NameError, LoadError
|
273
|
+
nil
|
274
|
+
end
|
275
|
+
end
|
276
|
+
end
|
277
|
+
end
|
278
|
+
|
279
|
+
class InkFile
|
280
|
+
attr_reader :svg_path, :out_path
|
281
|
+
DefaultVariants = {
|
282
|
+
"@2x" => {:scale => 2.0}
|
283
|
+
}
|
284
|
+
Rotations = {
|
285
|
+
"right" => 90,
|
286
|
+
"left" => -90,
|
287
|
+
"upsidedown" => 180
|
288
|
+
}
|
289
|
+
# 123x123, 12.3cm*12.3cm
|
290
|
+
RES_RE = /^(\d+(?:\.\d+)?(?:px|pt|pc|mm|cm|dm|m|in|ft|uu)?)[x*](\d+(?:\.\d+)?(?:px|pt|pc|mm|cm|dm|m|in|ft|uu)?)$/
|
291
|
+
# *123, *1.23
|
292
|
+
SCALE_RE = /^\*(\d+(?:\.\d+)?)$/
|
293
|
+
# 180dpi
|
294
|
+
DPI_RE = /^(\d+(?:\.\d+)?)dpi$/i
|
295
|
+
# (prefix)[(...)](suffix)
|
296
|
+
DEST_RE = /^([^\[]*)(?:\[(.*)\])?(.*)$/
|
297
|
+
# test.svg, test.SVG
|
298
|
+
SVG_RE = /\.svg$/i
|
299
|
+
# ext to format, supported inkscape output formats
|
300
|
+
EXT_RE = /\.(png|pdf|ps|eps)$/i
|
301
|
+
# supported inkscape output formats
|
302
|
+
FORMAT_RE = /^(png|pdf|ps|eps)$/i
|
303
|
+
# @name
|
304
|
+
AREA_NAME_RE = /^@(.*)$/
|
305
|
+
# @x:y:w:h
|
306
|
+
AREA_SPEC_RE = /^@(\d+(?:\.\d+)?):(\d+(?:\.\d+)?):(\d+(?:\.\d+)?):(\d+(?:\.\d+)?)$/
|
307
|
+
# right, left, upsidedown
|
308
|
+
ROTATE_RE = /^(right|left|upsidedown)$/
|
309
|
+
# show/hide layer or id, "+Layer 1", +#id, -*
|
310
|
+
SHOWHIDE_RE = /^([+-])(.+)$/
|
311
|
+
|
312
|
+
class SyntaxError < StandardError
|
313
|
+
end
|
314
|
+
|
315
|
+
class ProcessError < StandardError
|
316
|
+
end
|
317
|
+
|
318
|
+
def initialize(file, opts)
|
319
|
+
@file = file
|
320
|
+
@images = []
|
321
|
+
@force = opts[:force]
|
322
|
+
|
323
|
+
svg_path = nil
|
324
|
+
out_path = nil
|
325
|
+
File.read(file).lines.each_with_index do |line, index|
|
326
|
+
line.strip!
|
327
|
+
next if line.empty? or line.start_with? "#"
|
328
|
+
begin
|
329
|
+
case line
|
330
|
+
when /^svg:(.*)/i then svg_path = File.expand_path($1.strip, File.dirname(file))
|
331
|
+
when /^out:(.*)/i then out_path = File.expand_path($1.strip, File.dirname(file))
|
332
|
+
else
|
333
|
+
@images << InkImage.new(self, parse_line(line))
|
334
|
+
end
|
335
|
+
rescue SyntaxError => e
|
336
|
+
puts "#{file}:#{index+1}: #{e.message}"
|
337
|
+
exit
|
338
|
+
end
|
339
|
+
end
|
340
|
+
|
341
|
+
# order is: argument, config in inkfile, inkfile directory
|
342
|
+
@svg_path = opts[:svg_path] || svg_path || File.dirname(file)
|
343
|
+
@out_path = opts[:out_path] || out_path || File.dirname(file)
|
344
|
+
end
|
345
|
+
|
346
|
+
def parse_split_line(line)
|
347
|
+
# changed CSV API in ruby 1.9
|
348
|
+
if RUBY_VERSION.start_with? "1.8"
|
349
|
+
CSV::parse_line(line, fs = " ")
|
350
|
+
else
|
351
|
+
CSV::parse_line(line, {:col_sep => " "})
|
352
|
+
end
|
353
|
+
end
|
354
|
+
|
355
|
+
def parse_line(line)
|
356
|
+
cols = nil
|
357
|
+
begin
|
358
|
+
cols = parse_split_line(line)
|
359
|
+
rescue CSV::MalformedCSVError => e
|
360
|
+
raise SyntaxError, e.message
|
361
|
+
end
|
362
|
+
raise SyntaxError, "Invalid number of columns" if cols.count < 1
|
363
|
+
|
364
|
+
if not DEST_RE.match(cols[0])
|
365
|
+
raise SyntaxError, "Invalid destination format \"#{cols[0]}\""
|
366
|
+
end
|
367
|
+
|
368
|
+
opts = {}
|
369
|
+
opts[:prefix] = $1
|
370
|
+
variants = $2
|
371
|
+
opts[:suffix] = $3
|
372
|
+
opts[:format] = $1.downcase if EXT_RE.match(opts[:prefix] + opts[:suffix])
|
373
|
+
|
374
|
+
cols[1..-1].each do |col|
|
375
|
+
case col
|
376
|
+
when RES_RE then opts[:res] = InkscapeResolution.new($1, $2, "px")
|
377
|
+
when SVG_RE then opts[:svg] = col
|
378
|
+
when AREA_SPEC_RE then opts[:area] = [$1.to_f, $2.to_f, $3.to_f, $4.to_f]
|
379
|
+
when AREA_NAME_RE then opts[:area] = $1
|
380
|
+
when /^drawing$/ then opts[:area] = :drawing
|
381
|
+
when FORMAT_RE then opts[:format] = $1.downcase
|
382
|
+
when ROTATE_RE then opts[:rotate] = Rotations[$1]
|
383
|
+
when SCALE_RE then opts[:scale] = $1.to_f
|
384
|
+
when DPI_RE then opts[:dpi] = $1.to_f
|
385
|
+
when SHOWHIDE_RE
|
386
|
+
op = $1 == "+" ? :show : :hide
|
387
|
+
if $2.start_with? "#"
|
388
|
+
type = :id
|
389
|
+
name= $2[1..-1]
|
390
|
+
else
|
391
|
+
type = :layer
|
392
|
+
name = $2 == "*" ? :all : $2
|
393
|
+
end
|
394
|
+
(opts[:showhide] ||= []).push({:op => op, :type => type, :name => name})
|
395
|
+
else
|
396
|
+
raise SyntaxError, "Unknown column \"#{col}\""
|
397
|
+
end
|
398
|
+
end
|
399
|
+
|
400
|
+
if not opts[:format]
|
401
|
+
raise SyntaxError, "Unknown or no output format could be determined"
|
402
|
+
end
|
403
|
+
|
404
|
+
variants = (variants.split("|") if variants) || []
|
405
|
+
opts[:variants] = variants.collect do |variant|
|
406
|
+
name, options = variant.split("=", 2)
|
407
|
+
if options
|
408
|
+
options = Hash[
|
409
|
+
options.split(",").map do |option|
|
410
|
+
case option
|
411
|
+
when ROTATE_RE then [:rotate, Rotations[$1]]
|
412
|
+
when RES_RE then [:res, InkscapeResolution.new($1, $2, "px")]
|
413
|
+
when SCALE_RE then [:scale, $1.to_f]
|
414
|
+
when DPI_RE then [:dpi, $1.to_f]
|
415
|
+
else
|
416
|
+
raise SyntaxError, "Invalid variant option \"#{option}\""
|
417
|
+
end
|
418
|
+
end
|
419
|
+
]
|
420
|
+
else
|
421
|
+
options = DefaultVariants[name]
|
422
|
+
raise SyntaxError, "Invalid default variant \"#{name}\"" if not options
|
423
|
+
end
|
424
|
+
|
425
|
+
[name, options]
|
426
|
+
end
|
427
|
+
|
428
|
+
opts
|
429
|
+
end
|
430
|
+
|
431
|
+
def variants_to_generate
|
432
|
+
l = []
|
433
|
+
@images.each do |image|
|
434
|
+
image.variants.each do |variant|
|
435
|
+
next if not @force and
|
436
|
+
File.exists? variant.out_path and
|
437
|
+
File.mtime(variant.out_path) > File.mtime(image.svg_path) and
|
438
|
+
File.mtime(variant.out_path) > File.mtime(@file)
|
439
|
+
if variant.out_path == image.svg_path
|
440
|
+
raise ProcessError, "Avoiding overwriting source SVG file #{image.svg_path}"
|
441
|
+
end
|
442
|
+
|
443
|
+
l << variant
|
444
|
+
end
|
445
|
+
end
|
446
|
+
|
447
|
+
l
|
448
|
+
end
|
449
|
+
|
450
|
+
def temp_rotate_svg(path, degrees, width, height)
|
451
|
+
if degrees != 180
|
452
|
+
out_width, out_height = height, width
|
453
|
+
else
|
454
|
+
out_width, out_height = width, height
|
455
|
+
end
|
456
|
+
svg =
|
457
|
+
"<?xml version=\"1.0\"?>" +
|
458
|
+
"<svg xmlns:xlink=\"http://www.w3.org/1999/xlink\" width=\"#{out_width}\" height=\"#{out_height}\">" +
|
459
|
+
"<g>" +
|
460
|
+
"<image transform=\"translate(#{out_width/2} #{out_height/2}) rotate(#{degrees})\"" +
|
461
|
+
" width=\"#{width}\" height=\"#{height}\" x=\"#{-width/2}\" y=\"#{-height/2}\"" +
|
462
|
+
" xlink:href=\"file:///#{URI.escape(path)}\" />" +
|
463
|
+
"</g>" +
|
464
|
+
"</svg>"
|
465
|
+
f = Tempfile.new("inkmake")
|
466
|
+
f.write(svg)
|
467
|
+
f.flush
|
468
|
+
f.seek(0)
|
469
|
+
[f, out_width, out_height]
|
470
|
+
end
|
471
|
+
|
472
|
+
def process
|
473
|
+
variants = variants_to_generate
|
474
|
+
if variants.empty?
|
475
|
+
return false
|
476
|
+
end
|
477
|
+
|
478
|
+
idfilemap = {}
|
479
|
+
InkscapeRemote.new do |inkscape|
|
480
|
+
variants.each do |variant|
|
481
|
+
if not File.exists? variant.image.svg_path
|
482
|
+
raise ProcessError, "Source SVG file #{variant.image.svg_path} does not exist"
|
483
|
+
end
|
484
|
+
|
485
|
+
out_res = nil
|
486
|
+
# order: 200x200, @id/area, svg res
|
487
|
+
if variant.image.res
|
488
|
+
out_res = variant.image.res
|
489
|
+
elsif variant.image.area == :drawing
|
490
|
+
res = inkscape.drawing_area(variant.image.svg_path)
|
491
|
+
out_res = InkscapeResolution.new(res[2], res[3], "uu")
|
492
|
+
elsif variant.image.area
|
493
|
+
if variant.image.area.kind_of? String
|
494
|
+
if not idfilemap.has_key? variant.image.svg_path
|
495
|
+
idfilemap[variant.image.svg_path] = inkscape.ids(variant.image.svg_path)
|
496
|
+
end
|
497
|
+
|
498
|
+
if not idfilemap[variant.image.svg_path].has_key? variant.image.area
|
499
|
+
raise ProcessError, "Unknown id \"#{variant.image.area}\" in file #{variant.image.svg_path} when exporting #{variant.out_path}"
|
500
|
+
end
|
501
|
+
|
502
|
+
res = idfilemap[variant.image.svg_path][variant.image.area]
|
503
|
+
out_res = InkscapeResolution.new(res[2], res[3], "uu")
|
504
|
+
else
|
505
|
+
a = variant.image.area
|
506
|
+
# x0:y0:x1:y1
|
507
|
+
out_res = InkscapeResolution.new(a[2]-a[0], a[3]-a[1], "uu")
|
508
|
+
end
|
509
|
+
else
|
510
|
+
out_res = variant.image.svg_res
|
511
|
+
end
|
512
|
+
|
513
|
+
scale = variant.options[:scale]
|
514
|
+
if scale
|
515
|
+
out_res = out_res.scale(scale)
|
516
|
+
end
|
517
|
+
|
518
|
+
out_res = variant.options[:res] if variant.options[:res]
|
519
|
+
|
520
|
+
rotate = (variant.image.format == "png" and variant.options[:rotate])
|
521
|
+
|
522
|
+
FileUtils.mkdir_p File.dirname(variant.out_path)
|
523
|
+
|
524
|
+
svg_path = variant.image.svg_path
|
525
|
+
if variant.image.showhide
|
526
|
+
svg_path = variant.image.svg_showhide_file.path
|
527
|
+
end
|
528
|
+
|
529
|
+
res = inkscape.export({
|
530
|
+
:svg_path => svg_path,
|
531
|
+
:out_path => variant.out_path,
|
532
|
+
:res => out_res,
|
533
|
+
:dpi => variant.options[:dpi],
|
534
|
+
:format => variant.image.format,
|
535
|
+
:area => variant.image.area,
|
536
|
+
:rotate_scale_hack => rotate
|
537
|
+
})
|
538
|
+
|
539
|
+
if rotate
|
540
|
+
tmp, width, height = temp_rotate_svg(variant.out_path, rotate, res[0].to_i, res[1].to_i)
|
541
|
+
res = inkscape.export({
|
542
|
+
:svg_path => tmp.path,
|
543
|
+
:out_path => variant.out_path,
|
544
|
+
:res => InkscapeResolution.new(width / 2, height / 2, "px"),
|
545
|
+
:format => variant.image.format
|
546
|
+
})
|
547
|
+
tmp.close!
|
548
|
+
end
|
549
|
+
|
550
|
+
rel_path = Pathname.new(variant.out_path).relative_path_from(Pathname.new(Dir.pwd))
|
551
|
+
if variant.image.format == "png"
|
552
|
+
puts "#{rel_path} #{res[0]}x#{res[1]}"
|
553
|
+
else
|
554
|
+
puts rel_path
|
555
|
+
end
|
556
|
+
end
|
557
|
+
end
|
558
|
+
|
559
|
+
return true
|
560
|
+
end
|
561
|
+
end
|
562
|
+
|
563
|
+
class InkImage
|
564
|
+
attr_reader :inkfile, :prefix, :variants, :suffix, :res, :format, :area, :showhide
|
565
|
+
|
566
|
+
def initialize(inkfile, opts)
|
567
|
+
@inkfile = inkfile
|
568
|
+
@prefix = opts[:prefix]
|
569
|
+
variant_opts = {
|
570
|
+
:rotate => opts[:rotate],
|
571
|
+
:scale => opts[:scale],
|
572
|
+
:dpi => opts[:dpi]
|
573
|
+
}
|
574
|
+
@variants = [InkVariant.new(self, "", variant_opts)]
|
575
|
+
opts[:variants].each do |name, options|
|
576
|
+
@variants << InkVariant.new(self, name, options)
|
577
|
+
end
|
578
|
+
@suffix = opts[:suffix]
|
579
|
+
@res = opts[:res]
|
580
|
+
@svg = opts[:svg]
|
581
|
+
@format = opts[:format]
|
582
|
+
@area = opts[:area]
|
583
|
+
@showhide = opts[:showhide]
|
584
|
+
end
|
585
|
+
|
586
|
+
def svg_path
|
587
|
+
File.expand_path(@svg || File.basename(@prefix + @suffix, ".*") + ".svg", inkfile.svg_path)
|
588
|
+
end
|
589
|
+
|
590
|
+
def svg_res
|
591
|
+
@svg_res ||=
|
592
|
+
begin
|
593
|
+
doc = REXML::Document.new File.read(svg_path)
|
594
|
+
svgattr = doc.elements.to_a("//svg")[0].attributes
|
595
|
+
if svgattr["width"] and svgattr["height"]
|
596
|
+
InkscapeResolution.new(svgattr["width"], svgattr["height"], "uu")
|
597
|
+
else
|
598
|
+
nil
|
599
|
+
end
|
600
|
+
end
|
601
|
+
end
|
602
|
+
|
603
|
+
def svg_showhide_file
|
604
|
+
@svg_showhide_file ||=
|
605
|
+
begin
|
606
|
+
doc = REXML::Document.new File.read(svg_path)
|
607
|
+
|
608
|
+
layers = {}
|
609
|
+
REXML::XPath.each(doc, "//svg:g[@inkscape:groupmode='layer']").each do |e|
|
610
|
+
label = e.attributes["label"]
|
611
|
+
next if not label
|
612
|
+
layers[label] = e
|
613
|
+
end
|
614
|
+
|
615
|
+
ids = {}
|
616
|
+
REXML::XPath.each(doc, "//svg:*[@id]").each do |e|
|
617
|
+
id = e.attributes["id"]
|
618
|
+
next if not id
|
619
|
+
ids[id] = e
|
620
|
+
end
|
621
|
+
|
622
|
+
@showhide.each do |sh|
|
623
|
+
elms = nil
|
624
|
+
if sh[:type] == :layer
|
625
|
+
if sh[:name] == :all
|
626
|
+
elms = layers.values
|
627
|
+
else
|
628
|
+
e = layers[sh[:name]]
|
629
|
+
if not e
|
630
|
+
raise InkFile::ProcessError, "Layer \"#{sh[:name]}\" not found in #{svg_path}"
|
631
|
+
end
|
632
|
+
elms = [e]
|
633
|
+
end
|
634
|
+
else
|
635
|
+
e = ids[sh[:name]]
|
636
|
+
if not e
|
637
|
+
raise InkFile::ProcessError, "Id \"#{sh[:name]}\" not found in #{svg_path}"
|
638
|
+
end
|
639
|
+
elms = [e]
|
640
|
+
end
|
641
|
+
|
642
|
+
elms.each do |e|
|
643
|
+
# NOTE: should be visibility for #ids to not affect flow etc?
|
644
|
+
e.delete_attribute("display")
|
645
|
+
# also remove display inside style attributes
|
646
|
+
if e.attributes["style"]
|
647
|
+
style_declarations = e.attributes["style"].split(";")
|
648
|
+
style_declarations_to_keep = []
|
649
|
+
style_declarations.each do | sd |
|
650
|
+
property, value = sd.split(":", 2)
|
651
|
+
if value && property == "display"
|
652
|
+
# throw it out
|
653
|
+
else
|
654
|
+
style_declarations_to_keep.push(sd)
|
655
|
+
end
|
656
|
+
end
|
657
|
+
e.attributes["style"] = style_declarations_to_keep.join(";")
|
658
|
+
end
|
659
|
+
if sh[:op] == :hide
|
660
|
+
e.add_attribute("display", "none")
|
661
|
+
else
|
662
|
+
# show is a nop
|
663
|
+
end
|
664
|
+
end
|
665
|
+
end
|
666
|
+
|
667
|
+
f = Tempfile.new("inkmake")
|
668
|
+
doc.write(:output => f)
|
669
|
+
f.flush
|
670
|
+
f
|
671
|
+
end
|
672
|
+
end
|
673
|
+
end
|
674
|
+
|
675
|
+
class InkVariant
|
676
|
+
attr_reader :image, :name, :options
|
677
|
+
|
678
|
+
def initialize(image, name, options)
|
679
|
+
@image = image
|
680
|
+
@name = name
|
681
|
+
@options = options
|
682
|
+
end
|
683
|
+
|
684
|
+
def out_path
|
685
|
+
File.expand_path(
|
686
|
+
"#{@image.prefix}#{@name}#{@image.suffix}",
|
687
|
+
@image.inkfile.out_path)
|
688
|
+
end
|
689
|
+
end
|
690
|
+
|
691
|
+
def self.run(argv)
|
692
|
+
inkfile_path = nil
|
693
|
+
inkfile_opts = {}
|
694
|
+
OptionParser.new do |o|
|
695
|
+
o.banner = "Usage: #{$0} [options] [Inkfile]"
|
696
|
+
o.on("-v", "--verbose", "Verbose output") { @verbose = true }
|
697
|
+
o.on("-s", "--svg PATH", "SVG source base path") { |v| inkfile_opts[:svg_path] = v }
|
698
|
+
o.on("-o", "--out PATH", "Output base path") { |v| inkfile_opts[:out_path] = v }
|
699
|
+
o.on("-f", "--force", "Force regenerate (skip time check)") { |v| inkfile_opts[:force] = true }
|
700
|
+
o.on("-i", "--inkscape PATH", "Inkscape binary path", "Default: #{InkscapeRemote.path || "not found"}") { |v| @inkscape_path = v }
|
701
|
+
o.on("-h", "--help", "Display help") { puts o; exit }
|
702
|
+
begin
|
703
|
+
inkfile_path = o.parse!(argv).first
|
704
|
+
rescue OptionParser::InvalidOption => e
|
705
|
+
puts e.message
|
706
|
+
exit 1
|
707
|
+
end
|
708
|
+
end
|
709
|
+
|
710
|
+
inkfile_path = File.expand_path(inkfile_path || "Inkfile", Dir.pwd)
|
711
|
+
|
712
|
+
begin
|
713
|
+
raise "Could not find Inkscape binary (maybe try --inkscape?)" if not InkscapeRemote.path
|
714
|
+
raise "Inkscape binary #{InkscapeRemote.path} does not exist or is not executable" if not InkscapeRemote.path or not File.executable? InkscapeRemote.path
|
715
|
+
rescue StandardError => e
|
716
|
+
puts e.message
|
717
|
+
exit 1
|
718
|
+
end
|
719
|
+
|
720
|
+
begin
|
721
|
+
if not InkFile.new(inkfile_path, inkfile_opts).process
|
722
|
+
puts "Everything seems to be up to date"
|
723
|
+
end
|
724
|
+
rescue InkFile::ProcessError, SystemCallError => e
|
725
|
+
puts e.message
|
726
|
+
exit 1
|
727
|
+
end
|
728
|
+
end
|
729
|
+
end
|
metadata
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: inkmake
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Mattias Wadman
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-05-01 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description:
|
14
|
+
email: mattias.wadman@gmail.com
|
15
|
+
executables:
|
16
|
+
- inkmake
|
17
|
+
extensions: []
|
18
|
+
extra_rdoc_files: []
|
19
|
+
files:
|
20
|
+
- bin/inkmake
|
21
|
+
- lib/inkmake.rb
|
22
|
+
homepage: https://github.com/wader/inkmake
|
23
|
+
licenses:
|
24
|
+
- MIT
|
25
|
+
metadata: {}
|
26
|
+
post_install_message:
|
27
|
+
rdoc_options: []
|
28
|
+
require_paths:
|
29
|
+
- lib
|
30
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
31
|
+
requirements:
|
32
|
+
- - ">="
|
33
|
+
- !ruby/object:Gem::Version
|
34
|
+
version: '0'
|
35
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - ">="
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: '0'
|
40
|
+
requirements: []
|
41
|
+
rubyforge_project:
|
42
|
+
rubygems_version: 2.2.2
|
43
|
+
signing_key:
|
44
|
+
specification_version: 4
|
45
|
+
summary: Makefile inspired export from SVG files using Inkscape as backend with some
|
46
|
+
added smartness
|
47
|
+
test_files: []
|