Add Buffer and Stream support to tile output #2238

This commit is contained in:
Lovell Fuller 2022-07-24 11:06:41 +01:00
parent 3e327a586c
commit b46ab510da
8 changed files with 161 additions and 56 deletions

View File

@ -544,9 +544,12 @@ const data = await sharp('input.png')
## tile ## tile
Use tile-based deep zoom (image pyramid) output. Use tile-based deep zoom (image pyramid) output.
Set the format and options for tile images via the `toFormat`, `jpeg`, `png` or `webp` functions. Set the format and options for tile images via the `toFormat`, `jpeg`, `png` or `webp` functions.
Use a `.zip` or `.szi` file extension with `toFile` to write to a compressed archive file format. Use a `.zip` or `.szi` file extension with `toFile` to write to a compressed archive file format.
The container will be set to `zip` when the output is a Buffer or Stream, otherwise it will default to `fs`.
### Parameters ### Parameters
* `options` **[Object][6]?** * `options` **[Object][6]?**
@ -562,6 +565,7 @@ Use a `.zip` or `.szi` file extension with `toFile` to write to a compressed arc
* `options.centre` **[boolean][10]** centre image in tile. (optional, default `false`) * `options.centre` **[boolean][10]** centre image in tile. (optional, default `false`)
* `options.center` **[boolean][10]** alternative spelling of centre. (optional, default `false`) * `options.center` **[boolean][10]** alternative spelling of centre. (optional, default `false`)
* `options.id` **[string][2]** when `layout` is `iiif`/`iiif3`, sets the `@id`/`id` attribute of `info.json` (optional, default `'https://example.com/iiif'`) * `options.id` **[string][2]** when `layout` is `iiif`/`iiif3`, sets the `@id`/`id` attribute of `info.json` (optional, default `'https://example.com/iiif'`)
* `options.basename` **[string][2]?** the name of the directory within the zip file when container is `zip`.
### Examples ### Examples
@ -577,6 +581,19 @@ sharp('input.tiff')
}); });
``` ```
```javascript
const zipFileWithTiles = await sharp(input)
.tile({ basename: "tiles" })
.toBuffer();
```
```javascript
const iiififier = sharp().tile({ layout: "iiif" });
readableStream
.pipe(iiififier)
.pipe(writeableStream);
```
* Throws **[Error][4]** Invalid parameters * Throws **[Error][4]** Invalid parameters
Returns **Sharp** Returns **Sharp**

View File

