@uploadista/flow-images-sharp 0.0.20-beta.9 → 0.1.0-beta.5

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/README.md CHANGED
@@ -17,11 +17,13 @@ Perfect for Node.js and Fastify/Express servers.
17
17
  ## Installation
18
18
 
19
19
  ```bash
20
- npm install @uploadista/flow-images-sharp sharp
20
+ npm install @uploadista/flow-images-sharp
21
21
  # or
22
- pnpm add @uploadista/flow-images-sharp sharp
22
+ pnpm add @uploadista/flow-images-sharp
23
23
  ```
24
24
 
25
+ Note: Sharp is bundled as a dependency, no separate installation required.
26
+
25
27
  ### Prerequisites
26
28
 
27
29
  - Node.js 18+
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":["chunks: Uint8Array[]","outputChunks: Buffer[]"],"sources":["../src/image-plugin.ts"],"sourcesContent":["import { PassThrough } from \"node:stream\";\nimport { UploadistaError } from \"@uploadista/core/errors\";\nimport {\n ImagePlugin,\n type OptimizeParams,\n type ResizeParams,\n type Transformation,\n} from \"@uploadista/core/flow\";\nimport { withOperationSpan } from \"@uploadista/observability\";\nimport { Effect, Layer, Stream } from \"effect\";\nimport sharp from \"sharp\";\n\nconst mapFitToSharp = (fit: \"fill\" | \"contain\" | \"cover\") => {\n switch (fit) {\n case \"fill\":\n return \"cover\";\n case \"contain\":\n return \"contain\";\n }\n};\n\n/**\n * Calculate position coordinates for overlays based on position string and offsets.\n */\nconst calculateOverlayPosition = (\n position: string,\n imageWidth: number,\n imageHeight: number,\n overlayWidth: number,\n overlayHeight: number,\n offsetX = 0,\n offsetY = 0,\n): { top: number; left: number } => {\n let top = 0;\n let left = 0;\n\n switch (position) {\n case \"top-left\":\n top = offsetY;\n left = offsetX;\n break;\n case \"top-right\":\n top = offsetY;\n left = imageWidth - overlayWidth - offsetX;\n break;\n case \"bottom-left\":\n top = imageHeight - overlayHeight - offsetY;\n left = offsetX;\n break;\n case \"bottom-right\":\n top = imageHeight - overlayHeight - offsetY;\n left = imageWidth - overlayWidth - offsetX;\n break;\n case \"center\":\n top = Math.floor((imageHeight - overlayHeight) / 2) + offsetY;\n left = Math.floor((imageWidth - overlayWidth) / 2) + offsetX;\n break;\n }\n\n return { top, left };\n};\n\nexport const imagePlugin = () =>\n Layer.succeed(\n ImagePlugin,\n ImagePlugin.of({\n optimize: (inputBytes, { quality, format }) => {\n return Effect.gen(function* () {\n const outputBytes = yield* Effect.tryPromise({\n try: async () =>\n await sharp(inputBytes).toFormat(format, { quality }).toBuffer(),\n catch: (error) => {\n return UploadistaError.fromCode(\"UNKNOWN_ERROR\", {\n cause: error,\n });\n },\n });\n return new Uint8Array(outputBytes);\n }).pipe(\n withOperationSpan(\"image\", \"optimize\", {\n \"image.format\": format,\n \"image.quality\": quality,\n \"image.input_size\": inputBytes.byteLength,\n }),\n );\n },\n resize: (inputBytes, { width, height, fit }) => {\n return Effect.gen(function* () {\n if (!width && !height) {\n throw new Error(\n \"Either width or height must be specified for resize\",\n );\n }\n\n const sharpFit = mapFitToSharp(fit);\n const outputBytes = yield* Effect.tryPromise({\n try: async () =>\n await sharp(inputBytes)\n .resize(width, height, { fit: sharpFit })\n .toBuffer(),\n catch: (error) => {\n return UploadistaError.fromCode(\"UNKNOWN_ERROR\", {\n cause: error,\n });\n },\n });\n\n return new Uint8Array(outputBytes);\n }).pipe(\n withOperationSpan(\"image\", \"resize\", {\n \"image.width\": width,\n \"image.height\": height,\n \"image.fit\": fit,\n \"image.input_size\": inputBytes.byteLength,\n }),\n );\n },\n transform: (inputBytes, transformation) => {\n return Effect.gen(function* () {\n let pipeline = sharp(inputBytes);\n\n switch (transformation.type) {\n case \"resize\": {\n const sharpFit = mapFitToSharp(transformation.fit);\n pipeline = pipeline.resize(\n transformation.width,\n transformation.height,\n {\n fit: sharpFit,\n },\n );\n break;\n }\n\n case \"blur\": {\n pipeline = pipeline.blur(transformation.sigma);\n break;\n }\n\n case \"rotate\": {\n const options = transformation.background\n ? { background: transformation.background }\n : undefined;\n pipeline = pipeline.rotate(transformation.angle, options);\n break;\n }\n\n case \"flip\": {\n if (transformation.direction === \"horizontal\") {\n pipeline = pipeline.flop();\n } else {\n pipeline = pipeline.flip();\n }\n break;\n }\n\n case \"grayscale\": {\n pipeline = pipeline.grayscale();\n break;\n }\n\n case \"sepia\": {\n // Apply sepia tone using tint\n pipeline = pipeline.tint({ r: 112, g: 66, b: 20 });\n break;\n }\n\n case \"brightness\": {\n // Convert -100 to +100 range to multiplier (0 to 2)\n const multiplier = 1 + transformation.value / 100;\n pipeline = pipeline.modulate({ brightness: multiplier });\n break;\n }\n\n case \"contrast\": {\n // Convert -100 to +100 range to linear adjustment\n const a = 1 + transformation.value / 100;\n pipeline = pipeline.linear(a, 0);\n break;\n }\n\n case \"sharpen\": {\n if (transformation.sigma !== undefined) {\n pipeline = pipeline.sharpen({ sigma: transformation.sigma });\n } else {\n pipeline = pipeline.sharpen();\n }\n break;\n }\n\n case \"watermark\": {\n // Fetch watermark image from URL\n const watermarkBuffer = yield* Effect.tryPromise({\n try: async () => {\n const response = await fetch(transformation.imagePath);\n if (!response.ok) {\n throw new Error(\n `Failed to fetch watermark: ${response.statusText}`,\n );\n }\n const arrayBuffer = await response.arrayBuffer();\n return Buffer.from(arrayBuffer);\n },\n catch: (error) => {\n return UploadistaError.fromCode(\"FILE_NOT_FOUND\", {\n body: `Watermark image not found or failed to fetch: ${transformation.imagePath}`,\n cause: error,\n });\n },\n }).pipe(\n withOperationSpan(\"image\", \"fetch-watermark\", {\n \"image.watermark_url\": transformation.imagePath,\n }),\n );\n\n // Get image metadata to calculate positioning\n const metadata = yield* Effect.tryPromise({\n try: async () => await pipeline.metadata(),\n catch: (error) => {\n return UploadistaError.fromCode(\"UNKNOWN_ERROR\", {\n body: \"Failed to read image metadata\",\n cause: error,\n });\n },\n });\n\n // Get watermark metadata\n const watermarkMetadata = yield* Effect.tryPromise({\n try: async () => await sharp(watermarkBuffer).metadata(),\n catch: (error) => {\n return UploadistaError.fromCode(\"UNKNOWN_ERROR\", {\n body: \"Failed to read watermark metadata\",\n cause: error,\n });\n },\n });\n\n if (\n !metadata.width ||\n !metadata.height ||\n !watermarkMetadata.width ||\n !watermarkMetadata.height\n ) {\n return yield* Effect.fail(\n UploadistaError.fromCode(\"UNKNOWN_ERROR\", {\n body: \"Could not determine image or watermark dimensions\",\n }),\n );\n }\n\n const { top, left } = calculateOverlayPosition(\n transformation.position,\n metadata.width,\n metadata.height,\n watermarkMetadata.width,\n watermarkMetadata.height,\n transformation.offsetX,\n transformation.offsetY,\n );\n\n // Apply watermark with opacity\n const watermarkWithOpacity = yield* Effect.tryPromise({\n try: async () =>\n await sharp(watermarkBuffer)\n .composite([\n {\n input: Buffer.from([\n 255,\n 255,\n 255,\n Math.round(transformation.opacity * 255),\n ]),\n raw: {\n width: 1,\n height: 1,\n channels: 4,\n },\n tile: true,\n blend: \"dest-in\",\n },\n ])\n .toBuffer(),\n catch: (error) => {\n return UploadistaError.fromCode(\"UNKNOWN_ERROR\", {\n body: \"Failed to apply watermark opacity\",\n cause: error,\n });\n },\n });\n\n pipeline = pipeline.composite([\n {\n input: watermarkWithOpacity,\n top,\n left,\n },\n ]);\n break;\n }\n\n case \"logo\": {\n // Fetch logo image from URL\n const logoBuffer = yield* Effect.tryPromise({\n try: async () => {\n const response = await fetch(transformation.imagePath);\n if (!response.ok) {\n throw new Error(\n `Failed to fetch logo: ${response.statusText}`,\n );\n }\n const arrayBuffer = await response.arrayBuffer();\n return Buffer.from(arrayBuffer);\n },\n catch: (error) => {\n return UploadistaError.fromCode(\"FILE_NOT_FOUND\", {\n body: `Logo image not found or failed to fetch: ${transformation.imagePath}`,\n cause: error,\n });\n },\n }).pipe(\n withOperationSpan(\"image\", \"fetch-logo\", {\n \"image.logo_url\": transformation.imagePath,\n }),\n );\n\n // Get image metadata\n const metadata = yield* Effect.tryPromise({\n try: async () => await pipeline.metadata(),\n catch: (error) => {\n return UploadistaError.fromCode(\"UNKNOWN_ERROR\", {\n body: \"Failed to read image metadata\",\n cause: error,\n });\n },\n });\n\n // Get logo metadata\n const logoMetadata = yield* Effect.tryPromise({\n try: async () => await sharp(logoBuffer).metadata(),\n catch: (error) => {\n return UploadistaError.fromCode(\"UNKNOWN_ERROR\", {\n body: \"Failed to read logo metadata\",\n cause: error,\n });\n },\n });\n\n if (\n !metadata.width ||\n !metadata.height ||\n !logoMetadata.width ||\n !logoMetadata.height\n ) {\n return yield* Effect.fail(\n UploadistaError.fromCode(\"UNKNOWN_ERROR\", {\n body: \"Could not determine image or logo dimensions\",\n }),\n );\n }\n\n // Scale logo\n const scaledLogoWidth = Math.round(\n logoMetadata.width * transformation.scale,\n );\n const scaledLogoHeight = Math.round(\n logoMetadata.height * transformation.scale,\n );\n\n const scaledLogo = yield* Effect.tryPromise({\n try: async () =>\n await sharp(logoBuffer)\n .resize(scaledLogoWidth, scaledLogoHeight)\n .toBuffer(),\n catch: (error) => {\n return UploadistaError.fromCode(\"UNKNOWN_ERROR\", {\n body: \"Failed to scale logo\",\n cause: error,\n });\n },\n });\n\n const { top, left } = calculateOverlayPosition(\n transformation.position,\n metadata.width,\n metadata.height,\n scaledLogoWidth,\n scaledLogoHeight,\n transformation.offsetX,\n transformation.offsetY,\n );\n\n pipeline = pipeline.composite([\n {\n input: scaledLogo,\n top,\n left,\n },\n ]);\n break;\n }\n\n case \"text\": {\n // Get image metadata\n const metadata = yield* Effect.tryPromise({\n try: async () => await pipeline.metadata(),\n catch: (error) => {\n return UploadistaError.fromCode(\"UNKNOWN_ERROR\", {\n body: \"Failed to read image metadata\",\n cause: error,\n });\n },\n });\n\n if (!metadata.width || !metadata.height) {\n return yield* Effect.fail(\n UploadistaError.fromCode(\"UNKNOWN_ERROR\", {\n body: \"Could not determine image dimensions\",\n }),\n );\n }\n\n // Create SVG text overlay\n const fontFamily = transformation.fontFamily || \"sans-serif\";\n\n // Estimate text dimensions (rough approximation)\n const textWidth =\n transformation.text.length * transformation.fontSize * 0.6;\n const textHeight = transformation.fontSize;\n\n const { top, left } = calculateOverlayPosition(\n transformation.position,\n metadata.width,\n metadata.height,\n textWidth,\n textHeight,\n transformation.offsetX,\n transformation.offsetY,\n );\n\n // Create positioned SVG\n const positionedSvg = `\n <svg width=\"${metadata.width}\" height=\"${metadata.height}\">\n <text\n x=\"${left}\"\n y=\"${top + transformation.fontSize}\"\n font-family=\"${fontFamily}\"\n font-size=\"${transformation.fontSize}\"\n fill=\"${transformation.color}\"\n >${transformation.text}</text>\n </svg>\n `;\n\n pipeline = pipeline.composite([\n {\n input: Buffer.from(positionedSvg),\n top: 0,\n left: 0,\n },\n ]);\n break;\n }\n\n default: {\n // TypeScript should ensure this is unreachable\n return yield* Effect.fail(\n UploadistaError.fromCode(\"UNKNOWN_ERROR\", {\n body: `Unsupported transformation type: ${(transformation as { type: string }).type}`,\n }),\n );\n }\n }\n\n // Convert pipeline to buffer\n const outputBytes = yield* Effect.tryPromise({\n try: async () => await pipeline.toBuffer(),\n catch: (error) => {\n return UploadistaError.fromCode(\"UNKNOWN_ERROR\", {\n body: `Failed to apply transformation: ${transformation.type}`,\n cause: error,\n });\n },\n });\n\n return new Uint8Array(outputBytes);\n }).pipe(\n withOperationSpan(\"image\", \"transform\", {\n \"image.transformation_type\": transformation.type,\n \"image.input_size\": inputBytes.byteLength,\n }),\n );\n },\n\n /**\n * Indicates that this plugin supports streaming operations.\n */\n supportsStreaming: true,\n\n /**\n * Streaming optimization using Sharp's pipeline.\n *\n * Collects input stream chunks, processes through Sharp, and returns\n * the result as a stream. This avoids double-buffering when combined\n * with streaming DataStore reads.\n */\n optimizeStream: (\n inputStream: Stream.Stream<Uint8Array, UploadistaError>,\n { quality, format }: OptimizeParams,\n ): Effect.Effect<\n Stream.Stream<Uint8Array, UploadistaError>,\n UploadistaError\n > => {\n return Effect.gen(function* () {\n // Collect input stream to buffer (Sharp needs full image to decode)\n const chunks: Uint8Array[] = [];\n yield* Stream.runForEach(inputStream, (chunk) =>\n Effect.sync(() => {\n chunks.push(chunk);\n }),\n );\n\n // Combine chunks\n const totalLength = chunks.reduce((sum, c) => sum + c.byteLength, 0);\n const inputBuffer = new Uint8Array(totalLength);\n let offset = 0;\n for (const chunk of chunks) {\n inputBuffer.set(chunk, offset);\n offset += chunk.byteLength;\n }\n\n // Process through Sharp and output as stream\n return Stream.async<Uint8Array, UploadistaError>((emit) => {\n const sharpInstance = sharp(inputBuffer).toFormat(format, {\n quality,\n });\n\n // Use Sharp's streaming output\n const outputStream = new PassThrough();\n const outputChunks: Buffer[] = [];\n\n sharpInstance\n .pipe(outputStream)\n .on(\"data\", (chunk: Buffer) => {\n outputChunks.push(chunk);\n })\n .on(\"end\", () => {\n // Emit all collected chunks as a single Uint8Array\n const outputBuffer = Buffer.concat(outputChunks);\n emit.single(new Uint8Array(outputBuffer));\n emit.end();\n })\n .on(\"error\", (error: Error) => {\n emit.fail(\n UploadistaError.fromCode(\"UNKNOWN_ERROR\", {\n body: `Sharp streaming optimization failed: ${error.message}`,\n cause: error,\n }),\n );\n });\n\n // Cleanup\n return Effect.sync(() => {\n outputStream.destroy();\n });\n });\n }).pipe(\n withOperationSpan(\"image\", \"optimize-stream\", {\n \"image.format\": format,\n \"image.quality\": quality,\n }),\n );\n },\n\n /**\n * Streaming resize using Sharp's pipeline.\n *\n * Collects input stream chunks, processes through Sharp's resize,\n * and returns the result as a stream.\n */\n resizeStream: (\n inputStream: Stream.Stream<Uint8Array, UploadistaError>,\n { width, height, fit }: ResizeParams,\n ): Effect.Effect<\n Stream.Stream<Uint8Array, UploadistaError>,\n UploadistaError\n > => {\n return Effect.gen(function* () {\n if (!width && !height) {\n return yield* Effect.fail(\n UploadistaError.fromCode(\"VALIDATION_ERROR\", {\n body: \"Either width or height must be specified for resize\",\n }),\n );\n }\n\n // Collect input stream to buffer (Sharp needs full image to decode)\n const chunks: Uint8Array[] = [];\n yield* Stream.runForEach(inputStream, (chunk) =>\n Effect.sync(() => {\n chunks.push(chunk);\n }),\n );\n\n // Combine chunks\n const totalLength = chunks.reduce((sum, c) => sum + c.byteLength, 0);\n const inputBuffer = new Uint8Array(totalLength);\n let offset = 0;\n for (const chunk of chunks) {\n inputBuffer.set(chunk, offset);\n offset += chunk.byteLength;\n }\n\n const sharpFit = mapFitToSharp(fit);\n\n // Process through Sharp and output as stream\n return Stream.async<Uint8Array, UploadistaError>((emit) => {\n const sharpInstance = sharp(inputBuffer).resize(width, height, {\n fit: sharpFit,\n });\n\n // Use Sharp's streaming output\n const outputStream = new PassThrough();\n const outputChunks: Buffer[] = [];\n\n sharpInstance\n .pipe(outputStream)\n .on(\"data\", (chunk: Buffer) => {\n outputChunks.push(chunk);\n })\n .on(\"end\", () => {\n // Emit all collected chunks as a single Uint8Array\n const outputBuffer = Buffer.concat(outputChunks);\n emit.single(new Uint8Array(outputBuffer));\n emit.end();\n })\n .on(\"error\", (error: Error) => {\n emit.fail(\n UploadistaError.fromCode(\"UNKNOWN_ERROR\", {\n body: `Sharp streaming resize failed: ${error.message}`,\n cause: error,\n }),\n );\n });\n\n // Cleanup\n return Effect.sync(() => {\n outputStream.destroy();\n });\n });\n }).pipe(\n withOperationSpan(\"image\", \"resize-stream\", {\n \"image.width\": width,\n \"image.height\": height,\n \"image.fit\": fit,\n }),\n );\n },\n\n /**\n * Streaming transformation using Sharp's pipeline.\n *\n * Collects input stream chunks, applies the transformation,\n * and returns the result as a stream.\n */\n transformStream: (\n inputStream: Stream.Stream<Uint8Array, UploadistaError>,\n transformation: Transformation,\n ): Effect.Effect<\n Stream.Stream<Uint8Array, UploadistaError>,\n UploadistaError\n > => {\n return Effect.gen(function* () {\n // Collect input stream to buffer (Sharp needs full image to decode)\n const chunks: Uint8Array[] = [];\n yield* Stream.runForEach(inputStream, (chunk) =>\n Effect.sync(() => {\n chunks.push(chunk);\n }),\n );\n\n // Combine chunks\n const totalLength = chunks.reduce((sum, c) => sum + c.byteLength, 0);\n const inputBuffer = new Uint8Array(totalLength);\n let offset = 0;\n for (const chunk of chunks) {\n inputBuffer.set(chunk, offset);\n offset += chunk.byteLength;\n }\n\n // Apply transformation (reuse buffered transform logic)\n let pipeline = sharp(inputBuffer);\n\n switch (transformation.type) {\n case \"resize\": {\n const sharpFit = mapFitToSharp(transformation.fit);\n pipeline = pipeline.resize(\n transformation.width,\n transformation.height,\n { fit: sharpFit },\n );\n break;\n }\n\n case \"blur\": {\n pipeline = pipeline.blur(transformation.sigma);\n break;\n }\n\n case \"rotate\": {\n const options = transformation.background\n ? { background: transformation.background }\n : undefined;\n pipeline = pipeline.rotate(transformation.angle, options);\n break;\n }\n\n case \"flip\": {\n if (transformation.direction === \"horizontal\") {\n pipeline = pipeline.flop();\n } else {\n pipeline = pipeline.flip();\n }\n break;\n }\n\n case \"grayscale\": {\n pipeline = pipeline.grayscale();\n break;\n }\n\n case \"sepia\": {\n pipeline = pipeline.tint({ r: 112, g: 66, b: 20 });\n break;\n }\n\n case \"brightness\": {\n const multiplier = 1 + transformation.value / 100;\n pipeline = pipeline.modulate({ brightness: multiplier });\n break;\n }\n\n case \"contrast\": {\n const a = 1 + transformation.value / 100;\n pipeline = pipeline.linear(a, 0);\n break;\n }\n\n case \"sharpen\": {\n if (transformation.sigma !== undefined) {\n pipeline = pipeline.sharpen({ sigma: transformation.sigma });\n } else {\n pipeline = pipeline.sharpen();\n }\n break;\n }\n\n case \"watermark\":\n case \"logo\":\n case \"text\": {\n // These transformations require async operations and metadata lookups\n // Fall back to the buffered transform for these complex cases\n return yield* Effect.fail(\n UploadistaError.fromCode(\"UNKNOWN_ERROR\", {\n body: `Streaming not supported for ${transformation.type} transformation. Use buffered mode.`,\n }),\n );\n }\n\n default: {\n return yield* Effect.fail(\n UploadistaError.fromCode(\"UNKNOWN_ERROR\", {\n body: `Unsupported transformation type: ${(transformation as { type: string }).type}`,\n }),\n );\n }\n }\n\n // Process through Sharp and output as stream\n return Stream.async<Uint8Array, UploadistaError>((emit) => {\n // Use Sharp's streaming output\n const outputStream = new PassThrough();\n const outputChunks: Buffer[] = [];\n\n pipeline\n .pipe(outputStream)\n .on(\"data\", (chunk: Buffer) => {\n outputChunks.push(chunk);\n })\n .on(\"end\", () => {\n // Emit all collected chunks as a single Uint8Array\n const outputBuffer = Buffer.concat(outputChunks);\n emit.single(new Uint8Array(outputBuffer));\n emit.end();\n })\n .on(\"error\", (error: Error) => {\n emit.fail(\n UploadistaError.fromCode(\"UNKNOWN_ERROR\", {\n body: `Sharp streaming transform failed: ${error.message}`,\n cause: error,\n }),\n );\n });\n\n // Cleanup\n return Effect.sync(() => {\n outputStream.destroy();\n });\n });\n }).pipe(\n withOperationSpan(\"image\", \"transform-stream\", {\n \"image.transformation_type\": transformation.type,\n }),\n );\n },\n }),\n );\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../src/image-plugin.ts"],"sourcesContent":["import { PassThrough } from \"node:stream\";\nimport { UploadistaError } from \"@uploadista/core/errors\";\nimport {\n ImagePlugin,\n type OptimizeParams,\n type ResizeParams,\n type Transformation,\n} from \"@uploadista/core/flow\";\nimport { withOperationSpan } from \"@uploadista/observability\";\nimport { Effect, Layer, Stream } from \"effect\";\nimport sharp from \"sharp\";\n\nconst mapFitToSharp = (fit: \"fill\" | \"contain\" | \"cover\") => {\n switch (fit) {\n case \"fill\":\n return \"cover\";\n case \"contain\":\n return \"contain\";\n }\n};\n\n/**\n * Calculate position coordinates for overlays based on position string and offsets.\n */\nconst calculateOverlayPosition = (\n position: string,\n imageWidth: number,\n imageHeight: number,\n overlayWidth: number,\n overlayHeight: number,\n offsetX = 0,\n offsetY = 0,\n): { top: number; left: number } => {\n let top = 0;\n let left = 0;\n\n switch (position) {\n case \"top-left\":\n top = offsetY;\n left = offsetX;\n break;\n case \"top-right\":\n top = offsetY;\n left = imageWidth - overlayWidth - offsetX;\n break;\n case \"bottom-left\":\n top = imageHeight - overlayHeight - offsetY;\n left = offsetX;\n break;\n case \"bottom-right\":\n top = imageHeight - overlayHeight - offsetY;\n left = imageWidth - overlayWidth - offsetX;\n break;\n case \"center\":\n top = Math.floor((imageHeight - overlayHeight) / 2) + offsetY;\n left = Math.floor((imageWidth - overlayWidth) / 2) + offsetX;\n break;\n }\n\n return { top, left };\n};\n\nexport const imagePlugin = () =>\n Layer.succeed(\n ImagePlugin,\n ImagePlugin.of({\n optimize: (inputBytes, { quality, format }) => {\n return Effect.gen(function* () {\n const outputBytes = yield* Effect.tryPromise({\n try: async () =>\n await sharp(inputBytes).toFormat(format, { quality }).toBuffer(),\n catch: (error) => {\n return UploadistaError.fromCode(\"UNKNOWN_ERROR\", {\n cause: error,\n });\n },\n });\n return new Uint8Array(outputBytes);\n }).pipe(\n withOperationSpan(\"image\", \"optimize\", {\n \"image.format\": format,\n \"image.quality\": quality,\n \"image.input_size\": inputBytes.byteLength,\n }),\n );\n },\n resize: (inputBytes, { width, height, fit }) => {\n return Effect.gen(function* () {\n if (!width && !height) {\n throw new Error(\n \"Either width or height must be specified for resize\",\n );\n }\n\n const sharpFit = mapFitToSharp(fit);\n const outputBytes = yield* Effect.tryPromise({\n try: async () =>\n await sharp(inputBytes)\n .resize(width, height, { fit: sharpFit })\n .toBuffer(),\n catch: (error) => {\n return UploadistaError.fromCode(\"UNKNOWN_ERROR\", {\n cause: error,\n });\n },\n });\n\n return new Uint8Array(outputBytes);\n }).pipe(\n withOperationSpan(\"image\", \"resize\", {\n \"image.width\": width,\n \"image.height\": height,\n \"image.fit\": fit,\n \"image.input_size\": inputBytes.byteLength,\n }),\n );\n },\n transform: (inputBytes, transformation) => {\n return Effect.gen(function* () {\n let pipeline = sharp(inputBytes);\n\n switch (transformation.type) {\n case \"resize\": {\n const sharpFit = mapFitToSharp(transformation.fit);\n pipeline = pipeline.resize(\n transformation.width,\n transformation.height,\n {\n fit: sharpFit,\n },\n );\n break;\n }\n\n case \"blur\": {\n pipeline = pipeline.blur(transformation.sigma);\n break;\n }\n\n case \"rotate\": {\n const options = transformation.background\n ? { background: transformation.background }\n : undefined;\n pipeline = pipeline.rotate(transformation.angle, options);\n break;\n }\n\n case \"flip\": {\n if (transformation.direction === \"horizontal\") {\n pipeline = pipeline.flop();\n } else {\n pipeline = pipeline.flip();\n }\n break;\n }\n\n case \"grayscale\": {\n pipeline = pipeline.grayscale();\n break;\n }\n\n case \"sepia\": {\n // Apply sepia tone using tint\n pipeline = pipeline.tint({ r: 112, g: 66, b: 20 });\n break;\n }\n\n case \"brightness\": {\n // Convert -100 to +100 range to multiplier (0 to 2)\n const multiplier = 1 + transformation.value / 100;\n pipeline = pipeline.modulate({ brightness: multiplier });\n break;\n }\n\n case \"contrast\": {\n // Convert -100 to +100 range to linear adjustment\n const a = 1 + transformation.value / 100;\n pipeline = pipeline.linear(a, 0);\n break;\n }\n\n case \"sharpen\": {\n if (transformation.sigma !== undefined) {\n pipeline = pipeline.sharpen({ sigma: transformation.sigma });\n } else {\n pipeline = pipeline.sharpen();\n }\n break;\n }\n\n case \"watermark\": {\n // Fetch watermark image from URL\n const watermarkBuffer = yield* Effect.tryPromise({\n try: async () => {\n const response = await fetch(transformation.imagePath);\n if (!response.ok) {\n throw new Error(\n `Failed to fetch watermark: ${response.statusText}`,\n );\n }\n const arrayBuffer = await response.arrayBuffer();\n return Buffer.from(arrayBuffer);\n },\n catch: (error) => {\n return UploadistaError.fromCode(\"FILE_NOT_FOUND\", {\n body: `Watermark image not found or failed to fetch: ${transformation.imagePath}`,\n cause: error,\n });\n },\n }).pipe(\n withOperationSpan(\"image\", \"fetch-watermark\", {\n \"image.watermark_url\": transformation.imagePath,\n }),\n );\n\n // Get image metadata to calculate positioning\n const metadata = yield* Effect.tryPromise({\n try: async () => await pipeline.metadata(),\n catch: (error) => {\n return UploadistaError.fromCode(\"UNKNOWN_ERROR\", {\n body: \"Failed to read image metadata\",\n cause: error,\n });\n },\n });\n\n // Get watermark metadata\n const watermarkMetadata = yield* Effect.tryPromise({\n try: async () => await sharp(watermarkBuffer).metadata(),\n catch: (error) => {\n return UploadistaError.fromCode(\"UNKNOWN_ERROR\", {\n body: \"Failed to read watermark metadata\",\n cause: error,\n });\n },\n });\n\n if (\n !metadata.width ||\n !metadata.height ||\n !watermarkMetadata.width ||\n !watermarkMetadata.height\n ) {\n return yield* Effect.fail(\n UploadistaError.fromCode(\"UNKNOWN_ERROR\", {\n body: \"Could not determine image or watermark dimensions\",\n }),\n );\n }\n\n const { top, left } = calculateOverlayPosition(\n transformation.position,\n metadata.width,\n metadata.height,\n watermarkMetadata.width,\n watermarkMetadata.height,\n transformation.offsetX,\n transformation.offsetY,\n );\n\n // Apply watermark with opacity\n const watermarkWithOpacity = yield* Effect.tryPromise({\n try: async () =>\n await sharp(watermarkBuffer)\n .composite([\n {\n input: Buffer.from([\n 255,\n 255,\n 255,\n Math.round(transformation.opacity * 255),\n ]),\n raw: {\n width: 1,\n height: 1,\n channels: 4,\n },\n tile: true,\n blend: \"dest-in\",\n },\n ])\n .toBuffer(),\n catch: (error) => {\n return UploadistaError.fromCode(\"UNKNOWN_ERROR\", {\n body: \"Failed to apply watermark opacity\",\n cause: error,\n });\n },\n });\n\n pipeline = pipeline.composite([\n {\n input: watermarkWithOpacity,\n top,\n left,\n },\n ]);\n break;\n }\n\n case \"logo\": {\n // Fetch logo image from URL\n const logoBuffer = yield* Effect.tryPromise({\n try: async () => {\n const response = await fetch(transformation.imagePath);\n if (!response.ok) {\n throw new Error(\n `Failed to fetch logo: ${response.statusText}`,\n );\n }\n const arrayBuffer = await response.arrayBuffer();\n return Buffer.from(arrayBuffer);\n },\n catch: (error) => {\n return UploadistaError.fromCode(\"FILE_NOT_FOUND\", {\n body: `Logo image not found or failed to fetch: ${transformation.imagePath}`,\n cause: error,\n });\n },\n }).pipe(\n withOperationSpan(\"image\", \"fetch-logo\", {\n \"image.logo_url\": transformation.imagePath,\n }),\n );\n\n // Get image metadata\n const metadata = yield* Effect.tryPromise({\n try: async () => await pipeline.metadata(),\n catch: (error) => {\n return UploadistaError.fromCode(\"UNKNOWN_ERROR\", {\n body: \"Failed to read image metadata\",\n cause: error,\n });\n },\n });\n\n // Get logo metadata\n const logoMetadata = yield* Effect.tryPromise({\n try: async () => await sharp(logoBuffer).metadata(),\n catch: (error) => {\n return UploadistaError.fromCode(\"UNKNOWN_ERROR\", {\n body: \"Failed to read logo metadata\",\n cause: error,\n });\n },\n });\n\n if (\n !metadata.width ||\n !metadata.height ||\n !logoMetadata.width ||\n !logoMetadata.height\n ) {\n return yield* Effect.fail(\n UploadistaError.fromCode(\"UNKNOWN_ERROR\", {\n body: \"Could not determine image or logo dimensions\",\n }),\n );\n }\n\n // Scale logo\n const scaledLogoWidth = Math.round(\n logoMetadata.width * transformation.scale,\n );\n const scaledLogoHeight = Math.round(\n logoMetadata.height * transformation.scale,\n );\n\n const scaledLogo = yield* Effect.tryPromise({\n try: async () =>\n await sharp(logoBuffer)\n .resize(scaledLogoWidth, scaledLogoHeight)\n .toBuffer(),\n catch: (error) => {\n return UploadistaError.fromCode(\"UNKNOWN_ERROR\", {\n body: \"Failed to scale logo\",\n cause: error,\n });\n },\n });\n\n const { top, left } = calculateOverlayPosition(\n transformation.position,\n metadata.width,\n metadata.height,\n scaledLogoWidth,\n scaledLogoHeight,\n transformation.offsetX,\n transformation.offsetY,\n );\n\n pipeline = pipeline.composite([\n {\n input: scaledLogo,\n top,\n left,\n },\n ]);\n break;\n }\n\n case \"text\": {\n // Get image metadata\n const metadata = yield* Effect.tryPromise({\n try: async () => await pipeline.metadata(),\n catch: (error) => {\n return UploadistaError.fromCode(\"UNKNOWN_ERROR\", {\n body: \"Failed to read image metadata\",\n cause: error,\n });\n },\n });\n\n if (!metadata.width || !metadata.height) {\n return yield* Effect.fail(\n UploadistaError.fromCode(\"UNKNOWN_ERROR\", {\n body: \"Could not determine image dimensions\",\n }),\n );\n }\n\n // Create SVG text overlay\n const fontFamily = transformation.fontFamily || \"sans-serif\";\n\n // Estimate text dimensions (rough approximation)\n const textWidth =\n transformation.text.length * transformation.fontSize * 0.6;\n const textHeight = transformation.fontSize;\n\n const { top, left } = calculateOverlayPosition(\n transformation.position,\n metadata.width,\n metadata.height,\n textWidth,\n textHeight,\n transformation.offsetX,\n transformation.offsetY,\n );\n\n // Create positioned SVG\n const positionedSvg = `\n <svg width=\"${metadata.width}\" height=\"${metadata.height}\">\n <text\n x=\"${left}\"\n y=\"${top + transformation.fontSize}\"\n font-family=\"${fontFamily}\"\n font-size=\"${transformation.fontSize}\"\n fill=\"${transformation.color}\"\n >${transformation.text}</text>\n </svg>\n `;\n\n pipeline = pipeline.composite([\n {\n input: Buffer.from(positionedSvg),\n top: 0,\n left: 0,\n },\n ]);\n break;\n }\n\n default: {\n // TypeScript should ensure this is unreachable\n return yield* Effect.fail(\n UploadistaError.fromCode(\"UNKNOWN_ERROR\", {\n body: `Unsupported transformation type: ${(transformation as { type: string }).type}`,\n }),\n );\n }\n }\n\n // Convert pipeline to buffer\n const outputBytes = yield* Effect.tryPromise({\n try: async () => await pipeline.toBuffer(),\n catch: (error) => {\n return UploadistaError.fromCode(\"UNKNOWN_ERROR\", {\n body: `Failed to apply transformation: ${transformation.type}`,\n cause: error,\n });\n },\n });\n\n return new Uint8Array(outputBytes);\n }).pipe(\n withOperationSpan(\"image\", \"transform\", {\n \"image.transformation_type\": transformation.type,\n \"image.input_size\": inputBytes.byteLength,\n }),\n );\n },\n\n /**\n * Indicates that this plugin supports streaming operations.\n */\n supportsStreaming: true,\n\n /**\n * Streaming optimization using Sharp's pipeline.\n *\n * Collects input stream chunks, processes through Sharp, and returns\n * the result as a stream. This avoids double-buffering when combined\n * with streaming DataStore reads.\n */\n optimizeStream: (\n inputStream: Stream.Stream<Uint8Array, UploadistaError>,\n { quality, format }: OptimizeParams,\n ): Effect.Effect<\n Stream.Stream<Uint8Array, UploadistaError>,\n UploadistaError\n > => {\n return Effect.gen(function* () {\n // Collect input stream to buffer (Sharp needs full image to decode)\n const chunks: Uint8Array[] = [];\n yield* Stream.runForEach(inputStream, (chunk) =>\n Effect.sync(() => {\n chunks.push(chunk);\n }),\n );\n\n // Combine chunks\n const totalLength = chunks.reduce((sum, c) => sum + c.byteLength, 0);\n const inputBuffer = new Uint8Array(totalLength);\n let offset = 0;\n for (const chunk of chunks) {\n inputBuffer.set(chunk, offset);\n offset += chunk.byteLength;\n }\n\n // Process through Sharp and output as stream\n return Stream.async<Uint8Array, UploadistaError>((emit) => {\n const sharpInstance = sharp(inputBuffer).toFormat(format, {\n quality,\n });\n\n // Use Sharp's streaming output\n const outputStream = new PassThrough();\n const outputChunks: Buffer[] = [];\n\n sharpInstance\n .pipe(outputStream)\n .on(\"data\", (chunk: Buffer) => {\n outputChunks.push(chunk);\n })\n .on(\"end\", () => {\n // Emit all collected chunks as a single Uint8Array\n const outputBuffer = Buffer.concat(outputChunks);\n emit.single(new Uint8Array(outputBuffer));\n emit.end();\n })\n .on(\"error\", (error: Error) => {\n emit.fail(\n UploadistaError.fromCode(\"UNKNOWN_ERROR\", {\n body: `Sharp streaming optimization failed: ${error.message}`,\n cause: error,\n }),\n );\n });\n\n // Cleanup\n return Effect.sync(() => {\n outputStream.destroy();\n });\n });\n }).pipe(\n withOperationSpan(\"image\", \"optimize-stream\", {\n \"image.format\": format,\n \"image.quality\": quality,\n }),\n );\n },\n\n /**\n * Streaming resize using Sharp's pipeline.\n *\n * Collects input stream chunks, processes through Sharp's resize,\n * and returns the result as a stream.\n */\n resizeStream: (\n inputStream: Stream.Stream<Uint8Array, UploadistaError>,\n { width, height, fit }: ResizeParams,\n ): Effect.Effect<\n Stream.Stream<Uint8Array, UploadistaError>,\n UploadistaError\n > => {\n return Effect.gen(function* () {\n if (!width && !height) {\n return yield* Effect.fail(\n UploadistaError.fromCode(\"VALIDATION_ERROR\", {\n body: \"Either width or height must be specified for resize\",\n }),\n );\n }\n\n // Collect input stream to buffer (Sharp needs full image to decode)\n const chunks: Uint8Array[] = [];\n yield* Stream.runForEach(inputStream, (chunk) =>\n Effect.sync(() => {\n chunks.push(chunk);\n }),\n );\n\n // Combine chunks\n const totalLength = chunks.reduce((sum, c) => sum + c.byteLength, 0);\n const inputBuffer = new Uint8Array(totalLength);\n let offset = 0;\n for (const chunk of chunks) {\n inputBuffer.set(chunk, offset);\n offset += chunk.byteLength;\n }\n\n const sharpFit = mapFitToSharp(fit);\n\n // Process through Sharp and output as stream\n return Stream.async<Uint8Array, UploadistaError>((emit) => {\n const sharpInstance = sharp(inputBuffer).resize(width, height, {\n fit: sharpFit,\n });\n\n // Use Sharp's streaming output\n const outputStream = new PassThrough();\n const outputChunks: Buffer[] = [];\n\n sharpInstance\n .pipe(outputStream)\n .on(\"data\", (chunk: Buffer) => {\n outputChunks.push(chunk);\n })\n .on(\"end\", () => {\n // Emit all collected chunks as a single Uint8Array\n const outputBuffer = Buffer.concat(outputChunks);\n emit.single(new Uint8Array(outputBuffer));\n emit.end();\n })\n .on(\"error\", (error: Error) => {\n emit.fail(\n UploadistaError.fromCode(\"UNKNOWN_ERROR\", {\n body: `Sharp streaming resize failed: ${error.message}`,\n cause: error,\n }),\n );\n });\n\n // Cleanup\n return Effect.sync(() => {\n outputStream.destroy();\n });\n });\n }).pipe(\n withOperationSpan(\"image\", \"resize-stream\", {\n \"image.width\": width,\n \"image.height\": height,\n \"image.fit\": fit,\n }),\n );\n },\n\n /**\n * Streaming transformation using Sharp's pipeline.\n *\n * Collects input stream chunks, applies the transformation,\n * and returns the result as a stream.\n */\n transformStream: (\n inputStream: Stream.Stream<Uint8Array, UploadistaError>,\n transformation: Transformation,\n ): Effect.Effect<\n Stream.Stream<Uint8Array, UploadistaError>,\n UploadistaError\n > => {\n return Effect.gen(function* () {\n // Collect input stream to buffer (Sharp needs full image to decode)\n const chunks: Uint8Array[] = [];\n yield* Stream.runForEach(inputStream, (chunk) =>\n Effect.sync(() => {\n chunks.push(chunk);\n }),\n );\n\n // Combine chunks\n const totalLength = chunks.reduce((sum, c) => sum + c.byteLength, 0);\n const inputBuffer = new Uint8Array(totalLength);\n let offset = 0;\n for (const chunk of chunks) {\n inputBuffer.set(chunk, offset);\n offset += chunk.byteLength;\n }\n\n // Apply transformation (reuse buffered transform logic)\n let pipeline = sharp(inputBuffer);\n\n switch (transformation.type) {\n case \"resize\": {\n const sharpFit = mapFitToSharp(transformation.fit);\n pipeline = pipeline.resize(\n transformation.width,\n transformation.height,\n { fit: sharpFit },\n );\n break;\n }\n\n case \"blur\": {\n pipeline = pipeline.blur(transformation.sigma);\n break;\n }\n\n case \"rotate\": {\n const options = transformation.background\n ? { background: transformation.background }\n : undefined;\n pipeline = pipeline.rotate(transformation.angle, options);\n break;\n }\n\n case \"flip\": {\n if (transformation.direction === \"horizontal\") {\n pipeline = pipeline.flop();\n } else {\n pipeline = pipeline.flip();\n }\n break;\n }\n\n case \"grayscale\": {\n pipeline = pipeline.grayscale();\n break;\n }\n\n case \"sepia\": {\n pipeline = pipeline.tint({ r: 112, g: 66, b: 20 });\n break;\n }\n\n case \"brightness\": {\n const multiplier = 1 + transformation.value / 100;\n pipeline = pipeline.modulate({ brightness: multiplier });\n break;\n }\n\n case \"contrast\": {\n const a = 1 + transformation.value / 100;\n pipeline = pipeline.linear(a, 0);\n break;\n }\n\n case \"sharpen\": {\n if (transformation.sigma !== undefined) {\n pipeline = pipeline.sharpen({ sigma: transformation.sigma });\n } else {\n pipeline = pipeline.sharpen();\n }\n break;\n }\n\n case \"watermark\":\n case \"logo\":\n case \"text\": {\n // These transformations require async operations and metadata lookups\n // Fall back to the buffered transform for these complex cases\n return yield* Effect.fail(\n UploadistaError.fromCode(\"UNKNOWN_ERROR\", {\n body: `Streaming not supported for ${transformation.type} transformation. Use buffered mode.`,\n }),\n );\n }\n\n default: {\n return yield* Effect.fail(\n UploadistaError.fromCode(\"UNKNOWN_ERROR\", {\n body: `Unsupported transformation type: ${(transformation as { type: string }).type}`,\n }),\n );\n }\n }\n\n // Process through Sharp and output as stream\n return Stream.async<Uint8Array, UploadistaError>((emit) => {\n // Use Sharp's streaming output\n const outputStream = new PassThrough();\n const outputChunks: Buffer[] = [];\n\n pipeline\n .pipe(outputStream)\n .on(\"data\", (chunk: Buffer) => {\n outputChunks.push(chunk);\n })\n .on(\"end\", () => {\n // Emit all collected chunks as a single Uint8Array\n const outputBuffer = Buffer.concat(outputChunks);\n emit.single(new Uint8Array(outputBuffer));\n emit.end();\n })\n .on(\"error\", (error: Error) => {\n emit.fail(\n UploadistaError.fromCode(\"UNKNOWN_ERROR\", {\n body: `Sharp streaming transform failed: ${error.message}`,\n cause: error,\n }),\n );\n });\n\n // Cleanup\n return Effect.sync(() => {\n outputStream.destroy();\n });\n });\n }).pipe(\n withOperationSpan(\"image\", \"transform-stream\", {\n \"image.transformation_type\": transformation.type,\n }),\n );\n },\n }),\n );\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@uploadista/flow-images-sharp",
3
3
  "type": "module",
4
- "version": "0.0.20-beta.9",
4
+ "version": "0.1.0-beta.5",
5
5
  "description": "Sharp image processing service for Uploadista Flow",
6
6
  "license": "MIT",
7
7
  "author": "Uploadista",
@@ -16,8 +16,8 @@
16
16
  "dependencies": {
17
17
  "sharp": "0.34.5",
18
18
  "tinycolor2": "1.6.0",
19
- "@uploadista/core": "0.0.20-beta.9",
20
- "@uploadista/observability": "0.0.20-beta.9"
19
+ "@uploadista/core": "0.1.0-beta.5",
20
+ "@uploadista/observability": "0.1.0-beta.5"
21
21
  },
22
22
  "peerDependencies": {
23
23
  "effect": "^3.0.0",
@@ -25,20 +25,20 @@
25
25
  },
26
26
  "devDependencies": {
27
27
  "@effect/vitest": "0.27.0",
28
- "@types/node": "24.10.4",
28
+ "@types/node": "24.10.8",
29
29
  "@types/tinycolor2": "1.4.6",
30
- "effect": "3.19.12",
31
- "tsdown": "0.18.0",
32
- "vitest": "4.0.15",
33
- "zod": "4.2.0",
34
- "@uploadista/typescript-config": "0.0.20-beta.9"
30
+ "effect": "3.19.14",
31
+ "tsdown": "0.19.0",
32
+ "vitest": "4.0.17",
33
+ "zod": "4.3.5",
34
+ "@uploadista/typescript-config": "0.1.0-beta.5"
35
35
  },
36
36
  "scripts": {
37
37
  "build": "tsc --noEmit && tsdown",
38
38
  "format": "biome format --write ./src",
39
39
  "lint": "biome lint --write ./src",
40
40
  "check": "biome check --write ./src",
41
- "test": "vitest",
41
+ "test": "vitest run",
42
42
  "test:run": "vitest run",
43
43
  "test:watch": "vitest watch"
44
44
  }
@@ -52,7 +52,7 @@ describe("Sharp Image Plugin", () => {
52
52
  getImageMetadata(optimized),
53
53
  );
54
54
  expect(metadata.format).toBe("jpeg");
55
- }).pipe(Effect.provide(imagePlugin)),
55
+ }).pipe(Effect.provide(imagePlugin())),
56
56
  );
