From 140e6eb428c80267273ba8580d8fe87485e3c976 Mon Sep 17 00:00:00 2001 From: Zach Bjornson Date: Sat, 21 Apr 2018 12:24:27 -0700 Subject: [PATCH 1/3] disableChromaSubsampling: false -> chromaSubsampling: true --- Readme.md | 2 +- lib/canvas.js | 13 +++++++++++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/Readme.md b/Readme.md index ff92a8548..8a9d87e59 100644 --- a/Readme.md +++ b/Readme.md @@ -171,7 +171,7 @@ var stream = canvas.jpegStream({ bufsize: 4096 // output buffer size in bytes, default: 4096 , quality: 75 // JPEG quality (0-100) default: 75 , progressive: false // true for progressive compression, default: false - , disableChromaSubsampling: false // true to disable 2x2 subsampling of the chroma components, default: false + , chromaSubsampling: true // false to disable 2x2 subsampling of the chroma components, default: true }); ``` diff --git a/lib/canvas.js b/lib/canvas.js index 8f2d74a42..a589a2e56 100644 --- a/lib/canvas.js +++ b/lib/canvas.js @@ -90,12 +90,21 @@ Canvas.prototype.createJPEGStream = function(options){ // Don't allow the buffer size to exceed the size of the canvas (#674) var maxBufSize = this.width * this.height * 4; var clampedBufSize = Math.min(options.bufsize || 4096, maxBufSize); + var chromaFactor; + if (typeof options.chromaSubsampling === "number") { + // libjpeg-turbo seems to complain about values above 2, but hopefully this + // can be supported in the future. For now 1 and 2 are valid. + // https://github.com/Automattic/node-canvas/pull/1092#issuecomment-366558028 + chromaFactor = options.chromaSubsampling; + } else { + chromaFactor = options.chromaSubsampling === false ? 1 : 2; + } return new JPEGStream(this, { bufsize: clampedBufSize , quality: options.quality || 75 , progressive: options.progressive || false - , chromaHSampFactor: options.disableChromaSubsampling ? 1 : 2 - , chromaVSampFactor: options.disableChromaSubsampling ? 1 : 2 + , chromaHSampFactor: chromaFactor + , chromaVSampFactor: chromaFactor }); }; From 81f4517745b282d92c0d13c5b461ee9da41eaa9f Mon Sep 17 00:00:00 2001 From: Zach Bjornson Date: Thu, 19 Apr 2018 22:15:24 -0700 Subject: [PATCH 2/3] Support toBuffer("image/jpeg"), unify encoding configs See updated Readme.md and CHANGELOG.md Still needs more testing and possibly cleanup of the closure mess. --- CHANGELOG.md | 44 +++- Readme.md | 170 ++++++++++----- binding.gyp | 9 +- lib/canvas.js | 54 ++--- lib/jpegstream.js | 11 +- lib/pngstream.js | 24 +-- package.json | 4 +- src/Canvas.cc | 424 +++++++++++++++++++++----------------- src/Canvas.h | 3 +- src/JPEGStream.h | 58 ++++-- src/PNG.h | 8 +- src/backend/Backend.cc | 1 - src/backend/Backend.h | 4 - src/backend/PdfBackend.cc | 69 +++---- src/backend/PdfBackend.h | 4 + src/backend/SvgBackend.cc | 73 +++---- src/backend/SvgBackend.h | 4 + src/closure.cc | 41 ++-- src/closure.h | 55 ++++- src/toBuffer.cc | 4 +- test/canvas.test.js | 224 +++++++++++--------- 21 files changed, 745 insertions(+), 543 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ee963ac7a..b7473174c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,12 +8,43 @@ project adheres to [Semantic Versioning](http://semver.org/). 2.0.0 (unreleased -- encompasses all alpha versions) ================== +**Upgrading from 1.x** +```js +// (1) The quality argument for canvas.jpegStream now goes from 0 to 1 instead +// of from 0 to 100: +canvas.jpegStream({quality: 50}) // old +canvas.jpegStream({quality: 0.5}) // new + +// (2) The ZLIB compression level and PNG filter options for canvas.toBuffer are +// now named instead of positional arguments: +canvas.toBuffer(undefined, 3, canvas.PNG_FILTER_NONE) // old +canvas.toBuffer(undefined, {compressionLevel: 3, filters: canvas.PNG_FILTER_NONE}) // new +// or specify the mime type explicitly: +canvas.toBuffer("image/png", {compressionLevel: 3, filters: canvas.PNG_FILTER_NONE}) // new + +// (3) #2 also applies for canvas.pngStream, although these arguments were not +// documented: +canvas.pngStream(3, canvas.PNG_FILTER_NONE) // old +canvas.pngStream({compressionLevel: 3, filters: canvas.PNG_FILTER_NONE}) // new + +// (4) canvas.syncPNGStream() and canvas.syncJPEGStream() have been removed: +canvas.syncPNGStream() // old +canvas.pngStream() // new + +canvas.syncJPEGStream() // old +canvas.jpegStream() // new +``` + ### Breaking * Drop support for Node.js <4.x - * Remove sync streams (bc53059). Note that all or most streams are still - synchronous to some degree; this change just removed `syncPNGStream` and - friends. + * Remove sync stream functions (bc53059). Note that most streams are still + synchronous (run in the main thread); this change just removed `syncPNGStream` + and `syncJPEGStream`. * Pango is now *required* on all platforms (7716ae4). + * Make the `quality` argument for JPEG output go from 0 to 1 to match HTML spec. + * Make the `compressionLevel` and `filters` arguments for `canvas.toBuffer()` + named instead of positional. Same for `canvas.pngStream()`, although these + arguments were not documented. ### Fixed * Prevent segfaults caused by loading invalid fonts (#1105) @@ -35,8 +66,8 @@ project adheres to [Semantic Versioning](http://semver.org/). ### Added * Prebuilds (#992) with different libc versions to the prebuilt binary (#1140) - * Support canvas.getContext("2d", {alpha: boolean}) and - canvas.getContext("2d", {pixelFormat: "..."}) + * Support `canvas.getContext("2d", {alpha: boolean})` and + `canvas.getContext("2d", {pixelFormat: "..."})` * Support indexed PNG encoding. * Support `currentTransform` (d6714ee) * Export `CanvasGradient` (6a4c0ab) @@ -48,6 +79,9 @@ project adheres to [Semantic Versioning](http://semver.org/). * Browser-compatible API (6a29a23) * Support for jpeg on Windows (42e9a74) * Support for backends (1a6dffe) + * Support for `canvas.toBuffer("image/jpeg")` + * Unified configuration options for `canvas.toBuffer()`, `canvas.pngStream()` + and `canvas.jpegStream()` 1.6.x (unreleased) ================== diff --git a/Readme.md b/Readme.md index 8a9d87e59..a5f5f9475 100644 --- a/Readme.md +++ b/Readme.md @@ -5,6 +5,9 @@ ## This is the documentation for version 2.0.0-alpha Alpha versions of 2.0 can be installed using `npm install canvas@next`. +See the [changelog](https://github.com/Automattic/node-canvas/blob/master/CHANGELOG.md) +for a guide to upgrading from 1.x to 2.x. + **For version 1.x documentation, see [the v1.x branch](https://github.com/Automattic/node-canvas/tree/v1.x)** ----- @@ -80,9 +83,11 @@ loadImage('examples/images/lime-cat.jpg').then((image) => { }) ``` -## Non-Standard API +## Non-Standard APIs - node-canvas extends the canvas API to provide interfacing with node, for example streaming PNG data, converting to a `Buffer` instance, etc. Among the interfacing API, in some cases the drawing API has been extended for SSJS image manipulation / creation usage, however keep in mind these additions may fail to render properly within browsers. +node-canvas implements the [HTML Canvas API](https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API) as closely as possible. +(See [Compatibility Status](https://github.com/Automattic/node-canvas/wiki/Compatibility-Status) +for the current API compliance.) All non-standard APIs are documented below. ### Image#src=Buffer @@ -125,84 +130,145 @@ img.dataMode = Image.MODE_MIME | Image.MODE_IMAGE; // Both are tracked If image data is not tracked, and the Image is drawn to an image rather than a PDF canvas, the output will be junk. Enabling mime data tracking has no benefits (only a slow down) unless you are generating a PDF. -### Canvas#pngStream(options) +### Canvas#toBuffer() - To create a `PNGStream` simply call `canvas.pngStream()`, and the stream will start to emit _data_ events, emitting _end_ when the data stream ends. If an exception occurs the _error_ event is emitted. +Creates a [`Buffer`](https://nodejs.org/api/buffer.html) object representing the +image contained in the canvas. + +> `canvas.toBuffer((err: Error|null, result: Buffer) => void[, mimeType[, config]]) => void` +> `canvas.toBuffer([mimeType[, config]]) => Buffer` + +* **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 (there + is no async work to do in those cases). +* **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. +* **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/png`, an object specifying the ZLIB compression level (between 0 + and 9), the compression filter(s), the palette (indexed PNGs only) and/or + the background palette index (indexed PNGs only): + `{compressionLevel: 6, filters: canvas.PNG_ALL_FILTERS, palette: undefined, backgroundIndex: 0}`. + All properties are optional. + +**Return value** + +If no callback is provided, a [`Buffer`](https://nodejs.org/api/buffer.html). +If a callback is provided, none. + +#### Examples ```javascript -var fs = require('fs') - , out = fs.createWriteStream(__dirname + '/text.png') - , stream = canvas.pngStream(); +// Default: buf contains a PNG-encoded image +const buf = canvas.toBuffer() -stream.pipe(out); +// PNG-encoded, zlib compression level 3 for faster compression but bigger files, no filtering +const buf2 = canvas.toBuffer('image/png', {compressionLevel: 3, filters: canvas.PNG_FILTER_NONE}) -out.on('finish', function(){ - console.log('The PNG file was created.'); -}); +// JPEG-encoded, 50% quality +const buf3 = canvas.toBuffer('image/jpeg', {quality: 0.5}) + +// Asynchronous PNG +canvas.toBuffer((err, buf) => { + if (err) throw err; // encoding failed + // buf is PNG-encoded image +}) + +canvas.toBuffer((err, buf) => { + if (err) throw err; // encoding failed + // buf is JPEG-encoded image at 95% quality +}, 'image/jpeg', {quality: 0.95}) + +// ARGB32 pixel values, native-endian +const buf4 = canvas.toBuffer('raw') +const {stride, width} = canvas +// In memory, this is `canvas.height * canvas.stride` bytes long. +// The top row of pixels, in ARGB order, left-to-right, is: +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 +const myCanvas = createCanvas(w, h, 'pdf') +myCanvas.toBuffer() // returns a buffer containing a PDF-encoded canvas +``` + +### Canvas#pngStream(options) + +Creates a [`ReadableStream`](https://nodejs.org/api/stream.html#stream_class_stream_readable) +that emits PNG-encoded data. + +> `canvas.pngStream([config]) => ReadableStream` + +* `config` An object specifying the ZLIB compression level (between 0 and 9), + the compression filter(s), the palette (indexed PNGs only) and/or the + background palette index (indexed PNGs only): + `{compressionLevel: 6, filters: canvas.PNG_ALL_FILTERS, palette: undefined, backgroundIndex: 0}`. + All properties are optional. + +#### Examples + +```javascript +const fs = require('fs') +const out = fs.createWriteStream(__dirname + '/test.png') +const stream = canvas.pngStream() +stream.pipe(out) +out.on('finish', () => console.log('The PNG file was created.')) ``` To encode indexed PNGs from canvases with `pixelFormat: 'A8'` or `'A1'`, provide an options object: ```js -var palette = new Uint8ClampedArray([ +const palette = new Uint8ClampedArray([ //r g b a 0, 50, 50, 255, // index 1 10, 90, 90, 255, // index 2 127, 127, 255, 255 // ... -]); +]) canvas.pngStream({ palette: palette, backgroundIndex: 0 // optional, defaults to 0 }) ``` -### Canvas#jpegStream() and Canvas#syncJPEGStream() - -You can likewise create a `JPEGStream` by calling `canvas.jpegStream()` with -some optional parameters; functionality is otherwise identical to -`pngStream()`. See `examples/crop.js` for an example. - -_Note: At the moment, `jpegStream()` is the same as `syncJPEGStream()`, both -are synchronous_ - -```javascript -var stream = canvas.jpegStream({ - bufsize: 4096 // output buffer size in bytes, default: 4096 - , quality: 75 // JPEG quality (0-100) default: 75 - , progressive: false // true for progressive compression, default: false - , chromaSubsampling: true // false to disable 2x2 subsampling of the chroma components, default: true -}); -``` - -### Canvas#toBuffer() +### Canvas#jpegStream() -A call to `Canvas#toBuffer()` will return a node `Buffer` instance containing image data. +Creates a [`ReadableStream`](https://nodejs.org/api/stream.html#stream_class_stream_readable) +that emits JPEG-encoded data. -```javascript -// PNG Buffer, default settings -var buf = canvas.toBuffer(); +_Note: At the moment, `jpegStream()` is synchronous under the hood. That is, it +runs in the main thread, not in the libuv threadpool._ -// PNG Buffer, zlib compression level 3 (from 0-9), faster but bigger -var buf2 = canvas.toBuffer(undefined, 3, canvas.PNG_FILTER_NONE); +> `canvas.pngStream([config]) => ReadableStream` -// ARGB32 Buffer, native-endian -var buf3 = canvas.toBuffer('raw'); -var stride = canvas.stride; -// In memory, this is `canvas.height * canvas.stride` bytes long. -// The top row of pixels, in ARGB order, left-to-right, is: -var topPixelsARGBLeftToRight = buf3.slice(0, canvas.width * 4); -var row3 = buf3.slice(2 * canvas.stride, 2 * canvas.stride + canvas.width * 4); -``` +* `config` 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. -### Canvas#toBuffer() async - -Optionally we may pass a callback function to `Canvas#toBuffer()`, and this process will be performed asynchronously, and will `callback(err, buf)`. +#### Examples ```javascript -canvas.toBuffer(function(err, buf){ - -}); +const fs = require('fs') +const out = fs.createWriteStream(__dirname + '/test.jpeg') +const stream = canvas.jpegStream() +stream.pipe(out) +out.on('finish', () => console.log('The JPEG file was created.')) + +// Disable 2x2 chromaSubsampling for deeper colors and use a higher quality +const stream = canvas.jpegStream({ + quality: 95, + chromaSubsampling: false +}) ``` ### Canvas#toDataURL() sync and async diff --git a/binding.gyp b/binding.gyp index 23e2316da..79ea2fdf2 100644 --- a/binding.gyp +++ b/binding.gyp @@ -135,7 +135,14 @@ 'data; +Canvas::ToPngBufferAsync(uv_work_t *req) { + PngClosure* closure = static_cast(req->data); closure->status = canvas_write_to_png_stream( - closure->canvas->surface() - , toBuffer - , closure); + closure->canvas->surface(), + toBuffer, + closure); +} + +void +Canvas::ToJpegBufferAsync(uv_work_t *req) { + JpegClosure* closure = static_cast(req->data); + + write_to_jpeg_buffer(closure->canvas->surface(), closure, &closure->data, &closure->len); } /* @@ -210,7 +217,7 @@ void Canvas::ToBufferAsyncAfter(uv_work_t *req) { Nan::HandleScope scope; Nan::AsyncResource async("canvas:ToBufferAsyncAfter"); - closure_t *closure = (closure_t *) req->data; + Closure* closure = static_cast(req->data); delete req; if (closure->status) { @@ -218,41 +225,128 @@ Canvas::ToBufferAsyncAfter(uv_work_t *req) { closure->pfn->Call(1, argv, &async); } else { Local buf = Nan::CopyBuffer((char*)closure->data, closure->len).ToLocalChecked(); - memcpy(Buffer::Data(buf), closure->data, closure->len); Local argv[2] = { Nan::Null(), buf }; closure->pfn->Call(sizeof argv / sizeof *argv, argv, &async); } closure->canvas->Unref(); - delete closure->pfn; - closure_destroy(closure); - free(closure); + delete closure->pfn; // TODO move to destructor + delete closure; +} + +static void parsePNGArgs(Local arg, PngClosure& pngargs) { + if (arg->IsObject()) { + Local obj = arg->ToObject(); + + Local cLevel = obj->Get(Nan::New("compressionLevel").ToLocalChecked()); + if (cLevel->IsUint32()) { + uint32_t val = cLevel->Uint32Value(); + // See quote below from spec section 4.12.5.5. + if (val <= 9) pngargs.compressionLevel = val; + } + + Local filters = obj->Get(Nan::New("filters").ToLocalChecked()); + if (filters->IsUint32()) pngargs.filters = filters->Uint32Value(); + + Local palette = obj->Get(Nan::New("palette").ToLocalChecked()); + if (palette->IsUint8ClampedArray()) { + Local palette_ta = palette.As(); + pngargs.nPaletteColors = palette_ta->Length(); + if (pngargs.nPaletteColors % 4 != 0) { + throw "Palette length must be a multiple of 4."; + } + pngargs.nPaletteColors /= 4; + Nan::TypedArrayContents _paletteColors(palette_ta); + pngargs.palette = *_paletteColors; + // Optional background color index: + Local backgroundIndexVal = obj->Get(Nan::New("backgroundIndex").ToLocalChecked()); + if (backgroundIndexVal->IsUint32()) { + pngargs.backgroundIndex = static_cast(backgroundIndexVal->Uint32Value()); + } + } + } +} + +static void parseJPEGArgs(Local arg, JpegClosure& jpegargs) { + // "If Type(quality) is not Number, or if quality is outside that range, the + // user agent must use its default quality value, as if the quality argument + // had not been given." - 4.12.5.5 + if (arg->IsObject()) { + Local obj = arg->ToObject(); + + Local qual = obj->Get(Nan::New("quality").ToLocalChecked()); + if (qual->IsNumber()) { + double quality = qual->NumberValue(); + if (quality >= 0.0 && quality <= 1.0) { + jpegargs.quality = static_cast(100.0 * quality); + } + } + + Local chroma = obj->Get(Nan::New("chromaSubsampling").ToLocalChecked()); + if (chroma->IsBoolean()) { + bool subsample = chroma->BooleanValue(); + jpegargs.chromaSubsampling = subsample ? 2 : 1; + } else if (chroma->IsNumber()) { + jpegargs.chromaSubsampling = chroma->Uint32Value(); + } + + Local progressive = obj->Get(Nan::New("progressive").ToLocalChecked()); + if (!progressive->IsUndefined()) { + jpegargs.progressive = progressive->BooleanValue(); + } + } +} + +static uint32_t getSafeBufSize(Canvas* canvas) { + // Don't allow the buffer size to exceed the size of the canvas (#674) + // TODO not sure if this is really correct, but it fixed #674 + return min(canvas->getWidth() * canvas->getHeight() * 4, static_cast(PAGE_SIZE)); } /* - * Convert PNG data to a node::Buffer, async when a - * callback function is passed. + * Converts/encodes data to a Buffer. Async when a callback function is passed. + + * PDF/SVG canvases: + (any) => Buffer + + * ARGB data: + ("raw") => Buffer + + * PNG-encoded + () => Buffer + (undefined|"image/png", {compressionLevel?: number, filter?: number}) => Buffer + ((err: null|Error, buffer) => any) + ((err: null|Error, buffer) => any, undefined|"image/png", {compressionLevel?: number, filter?: number}) + + * JPEG-encoded + ("image/jpeg") => Buffer + ("image/jpeg", {quality?: number, progressive?: Boolean, chromaSubsampling?: Boolean|number}) => Buffer + ((err: null|Error, buffer) => any, "image/jpeg") + ((err: null|Error, buffer) => any, "image/jpeg", {quality?: number, progressive?: Boolean, chromaSubsampling?: Boolean|number}) */ NAN_METHOD(Canvas::ToBuffer) { cairo_status_t status; - uint32_t compression_level = 6; - uint32_t filter = PNG_ALL_FILTERS; Canvas *canvas = Nan::ObjectWrap::Unwrap(info.This()); - // TODO: async / move this out + // Vector canvases, sync only const string name = canvas->backend()->getName(); if (name == "pdf" || name == "svg") { cairo_surface_finish(canvas->surface()); - closure_t *closure = (closure_t *) canvas->backend()->closure(); + PdfSvgClosure* closure; + if (name == "pdf") { + closure = static_cast(canvas->backend())->closure(); + } else { + closure = static_cast(canvas->backend())->closure(); + } Local buf = Nan::CopyBuffer((char*) closure->data, closure->len).ToLocalChecked(); info.GetReturnValue().Set(buf); return; } - if (info.Length() >= 1 && info[0]->StrictEquals(Nan::New("raw").ToLocalChecked())) { - // Return raw ARGB data -- just a memcpy() + // Raw ARGB data -- just a memcpy() + if (info[0]->StrictEquals(Nan::New("raw").ToLocalChecked())) { cairo_surface_t *surface = canvas->surface(); cairo_surface_flush(surface); const unsigned char *data = cairo_image_surface_get_data(surface); @@ -261,54 +355,49 @@ NAN_METHOD(Canvas::ToBuffer) { return; } - if (info.Length() > 1 && !(info[1]->IsUndefined() && info[2]->IsUndefined())) { - if (!info[1]->IsUndefined()) { - bool good = true; - if (info[1]->IsNumber()) { - compression_level = info[1]->Uint32Value(); - } else if (info[1]->IsString()) { - if (info[1]->StrictEquals(Nan::New("0").ToLocalChecked())) { - compression_level = 0; - } else { - uint32_t tmp = info[1]->Uint32Value(); - if (tmp == 0) { - good = false; - } else { - compression_level = tmp; - } - } - } else { - good = false; - } - - if (good) { - if (compression_level > 9) { - return Nan::ThrowRangeError("Allowed compression levels lie in the range [0, 9]."); - } - } else { - return Nan::ThrowTypeError("Compression level must be a number."); - } - } + // Sync PNG, default + if (info[0]->IsUndefined() || info[0]->StrictEquals(Nan::New("image/png").ToLocalChecked())) { + try { + PngClosure closure(canvas); + parsePNGArgs(info[1], closure); + if (closure.nPaletteColors == 0xFFFFFFFF) { + Nan::ThrowError("Palette length must be a multiple of 4."); + return; + } + + Nan::TryCatch try_catch; + status = canvas_write_to_png_stream(canvas->surface(), toBuffer, &closure); - if (!info[2]->IsUndefined()) { - if (info[2]->IsUint32()) { - filter = info[2]->Uint32Value(); + if (try_catch.HasCaught()) { + try_catch.ReThrow(); + } else if (status) { + throw status; } else { - return Nan::ThrowTypeError("Invalid filter value."); + Local buf = Nan::CopyBuffer((char *)closure.data, closure.len).ToLocalChecked(); + info.GetReturnValue().Set(buf); } + } catch (cairo_status_t ex) { + Nan::ThrowError(Canvas::Error(ex)); + } catch (const char* ex) { + Nan::ThrowError(ex); } + return; } - // Async - if (info[0]->IsFunction()) { - closure_t *closure = (closure_t *) malloc(sizeof(closure_t)); - status = closure_init(closure, canvas, compression_level, filter); + // Async PNG + if (info[0]->IsFunction() && + (info[1]->IsUndefined() || info[1]->StrictEquals(Nan::New("image/png").ToLocalChecked()))) { - // ensure closure is ok - if (status) { - closure_destroy(closure); - free(closure); - return Nan::ThrowError(Canvas::Error(status)); + PngClosure* closure; + try { + closure = new PngClosure(canvas); + parsePNGArgs(info[2], *closure); + } catch (cairo_status_t ex) { + Nan::ThrowError(Canvas::Error(ex)); + return; + } catch (const char* ex) { + Nan::ThrowError(ex); + return; } // TODO: only one callback fn in closure @@ -319,37 +408,62 @@ NAN_METHOD(Canvas::ToBuffer) { req->data = closure; // Make sure the surface exists since we won't have an isolate context in the async block: canvas->surface(); - uv_queue_work(uv_default_loop(), req, ToBufferAsync, (uv_after_work_cb)ToBufferAsyncAfter); + uv_queue_work(uv_default_loop(), req, ToPngBufferAsync, (uv_after_work_cb)ToBufferAsyncAfter); return; - // Sync - } else { - closure_t closure; - status = closure_init(&closure, canvas, compression_level, filter); + } - // ensure closure is ok - if (status) { - closure_destroy(&closure); - return Nan::ThrowError(Canvas::Error(status)); +#ifdef HAVE_JPEG + // Sync JPEG + Local jpegStr = Nan::New("image/jpeg").ToLocalChecked(); + if (info[0]->StrictEquals(jpegStr)) { + try { + JpegClosure closure(canvas); + parseJPEGArgs(info[1], closure); + + Nan::TryCatch try_catch; + unsigned char *outbuff = NULL; + uint32_t outsize = 0; + write_to_jpeg_buffer(canvas->surface(), &closure, &outbuff, &outsize); + + if (try_catch.HasCaught()) { + try_catch.ReThrow(); + } else { + char *signedOutBuff = reinterpret_cast(outbuff); + Local buf = Nan::CopyBuffer(signedOutBuff, outsize).ToLocalChecked(); + info.GetReturnValue().Set(buf); + } + } catch (cairo_status_t ex) { + Nan::ThrowError(Canvas::Error(ex)); } + return; + } - Nan::TryCatch try_catch; - status = canvas_write_to_png_stream(canvas->surface(), toBuffer, &closure); - - if (try_catch.HasCaught()) { - closure_destroy(&closure); - try_catch.ReThrow(); - return; - } else if (status) { - closure_destroy(&closure); - return Nan::ThrowError(Canvas::Error(status)); - } else { - Local buf = Nan::CopyBuffer((char *)closure.data, closure.len).ToLocalChecked(); - closure_destroy(&closure); - info.GetReturnValue().Set(buf); + // Async JPEG + if (info[0]->IsFunction() && info[1]->StrictEquals(jpegStr)) { + JpegClosure* closure; + try { + closure = new JpegClosure(canvas); + } catch (cairo_status_t ex) { + Nan::ThrowError(Canvas::Error(ex)); return; } + + parseJPEGArgs(info[1], *closure); + + // TODO: only one callback fn in closure // TODO what does this comment mean? + canvas->Ref(); + closure->pfn = new Nan::Callback(info[0].As()); + + uv_work_t* req = new uv_work_t; + req->data = closure; + // Make sure the surface exists since we won't have an isolate context in the async block: + canvas->surface(); + uv_queue_work(uv_default_loop(), req, ToJpegBufferAsync, (uv_after_work_cb)ToBufferAsyncAfter); + + return; } +#endif } /* @@ -360,7 +474,7 @@ static cairo_status_t streamPNG(void *c, const uint8_t *data, unsigned len) { Nan::HandleScope scope; Nan::AsyncResource async("canvas:StreamPNG"); - closure_t *closure = (closure_t *) c; + PngClosure* closure = (PngClosure*) c; Local buf = Nan::CopyBuffer((char *)data, len).ToLocalChecked(); Local argv[3] = { Nan::Null() @@ -371,94 +485,20 @@ streamPNG(void *c, const uint8_t *data, unsigned len) { } /* - * Stream PNG data synchronously. - * TODO the compression level and filter args don't seem to be documented. - * Maybe move them to named properties in the options object? - * StreamPngSync(this, options: {palette?: Uint8ClampedArray}) - * StreamPngSync(this, compression_level?: uint32, filter?: uint32) + * Stream PNG data synchronously. TODO async + * StreamPngSync(this, options: {palette?: Uint8ClampedArray, backgroundIndex?: uint32, compressionLevel: uint32, filters: uint32}) */ NAN_METHOD(Canvas::StreamPNGSync) { - uint32_t compression_level = 6; - uint32_t filter = PNG_ALL_FILTERS; - // TODO: async as well if (!info[0]->IsFunction()) return Nan::ThrowTypeError("callback function required"); - + Canvas *canvas = Nan::ObjectWrap::Unwrap(info.This()); - uint8_t* paletteColors = NULL; - size_t nPaletteColors = 0; - uint8_t backgroundIndex = 0; - - if (info.Length() > 1 && !(info[1]->IsUndefined() && info[2]->IsUndefined())) { - if (!info[1]->IsUndefined()) { - bool good = true; - if (info[1]->IsNumber()) { - compression_level = info[1]->Uint32Value(); - } else if (info[1]->IsString()) { - if (info[1]->StrictEquals(Nan::New("0").ToLocalChecked())) { - compression_level = 0; - } else { - uint32_t tmp = info[1]->Uint32Value(); - if (tmp == 0) { - good = false; - } else { - compression_level = tmp; - } - } - } else if (info[1]->IsObject()) { - // If canvas is A8 or A1 and options obj has Uint8ClampedArray palette, - // encode as indexed PNG. - cairo_format_t format = canvas->backend()->getFormat(); - if (format == CAIRO_FORMAT_A8 || format == CAIRO_FORMAT_A1) { - Local attrs = info[1]->ToObject(); - Local palette = attrs->Get(Nan::New("palette").ToLocalChecked()); - if (palette->IsUint8ClampedArray()) { - Local palette_ta = palette.As(); - nPaletteColors = palette_ta->Length(); - if (nPaletteColors % 4 != 0) { - Nan::ThrowError("Palette length must be a multiple of 4."); - } - nPaletteColors /= 4; - Nan::TypedArrayContents _paletteColors(palette_ta); - paletteColors = *_paletteColors; - // Optional background color index: - Local backgroundIndexVal = attrs->Get(Nan::New("backgroundIndex").ToLocalChecked()); - if (backgroundIndexVal->IsUint32()) { - backgroundIndex = static_cast(backgroundIndexVal->Uint32Value()); - } - } - } - } else { - good = false; - } - if (good) { - if (compression_level > 9) { - return Nan::ThrowRangeError("Allowed compression levels lie in the range [0, 9]."); - } - } else { - return Nan::ThrowTypeError("Compression level must be a number."); - } - } + PngClosure closure(canvas); + parsePNGArgs(info[1], closure); - if (!info[2]->IsUndefined()) { - if (info[2]->IsUint32()) { - filter = info[2]->Uint32Value(); - } else { - return Nan::ThrowTypeError("Invalid filter value."); - } - } - } - - - closure_t closure; closure.fn = Local::Cast(info[0]); - closure.compression_level = compression_level; - closure.filter = filter; - closure.palette = paletteColors; - closure.nPaletteColors = nPaletteColors; - closure.backgroundIndex = backgroundIndex; Nan::TryCatch try_catch; @@ -480,6 +520,14 @@ NAN_METHOD(Canvas::StreamPNGSync) { return; } + +struct PdfStreamInfo { + Local fn; + uint32_t len; + uint8_t* data; +}; + + /* * Canvas::StreamPDF FreeCallback */ @@ -494,28 +542,27 @@ static cairo_status_t streamPDF(void *c, const uint8_t *data, unsigned len) { Nan::HandleScope scope; Nan::AsyncResource async("canvas:StreamPDF"); - closure_t *closure = static_cast(c); + PdfStreamInfo* streaminfo = static_cast(c); Local buf = Nan::NewBuffer(const_cast(reinterpret_cast(data)), len, stream_pdf_free, 0).ToLocalChecked(); Local argv[3] = { Nan::Null() , buf , Nan::New(len) }; - async.runInAsyncScope(Nan::GetCurrentContext()->Global(), closure->fn, sizeof argv / sizeof *argv, argv); + async.runInAsyncScope(Nan::GetCurrentContext()->Global(), streaminfo->fn, sizeof argv / sizeof *argv, argv); return CAIRO_STATUS_SUCCESS; } -cairo_status_t canvas_write_to_pdf_stream(cairo_surface_t *surface, cairo_write_func_t write_func, void *closure) { - closure_t *pdf_closure = static_cast(closure); - size_t whole_chunks = pdf_closure->len / PAGE_SIZE; - size_t remainder = pdf_closure->len - whole_chunks * PAGE_SIZE; +cairo_status_t canvas_write_to_pdf_stream(cairo_surface_t *surface, cairo_write_func_t write_func, PdfStreamInfo* streaminfo) { + size_t whole_chunks = streaminfo->len / PAGE_SIZE; + size_t remainder = streaminfo->len - whole_chunks * PAGE_SIZE; for (size_t i = 0; i < whole_chunks; ++i) { - write_func(pdf_closure, &pdf_closure->data[i * PAGE_SIZE], PAGE_SIZE); + write_func(streaminfo, &streaminfo->data[i * PAGE_SIZE], PAGE_SIZE); } if (remainder) { - write_func(pdf_closure, &pdf_closure->data[whole_chunks * PAGE_SIZE], remainder); + write_func(streaminfo, &streaminfo->data[whole_chunks * PAGE_SIZE], remainder); } return CAIRO_STATUS_SUCCESS; @@ -536,26 +583,28 @@ NAN_METHOD(Canvas::StreamPDFSync) { cairo_surface_finish(canvas->surface()); - closure_t closure; - closure.data = static_cast(canvas->backend()->closure())->data; - closure.len = static_cast(canvas->backend()->closure())->len; - closure.fn = info[0].As(); + PdfSvgClosure* closure = static_cast(canvas->backend())->closure(); + Local fn = info[0].As(); + PdfStreamInfo streaminfo; + streaminfo.fn = fn; + streaminfo.data = closure->data; + streaminfo.len = closure->len; Nan::TryCatch try_catch; - cairo_status_t status = canvas_write_to_pdf_stream(canvas->surface(), streamPDF, &closure); + cairo_status_t status = canvas_write_to_pdf_stream(canvas->surface(), streamPDF, &streaminfo); if (try_catch.HasCaught()) { try_catch.ReThrow(); } else if (status) { Local error = Canvas::Error(status); - Nan::Call(closure.fn, Nan::GetCurrentContext()->Global(), 1, &error); + Nan::Call(fn, Nan::GetCurrentContext()->Global(), 1, &error); } else { Local argv[3] = { Nan::Null() , Nan::Null() , Nan::New(0) }; - Nan::Call(closure.fn, Nan::GetCurrentContext()->Global(), sizeof argv / sizeof *argv, argv); + Nan::Call(fn, Nan::GetCurrentContext()->Global(), sizeof argv / sizeof *argv, argv); } } @@ -566,26 +615,17 @@ NAN_METHOD(Canvas::StreamPDFSync) { #ifdef HAVE_JPEG NAN_METHOD(Canvas::StreamJPEGSync) { - // TODO: async as well - if (!info[0]->IsNumber()) - return Nan::ThrowTypeError("buffer size required"); - if (!info[1]->IsNumber()) - return Nan::ThrowTypeError("quality setting required"); - if (!info[2]->IsBoolean()) - return Nan::ThrowTypeError("progressive setting required"); - if (!info[3]->IsNumber()) - return Nan::ThrowTypeError("chromaHSampFactor required"); - if (!info[4]->IsNumber()) - return Nan::ThrowTypeError("chromaVSampFactor required"); - if (!info[5]->IsFunction()) + if (!info[1]->IsFunction()) return Nan::ThrowTypeError("callback function required"); Canvas *canvas = Nan::ObjectWrap::Unwrap(info.This()); - closure_t closure; - closure.fn = Local::Cast(info[5]); + JpegClosure closure(canvas); + parseJPEGArgs(info[0], closure); + closure.fn = Local::Cast(info[1]); Nan::TryCatch try_catch; - write_to_jpeg_stream(canvas->surface(), info[0]->NumberValue(), info[1]->NumberValue(), info[2]->BooleanValue(), info[3]->NumberValue(), info[4]->NumberValue(), &closure); + uint32_t bufsize = getSafeBufSize(canvas); + write_to_jpeg_stream(canvas->surface(), bufsize, &closure); if (try_catch.HasCaught()) { try_catch.ReThrow(); diff --git a/src/Canvas.h b/src/Canvas.h index 69b78d173..847da4a00 100644 --- a/src/Canvas.h +++ b/src/Canvas.h @@ -63,7 +63,8 @@ class Canvas: public Nan::ObjectWrap { static NAN_METHOD(StreamJPEGSync); static NAN_METHOD(RegisterFont); static Local Error(cairo_status_t status); - static void ToBufferAsync(uv_work_t *req); + static void ToPngBufferAsync(uv_work_t *req); + static void ToJpegBufferAsync(uv_work_t *req); static void ToBufferAsyncAfter(uv_work_t *req); static PangoWeight GetWeightFromCSSString(const char *weight); static PangoStyle GetStyleFromCSSString(const char *style); diff --git a/src/JPEGStream.h b/src/JPEGStream.h index ff6c3f6d0..473be72fc 100644 --- a/src/JPEGStream.h +++ b/src/JPEGStream.h @@ -6,6 +6,7 @@ #ifndef __NODE_JPEG_STREAM_H__ #define __NODE_JPEG_STREAM_H__ +#include "closure.h" #include "Canvas.h" #include #include @@ -17,7 +18,7 @@ typedef struct { struct jpeg_destination_mgr pub; - closure_t *closure; + JpegClosure* closure; JOCTET *buffer; int bufsize; } closure_destination_mgr; @@ -74,7 +75,7 @@ term_closure_destination(j_compress_ptr cinfo){ } void -jpeg_closure_dest(j_compress_ptr cinfo, closure_t * closure, int bufsize){ +jpeg_closure_dest(j_compress_ptr cinfo, JpegClosure* closure, int bufsize){ closure_destination_mgr * dest; /* The destination object is made permanent so that multiple JPEG images @@ -100,34 +101,27 @@ jpeg_closure_dest(j_compress_ptr cinfo, closure_t * closure, int bufsize){ cinfo->dest->free_in_buffer = dest->bufsize; } -void -write_to_jpeg_stream(cairo_surface_t *surface, int bufsize, int quality, bool progressive, int chromaHSampFactor, int chromaVSampFactor, closure_t *closure){ +void encode_jpeg(jpeg_compress_struct cinfo, cairo_surface_t *surface, int quality, bool progressive, int chromaHSampFactor, int chromaVSampFactor) { int w = cairo_image_surface_get_width(surface); int h = cairo_image_surface_get_height(surface); - struct jpeg_compress_struct cinfo; - struct jpeg_error_mgr jerr; - JSAMPROW slr; - cinfo.err = jpeg_std_error(&jerr); - jpeg_create_compress(&cinfo); cinfo.in_color_space = JCS_RGB; cinfo.input_components = 3; cinfo.image_width = w; cinfo.image_height = h; jpeg_set_defaults(&cinfo); if (progressive) - jpeg_simple_progression(&cinfo); - jpeg_set_quality(&cinfo, quality, (quality<25)?0:1); + jpeg_simple_progression(&cinfo); + jpeg_set_quality(&cinfo, quality, (quality < 25) ? 0 : 1); cinfo.comp_info[0].h_samp_factor = chromaHSampFactor; cinfo.comp_info[0].v_samp_factor = chromaVSampFactor; - jpeg_closure_dest(&cinfo, closure, bufsize); - + JSAMPROW slr; jpeg_start_compress(&cinfo, TRUE); unsigned char *dst; - unsigned int *src = (unsigned int *) cairo_image_surface_get_data(surface); + unsigned int *src = (unsigned int *)cairo_image_surface_get_data(surface); int sl = 0; - dst = (unsigned char *) malloc(w * 3); + dst = (unsigned char *)malloc(w * 3); while (sl < h) { unsigned char *dp = dst; int x = 0; @@ -148,4 +142,38 @@ write_to_jpeg_stream(cairo_surface_t *surface, int bufsize, int quality, bool pr jpeg_destroy_compress(&cinfo); } +void +write_to_jpeg_stream(cairo_surface_t *surface, int bufsize, JpegClosure* closure) { + struct jpeg_compress_struct cinfo; + struct jpeg_error_mgr jerr; + cinfo.err = jpeg_std_error(&jerr); + jpeg_create_compress(&cinfo); + jpeg_closure_dest(&cinfo, closure, bufsize); + encode_jpeg( + cinfo, + surface, + closure->quality, + closure->progressive, + closure->chromaSubsampling, + closure->chromaSubsampling); +} + +void +write_to_jpeg_buffer(cairo_surface_t* surface, JpegClosure* closure, unsigned char** outbuff, uint32_t* outsize) { + struct jpeg_compress_struct cinfo; + struct jpeg_error_mgr jerr; + cinfo.err = jpeg_std_error(&jerr); + jpeg_create_compress(&cinfo); + unsigned long ulOutsize; + jpeg_mem_dest(&cinfo, outbuff, &ulOutsize); + encode_jpeg( + cinfo, + surface, + closure->quality, + closure->progressive, + closure->chromaSubsampling, + closure->chromaSubsampling); + *outsize = static_cast(ulOutsize); +} + #endif diff --git a/src/PNG.h b/src/PNG.h index d202f78f8..e35f24f09 100644 --- a/src/PNG.h +++ b/src/PNG.h @@ -89,7 +89,7 @@ static void canvas_convert_565_to_888(png_structp png, png_row_infop row_info, p struct canvas_png_write_closure_t { cairo_write_func_t write_func; - closure_t *closure; + PngClosure* closure; }; #ifdef PNG_SETJMP_SUPPORTED @@ -164,8 +164,8 @@ static cairo_status_t canvas_write_png(cairo_surface_t *surface, png_rw_ptr writ #endif png_set_write_fn(png, closure, write_func, canvas_png_flush); - png_set_compression_level(png, closure->closure->compression_level); - png_set_filter(png, 0, closure->closure->filter); + png_set_compression_level(png, closure->closure->compressionLevel); + png_set_filter(png, 0, closure->closure->filters); cairo_format_t format = cairo_image_surface_get_format(surface); @@ -279,7 +279,7 @@ static void canvas_stream_write_func(png_structp png, png_bytep data, png_size_t } } -static cairo_status_t canvas_write_to_png_stream(cairo_surface_t *surface, cairo_write_func_t write_func, closure_t *closure) { +static cairo_status_t canvas_write_to_png_stream(cairo_surface_t *surface, cairo_write_func_t write_func, PngClosure* closure) { struct canvas_png_write_closure_t png_closure; if (cairo_surface_status(surface)) { diff --git a/src/backend/Backend.cc b/src/backend/Backend.cc index 6075f9544..74e05c17c 100644 --- a/src/backend/Backend.cc +++ b/src/backend/Backend.cc @@ -7,7 +7,6 @@ Backend::Backend(string name, int width, int height) , height(height) , surface(NULL) , canvas(NULL) - , _closure(NULL) {} Backend::~Backend() diff --git a/src/backend/Backend.h b/src/backend/Backend.h index bcee4ddb1..d3871c3cf 100644 --- a/src/backend/Backend.h +++ b/src/backend/Backend.h @@ -30,10 +30,6 @@ class Backend : public Nan::ObjectWrap public: virtual ~Backend(); - // TODO Used only by SVG and PDF, move there - void* _closure; - inline void* closure(){ return _closure; } - void setCanvas(Canvas* canvas); virtual cairo_surface_t* createSurface() = 0; diff --git a/src/backend/PdfBackend.cc b/src/backend/PdfBackend.cc index a976b6f86..03b70e38e 100644 --- a/src/backend/PdfBackend.cc +++ b/src/backend/PdfBackend.cc @@ -11,63 +11,50 @@ using namespace v8; PdfBackend::PdfBackend(int width, int height) - : Backend("pdf", width, height) -{ - _closure = malloc(sizeof(closure_t)); - assert(_closure); - createSurface(); + : Backend("pdf", width, height) { + createSurface(); } -PdfBackend::~PdfBackend() -{ - cairo_surface_finish(this->surface); - closure_destroy((closure_t*)_closure); - free(_closure); - - destroySurface(); +PdfBackend::~PdfBackend() { + cairo_surface_finish(surface); + if (_closure) delete _closure; + destroySurface(); } -cairo_surface_t* PdfBackend::createSurface() -{ - cairo_status_t status = closure_init((closure_t*)_closure, this->canvas, 0, PNG_NO_FILTERS); - assert(status == CAIRO_STATUS_SUCCESS); - - this->surface = cairo_pdf_surface_create_for_stream(toBuffer, _closure, width, height); - - return this->surface; +cairo_surface_t* PdfBackend::createSurface() { + if (!_closure) _closure = new PdfSvgClosure(canvas); + surface = cairo_pdf_surface_create_for_stream(toBuffer, _closure, width, height); + return surface; } -cairo_surface_t* PdfBackend::recreateSurface() -{ - cairo_pdf_surface_set_size(this->surface, width, height); +cairo_surface_t* PdfBackend::recreateSurface() { + cairo_pdf_surface_set_size(surface, width, height); - return this->surface; + return surface; } Nan::Persistent PdfBackend::constructor; -void PdfBackend::Initialize(Handle target) -{ - Nan::HandleScope scope; +void PdfBackend::Initialize(Handle target) { + Nan::HandleScope scope; - Local ctor = Nan::New(PdfBackend::New); - PdfBackend::constructor.Reset(ctor); - ctor->InstanceTemplate()->SetInternalFieldCount(1); - ctor->SetClassName(Nan::New("PdfBackend").ToLocalChecked()); - target->Set(Nan::New("PdfBackend").ToLocalChecked(), ctor->GetFunction()); + Local ctor = Nan::New(PdfBackend::New); + PdfBackend::constructor.Reset(ctor); + ctor->InstanceTemplate()->SetInternalFieldCount(1); + ctor->SetClassName(Nan::New("PdfBackend").ToLocalChecked()); + target->Set(Nan::New("PdfBackend").ToLocalChecked(), ctor->GetFunction()); } -NAN_METHOD(PdfBackend::New) -{ - int width = 0; - int height = 0; - if (info[0]->IsNumber()) width = info[0]->Uint32Value(); - if (info[1]->IsNumber()) height = info[1]->Uint32Value(); +NAN_METHOD(PdfBackend::New) { + int width = 0; + int height = 0; + if (info[0]->IsNumber()) width = info[0]->Uint32Value(); + if (info[1]->IsNumber()) height = info[1]->Uint32Value(); - PdfBackend* backend = new PdfBackend(width, height); + PdfBackend* backend = new PdfBackend(width, height); - backend->Wrap(info.This()); - info.GetReturnValue().Set(info.This()); + backend->Wrap(info.This()); + info.GetReturnValue().Set(info.This()); } diff --git a/src/backend/PdfBackend.h b/src/backend/PdfBackend.h index 9287c0fd5..0e41157d9 100644 --- a/src/backend/PdfBackend.h +++ b/src/backend/PdfBackend.h @@ -3,6 +3,7 @@ #include +#include "../closure.h" #include "Backend.h" using namespace std; @@ -14,6 +15,9 @@ class PdfBackend : public Backend cairo_surface_t* recreateSurface(); public: + PdfSvgClosure* _closure = NULL; + inline PdfSvgClosure* closure() { return _closure; } + PdfBackend(int width, int height); ~PdfBackend(); diff --git a/src/backend/SvgBackend.cc b/src/backend/SvgBackend.cc index 71249a16b..26f5df360 100644 --- a/src/backend/SvgBackend.cc +++ b/src/backend/SvgBackend.cc @@ -11,65 +11,52 @@ using namespace v8; SvgBackend::SvgBackend(int width, int height) - : Backend("svg", width, height) -{ - _closure = malloc(sizeof(closure_t)); - assert(_closure); - createSurface(); + : Backend("svg", width, height) { + createSurface(); } -SvgBackend::~SvgBackend() -{ - cairo_surface_finish(this->surface); - closure_destroy((closure_t*)_closure); - free(_closure); - - destroySurface(); +SvgBackend::~SvgBackend() { + cairo_surface_finish(surface); + if (_closure) delete _closure; + destroySurface(); } -cairo_surface_t* SvgBackend::createSurface() -{ - cairo_status_t status = closure_init((closure_t*)_closure, this->canvas, 0, PNG_NO_FILTERS); - assert(status == CAIRO_STATUS_SUCCESS); - - this->surface = cairo_svg_surface_create_for_stream(toBuffer, _closure, width, height); - - return this->surface; +cairo_surface_t* SvgBackend::createSurface() { + if (!_closure) _closure = new PdfSvgClosure(canvas); + surface = cairo_svg_surface_create_for_stream(toBuffer, _closure, width, height); + return surface; } -cairo_surface_t* SvgBackend::recreateSurface() -{ - cairo_surface_finish(this->surface); - closure_destroy((closure_t*)_closure); - cairo_surface_destroy(this->surface); +cairo_surface_t* SvgBackend::recreateSurface() { + cairo_surface_finish(surface); + delete _closure; + cairo_surface_destroy(surface); - return createSurface(); + return createSurface(); } Nan::Persistent SvgBackend::constructor; -void SvgBackend::Initialize(Handle target) -{ - Nan::HandleScope scope; +void SvgBackend::Initialize(Handle target) { + Nan::HandleScope scope; - Local ctor = Nan::New(SvgBackend::New); - SvgBackend::constructor.Reset(ctor); - ctor->InstanceTemplate()->SetInternalFieldCount(1); - ctor->SetClassName(Nan::New("SvgBackend").ToLocalChecked()); - target->Set(Nan::New("SvgBackend").ToLocalChecked(), ctor->GetFunction()); + Local ctor = Nan::New(SvgBackend::New); + SvgBackend::constructor.Reset(ctor); + ctor->InstanceTemplate()->SetInternalFieldCount(1); + ctor->SetClassName(Nan::New("SvgBackend").ToLocalChecked()); + target->Set(Nan::New("SvgBackend").ToLocalChecked(), ctor->GetFunction()); } -NAN_METHOD(SvgBackend::New) -{ - int width = 0; - int height = 0; - if (info[0]->IsNumber()) width = info[0]->Uint32Value(); - if (info[1]->IsNumber()) height = info[1]->Uint32Value(); +NAN_METHOD(SvgBackend::New) { + int width = 0; + int height = 0; + if (info[0]->IsNumber()) width = info[0]->Uint32Value(); + if (info[1]->IsNumber()) height = info[1]->Uint32Value(); - SvgBackend* backend = new SvgBackend(width, height); + SvgBackend* backend = new SvgBackend(width, height); - backend->Wrap(info.This()); - info.GetReturnValue().Set(info.This()); + backend->Wrap(info.This()); + info.GetReturnValue().Set(info.This()); } diff --git a/src/backend/SvgBackend.h b/src/backend/SvgBackend.h index fda10d66e..47391ba89 100644 --- a/src/backend/SvgBackend.h +++ b/src/backend/SvgBackend.h @@ -4,6 +4,7 @@ #include #include "Backend.h" +#include "../closure.h" using namespace std; @@ -14,6 +15,9 @@ class SvgBackend : public Backend cairo_surface_t* recreateSurface(); public: + PdfSvgClosure* _closure = NULL; + inline PdfSvgClosure* closure() { return _closure; } + SvgBackend(int width, int height); ~SvgBackend(); diff --git a/src/closure.cc b/src/closure.cc index 968da90a5..86a46d9b5 100644 --- a/src/closure.cc +++ b/src/closure.cc @@ -1,30 +1,27 @@ #include "closure.h" +PdfSvgClosure::PdfSvgClosure(Canvas* canvas) : Closure(canvas) { + //data = new (std::nothrow) uint8_t[max_len]; // toBuffer.cc uses realloc + data = static_cast(malloc(max_len)); + if (!data) throw CAIRO_STATUS_NO_MEMORY; +} -/* - * Initialize the given closure. - */ - -cairo_status_t -closure_init(closure_t *closure, Canvas *canvas, unsigned int compression_level, unsigned int filter) { - closure->len = 0; - closure->canvas = canvas; - closure->data = (uint8_t *) malloc(closure->max_len = PAGE_SIZE); - if (!closure->data) return CAIRO_STATUS_NO_MEMORY; - closure->compression_level = compression_level; - closure->filter = filter; - return CAIRO_STATUS_SUCCESS; +PdfSvgClosure::~PdfSvgClosure() { + if (len) { + //delete[] data; + free(data); + Nan::AdjustExternalMemory(-static_cast(max_len)); + } } -/* - * Free the given closure's data, - * and hint V8 at the memory dealloc. - */ +PngClosure::PngClosure(Canvas* canvas) : Closure(canvas) { + data = static_cast(malloc(max_len)); + if (!data) throw CAIRO_STATUS_NO_MEMORY; +} -void -closure_destroy(closure_t *closure) { - if (closure->len) { - free(closure->data); - Nan::AdjustExternalMemory(-static_cast(closure->max_len)); +PngClosure::~PngClosure() { + if (len) { + free(data); + Nan::AdjustExternalMemory(-static_cast(max_len)); } } diff --git a/src/closure.h b/src/closure.h index 76cd2fe15..cdfa91214 100644 --- a/src/closure.h +++ b/src/closure.h @@ -17,11 +17,57 @@ #endif #include +#include #include "Canvas.h" /* - * PNG stream closure. + * Image encoding closures. + */ + +struct Closure { + Nan::Callback *pfn; + Local fn; + unsigned len = 0; + unsigned max_len = PAGE_SIZE; + uint8_t* data = NULL; + Canvas* canvas = NULL; + cairo_status_t status = CAIRO_STATUS_SUCCESS; + + Closure(Canvas* canvas) : canvas(canvas) {}; +}; + +struct PdfSvgClosure : Closure { + PdfSvgClosure(Canvas* canvas); + ~PdfSvgClosure(); +}; + +struct PngClosure : Closure { + uint32_t compressionLevel = 6; + uint32_t filters = PNG_ALL_FILTERS; + // Indexed PNGs: + uint32_t nPaletteColors = 0; + uint8_t* palette = NULL; + uint8_t backgroundIndex = 0; + + PngClosure(Canvas* canvas); + ~PngClosure(); +}; + +struct JpegClosure : Closure { + uint32_t quality = 75; + uint32_t chromaSubsampling = 2; + bool progressive = false; + + JpegClosure(Canvas* canvas) : Closure(canvas) {}; + ~JpegClosure() { + // jpeg_mem_dest mallocs 'data' + if (data) free(data); + } +}; + +/* + * Image encoding closure. */ typedef struct { @@ -32,11 +78,6 @@ typedef struct { uint8_t *data; Canvas *canvas; cairo_status_t status; - uint32_t compression_level; - uint32_t filter; - uint8_t *palette; - size_t nPaletteColors; - uint8_t backgroundIndex; } closure_t; /* @@ -44,7 +85,7 @@ typedef struct { */ cairo_status_t -closure_init(closure_t *closure, Canvas *canvas, unsigned int compression_level, unsigned int filter); +closure_init(closure_t *closure, Canvas *canvas); /* * Free the given closure's data, diff --git a/src/toBuffer.cc b/src/toBuffer.cc index 3660532b7..d0a038894 100644 --- a/src/toBuffer.cc +++ b/src/toBuffer.cc @@ -8,9 +8,11 @@ * Canvas::ToBuffer callback. */ +// TODO try to use std::vector instead + cairo_status_t toBuffer(void *c, const uint8_t *odata, unsigned len) { - closure_t *closure = (closure_t *) c; + Closure* closure = (Closure*)c; if (closure->len + len > closure->max_len) { uint8_t *data; diff --git a/test/canvas.test.js b/test/canvas.test.js index 5c4aa020c..d3338e24b 100644 --- a/test/canvas.test.js +++ b/test/canvas.test.js @@ -487,100 +487,148 @@ describe('Canvas', function () { assert.equal('end', ctx.textAlign); }); - it('Canvas#toBuffer()', function () { - var buf = createCanvas(200,200).toBuffer(); - assert.equal('PNG', buf.slice(1,4).toString()); - }); - - it('Canvas#toBuffer() async', function (done) { - createCanvas(200, 200).toBuffer(function(err, buf){ - assert.ok(!err); + describe('#toBuffer', function () { + it('Canvas#toBuffer()', function () { + var buf = createCanvas(200,200).toBuffer(); assert.equal('PNG', buf.slice(1,4).toString()); - done(); }); - }); - - describe('#toBuffer("raw")', function() { - var canvas = createCanvas(11, 10) - , ctx = canvas.getContext('2d'); - - ctx.clearRect(0, 0, 11, 10); - - ctx.fillStyle = 'rgba(200, 200, 200, 0.505)'; - ctx.fillRect(0, 0, 5, 5); - - ctx.fillStyle = 'red'; - ctx.fillRect(5, 0, 5, 5); - - ctx.fillStyle = '#00ff00'; - ctx.fillRect(0, 5, 5, 5); - - ctx.fillStyle = 'black'; - ctx.fillRect(5, 5, 4, 5); - - /** Output: - * *****RRRRR- - * *****RRRRR- - * *****RRRRR- - * *****RRRRR- - * *****RRRRR- - * GGGGGBBBB-- - * GGGGGBBBB-- - * GGGGGBBBB-- - * GGGGGBBBB-- - * GGGGGBBBB-- - */ - - var buf = canvas.toBuffer('raw'); - var stride = canvas.stride; - - var endianness = os.endianness(); - - function assertPixel(u32, x, y, message) { - var expected = '0x' + u32.toString(16); - - // Buffer doesn't have readUInt32(): it only has readUInt32LE() and - // readUInt32BE(). - var px = buf['readUInt32' + endianness](y * stride + x * 4); - var actual = '0x' + px.toString(16); - - assert.equal(actual, expected, message); - } - - it('should have the correct size', function() { - assert.equal(buf.length, stride * 10); - }); - - it('does not premultiply alpha', function() { - assertPixel(0x80646464, 0, 0, 'first semitransparent pixel'); - assertPixel(0x80646464, 4, 4, 'last semitransparent pixel'); + + it('Canvas#toBuffer("image/png")', function () { + var buf = createCanvas(200,200).toBuffer('image/png'); + assert.equal('PNG', buf.slice(1,4).toString()); }); - - it('draws red', function() { - assertPixel(0xffff0000, 5, 0, 'first red pixel'); - assertPixel(0xffff0000, 9, 4, 'last red pixel'); + + it('Canvas#toBuffer("image/png", {compressionLevel: 5})', function () { + var buf = createCanvas(200,200).toBuffer('image/png', {compressionLevel: 5}); + assert.equal('PNG', buf.slice(1,4).toString()); }); - it('draws green', function() { - assertPixel(0xff00ff00, 0, 5, 'first green pixel'); - assertPixel(0xff00ff00, 4, 9, 'last green pixel'); + it('Canvas#toBuffer("image/jpeg")', function () { + var buf = createCanvas(200,200).toBuffer('image/jpeg'); + assert.equal(buf[0], 0xff); + assert.equal(buf[1], 0xd8); + assert.equal(buf[buf.byteLength - 2], 0xff); + assert.equal(buf[buf.byteLength - 1], 0xd9); }); - it('draws black', function() { - assertPixel(0xff000000, 5, 5, 'first black pixel'); - assertPixel(0xff000000, 8, 9, 'last black pixel'); + it('Canvas#toBuffer("image/jpeg", {quality: 0.95})', function () { + var buf = createCanvas(200,200).toBuffer('image/jpeg', {quality: 0.95}); + assert.equal(buf[0], 0xff); + assert.equal(buf[1], 0xd8); + assert.equal(buf[buf.byteLength - 2], 0xff); + assert.equal(buf[buf.byteLength - 1], 0xd9); }); - it('leaves undrawn pixels black, transparent', function() { - assertPixel(0x0, 9, 5, 'first undrawn pixel'); - assertPixel(0x0, 9, 9, 'last undrawn pixel'); + it('Canvas#toBuffer(callback)', function (done) { + createCanvas(200, 200).toBuffer(function(err, buf){ + assert.ok(!err); + assert.equal('PNG', buf.slice(1,4).toString()); + done(); + }); }); - it('is immutable', function() { - ctx.fillStyle = 'white'; - ctx.fillRect(0, 0, 10, 10); - canvas.toBuffer('raw'); // (side-effect: flushes canvas) - assertPixel(0xffff0000, 5, 0, 'first red pixel'); + it('Canvas#toBuffer(callback, "image/jpeg")', function () { + var buf = createCanvas(200,200).toBuffer(function (err, buff) { + assert.ok(!err); + assert.equal(buf[0], 0xff); + assert.equal(buf[1], 0xd8); + assert.equal(buf[buf.byteLength - 2], 0xff); + assert.equal(buf[buf.byteLength - 1], 0xd9); + }, 'image/jpeg'); + }); + + it('Canvas#toBuffer(callback, "image/jpeg", {quality: 0.95})', function () { + var buf = createCanvas(200,200).toBuffer(function (err, buff) { + assert.ok(!err); + assert.equal(buf[0], 0xff); + assert.equal(buf[1], 0xd8); + assert.equal(buf[buf.byteLength - 2], 0xff); + assert.equal(buf[buf.byteLength - 1], 0xd9); + }, 'image/jpeg', {quality: 0.95}); + }); + + describe('#toBuffer("raw")', function() { + var canvas = createCanvas(11, 10) + , ctx = canvas.getContext('2d'); + + ctx.clearRect(0, 0, 11, 10); + + ctx.fillStyle = 'rgba(200, 200, 200, 0.505)'; + ctx.fillRect(0, 0, 5, 5); + + ctx.fillStyle = 'red'; + ctx.fillRect(5, 0, 5, 5); + + ctx.fillStyle = '#00ff00'; + ctx.fillRect(0, 5, 5, 5); + + ctx.fillStyle = 'black'; + ctx.fillRect(5, 5, 4, 5); + + /** Output: + * *****RRRRR- + * *****RRRRR- + * *****RRRRR- + * *****RRRRR- + * *****RRRRR- + * GGGGGBBBB-- + * GGGGGBBBB-- + * GGGGGBBBB-- + * GGGGGBBBB-- + * GGGGGBBBB-- + */ + + var buf = canvas.toBuffer('raw'); + var stride = canvas.stride; + + var endianness = os.endianness(); + + function assertPixel(u32, x, y, message) { + var expected = '0x' + u32.toString(16); + + // Buffer doesn't have readUInt32(): it only has readUInt32LE() and + // readUInt32BE(). + var px = buf['readUInt32' + endianness](y * stride + x * 4); + var actual = '0x' + px.toString(16); + + assert.equal(actual, expected, message); + } + + it('should have the correct size', function() { + assert.equal(buf.length, stride * 10); + }); + + it('does not premultiply alpha', function() { + assertPixel(0x80646464, 0, 0, 'first semitransparent pixel'); + assertPixel(0x80646464, 4, 4, 'last semitransparent pixel'); + }); + + it('draws red', function() { + assertPixel(0xffff0000, 5, 0, 'first red pixel'); + assertPixel(0xffff0000, 9, 4, 'last red pixel'); + }); + + it('draws green', function() { + assertPixel(0xff00ff00, 0, 5, 'first green pixel'); + assertPixel(0xff00ff00, 4, 9, 'last green pixel'); + }); + + it('draws black', function() { + assertPixel(0xff000000, 5, 5, 'first black pixel'); + assertPixel(0xff000000, 8, 9, 'last black pixel'); + }); + + it('leaves undrawn pixels black, transparent', function() { + assertPixel(0x0, 9, 5, 'first undrawn pixel'); + assertPixel(0x0, 9, 9, 'last undrawn pixel'); + }); + + it('is immutable', function() { + ctx.fillStyle = 'white'; + ctx.fillRect(0, 0, 10, 10); + canvas.toBuffer('raw'); // (side-effect: flushes canvas) + assertPixel(0xffff0000, 5, 0, 'first red pixel'); + }); }); }); @@ -1348,16 +1396,6 @@ describe('Canvas', function () { }); }); - it('Canvas#jpegStream() should clamp buffer size (#674)', function (done) { - var c = createCanvas(10, 10); - var SIZE = 10 * 1024 * 1024; - var s = c.jpegStream({bufsize: SIZE}); - s.on('data', function (chunk) { - assert(chunk.length < SIZE); - }); - s.on('end', done); - }); - it('Context2d#fill()', function() { var canvas = createCanvas(2, 2); var ctx = canvas.getContext('2d'); From 80a5a0de5f80aa8e02ab31352da1d7b0ac2b2917 Mon Sep 17 00:00:00 2001 From: Zach Bjornson Date: Thu, 31 May 2018 11:19:51 -0700 Subject: [PATCH 3/3] Standardize on createPNGStream/createJPEGStream Vs. canvas.pngStream(). Follows node's core fs.createReadStream, etc. --- CHANGELOG.md | 14 ++++++++------ Readme.md | 22 +++++++++++----------- examples/ray.js | 2 +- examples/resize.js | 2 +- test/canvas.test.js | 8 ++++---- 5 files changed, 25 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b7473174c..88215e7f9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,10 +10,10 @@ project adheres to [Semantic Versioning](http://semver.org/). **Upgrading from 1.x** ```js -// (1) The quality argument for canvas.jpegStream now goes from 0 to 1 instead -// of from 0 to 100: -canvas.jpegStream({quality: 50}) // old -canvas.jpegStream({quality: 0.5}) // new +// (1) The quality argument for canvas.createJPEGStream/canvas.jpegStream now +// goes from 0 to 1 instead of from 0 to 100: +canvas.createJPEGStream({quality: 50}) // old +canvas.createJPEGStream({quality: 0.5}) // new // (2) The ZLIB compression level and PNG filter options for canvas.toBuffer are // now named instead of positional arguments: @@ -29,10 +29,12 @@ canvas.pngStream({compressionLevel: 3, filters: canvas.PNG_FILTER_NONE}) // new // (4) canvas.syncPNGStream() and canvas.syncJPEGStream() have been removed: canvas.syncPNGStream() // old -canvas.pngStream() // new +canvas.createSyncPNGStream() // old +canvas.createPNGStream() // new canvas.syncJPEGStream() // old -canvas.jpegStream() // new +canvas.createSyncJPEGStream() // old +canvas.createJPEGStream() // new ``` ### Breaking diff --git a/Readme.md b/Readme.md index a5f5f9475..994afa94e 100644 --- a/Readme.md +++ b/Readme.md @@ -201,12 +201,12 @@ const myCanvas = createCanvas(w, h, 'pdf') myCanvas.toBuffer() // returns a buffer containing a PDF-encoded canvas ``` -### Canvas#pngStream(options) +### Canvas#createPNGStream(options) Creates a [`ReadableStream`](https://nodejs.org/api/stream.html#stream_class_stream_readable) that emits PNG-encoded data. -> `canvas.pngStream([config]) => ReadableStream` +> `canvas.createPNGStream([config]) => ReadableStream` * `config` An object specifying the ZLIB compression level (between 0 and 9), the compression filter(s), the palette (indexed PNGs only) and/or the @@ -219,7 +219,7 @@ that emits PNG-encoded data. ```javascript const fs = require('fs') const out = fs.createWriteStream(__dirname + '/test.png') -const stream = canvas.pngStream() +const stream = canvas.createPNGStream() stream.pipe(out) out.on('finish', () => console.log('The PNG file was created.')) ``` @@ -234,21 +234,21 @@ const palette = new Uint8ClampedArray([ 127, 127, 255, 255 // ... ]) -canvas.pngStream({ +canvas.createPNGStream({ palette: palette, backgroundIndex: 0 // optional, defaults to 0 }) ``` -### Canvas#jpegStream() +### Canvas#createJPEGStream() -Creates a [`ReadableStream`](https://nodejs.org/api/stream.html#stream_class_stream_readable) +Creates a [`createJPEGStream`](https://nodejs.org/api/stream.html#stream_class_stream_readable) that emits JPEG-encoded data. -_Note: At the moment, `jpegStream()` is synchronous under the hood. That is, it +_Note: At the moment, `createJPEGStream()` is synchronous under the hood. That is, it runs in the main thread, not in the libuv threadpool._ -> `canvas.pngStream([config]) => ReadableStream` +> `canvas.createJPEGStream([config]) => ReadableStream` * `config` an object specifying the quality (0 to 1), if progressive compression should be used and/or if chroma subsampling should be used: @@ -260,12 +260,12 @@ runs in the main thread, not in the libuv threadpool._ ```javascript const fs = require('fs') const out = fs.createWriteStream(__dirname + '/test.jpeg') -const stream = canvas.jpegStream() +const stream = canvas.createJPEGStream() stream.pipe(out) out.on('finish', () => console.log('The JPEG file was created.')) // Disable 2x2 chromaSubsampling for deeper colors and use a higher quality -const stream = canvas.jpegStream({ +const stream = canvas.createJPEGStream({ quality: 95, chromaSubsampling: false }) @@ -281,7 +281,7 @@ var dataUrl = canvas.toDataURL('image/png'); canvas.toDataURL(function(err, png){ }); // defaults to PNG canvas.toDataURL('image/png', function(err, png){ }); canvas.toDataURL('image/jpeg', function(err, jpeg){ }); // sync JPEG is not supported -canvas.toDataURL('image/jpeg', {opts...}, function(err, jpeg){ }); // see Canvas#jpegStream for valid options +canvas.toDataURL('image/jpeg', {opts...}, function(err, jpeg){ }); // see Canvas#createJPEGStream for valid options canvas.toDataURL('image/jpeg', quality, function(err, jpeg){ }); // spec-following; quality from 0 to 1 ``` diff --git a/examples/ray.js b/examples/ray.js index 93fd80184..68c85246a 100644 --- a/examples/ray.js +++ b/examples/ray.js @@ -82,4 +82,4 @@ render(1) console.log('Rendered in %s seconds', (new Date() - start) / 1000) -canvas.pngStream().pipe(fs.createWriteStream(path.join(__dirname, 'ray.png'))) +canvas.createPNGStream().pipe(fs.createWriteStream(path.join(__dirname, 'ray.png'))) diff --git a/examples/resize.js b/examples/resize.js index 151630ed5..d50aa9231 100644 --- a/examples/resize.js +++ b/examples/resize.js @@ -21,7 +21,7 @@ img.onload = function () { ctx.imageSmoothingEnabled = true ctx.drawImage(img, 0, 0, width, height) - canvas.pngStream().pipe(out) + canvas.createPNGStream().pipe(out) out.on('finish', function () { console.log('Resized and saved in %dms', new Date() - start) diff --git a/test/canvas.test.js b/test/canvas.test.js index d3338e24b..600633bed 100644 --- a/test/canvas.test.js +++ b/test/canvas.test.js @@ -1352,9 +1352,9 @@ describe('Canvas', function () { }); }); - it('Canvas#jpegStream()', function (done) { + it('Canvas#createJPEGStream()', function (done) { var canvas = createCanvas(640, 480); - var stream = canvas.jpegStream(); + var stream = canvas.createJPEGStream(); assert(stream instanceof Readable); var firstChunk = true; var bytes = 0; @@ -1378,9 +1378,9 @@ describe('Canvas', function () { // based on https://en.wikipedia.org/wiki/JPEG_File_Interchange_Format // end of image marker (FF D9) must exist to maintain JPEG standards - it('EOI at end of Canvas#jpegStream()', function (done) { + it('EOI at end of Canvas#createJPEGStream()', function (done) { var canvas = createCanvas(640, 480); - var stream = canvas.jpegStream(); + var stream = canvas.createJPEGStream(); var chunks = [] stream.on('data', function(chunk){ chunks.push(chunk)