UNPKG

25.7 kBtext/coffeescriptView Raw
1`#!/usr/bin/env node
2`
3path = require 'path'
4fs = require 'fs'
5xmldom = require 'xmldom'
6DOMParser = xmldom.DOMParser
7domImplementation = new xmldom.DOMImplementation()
8XMLSerializer = xmldom.XMLSerializer
9prettyXML = require 'prettify-xml'
10
11SVGNS = 'http://www.w3.org/2000/svg'
12XLINKNS = 'http://www.w3.org/1999/xlink'
13
14splitIntoLines = (data) ->
15 data.replace('\r\n', '\n').replace('\r', '\n').split('\n')
16whitespace = /[\s\uFEFF\xA0]+/ ## based on https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/Trim
17
18extensionOf = (filename) -> path.extname(filename).toLowerCase()
19
20class SVGTilerException
21 constructor: (@message) ->
22 toString: ->
23 "svgtiler: #{@message}"
24
25overflowBox = (xml) ->
26 if xml.documentElement.hasAttribute 'overflowBox'
27 xml.documentElement.getAttribute('overflowBox').split /\s*,?\s+/
28 .map parseFloat
29 else
30 null
31
32svgBBox = (xml) ->
33 ## xxx Many unsupported features!
34 ## - transformations
35 ## - used symbols/defs
36 ## - paths
37 ## - text
38 ## - line widths which extend bounding box
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
94zIndex = (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
101class Symbol
102 @svgEncoding: 'utf8'
103 @forceWidth: null ## default: no size forcing
104 @forceHeight: null ## default: no size forcing
105
106 ###
107 Attempt to render pixels as pixels, as needed for old-school graphics.
108 SVG 1.1 and Inkscape define image-rendering="optimizeSpeed" for this.
109 Chrome doesn't support this, but supports a CSS3 (or SVG) specification of
110 "image-rendering:pixelated". Combining these seems to work everywhere.
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 ## No <'s -> interpret as filename
126 extension = extensionOf data
127 ## <image> tag documentation: "Conforming SVG viewers need to
128 ## support at least PNG, JPEG and SVG format files."
129 ## [https://svgwg.org/svg2-draft/embedded.html#ImageElement]
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 ## ECMA6: @key.includes substring
149
150zeroSizeReplacement = 1
151
152class 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 SVG's viewBox has a special rule that "A value of zero [in <width>
166 or <height>] disables rendering of the element." Avoid this.
167 [https://www.w3.org/TR/SVG11/coords.html#ViewBoxAttribute]
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 id/href follows the IRI spec [https://tools.ietf.org/html/rfc3987]:
192 ifragment = *( ipchar / "/" / "?" )
193 ipchar = iunreserved / pct-encoded / sub-delims / ":" / "@"
194 iunreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" / ucschar
195 pct-encoded = "%" HEXDIG HEXDIG
196 sub-delims = "!" / "$" / "&" / "'" / "(" / ")"
197 / "*" / "+" / "," / ";" / "="
198 ucschar = %xA0-D7FF / %xF900-FDCF / #%xFDF0-FFEF
199 / %x10000-1FFFD / %x20000-2FFFD / %x30000-3FFFD
200 / %x40000-4FFFD / %x50000-5FFFD / %x60000-6FFFD
201 / %x70000-7FFFD / %x80000-8FFFD / %x90000-9FFFD
202 / %xA0000-AFFFD / %xB0000-BFFFD / %xC0000-CFFFD
203 / %xD0000-DFFFD / %xE1000-EFFFD
204 We also want to escape colon (:) which seems to cause trouble.
205 We use encodeURIComponent which escapes everything except
206 A-Z a-z 0-9 - _ . ! ~ * ' ( )
207 [https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent]
208 Unfortunately, Inkscape seems to ignore any %-encoded symbols; see
209 https://bugs.launchpad.net/inkscape/+bug/1737778
210 So we replace '%' with '$', an allowed character that's already escaped.
211 ###
212 encodeURIComponent @key
213 .replace /%/g, '$'
214 #use: -> @ ## do nothing for static symbol
215
216class 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
228class 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
242class 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 ## Cache return value of function so that only one Symbol generated
260 ## for each key. It still may be a DynamicSymbol, which will allow
261 ## it to make multiple versions, but keep track of which are the same.
262 value = @function key
263 if value?
264 @map[key] = Symbol.parse key, value
265 else
266 value
267 else
268 undefined
269
270class 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 ## Single whitespace character at beginning defines blank character
281 key = ''
282 else
283 ## Multiple whitespace at beginning defines first whitespace character
284 key = line[0]
285 else
286 key = line[...separator.index]
287 map[key] = line[separator.index + separator[0].length..]
288 new @ map
289
290class 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
296class 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
302class 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
313blankCells =
314 '': true
315 ' ': true ## for ASCII art in particular
316
317allBlank = (list) ->
318 for x in list
319 if x? and x not of blankCells
320 return false
321 true
322
323class Drawing extends Input
324 constructor: (@data) ->
325 super()
326 @load: (data) ->
327 ## Turn strings into arrays
328 data = for row in data
329 for cell in row
330 cell
331 unless Drawing.keepMargins
332 ## Top margin
333 while data.length > 0 and allBlank data[0]
334 data.shift()
335 ## Bottom margin
336 while data.length > 0 and allBlank data[data.length-1]
337 data.pop()
338 if data.length > 0
339 ## Left margin
340 while allBlank (row[0] for row in data)
341 for row in data
342 row.shift()
343 ## Right margin
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 ## Default filename is the input filename with extension replaced by .svg
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 #svg.appendChild defs = doc.createElementNS SVGNS, 'defs'
369 ## Look up all symbols in the drawing.
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 ## Instantiate (.use) all (dynamic) symbols in the drawing.
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 ## Include all used symbols in SVG
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 ## Remove a layer of indirection for <svg> and <symbol>
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 ## Set/overwrite any viewbox attribute with one from symbol.
404 if symbol.viewBox?
405 node.setAttribute 'viewBox', symbol.viewBox
406 ## Lay out the symbols in the drawing via SVG <use>.
407 viewBox = [0, 0, 0, 0] ## initially x-min, y-min, x-max, y-max
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 ## Change from x-min, y-min, x-max, y-max to x-min, y-min, width, height
436 viewBox[2] = viewBox[2] - viewBox[0]
437 viewBox[3] = viewBox[3] - viewBox[1]
438 ## Sort by level
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 ## Parsing xlink:href in user's SVG fragments, and then serializing,
449 ## can lead to these null namespace definitions. Remove.
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
457class ASCIIDrawing extends Drawing
458 @title: "ASCII drawing (one character per symbol)"
459 @parse: (data) ->
460 @load splitIntoLines data
461
462class DSVDrawing extends Drawing
463 @parse: (data) ->
464 ## Remove trailing newline / final blank line.
465 if data[-2..] == '\r\n'
466 data = data[...-2]
467 else if data[-1..] in ['\r', '\n']
468 data = data[...-1]
469 ## CSV parser.
470 @load require('csv-parse/lib/sync') data,
471 delimiter: @delimiter
472 relax_column_count: true
473
474class SSVDrawing extends DSVDrawing
475 @title: "Space-delimiter drawing (one word per symbol)"
476 @delimiter: ' '
477 @parse: (data) ->
478 ## Coallesce non-newline whitespace into single space
479 super data.replace /[ \t\f\v]+/g, ' '
480
481class CSVDrawing extends DSVDrawing
482 @title: "Comma-separated drawing (spreadsheet export)"
483 @delimiter: ','
484
485class TSVDrawing extends DSVDrawing
486 @title: "Tab-separated drawing (spreadsheet export)"
487 @delimiter: '\t'
488
489class 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 ## use Drawing default if not filename?
511 filename
512
513class 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 ## https://www.npmjs.com/package/xlsx#common-spreadsheet-format
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
532class 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
549extensionMap =
550 '.txt': ASCIIMapping
551 '.js': JSMapping
552 '.coffee': CoffeeMapping
553 '.asc': ASCIIDrawing
554 '.ssv': SSVDrawing
555 '.csv': CSVDrawing
556 '.tsv': TSVDrawing
557 ## Parsable by xlsx package:
558 '.xlsx': XLSXDrawings ## Excel 2007+ XML Format
559 '.xlsm': XLSXDrawings ## Excel 2007+ Macro XML Format
560 '.xlsb': XLSXDrawings ## Excel 2007+ Binary Format
561 '.xls': XLSXDrawings ## Excel 2.0 or 2003-2004 (SpreadsheetML)
562 '.ods': XLSXDrawings ## OpenDocument Spreadsheet
563 '.fods': XLSXDrawings ## Flat OpenDocument Spreadsheet
564 '.dif': XLSXDrawings ## Data Interchange Format (DIF)
565 '.prn': XLSXDrawings ## Lotus Formatted Text
566 '.dbf': XLSXDrawings ## dBASE II/III/IV / Visual FoxPro
567
568sanitize = true
569bufferSize = 16*1024
570
571postprocess = (format, filename) ->
572 return unless sanitize
573 try
574 switch format
575 when 'pdf'
576 ## Blank out /CreationDate in PDF for easier version control.
577 ## Replace these commands with spaces to avoid in-file pointer errors.
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' ## must use single-byte encoding!
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
591svg2 = (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 ## In sychronous mode, we let inkscape directly output its error messages,
606 ## and add warnings about any failures that occur.
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 ## In asychronous mode, we capture inkscape's outputs, and print them only
617 ## when the process has finished, along with which file failed, to avoid
618 ## mixing up messages from parallel executions.
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
635help = ->
636 console.log """
637Usage: #{process.argv[1]} (...options and filenames...)
638Documentation: https://github.com/edemaine/svgtiler#svg-tiler
639
640Optional 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
652Filename 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
662SYMBOL 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 #object with one or more attributes
673 process.exit()
674
675main = ->
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
725unless window?
726 main()