57
57
 
58
58
  it.effect("should optimize to webp format", () =>
@@ -71,7 +71,7 @@ describe("Sharp Image Plugin", () => {
71
71
  getImageMetadata(optimized),
72
72
  );
73
73
  expect(metadata.format).toBe("webp");
74
- }).pipe(Effect.provide(imagePlugin)),
74
+ }).pipe(Effect.provide(imagePlugin())),
75
75
  );
76
76
 
77
77
  it.effect("should optimize to png format", () =>
@@ -90,7 +90,7 @@ describe("Sharp Image Plugin", () => {
90
90
  getImageMetadata(optimized),
91
91
  );
92
92
  expect(metadata.format).toBe("png");
93
- }).pipe(Effect.provide(imagePlugin)),
93
+ }).pipe(Effect.provide(imagePlugin())),
94
94
  );
95
95
 
96
96
  it.effect("should handle different quality levels", () =>
@@ -114,7 +114,7 @@ describe("Sharp Image Plugin", () => {
114
114
 
115
115
  // Lower quality should result in smaller file size
116
116
  expect(lowQuality.length).toBeLessThan(highQuality.length);
117
- }).pipe(Effect.provide(imagePlugin)),
117
+ }).pipe(Effect.provide(imagePlugin())),
118
118
  );
