Permalink
Browse files

Added quick PDF support

need to clean things up, add async support, test images etc. I believe
some guys forked node-canvas to add some kind of image caching
for when an image appears several times within a PDF document, though
that may be out of scope
  • Loading branch information...
1 parent 9b6c6a8 commit a0639078327cf0c490d3da9a8ed908c3cbac42d9 @tj tj committed Apr 10, 2012
Showing with 154 additions and 21 deletions.
  1. +2 −1 .gitignore
  2. +39 −0 examples/multi-page-pdf.js
  3. +22 −0 examples/small-pdf.js
  4. +60 −19 src/Canvas.cc
  5. +14 −1 src/Canvas.h
  6. +16 −0 src/CanvasRenderingContext2d.cc
  7. +1 −0 src/CanvasRenderingContext2d.h
View
@@ -5,7 +5,8 @@ test/images/*.png
examples/*.png
examples/*.jpg
testing
-test.png
+out.png
+out.pdf
.pomo
node_modules
View
@@ -0,0 +1,39 @@
+
+var Canvas = require('../')
+ , canvas = new Canvas(500, 500, 'pdf')
+ , ctx = canvas.getContext('2d')
+ , fs = require('fs');
+
+var x, y;
+
+function reset() {
+ x = 50;
+ y = 80;
+}
+
+function h1(str) {
+ ctx.font = '22px Helvetica';
+ ctx.fillText(str, x, y);
+}
+
+function p(str) {
+ ctx.font = '10px Arial';
+ ctx.fillText(str, x, y += 20);
+}
+
+reset();
+h1('PDF demo');
+p('Multi-page PDF demonstration');
+ctx.nextPage();
+
+reset();
+h1('Page #2');
+p('This is the second page');
+ctx.nextPage();
+
+reset();
+h1('Page #3');
+p('This is the third page');
+
+fs.writeFile('out.pdf', canvas.toBuffer());
+console.log('created out.pdf');
View
@@ -0,0 +1,22 @@
+
+var Canvas = require('../')
+ , canvas = new Canvas(500, 500, 'pdf')
+ , ctx = canvas.getContext('2d')
+ , fs = require('fs');
+
+var y = 80
+ , x = 50;
+
+ctx.font = '22px Helvetica';
+ctx.fillText('node-canvas pdf', x, y);
+
+ctx.font = '10px Arial';
+ctx.fillText('Just a quick example of PDFs with node-canvas', x, y += 20);
+
+ctx.globalAlpha = .5;
+ctx.fillRect(x, y += 20, 10, 10);
+ctx.fillRect(x += 20, y, 10, 10);
+ctx.fillRect(x += 20, y, 10, 10);
+
+fs.writeFile('out.pdf', canvas.toBuffer());
+console.log('created out.pdf');
View
@@ -12,6 +12,7 @@
#include <string.h>
#include <node_buffer.h>
#include <node_version.h>
+#include <cairo-pdf.h>
#include "closure.h"
#ifdef HAVE_JPEG
@@ -53,9 +54,13 @@ Handle<Value>
Canvas::New(const Arguments &args) {
HandleScope scope;
int width = 0, height = 0;
+ canvas_type_t type = CANVAS_TYPE_IMAGE;
if (args[0]->IsNumber()) width = args[0]->Uint32Value();
if (args[1]->IsNumber()) height = args[1]->Uint32Value();
- Canvas *canvas = new Canvas(width, height);
+ if (args[2]->IsString()) type = !strcmp("pdf", *String::AsciiValue(args[2]))
+ ? CANVAS_TYPE_PDF
+ : CANVAS_TYPE_IMAGE;
+ Canvas *canvas = new Canvas(width, height, type);
canvas->Wrap(args.This());
return args.This();
}
@@ -213,6 +218,15 @@ Canvas::ToBuffer(const Arguments &args) {
cairo_status_t status;
Canvas *canvas = ObjectWrap::Unwrap<Canvas>(args.This());
+ // TODO: async / move this out
+ if (canvas->isPDF()) {
+ cairo_surface_finish(canvas->surface());
+ closure_t *closure = (closure_t *) canvas->closure();
+ Buffer *buf = Buffer::New(closure->len);
+ memcpy(Buffer::Data(buf), closure->data, closure->len);
+ return buf->handle_;
+ }
+
// Async
if (args[0]->IsFunction()) {
closure_t *closure = (closure_t *) malloc(sizeof(closure_t));
@@ -353,19 +367,39 @@ Canvas::StreamJPEGSync(const Arguments &args) {
* Initialize cairo surface.
*/
-Canvas::Canvas(int w, int h): ObjectWrap() {
+Canvas::Canvas(int w, int h, canvas_type_t t): ObjectWrap() {
+ type = t;
width = w;
height = h;
- _surface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, w, h);
- V8::AdjustAmountOfExternalAllocatedMemory(4 * width * height);
+ _surface = NULL;
+ _closure = NULL;
+
+ if (CANVAS_TYPE_PDF == t) {
+ _closure = malloc(sizeof(closure_t));
+ assert(_closure);
+ cairo_status_t status = closure_init((closure_t *) _closure, this);
+ assert(status == CAIRO_STATUS_SUCCESS);
+ _surface = cairo_pdf_surface_create_for_stream(toBuffer, _closure, w, h);
+ } else {
+ _surface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, w, h);
+ assert(_surface);
+ V8::AdjustAmountOfExternalAllocatedMemory(4 * w * h);
+ }
}
/*
* Destroy cairo surface.
*/
Canvas::~Canvas() {
- cairo_surface_destroy(_surface);
+ switch (type) {
+ case CANVAS_TYPE_PDF:
+ closure_destroy((closure_t *) _closure);
@c-spencer

c-spencer Apr 12, 2012

Contributor

Still need to destroy the surface, and free _closure.

@tj

tj Apr 12, 2012

Contributor

doh. good catch

+ break;
+ case CANVAS_TYPE_IMAGE:
+ cairo_surface_destroy(_surface);
+ break;
+ }
}
/*
@@ -374,20 +408,27 @@ Canvas::~Canvas() {
void
Canvas::resurface(Handle<Object> canvas) {
- // Re-surface
- int old_width = cairo_image_surface_get_width(_surface);
- int old_height = cairo_image_surface_get_height(_surface);
- cairo_surface_destroy(_surface);
- _surface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, width, height);
- V8::AdjustAmountOfExternalAllocatedMemory(4 * (width * height - old_width * old_height));
-
- // Reset context
- Handle<Value> context = canvas->Get(String::New("context"));
- if (!context->IsUndefined()) {
- Context2d *context2d = ObjectWrap::Unwrap<Context2d>(context->ToObject());
- cairo_t *prev = context2d->context();
- context2d->setContext(cairo_create(surface()));
- cairo_destroy(prev);
+ switch (type) {
+ case CANVAS_TYPE_PDF:
+ cairo_pdf_surface_set_size(_surface, width, height);
+ break;
+ case CANVAS_TYPE_IMAGE:
+ // Re-surface
+ int old_width = cairo_image_surface_get_width(_surface);
+ int old_height = cairo_image_surface_get_height(_surface);
+ cairo_surface_destroy(_surface);
+ _surface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, width, height);
+ V8::AdjustAmountOfExternalAllocatedMemory(4 * (width * height - old_width * old_height));
+
+ // Reset context
+ Handle<Value> context = canvas->Get(String::New("context"));
+ if (!context->IsUndefined()) {
+ Context2d *context2d = ObjectWrap::Unwrap<Context2d>(context->ToObject());
+ cairo_t *prev = context2d->context();
+ context2d->setContext(cairo_create(surface()));
+ cairo_destroy(prev);
+ }
+ break;
}
}
View
@@ -27,13 +27,23 @@ using namespace node;
#endif
/*
+ * Canvas types.
+ */
+
+typedef enum {
+ CANVAS_TYPE_IMAGE,
+ CANVAS_TYPE_PDF
+} canvas_type_t;
+
+/*
* Canvas.
*/
class Canvas: public node::ObjectWrap {
public:
int width;
int height;
+ canvas_type_t type;
static Persistent<FunctionTemplate> constructor;
static void Initialize(Handle<Object> target);
static Handle<Value> New(const Arguments &args);
@@ -59,15 +69,18 @@ class Canvas: public node::ObjectWrap {
static int EIO_AfterToBuffer(eio_req *req);
#endif
+ inline bool isPDF(){ return CANVAS_TYPE_PDF == type; }
inline cairo_surface_t *surface(){ return _surface; }
+ inline void *closure(){ return _closure; }
inline uint8_t *data(){ return cairo_image_surface_get_data(_surface); }
inline int stride(){ return cairo_image_surface_get_stride(_surface); }
- Canvas(int width, int height);
+ Canvas(int width, int height, canvas_type_t type);
void resurface(Handle<Object> canvas);
private:
~Canvas();
cairo_surface_t *_surface;
+ void *_closure;
};
#endif
@@ -62,6 +62,7 @@ Context2d::Initialize(Handle<Object> target) {
Local<ObjectTemplate> proto = constructor->PrototypeTemplate();
NODE_SET_PROTOTYPE_METHOD(constructor, "drawImage", DrawImage);
NODE_SET_PROTOTYPE_METHOD(constructor, "putImageData", PutImageData);
+ NODE_SET_PROTOTYPE_METHOD(constructor, "nextPage", NextPage);
NODE_SET_PROTOTYPE_METHOD(constructor, "save", Save);
NODE_SET_PROTOTYPE_METHOD(constructor, "restore", Restore);
NODE_SET_PROTOTYPE_METHOD(constructor, "rotate", Rotate);
@@ -412,6 +413,21 @@ Context2d::New(const Arguments &args) {
}
/*
+ * Create a new page.
+ */
+
+Handle<Value>
+Context2d::NextPage(const Arguments &args) {
+ HandleScope scope;
+ Context2d *context = ObjectWrap::Unwrap<Context2d>(args.This());
+ if (!context->canvas()->isPDF()) {
+ return ThrowException(Exception::Error(String::New("only PDF canvases support .nextPage()")));
+ }
+ cairo_show_page(context->context());
+ return Undefined();
+}
+
+/*
* Put image data.
*
* - imageData, dx, dy
@@ -57,6 +57,7 @@ class Context2d: public node::ObjectWrap {
static Handle<Value> IsPointInPath(const Arguments &args);
static Handle<Value> BeginPath(const Arguments &args);
static Handle<Value> ClosePath(const Arguments &args);
+ static Handle<Value> NextPage(const Arguments &args);
static Handle<Value> Clip(const Arguments &args);
static Handle<Value> Fill(const Arguments &args);
static Handle<Value> Stroke(const Arguments &args);

6 comments on commit a063907

Contributor

jakeg replied Apr 11, 2012

yes, we do have it working with caching if an image is used multiple times over here: https://github.com/AllYearbooks/node-canvas/commits/master

Contributor

tj replied Apr 11, 2012

awesome, I dont personally have a need for several in one PDF but it might not hurt to have I guess if there's some kind of letter head or something, what was your use-case?

Contributor

jakeg replied Apr 11, 2012

Contributor

tj replied Apr 11, 2012

ah! very nice :) that's cool

Contributor

c-spencer replied Apr 12, 2012

Yeah the Image mime data stuff reduces time to draw, and size of the resulting pdf by a lot when working with jpeg images, as the jpegs don't actually have to be decoded, and duplicates use the same data.

Would you be interested in accepting pull requests for adding mime-data handling?

There's also the use of cairo_show_text rather than cairo_text_path, so that text is selectable in the pdf. That introduces some issues with rotated text appearing badly in some pdf viewers, however. (we added a new flag to force use of cairo_text_path when needed, though that's a pretty ugly solution)

Contributor

tj replied Apr 12, 2012

@c-spencer yeah sure, definitely worth checking those out, I just wanted to get something committed to the repo to get things rolling

Please sign in to comment.