Transforming elements

How to transform elements in a user's design.

Apps can detect when a user has selected one or more elements and then transform those elements. This unlocks a number of powerful features, such as image effects and text manipulation.

Apps can transform the following types of elements:

  • Images
  • Text

In the future, more types of elements will be supported.

When a user selects an element, Canva emits a selection event. This event contains information about the selected elements, which apps can then use to transform them.

To listen for selection events:

  1. Import the SelectionEvent type and the selection object:

    import { SelectionEvent, selection } from "@canva/preview/design";
    ts
  2. Use the useState hook to keep track of the current selection (if any):

    const [event, setEvent] = React.useState<
    SelectionEvent<"image"> | undefined
    >();
    ts

    The SelectionEvent type is a generic that accepts the type of element as its only argument.

    The argument may be one of the following values:

    • "image"
    • "text"

    When an element isn't selected, there isn't a selection event, so the state may be undefined.

  3. Register a callback that stores the selection event in the event variable:

    React.useEffect(() => {
    selection.registerOnChange({
    scope: "image",
    onChange: (event) => {
    setEvent(event);
    },
    });
    }, []);
    ts

    The scope property determines what types of events to listen for. The value must match what's passed into the SelectionEvent generic, meaning either of the following values:

    • "image"
    • "text"

Based on these changes, the app can access the selection event via the event variable:

console.log(event);
ts

The selection event is an object that contains the selection scope — for example, "image" — and a count property that contains the number of selected elements.

You can use the count property to check if an element is selected:

const isElementSelected = event && event.count > 0;
ts

This is useful for determining what UI to render based on the selection of elements — for example, disabling a button if an element isn't selected.

The Apps SDK exposes an setContent method that accepts two arguments:

  • a selection event
  • a function that transforms the selected element(s)

The function receives a value object as its only parameter. The contents of this object — and the expected return value of the function — depends on the selection scope:

For images, the value object contains a ref property:

await selection.setContent(event, (value) => {
console.log(value.ref);
});
ts

This property contains a unique identifier that points to an image in Canva's backend. By returning a reference that points to a different image, the app can replace the image:

await selection.setContent(event, (value) => {
return {
ref: "<INSERT_REFERENCE_HERE>",
};
});
ts

Within this function, the app has a lot of control over how it transforms the image. Generally speaking though, an app will need to:

  • Get the URL of the selected image
  • Download and transform the selected image
  • Upload and return the transformed image

The remainder of this step demonstrates how to create an app that implements these behaviors. The app itself inverts the colors of the selected image.

Import the getTemporaryUrl method from the @content/preview/asset package:

import { getTemporaryUrl } from "@canva/preview/asset";
ts

This method accepts a ref and returns a URL for downloading the underlying asset:

const { url } = await getTemporaryUrl({
type: "IMAGE",
ref: value.ref,
});
console.log(url);
ts

Send the URL to an endpoint that can transform the image:

const response = await fetch("http://localhost:3000/invert-image", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
url,
}),
});
ts

The following code sample demonstrates how to create an endpoint that downloads the user's image, inverts its colors, and returns the necessary data to upload the image to Canva:

import axios from "axios";
import cors from "cors";
import express from "express";
import Jimp from "jimp";
import path from "path";
// TODO: Add the URL of the server here — it must be available to Canva's backend
const PUBLIC_SERVER_URL = "<INSERT_PUBLIC_SERVER_URL_HERE>";
const app = express();
app.use(cors());
app.use(express.json());
app.use("/uploads", express.static(path.join(__dirname, "uploads")));
app.post("/invert-image", async (req, res) => {
// Download the image
const response = await axios({
url: req.body.url,
method: "GET",
responseType: "arraybuffer",
});
// Invert the image's colors
const image = await Jimp.read(Buffer.from(response.data));
image.invert();
// Save the transformed image to "uploads" directory
const id = Date.now().toString();
const imageName = `${id}.jpg`;
const imagePath = path.join(__dirname, "uploads", imageName);
await image.writeAsync(imagePath);
// Create a thumbnail of the transformed image
const thumbnailName = `${id}_thumbnail.jpg`;
const thumbnailPath = path.join(__dirname, "uploads", thumbnailName);
const thumbnailWidth = 300;
const thumbnailHeight = Jimp.AUTO;
image.resize(thumbnailWidth, thumbnailHeight);
await image.writeAsync(thumbnailPath);
// Get the image's MIME type
const mimeType = image.getMIME();
// Return the URLs of the transformed image and thumbnail
res.json({
id,
url: `${PUBLIC_SERVER_URL}/uploads/${imageName}`,
thumbnailUrl: `${PUBLIC_SERVER_URL}/uploads/${thumbnailName}`,
mimeType,
});
});
app.listen(process.env.PORT || 3000, () => {
console.log("The server is running...");
});
ts

