From ce06b4ec8960737400095c439a04c174be750f2e Mon Sep 17 00:00:00 2001 From: Mark van Seventer Date: Fri, 14 Aug 2026 21:16:05 -0700 Subject: [PATCH] Apply format options only to output format. --- CHANGELOG.md | 5 +++++ README.md | 2 +- lib/cli.js | 21 ++++++++++++++------- lib/constants.js | 2 +- lib/convert.js | 16 ++++++++++++---- lib/index.js | 9 +++++++-- lib/queue.js | 4 ++-- test/cli.js | 6 ++++++ test/convert.js | 6 ++++++ 9 files changed, 54 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a119d62..99fbed5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## 6.0.0 + +- Fixed format-specific output options being applied to all formats ([#92](https://github.com/vseventer/sharp-cli/issues/92)). +- Replaced `jpg` with [`jpeg`](https://sharp.pixelplumbing.com/api-output#jpeg) as an output format option. + ## 5.3.0 (August 13, 2026) > Last release compatible with Node.js 18. diff --git a/README.md b/README.md index 44d09e6..1aa3cc8 100644 --- a/README.md +++ b/README.md @@ -102,7 +102,7 @@ Output Options --bigtiff Use BigTIFF variant [boolean] -c, --compressionLevel zlib compression level [number] [default: 6] -f, --format Force output to a given format - [choices: "avif", "gif", "heif", "jpeg", "jpg", "png", "tiff", "webp"] [default: input] + [choices: "avif", "gif", "heif", "jpeg", "png", "tiff", "webp"] [default: input] --keepDuplicateFrames Keep duplicate frames in the output instead of combining them [boolean] -m, --metadata, --withMetadata Include all metadata (EXIF, XMP, IPTC) from the input image in the diff --git a/lib/cli.js b/lib/cli.js index b092fe9..d294919 100644 --- a/lib/cli.js +++ b/lib/cli.js @@ -739,7 +739,8 @@ cli.parse = function recursiveParse(args, context = {}) { ) { queue.unshift([ "heif", - (sharp) => { + (sharp, { format = "heif" }) => { + if (format !== "heif") return sharp; return sharp.heif({ bitdepth: argv.hbitdepth, compression: argv.hcompression, @@ -761,7 +762,8 @@ cli.parse = function recursiveParse(args, context = {}) { ) { queue.unshift([ "avif", - (sharp) => { + (sharp, { format = "avif" }) => { + if (format !== "avif") return sharp; return sharp.avif({ chromaSubsampling: argv.chromaSubsampling, effort: argv.effort, @@ -788,7 +790,8 @@ cli.parse = function recursiveParse(args, context = {}) { ) { queue.unshift([ "gif", - (sharp) => { + (sharp, { format = "gif" }) => { + if (format !== "gif") return sharp; return sharp.gif({ colors: argv.colors, force: false, @@ -821,7 +824,8 @@ cli.parse = function recursiveParse(args, context = {}) { ) { queue.unshift([ "jpeg", - (sharp) => { + (sharp, { format = "jpeg" }) => { + if (format !== "jpeg") return sharp; return sharp.jpeg({ chromaSubsampling: argv.chromaSubsampling, force: false, @@ -850,7 +854,8 @@ cli.parse = function recursiveParse(args, context = {}) { ) { queue.unshift([ "png", - (sharp) => { + (sharp, { format = "png" }) => { + if (format !== "png") return sharp; return sharp.png({ adaptiveFiltering: argv.adaptiveFiltering, colors: argv.colors, @@ -883,7 +888,8 @@ cli.parse = function recursiveParse(args, context = {}) { ) { queue.unshift([ "tiff", - (sharp) => { + (sharp, { format = "tiff" }) => { + if (format !== "tiff") return sharp; return sharp.tiff({ background: argv.tileBackground, bigtiff: argv.bigtiff, @@ -920,7 +926,8 @@ cli.parse = function recursiveParse(args, context = {}) { ) { queue.unshift([ "webp", - (sharp) => { + (sharp, { format = "webp" }) => { + if (format !== "webp") return sharp; return sharp.webp({ alphaQuality: argv.alphaQuality, effort: argv.effort, diff --git a/lib/constants.js b/lib/constants.js index a498c98..4bf6fe6 100644 --- a/lib/constants.js +++ b/lib/constants.js @@ -35,7 +35,7 @@ export default { EXTEND_WITH: ["background", "copy", "repeat", "mirror"], FAIL_ON: ["none", "truncated", "error", "warning"], FIT: Object.keys(sharp.fit), - FORMAT: ["avif", "gif", "heif", "jpeg", "jpg", "png", "tiff", "webp"], + FORMAT: ["avif", "gif", "heif", "jpeg", "png", "tiff", "webp"], GRAVITY: Object.keys(sharp.gravity), HEIF_COMPRESSION: ["hevc", "av1"], INTERPOLATORS: Object.keys(sharp.interpolators), diff --git a/lib/convert.js b/lib/convert.js index 10c6ade..7a39b2a 100644 --- a/lib/convert.js +++ b/lib/convert.js @@ -46,10 +46,16 @@ const EXTENSIONS = { webp: ".webp", }; +// Helpers. +const getFormat = (format, src) => { + if (format) return format; + return path.extname(src).slice(1); +}; + // Exports. export default { // Convert a list of files. - files: (input, output, options) => { + files: (input, output, options, format) => { // Resolve files. const files = input.reduce((list, input) => { return list.concat(globSync(input, { absolute: true })); @@ -63,7 +69,9 @@ export default { const isBatch = files.length > 1; const promises = files.map((src) => { // Create pipeline. - const transformer = queue.drain(sharp(options)); + const transformer = queue.drain(sharp(options), { + format: getFormat(format, src), + }); // Process output as a template. const parts = path.parse(src); @@ -99,9 +107,9 @@ export default { }, // Convert a stream. - stream: (inStream, outStream, options) => { + stream: (inStream, outStream, options, format) => { // Create pipeline. - const transformer = queue.drain(sharp(options)); + const transformer = queue.drain(sharp(options), { format }); // Gather return value. const info = {}; diff --git a/lib/index.js b/lib/index.js index 10ae1f6..cfbcff1 100644 --- a/lib/index.js +++ b/lib/index.js @@ -36,9 +36,14 @@ export default (args, options = {}) => { .then((argv) => { const options = pick(argv, cli.inputOptions); if (argv.input) { - return convert.files(argv.input, argv.output, options); + return convert.files(argv.input, argv.output, options, argv.format); } - return convert.stream(process.stdin, process.stdout, options); + return convert.stream( + process.stdin, + process.stdout, + options, + argv.format, + ); }) .then((output) => { const info = Array.isArray(output) ? output : [output]; diff --git a/lib/queue.js b/lib/queue.js index 5f2ccf7..94463dd 100644 --- a/lib/queue.js +++ b/lib/queue.js @@ -28,8 +28,8 @@ const queue = []; Object.defineProperties(queue, { // Add drain handler. drain: { - value: (initialValue) => - queue.reduce((acc, [, cb]) => cb(acc), initialValue), + value: (initialValue, config = {}) => + queue.reduce((acc, [, cb]) => cb(acc, config), initialValue), }, // Add pipeline getter. diff --git a/test/cli.js b/test/cli.js index 702d3ce..28b495c 100644 --- a/test/cli.js +++ b/test/cli.js @@ -345,6 +345,12 @@ describe(`${pkg.name} [command..]`, () => { const pipeline = queue.drain(sharp()); sinon.assert.calledWithMatch(pipeline.avif, { effort }); }); + + it("must only apply the option to the selected format", () => { + const pipeline = queue.drain(sharp(), { format: "png" }); + sinon.assert.calledWithMatch(pipeline.png, { effort }); + sinon.assert.notCalled(pipeline.webp); + }); }); describe("--failOn", () => { diff --git a/test/convert.js b/test/convert.js index ff20218..106e6fb 100644 --- a/test/convert.js +++ b/test/convert.js @@ -32,6 +32,7 @@ import fs from "fs-extra"; import tempy from "tempy"; // Local modules. +import cli from "../lib/cli.js"; import convert from "../lib/convert.js"; import queue from "../lib/queue.js"; import tile from "../cmd/output.js"; @@ -75,6 +76,11 @@ describe("convert", () => { expect(info.path).to.contain(".avif"); }); }); + it("must select format options based on the input extension", async () => { + await cli.parse(["--effort", 7, "-i", input, "-o", dest]); + const [info] = await convert.files([input], dest, options); + expect(info).to.have.property("format", "jpeg"); + }); it("must convert a file and output to an existing directory", () => { // Negative test for directory that does not exist. const rand = "" + Math.random();