From e54183d8d4ed592289d1997404174af9fa349a65 Mon Sep 17 00:00:00 2001 From: Zach Bjornson Date: Sun, 27 Dec 2015 19:42:28 -0800 Subject: [PATCH] Make toDataURL more spec-compliant: * Use default arguments when undefined arguments are passed. * Throw TypeError on first invalid argument * Accept a number 'encoderOptions' as the quality * Fall through to image/png if an unsupported encoding is requested * Return "data:," if the canvas has no pixels * Lower-case the format before testing for support --- History.md | 5 +++ Readme.md | 1 + lib/canvas.js | 77 ++++++++++++++++++++++--------------------- test/canvas.test.js | 79 ++++++++++++++++++++++++++++++++++++++------- 4 files changed, 114 insertions(+), 48 deletions(-) diff --git a/History.md b/History.md index 0fb9494..0974300 100644 --- a/History.md +++ b/History.md @@ -1,3 +1,8 @@ +future / future +================== + + * Allow optional arguments in `toDataURL` to be `undefined` and improve `toDataURL`'s spec compliance (#690) + 1.3.5 / 2015-12-07 ================== diff --git a/Readme.md b/Readme.md index 87abec2..6bf9631 100644 --- a/Readme.md +++ b/Readme.md @@ -167,6 +167,7 @@ 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', quality, function(err, jpeg){ }); // spec-following; quality from 0 to 1 ``` ### CanvasRenderingContext2d#patternQuality diff --git a/lib/canvas.js b/lib/canvas.js index 9f72fb3..587f59e 100644 --- a/lib/canvas.js +++ b/lib/canvas.js @@ -18,7 +18,8 @@ var canvas = require('./bindings') , JPEGStream = require('./jpegstream') , FontFace = canvas.FontFace , fs = require('fs') - , packageJson = require("../package.json"); + , packageJson = require("../package.json") + , FORMATS = ['image/png', 'image/jpeg']; /** * Export `Canvas` as the module. @@ -186,61 +187,59 @@ Canvas.prototype.createSyncJPEGStream = function(options){ * Return a data url. Pass a function for async support (required for "image/jpeg"). * * @param {String} type, optional, one of "image/png" or "image/jpeg", defaults to "image/png" - * @param {Object} opts, optional, options for jpeg compression (see documentation for Canvas#jpegStream) + * @param {Object|Number} encoderOptions, optional, options for jpeg compression (see documentation for Canvas#jpegStream) or the JPEG encoding quality from 0 to 1. * @param {Function} fn, optional, callback for asynchronous operation. Required for type "image/jpeg". * @return {String} data URL if synchronous (callback omitted) * @api public */ Canvas.prototype.toDataURL = function(a1, a2, a3){ - // valid arg patterns (args -> type, opts, fn): + // valid arg patterns (args -> [type, opts, fn]): // [] -> ['image/png', null, null] + // [qual] -> ['image/png', null, null] + // [undefined] -> ['image/png', null, null] // ['image/png'] -> ['image/png', null, null] + // ['image/png', qual] -> ['image/png', null, null] // [fn] -> ['image/png', null, fn] // [type, fn] -> [type, null, fn] + // [undefined, fn] -> ['image/png', null, fn] + // ['image/png', qual, fn] -> ['image/png', null, fn] // ['image/jpeg', fn] -> ['image/jpeg', null, fn] // ['image/jpeg', opts, fn] -> ['image/jpeg', opts, fn] + // ['image/jpeg', qual, fn] -> ['image/jpeg', {quality: qual}, fn] + // ['image/jpeg', undefined, fn] -> ['image/jpeg', null, fn] - var type; + if (this.width === 0 || this.height === 0) { + // Per spec, if the bitmap has no pixels, return this string: + return "data:,"; + } + + var type = 'image/png'; var opts = {}; var fn; - switch (arguments.length) { - case 0: - type = 'image/png'; - break; - case 1: - if ('image/png' === a1) { - type = a1; - } else if ('function' === typeof a1) { - type = 'image/png'; - fn = a1; - } else if ('image/jpeg' === a1) { - throw new Error('type "image/jpeg" only supports asynchronous operation'); - } else { - throw new Error('invalid arguments'); - } - break; - case 2: - if (('image/png' === a1 || 'image/jpeg' === a1) && 'function' === typeof a2) { - type = a1; - fn = a2; - } else if ('image/jpeg' === a1 && 'object' === typeof a2) { - throw new Error('type "image/jpeg" only supports asynchronous operation'); - } else if ('image/png' === a1 && 'object' === typeof a2) { - throw new Error('type "image/png" does not accept an options object'); - } else { - throw new Error('invalid arguments'); - } - break; - case 3: - if ('image/jpeg' === a1 && 'object' === typeof a2 && 'function' === typeof a3) { - type = a1; + if ('function' === typeof a1) { + fn = a1; + } else { + if ('string' === typeof a1 && FORMATS.indexOf(a1.toLowerCase()) !== -1) { + type = a1.toLowerCase(); + } + + if ('function' === typeof a2) { + fn = a2; + } else { + if ('object' === typeof a2) { opts = a2; + } else if ('number' === typeof a2) { + opts = {quality: Math.min(0, Math.max(1, a2)) * 100}; + } + + if ('function' === typeof a3) { fn = a3; - } else { - throw new Error('invalid arguments'); + } else if (undefined !== a3) { + throw new TypeError(typeof a3 + ' is not a function'); } + } } if ('image/png' === type) { @@ -254,6 +253,10 @@ Canvas.prototype.toDataURL = function(a1, a2, a3){ } } else if ('image/jpeg' === type) { + if (undefined === fn) { + throw new Error('Missing required callback function for format "image/jpeg"'); + } + var stream = this.jpegStream(opts); // note that jpegStream is synchronous var buffers = []; diff --git a/test/canvas.test.js b/test/canvas.test.js index 64c31c5..6f1ea83 100644 --- a/test/canvas.test.js +++ b/test/canvas.test.js @@ -385,17 +385,33 @@ describe('Canvas', function () { assert.ok(0 == canvas.toDataURL().indexOf('data:image/png;base64,')); }); + it('toDataURL(0.5) works and defaults to PNG', function () { + assert.ok(0 == canvas.toDataURL(0.5).indexOf('data:image/png;base64,')); + }); + + it('toDataURL(undefined) works and defaults to PNG', function () { + assert.ok(0 == canvas.toDataURL(undefined).indexOf('data:image/png;base64,')); + }); + it('toDataURL("image/png") works', function () { assert.ok(0 == canvas.toDataURL('image/png').indexOf('data:image/png;base64,')); }); + it('toDataURL("image/png", 0.5) works', function () { + assert.ok(0 == canvas.toDataURL('image/png').indexOf('data:image/png;base64,')); + }); + + it('toDataURL("iMaGe/PNg") works', function () { + assert.ok(0 == canvas.toDataURL('iMaGe/PNg').indexOf('data:image/png;base64,')); + }); + it('toDataURL("image/jpeg") throws', function () { assert.throws( function () { canvas.toDataURL('image/jpeg'); }, function (err) { - return err.message === 'type "image/jpeg" only supports asynchronous operation'; + return err.message === 'Missing required callback function for format "image/jpeg"'; } ); }); @@ -408,6 +424,22 @@ describe('Canvas', function () { }); }); + it('toDataURL(0.5, function (err, str) {...}) works and defaults to PNG', function (done) { + new Canvas(200,200).toDataURL(0.5, function(err, str){ + assert.ifError(err); + assert.ok(0 === str.indexOf('data:image/png;base64,')); + done(); + }); + }); + + it('toDataURL(undefined, function (err, str) {...}) works and defaults to PNG', function (done) { + new Canvas(200,200).toDataURL(undefined, function(err, str){ + assert.ifError(err); + assert.ok(0 === str.indexOf('data:image/png;base64,')); + done(); + }); + }); + it('toDataURL("image/png", function (err, str) {...}) works', function (done) { new Canvas(200,200).toDataURL('image/png', function(err, str){ assert.ifError(err); @@ -416,15 +448,16 @@ describe('Canvas', function () { }); }); - it('toDataURL("image/png", {}) throws', function () { - assert.throws( - function () { - canvas.toDataURL('image/png', {}); - }, - function (err) { - return err.message === 'type "image/png" does not accept an options object'; - } - ); + it('toDataURL("image/png", 0.5, function (err, str) {...}) works', function (done) { + new Canvas(200,200).toDataURL('image/png', 0.5, function(err, str){ + assert.ifError(err); + assert.ok(0 === str.indexOf('data:image/png;base64,')); + done(); + }); + }); + + it('toDataURL("image/png", {}) works', function () { + assert.ok(0 == canvas.toDataURL('image/png', {}).indexOf('data:image/png;base64,')); }); it('toDataURL("image/jpeg", {}) throws', function () { @@ -433,7 +466,7 @@ describe('Canvas', function () { canvas.toDataURL('image/jpeg', {}); }, function (err) { - return err.message === 'type "image/jpeg" only supports asynchronous operation'; + return err.message === 'Missing required callback function for format "image/jpeg"'; } ); }); @@ -446,6 +479,30 @@ describe('Canvas', function () { }); }); + it('toDataURL("iMAge/JPEG", function (err, str) {...}) works', function (done) { + new Canvas(200,200).toDataURL('iMAge/JPEG', function(err, str){ + assert.ifError(err); + assert.ok(0 === str.indexOf('data:image/jpeg;base64,')); + done(); + }); + }); + + it('toDataURL("image/jpeg", undefined, function (err, str) {...}) works', function (done) { + new Canvas(200,200).toDataURL('image/jpeg', undefined, function(err, str){ + assert.ifError(err); + assert.ok(0 === str.indexOf('data:image/jpeg;base64,')); + done(); + }); + }); + + it('toDataURL("image/jpeg", 0.5, function (err, str) {...}) works', function (done) { + new Canvas(200,200).toDataURL('image/jpeg', 0.5, function(err, str){ + assert.ifError(err); + assert.ok(0 === str.indexOf('data:image/jpeg;base64,')); + done(); + }); + }); + it('toDataURL("image/jpeg", opts, function (err, str) {...}) works', function (done) { new Canvas(200,200).toDataURL('image/jpeg', {quality: 100}, function(err, str){ assert.ifError(err);