@visulima/storage 1.0.0-alpha.1 → 1.0.0-alpha.11

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 (225) hide show
  1. package/CHANGELOG.md +170 -0
  2. package/LICENSE.md +6 -6
  3. package/README.md +33 -14
  4. package/dist/adapter/nuxt/module.d.ts +39 -0
  5. package/dist/adapter/nuxt/module.js +93 -0
  6. package/dist/adapter/nuxt/package.json +20 -0
  7. package/dist/handler/base/base-handler-core.d.ts +91 -0
  8. package/dist/handler/base/base-handler-fetch.d.ts +76 -0
  9. package/dist/handler/base/base-handler-node.d.ts +137 -0
  10. package/dist/handler/http/fetch/index.d.ts +4 -0
  11. package/dist/handler/http/fetch/index.js +4 -0
  12. package/dist/handler/http/hono/index.d.ts +67 -0
  13. package/dist/handler/http/hono/index.js +65 -0
  14. package/dist/handler/http/nextjs/index.d.ts +61 -0
  15. package/dist/handler/http/nextjs/index.js +37 -0
  16. package/dist/handler/http/node/index.d.ts +4 -0
  17. package/dist/handler/http/node/index.js +8 -0
  18. package/dist/handler/http/solid-start/index.d.ts +60 -0
  19. package/dist/handler/http/solid-start/index.js +44 -0
  20. package/dist/handler/multipart/multipart-base.d.ts +84 -0
  21. package/dist/handler/multipart/multipart-fetch.d.ts +64 -0
  22. package/dist/handler/multipart/multipart.d.ts +49 -0
  23. package/dist/handler/rest/rest-base.d.ts +106 -0
  24. package/dist/handler/rest/rest-fetch.d.ts +88 -0
  25. package/dist/handler/rest/rest.d.ts +93 -0
  26. package/dist/handler/tus/tus-base.d.ts +152 -0
  27. package/dist/handler/tus/tus-fetch.d.ts +72 -0
  28. package/dist/handler/tus/tus.d.ts +78 -0
  29. package/dist/handler/types.d.ts +53 -0
  30. package/dist/handler/utils/request-parser.d.ts +72 -0
  31. package/dist/handler/utils/response-builder.d.ts +83 -0
  32. package/dist/handler/utils/storage-utils.d.ts +10 -0
  33. package/dist/handler/utils/stream-utils.d.ts +29 -0
  34. package/dist/handler/utils/upload-handlers.d.ts +76 -0
  35. package/dist/index.d.ts +17 -0
  36. package/dist/index.js +13 -0
  37. package/dist/metrics/index.d.ts +2 -0
  38. package/dist/metrics/no-op-metrics.d.ts +15 -0
  39. package/dist/metrics/opentelemetry-metrics.d.ts +55 -0
  40. package/dist/openapi/index.d.ts +5 -0
  41. package/dist/openapi/index.js +5 -0
  42. package/dist/openapi/rest.d.ts +7 -0
  43. package/dist/openapi/shared.d.ts +13 -0
  44. package/dist/openapi/transform.d.ts +3 -0
  45. package/dist/openapi/tus.d.ts +7 -0
  46. package/dist/openapi/xhr.d.ts +7 -0
  47. package/dist/packem_shared/AwsLightFile-tTneXZgG.js +11 -0
  48. package/dist/packem_shared/AwsLightMetaStorage-D9qWQt6q.js +4 -0
  49. package/dist/packem_shared/AwsLightStorage-CEJTYBVv.js +133 -0
  50. package/dist/packem_shared/AzureFile-CesgFzps.js +8 -0
  51. package/dist/packem_shared/AzureSMetaStorage-BEbXX9Uk.js +89 -0
  52. package/dist/packem_shared/AzureStorage-CH6nvQx8.js +357 -0
  53. package/dist/packem_shared/BaseTransformer-9965D4pb.js +83 -0
  54. package/dist/packem_shared/DiskStorage-DziI2mwX.js +12 -0
  55. package/dist/packem_shared/DiskStorageWithChecksum-B_XY8_cO.js +240 -0
  56. package/dist/packem_shared/ERRORS-D0apMqnc.js +97 -0
  57. package/dist/packem_shared/File-Bb3P23dr.js +69 -0
  58. package/dist/packem_shared/GCSConfig-vPP22kN6.js +11 -0
  59. package/dist/packem_shared/GCSFile-BIEunhAN.js +8 -0
  60. package/dist/packem_shared/GCSMetaStorage-FypK8Tn1.js +7 -0
  61. package/dist/packem_shared/GCStorage-CasTo9rF.js +408 -0
  62. package/dist/packem_shared/LocalMetaStorage-BnllcjV9.js +7 -0
  63. package/dist/packem_shared/MediaTransformer-BErvw2vE.js +1196 -0
  64. package/dist/packem_shared/MetaStorage-pECeFOad.js +46 -0
  65. package/dist/packem_shared/Metadata-DRLXeJ0F.js +89 -0
  66. package/dist/packem_shared/Multipart-CDDlDOs7.js +163 -0
  67. package/dist/packem_shared/Multipart-vxBL2VXm.js +128 -0
  68. package/dist/packem_shared/NetlifyBlobFile-CXzyjqrD.js +14 -0
  69. package/dist/packem_shared/NetlifyBlobMetaStorage-DIkZ2umW.js +9 -0
  70. package/dist/packem_shared/NetlifyBlobStorage-DEpx_4q-.js +376 -0
  71. package/dist/packem_shared/NoOpMetrics-DhAk5rXc.js +10 -0
  72. package/dist/packem_shared/OpenTelemetryMetrics-BnxhqIaH.js +67 -0
  73. package/dist/packem_shared/Rest-CEj3EhMj.js +228 -0
  74. package/dist/packem_shared/Rest-D14vM7aJ.js +267 -0
  75. package/dist/packem_shared/S3File-DZiyk9Qt.js +11 -0
  76. package/dist/packem_shared/S3MetaStorage-yT4CznlV.js +77 -0
  77. package/dist/packem_shared/S3Storage-B0scgwPa.js +317 -0
  78. package/dist/packem_shared/TUS_RESUMABLE-Dtfsffnj.js +434 -0
  79. package/dist/packem_shared/Tus-Cjz87bpz.js +200 -0
  80. package/dist/packem_shared/Tus-WOVg-YnB.js +195 -0
  81. package/dist/packem_shared/ValidationError-BfF1aE4h.js +26 -0
  82. package/dist/packem_shared/VercelBlobFile-BCg4aTEq.js +18 -0
  83. package/dist/packem_shared/VercelBlobMetaStorage-c0M-TvAR.js +9 -0
  84. package/dist/packem_shared/VercelBlobStorage-wOYxv8F7.js +261 -0
  85. package/dist/packem_shared/_commonjsHelpers-BqLXS_qQ.js +5 -0
  86. package/dist/packem_shared/aws-light-meta-storage-Ba2b_h-o.js +447 -0
  87. package/dist/packem_shared/backblaze-BlMnIcBC.js +20 -0
  88. package/dist/packem_shared/base-handler-core-BoAqTyz7.js +303 -0
  89. package/dist/packem_shared/base-handler-fetch-CinvR65S.js +291 -0
  90. package/dist/packem_shared/base-handler-node-c02H0AxN.js +731 -0
  91. package/dist/packem_shared/cache-B88MXQ_2.js +18 -0
  92. package/dist/packem_shared/cloudflare-Bi1q8wXE.js +21 -0
  93. package/dist/packem_shared/defaultCloudStorageFileNameValidation-CGoYrGB2.js +11 -0
  94. package/dist/packem_shared/digitalOcean-CWQRJM3L.js +21 -0
  95. package/dist/packem_shared/disk-storage-DeivH-J5.js +816 -0
  96. package/dist/packem_shared/gcs-meta-storage-B0Baj0Xr.js +159 -0
  97. package/dist/packem_shared/has-content-CY66ehMK.js +3 -0
  98. package/dist/packem_shared/headers-YLRp1dLo.js +1318 -0
  99. package/dist/packem_shared/is-expired-CTThU1q5.js +8 -0
  100. package/dist/packem_shared/isRetryableError-Dycp7127.js +82 -0
  101. package/dist/packem_shared/isValidMediaType-BeDgiObq.js +35 -0
  102. package/dist/packem_shared/local-meta-storage-BlOk6m7F.js +647 -0
  103. package/dist/packem_shared/minio-C2YBZQOw.js +22 -0
  104. package/dist/packem_shared/multipart-base-C994naBi.js +102 -0
  105. package/dist/packem_shared/part-match-BMNqHDYD.js +96 -0
  106. package/dist/packem_shared/path-aV9lMdcn-DMU5H1-h.js +162 -0
  107. package/dist/packem_shared/response-builder-BtnRiBUI.js +45 -0
  108. package/dist/packem_shared/rest-base-D9URIFkc.js +457 -0
  109. package/dist/packem_shared/restOpenApiSpec-D1zbmid7.js +800 -0
  110. package/dist/packem_shared/s3-base-storage-CG21uusd.js +513 -0
  111. package/dist/packem_shared/sharedGet-DAyJfjhl.js +1018 -0
  112. package/dist/packem_shared/storage-CpJpqLdn.js +1184 -0
  113. package/dist/packem_shared/tigris-Hac8TKnX.js +21 -0
  114. package/dist/packem_shared/transformOpenApiSpec-BC0r6PzI.js +975 -0
  115. package/dist/packem_shared/tusOpenApiSpec-BLIjKf0s.js +740 -0
  116. package/dist/packem_shared/update-size-CCGm6i1J.js +8 -0
  117. package/dist/packem_shared/validator-DuO17wFF.js +79 -0
  118. package/dist/packem_shared/waitForStorage-Cscw85sx.js +18 -0
  119. package/dist/packem_shared/wasabi-DIyflHSd.js +21 -0
  120. package/dist/packem_shared/xhrOpenApiSpec-kItrSAsc.js +264 -0
  121. package/dist/storage/aws/clients/backblaze.d.ts +12 -0
  122. package/dist/storage/aws/clients/cloudflare.d.ts +13 -0
  123. package/dist/storage/aws/clients/digital-ocean.d.ts +12 -0
  124. package/dist/storage/aws/clients/index.d.ts +6 -0
  125. package/dist/storage/aws/clients/index.js +6 -0
  126. package/dist/storage/aws/clients/minio.d.ts +13 -0
  127. package/dist/storage/aws/clients/tigris.d.ts +12 -0
  128. package/dist/storage/aws/clients/types.d.ts +95 -0
  129. package/dist/storage/aws/clients/wasabi.d.ts +12 -0
  130. package/dist/storage/aws/index.d.ts +4 -0
  131. package/dist/storage/aws/index.js +3 -0
  132. package/dist/storage/aws/s3-base-storage.d.ts +248 -0
  133. package/dist/storage/aws/s3-client-adapter.d.ts +110 -0
  134. package/dist/storage/aws/s3-file.d.ts +10 -0
  135. package/dist/storage/aws/s3-meta-storage.d.ts +15 -0
  136. package/dist/storage/aws/s3-storage.d.ts +70 -0
  137. package/dist/storage/aws/types.d.ts +119 -0
  138. package/dist/storage/aws-light/aws-light-api-adapter.d.ts +130 -0
  139. package/dist/storage/aws-light/aws-light-file.d.ts +10 -0
  140. package/dist/storage/aws-light/aws-light-meta-storage.d.ts +19 -0
  141. package/dist/storage/aws-light/aws-light-storage.d.ts +65 -0
  142. package/dist/storage/aws-light/index.d.ts +4 -0
  143. package/dist/storage/aws-light/index.js +3 -0
  144. package/dist/storage/aws-light/types.d.ts +37 -0
  145. package/dist/storage/azure/azure-file.d.ts +6 -0
  146. package/dist/storage/azure/azure-meta-storage.d.ts +15 -0
  147. package/dist/storage/azure/azure-storage.d.ts +69 -0
  148. package/dist/storage/azure/index.d.ts +4 -0
  149. package/dist/storage/azure/index.js +3 -0
  150. package/dist/storage/azure/types.d.ts +62 -0
  151. package/dist/storage/gcs/fetch-error.d.ts +11 -0
  152. package/dist/storage/gcs/gcs-config.d.ts +2 -0
  153. package/dist/storage/gcs/gcs-file.d.ts +6 -0
  154. package/dist/storage/gcs/gcs-meta-storage.d.ts +27 -0
  155. package/dist/storage/gcs/gcs-storage.d.ts +91 -0
  156. package/dist/storage/gcs/index.d.ts +5 -0
  157. package/dist/storage/gcs/index.js +4 -0
  158. package/dist/storage/gcs/types.d.ts +55 -0
  159. package/dist/storage/gcs/utils.d.ts +7 -0
  160. package/dist/storage/local/disk-storage-with-checksum.d.ts +15 -0
  161. package/dist/storage/local/disk-storage.d.ts +135 -0
  162. package/dist/storage/local/index.d.ts +4 -0
  163. package/dist/storage/local/index.js +3 -0
  164. package/dist/storage/local/local-meta-storage.d.ts +32 -0
  165. package/dist/storage/meta-storage.d.ts +30 -0
  166. package/dist/storage/netlify-blob/index.d.ts +4 -0
  167. package/dist/storage/netlify-blob/index.js +3 -0
  168. package/dist/storage/netlify-blob/netlify-blob-file.d.ts +12 -0
  169. package/dist/storage/netlify-blob/netlify-blob-meta-storage.d.ts +7 -0
  170. package/dist/storage/netlify-blob/netlify-blob-storage.d.ts +93 -0
  171. package/dist/storage/netlify-blob/types.d.ts +30 -0
  172. package/dist/storage/storage.d.ts +353 -0
  173. package/dist/storage/types.d.ts +221 -0
  174. package/dist/storage/utils/file/file.d.ts +25 -0
  175. package/dist/storage/utils/file/get-file-status.d.ts +9 -0
  176. package/dist/storage/utils/file/has-content.d.ts +9 -0
  177. package/dist/storage/utils/file/index.d.ts +10 -0
  178. package/dist/storage/utils/file/is-expired.d.ts +8 -0
  179. package/dist/storage/utils/file/metadata.d.ts +18 -0
  180. package/dist/storage/utils/file/part-match.d.ts +11 -0
  181. package/dist/storage/utils/file/types.d.ts +37 -0
  182. package/dist/storage/utils/file/update-metadata.d.ts +10 -0
  183. package/dist/storage/utils/file/update-size.d.ts +10 -0
  184. package/dist/storage/vercel-blob/index.d.ts +4 -0
  185. package/dist/storage/vercel-blob/index.js +3 -0
  186. package/dist/storage/vercel-blob/types.d.ts +32 -0
  187. package/dist/storage/vercel-blob/vercel-blob-file.d.ts +16 -0
  188. package/dist/storage/vercel-blob/vercel-blob-meta-storage.d.ts +7 -0
  189. package/dist/storage/vercel-blob/vercel-blob-storage.d.ts +76 -0
  190. package/dist/transformer/audio-transformer.d.ts +128 -0
  191. package/dist/transformer/audio-transformer.js +278 -0
  192. package/dist/transformer/base-transformer.d.ts +63 -0
  193. package/dist/transformer/image-transformer.d.ts +571 -0
  194. package/dist/transformer/image-transformer.js +1100 -0
  195. package/dist/transformer/index.d.ts +5 -0
  196. package/dist/transformer/index.js +4 -0
  197. package/dist/transformer/media-transformer.d.ts +332 -0
  198. package/dist/transformer/types.d.ts +652 -0
  199. package/dist/transformer/utils.d.ts +33 -0
  200. package/dist/transformer/validation-error.d.ts +23 -0
  201. package/dist/transformer/video-transformer.d.ts +145 -0
  202. package/dist/transformer/video-transformer.js +310 -0
  203. package/dist/utils/cache.d.ts +32 -0
  204. package/dist/utils/chunked-upload.d.ts +65 -0
  205. package/dist/utils/detect-file-type.d.ts +28 -0
  206. package/dist/utils/errors.d.ts +74 -0
  207. package/dist/utils/file-path-url-matcher.d.ts +18 -0
  208. package/dist/utils/headers.d.ts +111 -0
  209. package/dist/utils/http.d.ts +92 -0
  210. package/dist/utils/locker.d.ts +26 -0
  211. package/dist/utils/pipes/stream-checksum.d.ts +47 -0
  212. package/dist/utils/pipes/stream-length.d.ts +22 -0
  213. package/dist/utils/primitives/get-last-one.d.ts +3 -0
  214. package/dist/utils/primitives/is-record.d.ts +2 -0
  215. package/dist/utils/primitives/map-values.d.ts +9 -0
  216. package/dist/utils/primitives/pick.d.ts +2 -0
  217. package/dist/utils/primitives/to-milliseconds.d.ts +12 -0
  218. package/dist/utils/primitives/to-seconds.d.ts +12 -0
  219. package/dist/utils/range-checksum.d.ts +33 -0
  220. package/dist/utils/range-hasher.d.ts +46 -0
  221. package/dist/utils/retry.d.ts +64 -0
  222. package/dist/utils/types.d.ts +108 -0
  223. package/dist/utils/validation-error.d.ts +22 -0
  224. package/dist/utils/validator.d.ts +36 -0
  225. package/package.json +9 -9
