Skip to content

Commit aeb231e

Browse files
committed
Add support for WebP decoding
1 parent 586b395 commit aeb231e

File tree

8 files changed

+198
-66
lines changed

8 files changed

+198
-66
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ canvas.createJPEGStream() // new
119119
* Throw error if calling jpegStream when canvas was not built with JPEG support
120120
* Emit error if trying to load GIF, SVG or JPEG image when canvas was not built
121121
with support for that format
122+
* Support for WebP Image loading
122123

123124
1.6.x (unreleased)
124125
==================

lib/image.js

Lines changed: 127 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,87 @@
1-
'use strict';
1+
const fs = require('fs')
2+
const get = require('simple-get')
3+
const webp = require('@cwasm/webp')
24

3-
/*!
4-
* Canvas - Image
5-
* Copyright (c) 2010 LearnBoost <[email protected]>
6-
* MIT Licensed
5+
const bindings = require('./bindings')
6+
7+
/** @typedef {Object} Image */
8+
const Image = module.exports = bindings.Image
9+
10+
const proto = Image.prototype
11+
const _getSource = proto.getSource
12+
const _setSource = proto.setSource
13+
14+
delete proto.getSource
15+
delete proto.setSource
16+
17+
/**
18+
* @param {Image} image
19+
* @param {Error} err
720
*/
21+
function signalError (image, err) {
22+
if (typeof image.onerror === 'function') return image.onerror(err)
23+
24+
throw err
25+
}
826

927
/**
10-
* Module dependencies.
28+
* @param {Image} image
29+
* @param {string} value
1130
*/
31+
function loadDataUrl (image, value) {
32+
const firstComma = value.indexOf(',')
33+
const isBase64 = value.lastIndexOf('base64', firstComma) !== -1
34+
const source = value.slice(firstComma + 1)
1235

13-
const bindings = require('./bindings')
14-
const Image = module.exports = bindings.Image
15-
const http = require("http")
16-
const https = require("https")
36+
let data
37+
try {
38+
data = Buffer.from(source, isBase64 ? 'base64' : 'utf8')
39+
} catch (err) {
40+
return signalError(image, err)
41+
}
1742

18-
const proto = Image.prototype;
19-
const _getSource = proto.getSource;
20-
const _setSource = proto.setSource;
43+
return setSource(image, data, value)
44+
}
45+
46+
/**
47+
* @param {Image} image
48+
* @param {string} value
49+
*/
50+
function loadHttpUrl (image, value) {
51+
return get.concat(value, (err, res, data) => {
52+
if (err) return signalError(image, err)
2153

22-
delete proto.getSource;
23-
delete proto.setSource;
54+
if (res.statusCode < 200 || res.statusCode >= 300) {
55+
return signalError(image, new Error(`Server responded with ${res.statusCode}`))
56+
}
57+
58+
return setSource(image, data, value)
59+
})
60+
}
61+
62+
/**
63+
* @param {Image} image
64+
* @param {string} value
65+
*/
66+
function loadFileUrl (image, value) {
67+
fs.readFile(value.replace('file://', ''), (err, data) => {
68+
if (err) return signalError(image, err)
69+
70+
setSource(image, data, value)
71+
})
72+
}
73+
74+
/**
75+
* @param {Image} image
76+
* @param {string} value
77+
*/
78+
function loadLocalFile (image, value) {
79+
fs.readFile(value, (err, data) => {
80+
if (err) return signalError(image, err)
81+
82+
setSource(image, data, value)
83+
})
84+
}
2485

