Add features from libvips 7.40+

Load TIFF from Buffer/Stream

Interlaced PNG output no longer needs tilecache

Option to disable PNG adaptive row filtering
This commit is contained in:
Lovell Fuller 2014-11-07 15:38:41 +00:00
parent 740838b47c
commit 7537adf399
11 changed files with 183 additions and 21 deletions

View File

@ -29,7 +29,7 @@ This module is powered by the blazingly fast [libvips](https://github.com/jcupit
### Prerequisites
* Node.js v0.10+
* [libvips](https://github.com/jcupitt/libvips) v7.38.5+
* [libvips](https://github.com/jcupitt/libvips) v7.38.5+ (7.40.9+ recommended)
To install the latest version of libvips on the following Operating Systems:
@ -214,12 +214,12 @@ sharp(inputBuffer)
Constructor to which further methods are chained. `input`, if present, can be one of:
* Buffer containing JPEG, PNG or WebP image data, or
* Buffer containing JPEG, PNG, WebP or TIFF (libvips 7.40.0+) image data, or
* String containing the filename of an image, with most major formats supported.
The object returned implements the [stream.Duplex](http://nodejs.org/api/stream.html#stream_class_stream_duplex) class.
JPEG, PNG or WebP format image data can be streamed into the object when `input` is not provided.
JPEG, PNG, WebP or TIFF (libvips 7.40.0+) format image data can be streamed into the object when `input` is not provided.
JPEG, PNG or WebP format image data can be streamed out from this object.
@ -388,6 +388,12 @@ An advanced setting for the _zlib_ compression level of the lossless PNG output
`compressionLevel` is a Number between 0 and 9.
#### withoutAdaptiveFiltering()
_Requires libvips 7.41.0+_
An advanced and experimental PNG output setting to disable adaptive row filtering.
### Output methods
#### toFile(filename, [callback])

View File

@ -4,10 +4,12 @@ var path = require('path');
var util = require('util');
var stream = require('stream');
var semver = require('semver');
var color = require('color');
var BluebirdPromise = require('bluebird');
var sharp = require('./build/Release/sharp');
var libvipsVersion = sharp.libvipsVersion();
var Sharp = function(input) {
if (!(this instanceof Sharp)) {
@ -49,6 +51,7 @@ var Sharp = function(input) {
progressive: false,
quality: 80,
compressionLevel: 6,
withoutAdaptiveFiltering: false,
streamOut: false,
withMetadata: false
};
@ -58,14 +61,22 @@ var Sharp = function(input) {
} else if (typeof input === 'object' && input instanceof Buffer) {
// input=buffer
if (
(input.length > 1) &&
(input[0] === 0xff && input[1] === 0xd8) || // JPEG
(input[0] === 0x89 && input[1] === 0x50) || // PNG
(input[0] === 0x52 && input[1] === 0x49) // WebP
(input.length > 3) &&
// JPEG
(input[0] === 0xFF && input[1] === 0xD8) ||
// PNG
(input[0] === 0x89 && input[1] === 0x50) ||
// WebP
(input[0] === 0x52 && input[1] === 0x49) ||
// TIFF - requires libvips 7.40.0+
(semver.gte(libvipsVersion, '7.40.0') && (
(input[0] === 0x4D && input[1] === 0x4D && input[2] === 0x00 && (input[3] === 0x2A || input[3] === 0x2B)) ||
(input[0] === 0x49 && input[1] === 0x49 && (input[2] === 0x2A || input[2] === 0x2B) && input[3] === 0x00)
))
) {
this.options.bufferIn = input;
} else {
throw new Error('Buffer contains an unsupported image format. JPEG, PNG and WebP are currently supported.');
throw new Error('Buffer contains an unsupported image format. JPEG, PNG, WebP and TIFF are currently supported.');
}
} else {
// input=stream
@ -269,6 +280,9 @@ Sharp.prototype.quality = function(quality) {
return this;
};
/*
zlib compression level for PNG output
*/
Sharp.prototype.compressionLevel = function(compressionLevel) {
if (!Number.isNaN(compressionLevel) && compressionLevel >= 0 && compressionLevel <= 9) {
this.options.compressionLevel = compressionLevel;
@ -278,6 +292,18 @@ Sharp.prototype.compressionLevel = function(compressionLevel) {
return this;
};
/*
Disable the use of adaptive row filtering for PNG output - requires libvips 7.41.0+
*/
Sharp.prototype.withoutAdaptiveFiltering = function(withoutAdaptiveFiltering) {
if (semver.gte(libvipsVersion, '7.41.0')) {
this.options.withoutAdaptiveFiltering = (typeof withoutAdaptiveFiltering === 'boolean') ? withoutAdaptiveFiltering : true;
} else {
console.error('withoutAdaptiveFiltering requires libvips 7.41.0+');
}
return this;
};
Sharp.prototype.withMetadata = function(withMetadata) {
this.options.withMetadata = (typeof withMetadata === 'boolean') ? withMetadata : true;
return this;
@ -506,3 +532,10 @@ module.exports.concurrency = function(concurrency) {
module.exports.counters = function() {
return sharp.counters();
};
/*
Get the version of the libvips library
*/
module.exports.libvipsVersion = function() {
return libvipsVersion;
};

View File

@ -41,9 +41,10 @@
"stream"
],
"dependencies": {
"bluebird": "^2.3.10",
"bluebird": "^2.3.11",
"color": "^0.7.1",
"nan": "^1.4.0"
"nan": "^1.4.0",
"semver": "^4.1.0"
},
"devDependencies": {
"mocha": "^2.0.1",

View File

@ -27,12 +27,25 @@ bool is_tiff(std::string const &str) {
return ends_with(str, ".tif") || ends_with(str, ".tiff") || ends_with(str, ".TIF") || ends_with(str, ".TIFF");
}
// Buffer content checkers
#if (VIPS_MAJOR_VERSION >= 7 && VIPS_MINOR_VERSION >= 40)
static bool buffer_is_tiff(char *buffer, size_t len) {
return (
len >= 4 && (
(buffer[0] == 'M' && buffer[1] == 'M' && buffer[2] == '\0' && (buffer[3] == '*' || buffer[3] == '+')) ||
(buffer[0] == 'I' && buffer[1] == 'I' && (buffer[2] == '*' || buffer[2] == '+') && buffer[3] == '\0')
)
);
}
#endif
unsigned char const MARKER_JPEG[] = {0xff, 0xd8};
unsigned char const MARKER_PNG[] = {0x89, 0x50};
unsigned char const MARKER_WEBP[] = {0x52, 0x49};
/*
Initialise a VipsImage from a buffer. Supports JPEG, PNG and WebP.
Initialise a VipsImage from a buffer. Supports JPEG, PNG, WebP and TIFF.
Returns the ImageType detected, if any.
*/
ImageType
@ -42,14 +55,20 @@ sharp_init_image_from_buffer(VipsImage **image, void *buffer, size_t const lengt
if (!vips_jpegload_buffer(buffer, length, image, "access", access, NULL)) {
imageType = JPEG;
}
} else if(memcmp(MARKER_PNG, buffer, 2) == 0) {
} else if (memcmp(MARKER_PNG, buffer, 2) == 0) {
if (!vips_pngload_buffer(buffer, length, image, "access", access, NULL)) {
imageType = PNG;
}
} else if(memcmp(MARKER_WEBP, buffer, 2) == 0) {
} else if (memcmp(MARKER_WEBP, buffer, 2) == 0) {
if (!vips_webpload_buffer(buffer, length, image, "access", access, NULL)) {
imageType = WEBP;
}
#if (VIPS_MAJOR_VERSION >= 7 && VIPS_MINOR_VERSION >= 40)
} else if (buffer_is_tiff(static_cast<char*>(buffer), length)) {
if (!vips_tiffload_buffer(buffer, length, image, "access", access, NULL)) {
imageType = TIFF;
}
#endif
}
return imageType;
}

View File

@ -61,6 +61,7 @@ struct ResizeBaton {
VipsAccess accessMethod;
int quality;
int compressionLevel;
bool withoutAdaptiveFiltering;
std::string err;
bool withMetadata;
@ -517,7 +518,8 @@ class ResizeWorker : public NanAsyncWorker {
image = colourspaced;
}
// Generate image tile cache when interlace output is required
#if !(VIPS_MAJOR_VERSION >= 7 && VIPS_MINOR_VERSION >= 40 && VIPS_MINOR_VERSION >= 5)
// Generate image tile cache when interlace output is required - no longer required as of libvips 7.40.5+
if (baton->progressive) {
VipsImage *cached = vips_image_new();
vips_object_local(hook, cached);
@ -527,6 +529,7 @@ class ResizeWorker : public NanAsyncWorker {
g_object_unref(image);
image = cached;
}
#endif
// Output
if (baton->output == "__jpeg" || (baton->output == "__input" && inputImageType == JPEG)) {
@ -537,11 +540,21 @@ class ResizeWorker : public NanAsyncWorker {
}
baton->outputFormat = "jpeg";
} else if (baton->output == "__png" || (baton->output == "__input" && inputImageType == PNG)) {
#if (VIPS_MAJOR_VERSION >= 7 && VIPS_MINOR_VERSION >= 41)
// Select PNG row filter
int filter = baton->withoutAdaptiveFiltering ? VIPS_FOREIGN_PNG_FILTER_NONE : VIPS_FOREIGN_PNG_FILTER_ALL;
// Write PNG to buffer
if (vips_pngsave_buffer(image, &baton->bufferOut, &baton->bufferOutLength, "strip", !baton->withMetadata,
"compression", baton->compressionLevel, "interlace", baton->progressive, "filter", filter, NULL)) {
return Error(baton, hook);
}
#else
// Write PNG to buffer
if (vips_pngsave_buffer(image, &baton->bufferOut, &baton->bufferOutLength, "strip", !baton->withMetadata,
"compression", baton->compressionLevel, "interlace", baton->progressive, NULL)) {
return Error(baton, hook);
}
#endif
baton->outputFormat = "png";
} else if (baton->output == "__webp" || (baton->output == "__input" && inputImageType == WEBP)) {
// Write WEBP to buffer
@ -564,11 +577,21 @@ class ResizeWorker : public NanAsyncWorker {
}
baton->outputFormat = "jpeg";
} else if (output_png || (match_input && inputImageType == PNG)) {
#if (VIPS_MAJOR_VERSION >= 7 && VIPS_MINOR_VERSION >= 41)
// Select PNG row filter
int filter = baton->withoutAdaptiveFiltering ? VIPS_FOREIGN_PNG_FILTER_NONE : VIPS_FOREIGN_PNG_FILTER_ALL;
// Write PNG to file
if (vips_pngsave(image, baton->output.c_str(), "strip", !baton->withMetadata,
"compression", baton->compressionLevel, "interlace", baton->progressive, "filter", filter, NULL)) {
return Error(baton, hook);
}
#else
// Write PNG to file
if (vips_pngsave(image, baton->output.c_str(), "strip", !baton->withMetadata,
"compression", baton->compressionLevel, "interlace", baton->progressive, NULL)) {
return Error(baton, hook);
}
#endif
baton->outputFormat = "png";
} else if (output_webp || (match_input && inputImageType == WEBP)) {
// Write WEBP to file
@ -798,6 +821,7 @@ NAN_METHOD(resize) {
baton->progressive = options->Get(NanNew<String>("progressive"))->BooleanValue();
baton->quality = options->Get(NanNew<String>("quality"))->Int32Value();
baton->compressionLevel = options->Get(NanNew<String>("compressionLevel"))->Int32Value();
baton->withoutAdaptiveFiltering = options->Get(NanNew<String>("withoutAdaptiveFiltering"))->BooleanValue();
baton->withMetadata = options->Get(NanNew<String>("withMetadata"))->BooleanValue();
// Output filename or __format for Buffer
baton->output = *String::Utf8Value(options->Get(NanNew<String>("output"))->ToString());

View File

@ -33,6 +33,7 @@ extern "C" void init(Handle<Object> target) {
NODE_SET_METHOD(target, "cache", cache);
NODE_SET_METHOD(target, "concurrency", concurrency);
NODE_SET_METHOD(target, "counters", counters);
NODE_SET_METHOD(target, "libvipsVersion", libvipsVersion);
}
NODE_MODULE(sharp, init)

View File

@ -62,3 +62,13 @@ NAN_METHOD(counters) {
counters->Set(NanNew<String>("process"), NanNew<Number>(counter_process));
NanReturnValue(counters);
}
/*
Get libvips version
*/
NAN_METHOD(libvipsVersion) {
NanScope();
char version[9];
snprintf(version, 9, "%d.%d.%d", vips_version(0), vips_version(1), vips_version(2));
NanReturnValue(NanNew<String>(version));
}

View File

@ -6,5 +6,6 @@
NAN_METHOD(cache);
NAN_METHOD(concurrency);
NAN_METHOD(counters);
NAN_METHOD(libvipsVersion);
#endif

View File

@ -9,9 +9,10 @@
},
"devDependencies": {
"imagemagick": "^0.1.3",
"imagemagick-native": "^1.4.0",
"gm": "^1.16.0",
"imagemagick-native": "^1.5.0",
"gm": "^1.17.0",
"async": "^0.9.0",
"semver": "^4.1.0",
"benchmark": "^1.0.0"
},
"license": "Apache 2.0",

View File

@ -5,6 +5,7 @@ var fs = require('fs');
var async = require('async');
var assert = require('assert');
var Benchmark = require('benchmark');
var semver = require('semver');
var imagemagick = require('imagemagick');
var imagemagickNative = require('imagemagick-native');
@ -314,7 +315,8 @@ async.series({
},
png: function(callback) {
var inputPngBuffer = fs.readFileSync(fixtures.inputPng);
(new Benchmark.Suite('png')).add('imagemagick-file-file', {
var pngSuite = new Benchmark.Suite('png');
pngSuite.add('imagemagick-file-file', {
defer: true,
fn: function(deferred) {
imagemagick.resize({
@ -422,7 +424,23 @@ async.series({
}
});
}
}).on('cycle', function(event) {
});
if (semver.gte(sharp.libvipsVersion(), '7.41.0')) {
pngSuite.add('sharp-withoutAdaptiveFiltering', {
defer: true,
fn: function(deferred) {
sharp(inputPngBuffer).resize(width, height).withoutAdaptiveFiltering().toBuffer(function(err, buffer) {
if (err) {
throw err;
} else {
assert.notStrictEqual(null, buffer);
deferred.resolve();
}
});
}
});
}
pngSuite.on('cycle', function(event) {
console.log(' png ' + String(event.target));
}).on('complete', function() {
callback(null, this.filter('fastest').pluck('name'));

View File

@ -3,6 +3,8 @@
var fs = require('fs');
var assert = require('assert');
var semver = require('semver');
var sharp = require('../../index');
var fixtures = require('../fixtures');
@ -343,9 +345,9 @@ describe('Input/output', function() {
});
describe('PNG compression level', function() {
describe('PNG output', function() {
it('valid', function(done) {
it('compression level is valid', function(done) {
var isValid = false;
try {
sharp().compressionLevel(0);
@ -355,7 +357,7 @@ describe('Input/output', function() {
done();
});
it('invalid', function(done) {
it('compression level is invalid', function(done) {
var isValid = false;
try {
sharp().compressionLevel(-1);
@ -365,6 +367,52 @@ describe('Input/output', function() {
done();
});
if (semver.gte(sharp.libvipsVersion(), '7.41.0')) {
it('withoutAdaptiveFiltering generates smaller file [libvips 7.41.0+]', function(done) {
// First generate with adaptive filtering
sharp(fixtures.inputPng)
.resize(320, 240)
.withoutAdaptiveFiltering(false)
.toBuffer(function(err, dataAdaptive, info) {
if (err) throw err;
assert.strictEqual(true, dataAdaptive.length > 0);
assert.strictEqual('png', info.format);
assert.strictEqual(320, info.width);
assert.strictEqual(240, info.height);
// Then generate without
sharp(fixtures.inputPng)
.resize(320, 240)
.withoutAdaptiveFiltering()
.toBuffer(function(err, dataWithoutAdaptive, info) {
if (err) throw err;
assert.strictEqual(true, dataWithoutAdaptive.length > 0);
assert.strictEqual('png', info.format);
assert.strictEqual(320, info.width);
assert.strictEqual(240, info.height);
assert.strictEqual(true, dataWithoutAdaptive.length < dataAdaptive.length);
done();
});
});
});
}
});
if (semver.gte(sharp.libvipsVersion(), '7.40.0')) {
it('Load TIFF from Buffer [libvips 7.40.0+]', function(done) {
var inputTiffBuffer = fs.readFileSync(fixtures.inputTiff);
sharp(inputTiffBuffer)
.resize(320, 240)
.jpeg()
.toBuffer(function(err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual('jpeg', info.format);
assert.strictEqual(320, info.width);
assert.strictEqual(240, info.height);
done();
});
});
}
});