119
119
  });
120
120
 
@@ -135,7 +135,7 @@ describe("Sharp Image Plugin", () => {
135
135
  const metadata = yield* Effect.promise(() => getImageMetadata(resized));
136
136
  expect(metadata.width).toBe(200);
137
137
  expect(metadata.height).toBe(150);
138
- }).pipe(Effect.provide(imagePlugin)),
138
+ }).pipe(Effect.provide(imagePlugin())),
139
139
  );
140
140
 
141
141
  it.effect("should resize with width only", () =>
@@ -154,7 +154,7 @@ describe("Sharp Image Plugin", () => {
154
154
  expect(metadata.width).toBe(200);
155
155
  // Height should be proportional
156
156
  expect(metadata.height).toBeLessThanOrEqual(300);
157
- }).pipe(Effect.provide(imagePlugin)),
157
+ }).pipe(Effect.provide(imagePlugin())),
158
158
  );
159
159
 
160
160
  it.effect("should resize with height only", () =>
@@ -172,7 +172,7 @@ describe("Sharp Image Plugin", () => {
172
172
  const metadata = yield* Effect.promise(() => getImageMetadata(resized));
173
173
  expect(metadata.height).toBe(150);
174
174
  expect(metadata.width).toBeLessThanOrEqual(400);
175
- }).pipe(Effect.provide(imagePlugin)),
175
+ }).pipe(Effect.provide(imagePlugin())),
176
176
  );