It's worth noting that:

  • Cross-Origin Resource Sharing (CORS) must be enabled.
  • The returned URLs must be exposed via the internet. This is because Canva's backend must be able to download them, so localhost URLs won't work.
  1. Import the upload method from the @content/asset package:

    import { getTemporaryUrl, upload } from "@content/asset";
    ts
  2. Upload the transformed image to Canva's backend and return a reference to the image:

    await selection.setContent(event, async (value) => {
    // Get the URL of an asset
    const { url } = await getTemporaryUrl({
    type: "IMAGE",
    ref: value.ref,
    });
    // Send the URL to the app's backend
    const response = await fetch("http://localhost:3000/invert-image", {
    method: "POST",
    headers: {
    "Content-Type": "application/json",
    },
    body: JSON.stringify({
    url,
    }),
    });
    const data = await response.json();
    // Upload the transformed image
    const { ref } = await upload({
    type: "IMAGE",
    id: data.id,
    url: data.url,
    mimeType: data.mimeType,
    thumbnailUrl: data.thumbnailUrl,
    parentRef: value.ref,
    });
    // Return the transformed image
    return {
    ref,
    };
    });
    ts
import { selection, SelectionEvent } from "@canva/preview/design";
import { getTemporaryUrl, upload } from "@canva/asset";
import React from "react";
export const App = () => {
const [event, setEvent] = React.useState<
SelectionEvent<"image"> | undefined
>();
React.useEffect(() => {
selection.registerOnChange({
scope: "image",
onChange: (event) => {
setEvent(event);
},
});
}, []);
const isElementSelected = event && event.count > 0;
async function handleClick() {
if (!event || !isElementSelected) {
return;
}
selection.setContent(event, async (value) => {
// Get the URL of an asset
const { url } = await getTemporaryUrl({
type: "IMAGE",
ref: value.ref,
});
// Send the URL to the app's backend
const response = await fetch("http://localhost:3000/invert-image", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
url,
}),
});
const data = await response.json();
// Upload the transformed image
const { ref } = await upload({
type: "IMAGE",
id: data.id,
url: data.url,
mimeType: data.mimeType,
thumbnailUrl: data.thumbnailUrl,
parentRef: value.ref,
});
// Return the transformed image
return {
ref,
};
});
}
return (
<div>
<div>
<button onClick={handleClick} disabled={!isElementSelected}>
Invert
</button>
</div>
</div>
);
};
tsx
import axios from "axios";
import cors from "cors";
import express from "express";
import Jimp from "jimp";
import path from "path";
// TODO: Add the URL of the server here — it must be available to Canva's backend
const PUBLIC_SERVER_URL = "<INSERT_PUBLIC_SERVER_URL_HERE>";
const app = express();
app.use(cors());
app.use(express.json());
app.use("/uploads", express.static(path.join(__dirname, "uploads")));
app.post("/invert-image", async (req, res) => {
// Download the image
const response = await axios({
url: req.body.url,
method: "GET",
responseType: "arraybuffer",
});
// Invert the image's colors
const image = await Jimp.read(Buffer.from(response.data));
image.invert();
// Save the transformed image to "uploads" directory
const id = Date.now().toString();
const imageName = `${id}.jpg`;
const imagePath = path.join(__dirname, "uploads", imageName);
await image.writeAsync(imagePath);
// Create a thumbnail of the transformed image
const thumbnailName = `${id}_thumbnail.jpg`;
const thumbnailPath = path.join(__dirname, "uploads", thumbnailName);
const thumbnailWidth = 300;
const thumbnailHeight = Jimp.AUTO;
image.resize(thumbnailWidth, thumbnailHeight);
await image.writeAsync(thumbnailPath);
// Get the image's MIME type
const mimeType = image.getMIME();
// Return the URLs of the transformed image and thumbnail
res.json({
id,
url: `${PUBLIC_SERVER_URL}/uploads/${imageName}`,
thumbnailUrl: `${PUBLIC_SERVER_URL}/uploads/${thumbnailName}`,
mimeType,
});
});
app.listen(process.env.PORT || 3000, () => {
console.log("The server is running...");
});
ts
import { selection, SelectionEvent } from "@canva/preview/design";
import React from "react";
export const App = () => {
const [event, setEvent] = React.useState<
SelectionEvent<"text"> | undefined
>();
React.useEffect(() => {
selection.registerOnChange({
scope: "text",
onChange: (event) => {
setEvent(event);
},
});
}, []);
const isElementSelected = event && event.count > 0;
async function handleClick() {
if (!event || !isElementSelected) {
return;
}
await selection.setContent(event, () => {
return {
text: "You updated the selected text!",
};
});
}
return (
<div>
<button onClick={handleClick} disabled={!isElementSelected}>
Update selected text
</button>
</div>
);
};
tsx