Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for WebP decoding #1280

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ install:
node_js:
- '10'
- '8'
- '6'
addons:
apt:
sources:
Expand Down
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down Expand Up @@ -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)
==================
Expand Down
2 changes: 1 addition & 1 deletion Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion appveyor.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
version: 0.0.{build}
environment:
matrix:
- nodejs_version: "6"
- nodejs_version: "8"
image:
- Visual Studio 2013
- Visual Studio 2015
Expand Down
210 changes: 146 additions & 64 deletions lib/image.js
Original file line number Diff line number Diff line change
@@ -1,26 +1,89 @@
'use strict';
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@LinusU until the memory issue is resolved, do you wanna pull out the nice refactoring you did in this file into a separate PR? Your version is way more readable 😍

const fs = require('fs')
const get = require('simple-get')
const webp = require('@cwasm/webp')

/*!
* Canvas - Image
* Copyright (c) 2010 LearnBoost <tj@learnboost.com>
* 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', {
/**
Expand All @@ -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.
Expand All @@ -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)
}
8 changes: 5 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand All @@ -49,7 +51,7 @@
"standard": "^12.0.1"
},
"engines": {
"node": ">=6"
"node": ">=8"
},
"license": "MIT"
}
46 changes: 46 additions & 0 deletions src/Image.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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<uint8_t> data(imageData->Get(Nan::New("data").ToLocalChecked()));

assert((width * height * 4) == data.length());

status = img->loadFromImageData(*data, width, height);
}

if (status) {
Expand All @@ -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.
Expand Down
1 change: 1 addition & 0 deletions src/Image.h
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Binary file added test/fixtures/test.webp
Binary file not shown.
Loading