Skip to content

Commit

Permalink
Selective conversion via timestamps, -f option (fix #2)
Browse files Browse the repository at this point in the history
  • Loading branch information
edemaine committed May 31, 2022
1 parent ecdce9c commit 944de4e
Show file tree
Hide file tree
Showing 2 changed files with 59 additions and 15 deletions.
17 changes: 14 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,23 @@ svgink --pdf -o pdf filename1.svg filename2.svg

A major advantage of `svgink` is that it quickly converts many SVG files.

First, `svgink` uses Inkscape's
First, `svgink` skips converting SVG files that are older than the
corresponding PDF/PNG file (similar to `make`).
You can override this behavior via the `--force` command-line option,
which forces all conversions to be done.
This is useful if you update Inkscape, update `svgink`, or
a conversion failed and somehow generated a bad file.
(Alternatively, you can `touch` the relevant SVG files
or `rm` the relevant PDF/PNG files.)

Second, `svgink` uses Inkscape's
[shell protocol](https://wiki.inkscape.org/wiki/Using_the_Command_Line#Shell_mode)
to run a sequence of conversions with a single Inkscape process.
This is much faster than running Inkscape individually on each SVG file
(especially on Windows, where spawning a process takes seconds),
which is what might happen most naturally with a Makefile.
which is what might happen most naturally with conversions driven by `make`.

Second, `svgink` runs multiple Inkscape processes to exploit multicore CPUs.
Third, `svgink` runs multiple Inkscape processes to exploit multicore CPUs.
By default, it runs half as many Inkscape processes as there are logical cores
on your machine (to account for typical hyperthreading which presents *n*
physical cores as 2 *n* logical cores).
Expand Down Expand Up @@ -78,6 +87,7 @@ Usage: svgink (...options and filenames...)
Filenames should specify SVG files.
Optional arguments:
-h / --help Show this help message and exit.
-f / --force Force conversion even if output newer than SVG input
-o DIR / --output DIR Write all output files to directory DIR
--op DIR / --output-pdf DIR Write all .pdf files to directory DIR
--oP DIR / --output-png DIR Write all .png files to directory DIR
Expand Down Expand Up @@ -172,6 +182,7 @@ The API provides two classes:
The constructors for `SVGProcessor` and `Inkscape` take a single optional
argument, which is a settings object. It can have the following properties:

* `force`: Whether to force conversion, even if SVG file is older than target.
* `inkscape`: Path to inkscape. Default searches PATH for `inkscape`.
* `jobs`: Maximum number of Inkscapes to run in parallel.
Default = half the number of logical CPUs
Expand Down
57 changes: 45 additions & 12 deletions svgink.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ child_process = require 'child_process'
fs = require 'fs/promises'

defaultSettings =
## Whether to force conversion, even if SVG file is older than target.
force: false
## Path to inkscape. Default searches PATH.
inkscape: 'inkscape'
## Maximum number of Inkscapes to run in parallel.
Expand Down Expand Up @@ -161,19 +163,34 @@ class SVGProcessor
@inkscapes = []
@queue = []
@spawning = false
@jobs = 0
convert: (input, output) ->
## Convert input filename to output filename, and then sanitize.
## Returns a Promise.
## Convert input filename to output filename, and then sanitize,
## unless output is newer than input or forced. Returns a Promise.
@jobs++
new Promise (resolve, reject) =>
for filename in [input, output]
if invalidFilename filename
jobs--
return reject new InkscapeError "Inkscape shell does not support filenames with semicolons or leading/trailing spaces: #{filename}"
@queue.push {input, output, resolve, reject}
@update()
## Compare input and output modification times, unless forced.
unless @settings.force
try
outputStat = await fs.stat output
inputStat = await fs.stat input
unless inputStat? and outputStat? and inputStat.mtime < outputStat.mtime
@queue.push {input, output, resolve, reject}
@update()
else
@jobs--
resolve {skip: true}
@update() if @closing # resolve close() in case last job is skipped
undefined
run: (job) ->
## Queue job for Inkscape to run. Returns a Promise.
## Job can be a string to send to the shell, or an object with
## `input` and `output` properties, for conversion (without sanitization).
@jobs++
job = {job} if typeof job == 'string'
new Promise (resolve, reject) =>
@queue.push {...job, resolve, reject}
Expand All @@ -188,8 +205,9 @@ class SVGProcessor
## Filter out any Inkscape processes that died, e.g. from idle timeout.
@inkscapes = (inkscape for inkscape in @inkscapes when not inkscape.dead)
## Check for completed closing.
if @closing and not @queue.length and
@inkscapes.every (inkscape) -> not inkscape.job?
if @closing and @jobs <= 0
#and not @queue.length and
#@inkscapes.every (inkscape) -> not inkscape.job?
## Schedule close() promise to resolve after job promise resolves.
setTimeout (=> @closing()), 0
return
Expand Down Expand Up @@ -228,6 +246,8 @@ class SVGProcessor
data
.then (data) =>
job.resolve data
@jobs--
@update()
.catch (error) =>
job.reject error
sanitize: (output) ->
Expand Down Expand Up @@ -257,6 +277,7 @@ Documentation: https://github.com/edemaine/svgink
Filenames should specify SVG files.
Optional arguments:
-h / --help Show this help message and exit.
-f / --force Force conversion even if output newer than SVG input
-o DIR / --output DIR Write all output files to directory DIR
--op DIR / --output-pdf DIR Write all .pdf files to directory DIR
--oP DIR / --output-png DIR Write all .png files to directory DIR
Expand All @@ -274,14 +295,20 @@ main = (args = process.argv[2..]) ->
formats = []
outputDir = null
outputDirExt = {}
files = skip = 0
files =
input: 0
output: 0
skip: 0
skip = 0
for arg, i in args
if skip
skip--
continue
switch arg
when '-h', '--help'
help()
when '-f', '--force'
settings.force = true
when '-i', '--inkscape'
skip = 1
settings.inkscape = args[i+1]
Expand All @@ -308,9 +335,10 @@ main = (args = process.argv[2..]) ->
else
console.warn "Invalid argument to --jobs: #{args[i+1]}"
else
files++
files.input++
input = arg
for format in formats
files.output++
output = path.parse input
delete output.base
if output.ext != ".#{format}"
Expand All @@ -325,9 +353,13 @@ main = (args = process.argv[2..]) ->
do (input, output) ->
processor.convert input, output
.then (data) ->
console.log "* #{input} -> #{output}"
console.log data.stdout if data.stdout
console.log data.stderr if data.stderr
if data.skip
files.skip++
console.log "- #{input} -> #{output} (skipped)"
else
console.log "* #{input} -> #{output}"
console.log data.stdout if data.stdout
console.log data.stderr if data.stderr
.catch (error) ->
console.log "! #{input} -> #{output} FAILED"
console.log error
Expand All @@ -339,7 +371,8 @@ main = (args = process.argv[2..]) ->
console.log '! Not enough filename arguments'
help()
else
console.log "> Converted #{files} SVG files into #{files * formats.length} files in #{Math.round((new Date) - start) / 1000} seconds"
console.log "> Converted #{files.input} SVG files into #{files.output} files (#{files.output - files.skip} updated) in #{Math.round((new Date) - start) / 1000} seconds"
console.log "> Skipped #{files.skip} conversions. To force conversion, use --force" if files.skip

module.exports = {
defaultSettings
Expand Down

0 comments on commit 944de4e

Please sign in to comment.