Selected output format > unknown file extension #344

This commit is contained in:
Lovell Fuller 2016-02-07 20:13:13 +00:00
parent 5c1067c63f
commit 677b2b9089
8 changed files with 211 additions and 157 deletions

View File

@ -396,7 +396,9 @@ sharp('input.png')
#### toFile(path, [callback])
`path` is a String containing the path to write the image data to. The format is inferred from the extension, with JPEG, PNG, WebP, TIFF and DZI supported.
`path` is a String containing the path to write the image data to.
If an explicit output format is not selected, it will be inferred from the extension, with JPEG, PNG, WebP, TIFF and DZI supported.
`callback`, if present, is called with two arguments `(err, info)` where:

View File

@ -25,6 +25,10 @@
[#340](https://github.com/lovell/sharp/issues/340)
[@janaz](https://github.com/janaz)
* Ensure selected format takes precedence over any unknown output filename extension.
[#344](https://github.com/lovell/sharp/issues/344)
[@ubaltaci](https://github.com/ubaltaci)
* Add support for libvips' PBM, PGM, PPM and FITS image format loaders.
[#347](https://github.com/lovell/sharp/issues/347)
[@oaleynik](https://github.com/oaleynik)

View File

@ -86,7 +86,8 @@ var Sharp = function(input, options) {
// overlay
overlayPath: '',
// output options
output: '__input',
formatOut: 'input',
fileOut: '',
progressive: false,
quality: 80,
compressionLevel: 6,
@ -667,8 +668,8 @@ Sharp.prototype.limitInputPixels = function(limit) {
/*
Write output image data to a file
*/
Sharp.prototype.toFile = function(output, callback) {
if (!output || output.length === 0) {
Sharp.prototype.toFile = function(fileOut, callback) {
if (!fileOut || fileOut.length === 0) {
var errOutputInvalid = new Error('Invalid output');
if (typeof callback === 'function') {
callback(errOutputInvalid);
@ -676,7 +677,7 @@ Sharp.prototype.toFile = function(output, callback) {
return BluebirdPromise.reject(errOutputInvalid);
}
} else {
if (this.options.fileIn === output) {
if (this.options.fileIn === fileOut) {
var errOutputIsInput = new Error('Cannot use same file for input and output');
if (typeof callback === 'function') {
callback(errOutputIsInput);
@ -684,7 +685,7 @@ Sharp.prototype.toFile = function(output, callback) {
return BluebirdPromise.reject(errOutputIsInput);
}
} else {
this.options.output = output;
this.options.fileOut = fileOut;
return this._pipeline(callback);
}
}
@ -702,7 +703,7 @@ Sharp.prototype.toBuffer = function(callback) {
Force JPEG output
*/
Sharp.prototype.jpeg = function() {
this.options.output = '__jpeg';
this.options.formatOut = 'jpeg';
return this;
};
@ -710,7 +711,7 @@ Sharp.prototype.jpeg = function() {
Force PNG output
*/
Sharp.prototype.png = function() {
this.options.output = '__png';
this.options.formatOut = 'png';
return this;
};
@ -718,7 +719,7 @@ Sharp.prototype.png = function() {
Force WebP output
*/
Sharp.prototype.webp = function() {
this.options.output = '__webp';
this.options.formatOut = 'webp';
return this;
};
@ -726,7 +727,7 @@ Sharp.prototype.webp = function() {
Force raw, uint8 output
*/
Sharp.prototype.raw = function() {
this.options.output = '__raw';
this.options.formatOut = 'raw';
return this;
};
@ -734,15 +735,17 @@ Sharp.prototype.raw = function() {
Force output to a given format
@param format is either the id as a String or an Object with an 'id' attribute
*/
Sharp.prototype.toFormat = function(format) {
var id = format;
if (typeof format === 'object') {
id = format.id;
Sharp.prototype.toFormat = function(formatOut) {
if (isObject(formatOut) && isDefined(formatOut.id)) {
formatOut = formatOut.id;
}
if (typeof id === 'string' && typeof module.exports.format[id] === 'object' && typeof this[id] === 'function') {
this[id]();
if (
isDefined(formatOut) &&
['jpeg', 'png', 'webp', 'raw', 'tiff', 'dz', 'input'].indexOf(formatOut) !== -1
) {
this.options.formatOut = formatOut;
} else {
throw new Error('Unsupported format ' + format);
throw new Error('Unsupported output format ' + formatOut);
}
return this;
};

View File

@ -53,6 +53,26 @@ namespace sharp {
return EndsWith(str, ".dzi") || EndsWith(str, ".DZI");
}
/*
Provide a string identifier for the given image type.
*/
std::string ImageTypeId(ImageType const imageType) {
std::string id;
switch (imageType) {
case ImageType::JPEG: id = "jpeg"; break;
case ImageType::PNG: id = "png"; break;
case ImageType::WEBP: id = "webp"; break;
case ImageType::TIFF: id = "tiff"; break;
case ImageType::MAGICK: id = "magick"; break;
case ImageType::OPENSLIDE: id = "openslide"; break;
case ImageType::PPM: id = "ppm"; break;
case ImageType::FITS: id = "fits"; break;
case ImageType::RAW: id = "raw"; break;
case ImageType::UNKNOWN: id = "unknown"; break;
}
return id;
}
/*
Determine image format of a buffer.
*/

View File

@ -34,6 +34,11 @@ namespace sharp {
bool IsTiff(std::string const &str);
bool IsDz(std::string const &str);
/*
Provide a string identifier for the given image type.
*/
std::string ImageTypeId(ImageType const imageType);
/*
Determine image format of a buffer.
*/

View File

@ -33,6 +33,7 @@ using vips::VImage;
using vips::VError;
using sharp::ImageType;
using sharp::ImageTypeId;
using sharp::DetermineImageType;
using sharp::HasProfile;
using sharp::HasAlpha;
@ -113,18 +114,7 @@ class MetadataWorker : public AsyncWorker {
}
if (imageType != ImageType::UNKNOWN) {
// Image type
switch (imageType) {
case ImageType::JPEG: baton->format = "jpeg"; break;
case ImageType::PNG: baton->format = "png"; break;
case ImageType::WEBP: baton->format = "webp"; break;
case ImageType::TIFF: baton->format = "tiff"; break;
case ImageType::MAGICK: baton->format = "magick"; break;
case ImageType::OPENSLIDE: baton->format = "openslide"; break;
case ImageType::PPM: baton->format = "ppm"; break;
case ImageType::FITS: baton->format = "fits"; break;
case ImageType::RAW: baton->format = "raw"; break;
case ImageType::UNKNOWN: break;
}
baton->format = ImageTypeId(imageType);
// VipsImage attributes
baton->width = image.width();
baton->height = image.height();

View File

@ -49,6 +49,7 @@ using sharp::Blur;
using sharp::Sharpen;
using sharp::ImageType;
using sharp::ImageTypeId;
using sharp::DetermineImageType;
using sharp::HasProfile;
using sharp::HasAlpha;
@ -82,8 +83,8 @@ struct PipelineBaton {
int rawWidth;
int rawHeight;
int rawChannels;
std::string output;
std::string outputFormat;
std::string formatOut;
std::string fileOut;
void *bufferOut;
size_t bufferOutLength;
int topOffsetPre;
@ -139,7 +140,8 @@ struct PipelineBaton {
rawWidth(0),
rawHeight(0),
rawChannels(0),
outputFormat(""),
formatOut(""),
fileOut(""),
bufferOutLength(0),
topOffsetPre(-1),
topOffsetPost(-1),
@ -707,7 +709,9 @@ class PipelineWorker : public AsyncWorker {
}
// Output
if (baton->output == "__jpeg" || (baton->output == "__input" && inputImageType == ImageType::JPEG)) {
if (baton->fileOut == "") {
// Buffer output
if (baton->formatOut == "jpeg" || (baton->formatOut == "input" && inputImageType == ImageType::JPEG)) {
// Write JPEG to buffer
baton->bufferOut = static_cast<char*>(const_cast<void*>(vips_blob_get(image.jpegsave_buffer(VImage::option()
->set("strip", !baton->withMetadata)
@ -719,8 +723,8 @@ class PipelineWorker : public AsyncWorker {
->set("optimize_scans", baton->optimiseScans)
->set("interlace", baton->progressive)
), &baton->bufferOutLength)));
baton->outputFormat = "jpeg";
} else if (baton->output == "__png" || (baton->output == "__input" && inputImageType == ImageType::PNG)) {
baton->formatOut = "jpeg";
} else if (baton->formatOut == "png" || (baton->formatOut == "input" && inputImageType == ImageType::PNG)) {
// Write PNG to buffer
baton->bufferOut = static_cast<char*>(const_cast<void*>(vips_blob_get(image.pngsave_buffer(VImage::option()
->set("strip", !baton->withMetadata)
@ -728,15 +732,15 @@ class PipelineWorker : public AsyncWorker {
->set("interlace", baton->progressive)
->set("filter", baton->withoutAdaptiveFiltering ? VIPS_FOREIGN_PNG_FILTER_NONE : VIPS_FOREIGN_PNG_FILTER_ALL)
), &baton->bufferOutLength)));
baton->outputFormat = "png";
} else if (baton->output == "__webp" || (baton->output == "__input" && inputImageType == ImageType::WEBP)) {
baton->formatOut = "png";
} else if (baton->formatOut == "webp" || (baton->formatOut == "input" && inputImageType == ImageType::WEBP)) {
// Write WEBP to buffer
baton->bufferOut = static_cast<char*>(const_cast<void*>(vips_blob_get(image.webpsave_buffer(VImage::option()
->set("strip", !baton->withMetadata)
->set("Q", baton->quality)
), &baton->bufferOutLength)));
baton->outputFormat = "webp";
} else if (baton->output == "__raw") {
baton->formatOut = "webp";
} else if (baton->formatOut == "raw") {
// Write raw, uncompressed image data to buffer
if (baton->greyscale || image.interpretation() == VIPS_INTERPRETATION_B_W) {
// Extract first band for greyscale image
@ -752,17 +756,28 @@ class PipelineWorker : public AsyncWorker {
(baton->err).append("Could not allocate enough memory for raw output");
return Error();
}
baton->outputFormat = "raw";
baton->formatOut = "raw";
} else {
bool outputJpeg = IsJpeg(baton->output);
bool outputPng = IsPng(baton->output);
bool outputWebp = IsWebp(baton->output);
bool outputTiff = IsTiff(baton->output);
bool outputDz = IsDz(baton->output);
bool matchInput = !(outputJpeg || outputPng || outputWebp || outputTiff || outputDz);
if (outputJpeg || (matchInput && inputImageType == ImageType::JPEG)) {
// Unsupported output format
(baton->err).append("Unsupported output format ");
if (baton->formatOut == "input") {
(baton->err).append(ImageTypeId(inputImageType));
} else {
(baton->err).append(baton->formatOut);
}
return Error();
}
} else {
// File output
bool isJpeg = IsJpeg(baton->fileOut);
bool isPng = IsPng(baton->fileOut);
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);
if (baton->formatOut == "jpeg" || isJpeg || (matchInput && inputImageType == ImageType::JPEG)) {
// Write JPEG to file
image.jpegsave(const_cast<char*>(baton->output.data()), VImage::option()
image.jpegsave(const_cast<char*>(baton->fileOut.data()), VImage::option()
->set("strip", !baton->withMetadata)
->set("Q", baton->quality)
->set("optimize_coding", TRUE)
@ -772,41 +787,42 @@ class PipelineWorker : public AsyncWorker {
->set("optimize_scans", baton->optimiseScans)
->set("interlace", baton->progressive)
);
baton->outputFormat = "jpeg";
} else if (outputPng || (matchInput && inputImageType == ImageType::PNG)) {
baton->formatOut = "jpeg";
} else if (baton->formatOut == "png" || isPng || (matchInput && inputImageType == ImageType::PNG)) {
// Write PNG to file
image.pngsave(const_cast<char*>(baton->output.data()), VImage::option()
image.pngsave(const_cast<char*>(baton->fileOut.data()), VImage::option()
->set("strip", !baton->withMetadata)
->set("compression", baton->compressionLevel)
->set("interlace", baton->progressive)
->set("filter", baton->withoutAdaptiveFiltering ? VIPS_FOREIGN_PNG_FILTER_NONE : VIPS_FOREIGN_PNG_FILTER_ALL)
);
baton->outputFormat = "png";
} else if (outputWebp || (matchInput && inputImageType == ImageType::WEBP)) {
baton->formatOut = "png";
} else if (baton->formatOut == "webp" || isWebp || (matchInput && inputImageType == ImageType::WEBP)) {
// Write WEBP to file
image.webpsave(const_cast<char*>(baton->output.data()), VImage::option()
image.webpsave(const_cast<char*>(baton->fileOut.data()), VImage::option()
->set("strip", !baton->withMetadata)
->set("Q", baton->quality)
);
baton->outputFormat = "webp";
} else if (outputTiff || (matchInput && inputImageType == ImageType::TIFF)) {
baton->formatOut = "webp";
} else if (baton->formatOut == "tiff" || isTiff || (matchInput && inputImageType == ImageType::TIFF)) {
// Write TIFF to file
image.tiffsave(const_cast<char*>(baton->output.data()), VImage::option()
image.tiffsave(const_cast<char*>(baton->fileOut.data()), VImage::option()
->set("strip", !baton->withMetadata)
->set("Q", baton->quality)
->set("compression", VIPS_FOREIGN_TIFF_COMPRESSION_JPEG)
);
baton->outputFormat = "tiff";
} else if (outputDz) {
baton->formatOut = "tiff";
} else if (baton->formatOut == "dz" || IsDz(baton->fileOut)) {
// Write DZ to file
image.dzsave(const_cast<char*>(baton->output.data()), VImage::option()
image.dzsave(const_cast<char*>(baton->fileOut.data()), VImage::option()
->set("strip", !baton->withMetadata)
->set("tile_size", baton->tileSize)
->set("overlap", baton->tileOverlap)
);
baton->outputFormat = "dz";
baton->formatOut = "dz";
} else {
(baton->err).append("Unsupported output " + baton->output);
// Unsupported output format
(baton->err).append("Unsupported output format " + baton->fileOut);
return Error();
}
}
@ -840,7 +856,7 @@ class PipelineWorker : public AsyncWorker {
}
// Info Object
Local<Object> info = New<Object>();
Set(info, New("format").ToLocalChecked(), New<String>(baton->outputFormat).ToLocalChecked());
Set(info, New("format").ToLocalChecked(), New<String>(baton->formatOut).ToLocalChecked());
Set(info, New("width").ToLocalChecked(), New<Uint32>(static_cast<uint32_t>(width)));
Set(info, New("height").ToLocalChecked(), New<Uint32>(static_cast<uint32_t>(height)));
Set(info, New("channels").ToLocalChecked(), New<Uint32>(static_cast<uint32_t>(baton->channels)));
@ -856,7 +872,7 @@ class PipelineWorker : public AsyncWorker {
} else {
// Add file size to info
GStatBuf st;
g_stat(baton->output.data(), &st);
g_stat(baton->fileOut.data(), &st);
Set(info, New("size").ToLocalChecked(), New<Uint32>(static_cast<uint32_t>(st.st_size)));
argv[1] = info;
}
@ -1091,8 +1107,9 @@ NAN_METHOD(pipeline) {
baton->optimiseScans = attrAs<bool>(options, "optimiseScans");
baton->withMetadata = attrAs<bool>(options, "withMetadata");
baton->withMetadataOrientation = attrAs<int32_t>(options, "withMetadataOrientation");
// Output filename or __format for Buffer
baton->output = attrAsStr(options, "output");
// Output
baton->formatOut = attrAsStr(options, "formatOut");
baton->fileOut = attrAsStr(options, "fileOut");
baton->tileSize = attrAs<int32_t>(options, "tileSize");
baton->tileOverlap = attrAs<int32_t>(options, "tileOverlap");
// Function to notify of queue length changes

View File

@ -392,10 +392,12 @@ describe('Input/output', function() {
});
});
describe('Output filename without extension uses input format', function() {
describe('Output filename with unknown extension', function() {
it('JPEG', function(done) {
sharp(fixtures.inputJpg).resize(320, 80).toFile(fixtures.outputZoinks, function(err, info) {
it('Match JPEG input', function(done) {
sharp(fixtures.inputJpg)
.resize(320, 80)
.toFile(fixtures.outputZoinks, function(err, info) {
if (err) throw err;
assert.strictEqual(true, info.size > 0);
assert.strictEqual('jpeg', info.format);
@ -406,8 +408,10 @@ describe('Input/output', function() {
});
});
it('PNG', function(done) {
sharp(fixtures.inputPng).resize(320, 80).toFile(fixtures.outputZoinks, function(err, info) {
it('Match PNG input', function(done) {
sharp(fixtures.inputPng)
.resize(320, 80)
.toFile(fixtures.outputZoinks, function(err, info) {
if (err) throw err;
assert.strictEqual(true, info.size > 0);
assert.strictEqual('png', info.format);
@ -418,20 +422,11 @@ describe('Input/output', function() {
});
});
it('Transparent PNG', function(done) {
sharp(fixtures.inputPngWithTransparency).resize(320, 80).toFile(fixtures.outputZoinks, function(err, info) {
if (err) throw err;
assert.strictEqual(true, info.size > 0);
assert.strictEqual('png', info.format);
assert.strictEqual(320, info.width);
assert.strictEqual(80, info.height);
done();
});
});
if (sharp.format.webp.input.file) {
it('WebP', function(done) {
sharp(fixtures.inputWebP).resize(320, 80).toFile(fixtures.outputZoinks, function(err, info) {
it('Match WebP input', function(done) {
sharp(fixtures.inputWebP)
.resize(320, 80)
.toFile(fixtures.outputZoinks, function(err, info) {
if (err) throw err;
assert.strictEqual(true, info.size > 0);
assert.strictEqual('webp', info.format);
@ -443,8 +438,10 @@ describe('Input/output', function() {
});
}
it('TIFF', function(done) {
sharp(fixtures.inputTiff).resize(320, 80).toFile(fixtures.outputZoinks, function(err, info) {
it('Match TIFF input', function(done) {
sharp(fixtures.inputTiff)
.resize(320, 80)
.toFile(fixtures.outputZoinks, function(err, info) {
if (err) throw err;
assert.strictEqual(true, info.size > 0);
assert.strictEqual('tiff', info.format);
@ -455,13 +452,29 @@ describe('Input/output', function() {
});
});
it('Fail with GIF', function(done) {
sharp(fixtures.inputGif).resize(320, 80).toFile(fixtures.outputZoinks, function(err) {
it('Match GIF input, therefore fail', function(done) {
sharp(fixtures.inputGif)
.resize(320, 80)
.toFile(fixtures.outputZoinks, function(err) {
assert(!!err);
done();
});
});
it('Force JPEG format for PNG input', function(done) {
sharp(fixtures.inputPng)
.resize(320, 80)
.jpeg()
.toFile(fixtures.outputZoinks, function(err, info) {
if (err) throw err;
assert.strictEqual(true, info.size > 0);
assert.strictEqual('jpeg', info.format);
assert.strictEqual(320, info.width);
assert.strictEqual(80, info.height);
fs.unlinkSync(fixtures.outputZoinks);
done();
});
});
});
describe('PNG output', function() {