Skip to content

Latest commit

 

History

History
504 lines (432 loc) · 21.6 KB

README.md

File metadata and controls

504 lines (432 loc) · 21.6 KB

svgink: Efficiently convert SVG files to PDF or PNG via Inkscape

Table of Contents

Overview

How many times have you clicked File menu / Save a Copy / Save as type / Portable Document Format (*.pdf) / Save / OK / Replace (in Inkscape, or similar sequences in other drawing programs), after every edit you make to an SVG drawing?

svgink makes it easy to convert to PDF any changed SVG files:

svgink --pdf *.svg
# Shorthand:
svgink -p *.svg

For longer figure drawing sessions, you can keep svgink running and watching for any changes to SVG files, automatically converting any that you change:

# Watch for changed and new SVG files and autoconvert to PDF.
# (Quotes are necessary to enable detection of new SVG files.)
svgink --watch --pdf '*.svg'
# Watch for changed and new SVG files in any recursive subdirectory
svgink --watch --pdf '**/*.svg'
# Shorthand:
svgink -w -p '*.svg'
svgink -w -p '**/*.svg'
# Press Ctrl+C to abort a running svgink.

Usage

Inkscape can convert an SVG file into PDF or PNG using its command-line interface like so:

# One file with custom output filename/directory:
inkscape --export-filename=output.pdf input.svg
# Multiple files with output filename/directory matching input:
inkscape --export-type=pdf filename1.svg filename2.svg

The svgink command-line tool provides a simpler command-line tool to do the same thing, in particular evading your having to remember the exact --export option format.

# Basic use, PDF mode:
svgink --pdf filename1.svg filename2.svg
# Basic use, PNG mode:
svgink --png filename1.svg filename2.svg
# Convert to both PDF and PNG:
svgink --pdf --png filename1.svg filename2.svg
# Shorthands for above three commands:
svgink -p filename1.svg filename2.svg
svgink -P filename1.svg filename2.svg
svgink -p -P filename1.svg filename2.svg
# Custom output directories:
svgink -p -o pdf filename1.svg filename2.svg
svgink -p -P --op pdf --oP png filename1.svg filename2.svg
# Force conversion even if SVG files haven't changed:
svgink -p -f filename1.svg filename2.svg
# Continuously watch for pattern of new or changed SVG files:
svgink --watch --pdf '**/*.svg'

Efficiency

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

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 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 conversions driven by make.

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). You can override the number of Inkscape processes to use via -j 4 or similar. Note that the processes are started in sequence rather than in parallel, so you may not get full job parallelism unless you have enough jobs. (Why in sequence? On Windows, starting many Inkscape processes in parallel slows them all down; and on Linux, starting Inkscape processes is fast enough to not be a big deal. This behavior also prevents svgink from starting more Inkscape processes than necessary, in case all jobs complete faster than the startup process.)

Output Sanitization

svgink also tries to make version control easier with compiled PDF outputs (which are useful to check in to avoid requiring Inkscape to build). Normally, Inkscape includes a /CreationDate field in the generated PDF, so each time you build the PDF files, the files change. By default, svgink strips this date out, so the generated PDF files should be identical across multiple runs (assuming matching Inkscape versions). You can turn off this behavior via the --no-sanitize command-line option.

Watch: Automatic Conversion of Changed SVG Files

svgink provides a "watch" mechanism to continuously convert files whenever they change. Use this when actively editing SVG files.

# Watch matching files
svgink --watch --pdf 'fig*.svg'
# Watch *.svg in all descendant directories
svgink --watch --pdf '**/*.svg'
# Watch a directory, which implicitly watches *.svg in that directory
svgink --watch --pdf figs

Use Ctrl+C to stop watching.

In the first two examples, the quotes prevent the shell from expanding glob patterns (here, * and **) to the current list of matching files. Without quotes, svgink will just detect changes to the initial list of matching files. Adding quotes allows svgink to also detect a new matching file, e.g., a newly created fignew.svg.