@@ -0,0 +1,1196 @@
1
+ import { createHash } from 'node:crypto';
2
+ import mime from 'mime';
3
+ import { N as NoOpCache } from './cache-B88MXQ_2.js';
4
+ import { isKnownContentType } from './isValidMediaType-BeDgiObq.js';
5
+ import ValidationError from './ValidationError-BfF1aE4h.js';
6
+
7
+ class MediaTransformer {
8
+ /**
9
+ * Creates a new MediaTransformer instance.
10
+ * @param storage The storage backend for retrieving and storing media files.
11
+ * @param config Configuration options for the media transformer including transformer classes and settings.
12
+ * @throws Error if no transformer classes are provided in the configuration.
13
+ */
14
+ constructor(storage, config = {}) {
15
+ this.storage = storage;
16
+ this.logger = config.logger || this.storage.logger;
17
+ this.config = {
18
+ audioBitrate: 128e3,
19
+ // 128 kbps
20
+ audioCodec: "aac",
21
+ cache: config.cache ?? new NoOpCache(),
22
+ cacheTtl: 3600,
23
+ // 1 hour
24
+ maxAudioSize: 100 * 1024 * 1024,
25
+ // 100MB
26
+ maxImageSize: 10 * 1024 * 1024,
27
+ // 10MB
28
+ maxVideoSize: 500 * 1024 * 1024,
29
+ // 500MB
30
+ saveTransformedFiles: false,
31
+ // Save transformed files to storage
32
+ supportedAudioFormats: [],
33
+ supportedImageFormats: [],
34
+ supportedVideoFormats: [],
35
+ videoBitrate: 2e6,
36
+ // 2 Mbps
37
+ videoCodec: "avc",
38
+ ...config
39
+ };
40
+ if (config.ImageTransformer) {
41
+ if (!Array.isArray(this.config.supportedImageFormats) || this.config.supportedImageFormats.length === 0) {
42
+ this.config.supportedImageFormats = ["jpeg", "png", "webp", "avif", "tiff", "gif", "svg"];
43
+ }
44
+ this.imageTransformer = new config.ImageTransformer(this.storage, {
45
+ cache: this.config.cache,
46
+ cacheTtl: this.config.cacheTtl,
47
+ logger: this.logger,
48
+ maxImageSize: this.config.maxImageSize,
49
+ supportedFormats: this.config.supportedImageFormats
50
+ });
51
+ }
52
+ if (config.VideoTransformer) {
53
+ if (!Array.isArray(this.config.supportedVideoFormats) || this.config.supportedVideoFormats.length === 0) {
54
+ this.config.supportedVideoFormats = ["mp4", "webm", "mkv", "avi", "mov", "flv", "wmv"];
55
+ }
56
+ this.videoTransformer = new config.VideoTransformer(this.storage, {
57
+ cache: this.config.cache,
58
+ cacheTtl: this.config.cacheTtl,
59
+ defaultBitrate: this.config.videoBitrate,
60
+ defaultCodec: this.config.videoCodec,
61
+ logger: this.logger,
62
+ maxVideoSize: this.config.maxVideoSize,
63
+ supportedFormats: this.config.supportedVideoFormats
64
+ });
65
+ }
66
+ if (config.AudioTransformer) {
67
+ if (!Array.isArray(this.config.supportedAudioFormats) || this.config.supportedAudioFormats.length === 0) {
68
+ this.config.supportedAudioFormats = ["mp3", "wav", "ogg", "aac", "flac", "m4a", "wma", "aiff"];
69
+ }
70
+ this.audioTransformer = new config.AudioTransformer(this.storage, {
71
+ cache: this.config.cache,
72
+ cacheTtl: this.config.cacheTtl,
73
+ defaultBitrate: this.config.audioBitrate,
74
+ defaultCodec: this.config.audioCodec,
75
+ logger: this.logger,
76
+ maxAudioSize: this.config.maxAudioSize,
77
+ supportedFormats: this.config.supportedAudioFormats
78
+ });
79
+ }
80
+ if (config.AudioTransformer === void 0 && config.ImageTransformer === void 0 && config.VideoTransformer === void 0) {
81
+ throw new Error(
82
+ "MediaTransformer initialization failed: No transformers are configured. At least one transformer (AudioTransformer, ImageTransformer, or VideoTransformer) must be provided in the configuration to enable media processing functionality."
83
+ );
84
+ }
85
+ }
86
+ storage;
87
+ imageTransformer;
88
+ videoTransformer;
89
+ audioTransformer;
90
+ config;
91
+ logger;
92
+ supportedFormats() {
93
+ return [...this.config.supportedAudioFormats, ...this.config.supportedImageFormats, ...this.config.supportedVideoFormats];
94
+ }
95
+ /**
96
+ * Handles media transformation based on query parameters.
97
+ * @param fileId File identifier.
98
+ * @param query Query parameters for transformation.
99
+ * @returns Unified transformation result.
100
+ */
101
+ async handle(fileId, query) {
102
+ const parsedQuery = this.parseQuery(query);
103
+ const fileQuery = { id: fileId };
104
+ const file = await this.storage.get(fileQuery);
105
+ const mediaType = this.detectMediaType(file.contentType);
106
+ this.validateQueryParameters(parsedQuery, mediaType);
107
+ if (this.config.saveTransformedFiles && this.hasTransformations(parsedQuery)) {
108
+ const transformedFileId = this.generateTransformedFileId(fileId, parsedQuery, mediaType);
109
+ try {
110
+ const transformedFile = await this.storage.get({ id: transformedFileId });
111
+ this.logger?.debug?.(`Serving cached transformed file: ${transformedFileId}`);
112
+ return this.createMediaTransformResult(transformedFile, mediaType, file);
113
+ } catch {
114
+ this.logger?.debug?.(`Transformed file not found, processing: ${transformedFileId}`);
115
+ }
116
+ }
117
+ let result;
118
+ switch (mediaType) {
119
+ case "audio": {
120
+ if (!this.audioTransformer) {
121
+ throw new Error(
122
+ "Audio transformer not available. Please install audio transformation dependencies or provide an audio transformer instance."
123
+ );
124
+ }
125
+ result = await this.handleAudioTransformation(fileId, parsedQuery);
126
+ break;
127
+ }
128
+ case "image": {
129
+ if (!this.imageTransformer) {
130
+ throw new Error(
131
+ "Image transformer not available. Please install image transformation dependencies or provide an image transformer instance."
132
+ );
133
+ }
134
+ result = await this.handleImageTransformation(fileId, parsedQuery);
135
+ break;
136
+ }
137
+ case "video": {
138
+ if (!this.videoTransformer) {
139
+ throw new Error(
140
+ "Video transformer not available. Please install video transformation dependencies or provide a video transformer instance."
141
+ );
142
+ }
143
+ result = await this.handleVideoTransformation(fileId, parsedQuery);
144
+ break;
145
+ }
146
+ default: {
147
+ throw new Error(`Unsupported media type: ${file.contentType}`);
148
+ }
149
+ }
150
+ if (this.config.saveTransformedFiles && this.hasTransformations(parsedQuery)) {
151
+ await this.saveTransformedFile(result, fileId, parsedQuery, mediaType);
152
+ }
153
+ return result;
154
+ }
155
+ /**
156
+ * Fetches media transformation with URL query string support.
157
+ * @param fileId File identifier.
158
+ * @param queryString URL query string (e.g., "width=800&height=600&format=webp").
159
+ * @returns Unified transformation result.
160
+ */
161
+ async fetch(fileId, queryString) {
162
+ const query = new URLSearchParams(queryString);
163
+ return this.handle(fileId, query);
164
+ }
165
+ /**
166
+ * Clears cache for a specific file across all transformers.
167
+ * @param fileId Optional file identifier to clear cache for. If omitted, clears all cache.
168
+ */
169
+ clearCache(fileId) {
170
+ this.imageTransformer?.clearCache(fileId);
171
+ this.videoTransformer?.clearCache(fileId);
172
+ this.audioTransformer?.clearCache(fileId);
173
+ }
174
+ /**
175
+ * Clears all saved transformed files from storage.
176
+ * @remarks This is a maintenance operation and may take time for large numbers of files.
177
+ */
178
+ async clearSavedTransformedFiles() {
179
+ if (!this.config.saveTransformedFiles) {
180
+ return;
181
+ }
182
+ try {
183
+ this.logger?.warn?.("clearSavedTransformedFiles requires custom storage implementation to find and delete transformed files");
184
+ } catch (error) {
185
+ this.logger?.error?.(`Failed to clear saved transformed files: ${error}`);
186
+ }
187
+ }
188
+ /**
189
+ * Clears saved transformed files for a specific original file.
190
+ * @param originalFileId Original file identifier to clear transformed files for.
191
+ */
192
+ async clearSavedTransformedFilesForFile(originalFileId) {
193
+ if (!this.config.saveTransformedFiles) {
194
+ return;
195
+ }
196
+ try {
197
+ this.logger?.warn?.("clearSavedTransformedFilesForFile requires custom storage implementation");
198
+ } catch (error) {
199
+ this.logger?.error?.(`Failed to clear saved transformed files for ${originalFileId}: ${error}`);
200
+ }
201
+ }
202
+ /**
203
+ * Gets cache statistics (combined from all transformers).
204
+ * @returns Cache statistics for audio, image, and video transformers.
205
+ */
206
+ getCacheStats() {
207
+ const stats = {
208
+ audio: this.audioTransformer?.getCacheStats(),
209
+ image: this.imageTransformer?.getCacheStats(),
210
+ video: this.videoTransformer?.getCacheStats()
211
+ };
212
+ if (stats.image === void 0) delete stats.image;
213
+ if (stats.video === void 0) delete stats.video;
214
+ if (stats.audio === void 0) delete stats.audio;
215
+ return stats;
216
+ }
217
+ /**
218
+ * Validates query parameters for the given media type.
219
+ * @param query The query parameters to validate.
220
+ * @param mediaType The media type being validated ('image', 'video', or 'audio').
221
+ * @throws {ValidationError} When invalid parameters are provided.
222
+ */
223
+ validateQueryParameters(query, mediaType) {
224
+ switch (mediaType) {
225
+ case "audio": {
226
+ this.validateAudioQueryParameters(query);
227
+ break;
228
+ }
229
+ case "image": {
230
+ this.validateImageQueryParameters(query);
231
+ break;
232
+ }
233
+ case "video": {
234
+ this.validateVideoQueryParameters(query);
235
+ break;
236
+ }
237
+ }
238
+ }
239
+ /**
240
+ * Validates query parameters for image transformations. Checks for invalid video/audio-only parameters, invalid fit values, invalid rotation angles, invalid formats, and incomplete crop parameter sets.
241
+ * @param query Query parameters to validate.
242
+ * @throws {ValidationError} When validation fails.
243
+ */
244
+ // eslint-disable-next-line class-methods-use-this
245
+ validateImageQueryParameters(query) {
246
+ const allowedParameters = /* @__PURE__ */ new Set([
247
+ "alphaQuality",
248
+ "angle",
249
+ "background",
250
+ "bandbool",
251
+ "blur",
252
+ "boolean",
253
+ "brightness",
254
+ "channel",
255
+ "clahe",
256
+ "colourspace",
257
+ "compressionLevel",
258
+ "convolve",
259
+ "cropHeight",
260
+ "cropWidth",
261
+ "delay",
262
+ "dilate",
263
+ "effort",
264
+ "erode",
265
+ "extractChannel",
266
+ "fastShrinkOnLoad",
267
+ "fit",
268
+ "flatten",
269
+ "flip",
270
+ "flop",
271
+ "format",
272
+ "frameRate",
273
+ "gamma",
274
+ "grayscale",
275
+ "greyscale",
276
+ "height",
277
+ "hue",
278
+ "joinChannel",
279
+ "kernel",
280
+ "left",
281
+ "lightness",
282
+ "linear",
283
+ "loop",
284
+ "maxSlope",
285
+ "median",
286
+ "modulate",
287
+ "negate",
288
+ "normalise",
289
+ "normalize",
290
+ "position",
291
+ "quality",
292
+ "recombine",
293
+ "saturation",
294
+ "sharpen",
295
+ "threshold",
296
+ "tint",
297
+ "top",
298
+ "unflatten",
299
+ "width",
300
+ "withoutEnlargement",
301
+ "withoutReduction"
302
+ ]);
303
+ const videoAudioOnlyParameters = /* @__PURE__ */ new Set(["bitrate", "codec", "frameRate", "keyFrameInterval", "numberOfChannels", "sampleRate"]);
304
+ const invalidParameters = [];
305
+ for (const parameter of videoAudioOnlyParameters) {
306
+ if (query[parameter] !== void 0) {
307
+ invalidParameters.push(parameter);
308
+ }
309
+ }
310
+ if (invalidParameters.length > 0) {
311
+ const sortedInvalidParameters = [...invalidParameters].toSorted();
312
+ throw new ValidationError(
313
+ `Invalid query parameters for image transformation: ${sortedInvalidParameters.join(", ")}. Images support: ${[...allowedParameters].toSorted().join(", ")}. Video/audio parameters (${sortedInvalidParameters.join(", ")}) are not supported for images.`,
314
+ "INVALID_PARAMS_FOR_IMAGE",
315
+ "image",
316
+ sortedInvalidParameters,
317
+ [...allowedParameters].toSorted()
318
+ );
319
+ }
320
+ if (query.fit && !["contain", "cover", "fill", "inside", "outside"].includes(query.fit)) {
321
+ throw new ValidationError(
322
+ `Invalid fit value: "${query.fit}". Supported values: "cover", "contain", "fill", "inside", "outside"`,
323
+ "INVALID_FIT_VALUE",
324
+ "image",
325
+ ["fit"],
326
+ ["cover", "contain", "fill", "inside", "outside"]
327
+ );
328
+ }
329
+ if (query.format && !["avif", "gif", "jpeg", "png", "tiff", "webp"].includes(query.format)) {
330
+ throw new ValidationError(
331
+ `Invalid format for image: "${query.format}". Supported formats: jpeg, png, webp, avif, tiff, gif`,
332
+ "INVALID_FORMAT",
333
+ "image",
334
+ ["format"],
335
+ ["jpeg", "png", "webp", "avif", "tiff", "gif"]
336
+ );
337
+ }
338
+ const cropParameters = ["left", "top", "cropWidth", "cropHeight"];
339
+ const providedCropParameters = cropParameters.filter((parameter) => query[parameter] !== void 0);
340
+ if (providedCropParameters.length > 0 && providedCropParameters.length !== cropParameters.length) {
341
+ throw new ValidationError(
342
+ `Incomplete crop parameters: ${providedCropParameters.join(", ")}. All crop parameters must be provided: left, top, cropWidth, cropHeight`,
343
+ "INCOMPLETE_CROP_PARAMS",
344
+ "image",
345
+ providedCropParameters,
346
+ cropParameters
347
+ );
348
+ }
349
+ }
350
+ /**
351
+ * Validates query parameters for video transformations. Checks for invalid audio-only parameters, invalid fit values, invalid rotation angles, invalid codecs, invalid formats, incomplete crop parameter sets, and invalid numeric values.
352
+ * @param query Query parameters to validate.
353
+ * @throws {ValidationError} When validation fails.
354
+ */
355
+ // eslint-disable-next-line class-methods-use-this
356
+ validateVideoQueryParameters(query) {
357
+ const allowedParameters = /* @__PURE__ */ new Set([
358
+ "angle",
359
+ "background",
360
+ "bitrate",
361
+ "codec",
362
+ "cropHeight",
363
+ "cropWidth",
364
+ "fit",
365
+ "format",
366
+ "frameRate",
367
+ "height",
368
+ "keyFrameInterval",
369
+ "left",
370
+ "position",
371
+ "quality",
372
+ "top",
373
+ "width",
374
+ "withoutEnlargement",
375
+ "withoutReduction"
376
+ ]);
377
+ const audioOnlyParameters = /* @__PURE__ */ new Set(["numberOfChannels", "sampleRate"]);
378
+ const invalidParameters = [];
379
+ for (const parameter of audioOnlyParameters) {
380
+ if (query[parameter] !== void 0) {
381
+ invalidParameters.push(parameter);
382
+ }
383
+ }
384
+ if (invalidParameters.length > 0) {
385
+ const sortedInvalidParameters = [...invalidParameters].toSorted();
386
+ throw new ValidationError(
387
+ `Invalid query parameters for video transformation: ${sortedInvalidParameters.join(", ")}. Videos support: ${[...allowedParameters].toSorted().join(", ")}. Audio-only parameters (${sortedInvalidParameters.join(", ")}) are not supported for videos.`,
388
+ "INVALID_PARAMS_FOR_VIDEO",
389
+ "video",
390
+ sortedInvalidParameters,
391
+ [...allowedParameters].toSorted()
392
+ );
393
+ }
394
+ if (query.fit && !["contain", "cover", "fill", "inside", "outside"].includes(query.fit)) {
395
+ throw new ValidationError(
396
+ `Invalid fit value: "${query.fit}". Supported values: "cover", "contain", "fill", "inside", "outside"`,
397
+ "INVALID_FIT_VALUE",
398
+ "video",
399
+ ["fit"],
400
+ ["cover", "contain", "fill", "inside", "outside"]
401
+ );
402
+ }
403
+ if (query.codec && !["av1", "avc", "hevc", "vp8", "vp9"].includes(query.codec)) {
404
+ throw new ValidationError(
405
+ `Invalid codec for video: "${query.codec}". Supported codecs: avc, hevc, vp8, vp9, av1`,
406
+ "INVALID_VIDEO_CODEC",
407
+ "video",
408
+ ["codec"],
409
+ ["avc", "hevc", "vp8", "vp9", "av1"]
410
+ );
411
+ }
412
+ if (query.format && !["mkv", "mp4", "ogg", "webm"].includes(query.format)) {
413
+ throw new ValidationError(
414
+ `Invalid format for video: "${query.format}". Supported formats: mp4, webm, mkv, ogg`,
415
+ "INVALID_VIDEO_FORMAT",
416
+ "video",
417
+ ["format"],
418
+ ["mp4", "webm", "mkv", "ogg"]
419
+ );
420
+ }
421
+ const cropParameters = ["left", "top", "cropWidth", "cropHeight"];
422
+ const providedCropParameters = cropParameters.filter((parameter) => query[parameter] !== void 0);
423
+ if (providedCropParameters.length > 0 && providedCropParameters.length !== cropParameters.length) {
424
+ throw new ValidationError(
425
+ `Incomplete crop parameters: ${providedCropParameters.join(", ")}. All crop parameters must be provided: left, top, cropWidth, cropHeight`,
426
+ "INCOMPLETE_CROP_PARAMS",
427
+ "video",
428
+ providedCropParameters,
429
+ cropParameters
430
+ );
431
+ }
432
+ if (query.width !== void 0 && (typeof query.width !== "number" || query.width <= 0)) {
433
+ throw new ValidationError(`Invalid width: ${query.width}. Must be a positive number.`, "INVALID_WIDTH", "video", ["width"], []);
434
+ }
435
+ if (query.height !== void 0 && (typeof query.height !== "number" || query.height <= 0)) {
436
+ throw new ValidationError(`Invalid height: ${query.height}. Must be a positive number.`, "INVALID_HEIGHT", "video", ["height"], []);
437
+ }
438
+ if (query.bitrate !== void 0 && (typeof query.bitrate !== "number" || query.bitrate <= 0)) {
439
+ throw new ValidationError(`Invalid bitrate: ${query.bitrate}. Must be a positive number.`, "INVALID_BITRATE", "video", ["bitrate"], []);
440
+ }
441
+ if (query.frameRate !== void 0 && (typeof query.frameRate !== "number" || query.frameRate <= 0)) {
442
+ throw new ValidationError(`Invalid frameRate: ${query.frameRate}. Must be a positive number.`, "INVALID_FRAME_RATE", "video", ["frameRate"], []);
443
+ }
444
+ if (query.keyFrameInterval !== void 0 && (typeof query.keyFrameInterval !== "number" || query.keyFrameInterval <= 0)) {
445
+ throw new ValidationError(
446
+ `Invalid keyFrameInterval: ${query.keyFrameInterval}. Must be a positive number.`,
447
+ "INVALID_KEY_FRAME_INTERVAL",
448
+ "video",
449
+ ["keyFrameInterval"],
450
+ []
451
+ );
452
+ }
453
+ }
454
+ /**
455
+ * Generates a unique ID for transformed files based on original file and transformations.
456
+ * @param originalFileId The original file identifier.
457
+ * @param query The transformation query parameters.
458
+ * @param mediaType The media type (image, video, audio).
459
+ * @returns Unique deterministic identifier for the transformed file.
460
+ * @private
461
+ */
462
+ // eslint-disable-next-line class-methods-use-this
463
+ generateTransformedFileId(originalFileId, query, mediaType) {
464
+ const transformParameters = Object.keys(query).filter((key) => query[key] !== void 0).toSorted().map((key) => `${key}:${query[key]}`).join("|");
465
+ const hashInput = `${originalFileId}|${mediaType}|${transformParameters}`;
466
+ const hash = createHash("sha256").update(hashInput).digest("hex").slice(0, 16);
467
+ return `${originalFileId}_transformed_${hash}`;
468
+ }
469
+ /**
470
+ * Checks if the query contains any transformations.
471
+ * @param query The transformation query parameters.
472
+ * @returns True if any transformations are requested.
473
+ * @private
474
+ */
475
+ hasTransformations(query) {
476
+ return this.hasImageTransformations(query) || this.hasVideoTransformations(query) || this.hasAudioTransformations(query);
477
+ }
478
+ /**
479
+ * Creates a MediaTransformResult from a stored transformed file.
480
+ * @param storedFile The stored transformed file.
481
+ * @param mediaType The media type (image, video, audio).
482
+ * @param originalFile The original file information.
483
+ * @returns Media transformation result with metadata.
484
+ * @private
485
+ */
486
+ createMediaTransformResult(storedFile, mediaType, originalFile) {
487
+ const baseResult = {
488
+ buffer: storedFile.content,
489
+ format: this.getFormatFromContentType(storedFile.contentType || ""),
490
+ mediaType,
491
+ originalFile,
492
+ size: storedFile.content.length
493
+ };
494
+ switch (mediaType) {
495
+ case "audio": {
496
+ return {
497
+ ...baseResult,
498
+ bitrate: storedFile.bitrate,
499
+ duration: storedFile.duration,
500
+ mediaType: "audio",
501
+ numberOfChannels: storedFile.numberOfChannels,
502
+ sampleRate: storedFile.sampleRate
503
+ };
504
+ }
505
+ case "image": {
506
+ return {
507
+ ...baseResult,
508
+ height: storedFile.height,
509
+ mediaType: "image",
510
+ width: storedFile.width
511
+ };
512
+ }
513
+ case "video": {
514
+ return {
515
+ ...baseResult,
516
+ bitrate: storedFile.bitrate,
517
+ duration: storedFile.duration,
518
+ height: storedFile.height,
519
+ mediaType: "video",
520
+ width: storedFile.width
521
+ };
522
+ }
523
+ default: {
524
+ return baseResult;
525
+ }
526
+ }
527
+ }
528
+ /**
529
+ * Extracts format from content type.
530
+ * @param contentType MIME content type string.
531
+ * @returns Format string extracted from content type.
532
+ * @private
533
+ */
534
+ // eslint-disable-next-line class-methods-use-this
535
+ getFormatFromContentType(contentType) {
536
+ if (!contentType) {
537
+ return "";
538
+ }
539
+ const extension = mime.getExtension(contentType);
540
+ return extension || "application/octet-stream";
541
+ }
542
+ /**
543
+ * Saves transformed file to storage.
544
+ * @param result The transformation result to save.
545
+ * @param originalFileId The original file identifier.
546
+ * @param query The transformation query parameters.
547
+ * @param mediaType The media type (image, video, audio).
548
+ * @returns Promise that resolves when file is saved.
549
+ * @private
550
+ */
551
+ async saveTransformedFile(result, originalFileId, query, mediaType) {
552
+ try {
553
+ const transformedFileId = this.generateTransformedFileId(originalFileId, query, mediaType);
554
+ const metadata = {
555
+ format: result.format,
556
+ mediaType,
557
+ originalFileId,
558
+ size: result.size,
559
+ transformation: query,
560
+ transformedAt: (/* @__PURE__ */ new Date()).toISOString()
561
+ };
562
+ if (mediaType === "image" && result.width && result.height) {
563
+ metadata.width = result.width;
564
+ metadata.height = result.height;
565
+ } else if (mediaType === "video" && result.width && result.height) {
566
+ metadata.width = result.width;
567
+ metadata.height = result.height;
568
+ metadata.duration = result.duration;
569
+ metadata.bitrate = result.bitrate;
570
+ } else if (mediaType === "audio") {
571
+ metadata.duration = result.duration;
572
+ metadata.numberOfChannels = result.numberOfChannels;
573
+ metadata.sampleRate = result.sampleRate;
574
+ metadata.bitrate = result.bitrate;
575
+ }
576
+ await this.storage.create({
577
+ contentType: mime.getType(result.format) ?? void 0,
578
+ metadata,
579
+ originalName: `${transformedFileId}.${result.format}`,
580
+ size: result.size
581
+ });
582
+ this.logger?.debug?.(`Saved transformed file: ${transformedFileId}`);
583
+ } catch (error) {
584
+ this.logger?.error?.(`Failed to save transformed file: ${error}`);
585
+ }
586
+ }
587
+ /**
588
+ * Validates query parameters for audio transformations. Checks for invalid video-only parameters, invalid codecs, invalid formats, and invalid numeric values.
589
+ * @param query Query parameters to validate.
590
+ * @throws {ValidationError} When validation fails.
591
+ */
592
+ // eslint-disable-next-line class-methods-use-this
593
+ validateAudioQueryParameters(query) {
594
+ const allowedParameters = /* @__PURE__ */ new Set(["bitrate", "codec", "format", "numberOfChannels", "quality", "sampleRate"]);
595
+ const videoOnlyParameters = /* @__PURE__ */ new Set([
596
+ "angle",
597
+ "background",
598
+ "cropHeight",
599
+ "cropWidth",
600
+ "fit",
601
+ "frameRate",
602
+ "height",
603
+ "keyFrameInterval",
604
+ "left",
605
+ "position",
606
+ "top",
607
+ "width",
608
+ "withoutEnlargement",
609
+ "withoutReduction"
610
+ ]);
611
+ const invalidParameters = [];
612
+ for (const parameter of videoOnlyParameters) {
613
+ if (query[parameter] !== void 0) {
614
+ invalidParameters.push(parameter);
615
+ }
616
+ }
617
+ if (invalidParameters.length > 0) {
618
+ const sortedInvalidParameters = [...invalidParameters].toSorted();
619
+ throw new ValidationError(
620
+ `Invalid query parameters for audio transformation: ${sortedInvalidParameters.join(", ")}. Audio supports: ${[...allowedParameters].toSorted().join(", ")}. Video-only parameters (${sortedInvalidParameters.join(", ")}) are not supported for audio.`,
621
+ "INVALID_PARAMS_FOR_AUDIO",
622
+ "audio",
623
+ sortedInvalidParameters,
624
+ [...allowedParameters].toSorted()
625
+ );
626
+ }
627
+ if (query.codec && !["aac", "flac", "mp3", "opus", "vorbis"].includes(query.codec)) {
628
+ throw new ValidationError(
629
+ `Invalid codec for audio: "${query.codec}". Supported codecs: aac, opus, mp3, vorbis, flac`,
630
+ "INVALID_AUDIO_CODEC",
631
+ "audio",
632
+ ["codec"],
633
+ ["aac", "opus", "mp3", "vorbis", "flac"]
634
+ );
635
+ }
636
+ if (query.format && !["aac", "flac", "mp3", "ogg", "wav"].includes(query.format)) {
637
+ throw new ValidationError(
638
+ `Invalid format for audio: "${query.format}". Supported formats: mp3, wav, ogg, aac, flac`,
639
+ "INVALID_AUDIO_FORMAT",
640
+ "audio",
641
+ ["format"],
642
+ ["mp3", "wav", "ogg", "aac", "flac"]
643
+ );
644
+ }
645
+ if (query.bitrate !== void 0 && (typeof query.bitrate !== "number" || query.bitrate <= 0)) {
646
+ throw new ValidationError(`Invalid bitrate: ${query.bitrate}. Must be a positive number.`, "INVALID_BITRATE", "audio", ["bitrate"], []);
647
+ }
648
+ if (query.numberOfChannels !== void 0 && (typeof query.numberOfChannels !== "number" || query.numberOfChannels <= 0 || query.numberOfChannels > 8)) {
649
+ throw new ValidationError(
650
+ `Invalid numberOfChannels: ${query.numberOfChannels}. Must be a number between 1 and 8.`,
651
+ "INVALID_CHANNEL_COUNT",
652
+ "audio",
653
+ ["numberOfChannels"],
654
+ ["1", "2", "3", "4", "5", "6", "7", "8"]
655
+ );
656
+ }
657
+ if (query.sampleRate !== void 0) {
658
+ const validSampleRates = [8e3, 11025, 16e3, 22050, 32e3, 44100, 48e3, 88200, 96e3, 192e3];
659
+ if (typeof query.sampleRate !== "number" || !validSampleRates.includes(query.sampleRate)) {
660
+ throw new ValidationError(
661
+ `Invalid sampleRate: ${query.sampleRate}. Supported sample rates: ${validSampleRates.join(", ")}`,
662
+ "INVALID_SAMPLE_RATE",
663
+ "audio",
664
+ ["sampleRate"],
665
+ validSampleRates.map(String)
666
+ );
667
+ }
668
+ }
669
+ }
670
+ /**
671
+ * Handles image transformation.
672
+ * @param fileId The file identifier.
673
+ * @param query The transformation query parameters.
674
+ * @returns Promise resolving to media transformation result.
675
+ * @private
676
+ */
677
+ async handleImageTransformation(fileId, query) {
678
+ const steps = [];
679
+ if (this.hasImageTransformations(query)) {
680
+ if (query.width || query.height || query.fit) {
681
+ steps.push({
682
+ options: {
683
+ fit: query.fit,
684
+ height: query.height,
685
+ position: query.position,
686
+ width: query.width,
687
+ withoutEnlargement: query.withoutEnlargement,
688
+ withoutReduction: query.withoutReduction
689
+ },
690
+ type: "resize"
691
+ });
692
+ }
693
+ if (query.left !== void 0 && query.top !== void 0 && query.cropWidth && query.cropHeight) {
694
+ steps.push({
695
+ options: {
696
+ height: query.cropHeight,
697
+ left: query.left,
698
+ top: query.top,
699
+ width: query.cropWidth
700
+ },
701
+ type: "crop"
702
+ });
703
+ }
704
+ if (query.angle) {
705
+ steps.push({
706
+ options: {
707
+ angle: query.angle,
708
+ background: query.background
709
+ },
710
+ type: "rotate"
711
+ });
712
+ }
713
+ if (query.format) {
714
+ steps.push({
715
+ options: {
716
+ format: query.format,
717
+ quality: query.quality
718
+ },
719
+ type: "format"
720
+ });
721
+ } else if (query.quality) {
722
+ steps.push({
723
+ options: {
724
+ quality: query.quality
725
+ },
726
+ type: "quality"
727
+ });
728
+ }
729
+ }
730
+ const result = steps.length > 0 ? await this.imageTransformer.transform(fileId, steps) : await this.imageTransformer.transform(fileId, []);
731
+ return this.convertImageResult(result);
732
+ }
733
+ /**
734
+ * Handles video transformation.
735
+ * @param fileId The file identifier.
736
+ * @param query The transformation query parameters.
737
+ * @returns Promise resolving to media transformation result.
738
+ * @private
739
+ */
740
+ async handleVideoTransformation(fileId, query) {
741
+ const steps = [];
742
+ if (this.hasVideoTransformations(query)) {
743
+ if (query.width || query.height || query.fit) {
744
+ steps.push({
745
+ options: {
746
+ fit: query.fit,
747
+ height: query.height,
748
+ position: query.position,
749
+ width: query.width,
750
+ withoutEnlargement: query.withoutEnlargement,
751
+ withoutReduction: query.withoutReduction
752
+ },
753
+ type: "resize"
754
+ });
755
+ }
756
+ if (query.left !== void 0 && query.top !== void 0 && query.cropWidth && query.cropHeight) {
757
+ steps.push({
758
+ options: {
759
+ height: Number(query.cropHeight),
760
+ left: query.left,
761
+ top: query.top,
762
+ width: query.cropWidth
763
+ },
764
+ type: "crop"
765
+ });
766
+ }
767
+ if (query.angle) {
768
+ steps.push({
769
+ options: {
770
+ angle: query.angle,
771
+ background: query.background
772
+ },
773
+ type: "rotate"
774
+ });
775
+ }
776
+ if (query.codec || query.bitrate) {
777
+ steps.push({
778
+ options: {
779
+ bitrate: query.bitrate || query.quality,
780
+ codec: query.codec,
781
+ frameRate: query.frameRate,
782
+ keyFrameInterval: query.keyFrameInterval
783
+ },
784
+ type: "codec"
785
+ });
786
+ }
787
+ if (query.format) {
788
+ steps.push({
789
+ options: {
790
+ format: query.format
791
+ },
792
+ type: "format"
793
+ });
794
+ }
795
+ }
796
+ const result = steps.length > 0 ? await this.videoTransformer.transform(fileId, steps) : await this.videoTransformer.transform(fileId, []);
797
+ return this.convertVideoResult(result);
798
+ }
799
+ /**
800
+ * Handles audio transformation.
801
+ * @param fileId The file identifier.
802
+ * @param query The transformation query parameters.
803
+ * @returns Promise resolving to media transformation result.
804
+ * @private
805
+ */
806
+ async handleAudioTransformation(fileId, query) {
807
+ const steps = [];
808
+ if (this.hasAudioTransformations(query)) {
809
+ if (query.sampleRate) {
810
+ steps.push({
811
+ options: {
812
+ sampleRate: query.sampleRate
813
+ },
814
+ type: "resample"
815
+ });
816
+ }
817
+ if (query.numberOfChannels) {
818
+ steps.push({
819
+ options: {
820
+ numberOfChannels: query.numberOfChannels
821
+ },
822
+ type: "channels"
823
+ });
824
+ }
825
+ if (query.codec || query.bitrate) {
826
+ steps.push({
827
+ options: {
828
+ bitrate: query.bitrate || query.quality,
829
+ codec: query.codec
830
+ },
831
+ type: "codec"
832
+ });
833
+ }
834
+ if (query.format) {
835
+ steps.push({
836
+ options: {
837
+ format: query.format
838
+ },
839
+ type: "format"
840
+ });
841
+ }
842
+ }
843
+ const result = steps.length > 0 ? await this.audioTransformer.transform(fileId, steps) : await this.audioTransformer.transform(fileId, []);
844
+ return this.convertAudioResult(result);
845
+ }
846
+ /**
847
+ * Parses query parameters from various input formats.
848
+ * @param query Query parameters in various formats.
849
+ * @returns Normalized MediaTransformQuery object.
850
+ * @private
851
+ */
852
+ parseQuery(query) {
853
+ if (typeof query === "string") {
854
+ return this.parseURLSearchParams(new URLSearchParams(query));
855
+ }
856
+ if (query instanceof URLSearchParams) {
857
+ return this.parseURLSearchParams(query);
858
+ }
859
+ const transformQuery = query;
860
+ if (query.left) {
861
+ transformQuery.left = Number.parseInt(query.left, 10);
862
+ }
863
+ if (query.top) {
864
+ transformQuery.top = Number.parseInt(query.top, 10);
865
+ }
866
+ if (query.cropWidth) {
867
+ transformQuery.cropWidth = Number.parseInt(query.cropWidth, 10);
868
+ }
869
+ if (query.cropHeight) {
870
+ transformQuery.cropHeight = Number.parseInt(query.cropHeight, 10);
871
+ }
872
+ if (query.angle) {
873
+ transformQuery.angle = Number.parseInt(query.angle, 10);
874
+ }
875
+ if (query.quality) {
876
+ transformQuery.quality = Number.parseInt(query.quality, 10);
877
+ }
878
+ if (query.width) {
879
+ transformQuery.width = Number.parseInt(query.width, 10);
880
+ }
881
+ if (query.height) {
882
+ transformQuery.height = Number.parseInt(query.height, 10);
883
+ }
884
+ if (query.bitrate) {
885
+ transformQuery.bitrate = Number.parseInt(query.bitrate, 10);
886
+ }
887
+ if (query.frameRate) {
888
+ transformQuery.frameRate = Number.parseInt(query.frameRate, 10);
889
+ }
890
+ if (query.keyFrameInterval) {
891
+ transformQuery.keyFrameInterval = Number.parseInt(query.keyFrameInterval, 10);
892
+ }
893
+ if (query.numberOfChannels) {
894
+ transformQuery.numberOfChannels = Number.parseInt(query.numberOfChannels, 10);
895
+ }
896
+ if (query.sampleRate) {
897
+ transformQuery.sampleRate = Number.parseInt(query.sampleRate, 10);
898
+ }
899
+ return transformQuery;
900
+ }
901
+ /**
902
+ * Parses boolean parameter from string.
903
+ * @param value String value to parse.
904
+ * @returns Boolean value or undefined.
905
+ * @private
906
+ */
907
+ // eslint-disable-next-line class-methods-use-this
908
+ parseBooleanParameter(value) {
909
+ if (value === null) {
910
+ return void 0;
911
+ }
912
+ return value === "true" || value === "1";
913
+ }
914
+ /**
915
+ * Parses URLSearchParams into MediaTransformQuery.
916
+ * @param parameters URL search parameters object.
917
+ * @returns MediaTransformQuery object with parsed parameters.
918
+ * @private
919
+ */
920
+ parseURLSearchParams(parameters) {
921
+ const query = {};
922
+ if (parameters.has("format")) {
923
+ query.format = parameters.get("format") ?? void 0;
924
+ }
925
+ if (parameters.has("quality")) {
926
+ query.quality = parameters.get("quality") ? Number.parseInt(parameters.get("quality"), 10) : void 0;
927
+ }
928
+ if (parameters.has("width")) {
929
+ query.width = parameters.get("width") ? Number.parseInt(parameters.get("width"), 10) : void 0;
930
+ }
931
+ if (parameters.has("height")) {
932
+ query.height = parameters.get("height") ? Number.parseInt(parameters.get("height"), 10) : void 0;
933
+ }
934
+ if (parameters.has("fit")) {
935
+ const fitValue = parameters.get("fit");
936
+ if (fitValue && ["contain", "cover", "fill", "inside", "outside"].includes(fitValue)) {
937
+ query.fit = fitValue;
938
+ }
939
+ }
940
+ if (parameters.has("position")) {
941
+ query.position = parameters.get("position") ?? void 0;
942
+ }
943
+ if (parameters.has("withoutEnlargement")) {
944
+ const value = parameters.get("withoutEnlargement");
945
+ query.withoutEnlargement = value === "true" ? true : void 0;
946
+ }
947
+ if (parameters.has("withoutReduction")) {
948
+ const value = parameters.get("withoutReduction");
949
+ query.withoutReduction = value === "true" ? true : void 0;
950
+ }
951
+ if (parameters.has("left")) {
952
+ query.left = parameters.get("left") ? Number.parseInt(parameters.get("left"), 10) : void 0;
953
+ }
954
+ if (parameters.has("top")) {
955
+ query.top = parameters.get("top") ? Number.parseInt(parameters.get("top"), 10) : void 0;
956
+ }
957
+ if (parameters.has("cropWidth")) {
958
+ query.cropWidth = parameters.get("cropWidth") ? Number.parseInt(parameters.get("cropWidth"), 10) : void 0;
959
+ }
960
+ if (parameters.has("cropHeight")) {
961
+ query.cropHeight = parameters.get("cropHeight") ? Number.parseInt(parameters.get("cropHeight"), 10) : void 0;
962
+ }
963
+ if (parameters.has("angle")) {
964
+ const angleValue = parameters.get("angle") ? Number.parseInt(parameters.get("angle"), 10) : void 0;
965
+ if (angleValue !== void 0) {
966
+ query.angle = angleValue;
967
+ }
968
+ }
969
+ if (parameters.has("background")) {
970
+ query.background = parameters.get("background") ?? void 0;
971
+ }
972
+ if (parameters.has("blur")) {
973
+ query.blur = this.parseBooleanParameter(parameters.get("blur"));
974
+ }
975
+ if (parameters.has("sharpen")) {
976
+ query.sharpen = this.parseBooleanParameter(parameters.get("sharpen"));
977
+ }
978
+ if (parameters.has("median")) {
979
+ query.median = parameters.get("median") ? Number.parseInt(parameters.get("median"), 10) : void 0;
980
+ }
981
+ if (parameters.has("clahe")) {
982
+ query.clahe = this.parseBooleanParameter(parameters.get("clahe"));
983
+ }
984
+ if (parameters.has("threshold")) {
985
+ query.threshold = parameters.get("threshold") ? Number.parseInt(parameters.get("threshold"), 10) : void 0;
986
+ }
987
+ if (parameters.has("gamma")) {
988
+ query.gamma = this.parseBooleanParameter(parameters.get("gamma"));
989
+ }
990
+ if (parameters.has("negate")) {
991
+ query.negate = this.parseBooleanParameter(parameters.get("negate"));
992
+ }
993
+ if (parameters.has("normalise") || parameters.has("normalize")) {
994
+ query.normalise = this.parseBooleanParameter(parameters.get("normalise")) || this.parseBooleanParameter(parameters.get("normalize"));
995
+ }
996
+ if (parameters.has("flatten")) {
997
+ query.flatten = this.parseBooleanParameter(parameters.get("flatten"));
998
+ }
999
+ if (parameters.has("unflatten")) {
1000
+ query.unflatten = this.parseBooleanParameter(parameters.get("unflatten"));
1001
+ }
1002
+ if (parameters.has("flip")) {
1003
+ query.flip = this.parseBooleanParameter(parameters.get("flip"));
1004
+ }
1005
+ if (parameters.has("flop")) {
1006
+ query.flop = this.parseBooleanParameter(parameters.get("flop"));
1007
+ }
1008
+ if (parameters.has("affine")) {
1009
+ query.affine = this.parseBooleanParameter(parameters.get("affine"));
1010
+ }
1011
+ if (parameters.has("dilate")) {
1012
+ query.dilate = this.parseBooleanParameter(parameters.get("dilate"));
1013
+ }
1014
+ if (parameters.has("erode")) {
1015
+ query.erode = this.parseBooleanParameter(parameters.get("erode"));
1016
+ }
1017
+ if (parameters.has("pipelineColourspace")) {
1018
+ query.pipelineColourspace = this.parseBooleanParameter(parameters.get("pipelineColourspace"));
1019
+ }
1020
+ if (parameters.has("toColourspace")) {
1021
+ query.toColourspace = this.parseBooleanParameter(parameters.get("toColourspace"));
1022
+ }
1023
+ if (parameters.has("removeAlpha")) {
1024
+ query.removeAlpha = this.parseBooleanParameter(parameters.get("removeAlpha"));
1025
+ }
1026
+ if (parameters.has("ensureAlpha")) {
1027
+ query.ensureAlpha = this.parseBooleanParameter(parameters.get("ensureAlpha"));
1028
+ }
1029
+ if (parameters.has("greyscale") || parameters.has("grayscale")) {
1030
+ query.greyscale = this.parseBooleanParameter(parameters.get("greyscale")) || this.parseBooleanParameter(parameters.get("grayscale"));
1031
+ }
1032
+ if (parameters.has("modulate")) {
1033
+ query.modulate = this.parseBooleanParameter(parameters.get("modulate"));
1034
+ }
1035
+ if (parameters.has("tint")) {
1036
+ query.tint = this.parseBooleanParameter(parameters.get("tint"));
1037
+ }
1038
+ if (parameters.has("brightness")) {
1039
+ query.brightness = parameters.get("brightness") ? Number.parseFloat(parameters.get("brightness")) : void 0;
1040
+ }
1041
+ if (parameters.has("saturation")) {
1042
+ query.saturation = parameters.get("saturation") ? Number.parseFloat(parameters.get("saturation")) : void 0;
1043
+ }
1044
+ if (parameters.has("hue")) {
1045
+ query.hue = parameters.get("hue") ? Number.parseInt(parameters.get("hue"), 10) : void 0;
1046
+ }
1047
+ if (parameters.has("lightness")) {
1048
+ query.lightness = parameters.get("lightness") ? Number.parseInt(parameters.get("lightness"), 10) : void 0;
1049
+ }
1050
+ if (parameters.has("kernel")) {
1051
+ query.kernel = parameters.get("kernel") ?? void 0;
1052
+ }
1053
+ if (parameters.has("fastShrinkOnLoad")) {
1054
+ query.fastShrinkOnLoad = parameters.get("fastShrinkOnLoad") === "true";
1055
+ }
1056
+ if (parameters.has("codec")) {
1057
+ const codecValue = parameters.get("codec");
1058
+ const validCodecs = ["avc", "hevc", "vp8", "vp9", "av1", "aac", "opus", "mp3", "vorbis", "flac"];
1059
+ if (codecValue && validCodecs.includes(codecValue)) {
1060
+ query.codec = codecValue;
1061
+ }
1062
+ }
1063
+ if (parameters.has("bitrate")) {
1064
+ query.bitrate = parameters.get("bitrate") ? Number.parseInt(parameters.get("bitrate"), 10) : void 0;
1065
+ }
1066
+ if (parameters.has("frameRate")) {
1067
+ query.frameRate = parameters.get("frameRate") ? Number.parseInt(parameters.get("frameRate"), 10) : void 0;
1068
+ }
1069
+ if (parameters.has("keyFrameInterval")) {
1070
+ query.keyFrameInterval = parameters.get("keyFrameInterval") ? Number.parseInt(parameters.get("keyFrameInterval"), 10) : void 0;
1071
+ }
1072
+ if (parameters.has("numberOfChannels")) {
1073
+ query.numberOfChannels = parameters.get("numberOfChannels") ? Number.parseInt(parameters.get("numberOfChannels"), 10) : void 0;
1074
+ }
1075
+ if (parameters.has("sampleRate")) {
1076
+ query.sampleRate = parameters.get("sampleRate") ? Number.parseInt(parameters.get("sampleRate"), 10) : void 0;
1077
+ }
1078
+ return query;
1079
+ }
1080
+ /**
1081
+ * Detects media type from MIME type.
1082
+ * @param contentType MIME content type string.
1083
+ * @returns Detected media type.
1084
+ * @throws Error if content type is missing or unsupported.
1085
+ * @private
1086
+ */
1087
+ // eslint-disable-next-line class-methods-use-this
1088
+ detectMediaType(contentType) {
1089
+ if (!contentType) {
1090
+ throw new Error("Cannot detect media type: no content type provided");
1091
+ }
1092
+ if (!isKnownContentType(contentType)) {
1093
+ throw new Error(`Unknown or invalid content type: ${contentType}`);
1094
+ }
1095
+ if (contentType.startsWith("image/")) {
1096
+ return "image";
1097
+ }
1098
+ if (contentType.startsWith("video/")) {
1099
+ return "video";
1100
+ }
1101
+ if (contentType.startsWith("audio/")) {
1102
+ return "audio";
1103
+ }
1104
+ throw new Error(`Unsupported media type for content type: ${contentType}`);
1105
+ }
1106
+ /**
1107
+ * Checks if query has image transformations.
1108
+ * @param query The transformation query parameters.
1109
+ * @returns True if image transformations are requested.
1110
+ * @private
1111
+ */
1112
+ // eslint-disable-next-line class-methods-use-this
1113
+ hasImageTransformations(query) {
1114
+ return !!(query.width || query.height || query.fit || query.position || query.withoutEnlargement || query.withoutReduction || query.left !== void 0 || query.top !== void 0 || query.cropWidth || query.cropHeight || query.angle !== void 0 || query.background || query.blur || query.sharpen || query.median || query.clahe || query.threshold !== void 0 || query.gamma || query.negate || query.normalise || query.flatten || query.unflatten || query.flip || query.flop || query.greyscale || query.modulate || query.tint || query.brightness !== void 0 || query.saturation !== void 0 || query.hue !== void 0 || query.lightness !== void 0 || query.kernel || query.fastShrinkOnLoad || query.affine || query.dilate || query.erode || query.pipelineColourspace || query.toColourspace || query.removeAlpha || query.ensureAlpha || query.format || query.quality);
1115
+ }
1116
+ /**
1117
+ * Checks if query has video transformations.
1118
+ * @param query The transformation query parameters.
1119
+ * @returns True if video transformations are requested.
1120
+ * @private
1121
+ */
1122
+ // eslint-disable-next-line class-methods-use-this
1123
+ hasVideoTransformations(query) {
1124
+ return !!(query.width || query.height || query.fit || query.position || query.withoutEnlargement || query.withoutReduction || query.left !== void 0 || query.top !== void 0 || query.cropWidth || query.cropHeight || query.angle || query.background || query.codec || query.bitrate || query.frameRate || query.keyFrameInterval || query.format || query.quality);
1125
+ }
1126
+ /**
1127
+ * Checks if query has audio transformations.
1128
+ * @param query The transformation query parameters.
1129
+ * @returns True if audio transformations are requested.
1130
+ * @private
1131
+ */
1132
+ // eslint-disable-next-line class-methods-use-this
1133
+ hasAudioTransformations(query) {
1134
+ return !!(query.sampleRate || query.numberOfChannels || query.codec || query.bitrate || query.format || query.quality);
1135
+ }
1136
+ /**
1137
+ * Converts ImageTransformer result to unified format.
1138
+ * @param result The image transformation result.
1139
+ * @returns Unified media transformation result.
1140
+ * @private
1141
+ */
1142
+ // eslint-disable-next-line class-methods-use-this
1143
+ convertImageResult(result) {
1144
+ return {
1145
+ buffer: result.buffer,
1146
+ format: result.format,
1147
+ height: result.height,
1148
+ mediaType: "image",
1149
+ originalFile: result.originalFile,
1150
+ size: result.size,
1151
+ width: result.width
1152
+ };
1153
+ }
1154
+ /**
1155
+ * Converts VideoTransformer result to unified format.
1156
+ * @param result The video transformation result.
1157
+ * @returns Unified media transformation result.
1158
+ * @private
1159
+ */
1160
+ // eslint-disable-next-line class-methods-use-this
1161
+ convertVideoResult(result) {
1162
+ return {
1163
+ bitrate: result.bitrate,
1164
+ buffer: result.buffer,
1165
+ duration: result.duration,
1166
+ format: result.format,
1167
+ height: result.height,
1168
+ mediaType: "video",
1169
+ originalFile: result.originalFile,
1170
+ size: result.size,
1171
+ width: result.width
1172
+ };
1173
+ }
1174
+ /**
1175
+ * Converts AudioTransformer result to unified format.
1176
+ * @param result The audio transformation result.
1177
+ * @returns Unified media transformation result.
1178
+ * @private
1179
+ */
1180
+ // eslint-disable-next-line class-methods-use-this
1181
+ convertAudioResult(result) {
1182
+ return {
1183
+ bitrate: result.bitrate,
1184
+ buffer: result.buffer,
1185
+ duration: result.duration,
1186
+ format: result.format,
1187
+ mediaType: "audio",
1188
+ numberOfChannels: result.numberOfChannels,
1189
+ originalFile: result.originalFile,
1190
+ sampleRate: result.sampleRate,
1191
+ size: result.size
1192
+ };
1193
+ }
1194
+ }
1195
+
1196
+ export { MediaTransformer as default };