@ttt-productions/media-processing-core 0.8.17 → 0.8.18
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/dist/audio/audio-processor.d.ts.map +1 -1
- package/dist/audio/audio-processor.js +1 -2
- package/dist/audio/audio-processor.js.map +1 -1
- package/dist/audio/probe.d.ts.map +1 -1
- package/dist/audio/probe.js +0 -2
- package/dist/audio/probe.js.map +1 -1
- package/dist/duration/media-duration.d.ts +0 -11
- package/dist/duration/media-duration.d.ts.map +1 -1
- package/dist/duration/media-duration.js +1 -28
- package/dist/duration/media-duration.js.map +1 -1
- package/dist/image/formats.d.ts +0 -11
- package/dist/image/formats.d.ts.map +1 -1
- package/dist/image/formats.js +0 -10
- package/dist/image/formats.js.map +1 -1
- package/dist/image/image-processor.d.ts.map +1 -1
- package/dist/image/image-processor.js +1 -20
- package/dist/image/image-processor.js.map +1 -1
- package/dist/image/resize.d.ts +0 -14
- package/dist/image/resize.d.ts.map +1 -1
- package/dist/image/resize.js +0 -15
- package/dist/image/resize.js.map +1 -1
- package/dist/inspection/inspect-media.d.ts +0 -7
- package/dist/inspection/inspect-media.d.ts.map +1 -1
- package/dist/inspection/inspect-media.js +4 -64
- package/dist/inspection/inspect-media.js.map +1 -1
- package/dist/io/fs.d.ts.map +1 -1
- package/dist/io/fs.js +0 -1
- package/dist/io/fs.js.map +1 -1
- package/dist/io/local-input.d.ts +0 -20
- package/dist/io/local-input.d.ts.map +1 -1
- package/dist/io/local-input.js +0 -20
- package/dist/io/local-input.js.map +1 -1
- package/dist/io/types.d.ts +0 -3
- package/dist/io/types.d.ts.map +1 -1
- package/dist/io/types.js.map +1 -1
- package/dist/moderation/merge.d.ts.map +1 -1
- package/dist/moderation/merge.js +0 -1
- package/dist/moderation/merge.js.map +1 -1
- package/dist/moderation/types.d.ts +0 -2
- package/dist/moderation/types.d.ts.map +1 -1
- package/dist/moderation/types.js.map +1 -1
- package/dist/run-pipeline.d.ts +0 -17
- package/dist/run-pipeline.d.ts.map +1 -1
- package/dist/run-pipeline.js +2 -27
- package/dist/run-pipeline.js.map +1 -1
- package/dist/server/download-token.d.ts +0 -6
- package/dist/server/download-token.d.ts.map +1 -1
- package/dist/server/download-token.js +0 -10
- package/dist/server/download-token.js.map +1 -1
- package/dist/server/firebase-media-io.d.ts +1 -11
- package/dist/server/firebase-media-io.d.ts.map +1 -1
- package/dist/server/firebase-media-io.js +0 -5
- package/dist/server/firebase-media-io.js.map +1 -1
- package/dist/server/gcs-errors.d.ts +0 -8
- package/dist/server/gcs-errors.d.ts.map +1 -1
- package/dist/server/gcs-errors.js +0 -8
- package/dist/server/gcs-errors.js.map +1 -1
- package/dist/server/pending-media.d.ts +1 -32
- package/dist/server/pending-media.d.ts.map +1 -1
- package/dist/server/pending-media.js +1 -24
- package/dist/server/pending-media.js.map +1 -1
- package/dist/server/r2-retry.d.ts +0 -48
- package/dist/server/r2-retry.d.ts.map +1 -1
- package/dist/server/r2-retry.js +4 -90
- package/dist/server/r2-retry.js.map +1 -1
- package/dist/server/staged-object.d.ts +0 -44
- package/dist/server/staged-object.d.ts.map +1 -1
- package/dist/server/staged-object.js +0 -44
- package/dist/server/staged-object.js.map +1 -1
- package/dist/server/storage-ops.d.ts +2 -62
- package/dist/server/storage-ops.d.ts.map +1 -1
- package/dist/server/storage-ops.js +9 -88
- package/dist/server/storage-ops.js.map +1 -1
- package/dist/types.d.ts +0 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/utils/safe-path.d.ts.map +1 -1
- package/dist/utils/safe-path.js +1 -2
- package/dist/utils/safe-path.js.map +1 -1
- package/dist/video/ffmpeg.d.ts +0 -22
- package/dist/video/ffmpeg.d.ts.map +1 -1
- package/dist/video/ffmpeg.js +6 -21
- package/dist/video/ffmpeg.js.map +1 -1
- package/dist/video/probe.d.ts.map +1 -1
- package/dist/video/probe.js +0 -1
- package/dist/video/probe.js.map +1 -1
- package/dist/video/video-processor.d.ts.map +1 -1
- package/dist/video/video-processor.js +2 -15
- package/dist/video/video-processor.js.map +1 -1
- package/package.json +2 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"audio-processor.d.ts","sourceRoot":"","sources":["../../src/audio/audio-processor.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAEV,qBAAqB,EACrB,mBAAmB,EACpB,MAAM,gCAAgC,CAAC;AAOxC,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AA4BvD,wBAAsB,YAAY,CAChC,IAAI,EAAE,mBAAmB,EACzB,GAAG,EAAE;IACH,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc,EAAE,MAAM,CAAC;IACvB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,EACD,IAAI,CAAC,EAAE,mBAAmB,GACzB,OAAO,CAAC,qBAAqB,CAAC,
|
|
1
|
+
{"version":3,"file":"audio-processor.d.ts","sourceRoot":"","sources":["../../src/audio/audio-processor.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAEV,qBAAqB,EACrB,mBAAmB,EACpB,MAAM,gCAAgC,CAAC;AAOxC,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AA4BvD,wBAAsB,YAAY,CAChC,IAAI,EAAE,mBAAmB,EACzB,GAAG,EAAE;IACH,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc,EAAE,MAAM,CAAC;IACvB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,EACD,IAAI,CAAC,EAAE,mBAAmB,GACzB,OAAO,CAAC,qBAAqB,CAAC,CAyJhC"}
|
|
@@ -23,7 +23,7 @@ function acceptAllowsAudio(spec) {
|
|
|
23
23
|
function acceptAllowsMime(spec, actualMime) {
|
|
24
24
|
const mimes = spec.accept?.mimes?.filter(Boolean) ?? [];
|
|
25
25
|
if (mimes.length === 0)
|
|
26
|
-
return true;
|
|
26
|
+
return true;
|
|
27
27
|
if (!actualMime)
|
|
28
28
|
return false;
|
|
29
29
|
return mimes.some((a) => matchMime(a, actualMime));
|
|
@@ -100,7 +100,6 @@ export async function processAudio(spec, ctx, opts) {
|
|
|
100
100
|
},
|
|
101
101
|
};
|
|
102
102
|
}
|
|
103
|
-
// Normalize to AAC in m4a (small + widely supported)
|
|
104
103
|
const outPath = outputPathFor(ctx.outputBasePath, TIMED_MEDIA_MAIN_OUTPUT_KEY, 'm4a');
|
|
105
104
|
tempFiles.push(outPath);
|
|
106
105
|
const args = ['-y', '-i', ctx.inputPath, '-vn', '-c:a', 'aac', '-b:a', '128k', outPath];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"audio-processor.js","sourceRoot":"","sources":["../../src/audio/audio-processor.ts"],"names":[],"mappings":"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","sourcesContent":["import type {\n MediaOutput,\n MediaProcessingResult,\n MediaProcessingSpec,\n} from '@ttt-productions/media-schemas';\nimport { mkdir, stat } from 'node:fs/promises';\nimport path from 'node:path';\nimport { safeOutputPathFor } from '../utils/safe-path.js';\nimport { ensureFfmpegAvailable, runFfmpeg } from '../video/ffmpeg.js';\nimport { probeAudio } from './probe.js';\nimport { unreadableLengthError } from '../duration/media-duration.js';\nimport type { ProcessMediaOptions } from '../types.js';\nimport { TIMED_MEDIA_MAIN_OUTPUT_KEY } from '@ttt-productions/media-schemas';\n\nfunction matchMime(accepted: string, actual: string): boolean {\n const a = accepted.trim().toLowerCase();\n const m = actual.trim().toLowerCase();\n if (!a) return true;\n if (a === '*/*') return true;\n if (a.endsWith('/*')) return m.startsWith(a.slice(0, -1));\n return a === m;\n}\n\nfunction acceptAllowsAudio(spec: MediaProcessingSpec): boolean {\n const kinds = spec.accept?.kinds?.filter(Boolean) ?? [];\n return kinds.length === 0 ? true : kinds.includes('audio');\n}\n\nfunction acceptAllowsMime(spec: MediaProcessingSpec, actualMime?: string): boolean {\n const mimes = spec.accept?.mimes?.filter(Boolean) ?? [];\n if (mimes.length === 0) return true; // empty => accept anything\n if (!actualMime) return false;\n return mimes.some((a) => matchMime(a, actualMime));\n}\n\nfunction outputPathFor(base: string, key: string, ext: string): string {\n return safeOutputPathFor(base, key, ext);\n}\n\nexport async function processAudio(\n spec: MediaProcessingSpec,\n ctx: {\n inputPath: string;\n outputBasePath: string;\n inputMime?: string;\n },\n opts?: ProcessMediaOptions,\n): Promise<MediaProcessingResult> {\n const tempFiles: string[] = [];\n\n try {\n if (opts?.signal?.aborted) {\n return {\n ok: false,\n mediaType: 'audio',\n error: { code: 'processing_canceled', message: 'Processing canceled.' },\n };\n }\n const ff = await ensureFfmpegAvailable();\n if (!ff.ok) {\n return {\n ok: false,\n mediaType: 'audio',\n error: {\n code: 'processing_failed',\n message: 'ffmpeg is not available.',\n details: { error: String((ff as any).error ?? 'unknown') },\n },\n };\n }\n if (!acceptAllowsAudio(spec)) {\n return {\n ok: false,\n mediaType: 'audio',\n error: {\n code: 'invalid_mime',\n message: 'Spec does not allow audio uploads.',\n details: { accept: spec.accept },\n },\n };\n }\n\n if (!acceptAllowsMime(spec, ctx.inputMime)) {\n return {\n ok: false,\n mediaType: 'audio',\n error: {\n code: 'invalid_mime',\n message: 'Audio mime type is not allowed.',\n details: { inputMime: ctx.inputMime, accept: spec.accept },\n },\n };\n }\n\n const outDir = path.dirname(ctx.outputBasePath);\n await mkdir(outDir, { recursive: true });\n\n const probe = await probeAudio(ctx.inputPath, { signal: opts?.signal });\n\n if (opts?.signal?.aborted) {\n return {\n ok: false,\n mediaType: 'audio',\n error: { code: 'processing_canceled', message: 'Processing canceled.' },\n };\n }\n\n const maxDur = spec.maxDurationSec ?? spec.audio?.maxDurationSec;\n if (maxDur && probe.durationSec === undefined) {\n return { ok: false, mediaType: 'audio', error: unreadableLengthError('audio', maxDur) };\n }\n if (maxDur && probe.durationSec !== undefined && probe.durationSec > maxDur) {\n return {\n ok: false,\n mediaType: 'audio',\n error: {\n code: 'too_long',\n message: 'Audio is too long.',\n details: { durationSec: probe.durationSec, maxDurationSec: maxDur },\n },\n };\n }\n\n // Normalize to AAC in m4a (small + widely supported)\n const outPath = outputPathFor(ctx.outputBasePath, TIMED_MEDIA_MAIN_OUTPUT_KEY, 'm4a');\n\n tempFiles.push(outPath);\n\n const args = ['-y', '-i', ctx.inputPath, '-vn', '-c:a', 'aac', '-b:a', '128k', outPath];\n\n const r = await runFfmpeg(args, {\n signal: opts?.signal,\n durationMs: probe.durationSec ? probe.durationSec * 1000 : undefined,\n onProgress: (p) => {\n opts?.onProgress?.({\n phase: 'process',\n percent: p.percent,\n detail: { step: 'audio_transcode', outTimeMs: p.outTimeMs },\n });\n },\n phase: 'transcode',\n });\n if (r.code !== 0) {\n return {\n ok: false,\n mediaType: 'audio',\n error: {\n code: 'processing_failed',\n message: 'ffmpeg audio transcode failed.',\n details: { stderr: r.stderr, stdout: r.stdout },\n },\n };\n }\n\n const s = await stat(outPath);\n\n const outputs: MediaOutput[] = [\n {\n key: TIMED_MEDIA_MAIN_OUTPUT_KEY,\n url: `file://${outPath}`,\n path: outPath,\n mime: 'audio/mp4',\n sizeBytes: s.size,\n durationSec: probe.durationSec,\n },\n ];\n\n tempFiles.length = 0;\n\n return {\n ok: true,\n mediaType: 'audio',\n outputs,\n meta: {\n mime: ctx.inputMime,\n durationSec: probe.durationSec,\n },\n };\n } catch (e: any) {\n if (opts?.signal?.aborted) {\n return {\n ok: false,\n mediaType: 'audio',\n error: { code: 'processing_canceled', message: 'Processing canceled.' },\n };\n }\n return {\n ok: false,\n mediaType: 'audio',\n error: {\n code: 'processing_failed',\n message: e?.message ? String(e.message) : 'Audio processing failed',\n details: { name: e?.name },\n },\n };\n } finally {\n if (tempFiles.length) {\n const { rm } = await import('node:fs/promises');\n await Promise.all(tempFiles.map((f) => rm(f, { force: true }).catch(() => {})));\n }\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"audio-processor.js","sourceRoot":"","sources":["../../src/audio/audio-processor.ts"],"names":[],"mappings":"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","sourcesContent":["import type {\n MediaOutput,\n MediaProcessingResult,\n MediaProcessingSpec,\n} from '@ttt-productions/media-schemas';\nimport { mkdir, stat } from 'node:fs/promises';\nimport path from 'node:path';\nimport { safeOutputPathFor } from '../utils/safe-path.js';\nimport { ensureFfmpegAvailable, runFfmpeg } from '../video/ffmpeg.js';\nimport { probeAudio } from './probe.js';\nimport { unreadableLengthError } from '../duration/media-duration.js';\nimport type { ProcessMediaOptions } from '../types.js';\nimport { TIMED_MEDIA_MAIN_OUTPUT_KEY } from '@ttt-productions/media-schemas';\n\nfunction matchMime(accepted: string, actual: string): boolean {\n const a = accepted.trim().toLowerCase();\n const m = actual.trim().toLowerCase();\n if (!a) return true;\n if (a === '*/*') return true;\n if (a.endsWith('/*')) return m.startsWith(a.slice(0, -1));\n return a === m;\n}\n\nfunction acceptAllowsAudio(spec: MediaProcessingSpec): boolean {\n const kinds = spec.accept?.kinds?.filter(Boolean) ?? [];\n return kinds.length === 0 ? true : kinds.includes('audio');\n}\n\nfunction acceptAllowsMime(spec: MediaProcessingSpec, actualMime?: string): boolean {\n const mimes = spec.accept?.mimes?.filter(Boolean) ?? [];\n if (mimes.length === 0) return true;\n if (!actualMime) return false;\n return mimes.some((a) => matchMime(a, actualMime));\n}\n\nfunction outputPathFor(base: string, key: string, ext: string): string {\n return safeOutputPathFor(base, key, ext);\n}\n\nexport async function processAudio(\n spec: MediaProcessingSpec,\n ctx: {\n inputPath: string;\n outputBasePath: string;\n inputMime?: string;\n },\n opts?: ProcessMediaOptions,\n): Promise<MediaProcessingResult> {\n const tempFiles: string[] = [];\n\n try {\n if (opts?.signal?.aborted) {\n return {\n ok: false,\n mediaType: 'audio',\n error: { code: 'processing_canceled', message: 'Processing canceled.' },\n };\n }\n const ff = await ensureFfmpegAvailable();\n if (!ff.ok) {\n return {\n ok: false,\n mediaType: 'audio',\n error: {\n code: 'processing_failed',\n message: 'ffmpeg is not available.',\n details: { error: String((ff as any).error ?? 'unknown') },\n },\n };\n }\n if (!acceptAllowsAudio(spec)) {\n return {\n ok: false,\n mediaType: 'audio',\n error: {\n code: 'invalid_mime',\n message: 'Spec does not allow audio uploads.',\n details: { accept: spec.accept },\n },\n };\n }\n\n if (!acceptAllowsMime(spec, ctx.inputMime)) {\n return {\n ok: false,\n mediaType: 'audio',\n error: {\n code: 'invalid_mime',\n message: 'Audio mime type is not allowed.',\n details: { inputMime: ctx.inputMime, accept: spec.accept },\n },\n };\n }\n\n const outDir = path.dirname(ctx.outputBasePath);\n await mkdir(outDir, { recursive: true });\n\n const probe = await probeAudio(ctx.inputPath, { signal: opts?.signal });\n\n if (opts?.signal?.aborted) {\n return {\n ok: false,\n mediaType: 'audio',\n error: { code: 'processing_canceled', message: 'Processing canceled.' },\n };\n }\n\n const maxDur = spec.maxDurationSec ?? spec.audio?.maxDurationSec;\n if (maxDur && probe.durationSec === undefined) {\n return { ok: false, mediaType: 'audio', error: unreadableLengthError('audio', maxDur) };\n }\n if (maxDur && probe.durationSec !== undefined && probe.durationSec > maxDur) {\n return {\n ok: false,\n mediaType: 'audio',\n error: {\n code: 'too_long',\n message: 'Audio is too long.',\n details: { durationSec: probe.durationSec, maxDurationSec: maxDur },\n },\n };\n }\n\n const outPath = outputPathFor(ctx.outputBasePath, TIMED_MEDIA_MAIN_OUTPUT_KEY, 'm4a');\n\n tempFiles.push(outPath);\n\n const args = ['-y', '-i', ctx.inputPath, '-vn', '-c:a', 'aac', '-b:a', '128k', outPath];\n\n const r = await runFfmpeg(args, {\n signal: opts?.signal,\n durationMs: probe.durationSec ? probe.durationSec * 1000 : undefined,\n onProgress: (p) => {\n opts?.onProgress?.({\n phase: 'process',\n percent: p.percent,\n detail: { step: 'audio_transcode', outTimeMs: p.outTimeMs },\n });\n },\n phase: 'transcode',\n });\n if (r.code !== 0) {\n return {\n ok: false,\n mediaType: 'audio',\n error: {\n code: 'processing_failed',\n message: 'ffmpeg audio transcode failed.',\n details: { stderr: r.stderr, stdout: r.stdout },\n },\n };\n }\n\n const s = await stat(outPath);\n\n const outputs: MediaOutput[] = [\n {\n key: TIMED_MEDIA_MAIN_OUTPUT_KEY,\n url: `file://${outPath}`,\n path: outPath,\n mime: 'audio/mp4',\n sizeBytes: s.size,\n durationSec: probe.durationSec,\n },\n ];\n\n tempFiles.length = 0;\n\n return {\n ok: true,\n mediaType: 'audio',\n outputs,\n meta: {\n mime: ctx.inputMime,\n durationSec: probe.durationSec,\n },\n };\n } catch (e: any) {\n if (opts?.signal?.aborted) {\n return {\n ok: false,\n mediaType: 'audio',\n error: { code: 'processing_canceled', message: 'Processing canceled.' },\n };\n }\n return {\n ok: false,\n mediaType: 'audio',\n error: {\n code: 'processing_failed',\n message: e?.message ? String(e.message) : 'Audio processing failed',\n details: { name: e?.name },\n },\n };\n } finally {\n if (tempFiles.length) {\n const { rm } = await import('node:fs/promises');\n await Promise.all(tempFiles.map((f) => rm(f, { force: true }).catch(() => {})));\n }\n }\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"probe.d.ts","sourceRoot":"","sources":["../../src/audio/probe.ts"],"names":[],"mappings":"AACA,OAAO,EAAsB,KAAK,sBAAsB,EAAE,MAAM,+BAA+B,CAAC;AAEhG,MAAM,WAAW,UAAU;IACzB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,wBAAsB,UAAU,CAC9B,SAAS,EAAE,MAAM,EACjB,IAAI,CAAC,EAAE,sBAAsB,GAC5B,OAAO,CAAC,UAAU,CAAC,
|
|
1
|
+
{"version":3,"file":"probe.d.ts","sourceRoot":"","sources":["../../src/audio/probe.ts"],"names":[],"mappings":"AACA,OAAO,EAAsB,KAAK,sBAAsB,EAAE,MAAM,+BAA+B,CAAC;AAEhG,MAAM,WAAW,UAAU;IACzB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,wBAAsB,UAAU,CAC9B,SAAS,EAAE,MAAM,EACjB,IAAI,CAAC,EAAE,sBAAsB,GAC5B,OAAO,CAAC,UAAU,CAAC,CAWrB"}
|
package/dist/audio/probe.js
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import { DEFAULT_PROBE_TIMEOUT_MS, runCmd } from '../video/ffmpeg.js';
|
|
2
2
|
import { measureDurationSec } from '../duration/media-duration.js';
|
|
3
3
|
export async function probeAudio(inputPath, opts) {
|
|
4
|
-
// ffprobe opening the file is the readability check: a file it cannot open
|
|
5
|
-
// throws here rather than reaching the length rule as merely unmeasurable.
|
|
6
4
|
const r = await runCmd('ffprobe', ['-v', 'error', inputPath], {
|
|
7
5
|
timeoutMs: DEFAULT_PROBE_TIMEOUT_MS,
|
|
8
6
|
signal: opts?.signal,
|
package/dist/audio/probe.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"probe.js","sourceRoot":"","sources":["../../src/audio/probe.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,wBAAwB,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AACtE,OAAO,EAAE,kBAAkB,EAA+B,MAAM,+BAA+B,CAAC;AAMhG,MAAM,CAAC,KAAK,UAAU,UAAU,CAC9B,SAAiB,EACjB,IAA6B;IAE7B,
|
|
1
|
+
{"version":3,"file":"probe.js","sourceRoot":"","sources":["../../src/audio/probe.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,wBAAwB,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AACtE,OAAO,EAAE,kBAAkB,EAA+B,MAAM,+BAA+B,CAAC;AAMhG,MAAM,CAAC,KAAK,UAAU,UAAU,CAC9B,SAAiB,EACjB,IAA6B;IAE7B,MAAM,CAAC,GAAG,MAAM,MAAM,CAAC,SAAS,EAAE,CAAC,IAAI,EAAE,OAAO,EAAE,SAAS,CAAC,EAAE;QAC5D,SAAS,EAAE,wBAAwB;QACnC,MAAM,EAAE,IAAI,EAAE,MAAM;KACrB,CAAC,CAAC;IAEH,IAAI,CAAC,CAAC,QAAQ,IAAI,CAAC,CAAC,IAAI,KAAK,CAAC,EAAE,CAAC;QAC/B,MAAM,IAAI,KAAK,CAAC,mBAAmB,CAAC,CAAC,IAAI,MAAM,CAAC,CAAC,MAAM,IAAI,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC;IACzE,CAAC;IAED,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC,SAAS,EAAE,IAAI,CAAC,EAAE,CAAC;AACpE,CAAC","sourcesContent":["import { DEFAULT_PROBE_TIMEOUT_MS, runCmd } from '../video/ffmpeg.js';\nimport { measureDurationSec, type MeasureDurationOptions } from '../duration/media-duration.js';\n\nexport interface AudioProbe {\n durationSec?: number;\n}\n\nexport async function probeAudio(\n inputPath: string,\n opts?: MeasureDurationOptions,\n): Promise<AudioProbe> {\n const r = await runCmd('ffprobe', ['-v', 'error', inputPath], {\n timeoutMs: DEFAULT_PROBE_TIMEOUT_MS,\n signal: opts?.signal,\n });\n\n if (r.timedOut || r.code !== 0) {\n throw new Error(`ffprobe failed (${r.code}): ${r.stderr || r.stdout}`);\n }\n\n return { durationSec: await measureDurationSec(inputPath, opts) };\n}\n"]}
|
|
@@ -1,13 +1,7 @@
|
|
|
1
1
|
import type { MediaProcessingError } from '@ttt-productions/media-schemas';
|
|
2
|
-
/** Wall-clock ceiling for the measuring pass; it bounds an adversarial input,
|
|
3
|
-
* not a real upload (a 920 MB, 30-minute WebM measures in about a second). */
|
|
4
2
|
export declare const DEFAULT_DURATION_MEASURE_TIMEOUT_MS = 120000;
|
|
5
3
|
export interface PacketLengthFold {
|
|
6
|
-
/** One line of ffprobe's `-of compact` packet and stream listing. */
|
|
7
4
|
add(line: string): void;
|
|
8
|
-
/** Seconds from the earliest packet start (never before zero) to the latest
|
|
9
|
-
* packet end across the timed (audio, and non-cover-art video) streams, or
|
|
10
|
-
* `undefined`. */
|
|
11
5
|
lengthSec(): number | undefined;
|
|
12
6
|
}
|
|
13
7
|
export declare function createPacketLengthFold(): PacketLengthFold;
|
|
@@ -15,12 +9,7 @@ export interface MeasureDurationOptions {
|
|
|
15
9
|
timeoutMs?: number;
|
|
16
10
|
signal?: AbortSignal;
|
|
17
11
|
}
|
|
18
|
-
/** The file's length in seconds, or `undefined` when it cannot be measured. */
|
|
19
12
|
export declare function measureDurationSec(localPath: string, opts?: MeasureDurationOptions): Promise<number | undefined>;
|
|
20
13
|
export type MeasureDurationFn = (localPath: string, opts?: MeasureDurationOptions) => Promise<number | undefined>;
|
|
21
|
-
/** The refusal for a file with no length under a spec that caps length: a
|
|
22
|
-
* length that cannot be measured cannot be shown to be within the cap. The
|
|
23
|
-
* code is `unsupported_format`, never `too_long` — the file is not known to
|
|
24
|
-
* be too long, and its copy must not say so. */
|
|
25
14
|
export declare function unreadableLengthError(kind: 'video' | 'audio', maxDurationSec: number): MediaProcessingError;
|
|
26
15
|
//# sourceMappingURL=media-duration.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"media-duration.d.ts","sourceRoot":"","sources":["../../src/duration/media-duration.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"media-duration.d.ts","sourceRoot":"","sources":["../../src/duration/media-duration.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,gCAAgC,CAAC;AAG3E,eAAO,MAAM,mCAAmC,SAAU,CAAC;AAuB3D,MAAM,WAAW,gBAAgB;IAC/B,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,SAAS,IAAI,MAAM,GAAG,SAAS,CAAC;CACjC;AAED,wBAAgB,sBAAsB,IAAI,gBAAgB,CA+CzD;AAED,MAAM,WAAW,sBAAsB;IACrC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAED,wBAAsB,kBAAkB,CACtC,SAAS,EAAE,MAAM,EACjB,IAAI,CAAC,EAAE,sBAAsB,GAC5B,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CA0B7B;AAED,MAAM,MAAM,iBAAiB,GAAG,CAC9B,SAAS,EAAE,MAAM,EACjB,IAAI,CAAC,EAAE,sBAAsB,KAC1B,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC;AAEjC,wBAAgB,qBAAqB,CACnC,IAAI,EAAE,OAAO,GAAG,OAAO,EACvB,cAAc,EAAE,MAAM,GACrB,oBAAoB,CAStB"}
|
|
@@ -1,26 +1,5 @@
|
|
|
1
|
-
// The ONE rule for a timed media file's length (ENG-002). The inspection's
|
|
2
|
-
// `durationSec` and both processors' too-long checks read it here, so they can
|
|
3
|
-
// never disagree about the same file.
|
|
4
|
-
//
|
|
5
|
-
// 1. The length is always MEASURED from the packets themselves: ffprobe
|
|
6
|
-
// lists every packet's timestamp and duration (demux only, no decode),
|
|
7
|
-
// and the length is the span from the earliest packet start (never before
|
|
8
|
-
// zero) to the latest packet end across the audio and video streams. A
|
|
9
|
-
// container header's declared length (`format.duration`) is never used:
|
|
10
|
-
// a header can claim a short length for a long file, and Chromium's
|
|
11
|
-
// MediaRecorder writes WebM with none. ffmpeg's `-progress` output time
|
|
12
|
-
// is not used either: it trails the true end by the B-frame reorder
|
|
13
|
-
// delay. The listing is read one line at a time and folded into a few
|
|
14
|
-
// numbers per stream, so a file of any packet count is measured in
|
|
15
|
-
// bounded memory.
|
|
16
|
-
// 2. A measure that fails, times out, is aborted, is cut short, or finds no
|
|
17
|
-
// timed packet is NO length. Every caller treats that as unknown and
|
|
18
|
-
// fails closed.
|
|
19
1
|
import { runCmd } from '../video/ffmpeg.js';
|
|
20
|
-
/** Wall-clock ceiling for the measuring pass; it bounds an adversarial input,
|
|
21
|
-
* not a real upload (a 920 MB, 30-minute WebM measures in about a second). */
|
|
22
2
|
export const DEFAULT_DURATION_MEASURE_TIMEOUT_MS = 120_000;
|
|
23
|
-
/** More streams than this is an adversarial file, and is no length. */
|
|
24
3
|
const MAX_TRACKED_STREAMS = 256;
|
|
25
4
|
const SECONDS = /^-?\d+(\.\d+)?$/;
|
|
26
5
|
const STREAM_INDEX = /^\d+$/;
|
|
@@ -94,14 +73,12 @@ export function createPacketLengthFold() {
|
|
|
94
73
|
if (span.end > end)
|
|
95
74
|
end = span.end;
|
|
96
75
|
}
|
|
97
|
-
// Packets before zero are encoder pre-roll (AAC priming, Opus pre-skip)
|
|
98
|
-
// that a player discards, so they add no playable length.
|
|
76
|
+
// Packets before zero are encoder pre-roll (AAC priming, Opus pre-skip) a player discards.
|
|
99
77
|
const sec = Math.round((end - Math.max(0, start)) * 1_000_000) / 1_000_000;
|
|
100
78
|
return Number.isFinite(sec) && sec > 0 ? sec : undefined;
|
|
101
79
|
},
|
|
102
80
|
};
|
|
103
81
|
}
|
|
104
|
-
/** The file's length in seconds, or `undefined` when it cannot be measured. */
|
|
105
82
|
export async function measureDurationSec(localPath, opts) {
|
|
106
83
|
const fold = createPacketLengthFold();
|
|
107
84
|
let r;
|
|
@@ -127,10 +104,6 @@ export async function measureDurationSec(localPath, opts) {
|
|
|
127
104
|
return undefined;
|
|
128
105
|
return fold.lengthSec();
|
|
129
106
|
}
|
|
130
|
-
/** The refusal for a file with no length under a spec that caps length: a
|
|
131
|
-
* length that cannot be measured cannot be shown to be within the cap. The
|
|
132
|
-
* code is `unsupported_format`, never `too_long` — the file is not known to
|
|
133
|
-
* be too long, and its copy must not say so. */
|
|
134
107
|
export function unreadableLengthError(kind, maxDurationSec) {
|
|
135
108
|
return {
|
|
136
109
|
code: 'unsupported_format',
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"media-duration.js","sourceRoot":"","sources":["../../src/duration/media-duration.ts"],"names":[],"mappings":"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","sourcesContent":["// The ONE rule for a timed media file's length (ENG-002). The inspection's\n// `durationSec` and both processors' too-long checks read it here, so they can\n// never disagree about the same file.\n//\n// 1. The length is always MEASURED from the packets themselves: ffprobe\n// lists every packet's timestamp and duration (demux only, no decode),\n// and the length is the span from the earliest packet start (never before\n// zero) to the latest packet end across the audio and video streams. A\n// container header's declared length (`format.duration`) is never used:\n// a header can claim a short length for a long file, and Chromium's\n// MediaRecorder writes WebM with none. ffmpeg's `-progress` output time\n// is not used either: it trails the true end by the B-frame reorder\n// delay. The listing is read one line at a time and folded into a few\n// numbers per stream, so a file of any packet count is measured in\n// bounded memory.\n// 2. A measure that fails, times out, is aborted, is cut short, or finds no\n// timed packet is NO length. Every caller treats that as unknown and\n// fails closed.\n\nimport type { MediaProcessingError } from '@ttt-productions/media-schemas';\nimport { runCmd } from '../video/ffmpeg.js';\n\n/** Wall-clock ceiling for the measuring pass; it bounds an adversarial input,\n * not a real upload (a 920 MB, 30-minute WebM measures in about a second). */\nexport const DEFAULT_DURATION_MEASURE_TIMEOUT_MS = 120_000;\n\n/** More streams than this is an adversarial file, and is no length. */\nconst MAX_TRACKED_STREAMS = 256;\n\nconst SECONDS = /^-?\\d+(\\.\\d+)?$/;\nconst STREAM_INDEX = /^\\d+$/;\n\nfunction seconds(raw: string | undefined): number | undefined {\n if (raw === undefined || !SECONDS.test(raw)) return undefined;\n const n = Number(raw);\n return Number.isFinite(n) ? n : undefined;\n}\n\nfunction fields(line: string): { section: string; values: Map<string, string> } {\n const [section = '', ...pairs] = line.split('|');\n const values = new Map<string, string>();\n for (const pair of pairs) {\n const eq = pair.indexOf('=');\n if (eq > 0) values.set(pair.slice(0, eq), pair.slice(eq + 1).trim());\n }\n return { section, values };\n}\n\nexport interface PacketLengthFold {\n /** One line of ffprobe's `-of compact` packet and stream listing. */\n add(line: string): void;\n /** Seconds from the earliest packet start (never before zero) to the latest\n * packet end across the timed (audio, and non-cover-art video) streams, or\n * `undefined`. */\n lengthSec(): number | undefined;\n}\n\nexport function createPacketLengthFold(): PacketLengthFold {\n const spans = new Map<number, { start: number; end: number }>();\n const timed = new Set<number>();\n let overflow = false;\n\n return {\n add(line) {\n const { section, values } = fields(line);\n if (section === 'packet') {\n const rawIndex = values.get('stream_index');\n if (rawIndex === undefined || !STREAM_INDEX.test(rawIndex)) return;\n const index = Number(rawIndex);\n const start = seconds(values.get('pts_time')) ?? seconds(values.get('dts_time'));\n if (start === undefined) return;\n const end = start + Math.max(0, seconds(values.get('duration_time')) ?? 0);\n const span = spans.get(index);\n if (span) {\n if (start < span.start) span.start = start;\n if (end > span.end) span.end = end;\n } else if (spans.size >= MAX_TRACKED_STREAMS) {\n overflow = true;\n } else {\n spans.set(index, { start, end });\n }\n } else if (section === 'stream') {\n const rawIndex = values.get('index');\n if (rawIndex === undefined || !STREAM_INDEX.test(rawIndex)) return;\n const type = values.get('codec_type');\n const coverArt = values.get('disposition:attached_pic') === '1';\n if (type === 'audio' || (type === 'video' && !coverArt)) timed.add(Number(rawIndex));\n }\n },\n lengthSec() {\n if (overflow) return undefined;\n let start = Infinity;\n let end = -Infinity;\n for (const index of timed) {\n const span = spans.get(index);\n if (!span) continue;\n if (span.start < start) start = span.start;\n if (span.end > end) end = span.end;\n }\n // Packets before zero are encoder pre-roll (AAC priming, Opus pre-skip)\n // that a player discards, so they add no playable length.\n const sec = Math.round((end - Math.max(0, start)) * 1_000_000) / 1_000_000;\n return Number.isFinite(sec) && sec > 0 ? sec : undefined;\n },\n };\n}\n\nexport interface MeasureDurationOptions {\n timeoutMs?: number;\n signal?: AbortSignal;\n}\n\n/** The file's length in seconds, or `undefined` when it cannot be measured. */\nexport async function measureDurationSec(\n localPath: string,\n opts?: MeasureDurationOptions,\n): Promise<number | undefined> {\n const fold = createPacketLengthFold();\n let r;\n try {\n r = await runCmd(\n 'ffprobe',\n [\n '-v',\n 'error',\n '-show_entries',\n 'stream=index,codec_type:stream_disposition=attached_pic:packet=stream_index,pts_time,dts_time,duration_time',\n '-of',\n 'compact',\n localPath,\n ],\n {\n timeoutMs: opts?.timeoutMs ?? DEFAULT_DURATION_MEASURE_TIMEOUT_MS,\n signal: opts?.signal,\n onStdoutLine: (line) => fold.add(line),\n },\n );\n } catch {\n return undefined;\n }\n if (r.timedOut || r.truncated || r.code !== 0) return undefined;\n return fold.lengthSec();\n}\n\nexport type MeasureDurationFn = (\n localPath: string,\n opts?: MeasureDurationOptions,\n) => Promise<number | undefined>;\n\n/** The refusal for a file with no length under a spec that caps length: a\n * length that cannot be measured cannot be shown to be within the cap. The\n * code is `unsupported_format`, never `too_long` — the file is not known to\n * be too long, and its copy must not say so. */\nexport function unreadableLengthError(\n kind: 'video' | 'audio',\n maxDurationSec: number,\n): MediaProcessingError {\n return {\n code: 'unsupported_format',\n message:\n kind === 'video'\n ? \"The video's length could not be read.\"\n : \"The audio's length could not be read.\",\n details: { maxDurationSec },\n };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"media-duration.js","sourceRoot":"","sources":["../../src/duration/media-duration.ts"],"names":[],"mappings":"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","sourcesContent":["import type { MediaProcessingError } from '@ttt-productions/media-schemas';\nimport { runCmd } from '../video/ffmpeg.js';\n\nexport const DEFAULT_DURATION_MEASURE_TIMEOUT_MS = 120_000;\n\nconst MAX_TRACKED_STREAMS = 256;\n\nconst SECONDS = /^-?\\d+(\\.\\d+)?$/;\nconst STREAM_INDEX = /^\\d+$/;\n\nfunction seconds(raw: string | undefined): number | undefined {\n if (raw === undefined || !SECONDS.test(raw)) return undefined;\n const n = Number(raw);\n return Number.isFinite(n) ? n : undefined;\n}\n\nfunction fields(line: string): { section: string; values: Map<string, string> } {\n const [section = '', ...pairs] = line.split('|');\n const values = new Map<string, string>();\n for (const pair of pairs) {\n const eq = pair.indexOf('=');\n if (eq > 0) values.set(pair.slice(0, eq), pair.slice(eq + 1).trim());\n }\n return { section, values };\n}\n\nexport interface PacketLengthFold {\n add(line: string): void;\n lengthSec(): number | undefined;\n}\n\nexport function createPacketLengthFold(): PacketLengthFold {\n const spans = new Map<number, { start: number; end: number }>();\n const timed = new Set<number>();\n let overflow = false;\n\n return {\n add(line) {\n const { section, values } = fields(line);\n if (section === 'packet') {\n const rawIndex = values.get('stream_index');\n if (rawIndex === undefined || !STREAM_INDEX.test(rawIndex)) return;\n const index = Number(rawIndex);\n const start = seconds(values.get('pts_time')) ?? seconds(values.get('dts_time'));\n if (start === undefined) return;\n const end = start + Math.max(0, seconds(values.get('duration_time')) ?? 0);\n const span = spans.get(index);\n if (span) {\n if (start < span.start) span.start = start;\n if (end > span.end) span.end = end;\n } else if (spans.size >= MAX_TRACKED_STREAMS) {\n overflow = true;\n } else {\n spans.set(index, { start, end });\n }\n } else if (section === 'stream') {\n const rawIndex = values.get('index');\n if (rawIndex === undefined || !STREAM_INDEX.test(rawIndex)) return;\n const type = values.get('codec_type');\n const coverArt = values.get('disposition:attached_pic') === '1';\n if (type === 'audio' || (type === 'video' && !coverArt)) timed.add(Number(rawIndex));\n }\n },\n lengthSec() {\n if (overflow) return undefined;\n let start = Infinity;\n let end = -Infinity;\n for (const index of timed) {\n const span = spans.get(index);\n if (!span) continue;\n if (span.start < start) start = span.start;\n if (span.end > end) end = span.end;\n }\n // Packets before zero are encoder pre-roll (AAC priming, Opus pre-skip) a player discards.\n const sec = Math.round((end - Math.max(0, start)) * 1_000_000) / 1_000_000;\n return Number.isFinite(sec) && sec > 0 ? sec : undefined;\n },\n };\n}\n\nexport interface MeasureDurationOptions {\n timeoutMs?: number;\n signal?: AbortSignal;\n}\n\nexport async function measureDurationSec(\n localPath: string,\n opts?: MeasureDurationOptions,\n): Promise<number | undefined> {\n const fold = createPacketLengthFold();\n let r;\n try {\n r = await runCmd(\n 'ffprobe',\n [\n '-v',\n 'error',\n '-show_entries',\n 'stream=index,codec_type:stream_disposition=attached_pic:packet=stream_index,pts_time,dts_time,duration_time',\n '-of',\n 'compact',\n localPath,\n ],\n {\n timeoutMs: opts?.timeoutMs ?? DEFAULT_DURATION_MEASURE_TIMEOUT_MS,\n signal: opts?.signal,\n onStdoutLine: (line) => fold.add(line),\n },\n );\n } catch {\n return undefined;\n }\n if (r.timedOut || r.truncated || r.code !== 0) return undefined;\n return fold.lengthSec();\n}\n\nexport type MeasureDurationFn = (\n localPath: string,\n opts?: MeasureDurationOptions,\n) => Promise<number | undefined>;\n\nexport function unreadableLengthError(\n kind: 'video' | 'audio',\n maxDurationSec: number,\n): MediaProcessingError {\n return {\n code: 'unsupported_format',\n message:\n kind === 'video'\n ? \"The video's length could not be read.\"\n : \"The audio's length could not be read.\",\n details: { maxDurationSec },\n };\n}\n"]}
|
package/dist/image/formats.d.ts
CHANGED
|
@@ -1,20 +1,9 @@
|
|
|
1
1
|
import type { Sharp } from 'sharp';
|
|
2
|
-
/** Output image formats the processor can emit. */
|
|
3
2
|
export type ImageOutputFormat = 'jpeg' | 'png' | 'webp' | 'avif';
|
|
4
|
-
/**
|
|
5
|
-
* Map a requested format string (case-insensitive) to its sharp format id and
|
|
6
|
-
* file extension. Unknown or missing formats fall back to jpeg.
|
|
7
|
-
*/
|
|
8
3
|
export declare function pickFormat(fmt?: string): {
|
|
9
4
|
ext: string;
|
|
10
5
|
format: ImageOutputFormat;
|
|
11
6
|
};
|
|
12
|
-
/** Map an image format string to its MIME type. */
|
|
13
7
|
export declare function mimeForImageFormat(fmt?: string): string | undefined;
|
|
14
|
-
/**
|
|
15
|
-
* Apply the chosen output format (and optional quality) to a sharp pipeline.
|
|
16
|
-
* png is lossless so quality is ignored; jpeg/webp/avif honor quality when set,
|
|
17
|
-
* otherwise sharp's per-format defaults apply.
|
|
18
|
-
*/
|
|
19
8
|
export declare function applyImageFormat(pipeline: Sharp, format: ImageOutputFormat, quality?: number): Sharp;
|
|
20
9
|
//# sourceMappingURL=formats.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"formats.d.ts","sourceRoot":"","sources":["../../src/image/formats.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"formats.d.ts","sourceRoot":"","sources":["../../src/image/formats.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,OAAO,CAAC;AAEnC,MAAM,MAAM,iBAAiB,GAAG,MAAM,GAAG,KAAK,GAAG,MAAM,GAAG,MAAM,CAAC;AAEjE,wBAAgB,UAAU,CAAC,GAAG,CAAC,EAAE,MAAM,GAAG;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,iBAAiB,CAAA;CAAE,CAanF;AAED,wBAAgB,kBAAkB,CAAC,GAAG,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CASnE;AAED,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,KAAK,EACf,MAAM,EAAE,iBAAiB,EACzB,OAAO,CAAC,EAAE,MAAM,GACf,KAAK,CAaP"}
|
package/dist/image/formats.js
CHANGED
|
@@ -1,7 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Map a requested format string (case-insensitive) to its sharp format id and
|
|
3
|
-
* file extension. Unknown or missing formats fall back to jpeg.
|
|
4
|
-
*/
|
|
5
1
|
export function pickFormat(fmt) {
|
|
6
2
|
switch ((fmt ?? 'jpeg').toLowerCase()) {
|
|
7
3
|
case 'png':
|
|
@@ -16,7 +12,6 @@ export function pickFormat(fmt) {
|
|
|
16
12
|
return { ext: 'jpg', format: 'jpeg' };
|
|
17
13
|
}
|
|
18
14
|
}
|
|
19
|
-
/** Map an image format string to its MIME type. */
|
|
20
15
|
export function mimeForImageFormat(fmt) {
|
|
21
16
|
if (!fmt)
|
|
22
17
|
return undefined;
|
|
@@ -33,11 +28,6 @@ export function mimeForImageFormat(fmt) {
|
|
|
33
28
|
return 'image/avif';
|
|
34
29
|
return `image/${f}`;
|
|
35
30
|
}
|
|
36
|
-
/**
|
|
37
|
-
* Apply the chosen output format (and optional quality) to a sharp pipeline.
|
|
38
|
-
* png is lossless so quality is ignored; jpeg/webp/avif honor quality when set,
|
|
39
|
-
* otherwise sharp's per-format defaults apply.
|
|
40
|
-
*/
|
|
41
31
|
export function applyImageFormat(pipeline, format, quality) {
|
|
42
32
|
const q = typeof quality === 'number' ? quality : undefined;
|
|
43
33
|
switch (format) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"formats.js","sourceRoot":"","sources":["../../src/image/formats.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"formats.js","sourceRoot":"","sources":["../../src/image/formats.ts"],"names":[],"mappings":"AAIA,MAAM,UAAU,UAAU,CAAC,GAAY;IACrC,QAAQ,CAAC,GAAG,IAAI,MAAM,CAAC,CAAC,WAAW,EAAE,EAAE,CAAC;QACtC,KAAK,KAAK;YACR,OAAO,EAAE,GAAG,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC;QACvC,KAAK,MAAM;YACT,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC;QACzC,KAAK,MAAM;YACT,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC;QACzC,KAAK,KAAK,CAAC;QACX,KAAK,MAAM,CAAC;QACZ;YACE,OAAO,EAAE,GAAG,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC;IAC1C,CAAC;AACH,CAAC;AAED,MAAM,UAAU,kBAAkB,CAAC,GAAY;IAC7C,IAAI,CAAC,GAAG;QAAE,OAAO,SAAS,CAAC;IAC3B,MAAM,CAAC,GAAG,GAAG,CAAC,WAAW,EAAE,CAAC;IAC5B,IAAI,CAAC,KAAK,KAAK;QAAE,OAAO,YAAY,CAAC;IACrC,IAAI,CAAC,KAAK,MAAM;QAAE,OAAO,YAAY,CAAC;IACtC,IAAI,CAAC,KAAK,KAAK;QAAE,OAAO,WAAW,CAAC;IACpC,IAAI,CAAC,KAAK,MAAM;QAAE,OAAO,YAAY,CAAC;IACtC,IAAI,CAAC,KAAK,MAAM;QAAE,OAAO,YAAY,CAAC;IACtC,OAAO,SAAS,CAAC,EAAE,CAAC;AACtB,CAAC;AAED,MAAM,UAAU,gBAAgB,CAC9B,QAAe,EACf,MAAyB,EACzB,OAAgB;IAEhB,MAAM,CAAC,GAAG,OAAO,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC;IAC5D,QAAQ,MAAM,EAAE,CAAC;QACf,KAAK,KAAK;YACR,OAAO,QAAQ,CAAC,GAAG,EAAE,CAAC;QACxB,KAAK,MAAM;YACT,OAAO,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC;QACvD,KAAK,MAAM;YACT,OAAO,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC;QACvD,KAAK,MAAM,CAAC;QACZ;YACE,OAAO,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC;IACzD,CAAC;AACH,CAAC","sourcesContent":["import type { Sharp } from 'sharp';\n\nexport type ImageOutputFormat = 'jpeg' | 'png' | 'webp' | 'avif';\n\nexport function pickFormat(fmt?: string): { ext: string; format: ImageOutputFormat } {\n switch ((fmt ?? 'jpeg').toLowerCase()) {\n case 'png':\n return { ext: 'png', format: 'png' };\n case 'webp':\n return { ext: 'webp', format: 'webp' };\n case 'avif':\n return { ext: 'avif', format: 'avif' };\n case 'jpg':\n case 'jpeg':\n default:\n return { ext: 'jpg', format: 'jpeg' };\n }\n}\n\nexport function mimeForImageFormat(fmt?: string): string | undefined {\n if (!fmt) return undefined;\n const f = fmt.toLowerCase();\n if (f === 'jpg') return 'image/jpeg';\n if (f === 'jpeg') return 'image/jpeg';\n if (f === 'png') return 'image/png';\n if (f === 'webp') return 'image/webp';\n if (f === 'avif') return 'image/avif';\n return `image/${f}`;\n}\n\nexport function applyImageFormat(\n pipeline: Sharp,\n format: ImageOutputFormat,\n quality?: number,\n): Sharp {\n const q = typeof quality === 'number' ? quality : undefined;\n switch (format) {\n case 'png':\n return pipeline.png();\n case 'webp':\n return pipeline.webp(q ? { quality: q } : undefined);\n case 'avif':\n return pipeline.avif(q ? { quality: q } : undefined);\n case 'jpeg':\n default:\n return pipeline.jpeg(q ? { quality: q } : undefined);\n }\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"image-processor.d.ts","sourceRoot":"","sources":["../../src/image/image-processor.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAGV,qBAAqB,EACrB,mBAAmB,EACpB,MAAM,gCAAgC,CAAC;AAMxC,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"image-processor.d.ts","sourceRoot":"","sources":["../../src/image/image-processor.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAGV,qBAAqB,EACrB,mBAAmB,EACpB,MAAM,gCAAgC,CAAC;AAMxC,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AA4KvD,wBAAsB,YAAY,CAChC,IAAI,EAAE,mBAAmB,EACzB,GAAG,EAAE;IACH,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc,EAAE,MAAM,CAAC;CACxB,EACD,IAAI,CAAC,EAAE,mBAAmB,GACzB,OAAO,CAAC,qBAAqB,CAAC,CA2GhC"}
|
|
@@ -23,7 +23,7 @@ function acceptAllowsImage(spec) {
|
|
|
23
23
|
function acceptAllowsMime(spec, actualMime) {
|
|
24
24
|
const mimes = spec.accept?.mimes?.filter(Boolean) ?? [];
|
|
25
25
|
if (mimes.length === 0)
|
|
26
|
-
return true;
|
|
26
|
+
return true;
|
|
27
27
|
if (!actualMime)
|
|
28
28
|
return false;
|
|
29
29
|
return mimes.some((a) => matchMime(a, actualMime));
|
|
@@ -39,14 +39,6 @@ function aspect(width, height) {
|
|
|
39
39
|
function aspectClose(a, b, tolerance) {
|
|
40
40
|
return Math.abs(a - b) <= tolerance;
|
|
41
41
|
}
|
|
42
|
-
/**
|
|
43
|
-
* Build a "normalized base" pipeline that enforces spec-level uniformity:
|
|
44
|
-
* - if imageCrop exists -> always crop to outputWidth/outputHeight
|
|
45
|
-
* - else if requiredWidth/requiredHeight exist -> crop/resize to those
|
|
46
|
-
* - else if requiredAspectRatio exists -> center-crop to that aspect (no scaling unless needed later)
|
|
47
|
-
*
|
|
48
|
-
* If the source violates constraints and we cannot/should not auto-fix, returns a rejection error.
|
|
49
|
-
*/
|
|
50
42
|
async function buildBase(spec, inputPath) {
|
|
51
43
|
const input = sharp(inputPath, { failOn: 'none' });
|
|
52
44
|
const meta = await input.metadata();
|
|
@@ -94,8 +86,6 @@ async function buildBase(spec, inputPath) {
|
|
|
94
86
|
const w = meta.width ?? undefined;
|
|
95
87
|
const h = meta.height ?? undefined;
|
|
96
88
|
if (!w || !h) {
|
|
97
|
-
// Can't validate/crop reliably without dimensions.
|
|
98
|
-
// We'll still proceed unless strict dims/aspect are required without crop/auto.
|
|
99
89
|
if (spec.requiredWidth || spec.requiredHeight || spec.requiredAspectRatio) {
|
|
100
90
|
if (!spec.allowAutoFormat && !spec.imageCrop) {
|
|
101
91
|
return {
|
|
@@ -109,24 +99,19 @@ async function buildBase(spec, inputPath) {
|
|
|
109
99
|
}
|
|
110
100
|
}
|
|
111
101
|
}
|
|
112
|
-
// Start from raw input each time; callers can clone as needed.
|
|
113
102
|
let base = sharp(inputPath);
|
|
114
|
-
// Keep metadata only if explicitly requested.
|
|
115
103
|
if (spec.image?.stripMetadata === false) {
|
|
116
104
|
base = base.withMetadata({ orientation: meta.orientation });
|
|
117
105
|
}
|
|
118
|
-
// 1) Explicit crop spec (most strict)
|
|
119
106
|
const crop = spec.imageCrop;
|
|
120
107
|
if (crop) {
|
|
121
108
|
base = resizeCover(base, crop.outputWidth, crop.outputHeight);
|
|
122
109
|
return { ok: true, base, inputMeta: meta };
|
|
123
110
|
}
|
|
124
|
-
// 2) Exact required size -> auto crop/resize only if allowAutoFormat OR the request is inherently "uniform"
|
|
125
111
|
const reqW = spec.requiredWidth;
|
|
126
112
|
const reqH = spec.requiredHeight;
|
|
127
113
|
if (reqW && reqH) {
|
|
128
114
|
const canFix = spec.allowAutoFormat === true;
|
|
129
|
-
// If not allowed to auto-fix, require exact match
|
|
130
115
|
if (!canFix && w && h && (w !== reqW || h !== reqH)) {
|
|
131
116
|
return {
|
|
132
117
|
ok: false,
|
|
@@ -137,11 +122,9 @@ async function buildBase(spec, inputPath) {
|
|
|
137
122
|
},
|
|
138
123
|
};
|
|
139
124
|
}
|
|
140
|
-
// Auto-fix path (or exact already): enforce normalized base
|
|
141
125
|
base = resizeCover(base, reqW, reqH);
|
|
142
126
|
return { ok: true, base, inputMeta: meta };
|
|
143
127
|
}
|
|
144
|
-
// 3) Aspect ratio requirement (no exact size)
|
|
145
128
|
const reqAspect = spec.requiredAspectRatio;
|
|
146
129
|
if (reqAspect && w && h) {
|
|
147
130
|
const a = aspect(w, h);
|
|
@@ -157,7 +140,6 @@ async function buildBase(spec, inputPath) {
|
|
|
157
140
|
},
|
|
158
141
|
};
|
|
159
142
|
}
|
|
160
|
-
// Auto center-crop to required aspect ratio (no forced scale yet).
|
|
161
143
|
base = base.extract(computeAspectCropBox(w, h, reqAspect));
|
|
162
144
|
}
|
|
163
145
|
}
|
|
@@ -222,7 +204,6 @@ export async function processImage(spec, ctx, opts) {
|
|
|
222
204
|
height: info.height,
|
|
223
205
|
});
|
|
224
206
|
}
|
|
225
|
-
// success: keep outputs
|
|
226
207
|
tempFiles.length = 0;
|
|
227
208
|
return {
|
|
228
209
|
ok: true,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"image-processor.js","sourceRoot":"","sources":["../../src/image/image-processor.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,MAAM,OAAO,CAAC;AAE1B,OAAO,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AACzC,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAE1D,OAAO,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAChF,OAAO,EACL,oBAAoB,EACpB,WAAW,EACX,YAAY,EACZ,eAAe,GAEhB,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,wBAAwB,EAAE,MAAM,gCAAgC,CAAC;AAE1E,SAAS,SAAS,CAAC,QAAgB,EAAE,MAAc;IACjD,MAAM,CAAC,GAAG,QAAQ,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;IACxC,MAAM,CAAC,GAAG,MAAM,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;IACtC,IAAI,CAAC,CAAC;QAAE,OAAO,IAAI,CAAC;IACpB,IAAI,CAAC,KAAK,KAAK;QAAE,OAAO,IAAI,CAAC;IAC7B,IAAI,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC;QAAE,OAAO,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;IAC1D,OAAO,CAAC,KAAK,CAAC,CAAC;AACjB,CAAC;AAED,SAAS,iBAAiB,CAAC,IAAyB;IAClD,MAAM,KAAK,GAAG,IAAI,CAAC,MAAM,EAAE,KAAK,EAAE,MAAM,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC;IACxD,OAAO,KAAK,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;AAC7D,CAAC;AAED,SAAS,gBAAgB,CAAC,IAAyB,EAAE,UAAmB;IACtE,MAAM,KAAK,GAAG,IAAI,CAAC,MAAM,EAAE,KAAK,EAAE,MAAM,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC;IACxD,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC,CAAC,2BAA2B;IAChE,IAAI,CAAC,UAAU;QAAE,OAAO,KAAK,CAAC;IAC9B,OAAO,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,SAAS,CAAC,CAAC,EAAE,UAAU,CAAC,CAAC,CAAC;AACrD,CAAC;AAED,SAAS,aAAa,CAAC,IAAY,EAAE,GAAW,EAAE,GAAW;IAC3D,OAAO,iBAAiB,CAAC,IAAI,EAAE,GAAG,EAAE,GAAG,CAAC,CAAC;AAC3C,CAAC;AAED,SAAS,MAAM,CAAC,KAAc,EAAE,MAAe;IAC7C,IAAI,CAAC,KAAK,IAAI,CAAC,MAAM;QAAE,OAAO,SAAS,CAAC;IACxC,OAAO,KAAK,GAAG,MAAM,CAAC;AACxB,CAAC;AAED,SAAS,WAAW,CAAC,CAAS,EAAE,CAAS,EAAE,SAAiB;IAC1D,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,SAAS,CAAC;AACtC,CAAC;AAED;;;;;;;GAOG;AACH,KAAK,UAAU,SAAS,CACtB,IAAyB,EACzB,SAAiB;IAIjB,MAAM,KAAK,GAAG,KAAK,CAAC,SAAS,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;IACnD,MAAM,IAAI,GAAG,MAAM,KAAK,CAAC,QAAQ,EAAE,CAAC;IAEpC,IAAI,CAAC,IAAI,CAAC,KAAK,IAAI,CAAC,IAAI,CAAC,MAAM,IAAI,IAAI,CAAC,KAAK,IAAI,CAAC,IAAI,IAAI,CAAC,MAAM,IAAI,CAAC,EAAE,CAAC;QACvE,OAAO;YACL,EAAE,EAAE,KAAK;YACT,KAAK,EAAE;gBACL,IAAI,EAAE,mBAAmB;gBACzB,OAAO,EAAE,6DAA6D;gBACtE,OAAO,EAAE,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,IAAI,EAAE,SAAS,EAAE;aAC1F;SACF,CAAC;IACJ,CAAC;IAED,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC;QACjB,OAAO;YACL,EAAE,EAAE,KAAK;YACT,KAAK,EAAE;gBACL,IAAI,EAAE,oBAAoB;gBAC1B,OAAO,EAAE,wDAAwD;gBACjE,OAAO,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE;aAC7B;SACF,CAAC;IACJ,CAAC;IAED,IAAI,CAAC,iBAAiB,CAAC,IAAI,CAAC,EAAE,CAAC;QAC7B,OAAO;YACL,EAAE,EAAE,KAAK;YACT,KAAK,EAAE;gBACL,IAAI,EAAE,cAAc;gBACpB,OAAO,EAAE,oCAAoC;gBAC7C,OAAO,EAAE,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE;aACjC;SACF,CAAC;IACJ,CAAC;IAED,MAAM,YAAY,GAAG,kBAAkB,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IACrD,IAAI,CAAC,gBAAgB,CAAC,IAAI,EAAE,YAAY,CAAC,EAAE,CAAC;QAC1C,OAAO;YACL,EAAE,EAAE,KAAK;YACT,KAAK,EAAE;gBACL,IAAI,EAAE,cAAc;gBACpB,OAAO,EAAE,iCAAiC;gBAC1C,OAAO,EAAE,EAAE,YAAY,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE;aAC/C;SACF,CAAC;IACJ,CAAC;IAED,MAAM,CAAC,GAAG,IAAI,CAAC,KAAK,IAAI,SAAS,CAAC;IAClC,MAAM,CAAC,GAAG,IAAI,CAAC,MAAM,IAAI,SAAS,CAAC;IAEnC,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC;QACb,mDAAmD;QACnD,gFAAgF;QAChF,IAAI,IAAI,CAAC,aAAa,IAAI,IAAI,CAAC,cAAc,IAAI,IAAI,CAAC,mBAAmB,EAAE,CAAC;YAC1E,IAAI,CAAC,IAAI,CAAC,eAAe,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,CAAC;gBAC7C,OAAO;oBACL,EAAE,EAAE,KAAK;oBACT,KAAK,EAAE;wBACL,IAAI,EAAE,qBAAqB;wBAC3B,OAAO,EAAE,iDAAiD;wBAC1D,OAAO,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE;qBACjC;iBACF,CAAC;YACJ,CAAC;QACH,CAAC;IACH,CAAC;IAED,+DAA+D;IAC/D,IAAI,IAAI,GAAG,KAAK,CAAC,SAAS,CAAC,CAAC;IAE5B,8CAA8C;IAC9C,IAAI,IAAI,CAAC,KAAK,EAAE,aAAa,KAAK,KAAK,EAAE,CAAC;QACxC,IAAI,GAAG,IAAI,CAAC,YAAY,CAAC,EAAE,WAAW,EAAE,IAAI,CAAC,WAAW,EAAE,CAAC,CAAC;IAC9D,CAAC;IAED,sCAAsC;IACtC,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC;IAC5B,IAAI,IAAI,EAAE,CAAC;QACT,IAAI,GAAG,WAAW,CAAC,IAAI,EAAE,IAAI,CAAC,WAAW,EAAE,IAAI,CAAC,YAAY,CAAC,CAAC;QAC9D,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC;IAC7C,CAAC;IAED,4GAA4G;IAC5G,MAAM,IAAI,GAAG,IAAI,CAAC,aAAa,CAAC;IAChC,MAAM,IAAI,GAAG,IAAI,CAAC,cAAc,CAAC;IACjC,IAAI,IAAI,IAAI,IAAI,EAAE,CAAC;QACjB,MAAM,MAAM,GAAG,IAAI,CAAC,eAAe,KAAK,IAAI,CAAC;QAC7C,kDAAkD;QAClD,IAAI,CAAC,MAAM,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,KAAK,IAAI,IAAI,CAAC,KAAK,IAAI,CAAC,EAAE,CAAC;YACpD,OAAO;gBACL,EAAE,EAAE,KAAK;gBACT,KAAK,EAAE;oBACL,IAAI,EAAE,qBAAqB;oBAC3B,OAAO,EAAE,oDAAoD;oBAC7D,OAAO,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,cAAc,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE;iBAC5E;aACF,CAAC;QACJ,CAAC;QAED,4DAA4D;QAC5D,IAAI,GAAG,WAAW,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC;QAErC,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC;IAC7C,CAAC;IAED,8CAA8C;IAC9C,MAAM,SAAS,GAAG,IAAI,CAAC,mBAAmB,CAAC;IAC3C,IAAI,SAAS,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;QACxB,MAAM,CAAC,GAAG,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;QACvB,MAAM,GAAG,GAAG,IAAI,CAAC,oBAAoB,IAAI,IAAI,CAAC;QAC9C,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,EAAE,SAAS,EAAE,GAAG,CAAC,EAAE,CAAC;YACzC,IAAI,CAAC,IAAI,CAAC,eAAe,EAAE,CAAC;gBAC1B,OAAO;oBACL,EAAE,EAAE,KAAK;oBACT,KAAK,EAAE;wBACL,IAAI,EAAE,uBAAuB;wBAC7B,OAAO,EAAE,wDAAwD;wBACjE,OAAO,EAAE,EAAE,mBAAmB,EAAE,SAAS,EAAE,WAAW,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE;qBACjF;iBACF,CAAC;YACJ,CAAC;YAED,mEAAmE;YACnE,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,oBAAoB,CAAC,CAAC,EAAE,CAAC,EAAE,SAAS,CAAC,CAAC,CAAC;QAC7D,CAAC;IACH,CAAC;IAED,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC;AAC7C,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,YAAY,CAChC,IAAyB,EACzB,GAGC,EACD,IAA0B;IAE1B,MAAM,SAAS,GAAa,EAAE,CAAC;IAE/B,IAAI,CAAC;QACH,IAAI,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC;YAC1B,OAAO;gBACL,EAAE,EAAE,KAAK;gBACT,SAAS,EAAE,OAAO;gBAClB,KAAK,EAAE,EAAE,IAAI,EAAE,qBAAqB,EAAE,OAAO,EAAE,sBAAsB,EAAE;aACxE,CAAC;QACJ,CAAC;QAED,MAAM,QAAQ,GAAG,IAAI,CAAC,KAAK,EAAE,QAAQ,EAAE,MAAM;YAC3C,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,QAAQ;YACrB,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,wBAAwB,EAAE,CAAC,CAAC;QAExC,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;QAChD,MAAM,KAAK,CAAC,MAAM,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QAEzC,MAAM,OAAO,GAAG,MAAM,SAAS,CAAC,IAAI,EAAE,GAAG,CAAC,SAAS,CAAC,CAAC;QACrD,IAAI,CAAC,OAAO,CAAC,EAAE,EAAE,CAAC;YAChB,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,CAAC,KAAK,EAAE,CAAC;QACjE,CAAC;QAED,MAAM,EAAE,IAAI,EAAE,SAAS,EAAE,GAAG,OAAO,CAAC;QAEpC,MAAM,OAAO,GAAkB,EAAE,CAAC;QAClC,MAAM,aAAa,GAAG,QAAQ,CAAC,MAAM,IAAI,CAAC,CAAC;QAC3C,IAAI,GAAG,GAAG,CAAC,CAAC;QAEZ,KAAK,MAAM,CAAC,IAAI,QAAQ,EAAE,CAAC;YACzB,GAAG,IAAI,CAAC,CAAC;YAET,IAAI,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC;gBAC1B,OAAO;oBACL,EAAE,EAAE,KAAK;oBACT,SAAS,EAAE,OAAO;oBAClB,KAAK,EAAE,EAAE,IAAI,EAAE,qBAAqB,EAAE,OAAO,EAAE,sBAAsB,EAAE;iBACxE,CAAC;YACJ,CAAC;YAED,IAAI,EAAE,UAAU,EAAE,CAAC;gBACjB,KAAK,EAAE,SAAS;gBAChB,OAAO,EAAE,CAAC,GAAG,GAAG,CAAC,CAAC,GAAG,aAAa;gBAClC,MAAM,EAAE,EAAE,IAAI,EAAE,eAAe,EAAE,GAAG,EAAE,CAAC,CAAC,GAAG,EAAE;aAC9C,CAAC,CAAC;YAEH,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,UAAU,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC;YAC7C,MAAM,OAAO,GAAG,aAAa,CAAC,GAAG,CAAC,cAAc,EAAE,CAAC,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;YAC9D,SAAS,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;YAExB,IAAI,CAAC,GAAG,IAAI,CAAC,KAAK,EAAE,CAAC;YAErB,IAAI,CAAC,CAAC,IAAI,EAAE,CAAC;gBACX,CAAC,GAAG,WAAW,CACb,CAAC,EACD,CAAC,CAAC,IAAI,CAAC,KAAK,EACZ,CAAC,CAAC,IAAI,CAAC,MAAM,EACb,eAAe,CAAE,CAAC,CAAC,IAAI,CAAC,OAAmB,IAAI,QAAQ,CAAC,CACzD,CAAC;YACJ,CAAC;iBAAM,IAAI,CAAC,CAAC,QAAQ,IAAI,CAAC,CAAC,SAAS,EAAE,CAAC;gBACrC,CAAC,GAAG,YAAY,CAAC,CAAC,EAAE,CAAC,CAAC,QAAQ,EAAE,CAAC,CAAC,SAAS,CAAC,CAAC;YAC/C,CAAC;YAED,CAAC,GAAG,gBAAgB,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC;YAE3C,MAAM,IAAI,GAAG,MAAM,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;YAErC,OAAO,CAAC,IAAI,CAAC;gBACX,GAAG,EAAE,CAAC,CAAC,GAAG;gBACV,GAAG,EAAE,UAAU,OAAO,EAAE;gBACxB,IAAI,EAAE,OAAO;gBACb,IAAI,EAAE,kBAAkB,CAAC,IAAI,CAAC,MAAM,CAAC;gBACrC,SAAS,EAAE,IAAI,CAAC,IAAI;gBACpB,KAAK,EAAE,IAAI,CAAC,KAAK;gBACjB,MAAM,EAAE,IAAI,CAAC,MAAM;aACpB,CAAC,CAAC;QACL,CAAC;QAED,wBAAwB;QACxB,SAAS,CAAC,MAAM,GAAG,CAAC,CAAC;QAErB,OAAO;YACL,EAAE,EAAE,IAAI;YACR,SAAS,EAAE,OAAO;YAClB,OAAO;YACP,IAAI,EAAE;gBACJ,IAAI,EAAE,kBAAkB,CAAC,SAAS,CAAC,MAAM,CAAC;gBAC1C,KAAK,EAAE,SAAS,CAAC,KAAK;gBACtB,MAAM,EAAE,SAAS,CAAC,MAAM;aACzB;SACF,CAAC;IACJ,CAAC;IAAC,OAAO,CAAM,EAAE,CAAC;QAChB,OAAO;YACL,EAAE,EAAE,KAAK;YACT,SAAS,EAAE,OAAO;YAClB,KAAK,EAAE;gBACL,IAAI,EAAE,mBAAmB;gBACzB,OAAO,EAAE,CAAC,EAAE,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,yBAAyB;gBACnE,OAAO,EAAE,EAAE,IAAI,EAAE,CAAC,EAAE,IAAI,EAAE;aAC3B;SACF,CAAC;IACJ,CAAC;YAAS,CAAC;QACT,IAAI,SAAS,CAAC,MAAM,EAAE,CAAC;YACrB,MAAM,EAAE,EAAE,EAAE,GAAG,MAAM,MAAM,CAAC,kBAAkB,CAAC,CAAC;YAChD,MAAM,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC,CAAC,CAAC;QAClF,CAAC;IACH,CAAC;AACH,CAAC","sourcesContent":["import type {\n MediaOutput,\n MediaProcessingError,\n MediaProcessingResult,\n MediaProcessingSpec,\n} from '@ttt-productions/media-schemas';\nimport sharp from 'sharp';\nimport type { Sharp, Metadata } from 'sharp';\nimport { mkdir } from 'node:fs/promises';\nimport path from 'node:path';\nimport { safeOutputPathFor } from '../utils/safe-path.js';\nimport type { ProcessMediaOptions } from '../types.js';\nimport { applyImageFormat, mimeForImageFormat, pickFormat } from './formats.js';\nimport {\n computeAspectCropBox,\n resizeCover,\n resizeInside,\n toSharpPosition,\n type Gravity,\n} from './resize.js';\nimport { IMAGE_DEFAULT_OUTPUT_KEY } from '@ttt-productions/media-schemas';\n\nfunction matchMime(accepted: string, actual: string): boolean {\n const a = accepted.trim().toLowerCase();\n const m = actual.trim().toLowerCase();\n if (!a) return true;\n if (a === '*/*') return true;\n if (a.endsWith('/*')) return m.startsWith(a.slice(0, -1));\n return a === m;\n}\n\nfunction acceptAllowsImage(spec: MediaProcessingSpec): boolean {\n const kinds = spec.accept?.kinds?.filter(Boolean) ?? [];\n return kinds.length === 0 ? true : kinds.includes('image');\n}\n\nfunction acceptAllowsMime(spec: MediaProcessingSpec, actualMime?: string): boolean {\n const mimes = spec.accept?.mimes?.filter(Boolean) ?? [];\n if (mimes.length === 0) return true; // empty => accept anything\n if (!actualMime) return false;\n return mimes.some((a) => matchMime(a, actualMime));\n}\n\nfunction outputPathFor(base: string, key: string, ext: string): string {\n return safeOutputPathFor(base, key, ext);\n}\n\nfunction aspect(width?: number, height?: number): number | undefined {\n if (!width || !height) return undefined;\n return width / height;\n}\n\nfunction aspectClose(a: number, b: number, tolerance: number): boolean {\n return Math.abs(a - b) <= tolerance;\n}\n\n/**\n * Build a \"normalized base\" pipeline that enforces spec-level uniformity:\n * - if imageCrop exists -> always crop to outputWidth/outputHeight\n * - else if requiredWidth/requiredHeight exist -> crop/resize to those\n * - else if requiredAspectRatio exists -> center-crop to that aspect (no scaling unless needed later)\n *\n * If the source violates constraints and we cannot/should not auto-fix, returns a rejection error.\n */\nasync function buildBase(\n spec: MediaProcessingSpec,\n inputPath: string,\n): Promise<\n { ok: true; base: Sharp; inputMeta: Metadata } | { ok: false; error: MediaProcessingError }\n> {\n const input = sharp(inputPath, { failOn: 'none' });\n const meta = await input.metadata();\n\n if (!meta.width || !meta.height || meta.width <= 0 || meta.height <= 0) {\n return {\n ok: false,\n error: {\n code: 'processing_failed',\n message: 'Unable to read valid image dimensions. File may be corrupt.',\n details: { width: meta.width, height: meta.height, format: meta.format, path: inputPath },\n },\n };\n }\n\n if (!meta.format) {\n return {\n ok: false,\n error: {\n code: 'unsupported_format',\n message: 'Unable to determine image format. File may be corrupt.',\n details: { path: inputPath },\n },\n };\n }\n\n if (!acceptAllowsImage(spec)) {\n return {\n ok: false,\n error: {\n code: 'invalid_mime',\n message: 'Spec does not allow image uploads.',\n details: { accept: spec.accept },\n },\n };\n }\n\n const detectedMime = mimeForImageFormat(meta.format);\n if (!acceptAllowsMime(spec, detectedMime)) {\n return {\n ok: false,\n error: {\n code: 'invalid_mime',\n message: 'Image mime type is not allowed.',\n details: { detectedMime, accept: spec.accept },\n },\n };\n }\n\n const w = meta.width ?? undefined;\n const h = meta.height ?? undefined;\n\n if (!w || !h) {\n // Can't validate/crop reliably without dimensions.\n // We'll still proceed unless strict dims/aspect are required without crop/auto.\n if (spec.requiredWidth || spec.requiredHeight || spec.requiredAspectRatio) {\n if (!spec.allowAutoFormat && !spec.imageCrop) {\n return {\n ok: false,\n error: {\n code: 'dimensions_mismatch',\n message: 'Unable to read image dimensions for validation.',\n details: { width: w, height: h },\n },\n };\n }\n }\n }\n\n // Start from raw input each time; callers can clone as needed.\n let base = sharp(inputPath);\n\n // Keep metadata only if explicitly requested.\n if (spec.image?.stripMetadata === false) {\n base = base.withMetadata({ orientation: meta.orientation });\n }\n\n // 1) Explicit crop spec (most strict)\n const crop = spec.imageCrop;\n if (crop) {\n base = resizeCover(base, crop.outputWidth, crop.outputHeight);\n return { ok: true, base, inputMeta: meta };\n }\n\n // 2) Exact required size -> auto crop/resize only if allowAutoFormat OR the request is inherently \"uniform\"\n const reqW = spec.requiredWidth;\n const reqH = spec.requiredHeight;\n if (reqW && reqH) {\n const canFix = spec.allowAutoFormat === true;\n // If not allowed to auto-fix, require exact match\n if (!canFix && w && h && (w !== reqW || h !== reqH)) {\n return {\n ok: false,\n error: {\n code: 'dimensions_mismatch',\n message: 'Image dimensions do not match the required format.',\n details: { requiredWidth: reqW, requiredHeight: reqH, width: w, height: h },\n },\n };\n }\n\n // Auto-fix path (or exact already): enforce normalized base\n base = resizeCover(base, reqW, reqH);\n\n return { ok: true, base, inputMeta: meta };\n }\n\n // 3) Aspect ratio requirement (no exact size)\n const reqAspect = spec.requiredAspectRatio;\n if (reqAspect && w && h) {\n const a = aspect(w, h);\n const tol = spec.aspectRatioTolerance ?? 0.02;\n if (a && !aspectClose(a, reqAspect, tol)) {\n if (!spec.allowAutoFormat) {\n return {\n ok: false,\n error: {\n code: 'aspect_ratio_mismatch',\n message: 'Image aspect ratio does not match the required format.',\n details: { requiredAspectRatio: reqAspect, aspectRatio: a, width: w, height: h },\n },\n };\n }\n\n // Auto center-crop to required aspect ratio (no forced scale yet).\n base = base.extract(computeAspectCropBox(w, h, reqAspect));\n }\n }\n\n return { ok: true, base, inputMeta: meta };\n}\n\nexport async function processImage(\n spec: MediaProcessingSpec,\n ctx: {\n inputPath: string;\n outputBasePath: string;\n },\n opts?: ProcessMediaOptions,\n): Promise<MediaProcessingResult> {\n const tempFiles: string[] = [];\n\n try {\n if (opts?.signal?.aborted) {\n return {\n ok: false,\n mediaType: 'image',\n error: { code: 'processing_canceled', message: 'Processing canceled.' },\n };\n }\n\n const variants = spec.image?.variants?.length\n ? spec.image.variants\n : [{ key: IMAGE_DEFAULT_OUTPUT_KEY }];\n\n const outDir = path.dirname(ctx.outputBasePath);\n await mkdir(outDir, { recursive: true });\n\n const baseRes = await buildBase(spec, ctx.inputPath);\n if (!baseRes.ok) {\n return { ok: false, mediaType: 'image', error: baseRes.error };\n }\n\n const { base, inputMeta } = baseRes;\n\n const outputs: MediaOutput[] = [];\n const totalVariants = variants.length || 1;\n let idx = 0;\n\n for (const v of variants) {\n idx += 1;\n\n if (opts?.signal?.aborted) {\n return {\n ok: false,\n mediaType: 'image',\n error: { code: 'processing_canceled', message: 'Processing canceled.' },\n };\n }\n\n opts?.onProgress?.({\n phase: 'process',\n percent: (idx - 1) / totalVariants,\n detail: { step: 'image_variant', key: v.key },\n });\n\n const { ext, format } = pickFormat(v.format);\n const outPath = outputPathFor(ctx.outputBasePath, v.key, ext);\n tempFiles.push(outPath);\n\n let p = base.clone();\n\n if (v.crop) {\n p = resizeCover(\n p,\n v.crop.width,\n v.crop.height,\n toSharpPosition((v.crop.gravity as Gravity) ?? 'center'),\n );\n } else if (v.maxWidth || v.maxHeight) {\n p = resizeInside(p, v.maxWidth, v.maxHeight);\n }\n\n p = applyImageFormat(p, format, v.quality);\n\n const info = await p.toFile(outPath);\n\n outputs.push({\n key: v.key,\n url: `file://${outPath}`,\n path: outPath,\n mime: mimeForImageFormat(info.format),\n sizeBytes: info.size,\n width: info.width,\n height: info.height,\n });\n }\n\n // success: keep outputs\n tempFiles.length = 0;\n\n return {\n ok: true,\n mediaType: 'image',\n outputs,\n meta: {\n mime: mimeForImageFormat(inputMeta.format),\n width: inputMeta.width,\n height: inputMeta.height,\n },\n };\n } catch (e: any) {\n return {\n ok: false,\n mediaType: 'image',\n error: {\n code: 'processing_failed',\n message: e?.message ? String(e.message) : 'Image processing failed',\n details: { name: e?.name },\n },\n };\n } finally {\n if (tempFiles.length) {\n const { rm } = await import('node:fs/promises');\n await Promise.all(tempFiles.map((f) => rm(f, { force: true }).catch(() => {})));\n }\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"image-processor.js","sourceRoot":"","sources":["../../src/image/image-processor.ts"],"names":[],"mappings":"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","sourcesContent":["import type {\n MediaOutput,\n MediaProcessingError,\n MediaProcessingResult,\n MediaProcessingSpec,\n} from '@ttt-productions/media-schemas';\nimport sharp from 'sharp';\nimport type { Sharp, Metadata } from 'sharp';\nimport { mkdir } from 'node:fs/promises';\nimport path from 'node:path';\nimport { safeOutputPathFor } from '../utils/safe-path.js';\nimport type { ProcessMediaOptions } from '../types.js';\nimport { applyImageFormat, mimeForImageFormat, pickFormat } from './formats.js';\nimport {\n computeAspectCropBox,\n resizeCover,\n resizeInside,\n toSharpPosition,\n type Gravity,\n} from './resize.js';\nimport { IMAGE_DEFAULT_OUTPUT_KEY } from '@ttt-productions/media-schemas';\n\nfunction matchMime(accepted: string, actual: string): boolean {\n const a = accepted.trim().toLowerCase();\n const m = actual.trim().toLowerCase();\n if (!a) return true;\n if (a === '*/*') return true;\n if (a.endsWith('/*')) return m.startsWith(a.slice(0, -1));\n return a === m;\n}\n\nfunction acceptAllowsImage(spec: MediaProcessingSpec): boolean {\n const kinds = spec.accept?.kinds?.filter(Boolean) ?? [];\n return kinds.length === 0 ? true : kinds.includes('image');\n}\n\nfunction acceptAllowsMime(spec: MediaProcessingSpec, actualMime?: string): boolean {\n const mimes = spec.accept?.mimes?.filter(Boolean) ?? [];\n if (mimes.length === 0) return true;\n if (!actualMime) return false;\n return mimes.some((a) => matchMime(a, actualMime));\n}\n\nfunction outputPathFor(base: string, key: string, ext: string): string {\n return safeOutputPathFor(base, key, ext);\n}\n\nfunction aspect(width?: number, height?: number): number | undefined {\n if (!width || !height) return undefined;\n return width / height;\n}\n\nfunction aspectClose(a: number, b: number, tolerance: number): boolean {\n return Math.abs(a - b) <= tolerance;\n}\n\nasync function buildBase(\n spec: MediaProcessingSpec,\n inputPath: string,\n): Promise<\n { ok: true; base: Sharp; inputMeta: Metadata } | { ok: false; error: MediaProcessingError }\n> {\n const input = sharp(inputPath, { failOn: 'none' });\n const meta = await input.metadata();\n\n if (!meta.width || !meta.height || meta.width <= 0 || meta.height <= 0) {\n return {\n ok: false,\n error: {\n code: 'processing_failed',\n message: 'Unable to read valid image dimensions. File may be corrupt.',\n details: { width: meta.width, height: meta.height, format: meta.format, path: inputPath },\n },\n };\n }\n\n if (!meta.format) {\n return {\n ok: false,\n error: {\n code: 'unsupported_format',\n message: 'Unable to determine image format. File may be corrupt.',\n details: { path: inputPath },\n },\n };\n }\n\n if (!acceptAllowsImage(spec)) {\n return {\n ok: false,\n error: {\n code: 'invalid_mime',\n message: 'Spec does not allow image uploads.',\n details: { accept: spec.accept },\n },\n };\n }\n\n const detectedMime = mimeForImageFormat(meta.format);\n if (!acceptAllowsMime(spec, detectedMime)) {\n return {\n ok: false,\n error: {\n code: 'invalid_mime',\n message: 'Image mime type is not allowed.',\n details: { detectedMime, accept: spec.accept },\n },\n };\n }\n\n const w = meta.width ?? undefined;\n const h = meta.height ?? undefined;\n\n if (!w || !h) {\n if (spec.requiredWidth || spec.requiredHeight || spec.requiredAspectRatio) {\n if (!spec.allowAutoFormat && !spec.imageCrop) {\n return {\n ok: false,\n error: {\n code: 'dimensions_mismatch',\n message: 'Unable to read image dimensions for validation.',\n details: { width: w, height: h },\n },\n };\n }\n }\n }\n\n let base = sharp(inputPath);\n\n if (spec.image?.stripMetadata === false) {\n base = base.withMetadata({ orientation: meta.orientation });\n }\n\n const crop = spec.imageCrop;\n if (crop) {\n base = resizeCover(base, crop.outputWidth, crop.outputHeight);\n return { ok: true, base, inputMeta: meta };\n }\n\n const reqW = spec.requiredWidth;\n const reqH = spec.requiredHeight;\n if (reqW && reqH) {\n const canFix = spec.allowAutoFormat === true;\n if (!canFix && w && h && (w !== reqW || h !== reqH)) {\n return {\n ok: false,\n error: {\n code: 'dimensions_mismatch',\n message: 'Image dimensions do not match the required format.',\n details: { requiredWidth: reqW, requiredHeight: reqH, width: w, height: h },\n },\n };\n }\n\n base = resizeCover(base, reqW, reqH);\n\n return { ok: true, base, inputMeta: meta };\n }\n\n const reqAspect = spec.requiredAspectRatio;\n if (reqAspect && w && h) {\n const a = aspect(w, h);\n const tol = spec.aspectRatioTolerance ?? 0.02;\n if (a && !aspectClose(a, reqAspect, tol)) {\n if (!spec.allowAutoFormat) {\n return {\n ok: false,\n error: {\n code: 'aspect_ratio_mismatch',\n message: 'Image aspect ratio does not match the required format.',\n details: { requiredAspectRatio: reqAspect, aspectRatio: a, width: w, height: h },\n },\n };\n }\n\n base = base.extract(computeAspectCropBox(w, h, reqAspect));\n }\n }\n\n return { ok: true, base, inputMeta: meta };\n}\n\nexport async function processImage(\n spec: MediaProcessingSpec,\n ctx: {\n inputPath: string;\n outputBasePath: string;\n },\n opts?: ProcessMediaOptions,\n): Promise<MediaProcessingResult> {\n const tempFiles: string[] = [];\n\n try {\n if (opts?.signal?.aborted) {\n return {\n ok: false,\n mediaType: 'image',\n error: { code: 'processing_canceled', message: 'Processing canceled.' },\n };\n }\n\n const variants = spec.image?.variants?.length\n ? spec.image.variants\n : [{ key: IMAGE_DEFAULT_OUTPUT_KEY }];\n\n const outDir = path.dirname(ctx.outputBasePath);\n await mkdir(outDir, { recursive: true });\n\n const baseRes = await buildBase(spec, ctx.inputPath);\n if (!baseRes.ok) {\n return { ok: false, mediaType: 'image', error: baseRes.error };\n }\n\n const { base, inputMeta } = baseRes;\n\n const outputs: MediaOutput[] = [];\n const totalVariants = variants.length || 1;\n let idx = 0;\n\n for (const v of variants) {\n idx += 1;\n\n if (opts?.signal?.aborted) {\n return {\n ok: false,\n mediaType: 'image',\n error: { code: 'processing_canceled', message: 'Processing canceled.' },\n };\n }\n\n opts?.onProgress?.({\n phase: 'process',\n percent: (idx - 1) / totalVariants,\n detail: { step: 'image_variant', key: v.key },\n });\n\n const { ext, format } = pickFormat(v.format);\n const outPath = outputPathFor(ctx.outputBasePath, v.key, ext);\n tempFiles.push(outPath);\n\n let p = base.clone();\n\n if (v.crop) {\n p = resizeCover(\n p,\n v.crop.width,\n v.crop.height,\n toSharpPosition((v.crop.gravity as Gravity) ?? 'center'),\n );\n } else if (v.maxWidth || v.maxHeight) {\n p = resizeInside(p, v.maxWidth, v.maxHeight);\n }\n\n p = applyImageFormat(p, format, v.quality);\n\n const info = await p.toFile(outPath);\n\n outputs.push({\n key: v.key,\n url: `file://${outPath}`,\n path: outPath,\n mime: mimeForImageFormat(info.format),\n sizeBytes: info.size,\n width: info.width,\n height: info.height,\n });\n }\n\n tempFiles.length = 0;\n\n return {\n ok: true,\n mediaType: 'image',\n outputs,\n meta: {\n mime: mimeForImageFormat(inputMeta.format),\n width: inputMeta.width,\n height: inputMeta.height,\n },\n };\n } catch (e: any) {\n return {\n ok: false,\n mediaType: 'image',\n error: {\n code: 'processing_failed',\n message: e?.message ? String(e.message) : 'Image processing failed',\n details: { name: e?.name },\n },\n };\n } finally {\n if (tempFiles.length) {\n const { rm } = await import('node:fs/promises');\n await Promise.all(tempFiles.map((f) => rm(f, { force: true }).catch(() => {})));\n }\n }\n}\n"]}
|
package/dist/image/resize.d.ts
CHANGED
|
@@ -1,22 +1,8 @@
|
|
|
1
1
|
import type { Sharp, Gravity as SharpGravity } from 'sharp';
|
|
2
|
-
/** Logical crop/resize gravity, mapped to sharp positions by toSharpPosition. */
|
|
3
2
|
export type Gravity = 'center' | 'top' | 'bottom' | 'left' | 'right';
|
|
4
|
-
/** Map a logical gravity to a sharp position string. Defaults to centre. */
|
|
5
3
|
export declare function toSharpPosition(g?: Gravity): SharpGravity;
|
|
6
|
-
/**
|
|
7
|
-
* Cover-resize to an exact width/height (crop to fill), never enlarging beyond
|
|
8
|
-
* the source. Used for explicit crops and required-size normalization.
|
|
9
|
-
*/
|
|
10
4
|
export declare function resizeCover(pipeline: Sharp, width: number, height: number, position?: SharpGravity): Sharp;
|
|
11
|
-
/**
|
|
12
|
-
* Fit-inside resize bounded by maxWidth/maxHeight (preserves aspect ratio),
|
|
13
|
-
* never enlarging beyond the source. Either bound may be omitted.
|
|
14
|
-
*/
|
|
15
5
|
export declare function resizeInside(pipeline: Sharp, maxWidth?: number, maxHeight?: number): Sharp;
|
|
16
|
-
/**
|
|
17
|
-
* Compute a centered crop box that brings (width x height) to targetAspect,
|
|
18
|
-
* shrinking only the longer dimension. Returns sharp .extract() coordinates.
|
|
19
|
-
*/
|
|
20
6
|
export declare function computeAspectCropBox(width: number, height: number, targetAspect: number): {
|
|
21
7
|
left: number;
|
|
22
8
|
top: number;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"resize.d.ts","sourceRoot":"","sources":["../../src/image/resize.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"resize.d.ts","sourceRoot":"","sources":["../../src/image/resize.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,OAAO,IAAI,YAAY,EAAE,MAAM,OAAO,CAAC;AAE5D,MAAM,MAAM,OAAO,GAAG,QAAQ,GAAG,KAAK,GAAG,QAAQ,GAAG,MAAM,GAAG,OAAO,CAAC;AAErE,wBAAgB,eAAe,CAAC,CAAC,CAAC,EAAE,OAAO,GAAG,YAAY,CAczD;AAED,wBAAgB,WAAW,CACzB,QAAQ,EAAE,KAAK,EACf,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,EACd,QAAQ,GAAE,YAAuB,GAChC,KAAK,CAMP;AAED,wBAAgB,YAAY,CAAC,QAAQ,EAAE,KAAK,EAAE,QAAQ,CAAC,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,KAAK,CAK1F;AAED,wBAAgB,oBAAoB,CAClC,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,EACd,YAAY,EAAE,MAAM,GACnB;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAe9D"}
|
package/dist/image/resize.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
/** Map a logical gravity to a sharp position string. Defaults to centre. */
|
|
2
1
|
export function toSharpPosition(g) {
|
|
3
2
|
switch (g) {
|
|
4
3
|
case 'top':
|
|
@@ -14,10 +13,6 @@ export function toSharpPosition(g) {
|
|
|
14
13
|
return 'centre';
|
|
15
14
|
}
|
|
16
15
|
}
|
|
17
|
-
/**
|
|
18
|
-
* Cover-resize to an exact width/height (crop to fill), never enlarging beyond
|
|
19
|
-
* the source. Used for explicit crops and required-size normalization.
|
|
20
|
-
*/
|
|
21
16
|
export function resizeCover(pipeline, width, height, position = 'centre') {
|
|
22
17
|
return pipeline.resize(width, height, {
|
|
23
18
|
fit: 'cover',
|
|
@@ -25,30 +20,20 @@ export function resizeCover(pipeline, width, height, position = 'centre') {
|
|
|
25
20
|
withoutEnlargement: true,
|
|
26
21
|
});
|
|
27
22
|
}
|
|
28
|
-
/**
|
|
29
|
-
* Fit-inside resize bounded by maxWidth/maxHeight (preserves aspect ratio),
|
|
30
|
-
* never enlarging beyond the source. Either bound may be omitted.
|
|
31
|
-
*/
|
|
32
23
|
export function resizeInside(pipeline, maxWidth, maxHeight) {
|
|
33
24
|
return pipeline.resize(maxWidth ?? null, maxHeight ?? null, {
|
|
34
25
|
fit: 'inside',
|
|
35
26
|
withoutEnlargement: true,
|
|
36
27
|
});
|
|
37
28
|
}
|
|
38
|
-
/**
|
|
39
|
-
* Compute a centered crop box that brings (width x height) to targetAspect,
|
|
40
|
-
* shrinking only the longer dimension. Returns sharp .extract() coordinates.
|
|
41
|
-
*/
|
|
42
29
|
export function computeAspectCropBox(width, height, targetAspect) {
|
|
43
30
|
const a = width / height;
|
|
44
31
|
let cropW = width;
|
|
45
32
|
let cropH = height;
|
|
46
33
|
if (a > targetAspect) {
|
|
47
|
-
// too wide -> reduce width
|
|
48
34
|
cropW = Math.max(1, Math.floor(height * targetAspect));
|
|
49
35
|
}
|
|
50
36
|
else {
|
|
51
|
-
// too tall -> reduce height
|
|
52
37
|
cropH = Math.max(1, Math.floor(width / targetAspect));
|
|
53
38
|
}
|
|
54
39
|
const left = Math.max(0, Math.floor((width - cropW) / 2));
|
package/dist/image/resize.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"resize.js","sourceRoot":"","sources":["../../src/image/resize.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"resize.js","sourceRoot":"","sources":["../../src/image/resize.ts"],"names":[],"mappings":"AAIA,MAAM,UAAU,eAAe,CAAC,CAAW;IACzC,QAAQ,CAAC,EAAE,CAAC;QACV,KAAK,KAAK;YACR,OAAO,OAAO,CAAC;QACjB,KAAK,QAAQ;YACX,OAAO,OAAO,CAAC;QACjB,KAAK,MAAM;YACT,OAAO,MAAM,CAAC;QAChB,KAAK,OAAO;YACV,OAAO,MAAM,CAAC;QAChB,KAAK,QAAQ,CAAC;QACd;YACE,OAAO,QAAQ,CAAC;IACpB,CAAC;AACH,CAAC;AAED,MAAM,UAAU,WAAW,CACzB,QAAe,EACf,KAAa,EACb,MAAc,EACd,WAAyB,QAAQ;IAEjC,OAAO,QAAQ,CAAC,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE;QACpC,GAAG,EAAE,OAAO;QACZ,QAAQ;QACR,kBAAkB,EAAE,IAAI;KACzB,CAAC,CAAC;AACL,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,QAAe,EAAE,QAAiB,EAAE,SAAkB;IACjF,OAAO,QAAQ,CAAC,MAAM,CAAC,QAAQ,IAAI,IAAI,EAAE,SAAS,IAAI,IAAI,EAAE;QAC1D,GAAG,EAAE,QAAQ;QACb,kBAAkB,EAAE,IAAI;KACzB,CAAC,CAAC;AACL,CAAC;AAED,MAAM,UAAU,oBAAoB,CAClC,KAAa,EACb,MAAc,EACd,YAAoB;IAEpB,MAAM,CAAC,GAAG,KAAK,GAAG,MAAM,CAAC;IACzB,IAAI,KAAK,GAAG,KAAK,CAAC;IAClB,IAAI,KAAK,GAAG,MAAM,CAAC;IAEnB,IAAI,CAAC,GAAG,YAAY,EAAE,CAAC;QACrB,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,MAAM,GAAG,YAAY,CAAC,CAAC,CAAC;IACzD,CAAC;SAAM,CAAC;QACN,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,KAAK,GAAG,YAAY,CAAC,CAAC,CAAC;IACxD,CAAC;IAED,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,KAAK,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;IAC1D,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,MAAM,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;IAE1D,OAAO,EAAE,IAAI,EAAE,GAAG,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC;AACpD,CAAC","sourcesContent":["import type { Sharp, Gravity as SharpGravity } from 'sharp';\n\nexport type Gravity = 'center' | 'top' | 'bottom' | 'left' | 'right';\n\nexport function toSharpPosition(g?: Gravity): SharpGravity {\n switch (g) {\n case 'top':\n return 'north';\n case 'bottom':\n return 'south';\n case 'left':\n return 'west';\n case 'right':\n return 'east';\n case 'center':\n default:\n return 'centre';\n }\n}\n\nexport function resizeCover(\n pipeline: Sharp,\n width: number,\n height: number,\n position: SharpGravity = 'centre',\n): Sharp {\n return pipeline.resize(width, height, {\n fit: 'cover',\n position,\n withoutEnlargement: true,\n });\n}\n\nexport function resizeInside(pipeline: Sharp, maxWidth?: number, maxHeight?: number): Sharp {\n return pipeline.resize(maxWidth ?? null, maxHeight ?? null, {\n fit: 'inside',\n withoutEnlargement: true,\n });\n}\n\nexport function computeAspectCropBox(\n width: number,\n height: number,\n targetAspect: number,\n): { left: number; top: number; width: number; height: number } {\n const a = width / height;\n let cropW = width;\n let cropH = height;\n\n if (a > targetAspect) {\n cropW = Math.max(1, Math.floor(height * targetAspect));\n } else {\n cropH = Math.max(1, Math.floor(width / targetAspect));\n }\n\n const left = Math.max(0, Math.floor((width - cropW) / 2));\n const top = Math.max(0, Math.floor((height - cropH) / 2));\n\n return { left, top, width: cropW, height: cropH };\n}\n"]}
|