On September 25th, 2024, we released v2 of the Apps SDK. To learn what’s new and how to upgrade, see Migration FAQ and Migration guide.

Replacing elements

How to replace elements in a design.

In addition to reading content in a user's design, apps can also replace certain types of content. This unlocks a range of powerful features, such as image effects and text manipulation.

Check out the Selection guidelines

Our design guidelines help you create a high-quality app that easily passes app review.

How to replace images

Loading…

Step 1: Enable the required permissions

In the Developer Portal, enable the following permissions:

  • canva:design:content:read
  • canva:design:content:write
  • canva:asset:private:read
  • canva:asset:private:write

In the future, the Apps SDK will throw an error if the required permissions are not enabled.

To learn more, see Configuring permissions.

Step 2: Get the selected image content

Use the useSelection hook or register a callback with the selection.registerOnChange method:

import React from "react";
import { Button, Rows } from "@canva/app-ui-kit";
import { useSelection } from "utils/use_selection_hook"; // https://github.com/canva-sdks/canva-apps-sdk-starter-kit/blob/main/utils/use_selection_hook.ts
import * as styles from "styles/components.css";
export function App() {
const currentSelection = useSelection("image");
const isElementSelected = currentSelection.count > 0;
async function handleClick() {
if (!isElementSelected) {
return;
}
const draft = await currentSelection.read();
console.log(draft.contents); // => [{ ref: "..." }]
}
return (
<div className={styles.scrollContainer}>
<Rows spacing="1u">
<Button
variant="primary"
disabled={!isElementSelected}
onClick={handleClick}
>
Replace selected image content
</Button>
</Rows>
</div>
);
}
TSX
import React from "react";
import { Button, Rows } from "@canva/app-ui-kit";
import { selection, SelectionEvent } from "@canva/design";
import * as styles from "styles/components.css";
export function App() {
const [currentSelection, setCurrentSelection] =
React.useState<SelectionEvent<"image">>(undefined);
const isElementSelected = (currentSelection?.count ?? 0) > 0;
React.useEffect(() => {
return selection.registerOnChange({
scope: "image",
onChange: setCurrentSelection,
});
}, []);
async function handleClick() {
if (!isElementSelected || !currentSelection) {
return;
}
const draft = await currentSelection.read();
console.log(draft.contents); // => [{ ref: "..." }]
}
return (
<div className={styles.scrollContainer}>
<Rows spacing="1u">
<Button
variant="primary"
disabled={!isElementSelected}
onClick={handleClick}
>
Replace selected image content
</Button>
</Rows>
</div>
);
}
TSX

The selection event has a read method that returns an array of the selected images. Each image is represented as an object with a ref property. The ref contains a unique identifier that points to an asset in Canva's backend:

const draft = await currentSelection.read();
console.log(draft.contents); // => [{ ref: "..." }]
TS

To learn more, see Reading elements.

Step 3: Download the selected images

To access the ref property of each image, loop through the selected images:

const draft = await currentSelection.read();
for (const content of draft.contents) {
console.log(content.ref); // => "..."
}
TS

The value of the ref property is an opaque string. This means it's not intended to be read or manipulated. You can, however, convert the ref into a URL and then download the image data from that URL.

To convert the ref into a URL:

  1. Import the getTemporaryUrl method from the @canva/asset package:

    import { getTemporaryUrl } from "@canva/asset";
    TS
  2. Call the method, passing in the ref and the type of asset:

    const { url } = await getTemporaryUrl({
    type: "image",
    ref: content.ref,
    });
    console.log("Temporary URL:", url);
    TS

    The returned URL is temporary and expires after a short period of time. Your app should immediately download the image to ensure that it has ongoing access to it.

Step 4: Transform the images

Once the app has access to the URL of one or more images:

  1. Download the images.
  2. Transform the images.
  3. Upload the transformed images to Canva's backend.

In some cases, this process can take place entirely via the frontend. In other cases, such as when integrating with AI models, it makes more sense to handle the transformation via the app's backend.

