Allow PNG and WebP tile-based output in addition to JPEG (#622)

This commit is contained in:
Patrick Paskaris 2016-11-13 15:36:43 -05:00 committed by Lovell Fuller
parent 6b426014ad
commit bc84d1e47a
5 changed files with 179 additions and 2 deletions

View File

@ -285,7 +285,13 @@ const tile = function tile (tile) {
}
}
}
return this;
// Format
if (is.inArray(this.options.formatOut, ['jpeg', 'png', 'webp'])) {
this.options.tileFormat = this.options.formatOut;
} else if (this.options.formatOut !== 'input') {
throw new Error('Invalid tile format ' + this.options.formatOut);
}
return this._updateFormatOut('dz');
};
/**

View File

@ -28,7 +28,8 @@
"F. Orlando Galashan <frulo@gmx.de>",
"Kleis Auke Wolthuizen <info@kleisauke.nl>",
"Matt Hirsch <mhirsch@media.mit.edu>",
"Matthias Thoemmes <thoemmes@gmail.com>"
"Matthias Thoemmes <thoemmes@gmail.com>",
"Patrick Paskaris <patrick@paskaris.gr>"
],
"scripts": {
"clean": "rm -rf node_modules/ build/ vendor/ coverage/ test/fixtures/output.*",

View File

@ -846,6 +846,35 @@ class PipelineWorker : public Nan::AsyncWorker {
if (isDzZip) {
baton->tileContainer = VIPS_FOREIGN_DZ_CONTAINER_ZIP;
}
// Forward format options through suffix
std::string suffix;
if (baton->tileFormat == "png") {
std::vector<std::pair<std::string, std::string>> options {
{"interlace", baton->pngProgressive ? "TRUE" : "FALSE"},
{"compression", std::to_string(baton->pngCompressionLevel)},
{"filter", baton->pngAdaptiveFiltering ? "all" : "none"}
};
suffix = AssembleSuffixString(".png", options);
} else if (baton->tileFormat == "webp") {
std::vector<std::pair<std::string, std::string>> options {
{"Q", std::to_string(baton->webpQuality)}
};
suffix = AssembleSuffixString(".webp", options);
} else {
std::string extname = baton->tileLayout == VIPS_FOREIGN_DZ_LAYOUT_GOOGLE
|| baton->tileLayout == VIPS_FOREIGN_DZ_LAYOUT_ZOOMIFY
? ".jpg" : ".jpeg";
std::vector<std::pair<std::string, std::string>> options {
{"Q", std::to_string(baton->jpegQuality)},
{"interlace", baton->jpegProgressive ? "TRUE" : "FALSE"},
{"no_subsample", baton->jpegChromaSubsampling == "4:4:4" ? "TRUE": "FALSE"},
{"trellis_quant", baton->jpegTrellisQuantisation ? "TRUE" : "FALSE"},
{"overshoot_deringing", baton->jpegOvershootDeringing ? "TRUE": "FALSE"},
{"optimize_scans", baton->jpegOptimiseScans ? "TRUE": "FALSE"},
{"optimize_coding", "TRUE"}
};
suffix = AssembleSuffixString(extname, options);
}
// Write DZ to file
image.dzsave(const_cast<char*>(baton->fileOut.data()), VImage::option()
->set("strip", !baton->withMetadata)
@ -853,6 +882,7 @@ class PipelineWorker : public Nan::AsyncWorker {
->set("overlap", baton->tileOverlap)
->set("container", baton->tileContainer)
->set("layout", baton->tileLayout)
->set("suffix", const_cast<char*>(suffix.data()))
);
baton->formatOut = "dz";
} else if (baton->formatOut == "v" || isV || (matchInput && inputImageType == ImageType::VIPS)) {
@ -990,6 +1020,23 @@ class PipelineWorker : public Nan::AsyncWorker {
return std::make_tuple(rotate, flip, flop);
}
/*
Assemble the suffix argument to dzsave, which is the format (by extname)
alongisde comma-separated arguments to the corresponding `formatsave` vips
action.
*/
std::string
AssembleSuffixString(std::string extname, std::vector<std::pair<std::string, std::string>> options) {
std::string argument;
for (auto const &option : options) {
if (!argument.empty()) {
argument += ",";
}
argument += option.first + "=" + option.second;
}
return extname + "[" + argument + "]";
}
/*
Clear all thread-local data.
*/
@ -1167,6 +1214,7 @@ NAN_METHOD(pipeline) {
} else {
baton->tileLayout = VIPS_FOREIGN_DZ_LAYOUT_DZ;
}
baton->tileFormat = AttrAsStr(options, "tileFormat");
// Function to notify of queue length changes
Nan::Callback *queueListener = new Nan::Callback(AttrAs<v8::Function>(options, "queueListener"));

