1 | `#!/usr/bin/env node
|
2 | `
|
3 | path = require 'path'
|
4 | fs = require 'fs'
|
5 | xmldom = require 'xmldom'
|
6 | DOMParser = xmldom.DOMParser
|
7 | domImplementation = new xmldom.DOMImplementation()
|
8 | XMLSerializer = xmldom.XMLSerializer
|
9 | prettyXML = require 'prettify-xml'
|
10 |
|
11 | SVGNS = 'http://www.w3.org/2000/svg'
|
12 | XLINKNS = 'http://www.w3.org/1999/xlink'
|
13 |
|
14 | splitIntoLines = (data) ->
|
15 | data.replace('\r\n', '\n').replace('\r', '\n').split('\n')
|
16 | whitespace = /[\s\uFEFF\xA0]+/
|
17 |
|
18 | extensionOf = (filename) -> path.extname(filename).toLowerCase()
|
19 |
|
20 | class SVGTilerException
|
21 | constructor: (@message) ->
|
22 | toString: ->
|
23 | "svgtiler: #{@message}"
|
24 |
|
25 | overflowBox = (xml) ->
|
26 | if xml.documentElement.hasAttribute 'overflowBox'
|
27 | xml.documentElement.getAttribute('overflowBox').split /\s*,?\s+/
|
28 | .map parseFloat
|
29 | else
|
30 | null
|
31 |
|
32 | svgBBox = (xml) ->
|
33 |
|
34 |
|
35 |
|
36 |
|
37 |
|
38 |
|
39 | if xml.documentElement.hasAttribute 'viewBox'
|
40 | xml.documentElement.getAttribute('viewBox').split /\s*,?\s+/
|
41 | .map parseFloat
|
42 | else
|
43 | recurse = (node) ->
|
44 | if node.nodeType != node.ELEMENT_NODE or
|
45 | node.tagName in ['defs', 'symbol', 'use']
|
46 | return [null, null, null, null]
|
47 | switch node.tagName
|
48 | when 'rect', 'image'
|
49 | [parseFloat node.getAttribute('x') or 0
|
50 | parseFloat node.getAttribute('y') or 0
|
51 | parseFloat node.getAttribute('width') or '100%'
|
52 | parseFloat node.getAttribute('height') or '100%']
|
53 | when 'circle'
|
54 | cx = parseFloat node.getAttribute('cx') or 0
|
55 | cy = parseFloat node.getAttribute('cy') or 0
|
56 | r = parseFloat node.getAttribute('r') or 0
|
57 | [cx - r, cy - r, 2*r, 2*r]
|
58 | when 'ellipse'
|
59 | cx = parseFloat node.getAttribute('cx') or 0
|
60 | cy = parseFloat node.getAttribute('cy') or 0
|
61 | rx = parseFloat node.getAttribute('rx') or 0
|
62 | ry = parseFloat node.getAttribute('ry') or 0
|
63 | [cx - rx, cy - ry, 2*rx, 2*ry]
|
64 | when 'line'
|
65 | x1 = parseFloat node.getAttribute('x1') or 0
|
66 | y1 = parseFloat node.getAttribute('y1') or 0
|
67 | x2 = parseFloat node.getAttribute('x2') or 0
|
68 | y2 = parseFloat node.getAttribute('y2') or 0
|
69 | xmin = Math.min x1, x2
|
70 | ymin = Math.min y1, y2
|
71 | [xmin, ymin, Math.max(x1, x2) - xmin, Math.max(y1, y2) - ymin]
|
72 | when 'polyline', 'polygon'
|
73 | points = for point in node.getAttribute('points').trim().split /\s+/
|
74 | for coord in point.split /,/
|
75 | parseFloat coord
|
76 | xs = (point[0] for point in points)
|
77 | ys = (point[1] for point in points)
|
78 | xmin = Math.min xs...
|
79 | ymin = Math.min ys...
|
80 | [xmin, ymin, Math.max(xs...) - xmin, Math.max(ys...) - ymin]
|
81 | else
|
82 | viewBoxes = (recurse(child) for child in node.childNodes)
|
83 | xmin = Math.min (viewBox[0] for viewBox in viewBoxes when viewBox[0])...
|
84 | ymin = Math.min (viewBox[1] for viewBox in viewBoxes when viewBox[1])...
|
85 | xmax = Math.max (viewBox[0]+viewBox[2] for viewBox in viewBoxes when viewBox[0] and viewBox[2])...
|
86 | ymax = Math.max (viewBox[1]+viewBox[3] for viewBox in viewBoxes when viewBox[1] and viewBox[3])...
|
87 | [xmin, ymin, xmax - xmin, ymax - ymin]
|
88 | viewBox = recurse xml.documentElement
|
89 | if Infinity in viewBox or -Infinity in viewBox
|
90 | null
|
91 | else
|
92 | viewBox
|
93 |
|
94 | zIndex = (node) ->
|
95 | style = node.getAttribute 'style'
|
96 | return 0 unless style
|
97 | match = /(?:^|\W)z-index\s*:\s*([-\d]+)/i.exec style
|
98 | return 0 unless match?
|
99 | parseInt match[1]
|
100 |
|
101 | class Symbol
|
102 | @svgEncoding: 'utf8'
|
103 | @forceWidth: null
|
104 | @forceHeight: null
|
105 |
|
106 | |
107 |
|
108 |
|
109 |
|
110 |
|
111 |
|
112 | @imageRendering:
|
113 | ' image-rendering="optimizeSpeed" style="image-rendering:pixelated"'
|
114 |
|
115 | @parse: (key, data) ->
|
116 | unless data?
|
117 | throw new SVGTilerException "Attempt to create symbol '#{key}' without data"
|
118 | else if typeof data == 'function'
|
119 | new DynamicSymbol key, data
|
120 | else if data.function?
|
121 | new DynamicSymbol key, data.function
|
122 | else
|
123 | new StaticSymbol key,
|
124 | if typeof data == 'string'
|
125 | if data.indexOf('<') < 0
|
126 | extension = extensionOf data
|
127 |
|
128 |
|
129 |
|
130 | switch extension
|
131 | when '.png', '.jpg', '.jpeg', '.gif'
|
132 | size = require('image-size') data
|
133 | svg: """
|
134 | <image xlink:href="#{encodeURIComponent data}" width="#{size.width}" height="#{size.height}"#{@imageRendering}/>
|
135 | """
|
136 | when '.svg'
|
137 | filename: data
|
138 | svg: fs.readFileSync data,
|
139 | encoding: @svgEncoding
|
140 | else
|
141 | throw new SVGTilerException "Unrecognized extension in filename '#{data}' for symbol '#{key}'"
|
142 | else
|
143 | svg: data
|
144 | else
|
145 | data
|
146 | includes: (substring) ->
|
147 | @key.indexOf(substring) >= 0
|
148 |
|
149 |
|
150 | zeroSizeReplacement = 1
|
151 |
|
152 | class StaticSymbol extends Symbol
|
153 | constructor: (@key, options) ->
|
154 | super()
|
155 | for own key, value of options
|
156 | @[key] = value
|
157 | @xml = new DOMParser().parseFromString @svg
|
158 | @viewBox = svgBBox @xml
|
159 | @overflowBox = overflowBox @xml
|
160 | @width = @height = null
|
161 | if @viewBox?
|
162 | @width = @viewBox[2]
|
163 | @height = @viewBox[3]
|
164 | |
165 |
|
166 |
|
167 |
|
168 |
|
169 | if @xml.documentElement.hasAttribute('style') and
|
170 | /overflow\s*:\s*visible/.test @xml.documentElement.getAttribute('style')
|
171 | if @width == 0
|
172 | @viewBox[2] = zeroSizeReplacement
|
173 | if @height == 0
|
174 | @viewBox[3] = zeroSizeReplacement
|
175 | if Symbol.forceWidth?
|
176 | @width = Symbol.forceWidth
|
177 | if Symbol.forceHeight?
|
178 | @height = Symbol.forceHeight
|
179 | warnings = []
|
180 | unless @width?
|
181 | warnings.push 'width'
|
182 | @width = 0 unless @width?
|
183 | unless @height?
|
184 | warnings.push 'height'
|
185 | @height = 0 unless @height?
|
186 | if warnings.length > 0
|
187 | console.warn "Failed to detect #{warnings.join ' and '} of SVG for symbol '#{@key}'"
|
188 | @zIndex = zIndex @xml.documentElement
|
189 | id: ->
|
190 | |
191 |
|
192 |
|
193 |
|
194 |
|
195 |
|
196 |
|
197 |
|
198 |
|
199 |
|
200 |
|
201 |
|
202 |
|
203 |
|
204 |
|
205 |
|
206 |
|
207 |
|
208 |
|
209 |
|
210 |
|
211 |
|
212 | encodeURIComponent @key
|
213 | .replace /%/g, '$'
|
214 |
|
215 |
|
216 | class DynamicSymbol extends Symbol
|
217 | constructor: (@key, @func) ->
|
218 | super()
|
219 | @versions = {}
|
220 | @nversions = 0
|
221 | use: (context) ->
|
222 | result = @func.call context
|
223 | if result of @versions
|
224 | @versions[result]
|
225 | else
|
226 | @versions[result] = Symbol.parse "#{@key}-v#{@nversions++}", result
|
227 |
|
228 | class Input
|
229 | @encoding: 'utf8'
|
230 | @parseFile: (filename) ->
|
231 | input = @parse fs.readFileSync filename,
|
232 | encoding: @encoding
|
233 | input.filename = filename
|
234 | input
|
235 | @load: (filename) ->
|
236 | extension = extensionOf filename
|
237 | if extension of extensionMap
|
238 | extensionMap[extension].parseFile filename
|
239 | else
|
240 | throw new SVGTilerException "Unrecognized extension in filename #{filename}"
|
241 |
|
242 | class Mapping extends Input
|
243 | constructor: (data) ->
|
244 | super()
|
245 | @map = {}
|
246 | if typeof data == 'function'
|
247 | @function = data
|
248 | else
|
249 | @merge data
|
250 | merge: (data) ->
|
251 | for own key, value of data
|
252 | unless value instanceof Symbol
|
253 | value = Symbol.parse key, value
|
254 | @map[key] = value
|
255 | lookup: (key) ->
|
256 | if key of @map
|
257 | @map[key]
|
258 | else if @function?
|
259 |
|
260 |
|
261 |
|
262 | value = @function key
|
263 | if value?
|
264 | @map[key] = Symbol.parse key, value
|
265 | else
|
266 | value
|
267 | else
|
268 | undefined
|
269 |
|
270 | class ASCIIMapping extends Mapping
|
271 | @title: "ASCII mapping file"
|
272 | @help: "Each line is <symbol-name><space><raw SVG or filename.svg>"
|
273 | @parse: (data) ->
|
274 | map = {}
|
275 | for line in splitIntoLines data
|
276 | separator = whitespace.exec line
|
277 | continue unless separator?
|
278 | if separator.index == 0
|
279 | if separator[0].length == 1
|
280 |
|
281 | key = ''
|
282 | else
|
283 |
|
284 | key = line[0]
|
285 | else
|
286 | key = line[...separator.index]
|
287 | map[key] = line[separator.index + separator[0].length..]
|
288 | new @ map
|
289 |
|
290 | class JSMapping extends Mapping
|
291 | @title: "JavaScript mapping file"
|
292 | @help: "Object mapping symbol names to SYMBOL e.g. dot: 'dot.svg'"
|
293 | @parse: (data) ->
|
294 | new @ eval data
|
295 |
|
296 | class CoffeeMapping extends Mapping
|
297 | @title: "CoffeeScript mapping file"
|
298 | @help: "Object mapping symbol names to SYMBOL e.g. dot: 'dot.svg'"
|
299 | @parse: (data) ->
|
300 | new @ require('coffeescript').eval data
|
301 |
|
302 | class Mappings
|
303 | constructor: (@maps = []) ->
|
304 | push: (map) ->
|
305 | @maps.push map
|
306 | lookup: (key) ->
|
307 | return unless @maps.length
|
308 | for i in [@maps.length-1..0]
|
309 | value = @maps[i].lookup key
|
310 | return value if value?
|
311 | undefined
|
312 |
|
313 | blankCells =
|
314 | '': true
|
315 | ' ': true
|
316 |
|
317 | allBlank = (list) ->
|
318 | for x in list
|
319 | if x? and x not of blankCells
|
320 | return false
|
321 | true
|
322 |
|
323 | class Drawing extends Input
|
324 | constructor: (@data) ->
|
325 | super()
|
326 | @load: (data) ->
|
327 |
|
328 | data = for row in data
|
329 | for cell in row
|
330 | cell
|
331 | unless Drawing.keepMargins
|
332 |
|
333 | while data.length > 0 and allBlank data[0]
|
334 | data.shift()
|
335 |
|
336 | while data.length > 0 and allBlank data[data.length-1]
|
337 | data.pop()
|
338 | if data.length > 0
|
339 |
|
340 | while allBlank (row[0] for row in data)
|
341 | for row in data
|
342 | row.shift()
|
343 |
|
344 | j = Math.max (row.length for row in data)...
|
345 | while j >= 0 and allBlank (row[j] for row in data)
|
346 | for row in data
|
347 | if j < row.length
|
348 | row.pop()
|
349 | j--
|
350 | new @ data
|
351 | writeSVG: (mappings, filename) ->
|
352 |
|
353 | unless filename?
|
354 | filename = path.parse @filename
|
355 | if filename.ext == '.svg'
|
356 | filename.base += '.svg'
|
357 | else
|
358 | filename.base = filename.base[...-filename.ext.length] + '.svg'
|
359 | filename = path.format filename
|
360 | console.log '->', filename
|
361 | fs.writeFileSync filename, @renderSVG mappings
|
362 | filename
|
363 | renderSVG: (mappings) ->
|
364 | doc = domImplementation.createDocument SVGNS, 'svg'
|
365 | svg = doc.documentElement
|
366 | svg.setAttribute 'xmlns:xlink', XLINKNS
|
367 | svg.setAttribute 'version', '1.1'
|
368 |
|
369 |
|
370 | missing = {}
|
371 | symbols =
|
372 | for row in @data
|
373 | for cell in row
|
374 | symbol = mappings.lookup cell
|
375 | unless symbol?
|
376 | missing[cell] = true
|
377 | symbol
|
378 | missing =("'#{key}'" for own key of missing)
|
379 | if missing.length
|
380 | console.warn "Failed to recognize symbols:", missing.join ', '
|
381 |
|
382 | symbolsByKey = {}
|
383 | symbols =
|
384 | for row, i in symbols
|
385 | for symbol, j in row
|
386 | if symbol?.use?
|
387 | symbol = symbol.use new Context symbols, i, j
|
388 | symbolsByKey[symbol?.key] = symbol
|
389 |
|
390 | for key, symbol of symbolsByKey
|
391 | continue unless symbol?
|
392 | svg.appendChild node = doc.createElementNS SVGNS, 'symbol'
|
393 | node.setAttribute 'id', symbol.id()
|
394 | if symbol.xml.documentElement.tagName in ['svg', 'symbol']
|
395 |
|
396 | for attribute in symbol.xml.documentElement.attributes
|
397 | unless attribute.name in ['version'] or attribute.name[...5] == 'xmlns'
|
398 | node.setAttribute attribute.name, attribute.value
|
399 | for child in symbol.xml.documentElement.childNodes
|
400 | node.appendChild child.cloneNode true
|
401 | else
|
402 | node.appendChild symbol.xml.documentElement.cloneNode true
|
403 |
|
404 | if symbol.viewBox?
|
405 | node.setAttribute 'viewBox', symbol.viewBox
|
406 |
|
407 | viewBox = [0, 0, 0, 0]
|
408 | levels = {}
|
409 | y = 0
|
410 | for row, i in symbols
|
411 | rowHeight = 0
|
412 | x = 0
|
413 | for symbol, j in row
|
414 | continue unless symbol?
|
415 | levels[symbol.zIndex] ?= []
|
416 | levels[symbol.zIndex].push use = doc.createElementNS SVGNS, 'use'
|
417 | use.setAttribute 'xlink:href', '#' + symbol.id()
|
418 | use.setAttributeNS SVGNS, 'x', x
|
419 | use.setAttributeNS SVGNS, 'y', y
|
420 | use.setAttributeNS SVGNS, 'width', symbol.viewBox?[2] ? symbol.width
|
421 | use.setAttributeNS SVGNS, 'height', symbol.viewBox?[3] ? symbol.height
|
422 | if symbol.overflowBox?
|
423 | dx = symbol.overflowBox[0] - symbol.viewBox[0]
|
424 | dy = symbol.overflowBox[1] - symbol.viewBox[1]
|
425 | viewBox[0] = Math.min viewBox[0], x + dx
|
426 | viewBox[1] = Math.min viewBox[1], y + dy
|
427 | viewBox[2] = Math.max viewBox[2], x + dx + symbol.overflowBox[2]
|
428 | viewBox[3] = Math.max viewBox[3], y + dy + symbol.overflowBox[3]
|
429 | x += symbol.width
|
430 | viewBox[2] = Math.max viewBox[2], x
|
431 | if symbol.height > rowHeight
|
432 | rowHeight = symbol.height
|
433 | y += rowHeight
|
434 | viewBox[3] = Math.max viewBox[3], y
|
435 |
|
436 | viewBox[2] = viewBox[2] - viewBox[0]
|
437 | viewBox[3] = viewBox[3] - viewBox[1]
|
438 |
|
439 | levelOrder = (level for level of levels).sort (x, y) -> x-y
|
440 | for level in levelOrder
|
441 | for node in levels[level]
|
442 | svg.appendChild node
|
443 | svg.setAttributeNS SVGNS, 'viewBox', viewBox.join ' '
|
444 | svg.setAttributeNS SVGNS, 'width', viewBox[2]
|
445 | svg.setAttributeNS SVGNS, 'height', viewBox[3]
|
446 | svg.setAttributeNS SVGNS, 'preserveAspectRatio', 'xMinYMin meet'
|
447 | out = new XMLSerializer().serializeToString doc
|
448 |
|
449 |
|
450 | out = out.replace /\sxmlns:xlink=""/g, ''
|
451 | '''
|
452 | <?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
453 | <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
454 |
|
455 | ''' + prettyXML out
|
456 |
|
457 | class ASCIIDrawing extends Drawing
|
458 | @title: "ASCII drawing (one character per symbol)"
|
459 | @parse: (data) ->
|
460 | @load splitIntoLines data
|
461 |
|
462 | class DSVDrawing extends Drawing
|
463 | @parse: (data) ->
|
464 |
|
465 | if data[-2..] == '\r\n'
|
466 | data = data[...-2]
|
467 | else if data[-1..] in ['\r', '\n']
|
468 | data = data[...-1]
|
469 |
|
470 | @load require('csv-parse/lib/sync') data,
|
471 | delimiter: @delimiter
|
472 | relax_column_count: true
|
473 |
|
474 | class SSVDrawing extends DSVDrawing
|
475 | @title: "Space-delimiter drawing (one word per symbol)"
|
476 | @delimiter: ' '
|
477 | @parse: (data) ->
|
478 |
|
479 | super data.replace /[ \t\f\v]+/g, ' '
|
480 |
|
481 | class CSVDrawing extends DSVDrawing
|
482 | @title: "Comma-separated drawing (spreadsheet export)"
|
483 | @delimiter: ','
|
484 |
|
485 | class TSVDrawing extends DSVDrawing
|
486 | @title: "Tab-separated drawing (spreadsheet export)"
|
487 | @delimiter: '\t'
|
488 |
|
489 | class Drawings extends Input
|
490 | @filenameSeparator = '_'
|
491 | constructor: (@drawings) ->
|
492 | super()
|
493 | @load: (datas) ->
|
494 | new @ (
|
495 | for data in datas
|
496 | drawing = Drawing.load data
|
497 | drawing.subname = data.subname
|
498 | drawing
|
499 | )
|
500 | writeSVG: (mappings, filename) ->
|
501 | for drawing in @drawings
|
502 | drawing.writeSVG mappings,
|
503 | if @drawings.length > 1
|
504 | filename2 = path.parse filename ? @filename
|
505 | filename2.base = filename2.base[...-filename2.ext.length]
|
506 | filename2.base += @constructor.filenameSeparator + drawing.subname
|
507 | filename2.base += '.svg'
|
508 | path.format filename2
|
509 | else
|
510 | drawing.filename = @filename
|
511 | filename
|
512 |
|
513 | class XLSXDrawings extends Drawings
|
514 | @encoding: 'binary'
|
515 | @title: "Spreadsheet drawing(s) (Excel/OpenDocument/Lotus/dBASE)"
|
516 | @parse: (data) ->
|
517 | xlsx = require 'xlsx'
|
518 | workbook = xlsx.read data, type: 'binary'
|
519 |
|
520 | @load (
|
521 | for subname in workbook.SheetNames
|
522 | sheet = workbook.Sheets[subname]
|
523 | if subname.length == 31
|
524 | console.warn "Warning: Sheet '#{subname}' has length exactly 31, which may be caused by Google Sheets export truncation"
|
525 | rows = xlsx.utils.sheet_to_json sheet,
|
526 | header: 1
|
527 | defval: ''
|
528 | rows.subname = subname
|
529 | rows
|
530 | )
|
531 |
|
532 | class Context
|
533 | constructor: (@symbols, @i, @j) ->
|
534 | @symbol = @symbols[@i]?[@j]
|
535 | @key = @symbol?.key
|
536 | neighbor: (dj, di) ->
|
537 | new Context @symbols, @i + di, @j + dj
|
538 | includes: (args...) ->
|
539 | @symbol? and @symbol.includes args...
|
540 | row: (di = 0) ->
|
541 | i = @i + di
|
542 | for symbol, j in @symbols[i] ? []
|
543 | new Context @symbols, i, j
|
544 | column: (dj = 0) ->
|
545 | j = @j + dj
|
546 | for row, i in @symbols
|
547 | new Context @symbols, i, j
|
548 |
|
549 | extensionMap =
|
550 | '.txt': ASCIIMapping
|
551 | '.js': JSMapping
|
552 | '.coffee': CoffeeMapping
|
553 | '.asc': ASCIIDrawing
|
554 | '.ssv': SSVDrawing
|
555 | '.csv': CSVDrawing
|
556 | '.tsv': TSVDrawing
|
557 |
|
558 | '.xlsx': XLSXDrawings
|
559 | '.xlsm': XLSXDrawings
|
560 | '.xlsb': XLSXDrawings
|
561 | '.xls': XLSXDrawings
|
562 | '.ods': XLSXDrawings
|
563 | '.fods': XLSXDrawings
|
564 | '.dif': XLSXDrawings
|
565 | '.prn': XLSXDrawings
|
566 | '.dbf': XLSXDrawings
|
567 |
|
568 | sanitize = true
|
569 | bufferSize = 16*1024
|
570 |
|
571 | postprocess = (format, filename) ->
|
572 | return unless sanitize
|
573 | try
|
574 | switch format
|
575 | when 'pdf'
|
576 |
|
577 |
|
578 | buffer = Buffer.alloc bufferSize
|
579 | fileSize = fs.statSync(filename).size
|
580 | position = Math.max 0, fileSize - bufferSize
|
581 | file = fs.openSync filename, 'r+'
|
582 | readSize = fs.readSync file, buffer, 0, bufferSize, position
|
583 | string = buffer.toString 'binary'
|
584 | match = /\/CreationDate\s*\((?:[^()\\]|\\[^])*\)/.exec string
|
585 | if match?
|
586 | fs.writeSync file, ' '.repeat(match[0].length), position + match.index
|
587 | fs.closeSync file
|
588 | catch e
|
589 | console.log "Failed to postprocess '#{filename}': #{e}"
|
590 |
|
591 | svg2 = (format, svg, sync) ->
|
592 | child_process = require 'child_process'
|
593 | filename = path.parse svg
|
594 | if filename.ext == ".#{format}"
|
595 | filename.base += ".#{format}"
|
596 | else
|
597 | filename.base = "#{filename.base[...-filename.ext.length]}.#{format}"
|
598 | output = path.format filename
|
599 | args = [
|
600 | '-z'
|
601 | "--file=#{svg}"
|
602 | "--export-#{format}=#{output}"
|
603 | ]
|
604 | if sync
|
605 |
|
606 |
|
607 | console.log '=>', output
|
608 | result = child_process.spawnSync 'inkscape', args, stdio: 'inherit'
|
609 | if result.error
|
610 | console.log result.error.message
|
611 | else if result.status or result.signal
|
612 | console.log ":-( #{output} FAILED"
|
613 | else
|
614 | postprocess format, output
|
615 | else
|
616 |
|
617 |
|
618 |
|
619 | (resolve) ->
|
620 | console.log '=>', output
|
621 | inkscape = require('child_process').spawn 'inkscape', args
|
622 | out = ''
|
623 | inkscape.stdout.on 'data', (buf) -> out += buf
|
624 | inkscape.stderr.on 'data', (buf) -> out += buf
|
625 | inkscape.on 'error', (error) ->
|
626 | console.log error.message
|
627 | inkscape.on 'exit', (status, signal) ->
|
628 | if status or signal
|
629 | console.log ":-( #{output} FAILED:"
|
630 | console.log out
|
631 | else
|
632 | postprocess format, output
|
633 | resolve()
|
634 |
|
635 | help = ->
|
636 | console.log """
|
637 | Usage: #{process.argv[1]} (...options and filenames...)
|
638 | Documentation: https://github.com/edemaine/svgtiler#svg-tiler
|
639 |
|
640 | Optional arguments:
|
641 | --help Show this help message and exit.
|
642 | -m / --margin Don't delete blank extreme rows/columns
|
643 | --tw TILE_WIDTH / --tile-width TILE_WIDTH
|
644 | Force all symbol tiles to have specified width
|
645 | --th TILE_HEIGHT / --tile-height TILE_HEIGHT
|
646 | Force all symbol tiles to have specified height
|
647 | -p / --pdf Convert output SVG files to PDF via Inkscape
|
648 | -P / --png Convert output SVG files to PNG via Inkscape
|
649 | --no-sanitize Don't sanitize PDF output by blanking out /CreationDate
|
650 | -j N / --jobs N Run up to N Inkscape jobs in parallel
|
651 |
|
652 | Filename arguments: (mappings before drawings!)
|
653 |
|
654 | """
|
655 | for extension, klass of extensionMap
|
656 | if extension.length < 10
|
657 | extension += ' '.repeat 10 - extension.length
|
658 | console.log " *#{extension} #{klass.title}"
|
659 | console.log " #{klass.help}" if klass.help?
|
660 | console.log """
|
661 |
|
662 | SYMBOL specifiers: (omit the quotes in anything except .js and .coffee files)
|
663 |
|
664 | 'filename.svg': load SVG from specified file
|
665 | 'filename.png': include PNG image from specified file
|
666 | 'filename.jpg': include JPEG image from specified file
|
667 | '<svg>...</svg>': raw SVG
|
668 | -> ...@key...: function computing SVG, with `this` bound to Context with
|
669 | `key` set to symbol name, `i` and `j` set to coordinates,
|
670 | and supporting `neighbor` and `includes` methods.
|
671 | """
|
672 |
|
673 | process.exit()
|
674 |
|
675 | main = ->
|
676 | mappings = new Mappings
|
677 | args = process.argv[2..]
|
678 | files = skip = 0
|
679 | formats = []
|
680 | jobs = []
|
681 | sync = true
|
682 | for arg, i in args
|
683 | if skip
|
684 | skip--
|
685 | continue
|
686 | switch arg
|
687 | when '-h', '--help'
|
688 | help()
|
689 | when '-m', '--margin'
|
690 | Drawing.keepMargins = true
|
691 | when '--tw', '--tile-width'
|
692 | Symbol.forceWidth = parseFloat args[i+1]
|
693 | skip = 1
|
694 | when '--th', '--tile-height'
|
695 | Symbol.forceHeight = parseFloat args[i+1]
|
696 | skip = 1
|
697 | when '-p', '--pdf'
|
698 | formats.push 'pdf'
|
699 | when '-P', '--png'
|
700 | formats.push 'png'
|
701 | when '--no-sanitize'
|
702 | sanitize = false
|
703 | when '-j', '--jobs'
|
704 | jobs = new require('async-limiter') concurrency: parseInt args[i+1]
|
705 | sync = false
|
706 | skip = 1
|
707 | else
|
708 | files++
|
709 | console.log '*', arg
|
710 | input = Input.load arg
|
711 | if input instanceof Mapping
|
712 | mappings.push input
|
713 | else if input instanceof Drawing or input instanceof Drawings
|
714 | filenames = input.writeSVG mappings
|
715 | for format in formats
|
716 | if typeof filenames == 'string'
|
717 | jobs.push svg2 format, filenames, sync
|
718 | else
|
719 | for filename in filenames
|
720 | jobs.push svg2 format, filename, sync
|
721 | unless files
|
722 | console.log 'Not enough filename arguments'
|
723 | help()
|
724 |
|
725 | unless window?
|
726 | main()
|