@ -15,6 +15,9 @@ Requires libvips v8.13.0
* Use combined bounding box of alpha and non-alpha channels for `trim` operation. * Use combined bounding box of alpha and non-alpha channels for `trim` operation.
[#2166](https://github.com/lovell/sharp/issues/2166) [#2166](https://github.com/lovell/sharp/issues/2166)
* Add Buffer and Stream support to tile-based output.
[#2238](https://github.com/lovell/sharp/issues/2238)
* Add input `fileSuffix` and output `alias` to `format` information. * Add input `fileSuffix` and output `alias` to `format` information.
[#2642](https://github.com/lovell/sharp/issues/2642) [#2642](https://github.com/lovell/sharp/issues/2642)

File diff suppressed because one or more lines are too long

View File

@ -285,6 +285,7 @@ const Sharp = function (input, options) {
tileBackground: [255, 255, 255, 255], tileBackground: [255, 255, 255, 255],
tileCentre: false, tileCentre: false,
tileId: 'https://example.com/iiif', tileId: 'https://example.com/iiif',
tileBasename: '',
timeoutSeconds: 0, timeoutSeconds: 0,
linearA: 1, linearA: 1,
linearB: 0, linearB: 0,

View File

@ -943,9 +943,12 @@ function raw (options) {
/** /**
* Use tile-based deep zoom (image pyramid) output. * Use tile-based deep zoom (image pyramid) output.
*
* Set the format and options for tile images via the `toFormat`, `jpeg`, `png` or `webp` functions. * Set the format and options for tile images via the `toFormat`, `jpeg`, `png` or `webp` functions.
* Use a `.zip` or `.szi` file extension with `toFile` to write to a compressed archive file format. * Use a `.zip` or `.szi` file extension with `toFile` to write to a compressed archive file format.
* *
* The container will be set to `zip` when the output is a Buffer or Stream, otherwise it will default to `fs`.
*
* @example * @example
* sharp('input.tiff') * sharp('input.tiff')
* .png() * .png()
@ -957,6 +960,17 @@ function raw (options) {
* // output_files contains 512x512 tiles grouped by zoom level * // output_files contains 512x512 tiles grouped by zoom level
* }); * });
* *
* @example
* const zipFileWithTiles = await sharp(input)
* .tile({ basename: "tiles" })
* .toBuffer();
*
* @example
* const iiififier = sharp().tile({ layout: "iiif" });
* readableStream
* .pipe(iiififier)
* .pipe(writeableStream);
*
* @param {Object} [options] * @param {Object} [options]
* @param {number} [options.size=256] tile size in pixels, a value between 1 and 8192. * @param {number} [options.size=256] tile size in pixels, a value between 1 and 8192.
* @param {number} [options.overlap=0] tile overlap in pixels, a value between 0 and 8192. * @param {number} [options.overlap=0] tile overlap in pixels, a value between 0 and 8192.
@ -969,6 +983,7 @@ function raw (options) {
* @param {boolean} [options.centre=false] centre image in tile. * @param {boolean} [options.centre=false] centre image in tile.
* @param {boolean} [options.center=false] alternative spelling of centre. * @param {boolean} [options.center=false] alternative spelling of centre.
* @param {string} [options.id='https://example.com/iiif'] when `layout` is `iiif`/`iiif3`, sets the `@id`/`id` attribute of `info.json` * @param {string} [options.id='https://example.com/iiif'] when `layout` is `iiif`/`iiif3`, sets the `@id`/`id` attribute of `info.json`
* @param {string} [options.basename] the name of the directory within the zip file when container is `zip`.
* @returns {Sharp} * @returns {Sharp}
* @throws {Error} Invalid parameters * @throws {Error} Invalid parameters
*/ */
@ -1050,6 +1065,14 @@ function tile (options) {
throw is.invalidParameterError('id', 'string', options.id); throw is.invalidParameterError('id', 'string', options.id);
} }
} }
// Basename for zip container
if (is.defined(options.basename)) {
if (is.string(options.basename)) {
this.options.tileBasename = options.basename;
} else {
throw is.invalidParameterError('basename', 'string', options.basename);
}
}
} }
// Format // Format
if (is.inArray(this.options.formatOut, ['jpeg', 'png', 'webp'])) { if (is.inArray(this.options.formatOut, ['jpeg', 'png', 'webp'])) {

View File

@ -941,6 +941,19 @@ class PipelineWorker : public Napi::AsyncWorker {
area->free_fn = nullptr; area->free_fn = nullptr;
vips_area_unref(area); vips_area_unref(area);
baton->formatOut = "heif"; baton->formatOut = "heif";
} else if (baton->formatOut == "dz") {
// Write DZ to buffer
baton->tileContainer = VIPS_FOREIGN_DZ_CONTAINER_ZIP;
if (!sharp::HasAlpha(image)) {
baton->tileBackground.pop_back();
}
vips::VOption *options = BuildOptionsDZ(baton);
VipsArea *area = reinterpret_cast<VipsArea*>(image.dzsave_buffer(options));
baton->bufferOut = static_cast<char*>(area->data);
baton->bufferOutLength = area->length;
area->free_fn = nullptr;
vips_area_unref(area);
baton->formatOut = "dz";
} else if (baton->formatOut == "raw" || } else if (baton->formatOut == "raw" ||
(baton->formatOut == "input" && inputImageType == sharp::ImageType::RAW)) { (baton->formatOut == "input" && inputImageType == sharp::ImageType::RAW)) {
// Write raw, uncompressed image data to buffer // Write raw, uncompressed image data to buffer
@ -1096,66 +1109,14 @@ class PipelineWorker : public Napi::AsyncWorker {
->set("lossless", baton->heifLossless)); ->set("lossless", baton->heifLossless));
baton->formatOut = "heif"; baton->formatOut = "heif";
} else if (baton->formatOut == "dz" || isDz || isDzZip) { } else if (baton->formatOut == "dz" || isDz || isDzZip) {
// Write DZ to file
if (isDzZip) { if (isDzZip) {
baton->tileContainer = VIPS_FOREIGN_DZ_CONTAINER_ZIP; 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)},
{"alpha_q", std::to_string(baton->webpAlphaQuality)},
{"lossless", baton->webpLossless ? "TRUE" : "FALSE"},
{"near_lossless", baton->webpNearLossless ? "TRUE" : "FALSE"},
{"smart_subsample", baton->webpSmartSubsample ? "TRUE" : "FALSE"},
{"min_size", baton->webpMinSize ? "TRUE" : "FALSE"},
{"mixed", baton->webpMixed ? "TRUE" : "FALSE"},
{"effort", std::to_string(baton->webpEffort)}
};
suffix = AssembleSuffixString(".webp", options);
} else {
std::vector<std::pair<std::string, std::string>> options {
{"Q", std::to_string(baton->jpegQuality)},
{"interlace", baton->jpegProgressive ? "TRUE" : "FALSE"},
{"subsample_mode", baton->jpegChromaSubsampling == "4:4:4" ? "off" : "on"},
{"trellis_quant", baton->jpegTrellisQuantisation ? "TRUE" : "FALSE"},
{"quant_table", std::to_string(baton->jpegQuantisationTable)},
{"overshoot_deringing", baton->jpegOvershootDeringing ? "TRUE": "FALSE"},
{"optimize_scans", baton->jpegOptimiseScans ? "TRUE": "FALSE"},
{"optimize_coding", baton->jpegOptimiseCoding ? "TRUE": "FALSE"}
};
std::string extname = baton->tileLayout == VIPS_FOREIGN_DZ_LAYOUT_DZ ? ".jpeg" : ".jpg";
suffix = AssembleSuffixString(extname, options);
}
// Remove alpha channel from tile background if image does not contain an alpha channel
if (!sharp::HasAlpha(image)) { if (!sharp::HasAlpha(image)) {
baton->tileBackground.pop_back(); baton->tileBackground.pop_back();
} }
// Write DZ to file vips::VOption *options = BuildOptionsDZ(baton);
vips::VOption *options = VImage::option()
->set("strip", !baton->withMetadata)
->set("tile_size", baton->tileSize)
->set("overlap", baton->tileOverlap)
->set("container", baton->tileContainer)
->set("layout", baton->tileLayout)
->set("suffix", const_cast<char*>(suffix.data()))
->set("angle", CalculateAngleRotation(baton->tileAngle))
->set("background", baton->tileBackground)
->set("centre", baton->tileCentre)
->set("id", const_cast<char*>(baton->tileId.data()))
->set("skip_blanks", baton->tileSkipBlanks);
// libvips chooses a default depth based on layout. Instead of replicating that logic here by
// not passing anything - libvips will handle choice
if (baton->tileDepth < VIPS_FOREIGN_DZ_DEPTH_LAST) {
options->set("depth", baton->tileDepth);
}
image.dzsave(const_cast<char*>(baton->fileOut.data()), options); image.dzsave(const_cast<char*>(baton->fileOut.data()), options);
baton->formatOut = "dz"; baton->formatOut = "dz";
} else if (baton->formatOut == "v" || (mightMatchInput && isV) || } else if (baton->formatOut == "v" || (mightMatchInput && isV) ||
@ -1312,7 +1273,7 @@ class PipelineWorker : public Napi::AsyncWorker {
/* /*
Assemble the suffix argument to dzsave, which is the format (by extname) Assemble the suffix argument to dzsave, which is the format (by extname)
alongisde comma-separated arguments to the corresponding `formatsave` vips alongside comma-separated arguments to the corresponding `formatsave` vips
action. action.
*/ */
std::string std::string
@ -1327,6 +1288,67 @@ class PipelineWorker : public Napi::AsyncWorker {
return extname + "[" + argument + "]"; return extname + "[" + argument + "]";
} }
/*
Build VOption for dzsave
*/
vips::VOption*
BuildOptionsDZ(PipelineBaton *baton) {
// 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)},
{"alpha_q", std::to_string(baton->webpAlphaQuality)},
{"lossless", baton->webpLossless ? "TRUE" : "FALSE"},
{"near_lossless", baton->webpNearLossless ? "TRUE" : "FALSE"},
{"smart_subsample", baton->webpSmartSubsample ? "TRUE" : "FALSE"},
{"min_size", baton->webpMinSize ? "TRUE" : "FALSE"},
{"mixed", baton->webpMixed ? "TRUE" : "FALSE"},
{"effort", std::to_string(baton->webpEffort)}
};
suffix = AssembleSuffixString(".webp", options);
} else {
std::vector<std::pair<std::string, std::string>> options {
{"Q", std::to_string(baton->jpegQuality)},
{"interlace", baton->jpegProgressive ? "TRUE" : "FALSE"},
{"subsample_mode", baton->jpegChromaSubsampling == "4:4:4" ? "off" : "on"},
{"trellis_quant", baton->jpegTrellisQuantisation ? "TRUE" : "FALSE"},
{"quant_table", std::to_string(baton->jpegQuantisationTable)},
{"overshoot_deringing", baton->jpegOvershootDeringing ? "TRUE": "FALSE"},
{"optimize_scans", baton->jpegOptimiseScans ? "TRUE": "FALSE"},
{"optimize_coding", baton->jpegOptimiseCoding ? "TRUE": "FALSE"}
};
std::string extname = baton->tileLayout == VIPS_FOREIGN_DZ_LAYOUT_DZ ? ".jpeg" : ".jpg";
suffix = AssembleSuffixString(extname, options);
}
vips::VOption *options = VImage::option()
->set("strip", !baton->withMetadata)
->set("tile_size", baton->tileSize)
->set("overlap", baton->tileOverlap)
->set("container", baton->tileContainer)
->set("layout", baton->tileLayout)
->set("suffix", const_cast<char*>(suffix.data()))
->set("angle", CalculateAngleRotation(baton->tileAngle))
->set("background", baton->tileBackground)
->set("centre", baton->tileCentre)
->set("id", const_cast<char*>(baton->tileId.data()))
->set("skip_blanks", baton->tileSkipBlanks);
if (baton->tileDepth < VIPS_FOREIGN_DZ_DEPTH_LAST) {
options->set("depth", baton->tileDepth);
}
if (!baton->tileBasename.empty()) {
options->set("basename", const_cast<char*>(baton->tileBasename.data()));
}
return options;
}
/* /*
Clear all thread-local data. Clear all thread-local data.
*/ */
@ -1600,6 +1622,7 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) {
sharp::AttrAsStr(options, "tileDepth").data())); sharp::AttrAsStr(options, "tileDepth").data()));
baton->tileCentre = sharp::AttrAsBool(options, "tileCentre"); baton->tileCentre = sharp::AttrAsBool(options, "tileCentre");
baton->tileId = sharp::AttrAsStr(options, "tileId"); baton->tileId = sharp::AttrAsStr(options, "tileId");
baton->tileBasename = sharp::AttrAsStr(options, "tileBasename");
// Force random access for certain operations // Force random access for certain operations
if (baton->input->access == VIPS_ACCESS_SEQUENTIAL) { if (baton->input->access == VIPS_ACCESS_SEQUENTIAL) {

View File

@ -212,6 +212,7 @@ struct PipelineBaton {
int tileSkipBlanks; int tileSkipBlanks;
VipsForeignDzDepth tileDepth; VipsForeignDzDepth tileDepth;
std::string tileId; std::string tileId;
std::string tileBasename;
std::unique_ptr<double[]> recombMatrix; std::unique_ptr<double[]> recombMatrix;
PipelineBaton(): PipelineBaton():

View File

@ -317,6 +317,14 @@ describe('Tile', function () {
}); });
}); });
it('Invalid basename parameter value fails', function () {
assert.throws(function () {
sharp().tile({
basename: true
});
});
});
it('Deep Zoom layout', function (done) { it('Deep Zoom layout', function (done) {
const directory = fixtures.path('output.dzi_files'); const directory = fixtures.path('output.dzi_files');
rimraf(directory, function () { rimraf(directory, function () {
@ -952,4 +960,33 @@ describe('Tile', function () {
}); });
}); });
}); });
it('Write ZIP container to Buffer', function (done) {
const container = fixtures.path('output.dz.tiles.zip');
const extractTo = fixtures.path('output.dz.tiles');
const directory = path.join(extractTo, 'output.dz.tiles_files');
rimraf(directory, function () {
sharp(fixtures.inputJpg)
.tile({ basename: 'output.dz.tiles' })
.toBuffer(function (err, data, 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);
fs.writeFileSync(container, data);
fs.stat(container, function (err, stat) {
if (err) throw err;
assert.strictEqual(true, stat.isFile());
assert.strictEqual(true, stat.size > 0);
extractZip(container, { dir: path.dirname(extractTo) })
.then(() => {
assertDeepZoomTiles(directory, 256, 13, done);
})
.catch(done);
});
});
});
});
}); });