Add support for PNG format. Close #2.

This commit is contained in:
Lovell Fuller 2013-10-26 16:32:18 +01:00
parent daeebcc7dc
commit 21f12e74ba
8 changed files with 191 additions and 111 deletions

1
.gitignore vendored
View File

@ -13,5 +13,6 @@ results
build
node_modules
tests/output.jpg
tests/output.png
npm-debug.log

View File

@ -1,8 +0,0 @@
language: node_js
node_js:
- "0.11"
- "0.10"
before_install:
- sudo apt-get update -qq
- sudo apt-get install -qq libvips-dev imagemagick
- sudo ln -s /usr/lib/pkgconfig/vips-7.26.pc /usr/lib/pkgconfig/vips.pc

View File

@ -7,9 +7,9 @@ _adj_
3. shrewd or astute: a sharp bargainer.
4. (Informal.) very stylish: a sharp dresser; a sharp jacket.
The typical use case for this high speed Node.js module is to convert a large JPEG image to smaller JPEG images of varying dimensions.
The typical use case for this high speed Node.js module is to convert large JPEG and PNG images to smaller JPEG and PNG images of varying dimensions.
It is somewhat opinionated in that it only deals with JPEG images, always obeys the requested dimensions by either cropping or embedding and insists on a mild sharpen of the resulting image.
It is somewhat opinionated in that it only deals with JPEG and PNG images, always obeys the requested dimensions by either cropping or embedding and insists on a mild sharpen of the resulting image.
Under the hood you'll find the blazingly fast [libvips](https://github.com/jcupitt/libvips) image processing library, originally created in 1989 at Birkbeck College and currently maintained by the University of Southampton.
@ -19,7 +19,7 @@ Performance is 4x-8x faster than ImageMagick and 2x-4x faster than GraphicsMagic
* Node.js v0.8+
* node-gyp
* libvips-dev 7.28+
* libvips-dev 7.28+ (7.36+ for optimal JPEG Huffman coding)
```
sudo npm install -g node-gyp
@ -40,7 +40,7 @@ When installed as a package, please symlink `vips-7.28.pc` (or later, installed
### crop(inputPath, outputPath, width, height, callback)
Scale and crop JPEG `inputPath` to `width` x `height` and write JPEG to `outputPath` calling `callback` when complete.
Scale and crop `inputPath` to `width` x `height` and write to `outputPath` calling `callback` when complete.
Example:
@ -56,29 +56,29 @@ sharp.crop("input.jpg", "output.jpg", 300, 200, function(err) {
### embedWhite(inputPath, outputPath, width, height, callback)
Scale and embed JPEG `inputPath` to `width` x `height` using a white canvas and write JPEG to `outputPath` calling `callback` when complete.
Scale and embed `inputPath` to `width` x `height` using a white canvas and write to `outputPath` calling `callback` when complete.
```javascript
sharp.embedWhite("input.jpg", "output.jpg", 200, 300, function(err) {
sharp.embedWhite("input.jpg", "output.png", 200, 300, function(err) {
if (err) {
throw err;
}
// output.jpg is a 200 pixels wide and 300 pixels high image
// containing a scaled version of input.jpg embedded on a white canvas
// containing a scaled version of input.png embedded on a white canvas
});
```
### embedBlack(inputPath, outputPath, width, height, callback)
Scale and embed JPEG `inputPath` to `width` x `height` using a black canvas and write JPEG to `outputPath` calling `callback` when complete.
Scale and embed `inputPath` to `width` x `height` using a black canvas and write to `outputPath` calling `callback` when complete.
```javascript
sharp.embedBlack("input.jpg", "output.jpg", 200, 300, function(err) {
sharp.embedBlack("input.png", "output.png", 200, 300, function(err) {
if (err) {
throw err;
}
// output.jpg is a 200 pixels wide and 300 pixels high image
// containing a scaled version of input.jpg embedded on a black canvas
// output.png is a 200 pixels wide and 300 pixels high image
// containing a scaled version of input.png embedded on a black canvas
});
```
@ -89,29 +89,37 @@ sharp.embedBlack("input.jpg", "output.jpg", 200, 300, function(err) {
## Performance
### AMD Athlon 4x core 3.3GHz 512KB L2
Test environment:
* imagemagick x 5.55 ops/sec ±0.45% (31 runs sampled)
* gm x 10.31 ops/sec ±3.57% (53 runs sampled)
* epeg x 27.79 ops/sec ±0.12% (69 runs sampled)
* sharp x 31.52 ops/sec ±8.74% (80 runs sampled)
* AMD Athlon 4 core 3.3GHz 512KB L2 CPU 1333 DDR3
* libvips 7.36
* libjpeg-turbo8 1.2.1
* libpng 1.6.6
* zlib1g 1.2.7
### AWS t1.micro
#### JPEG
* imagemagick x 1.36 ops/sec ±0.96% (11 runs sampled)
* sharp x 12.42 ops/sec ±5.84% (64 runs sampled)
* imagemagick x 5.53 ops/sec ±0.55% (31 runs sampled)
* gm x 10.86 ops/sec ±0.43% (56 runs sampled)
* epeg x 28.07 ops/sec ±0.07% (70 runs sampled)
* sharp x 31.60 ops/sec ±8.80% (80 runs sampled)
### AWS m1.medium
#### PNG
* imagemagick x 1.38 ops/sec ±0.45% (11 runs sampled)
* sharp x 12.66 ops/sec ±5.54% (65 runs sampled)
* imagemagick x 4.65 ops/sec ±0.37% (27 runs sampled)
* gm x 21.65 ops/sec ±0.18% (56 runs sampled)
* sharp x 39.47 ops/sec ±6.78% (68 runs sampled)
### AWS c1.medium
## Licence
* imagemagick x 2.10 ops/sec ±0.67% (15 runs sampled)
* sharp x 18.97 ops/sec ±10.54% (52 runs sampled)
Copyright 2013 Lovell Fuller
### AWS m3.xlarge
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0.html)
* imagemagick x 4.46 ops/sec ±0.33% (26 runs sampled)
* sharp x 28.89 ops/sec ±7.75% (74 runs sampled)
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@ -1,13 +1,13 @@
var sharp = require("./build/Release/sharp");
module.exports.crop = function(input, output, width, height, callback) {
sharp.resize(input, output, width, height, "c", callback)
}
sharp.resize(input, output, width, height, "c", callback);
};
module.exports.embedWhite = function(input, output, width, height, callback) {
sharp.resize(input, output, width, height, "w", callback)
}
sharp.resize(input, output, width, height, "w", callback);
};
module.exports.embedBlack = function(input, output, width, height, callback) {
sharp.resize(input, output, width, height, "b", callback)
}
sharp.resize(input, output, width, height, "b", callback);
};

View File

@ -1,26 +1,19 @@
{
"name": "sharp",
"version": "0.0.4",
"version": "0.0.5",
"author": "Lovell Fuller",
"description": "High performance module to resize JPEG and PNG images using the libvips image processing library",
"scripts": {
"test": "node tests/perf.js"
},
"main": "index.js",
"description": "High performance Node.js module to resize JPEG images using the libvips image processing library",
"repository": {
"type": "git",
"url": "git://github.com/lovell/sharp"
},
"devDependencies": {
"imagemagick": "*",
"gm": "*",
"epeg": "*",
"benchmark": "*"
},
"scripts": {
"test": "node tests/perf.js"
},
"engines": {
"node": ">=0.8"
},
"keywords": [
"jpeg",
"png",
"resize",
"thumbnail",
"sharpen",
@ -29,6 +22,15 @@
"libvips",
"fast"
],
"author": "Lovell Fuller",
"license": "Apache 2.0"
"devDependencies": {
"imagemagick": "*",
"gm": "*",
"epeg": "*",
"async": "*",
"benchmark": "*"
},
"license": "Apache 2.0",
"engines": {
"node": ">=0.8"
}
}

View File

@ -35,11 +35,22 @@ struct ResizeBaton {
Persistent<Function> callback;
};
bool EndsWith(std::string const &str, std::string const &end) {
return str.length() >= end.length() && 0 == str.compare(str.length() - end.length(), end.length(), end);
}
void ResizeAsync(uv_work_t *work) {
ResizeBaton* baton = static_cast<ResizeBaton*>(work->data);
VipsImage *in = vips_image_new_mode((baton->src).c_str(), "p");
vips_jpegload((baton->src).c_str(), &in, NULL);
if (EndsWith(baton->src, ".jpg") || EndsWith(baton->src, ".jpeg")) {
vips_jpegload((baton->src).c_str(), &in, NULL);
} else if (EndsWith(baton->src, ".png")) {
vips_pngload((baton->src).c_str(), &in, NULL);
} else {
(baton->err).append("Unsupported input file type");
return;
}
if (in == NULL) {
(baton->err).append(vips_error_buffer());
vips_error_clear();
@ -114,9 +125,18 @@ void ResizeAsync(uv_work_t *work) {
}
img = t[3];
if (vips_jpegsave(img, baton->dst.c_str(), "Q", 80, "profile", "none", "optimize_coding", TRUE, NULL)) {
(baton->err).append(vips_error_buffer());
vips_error_clear();
if (EndsWith(baton->dst, ".jpg") || EndsWith(baton->dst, ".jpeg")) {
if (vips_jpegsave(img, baton->dst.c_str(), "Q", 80, "profile", "none", "optimize_coding", TRUE, NULL)) {
(baton->err).append(vips_error_buffer());
vips_error_clear();
}
} else if (EndsWith(baton->dst, ".png")) {
if (vips_pngsave(img, baton->dst.c_str(), "compression", 6, "interlace", FALSE, NULL)) {
(baton->err).append(vips_error_buffer());
vips_error_clear();
}
} else {
(baton->err).append("Unsupported output file type");
}
}

BIN
tests/50020484-00001.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

View File

@ -2,63 +2,120 @@ var sharp = require("../index");
var imagemagick = require("imagemagick");
var gm = require("gm");
var epeg = require("epeg");
var async = require("async");
var assert = require("assert");
var Benchmark = require("benchmark");
var input = __dirname + "/2569067123_aca715a2ee_o.jpg"; // http://www.flickr.com/photos/grizdave/2569067123/
var output = __dirname + "/output.jpg";
var inputJpg = __dirname + "/2569067123_aca715a2ee_o.jpg"; // http://www.flickr.com/photos/grizdave/2569067123/
var outputJpg = __dirname + "/output.jpg";
var inputPng = __dirname + "/50020484-00001.png"; // http://c.searspartsdirect.com/lis_png/PLDM/50020484-00001.png
var outputPng = __dirname + "/output.png";
var width = 640;
var height = 480;
var suite = new Benchmark.Suite;
suite.add("imagemagick", {
"defer": true,
"fn": function(deferred) {
imagemagick.resize({
srcPath: input,
dstPath: output,
quality: 0.75,
width: width,
height: height
}, function(err) {
if (err) {
throw err;
} else {
async.series({
jpeg: function(callback) {
(new Benchmark.Suite("jpeg")).add("imagemagick", {
"defer": true,
"fn": function(deferred) {
imagemagick.resize({
srcPath: inputJpg,
dstPath: outputJpg,
quality: 0.8,
width: width,
height: height
}, function(err) {
if (err) {
throw err;
} else {
deferred.resolve();
}
});
}
}).add("gm", {
"defer": true,
"fn": function(deferred) {
gm(inputJpg).crop(width, height).quality(80).write(outputJpg, function (err) {
if (err) {
throw err;
} else {
deferred.resolve();
}
});
}
}).add("epeg", {
"defer": true,
"fn": function(deferred) {
var image = new epeg.Image({path: inputJpg});
image.downsize(width, height, 80).saveTo(outputJpg);
deferred.resolve();
}
});
}
}).add("gm", {
"defer": true,
"fn": function(deferred) {
gm(input).crop(width, height).write(output, function (err) {
if (err) {
throw err;
} else {
deferred.resolve();
}).add("sharp", {
"defer": true,
"fn": function(deferred) {
sharp.crop(inputJpg, outputJpg, width, height, function(err) {
if (err) {
throw err;
} else {
deferred.resolve();
}
});
}
});
}
}).add("epeg", {
"defer": true,
"fn": function(deferred) {
var image = new epeg.Image({path: input});
image.downsize(width, height).saveTo(output);
deferred.resolve();
}
}).add("sharp", {
"defer": true,
"fn": function(deferred) {
sharp.crop(input, output, width, height, function(err) {
if (err) {
throw err;
} else {
deferred.resolve();
}).on("cycle", function(event) {
console.log("jpeg " + String(event.target));
}).on("complete", function() {
callback(null, this.filter("fastest").pluck("name"));
}).run();
},
png: function(callback) {
(new Benchmark.Suite("png")).add("imagemagick", {
"defer": true,
"fn": function(deferred) {
imagemagick.resize({
srcPath: inputPng,
dstPath: outputPng,
width: width,
height: height
}, function(err) {
if (err) {
throw err;
} else {
deferred.resolve();
}
});
}
});
}).add("gm", {
"defer": true,
"fn": function(deferred) {
gm(inputPng).crop(width, height).write(outputPng, function (err) {
if (err) {
throw err;
} else {
deferred.resolve();
}
});
}
}).add("sharp", {
"defer": true,
"fn": function(deferred) {
sharp.crop(inputPng, outputPng, width, height, function(err) {
if (err) {
throw err;
} else {
deferred.resolve();
}
});
}
}).on("cycle", function(event) {
console.log(" png " + String(event.target));
}).on("complete", function() {
callback(null, this.filter("fastest").pluck("name"));
}).run();
}
}).on("cycle", function(event) {
console.log(String(event.target));
}).on("complete", function() {
assert(this.filter("fastest").pluck("name") == "sharp");
}).run();
}, function(err, results) {
results.forEach(function(format, fastest) {
assert(fastest === "sharp", "sharp was slower than " + fastest + " for " + format);
});
});