@uploadista/flow-images-sharp 0.0.3
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/.turbo/turbo-build.log +5 -0
- package/LICENSE +21 -0
- package/README.md +397 -0
- package/dist/image-plugin.d.ts +4 -0
- package/dist/image-plugin.d.ts.map +1 -0
- package/dist/image-plugin.js +46 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1 -0
- package/package.json +33 -0
- package/src/image-plugin.ts +57 -0
- package/src/index.ts +1 -0
- package/tsconfig.json +14 -0
- package/tsconfig.tsbuildinfo +1 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 uploadista
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
# @uploadista/flow-images-sharp
|
|
2
|
+
|
|
3
|
+
Sharp-based image processing for Uploadista flows. High-performance image operations on Node.js servers.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
Sharp provides fast, production-grade image processing:
|
|
8
|
+
|
|
9
|
+
- **Resizing**: Scale images with smart cropping
|
|
10
|
+
- **Optimization**: Reduce size while preserving quality
|
|
11
|
+
- **Format Conversion**: Convert between formats (JPEG, PNG, WebP, AVIF)
|
|
12
|
+
- **Effects**: Blur, rotate, extract regions
|
|
13
|
+
- **Metadata**: Extract EXIF and image information
|
|
14
|
+
|
|
15
|
+
Perfect for Node.js and Fastify/Express servers.
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm install @uploadista/flow-images-sharp sharp
|
|
21
|
+
# or
|
|
22
|
+
pnpm add @uploadista/flow-images-sharp sharp
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### Prerequisites
|
|
26
|
+
|
|
27
|
+
- Node.js 18+
|
|
28
|
+
- libvips (usually installed with sharp)
|
|
29
|
+
|
|
30
|
+
## Quick Start
|
|
31
|
+
|
|
32
|
+
```typescript
|
|
33
|
+
import { imagePlugin } from "@uploadista/flow-images-sharp";
|
|
34
|
+
import { Effect } from "effect";
|
|
35
|
+
|
|
36
|
+
const program = Effect.gen(function* () {
|
|
37
|
+
const plugin = yield* imagePlugin;
|
|
38
|
+
|
|
39
|
+
// Resize image
|
|
40
|
+
const resized = yield* plugin.resize(inputBytes, {
|
|
41
|
+
width: 800,
|
|
42
|
+
height: 600,
|
|
43
|
+
fit: "cover",
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// Optimize
|
|
47
|
+
const optimized = yield* plugin.optimize(inputBytes, {
|
|
48
|
+
quality: 85,
|
|
49
|
+
format: "webp",
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
Effect.runSync(program.pipe(Effect.provide(imagePlugin)));
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Features
|
|
57
|
+
|
|
58
|
+
- ✅ **Fast Processing**: libvips backend (10x faster than ImageMagick)
|
|
59
|
+
- ✅ **Multiple Formats**: JPEG, PNG, WebP, AVIF, TIFF, GIF
|
|
60
|
+
- ✅ **Smart Resizing**: Multiple fit strategies
|
|
61
|
+
- ✅ **Metadata Extraction**: EXIF and image properties
|
|
62
|
+
- ✅ **Effects**: Blur, sharpen, normalize
|
|
63
|
+
- ✅ **Streaming**: Memory-efficient processing
|
|
64
|
+
|
|
65
|
+
## API Reference
|
|
66
|
+
|
|
67
|
+
### Main Exports
|
|
68
|
+
|
|
69
|
+
#### `imagePlugin: Layer<ImagePlugin>`
|
|
70
|
+
|
|
71
|
+
Pre-configured Sharp image processing layer.
|
|
72
|
+
|
|
73
|
+
```typescript
|
|
74
|
+
import { imagePlugin } from "@uploadista/flow-images-sharp";
|
|
75
|
+
|
|
76
|
+
const layer = imagePlugin;
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Available Operations
|
|
80
|
+
|
|
81
|
+
#### `resize(input, options): Effect<Uint8Array>`
|
|
82
|
+
|
|
83
|
+
Scale image to dimensions.
|
|
84
|
+
|
|
85
|
+
```typescript
|
|
86
|
+
interface ResizeOptions {
|
|
87
|
+
width?: number;
|
|
88
|
+
height?: number;
|
|
89
|
+
fit: "cover" | "contain" | "fill";
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const resized = yield* plugin.resize(imageBytes, {
|
|
93
|
+
width: 800,
|
|
94
|
+
height: 600,
|
|
95
|
+
fit: "cover",
|
|
96
|
+
});
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
#### `optimize(input, options): Effect<Uint8Array>`
|
|
100
|
+
|
|
101
|
+
Compress and convert image.
|
|
102
|
+
|
|
103
|
+
```typescript
|
|
104
|
+
interface OptimizeOptions {
|
|
105
|
+
quality: 1-100;
|
|
106
|
+
format: "jpeg" | "png" | "webp" | "avif";
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const optimized = yield* plugin.optimize(imageBytes, {
|
|
110
|
+
quality: 85,
|
|
111
|
+
format: "webp",
|
|
112
|
+
});
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Flow Configuration
|
|
116
|
+
|
|
117
|
+
### Basic Resize
|
|
118
|
+
|
|
119
|
+
```typescript
|
|
120
|
+
{
|
|
121
|
+
nodes: [
|
|
122
|
+
{ id: "input", type: "input" },
|
|
123
|
+
{
|
|
124
|
+
id: "resize",
|
|
125
|
+
type: "resize",
|
|
126
|
+
params: { width: 1200, height: 800, fit: "cover" },
|
|
127
|
+
},
|
|
128
|
+
{ id: "store", type: "s3" },
|
|
129
|
+
{ id: "output", type: "output" },
|
|
130
|
+
],
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### Multi-Size Variants
|
|
135
|
+
|
|
136
|
+
```typescript
|
|
137
|
+
{
|
|
138
|
+
nodes: [
|
|
139
|
+
{ id: "input", type: "input" },
|
|
140
|
+
{
|
|
141
|
+
id: "split",
|
|
142
|
+
type: "multiplex",
|
|
143
|
+
params: { outputCount: 3 },
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
id: "thumb",
|
|
147
|
+
type: "resize",
|
|
148
|
+
params: { width: 200, height: 200, fit: "cover" },
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
id: "medium",
|
|
152
|
+
type: "resize",
|
|
153
|
+
params: { width: 800, height: 600, fit: "contain" },
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
id: "full",
|
|
157
|
+
type: "optimize",
|
|
158
|
+
params: { quality: 90, format: "webp" },
|
|
159
|
+
},
|
|
160
|
+
{ id: "store", type: "s3" },
|
|
161
|
+
{ id: "output", type: "output" },
|
|
162
|
+
],
|
|
163
|
+
}
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
## Examples
|
|
167
|
+
|
|
168
|
+
### Example 1: Thumbnail Generation
|
|
169
|
+
|
|
170
|
+
```typescript
|
|
171
|
+
const flow = {
|
|
172
|
+
nodes: [
|
|
173
|
+
{ id: "input", type: "input" },
|
|
174
|
+
{
|
|
175
|
+
id: "thumbnail",
|
|
176
|
+
type: "resize",
|
|
177
|
+
params: { width: 200, height: 200, fit: "cover" },
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
id: "optimize",
|
|
181
|
+
type: "optimize",
|
|
182
|
+
params: { quality: 80, format: "webp" },
|
|
183
|
+
},
|
|
184
|
+
{ id: "s3", type: "s3", params: { prefix: "thumbnails/" } },
|
|
185
|
+
{ id: "output", type: "output" },
|
|
186
|
+
],
|
|
187
|
+
edges: [
|
|
188
|
+
{ from: "input", to: "thumbnail" },
|
|
189
|
+
{ from: "thumbnail", to: "optimize" },
|
|
190
|
+
{ from: "optimize", to: "s3" },
|
|
191
|
+
{ from: "s3", to: "output" },
|
|
192
|
+
],
|
|
193
|
+
};
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
### Example 2: Responsive Images
|
|
197
|
+
|
|
198
|
+
```typescript
|
|
199
|
+
const responsiveFlow = {
|
|
200
|
+
nodes: [
|
|
201
|
+
{ id: "input", type: "input" },
|
|
202
|
+
{
|
|
203
|
+
id: "split",
|
|
204
|
+
type: "multiplex",
|
|
205
|
+
params: { outputCount: 4 },
|
|
206
|
+
},
|
|
207
|
+
// Mobile
|
|
208
|
+
{
|
|
209
|
+
id: "mobile",
|
|
210
|
+
type: "resize",
|
|
211
|
+
params: { width: 400, height: 300, fit: "cover" },
|
|
212
|
+
},
|
|
213
|
+
// Tablet
|
|
214
|
+
{
|
|
215
|
+
id: "tablet",
|
|
216
|
+
type: "resize",
|
|
217
|
+
params: { width: 800, height: 600, fit: "contain" },
|
|
218
|
+
},
|
|
219
|
+
// Desktop
|
|
220
|
+
{
|
|
221
|
+
id: "desktop",
|
|
222
|
+
type: "resize",
|
|
223
|
+
params: { width: 1200, height: 900, fit: "cover" },
|
|
224
|
+
},
|
|
225
|
+
// High-DPI
|
|
226
|
+
{
|
|
227
|
+
id: "retina",
|
|
228
|
+
type: "resize",
|
|
229
|
+
params: { width: 2400, height: 1800, fit: "cover" },
|
|
230
|
+
},
|
|
231
|
+
{ id: "s3", type: "s3" },
|
|
232
|
+
{ id: "output", type: "output" },
|
|
233
|
+
],
|
|
234
|
+
};
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
### Example 3: Format Conversion
|
|
238
|
+
|
|
239
|
+
```typescript
|
|
240
|
+
const conversionFlow = {
|
|
241
|
+
nodes: [
|
|
242
|
+
{ id: "input", type: "input" },
|
|
243
|
+
{
|
|
244
|
+
id: "convert",
|
|
245
|
+
type: "optimize",
|
|
246
|
+
params: { quality: 85, format: "webp" },
|
|
247
|
+
},
|
|
248
|
+
{ id: "s3", type: "s3" },
|
|
249
|
+
{ id: "output", type: "output" },
|
|
250
|
+
],
|
|
251
|
+
};
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
## Performance
|
|
255
|
+
|
|
256
|
+
| Operation | Time (1MB image) |
|
|
257
|
+
|-----------|-----------------|
|
|
258
|
+
| Resize to 800x600 | ~50-100ms |
|
|
259
|
+
| Optimize (WebP) | ~100-200ms |
|
|
260
|
+
| Convert JPEG→WebP | ~150-250ms |
|
|
261
|
+
| Extract metadata | ~10-20ms |
|
|
262
|
+
|
|
263
|
+
Performance scales linearly with image size.
|
|
264
|
+
|
|
265
|
+
## Best Practices
|
|
266
|
+
|
|
267
|
+
### 1. Choose Right Fit Strategy
|
|
268
|
+
|
|
269
|
+
```typescript
|
|
270
|
+
// cover: Crop to fill (best for thumbnails)
|
|
271
|
+
{ fit: "cover" }
|
|
272
|
+
|
|
273
|
+
// contain: Fit inside, preserve aspect (best for previews)
|
|
274
|
+
{ fit: "contain" }
|
|
275
|
+
|
|
276
|
+
// fill: Stretch to fill (avoid unless needed)
|
|
277
|
+
{ fit: "fill" }
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
### 2. Optimize Quality by Format
|
|
281
|
+
|
|
282
|
+
```typescript
|
|
283
|
+
// JPEG: 80-85 quality (good balance)
|
|
284
|
+
{ quality: 85, format: "jpeg" }
|
|
285
|
+
|
|
286
|
+
// WebP: 80-90 quality (better compression)
|
|
287
|
+
{ quality: 85, format: "webp" }
|
|
288
|
+
|
|
289
|
+
// AVIF: 75-80 quality (excellent compression)
|
|
290
|
+
{ quality: 80, format: "avif" }
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
### 3. Create Multiple Sizes
|
|
294
|
+
|
|
295
|
+
```typescript
|
|
296
|
+
// Mobile, tablet, desktop
|
|
297
|
+
[
|
|
298
|
+
{ width: 400 },
|
|
299
|
+
{ width: 800 },
|
|
300
|
+
{ width: 1200 },
|
|
301
|
+
{ width: 2400 }, // @2x for retina
|
|
302
|
+
]
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
## Deployment
|
|
306
|
+
|
|
307
|
+
### Docker
|
|
308
|
+
|
|
309
|
+
```dockerfile
|
|
310
|
+
FROM node:18-alpine
|
|
311
|
+
|
|
312
|
+
# Sharp needs libvips
|
|
313
|
+
RUN apk add --no-cache vips-dev
|
|
314
|
+
|
|
315
|
+
WORKDIR /app
|
|
316
|
+
COPY . .
|
|
317
|
+
RUN npm ci --omit=dev && npm run build
|
|
318
|
+
|
|
319
|
+
CMD ["node", "dist/server.js"]
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
### Express Server
|
|
323
|
+
|
|
324
|
+
```typescript
|
|
325
|
+
import { imagePlugin } from "@uploadista/flow-images-sharp";
|
|
326
|
+
|
|
327
|
+
app.post("/resize", async (req, res) => {
|
|
328
|
+
const imageBytes = req.body; // image data
|
|
329
|
+
|
|
330
|
+
const program = Effect.gen(function* () {
|
|
331
|
+
const plugin = yield* imagePlugin;
|
|
332
|
+
return yield* plugin.resize(imageBytes, {
|
|
333
|
+
width: 800,
|
|
334
|
+
height: 600,
|
|
335
|
+
fit: "cover",
|
|
336
|
+
});
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
const resized = await Effect.runPromise(
|
|
340
|
+
program.pipe(Effect.provide(imagePlugin))
|
|
341
|
+
);
|
|
342
|
+
res.send(Buffer.from(resized));
|
|
343
|
+
});
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
## Troubleshooting
|
|
347
|
+
|
|
348
|
+
### "Cannot find module 'sharp'"
|
|
349
|
+
|
|
350
|
+
Install sharp:
|
|
351
|
+
```bash
|
|
352
|
+
npm install sharp
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
### "libvips not found"
|
|
356
|
+
|
|
357
|
+
Install system dependency:
|
|
358
|
+
```bash
|
|
359
|
+
# macOS
|
|
360
|
+
brew install vips
|
|
361
|
+
|
|
362
|
+
# Ubuntu
|
|
363
|
+
apt-get install libvips-dev
|
|
364
|
+
|
|
365
|
+
# Alpine
|
|
366
|
+
apk add vips-dev
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
### Out of Memory
|
|
370
|
+
|
|
371
|
+
Sharp processes images in memory. For large files:
|
|
372
|
+
|
|
373
|
+
```typescript
|
|
374
|
+
// Limit by reducing quality/format
|
|
375
|
+
{ quality: 60, format: "jpeg" }
|
|
376
|
+
|
|
377
|
+
// Or pre-resize first
|
|
378
|
+
const preResized = yield* plugin.resize(input, { width: 4000 });
|
|
379
|
+
const final = yield* plugin.optimize(preResized, { quality: 85 });
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
## Related Packages
|
|
383
|
+
|
|
384
|
+
- [@uploadista/flow-images-nodes](../nodes) - Base image types
|
|
385
|
+
- [@uploadista/flow-images-photon](../photon) - Edge processing
|
|
386
|
+
- [@uploadista/flow-utility-zipjs](../../utility/zipjs) - Archive
|
|
387
|
+
- [@uploadista/server](../../servers/server) - Upload server
|
|
388
|
+
|
|
389
|
+
## License
|
|
390
|
+
|
|
391
|
+
See [LICENSE](../../../LICENSE) in the main repository.
|
|
392
|
+
|
|
393
|
+
## See Also
|
|
394
|
+
|
|
395
|
+
- [Sharp Documentation](https://sharp.pixelplumbing.com/) - Official Sharp docs
|
|
396
|
+
- [FLOW_NODES.md](../FLOW_NODES.md) - All available nodes
|
|
397
|
+
- [Photon Node](../photon/README.md) - Edge alternative
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"image-plugin.d.ts","sourceRoot":"","sources":["../src/image-plugin.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AACpD,OAAO,EAAU,KAAK,EAAE,MAAM,QAAQ,CAAC;AAYvC,eAAO,MAAM,WAAW,wCA0CvB,CAAC"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { UploadistaError } from "@uploadista/core/errors";
|
|
2
|
+
import { ImagePlugin } from "@uploadista/core/flow";
|
|
3
|
+
import { Effect, Layer } from "effect";
|
|
4
|
+
import sharp from "sharp";
|
|
5
|
+
const mapFitToSharp = (fit) => {
|
|
6
|
+
switch (fit) {
|
|
7
|
+
case "fill":
|
|
8
|
+
return "cover";
|
|
9
|
+
case "contain":
|
|
10
|
+
return "contain";
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
export const imagePlugin = Layer.succeed(ImagePlugin, ImagePlugin.of({
|
|
14
|
+
optimize: (inputBytes, { quality, format }) => {
|
|
15
|
+
return Effect.gen(function* () {
|
|
16
|
+
const outputBytes = yield* Effect.tryPromise({
|
|
17
|
+
try: async () => await sharp(inputBytes).toFormat(format, { quality }).toBuffer(),
|
|
18
|
+
catch: (error) => {
|
|
19
|
+
return UploadistaError.fromCode("UNKNOWN_ERROR", {
|
|
20
|
+
cause: error,
|
|
21
|
+
});
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
return new Uint8Array(outputBytes);
|
|
25
|
+
});
|
|
26
|
+
},
|
|
27
|
+
resize: (inputBytes, { width, height, fit }) => {
|
|
28
|
+
return Effect.gen(function* () {
|
|
29
|
+
if (!width && !height) {
|
|
30
|
+
throw new Error("Either width or height must be specified for resize");
|
|
31
|
+
}
|
|
32
|
+
const sharpFit = mapFitToSharp(fit);
|
|
33
|
+
const outputBytes = yield* Effect.tryPromise({
|
|
34
|
+
try: async () => await sharp(inputBytes)
|
|
35
|
+
.resize(width, height, { fit: sharpFit })
|
|
36
|
+
.toBuffer(),
|
|
37
|
+
catch: (error) => {
|
|
38
|
+
return UploadistaError.fromCode("UNKNOWN_ERROR", {
|
|
39
|
+
cause: error,
|
|
40
|
+
});
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
return new Uint8Array(outputBytes);
|
|
44
|
+
});
|
|
45
|
+
},
|
|
46
|
+
}));
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,gBAAgB,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./image-plugin";
|
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@uploadista/flow-images-sharp",
|
|
3
|
+
"type": "module",
|
|
4
|
+
"version": "0.0.3",
|
|
5
|
+
"description": "Sharp image processing service for Uploadista Flow",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "Uploadista",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js",
|
|
12
|
+
"default": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"sharp": "0.34.4",
|
|
17
|
+
"effect": "3.18.4",
|
|
18
|
+
"tinycolor2": "1.6.0",
|
|
19
|
+
"zod": "4.1.12",
|
|
20
|
+
"@uploadista/core": "0.0.3"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"@types/node": "24.8.1",
|
|
24
|
+
"@types/tinycolor2": "1.4.6",
|
|
25
|
+
"@uploadista/typescript-config": "0.0.3"
|
|
26
|
+
},
|
|
27
|
+
"scripts": {
|
|
28
|
+
"build": "tsc -b",
|
|
29
|
+
"format": "biome format --write ./src",
|
|
30
|
+
"lint": "biome lint --write ./src",
|
|
31
|
+
"check": "biome check --write ./src"
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { UploadistaError } from "@uploadista/core/errors";
|
|
2
|
+
import { ImagePlugin } from "@uploadista/core/flow";
|
|
3
|
+
import { Effect, Layer } from "effect";
|
|
4
|
+
import sharp from "sharp";
|
|
5
|
+
|
|
6
|
+
const mapFitToSharp = (fit: "fill" | "contain" | "cover") => {
|
|
7
|
+
switch (fit) {
|
|
8
|
+
case "fill":
|
|
9
|
+
return "cover";
|
|
10
|
+
case "contain":
|
|
11
|
+
return "contain";
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export const imagePlugin = Layer.succeed(
|
|
16
|
+
ImagePlugin,
|
|
17
|
+
ImagePlugin.of({
|
|
18
|
+
optimize: (inputBytes, { quality, format }) => {
|
|
19
|
+
return Effect.gen(function* () {
|
|
20
|
+
const outputBytes = yield* Effect.tryPromise({
|
|
21
|
+
try: async () =>
|
|
22
|
+
await sharp(inputBytes).toFormat(format, { quality }).toBuffer(),
|
|
23
|
+
catch: (error) => {
|
|
24
|
+
return UploadistaError.fromCode("UNKNOWN_ERROR", {
|
|
25
|
+
cause: error,
|
|
26
|
+
});
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
return new Uint8Array(outputBytes);
|
|
30
|
+
});
|
|
31
|
+
},
|
|
32
|
+
resize: (inputBytes, { width, height, fit }) => {
|
|
33
|
+
return Effect.gen(function* () {
|
|
34
|
+
if (!width && !height) {
|
|
35
|
+
throw new Error(
|
|
36
|
+
"Either width or height must be specified for resize",
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const sharpFit = mapFitToSharp(fit);
|
|
41
|
+
const outputBytes = yield* Effect.tryPromise({
|
|
42
|
+
try: async () =>
|
|
43
|
+
await sharp(inputBytes)
|
|
44
|
+
.resize(width, height, { fit: sharpFit })
|
|
45
|
+
.toBuffer(),
|
|
46
|
+
catch: (error) => {
|
|
47
|
+
return UploadistaError.fromCode("UNKNOWN_ERROR", {
|
|
48
|
+
cause: error,
|
|
49
|
+
});
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
return new Uint8Array(outputBytes);
|
|
54
|
+
});
|
|
55
|
+
},
|
|
56
|
+
}),
|
|
57
|
+
);
|
package/src/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./image-plugin";
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "@uploadista/typescript-config/server.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"baseUrl": "./",
|
|
5
|
+
"paths": {
|
|
6
|
+
"@/*": ["./src/*"]
|
|
7
|
+
},
|
|
8
|
+
"outDir": "./dist",
|
|
9
|
+
"rootDir": "./src",
|
|
10
|
+
"lib": ["ESNext", "DOM", "DOM.Iterable"],
|
|
11
|
+
"types": []
|
|
12
|
+
},
|
|
13
|
+
"include": ["src"]
|
|
14
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"root":["./src/image-plugin.ts","./src/index.ts"],"version":"5.9.3"}
|