Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 22 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,12 @@ imgvidcompress video ./clips --to .webm --quality 55
imgvidcompress ./photos --overwrite
```

A video dry run resolves ffmpeg, checks the selected codecs and probes every
source so the plan can report stream drops such as subtitles or cover art. It
is therefore slower than a file-list preview, but it fails before writing when
the planned conversion cannot run. Dry runs do not encode, so they never
estimate output size or savings.

## Formats

Support is bounded by what your sharp and ffmpeg builds can actually do, not by a list in this README. Both are build-dependent, so ask the tool:
Expand Down Expand Up @@ -294,7 +300,9 @@ type JobResult =
| ({ status: "failed" } & FailedResult);
```

All three carry `kind`, `inputPath` and `outputPath`. Beyond that:
All three carry `kind`, `inputPath`, `outputPath` and the resolved
`targetFormat`. Video results also carry `videoCodec` and `audioCodec` once the
file reaches planning. Beyond that:

| Status | Additional fields |
| -------------- | ---------------------------------------------------------------------------------- |
Expand All @@ -304,6 +312,10 @@ All three carry `kind`, `inputPath` and `outputPath`. Beyond that:

`savedBytes` is positive when space was saved and negative if the output grew. `savedRatio` is the fraction of the original removed, 0-1.

The summary's `planned` and `plannedInputBytes` fields count files a dry run
would send to an encoder. Existing `inputBytes`, `outputBytes`, `savedBytes`
and `savedRatio` remain actual encoded totals; dry runs leave them at zero.

`isCompressed`, `isSkipped` and `isFailed` are exported as type guards, so consumers never hand-check `status`:

```ts
Expand Down Expand Up @@ -397,6 +409,8 @@ imgvidcompress formats --json | jq '.video.curated[].extension'
"compressed": 2,
"skipped": 0,
"failed": 0,
"planned": 0,
"plannedInputBytes": 0,
"inputBytes": 64931,
"outputBytes": 33106,
"savedBytes": 31825,
Expand All @@ -409,6 +423,7 @@ imgvidcompress formats --json | jq '.video.curated[].extension'
"kind": "image",
"inputPath": "…/photo-medium.jpg",
"outputPath": "…/compressed/photo-medium.webp",
"targetFormat": ".webp",
"inputBytes": 44331,
"outputBytes": 17902,
"savedBytes": 26429,
Expand Down Expand Up @@ -459,12 +474,16 @@ imgvidcompress ./public/img --dry-run --json \
|| { echo "An image exceeds 500 KB"; exit 1; }
```

**Report the saving without writing anything**
**Inspect the plan without writing anything**

```bash
imgvidcompress ./assets --dry-run --json | jq '.summary'
imgvidcompress ./assets --dry-run --json \
| jq '{plannedInputBytes: .summary.plannedInputBytes, files: [.results[] | {inputPath, outputPath, targetFormat, videoCodec, audioCodec, reason, warnings}]}'
```

No savings value is included in the plan because savings cannot be known
without encoding.

**Use it from a build script**

```ts
Expand Down
28 changes: 24 additions & 4 deletions src/cli/render.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,9 +129,7 @@ export class Renderer {
// the list here would just double every line in the log. Skipped files are
// included: "1 skipped" with no indication of which file, or why, is the
// kind of summary that sends someone hunting through the output directory.
const rows = this.isTty
? results
: results.filter((r) => r.status === "skipped" && report.dryRun);
const rows = this.isTty ? results : [];

if (rows.length > 0) this.write("\n");
for (const result of rows) {
Expand Down Expand Up @@ -159,8 +157,15 @@ export class Renderer {
}

if (report.dryRun) {
const skipped = summary.skipped - summary.planned;
const parts = [
`${summary.planned} file(s) planned (${formatBytes(summary.plannedInputBytes)} input)`,
skipped > 0 ? `${skipped} skipped` : null,
summary.failed > 0 ? pc.red(`${summary.failed} failed`) : null,
"nothing written",
].filter((part): part is string => part !== null);
this.write(
`\n ${pc.bold(pc.yellow("Dry run"))} ${pc.dim(`— ${summary.totalFiles} file(s) planned, nothing written.`)}\n\n`,
`\n ${pc.bold(pc.yellow("Dry run"))} ${pc.dim(`— ${parts.join(" · ")}.`)}\n\n`,
);
return;
}
Expand Down Expand Up @@ -237,6 +242,9 @@ function plainLine(result: JobResult): string {
}
case "skipped": {
const notes = (result.warnings ?? []).map((w) => `\n warn ${w}`).join("");
if (result.reason === "dry-run") {
return ` plan ${basename(result.inputPath)} → ${basename(result.outputPath)} ${formatBytes(result.inputBytes)} · ${settingsLabel(result)}${notes}`;
}
return ` skip ${basename(result.inputPath)} (${result.reason})${notes}`;
}
case "failed":
Expand All @@ -255,12 +263,24 @@ function detailLine(result: JobResult): string {
return `${pc.green("✓")} ${name} ${padStart(arrow, 30)} ${tone(`${grew ? "+" : "−"}${formatPercent(Math.abs(result.savedRatio))}`)}`;
}
case "skipped":
if (result.reason === "dry-run") {
return `${pc.yellow("○")} ${name} ${pc.dim(`planned → ${basename(result.outputPath)} ${formatBytes(result.inputBytes)} · ${settingsLabel(result)}`)}`;
}
return `${pc.yellow("○")} ${name} ${pc.dim(skipLabel(result.reason))}`;
case "failed":
return `${pc.red("✗")} ${name} ${pc.red(result.error.code)}`;
}
}

function settingsLabel(result: Extract<JobResult, { status: "skipped" }>): string {
const settings = [`format: ${result.targetFormat}`];
if (result.kind === "video") {
settings.push(`video: ${result.videoCodec ?? "ffmpeg default"}`);
settings.push(`audio: ${result.audioCodec ?? "ffmpeg default"}`);
}
return settings.join(" · ");
}

function skipLabel(reason: string): string {
switch (reason) {
case "output-larger-than-input":
Expand Down
Loading
Loading