Skip to content

Commit 27559d2

Browse files
committed
Add support for WebP decoding
1 parent 2a21e34 commit 27559d2

8 files changed

Lines changed: 415 additions & 204 deletions

File tree

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: 146 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,89 @@
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 <tj@learnboost.com>
6-
* MIT Licensed
5+
const bindings = require('./bindings')
6+
7+
const kOriginalSource = Symbol('original-source')
8+
9+
/** @typedef {Object} Image */
10+
const Image = module.exports = bindings.Image
11+
12+
const proto = Image.prototype
13+
const _getSource = proto.getSource
14+
const _setSource = proto.setSource
15+
16+
delete proto.getSource
17+
delete proto.setSource
18+
19+
/**
20+
* @param {Image} image
21+
* @param {Error} err
722
*/
23+
function signalError (image, err) {
24+
if (typeof image.onerror === 'function') return image.onerror(err)
25+
26+
throw err
27+
}
828

929
/**
10-
* Module dependencies.
30+
* @param {Image} image
31+
* @param {string} value
1132
*/
33+
function loadDataUrl (image, value) {
34+
const firstComma = value.indexOf(',')
35+
const isBase64 = value.lastIndexOf('base64', firstComma) !== -1
36+
const source = value.slice(firstComma + 1)
1237

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

18-
const proto = Image.prototype;
19-
const _getSource = proto.getSource;
20-
const _setSource = proto.setSource;
45+
return setSource(image, data, value)
46+
}
2147

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

2588
Object.defineProperty(Image.prototype, 'src', {
2689
/**
@@ -33,49 +96,52 @@ Object.defineProperty(Image.prototype, 'src', {
3396
* @param {String|Buffer} val filename, buffer, data URI, URL
3497
* @api public
3598
*/
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);
99+
set (val) {
100+
// Clear current source
101+
clearSource(this)
102+
103+
// Allow directly setting a buffer
104+
if (Buffer.isBuffer(val)) {
105+
this[kOriginalSource] = val
106+
Promise.resolve().then(() => setSource(this, val, val))
107+
return
108+
}
109+
110+
// Coerce into string and strip leading & trailing whitespace
111+
val = String(val).trim()
112+
this[kOriginalSource] = val
113+
114+
// Clear image
115+
if (val === '') {
116+
return
117+
}
118+
119+
// Data URL
120+
if (/^data:/.test(val)) {
121+
return loadDataUrl(this, val)
122+
}
123+
124+
// HTTP(S) URL
125+
if (/^https?:\/\//.test(val)) {
126+
return loadHttpUrl(this, val)
69127
}
128+
129+
// File URL
130+
if (/^file:\/\//.test(val)) {
131+
return loadFileUrl(this, val)
132+
}
133+
134+
// Assume local file path
135+
loadLocalFile(this, val)
70136
},
71137

72-
get() {
73-
// TODO https://github.com/Automattic/node-canvas/issues/118
74-
return getSource(this);
138+
/** @returns {String|Buffer} */
139+
get () {
140+
return this[kOriginalSource] || ''
75141
},
76142

77143
configurable: true
78-
});
144+
})
79145

80146
/**
81147
* Inspect image.
@@ -86,19 +152,35 @@ Object.defineProperty(Image.prototype, 'src', {
86152
* @api public
87153
*/
88154

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-
};
155+
Image.prototype.inspect = function () {
156+
return '[Image' +
157+
(this.complete ? ':' + this.width + 'x' + this.height : '') +
158+
(this.src ? ' ' + this.src : '') +
159+
(this.complete ? ' complete' : '') +
160+
']'
161+
}
162+
163+
/**
164+
* @param {Buffer} source
165+
*/
166+
function isWebP (source) {
167+
return (source.toString('ascii', 0, 4) === 'RIFF' && source.toString('ascii', 8, 12) === 'WEBP')
168+
}
96169

97-
function getSource(img){
98-
return img._originalSource || _getSource.call(img);
170+
/**
171+
* @param {Image} image
172+
* @param {Buffer} source
173+
* @param {Buffer|string} originalSource
174+
*/
175+
function setSource (image, source, originalSource) {
176+
if (image[kOriginalSource] === originalSource) {
177+
_setSource.call(image, isWebP(source) ? webp.decode(source) : source)
178+
}
99179
}
100180

101-
function setSource(img, src, origSrc){
102-
_setSource.call(img, src);
103-
img._originalSource = origSrc;
181+
/**
182+
* @param {Image} image
183+
*/
184+
function clearSource (image) {
185+
_setSource.call(image, null)
104186
}

package.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
"scripts": {
2626
"prebenchmark": "node-gyp build",
2727
"benchmark": "node benchmarks/run.js",
28-
"pretest": "standard examples/*.js test/server.js test/public/*.js benchmark/run.js util/has_lib.js browser.js index.js && node-gyp build",
28+
"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",
2929
"test": "mocha test/*.test.js",
3030
"pretest-server": "node-gyp build",
3131
"test-server": "node test/server.js",
@@ -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: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,11 @@ NAN_METHOD(Image::SetSource){
234234
// Clear errno in case some unrelated previous syscall failed
235235
errno = 0;
236236

237+
// Just clear the data
238+
if (value->IsNull()) {
239+
return;
240+
}
241+
237242
// url string
238243
if (value->IsString()) {
239244
Nan::Utf8String src(value);
@@ -245,6 +250,16 @@ NAN_METHOD(Image::SetSource){
245250
uint8_t *buf = (uint8_t *) Buffer::Data(value->ToObject());
246251
unsigned len = Buffer::Length(value->ToObject());
247252
status = img->loadFromBuffer(buf, len);
253+
// ImageData
254+
} else if (value->IsObject()) {
255+
auto imageData = value->ToObject();
256+
auto width = imageData->Get(Nan::New("width").ToLocalChecked())->Int32Value();
257+
auto height = imageData->Get(Nan::New("height").ToLocalChecked())->Int32Value();
258+
Nan::TypedArrayContents<uint8_t> data(imageData->Get(Nan::New("data").ToLocalChecked()));
259+
260+
assert((width * height * 4) == data.length());
261+
262+
status = img->loadFromImageData(*data, width, height);
248263
}
249264

250265
if (status) {
@@ -270,6 +285,37 @@ NAN_METHOD(Image::SetSource){
270285
}
271286
}
272287

288+
cairo_status_t
289+
Image::loadFromImageData(uint8_t *data, uint32_t width, uint32_t height) {
290+
_surface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, width, height);
291+
auto status = cairo_surface_status(_surface);
292+
293+
if (status != CAIRO_STATUS_SUCCESS) return status;
294+
295+
auto stride = cairo_image_surface_get_stride(_surface);
296+
auto target = cairo_image_surface_get_data(_surface);
297+
298+
for (auto y = 0; y < height; ++y) {
299+
auto pixel = (target + (stride * y));
300+
301+
for (auto x = 0; x < width; ++x) {
302+
uint8_t r = *(data++);
303+
uint8_t g = *(data++);
304+
uint8_t b = *(data++);
305+
uint8_t a = *(data++);
306+
307+
*(pixel++) = b;
308+
*(pixel++) = g;
309+
*(pixel++) = r;
310+
*(pixel++) = a;
311+
}
312+
}
313+
314+
cairo_surface_mark_dirty(_surface);
315+
316+
return CAIRO_STATUS_SUCCESS;
317+
}
318+
273319
/*
274320
* Load image data from `buf` by sniffing
275321
* 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
Loading

0 commit comments

Comments
 (0)