Skip to content

Commit bc84d1e

Browse files
ppaskarislovell
authored andcommitted
Allow PNG and WebP tile-based output in addition to JPEG (lovell#622)
1 parent 6b42601 commit bc84d1e

5 files changed

Lines changed: 179 additions & 2 deletions

File tree

lib/output.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -285,7 +285,13 @@ const tile = function tile (tile) {
285285
}
286286
}
287287
}
288-
return this;
288+
// Format
289+
if (is.inArray(this.options.formatOut, ['jpeg', 'png', 'webp'])) {
290+
this.options.tileFormat = this.options.formatOut;
291+
} else if (this.options.formatOut !== 'input') {
292+
throw new Error('Invalid tile format ' + this.options.formatOut);
293+
}
294+
return this._updateFormatOut('dz');
289295
};
290296

291297
/**

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@
2828
"F. Orlando Galashan <frulo@gmx.de>",
2929
"Kleis Auke Wolthuizen <info@kleisauke.nl>",
3030
"Matt Hirsch <mhirsch@media.mit.edu>",
31-
"Matthias Thoemmes <thoemmes@gmail.com>"
31+
"Matthias Thoemmes <thoemmes@gmail.com>",
32+
"Patrick Paskaris <patrick@paskaris.gr>"
3233
],
3334
"scripts": {
3435
"clean": "rm -rf node_modules/ build/ vendor/ coverage/ test/fixtures/output.*",

src/pipeline.cc

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -846,13 +846,43 @@ class PipelineWorker : public Nan::AsyncWorker {
846846
if (isDzZip) {
847847
baton->tileContainer = VIPS_FOREIGN_DZ_CONTAINER_ZIP;
848848
}
849+
// Forward format options through suffix
850+
std::string suffix;
851+
if (baton->tileFormat == "png") {
852+
std::vector<std::pair<std::string, std::string>> options {
853+
{"interlace", baton->pngProgressive ? "TRUE" : "FALSE"},
854+
{"compression", std::to_string(baton->pngCompressionLevel)},
855+
{"filter", baton->pngAdaptiveFiltering ? "all" : "none"}
856+
};
857+
suffix = AssembleSuffixString(".png", options);
858+
} else if (baton->tileFormat == "webp") {
859+
std::vector<std::pair<std::string, std::string>> options {
860+
{"Q", std::to_string(baton->webpQuality)}
861+
};
862+
suffix = AssembleSuffixString(".webp", options);
863+
} else {
864+
std::string extname = baton->tileLayout == VIPS_FOREIGN_DZ_LAYOUT_GOOGLE
865+
|| baton->tileLayout == VIPS_FOREIGN_DZ_LAYOUT_ZOOMIFY
866+
? ".jpg" : ".jpeg";
867+
std::vector<std::pair<std::string, std::string>> options {
868+
{"Q", std::to_string(baton->jpegQuality)},
869+
{"interlace", baton->jpegProgressive ? "TRUE" : "FALSE"},
870+
{"no_subsample", baton->jpegChromaSubsampling == "4:4:4" ? "TRUE": "FALSE"},
871+
{"trellis_quant", baton->jpegTrellisQuantisation ? "TRUE" : "FALSE"},
872+
{"overshoot_deringing", baton->jpegOvershootDeringing ? "TRUE": "FALSE"},
873+
{"optimize_scans", baton->jpegOptimiseScans ? "TRUE": "FALSE"},
874+
{"optimize_coding", "TRUE"}
875+
};
876+
suffix = AssembleSuffixString(extname, options);
877+
}
849878
// Write DZ to file
850879
image.dzsave(const_cast<char*>(baton->fileOut.data()), VImage::option()
851880
->set("strip", !baton->withMetadata)
852881
->set("tile_size", baton->tileSize)
853882
->set("overlap", baton->tileOverlap)
854883
->set("container", baton->tileContainer)
855884
->set("layout", baton->tileLayout)
885+
->set("suffix", const_cast<char*>(suffix.data()))
856886
);
857887
baton->formatOut = "dz";
858888
} else if (baton->formatOut == "v" || isV || (matchInput && inputImageType == ImageType::VIPS)) {
@@ -990,6 +1020,23 @@ class PipelineWorker : public Nan::AsyncWorker {
9901020
return std::make_tuple(rotate, flip, flop);
9911021
}
9921022

1023+
/*
1024+
Assemble the suffix argument to dzsave, which is the format (by extname)
1025+
alongisde comma-separated arguments to the corresponding `formatsave` vips
1026+
action.
1027+
*/
1028+
std::string
1029+
AssembleSuffixString(std::string extname, std::vector<std::pair<std::string, std::string>> options) {
1030+
std::string argument;
1031+
for (auto const &option : options) {
1032+
if (!argument.empty()) {
1033+
argument += ",";
1034+
}
1035+
argument += option.first + "=" + option.second;
1036+
}
1037+
return extname + "[" + argument + "]";
1038+
}
1039+
9931040
/*
9941041
Clear all thread-local data.
9951042
*/
@@ -1167,6 +1214,7 @@ NAN_METHOD(pipeline) {
11671214
} else {
11681215
baton->tileLayout = VIPS_FOREIGN_DZ_LAYOUT_DZ;
11691216
}
1217+
baton->tileFormat = AttrAsStr(options, "tileFormat");
11701218

11711219
// Function to notify of queue length changes
11721220
Nan::Callback *queueListener = new Nan::Callback(AttrAs<v8::Function>(options, "queueListener"));

