@uploadista/flow-images-nodes 0.0.18 → 0.0.20-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +121 -53
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +108 -40
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -4
- package/src/optimize-node.ts +116 -38
- package/src/resize-node.ts +58 -2
- package/src/transform-image-node.ts +129 -4
package/src/optimize-node.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { UploadistaError } from "@uploadista/core";
|
|
1
2
|
import {
|
|
2
3
|
applyFileNaming,
|
|
3
4
|
buildNamingContext,
|
|
@@ -7,6 +8,8 @@ import {
|
|
|
7
8
|
ImagePlugin,
|
|
8
9
|
type OptimizeParams,
|
|
9
10
|
STORAGE_OUTPUT_TYPE_ID,
|
|
11
|
+
type StreamingConfig,
|
|
12
|
+
type TransformMode,
|
|
10
13
|
} from "@uploadista/core/flow";
|
|
11
14
|
import { Effect } from "effect";
|
|
12
15
|
|
|
@@ -29,28 +32,111 @@ const formatToExtension: Record<OptimizeParams["format"], string> = {
|
|
|
29
32
|
/**
|
|
30
33
|
* Creates an optimize node that optimizes images for web delivery.
|
|
31
34
|
*
|
|
35
|
+
* Supports both buffered and streaming modes for memory-efficient processing
|
|
36
|
+
* of large images. In streaming mode, the image is read and processed
|
|
37
|
+
* incrementally, reducing peak memory usage.
|
|
38
|
+
*
|
|
32
39
|
* @param id - Unique node identifier
|
|
33
40
|
* @param params - Optimize parameters (quality, format)
|
|
34
41
|
* @param options - Optional configuration
|
|
35
42
|
* @param options.keepOutput - Whether to keep output in flow results
|
|
36
43
|
* @param options.naming - File naming configuration (auto suffix: `${format}`)
|
|
44
|
+
* @param options.mode - Transform mode: "buffered" (default), "streaming", or "auto"
|
|
45
|
+
* @param options.streamingConfig - Streaming configuration (file size threshold, chunk size)
|
|
37
46
|
*
|
|
38
47
|
* @example
|
|
39
48
|
* ```typescript
|
|
40
|
-
* //
|
|
49
|
+
* // Buffered mode (default) - "photo.jpg" -> "photo-webp.webp"
|
|
41
50
|
* const optimize = yield* createOptimizeNode("opt-1", { quality: 80, format: "webp" }, {
|
|
42
51
|
* naming: { mode: "auto" }
|
|
43
52
|
* });
|
|
53
|
+
*
|
|
54
|
+
* // Streaming mode for large files
|
|
55
|
+
* const optimizeStreaming = yield* createOptimizeNode("opt-2", { quality: 80, format: "webp" }, {
|
|
56
|
+
* mode: "streaming",
|
|
57
|
+
* naming: { mode: "auto" }
|
|
58
|
+
* });
|
|
59
|
+
*
|
|
60
|
+
* // Auto mode - uses streaming for files > 1MB
|
|
61
|
+
* const optimizeAuto = yield* createOptimizeNode("opt-3", { quality: 80, format: "webp" }, {
|
|
62
|
+
* mode: "auto",
|
|
63
|
+
* streamingConfig: { fileSizeThreshold: 1_048_576 },
|
|
64
|
+
* naming: { mode: "auto" }
|
|
65
|
+
* });
|
|
44
66
|
* ```
|
|
45
67
|
*/
|
|
46
68
|
export function createOptimizeNode(
|
|
47
69
|
id: string,
|
|
48
70
|
{ quality, format }: OptimizeParams,
|
|
49
|
-
options?: {
|
|
71
|
+
options?: {
|
|
72
|
+
keepOutput?: boolean;
|
|
73
|
+
naming?: FileNamingConfig;
|
|
74
|
+
mode?: TransformMode;
|
|
75
|
+
streamingConfig?: StreamingConfig;
|
|
76
|
+
},
|
|
50
77
|
) {
|
|
51
78
|
return Effect.gen(function* () {
|
|
52
79
|
const imageService = yield* ImagePlugin;
|
|
53
80
|
|
|
81
|
+
// Determine if streaming is available and requested
|
|
82
|
+
const supportsStreaming = imageService.supportsStreaming ?? false;
|
|
83
|
+
const requestedMode = options?.mode ?? "buffered";
|
|
84
|
+
|
|
85
|
+
// If streaming requested but not supported, fall back to buffered
|
|
86
|
+
const effectiveMode: TransformMode =
|
|
87
|
+
requestedMode === "buffered"
|
|
88
|
+
? "buffered"
|
|
89
|
+
: supportsStreaming
|
|
90
|
+
? requestedMode
|
|
91
|
+
: "buffered";
|
|
92
|
+
|
|
93
|
+
// Helper to build output metadata from optimized result
|
|
94
|
+
const buildOutputMetadata = (file: {
|
|
95
|
+
metadata?: Record<string, unknown>;
|
|
96
|
+
flow?: { flowId?: string; jobId?: string };
|
|
97
|
+
}) => {
|
|
98
|
+
const newType = formatToMimeType[format];
|
|
99
|
+
const newExtension = formatToExtension[format];
|
|
100
|
+
|
|
101
|
+
// Get original fileName
|
|
102
|
+
const fileName = file.metadata?.fileName;
|
|
103
|
+
let newFileName: string | undefined;
|
|
104
|
+
|
|
105
|
+
if (fileName && typeof fileName === "string") {
|
|
106
|
+
// Apply naming if configured
|
|
107
|
+
if (options?.naming) {
|
|
108
|
+
const namingConfig: FileNamingConfig = {
|
|
109
|
+
...options.naming,
|
|
110
|
+
autoSuffix:
|
|
111
|
+
options.naming.autoSuffix ?? ((ctx) => ctx.format ?? format),
|
|
112
|
+
};
|
|
113
|
+
const namingContext = buildNamingContext(
|
|
114
|
+
file as Parameters<typeof buildNamingContext>[0],
|
|
115
|
+
{
|
|
116
|
+
flowId: file.flow?.flowId ?? "",
|
|
117
|
+
jobId: file.flow?.jobId ?? "",
|
|
118
|
+
nodeId: id,
|
|
119
|
+
nodeType: "optimize",
|
|
120
|
+
},
|
|
121
|
+
{ format, quality },
|
|
122
|
+
);
|
|
123
|
+
// Apply naming to get base name with suffix
|
|
124
|
+
const namedFile = applyFileNaming(
|
|
125
|
+
file as Parameters<typeof applyFileNaming>[0],
|
|
126
|
+
namingContext,
|
|
127
|
+
namingConfig,
|
|
128
|
+
);
|
|
129
|
+
// Replace extension with new format extension
|
|
130
|
+
newFileName = `${getBaseName(namedFile)}.${newExtension}`;
|
|
131
|
+
} else {
|
|
132
|
+
// No naming config, just update extension
|
|
133
|
+
newFileName = fileName.replace(/\.[^.]+$/, `.${newExtension}`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return { newType, newFileName };
|
|
138
|
+
};
|
|
139
|
+
|
|
54
140
|
return yield* createTransformNode({
|
|
55
141
|
id,
|
|
56
142
|
name: "Optimize",
|
|
@@ -61,46 +147,14 @@ export function createOptimizeNode(
|
|
|
61
147
|
// Note: naming is handled in transform since format changes extension
|
|
62
148
|
nodeType: "optimize",
|
|
63
149
|
namingVars: { format, quality },
|
|
150
|
+
mode: effectiveMode,
|
|
151
|
+
streamingConfig: options?.streamingConfig,
|
|
152
|
+
// Buffered transform (used when mode is "buffered" or "auto" selects buffered)
|
|
64
153
|
transform: (inputBytes, file) =>
|
|
65
154
|
Effect.map(
|
|
66
155
|
imageService.optimize(inputBytes, { quality, format }),
|
|
67
156
|
(optimizedBytes) => {
|
|
68
|
-
|
|
69
|
-
const newType = formatToMimeType[format];
|
|
70
|
-
const newExtension = formatToExtension[format];
|
|
71
|
-
|
|
72
|
-
// Get original fileName
|
|
73
|
-
const fileName = file.metadata?.fileName;
|
|
74
|
-
let newFileName: string | undefined;
|
|
75
|
-
|
|
76
|
-
if (fileName && typeof fileName === "string") {
|
|
77
|
-
// Apply naming if configured
|
|
78
|
-
if (options?.naming) {
|
|
79
|
-
const namingConfig: FileNamingConfig = {
|
|
80
|
-
...options.naming,
|
|
81
|
-
autoSuffix:
|
|
82
|
-
options.naming.autoSuffix ?? ((ctx) => ctx.format ?? format),
|
|
83
|
-
};
|
|
84
|
-
const namingContext = buildNamingContext(
|
|
85
|
-
file,
|
|
86
|
-
{
|
|
87
|
-
flowId: file.flow?.flowId ?? "",
|
|
88
|
-
jobId: file.flow?.jobId ?? "",
|
|
89
|
-
nodeId: id,
|
|
90
|
-
nodeType: "optimize",
|
|
91
|
-
},
|
|
92
|
-
{ format, quality },
|
|
93
|
-
);
|
|
94
|
-
// Apply naming to get base name with suffix
|
|
95
|
-
const namedFile = applyFileNaming(file, namingContext, namingConfig);
|
|
96
|
-
// Replace extension with new format extension
|
|
97
|
-
newFileName = `${getBaseName(namedFile)}.${newExtension}`;
|
|
98
|
-
} else {
|
|
99
|
-
// No naming config, just update extension
|
|
100
|
-
newFileName = fileName.replace(/\.[^.]+$/, `.${newExtension}`);
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
157
|
+
const { newType, newFileName } = buildOutputMetadata(file);
|
|
104
158
|
return {
|
|
105
159
|
bytes: optimizedBytes,
|
|
106
160
|
type: newType,
|
|
@@ -110,6 +164,30 @@ export function createOptimizeNode(
|
|
|
110
164
|
| { bytes: Uint8Array; type: string; fileName?: string };
|
|
111
165
|
},
|
|
112
166
|
),
|
|
167
|
+
// Streaming transform (used when mode is "streaming" or "auto" selects streaming)
|
|
168
|
+
streamingTransform: imageService.optimizeStream
|
|
169
|
+
? (inputStream, file) => {
|
|
170
|
+
const optimizeStreamFn = imageService.optimizeStream;
|
|
171
|
+
if (!optimizeStreamFn) {
|
|
172
|
+
throw UploadistaError.fromCode("UNKNOWN_ERROR");
|
|
173
|
+
}
|
|
174
|
+
return Effect.gen(function* () {
|
|
175
|
+
// Use the streaming optimization
|
|
176
|
+
const outputStream = yield* optimizeStreamFn(inputStream, {
|
|
177
|
+
quality,
|
|
178
|
+
format,
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
const { newType, newFileName } = buildOutputMetadata(file);
|
|
182
|
+
|
|
183
|
+
return {
|
|
184
|
+
stream: outputStream,
|
|
185
|
+
type: newType,
|
|
186
|
+
fileName: newFileName,
|
|
187
|
+
};
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
: undefined,
|
|
113
191
|
});
|
|
114
192
|
});
|
|
115
193
|
}
|
package/src/resize-node.ts
CHANGED
|
@@ -4,34 +4,71 @@ import {
|
|
|
4
4
|
ImagePlugin,
|
|
5
5
|
type ResizeParams,
|
|
6
6
|
STORAGE_OUTPUT_TYPE_ID,
|
|
7
|
+
type StreamingConfig,
|
|
8
|
+
type TransformMode,
|
|
7
9
|
} from "@uploadista/core/flow";
|
|
8
10
|
import { Effect } from "effect";
|
|
9
11
|
|
|
10
12
|
/**
|
|
11
13
|
* Creates a resize node that resizes images to specified dimensions.
|
|
12
14
|
*
|
|
15
|
+
* Supports both buffered and streaming modes for memory-efficient processing
|
|
16
|
+
* of large images. In streaming mode, the image is read and processed
|
|
17
|
+
* incrementally, reducing peak memory usage.
|
|
18
|
+
*
|
|
13
19
|
* @param id - Unique node identifier
|
|
14
20
|
* @param params - Resize parameters (width, height, fit)
|
|
15
21
|
* @param options - Optional configuration
|
|
16
22
|
* @param options.keepOutput - Whether to keep output in flow results
|
|
17
23
|
* @param options.naming - File naming configuration (auto suffix: `${width}x${height}`)
|
|
24
|
+
* @param options.mode - Transform mode: "buffered", "streaming", or "auto" (default)
|
|
25
|
+
* @param options.streamingConfig - Streaming configuration (file size threshold, chunk size)
|
|
18
26
|
*
|
|
19
27
|
* @example
|
|
20
28
|
* ```typescript
|
|
21
|
-
* //
|
|
29
|
+
* // Auto mode (default) - uses streaming for files > 1MB, otherwise buffered
|
|
22
30
|
* const resize = yield* createResizeNode("resize-1", { width: 800, height: 600 }, {
|
|
23
31
|
* naming: { mode: "auto" }
|
|
24
32
|
* });
|
|
33
|
+
*
|
|
34
|
+
* // Force buffered mode for small files
|
|
35
|
+
* const resizeBuffered = yield* createResizeNode("resize-2", { width: 800, height: 600 }, {
|
|
36
|
+
* mode: "buffered",
|
|
37
|
+
* naming: { mode: "auto" }
|
|
38
|
+
* });
|
|
39
|
+
*
|
|
40
|
+
* // Force streaming mode for memory efficiency
|
|
41
|
+
* const resizeStreaming = yield* createResizeNode("resize-3", { width: 800, height: 600 }, {
|
|
42
|
+
* mode: "streaming",
|
|
43
|
+
* naming: { mode: "auto" }
|
|
44
|
+
* });
|
|
25
45
|
* ```
|
|
26
46
|
*/
|
|
27
47
|
export function createResizeNode(
|
|
28
48
|
id: string,
|
|
29
49
|
{ width, height, fit }: ResizeParams,
|
|
30
|
-
options?: {
|
|
50
|
+
options?: {
|
|
51
|
+
keepOutput?: boolean;
|
|
52
|
+
naming?: FileNamingConfig;
|
|
53
|
+
mode?: TransformMode;
|
|
54
|
+
streamingConfig?: StreamingConfig;
|
|
55
|
+
},
|
|
31
56
|
) {
|
|
32
57
|
return Effect.gen(function* () {
|
|
33
58
|
const imageService = yield* ImagePlugin;
|
|
34
59
|
|
|
60
|
+
// Determine if streaming is available and requested
|
|
61
|
+
const supportsStreaming = imageService.supportsStreaming ?? false;
|
|
62
|
+
const requestedMode = options?.mode ?? "auto";
|
|
63
|
+
|
|
64
|
+
// If streaming requested but not supported, fall back to buffered
|
|
65
|
+
const effectiveMode: TransformMode =
|
|
66
|
+
requestedMode === "buffered"
|
|
67
|
+
? "buffered"
|
|
68
|
+
: supportsStreaming
|
|
69
|
+
? requestedMode
|
|
70
|
+
: "buffered";
|
|
71
|
+
|
|
35
72
|
// Build naming config with auto suffix for resize
|
|
36
73
|
const namingConfig: FileNamingConfig | undefined = options?.naming
|
|
37
74
|
? {
|
|
@@ -53,8 +90,27 @@ export function createResizeNode(
|
|
|
53
90
|
naming: namingConfig,
|
|
54
91
|
nodeType: "resize",
|
|
55
92
|
namingVars: { width, height },
|
|
93
|
+
mode: effectiveMode,
|
|
94
|
+
streamingConfig: options?.streamingConfig,
|
|
95
|
+
// Buffered transform (used when mode is "buffered" or "auto" selects buffered)
|
|
56
96
|
transform: (inputBytes) =>
|
|
57
97
|
imageService.resize(inputBytes, { height, width, fit }),
|
|
98
|
+
// Streaming transform (used when mode is "streaming" or "auto" selects streaming)
|
|
99
|
+
streamingTransform: imageService.resizeStream
|
|
100
|
+
? (inputStream) =>
|
|
101
|
+
Effect.gen(function* () {
|
|
102
|
+
const resizeStreamFn = imageService.resizeStream;
|
|
103
|
+
if (!resizeStreamFn) {
|
|
104
|
+
throw new Error("resizeStream not available");
|
|
105
|
+
}
|
|
106
|
+
const outputStream = yield* resizeStreamFn(inputStream, {
|
|
107
|
+
width,
|
|
108
|
+
height,
|
|
109
|
+
fit,
|
|
110
|
+
});
|
|
111
|
+
return { stream: outputStream };
|
|
112
|
+
})
|
|
113
|
+
: undefined,
|
|
58
114
|
});
|
|
59
115
|
});
|
|
60
116
|
}
|
|
@@ -3,9 +3,12 @@ import {
|
|
|
3
3
|
type FileNamingConfig,
|
|
4
4
|
ImagePlugin,
|
|
5
5
|
STORAGE_OUTPUT_TYPE_ID,
|
|
6
|
+
type StreamingConfig,
|
|
6
7
|
type TransformImageParams,
|
|
8
|
+
type TransformMode,
|
|
7
9
|
} from "@uploadista/core/flow";
|
|
8
|
-
import {
|
|
10
|
+
import type { UploadistaError } from "@uploadista/core/errors";
|
|
11
|
+
import { Effect, Stream } from "effect";
|
|
9
12
|
|
|
10
13
|
/**
|
|
11
14
|
* Apply a chain of transformations to an image by reducing over the transformations array.
|
|
@@ -26,6 +29,62 @@ function applyTransformationChain(
|
|
|
26
29
|
);
|
|
27
30
|
}
|
|
28
31
|
|
|
32
|
+
/**
|
|
33
|
+
* Apply a chain of transformations using streaming where possible.
|
|
34
|
+
* Falls back to buffered processing for transformations that don't support streaming.
|
|
35
|
+
*
|
|
36
|
+
* @param imageService - The image plugin service to use for transformations
|
|
37
|
+
* @param inputStream - The input image as a stream
|
|
38
|
+
* @param transformations - Array of transformations to apply in sequence
|
|
39
|
+
* @returns Effect that resolves to the final transformed image stream
|
|
40
|
+
*/
|
|
41
|
+
function applyStreamingTransformationChain(
|
|
42
|
+
imageService: ReturnType<typeof ImagePlugin.of>,
|
|
43
|
+
inputStream: Stream.Stream<Uint8Array, UploadistaError>,
|
|
44
|
+
transformations: TransformImageParams["transformations"],
|
|
45
|
+
): Effect.Effect<Stream.Stream<Uint8Array, UploadistaError>, UploadistaError> {
|
|
46
|
+
const transformStreamFn = imageService.transformStream;
|
|
47
|
+
|
|
48
|
+
if (!transformStreamFn) {
|
|
49
|
+
// If streaming not supported, collect to buffer and use buffered chain
|
|
50
|
+
return Effect.gen(function* () {
|
|
51
|
+
// Collect stream to buffer
|
|
52
|
+
const chunks: Uint8Array[] = [];
|
|
53
|
+
yield* Stream.runForEach(inputStream, (chunk) =>
|
|
54
|
+
Effect.sync(() => {
|
|
55
|
+
chunks.push(chunk);
|
|
56
|
+
}),
|
|
57
|
+
);
|
|
58
|
+
const totalLength = chunks.reduce((sum, c) => sum + c.byteLength, 0);
|
|
59
|
+
const inputBuffer = new Uint8Array(totalLength);
|
|
60
|
+
let offset = 0;
|
|
61
|
+
for (const chunk of chunks) {
|
|
62
|
+
inputBuffer.set(chunk, offset);
|
|
63
|
+
offset += chunk.byteLength;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Apply transformations
|
|
67
|
+
const result = yield* applyTransformationChain(
|
|
68
|
+
imageService,
|
|
69
|
+
inputBuffer,
|
|
70
|
+
transformations,
|
|
71
|
+
);
|
|
72
|
+
return Stream.make(result);
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Apply each transformation in sequence using streaming
|
|
77
|
+
return Effect.reduce(
|
|
78
|
+
transformations,
|
|
79
|
+
inputStream,
|
|
80
|
+
(currentStream, transformation) =>
|
|
81
|
+
Effect.flatMap(
|
|
82
|
+
transformStreamFn(currentStream, transformation),
|
|
83
|
+
(outputStream) => Effect.succeed(outputStream),
|
|
84
|
+
),
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
29
88
|
/**
|
|
30
89
|
* Creates a transform image node that applies multiple transformations sequentially.
|
|
31
90
|
*
|
|
@@ -33,24 +92,32 @@ function applyTransformationChain(
|
|
|
33
92
|
* together. Each transformation is applied to the output of the previous transformation,
|
|
34
93
|
* allowing for powerful image manipulation pipelines.
|
|
35
94
|
*
|
|
95
|
+
* Supports both buffered and streaming modes for memory-efficient processing
|
|
96
|
+
* of large images. In streaming mode, each transformation is applied in sequence
|
|
97
|
+
* using streaming where supported.
|
|
98
|
+
*
|
|
36
99
|
* Supported transformations include:
|
|
37
100
|
* - Basic: resize, blur, rotate, flip
|
|
38
101
|
* - Filters: grayscale, sepia, brightness, contrast
|
|
39
102
|
* - Effects: sharpen
|
|
40
|
-
* - Advanced: watermark, logo, text
|
|
103
|
+
* - Advanced: watermark, logo, text (streaming not supported for these)
|
|
41
104
|
*
|
|
42
105
|
* Note: Watermark and logo transformations require imagePath to be a valid URL.
|
|
43
106
|
* Images will be fetched from the provided URL during transformation.
|
|
107
|
+
* Streaming mode is not supported for watermark, logo, and text transformations;
|
|
108
|
+
* these will cause fallback to buffered mode.
|
|
44
109
|
*
|
|
45
110
|
* @param id - Unique identifier for this node
|
|
46
111
|
* @param params - Parameters including the transformations array
|
|
47
112
|
* @param options - Optional configuration
|
|
48
113
|
* @param options.keepOutput - Whether to keep output in flow results
|
|
49
114
|
* @param options.naming - File naming configuration (auto suffix: `transformed`)
|
|
115
|
+
* @param options.mode - Transform mode: "buffered", "streaming", or "auto" (default)
|
|
116
|
+
* @param options.streamingConfig - Streaming configuration (file size threshold, chunk size)
|
|
50
117
|
*
|
|
51
118
|
* @example
|
|
52
119
|
* ```typescript
|
|
53
|
-
* //
|
|
120
|
+
* // Auto mode (default) - uses streaming for files > 1MB, otherwise buffered
|
|
54
121
|
* const node = yield* createTransformImageNode("transform-1", {
|
|
55
122
|
* transformations: [
|
|
56
123
|
* { type: 'resize', width: 800, height: 600, fit: 'cover' },
|
|
@@ -59,16 +126,57 @@ function applyTransformationChain(
|
|
|
59
126
|
* }, {
|
|
60
127
|
* naming: { mode: "auto" }
|
|
61
128
|
* });
|
|
129
|
+
*
|
|
130
|
+
* // Force buffered mode for small files
|
|
131
|
+
* const nodeBuffered = yield* createTransformImageNode("transform-2", {
|
|
132
|
+
* transformations: [
|
|
133
|
+
* { type: 'resize', width: 800, height: 600, fit: 'cover' },
|
|
134
|
+
* { type: 'blur', sigma: 5 }
|
|
135
|
+
* ]
|
|
136
|
+
* }, {
|
|
137
|
+
* mode: "buffered",
|
|
138
|
+
* naming: { mode: "auto" }
|
|
139
|
+
* });
|
|
140
|
+
*
|
|
141
|
+
* // Force streaming mode for memory efficiency
|
|
142
|
+
* const nodeStreaming = yield* createTransformImageNode("transform-3", {
|
|
143
|
+
* transformations: [{ type: 'grayscale' }]
|
|
144
|
+
* }, {
|
|
145
|
+
* mode: "streaming",
|
|
146
|
+
* naming: { mode: "auto" }
|
|
147
|
+
* });
|
|
62
148
|
* ```
|
|
63
149
|
*/
|
|
64
150
|
export function createTransformImageNode(
|
|
65
151
|
id: string,
|
|
66
152
|
{ transformations }: TransformImageParams,
|
|
67
|
-
options?: {
|
|
153
|
+
options?: {
|
|
154
|
+
keepOutput?: boolean;
|
|
155
|
+
naming?: FileNamingConfig;
|
|
156
|
+
mode?: TransformMode;
|
|
157
|
+
streamingConfig?: StreamingConfig;
|
|
158
|
+
},
|
|
68
159
|
) {
|
|
69
160
|
return Effect.gen(function* () {
|
|
70
161
|
const imageService = yield* ImagePlugin;
|
|
71
162
|
|
|
163
|
+
// Determine if streaming is available and requested
|
|
164
|
+
const supportsStreaming = imageService.supportsStreaming ?? false;
|
|
165
|
+
const requestedMode = options?.mode ?? "auto";
|
|
166
|
+
|
|
167
|
+
// Check if any transformations don't support streaming
|
|
168
|
+
const hasUnsupportedTransformations = transformations.some(
|
|
169
|
+
(t) => t.type === "watermark" || t.type === "logo" || t.type === "text",
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
// If streaming requested but not supported or unsupported transformations, fall back to buffered
|
|
173
|
+
const effectiveMode: TransformMode =
|
|
174
|
+
requestedMode === "buffered"
|
|
175
|
+
? "buffered"
|
|
176
|
+
: supportsStreaming && !hasUnsupportedTransformations
|
|
177
|
+
? requestedMode
|
|
178
|
+
: "buffered";
|
|
179
|
+
|
|
72
180
|
// Build naming config with auto suffix for transform-image
|
|
73
181
|
const namingConfig: FileNamingConfig | undefined = options?.naming
|
|
74
182
|
? {
|
|
@@ -86,8 +194,25 @@ export function createTransformImageNode(
|
|
|
86
194
|
keepOutput: options?.keepOutput,
|
|
87
195
|
naming: namingConfig,
|
|
88
196
|
nodeType: "transform-image",
|
|
197
|
+
mode: effectiveMode,
|
|
198
|
+
streamingConfig: options?.streamingConfig,
|
|
199
|
+
// Buffered transform (used when mode is "buffered" or "auto" selects buffered)
|
|
89
200
|
transform: (inputBytes) =>
|
|
90
201
|
applyTransformationChain(imageService, inputBytes, transformations),
|
|
202
|
+
// Streaming transform (used when mode is "streaming" or "auto" selects streaming)
|
|
203
|
+
streamingTransform:
|
|
204
|
+
supportsStreaming && !hasUnsupportedTransformations
|
|
205
|
+
? (inputStream) =>
|
|
206
|
+
Effect.gen(function* () {
|
|
207
|
+
const outputStream =
|
|
208
|
+
yield* applyStreamingTransformationChain(
|
|
209
|
+
imageService,
|
|
210
|
+
inputStream,
|
|
211
|
+
transformations,
|
|
212
|
+
);
|
|
213
|
+
return { stream: outputStream };
|
|
214
|
+
})
|
|
215
|
+
: undefined,
|
|
91
216
|
});
|
|
92
217
|
});
|
|
93
218
|
}
|