To transform images via the app's frontend:

  1. Download each image.
  2. Draw each image into an HTMLCanvasElement.
  3. Apply some sort of transformation to each image.
  4. Get the data URL(opens in a new tab or window) of the transformed images.

The following code sample contains a reusable function that handles this logic for you:

import { getTemporaryUrl, ImageMimeType, ImageRef } from "@canva/asset";
/**
* Downloads and transforms a raster image.
* @param ref - A unique identifier that points to an image asset in Canva's backend.
* @param transformer - A function that transforms the image.
* @returns The data URL and MIME type of the transformed image.
*/
async function transformRasterImage(
ref: ImageRef,
transformer: (ctx: CanvasRenderingContext2D, imageData: ImageData) => void
): Promise<{ dataUrl: string; mimeType: ImageMimeType }> {
// Get a temporary URL for the asset
const { url } = await getTemporaryUrl({
type: "image",
ref,
});
// Download the image
const response = await fetch(url, { mode: "cors" });
const imageBlob = await response.blob();
// Extract MIME type from the downloaded image
const mimeType = imageBlob.type;
// Warning: This doesn't attempt to handle SVG images
if (!isSupportedMimeType(mimeType)) {
throw new Error(`Unsupported mime type: ${mimeType}`);
}
// Create an object URL for the image
const objectURL = URL.createObjectURL(imageBlob);
// Define an image element and load image from the object URL
const image = new Image();
image.crossOrigin = "Anonymous";
await new Promise((resolve, reject) => {
image.onload = resolve;
image.onerror = () => reject(new Error("Image could not be loaded"));
image.src = objectURL;
});
// Create a canvas and draw the image onto it
const canvas = document.createElement("canvas");
canvas.width = image.width;
canvas.height = image.height;
const ctx = canvas.getContext("2d");
if (!ctx) {
throw new Error("CanvasRenderingContext2D is not available");
}
ctx.drawImage(image, 0, 0);
// Get the image data from the canvas to manipulate pixels
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
transformer(ctx, imageData);
// Put the transformed image data back onto the canvas
ctx.putImageData(imageData, 0, 0);
// Clean up: Revoke the object URL to free up memory
URL.revokeObjectURL(objectURL);
// Convert the canvas content to a data URL with the original MIME type
const dataUrl = canvas.toDataURL(mimeType);
return { dataUrl, mimeType };
}
function isSupportedMimeType(
input: string
): input is "image/jpeg" | "image/heic" | "image/png" | "image/webp" {
// This does not include "image/svg+xml"
const mimeTypes = ["image/jpeg", "image/heic", "image/png", "image/webp"];
return mimeTypes.includes(input);
}
TS

To use the transformRasterImage, function, pass an image reference in as the first argument and a function for transforming the image as the second argument. The following usage inverts the colors of an image:

const { dataUrl, mimeType } = await transformRasterImage(
content.ref,
(_, { data }) => {
// Invert the colors of each pixel
for (let i = 0; i < data.length; i += 4) {
data[i] = 255 - data[i];
data[i + 1] = 255 - data[i + 1];
data[i + 2] = 255 - data[i + 2];
}
}
);
console.log("The data URL of the transformed image is:", dataUrl);
TS

When processing images in the frontend, the final image is uploaded to Canva's backend from the user's device. This approach may incur data charges for users, especially on mobile.

To transform images via your app's backend, use the Fetch API to send the URL of the image 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,
}),
});
TS

Your app's backend must verify incoming HTTP requests. To learn more, see HTTP request verification.

On the backend, transform the image and return a URL that Canva can use to download the new image:

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

On the frontend, parse the response to access the returned data:

// Parse the response as JSON
const json = await response.json();
console.log(json.url); // => "https://..."
TS

The URL of the new image must be available via the public internet. This is because Canva's backend must be able to download the image. To learn more, see Uploading assets.

Step 5: Upload the replaced images

Once the app's frontend has a URL or data URL for the new image, upload the image to Canva's backend.