2586
Object.defineProperty(Image.prototype, 'src', {
2687
/**
@@ -33,49 +94,44 @@ Object.defineProperty(Image.prototype, 'src', {
3394
* @param {String|Buffer} val filename, buffer, data URI, URL
3495
* @api public
3596
*/
36-
set(val) {
37-
if (typeof val === 'string') {
38-
if (/^\s*data:/.test(val)) { // data: URI
39-
const commaI = val.indexOf(',')
40-
// 'base64' must come before the comma
41-
const isBase64 = val.lastIndexOf('base64', commaI) !== -1
42-
const content = val.slice(commaI + 1)
43-
setSource(this, Buffer.from(content, isBase64 ? 'base64' : 'utf8'), val);
44-
} else if (/^\s*https?:\/\//.test(val)) { // remote URL
45-
const onerror = err => {
46-
if (typeof this.onerror === 'function') {
47-
this.onerror(err)
48-
} else {
49-
throw err
50-
}
51-
}
52-
53-
const type = /^\s*https:\/\//.test(val) ? https : http
54-
type.get(val, res => {
55-
if (res.statusCode !== 200) {
56-
return onerror(new Error(`Server responded with ${res.statusCode}`))
57-
}
58-
const buffers = []
59-
res.on('data', buffer => buffers.push(buffer))
60-
res.on('end', () => {
61-
setSource(this, Buffer.concat(buffers));
62-
})
63-
}).on('error', onerror)
64-
} else { // local file path assumed
65-
setSource(this, val);
66-
}
67-
} else if (Buffer.isBuffer(val)) {
68-
setSource(this, val);
97+
set (val) {
98+
if (Buffer.isBuffer(val)) {
99+
return setSource(this, val, val)
100+
}
101+
102+
if (typeof val !== 'string') {
103+
return // ignore invalid values
104+
}
105+
106+
// Strip leading & trailing whitespace
107+
val = val.trim()
108+
109+
// Data URL
110+
if (/^data:/.test(val)) {
111+
return loadDataUrl(this, val)
69112
}
113+
114+
// HTTP(S) URL
115+
if (/^https?:\/\//.test(val)) {
116+
return loadHttpUrl(this, val)
117+
}
118+
119+
// File URL
120+
if (/^file:\/\//.test(val)) {
121+
return loadFileUrl(this, val)
122+
}
123+
124+
// Assume local file path
125+
loadLocalFile(this, val)
70126
},
71127

72-
get() {
128+
get () {
73129
// TODO https://github.com/Automattic/node-canvas/issues/118
74-
return getSource(this);
130+
return this._originalSource || _getSource.call(this)
75131
},
76132

77133
configurable: true
78-
});
134+
})
79135

80136
/**
81137
* Inspect image.
@@ -86,19 +142,27 @@ Object.defineProperty(Image.prototype, 'src', {
86142
* @api public
87143
*/
88144

89-
Image.prototype.inspect = function(){
90-
return '[Image'
91-
+ (this.complete ? ':' + this.width + 'x' + this.height : '')
92-
+ (this.src ? ' ' + this.src : '')
93-
+ (this.complete ? ' complete' : '')
94-
+ ']';
95-
};
145+
Image.prototype.inspect = function () {
146+
return '[Image' +
147+
(this.complete ? ':' + this.width + 'x' + this.height : '') +
148+
(this.src ? ' ' + this.src : '') +
149+
(this.complete ? ' complete' : '') +
150+
']'
151+
}
96152

97-
function getSource(img){
98-
return img._originalSource || _getSource.call(img);
153+
/**
154+
* @param {Buffer} source
155+
*/
156+
function isWebP (source) {
157+
return (source.toString('ascii', 0, 4) === 'RIFF' && source.toString('ascii', 8, 12) === 'WEBP')
99158
}
100159

101-
function setSource(img, src, origSrc){
102-
_setSource.call(img, src);
103-
img._originalSource = origSrc;
160+
/**
161+
* @param {Image} image
162+
* @param {Buffer} source
163+
* @param {Buffer|string} originalSource
164+
*/
165+
function setSource (image, source, originalSource) {
166+
_setSource.call(image, isWebP(source) ? webp.decode(source) : source)
167+
image._originalSource = originalSource
104168
}

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,10 @@
3939
"package_name": "{module_name}-v{version}-{node_abi}-{platform}-{libc}-{arch}.tar.gz"
4040
},
4141
"dependencies": {
42+
"@cwasm/webp": "^0.1.0",
4243
"nan": "^2.11.1",
43-
"node-pre-gyp": "^0.11.0"
44+
"node-pre-gyp": "^0.11.0",
45+
"simple-get": "^3.0.3"
4446
},
4547
"devDependencies": {
4648
"assert-rejects": "^1.0.0",

src/Image.cc

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,16 @@ NAN_METHOD(Image::SetSource){
245245
uint8_t *buf = (uint8_t *) Buffer::Data(value->ToObject());
246246
unsigned len = Buffer::Length(value->ToObject());
247247
status = img->loadFromBuffer(buf, len);
248+
// ImageData
249+
} else if (value->IsObject()) {
250+
auto imageData = value->ToObject();
251+
auto width = imageData->Get(Nan::New("width").ToLocalChecked())->Int32Value();
252+
auto height = imageData->Get(Nan::New("height").ToLocalChecked())->Int32Value();
253+
Nan::TypedArrayContents<uint8_t> data(imageData->Get(Nan::New("data").ToLocalChecked()));
254+
255+
assert((width * height * 4) == data.length());
256+
257+
status = img->loadFromImageData(*data, width, height);
248258
}
249259

250260
if (status) {
@@ -270,6 +280,37 @@ NAN_METHOD(Image::SetSource){
270280
}
271281
}
272282

283+
cairo_status_t
284+
Image::loadFromImageData(uint8_t *data, uint32_t width, uint32_t height) {
285+
_surface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, width, height);
286+
auto status = cairo_surface_status(_surface);
287+
288+
if (status != CAIRO_STATUS_SUCCESS) return status;
289+
290+
auto stride = cairo_image_surface_get_stride(_surface);
291+
auto target = cairo_image_surface_get_data(_surface);
292+
293+
for (auto y = 0; y < height; ++y) {
294+
auto pixel = (target + (stride * y));
295+
296+
for (auto x = 0; x < width; ++x) {
297+
uint8_t r = *(data++);
298+
uint8_t g = *(data++);
299+
uint8_t b = *(data++);
300+
uint8_t a = *(data++);
301+
302+
*(pixel++) = b;
303+
*(pixel++) = g;
304+
*(pixel++) = r;
305+
*(pixel++) = a;
306+
}
307+
}
308+
309+
cairo_surface_mark_dirty(_surface);
310+
311+
return CAIRO_STATUS_SUCCESS;
312+
}
313+
273314
/*
274315
* Load image data from `buf` by sniffing
275316
* the bytes to determine format.

src/Image.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ class Image: public Nan::ObjectWrap {
6767
cairo_surface_t *surface();
6868
cairo_status_t loadSurface();
6969
cairo_status_t loadFromBuffer(uint8_t *buf, unsigned len);
70+
cairo_status_t loadFromImageData(uint8_t *data, uint32_t width, uint32_t height);
7071
cairo_status_t loadPNGFromBuffer(uint8_t *buf);
7172
cairo_status_t loadPNG();
7273
void clearData();

test/fixtures/test.webp

4.77 KB
Binary file not shown.

test/image.test.js

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ const png_clock = `${__dirname}/fixtures/clock.png`
1818
const jpg_chrome = `${__dirname}/fixtures/chrome.jpg`
1919
const jpg_face = `${__dirname}/fixtures/face.jpeg`
2020
const svg_tree = `${__dirname}/fixtures/tree.svg`
21+
const webp_test = `${__dirname}/fixtures/test.webp`
2122

2223
describe('Image', function () {
2324
it('Prototype and ctor are well-shaped, don\'t hit asserts on accessors (GH-803)', function () {
@@ -85,7 +86,7 @@ describe('Image', function () {
8586
it('loads SVG data URL base64', function () {
8687
const base64Enc = fs.readFileSync(svg_tree, 'base64')
8788
const dataURL = `data:image/svg+xml;base64,${base64Enc}`
88-
return loadImage(dataURL).then((img) => {
89+
return loadImage(dataURL).then((img) => {
8990
assert.strictEqual(img.onerror, null)
9091
assert.strictEqual(img.onload, null)
9192
assert.strictEqual(img.width, 200)
@@ -97,7 +98,7 @@ describe('Image', function () {
9798
it('loads SVG data URL utf8', function () {
9899
const utf8Encoded = fs.readFileSync(svg_tree, 'utf8')
99100
const dataURL = `data:image/svg+xml;utf8,${utf8Encoded}`
100-
return loadImage(dataURL).then((img) => {
101+
return loadImage(dataURL).then((img) => {
101102
assert.strictEqual(img.onerror, null)
102103
assert.strictEqual(img.onload, null)
103104
assert.strictEqual(img.width, 200)
@@ -106,6 +107,18 @@ describe('Image', function () {
106107
})
107108
})
108109

110+
it('loads WebP image', function () {
111+
return loadImage(webp_test).then((img) => {
112+
assert.strictEqual(img.onerror, null)
113+
assert.strictEqual(img.onload, null)
114+
115+
assert.strictEqual(img.src, webp_test)
116+
assert.strictEqual(img.width, 128)
117+
assert.strictEqual(img.height, 128)
118+
assert.strictEqual(img.complete, true)
119+
})
120+
})
121+
109122
it('calls Image#onload multiple times', function () {
110123
return loadImage(png_clock).then((img) => {
111124
let onloadCalled = 0

test/public/tests.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1796,6 +1796,16 @@ tests['drawImage(img) svg with scaling from ctx'] = function (ctx, done) {
17961796
img.src = imageSrc('tree.svg')
17971797
}
17981798

1799+
tests['drawImage(img) WebP'] = function (ctx, done) {
1800+
var img = new Image()
1801+
img.onload = function () {
1802+
ctx.drawImage(img, 0, 0)
1803+
done(null)
1804+
}
1805+
img.onerror = done
1806+
img.src = imageSrc('test.webp')
1807+
}
1808+
17991809
tests['drawImage(img,x,y)'] = function (ctx, done) {
18001810
var img = new Image()
18011811
img.onload = function () {

0 commit comments

Comments
 (0)