diff --git a/.travis.yml b/.travis.yml index e7e3fca5f..1396a6391 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,7 +4,6 @@ install: node_js: - '10' - '8' - - '6' addons: apt: sources: diff --git a/CHANGELOG.md b/CHANGELOG.md index 67b20faf0..86c14cbcc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,7 +38,7 @@ canvas.createJPEGStream() // new ``` ### Breaking - * Drop support for Node.js <6.x + * Drop support for Node.js <8.x * Remove sync stream functions (bc53059). Note that most streams are still synchronous (run in the main thread); this change just removed `syncPNGStream` and `syncJPEGStream`. @@ -119,6 +119,7 @@ canvas.createJPEGStream() // new * Throw error if calling jpegStream when canvas was not built with JPEG support * Emit error if trying to load GIF, SVG or JPEG image when canvas was not built with support for that format + * Support for WebP Image loading 1.6.x (unreleased) ================== diff --git a/Readme.md b/Readme.md index 7e73a5419..c5d49c03c 100644 --- a/Readme.md +++ b/Readme.md @@ -33,7 +33,7 @@ $ npm install canvas By default, binaries for macOS, Linux and Windows will be downloaded. If you want to build from source, use `npm install --build-from-source`. -Currently the minimum version of node required is __6.0.0__ +Currently the minimum version of node required is __8.0.0__ ### Compiling diff --git a/appveyor.yml b/appveyor.yml index fe7d2e13c..166c6c6ea 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,7 +1,7 @@ version: 0.0.{build} environment: matrix: - - nodejs_version: "6" + - nodejs_version: "8" image: - Visual Studio 2013 - Visual Studio 2015 diff --git a/lib/image.js b/lib/image.js index ff84f2b24..cadf2559a 100644 --- a/lib/image.js +++ b/lib/image.js @@ -1,26 +1,89 @@ -'use strict'; +const fs = require('fs') +const get = require('simple-get') +const webp = require('@cwasm/webp') -/*! - * Canvas - Image - * Copyright (c) 2010 LearnBoost - * MIT Licensed +const bindings = require('./bindings') + +const kOriginalSource = Symbol('original-source') + +/** @typedef {Object} Image */ +const Image = module.exports = bindings.Image + +const proto = Image.prototype +const _getSource = proto.getSource +const _setSource = proto.setSource + +delete proto.getSource +delete proto.setSource + +/** + * @param {Image} image + * @param {Error} err */ +function signalError (image, err) { + if (typeof image.onerror === 'function') return image.onerror(err) + + throw err +} /** - * Module dependencies. + * @param {Image} image + * @param {string} value */ +function loadDataUrl (image, value) { + const firstComma = value.indexOf(',') + const isBase64 = value.lastIndexOf('base64', firstComma) !== -1 + const source = value.slice(firstComma + 1) -const bindings = require('./bindings') -const Image = module.exports = bindings.Image -const http = require("http") -const https = require("https") + let data + try { + data = Buffer.from(source, isBase64 ? 'base64' : 'utf8') + } catch (err) { + return signalError(image, err) + } -const proto = Image.prototype; -const _getSource = proto.getSource; -const _setSource = proto.setSource; + return setSource(image, data, value) +} -delete proto.getSource; -delete proto.setSource; +/** + * @param {Image} image + * @param {string} value + */ +function loadHttpUrl (image, value) { + return get.concat(value, (err, res, data) => { + if (err) return signalError(image, err) + + if (res.statusCode < 200 || res.statusCode >= 300) { + return signalError(image, new Error(`Server responded with ${res.statusCode}`)) + } + + return setSource(image, data, value) + }) +} + +/** + * @param {Image} image + * @param {string} value + */ +function loadFileUrl (image, value) { + fs.readFile(value.replace('file://', ''), (err, data) => { + if (err) return signalError(image, err) + + setSource(image, data, value) + }) +} + +/** + * @param {Image} image + * @param {string} value + */ +function loadLocalFile (image, value) { + fs.readFile(value, (err, data) => { + if (err) return signalError(image, err) + + setSource(image, data, value) + }) +} Object.defineProperty(Image.prototype, 'src', { /** @@ -33,49 +96,52 @@ Object.defineProperty(Image.prototype, 'src', { * @param {String|Buffer} val filename, buffer, data URI, URL * @api public */ - set(val) { - if (typeof val === 'string') { - if (/^\s*data:/.test(val)) { // data: URI - const commaI = val.indexOf(',') - // 'base64' must come before the comma - const isBase64 = val.lastIndexOf('base64', commaI) !== -1 - const content = val.slice(commaI + 1) - setSource(this, Buffer.from(content, isBase64 ? 'base64' : 'utf8'), val); - } else if (/^\s*https?:\/\//.test(val)) { // remote URL - const onerror = err => { - if (typeof this.onerror === 'function') { - this.onerror(err) - } else { - throw err - } - } - - const type = /^\s*https:\/\//.test(val) ? https : http - type.get(val, res => { - if (res.statusCode !== 200) { - return onerror(new Error(`Server responded with ${res.statusCode}`)) - } - const buffers = [] - res.on('data', buffer => buffers.push(buffer)) - res.on('end', () => { - setSource(this, Buffer.concat(buffers)); - }) - }).on('error', onerror) - } else { // local file path assumed - setSource(this, val); - } - } else if (Buffer.isBuffer(val)) { - setSource(this, val); + set (val) { + // Clear current source + clearSource(this) + + // Allow directly setting a buffer + if (Buffer.isBuffer(val)) { + this[kOriginalSource] = val + Promise.resolve().then(() => setSource(this, val, val)) + return + } + + // Coerce into string and strip leading & trailing whitespace + val = String(val).trim() + this[kOriginalSource] = val + + // Clear image + if (val === '') { + return + } + + // Data URL + if (/^data:/.test(val)) { + return loadDataUrl(this, val) + } + + // HTTP(S) URL + if (/^https?:\/\//.test(val)) { + return loadHttpUrl(this, val) } + + // File URL + if (/^file:\/\//.test(val)) { + return loadFileUrl(this, val) + } + + // Assume local file path + loadLocalFile(this, val) }, - get() { - // TODO https://github.com/Automattic/node-canvas/issues/118 - return getSource(this); + /** @returns {String|Buffer} */ + get () { + return this[kOriginalSource] || '' }, configurable: true -}); +}) /** * Inspect image. @@ -86,19 +152,35 @@ Object.defineProperty(Image.prototype, 'src', { * @api public */ -Image.prototype.inspect = function(){ - return '[Image' - + (this.complete ? ':' + this.width + 'x' + this.height : '') - + (this.src ? ' ' + this.src : '') - + (this.complete ? ' complete' : '') - + ']'; -}; +Image.prototype.inspect = function () { + return '[Image' + + (this.complete ? ':' + this.width + 'x' + this.height : '') + + (this.src ? ' ' + this.src : '') + + (this.complete ? ' complete' : '') + + ']' +} + +/** + * @param {Buffer} source + */ +function isWebP (source) { + return (source.toString('ascii', 0, 4) === 'RIFF' && source.toString('ascii', 8, 12) === 'WEBP') +} -function getSource(img){ - return img._originalSource || _getSource.call(img); +/** + * @param {Image} image + * @param {Buffer} source + * @param {Buffer|string} originalSource + */ +function setSource (image, source, originalSource) { + if (image[kOriginalSource] === originalSource) { + _setSource.call(image, isWebP(source) ? webp.decode(source) : source) + } } -function setSource(img, src, origSrc){ - _setSource.call(img, src); - img._originalSource = origSrc; +/** + * @param {Image} image + */ +function clearSource (image) { + _setSource.call(image, null) } diff --git a/package.json b/package.json index ddb7e012a..5285ad8b7 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "scripts": { "prebenchmark": "node-gyp build", "benchmark": "node benchmarks/run.js", - "pretest": "standard examples/*.js test/server.js test/public/*.js benchmark/run.js util/has_lib.js browser.js index.js && node-gyp build", + "pretest": "standard examples/*.js test/server.js test/public/*.js test/image.test.js benchmark/run.js util/has_lib.js browser.js index.js && node-gyp build", "test": "mocha test/*.test.js", "pretest-server": "node-gyp build", "test-server": "node test/server.js", @@ -39,8 +39,10 @@ "package_name": "{module_name}-v{version}-{node_abi}-{platform}-{libc}-{arch}.tar.gz" }, "dependencies": { + "@cwasm/webp": "^0.1.0", "nan": "^2.11.1", - "node-pre-gyp": "^0.11.0" + "node-pre-gyp": "^0.11.0", + "simple-get": "^3.0.3" }, "devDependencies": { "assert-rejects": "^1.0.0", @@ -49,7 +51,7 @@ "standard": "^12.0.1" }, "engines": { - "node": ">=6" + "node": ">=8" }, "license": "MIT" } diff --git a/src/Image.cc b/src/Image.cc index a5d30cc56..c731b7eae 100644 --- a/src/Image.cc +++ b/src/Image.cc @@ -234,6 +234,11 @@ NAN_METHOD(Image::SetSource){ // Clear errno in case some unrelated previous syscall failed errno = 0; + // Just clear the data + if (value->IsNull()) { + return; + } + // url string if (value->IsString()) { Nan::Utf8String src(value); @@ -245,6 +250,16 @@ NAN_METHOD(Image::SetSource){ uint8_t *buf = (uint8_t *) Buffer::Data(value->ToObject()); unsigned len = Buffer::Length(value->ToObject()); status = img->loadFromBuffer(buf, len); + // ImageData + } else if (value->IsObject()) { + auto imageData = value->ToObject(); + auto width = imageData->Get(Nan::New("width").ToLocalChecked())->Int32Value(); + auto height = imageData->Get(Nan::New("height").ToLocalChecked())->Int32Value(); + Nan::TypedArrayContents data(imageData->Get(Nan::New("data").ToLocalChecked())); + + assert((width * height * 4) == data.length()); + + status = img->loadFromImageData(*data, width, height); } if (status) { @@ -270,6 +285,37 @@ NAN_METHOD(Image::SetSource){ } } +cairo_status_t +Image::loadFromImageData(uint8_t *data, uint32_t width, uint32_t height) { + _surface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, width, height); + auto status = cairo_surface_status(_surface); + + if (status != CAIRO_STATUS_SUCCESS) return status; + + auto stride = cairo_image_surface_get_stride(_surface); + auto target = cairo_image_surface_get_data(_surface); + + for (auto y = 0; y < height; ++y) { + auto pixel = (target + (stride * y)); + + for (auto x = 0; x < width; ++x) { + uint8_t r = *(data++); + uint8_t g = *(data++); + uint8_t b = *(data++); + uint8_t a = *(data++); + + *(pixel++) = b; + *(pixel++) = g; + *(pixel++) = r; + *(pixel++) = a; + } + } + + cairo_surface_mark_dirty(_surface); + + return CAIRO_STATUS_SUCCESS; +} + /* * Load image data from `buf` by sniffing * the bytes to determine format. diff --git a/src/Image.h b/src/Image.h index 6c0dd49c9..f8fcd41ba 100644 --- a/src/Image.h +++ b/src/Image.h @@ -67,6 +67,7 @@ class Image: public Nan::ObjectWrap { cairo_surface_t *surface(); cairo_status_t loadSurface(); cairo_status_t loadFromBuffer(uint8_t *buf, unsigned len); + cairo_status_t loadFromImageData(uint8_t *data, uint32_t width, uint32_t height); cairo_status_t loadPNGFromBuffer(uint8_t *buf); cairo_status_t loadPNG(); void clearData(); diff --git a/test/fixtures/test.webp b/test/fixtures/test.webp new file mode 100644 index 000000000..3e4bca1d8 Binary files /dev/null and b/test/fixtures/test.webp differ diff --git a/test/image.test.js b/test/image.test.js index 1162f3cac..8c866968e 100644 --- a/test/image.test.js +++ b/test/image.test.js @@ -13,27 +13,39 @@ const assert = require('assert') const assertRejects = require('assert-rejects') const fs = require('fs') -const png_checkers = `${__dirname}/fixtures/checkers.png` -const png_clock = `${__dirname}/fixtures/clock.png` -const jpg_chrome = `${__dirname}/fixtures/chrome.jpg` -const jpg_face = `${__dirname}/fixtures/face.jpeg` -const svg_tree = `${__dirname}/fixtures/tree.svg` +const jpgChrome = `${__dirname}/fixtures/chrome.jpg` +const jpgCrash = `${__dirname}/fixtures/159-crash1.jpg` +const jpgFace = `${__dirname}/fixtures/face.jpeg` +const pngCheckers = `${__dirname}/fixtures/checkers.png` +const pngClock = `${__dirname}/fixtures/clock.png` +const svgTree = `${__dirname}/fixtures/tree.svg` +const webpTest = `${__dirname}/fixtures/test.webp` + +function waitFor (fn) { + return new Promise((resolve) => { + const id = setInterval(() => fn() && resolve(clearInterval(id)), 20) + }) +} + +function sleep (ms) { + return new Promise((resolve) => setTimeout(resolve, ms)) +} describe('Image', function () { it('Prototype and ctor are well-shaped, don\'t hit asserts on accessors (GH-803)', function () { - var img = new Image(); - assert.throws(function () { Image.prototype.width; }, /incompatible receiver/); - assert(!img.hasOwnProperty('width')); - assert('width' in img); - assert(Image.prototype.hasOwnProperty('width')); - }); + const img = new Image() + assert.throws(() => Image.prototype.width, /incompatible receiver/) + assert(!img.hasOwnProperty('width')) + assert('width' in img) + assert(Image.prototype.hasOwnProperty('width')) + }) it('loads JPEG image', function () { - return loadImage(jpg_face).then((img) => { + return loadImage(jpgFace).then((img) => { assert.strictEqual(img.onerror, null) assert.strictEqual(img.onload, null) - assert.strictEqual(img.src, jpg_face) + assert.strictEqual(img.src, jpgFace) assert.strictEqual(img.width, 485) assert.strictEqual(img.height, 401) assert.strictEqual(img.complete, true) @@ -41,7 +53,7 @@ describe('Image', function () { }) it('loads JPEG data URL', function () { - const base64Encoded = fs.readFileSync(jpg_face, 'base64') + const base64Encoded = fs.readFileSync(jpgFace, 'base64') const dataURL = `data:image/png;base64,${base64Encoded}` return loadImage(dataURL).then((img) => { @@ -56,11 +68,11 @@ describe('Image', function () { }) it('loads PNG image', function () { - return loadImage(png_clock).then((img) => { + return loadImage(pngClock).then((img) => { assert.strictEqual(img.onerror, null) assert.strictEqual(img.onload, null) - assert.strictEqual(img.src, png_clock) + assert.strictEqual(img.src, pngClock) assert.strictEqual(img.width, 320) assert.strictEqual(img.height, 320) assert.strictEqual(img.complete, true) @@ -68,7 +80,7 @@ describe('Image', function () { }) it('loads PNG data URL', function () { - const base64Encoded = fs.readFileSync(png_clock, 'base64') + const base64Encoded = fs.readFileSync(pngClock, 'base64') const dataURL = `data:image/png;base64,${base64Encoded}` return loadImage(dataURL).then((img) => { @@ -83,9 +95,9 @@ describe('Image', function () { }) it('loads SVG data URL base64', function () { - const base64Enc = fs.readFileSync(svg_tree, 'base64') + const base64Enc = fs.readFileSync(svgTree, 'base64') const dataURL = `data:image/svg+xml;base64,${base64Enc}` - return loadImage(dataURL).then((img) => { + return loadImage(dataURL).then((img) => { assert.strictEqual(img.onerror, null) assert.strictEqual(img.onload, null) assert.strictEqual(img.width, 200) @@ -95,9 +107,9 @@ describe('Image', function () { }) it('loads SVG data URL utf8', function () { - const utf8Encoded = fs.readFileSync(svg_tree, 'utf8') + const utf8Encoded = fs.readFileSync(svgTree, 'utf8') const dataURL = `data:image/svg+xml;utf8,${utf8Encoded}` - return loadImage(dataURL).then((img) => { + return loadImage(dataURL).then((img) => { assert.strictEqual(img.onerror, null) assert.strictEqual(img.onload, null) assert.strictEqual(img.width, 200) @@ -106,163 +118,207 @@ describe('Image', function () { }) }) - it('calls Image#onload multiple times', function () { - return loadImage(png_clock).then((img) => { - let onloadCalled = 0 - - img.onload = () => { onloadCalled += 1 } + it('loads WebP image', function () { + return loadImage(webpTest).then((img) => { + assert.strictEqual(img.onerror, null) + assert.strictEqual(img.onload, null) - img.src = png_checkers - assert.strictEqual(img.src, png_checkers) + assert.strictEqual(img.src, webpTest) + assert.strictEqual(img.width, 128) + assert.strictEqual(img.height, 128) assert.strictEqual(img.complete, true) - assert.strictEqual(img.width, 2) - assert.strictEqual(img.height, 2) - - img.src = png_clock - assert.strictEqual(img.src, png_clock) - assert.strictEqual(true, img.complete) - assert.strictEqual(320, img.width) - assert.strictEqual(320, img.height) - - assert.strictEqual(onloadCalled, 2) - - onloadCalled = 0 - img.onload = () => { onloadCalled += 1 } + }) + }) - img.src = png_clock - assert.strictEqual(onloadCalled, 1) + it('calls Image#onload multiple times', function () { + return loadImage(pngClock).then((img) => { + return Promise.resolve() + .then(() => { + return new Promise((resolve, reject) => { + img.onerror = reject + img.onload = resolve + img.src = pngCheckers + }) + }) + .then(() => { + assert.strictEqual(img.src, pngCheckers) + assert.strictEqual(img.complete, true) + assert.strictEqual(img.width, 2) + assert.strictEqual(img.height, 2) + }).then(() => { + return new Promise((resolve, reject) => { + img.onerror = reject + img.onload = resolve + img.src = pngClock + }) + }) + .then(() => { + assert.strictEqual(img.src, pngClock) + assert.strictEqual(img.complete, true) + assert.strictEqual(img.width, 320) + assert.strictEqual(img.height, 320) + }).then(() => { + return new Promise((resolve, reject) => { + img.onerror = reject + img.onload = resolve + img.src = pngClock + }) + }) }) }) it('handles errors', function () { - return assertRejects(loadImage(`${png_clock}fail`), Error) + return assertRejects(loadImage(`${pngClock}fail`), Error) }) - it('returns a nice, coded error for fopen failures', function (done) { - const img = new Image() - img.onerror = err => { - assert.equal(err.code, 'ENOENT') - assert.equal(err.path, 'path/to/nothing') - assert.equal(err.syscall, 'fopen') - done() - } - img.src = 'path/to/nothing' + it('returns a nice, coded error for fopen failures', async () => { + await assertRejects(loadImage('path/to/nothing'), (err) => { + assert.strictEqual(err.code, 'ENOENT') + assert.strictEqual(err.path, 'path/to/nothing') + assert.strictEqual(err.syscall, 'open') + return true + }) }) - it('captures errors from libjpeg', function (done) { - const img = new Image() - img.onerror = err => { - assert.equal(err.message, "JPEG datastream contains no image") - done() - } - img.src = `${__dirname}/fixtures/159-crash1.jpg` + it('captures errors from libjpeg', async () => { + await assertRejects(loadImage(jpgCrash), (err) => { + assert.strictEqual(err.message, 'JPEG datastream contains no image') + return true + }) }) - it('calls Image#onerror multiple times', function () { - return loadImage(png_clock).then((img) => { - let onloadCalled = 0 - let onerrorCalled = 0 - - img.onload = () => { onloadCalled += 1 } - img.onerror = () => { onerrorCalled += 1 } + it('calls Image#onerror multiple times', async () => { + const img = await loadImage(pngClock) - img.src = `${png_clock}s1` - assert.strictEqual(img.src, `${png_clock}s1`) + await new Promise((resolve, reject) => { + img.onload = () => reject(new Error('onload unexpectedly called')) + img.onerror = () => resolve() + img.src = `${pngClock}s1` + assert.strictEqual(img.src, `${pngClock}s1`) + }) - img.src = `${png_clock}s2` - assert.strictEqual(img.src, `${png_clock}s2`) + await new Promise((resolve, reject) => { + img.onload = () => reject(new Error('onload unexpectedly called')) + img.onerror = () => resolve() + img.src = `${pngClock}s2` + assert.strictEqual(img.src, `${pngClock}s2`) + }) - assert.strictEqual(onerrorCalled, 2) + await new Promise((resolve, reject) => { + img.onload = () => reject(new Error('onload unexpectedly called')) + img.onerror = () => resolve() + img.src = `${pngClock}s3` + assert.strictEqual(img.src, `${pngClock}s3`) + }) + }) - onerrorCalled = 0 - img.onerror = () => { onerrorCalled += 1 } + it('Image#{width,height}', async () => { + const img = await loadImage(pngClock) - img.src = `${png_clock}s3` - assert.strictEqual(img.src, `${png_clock}s3`) + img.src = '' + assert.strictEqual(img.width, 0) + assert.strictEqual(img.height, 0) - assert.strictEqual(onerrorCalled, 1) - assert.strictEqual(onloadCalled, 0) - }) - }) + img.src = pngClock + assert.strictEqual(img.width, 0) + assert.strictEqual(img.height, 0) - it('Image#{width,height}', function () { - return loadImage(png_clock).then((img) => { - img.src = '' - assert.strictEqual(img.width, 0) - assert.strictEqual(img.height, 0) + await new Promise((resolve) => { img.onload = resolve }) - img.src = png_clock - assert.strictEqual(img.width, 320) - assert.strictEqual(img.height, 320) - }) + assert.strictEqual(img.width, 320) + assert.strictEqual(img.height, 320) }) - it('Image#src set empty buffer', function () { - return loadImage(png_clock).then((img) => { - let onerrorCalled = 0 + it('Image#src set empty buffer', async () => { + const img = new Image() - img.onerror = () => { onerrorCalled += 1 } + img.src = Buffer.alloc(0) - img.src = Buffer.alloc(0) - assert.strictEqual(img.width, 0) - assert.strictEqual(img.height, 0) + assert.strictEqual(img.width, 0) + assert.strictEqual(img.height, 0) - assert.strictEqual(onerrorCalled, 1) + await new Promise((resolve, reject) => { + img.onerror = () => resolve() + img.onload = () => reject(new Error('onload unexpectedly called')) }) }) - it('should unbind Image#onload', function() { - return loadImage(png_clock).then((img) => { - let onloadCalled = 0 + it('should unbind Image#onload', async () => { + const img = new Image() - img.onload = () => { onloadCalled += 1 } + let onloadCalled = 0 + img.onload = () => { onloadCalled += 1 } - img.src = png_checkers - assert.strictEqual(img.src, png_checkers) - assert.strictEqual(img.complete, true) - assert.strictEqual(img.width, 2) - assert.strictEqual(img.height, 2) + img.src = pngCheckers + assert.strictEqual(img.src, pngCheckers) + assert.strictEqual(img.complete, false) + assert.strictEqual(img.width, 0) + assert.strictEqual(img.height, 0) - assert.strictEqual(onloadCalled, 1) + await waitFor(() => onloadCalled === 1) - onloadCalled = 0 - img.onload = null + assert.strictEqual(onloadCalled, 1) + assert.strictEqual(img.complete, true) + assert.strictEqual(img.width, 2) + assert.strictEqual(img.height, 2) - img.src = png_clock - assert.strictEqual(img.src, png_clock) - assert.strictEqual(img.complete, true) - assert.strictEqual(img.width, 320) - assert.strictEqual(img.height, 320) + onloadCalled = 0 + img.onload = null - assert.strictEqual(onloadCalled, 0) - }) + img.src = pngClock + assert.strictEqual(img.src, pngClock) + assert.strictEqual(img.complete, false) + assert.strictEqual(img.width, 0) + assert.strictEqual(img.height, 0) + + await sleep(20) + + assert.strictEqual(onloadCalled, 0) + assert.strictEqual(img.complete, true) + assert.strictEqual(img.width, 320) + assert.strictEqual(img.height, 320) }) - it('should unbind Image#onerror', function() { - return loadImage(png_clock).then((img) => { - let onloadCalled = 0 - let onerrorCalled = 0 + it('should unbind Image#onerror', async () => { + const img = new Image() + + let onloadCalled = 0 + let onerrorCalled = 0 - img.onload = () => { onloadCalled += 1 } - img.onerror = () => { onerrorCalled += 1 } + img.onload = () => { onloadCalled += 1 } + img.onerror = () => { onerrorCalled += 1 } - img.src = `${png_clock}s1` - assert.strictEqual(img.src, `${png_clock}s1`) + img.src = `${pngClock}s1` + assert.strictEqual(img.src, `${pngClock}s1`) - img.src = `${png_clock}s2` - assert.strictEqual(img.src, `${png_clock}s2`) + await waitFor(() => onerrorCalled === 1) - assert.strictEqual(onerrorCalled, 2) + img.src = `${pngClock}s2` + assert.strictEqual(img.src, `${pngClock}s2`) - onerrorCalled = 0 - img.onerror = null + await waitFor(() => onerrorCalled === 2) - img.src = `${png_clock}s3` - assert.strictEqual(img.src, `${png_clock}s3`) + onerrorCalled = 0 + img.onerror = null + + let globalErrorEmitted = false + + const originalEmit = process.emit + process.emit = () => (globalErrorEmitted = true) + + try { + img.src = `${pngClock}s3` + assert.strictEqual(img.src, `${pngClock}s3`) + + await sleep(20) assert.strictEqual(onloadCalled, 0) assert.strictEqual(onerrorCalled, 0) - }) + } finally { + process.emit = originalEmit + } + + assert.strictEqual(globalErrorEmitted, true) }) it('does not crash on invalid images', function () { @@ -274,7 +330,7 @@ describe('Image', function () { return copy } - const source = fs.readFileSync(jpg_chrome) + const source = fs.readFileSync(jpgChrome) const corruptSources = [ withIncreasedByte(source, 0), @@ -295,9 +351,22 @@ describe('Image', function () { }) it('does not contain `source` property', function () { - var keys = Reflect.ownKeys(Image.prototype); - assert.ok(!keys.includes('source')); - assert.ok(!keys.includes('getSource')); - assert.ok(!keys.includes('setSource')); - }); + const keys = Reflect.ownKeys(Image.prototype) + assert.ok(!keys.includes('source')) + assert.ok(!keys.includes('getSource')) + assert.ok(!keys.includes('setSource')) + }) + + it('aborts in-progress loads', async () => { + const img = new Image() + + let onloadCalled = 0 + img.onload = () => onloadCalled++ + + img.src = pngCheckers + img.src = jpgChrome + img.src = pngClock + + await waitFor(() => onloadCalled === 1) + }) }) diff --git a/test/public/tests.js b/test/public/tests.js index 899986158..c4df5d283 100644 --- a/test/public/tests.js +++ b/test/public/tests.js @@ -1796,6 +1796,16 @@ tests['drawImage(img) svg with scaling from ctx'] = function (ctx, done) { img.src = imageSrc('tree.svg') } +tests['drawImage(img) WebP'] = function (ctx, done) { + var img = new Image() + img.onload = function () { + ctx.drawImage(img, 0, 0) + done(null) + } + img.onerror = done + img.src = imageSrc('test.webp') +} + tests['drawImage(img,x,y)'] = function (ctx, done) { var img = new Image() img.onload = function () {