To upload the image:

  1. Import the upload method from the @canva/asset package:

    import { getTemporaryUrl, upload } from "@canva/asset";
    TS
  2. Call the method, passing in the required properties:

    // Upload the replaced image
    const asset = await upload({
    type: "image",
    mimeType: "image/png",
    url: "URL OR DATA URL GOES HERE",
    thumbnailUrl: "URL OR DATA URL GOES HERE",
    parentRef: content.ref,
    aiDisclosure: "none",
    });
    TS

    To learn more about the required properties, see upload.

    Handling derived assets

    The upload method accepts a parentRef property. This property doesn't have a visible impact on the app, but it must contain the ref of the originally selected image.

    Here's why:

    Canva licenses assets from a number of creators. By setting the parentRef property, Canva can keep track of the original asset and ensure that any licensing requirements are met.

    A side-effect of this requirement is that apps are not allowed to combine multiple assets into a single asset. This is because the tracking mechanism doesn't account for assets derived from multiple assets.

    If an app doesn't set the parentRef property it will be rejected during the app review process.

Step 6: Replace the images

In the loop, replace the current ref of the image with the new ref:

for (const content of draft.contents) {
// Get a temporary URL for the asset
// Download and transform the image
// Upload the transformed image
// ...
// Replace the image
content.ref = asset.ref;
}
TS

Step 7: Save the changes

By default, any changes to the content are not reflected in the user's design. To persist the changes and update the user's design, call the save method that's returned by the read method:

await draft.save();
TS

This method should be called after the loop.

How to replace plaintext

Loading…

Step 1: Enable the required permissions

In the Developer Portal, enable the following permissions:

  • canva:design:content:read
  • canva:design:content:write

In the future, the Apps SDK will throw an error if the required permissions are not enabled.

To learn more, see Configuring permissions.

Step 2: Get the selected plaintext content

Use the useSelection hook or register a callback with the selection.registerOnChange method:

import React from "react";
import { useSelection } from "utils/use_selection_hook"; // https://github.com/canva-sdks/canva-apps-sdk-starter-kit/blob/main/utils/use_selection_hook.ts
import { Button, Rows } from "@canva/app-ui-kit";
import * as styles from "styles/components.css";
export function App() {
const currentSelection = useSelection("plaintext");
const isElementSelected = currentSelection.count > 0;
async function replaceText() {
if (!isElementSelected) {
return;
}
const draft = await currentSelection.read();
console.log(draft.contents);
}
return (
<div className={styles.scrollContainer}>
<Rows spacing="1u">
<Button
variant="primary"
disabled={!isElementSelected}
onClick={replaceText}
>
Replace selected plaintext content
</Button>
</Rows>
</div>
);
}
TSX
import React from "react";
import { Button, Rows } from "@canva/app-ui-kit";
import { selection, SelectionEvent } from "@canva/design";
import * as styles from "styles/components.css";
export function App() {
const [currentSelection, setCurrentSelection] =
React.useState<SelectionEvent<"plaintext">>(undefined);
const isElementSelected = (currentSelection?.count ?? 0) > 0;
React.useEffect(() => {
return selection.registerOnChange({
scope: "plaintext",
onChange: setCurrentSelection,
});
}, []);
async function replaceText() {
if (!isElementSelected || !currentSelection) {
return;
}
const draft = await currentSelection.read();
console.log(draft.contents); // => [{ text: "..." }]
}
return (
<div className={styles.scrollContainer}>
<Rows spacing="1u">
<Button
variant="primary"
disabled={!isElementSelected}
onClick={replaceText}
>
Replace selected plaintext content
</Button>
</Rows>
</div>
);
}
TSX

To learn more, see Reading elements.

Step 3: Replace the content of the selected elements

Loop through the contents of the selected elements and replace the text property with a new value:

const draft = await currentSelection.read();
for (const content of draft.contents) {
content.text = `${content.text} was modified!`;
}
TS

Step 4: Save the changes

By default, any changes to the content are not reflected in the user's design. To persist the changes and update the user's design, call the save method that's returned by the read method:

await draft.save();
TS

This method should be called after the loop.

How to replace richtext

Step 1: Enable the required permissions

In the Developer Portal, enable the following permissions:

  • canva:design:content:read
  • canva:design:content:write

