Transforming elements
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.
Supported element types
Apps can transform the following types of elements:
- Images
- Text
In the future, more types of elements will be supported.
How to transform elements
Step 1: Listen for selection events
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:
-
Import the
SelectionEvent
type and theselection
object:import { SelectionEvent, selection } from "@canva/preview/design";ts -
Use the
useState
hook to keep track of the current selection (if any):const [event, setEvent] = React.useState<SelectionEvent<"image"> | undefined>();tsThe
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
. -
Register a callback that stores the selection event in the
event
variable:React.useEffect(() => {selection.registerOnChange({scope: "image",onChange: (event) => {setEvent(event);},});}, []);tsThe
scope
property determines what types of events to listen for. The value must match what's passed into theSelectionEvent
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);
Step 2: Check if an element is selected
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;
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.
Step 3: Transform the selected element
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:
- Images
- Text
For images, the value
object contains a ref
property:
await selection.setContent(event, (value) => {console.log(value.ref);});
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>",};});
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.
Getting the URL of the selected image
Import the getTemporaryUrl
method from the @content/preview/asset
package:
import { getTemporaryUrl } from "@canva/preview/asset";
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);
Downloading and transforming the selected image
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,}),});
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 backendconst 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 imageconst response = await axios({url: req.body.url,method: "GET",responseType: "arraybuffer",});// Invert the image's colorsconst image = await Jimp.read(Buffer.from(response.data));image.invert();// Save the transformed image to "uploads" directoryconst 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 imageconst 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 typeconst mimeType = image.getMIME();// Return the URLs of the transformed image and thumbnailres.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...");});
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.
Uploading and returning the transformed image
-
Import the
upload
method from the@content/asset
package:import { getTemporaryUrl, upload } from "@content/asset";ts -
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 assetconst { url } = await getTemporaryUrl({type: "IMAGE",ref: value.ref,});// Send the URL to the app's backendconst 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 imageconst { ref } = await upload({type: "IMAGE",id: data.id,url: data.url,mimeType: data.mimeType,thumbnailUrl: data.thumbnailUrl,parentRef: value.ref,});// Return the transformed imagereturn {ref,};});ts
API reference
Code samples
Transforming images
Frontend
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 assetconst { url } = await getTemporaryUrl({type: "IMAGE",ref: value.ref,});// Send the URL to the app's backendconst 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 imageconst { ref } = await upload({type: "IMAGE",id: data.id,url: data.url,mimeType: data.mimeType,thumbnailUrl: data.thumbnailUrl,parentRef: value.ref,});// Return the transformed imagereturn {ref,};});}return (<div><div><button onClick={handleClick} disabled={!isElementSelected}>Invert</button></div></div>);};
Backend
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 backendconst 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 imageconst response = await axios({url: req.body.url,method: "GET",responseType: "arraybuffer",});// Invert the image's colorsconst image = await Jimp.read(Buffer.from(response.data));image.invert();// Save the transformed image to "uploads" directoryconst 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 imageconst 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 typeconst mimeType = image.getMIME();// Return the URLs of the transformed image and thumbnailres.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...");});
Transforming text
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>);};