Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

added support for multi-source input #53

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
183 changes: 139 additions & 44 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ function quote(val) {
// escape and quote the value if it is a string and this isn't windows
if (typeof val === 'string' && process.platform !== 'win32')
val = '"' + val.replace(/(["\\$`])/g, '\\$1') + '"';

return val;
}

Expand All @@ -16,76 +16,171 @@ function wkhtmltopdf(input, options, callback) {
callback = options;
options = {};
}

var output = options.output;
delete options.output;

// make sure the special keys are last
var extraKeys = [];
var keys = Object.keys(options).filter(function(key) {
if (key === 'toc' || key === 'cover' || key === 'page') {
extraKeys.push(key);
return false;
}

return true;
}).concat(extraKeys);

var args = [wkhtmltopdf.command, '--quiet'];
keys.forEach(function(key) {
var val = options[key];
if (key !== 'toc' && key !== 'cover' && key !== 'page')
key = key.length === 1 ? '-' + key : '--' + slang.dasherize(key);

if (val !== false)
args.push(key);

if (typeof val !== 'boolean')
args.push(quote(val));
});

var isUrl = /^(https?|file):\/\//.test(input);
args.push(isUrl ? quote(input) : '-'); // stdin if HTML given directly

try {
var args = [wkhtmltopdf.command, '--quiet'].concat(processOptions(options)).concat(processInput(input));
}
catch (err) {
emitError(err, callback, process.stderr);
}

args.push(output ? quote(output) : '-'); // stdout if no output file

// console.log(args);
return genPDF(input, args, callback);
}

function genPDF(input, args, callback) {

if (process.platform === 'win32') {
var child = spawn(args[0], args.slice(1));
} else {
// this nasty business prevents piping problems on linux
var child = spawn('/bin/sh', ['-c', args.join(' ') + ' | cat']);
}

// call the callback with null error when the process exits successfully
if (callback)
child.on('exit', function() { callback(null); });

// setup error handling
var stream = child.stdout;
function handleError(err) {
child.removeAllListeners('exit');
child.kill();

// call the callback if there is one
if (callback)
callback(err);

// if not, or there are listeners for errors, emit the error event
if (!callback || stream.listeners('error').length > 0)
stream.emit('error', err);
}

emitError(err, callback, stream);
}

child.once('error', handleError);
child.stderr.once('data', function(err) {
handleError(new Error((err || '').toString().trim()));
});

// write input to stdin if it isn't a url
if (!isUrl)
if (!Array.isArray(input) && !isURL(input)) {
child.stdin.end(input);

}

// return stdout stream so we can pipe
return stream;
}

function emitError(err, callback, stream) {
// call the callback if there is one
if (callback)
callback(err);

// if not, or there are listeners for errors, emit the error event
if (!callback || stream.listeners('error').length > 0)
stream.emit('error', err);
}

wkhtmltopdf.command = 'wkhtmltopdf';
module.exports = wkhtmltopdf;


function processOptions(options) {

// make sure the special keys are last
var extraKeys = [];
var keys = Object.keys(options).filter(function(key) {
if (key === 'toc' || key === 'cover' || key === 'page') {
extraKeys.push(key);
return false;
}

return true;
}).concat(extraKeys);

var opts = [];

keys.forEach(function(key) {
var val = options[key];
if (key !== 'toc' && key !== 'cover' && key !== 'page')
key = key.length === 1 ? '-' + key : '--' + slang.dasherize(key);

if (val !== false)
opts.push(key);

if (typeof val !== 'boolean')
opts.push(quote(val));
});

return opts;
}

function processInput(inputArgs) {

var resolvedInput = [];

if (Array.isArray(inputArgs)) {
resolvedInput = inputArgs.map(resolveInputObject).reduce(function(accum, val) {
return accum.concat(val);
}, []);
}
else if (isURL(inputArgs)) {
resolvedInput.push(quote(inputArgs));
}
else {
resolvedInput.push('-'); // stdin
}

return resolvedInput;
}

function isURL(possibleURL) {
return /^(https?|file):\/\//.test(possibleURL);
}

function resolveInputObject(input) {

var type, url, options;

if (typeof input == 'string') {
if (input == 'toc') {
type = input;
}
else if (isURL(input)) {
url = input;
}
}
else {
type = input.type;
url = input.url;
if (input.options) {
options = processOptions(input.options);
}
}

if (!options) {
options = [];
}
else if (!Array.isArray(options)) {
throw Error("Invalid 'options' Array for page '" + url + "'");
}

if (type == 'toc' && url) {
throw Error("URL is invalid for page type 'toc' in '" + url + "'");
}
else if (type != 'toc' && !isURL(url)) {
throw Error("Invalid 'url' for page: " + type + "'" + url + "'");
}

if (!type) {
type = '';
}

if (!url) {
url = '';
}
else {
url = quote(url);
}

return [type, url].concat(options);

}

9 changes: 9 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,14 @@
"type": "git",
"url": "https://github.com/devongovett/node-wkhtmltopdf.git"
},
"scripts": {
"test": "mocha -R spec"
},
"devDependencies": {
"mocha": "~2.2.5",
"rewire": "~2.3.4"
},