In the future, the Apps SDK will throw an error if the required permissions are not enabled.

To learn more, see Configuring permissions.

Step 2: Get the selected richtext content

The useSelection hook is not yet compatible with richtext selection, so import selection and SelectionEvent after installing @canva/design to register the callback:

import React from "react";
import { Button, Rows } from "@canva/app-ui-kit";
import { selection, SelectionEvent } from "@canva/design";
import * as styles from "styles/components.css";
export function App() {
const [currentSelection, setCurrentSelection] = React.useState<
SelectionEvent<"richtext"> | undefined
>();
const isElementSelected = (currentSelection?.count ?? 0) > 0;
React.useEffect(() => {
return selection.registerOnChange({
scope: "richtext",
onChange: setCurrentSelection,
});
}, []);
async function replaceText() {
if (!isElementSelected || !currentSelection) {
return;
}
const draft = await currentSelection.read();
for (const content of draft.contents) {
const regions = content.readTextRegions();
console.log(regions);
}
}
return (
<div className={styles.scrollContainer}>
<Rows spacing="1u">
<Button
variant="primary"
disabled={!isElementSelected}
onClick={replaceText}
>
Replace selected text content
</Button>
</Rows>
</div>
);
}
TSX

To learn more, see Reading elements.

Step 3: Replace the content of the selected elements

When working with richtext, each content object is a richtext range that exposes a variety of methods for interacting with that particular portion of richtext.

The following snippet demonstrates how to loop through the selected text regions and format each character:

// Keep track of the current color across text regions
let currentColorIndex = 0;
// Loop through all selected rich text content
for (const content of draft.contents) {
// Get the text regions
const regions = content.readTextRegions();
// Loop through each text region
for (const region of regions) {
// Loop through each character in the regions's text
for (let i = 0; i < region.text.length; i++) {
// Get the color for the current character
const colorIndex = (currentColorIndex + i) % RAINBOW_COLORS.length;
const color = RAINBOW_COLORS[colorIndex];
// Format the current character
content.formatText({ start: colorIndex + i, length: 1 }, { color });
}
// Update the current color
currentColorIndex += region.text.length;
}
}
TSX

To learn more, see Richtext ranges.

Step 4: Save the changes

By default, any changes to the content are not reflected in the user's design. To persist the changes and update the user's design, call the save method that's returned by the read method:

await draft.save();
TS

This method should be called after the loop.

How to replace videos

Step 1: Enable the required permissions

In the Developer Portal, enable the following permissions:

  • canva:design:content:read
  • canva:design:content:write
  • canva:asset:private:read
  • canva:asset:private:write

In the future, the Apps SDK will throw an error if the required permissions are not enabled.

To learn more, see Configuring permissions.

Step 2: Get the content of the selected elements

Use the useSelection hook or register a callback with the selection.registerOnChange method:

import React from "react";
import { Button, Rows } from "@canva/app-ui-kit";
import { useSelection } from "utils/use_selection_hook"; // https://github.com/canva-sdks/canva-apps-sdk-starter-kit/blob/main/utils/use_selection_hook.ts
import * as styles from "styles/components.css";
export function App() {
const currentSelection = useSelection("video");
const isElementSelected = currentSelection.count > 0;
async function handleClick() {
if (!isElementSelected) {
return;
}
const draft = await currentSelection.read();
console.log(draft.contents); // => [{ ref: "..." }]
}
return (
<div className={styles.scrollContainer}>
<Rows spacing="1u">
<Button
variant="primary"
disabled={!isElementSelected}
onClick={handleClick}
>
Replace selected video content
</Button>
</Rows>
</div>
);
}
TSX
import React from "react";
import { Button, Rows } from "@canva/app-ui-kit";
import { selection, SelectionEvent } from "@canva/design";
import * as styles from "styles/components.css";
export function App() {
const [currentSelection, setCurrentSelection] =
React.useState<SelectionEvent<"video">>(undefined);
const isElementSelected = (currentSelection?.count ?? 0) > 0;
React.useEffect(() => {
return selection.registerOnChange({
scope: "video",
onChange: setCurrentSelection,
});
}, []);
async function handleClick() {
if (!isElementSelected || !currentSelection) {
return;
}
const draft = await currentSelection.read();
console.log(draft.contents); // => [{ ref: "..." }]
}
return (
<div className={styles.scrollContainer}>
<Rows spacing="1u">
<Button
variant="primary"
disabled={!isElementSelected}
onClick={handleClick}
>
Replace selected video content
</Button>
</Rows>
</div>
);
}
TSX

