@ttt-productions/media-processing-core 0.8.16 → 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.
Files changed (141) hide show
  1. package/dist/audio/audio-processor.d.ts +2 -2
  2. package/dist/audio/audio-processor.d.ts.map +1 -1
  3. package/dist/audio/audio-processor.js +63 -50
  4. package/dist/audio/audio-processor.js.map +1 -1
  5. package/dist/audio/probe.d.ts +1 -1
  6. package/dist/audio/probe.d.ts.map +1 -1
  7. package/dist/audio/probe.js +6 -5
  8. package/dist/audio/probe.js.map +1 -1
  9. package/dist/duration/media-duration.d.ts +2 -13
  10. package/dist/duration/media-duration.d.ts.map +1 -1
  11. package/dist/duration/media-duration.js +24 -49
  12. package/dist/duration/media-duration.js.map +1 -1
  13. package/dist/image/formats.d.ts +2 -13
  14. package/dist/image/formats.d.ts.map +1 -1
  15. package/dist/image/formats.js +25 -35
  16. package/dist/image/formats.js.map +1 -1
  17. package/dist/image/image-processor.d.ts +2 -2
  18. package/dist/image/image-processor.d.ts.map +1 -1
  19. package/dist/image/image-processor.js +48 -57
  20. package/dist/image/image-processor.js.map +1 -1
  21. package/dist/image/resize.d.ts +2 -16
  22. package/dist/image/resize.d.ts.map +1 -1
  23. package/dist/image/resize.js +13 -28
  24. package/dist/image/resize.js.map +1 -1
  25. package/dist/index.d.ts +19 -16
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +18 -16
  28. package/dist/index.js.map +1 -1
  29. package/dist/inspection/inspect-media.d.ts +13 -20
  30. package/dist/inspection/inspect-media.d.ts.map +1 -1
  31. package/dist/inspection/inspect-media.js +197 -201
  32. package/dist/inspection/inspect-media.js.map +1 -1
  33. package/dist/io/fs.d.ts +1 -1
  34. package/dist/io/fs.d.ts.map +1 -1
  35. package/dist/io/fs.js +4 -5
  36. package/dist/io/fs.js.map +1 -1
  37. package/dist/io/local-input.d.ts +1 -21
  38. package/dist/io/local-input.d.ts.map +1 -1
  39. package/dist/io/local-input.js +15 -35
  40. package/dist/io/local-input.js.map +1 -1
  41. package/dist/io/types.d.ts +0 -3
  42. package/dist/io/types.d.ts.map +1 -1
  43. package/dist/io/types.js.map +1 -1
  44. package/dist/moderation/merge.d.ts +1 -1
  45. package/dist/moderation/merge.d.ts.map +1 -1
  46. package/dist/moderation/merge.js +1 -2
  47. package/dist/moderation/merge.js.map +1 -1
  48. package/dist/moderation/types.d.ts +3 -5
  49. package/dist/moderation/types.d.ts.map +1 -1
  50. package/dist/moderation/types.js.map +1 -1
  51. package/dist/process-media.d.ts +2 -3
  52. package/dist/process-media.d.ts.map +1 -1
  53. package/dist/process-media.js +9 -9
  54. package/dist/process-media.js.map +1 -1
  55. package/dist/processing/errors.d.ts.map +1 -1
  56. package/dist/processing/errors.js +1 -1
  57. package/dist/processing/errors.js.map +1 -1
  58. package/dist/processing/result.d.ts +2 -2
  59. package/dist/processing/result.js +1 -1
  60. package/dist/processing/result.js.map +1 -1
  61. package/dist/run-pipeline.d.ts +5 -22
  62. package/dist/run-pipeline.d.ts.map +1 -1
  63. package/dist/run-pipeline.js +72 -81
  64. package/dist/run-pipeline.js.map +1 -1
  65. package/dist/server/download-token.d.ts +0 -6
  66. package/dist/server/download-token.d.ts.map +1 -1
  67. package/dist/server/download-token.js +3 -13
  68. package/dist/server/download-token.js.map +1 -1
  69. package/dist/server/firebase-media-io.d.ts +3 -13
  70. package/dist/server/firebase-media-io.d.ts.map +1 -1
  71. package/dist/server/firebase-media-io.js +1 -6
  72. package/dist/server/firebase-media-io.js.map +1 -1
  73. package/dist/server/gcs-errors.d.ts +0 -8
  74. package/dist/server/gcs-errors.d.ts.map +1 -1
  75. package/dist/server/gcs-errors.js +2 -10
  76. package/dist/server/gcs-errors.js.map +1 -1
  77. package/dist/server/index.d.ts +7 -6
  78. package/dist/server/index.d.ts.map +1 -1
  79. package/dist/server/index.js +7 -6
  80. package/dist/server/index.js.map +1 -1
  81. package/dist/server/pending-media.d.ts +3 -34
  82. package/dist/server/pending-media.d.ts.map +1 -1
  83. package/dist/server/pending-media.js +13 -30
  84. package/dist/server/pending-media.js.map +1 -1
  85. package/dist/server/r2-retry.d.ts +4 -52
  86. package/dist/server/r2-retry.d.ts.map +1 -1
  87. package/dist/server/r2-retry.js +49 -134
  88. package/dist/server/r2-retry.js.map +1 -1
  89. package/dist/server/staged-object.d.ts +2 -46
  90. package/dist/server/staged-object.d.ts.map +1 -1
  91. package/dist/server/staged-object.js +4 -48
  92. package/dist/server/staged-object.js.map +1 -1
  93. package/dist/server/storage-ops.d.ts +5 -67
  94. package/dist/server/storage-ops.d.ts.map +1 -1
  95. package/dist/server/storage-ops.js +110 -185
  96. package/dist/server/storage-ops.js.map +1 -1
  97. package/dist/types.d.ts +2 -4
  98. package/dist/types.d.ts.map +1 -1
  99. package/dist/types.js.map +1 -1
  100. package/dist/utils/log.d.ts.map +1 -1
  101. package/dist/utils/log.js +3 -3
  102. package/dist/utils/log.js.map +1 -1
  103. package/dist/utils/paths.d.ts.map +1 -1
  104. package/dist/utils/paths.js +1 -4
  105. package/dist/utils/paths.js.map +1 -1
  106. package/dist/utils/safe-path.d.ts.map +1 -1
  107. package/dist/utils/safe-path.js +8 -9
  108. package/dist/utils/safe-path.js.map +1 -1
  109. package/dist/validation/validate-duration.d.ts.map +1 -1
  110. package/dist/validation/validate-duration.js.map +1 -1
  111. package/dist/validation/validate-mime.d.ts.map +1 -1
  112. package/dist/validation/validate-mime.js.map +1 -1
  113. package/dist/validation/validate-size.d.ts.map +1 -1
  114. package/dist/validation/validate-size.js.map +1 -1
  115. package/dist/video/ffmpeg.d.ts +1 -23
  116. package/dist/video/ffmpeg.d.ts.map +1 -1
  117. package/dist/video/ffmpeg.js +57 -64
  118. package/dist/video/ffmpeg.js.map +1 -1
  119. package/dist/video/probe.d.ts +1 -1
  120. package/dist/video/probe.d.ts.map +1 -1
  121. package/dist/video/probe.js +10 -8
  122. package/dist/video/probe.js.map +1 -1
  123. package/dist/video/video-processor.d.ts +2 -2
  124. package/dist/video/video-processor.d.ts.map +1 -1
  125. package/dist/video/video-processor.js +125 -96
  126. package/dist/video/video-processor.js.map +1 -1
  127. package/dist/workspace/temp.js +4 -4
  128. package/dist/workspace/temp.js.map +1 -1
  129. package/package.json +2 -2
  130. package/dist/audio/index.d.ts +0 -3
  131. package/dist/audio/index.d.ts.map +0 -1
  132. package/dist/audio/index.js +0 -3
  133. package/dist/audio/index.js.map +0 -1
  134. package/dist/image/index.d.ts +0 -4
  135. package/dist/image/index.d.ts.map +0 -1
  136. package/dist/image/index.js +0 -4
  137. package/dist/image/index.js.map +0 -1
  138. package/dist/video/index.d.ts +0 -4
  139. package/dist/video/index.d.ts.map +0 -1
  140. package/dist/video/index.js +0 -4
  141. package/dist/video/index.js.map +0 -1
