diff --git a/docs/api.md b/docs/api.md index 52ee10f3..d5d36ad8 100644 --- a/docs/api.md +++ b/docs/api.md @@ -527,14 +527,17 @@ The default behaviour, when `withMetadata` is not used, is to strip all metadata #### tile(options) -The size, overlap and directory layout to use when generating square Deep Zoom image pyramid tiles. +The size, overlap, container and directory layout to use when generating square Deep Zoom image pyramid tiles. `options` is an Object with one or more of the following attributes: * `size` is an integral Number between 1 and 8192. The default value is 256 pixels. * `overlap` is an integral Number between 0 and 8192. The default value is 0 pixels. +* `container` is a String, with value `fs` or `zip`. The default value is `fs`. * `layout` is a String, with value `dz`, `zoomify` or `google`. The default value is `dz`. +You can also use the file extension .zip or .szi to write to a ZIP container instead of the filesystem. + ```javascript sharp('input.tiff') .tile({ diff --git a/index.js b/index.js index 6972bd42..94e8a7e7 100644 --- a/index.js +++ b/index.js @@ -660,6 +660,14 @@ Sharp.prototype.tile = function(tile) { throw new Error('Invalid tile overlap (0 to 8192) ' + tile.overlap); } } + // Container + if (isDefined(tile.container)) { + if (isString(tile.container) && contains(tile.container, ['fs', 'zip'])) { + this.options.tileContainer = tile.container; + } else { + throw new Error('Invalid tile container ' + tile.container); + } + } // Layout if (isDefined(tile.layout)) { if (isString(tile.layout) && contains(tile.layout, ['dz', 'google', 'zoomify'])) { diff --git a/package.json b/package.json index 961761cf..12afd284 100644 --- a/package.json +++ b/package.json @@ -58,6 +58,7 @@ }, "devDependencies": { "async": "^1.5.2", + "bufferutil": "^1.2.1", "coveralls": "^2.11.9", "exif-reader": "^1.0.0", "icc": "^0.0.2", @@ -66,7 +67,7 @@ "mocha-jshint": "^2.3.1", "node-cpplint": "^0.4.0", "rimraf": "^2.5.2", - "bufferutil": "^1.2.1" + "unzip": "^0.1.11" }, "license": "Apache-2.0", "config": { diff --git a/src/common.cc b/src/common.cc index 465ee79e..5af70e6a 100644 --- a/src/common.cc +++ b/src/common.cc @@ -52,6 +52,9 @@ namespace sharp { bool IsDz(std::string const &str) { return EndsWith(str, ".dzi") || EndsWith(str, ".DZI"); } + bool IsDzZip(std::string const &str) { + return EndsWith(str, ".zip") || EndsWith(str, ".ZIP") || EndsWith(str, ".szi") || EndsWith(str, ".SZI"); + } /* Provide a string identifier for the given image type. diff --git a/src/common.h b/src/common.h index be7a2e78..e2e3ce4e 100644 --- a/src/common.h +++ b/src/common.h @@ -35,6 +35,7 @@ namespace sharp { bool IsWebp(std::string const &str); bool IsTiff(std::string const &str); bool IsDz(std::string const &str); + bool IsDzZip(std::string const &str); /* Provide a string identifier for the given image type. diff --git a/src/pipeline.cc b/src/pipeline.cc index 79905bdc..43def594 100644 --- a/src/pipeline.cc +++ b/src/pipeline.cc @@ -65,6 +65,7 @@ using sharp::IsPng; using sharp::IsWebp; using sharp::IsTiff; using sharp::IsDz; +using sharp::IsDzZip; using sharp::FreeCallback; using sharp::CalculateCrop; using sharp::counterProcess; @@ -743,7 +744,8 @@ class PipelineWorker : public AsyncWorker { bool isWebp = IsWebp(baton->fileOut); bool isTiff = IsTiff(baton->fileOut); bool isDz = IsDz(baton->fileOut); - bool matchInput = baton->formatOut == "input" && !(isJpeg || isPng || isWebp || isTiff || isDz); + bool isDzZip = IsDzZip(baton->fileOut); + bool matchInput = baton->formatOut == "input" && !(isJpeg || isPng || isWebp || isTiff || isDz || isDzZip); if (baton->formatOut == "jpeg" || isJpeg || (matchInput && inputImageType == ImageType::JPEG)) { // Write JPEG to file image.jpegsave(const_cast(baton->fileOut.data()), VImage::option() @@ -784,12 +786,16 @@ class PipelineWorker : public AsyncWorker { ); baton->formatOut = "tiff"; baton->channels = std::min(baton->channels, 3); - } else if (baton->formatOut == "dz" || IsDz(baton->fileOut)) { + } else if (baton->formatOut == "dz" || isDz || isDzZip) { + if (isDzZip) { + baton->tileContainer = VIPS_FOREIGN_DZ_CONTAINER_ZIP; + } // Write DZ to file image.dzsave(const_cast(baton->fileOut.data()), VImage::option() ->set("strip", !baton->withMetadata) ->set("tile_size", baton->tileSize) ->set("overlap", baton->tileOverlap) + ->set("container", baton->tileContainer) ->set("layout", baton->tileLayout) ); baton->formatOut = "dz"; @@ -1058,6 +1064,12 @@ NAN_METHOD(pipeline) { // Tile output baton->tileSize = attrAs(options, "tileSize"); baton->tileOverlap = attrAs(options, "tileOverlap"); + std::string tileContainer = attrAsStr(options, "tileContainer"); + if (tileContainer == "zip") { + baton->tileContainer = VIPS_FOREIGN_DZ_CONTAINER_ZIP; + } else { + baton->tileContainer = VIPS_FOREIGN_DZ_CONTAINER_FS; + } std::string tileLayout = attrAsStr(options, "tileLayout"); if (tileLayout == "google") { baton->tileLayout = VIPS_FOREIGN_DZ_LAYOUT_GOOGLE; diff --git a/src/pipeline.h b/src/pipeline.h index eba987e3..d7ff6c03 100644 --- a/src/pipeline.h +++ b/src/pipeline.h @@ -81,6 +81,7 @@ struct PipelineBaton { int withMetadataOrientation; int tileSize; int tileOverlap; + VipsForeignDzContainer tileContainer; VipsForeignDzLayout tileLayout; PipelineBaton(): @@ -130,6 +131,7 @@ struct PipelineBaton { withMetadataOrientation(-1), tileSize(256), tileOverlap(0), + tileContainer(VIPS_FOREIGN_DZ_CONTAINER_FS), tileLayout(VIPS_FOREIGN_DZ_LAYOUT_DZ) { background[0] = 0.0; background[1] = 0.0; diff --git a/test/unit/tile.js b/test/unit/tile.js index f0dd044c..d0d06573 100644 --- a/test/unit/tile.js +++ b/test/unit/tile.js @@ -6,6 +6,7 @@ var assert = require('assert'); var async = require('async'); var rimraf = require('rimraf'); +var unzip = require('unzip'); var sharp = require('../../index'); var fixtures = require('../fixtures'); @@ -88,6 +89,26 @@ describe('Tile', function() { }); }); + it('Valid container values pass', function() { + ['fs', 'zip'].forEach(function(container) { + assert.doesNotThrow(function() { + sharp().tile({ + container: container + }); + }); + }); + }); + + it('Invalid container values fail', function() { + ['zoinks', 1].forEach(function(container) { + assert.throws(function() { + sharp().tile({ + container: container + }); + }); + }); + }); + it('Valid layout values pass', function() { ['dz', 'google', 'zoomify'].forEach(function(layout) { assert.doesNotThrow(function() { @@ -190,6 +211,58 @@ describe('Tile', function() { }); }); + it('Write to ZIP container using file extension', function(done) { + var container = fixtures.path('output.dz.container.zip'); + var extractTo = fixtures.path('output.dz.container'); + var directory = path.join(extractTo, 'output.dz.container_files'); + rimraf(directory, function() { + sharp(fixtures.inputJpg) + .toFile(container, function(err, info) { + if (err) throw err; + assert.strictEqual('dz', info.format); + fs.stat(container, function(err, stat) { + if (err) throw err; + assert.strictEqual(true, stat.isFile()); + assert.strictEqual(true, stat.size > 0); + fs.createReadStream(container) + .pipe(unzip.Extract({path: path.dirname(extractTo)})) + .on('error', function(err) { throw err; }) + .on('close', function() { + assertDeepZoomTiles(directory, 256, 13, done); + }); + }); + }); + }); + }); + + it('Write to ZIP container using container tile option', function(done) { + var container = fixtures.path('output.dz.containeropt.zip'); + var extractTo = fixtures.path('output.dz.containeropt'); + var directory = path.join(extractTo, 'output.dz.containeropt_files'); + rimraf(directory, function() { + sharp(fixtures.inputJpg) + .tile({ + container: 'zip' + }) + .toFile(fixtures.path('output.dz.containeropt.dzi'), function(err, info) { + // Vips overrides .dzi extension to .zip used by container var below + if (err) throw err; + assert.strictEqual('dz', info.format); + fs.stat(container, function(err, stat) { + if (err) throw err; + assert.strictEqual(true, stat.isFile()); + assert.strictEqual(true, stat.size > 0); + fs.createReadStream(container) + .pipe(unzip.Extract({path: path.dirname(extractTo)})) + .on('error', function(err) { throw err; }) + .on('close', function() { + assertDeepZoomTiles(directory, 256, 13, done); + }); + }); + }); + }); + }); + } });