"bugs": "http://github.com/devongovett/node-wkhtmltopdf/issues"

}
27 changes: 25 additions & 2 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,11 @@ wkhtmltopdf('http://google.com/', { pageSize: 'letter' }, function (code, signal
});
wkhtmltopdf('http://google.com/', function (code, signal) {
});

```

`wkhtmltopdf` is just a function, which you call with either a URL or an inline HTML string, and it returns
a stream that you can read from or pipe to wherever you like (e.g. a file, or an HTTP response).
`wkhtmltopdf` is just a function, which you call with a URL or inline HTML string, or an Array of objects (see [Multi-Source-Input](#multi-source-input)),
and it returns a stream that you can read from or pipe to wherever you like (e.g. a file, or an HTTP response).

There are [many options](http://wkhtmltopdf.org/docs.html) available to
wkhtmltopdf. All of the command line options are supported as documented on the page linked to above. The
Expand All @@ -37,6 +38,22 @@ options are camelCased instead-of-dashed as in the command line tool.
There is also an `output` option that can be used to write the output directly to a filename, instead of returning
a stream.

### Multi-Source-Input

`wkhtmltopdf` supports the ability to construct a PDF from several source documents, and can even generate a table-of-contents based on an outline inferred from the source HTML structure. To combine several documents into a single PDF, pass an array as the first argument. Each element of the array represents a single source for the resulting PDF, and must be either:

* A string containing the source URL or the string `toc` to generate a Table of Contents
* An object conforming to the following structure:

```
{
type: STRING, // Optional: 'cover' or 'toc'
url: STRING, // URL to source. Omit for type: 'toc'
options: {...}, // Page-specific options. Same format as global options
}
```


## Installation

First, you need to install the wkhtmltopdf command line tool on your system. The easiest way to do this is to
Expand All @@ -50,6 +67,12 @@ Finally, to install the node module, use `npm`:
Be sure `wkhtmltopdf` is in your PATH when you're done installing. If you don't want to do this for some reason, you can change
the `wkhtmltopdf.command` property to the path to the `wkhtmltopdf` command line tool.

## Testing

```
npm test
```

## License

MIT
58 changes: 58 additions & 0 deletions test/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
var rewire = require('rewire');
var wkhtmltopdf = rewire('../index.js');

describe('wkhtmltopdf', function() {

wkhtmltopdf.__set__("genPDF", function(input, args, callback) {
return args;
});

describe('function', function() {
var options = {
headerCenter: "TEST PDF",
headerFontSize: 10,
headerSpacing: 5,
marginTop: 15,
output: "./google.pdf"
};

it('should produce a well-formed command-line invocation of wkhtmltopdf from a single source and global options', function() {
var args = wkhtmltopdf('http://google.com', options);

var expected = 'wkhtmltopdf --quiet --header-center "TEST PDF" --header-font-size 10 --header-spacing 5 --margin-top 15 "http://google.com" "./google.pdf"';

if (args.join(' ') != expected) {
throw new Error("generated args don't match expected");
}
});

it('should produce a well-formed command-line invocation of wkhtmltopdf from multiple pages with individual page options', function() {
options.output = 'multi-page.pdf';

var pages = [
{
type: 'cover',
url: 'http://google.com'
},
'toc',
{
url: 'https://github.com',
options: {
enableTocBackLinks: true,
pageOffset: -2
}
},
'http://wkhtmltopdf.org/usage/wkhtmltopdf.txt'
];

var args = wkhtmltopdf(pages, options);
var expected = 'wkhtmltopdf --quiet --header-center "TEST PDF" --header-font-size 10 --header-spacing 5 --margin-top 15 cover "http://google.com" toc "https://github.com" --enable-toc-back-links --page-offset -2 "http://wkhtmltopdf.org/usage/wkhtmltopdf.txt" "multi-page.pdf"';

if (args.join(' ') != expected) {
throw new Error("generated args don't match expected");
}
});

});

});