@@ -1,53 +1,43 @@
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
- switch ((fmt ?? "jpeg").toLowerCase()) {
7
- case "png":
8
- return { ext: "png", format: "png" };
9
- case "webp":
10
- return { ext: "webp", format: "webp" };
11
- case "avif":
12
- return { ext: "avif", format: "avif" };
13
- case "jpg":
14
- case "jpeg":
2
+ switch ((fmt ?? 'jpeg').toLowerCase()) {
3
+ case 'png':
4
+ return { ext: 'png', format: 'png' };
5
+ case 'webp':
6
+ return { ext: 'webp', format: 'webp' };
7
+ case 'avif':
8
+ return { ext: 'avif', format: 'avif' };
9
+ case 'jpg':
10
+ case 'jpeg':
15
11
  default:
16
- return { ext: "jpg", format: "jpeg" };
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;
23
18
  const f = fmt.toLowerCase();
24
- if (f === "jpg")
25
- return "image/jpeg";
26
- if (f === "jpeg")
27
- return "image/jpeg";
28
- if (f === "png")
29
- return "image/png";
30
- if (f === "webp")
31
- return "image/webp";
32
- if (f === "avif")
33
- return "image/avif";
19
+ if (f === 'jpg')
20
+ return 'image/jpeg';
21
+ if (f === 'jpeg')
22
+ return 'image/jpeg';
23
+ if (f === 'png')
24
+ return 'image/png';
25
+ if (f === 'webp')
26
+ return 'image/webp';
27
+ if (f === 'avif')
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
- const q = typeof quality === "number" ? quality : undefined;
32
+ const q = typeof quality === 'number' ? quality : undefined;
43
33
  switch (format) {
44
- case "png":
34
+ case 'png':
45
35
  return pipeline.png();
46
- case "webp":
36
+ case 'webp':
47
37
  return pipeline.webp(q ? { quality: q } : undefined);
48
- case "avif":
38
+ case 'avif':
49
39
  return pipeline.avif(q ? { quality: q } : undefined);
50
- case "jpeg":
40
+ case 'jpeg':
51
41
  default:
52
42
  return pipeline.jpeg(q ? { quality: q } : undefined);
53
43
  }
@@ -1 +1 @@
1
- {"version":3,"file":"formats.js","sourceRoot":"","sources":["../../src/image/formats.ts"],"names":[],"mappings":"AAMA;;;GAGG;AACH,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,mDAAmD;AACnD,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;;;;GAIG;AACH,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":["// shared image format / quality presets\nimport type { Sharp } from \"sharp\";\n\n/** Output image formats the processor can emit. */\nexport type ImageOutputFormat = \"jpeg\" | \"png\" | \"webp\" | \"avif\";\n\n/**\n * Map a requested format string (case-insensitive) to its sharp format id and\n * file extension. Unknown or missing formats fall back to jpeg.\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\n/** Map an image format string to its MIME type. */\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\n/**\n * Apply the chosen output format (and optional quality) to a sharp pipeline.\n * png is lossless so quality is ignored; jpeg/webp/avif honor quality when set,\n * otherwise sharp's per-format defaults apply.\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
+ {"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,5 +1,5 @@
1
- import type { MediaProcessingResult, MediaProcessingSpec } from "@ttt-productions/media-schemas";
2
- import type { ProcessMediaOptions } from "../types.js";
1
+ import type { MediaProcessingResult, MediaProcessingSpec } from '@ttt-productions/media-schemas';
2
+ import type { ProcessMediaOptions } from '../types.js';
3
3
  export declare function processImage(spec: MediaProcessingSpec, ctx: {
4
4
  inputPath: string;
5
5
  outputBasePath: string;
@@ -1 +1 @@
1
- {"version":3,"file":"image-processor.d.ts","sourceRoot":"","sources":["../../src/image/image-processor.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAqC,qBAAqB,EAAE,mBAAmB,EAAE,MAAM,gCAAgC,CAAC;AAMpI,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AA+LvD,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,CA6FhC"}
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"}
@@ -1,29 +1,29 @@
1
- import sharp from "sharp";
2
- import { mkdir } from "node:fs/promises";
3
- import path from "node:path";
4
- import { safeOutputPathFor } from "../utils/safe-path.js";
5
- import { applyImageFormat, mimeForImageFormat, pickFormat } from "./formats.js";
6
- import { computeAspectCropBox, resizeCover, resizeInside, toSharpPosition, } from "./resize.js";
7
- import { IMAGE_DEFAULT_OUTPUT_KEY } from "@ttt-productions/media-schemas";
1
+ import sharp from 'sharp';
2
+ import { mkdir } from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import { safeOutputPathFor } from '../utils/safe-path.js';
5
+ import { applyImageFormat, mimeForImageFormat, pickFormat } from './formats.js';
6
+ import { computeAspectCropBox, resizeCover, resizeInside, toSharpPosition, } from './resize.js';
7
+ import { IMAGE_DEFAULT_OUTPUT_KEY } from '@ttt-productions/media-schemas';
8
8
  function matchMime(accepted, actual) {
9
9
  const a = accepted.trim().toLowerCase();
10
10
  const m = actual.trim().toLowerCase();
11
11
  if (!a)
12
12
  return true;
13
- if (a === "*/*")
13
+ if (a === '*/*')
14
14
  return true;
15
- if (a.endsWith("/*"))
15
+ if (a.endsWith('/*'))
16
16
  return m.startsWith(a.slice(0, -1));
17
17
  return a === m;
18
18
  }
19
19
  function acceptAllowsImage(spec) {
20
20
  const kinds = spec.accept?.kinds?.filter(Boolean) ?? [];
21
- return kinds.length === 0 ? true : kinds.includes("image");
21
+ return kinds.length === 0 ? true : kinds.includes('image');
22
22
  }
23
23
  function acceptAllowsMime(spec, actualMime) {
24
24
  const mimes = spec.accept?.mimes?.filter(Boolean) ?? [];
25
25
  if (mimes.length === 0)
26
- return true; // empty => accept anything
26
+ return true;
27
27
  if (!actualMime)
28
28
  return false;
29
29
  return mimes.some((a) => matchMime(a, actualMime));
@@ -39,23 +39,15 @@ 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
- const input = sharp(inputPath, { failOn: "none" });
43
+ const input = sharp(inputPath, { failOn: 'none' });
52
44
  const meta = await input.metadata();
53
45
  if (!meta.width || !meta.height || meta.width <= 0 || meta.height <= 0) {
54
46
  return {
55
47
  ok: false,
56
48
  error: {
57
- code: "processing_failed",
58
- message: "Unable to read valid image dimensions. File may be corrupt.",
49
+ code: 'processing_failed',
50
+ message: 'Unable to read valid image dimensions. File may be corrupt.',
59
51
  details: { width: meta.width, height: meta.height, format: meta.format, path: inputPath },
60
52
  },
61
53
  };
@@ -64,8 +56,8 @@ async function buildBase(spec, inputPath) {
64
56
  return {
65
57
  ok: false,
66
58
  error: {
67
- code: "unsupported_format",
68
- message: "Unable to determine image format. File may be corrupt.",
59
+ code: 'unsupported_format',
60
+ message: 'Unable to determine image format. File may be corrupt.',
69
61
  details: { path: inputPath },
70
62
  },
71
63
  };
@@ -74,8 +66,8 @@ async function buildBase(spec, inputPath) {
74
66
  return {
75
67
  ok: false,
76
68
  error: {
77
- code: "invalid_mime",
78
- message: "Spec does not allow image uploads.",
69
+ code: 'invalid_mime',
70
+ message: 'Spec does not allow image uploads.',
79
71
  details: { accept: spec.accept },
80
72
  },
81
73
  };
@@ -85,8 +77,8 @@ async function buildBase(spec, inputPath) {
85
77
  return {
86
78
  ok: false,
87
79
  error: {
88
- code: "invalid_mime",
89
- message: "Image mime type is not allowed.",
80
+ code: 'invalid_mime',
81
+ message: 'Image mime type is not allowed.',
90
82
  details: { detectedMime, accept: spec.accept },
91
83
  },
92
84
  };
@@ -94,54 +86,45 @@ 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 {
102
92
  ok: false,
103
93
  error: {
104
- code: "dimensions_mismatch",
105
- message: "Unable to read image dimensions for validation.",
94
+ code: 'dimensions_mismatch',
95
+ message: 'Unable to read image dimensions for validation.',
106
96
  details: { width: w, height: h },
107
97
  },
108
98
  };
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,
133
118
  error: {
134
- code: "dimensions_mismatch",
135
- message: "Image dimensions do not match the required format.",
119
+ code: 'dimensions_mismatch',
120
+ message: 'Image dimensions do not match the required format.',
136
121
  details: { requiredWidth: reqW, requiredHeight: reqH, width: w, height: h },
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);
@@ -151,13 +134,12 @@ async function buildBase(spec, inputPath) {
151
134
  return {
152
135
  ok: false,
153
136
  error: {
154
- code: "aspect_ratio_mismatch",
155
- message: "Image aspect ratio does not match the required format.",
137
+ code: 'aspect_ratio_mismatch',
138
+ message: 'Image aspect ratio does not match the required format.',
156
139
  details: { requiredAspectRatio: reqAspect, aspectRatio: a, width: w, height: h },
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
  }
@@ -167,14 +149,20 @@ export async function processImage(spec, ctx, opts) {
167
149
  const tempFiles = [];
168
150
  try {
169
151
  if (opts?.signal?.aborted) {
170
- return { ok: false, mediaType: "image", error: { code: "processing_canceled", message: "Processing canceled." } };
152
+ return {
153
+ ok: false,
154
+ mediaType: 'image',
155
+ error: { code: 'processing_canceled', message: 'Processing canceled.' },
156
+ };
171
157
  }
172
- const variants = spec.image?.variants?.length ? spec.image.variants : [{ key: IMAGE_DEFAULT_OUTPUT_KEY }];
158
+ const variants = spec.image?.variants?.length
159
+ ? spec.image.variants
160
+ : [{ key: IMAGE_DEFAULT_OUTPUT_KEY }];
173
161
  const outDir = path.dirname(ctx.outputBasePath);
174
162
  await mkdir(outDir, { recursive: true });
175
163
  const baseRes = await buildBase(spec, ctx.inputPath);
176
164
  if (!baseRes.ok) {
177
- return { ok: false, mediaType: "image", error: baseRes.error };
165
+ return { ok: false, mediaType: 'image', error: baseRes.error };
178
166
  }
179
167
  const { base, inputMeta } = baseRes;
180
168
  const outputs = [];
@@ -183,19 +171,23 @@ export async function processImage(spec, ctx, opts) {
183
171
  for (const v of variants) {
184
172
  idx += 1;
185
173
  if (opts?.signal?.aborted) {
186
- return { ok: false, mediaType: "image", error: { code: "processing_canceled", message: "Processing canceled." } };
174
+ return {
175
+ ok: false,
176
+ mediaType: 'image',
177
+ error: { code: 'processing_canceled', message: 'Processing canceled.' },
178
+ };
187
179
  }
188
180
  opts?.onProgress?.({
189
- phase: "process",
181
+ phase: 'process',
190
182
  percent: (idx - 1) / totalVariants,
191
- detail: { step: "image_variant", key: v.key },
183
+ detail: { step: 'image_variant', key: v.key },
192
184
  });
193
185
  const { ext, format } = pickFormat(v.format);
194
186
  const outPath = outputPathFor(ctx.outputBasePath, v.key, ext);
195
187
  tempFiles.push(outPath);
196
188
  let p = base.clone();
197
189
  if (v.crop) {
198
- p = resizeCover(p, v.crop.width, v.crop.height, toSharpPosition(v.crop.gravity ?? "center"));
190
+ p = resizeCover(p, v.crop.width, v.crop.height, toSharpPosition(v.crop.gravity ?? 'center'));
199
191
  }
200
192
  else if (v.maxWidth || v.maxHeight) {
201
193
  p = resizeInside(p, v.maxWidth, v.maxHeight);
@@ -212,11 +204,10 @@ export async function processImage(spec, ctx, opts) {
212
204
  height: info.height,
213
205
  });
214
206
  }
215
- // success: keep outputs
216
207
  tempFiles.length = 0;
217
208
  return {
218
209
  ok: true,
219
- mediaType: "image",
210
+ mediaType: 'image',
220
211
  outputs,
221
212
  meta: {
222
213
  mime: mimeForImageFormat(inputMeta.format),
@@ -228,17 +219,17 @@ export async function processImage(spec, ctx, opts) {
228
219
  catch (e) {
229
220
  return {
230
221
  ok: false,
231
- mediaType: "image",
222
+ mediaType: 'image',
232
223
  error: {
233
- code: "processing_failed",
234
- message: e?.message ? String(e.message) : "Image processing failed",
224
+ code: 'processing_failed',
225
+ message: e?.message ? String(e.message) : 'Image processing failed',
235
226
  details: { name: e?.name },
236
227
  },
237
228
  };
238
229
  }
239
230
  finally {
240
231
  if (tempFiles.length) {
241
- const { rm } = await import("node:fs/promises");
232
+ const { rm } = await import('node:fs/promises');
242
233
  await Promise.all(tempFiles.map((f) => rm(f, { force: true }).catch(() => { })));
243
234
  }
244
235
  }
@@ -1 +1 @@
1
- {"version":3,"file":"image-processor.js","sourceRoot":"","sources":["../../src/image/image-processor.ts"],"names":[],"mappings":"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","sourcesContent":["import type { MediaOutput, MediaProcessingError, MediaProcessingResult, MediaProcessingSpec } 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 }\n | { 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 { ok: false, mediaType: \"image\", error: { code: \"processing_canceled\", message: \"Processing canceled.\" } };\n }\n\n const variants = spec.image?.variants?.length ? spec.image.variants : [{ 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 { ok: false, mediaType: \"image\", error: { code: \"processing_canceled\", message: \"Processing canceled.\" } };\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(p, v.crop.width, v.crop.height, toSharpPosition((v.crop.gravity as Gravity) ?? \"center\"));\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":"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;IACpC,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,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,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,IAAI,IAAI,GAAG,KAAK,CAAC,SAAS,CAAC,CAAC;IAE5B,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,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,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,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,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,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,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,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;\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"]}
@@ -1,22 +1,8 @@
1
- import type { Sharp, Gravity as SharpGravity } from "sharp";
2
- /** Logical crop/resize gravity, mapped to sharp positions by toSharpPosition. */
3
- export type Gravity = "center" | "top" | "bottom" | "left" | "right";
4
- /** Map a logical gravity to a sharp position string. Defaults to centre. */
1
+ import type { Sharp, Gravity as SharpGravity } from 'sharp';
2
+ export type Gravity = 'center' | 'top' | 'bottom' | 'left' | 'right';
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":"AACA,OAAO,KAAK,EAAE,KAAK,EAAE,OAAO,IAAI,YAAY,EAAE,MAAM,OAAO,CAAC;AAE5D,iFAAiF;AACjF,MAAM,MAAM,OAAO,GAAG,QAAQ,GAAG,KAAK,GAAG,QAAQ,GAAG,MAAM,GAAG,OAAO,CAAC;AAErE,4EAA4E;AAC5E,wBAAgB,eAAe,CAAC,CAAC,CAAC,EAAE,OAAO,GAAG,YAAY,CAczD;AAED;;;GAGG;AACH,wBAAgB,WAAW,CACzB,QAAQ,EAAE,KAAK,EACf,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,EACd,QAAQ,GAAE,YAAuB,GAChC,KAAK,CAMP;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAC1B,QAAQ,EAAE,KAAK,EACf,QAAQ,CAAC,EAAE,MAAM,EACjB,SAAS,CAAC,EAAE,MAAM,GACjB,KAAK,CAKP;AAED;;;GAGG;AACH,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,CAiB9D"}
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"}
@@ -1,54 +1,39 @@
1
- /** Map a logical gravity to a sharp position string. Defaults to centre. */
2
1
  export function toSharpPosition(g) {
3
2
  switch (g) {
4
- case "top":
5
- return "north";
6
- case "bottom":
7
- return "south";
8
- case "left":
9
- return "west";
10
- case "right":
11
- return "east";
12
- case "center":
3
+ case 'top':
4
+ return 'north';
5
+ case 'bottom':
6
+ return 'south';
7
+ case 'left':
8
+ return 'west';
9
+ case 'right':
10
+ return 'east';
11
+ case 'center':
13
12
  default:
14
- return "centre";
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
- export function resizeCover(pipeline, width, height, position = "centre") {
16
+ export function resizeCover(pipeline, width, height, position = 'centre') {
22
17
  return pipeline.resize(width, height, {
23
- fit: "cover",
18
+ fit: 'cover',
24
19
  position,
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
- fit: "inside",
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));
@@ -1 +1 @@
1
- {"version":3,"file":"resize.js","sourceRoot":"","sources":["../../src/image/resize.ts"],"names":[],"mappings":"AAMA,4EAA4E;AAC5E,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;;;GAGG;AACH,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;;;GAGG;AACH,MAAM,UAAU,YAAY,CAC1B,QAAe,EACf,QAAiB,EACjB,SAAkB;IAElB,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;;;GAGG;AACH,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,2BAA2B;QAC3B,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,MAAM,GAAG,YAAY,CAAC,CAAC,CAAC;IACzD,CAAC;SAAM,CAAC;QACN,4BAA4B;QAC5B,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":["// sharp resize helpers extracted from the image-processor\nimport type { Sharp, Gravity as SharpGravity } from \"sharp\";\n\n/** Logical crop/resize gravity, mapped to sharp positions by toSharpPosition. */\nexport type Gravity = \"center\" | \"top\" | \"bottom\" | \"left\" | \"right\";\n\n/** Map a logical gravity to a sharp position string. Defaults to centre. */\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\n/**\n * Cover-resize to an exact width/height (crop to fill), never enlarging beyond\n * the source. Used for explicit crops and required-size normalization.\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\n/**\n * Fit-inside resize bounded by maxWidth/maxHeight (preserves aspect ratio),\n * never enlarging beyond the source. Either bound may be omitted.\n */\nexport function resizeInside(\n pipeline: Sharp,\n maxWidth?: number,\n maxHeight?: number\n): Sharp {\n return pipeline.resize(maxWidth ?? null, maxHeight ?? null, {\n fit: \"inside\",\n withoutEnlargement: true,\n });\n}\n\n/**\n * Compute a centered crop box that brings (width x height) to targetAspect,\n * shrinking only the longer dimension. Returns sharp .extract() coordinates.\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 // too wide -> reduce width\n cropW = Math.max(1, Math.floor(height * targetAspect));\n } else {\n // too tall -> reduce height\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"]}
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"]}
package/dist/index.d.ts CHANGED
@@ -1,17 +1,20 @@
1
- export * from "./image/index.js";
2
- export * from "./image/image-processor.js";
3
- export * from "./video/index.js";
4
- export * from "./video/video-processor.js";
5
- export * from "./process-media.js";
6
- export * from "./workspace/temp.js";
7
- export * from "./audio/index.js";
8
- export * from "./audio/audio-processor.js";
9
- export * from "./io/types.js";
10
- export * from "./run-pipeline.js";
11
- export * from "./inspection/inspect-media.js";
12
- export * from "./duration/media-duration.js";
13
- export * from "./io/fs.js";
14
- export * from "./io/local-input.js";
15
- export * from "./moderation/types.js";
16
- export * from "./moderation/merge.js";
1
+ export * from './image/image-processor.js';
2
+ export * from './image/formats.js';
3
+ export * from './image/resize.js';
4
+ export * from './video/video-processor.js';
5
+ export * from './video/probe.js';
6
+ export * from './video/ffmpeg.js';
7
+ export * from './process-media.js';
8
+ export type { MediaPipelineProgress, MediaProcessors, ProcessMediaContext, ProcessMediaOptions, } from './types.js';
9
+ export * from './workspace/temp.js';
10
+ export * from './audio/audio-processor.js';
11
+ export * from './audio/probe.js';
12
+ export * from './io/types.js';
13
+ export * from './run-pipeline.js';
14
+ export * from './inspection/inspect-media.js';
15
+ export * from './duration/media-duration.js';
16
+ export * from './io/fs.js';
17
+ export * from './io/local-input.js';
18
+ export * from './moderation/types.js';
19
+ export * from './moderation/merge.js';
17
20
  //# sourceMappingURL=index.d.ts.map