@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.
- package/.turbo/turbo-build.log +5 -0
- package/.turbo/turbo-check.log +5 -0
- package/LICENSE +21 -0
- package/README.md +117 -0
- package/dist/describe-image-node.d.ts +69 -0
- package/dist/describe-image-node.d.ts.map +1 -0
- package/dist/describe-image-node.js +68 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5 -0
- package/dist/nodes/describe-image-node.d.ts +66 -0
- package/dist/nodes/describe-image-node.d.ts.map +1 -0
- package/dist/nodes/describe-image-node.js +43 -0
- package/dist/nodes/index.d.ts +5 -0
- package/dist/nodes/index.d.ts.map +1 -0
- package/dist/nodes/index.js +4 -0
- package/dist/nodes/optimize-node.d.ts +24 -0
- package/dist/nodes/optimize-node.d.ts.map +1 -0
- package/dist/nodes/optimize-node.js +13 -0
- package/dist/nodes/remove-background-node.d.ts +25 -0
- package/dist/nodes/remove-background-node.d.ts.map +1 -0
- package/dist/nodes/remove-background-node.js +48 -0
- package/dist/nodes/resize-node.d.ts +24 -0
- package/dist/nodes/resize-node.d.ts.map +1 -0
- package/dist/nodes/resize-node.js +13 -0
- package/dist/optimize-node.d.ts +24 -0
- package/dist/optimize-node.d.ts.map +1 -0
- package/dist/optimize-node.js +41 -0
- package/dist/remove-background-node.d.ts +28 -0
- package/dist/remove-background-node.d.ts.map +1 -0
- package/dist/remove-background-node.js +84 -0
- package/dist/resize-node.d.ts +24 -0
- package/dist/resize-node.d.ts.map +1 -0
- package/dist/resize-node.js +13 -0
- package/dist/services/idnex.d.ts +2 -0
- package/dist/services/idnex.d.ts.map +1 -0
- package/dist/services/idnex.js +1 -0
- package/dist/services/image-service-common.d.ts +2 -0
- package/dist/services/image-service-common.d.ts.map +1 -0
- package/dist/services/image-service-common.js +1 -0
- package/dist/services/image-service-node.d.ts +18 -0
- package/dist/services/image-service-node.d.ts.map +1 -0
- package/dist/services/image-service-node.js +170 -0
- package/dist/services/image-service-serverless.d.ts +4 -0
- package/dist/services/image-service-serverless.d.ts.map +1 -0
- package/dist/services/image-service-serverless.js +153 -0
- package/dist/services/image-service.d.ts +13 -0
- package/dist/services/image-service.d.ts.map +1 -0
- package/dist/services/image-service.js +3 -0
- package/dist/services/index.d.ts +3 -0
- package/dist/services/index.d.ts.map +1 -0
- package/dist/services/index.js +3 -0
- package/dist/types/index.d.ts +3 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +2 -0
- package/dist/types/optimize-node.d.ts +9 -0
- package/dist/types/optimize-node.d.ts.map +1 -0
- package/dist/types/optimize-node.js +5 -0
- package/dist/types/resize-node.d.ts +12 -0
- package/dist/types/resize-node.d.ts.map +1 -0
- package/dist/types/resize-node.js +8 -0
- package/dist/wait-for-url.d.ts +18 -0
- package/dist/wait-for-url.d.ts.map +1 -0
- package/dist/wait-for-url.js +36 -0
- package/package.json +30 -0
- package/src/describe-image-node.ts +101 -0
- package/src/index.ts +5 -0
- package/src/optimize-node.ts +61 -0
- package/src/remove-background-node.ts +130 -0
- package/src/resize-node.ts +23 -0
- package/src/wait-for-url.ts +52 -0
- package/tsconfig.json +14 -0
- 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 @@
|
|
|
1
|
+
{"version":3,"file":"idnex.d.ts","sourceRoot":"","sources":["../../src/services/idnex.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -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 @@
|
|
|
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 @@
|
|
|
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 @@
|
|
|
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,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"}
|