@macrulez/mediatoolz 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +182 -2
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +243 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/image-batch/backup.d.ts +22 -0
- package/dist/commands/image-batch/backup.js +55 -0
- package/dist/commands/image-batch/backup.js.map +1 -0
- package/dist/commands/image-batch/cache.d.ts +15 -0
- package/dist/commands/image-batch/cache.js +38 -0
- package/dist/commands/image-batch/cache.js.map +1 -0
- package/dist/commands/image-batch/cli.d.ts +15 -0
- package/dist/commands/image-batch/cli.js +323 -0
- package/dist/commands/image-batch/cli.js.map +1 -0
- package/dist/commands/image-batch/codecs.d.ts +38 -0
- package/dist/commands/image-batch/codecs.js +366 -0
- package/dist/commands/image-batch/codecs.js.map +1 -0
- package/dist/commands/image-batch/config.d.ts +121 -0
- package/dist/commands/image-batch/config.js +690 -0
- package/dist/commands/image-batch/config.js.map +1 -0
- package/dist/commands/image-batch/errors.d.ts +5 -0
- package/dist/commands/image-batch/errors.js +8 -0
- package/dist/commands/image-batch/errors.js.map +1 -0
- package/dist/commands/image-batch/geometry.d.ts +26 -0
- package/dist/commands/image-batch/geometry.js +90 -0
- package/dist/commands/image-batch/geometry.js.map +1 -0
- package/dist/commands/image-batch/inputs.d.ts +22 -0
- package/dist/commands/image-batch/inputs.js +87 -0
- package/dist/commands/image-batch/inputs.js.map +1 -0
- package/dist/commands/image-batch/manage-cli.d.ts +2 -0
- package/dist/commands/image-batch/manage-cli.js +276 -0
- package/dist/commands/image-batch/manage-cli.js.map +1 -0
- package/dist/commands/image-batch/manage.d.ts +51 -0
- package/dist/commands/image-batch/manage.js +1041 -0
- package/dist/commands/image-batch/manage.js.map +1 -0
- package/dist/commands/image-batch/overwrite.d.ts +14 -0
- package/dist/commands/image-batch/overwrite.js +31 -0
- package/dist/commands/image-batch/overwrite.js.map +1 -0
- package/dist/commands/image-batch/plan.d.ts +86 -0
- package/dist/commands/image-batch/plan.js +396 -0
- package/dist/commands/image-batch/plan.js.map +1 -0
- package/dist/commands/image-batch/process.d.ts +8 -0
- package/dist/commands/image-batch/process.js +97 -0
- package/dist/commands/image-batch/process.js.map +1 -0
- package/dist/commands/image-batch/report.d.ts +6 -0
- package/dist/commands/image-batch/report.js +251 -0
- package/dist/commands/image-batch/report.js.map +1 -0
- package/dist/commands/image-batch/restore.d.ts +29 -0
- package/dist/commands/image-batch/restore.js +125 -0
- package/dist/commands/image-batch/restore.js.map +1 -0
- package/dist/commands/image-batch/run.d.ts +127 -0
- package/dist/commands/image-batch/run.js +656 -0
- package/dist/commands/image-batch/run.js.map +1 -0
- package/dist/commands/image-batch/sharpen-ask.d.ts +6 -0
- package/dist/commands/image-batch/sharpen-ask.js +53 -0
- package/dist/commands/image-batch/sharpen-ask.js.map +1 -0
- package/dist/commands/image-batch/sharpen-manage.d.ts +26 -0
- package/dist/commands/image-batch/sharpen-manage.js +238 -0
- package/dist/commands/image-batch/sharpen-manage.js.map +1 -0
- package/dist/commands/image-batch/sharpen-presets.d.ts +24 -0
- package/dist/commands/image-batch/sharpen-presets.js +145 -0
- package/dist/commands/image-batch/sharpen-presets.js.map +1 -0
- package/dist/commands/image-batch/sharpen.d.ts +46 -0
- package/dist/commands/image-batch/sharpen.js +159 -0
- package/dist/commands/image-batch/sharpen.js.map +1 -0
- package/dist/commands/image-batch/template.d.ts +45 -0
- package/dist/commands/image-batch/template.js +142 -0
- package/dist/commands/image-batch/template.js.map +1 -0
- package/dist/commands/image-hash/cache.d.ts +14 -0
- package/dist/commands/image-hash/cache.js +40 -0
- package/dist/commands/image-hash/cache.js.map +1 -0
- package/dist/commands/image-hash/core.d.ts +61 -0
- package/dist/commands/image-hash/core.js +302 -0
- package/dist/commands/image-hash/core.js.map +1 -0
- package/dist/commands/image-hash/hash.d.ts +21 -0
- package/dist/commands/image-hash/hash.js +104 -0
- package/dist/commands/image-hash/hash.js.map +1 -0
- package/dist/commands/image-hash/report.d.ts +3 -0
- package/dist/commands/image-hash/report.js +96 -0
- package/dist/commands/image-hash/report.js.map +1 -0
- package/dist/commands/image-hash/run.d.ts +61 -0
- package/dist/commands/image-hash/run.js +317 -0
- package/dist/commands/image-hash/run.js.map +1 -0
- package/dist/commands/image-hash/sharp-loader.d.ts +64 -0
- package/dist/commands/image-hash/sharp-loader.js +81 -0
- package/dist/commands/image-hash/sharp-loader.js.map +1 -0
- package/dist/commands/ui/cli.d.ts +2 -0
- package/dist/commands/ui/cli.js +78 -0
- package/dist/commands/ui/cli.js.map +1 -0
- package/dist/commands/ui/fs-routes.d.ts +7 -0
- package/dist/commands/ui/fs-routes.js +135 -0
- package/dist/commands/ui/fs-routes.js.map +1 -0
- package/dist/commands/ui/http.d.ts +41 -0
- package/dist/commands/ui/http.js +81 -0
- package/dist/commands/ui/http.js.map +1 -0
- package/dist/commands/ui/jobs.d.ts +15 -0
- package/dist/commands/ui/jobs.js +73 -0
- package/dist/commands/ui/jobs.js.map +1 -0
- package/dist/commands/ui/modules/image-batch-backups.d.ts +2 -0
- package/dist/commands/ui/modules/image-batch-backups.js +116 -0
- package/dist/commands/ui/modules/image-batch-backups.js.map +1 -0
- package/dist/commands/ui/modules/image-batch-manage.d.ts +2 -0
- package/dist/commands/ui/modules/image-batch-manage.js +245 -0
- package/dist/commands/ui/modules/image-batch-manage.js.map +1 -0
- package/dist/commands/ui/modules/image-batch.d.ts +2 -0
- package/dist/commands/ui/modules/image-batch.js +225 -0
- package/dist/commands/ui/modules/image-batch.js.map +1 -0
- package/dist/commands/ui/modules/image-hash.d.ts +2 -0
- package/dist/commands/ui/modules/image-hash.js +158 -0
- package/dist/commands/ui/modules/image-hash.js.map +1 -0
- package/dist/commands/ui/modules/index.d.ts +2 -0
- package/dist/commands/ui/modules/index.js +4 -0
- package/dist/commands/ui/modules/index.js.map +1 -0
- package/dist/commands/ui/open-browser.d.ts +1 -0
- package/dist/commands/ui/open-browser.js +18 -0
- package/dist/commands/ui/open-browser.js.map +1 -0
- package/dist/commands/ui/scan.d.ts +20 -0
- package/dist/commands/ui/scan.js +57 -0
- package/dist/commands/ui/scan.js.map +1 -0
- package/dist/commands/ui/server.d.ts +18 -0
- package/dist/commands/ui/server.js +196 -0
- package/dist/commands/ui/server.js.map +1 -0
- package/dist/format/bytes.d.ts +2 -0
- package/dist/format/bytes.js +17 -0
- package/dist/format/bytes.js.map +1 -0
- package/dist/format/colors.d.ts +12 -0
- package/dist/format/colors.js +15 -0
- package/dist/format/colors.js.map +1 -0
- package/dist/format/diff-render.d.ts +2 -0
- package/dist/format/diff-render.js +35 -0
- package/dist/format/diff-render.js.map +1 -0
- package/dist/format/file-list.d.ts +6 -0
- package/dist/format/file-list.js +15 -0
- package/dist/format/file-list.js.map +1 -0
- package/dist/format/findings-list.d.ts +7 -0
- package/dist/format/findings-list.js +14 -0
- package/dist/format/findings-list.js.map +1 -0
- package/dist/format/style.d.ts +20 -0
- package/dist/format/style.js +36 -0
- package/dist/format/style.js.map +1 -0
- package/dist/format/table.d.ts +8 -0
- package/dist/format/table.js +44 -0
- package/dist/format/table.js.map +1 -0
- package/dist/format/vibes.d.ts +13 -0
- package/dist/format/vibes.js +82 -0
- package/dist/format/vibes.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/ui/assets/ImageBatchPage-D3f4z6YA.js +4 -0
- package/dist/ui/assets/ImageBatchPage-DwaunraE.css +1 -0
- package/dist/ui/assets/ImageHashPage-D0xZlLGw.css +1 -0
- package/dist/ui/assets/ImageHashPage-DjIlDPu-.js +1 -0
- package/dist/ui/assets/SourcePicker-D8aZjDwP.css +1 -0
- package/dist/ui/assets/SourcePicker-Db0dG6zc.js +1 -0
- package/dist/ui/assets/index-D1s-23_t.css +1 -0
- package/dist/ui/assets/index-yiehSw5D.js +3 -0
- package/dist/ui/index.html +17 -0
- package/dist/utils/closest.d.ts +2 -0
- package/dist/utils/closest.js +35 -0
- package/dist/utils/closest.js.map +1 -0
- package/dist/utils/glob.d.ts +4 -0
- package/dist/utils/glob.js +86 -0
- package/dist/utils/glob.js.map +1 -0
- package/dist/utils/split-list.d.ts +1 -0
- package/dist/utils/split-list.js +4 -0
- package/dist/utils/split-list.js.map +1 -0
- package/dist/utils/tty.d.ts +60 -0
- package/dist/utils/tty.js +390 -0
- package/dist/utils/tty.js.map +1 -0
- package/dist/utils/walk.d.ts +21 -0
- package/dist/utils/walk.js +116 -0
- package/dist/utils/walk.js.map +1 -0
- package/package.json +82 -4
|
@@ -0,0 +1,656 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { dirname, isAbsolute, relative, resolve } from 'node:path';
|
|
4
|
+
import { createStyle } from '../../format/style.js';
|
|
5
|
+
import { formatBytes } from '../../format/bytes.js';
|
|
6
|
+
import { defaultTerminal, isInteractive, multiSelect, promptChoice, } from '../../utils/tty.js';
|
|
7
|
+
import { computeHashes, friendlyMessage } from '../image-hash/hash.js';
|
|
8
|
+
import { loadSharp } from '../image-hash/sharp-loader.js';
|
|
9
|
+
import { backupDirFor, backupPathFor, copyIntoBackup, DEFAULT_BACKUP_DIR, newJournal, saveJournal, } from './backup.js';
|
|
10
|
+
import { cacheKeyFor, defaultCachePath, emptyBatchCache, loadBatchCache, saveBatchCache, } from './cache.js';
|
|
11
|
+
import { normalizeFormat, unsupportedFormats, } from './codecs.js';
|
|
12
|
+
import { emptyConfig, } from './config.js';
|
|
13
|
+
import { ImageBatchAbortError, ImageBatchUsageError } from './errors.js';
|
|
14
|
+
import { parseSize } from './geometry.js';
|
|
15
|
+
import { BATCH_IMAGE_EXTENSIONS, collectInputs } from './inputs.js';
|
|
16
|
+
import { createOverwriteResolver, } from './overwrite.js';
|
|
17
|
+
import { describeCollisions, isSource, planJobs, readSource, validateReplacePlan, } from './plan.js';
|
|
18
|
+
import { renderJob, writeAtomic } from './process.js';
|
|
19
|
+
import { attachSharpenPresets } from './sharpen-presets.js';
|
|
20
|
+
import { parseTemplate } from './template.js';
|
|
21
|
+
const DEFAULT_CONCURRENCY = 4;
|
|
22
|
+
const JOURNAL_FLUSH_EVERY = 25;
|
|
23
|
+
function emptyCounts() {
|
|
24
|
+
return {
|
|
25
|
+
written: 0,
|
|
26
|
+
'up-to-date': 0,
|
|
27
|
+
skipped: 0,
|
|
28
|
+
conflict: 0,
|
|
29
|
+
'would-write': 0,
|
|
30
|
+
'would-overwrite': 0,
|
|
31
|
+
replaced: 0,
|
|
32
|
+
kept: 0,
|
|
33
|
+
'already-processed': 0,
|
|
34
|
+
'would-replace': 0,
|
|
35
|
+
'would-keep': 0,
|
|
36
|
+
error: 0,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
function overrideLayer(overrides) {
|
|
40
|
+
if (!overrides)
|
|
41
|
+
return [];
|
|
42
|
+
const layer = {};
|
|
43
|
+
if (overrides.size)
|
|
44
|
+
layer.size = parseSize(overrides.size, 'size');
|
|
45
|
+
if (overrides.widths)
|
|
46
|
+
layer.widths = overrides.widths;
|
|
47
|
+
if (overrides.heights)
|
|
48
|
+
layer.heights = overrides.heights;
|
|
49
|
+
if (overrides.longEdge)
|
|
50
|
+
layer.longEdge = overrides.longEdge;
|
|
51
|
+
if (overrides.shortEdge)
|
|
52
|
+
layer.shortEdge = overrides.shortEdge;
|
|
53
|
+
if (overrides.megapixels)
|
|
54
|
+
layer.megapixels = overrides.megapixels;
|
|
55
|
+
if (overrides.percent)
|
|
56
|
+
layer.percent = overrides.percent;
|
|
57
|
+
if (overrides.matchOrientation)
|
|
58
|
+
layer.matchOrientation = true;
|
|
59
|
+
if (overrides.noResize)
|
|
60
|
+
layer.noResize = true;
|
|
61
|
+
if (overrides.maxBytes !== undefined)
|
|
62
|
+
layer.maxBytes = overrides.maxBytes;
|
|
63
|
+
if (overrides.sharpen)
|
|
64
|
+
layer.sharpen = overrides.sharpen;
|
|
65
|
+
if (overrides.formats) {
|
|
66
|
+
layer.formats = overrides.formats.map((format) => normalizeFormat(format, '--formats'));
|
|
67
|
+
}
|
|
68
|
+
if (overrides.quality !== undefined)
|
|
69
|
+
layer.quality = overrides.quality;
|
|
70
|
+
if (overrides.codecs)
|
|
71
|
+
layer.codecs = overrides.codecs;
|
|
72
|
+
if (overrides.fit)
|
|
73
|
+
layer.fit = overrides.fit;
|
|
74
|
+
if (overrides.name) {
|
|
75
|
+
layer.template = { text: overrides.name, tokens: parseTemplate(overrides.name, '--name') };
|
|
76
|
+
}
|
|
77
|
+
if (overrides.flatten !== undefined && overrides.flatten !== false) {
|
|
78
|
+
layer.flatten = overrides.flatten;
|
|
79
|
+
}
|
|
80
|
+
return Object.keys(layer).length > 0 ? [layer] : [];
|
|
81
|
+
}
|
|
82
|
+
async function pool(items, size, worker) {
|
|
83
|
+
let next = 0;
|
|
84
|
+
const runners = Array.from({ length: Math.max(1, Math.min(size, items.length)) }, async () => {
|
|
85
|
+
for (;;) {
|
|
86
|
+
const index = next++;
|
|
87
|
+
if (index >= items.length)
|
|
88
|
+
return;
|
|
89
|
+
await worker(items[index]);
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
await Promise.all(runners);
|
|
93
|
+
}
|
|
94
|
+
function summarize(source) {
|
|
95
|
+
return {
|
|
96
|
+
file: source.file.rel,
|
|
97
|
+
width: source.width,
|
|
98
|
+
height: source.height,
|
|
99
|
+
format: source.format,
|
|
100
|
+
bytes: source.bytes,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
const sha1 = (data) => createHash('sha1').update(data).digest('hex');
|
|
104
|
+
function isInside(parent, child) {
|
|
105
|
+
const rel = relative(parent, child);
|
|
106
|
+
return rel === '' || (!rel.startsWith('..') && !isAbsolute(rel));
|
|
107
|
+
}
|
|
108
|
+
function defaultPrompt(io, style) {
|
|
109
|
+
const s = createStyle(style);
|
|
110
|
+
return async (info) => {
|
|
111
|
+
const why = info.reason === 'changed'
|
|
112
|
+
? 'was made by an earlier run, but the source or the settings changed'
|
|
113
|
+
: 'already exists and was not made by this command';
|
|
114
|
+
const answer = await promptChoice(io, `${s.warn('Output exists:')} ${s.path(info.outRel)} ${s.muted(`(${formatBytes(info.existingBytes)}, ${why})`)}`, [
|
|
115
|
+
{ key: 'y', label: 'overwrite this' },
|
|
116
|
+
{ key: 'a', label: 'overwrite this and all next' },
|
|
117
|
+
{ key: 's', label: 'skip this' },
|
|
118
|
+
{ key: 'n', label: 'skip all next' },
|
|
119
|
+
{ key: 'q', label: 'quit' },
|
|
120
|
+
], s, 's');
|
|
121
|
+
return { y: 'yes', a: 'all', s: 'skip', n: 'none', q: 'quit' }[answer];
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
function defaultConfirm(io, style) {
|
|
125
|
+
const s = createStyle(style);
|
|
126
|
+
return async (summary) => {
|
|
127
|
+
const where = summary.backupDir
|
|
128
|
+
? `The originals are copied to ${s.path(summary.backupDir)} first.`
|
|
129
|
+
: s.problem('There is NO backup (--no-backup): the originals are overwritten for good.');
|
|
130
|
+
const answer = await promptChoice(io, `${s.warn('Replace')} ${s.value(`${summary.count} image${summary.count === 1 ? '' : 's'}`)} ${s.muted(`(${formatBytes(summary.bytes)})`)} ${s.warn('in place?')}\n ${where}`, [
|
|
131
|
+
{ key: 'y', label: 'replace' },
|
|
132
|
+
{ key: 'n', label: 'cancel' },
|
|
133
|
+
], s, 'n');
|
|
134
|
+
return answer === 'y';
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
function placementOf(options) {
|
|
138
|
+
const chosen = [options.out !== undefined, options.beside, options.replace].filter(Boolean).length;
|
|
139
|
+
if (chosen > 1) {
|
|
140
|
+
throw new ImageBatchUsageError('pick one place for the results: --out <dir>, --beside (next to each source) or --replace (over the sources)');
|
|
141
|
+
}
|
|
142
|
+
if (options.replace)
|
|
143
|
+
return 'replace';
|
|
144
|
+
if (options.beside)
|
|
145
|
+
return 'beside';
|
|
146
|
+
return 'out';
|
|
147
|
+
}
|
|
148
|
+
export async function runImageBatch(options) {
|
|
149
|
+
const cwd = resolve(options.cwd);
|
|
150
|
+
const io = options.io ?? defaultTerminal();
|
|
151
|
+
const interactive = options.interactive ?? isInteractive(io);
|
|
152
|
+
const style = options.style ?? {};
|
|
153
|
+
const config = attachSharpenPresets(options.config ?? emptyConfig(), {
|
|
154
|
+
cwd,
|
|
155
|
+
...(options.globalDir !== undefined ? { globalDir: options.globalDir } : {}),
|
|
156
|
+
extra: [options.overrides?.sharpen],
|
|
157
|
+
});
|
|
158
|
+
const now = options.now ?? new Date();
|
|
159
|
+
const placement = placementOf(options);
|
|
160
|
+
if (placement === 'out' && options.out === undefined && !options.list) {
|
|
161
|
+
throw new ImageBatchUsageError('image-batch needs a place for the results: --out <dir>, --beside (next to each source) or --replace (over the sources)');
|
|
162
|
+
}
|
|
163
|
+
if (placement !== 'out' && options.flat) {
|
|
164
|
+
throw new ImageBatchUsageError('--flat only makes sense with --out: with --beside and --replace every result stays in its own folder');
|
|
165
|
+
}
|
|
166
|
+
if (placement === 'replace' && options.overrides?.name !== undefined) {
|
|
167
|
+
throw new ImageBatchUsageError('--name has no effect with --replace: the result takes the name of the file it replaces');
|
|
168
|
+
}
|
|
169
|
+
if (placement !== 'replace' &&
|
|
170
|
+
(options.backup !== undefined || options.onlyIfSmaller === false)) {
|
|
171
|
+
throw new ImageBatchUsageError('--backup, --no-backup and --no-only-if-smaller belong to --replace');
|
|
172
|
+
}
|
|
173
|
+
const outDir = resolve(cwd, options.out ?? '.');
|
|
174
|
+
const onlyIfSmaller = options.onlyIfSmaller ?? true;
|
|
175
|
+
const report = {
|
|
176
|
+
placement,
|
|
177
|
+
out: placement === 'out' ? outDir : '',
|
|
178
|
+
backupEnabled: placement === 'replace' && options.backup !== false,
|
|
179
|
+
dryRun: Boolean(options.dryRun),
|
|
180
|
+
listOnly: Boolean(options.list),
|
|
181
|
+
sources: [],
|
|
182
|
+
failures: [],
|
|
183
|
+
unmatched: [],
|
|
184
|
+
derived: [],
|
|
185
|
+
vectors: [],
|
|
186
|
+
results: [],
|
|
187
|
+
counts: emptyCounts(),
|
|
188
|
+
deduped: 0,
|
|
189
|
+
bytesIn: 0,
|
|
190
|
+
bytesOut: 0,
|
|
191
|
+
aborted: false,
|
|
192
|
+
cancelled: false,
|
|
193
|
+
nothingSelected: false,
|
|
194
|
+
exitCode: 0,
|
|
195
|
+
};
|
|
196
|
+
if (options.configLabel !== undefined)
|
|
197
|
+
report.configLabel = options.configLabel;
|
|
198
|
+
if (options.placeholders && options.placeholders.types.length > 0 && !options.emit) {
|
|
199
|
+
throw new ImageBatchUsageError('placeholder hashes are written into the --emit file — pass --emit <file> as well');
|
|
200
|
+
}
|
|
201
|
+
const placeholders = options.emit ? (options.placeholders ?? config.placeholders) : undefined;
|
|
202
|
+
const backupBase = resolve(cwd, typeof options.backup === 'string' ? options.backup : DEFAULT_BACKUP_DIR);
|
|
203
|
+
const cachePath = options.cache === false
|
|
204
|
+
? null
|
|
205
|
+
: (options.cache ??
|
|
206
|
+
defaultCachePath(cacheKeyFor(placement, outDir, (options.paths.length > 0 ? options.paths : ['.']).map((path) => resolve(cwd, path))), options.cacheBase));
|
|
207
|
+
const cache = cachePath ? loadBatchCache(cachePath) : emptyBatchCache();
|
|
208
|
+
let cacheDirty = false;
|
|
209
|
+
let files = collectInputs({
|
|
210
|
+
paths: options.paths,
|
|
211
|
+
cwd,
|
|
212
|
+
extensions: options.extensions ?? BATCH_IMAGE_EXTENSIONS,
|
|
213
|
+
...(options.recursive !== undefined ? { recursive: options.recursive } : {}),
|
|
214
|
+
ignoreGlobs: [...(options.ignoreGlobs ?? []), `${DEFAULT_BACKUP_DIR}/`],
|
|
215
|
+
...(options.respectGitignore !== undefined
|
|
216
|
+
? { respectGitignore: options.respectGitignore }
|
|
217
|
+
: {}),
|
|
218
|
+
...(options.include ? { include: options.include } : {}),
|
|
219
|
+
...(options.exclude ? { exclude: options.exclude } : {}),
|
|
220
|
+
...(placement === 'out' && options.out !== undefined ? { outDir } : {}),
|
|
221
|
+
});
|
|
222
|
+
files = files.filter((file) => !isInside(backupBase, file.abs));
|
|
223
|
+
if (placement === 'beside') {
|
|
224
|
+
const known = files.filter((file) => cache.outputs[file.abs] !== undefined);
|
|
225
|
+
report.derived.push(...known.map((file) => file.rel));
|
|
226
|
+
files = files.filter((file) => cache.outputs[file.abs] === undefined);
|
|
227
|
+
}
|
|
228
|
+
if (files.length === 0)
|
|
229
|
+
return report;
|
|
230
|
+
const sharp = options.sharp ??
|
|
231
|
+
(await loadSharp({
|
|
232
|
+
cwd,
|
|
233
|
+
command: 'image-batch',
|
|
234
|
+
createError: (message) => new ImageBatchUsageError(message),
|
|
235
|
+
...(options.assumeYes !== undefined ? { assumeYes: options.assumeYes } : {}),
|
|
236
|
+
}));
|
|
237
|
+
const loaded = new Array(files.length);
|
|
238
|
+
await pool(files.map((file, index) => ({ file, index })), options.concurrency ?? DEFAULT_CONCURRENCY, async ({ file, index }) => {
|
|
239
|
+
loaded[index] = await readSource(sharp, file, index + 1, options.maxPixels);
|
|
240
|
+
});
|
|
241
|
+
let sources = [];
|
|
242
|
+
for (const item of loaded) {
|
|
243
|
+
if (!isSource(item)) {
|
|
244
|
+
report.failures.push({ file: item.file.rel, message: item.message });
|
|
245
|
+
}
|
|
246
|
+
else if (placement === 'replace' && item.format === 'svg') {
|
|
247
|
+
report.vectors.push(item.file.rel);
|
|
248
|
+
}
|
|
249
|
+
else {
|
|
250
|
+
sources.push(item);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
if (options.select) {
|
|
254
|
+
if (!interactive) {
|
|
255
|
+
throw new ImageBatchUsageError('--select needs an interactive terminal — use --list to print the files instead');
|
|
256
|
+
}
|
|
257
|
+
const s = createStyle(style);
|
|
258
|
+
const items = sources.map((source) => ({
|
|
259
|
+
label: source.file.rel,
|
|
260
|
+
hint: `${source.width}×${source.height} · ${source.format} · ${formatBytes(source.bytes)}`,
|
|
261
|
+
value: source,
|
|
262
|
+
selected: true,
|
|
263
|
+
}));
|
|
264
|
+
const picked = await multiSelect(io, items, {
|
|
265
|
+
title: `Pick the images to process (${items.length} found)`,
|
|
266
|
+
help: 'space toggle · a all · n none · i invert · / filter · enter confirm · q cancel',
|
|
267
|
+
filterable: true,
|
|
268
|
+
}, s);
|
|
269
|
+
if (picked === null || picked.length === 0) {
|
|
270
|
+
report.nothingSelected = true;
|
|
271
|
+
report.sources = [];
|
|
272
|
+
return report;
|
|
273
|
+
}
|
|
274
|
+
sources = picked;
|
|
275
|
+
}
|
|
276
|
+
report.sources = sources.map(summarize);
|
|
277
|
+
if (options.list)
|
|
278
|
+
return report;
|
|
279
|
+
const hashes = new Map();
|
|
280
|
+
if (placeholders && placeholders.types.length > 0) {
|
|
281
|
+
await pool(sources, options.concurrency ?? DEFAULT_CONCURRENCY, async (source) => {
|
|
282
|
+
try {
|
|
283
|
+
const computed = await computeHashes(sharp, readFileSync(source.file.abs), {
|
|
284
|
+
types: placeholders.types,
|
|
285
|
+
size: 100,
|
|
286
|
+
components: placeholders.components && placeholders.components !== 'auto'
|
|
287
|
+
? {
|
|
288
|
+
x: Number(placeholders.components[0]),
|
|
289
|
+
y: Number(placeholders.components[2]),
|
|
290
|
+
}
|
|
291
|
+
: placeholders.components === 'auto'
|
|
292
|
+
? 'auto'
|
|
293
|
+
: { x: 4, y: 3 },
|
|
294
|
+
...(placeholders.budget !== undefined ? { budget: placeholders.budget } : {}),
|
|
295
|
+
...(options.maxPixels !== undefined ? { maxPixels: options.maxPixels } : {}),
|
|
296
|
+
});
|
|
297
|
+
hashes.set(source.file.abs, computed);
|
|
298
|
+
}
|
|
299
|
+
catch (error) {
|
|
300
|
+
report.failures.push({
|
|
301
|
+
file: source.file.rel,
|
|
302
|
+
message: `placeholders: ${friendlyMessage(error, options.maxPixels)}`,
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
const planOptions = (list) => ({
|
|
308
|
+
sources: list,
|
|
309
|
+
config,
|
|
310
|
+
overrides: overrideLayer(options.overrides),
|
|
311
|
+
placement,
|
|
312
|
+
outDir,
|
|
313
|
+
...(options.flat ? { flat: true } : {}),
|
|
314
|
+
now,
|
|
315
|
+
});
|
|
316
|
+
let plan = planJobs(planOptions(sources));
|
|
317
|
+
if (placement === 'beside') {
|
|
318
|
+
for (let round = 0; round < 4; round++) {
|
|
319
|
+
const produced = new Map();
|
|
320
|
+
for (const job of plan.jobs)
|
|
321
|
+
produced.set(job.outAbs.toLowerCase(), job);
|
|
322
|
+
const derived = sources.filter((source) => {
|
|
323
|
+
const job = produced.get(source.file.abs.toLowerCase());
|
|
324
|
+
return job !== undefined && job.source !== source;
|
|
325
|
+
});
|
|
326
|
+
if (derived.length === 0)
|
|
327
|
+
break;
|
|
328
|
+
report.derived.push(...derived.map((source) => source.file.rel));
|
|
329
|
+
sources = sources.filter((source) => !derived.includes(source));
|
|
330
|
+
report.sources = sources.map(summarize);
|
|
331
|
+
plan = planJobs(planOptions(sources));
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
if (plan.collisions.length > 0) {
|
|
335
|
+
throw new ImageBatchUsageError(describeCollisions(plan.collisions));
|
|
336
|
+
}
|
|
337
|
+
if (placement === 'replace')
|
|
338
|
+
validateReplacePlan(plan.jobs);
|
|
339
|
+
const missingFormats = unsupportedFormats(sharp, plan.jobs.map((job) => job.format));
|
|
340
|
+
if (missingFormats.length > 0) {
|
|
341
|
+
throw new ImageBatchUsageError(`this sharp build cannot write ${missingFormats.join(', ')} (libvips was built without the encoder) — pick other formats`);
|
|
342
|
+
}
|
|
343
|
+
report.deduped = plan.deduped;
|
|
344
|
+
report.unmatched = plan.unmatched.map((source) => source.file.rel);
|
|
345
|
+
let backupDir = null;
|
|
346
|
+
let journal = null;
|
|
347
|
+
const wantsBackup = placement === 'replace' && options.backup !== false && !options.dryRun;
|
|
348
|
+
const ensureBackup = () => {
|
|
349
|
+
if (!wantsBackup)
|
|
350
|
+
return null;
|
|
351
|
+
if (backupDir === null) {
|
|
352
|
+
backupDir = backupDirFor(cwd, typeof options.backup === 'string' ? options.backup : undefined, now);
|
|
353
|
+
journal = newJournal(cwd, now);
|
|
354
|
+
report.backupDir = backupDir;
|
|
355
|
+
}
|
|
356
|
+
return backupDir;
|
|
357
|
+
};
|
|
358
|
+
if (placement === 'replace' && !options.dryRun && plan.jobs.length > 0) {
|
|
359
|
+
const summary = {
|
|
360
|
+
count: plan.jobs.length,
|
|
361
|
+
bytes: plan.jobs.reduce((sum, job) => sum + job.source.bytes, 0),
|
|
362
|
+
backupDir: wantsBackup ? resolve(backupBase, '<date>') : null,
|
|
363
|
+
};
|
|
364
|
+
if (!options.assumeYes) {
|
|
365
|
+
if (!interactive) {
|
|
366
|
+
throw new ImageBatchUsageError('--replace changes the original files — pass --yes to confirm (or --dry-run to preview what would change)');
|
|
367
|
+
}
|
|
368
|
+
const confirmed = await (options.confirm ?? defaultConfirm(io, style))(summary);
|
|
369
|
+
if (!confirmed) {
|
|
370
|
+
report.cancelled = true;
|
|
371
|
+
report.exitCode = 1;
|
|
372
|
+
return report;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
const explicitMode = options.overwrite;
|
|
377
|
+
const mode = explicitMode ?? (interactive ? 'ask' : 'skip');
|
|
378
|
+
const implicitSkip = explicitMode === undefined && !interactive;
|
|
379
|
+
const resolver = createOverwriteResolver(mode, mode === 'ask' ? (options.prompt ?? defaultPrompt(io, style)) : undefined);
|
|
380
|
+
let chain = Promise.resolve();
|
|
381
|
+
const decide = (info) => {
|
|
382
|
+
const next = chain.then(() => resolver.decide(info));
|
|
383
|
+
chain = next.catch(() => undefined);
|
|
384
|
+
return next;
|
|
385
|
+
};
|
|
386
|
+
const groups = new Map();
|
|
387
|
+
for (const job of plan.jobs) {
|
|
388
|
+
const list = groups.get(job.source);
|
|
389
|
+
if (list)
|
|
390
|
+
list.push(job);
|
|
391
|
+
else
|
|
392
|
+
groups.set(job.source, [job]);
|
|
393
|
+
}
|
|
394
|
+
const total = plan.jobs.length;
|
|
395
|
+
let done = 0;
|
|
396
|
+
const results = new Map();
|
|
397
|
+
const renderOptions = options.maxPixels !== undefined ? { maxPixels: options.maxPixels } : {};
|
|
398
|
+
const baseResult = (job) => ({
|
|
399
|
+
source: job.source.file.rel,
|
|
400
|
+
sourceAbs: job.source.file.abs,
|
|
401
|
+
output: job.outRel,
|
|
402
|
+
outputAbs: job.outAbs,
|
|
403
|
+
recipe: job.recipeLabel,
|
|
404
|
+
format: job.format,
|
|
405
|
+
width: job.width,
|
|
406
|
+
height: job.height,
|
|
407
|
+
status: 'written',
|
|
408
|
+
});
|
|
409
|
+
const replaceJob = async (job, result) => {
|
|
410
|
+
const source = job.source;
|
|
411
|
+
const entry = cache.outputs[job.outAbs];
|
|
412
|
+
result.before = source.bytes;
|
|
413
|
+
if (!options.force &&
|
|
414
|
+
entry !== undefined &&
|
|
415
|
+
entry.hash === source.hash &&
|
|
416
|
+
entry.settingsKey === job.settingsKey) {
|
|
417
|
+
result.status = 'already-processed';
|
|
418
|
+
result.bytes = source.bytes;
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
const buffer = readFileSync(source.file.abs);
|
|
422
|
+
const data = await renderJob(sharp, job, buffer, renderOptions);
|
|
423
|
+
if (onlyIfSmaller && data.length >= source.bytes) {
|
|
424
|
+
result.status = options.dryRun ? 'would-keep' : 'kept';
|
|
425
|
+
result.bytes = source.bytes;
|
|
426
|
+
if (!options.dryRun) {
|
|
427
|
+
cache.outputs[job.outAbs] = {
|
|
428
|
+
fingerprint: job.fingerprint,
|
|
429
|
+
settingsKey: job.settingsKey,
|
|
430
|
+
hash: source.hash,
|
|
431
|
+
bytes: source.bytes,
|
|
432
|
+
};
|
|
433
|
+
cacheDirty = true;
|
|
434
|
+
}
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
if (options.dryRun) {
|
|
438
|
+
result.status = 'would-replace';
|
|
439
|
+
result.bytes = data.length;
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
const dir = ensureBackup();
|
|
443
|
+
let saved = null;
|
|
444
|
+
if (dir !== null) {
|
|
445
|
+
saved = backupPathFor(dir, cwd, source.file.abs);
|
|
446
|
+
copyIntoBackup(source.file.abs, saved);
|
|
447
|
+
}
|
|
448
|
+
try {
|
|
449
|
+
writeAtomic(job.outAbs, data);
|
|
450
|
+
}
|
|
451
|
+
catch (error) {
|
|
452
|
+
if (saved !== null)
|
|
453
|
+
rmSync(saved, { force: true });
|
|
454
|
+
throw error;
|
|
455
|
+
}
|
|
456
|
+
const hashAfter = sha1(data);
|
|
457
|
+
if (journal !== null && saved !== null && dir !== null) {
|
|
458
|
+
journal.entries.push({
|
|
459
|
+
path: job.outAbs,
|
|
460
|
+
backup: saved,
|
|
461
|
+
bytesBefore: source.bytes,
|
|
462
|
+
bytesAfter: data.length,
|
|
463
|
+
hashBefore: source.hash,
|
|
464
|
+
hashAfter,
|
|
465
|
+
});
|
|
466
|
+
if (journal.entries.length % JOURNAL_FLUSH_EVERY === 0)
|
|
467
|
+
saveJournal(dir, journal);
|
|
468
|
+
}
|
|
469
|
+
cache.outputs[job.outAbs] = {
|
|
470
|
+
fingerprint: job.fingerprint,
|
|
471
|
+
settingsKey: job.settingsKey,
|
|
472
|
+
hash: hashAfter,
|
|
473
|
+
bytes: data.length,
|
|
474
|
+
};
|
|
475
|
+
cacheDirty = true;
|
|
476
|
+
result.status = 'replaced';
|
|
477
|
+
result.bytes = data.length;
|
|
478
|
+
};
|
|
479
|
+
const processGroup = async (jobs) => {
|
|
480
|
+
let buffer = null;
|
|
481
|
+
for (const job of jobs) {
|
|
482
|
+
if (report.aborted)
|
|
483
|
+
return;
|
|
484
|
+
const result = baseResult(job);
|
|
485
|
+
try {
|
|
486
|
+
if (placement === 'replace') {
|
|
487
|
+
await replaceJob(job, result);
|
|
488
|
+
}
|
|
489
|
+
else {
|
|
490
|
+
const exists = existsSync(job.outAbs);
|
|
491
|
+
const existingBytes = exists ? statSync(job.outAbs).size : 0;
|
|
492
|
+
const known = cache.outputs[job.outAbs];
|
|
493
|
+
const upToDate = exists &&
|
|
494
|
+
!options.force &&
|
|
495
|
+
known !== undefined &&
|
|
496
|
+
known.fingerprint === job.fingerprint &&
|
|
497
|
+
known.bytes === existingBytes;
|
|
498
|
+
if (upToDate) {
|
|
499
|
+
result.status = 'up-to-date';
|
|
500
|
+
result.bytes = existingBytes;
|
|
501
|
+
}
|
|
502
|
+
else {
|
|
503
|
+
let action = 'write';
|
|
504
|
+
if (exists && !options.force) {
|
|
505
|
+
if (options.dryRun) {
|
|
506
|
+
result.status = 'would-overwrite';
|
|
507
|
+
result.bytes = existingBytes;
|
|
508
|
+
results.set(job, result);
|
|
509
|
+
done += 1;
|
|
510
|
+
options.onProgress?.({ done, total });
|
|
511
|
+
continue;
|
|
512
|
+
}
|
|
513
|
+
action = await decide({
|
|
514
|
+
outRel: job.outRel,
|
|
515
|
+
sourceRel: job.source.file.rel,
|
|
516
|
+
existingBytes,
|
|
517
|
+
reason: known ? 'changed' : 'unknown',
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
if (action === 'skip') {
|
|
521
|
+
result.status = 'skipped';
|
|
522
|
+
result.bytes = existingBytes;
|
|
523
|
+
}
|
|
524
|
+
else if (action === 'fail') {
|
|
525
|
+
result.status = 'conflict';
|
|
526
|
+
result.bytes = existingBytes;
|
|
527
|
+
}
|
|
528
|
+
else if (options.dryRun) {
|
|
529
|
+
result.status = 'would-write';
|
|
530
|
+
}
|
|
531
|
+
else {
|
|
532
|
+
buffer ??= readFileSync(job.source.file.abs);
|
|
533
|
+
const data = await renderJob(sharp, job, buffer, renderOptions);
|
|
534
|
+
writeAtomic(job.outAbs, data);
|
|
535
|
+
cache.outputs[job.outAbs] = { fingerprint: job.fingerprint, bytes: data.length };
|
|
536
|
+
cacheDirty = true;
|
|
537
|
+
result.status = 'written';
|
|
538
|
+
result.bytes = data.length;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
catch (error) {
|
|
544
|
+
if (error instanceof ImageBatchAbortError) {
|
|
545
|
+
report.aborted = true;
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
result.status = 'error';
|
|
549
|
+
result.message = friendlyMessage(error, options.maxPixels);
|
|
550
|
+
}
|
|
551
|
+
results.set(job, result);
|
|
552
|
+
done += 1;
|
|
553
|
+
options.onProgress?.({ done, total });
|
|
554
|
+
}
|
|
555
|
+
};
|
|
556
|
+
try {
|
|
557
|
+
await pool([...groups.values()], options.concurrency ?? DEFAULT_CONCURRENCY, processGroup);
|
|
558
|
+
}
|
|
559
|
+
finally {
|
|
560
|
+
const finalJournal = journal;
|
|
561
|
+
const finalDir = backupDir;
|
|
562
|
+
if (finalJournal !== null && finalDir !== null && finalJournal.entries.length > 0) {
|
|
563
|
+
saveJournal(finalDir, finalJournal);
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
for (const job of plan.jobs) {
|
|
567
|
+
const result = results.get(job);
|
|
568
|
+
if (result)
|
|
569
|
+
report.results.push(result);
|
|
570
|
+
}
|
|
571
|
+
for (const result of report.results)
|
|
572
|
+
report.counts[result.status] += 1;
|
|
573
|
+
for (const result of report.results) {
|
|
574
|
+
if (result.status === 'written') {
|
|
575
|
+
report.bytesOut += result.bytes ?? 0;
|
|
576
|
+
}
|
|
577
|
+
else if (result.status === 'replaced') {
|
|
578
|
+
report.bytesIn += result.before ?? 0;
|
|
579
|
+
report.bytesOut += result.bytes ?? 0;
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
if (placement !== 'replace') {
|
|
583
|
+
const processed = new Set();
|
|
584
|
+
for (const job of plan.jobs) {
|
|
585
|
+
if (results.get(job)?.status === 'written')
|
|
586
|
+
processed.add(job.source);
|
|
587
|
+
}
|
|
588
|
+
report.bytesIn = [...processed].reduce((sum, source) => sum + source.bytes, 0);
|
|
589
|
+
}
|
|
590
|
+
if (!options.dryRun && cachePath && cacheDirty)
|
|
591
|
+
saveBatchCache(cachePath, cache);
|
|
592
|
+
if (options.emit && !options.dryRun && !report.aborted) {
|
|
593
|
+
writeEmit(resolve(cwd, options.emit), report, sources, hashes, options.keyPrefix ?? '');
|
|
594
|
+
report.emitPath = resolve(cwd, options.emit);
|
|
595
|
+
}
|
|
596
|
+
const failed = report.counts.error > 0 ||
|
|
597
|
+
report.counts.conflict > 0 ||
|
|
598
|
+
report.failures.length > 0 ||
|
|
599
|
+
report.aborted ||
|
|
600
|
+
(implicitSkip && report.counts.skipped > 0);
|
|
601
|
+
report.exitCode = failed ? 1 : 0;
|
|
602
|
+
return report;
|
|
603
|
+
}
|
|
604
|
+
function writeEmit(path, report, sources, hashes, keyPrefix) {
|
|
605
|
+
const skippedStatuses = new Set(['error', 'conflict']);
|
|
606
|
+
const bySource = new Map();
|
|
607
|
+
for (const result of report.results) {
|
|
608
|
+
if (skippedStatuses.has(result.status))
|
|
609
|
+
continue;
|
|
610
|
+
const list = bySource.get(result.source);
|
|
611
|
+
if (list)
|
|
612
|
+
list.push(result);
|
|
613
|
+
else
|
|
614
|
+
bySource.set(result.source, [result]);
|
|
615
|
+
}
|
|
616
|
+
const sourcesOut = {};
|
|
617
|
+
const placeholders = {};
|
|
618
|
+
for (const source of sources) {
|
|
619
|
+
const outputs = bySource.get(source.file.rel) ?? [];
|
|
620
|
+
const computed = hashes.get(source.file.abs);
|
|
621
|
+
const hashFields = {};
|
|
622
|
+
if (computed) {
|
|
623
|
+
for (const key of ['hazehash', 'blurhash', 'thumbhash', 'color', 'preview']) {
|
|
624
|
+
const value = computed[key];
|
|
625
|
+
if (value !== undefined)
|
|
626
|
+
hashFields[key] = value;
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
sourcesOut[source.file.rel] = {
|
|
630
|
+
width: source.width,
|
|
631
|
+
height: source.height,
|
|
632
|
+
format: source.format,
|
|
633
|
+
bytes: source.bytes,
|
|
634
|
+
...hashFields,
|
|
635
|
+
outputs: outputs.map((output) => ({
|
|
636
|
+
path: output.output,
|
|
637
|
+
width: output.width,
|
|
638
|
+
height: output.height,
|
|
639
|
+
format: output.format,
|
|
640
|
+
...(output.bytes !== undefined ? { bytes: output.bytes } : {}),
|
|
641
|
+
})),
|
|
642
|
+
};
|
|
643
|
+
if (Object.keys(hashFields).length > 0) {
|
|
644
|
+
for (const output of outputs) {
|
|
645
|
+
placeholders[`${keyPrefix}${output.output}`] = {
|
|
646
|
+
...hashFields,
|
|
647
|
+
width: output.width,
|
|
648
|
+
height: output.height,
|
|
649
|
+
};
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
654
|
+
writeFileSync(path, `${JSON.stringify({ version: 1, sources: sourcesOut, placeholders }, null, 2)}\n`);
|
|
655
|
+
}
|
|
656
|
+
//# sourceMappingURL=run.js.map
|