diff --git a/docs/api.md b/docs/api.md index 688e5260..e2c9630d 100644 --- a/docs/api.md +++ b/docs/api.md @@ -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: diff --git a/docs/changelog.md b/docs/changelog.md index ca7be0b0..812cd033 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -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) diff --git a/index.js b/index.js index ca0851ed..e74161a0 100644 --- a/index.js +++ b/index.js @@ -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; }; diff --git a/src/common.cc b/src/common.cc index 2e25934a..8fce1af4 100644 --- a/src/common.cc +++ b/src/common.cc @@ -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. */ diff --git a/src/common.h b/src/common.h index c2adf5e8..083e58c5 100644 --- a/src/common.h +++ b/src/common.h @@ -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. */ diff --git a/src/metadata.cc b/src/metadata.cc index 1d781315..196b6f54 100644 --- a/src/metadata.cc +++ b/src/metadata.cc @@ -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(); diff --git a/src/pipeline.cc b/src/pipeline.cc index b92ccd8c..655e9866 100644 --- a/src/pipeline.cc +++ b/src/pipeline.cc @@ -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,62 +709,75 @@ class PipelineWorker : public AsyncWorker { } // Output - if (baton->output == "__jpeg" || (baton->output == "__input" && inputImageType == ImageType::JPEG)) { - // Write JPEG to buffer - baton->bufferOut = static_cast(const_cast(vips_blob_get(image.jpegsave_buffer(VImage::option() - ->set("strip", !baton->withMetadata) - ->set("Q", baton->quality) - ->set("optimize_coding", TRUE) - ->set("no_subsample", baton->withoutChromaSubsampling) - ->set("trellis_quant", baton->trellisQuantisation) - ->set("overshoot_deringing", baton->overshootDeringing) - ->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)) { - // Write PNG to buffer - baton->bufferOut = static_cast(const_cast(vips_blob_get(image.pngsave_buffer(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->bufferOutLength))); - baton->outputFormat = "png"; - } else if (baton->output == "__webp" || (baton->output == "__input" && inputImageType == ImageType::WEBP)) { - // Write WEBP to buffer - baton->bufferOut = static_cast(const_cast(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") { - // Write raw, uncompressed image data to buffer - if (baton->greyscale || image.interpretation() == VIPS_INTERPRETATION_B_W) { - // Extract first band for greyscale image - image = image[0]; - } - if (image.format() != VIPS_FORMAT_UCHAR) { - // Cast pixels to uint8 (unsigned char) - image = image.cast(VIPS_FORMAT_UCHAR); - } - // Get raw image data - baton->bufferOut = static_cast(image.write_to_memory(&baton->bufferOutLength)); - if (baton->bufferOut == nullptr) { - (baton->err).append("Could not allocate enough memory for raw output"); + if (baton->fileOut == "") { + // Buffer output + if (baton->formatOut == "jpeg" || (baton->formatOut == "input" && inputImageType == ImageType::JPEG)) { + // Write JPEG to buffer + baton->bufferOut = static_cast(const_cast(vips_blob_get(image.jpegsave_buffer(VImage::option() + ->set("strip", !baton->withMetadata) + ->set("Q", baton->quality) + ->set("optimize_coding", TRUE) + ->set("no_subsample", baton->withoutChromaSubsampling) + ->set("trellis_quant", baton->trellisQuantisation) + ->set("overshoot_deringing", baton->overshootDeringing) + ->set("optimize_scans", baton->optimiseScans) + ->set("interlace", baton->progressive) + ), &baton->bufferOutLength))); + baton->formatOut = "jpeg"; + } else if (baton->formatOut == "png" || (baton->formatOut == "input" && inputImageType == ImageType::PNG)) { + // Write PNG to buffer + baton->bufferOut = static_cast(const_cast(vips_blob_get(image.pngsave_buffer(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->bufferOutLength))); + baton->formatOut = "png"; + } else if (baton->formatOut == "webp" || (baton->formatOut == "input" && inputImageType == ImageType::WEBP)) { + // Write WEBP to buffer + baton->bufferOut = static_cast(const_cast(vips_blob_get(image.webpsave_buffer(VImage::option() + ->set("strip", !baton->withMetadata) + ->set("Q", baton->quality) + ), &baton->bufferOutLength))); + 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 + image = image[0]; + } + if (image.format() != VIPS_FORMAT_UCHAR) { + // Cast pixels to uint8 (unsigned char) + image = image.cast(VIPS_FORMAT_UCHAR); + } + // Get raw image data + baton->bufferOut = static_cast(image.write_to_memory(&baton->bufferOutLength)); + if (baton->bufferOut == nullptr) { + (baton->err).append("Could not allocate enough memory for raw output"); + return Error(); + } + baton->formatOut = "raw"; + } else { + // 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(); } - baton->outputFormat = "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)) { + // 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(baton->output.data()), VImage::option() + image.jpegsave(const_cast(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(baton->output.data()), VImage::option() + image.pngsave(const_cast(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(baton->output.data()), VImage::option() + image.webpsave(const_cast(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(baton->output.data()), VImage::option() + image.tiffsave(const_cast(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(baton->output.data()), VImage::option() + image.dzsave(const_cast(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 info = New(); - Set(info, New("format").ToLocalChecked(), New(baton->outputFormat).ToLocalChecked()); + Set(info, New("format").ToLocalChecked(), New(baton->formatOut).ToLocalChecked()); Set(info, New("width").ToLocalChecked(), New(static_cast(width))); Set(info, New("height").ToLocalChecked(), New(static_cast(height))); Set(info, New("channels").ToLocalChecked(), New(static_cast(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(static_cast(st.st_size))); argv[1] = info; } @@ -1091,8 +1107,9 @@ NAN_METHOD(pipeline) { baton->optimiseScans = attrAs(options, "optimiseScans"); baton->withMetadata = attrAs(options, "withMetadata"); baton->withMetadataOrientation = attrAs(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(options, "tileSize"); baton->tileOverlap = attrAs(options, "tileOverlap"); // Function to notify of queue length changes diff --git a/test/unit/io.js b/test/unit/io.js index ddc33c88..845045ce 100644 --- a/test/unit/io.js +++ b/test/unit/io.js @@ -392,76 +392,89 @@ 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) { - 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(); - }); - }); - - it('PNG', 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); - assert.strictEqual(320, info.width); - assert.strictEqual(80, info.height); - fs.unlinkSync(fixtures.outputZoinks); - done(); - }); - }); - - 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 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('webp', info.format); + assert.strictEqual('jpeg', info.format); assert.strictEqual(320, info.width); assert.strictEqual(80, info.height); fs.unlinkSync(fixtures.outputZoinks); done(); }); + }); + + 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); + assert.strictEqual(320, info.width); + assert.strictEqual(80, info.height); + fs.unlinkSync(fixtures.outputZoinks); + done(); + }); + }); + + if (sharp.format.webp.input.file) { + 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); + assert.strictEqual(320, info.width); + assert.strictEqual(80, info.height); + fs.unlinkSync(fixtures.outputZoinks); + done(); + }); }); } - it('TIFF', 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); - assert.strictEqual(320, info.width); - assert.strictEqual(80, info.height); - fs.unlinkSync(fixtures.outputZoinks); - done(); - }); + 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); + assert.strictEqual(320, info.width); + assert.strictEqual(80, info.height); + fs.unlinkSync(fixtures.outputZoinks); + done(); + }); }); - it('Fail with GIF', function(done) { - sharp(fixtures.inputGif).resize(320, 80).toFile(fixtures.outputZoinks, function(err) { - assert(!!err); - done(); - }); + 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() {