diff --git a/docs/api-output.md b/docs/api-output.md index 2c1b3f69..0d3c9904 100644 --- a/docs/api-output.md +++ b/docs/api-output.md @@ -116,6 +116,8 @@ Use these JPEG options for output image. - `options.progressive` **[Boolean][6]** use progressive (interlace) scan (optional, default `false`) - `options.chromaSubsampling` **[String][1]** set to '4:4:4' to prevent chroma subsampling when quality <= 90 (optional, default `'4:2:0'`) - `options.trellisQuantisation` **[Boolean][6]** apply trellis quantisation, requires mozjpeg (optional, default `false`) + - `options.quantisationTable` **[Number][8]** [quantisation table][9] to use, integer 0-8 (optional, default `0`) + - `options.quantizationTable` **[Number][8]** alternative spelling of quantisationTable (optional, default `0`) - `options.overshootDeringing` **[Boolean][6]** apply overshoot deringing, requires mozjpeg (optional, default `false`) - `options.optimiseScans` **[Boolean][6]** optimise progressive scans, forces progressive, requires mozjpeg (optional, default `false`) - `options.optimizeScans` **[Boolean][6]** alternative spelling of optimiseScans (optional, default `false`) @@ -312,3 +314,5 @@ Returns **Sharp** [7]: https://nodejs.org/api/buffer.html [8]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number + +[9]: https://jcupitt.github.io/libvips/API/current/VipsForeignSave.html#vips-jpegsave diff --git a/lib/constructor.js b/lib/constructor.js index 6ca17db6..60f1de7c 100644 --- a/lib/constructor.js +++ b/lib/constructor.js @@ -193,6 +193,7 @@ const Sharp = function (input, options) { jpegOvershootDeringing: false, jpegOptimiseScans: false, jpegOptimiseCoding: true, + jpegQuantisationTable: 0, pngProgressive: false, pngCompressionLevel: 9, pngAdaptiveFiltering: false, diff --git a/lib/output.js b/lib/output.js index ec49559b..cecdcde9 100644 --- a/lib/output.js +++ b/lib/output.js @@ -150,6 +150,8 @@ function withMetadata (withMetadata) { * @param {Boolean} [options.optimizeScans=false] - alternative spelling of optimiseScans * @param {Boolean} [options.optimiseCoding=true] - optimise Huffman coding tables * @param {Boolean} [options.optimizeCoding=true] - alternative spelling of optimiseCoding + * @param {Number} [options.quantisationTable=0] - quantization table to use, integer 0-8, requires mozjpeg + * @param {Number} [options.quantizationTable=0] - alternative spelling of quantisationTable * @param {Boolean} [options.force=true] - force JPEG output, otherwise attempt to use input format * @returns {Sharp} * @throws {Error} Invalid options @@ -191,6 +193,14 @@ function jpeg (options) { if (is.defined(options.optimiseCoding)) { this._setBooleanOption('jpegOptimiseCoding', options.optimiseCoding); } + options.quantisationTable = is.number(options.quantizationTable) ? options.quantizationTable : options.quantisationTable; + if (is.defined(options.quantisationTable)) { + if (is.integer(options.quantisationTable) && is.inRange(options.quantisationTable, 0, 8)) { + this.options.jpegQuantisationTable = options.quantisationTable; + } else { + throw new Error('Invalid quantisation table (integer, 0-8) ' + options.quantisationTable); + } + } } return this._updateFormatOut('jpeg', options); } diff --git a/src/pipeline.cc b/src/pipeline.cc index 8b5cbdf2..b7084291 100644 --- a/src/pipeline.cc +++ b/src/pipeline.cc @@ -733,6 +733,7 @@ class PipelineWorker : public Nan::AsyncWorker { ->set("interlace", baton->jpegProgressive) ->set("no_subsample", baton->jpegChromaSubsampling == "4:4:4") ->set("trellis_quant", baton->jpegTrellisQuantisation) + ->set("quant_table", baton->jpegQuantisationTable) ->set("overshoot_deringing", baton->jpegOvershootDeringing) ->set("optimize_scans", baton->jpegOptimiseScans) ->set("optimize_coding", baton->jpegOptimiseCoding))); @@ -848,6 +849,7 @@ class PipelineWorker : public Nan::AsyncWorker { ->set("interlace", baton->jpegProgressive) ->set("no_subsample", baton->jpegChromaSubsampling == "4:4:4") ->set("trellis_quant", baton->jpegTrellisQuantisation) + ->set("quant_table", baton->jpegQuantisationTable) ->set("overshoot_deringing", baton->jpegOvershootDeringing) ->set("optimize_scans", baton->jpegOptimiseScans) ->set("optimize_coding", baton->jpegOptimiseCoding)); @@ -927,6 +929,7 @@ class PipelineWorker : public Nan::AsyncWorker { {"interlace", baton->jpegProgressive ? "TRUE" : "FALSE"}, {"no_subsample", baton->jpegChromaSubsampling == "4:4:4" ? "TRUE": "FALSE"}, {"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"} @@ -1266,6 +1269,7 @@ NAN_METHOD(pipeline) { baton->jpegProgressive = AttrTo(options, "jpegProgressive"); baton->jpegChromaSubsampling = AttrAsStr(options, "jpegChromaSubsampling"); baton->jpegTrellisQuantisation = AttrTo(options, "jpegTrellisQuantisation"); + baton->jpegQuantisationTable = AttrTo(options, "jpegQuantisationTable"); baton->jpegOvershootDeringing = AttrTo(options, "jpegOvershootDeringing"); baton->jpegOptimiseScans = AttrTo(options, "jpegOptimiseScans"); baton->jpegOptimiseCoding = AttrTo(options, "jpegOptimiseCoding"); diff --git a/src/pipeline.h b/src/pipeline.h index 0e8de558..0b5c8baf 100644 --- a/src/pipeline.h +++ b/src/pipeline.h @@ -102,6 +102,7 @@ struct PipelineBaton { bool jpegProgressive; std::string jpegChromaSubsampling; bool jpegTrellisQuantisation; + int jpegQuantisationTable; bool jpegOvershootDeringing; bool jpegOptimiseScans; bool jpegOptimiseCoding; @@ -188,6 +189,7 @@ struct PipelineBaton { jpegProgressive(false), jpegChromaSubsampling("4:2:0"), jpegTrellisQuantisation(false), + jpegQuantisationTable(0), jpegOvershootDeringing(false), jpegOptimiseScans(false), jpegOptimiseCoding(true), diff --git a/test/unit/io.js b/test/unit/io.js index e31f4af1..61462c46 100644 --- a/test/unit/io.js +++ b/test/unit/io.js @@ -389,6 +389,16 @@ describe('Input/output', function () { }); }); + describe('Invalid JPEG quantisation table', function () { + [-1, 88.2, 'test'].forEach(function (table) { + it(table.toString(), function () { + assert.throws(function () { + sharp().jpeg({ quantisationTable: table }); + }); + }); + }); + }); + it('Progressive JPEG image', function (done) { sharp(fixtures.inputJpg) .resize(320, 240) @@ -856,6 +866,37 @@ describe('Input/output', function () { }); }); + it('Specifying quantisation table provides different JPEG', function (done) { + // First generate with default quantisation table + sharp(fixtures.inputJpg) + .resize(320, 240) + .jpeg({ optimiseCoding: false }) + .toBuffer(function (err, withDefaultQuantisationTable, withInfo) { + if (err) throw err; + assert.strictEqual(true, withDefaultQuantisationTable.length > 0); + assert.strictEqual(withDefaultQuantisationTable.length, withInfo.size); + assert.strictEqual('jpeg', withInfo.format); + assert.strictEqual(320, withInfo.width); + assert.strictEqual(240, withInfo.height); + // Then generate with different quantisation table + sharp(fixtures.inputJpg) + .resize(320, 240) + .jpeg({ optimiseCoding: false, quantisationTable: 3 }) + .toBuffer(function (err, withQuantTable3, withoutInfo) { + if (err) throw err; + assert.strictEqual(true, withQuantTable3.length > 0); + assert.strictEqual(withQuantTable3.length, withoutInfo.size); + assert.strictEqual('jpeg', withoutInfo.format); + assert.strictEqual(320, withoutInfo.width); + assert.strictEqual(240, withoutInfo.height); + + // Verify image is same (as mozjpeg may not be present) size or less + assert.strictEqual(true, withQuantTable3.length <= withDefaultQuantisationTable.length); + done(); + }); + }); + }); + it('Convert SVG to PNG at default 72DPI', function (done) { sharp(fixtures.inputSvg) .resize(1024)