From 5e8771e77b5163efcaa7b6effe66712d9184cb48 Mon Sep 17 00:00:00 2001 From: Mark van Seventer Date: Mon, 24 Aug 2026 07:21:22 -0700 Subject: [PATCH 1/2] Fixes #49. --- CHANGELOG.md | 4 ++++ cmd/resizing/resize.js | 32 ++++++++++++++++++++++++++------ package.json | 2 +- test/cmd/resizing/resize.js | 24 ++++++++++++++++++++---- 4 files changed, 51 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 15f620c..3f70990 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 6.1.0-dev + +- Added percentage-based dimensions to the `resize` command ([#49](https://github.com/vseventer/sharp-cli/issues/49)). + ## 6.0.0 (August 21, 2026) - Added [`--limitInputChannels`](https://sharp.pixelplumbing.com/api-constructor/) input option. diff --git a/cmd/resizing/resize.js b/cmd/resizing/resize.js index d9520dc..ee94331 100644 --- a/cmd/resizing/resize.js +++ b/cmd/resizing/resize.js @@ -27,17 +27,31 @@ import constants from "../../lib/constants.js"; import { pick } from "../../lib/utils.js"; +// Helpers. +const resolveDimension = (dimension, metadataDimension) => { + if (dimension == null) return null; + if (typeof dimension === "string" && dimension.endsWith("%")) { + if (metadataDimension == null) { + throw new Error("Input metadata is required to resolve percentage"); + } + return Math.round( + (metadataDimension * Number(dimension.slice(0, -1))) / 100, + ); + } + return Number(dimension); +}; + // Configure. const positionals = { width: { default: null, - desc: "Pixels wide the resultant image should be", - type: "number", + desc: "Pixels or percentage wide the resultant image should be", + type: "string", }, height: { default: null, - desc: "Pixels high the resultant image should be", - type: "number", + desc: "Pixels or percentage high the resultant image should be", + type: "string", }, }; @@ -94,6 +108,10 @@ const builder = (yargs) => { "$0 resize --height 100", "The output will be 100 pixels high, auto-scaled width", ) + .example( + "$0 resize 50%", + "The output will be 50% of the input width, auto-scaled height", + ) .example( '$0 resize 200 300 --background rgba(255,255,255,0.5) --fit contain --kernel nearest --position "right top"', "The output will be 200 pixels wide and 300 pixels high containing a nearest-neighbour scaled version contained within the north-east corner of a semi-transparent white canvas", @@ -132,8 +150,10 @@ const handler = (args) => { // @see https://sharp.pixelplumbing.com/api-resize#resize return args["#queue"].push([ "resize", - (sharp) => { - return sharp.resize(args.width, args.height, pick(args, optionNames)); + (sharp, { metadata } = {}) => { + const width = resolveDimension(args.width, metadata?.width); + const height = resolveDimension(args.height, metadata?.height); + return sharp.resize(width, height, pick(args, optionNames)); }, ]); }; diff --git a/package.json b/package.json index 0f1aa0b..656ed29 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "sharp-cli", - "version": "6.0.0", + "version": "6.1.0-dev", "description": "CLI for sharp.", "keywords": [ "cli", diff --git a/test/cmd/resizing/resize.js b/test/cmd/resizing/resize.js index a84c093..c1b8bd1 100644 --- a/test/cmd/resizing/resize.js +++ b/test/cmd/resizing/resize.js @@ -62,7 +62,7 @@ export default function register() { it("must set the width flag", () => { const args = cli.parsed.argv; - assert.equal(args["width"], width); + assert.equal(args["width"], String(width)); }); it("must update the pipeline", () => { const pipeline = getPipeline(cli.parsed.argv); @@ -80,7 +80,7 @@ export default function register() { it("must set the height flag", () => { const args = cli.parsed.argv; - assert.equal(args["height"], height); + assert.equal(args["height"], String(height)); }); it("must update the pipeline", () => { const pipeline = getPipeline(cli.parsed.argv); @@ -100,8 +100,8 @@ export default function register() { // Tests. it("must set the width and height flags", () => { const args = cli.parsed.argv; - assert.equal(args["width"], width); - assert.equal(args["height"], height); + assert.equal(args["width"], String(width)); + assert.equal(args["height"], String(height)); }); it("must update the pipeline", () => { const pipeline = getPipeline(cli.parsed.argv); @@ -114,6 +114,22 @@ export default function register() { }); }); + describe("percentage dimensions", () => { + const metadata = { height: 331, width: 500 }; + + it("must resize width relative to input metadata", async () => { + await cli.parseAsync(["resize", "50%"]); + const pipeline = drain(cli.parsed.argv, { metadata }); + sinon.assert.calledWithMatch(pipeline.resize, 250, null); + }); + + it("must resize width and height relative to input metadata", async () => { + await cli.parseAsync(["resize", "50%", "75%"]); + const pipeline = drain(cli.parsed.argv, { metadata }); + sinon.assert.calledWithMatch(pipeline.resize, 250, 248); + }); + }); + describe("[options]", () => { // @see https://sharp.pixelplumbing.com/api-resize#resize describe("--background", () => { From c72309e88aa3e94ffd038fb5cca48f4b9be9814d Mon Sep 17 00:00:00 2001 From: Mark van Seventer Date: Tue, 25 Aug 2026 08:08:50 -0700 Subject: [PATCH 2/2] Add test. --- test/cmd/resizing/resize.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/test/cmd/resizing/resize.js b/test/cmd/resizing/resize.js index c1b8bd1..a6677c1 100644 --- a/test/cmd/resizing/resize.js +++ b/test/cmd/resizing/resize.js @@ -128,6 +128,14 @@ export default function register() { const pipeline = drain(cli.parsed.argv, { metadata }); sinon.assert.calledWithMatch(pipeline.resize, 250, 248); }); + + it("must fail when input metadata is not set", async () => { + await cli.parseAsync(["resize", "50%"]); + assert.throws( + () => drain(cli.parsed.argv), + "metadata is required to resolve percentage", + ); + }); }); describe("[options]", () => {