Globs are resolved via node-glob which supports notation such as {this,that}, *drawing*.svg, figs/**/*.svg, etc. On Windows, the preferred path separator for globs is forward slashes (/); you can also use backward slashes (\), except when they would serve to escape glob patterns. For example, figs\* will be treated as the literal filename figs*, while figs/* will be treated as "all files in directory figs".

To detect when globs might match new files, svgink watches all prefix directories of matched files. This may fail to detect new matching files in a directory that previously had no matching files; in this case, touch any matching directory to trigger rechecking, or restart svgink.

Cleaning Up Generated Files

To clean up PDF/PNG files that would be updated/generated by svgink (similar to make clean), you can specify the --clean command-line option. For example:

# Delete filename.{pdf,png}
svgink --clean --pdf --png filename.svg
# Delete fig*.pdf
svgink --clean --pdf fig*.svg

Installation

After installing Node (v12+) and installing Inkscape (v1+), you can install this tool via

npm install -g svgink

This should install an svgink command-line tool on your path.

Command-Line Interface

Run svgink to see the supported command-line options:

Usage: svgink (...options and filenames/directories/globs...)

Filenames or glob patterns should specify SVG files.
Directories implicitly refer to *.svg within the directory.
Optional arguments:
  -h / --help           Show this help message and exit.
  -p / --pdf            Convert SVG files to PDF via Inkscape
  -P / --png            Convert SVG files to PNG via Inkscape
  -w / --watch          Continuously watch for changed files and convert them
  -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
  --clean               Delete PDF/PNG files that would be generated
  -i PATH / --inkscape PATH     Specify PATH to Inkscape binary
  --no-sanitize         Don't sanitize PDF output by blanking out /CreationDate
  --relative            Run jobs with relative paths (default uses absolute)
  -j N / --jobs N       Run up to N Inkscape jobs in parallel

These options are intentionally similar to SVG Tiler.

API

You can also use svgink from your Node programs. For example, SVG Tiler uses svgink to convert generated SVG files to other formats.

First, install svgink as a dependency in your project:

npm install svgink

Then you can require('svgink') or import ... from 'svgink'.

Examples

Here is a simple example of converting two files:

import {SVGProcessor} from 'svgink';
const processor = new SVGProcessor();
processor.convert('input1.svg', 'output1.pdf')
.then(() => console.log('converted first file'));
processor.convert('input2.svg', 'output2.pdf');
.then(() => console.log('converted second file'));
await processor.close();
console.log('finished all conversions');

Here is a more advanced example of converting a blob specification:

import {SVGProcessor} from 'svgink';
const processor = new SVGProcessor();
process.on('converted', (job) =>
  console.log(`converted ${job.input} to ${job.output}`)
);
processor.convertGlob('*.svg', ['pdf', 'png']);
await processor.close();
console.log('finished all conversions');

Alternatively, you can access the command-line interface (including all printed messages) like so:

import {SVGProcessor} from 'svgink';
main(['-p', '-j', '4', '-o', 'pdf', '*.svg'])
.then(() => console.log('finished all conversions'));

SVGProcessor Class

The main interface to the API is via the SVGProcessor class, which handles spawning one or more Inkscape processes to convert one or more files or run Inkscape shell jobs. Create one with new SVGProcessor which takes an optional settings object. The resulting instance provides the following methods:

  • convertGlob(input, formats) queues conversion job(s) where input can be a filename, a directory name (which means "process all .svg files in that directory"), or a glob resolving to SVG files. Globs are resolved via node-glob which supports notation such as {this,that}, *drawing*.svg, figs/**/*.svg, etc. The output formats should be "pdf", ".pdf", "png", ".png", another format/extension supported by Inkscape, or an array thereof. To be notified of conversions and/or errors, you should listen to the corresponding events.
  • convertTo(input, formats) queues converting one filename to the specified format(s), followed by sanitizing the output. The input file should be SVG. The output formats should be "pdf", ".pdf", "png", ".png", another format/extension supported by Inkscape, or an array thereof. It returns a promise, or an array of promises if formats is an array, where each promise resolves to a {skip, stdout, stderr, input, output, inputAbs, outputAbs} object when the conversion and sanitization are complete. Here either skip is true meaning that conversion was skipped because the input was older than the output and settings.force was false, or stdout and stderr give the string contents from Inkscape's stdout and stderr for this job, which you should print to display warnings and/or errors. In addition, input is the original input filename, output is the generated output filename with format extension, and inputAbs and outputAbs are the corresponding absolute paths (unless settings.relative is true). The promise also has an output property with the output filename in case it's needed earlier.
  • convert(input, output) queues converting one filename to another, followed by sanitizing the output. The input file should be SVG. The output format is determined from the file extension. It returns a promise which resolves to a {stdout, stderr, skip, input, output, inputAbs, outputAbs} object when the conversion and sanitization are complete. The absolute paths inputAbs and outputAbs are generated automatically, unless settings.relative is true.
  • run(job) queues a given job. A job can be a string to send to Inkscape directly, an object with a job string property, or an object of the form {input: 'input.svg', output: 'output.pdf', inputAbs: '/full/path/input.svg', outputAbs: '/full/path/output.pdf} (where inputAbs and outputAbs are optional), but scheduling a conversion in this way will skip sanitization and force conversion (skip modification time checking). Returns a promise which resolves to a {stdout, stderr} object, along with the job or input/output properties from the given job, when the job is complete.
  • sanitize(output) optionally sanitizes the given output filename. You could override this method to support custom sanitization behavior. It normally returns a promise.
  • watch(inputs, formats) continuously watches for changes to the filename(s) in inputs, and when they change (and settle from changing), converts the file to all format(s) in formats (like convertTo). Each input in inputs can be a glob or directory name, as in convertGlob. To be notified of conversions and/or errors, you should listen to the corresponding events. This API does not currently normalize paths to absolute paths, so if you're going to be changing process.cwd(), you should normalize yourself.
  • wait() returns a promise which resolves when all jobs are complete. Only one wait() or close() should be active at once.
  • close() shuts down Inkscape processes once all conversion jobs added so far are done. (Do not add jobs after calling close().) It returns a promise which resolves when all jobs are complete (though Inkscape processes may still be shutting down).

Inkscape Class

You can also access the shell interface of a single Inkscape process via the lower-level Inkscape class. Create one with new Inkscape which takes an optional settings object. The resulting instance provides the following methods/attributes:

  • open(initialUnref) starts the Inkscape process. It returns a promise which resolves when Inkscape is ready for a job (has output a > prompt). initialUnref specifies whether to unref the Inkscape process before it finishes starting; set true for secondary Inkscape processes.
  • run(job) sends a given job to the Inkscape process, and returns a promise which resolves to a {stdout, stderr} object, along with the job or input/output properties from the given job, when Inkscape finishes the job. This method can be called only when Inkscape is ready (after the promise returned by open() or the last call to run() has resolved).
  • ready is a Boolean variable indicating whether Inkscape is ready for a new job via run().
  • close() attempts to gently close the Inkscape process via the quit command. If this times out, it kills the process via a signal.

Settings

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.
  • relative: Whether to run jobs with relative paths, or resolve to absolute paths. Absolute paths support changing directories between jobs, so this is the default, but relative paths are shorter and might bypass some limitations (such as no ; in a path name, or Cygwin quirks).
  • outputDir: Default directory to output files via convertTo. Default = null which means same directory as input.
  • outputDirExt: Object mapping from extensions (.pdf or .png) to directory to such output files via convertTo. Defaults = null which means to use outputDir.
  • clean: Whether to delete output files instead of creating them, like make clean.
  • 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 (= number of physical CPU cores assuming hyperthreading).
  • idle: If an Inkscape process sits idle for this many milliseconds, close it. Default = 1 minute. Set to null to disable.
  • startTimeout: If an Inkscape fails to start shell for this many milliseconds, fail. Default = 1 minute. Set to null to disable.
  • quitTimeout: If an Inkscape fails to close for this many milliseconds, kill it. Default = 1 second. Set to null to disable.
  • settle: Wait for an input file to stop changing for this many milliseconds before watch mode triggers conversion. Default = 1 second.
  • sanitize: Whether to sanitize PDF output by blanking out /CreationDate. Default = true.
  • bufferSize: Buffer size for sanitization. Default = 16KB.

The default settings are given by the defaultSettings export. If you want to override just some settings, you should duplicate and modify that object. For example:

import {SVGProcessor, defaultSettings} from 'svgink';
const processor = new SVGProcessor({...defaultSettings, jobs: 4});

Alternatively, you can modify defaultSettings to affect all future operations:

import {SVGProcessor, defaultSettings} from 'svgink';
defaultSettings.jobs = 4;
const processor = new SVGProcessor();

Events

SVGProcessor is an EventEmitter supporting the following events:

  • 'input' indicates that an input filename has is about to be converted to one or more formats via convertTo. This event is most useful in the context of convertGlob to determine which (or how many) filenames matched a glob pattern or directory.
  • 'converted' indicates that a file has just been successfully converted and sanitized into a single format. The event has one argument, a {skip, stdout, stderr, input, output, inputAbs, outputAbs} object as resolved from convert or convertTo. In particular:
    • input is the input filename.
    • output is the output filename.
    • inputAbs is the input absolute path (unless settings.relative is true).
    • outputAbs is the output absolute path (unless settings.relative is true).
    • skip is a Boolean indicating whether conversion was skipped because the input was older than the output and settings.force was false
    • stdout and stderr give the string contents from Inkscape's stdout and stderr for this job, which you should print to display warnings and/or errors. They are absent if skip is true.
  • 'cleaned' indicates that a file has been successfully deleted. The event has one argument, a {skip, input, output, inputAbs, outputAbs} object similarly to a 'converted' event. In particular, skip is a Boolean indicating whether cleaning was skipped because the output file did not exist.
  • 'ran' indicates that a general Inkscape job executed by run() has successfully completed. The event has one argument, an object with input and output properties as well as all properties from the job, as resolved from run().
  • 'error' indicates that something went wrong. The event has one argument, which is an Error of some sort (often InkscapeError), and may have input and output properties indicating the relevant filenames for conversion.

Limitations

Feel free to open a GitHub Issue if any of the following limitations pose an issue for you.

  • Assumes Inkscape version 1+
  • Several Inkscape conversion options are not yet represented by command-line options. For example:
    • PS, EPS, EMF, WMF, XAML conversion
    • export-dpi for PNG conversion
    • export-area-drawing instead of default export-area-page
  • The Inkscape shell protocol doesn't support filenames containing ; or starting or ending with spaces. It would be possible to work around this, especially without custom output directories, using the command line directly.

Related Work

There are several other packages and tools for interfacing with Inkscape and/or converting SVG files.

  • svg2pdf converts a directory of SVGs to a directory of PDFs, one Inkscape per job, and using threads instead of async. svgink's detection of default number of CPUs is based on svg2pdf's.
  • svink converts SVGs to PNGs, with support for lots of options corresponding to Inkscape's CLI, one Inkscape per job, and using threads (instead of async) only with multiple export IDs. svgink's name is inspired by svink's.
  • inkscape-export converts SVG files to PNG at one or more scales (without parallelism), with additional support for multi-frame animations.
  • node-inkscape provides a stream interface to an Inkscape process.
  • inkscape-cli provides an JavaScript interface to Inkscape's CLI.
  • svgexport renders SVG to PNG/JPEG using Puppeteer.
  • svg2 renders SVG to PNG using resvg-js.