From bc84d1e47af8ff960af7185d2028a999c864bec2 Mon Sep 17 00:00:00 2001 From: Patrick Paskaris Date: Sun, 13 Nov 2016 15:36:43 -0500 Subject: [PATCH] Allow PNG and WebP tile-based output in addition to JPEG (#622) --- lib/output.js | 8 ++- package.json | 3 +- src/pipeline.cc | 48 ++++++++++++++++++ src/pipeline.h | 1 + test/unit/tile.js | 121 ++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 179 insertions(+), 2 deletions(-) diff --git a/lib/output.js b/lib/output.js index b346241f..2048cf8f 100644 --- a/lib/output.js +++ b/lib/output.js @@ -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'); }; /** diff --git a/package.json b/package.json index 57f16b06..7f7b3451 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,8 @@ "F. Orlando Galashan ", "Kleis Auke Wolthuizen ", "Matt Hirsch ", - "Matthias Thoemmes " + "Matthias Thoemmes ", + "Patrick Paskaris " ], "scripts": { "clean": "rm -rf node_modules/ build/ vendor/ coverage/ test/fixtures/output.*", diff --git a/src/pipeline.cc b/src/pipeline.cc index da9253a0..7bd7eb50 100644 --- a/src/pipeline.cc +++ b/src/pipeline.cc @@ -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> 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> 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> 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(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(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> 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(options, "queueListener")); diff --git a/src/pipeline.h b/src/pipeline.h index 2bc8536c..66589c45 100644 --- a/src/pipeline.h +++ b/src/pipeline.h @@ -102,6 +102,7 @@ struct PipelineBaton { int tileOverlap; VipsForeignDzContainer tileContainer; VipsForeignDzLayout tileLayout; + std::string tileFormat; PipelineBaton(): input(nullptr), diff --git a/test/unit/tile.js b/test/unit/tile.js index 86209ede..6d34cf52 100644 --- a/test/unit/tile.js +++ b/test/unit/tile.js @@ -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');