diff --git a/CHANGELOG.md b/CHANGELOG.md index d18dcee..a4f0499 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## 6.0.0-dev - Added `--dry` option to process images without writing output files. +- Added `--print` option to print input and output metadata as JSON. - Removed `jpg` output format alias; use `jpeg` instead. - Internal refactor from CommonJS to ESM. - Updated `sharp` dependency to 0.35.3. diff --git a/README.md b/README.md index d09b495..6c1c0e4 100644 --- a/README.md +++ b/README.md @@ -74,10 +74,10 @@ Commands: pixel values made fully transparent Global Options - --dry Process images without writing output files [boolean] - -i, --input Path to (an) image file(s) [array] [required] [default: stdin] - -o, --output Directory or URI template to write the image files to - [string] [required] [default: stdout] + -n, --dry Process images without writing output files [boolean] + -i, --input Path to (an) image file(s) [array] [default: stdin] + -o, --output Directory or URI template to write the image files to [string] [default: stdout] + --print Print input and output metadata as JSON [boolean] --timeout Number of seconds after which processing will be stopped [number] Input Options diff --git a/lib/cli.js b/lib/cli.js index ea27439..cf21b49 100644 --- a/lib/cli.js +++ b/lib/cli.js @@ -85,6 +85,13 @@ const globalOptions = { type: "boolean", }, + print: { + desc: "Print input and output metadata as JSON", + group: global, + implies: IS_TEXT_TERMINAL ? undefined : "dry", + type: "boolean", + }, + // @see https://sharp.pixelplumbing.com/api-constructor/ input: { alias: "i", diff --git a/lib/convert.js b/lib/convert.js index 6184ea3..5b14f6f 100644 --- a/lib/convert.js +++ b/lib/convert.js @@ -100,19 +100,21 @@ export default { }); } + const inputMetadata = { ...metadata, path: src }; const transformer = drain(context.queue, image, { format, - metadata, + metadata: inputMetadata, }); - if (context.dry) { - return transformer - .toBuffer({ resolveWithObject: true }) - .then(({ info }) => Object.assign({}, info, { path: dest })); - } - return transformer - .toFile(dest) - .then((info) => Object.assign({}, info, { path: dest })); + const promise = context.dry + ? transformer + .toBuffer({ resolveWithObject: true }) + .then(({ info }) => info) + : transformer.toFile(dest); + return promise.then((info) => ({ + input: inputMetadata, + output: { ...info, path: dest }, + })); }); }); return Promise.all(promises); @@ -124,22 +126,29 @@ export default { return pipeline(inStream, image) .then(() => image.metadata()) .then((metadata) => { + const inputMetadata = { ...metadata, path: "stdin" }; const transformer = drain(context.queue, image, { format: context.format ?? metadata.format, - metadata, + metadata: inputMetadata, }); if (context.dry) { return transformer .toBuffer({ resolveWithObject: true }) - .then(({ info }) => info); + .then(({ info }) => ({ + input: inputMetadata, + output: { ...info, path: "stdout" }, + })); } // Gather return value. const info = {}; transformer.on("info", (_info) => Object.assign(info, _info)); - return pipeline(transformer, outStream).then(() => info); + return pipeline(transformer, outStream).then(() => ({ + input: inputMetadata, + output: { ...info, path: "stdout" }, + })); }); }, }; diff --git a/lib/index.js b/lib/index.js index 87585b0..2072c18 100644 --- a/lib/index.js +++ b/lib/index.js @@ -44,11 +44,19 @@ export default (args, options = {}) => { return convert .files(argv.input, argv.output, context) .then((output) => { - const info = Array.isArray(output) ? output : [output]; - info.forEach((file) => logger.log(file.path)); + if (argv.print) { + logger.log(JSON.stringify(output)); + } else { + const arr = Array.isArray(output) ? output : [output]; + arr.forEach((file) => logger.log(file.output.path)); + } }); } - return convert.stream(process.stdin, process.stdout, context); + return convert + .stream(process.stdin, process.stdout, context) + .then((output) => { + if (argv.print) logger.log(JSON.stringify(output)); + }); }) .catch((err) => { if (err instanceof Error) { diff --git a/test/cli.js b/test/cli.js index fc89435..96b8dd1 100644 --- a/test/cli.js +++ b/test/cli.js @@ -327,6 +327,16 @@ describe(`${pkg.name} [command..]`, () => { }); }); + describe("--dry", () => { + // Run. + before(() => cli.parseAsync(["--dry", ...ioFlags])); + + // Tests. + it("must set the dry flag", () => { + expect(cli.parsed.argv).to.have.property("dry", true); + }); + }); + describe("--effort", () => { // Default effort. const effort = 1; @@ -820,16 +830,6 @@ describe(`${pkg.name} [command..]`, () => { }); }); - describe("--dry", () => { - // Run. - before(() => cli.parseAsync(["--dry", ...ioFlags])); - - // Tests. - it("must set the dry flag", () => { - expect(cli.parsed.argv).to.have.property("dry", true); - }); - }); - describe("--overshootDeringing", () => { // Run. before(() => cli.parseAsync(["--overshootDeringing", ...ioFlags])); @@ -967,6 +967,16 @@ describe(`${pkg.name} [command..]`, () => { }); }); + describe("--print", () => { + // Run. + before(() => cli.parseAsync(["--print", "--dry", ...ioFlags])); + + // Tests. + it("must set the print flag", () => { + expect(cli.parsed.argv).to.have.property("print", true); + }); + }); + describe("--pyramid", () => { // Run. before(() => cli.parseAsync(["--pyramid", ...ioFlags])); diff --git a/test/convert.js b/test/convert.js index 3b72f71..ecfdc14 100644 --- a/test/convert.js +++ b/test/convert.js @@ -61,27 +61,45 @@ describe("convert", () => { it("must convert a file", () => { return convert .files([input], dest, createContext()) - .then(([info]) => expect(fs.existsSync(info.path)).to.be.true); + .then(([info]) => expect(fs.existsSync(info.output.path)).to.be.true()); }); it("must convert a file formatted based on extension", () => { return convert .files([input], path.join(dest, "{name}.avif"), createContext()) .then(([info]) => { - expect(info).to.have.property("format", "heif"); - expect(info).to.have.property("path"); - expect(info.path).to.contain(".avif"); + expect(info.output).to.have.property("format", "heif"); + expect(info.output).to.have.property("path"); + expect(info.output.path).to.contain(".avif"); }); }); it("must not write a file during a dry run", () => { const dryDest = path.join(dest, "dry.jpg"); const context = { ...createContext(), dry: true }; return convert.files([input], dryDest, context).then(([info]) => { - expect(info).to.have.property("format", "jpeg"); - expect(info).to.have.property("size"); - expect(info.path).to.equal(dryDest); + expect(info.output).to.have.property("format", "jpeg"); + expect(info.output).to.have.property("size"); + expect(info.output.path).to.equal(dryDest); expect(fs.existsSync(dryDest)).to.be.false(); }); }); + it("must return input and output metadata", () => { + const output = path.join(dest, "output.jpg"); + const context = { ...createContext(), dry: true, print: true }; + return convert.files([input], output, context).then(([info]) => { + expect(info.input).to.have.property("format", "jpeg"); + expect(info.input.path).to.equal(input); + expect(info.output).to.have.property("format", "jpeg"); + expect(info.output.path).to.equal(output); + }); + }); + it("must return metadata for each file in a batch", () => { + const context = { ...createContext(), dry: true, print: true }; + return convert.files([input, input], dest, context).then((info) => { + expect(info).to.have.length(2); + expect(info[0].input).to.have.property("format", "jpeg"); + expect(info[0].output).to.have.property("format", "jpeg"); + }); + }); it("must pass the output extension format to queued handlers", () => { const context = createContext(); let format; @@ -120,7 +138,9 @@ describe("convert", () => { const rand = Math.random(); return convert .files([input], path.join(dest, `{name}-${rand}{ext}`), createContext()) - .then(([info]) => expect(info.path).to.contain(`input-${rand}.jpg`)); + .then(([info]) => + expect(info.output.path).to.contain(`input-${rand}.jpg`), + ); }); it("must allow the same file as input and output", () => { return convert.files([copy], path.dirname(copy), createContext()); @@ -160,8 +180,8 @@ describe("convert", () => { createContext(), ) .then((info) => { - expect(info.format).to.exist(); - expect(info.path).not.to.exist(); + expect(info.output.format).to.exist(); + expect(info.output.path).to.equal("stdout"); expect(fs.existsSync(dest)).to.be.true(); }); }); @@ -207,8 +227,25 @@ describe("convert", () => { dry: true, }) .then((info) => { - expect(info).to.have.property("format", "jpeg"); - expect(info).to.have.property("size"); + expect(info.output).to.have.property("format", "jpeg"); + expect(info.output).to.have.property("size"); + }); + }); + it("must return input and output metadata", () => { + const context = { ...createContext(), dry: true, print: true }; + return convert + .stream( + fs.createReadStream(input), + new Writable({ + write(_chunk, _encoding, callback) { + callback(new Error("should not write output")); + }, + }), + context, + ) + .then((info) => { + expect(info.input).to.have.property("format", "jpeg"); + expect(info.output).to.have.property("format", "jpeg"); }); }); }); diff --git a/test/index.js b/test/index.js index 453b049..c5cfd50 100644 --- a/test/index.js +++ b/test/index.js @@ -87,6 +87,16 @@ describe("CLI", () => { expect(process.exitCode).not.to.equal(1); }); }); + it("must print metadata", () => { + return cli(["--dry", "--print", "-i", input, "-o", dest, "resize", "100"], { + logger, + }).then(() => { + const [metadata] = JSON.parse(logger.log.firstCall.args[0]); + expect(metadata.input).to.have.property("format", "jpeg"); + expect(metadata.output).to.have.property("width", 100); + sinon.assert.notCalled(logger.error); + }); + }); it("must display errors", () => { return cli(["-i", missing, "-o", dest], { logger }).then(() => { sinon.assert.notCalled(logger.log);