177
177
 
178
178
  it.effect("should handle 'cover' fit mode", () =>
@@ -191,7 +191,7 @@ describe("Sharp Image Plugin", () => {
191
191
  const metadata = yield* Effect.promise(() => getImageMetadata(resized));
192
192
  expect(metadata.width).toBe(200);
193
193
  expect(metadata.height).toBe(200);
194
- }).pipe(Effect.provide(imagePlugin)),
194
+ }).pipe(Effect.provide(imagePlugin())),
195
195
  );
196
196
 
197
197
  it.effect("should handle 'contain' fit mode", () =>
@@ -210,7 +210,7 @@ describe("Sharp Image Plugin", () => {
210
210
  const metadata = yield* Effect.promise(() => getImageMetadata(resized));
211
211
  expect(metadata.width).toBeLessThanOrEqual(200);
212
212
  expect(metadata.height).toBeLessThanOrEqual(200);
213
- }).pipe(Effect.provide(imagePlugin)),
213
+ }).pipe(Effect.provide(imagePlugin())),
214
214
  );
215
215
 
216
216
  it.effect("should handle 'fill' fit mode (maps to cover)", () =>
@@ -229,7 +229,7 @@ describe("Sharp Image Plugin", () => {
229
229
  const metadata = yield* Effect.promise(() => getImageMetadata(resized));
230
230
  expect(metadata.width).toBe(200);
231
231
  expect(metadata.height).toBe(200);
232
- }).pipe(Effect.provide(imagePlugin)),
232
+ }).pipe(Effect.provide(imagePlugin())),
233
233
  );
