Skip to content

Commit

Permalink
add support for embedding metadata in PDFs
Browse files Browse the repository at this point in the history
  • Loading branch information
zbjornson committed Jan 10, 2019
1 parent af58a74 commit e2cb855
Show file tree
Hide file tree
Showing 5 changed files with 109 additions and 17 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ project adheres to [Semantic Versioning](http://semver.org/).
### Changed
### Added
* Add support for multiple PDF page sizes
* Add support for embedding document metadata in PDFs

### Fixed
* Don't crash when font string is invalid (bug since 2.2.0) (#1328)
Expand Down
29 changes: 25 additions & 4 deletions Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -240,13 +240,20 @@ Enabling mime data tracking has no benefits (only a slow down) unless you are ge
Creates a [`Buffer`](https://nodejs.org/api/buffer.html) object representing the image contained in the canvas.
* **callback** If provided, the buffer will be provided in the callback instead of being returned by the function. Invoked with an error as the first argument if encoding failed, or the resulting buffer as the second argument if it succeeded. Not supported for mimeType `raw` or for PDF or SVG canvases.
* **mimeType** A string indicating the image format. Valid options are `image/png`, `image/jpeg` (if node-canvas was built with JPEG support) and `raw` (unencoded ARGB32 data in native-endian byte order, top-to-bottom). Defaults to `image/png`. If the canvas is a PDF or SVG canvas, this argument is ignored and a PDF or SVG is returned always.
* **mimeType** A string indicating the image format. Valid options are `image/png`, `image/jpeg` (if node-canvas was built with JPEG support), `raw` (unencoded ARGB32 data in native-endian byte order, top-to-bottom), `application/pdf` (for PDF canvases) and `image/svg+xml` (for SVG canvases). Defaults to `image/png` for image canvases, or the corresponding type for PDF or SVG canvas.
* **config**
* For `image/jpeg` an object specifying the quality (0 to 1), if progressive compression should be used and/or if chroma subsampling should be used: `{quality: 0.75, progressive: false, chromaSubsampling: true}`. All properties are optional.
* For `image/jpeg`, an object specifying the quality (0 to 1), if progressive compression should be used and/or if chroma subsampling should be used: `{quality: 0.75, progressive: false, chromaSubsampling: true}`. All properties are optional.
* For `image/png`, an object specifying the ZLIB compression level (between 0 and 9), the compression filter(s), the palette (indexed PNGs only), the the background palette index (indexed PNGs only) and/or the resolution (ppi): `{compressionLevel: 6, filters: canvas.PNG_ALL_FILTERS, palette: undefined, backgroundIndex: 0, resolution: undefined}`. All properties are optional.
Note that the PNG format encodes the resolution in pixels per meter, so if you specify `96`, the file will encode 3780 ppm (~96.01 ppi). The resolution is undefined by default to match common browser behavior.
* For `application/pdf`, an object specifying optional document metadata: `{title: string, author: string, subject: string, keywords: string, creator: string, creationDate: Date, modDate: Date}`. All properties are optional and default to `undefined`, except for `creationDate`, which defaults to the current date. *Adding metadata requires Cairo 1.16.0 or later.*
For a description of these properties, see page 550 of [PDF 32000-1:2008](https://www.adobe.com/content/dam/acom/en/devnet/acrobat/pdfs/PDF32000_2008.pdf).
Note that there is no standard separator for `keywords`. A space is recommended because it is in common use by other applications, and Cairo will enclose the list of keywords in quotes if a comma or semicolon is used.
**Return value**
If no callback is provided, a [`Buffer`](https://nodejs.org/api/buffer.html). If a callback is provided, none.
Expand Down Expand Up @@ -283,9 +290,15 @@ const topPixelsARGBLeftToRight = buf4.slice(0, width * 4)
// And the third row is:
const row3 = buf4.slice(2 * stride, 2 * stride + width * 4)
// SVG and PDF canvases ignore the mimeType argument
// SVG and PDF canvases
const myCanvas = createCanvas(w, h, 'pdf')
myCanvas.toBuffer() // returns a buffer containing a PDF-encoded canvas
// With optional metadata:
myCanvas.toBuffer('application/pdf', {
title: 'my picture',
keywords: 'node.js demo cairo',
creationDate: new Date()
})
```
### Canvas#createPNGStream()
Expand Down Expand Up @@ -358,6 +371,8 @@ const stream = canvas.createJPEGStream({
> canvas.createPDFStream(config?: any) => ReadableStream
> ```
* `config` an object specifying optional document metadata: `{title: string, author: string, subject: string, keywords: string, creator: string, creationDate: Date, modDate: Date}`. See `toBuffer()` for more information. *Adding metadata requires Cairo 1.16.0 or later.*
Applies to PDF canvases only. Creates a [`ReadableStream`](https://nodejs.org/api/stream.html#stream_class_stream_readable) that emits the encoded PDF. `canvas.toBuffer()` also produces an encoded PDF, but `createPDFStream()` can be used to reduce memory usage.
### Canvas#toDataURL()
Expand Down Expand Up @@ -442,6 +457,12 @@ ctx.fillText('Hello World 2', 50, 80)

canvas.toBuffer() // returns a PDF file
canvas.createPDFStream() // returns a ReadableStream that emits a PDF
// With optional document metadata (requires Cairo 1.16.0):
canvas.toBuffer('application/pdf', {
title: 'my picture',
keywords: 'node.js demo cairo',
creationDate: new Date()
})
```

It is also possible to create pages with different sizes by passing `width` and `height` to the `.addPage()` method:
Expand All @@ -463,7 +484,7 @@ See also:

## SVG Output Support

node-canvas can create SVG documents instead of images. The canva type must be set when creating the canvas as follows:
node-canvas can create SVG documents instead of images. The canvas type must be set when creating the canvas as follows:

```js
const canvas = createCanvas(200, 500, 'svg')
Expand Down
24 changes: 16 additions & 8 deletions examples/pdf-images.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
var fs = require('fs')
var Canvas = require('..')
const fs = require('fs')
const { Image, createCanvas } = require('..')

var Image = Canvas.Image
var canvas = Canvas.createCanvas(500, 500, 'pdf')
var ctx = canvas.getContext('2d')
const canvas = createCanvas(500, 500, 'pdf')
const ctx = canvas.getContext('2d')

var x, y
let x, y

function reset () {
x = 50
Expand All @@ -23,7 +22,7 @@ function p (str) {
}

function img (src) {
var img = new Image()
const img = new Image()
img.src = src
ctx.drawImage(img, x, (y += 20))
y += img.height
Expand All @@ -43,7 +42,16 @@ img('examples/images/lime-cat.jpg')
p('Figure 1.1 - Lime cat is awesome')
ctx.addPage()

fs.writeFile('out.pdf', canvas.toBuffer(), function (err) {
const buff = canvas.toBuffer('application/pdf', {
title: 'Squid and Cat!',
author: 'Octocat',
subject: 'An example PDF made with node-canvas',
keywords: 'node.js squid cat lime',
creator: 'my app',
modDate: new Date()
})

fs.writeFile('out.pdf', buff, function (err) {
if (err) throw err

console.log('created out.pdf')
Expand Down
12 changes: 10 additions & 2 deletions lib/canvas.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,12 +73,20 @@ Canvas.prototype.createPNGStream = function(options){
/**
* Create a `PDFStream` for `this` canvas.
*
* @param {object} [options]
* @param {string} [options.title]
* @param {string} [options.author]
* @param {string} [options.subject]
* @param {string} [options.keywords]
* @param {string} [options.creator]
* @param {Date} [options.creationDate]
* @param {Date} [options.modDate]
* @return {PDFStream}
* @public
*/
Canvas.prototype.pdfStream =
Canvas.prototype.createPDFStream = function(){
return new PDFStream(this);
Canvas.prototype.createPDFStream = function(options){
return new PDFStream(this, options);
};

/**
Expand Down
60 changes: 57 additions & 3 deletions src/Canvas.cc
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
#include <glib.h>
#include <cairo-pdf.h>
#include <cairo-svg.h>

#include <ctime>
#include "Util.h"
#include "Canvas.h"
#include "PNG.h"
Expand Down Expand Up @@ -308,10 +308,52 @@ static uint32_t getSafeBufSize(Canvas* canvas) {
return min(canvas->getWidth() * canvas->getHeight() * 4, static_cast<int>(PAGE_SIZE));
}

#if CAIRO_VERSION >= CAIRO_VERSION_ENCODE(1, 16, 0)

static inline void setPdfMetaStr(cairo_surface_t* surf, Local<Object> opts,
cairo_pdf_metadata_t t, const char* pName) {
auto propName = Nan::New(pName).ToLocalChecked();
if (opts->Get(propName)->IsString()) {
auto val = opts->Get(propName);
// (copies char data)
cairo_pdf_surface_set_metadata(surf, t, *Nan::Utf8String(val));
}
}

static inline void setPdfMetaDate(cairo_surface_t* surf, Local<Object> opts,
cairo_pdf_metadata_t t, const char* pName) {
auto propName = Nan::New(pName).ToLocalChecked();
if (opts->Get(propName)->IsDate()) {
auto val = opts->Get(propName).As<Date>();
auto date = static_cast<time_t>(val->ValueOf() / 1000); // ms -> s
char buf[sizeof "2011-10-08T07:07:09Z"];
strftime(buf, sizeof buf, "%FT%TZ", gmtime(&date));
cairo_pdf_surface_set_metadata(surf, t, buf);
}
}

static void setPdfMetadata(Canvas* canvas, Local<Object> opts) {
cairo_surface_t* surf = canvas->surface();

setPdfMetaStr(surf, opts, CAIRO_PDF_METADATA_TITLE, "title");
setPdfMetaStr(surf, opts, CAIRO_PDF_METADATA_AUTHOR, "author");
setPdfMetaStr(surf, opts, CAIRO_PDF_METADATA_SUBJECT, "subject");
setPdfMetaStr(surf, opts, CAIRO_PDF_METADATA_KEYWORDS, "keywords");
setPdfMetaStr(surf, opts, CAIRO_PDF_METADATA_CREATOR, "creator");
setPdfMetaDate(surf, opts, CAIRO_PDF_METADATA_CREATE_DATE, "creationDate");
setPdfMetaDate(surf, opts, CAIRO_PDF_METADATA_MOD_DATE, "modDate");
}

#endif // CAIRO 16+

/*
* Converts/encodes data to a Buffer. Async when a callback function is passed.
* PDF/SVG canvases:
* PDF canvases:
(any) => Buffer
("application/pdf", config) => Buffer
* SVG canvases:
(any) => Buffer
* ARGB data:
Expand All @@ -337,14 +379,20 @@ NAN_METHOD(Canvas::ToBuffer) {
// Vector canvases, sync only
const string name = canvas->backend()->getName();
if (name == "pdf" || name == "svg") {
cairo_surface_finish(canvas->surface());
// mime type may be present, but it's not checked
PdfSvgClosure* closure;
if (name == "pdf") {
closure = static_cast<PdfBackend*>(canvas->backend())->closure();
#if CAIRO_VERSION >= CAIRO_VERSION_ENCODE(1, 16, 0)
if (info[1]->IsObject()) { // toBuffer("application/pdf", config)
setPdfMetadata(canvas, Nan::To<Object>(info[1]).ToLocalChecked());
}
#endif // CAIRO 16+
} else {
closure = static_cast<SvgBackend*>(canvas->backend())->closure();
}

cairo_surface_finish(canvas->surface());
Local<Object> buf = Nan::CopyBuffer((char*)&closure->vec[0], closure->vec.size()).ToLocalChecked();
info.GetReturnValue().Set(buf);
return;
Expand Down Expand Up @@ -583,6 +631,12 @@ NAN_METHOD(Canvas::StreamPDFSync) {
if (canvas->backend()->getName() != "pdf")
return Nan::ThrowTypeError("wrong canvas type");

#if CAIRO_VERSION >= CAIRO_VERSION_ENCODE(1, 16, 0)
if (info[1]->IsObject()) {
setPdfMetadata(canvas, Nan::To<Object>(info[1]).ToLocalChecked());
}
#endif

cairo_surface_finish(canvas->surface());

PdfSvgClosure* closure = static_cast<PdfBackend*>(canvas->backend())->closure();
Expand Down

0 comments on commit e2cb855

Please sign in to comment.