Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/url-loader/src/lib/cloudinary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -48,6 +49,7 @@ export const transformationPlugins = [
removeBackgroundPlugin,
removePlugin,
replacePlugin,
replaceBackgroundPlugin,
restorePlugin,

// Cropping needs to be before any other general transformations
Expand Down
58 changes: 58 additions & 0 deletions packages/url-loader/src/plugins/replace-background.ts
Original file line number Diff line number Diff line change
@@ -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<ImageOptions>;
2 changes: 2 additions & 0 deletions packages/url-loader/src/types/image.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -19,6 +20,7 @@ export const imageOptionsSchema = assetOptionsSchema.merge(
...recolorProps,
...removeProps,
...replaceProps,
...replaceBackgroundProps,
...restoreProps,
...zoompanProps,
})
Expand Down
5 changes: 5 additions & 0 deletions packages/url-loader/tests/lib/cloudinary.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -883,6 +883,10 @@ describe('Cloudinary', () => {
removeShadow: true
},
removeBackground: true,
replaceBackground: {
prompt: 'space jellyfish',
seed: 2
},
restore: true,
zoompan: true,
},
Expand All @@ -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}`,
Expand Down
66 changes: 66 additions & 0 deletions packages/url-loader/tests/plugins/replace-background.spec.js
Original file line number Diff line number Diff line change
@@ -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}/`);
});
});
});