234
234
 
235
235
  it.effect("should handle upscaling", () =>
@@ -248,7 +248,7 @@ describe("Sharp Image Plugin", () => {
248
248
  const metadata = yield* Effect.promise(() => getImageMetadata(resized));
249
249
  expect(metadata.width).toBe(200);
250
250
  expect(metadata.height).toBe(200);
251
- }).pipe(Effect.provide(imagePlugin)),
251
+ }).pipe(Effect.provide(imagePlugin())),
252
252
  );
253
253
  });
254
254
 
@@ -271,7 +271,7 @@ describe("Sharp Image Plugin", () => {
271
271
  // After 90° rotation, dimensions should swap
272
272
  expect(metadata.width).toBe(100);
273
273
  expect(metadata.height).toBe(200);
274
- }).pipe(Effect.provide(imagePlugin)),
274
+ }).pipe(Effect.provide(imagePlugin())),
275
275
  );
276
276
 
277
277
  it.effect("should apply flip transformation", () =>
@@ -292,7 +292,7 @@ describe("Sharp Image Plugin", () => {
292
292
  );
293
293
  expect(metadata.width).toBe(200);
294
294
  expect(metadata.height).toBe(100);
295
- }).pipe(Effect.provide(imagePlugin)),
295
+ }).pipe(Effect.provide(imagePlugin())),
296
296
  );
