From 003e803c3a5d76198b08b955ffc6dc3c9979dad2 Mon Sep 17 00:00:00 2001 From: Benjamin Byholm Date: Thu, 9 Jun 2016 16:37:16 +0300 Subject: [PATCH] Crude PDF stream implementation --- lib/canvas.js | 26 +++++++++++++++ lib/pdfstream.js | 59 +++++++++++++++++++++++++++++++++ src/Canvas.cc | 79 +++++++++++++++++++++++++++++++++++++++++++++ src/Canvas.h | 1 + test/canvas.test.js | 18 +++++++++++ 5 files changed, 183 insertions(+) create mode 100644 lib/pdfstream.js diff --git a/lib/canvas.js b/lib/canvas.js index 20aeb266c..d008f5aa0 100644 --- a/lib/canvas.js +++ b/lib/canvas.js @@ -16,6 +16,7 @@ var canvas = require('./bindings') , cairoVersion = canvas.cairoVersion , Context2d = require('./context2d') , PNGStream = require('./pngstream') + , PDFStream = require('./pdfstream') , JPEGStream = require('./jpegstream') , FontFace = canvas.FontFace , fs = require('fs') @@ -70,6 +71,7 @@ if (canvas.freetypeVersion) { exports.Context2d = Context2d; exports.PNGStream = PNGStream; +exports.PDFStream = PDFStream; exports.JPEGStream = JPEGStream; exports.Image = Image; exports.ImageData = canvas.ImageData; @@ -161,6 +163,30 @@ Canvas.prototype.createSyncPNGStream = function(){ return new PNGStream(this, true); }; +/** + * Create a `PDFStream` for `this` canvas. + * + * @return {PDFStream} + * @api public + */ + +Canvas.prototype.pdfStream = +Canvas.prototype.createPDFStream = function(){ + return new PDFStream(this); +}; + +/** + * Create a synchronous `PDFStream` for `this` canvas. + * + * @return {PDFStream} + * @api public + */ + +Canvas.prototype.syncPDFStream = +Canvas.prototype.createSyncPDFStream = function(){ + return new PDFStream(this, true); +}; + /** * Create a `JPEGStream` for `this` canvas. * diff --git a/lib/pdfstream.js b/lib/pdfstream.js new file mode 100644 index 000000000..92560ccbc --- /dev/null +++ b/lib/pdfstream.js @@ -0,0 +1,59 @@ +'use strict'; + +/*! + * Canvas - PDFStream + */ + +/** + * Module dependencies. + */ + +var Stream = require('stream').Stream; + +/** + * Initialize a `PDFStream` with the given `canvas`. + * + * "data" events are emitted with `Buffer` chunks, once complete the + * "end" event is emitted. The following example will stream to a file + * named "./my.pdf". + * + * var out = fs.createWriteStream(__dirname + '/my.pdf') + * , stream = canvas.createPDFStream(); + * + * stream.pipe(out); + * + * @param {Canvas} canvas + * @param {Boolean} sync + * @api public + */ + +var PDFStream = module.exports = function PDFStream(canvas, sync) { + var self = this + , method = sync + ? 'streamPDFSync' + : 'streamPDF'; + this.sync = sync; + this.canvas = canvas; + this.readable = true; + // TODO: implement async + if ('streamPDF' == method) method = 'streamPDFSync'; + process.nextTick(function(){ + canvas[method](function(err, chunk, len){ + if (err) { + self.emit('error', err); + self.readable = false; + } else if (len) { + self.emit('data', chunk, len); + } else { + self.emit('end'); + self.readable = false; + } + }); + }); +}; + +/** + * Inherit from `EventEmitter`. + */ + +PDFStream.prototype.__proto__ = Stream.prototype; diff --git a/src/Canvas.cc b/src/Canvas.cc index 83de51ef1..777c1681b 100644 --- a/src/Canvas.cc +++ b/src/Canvas.cc @@ -40,6 +40,7 @@ Canvas::Initialize(Nan::ADDON_REGISTER_FUNCTION_ARGS_TYPE target) { Local proto = ctor->PrototypeTemplate(); Nan::SetPrototypeMethod(ctor, "toBuffer", ToBuffer); Nan::SetPrototypeMethod(ctor, "streamPNGSync", StreamPNGSync); + Nan::SetPrototypeMethod(ctor, "streamPDFSync", StreamPDFSync); #ifdef HAVE_JPEG Nan::SetPrototypeMethod(ctor, "streamJPEGSync", StreamJPEGSync); #endif @@ -434,6 +435,84 @@ NAN_METHOD(Canvas::StreamPNGSync) { return; } +/* + * Canvas::StreamPDF FreeCallback + */ + +void stream_pdf_free(char *, void *) {} + +/* + * Canvas::StreamPDF callback. + */ + +static cairo_status_t +streamPDF(void *c, const uint8_t *data, unsigned len) { + Nan::HandleScope scope; + closure_t *closure = 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) }; + Nan::MakeCallback(Nan::GetCurrentContext()->Global(), closure->fn, 3, 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; + + for (size_t i = 0; i < whole_chunks; ++i) { + write_func(pdf_closure, &pdf_closure->data[i * PAGE_SIZE], PAGE_SIZE); + } + + if (remainder) { + write_func(pdf_closure, &pdf_closure->data[whole_chunks * PAGE_SIZE], remainder); + } + + return CAIRO_STATUS_SUCCESS; +} + +/* + * Stream PDF data synchronously. + */ + +NAN_METHOD(Canvas::StreamPDFSync) { + if (!info[0]->IsFunction()) + return Nan::ThrowTypeError("callback function required"); + + Canvas *canvas = Nan::ObjectWrap::Unwrap(info.Holder()); + + if (!canvas->isPDF()) + return Nan::ThrowTypeError("wrong canvas type"); + + cairo_surface_finish(canvas->surface()); + + closure_t closure; + closure.data = static_cast(canvas->closure())->data; + closure.len = static_cast(canvas->closure())->len; + closure.fn = info[0].As(); + + Nan::TryCatch try_catch; + + cairo_status_t status = canvas_write_to_pdf_stream(canvas->surface(), streamPDF, &closure); + + if (try_catch.HasCaught()) { + try_catch.ReThrow(); + } else if (status) { + Local error = Canvas::Error(status); + Nan::Call(closure.fn, Nan::GetCurrentContext()->Global(), 1, &error); + } else { + Local argv[3] = { + Nan::Null() + , Nan::Null() + , Nan::New(0) }; + Nan::Call(closure.fn, Nan::GetCurrentContext()->Global(), 3, argv); + } +} + /* * Stream JPEG data synchronously. */ diff --git a/src/Canvas.h b/src/Canvas.h index bf58b9b59..dcf31e861 100644 --- a/src/Canvas.h +++ b/src/Canvas.h @@ -62,6 +62,7 @@ class Canvas: public Nan::ObjectWrap { static NAN_SETTER(SetWidth); static NAN_SETTER(SetHeight); static NAN_METHOD(StreamPNGSync); + static NAN_METHOD(StreamPDFSync); static NAN_METHOD(StreamJPEGSync); static Local Error(cairo_status_t status); #if NODE_VERSION_AT_LEAST(0, 6, 0) diff --git a/test/canvas.test.js b/test/canvas.test.js index d2da79b45..3574ab2c1 100644 --- a/test/canvas.test.js +++ b/test/canvas.test.js @@ -802,6 +802,24 @@ describe('Canvas', function () { }); }); + it('Canvas#createSyncPDFStream()', function (done) { + var canvas = new Canvas(20, 20, 'pdf'); + var stream = canvas.createSyncPDFStream(); + var firstChunk = true; + stream.on('data', function (chunk) { + if (firstChunk) { + firstChunk = false; + assert.equal('PDF', chunk.slice(1, 4).toString()); + } + }); + stream.on('end', function () { + done(); + }); + stream.on('error', function (err) { + done(err); + }); + }); + it('Canvas#jpegStream()', function (done) { var canvas = new Canvas(640, 480); var stream = canvas.jpegStream();