src/pipeline.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ struct PipelineBaton {
102102
int tileOverlap;
103103
VipsForeignDzContainer tileContainer;
104104
VipsForeignDzLayout tileLayout;
105+
std::string tileFormat;
105106

106107
PipelineBaton():
107108
input(nullptr),

test/unit/tile.js

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,22 @@ describe('Tile', function () {
128128
});
129129
});
130130

131+
it('Valid formats pass', function () {
132+
['jpeg', 'png', 'webp'].forEach(function (format) {
133+
assert.doesNotThrow(function () {
134+
sharp().toFormat(format).tile();
135+
});
136+
});
137+
});
138+
139+
it('Invalid formats fail', function () {
140+
['tiff', 'raw'].forEach(function (format) {
141+
assert.throws(function () {
142+
sharp().toFormat(format).tile();
143+
});
144+
});
145+
});
146+
131147
it('Prevent larger overlap than default size', function () {
132148
assert.throws(function () {
133149
sharp().tile({overlap: 257});
@@ -224,6 +240,111 @@ describe('Tile', function () {
224240
});
225241
});
226242

243+
it('Google layout with jpeg format', function (done) {
244+
const directory = fixtures.path('output.jpg.google.dzi');
245+
rimraf(directory, function () {
246+
sharp(fixtures.inputJpg)
247+
.jpeg({ quality: 1 })
248+
.tile({
249+
layout: 'google'
250+
})
251+
.toFile(directory, function (err, info) {
252+
if (err) throw err;
253+
assert.strictEqual('dz', info.format);
254+
assert.strictEqual(2725, info.width);
255+
assert.strictEqual(2225, info.height);
256+
assert.strictEqual(3, info.channels);
257+
assert.strictEqual('number', typeof info.size);
258+
const sample = path.join(directory, '0', '0', '0.jpg');
259+
sharp(sample).metadata(function (err, metadata) {
260+
if (err) throw err;
261+
assert.strictEqual('jpeg', metadata.format);
262+
assert.strictEqual('srgb', metadata.space);
263+
assert.strictEqual(3, metadata.channels);
264+
assert.strictEqual(false, metadata.hasProfile);
265+
assert.strictEqual(false, metadata.hasAlpha);
266+
assert.strictEqual(true, metadata.width === 256);
267+
assert.strictEqual(true, metadata.height === 256);
268+
fs.stat(sample, function (err, stat) {
269+
if (err) throw err;
270+
assert.strictEqual(true, stat.size < 2000);
271+
done();
272+
});
273+
});
274+
});
275+
});
276+
});
277+
278+
it('Google layout with png format', function (done) {
279+
const directory = fixtures.path('output.png.google.dzi');
280+
rimraf(directory, function () {
281+
sharp(fixtures.inputJpg)
282+
.png({ compressionLevel: 1 })
283+
.tile({
284+
layout: 'google'
285+
})
286+
.toFile(directory, function (err, info) {
287+
if (err) throw err;
288+
assert.strictEqual('dz', info.format);
289+
assert.strictEqual(2725, info.width);
290+
assert.strictEqual(2225, info.height);
291+
assert.strictEqual(3, info.channels);
292+
assert.strictEqual('number', typeof info.size);
293+
const sample = path.join(directory, '0', '0', '0.png');
294+
sharp(sample).metadata(function (err, metadata) {
295+
if (err) throw err;
296+
assert.strictEqual('png', metadata.format);
297+
assert.strictEqual('srgb', metadata.space);
298+
assert.strictEqual(3, metadata.channels);
299+
assert.strictEqual(false, metadata.hasProfile);
300+
assert.strictEqual(false, metadata.hasAlpha);
301+
assert.strictEqual(true, metadata.width === 256);
302+
assert.strictEqual(true, metadata.height === 256);
303+
fs.stat(sample, function (err, stat) {
304+
if (err) throw err;
305+
assert.strictEqual(true, stat.size > 44000);
306+
done();
307+
});
308+
});
309+
});
310+
});
311+
});
312+
313+
it('Google layout with webp format', function (done) {
314+
const directory = fixtures.path('output.webp.google.dzi');
315+
rimraf(directory, function () {
316+
sharp(fixtures.inputJpg)
317+
.webp({ quality: 1 })
318+
.tile({
319+
layout: 'google'
320+
})
321+
.toFile(directory, function (err, info) {
322+
if (err) throw err;
323+
assert.strictEqual('dz', info.format);
324+
assert.strictEqual(2725, info.width);
325+
assert.strictEqual(2225, info.height);
326+
assert.strictEqual(3, info.channels);
327+
assert.strictEqual('number', typeof info.size);
328+
const sample = path.join(directory, '0', '0', '0.webp');
329+
sharp(sample).metadata(function (err, metadata) {
330+
if (err) throw err;
331+
assert.strictEqual('webp', metadata.format);
332+
assert.strictEqual('srgb', metadata.space);
333+
assert.strictEqual(3, metadata.channels);
334+
assert.strictEqual(false, metadata.hasProfile);
335+
assert.strictEqual(false, metadata.hasAlpha);
336+
assert.strictEqual(true, metadata.width === 256);
337+
assert.strictEqual(true, metadata.height === 256);
338+
fs.stat(sample, function (err, stat) {
339+
if (err) throw err;
340+
assert.strictEqual(true, stat.size < 2000);
341+
done();
342+
});
343+
});
344+
});
345+
});
346+
});
347+
227348
it('Write to ZIP container using file extension', function (done) {
228349
const container = fixtures.path('output.dz.container.zip');
229350
const extractTo = fixtures.path('output.dz.container');

0 commit comments

Comments
 (0)