View File

@ -102,6 +102,7 @@ struct PipelineBaton {
int tileOverlap;
VipsForeignDzContainer tileContainer;
VipsForeignDzLayout tileLayout;
std::string tileFormat;
PipelineBaton():
input(nullptr),

View File

@ -128,6 +128,22 @@ describe('Tile', function () {
});
});
it('Valid formats pass', function () {
['jpeg', 'png', 'webp'].forEach(function (format) {
assert.doesNotThrow(function () {
sharp().toFormat(format).tile();
});
});
});
it('Invalid formats fail', function () {
['tiff', 'raw'].forEach(function (format) {
assert.throws(function () {
sharp().toFormat(format).tile();
});
});
});
it('Prevent larger overlap than default size', function () {
assert.throws(function () {
sharp().tile({overlap: 257});
@ -224,6 +240,111 @@ describe('Tile', function () {
});
});
it('Google layout with jpeg format', function (done) {
const directory = fixtures.path('output.jpg.google.dzi');
rimraf(directory, function () {
sharp(fixtures.inputJpg)
.jpeg({ quality: 1 })
.tile({
layout: 'google'
})
.toFile(directory, function (err, info) {
if (err) throw err;
assert.strictEqual('dz', info.format);
assert.strictEqual(2725, info.width);
assert.strictEqual(2225, info.height);
assert.strictEqual(3, info.channels);
assert.strictEqual('number', typeof info.size);
const sample = path.join(directory, '0', '0', '0.jpg');
sharp(sample).metadata(function (err, metadata) {
if (err) throw err;
assert.strictEqual('jpeg', metadata.format);
assert.strictEqual('srgb', metadata.space);
assert.strictEqual(3, metadata.channels);
assert.strictEqual(false, metadata.hasProfile);
assert.strictEqual(false, metadata.hasAlpha);
assert.strictEqual(true, metadata.width === 256);
assert.strictEqual(true, metadata.height === 256);
fs.stat(sample, function (err, stat) {
if (err) throw err;
assert.strictEqual(true, stat.size < 2000);
done();
});
});
});
});
});
it('Google layout with png format', function (done) {
const directory = fixtures.path('output.png.google.dzi');
rimraf(directory, function () {
sharp(fixtures.inputJpg)
.png({ compressionLevel: 1 })
.tile({
layout: 'google'
})
.toFile(directory, function (err, info) {
if (err) throw err;
assert.strictEqual('dz', info.format);
assert.strictEqual(2725, info.width);
assert.strictEqual(2225, info.height);
assert.strictEqual(3, info.channels);
assert.strictEqual('number', typeof info.size);
const sample = path.join(directory, '0', '0', '0.png');
sharp(sample).metadata(function (err, metadata) {
if (err) throw err;
assert.strictEqual('png', metadata.format);
assert.strictEqual('srgb', metadata.space);
assert.strictEqual(3, metadata.channels);
assert.strictEqual(false, metadata.hasProfile);
assert.strictEqual(false, metadata.hasAlpha);
assert.strictEqual(true, metadata.width === 256);
assert.strictEqual(true, metadata.height === 256);
fs.stat(sample, function (err, stat) {
if (err) throw err;
assert.strictEqual(true, stat.size > 44000);
done();
});
});
});
});
});
it('Google layout with webp format', function (done) {
const directory = fixtures.path('output.webp.google.dzi');
rimraf(directory, function () {
sharp(fixtures.inputJpg)
.webp({ quality: 1 })
.tile({
layout: 'google'
})
.toFile(directory, function (err, info) {
if (err) throw err;
assert.strictEqual('dz', info.format);
assert.strictEqual(2725, info.width);
assert.strictEqual(2225, info.height);
assert.strictEqual(3, info.channels);
assert.strictEqual('number', typeof info.size);
const sample = path.join(directory, '0', '0', '0.webp');
sharp(sample).metadata(function (err, metadata) {
if (err) throw err;
assert.strictEqual('webp', metadata.format);
assert.strictEqual('srgb', metadata.space);
assert.strictEqual(3, metadata.channels);
assert.strictEqual(false, metadata.hasProfile);
assert.strictEqual(false, metadata.hasAlpha);
assert.strictEqual(true, metadata.width === 256);
assert.strictEqual(true, metadata.height === 256);
fs.stat(sample, function (err, stat) {
if (err) throw err;
assert.strictEqual(true, stat.size < 2000);
done();
});
});
});
});
});
it('Write to ZIP container using file extension', function (done) {
const container = fixtures.path('output.dz.container.zip');
const extractTo = fixtures.path('output.dz.container');