The selection event has a read method that returns an array of the selected videos. Each video is represented as an object with a ref property. The ref contains a unique identifier that points to an asset in Canva's backend:

const draft = await currentSelection.read();
console.log(draft.contents); // => [{ ref: "..." }]
TS

To learn more, see Reading elements.

Step 3: Download the selected videos

To access the ref property of each video, loop through the selected videos:

const draft = await currentSelection.read();
for (const content of draft.contents) {
console.log(content.ref); // => "..."
}
TS

The value of the ref property is an opaque string. This means it's not intended to be read or manipulated. You can, however, convert the ref into a URL and then download the video data from that URL.

To convert the ref into a URL:

  1. Import the getTemporaryUrl method from the @canva/asset package:

    import { getTemporaryUrl } from "@canva/asset";
    TS
  2. Call the method, passing in the ref and the type of asset:

    const { url } = await getTemporaryUrl({
    type: "video",
    ref: content.ref,
    });
    console.log("Temporary URL:", url);
    TS

    The returned URL is temporary and expires after a short period of time. Your app should immediately download the video to ensure that it has ongoing access to it.

Step 4: Transform the videos

Once the app has access to the URL of one or more videos:

  1. Download the videos.
  2. Transform the videos.
  3. Upload the transformed videos to Canva's backend.

Technically speaking, it's possible for this process to take place entirely via the frontend. Generally though, it makes more sense to handle the transformation via the app's backend.

To transform videos via your app's backend:

  1. Use the Fetch API to send the URL of the video to the app's backend:

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

    Your app's backend must verify incoming HTTP requests. To learn more, see HTTP request verification.

  2. On the backend, transform the video and return a URL that Canva can use to download the new video.

    The URL of the new video must be available via the public internet. This is because Canva's backend must be able to download the video. To learn more, see Uploading assets.

  3. On the frontend, parse the response to access the returned data:

    // Parse the response as JSON
    const json = await response.json();
    console.log(json.url); // => "https://..."
    TS

Step 5: Upload the replaced videos

Once the app's frontend has a URL or data URL for the new video, upload the video to Canva's backend.

To upload the video:

  1. Import the upload method from the @canva/asset package:

    import { getTemporaryUrl, upload } from "@canva/asset";
    TS
  2. Call the method, passing in the required properties:

    // Upload the replaced video
    const asset = await upload({
    type: "video",
    mimeType: "video/mp4",
    url: "URL GOES HERE",
    thumbnailImageUrl: "URL GOES HERE",
    thumbnailVideoUrl: "URL GOES HERE",
    parentRef: content.ref,
    aiDisclosure: "none",
    });
    TS

    To learn more about the required properties, see upload.

    Handling derived assets

    The upload method accepts a parentRef property. This property doesn't have a visible impact on the app, but it must contain the ref of the originally selected video.

    Here's why:

    Canva licenses assets from a number of creators. By setting the parentRef property, Canva can keep track of the original asset and ensure that any licensing requirements are met.

    A side-effect of this requirement is that apps are not allowed to combine multiple assets into a single asset. This is because the tracking mechanism doesn't account for assets derived from multiple assets.

    If an app doesn't set the parentRef property it will be rejected during the app review process.

Step 6: Replace the videos

In the loop, replace the current ref of the video with the new ref:

for (const content of draft.contents) {
// Get a temporary URL for the asset
// Download and transform the video
// Upload the transformed video
// ...
// Replace the video
content.ref = asset.ref;
}
TS

Step 7: Save the changes

By default, any changes to the content are not reflected in the user's design. To persist the changes and update the user's design, call the save method that's returned by the read method:

