bytecut 0.0.0-stage → 0.0.1

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.
@@ -0,0 +1,248 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/core/engines/image.ts
4
+ import sharp from "sharp";
5
+
6
+ // src/core/placeholders/blurhash.ts
7
+ var ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz#$%*+,-.:;=?@[]^_{|}~";
8
+ function encode83(value, length) {
9
+ let out = "";
10
+ for (let i = 1; i <= length; i++) {
11
+ const digit = Math.floor(value / 83 ** (length - i)) % 83;
12
+ out += ALPHABET[digit];
13
+ }
14
+ return out;
15
+ }
16
+ function sRGBToLinear(value) {
17
+ const v = value / 255;
18
+ return v <= 0.04045 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4;
19
+ }
20
+ function linearToSRGB(value) {
21
+ const v = Math.max(0, Math.min(1, value));
22
+ return v <= 31308e-7 ? Math.trunc(v * 12.92 * 255 + 0.5) : Math.trunc((1.055 * v ** (1 / 2.4) - 0.055) * 255 + 0.5);
23
+ }
24
+ var signPow = (value, exp) => Math.sign(value) * Math.abs(value) ** exp;
25
+ function basisFactor(pixels, width, height, xc, yc) {
26
+ let r = 0;
27
+ let g = 0;
28
+ let b = 0;
29
+ const normalisation = xc === 0 && yc === 0 ? 1 : 2;
30
+ const bytesPerRow = width * 4;
31
+ for (let x = 0; x < width; x++) {
32
+ for (let y = 0; y < height; y++) {
33
+ const basis = normalisation * Math.cos(Math.PI * xc * x / width) * Math.cos(Math.PI * yc * y / height);
34
+ r += basis * sRGBToLinear(pixels[4 * x + y * bytesPerRow]);
35
+ g += basis * sRGBToLinear(pixels[4 * x + 1 + y * bytesPerRow]);
36
+ b += basis * sRGBToLinear(pixels[4 * x + 2 + y * bytesPerRow]);
37
+ }
38
+ }
39
+ const scale = 1 / (width * height);
40
+ return [r * scale, g * scale, b * scale];
41
+ }
42
+ function encodeBlurhash(pixels, width, height, componentX, componentY) {
43
+ if (componentX < 1 || componentX > 9 || componentY < 1 || componentY > 9) {
44
+ throw new RangeError("BlurHash needs 1 to 9 components per axis");
45
+ }
46
+ if (pixels.length !== width * height * 4) {
47
+ throw new RangeError("Pixel buffer does not match the given width and height");
48
+ }
49
+ const factors = [];
50
+ for (let y = 0; y < componentY; y++) {
51
+ for (let x = 0; x < componentX; x++) {
52
+ factors.push(basisFactor(pixels, width, height, x, y));
53
+ }
54
+ }
55
+ const dc = factors[0];
56
+ const ac = factors.slice(1);
57
+ let hash = encode83(componentX - 1 + (componentY - 1) * 9, 1);
58
+ let maximumValue;
59
+ if (ac.length > 0) {
60
+ const actualMax = Math.max(...ac.flatMap((f) => f.map(Math.abs)));
61
+ const quantisedMax = Math.max(0, Math.min(82, Math.floor(actualMax * 166 - 0.5)));
62
+ maximumValue = (quantisedMax + 1) / 166;
63
+ hash += encode83(quantisedMax, 1);
64
+ } else {
65
+ maximumValue = 1;
66
+ hash += encode83(0, 1);
67
+ }
68
+ hash += encode83(
69
+ (linearToSRGB(dc[0]) << 16) + (linearToSRGB(dc[1]) << 8) + linearToSRGB(dc[2]),
70
+ 4
71
+ );
72
+ for (const f of ac) {
73
+ const q = (v) => Math.max(0, Math.min(18, Math.floor(signPow(v / maximumValue, 0.5) * 9 + 9.5)));
74
+ hash += encode83(q(f[0]) * 19 * 19 + q(f[1]) * 19 + q(f[2]), 2);
75
+ }
76
+ return hash;
77
+ }
78
+ function componentsFor(width, height) {
79
+ if (width <= 0 || height <= 0) return [4, 3];
80
+ const ratio = width / height;
81
+ if (ratio > 1.25) return [4, 3];
82
+ if (ratio < 0.8) return [3, 4];
83
+ return [4, 4];
84
+ }
85
+
86
+ // src/core/engines/exif.ts
87
+ function hasGpsIfd(exif) {
88
+ const base = exif.subarray(0, 6).toString("latin1") === "Exif\0\0" ? 6 : 0;
89
+ if (exif.length < base + 8) return false;
90
+ const order = exif.toString("latin1", base, base + 2);
91
+ const little = order === "II";
92
+ if (!little && order !== "MM") return false;
93
+ const u16 = (at) => little ? exif.readUInt16LE(at) : exif.readUInt16BE(at);
94
+ const u32 = (at) => little ? exif.readUInt32LE(at) : exif.readUInt32BE(at);
95
+ const ifd0 = base + u32(base + 4);
96
+ if (ifd0 + 2 > exif.length) return false;
97
+ const count = u16(ifd0);
98
+ for (let i = 0; i < count; i++) {
99
+ const entry = ifd0 + 2 + i * 12;
100
+ if (entry + 12 > exif.length) return false;
101
+ if (u16(entry) === 34853) return true;
102
+ }
103
+ return false;
104
+ }
105
+
106
+ // src/core/engines/image.ts
107
+ function sourceFormat(format, compression) {
108
+ switch (format) {
109
+ case "jpeg":
110
+ case "png":
111
+ case "webp":
112
+ case "gif":
113
+ return format;
114
+ case "heif":
115
+ return compression === "av1" ? "avif" : "other";
116
+ default:
117
+ return "other";
118
+ }
119
+ }
120
+ var SAMPLE_EDGE = 64;
121
+ async function sampleColors(input) {
122
+ const { data, info } = await sharp(input).rotate().resize(SAMPLE_EDGE, SAMPLE_EDGE, { fit: "inside", kernel: "nearest" }).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
123
+ const seen = /* @__PURE__ */ new Set();
124
+ for (let i = 0; i + 3 < data.length; i += 4) {
125
+ const a = data[i + 3];
126
+ const key = a === 0 ? 4294967295 : data[i] >> 3 << 15 | data[i + 1] >> 3 << 10 | data[i + 2] >> 3 << 5 | a >> 6;
127
+ seen.add(key);
128
+ }
129
+ return { colorCount: seen.size, sampleSize: info.width * info.height };
130
+ }
131
+ function encoderNames() {
132
+ const v = sharp.versions;
133
+ return {
134
+ avif: `libaom-av1 ${v.aom ?? "unknown"}`,
135
+ webp: `libwebp ${v.webp ?? "unknown"}`
136
+ };
137
+ }
138
+ var LQIP_EDGE = 16;
139
+ var BLURHASH_EDGE = 32;
140
+ async function makePlaceholder(input, kind) {
141
+ switch (kind) {
142
+ case "dominant-color": {
143
+ const { dominant } = await sharp(input).rotate().stats();
144
+ const hex = (n) => n.toString(16).padStart(2, "0");
145
+ return { kind, value: `#${hex(dominant.r)}${hex(dominant.g)}${hex(dominant.b)}` };
146
+ }
147
+ case "lqip": {
148
+ const webp = await sharp(input).rotate().resize(LQIP_EDGE, LQIP_EDGE, { fit: "inside" }).webp({ quality: 40, effort: 6 }).toBuffer();
149
+ return { kind, value: `data:image/webp;base64,${webp.toString("base64")}` };
150
+ }
151
+ case "blurhash": {
152
+ const { data, info } = await sharp(input).rotate().resize(BLURHASH_EDGE, BLURHASH_EDGE, { fit: "inside" }).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
153
+ const [cx, cy] = componentsFor(info.width, info.height);
154
+ return { kind, value: encodeBlurhash(data, info.width, info.height, cx, cy) };
155
+ }
156
+ }
157
+ }
158
+ var sharpImageEngine = {
159
+ encoders: encoderNames,
160
+ placeholder: makePlaceholder,
161
+ versions() {
162
+ const v = sharp.versions;
163
+ return { sharp: v.sharp ?? "unknown", vips: v.vips ?? "unknown" };
164
+ },
165
+ async analyze(input) {
166
+ const meta = await sharp(input).metadata();
167
+ const swap = (meta.orientation ?? 1) >= 5;
168
+ const width = (swap ? meta.height : meta.width) ?? 0;
169
+ const height = (swap ? meta.width : meta.height) ?? 0;
170
+ const format = sourceFormat(meta.format, meta.compression);
171
+ const hasAlpha = meta.hasAlpha ?? false;
172
+ let alphaUsed = false;
173
+ if (hasAlpha && format !== "other") {
174
+ const { channels } = await sharp(input).stats();
175
+ const alpha = channels[channels.length - 1];
176
+ alphaUsed = alpha !== void 0 && alpha.min < 255;
177
+ }
178
+ const { colorCount, sampleSize } = format === "other" ? { colorCount: 0, sampleSize: 0 } : await sampleColors(input);
179
+ return {
180
+ width,
181
+ height,
182
+ format,
183
+ hasAlpha,
184
+ alphaUsed,
185
+ animated: (meta.pages ?? 1) > 1,
186
+ space: meta.space ?? "unknown",
187
+ depth: meta.depth ?? "unknown",
188
+ colorCount,
189
+ sampleSize,
190
+ metadata: {
191
+ exif: meta.exif?.byteLength ?? 0,
192
+ xmp: meta.xmp?.byteLength ?? 0,
193
+ iptc: meta.iptc?.byteLength ?? 0,
194
+ icc: meta.icc?.byteLength ?? 0,
195
+ gps: meta.exif ? hasGpsIfd(meta.exif) : false
196
+ }
197
+ };
198
+ },
199
+ async decodeRaw(input, options) {
200
+ let image = sharp(input).rotate();
201
+ if (options.width) image = image.resize({ width: options.width, withoutEnlargement: true });
202
+ const { data, info } = await image.toColourspace("srgb").ensureAlpha().raw().toBuffer({ resolveWithObject: true });
203
+ return {
204
+ data: new Uint8Array(data.buffer, data.byteOffset, data.byteLength),
205
+ width: info.width,
206
+ height: info.height,
207
+ channels: 4
208
+ };
209
+ },
210
+ async encodeRaw(image, request) {
211
+ const raw = sharp(image.data, {
212
+ raw: { width: image.width, height: image.height, channels: image.channels }
213
+ });
214
+ const encoded = request.format === "avif" ? raw.avif({ quality: request.quality, effort: request.effort }) : raw.webp({ quality: request.quality, effort: request.effort });
215
+ const { data, info } = await encoded.toBuffer({ resolveWithObject: true });
216
+ return {
217
+ data,
218
+ width: info.width,
219
+ height: info.height,
220
+ encoder: encoderNames()[request.format]
221
+ };
222
+ },
223
+ async encode(input, request) {
224
+ const meta = await sharp(input).metadata();
225
+ let image = sharp(input).rotate().resize({ width: request.width, withoutEnlargement: true });
226
+ image = meta.space === "cmyk" ? image.toColourspace("srgb") : image.keepIccProfile();
227
+ image = request.format === "avif" ? image.avif({
228
+ quality: request.quality,
229
+ effort: request.effort,
230
+ lossless: request.lossless
231
+ }) : image.webp({
232
+ quality: request.quality,
233
+ effort: request.effort,
234
+ lossless: request.lossless
235
+ });
236
+ const { data, info } = await image.toBuffer({ resolveWithObject: true });
237
+ return {
238
+ data,
239
+ width: info.width,
240
+ height: info.height,
241
+ encoder: encoderNames()[request.format]
242
+ };
243
+ }
244
+ };
245
+
246
+ export {
247
+ sharpImageEngine
248
+ };
@@ -0,0 +1,463 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ BytecutError,
4
+ detectProject,
5
+ hint,
6
+ matchesGlob
7
+ } from "./chunk-TWI2OZJZ.js";
8
+
9
+ // src/core/presets/hero.ts
10
+ var HERO_PRESET = {
11
+ name: "hero",
12
+ widths: [640, 1280, 1920, 2560],
13
+ formats: ["avif", "webp"],
14
+ encoders: {
15
+ avif: { quality: 65, effort: 4 },
16
+ webp: { quality: 82, effort: 4 }
17
+ },
18
+ priority: true
19
+ };
20
+
21
+ // src/core/presets/icon.ts
22
+ var ICON_PRESET = {
23
+ name: "icon",
24
+ widths: [64, 128, 256, 512],
25
+ formats: ["avif", "webp"],
26
+ encoders: {
27
+ avif: { quality: 80, effort: 4 },
28
+ webp: { quality: 90, effort: 4 }
29
+ },
30
+ lossless: true
31
+ };
32
+
33
+ // src/core/presets/photo.ts
34
+ var PHOTO_PRESET = {
35
+ name: "photo",
36
+ widths: [640, 1280, 1920],
37
+ formats: ["avif", "webp"],
38
+ encoders: {
39
+ avif: { quality: 60, effort: 4 },
40
+ webp: { quality: 78, effort: 4 }
41
+ }
42
+ };
43
+
44
+ // src/core/presets/index.ts
45
+ var IMAGE_PRESETS = {
46
+ photo: PHOTO_PRESET,
47
+ hero: HERO_PRESET,
48
+ icon: ICON_PRESET
49
+ };
50
+ function getImagePreset(name, catalog) {
51
+ const presets = catalog?.image ?? IMAGE_PRESETS;
52
+ const preset = presets[name];
53
+ if (!preset) {
54
+ throw new BytecutError(
55
+ "BAD_ARGUMENTS",
56
+ `Unknown preset "${name}". Image presets: ${Object.keys(presets).join(", ")}.`
57
+ );
58
+ }
59
+ return preset;
60
+ }
61
+
62
+ // src/core/presets/video.ts
63
+ var POSTER = { quality: 55, effort: 4, at: 0.1 };
64
+ var BACKGROUND_VIDEO_PRESET = {
65
+ name: "background-video",
66
+ codecs: ["av1", "h264"],
67
+ audio: "strip",
68
+ audioBitrate: "96k",
69
+ maxWidth: 1920,
70
+ maxFps: 30,
71
+ crf: { av1: 38, h264: 28 },
72
+ speed: { av1: "6", h264: "slow" },
73
+ poster: POSTER
74
+ };
75
+ var CONTENT_VIDEO_PRESET = {
76
+ name: "content-video",
77
+ codecs: ["av1", "h264"],
78
+ audio: "keep",
79
+ audioBitrate: "128k",
80
+ maxWidth: null,
81
+ maxFps: null,
82
+ crf: { av1: 32, h264: 23 },
83
+ speed: { av1: "6", h264: "slow" },
84
+ poster: POSTER
85
+ };
86
+ var GIF_TO_VIDEO_PRESET = {
87
+ name: "gif-to-video",
88
+ codecs: ["av1", "h264"],
89
+ audio: "strip",
90
+ audioBitrate: "96k",
91
+ maxWidth: 1920,
92
+ maxFps: null,
93
+ crf: { av1: 36, h264: 26 },
94
+ speed: { av1: "6", h264: "slow" },
95
+ poster: POSTER
96
+ };
97
+ var VIDEO_PRESETS = {
98
+ "background-video": BACKGROUND_VIDEO_PRESET,
99
+ "content-video": CONTENT_VIDEO_PRESET,
100
+ "gif-to-video": GIF_TO_VIDEO_PRESET
101
+ };
102
+ function getVideoPreset(name, catalog) {
103
+ return (catalog?.video ?? VIDEO_PRESETS)[name];
104
+ }
105
+
106
+ // src/core/presets/catalog.ts
107
+ var BUILTIN_CATALOG = {
108
+ image: IMAGE_PRESETS,
109
+ video: VIDEO_PRESETS,
110
+ root: Object.fromEntries(
111
+ [...Object.keys(IMAGE_PRESETS), ...Object.keys(VIDEO_PRESETS)].map((n) => [n, n])
112
+ ),
113
+ origin: {}
114
+ };
115
+ function rootPreset(catalog, name) {
116
+ return catalog?.root[name] ?? name;
117
+ }
118
+ var BUILTIN_POLICY = {
119
+ catalog: BUILTIN_CATALOG,
120
+ assetFor: () => void 0
121
+ };
122
+
123
+ // src/core/config/presets.ts
124
+ var IMAGE_TARGETS = {
125
+ low: { avif: 45, webp: 65 },
126
+ balanced: { avif: 60, webp: 78 },
127
+ high: { avif: 75, webp: 88 }
128
+ };
129
+ var VIDEO_TARGET_CRF_OFFSET = { low: 4, balanced: 0, high: -4 };
130
+ var IMAGE_ONLY = [
131
+ "widths",
132
+ "formats",
133
+ "quality",
134
+ "effort",
135
+ "lossless",
136
+ "priority",
137
+ "floor",
138
+ "maxSize"
139
+ ];
140
+ var VIDEO_ONLY = ["audio", "maxWidth", "maxFps", "codecs", "crf"];
141
+ var METRICS = ["ssimulacra2", "vmaf"];
142
+ function fail(message) {
143
+ throw new BytecutError("CONFIG_INVALID", `presets: ${message}`);
144
+ }
145
+ function applyImage(name, base, def) {
146
+ for (const key of VIDEO_ONLY) {
147
+ if (def[key] !== void 0) {
148
+ fail(`"${name}" extends an image preset, so \`${key}\` (a video setting) does not apply.`);
149
+ }
150
+ }
151
+ if (def.vmaf !== void 0) {
152
+ fail(`"${name}" extends an image preset, so \`vmaf\` (a video metric) does not apply.`);
153
+ }
154
+ const next = {
155
+ ...base,
156
+ name,
157
+ widths: [...base.widths],
158
+ formats: [...base.formats],
159
+ encoders: { avif: { ...base.encoders.avif }, webp: { ...base.encoders.webp } }
160
+ };
161
+ if (def.target === "lossless") next.lossless = true;
162
+ else if (def.target) {
163
+ next.encoders.avif.quality = IMAGE_TARGETS[def.target].avif;
164
+ next.encoders.webp.quality = IMAGE_TARGETS[def.target].webp;
165
+ }
166
+ if (def.widths) next.widths = [...new Set(def.widths)].sort((a, b) => a - b);
167
+ if (def.formats) next.formats = [...new Set(def.formats)];
168
+ if (def.quality?.avif !== void 0) next.encoders.avif.quality = def.quality.avif;
169
+ if (def.quality?.webp !== void 0) next.encoders.webp.quality = def.quality.webp;
170
+ if (def.effort?.avif !== void 0) next.encoders.avif.effort = def.effort.avif;
171
+ if (def.effort?.webp !== void 0) next.encoders.webp.effort = def.effort.webp;
172
+ if (def.lossless !== void 0) next.lossless = def.lossless;
173
+ if (def.priority !== void 0) next.priority = def.priority;
174
+ if (def.ssimulacra2 !== void 0) {
175
+ next.search = {
176
+ metric: "ssimulacra2",
177
+ target: def.ssimulacra2,
178
+ ...def.floor !== void 0 ? { floor: def.floor } : {},
179
+ ...def.maxSize !== void 0 ? { maxBytes: def.maxSize } : {}
180
+ };
181
+ }
182
+ return next;
183
+ }
184
+ function applyVideo(name, base, def) {
185
+ for (const key of IMAGE_ONLY) {
186
+ if (def[key] !== void 0) {
187
+ fail(`"${name}" extends a video preset, so \`${key}\` (an image setting) does not apply.`);
188
+ }
189
+ }
190
+ if (def.target === "lossless") {
191
+ fail(`"${name}": \`target: "lossless"\` is not available for video; use "high".`);
192
+ }
193
+ const next = {
194
+ ...base,
195
+ name,
196
+ codecs: [...base.codecs],
197
+ crf: { ...base.crf },
198
+ speed: { ...base.speed },
199
+ poster: { ...base.poster }
200
+ };
201
+ if (def.target) {
202
+ const offset = VIDEO_TARGET_CRF_OFFSET[def.target];
203
+ next.crf = { av1: base.crf.av1 + offset, h264: base.crf.h264 + offset };
204
+ }
205
+ if (def.crf?.av1 !== void 0) next.crf.av1 = def.crf.av1;
206
+ if (def.crf?.h264 !== void 0) next.crf.h264 = def.crf.h264;
207
+ if (def.codecs) next.codecs = [...new Set(def.codecs)];
208
+ if (def.audio) next.audio = def.audio;
209
+ if (def.maxWidth !== void 0) next.maxWidth = def.maxWidth;
210
+ if (def.maxFps !== void 0) next.maxFps = def.maxFps;
211
+ if (def.ssimulacra2 !== void 0)
212
+ next.search = { metric: "ssimulacra2", target: def.ssimulacra2 };
213
+ if (def.vmaf !== void 0) next.search = { metric: "vmaf", target: def.vmaf };
214
+ return next;
215
+ }
216
+ var CHANGE_KEYS = [...IMAGE_ONLY, ...VIDEO_ONLY, ...METRICS, "target"];
217
+ function buildCatalog(defs) {
218
+ const image = { ...BUILTIN_CATALOG.image };
219
+ const video = { ...BUILTIN_CATALOG.video };
220
+ const root = { ...BUILTIN_CATALOG.root };
221
+ const origin = {};
222
+ if (!defs) return { image, video, root, origin };
223
+ const builtinNames = new Set(Object.keys(BUILTIN_CATALOG.root));
224
+ const done = /* @__PURE__ */ new Set();
225
+ function resolve(name, stack) {
226
+ if (done.has(name)) return;
227
+ const def = defs[name];
228
+ if (!def) return;
229
+ if (stack.includes(name)) fail(`extends cycle: ${[...stack, name].join(" \u2192 ")}.`);
230
+ const parent = def.extends ?? (builtinNames.has(name) ? name : void 0);
231
+ if (parent === void 0) {
232
+ fail(
233
+ `"${name}" must say what it extends, e.g. { "extends": "photo" }. Built-ins: ${[...builtinNames].join(", ")}.`
234
+ );
235
+ }
236
+ if (parent === "svg") fail(`"${name}" extends "svg", which has no output yet.`);
237
+ if (parent === name) {
238
+ if (!builtinNames.has(name)) fail(`"${name}" extends itself.`);
239
+ } else if (defs[parent]) {
240
+ resolve(parent, [...stack, name]);
241
+ } else if (!builtinNames.has(parent)) {
242
+ const known = [...builtinNames, ...Object.keys(defs)];
243
+ fail(`"${name}" extends unknown preset "${parent}"${hint(parent, known)}.`);
244
+ }
245
+ const baseName = parent === name ? name : parent;
246
+ const chain = [];
247
+ if (parent === name) chain.push(name);
248
+ else chain.push(...origin[parent]?.chain ?? [parent], name);
249
+ if (image[baseName]) {
250
+ image[name] = applyImage(name, image[baseName], def);
251
+ } else if (video[baseName]) {
252
+ video[name] = applyVideo(name, video[baseName], def);
253
+ } else {
254
+ fail(`"${name}" extends "${baseName}", which is not an image or video preset.`);
255
+ }
256
+ root[name] = parent === name ? name : root[parent] ?? parent;
257
+ origin[name] = {
258
+ chain,
259
+ overrides: CHANGE_KEYS.filter((key) => def[key] !== void 0)
260
+ };
261
+ done.add(name);
262
+ }
263
+ for (const name of Object.keys(defs)) resolve(name, []);
264
+ return { image, video, root, origin };
265
+ }
266
+
267
+ // src/core/config/resolve.ts
268
+ function resolveConfigData(config, project, loaded) {
269
+ const name = loaded?.name ?? "config";
270
+ const catalog = buildCatalog(config.presets);
271
+ const assets = Object.entries(config.assets ?? {}).map(([pattern, preset]) => ({
272
+ pattern,
273
+ preset
274
+ }));
275
+ for (const { pattern, preset } of assets) {
276
+ if (!catalog.image[preset] && !catalog.video[preset]) {
277
+ const known = [...Object.keys(catalog.image), ...Object.keys(catalog.video)];
278
+ throw new BytecutError(
279
+ "CONFIG_INVALID",
280
+ `${name}: assets["${pattern}"]: unknown preset "${preset}"${hint(preset, known)}. Known presets: ${known.join(", ")}.`
281
+ );
282
+ }
283
+ }
284
+ const rules = {};
285
+ for (const [id, setting] of Object.entries(config.audit?.rules ?? {})) {
286
+ rules[id] = setting;
287
+ }
288
+ const budgets = {};
289
+ if (config.budgets?.maxFileSize !== void 0) budgets.maxFileBytes = config.budgets.maxFileSize;
290
+ if (config.budgets?.maxTotalSize !== void 0)
291
+ budgets.maxTotalBytes = config.budgets.maxTotalSize;
292
+ return {
293
+ loaded,
294
+ project: {
295
+ ...project,
296
+ ...config.include ? { include: config.include } : {},
297
+ ...config.outDir ? { outDir: config.outDir } : {},
298
+ ...config.publicPath ? { publicPath: config.publicPath } : {}
299
+ },
300
+ provenance: {
301
+ framework: config.framework ? "config" : "detected",
302
+ include: config.include ? "config" : "detected",
303
+ outDir: config.outDir ? "config" : "detected",
304
+ publicPath: config.publicPath ? "config" : "detected"
305
+ },
306
+ exclude: config.exclude ?? [],
307
+ catalog,
308
+ policy: {
309
+ catalog,
310
+ assetFor: (source) => assets.find((rule) => matchesGlob(rule.pattern, source))
311
+ },
312
+ assets,
313
+ rules,
314
+ auditIgnore: config.audit?.ignore ?? [],
315
+ budgets,
316
+ placeholder: config.manifest?.placeholder,
317
+ concurrency: config.concurrency,
318
+ ffmpegPath: config.ffmpegPath,
319
+ ffprobePath: config.ffprobePath,
320
+ ssimulacra2Path: config.ssimulacra2Path
321
+ };
322
+ }
323
+ async function resolveConfig(projectRoot, loaded) {
324
+ const framework = loaded?.config.framework;
325
+ const project = await detectProject(projectRoot, { framework });
326
+ return resolveConfigData(loaded?.config ?? {}, project, loaded);
327
+ }
328
+
329
+ // src/core/ffmpeg/resolve.ts
330
+ import { delimiter, dirname, join } from "path";
331
+ var ENV_VAR = {
332
+ ffmpeg: "BYTECUT_FFMPEG",
333
+ ffprobe: "BYTECUT_FFPROBE",
334
+ ssimulacra2: "BYTECUT_SSIMULACRA2"
335
+ };
336
+ function executableNames(name, platform, env) {
337
+ if (platform !== "win32") return [name];
338
+ const exts = (env.PATHEXT ?? ".EXE;.CMD;.BAT").split(";").filter(Boolean);
339
+ return [name, ...exts.map((ext) => name + ext.toLowerCase())];
340
+ }
341
+ function resolveBinary(name, options, deps) {
342
+ const explicit = [
343
+ ["flag", options.flag],
344
+ ["env", options.env[ENV_VAR[name]]],
345
+ ["config", options.config]
346
+ ];
347
+ for (const [source, value] of explicit) {
348
+ if (value === void 0 || value === "") continue;
349
+ return deps.isFile(value) ? { found: true, path: value, source } : { found: false, source, attempted: value };
350
+ }
351
+ const names = executableNames(name, options.platform, options.env);
352
+ if (name === "ffprobe" && options.ffmpegDir) {
353
+ for (const candidate of names) {
354
+ const path = join(options.ffmpegDir, candidate);
355
+ if (deps.isFile(path)) return { found: true, path, source: "ffmpeg-dir" };
356
+ }
357
+ }
358
+ const dirs = (options.env.PATH ?? "").split(delimiter).filter(Boolean);
359
+ for (const dir of dirs) {
360
+ for (const candidate of names) {
361
+ const path = join(dir, candidate);
362
+ if (deps.isFile(path)) return { found: true, path, source: "path" };
363
+ }
364
+ }
365
+ return { found: false, source: null };
366
+ }
367
+ function ffmpegDirOf(resolved) {
368
+ return resolved.found ? dirname(resolved.path) : void 0;
369
+ }
370
+
371
+ // src/core/ffmpeg/capabilities.ts
372
+ function parseVersion(output) {
373
+ const match = /^\w+ version (\S+)/m.exec(output);
374
+ return match?.[1] ?? null;
375
+ }
376
+ function parseListing(output, kind) {
377
+ const entry = kind === "encoders" ? /^\s*[VAS][A-Z.]{5}\s+(\S+)/ : /^\s*[T.][S.][C.]\s+(\S+)/;
378
+ const names = /* @__PURE__ */ new Set();
379
+ for (const line of output.split(/\r?\n/)) {
380
+ const name = entry.exec(line)?.[1];
381
+ if (name && name !== "=") names.add(name);
382
+ }
383
+ return names;
384
+ }
385
+ function toCapabilities(encoders, filters) {
386
+ return {
387
+ libsvtav1: encoders.has("libsvtav1"),
388
+ libaomAv1: encoders.has("libaom-av1"),
389
+ libx264: encoders.has("libx264"),
390
+ libvpxVp9: encoders.has("libvpx-vp9"),
391
+ libvmaf: filters.has("libvmaf")
392
+ };
393
+ }
394
+ function pickAv1Encoder(caps) {
395
+ if (caps.libsvtav1) return "libsvtav1";
396
+ if (caps.libaomAv1) return "libaom-av1";
397
+ return null;
398
+ }
399
+ async function probeFfmpeg(path, run, cache = /* @__PURE__ */ new Map()) {
400
+ const { stdout } = await run(path, ["-hide_banner", "-version"]);
401
+ const version = parseVersion(stdout) ?? "unknown";
402
+ const key = `${path}\0${version}`;
403
+ const cached = cache.get(key);
404
+ if (cached) return cached;
405
+ const [encoders, filters] = await Promise.all([
406
+ run(path, ["-hide_banner", "-encoders"]),
407
+ run(path, ["-hide_banner", "-filters"])
408
+ ]);
409
+ const info = {
410
+ path,
411
+ version,
412
+ capabilities: toCapabilities(
413
+ parseListing(encoders.stdout, "encoders"),
414
+ parseListing(filters.stdout, "filters")
415
+ )
416
+ };
417
+ cache.set(key, info);
418
+ return info;
419
+ }
420
+ async function probeFfprobeVersion(path, run) {
421
+ const { stdout } = await run(path, ["-hide_banner", "-version"]);
422
+ return parseVersion(stdout) ?? "unknown";
423
+ }
424
+
425
+ // src/core/ffmpeg/install-hint.ts
426
+ function installHint(platform, osReleaseId) {
427
+ switch (platform) {
428
+ case "darwin":
429
+ return "brew install ffmpeg";
430
+ case "win32":
431
+ return "winget install ffmpeg";
432
+ default:
433
+ switch (osReleaseId) {
434
+ case "fedora":
435
+ case "rhel":
436
+ case "centos":
437
+ return "sudo dnf install ffmpeg";
438
+ case "arch":
439
+ case "manjaro":
440
+ return "sudo pacman -S ffmpeg";
441
+ case "alpine":
442
+ return "sudo apk add ffmpeg";
443
+ default:
444
+ return "sudo apt install ffmpeg";
445
+ }
446
+ }
447
+ }
448
+
449
+ export {
450
+ IMAGE_PRESETS,
451
+ getImagePreset,
452
+ getVideoPreset,
453
+ rootPreset,
454
+ BUILTIN_POLICY,
455
+ resolveBinary,
456
+ ffmpegDirOf,
457
+ pickAv1Encoder,
458
+ probeFfmpeg,
459
+ probeFfprobeVersion,
460
+ installHint,
461
+ resolveConfigData,
462
+ resolveConfig
463
+ };