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