Skip to content
Image KB Reducer
Esc
↑↓navigate↵open⌘Jpreview
On this page

--json output

The machine-readable envelope every command can print to stdout.

Pass --json on any command and the tool prints exactly one JSON document to stdout, newline terminated — on every exit, success or failure, including a usage mistake that never reached a file — nothing else ever touches stdout under --json. Every human-facing line (progress, the final summary, missing-file reports, the target-unreachable suggestion sentence) goes to stderr instead, which is what makes this work:

imagekbreducer compress a.jpg --to 50kb --json | jq -r '.results[0].output'

Envelope shape

interface Envelope {
  version: 1
  ok: boolean       // results.every(r => r.ok) -- never tracked separately, so it can't drift
  results: FileResult[]
  error?: { code: "usage" | "internal"; message: string }   // invocation-level failures only
  exitCode?: number                                         // ditto -- the process's own code
}

results has one entry per input the invocation named — including one that was never found. A missing input becomes its own failure entry rather than just a stderr line; without that, a run that failed to find one of three files could report "ok": true (an empty check vacuously passes), which is exactly the shape a --json | jq -e .ok pipeline would misread as success.

An invocation that never reached a file

A usage mistake (exit 2) or an unexpected internal error (exit 1) happens before any input is processed, so there is no FileResult to carry it. --json still prints its one document — with results empty, ok stated as false literally rather than derived ([].every(...) is true, the vacuous-success shape this envelope exists to avoid), and the failure described at the top level where it actually happened:

{
  "version": 1,
  "ok": false,
  "exitCode": 2,
  "error": {
    "code": "usage",
    "message": "--to is required for compress"
  },
  "results": []
}

error.code is "usage" for exit 2 and "internal" for exit 1 — the only two values, because those are the only two failures that can happen with no file in hand. error and exitCode are absent, never null, on every run that reached at least one input; check for the key. The human sentence still goes to stderr as always — --json moves the machine-readable channel to stdout, it has never silenced stderr.

A successful file

{
  "input": "shots/a.png",
  "output": "dist/a-compressed.png",
  "ok": true,
  "dryRun": false,
  "format": "png",
  "width": 1200,
  "height": 800,
  "originalBytes": 3145728,
  "resultBytes": 49812,
  "targetBytes": 51200,
  "quality": null,
  "encodes": 5,
  "warnings": [{ "code": "metadata-stripped", "message": "…" }]
}
  • targetBytes is null, not omitted, whenever the command had no byte target at all — compress always has one (--to is required), resize/convert only have one when --to was given, and crop/rotate never take a --to flag and are always null.
  • quality is null, not omitted, for a format with no scalar quality knob — PNG, above.
  • acceptedClosest, present and true only when --accept-closest turned an otherwise-failed target-unreachable result into a written success. Absent (never false) on every ordinary success — check for the key, not its value.
  • resultBytes is the real size of the file written to disk, not merely what the encoder claimed.

A failed file

{
  "input": "shots/b.jpg",
  "ok": false,
  "exitCode": 10,
  "error": {
    "code": "target-unreachable",
    "reason": "FIXED_DIMENSIONS",
    "message": "…"
  },
  "closestBytes": 58203,
  "suggestions": [{ "kind": "allow-downscale" }, { "kind": "accept-closest", "bytes": 58203 }],
  "partialWritten": false
}

error.code is usually one of the engine’s own ErrorCodes (see Exit codes), but three values are CLI-side, not engine codes:

error.code Where it comes from
"not-found" an input expandInputs couldn’t turn into a real file — a literal path that doesn’t exist, or a glob that matched nothing
"would-overwrite" the computed output already existed and --force/--in-place wasn’t given
"usage" a usage mistake only discoverable per-file, after the file’s own sniffed format is known — today, only strip on a webp missing --reencode, or on a heic source at all

closestBytes and suggestions are only meaningful for target-unreachable; every other failure leaves suggestions as an empty array. partialWritten is always false — the one case that writes a partial file (--accept-closest) is reported as a success entry instead, since it is a successful write, just not of the winning candidate.

info --json

info prints a third shape under the same envelope, discriminated by "kind": "info" rather than SuccessResult/FailureResult’s ok:

{
  "kind": "info",
  "input": "photo.jpg",
  "ok": true,
  "format": "jpeg",
  "width": 800,
  "height": 600,
  "bytes": 245123,
  "orientation": 6,
  "hasAlpha": false,
  "isAnimated": false,
  "hasExif": true,
  "hasGps": false,
  "hasIptc": false,
  "hasXmp": false
}

info never has an output, dryRun, or encoded format-as-output-format — it writes nothing, so reusing SuccessResult’s shape would either fabricate those fields or make them lie by omission. Any field inspect() didn’t determine is omitted from this object entirely, never printed as a fabricated false.

--dry-run --json

The envelope’s byte counts are still the real numbers a real write would have produced — dryRun: true is the only signal that nothing was actually written.

Was this page helpful?