@uploadista/flow-images-nodes 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.
Files changed (73) hide show
  1. package/.turbo/turbo-build.log +5 -0
  2. package/.turbo/turbo-check.log +5 -0
  3. package/LICENSE +21 -0
  4. package/README.md +117 -0
  5. package/dist/describe-image-node.d.ts +69 -0
  6. package/dist/describe-image-node.d.ts.map +1 -0
  7. package/dist/describe-image-node.js +68 -0
  8. package/dist/index.d.ts +6 -0
  9. package/dist/index.d.ts.map +1 -0
  10. package/dist/index.js +5 -0
  11. package/dist/nodes/describe-image-node.d.ts +66 -0
  12. package/dist/nodes/describe-image-node.d.ts.map +1 -0
  13. package/dist/nodes/describe-image-node.js +43 -0
  14. package/dist/nodes/index.d.ts +5 -0
  15. package/dist/nodes/index.d.ts.map +1 -0
  16. package/dist/nodes/index.js +4 -0
  17. package/dist/nodes/optimize-node.d.ts +24 -0
  18. package/dist/nodes/optimize-node.d.ts.map +1 -0
  19. package/dist/nodes/optimize-node.js +13 -0
  20. package/dist/nodes/remove-background-node.d.ts +25 -0
  21. package/dist/nodes/remove-background-node.d.ts.map +1 -0
  22. package/dist/nodes/remove-background-node.js +48 -0
  23. package/dist/nodes/resize-node.d.ts +24 -0
  24. package/dist/nodes/resize-node.d.ts.map +1 -0
  25. package/dist/nodes/resize-node.js +13 -0
  26. package/dist/optimize-node.d.ts +24 -0
  27. package/dist/optimize-node.d.ts.map +1 -0
  28. package/dist/optimize-node.js +41 -0
  29. package/dist/remove-background-node.d.ts +28 -0
  30. package/dist/remove-background-node.d.ts.map +1 -0
  31. package/dist/remove-background-node.js +84 -0
  32. package/dist/resize-node.d.ts +24 -0
  33. package/dist/resize-node.d.ts.map +1 -0
  34. package/dist/resize-node.js +13 -0
  35. package/dist/services/idnex.d.ts +2 -0
  36. package/dist/services/idnex.d.ts.map +1 -0
  37. package/dist/services/idnex.js +1 -0
  38. package/dist/services/image-service-common.d.ts +2 -0
  39. package/dist/services/image-service-common.d.ts.map +1 -0
  40. package/dist/services/image-service-common.js +1 -0
  41. package/dist/services/image-service-node.d.ts +18 -0
  42. package/dist/services/image-service-node.d.ts.map +1 -0
  43. package/dist/services/image-service-node.js +170 -0
  44. package/dist/services/image-service-serverless.d.ts +4 -0
  45. package/dist/services/image-service-serverless.d.ts.map +1 -0
  46. package/dist/services/image-service-serverless.js +153 -0
  47. package/dist/services/image-service.d.ts +13 -0
  48. package/dist/services/image-service.d.ts.map +1 -0
  49. package/dist/services/image-service.js +3 -0
  50. package/dist/services/index.d.ts +3 -0
  51. package/dist/services/index.d.ts.map +1 -0
  52. package/dist/services/index.js +3 -0
  53. package/dist/types/index.d.ts +3 -0
  54. package/dist/types/index.d.ts.map +1 -0
  55. package/dist/types/index.js +2 -0
  56. package/dist/types/optimize-node.d.ts +9 -0
  57. package/dist/types/optimize-node.d.ts.map +1 -0
  58. package/dist/types/optimize-node.js +5 -0
  59. package/dist/types/resize-node.d.ts +12 -0
  60. package/dist/types/resize-node.d.ts.map +1 -0
  61. package/dist/types/resize-node.js +8 -0
  62. package/dist/wait-for-url.d.ts +18 -0
  63. package/dist/wait-for-url.d.ts.map +1 -0
  64. package/dist/wait-for-url.js +36 -0
  65. package/package.json +30 -0
  66. package/src/describe-image-node.ts +101 -0
  67. package/src/index.ts +5 -0
  68. package/src/optimize-node.ts +61 -0
  69. package/src/remove-background-node.ts +130 -0
  70. package/src/resize-node.ts +23 -0
  71. package/src/wait-for-url.ts +52 -0
  72. package/tsconfig.json +14 -0
  73. package/tsconfig.tsbuildinfo +1 -0
