mirror of
https://github.com/lovell/sharp.git
synced 2025-07-09 18:40:16 +02:00
Add Buffer and Stream support to tile output #2238
This commit is contained in:
parent
3e327a586c
commit
b46ab510da
@ -544,9 +544,12 @@ const data = await sharp('input.png')
|
||||
## tile
|
||||
|
||||
Use tile-based deep zoom (image pyramid) output.
|
||||
|
||||
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.
|
||||
|
||||
The container will be set to `zip` when the output is a Buffer or Stream, otherwise it will default to `fs`.
|
||||
|
||||
### Parameters
|
||||
|
||||
* `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.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.basename` **[string][2]?** the name of the directory within the zip file when container is `zip`.
|
||||
|
||||
### 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
|
||||
|
||||
Returns **Sharp**
|
||||
|
@ -15,6 +15,9 @@ Requires libvips v8.13.0
|
||||
* Use combined bounding box of alpha and non-alpha channels for `trim` operation.
|
||||
[#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.
|
||||
[#2642](https://github.com/lovell/sharp/issues/2642)
|
||||
|
||||
|
File diff suppressed because one or more lines are too long
@ -285,6 +285,7 @@ const Sharp = function (input, options) {
|
||||
tileBackground: [255, 255, 255, 255],
|
||||
tileCentre: false,
|
||||
tileId: 'https://example.com/iiif',
|
||||
tileBasename: '',
|
||||
timeoutSeconds: 0,
|
||||
linearA: 1,
|
||||
linearB: 0,
|
||||
|
@ -943,9 +943,12 @@ function raw (options) {
|
||||
|
||||
/**
|
||||
* Use tile-based deep zoom (image pyramid) output.
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* The container will be set to `zip` when the output is a Buffer or Stream, otherwise it will default to `fs`.
|
||||
*
|
||||
* @example
|
||||
* sharp('input.tiff')
|
||||
* .png()
|
||||
@ -957,6 +960,17 @@ function raw (options) {
|
||||
* // 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 {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.
|
||||
@ -969,6 +983,7 @@ function raw (options) {
|
||||
* @param {boolean} [options.centre=false] centre image in tile.
|
||||
* @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.basename] the name of the directory within the zip file when container is `zip`.
|
||||
* @returns {Sharp}
|
||||
* @throws {Error} Invalid parameters
|
||||
*/
|
||||
@ -1050,6 +1065,14 @@ function tile (options) {
|
||||
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
|
||||
if (is.inArray(this.options.formatOut, ['jpeg', 'png', 'webp'])) {
|
||||
|
133
src/pipeline.cc
133
src/pipeline.cc
@ -941,6 +941,19 @@ class PipelineWorker : public Napi::AsyncWorker {
|
||||
area->free_fn = nullptr;
|
||||
vips_area_unref(area);
|
||||
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" ||
|
||||
(baton->formatOut == "input" && inputImageType == sharp::ImageType::RAW)) {
|
||||
// Write raw, uncompressed image data to buffer
|
||||
@ -1096,66 +1109,14 @@ class PipelineWorker : public Napi::AsyncWorker {
|
||||
->set("lossless", baton->heifLossless));
|
||||
baton->formatOut = "heif";
|
||||
} else if (baton->formatOut == "dz" || isDz || isDzZip) {
|
||||
// Write DZ to file
|
||||
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)},
|
||||
{"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)) {
|
||||
baton->tileBackground.pop_back();
|
||||
}
|
||||
// Write DZ to file
|
||||
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);
|
||||
}
|
||||
vips::VOption *options = BuildOptionsDZ(baton);
|
||||
image.dzsave(const_cast<char*>(baton->fileOut.data()), options);
|
||||
baton->formatOut = "dz";
|
||||
} 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)
|
||||
alongisde comma-separated arguments to the corresponding `formatsave` vips
|
||||
alongside comma-separated arguments to the corresponding `formatsave` vips
|
||||
action.
|
||||
*/
|
||||
std::string
|
||||
@ -1327,6 +1288,67 @@ class PipelineWorker : public Napi::AsyncWorker {
|
||||
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.
|
||||
*/
|
||||
@ -1600,6 +1622,7 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) {
|
||||
sharp::AttrAsStr(options, "tileDepth").data()));
|
||||
baton->tileCentre = sharp::AttrAsBool(options, "tileCentre");
|
||||
baton->tileId = sharp::AttrAsStr(options, "tileId");
|
||||
baton->tileBasename = sharp::AttrAsStr(options, "tileBasename");
|
||||
|
||||
// Force random access for certain operations
|
||||
if (baton->input->access == VIPS_ACCESS_SEQUENTIAL) {
|
||||
|
@ -212,6 +212,7 @@ struct PipelineBaton {
|
||||
int tileSkipBlanks;
|
||||
VipsForeignDzDepth tileDepth;
|
||||
std::string tileId;
|
||||
std::string tileBasename;
|
||||
std::unique_ptr<double[]> recombMatrix;
|
||||
|
||||
PipelineBaton():
|
||||
|
@ -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) {
|
||||
const directory = fixtures.path('output.dzi_files');
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
Loading…
x
Reference in New Issue
Block a user