Skip to content

Commit 003e803

Browse files
committed
Crude PDF stream implementation
1 parent b470ce8 commit 003e803

File tree

5 files changed

+183
-0
lines changed

5 files changed

+183
-0
lines changed

lib/canvas.js

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ var canvas = require('./bindings')
1616
, cairoVersion = canvas.cairoVersion
1717
, Context2d = require('./context2d')
1818
, PNGStream = require('./pngstream')
19+
, PDFStream = require('./pdfstream')
1920
, JPEGStream = require('./jpegstream')
2021
, FontFace = canvas.FontFace
2122
, fs = require('fs')
@@ -70,6 +71,7 @@ if (canvas.freetypeVersion) {
7071

7172
exports.Context2d = Context2d;
7273
exports.PNGStream = PNGStream;
74+
exports.PDFStream = PDFStream;
7375
exports.JPEGStream = JPEGStream;
7476
exports.Image = Image;
7577
exports.ImageData = canvas.ImageData;
@@ -161,6 +163,30 @@ Canvas.prototype.createSyncPNGStream = function(){
161163
return new PNGStream(this, true);
162164
};
163165

166+
/**
167+
* Create a `PDFStream` for `this` canvas.
168+
*
169+
* @return {PDFStream}
170+
* @api public
171+
*/
172+
173+
Canvas.prototype.pdfStream =
174+
Canvas.prototype.createPDFStream = function(){
175+
return new PDFStream(this);
176+
};
177+
178+
/**
179+
* Create a synchronous `PDFStream` for `this` canvas.
180+
*
181+
* @return {PDFStream}
182+
* @api public
183+
*/
184+
185+
Canvas.prototype.syncPDFStream =
186+
Canvas.prototype.createSyncPDFStream = function(){
187+
return new PDFStream(this, true);
188+
};
189+
164190
/**
165191
* Create a `JPEGStream` for `this` canvas.
166192
*

lib/pdfstream.js

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
'use strict';
2+
3+
/*!
4+
* Canvas - PDFStream
5+
*/
6+
7+
/**
8+
* Module dependencies.
9+
*/
10+
11+
var Stream = require('stream').Stream;
12+
13+
/**
14+
* Initialize a `PDFStream` with the given `canvas`.
15+
*
16+
* "data" events are emitted with `Buffer` chunks, once complete the
17+
* "end" event is emitted. The following example will stream to a file
18+
* named "./my.pdf".
19+
*
20+
* var out = fs.createWriteStream(__dirname + '/my.pdf')
21+
* , stream = canvas.createPDFStream();
22+
*
23+
* stream.pipe(out);
24+
*
25+
* @param {Canvas} canvas
26+
* @param {Boolean} sync
27+
* @api public
28+
*/
29+
30+
var PDFStream = module.exports = function PDFStream(canvas, sync) {
31+
var self = this
32+
, method = sync
33+
? 'streamPDFSync'
34+
: 'streamPDF';
35+
this.sync = sync;
36+
this.canvas = canvas;
37+
this.readable = true;
38+
// TODO: implement async
39+
if ('streamPDF' == method) method = 'streamPDFSync';
40+
process.nextTick(function(){
41+
canvas[method](function(err, chunk, len){
42+
if (err) {
43+
self.emit('error', err);
44+
self.readable = false;
45+
} else if (len) {
46+
self.emit('data', chunk, len);
47+
} else {
48+
self.emit('end');
49+
self.readable = false;
50+
}
51+
});
52+
});
53+
};
54+
55+
/**
56+
* Inherit from `EventEmitter`.
57+
*/
58+
59+
PDFStream.prototype.__proto__ = Stream.prototype;

src/Canvas.cc

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ Canvas::Initialize(Nan::ADDON_REGISTER_FUNCTION_ARGS_TYPE target) {
4040
Local<ObjectTemplate> proto = ctor->PrototypeTemplate();
4141
Nan::SetPrototypeMethod(ctor, "toBuffer", ToBuffer);
4242
Nan::SetPrototypeMethod(ctor, "streamPNGSync", StreamPNGSync);
43+
Nan::SetPrototypeMethod(ctor, "streamPDFSync", StreamPDFSync);
4344
#ifdef HAVE_JPEG
4445
Nan::SetPrototypeMethod(ctor, "streamJPEGSync", StreamJPEGSync);
4546
#endif
@@ -434,6 +435,84 @@ NAN_METHOD(Canvas::StreamPNGSync) {
434435
return;
435436
}
436437

438+
/*
439+
* Canvas::StreamPDF FreeCallback
440+
*/
441+
442+
void stream_pdf_free(char *, void *) {}
443+
444+
/*
445+
* Canvas::StreamPDF callback.
446+
*/
447+
448+
static cairo_status_t
449+
streamPDF(void *c, const uint8_t *data, unsigned len) {
450+
Nan::HandleScope scope;
451+
closure_t *closure = static_cast<closure_t *>(c);
452+
Local<Object> buf = Nan::NewBuffer(const_cast<char *>(reinterpret_cast<const char *>(data)), len, stream_pdf_free, 0).ToLocalChecked();
453+
Local<Value> argv[3] = {
454+
Nan::Null()
455+
, buf
456+
, Nan::New<Number>(len) };
457+
Nan::MakeCallback(Nan::GetCurrentContext()->Global(), closure->fn, 3, argv);
458+
return CAIRO_STATUS_SUCCESS;
459+
}
460+
461+
462+
cairo_status_t canvas_write_to_pdf_stream(cairo_surface_t *surface, cairo_write_func_t write_func, void *closure) {
463+
closure_t *pdf_closure = static_cast<closure_t *>(closure);
464+
size_t whole_chunks = pdf_closure->len / PAGE_SIZE;
465+
size_t remainder = pdf_closure->len - whole_chunks * PAGE_SIZE;
466+
467+
for (size_t i = 0; i < whole_chunks; ++i) {
468+
write_func(pdf_closure, &pdf_closure->data[i * PAGE_SIZE], PAGE_SIZE);
469+
}
470+
471+
if (remainder) {
472+
write_func(pdf_closure, &pdf_closure->data[whole_chunks * PAGE_SIZE], remainder);
473+
}
474+
475+
return CAIRO_STATUS_SUCCESS;
476+
}
477+
478+
/*
479+
* Stream PDF data synchronously.
480+
*/
481+
482+
NAN_METHOD(Canvas::StreamPDFSync) {
483+
if (!info[0]->IsFunction())
484+
return Nan::ThrowTypeError("callback function required");
485+
486+
Canvas *canvas = Nan::ObjectWrap::Unwrap<Canvas>(info.Holder());
487+
488+
if (!canvas->isPDF())
489+
return Nan::ThrowTypeError("wrong canvas type");
490+
491+
cairo_surface_finish(canvas->surface());
492+
493+
closure_t closure;
494+
closure.data = static_cast<closure_t *>(canvas->closure())->data;
495+
closure.len = static_cast<closure_t *>(canvas->closure())->len;
496+
closure.fn = info[0].As<Function>();
497+
498+
Nan::TryCatch try_catch;
499+
500+
cairo_status_t status = canvas_write_to_pdf_stream(canvas->surface(), streamPDF, &closure);
501+
502+
if (try_catch.HasCaught()) {
503+
try_catch.ReThrow();
504+
} else if (status) {
505+
Local<Value> error = Canvas::Error(status);
506+
Nan::Call(closure.fn, Nan::GetCurrentContext()->Global(), 1, &error);
507+
} else {
508+
Local<Value> argv[3] = {
509+
Nan::Null()
510+
, Nan::Null()
511+
, Nan::New<Uint32>(0) };
512+
Nan::Call(closure.fn, Nan::GetCurrentContext()->Global(), 3, argv);
513+
}
514+
}
515+
437516
/*
438517
* Stream JPEG data synchronously.
439518
*/

src/Canvas.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ class Canvas: public Nan::ObjectWrap {
6262
static NAN_SETTER(SetWidth);
6363
static NAN_SETTER(SetHeight);
6464
static NAN_METHOD(StreamPNGSync);
65+
static NAN_METHOD(StreamPDFSync);
6566
static NAN_METHOD(StreamJPEGSync);
6667
static Local<Value> Error(cairo_status_t status);
6768
#if NODE_VERSION_AT_LEAST(0, 6, 0)

test/canvas.test.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -802,6 +802,24 @@ describe('Canvas', function () {
802802
});
803803
});
804804

805+
it('Canvas#createSyncPDFStream()', function (done) {
806+
var canvas = new Canvas(20, 20, 'pdf');
807+
var stream = canvas.createSyncPDFStream();
808+
var firstChunk = true;
809+
stream.on('data', function (chunk) {
810+
if (firstChunk) {
811+
firstChunk = false;
812+
assert.equal('PDF', chunk.slice(1, 4).toString());
813+
}
814+
});
815+
stream.on('end', function () {
816+
done();
817+
});
818+
stream.on('error', function (err) {
819+
done(err);
820+
});
821+
});
822+
805823
it('Canvas#jpegStream()', function (done) {
806824
var canvas = new Canvas(640, 480);
807825
var stream = canvas.jpegStream();

0 commit comments

Comments
 (0)