await draft.save();
TS

This method should be called after the loop.

Known limitations

  • You can't replace one type of element with a different type of element.

Additional considerations

  • If multiple elements are selected, the ordering of the elements is not stable and should not be relied upon.
  • If something is selected when selection.registerOnChange is called, the callback fires immediately.

API reference

Code sample

Images

import React from "react";
import { Button, Rows } from "@canva/app-ui-kit";
import { getTemporaryUrl, upload, ImageMimeType, ImageRef } from "@canva/asset";
import { useSelection } from "utils/use_selection_hook"; // https://github.com/canva-sdks/canva-apps-sdk-starter-kit/blob/main/utils/use_selection_hook.ts
import * as styles from "styles/components.css";
export function App() {
const currentSelection = useSelection("image");
const isElementSelected = currentSelection.count > 0;
async function handleClick() {
if (!isElementSelected) {
return;
}
const draft = await currentSelection.read();
for (const content of draft.contents) {
// Download and transform the image
const newImage = await transformRasterImage(
content.ref,
(_, { data }) => {
for (let i = 0; i < data.length; i += 4) {
data[i] = 255 - data[i];
data[i + 1] = 255 - data[i + 1];
data[i + 2] = 255 - data[i + 2];
}
}
);
// Upload the transformed image
const asset = await upload({
type: "image",
url: newImage.dataUrl,
mimeType: newImage.mimeType,
thumbnailUrl: newImage.dataUrl,
parentRef: content.ref,
aiDisclosure: "none",
});
// Replace the image
content.ref = asset.ref;
}
await draft.save();
}
return (
<div className={styles.scrollContainer}>
<Rows spacing="1u">
<Button
variant="primary"
disabled={!isElementSelected}
onClick={handleClick}
>
Replace selected image content
</Button>
</Rows>
</div>
);
}
/**
* Downloads and transforms a raster image.
* @param ref - A unique identifier that points to an image asset in Canva's backend.
* @param transformer - A function that transforms the image.
* @returns The data URL and MIME type of the transformed image.
*/
async function transformRasterImage(
ref: ImageRef,
transformer: (ctx: CanvasRenderingContext2D, imageData: ImageData) => void
): Promise<{ dataUrl: string; mimeType: ImageMimeType }> {
// Get a temporary URL for the asset
const { url } = await getTemporaryUrl({
type: "image",
ref,
});
// Download the image
const response = await fetch(url, { mode: "cors" });
const imageBlob = await response.blob();
// Extract MIME type from the downloaded image
const mimeType = imageBlob.type;
// Warning: This doesn't attempt to handle SVG images
if (!isSupportedMimeType(mimeType)) {
throw new Error(`Unsupported mime type: ${mimeType}`);
}
// Create an object URL for the image
const objectURL = URL.createObjectURL(imageBlob);
// Define an image element and load image from the object URL
const image = new Image();
image.crossOrigin = "Anonymous";
await new Promise((resolve, reject) => {
image.onload = resolve;
image.onerror = () => reject(new Error("Image could not be loaded"));
image.src = objectURL;
});
// Create a canvas and draw the image onto it
const canvas = document.createElement("canvas");
canvas.width = image.width;
canvas.height = image.height;
const ctx = canvas.getContext("2d");
if (!ctx) {
throw new Error("CanvasRenderingContext2D is not available");
}
ctx.drawImage(image, 0, 0);
// Get the image data from the canvas to manipulate pixels
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
transformer(ctx, imageData);
// Put the transformed image data back onto the canvas
ctx.putImageData(imageData, 0, 0);
// Clean up: Revoke the object URL to free up memory
URL.revokeObjectURL(objectURL);
// Convert the canvas content to a data URL with the original MIME type
const dataUrl = canvas.toDataURL(mimeType);
return { dataUrl, mimeType };
}
function isSupportedMimeType(
input: string
): input is "image/jpeg" | "image/heic" | "image/png" | "image/webp" {
// This does not include "image/svg+xml"
const mimeTypes = ["image/jpeg", "image/heic", "image/png", "image/webp"];
return mimeTypes.includes(input);
}
TSX