297
297
 
298
298
  it.effect("should apply flop transformation", () =>
@@ -313,7 +313,7 @@ describe("Sharp Image Plugin", () => {
313
313
  );
314
314
  expect(metadata.width).toBe(200);
315
315
  expect(metadata.height).toBe(100);
316
- }).pipe(Effect.provide(imagePlugin)),
316
+ }).pipe(Effect.provide(imagePlugin())),
317
317
  );
318
318
 
319
319
  it.effect("should apply blur transformation", () =>
@@ -330,7 +330,7 @@ describe("Sharp Image Plugin", () => {
330
330
 
331
331
  expect(transformed).toBeInstanceOf(Uint8Array);
332
332
  expect(transformed.length).toBeGreaterThan(0);
333
- }).pipe(Effect.provide(imagePlugin)),
333
+ }).pipe(Effect.provide(imagePlugin())),
334
334
  );
335
335
 
336
336
  it.effect("should apply grayscale transformation", () =>
@@ -346,7 +346,7 @@ describe("Sharp Image Plugin", () => {
346
346
 
347
347
  expect(transformed).toBeInstanceOf(Uint8Array);
348
348
  expect(transformed.length).toBeGreaterThan(0);
349
- }).pipe(Effect.provide(imagePlugin)),
349
+ }).pipe(Effect.provide(imagePlugin())),
350
350
  );