@@ -0,0 +1,41 @@
1
+ import { createTransformNode, ImagePlugin, } from "@uploadista/core/flow";
2
+ import { Effect } from "effect";
3
+ // Map image format to MIME type
4
+ const formatToMimeType = {
5
+ jpeg: "image/jpeg",
6
+ webp: "image/webp",
7
+ png: "image/png",
8
+ avif: "image/avif",
9
+ };
10
+ // Map image format to file extension
11
+ const formatToExtension = {
12
+ jpeg: "jpg",
13
+ webp: "webp",
14
+ png: "png",
15
+ avif: "avif",
16
+ };
17
+ export function createOptimizeNode(id, { quality, format }) {
18
+ return Effect.gen(function* () {
19
+ const imageService = yield* ImagePlugin;
20
+ return yield* createTransformNode({
21
+ id,
22
+ name: "Optimize",
23
+ description: "Optimizes an image for web delivery",
24
+ transform: (inputBytes, file) => Effect.map(imageService.optimize(inputBytes, { quality, format }), (optimizedBytes) => {
25
+ // Return bytes with updated metadata if format changes
26
+ const newType = formatToMimeType[format];
27
+ const newExtension = formatToExtension[format];
28
+ // Update file extension if format changed
29
+ const fileName = file.metadata?.fileName;
30
+ const newFileName = fileName && typeof fileName === "string"
31
+ ? fileName.replace(/\.[^.]+$/, `.${newExtension}`)
32
+ : undefined;
33
+ return {
34
+ bytes: optimizedBytes,
35
+ type: newType,
36
+ fileName: newFileName,
37
+ };
38
+ }),
39
+ });
40
+ });
41
+ }
@@ -0,0 +1,28 @@
1
+ import { UploadistaError } from "@uploadista/core/errors";
2
+ import { ImageAiPlugin } from "@uploadista/core/flow";
3
+ import { UploadServer } from "@uploadista/core/upload";
4
+ import { Effect } from "effect";
5
+ export declare function createRemoveBackgroundNode(id: string, { credentialId }?: {
6
+ credentialId?: string;
7
+ }): Effect.Effect<import("@uploadista/core").FlowNode<{
8
+ id: string;
9
+ offset: number;
10
+ storage: {
11
+ id: string;
12
+ type: string;
13
+ path?: string | undefined;
14
+ uploadId?: string | undefined;
15
+ bucket?: string | undefined;
16
+ };
17
+ size?: number | undefined;
18
+ metadata?: Record<string, string | number | boolean> | undefined;
19
+ creationDate?: string | undefined;
20
+ url?: string | undefined;
21
+ sizeIsDeferred?: boolean | undefined;
22
+ flow?: {
23
+ flowId: string;
24
+ nodeId: string;
25
+ jobId: string;
26
+ } | undefined;
27
+ }, import("@uploadista/core").UploadFile, UploadistaError>, never, ImageAiPlugin | UploadServer>;
28
+ //# sourceMappingURL=remove-background-node.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"remove-background-node.d.ts","sourceRoot":"","sources":["../src/remove-background-node.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAC1D,OAAO,EAGL,aAAa,EAGd,MAAM,uBAAuB,CAAC;AAE/B,OAAO,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AACvD,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAGhC,wBAAgB,0BAA0B,CACxC,EAAE,EAAE,MAAM,EACV,EAAE,YAAY,EAAE,GAAE;IAAE,YAAY,CAAC,EAAE,MAAM,CAAA;CAAO;;;;;;;;;;;;;;;;;;;;iGAkHjD"}
@@ -0,0 +1,84 @@
1
+ import { UploadistaError } from "@uploadista/core/errors";
2
+ import { completeNodeExecution, createFlowNode, ImageAiPlugin, NodeType, resolveUploadMetadata, } from "@uploadista/core/flow";
3
+ import { uploadFileSchema } from "@uploadista/core/types";
4
+ import { UploadServer } from "@uploadista/core/upload";
5
+ import { Effect } from "effect";
6
+ import { waitForUrlAvailability } from "./wait-for-url";
7
+ export function createRemoveBackgroundNode(id, { credentialId } = {}) {
8
+ return Effect.gen(function* () {
9
+ const imageAiService = yield* ImageAiPlugin;
10
+ const uploadServer = yield* UploadServer;
11
+ return yield* createFlowNode({
12
+ id,
13
+ name: "Remove Background",
14
+ description: "Removes the background from an image",
15
+ type: NodeType.process,
16
+ inputSchema: uploadFileSchema,
17
+ outputSchema: uploadFileSchema,
18
+ run: ({ data: file, flowId, jobId, storageId, clientId }) => {
19
+ return Effect.gen(function* () {
20
+ const flow = {
21
+ flowId,
22
+ nodeId: id,
23
+ jobId,
24
+ };
25
+ const fileUrl = file.url;
26
+ // Validate input
27
+ if (!fileUrl) {
28
+ return yield* UploadistaError.fromCode("FLOW_NODE_ERROR", {
29
+ cause: "URL is required for remove background operation",
30
+ }).toEffect();
31
+ }
32
+ yield* Effect.logInfo(`Removing background for file ${file.id} at URL: ${file.url}`);
33
+ // Wait for URL to be available with retry mechanism
34
+ yield* waitForUrlAvailability(fileUrl);
35
+ // Build context for ImageAI plugin
36
+ const context = {
37
+ clientId,
38
+ credentialId,
39
+ };
40
+ // Remove background with error handling
41
+ const backgroundRemovalResult = yield* imageAiService
42
+ .removeBackground(fileUrl, context)
43
+ .pipe(Effect.catchAll((error) => Effect.gen(function* () {
44
+ yield* Effect.logError("Failed to remove background", error);
45
+ return yield* UploadistaError.fromCode("FLOW_NODE_ERROR", {
46
+ cause: error instanceof Error
47
+ ? error.message
48
+ : "Failed to remove background from image",
49
+ }).toEffect();
50
+ })));
51
+ const { outputUrl } = backgroundRemovalResult;
52
+ const { type, fileName, metadata, metadataJson } = resolveUploadMetadata(file.metadata);
53
+ yield* Effect.logInfo(`Uploading processed file to storage`);
54
+ // Upload the transformed bytes back to the upload server with error handling
55
+ const result = yield* uploadServer
56
+ .uploadFromUrl({
57
+ storageId,
58
+ size: 0,
59
+ type,
60
+ fileName,
61
+ lastModified: 0,
62
+ metadata: metadataJson,
63
+ flow,
64
+ }, clientId, outputUrl)
65
+ .pipe(Effect.catchAll((error) => Effect.gen(function* () {
66
+ yield* Effect.logError("Failed to upload processed file", error);
67
+ return yield* UploadistaError.fromCode("FLOW_NODE_ERROR", {
68
+ cause: error instanceof Error
69
+ ? error.message
70
+ : "Failed to upload processed file",
71
+ }).toEffect();
72
+ })));
73
+ yield* Effect.logInfo(`Successfully removed background for file ${file.id}`);
74
+ return completeNodeExecution(metadata
75
+ ? {
76
+ ...result,
77
+ metadata,
78
+ }
79
+ : result);
80
+ });
81
+ },
82
+ });
83
+ });
84
+ }
@@ -0,0 +1,24 @@
1
+ import { ImagePlugin, type ResizeParams } from "@uploadista/core/flow";
2
+ import { Effect } from "effect";
3
+ export declare function createResizeNode(id: string, { width, height, fit }: ResizeParams): Effect.Effect<import("@uploadista/core").FlowNode<{
4
+ id: string;
5
+ offset: number;
6
+ storage: {
7
+ id: string;
8
+ type: string;
9
+ path?: string | undefined;
10
+ uploadId?: string | undefined;
11
+ bucket?: string | undefined;
12
+ };
13
+ size?: number | undefined;
14
+ metadata?: Record<string, string | number | boolean> | undefined;
15
+ creationDate?: string | undefined;
16
+ url?: string | undefined;
17
+ sizeIsDeferred?: boolean | undefined;
18
+ flow?: {
19
+ flowId: string;
20
+ nodeId: string;
21
+ jobId: string;
22
+ } | undefined;
23
+ }, import("@uploadista/core").UploadFile, import("@uploadista/core").UploadistaError>, never, ImagePlugin | import("@uploadista/core").UploadServer>;
24
+ //# sourceMappingURL=resize-node.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resize-node.d.ts","sourceRoot":"","sources":["../src/resize-node.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,WAAW,EACX,KAAK,YAAY,EAClB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAEhC,wBAAgB,gBAAgB,CAC9B,EAAE,EAAE,MAAM,EACV,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,EAAE,YAAY;;;;;;YAc8W,CAAC;gBAAsC,CAAC;cAAoC,CAAC;;;;;;;;;;;;qJADhe"}
@@ -0,0 +1,13 @@
1
+ import { createTransformNode, ImagePlugin, } from "@uploadista/core/flow";
2
+ import { Effect } from "effect";
3
+ export function createResizeNode(id, { width, height, fit }) {
4
+ return Effect.gen(function* () {
5
+ const imageService = yield* ImagePlugin;
6
+ return yield* createTransformNode({
7
+ id,
8
+ name: "Resize",
9
+ description: "Resizes an image to the specified dimensions",
10
+ transform: (inputBytes) => imageService.resize(inputBytes, { height, width, fit }),
11
+ });
12
+ });
13
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=idnex.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"idnex.d.ts","sourceRoot":"","sources":["../../src/services/idnex.ts"],"names":[],"mappings":""}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=image-service-common.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"image-service-common.d.ts","sourceRoot":"","sources":["../../src/services/image-service-common.ts"],"names":[],"mappings":""}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,18 @@
1
+ import { PhotonImage, Rgba } from "@cf-wasm/photon/node";
2
+ import { ImagePlugin } from "@uploadista/core/flow";
3
+ import { Layer } from "effect";
4
+ import type tinycolor from "tinycolor2";
5
+ export declare const calculateImageSize: (objectFit: "contain" | "cover" | "fill" | "none" | "scale-down", currentWidth: number, currentHeight: number, containerWidth: number, containerHeight: number) => {
6
+ width: number;
7
+ height: number;
8
+ };
9
+ export declare const tinyColorToPhotonRGBA: (color: tinycolor.Instance) => Rgba;
10
+ export declare const autoResize: (image: PhotonImage, newWidth?: number, newHeight?: number, options?: {
11
+ fit?: "contain" | "cover" | "fill";
12
+ fit_cover_letterbox_color?: tinycolor.Instance;
13
+ }) => PhotonImage;
14
+ export declare const imageToFormat: (image: PhotonImage, format: "webp" | "jpeg" | "png", options?: {
15
+ jpeg_quality?: number;
16
+ }) => Uint8Array<ArrayBufferLike>;
17
+ export declare const imageNode: Layer.Layer<ImagePlugin, never, never>;
18
+ //# sourceMappingURL=image-service-node.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"image-service-node.d.ts","sourceRoot":"","sources":["../../src/services/image-service-node.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,WAAW,EAKX,IAAI,EAEL,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AACpD,OAAO,EAAU,KAAK,EAAE,MAAM,QAAQ,CAAC;AACvC,OAAO,KAAK,SAAS,MAAM,YAAY,CAAC;AAKxC,eAAO,MAAM,kBAAkB,GAC7B,WAAW,SAAS,GAAG,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,YAAY,EAC/D,cAAc,MAAM,EACpB,eAAe,MAAM,EACrB,gBAAgB,MAAM,EACtB,iBAAiB,MAAM;;;CAsFxB,CAAC;AAEF,eAAO,MAAM,qBAAqB,GAAI,OAAO,SAAS,CAAC,QAAQ,SAG9D,CAAC;AAEF,eAAO,MAAM,UAAU,GACrB,OAAO,WAAW,EAClB,WAAW,MAAM,EACjB,YAAY,MAAM,EAClB,UAAU;IACR,GAAG,CAAC,EAAE,SAAS,GAAG,OAAO,GAAG,MAAM,CAAC;IACnC,yBAAyB,CAAC,EAAE,SAAS,CAAC,QAAQ,CAAC;CAChD,gBA4EF,CAAC;AAEF,eAAO,MAAM,aAAa,GACxB,OAAO,WAAW,EAClB,QAAQ,MAAM,GAAG,MAAM,GAAG,KAAK,EAC/B,UAAU;IACR,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB,gCAmBF,CAAC;AAEF,eAAO,MAAM,SAAS,wCA4CrB,CAAC"}
@@ -0,0 +1,170 @@
1
+ import { crop, PhotonImage, padding_bottom, padding_left, padding_right, padding_top, Rgba, resize, } from "@cf-wasm/photon/node";
2
+ import { ImagePlugin } from "@uploadista/core/flow";
3
+ import { Effect, Layer } from "effect";
4
+ const round2dp = (num) => Math.round((num + Number.EPSILON) * 100) / 100;
5
+ export const calculateImageSize = (objectFit, currentWidth, currentHeight, containerWidth, containerHeight) => {
6
+ let newSize = {};
7
+ switch (objectFit) {
8
+ case "contain": {
9
+ // Calculate dimensions while maintaining aspect ratio
10
+ const widthRatioContain = containerWidth / currentWidth;
11
+ const heightRatioContain = containerHeight / currentHeight;
12
+ const scaleContain = Math.min(widthRatioContain, heightRatioContain);
13
+ newSize = {
14
+ width: currentWidth * scaleContain,
15
+ height: currentHeight * scaleContain,
16
+ };
17
+ break;
18
+ }
19
+ case "cover": {
20
+ // Calculate dimensions while maintaining aspect ratio
21
+ const widthRatioCover = containerWidth / currentWidth;
22
+ const heightRatioCover = containerHeight / currentHeight;
23
+ const scaleCover = Math.max(widthRatioCover, heightRatioCover);
24
+ const newWidthCover = currentWidth * scaleCover;
25
+ const newHeightCover = currentHeight * scaleCover;
26
+ // Adjust dimensions if it doesn't cover the container completely
27
+ if (newWidthCover < containerWidth || newHeightCover < containerHeight) {
28
+ const scaleCoverAdjusted = Math.max(containerWidth / currentWidth, containerHeight / currentHeight);
29
+ newSize = {
30
+ width: currentWidth * scaleCoverAdjusted,
31
+ height: currentHeight * scaleCoverAdjusted,
32
+ };
33
+ break;
34
+ }
35
+ newSize = {
36
+ width: newWidthCover,
37
+ height: newHeightCover,
38
+ };
39
+ break;
40
+ }
41
+ case "fill":
42
+ // Stretch image to fill container
43
+ newSize = {
44
+ width: containerWidth,
45
+ height: containerHeight,
46
+ };
47
+ break;
48
+ case "none":
49
+ // Keep original image size
50
+ newSize = {
51
+ width: currentWidth,
52
+ height: currentHeight,
53
+ };
54
+ break;
55
+ case "scale-down": {
56
+ // Calculate dimensions based on contain and none values
57
+ const widthRatioScaleDown = containerWidth / currentWidth;
58
+ const heightRatioScaleDown = containerHeight / currentHeight;
59
+ const scaleScaleDown = Math.min(1, Math.min(widthRatioScaleDown, heightRatioScaleDown));
60
+ newSize = {
61
+ width: currentWidth * scaleScaleDown,
62
+ height: currentHeight * scaleScaleDown,
63
+ };
64
+ break;
65
+ }
66
+ default:
67
+ throw new Error("Invalid object fit value");
68
+ }
69
+ if (!newSize?.width || !newSize?.height) {
70
+ throw new Error("Invalid dimensions");
71
+ }
72
+ return {
73
+ width: round2dp(newSize.width),
74
+ height: round2dp(newSize.height),
75
+ };
76
+ // return newSize;
77
+ };
78
+ export const tinyColorToPhotonRGBA = (color) => {
79
+ const rgba = color.toRgb();
80
+ return new Rgba(rgba.r, rgba.g, rgba.b, rgba.a);
81
+ };
82
+ export const autoResize = (image, newWidth, newHeight, options) => {
83
+ const currentWidth = image.get_width();
84
+ const currentHeight = image.get_height();
85
+ if (!newWidth && !newHeight) {
86
+ throw new Error("At least one of width or height is required");
87
+ }
88
+ if (newWidth && !newHeight) {
89
+ newHeight = Math.floor((newWidth / currentWidth) * currentHeight);
90
+ }
91
+ else if (newHeight && !newWidth) {
92
+ newWidth = Math.floor((newHeight / currentHeight) * currentWidth);
93
+ }
94
+ if (!newWidth || !newHeight) {
95
+ throw new Error("Invalid width or height");
96
+ }
97
+ if (newWidth === currentWidth && newHeight === currentHeight) {
98
+ return image;
99
+ }
100
+ const fit = options?.fit || "cover";
101
+ const dem = calculateImageSize(fit, currentWidth, currentHeight, newWidth, newHeight);
102
+ image = resize(image, dem.width, dem.height, 1);
103
+ const [updatedWidth, updatedHeight] = [image.get_width(), image.get_height()];
104
+ if (fit === "contain" && options?.fit_cover_letterbox_color) {
105
+ const paddingX = Math.floor((newWidth - updatedWidth) / 2);
106
+ const paddingY = Math.floor((newHeight - updatedHeight) / 2);
107
+ if (paddingY > 0) {
108
+ image = padding_top(image, paddingY, tinyColorToPhotonRGBA(options.fit_cover_letterbox_color));
109
+ image = padding_bottom(image, paddingY, tinyColorToPhotonRGBA(options.fit_cover_letterbox_color));
110
+ }
111
+ if (paddingX > 0) {
112
+ image = padding_left(image, paddingX, tinyColorToPhotonRGBA(options.fit_cover_letterbox_color));
113
+ image = padding_right(image, paddingX, tinyColorToPhotonRGBA(options.fit_cover_letterbox_color));
114
+ }
115
+ }
116
+ else if (fit === "cover") {
117
+ //crop to center
118
+ const cropX = Math.floor(updatedWidth - newWidth) / 2;
119
+ const cropY = Math.floor(updatedHeight - newHeight) / 2;
120
+ //top left to down right
121
+ image = crop(image, cropX, cropY, newWidth + cropX, newHeight + cropY);
122
+ }
123
+ return image;
124
+ };
125
+ export const imageToFormat = (image, format, options) => {
126
+ let outputBytes;
127
+ switch (format) {
128
+ case "webp":
129
+ outputBytes = image.get_bytes_webp();
130
+ break;
131
+ case "jpeg":
132
+ outputBytes = image.get_bytes_jpeg(options?.jpeg_quality || 100);
133
+ break;
134
+ case "png":
135
+ outputBytes = image.get_bytes();
136
+ break;
137
+ default:
138
+ outputBytes = image.get_bytes_jpeg(options?.jpeg_quality || 100);
139
+ }
140
+ return outputBytes;
141
+ };
142
+ export const imageNode = Layer.succeed(ImagePlugin, ImagePlugin.of({
143
+ optimize: (inputBytes, { quality }) => {
144
+ // create a PhotonImage instance
145
+ const inputImage = PhotonImage.new_from_byteslice(inputBytes);
146
+ // get jpeg bytes
147
+ const outputBytes = inputImage.get_bytes_jpeg(quality);
148
+ // call free() method to free memory
149
+ inputImage.free();
150
+ return Effect.succeed(outputBytes);
151
+ },
152
+ resize: (inputBytes, { width, height, fit }) => {
153
+ if (!width && !height) {
154
+ throw new Error("Either width or height must be specified for resize");
155
+ }
156
+ // create a PhotonImage instance
157
+ const inputImage = PhotonImage.new_from_byteslice(inputBytes);
158
+ // resize image using photon
159
+ const outputImage = autoResize(inputImage, width ?? inputImage.get_width(), height ?? inputImage.get_height(), { fit });
160
+ // get webp bytes
161
+ const outputBytes = outputImage.get_bytes_webp();
162
+ // for other formats
163
+ // png : outputImage.get_bytes();
164
+ // jpeg : outputImage.get_bytes_jpeg(quality);
165
+ // call free() method to free memory
166
+ inputImage.free();
167
+ outputImage.free();
168
+ return Effect.succeed(outputBytes);
169
+ },
170
+ }));
@@ -0,0 +1,4 @@
1
+ import { ImagePlugin } from "@uploadista/core/flow";
2
+ import { Layer } from "effect";
3
+ export declare const imageServerless: Layer.Layer<ImagePlugin, never, never>;
4
+ //# sourceMappingURL=image-service-serverless.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"image-service-serverless.d.ts","sourceRoot":"","sources":["../../src/services/image-service-serverless.ts"],"names":[],"mappings":"AAUA,OAAO,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AACpD,OAAO,EAAU,KAAK,EAAE,MAAM,QAAQ,CAAC;AA6LvC,eAAO,MAAM,eAAe,wCA4C3B,CAAC"}
@@ -0,0 +1,153 @@
1
+ import { crop, PhotonImage, padding_bottom, padding_left, padding_right, padding_top, Rgba, resize, } from "@cf-wasm/photon";
2
+ import { ImagePlugin } from "@uploadista/core/flow";
3
+ import { Effect, Layer } from "effect";
4
+ const round2dp = (num) => Math.round((num + Number.EPSILON) * 100) / 100;
5
+ const calculateImageSize = (objectFit, currentWidth, currentHeight, containerWidth, containerHeight) => {
6
+ let newSize = {};
7
+ switch (objectFit) {
8
+ case "contain": {
9
+ // Calculate dimensions while maintaining aspect ratio
10
+ const widthRatioContain = containerWidth / currentWidth;
11
+ const heightRatioContain = containerHeight / currentHeight;
12
+ const scaleContain = Math.min(widthRatioContain, heightRatioContain);
13
+ newSize = {
14
+ width: currentWidth * scaleContain,
15
+ height: currentHeight * scaleContain,
16
+ };
17
+ break;
18
+ }
19
+ case "cover": {
20
+ // Calculate dimensions while maintaining aspect ratio
21
+ const widthRatioCover = containerWidth / currentWidth;
22
+ const heightRatioCover = containerHeight / currentHeight;
23
+ const scaleCover = Math.max(widthRatioCover, heightRatioCover);
24
+ const newWidthCover = currentWidth * scaleCover;
25
+ const newHeightCover = currentHeight * scaleCover;
26
+ // Adjust dimensions if it doesn't cover the container completely
27
+ if (newWidthCover < containerWidth || newHeightCover < containerHeight) {
28
+ const scaleCoverAdjusted = Math.max(containerWidth / currentWidth, containerHeight / currentHeight);
29
+ newSize = {
30
+ width: currentWidth * scaleCoverAdjusted,
31
+ height: currentHeight * scaleCoverAdjusted,
32
+ };
33
+ break;
34
+ }
35
+ newSize = {
36
+ width: newWidthCover,
37
+ height: newHeightCover,
38
+ };
39
+ break;
40
+ }
41
+ case "fill":
42
+ // Stretch image to fill container
43
+ newSize = {
44
+ width: containerWidth,
45
+ height: containerHeight,
46
+ };
47
+ break;
48
+ case "none":
49
+ // Keep original image size
50
+ newSize = {
51
+ width: currentWidth,
52
+ height: currentHeight,
53
+ };
54
+ break;
55
+ case "scale-down": {
56
+ // Calculate dimensions based on contain and none values
57
+ const widthRatioScaleDown = containerWidth / currentWidth;
58
+ const heightRatioScaleDown = containerHeight / currentHeight;
59
+ const scaleScaleDown = Math.min(1, Math.min(widthRatioScaleDown, heightRatioScaleDown));
60
+ newSize = {
61
+ width: currentWidth * scaleScaleDown,
62
+ height: currentHeight * scaleScaleDown,
63
+ };
64
+ break;
65
+ }
66
+ default:
67
+ throw new Error("Invalid object fit value");
68
+ }
69
+ if (!newSize?.width || !newSize?.height) {
70
+ throw new Error("Invalid dimensions");
71
+ }
72
+ return {
73
+ width: round2dp(newSize.width),
74
+ height: round2dp(newSize.height),
75
+ };
76
+ // return newSize;
77
+ };
78
+ const tinyColorToPhotonRGBA = (color) => {
79
+ const rgba = color.toRgb();
80
+ return new Rgba(rgba.r, rgba.g, rgba.b, rgba.a);
81
+ };
82
+ const autoResize = (image, newWidth, newHeight, options) => {
83
+ const currentWidth = image.get_width();
84
+ const currentHeight = image.get_height();
85
+ if (!newWidth && !newHeight) {
86
+ throw new Error("At least one of width or height is required");
87
+ }
88
+ if (newWidth && !newHeight) {
89
+ newHeight = Math.floor((newWidth / currentWidth) * currentHeight);
90
+ }
91
+ else if (newHeight && !newWidth) {
92
+ newWidth = Math.floor((newHeight / currentHeight) * currentWidth);
93
+ }
94
+ if (!newWidth || !newHeight) {
95
+ throw new Error("Invalid width or height");
96
+ }
97
+ if (newWidth === currentWidth && newHeight === currentHeight) {
98
+ return image;
99
+ }
100
+ const fit = options?.fit || "cover";
101
+ const dem = calculateImageSize(fit, currentWidth, currentHeight, newWidth, newHeight);
102
+ image = resize(image, dem.width, dem.height, 1);
103
+ const [updatedWidth, updatedHeight] = [image.get_width(), image.get_height()];
104
+ if (fit === "contain" && options?.fit_cover_letterbox_color) {
105
+ const paddingX = Math.floor((newWidth - updatedWidth) / 2);
106
+ const paddingY = Math.floor((newHeight - updatedHeight) / 2);
107
+ if (paddingY > 0) {
108
+ image = padding_top(image, paddingY, tinyColorToPhotonRGBA(options.fit_cover_letterbox_color));
109
+ image = padding_bottom(image, paddingY, tinyColorToPhotonRGBA(options.fit_cover_letterbox_color));
110
+ }
111
+ if (paddingX > 0) {
112
+ image = padding_left(image, paddingX, tinyColorToPhotonRGBA(options.fit_cover_letterbox_color));
113
+ image = padding_right(image, paddingX, tinyColorToPhotonRGBA(options.fit_cover_letterbox_color));
114
+ }
115
+ }
116
+ else if (fit === "cover") {
117
+ //crop to center
118
+ const cropX = Math.floor(updatedWidth - newWidth) / 2;
119
+ const cropY = Math.floor(updatedHeight - newHeight) / 2;
120
+ //top left to down right
121
+ image = crop(image, cropX, cropY, newWidth + cropX, newHeight + cropY);
122
+ }
123
+ return image;
124
+ };
125
+ export const imageServerless = Layer.succeed(ImagePlugin, ImagePlugin.of({
126
+ optimize: (inputBytes, { quality }) => {
127
+ // create a PhotonImage instance
128
+ const inputImage = PhotonImage.new_from_byteslice(inputBytes);
129
+ // get jpeg bytes
130
+ const outputBytes = inputImage.get_bytes_jpeg(quality);
131
+ // call free() method to free memory
132
+ inputImage.free();
133
+ return Effect.succeed(outputBytes);
134
+ },
135
+ resize: (inputBytes, { width, height, fit }) => {
136
+ if (!width && !height) {
137
+ throw new Error("Either width or height must be specified for resize");
138
+ }
139
+ // create a PhotonImage instance
140
+ const inputImage = PhotonImage.new_from_byteslice(inputBytes);
141
+ // resize image using photon
142
+ const outputImage = autoResize(inputImage, width ?? inputImage.get_width(), height ?? inputImage.get_height(), { fit });
143
+ // get webp bytes
144
+ const outputBytes = outputImage.get_bytes_webp();
145
+ // for other formats
146
+ // png : outputImage.get_bytes();
147
+ // jpeg : outputImage.get_bytes_jpeg(quality);
148
+ // call free() method to free memory
149
+ inputImage.free();
150
+ outputImage.free();
151
+ return Effect.succeed(outputBytes);
152
+ },
153
+ }));
@@ -0,0 +1,13 @@
1
+ import type { UploadistaError } from "@uploadista/core/errors";
2
+ import { Context, type Effect } from "effect";
3
+ import type { OptimizeParams } from "../types/optimize-node";
4
+ import type { ResizeParams } from "../types/resize-node";
5
+ export type ImageServiceShape = {
6
+ optimize: (input: Uint8Array, options: OptimizeParams) => Effect.Effect<Uint8Array, UploadistaError>;
7
+ resize: (input: Uint8Array, options: ResizeParams) => Effect.Effect<Uint8Array, UploadistaError>;
8
+ };
9
+ declare const ImageService_base: Context.TagClass<ImageService, "ImageService", ImageServiceShape>;
10
+ export declare class ImageService extends ImageService_base {
11
+ }
12
+ export {};
13
+ //# sourceMappingURL=image-service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"image-service.d.ts","sourceRoot":"","sources":["../../src/services/image-service.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAC/D,OAAO,EAAE,OAAO,EAAE,KAAK,MAAM,EAAE,MAAM,QAAQ,CAAC;AAC9C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAC7D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAEzD,MAAM,MAAM,iBAAiB,GAAG;IAC9B,QAAQ,EAAE,CACR,KAAK,EAAE,UAAU,EACjB,OAAO,EAAE,cAAc,KACpB,MAAM,CAAC,MAAM,CAAC,UAAU,EAAE,eAAe,CAAC,CAAC;IAChD,MAAM,EAAE,CACN,KAAK,EAAE,UAAU,EACjB,OAAO,EAAE,YAAY,KAClB,MAAM,CAAC,MAAM,CAAC,UAAU,EAAE,eAAe,CAAC,CAAC;CACjD,CAAC;;AAEF,qBAAa,YAAa,SAAQ,iBAG/B;CAAG"}
@@ -0,0 +1,3 @@
1
+ import { Context } from "effect";
2
+ export class ImageService extends Context.Tag("ImageService")() {
3
+ }
@@ -0,0 +1,3 @@
1
+ export * from "./image-service";
2
+ export * from "./image-service-node";
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/services/index.ts"],"names":[],"mappings":"AAAA,cAAc,iBAAiB,CAAC;AAChC,cAAc,sBAAsB,CAAC"}
@@ -0,0 +1,3 @@
1
+ export * from "./image-service";
2
+ export * from "./image-service-node";
3
+ // export * from "./image-service-serverless";
@@ -0,0 +1,3 @@
1
+ export * from "./optimize-node";
2
+ export * from "./resize-node";
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"AAAA,cAAc,iBAAiB,CAAC;AAChC,cAAc,eAAe,CAAC"}
@@ -0,0 +1,2 @@
1
+ export * from "./optimize-node";
2
+ export * from "./resize-node";
@@ -0,0 +1,9 @@
1
+ import { z } from "zod";
2
+ export declare const optimizeParamsSchema: z.ZodObject<{
3
+ quality: z.ZodNumber;
4
+ format: z.ZodEnum<{
5
+ jpeg: "jpeg";
6
+ }>;
7
+ }, z.core.$strip>;
8
+ export type OptimizeParams = z.infer<typeof optimizeParamsSchema>;
9
+ //# sourceMappingURL=optimize-node.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"optimize-node.d.ts","sourceRoot":"","sources":["../../src/types/optimize-node.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,eAAO,MAAM,oBAAoB;;;;;iBAG/B,CAAC;AAEH,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC"}
@@ -0,0 +1,5 @@
1
+ import { z } from "zod";
2
+ export const optimizeParamsSchema = z.object({
3
+ quality: z.number().min(0).max(100),
4
+ format: z.enum(["jpeg"]),
5
+ });