Plaintext

import React from "react";
import { Button, Rows } from "@canva/app-ui-kit";
import { useSelection } from "utils/use_selection_hook"; // https://github.com/canva-sdks/canva-apps-sdk-starter-kit/blob/main/utils/use_selection_hook.ts
import * as styles from "styles/components.css";
export function App() {
const currentSelection = useSelection("plaintext");
const isElementSelected = currentSelection.count > 0;
async function handleClick() {
if (!isElementSelected) {
return;
}
const draft = await currentSelection.read();
for (const content of draft.contents) {
content.text = `${content.text} was modified!`;
}
await draft.save();
}
return (
<div className={styles.scrollContainer}>
<Rows spacing="1u">
<Button
variant="primary"
disabled={!isElementSelected}
onClick={handleClick}
>
Replace selected plaintext content
</Button>
</Rows>
</div>
);
}
TSX

Richtext

import React from "react";
import { Button, Rows } from "@canva/app-ui-kit";
import { selection, SelectionEvent } from "@canva/design";
import * as styles from "styles/components.css";
const RAINBOW_COLORS = [
"#FF0000",
"#FF7F00",
"#FFFF00",
"#00FF00",
"#0000FF",
"#4B0082",
"#8B00FF",
];
export function App() {
const [currentSelection, setCurrentSelection] = React.useState<
SelectionEvent<"richtext"> | undefined
>();
const isElementSelected = (currentSelection?.count ?? 0) > 0;
React.useEffect(() => {
return selection.registerOnChange({
scope: "richtext",
onChange: setCurrentSelection,
});
}, []);
async function handleClick() {
if (!isElementSelected || !currentSelection) {
return;
}
// Get a snapshot of the currently selected text
const draft = await currentSelection.read();
// Keep track of the current color across text regions
let currentColorIndex = 0;
// Loop through all selected richtext content
for (const content of draft.contents) {
// Get the text regions
const regions = content.readTextRegions();
// Loop through each text region
for (const region of regions) {
// Loop through each character in the regions’s text
for (let i = 0; i < region.text.length; i++) {
// Get the color for the current character
const colorIndex = (currentColorIndex + i) % RAINBOW_COLORS.length;
const color = RAINBOW_COLORS[colorIndex];
// Format the current character
content.formatText({ start: colorIndex, length: 1 }, { color });
}
// Update the current color
currentColorIndex += region.text.length;
}
}
// Commit the changes
await draft.save();
}
return (
<div className={styles.scrollContainer}>
<Rows spacing="1u">
<Button
variant="primary"
disabled={!isElementSelected}
onClick={handleClick}
>
Replace selected richtext content
</Button>
</Rows>
</div>
);
}
TSX

Videos

import React from "react";
import { Button, Rows } from "@canva/app-ui-kit";
import { useSelection } from "utils/use_selection_hook"; // https://github.com/canva-sdks/canva-apps-sdk-starter-kit/blob/main/utils/use_selection_hook.ts
import { upload } from "@canva/asset";
import * as styles from "styles/components.css";
export function App() {
const currentSelection = useSelection("video");
const isElementSelected = currentSelection.count > 0;
async function handleClick() {
if (!isElementSelected) {
return;
}
const draft = await currentSelection.read();
for (const content of draft.contents) {
// Upload the replacement video
const asset = await upload({
type: "video",
mimeType: "video/mp4",
url: "https://www.canva.dev/example-assets/video-import/beach-video.mp4",
thumbnailImageUrl:
"https://www.canva.dev/example-assets/video-import/beach-thumbnail-image.jpg",
thumbnailVideoUrl:
"https://www.canva.dev/example-assets/video-import/beach-thumbnail-video.mp4",
parentRef: content.ref,
aiDisclosure: "none",
});
// Replace the video
content.ref = asset.ref;
}
await draft.save();
}
return (
<div className={styles.scrollContainer}>
<Rows spacing="1u">
<Button
variant="primary"
disabled={!isElementSelected}
onClick={handleClick}
>
Replace selected video content
</Button>
</Rows>
</div>
);
}
TSX