351
351
 
352
352
  it.effect("should apply multiple transformations together", () =>
@@ -382,7 +382,7 @@ describe("Sharp Image Plugin", () => {
382
382
  // Dimensions should remain same after 180° rotation
383
383
  expect(metadata.width).toBe(200);
384
384
  expect(metadata.height).toBe(100);
385
- }).pipe(Effect.provide(imagePlugin)),
385
+ }).pipe(Effect.provide(imagePlugin())),
386
386
  );
387
387
 
388
388
  it.effect("should handle sepia transformation", () =>
@@ -398,7 +398,7 @@ describe("Sharp Image Plugin", () => {
398
398
 
399
399
  expect(transformed).toBeInstanceOf(Uint8Array);
400
400
  expect(transformed.length).toBeGreaterThan(0);
401
- }).pipe(Effect.provide(imagePlugin)),
401
+ }).pipe(Effect.provide(imagePlugin())),
402
402
  );
403
403
  });
404
404
 
@@ -416,7 +416,7 @@ describe("Sharp Image Plugin", () => {
416
416
  );
417
417
 
418
418
  expect(result._tag).toBe("Left");
419
- }).pipe(Effect.provide(imagePlugin)),
419
+ }).pipe(Effect.provide(imagePlugin())),
420
420
  );
421
421
 
422
422
  it.effect("should fail resize with invalid dimensions", () =>
@@ -435,7 +435,7 @@ describe("Sharp Image Plugin", () => {
435
435
  );
436
436
 
437
437
  expect(result._tag).toBe("Left");
438
- }).pipe(Effect.provide(imagePlugin)),
438
+ }).pipe(Effect.provide(imagePlugin())),
439
439
  );
440
440
  });
441
441
 
@@ -464,7 +464,7 @@ describe("Sharp Image Plugin", () => {
464
464
  const metadata = yield* Effect.promise(() => getImageMetadata(resized));
465
465
  expect(metadata.width).toBe(500);
466
466
  expect(metadata.height).toBe(500);
467
- }).pipe(Effect.provide(imagePlugin)),
467
+ }).pipe(Effect.provide(imagePlugin())),
468
468
  );
469
469
  });
470
470
  });