diff --git a/packages/url-loader/src/lib/cloudinary.ts b/packages/url-loader/src/lib/cloudinary.ts index 6a46b424..e8abdf0d 100644 --- a/packages/url-loader/src/lib/cloudinary.ts +++ b/packages/url-loader/src/lib/cloudinary.ts @@ -20,6 +20,7 @@ import { recolorPlugin } from "../plugins/recolor.js"; import { removeBackgroundPlugin } from "../plugins/remove-background.js"; import { removePlugin } from "../plugins/remove.js"; import { replacePlugin } from "../plugins/replace.js"; +import { replaceBackgroundPlugin } from "../plugins/replace-background.js"; import { restorePlugin } from "../plugins/restore.js"; import { sanitizePlugin } from "../plugins/sanitize.js"; import { seoPlugin } from "../plugins/seo.js"; @@ -48,6 +49,7 @@ export const transformationPlugins = [ removeBackgroundPlugin, removePlugin, replacePlugin, + replaceBackgroundPlugin, restorePlugin, // Cropping needs to be before any other general transformations diff --git a/packages/url-loader/src/plugins/replace-background.ts b/packages/url-loader/src/plugins/replace-background.ts new file mode 100644 index 00000000..de664fd4 --- /dev/null +++ b/packages/url-loader/src/plugins/replace-background.ts @@ -0,0 +1,58 @@ +import { z } from "zod"; +import type { ImageOptions } from "../types/image.js"; +import type { TransformationPlugin } from "../types/plugins.js"; + +export const replaceBackgroundProps = { + replaceBackground: z + .union([ + z.boolean(), + z.string(), + z.object({ + seed: z.number().optional(), + prompt: z.string().optional(), + }), + ]) + .describe( + JSON.stringify({ + text: "Replaces the background of an image with an AI-generated background.", + url: "https://cloudinary.com/documentation/transformation_reference#e_gen_background_replace", + }) + ) + .optional(), +}; + +export const replaceBackgroundPlugin = { + props: replaceBackgroundProps, + assetTypes: ["image", "images"], + plugin: (settings) => { + const { cldAsset, options } = settings; + const { replaceBackground } = options; + + if (replaceBackground === false || typeof replaceBackground === "undefined") return {}; + + const properties = []; + + if ( typeof replaceBackground === 'object' ) { + + if ( typeof replaceBackground.prompt !== 'undefined' ) { + properties.push(`prompt_${replaceBackground.prompt}`); + } + + if ( typeof replaceBackground.seed === 'number' ) { + properties.push(`seed_${replaceBackground.seed}`); + } + } else if ( typeof replaceBackground === 'string' ) { + properties.push(`prompt_${replaceBackground}`); + } + + let transformation = 'e_gen_background_replace'; + + if ( properties.length > 0 ) { + transformation = `${transformation}:${properties.join(';')}`; + } + + cldAsset.addTransformation(transformation); + + return {}; + }, +} satisfies TransformationPlugin; diff --git a/packages/url-loader/src/types/image.ts b/packages/url-loader/src/types/image.ts index 18e2bcde..dbf89c14 100644 --- a/packages/url-loader/src/types/image.ts +++ b/packages/url-loader/src/types/image.ts @@ -5,6 +5,7 @@ import { fillBackgroundProps } from "../plugins/fill-background.js"; import { recolorProps } from "../plugins/recolor.js"; import { removeProps } from "../plugins/remove.js"; import { replaceProps } from "../plugins/replace.js"; +import { replaceBackgroundProps } from "../plugins/replace-background.js"; import { restoreProps } from "../plugins/restore.js"; import { zoompanProps } from "../plugins/zoompan.js"; import { assetOptionsSchema } from "./asset.js"; @@ -19,6 +20,7 @@ export const imageOptionsSchema = assetOptionsSchema.merge( ...recolorProps, ...removeProps, ...replaceProps, + ...replaceBackgroundProps, ...restoreProps, ...zoompanProps, }) diff --git a/packages/url-loader/tests/lib/cloudinary.spec.js b/packages/url-loader/tests/lib/cloudinary.spec.js index f79a73d2..2a40ecb0 100644 --- a/packages/url-loader/tests/lib/cloudinary.spec.js +++ b/packages/url-loader/tests/lib/cloudinary.spec.js @@ -883,6 +883,10 @@ describe('Cloudinary', () => { removeShadow: true }, removeBackground: true, + replaceBackground: { + prompt: 'space jellyfish', + seed: 2 + }, restore: true, zoompan: true, }, @@ -900,6 +904,7 @@ describe('Cloudinary', () => { 'e_gen_recolor:prompt_duck;to-color_blue;multiple_true', `e_background_removal`, `e_gen_remove:prompt_apple;multiple_true;remove-shadow_true`, + `e_gen_background_replace:prompt_space%20jellyfish;seed_2`, `e_gen_restore`, `c_${crop},w_${width},h_${height},g_auto,z_${zoom}`, `d_${defaultImage}`, diff --git a/packages/url-loader/tests/plugins/replace-background.spec.js b/packages/url-loader/tests/plugins/replace-background.spec.js new file mode 100644 index 00000000..4a91cb8b --- /dev/null +++ b/packages/url-loader/tests/plugins/replace-background.spec.js @@ -0,0 +1,66 @@ +import { Cloudinary } from "@cloudinary/url-gen"; +import { describe, expect, it } from 'vitest'; + +import { replaceBackgroundPlugin } from "../../src/plugins/replace-background"; + +const { plugin } = replaceBackgroundPlugin; + +const cld = new Cloudinary({ + cloud: { + cloudName: "test-cloud-name", + }, +}); + +const TEST_PUBLIC_ID = "test-public-id"; + +describe("Plugins", () => { + describe("Generative Replace Background", () => { + it("should replace the background", () => { + const cldImage = cld.image(TEST_PUBLIC_ID); + + const options = { + replaceBackground: true, + }; + + plugin({ + cldAsset: cldImage, + options, + }); + + expect(cldImage.toURL()).toContain('/e_gen_background_replace/'); + }); + + it("should replace the background with a prompt", () => { + const cldImage = cld.image(TEST_PUBLIC_ID); + + const options = { + replaceBackground: 'space jellyfish in space', + }; + + plugin({ + cldAsset: cldImage, + options, + }); + + expect(cldImage.toURL()).toContain(`/e_gen_background_replace:prompt_${encodeURIComponent(options.replaceBackground)}/`); + }); + + it("should replace the background with a prompt", () => { + const cldImage = cld.image(TEST_PUBLIC_ID); + + const options = { + replaceBackground: { + prompt: 'space jellyfish in outer space', + seed: 2 + } + }; + + plugin({ + cldAsset: cldImage, + options, + }); + + expect(cldImage.toURL()).toContain(`/e_gen_background_replace:prompt_${encodeURIComponent(options.replaceBackground.prompt)};seed_${options.replaceBackground.seed}/`); + }); + }); +});