mirror of
https://github.com/lovell/sharp.git
synced 2025-07-09 10:30:15 +02:00
Allow PNG and WebP tile-based output in addition to JPEG (#622)
This commit is contained in:
parent
6b426014ad
commit
bc84d1e47a
@ -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');
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -28,7 +28,8 @@
|
|||||||
"F. Orlando Galashan <frulo@gmx.de>",
|
"F. Orlando Galashan <frulo@gmx.de>",
|
||||||
"Kleis Auke Wolthuizen <info@kleisauke.nl>",
|
"Kleis Auke Wolthuizen <info@kleisauke.nl>",
|
||||||
"Matt Hirsch <mhirsch@media.mit.edu>",
|
"Matt Hirsch <mhirsch@media.mit.edu>",
|
||||||
"Matthias Thoemmes <thoemmes@gmail.com>"
|
"Matthias Thoemmes <thoemmes@gmail.com>",
|
||||||
|
"Patrick Paskaris <patrick@paskaris.gr>"
|
||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"clean": "rm -rf node_modules/ build/ vendor/ coverage/ test/fixtures/output.*",
|
"clean": "rm -rf node_modules/ build/ vendor/ coverage/ test/fixtures/output.*",
|
||||||
|
@ -846,6 +846,35 @@ class PipelineWorker : public Nan::AsyncWorker {
|
|||||||
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)}
|
||||||
|
};
|
||||||
|
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
|
// Write DZ to file
|
||||||
image.dzsave(const_cast<char*>(baton->fileOut.data()), VImage::option()
|
image.dzsave(const_cast<char*>(baton->fileOut.data()), VImage::option()
|
||||||
->set("strip", !baton->withMetadata)
|
->set("strip", !baton->withMetadata)
|
||||||
@ -853,6 +882,7 @@ class PipelineWorker : public Nan::AsyncWorker {
|
|||||||
->set("overlap", baton->tileOverlap)
|
->set("overlap", baton->tileOverlap)
|
||||||
->set("container", baton->tileContainer)
|
->set("container", baton->tileContainer)
|
||||||
->set("layout", baton->tileLayout)
|
->set("layout", baton->tileLayout)
|
||||||
|
->set("suffix", const_cast<char*>(suffix.data()))
|
||||||
);
|
);
|
||||||
baton->formatOut = "dz";
|
baton->formatOut = "dz";
|
||||||
} else if (baton->formatOut == "v" || isV || (matchInput && inputImageType == ImageType::VIPS)) {
|
} 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);
|
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.
|
Clear all thread-local data.
|
||||||
*/
|
*/
|
||||||
@ -1167,6 +1214,7 @@ NAN_METHOD(pipeline) {
|
|||||||
} else {
|
} else {
|
||||||
baton->tileLayout = VIPS_FOREIGN_DZ_LAYOUT_DZ;
|
baton->tileLayout = VIPS_FOREIGN_DZ_LAYOUT_DZ;
|
||||||
}
|
}
|
||||||
|
baton->tileFormat = AttrAsStr(options, "tileFormat");
|
||||||
|
|
||||||
// Function to notify of queue length changes
|
// Function to notify of queue length changes
|
||||||
Nan::Callback *queueListener = new Nan::Callback(AttrAs<v8::Function>(options, "queueListener"));
|
Nan::Callback *queueListener = new Nan::Callback(AttrAs<v8::Function>(options, "queueListener"));
|
||||||
|
@ -102,6 +102,7 @@ struct PipelineBaton {
|
|||||||
int tileOverlap;
|
int tileOverlap;
|
||||||
VipsForeignDzContainer tileContainer;
|
VipsForeignDzContainer tileContainer;
|
||||||
VipsForeignDzLayout tileLayout;
|
VipsForeignDzLayout tileLayout;
|
||||||
|
std::string tileFormat;
|
||||||
|
|
||||||
PipelineBaton():
|
PipelineBaton():
|
||||||
input(nullptr),
|
input(nullptr),
|
||||||
|
@ -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 () {
|
it('Prevent larger overlap than default size', function () {
|
||||||
assert.throws(function () {
|
assert.throws(function () {
|
||||||
sharp().tile({overlap: 257});
|
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) {
|
it('Write to ZIP container using file extension', function (done) {
|
||||||
const container = fixtures.path('output.dz.container.zip');
|
const container = fixtures.path('output.dz.container.zip');
|
||||||
const extractTo = fixtures.path('output.dz.container');
|
const extractTo = fixtures.path('output.dz.container');
|
||||||
|
Loading…
x
Reference in New Issue
Block a user