diff --git a/CHANGELOG.md b/CHANGELOG.md index 35cfcc457..a9b163b5e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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) diff --git a/Readme.md b/Readme.md index 657d56ecd..ed3e81129 100644 --- a/Readme.md +++ b/Readme.md @@ -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. @@ -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() @@ -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() @@ -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: @@ -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') diff --git a/examples/pdf-images.js b/examples/pdf-images.js index d19a7e9c7..2753dcb58 100644 --- a/examples/pdf-images.js +++ b/examples/pdf-images.js @@ -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 @@ -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 @@ -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') diff --git a/lib/canvas.js b/lib/canvas.js index ec16b92ea..cd43ff868 100644 --- a/lib/canvas.js +++ b/lib/canvas.js @@ -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); }; /** diff --git a/src/Canvas.cc b/src/Canvas.cc index 46af72730..f379ad879 100644 --- a/src/Canvas.cc +++ b/src/Canvas.cc @@ -12,7 +12,7 @@ #include #include #include - +#include #include "Util.h" #include "Canvas.h" #include "PNG.h" @@ -302,10 +302,52 @@ static uint32_t getSafeBufSize(Canvas* canvas) { return min(canvas->getWidth() * canvas->getHeight() * 4, static_cast(PAGE_SIZE)); } +#if CAIRO_VERSION >= CAIRO_VERSION_ENCODE(1, 16, 0) + +static inline void setPdfMetaStr(cairo_surface_t* surf, Local 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 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(); + auto date = static_cast(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 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: @@ -331,14 +373,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(canvas->backend())->closure(); +#if CAIRO_VERSION >= CAIRO_VERSION_ENCODE(1, 16, 0) + if (info[1]->IsObject()) { // toBuffer("application/pdf", config) + setPdfMetadata(canvas, Nan::To(info[1]).ToLocalChecked()); + } +#endif // CAIRO 16+ } else { closure = static_cast(canvas->backend())->closure(); } + cairo_surface_finish(canvas->surface()); Local buf = Nan::CopyBuffer((char*)&closure->vec[0], closure->vec.size()).ToLocalChecked(); info.GetReturnValue().Set(buf); return; @@ -577,6 +625,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(info[1]).ToLocalChecked()); + } +#endif + cairo_surface_finish(canvas->surface()); PdfSvgClosure* closure = static_cast(canvas->backend())->closure();