Skip to content

Commit e2cb855

Browse files
committed
add support for embedding metadata in PDFs
1 parent af58a74 commit e2cb855

File tree

5 files changed

+109
-17
lines changed

5 files changed

+109
-17
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ project adheres to [Semantic Versioning](http://semver.org/).
1010
### Changed
1111
### Added
1212
* Add support for multiple PDF page sizes
13+
* Add support for embedding document metadata in PDFs
1314

1415
### Fixed
1516
* Don't crash when font string is invalid (bug since 2.2.0) (#1328)

Readme.md

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -240,13 +240,20 @@ Enabling mime data tracking has no benefits (only a slow down) unless you are ge
240240
Creates a [`Buffer`](https://nodejs.org/api/buffer.html) object representing the image contained in the canvas.
241241
242242
* **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.
243-
* **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.
243+
* **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.
244244
* **config**
245-
* 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.
245+
* 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.
246+
246247
* 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.
247248
248249
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.
249250
251+
* 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.*
252+
253+
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).
254+
255+
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.
256+
250257
**Return value**
251258
252259
If no callback is provided, a [`Buffer`](https://nodejs.org/api/buffer.html). If a callback is provided, none.
@@ -283,9 +290,15 @@ const topPixelsARGBLeftToRight = buf4.slice(0, width * 4)
283290
// And the third row is:
284291
const row3 = buf4.slice(2 * stride, 2 * stride + width * 4)
285292
286-
// SVG and PDF canvases ignore the mimeType argument
293+
// SVG and PDF canvases
287294
const myCanvas = createCanvas(w, h, 'pdf')
288295
myCanvas.toBuffer() // returns a buffer containing a PDF-encoded canvas
296+
// With optional metadata:
297+
myCanvas.toBuffer('application/pdf', {
298+
title: 'my picture',
299+
keywords: 'node.js demo cairo',
300+
creationDate: new Date()
301+
})
289302
```
290303
291304
### Canvas#createPNGStream()
@@ -358,6 +371,8 @@ const stream = canvas.createJPEGStream({
358371
> canvas.createPDFStream(config?: any) => ReadableStream
359372
> ```
360373
374+
* `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.*
375+
361376
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.
362377
363378
### Canvas#toDataURL()
@@ -442,6 +457,12 @@ ctx.fillText('Hello World 2', 50, 80)
442457

443458
canvas.toBuffer() // returns a PDF file
444459
canvas.createPDFStream() // returns a ReadableStream that emits a PDF
460+
// With optional document metadata (requires Cairo 1.16.0):
461+
canvas.toBuffer('application/pdf', {
462+
title: 'my picture',
463+
keywords: 'node.js demo cairo',
464+
creationDate: new Date()
465+
})
445466
```
446467

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

464485
## SVG Output Support
465486

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

468489
```js
469490
const canvas = createCanvas(200, 500, 'svg')

examples/pdf-images.js

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
1-
var fs = require('fs')
2-
var Canvas = require('..')
1+
const fs = require('fs')
2+
const { Image, createCanvas } = require('..')
33

4-
var Image = Canvas.Image
5-
var canvas = Canvas.createCanvas(500, 500, 'pdf')
6-
var ctx = canvas.getContext('2d')
4+
const canvas = createCanvas(500, 500, 'pdf')
5+
const ctx = canvas.getContext('2d')
76

8-
var x, y
7+
let x, y
98

109
function reset () {
1110
x = 50
@@ -23,7 +22,7 @@ function p (str) {
2322
}
2423

2524
function img (src) {
26-
var img = new Image()
25+
const img = new Image()
2726
img.src = src
2827
ctx.drawImage(img, x, (y += 20))
2928
y += img.height
@@ -43,7 +42,16 @@ img('examples/images/lime-cat.jpg')
4342
p('Figure 1.1 - Lime cat is awesome')
4443
ctx.addPage()
4544

46-
fs.writeFile('out.pdf', canvas.toBuffer(), function (err) {
45+
const buff = canvas.toBuffer('application/pdf', {
46+
title: 'Squid and Cat!',
47+
author: 'Octocat',
48+
subject: 'An example PDF made with node-canvas',
49+
keywords: 'node.js squid cat lime',
50+
creator: 'my app',
51+
modDate: new Date()
52+
})
53+
54+
fs.writeFile('out.pdf', buff, function (err) {
4755
if (err) throw err
4856

4957
console.log('created out.pdf')

lib/canvas.js

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,12 +73,20 @@ Canvas.prototype.createPNGStream = function(options){
7373
/**
7474
* Create a `PDFStream` for `this` canvas.
7575
*
76+
* @param {object} [options]
77+
* @param {string} [options.title]
78+
* @param {string} [options.author]
79+
* @param {string} [options.subject]
80+
* @param {string} [options.keywords]
81+
* @param {string} [options.creator]
82+
* @param {Date} [options.creationDate]
83+
* @param {Date} [options.modDate]
7684
* @return {PDFStream}
7785
* @public
7886
*/
7987
Canvas.prototype.pdfStream =
80-
Canvas.prototype.createPDFStream = function(){
81-
return new PDFStream(this);
88+
Canvas.prototype.createPDFStream = function(options){
89+
return new PDFStream(this, options);
8290
};
8391

8492
/**

src/Canvas.cc

Lines changed: 57 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
#include <glib.h>
1717
#include <cairo-pdf.h>
1818
#include <cairo-svg.h>
19-
19+
#include <ctime>
2020
#include "Util.h"
2121
#include "Canvas.h"
2222
#include "PNG.h"
@@ -308,10 +308,52 @@ static uint32_t getSafeBufSize(Canvas* canvas) {
308308
return min(canvas->getWidth() * canvas->getHeight() * 4, static_cast<int>(PAGE_SIZE));
309309
}
310310

311+
#if CAIRO_VERSION >= CAIRO_VERSION_ENCODE(1, 16, 0)
312+
313+
static inline void setPdfMetaStr(cairo_surface_t* surf, Local<Object> opts,
314+
cairo_pdf_metadata_t t, const char* pName) {
315+
auto propName = Nan::New(pName).ToLocalChecked();
316+
if (opts->Get(propName)->IsString()) {
317+
auto val = opts->Get(propName);
318+
// (copies char data)
319+
cairo_pdf_surface_set_metadata(surf, t, *Nan::Utf8String(val));
320+
}
321+
}
322+
323+
static inline void setPdfMetaDate(cairo_surface_t* surf, Local<Object> opts,
324+
cairo_pdf_metadata_t t, const char* pName) {
325+
auto propName = Nan::New(pName).ToLocalChecked();
326+
if (opts->Get(propName)->IsDate()) {
327+
auto val = opts->Get(propName).As<Date>();
328+
auto date = static_cast<time_t>(val->ValueOf() / 1000); // ms -> s
329+
char buf[sizeof "2011-10-08T07:07:09Z"];
330+
strftime(buf, sizeof buf, "%FT%TZ", gmtime(&date));
331+
cairo_pdf_surface_set_metadata(surf, t, buf);
332+
}
333+
}
334+
335+
static void setPdfMetadata(Canvas* canvas, Local<Object> opts) {
336+
cairo_surface_t* surf = canvas->surface();
337+
338+
setPdfMetaStr(surf, opts, CAIRO_PDF_METADATA_TITLE, "title");
339+
setPdfMetaStr(surf, opts, CAIRO_PDF_METADATA_AUTHOR, "author");
340+
setPdfMetaStr(surf, opts, CAIRO_PDF_METADATA_SUBJECT, "subject");
341+
setPdfMetaStr(surf, opts, CAIRO_PDF_METADATA_KEYWORDS, "keywords");
342+
setPdfMetaStr(surf, opts, CAIRO_PDF_METADATA_CREATOR, "creator");
343+
setPdfMetaDate(surf, opts, CAIRO_PDF_METADATA_CREATE_DATE, "creationDate");
344+
setPdfMetaDate(surf, opts, CAIRO_PDF_METADATA_MOD_DATE, "modDate");
345+
}
346+
347+
#endif // CAIRO 16+
348+
311349
/*
312350
* Converts/encodes data to a Buffer. Async when a callback function is passed.
313351
314-
* PDF/SVG canvases:
352+
* PDF canvases:
353+
(any) => Buffer
354+
("application/pdf", config) => Buffer
355+
356+
* SVG canvases:
315357
(any) => Buffer
316358
317359
* ARGB data:
@@ -337,14 +379,20 @@ NAN_METHOD(Canvas::ToBuffer) {
337379
// Vector canvases, sync only
338380
const string name = canvas->backend()->getName();
339381
if (name == "pdf" || name == "svg") {
340-
cairo_surface_finish(canvas->surface());
382+
// mime type may be present, but it's not checked
341383
PdfSvgClosure* closure;
342384
if (name == "pdf") {
343385
closure = static_cast<PdfBackend*>(canvas->backend())->closure();
386+
#if CAIRO_VERSION >= CAIRO_VERSION_ENCODE(1, 16, 0)
387+
if (info[1]->IsObject()) { // toBuffer("application/pdf", config)
388+
setPdfMetadata(canvas, Nan::To<Object>(info[1]).ToLocalChecked());
389+
}
390+
#endif // CAIRO 16+
344391
} else {
345392
closure = static_cast<SvgBackend*>(canvas->backend())->closure();
346393
}
347394

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

634+
#if CAIRO_VERSION >= CAIRO_VERSION_ENCODE(1, 16, 0)
635+
if (info[1]->IsObject()) {
636+
setPdfMetadata(canvas, Nan::To<Object>(info[1]).ToLocalChecked());
637+
}
638+
#endif
639+
586640
cairo_surface_finish(canvas->surface());
587641

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

0 commit comments

Comments
 (0)