Creating image overlays

Using the Apps SDK to support in-place editing for images.

An image overlay is an interactive editing surface that is placed on top of a selected image. They enable users to edit images and preview adjustments to images immediately and in context.

For example, imagine an app that applies effects to an image. Overlays allow the user to select an image in their design, choose an effect via the app, and see an in-place preview of what the effect looks like before applying it.

  • Apps can create image overlays for raster images and/or vector images.
  • Overlays are compatible with most image content, including image elements and page backgrounds.
  • Overlays can receive pointer events, which means apps can support brush controls.

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.

  1. Import the useOverlay hook from the utils directory in the starter kit:

    import { useOverlay } from "utils/use_overlay_hook";
    tsx
  2. Call the hook with an argument of "image_selection":

    const overlay = useOverlay("image_selection");
    tsx
  3. Create a function that calls the overlay's open method:

    function handleOpen() {
    overlay.open();
    }
    tsx
  4. Render a button that, when clicked, calls the handleOpen function:

    return (
    <div className={styles.scrollContainer}>
    <Rows spacing="1u">
    <Button variant="primary" onClick={handleOpen}>
    Edit image
    </Button>
    </Rows>
    </div>
    );
    tsx
  5. Use the canOpen property to disable the button if the overlay can't be opened:

    <Button variant="primary" disabled={!overlay.canOpen} onClick={handleOpen}>
    Edit image
    </Button>
    tsx

    These are some examples of when an overlay can't be opened:

    • an image isn't selected
    • more than one image is selected
    • an overlay is already open

    This is what the app looks like when the overlay can't be opened:

    This is what the app looks like when the overlay can be opened:

At this stage, the code should look like this:

import { Button, Rows } from "@canva/app-ui-kit";
import * as React from "react";
import styles from "styles/components.css";
import { useOverlay } from "utils/use_overlay_hook";
export function App() {
const overlay = useOverlay("image_selection");
function handleOpen() {
overlay.open();
}
return (
<div className={styles.scrollContainer}>
<Rows spacing="2u">
<Button
variant="primary"
disabled={!overlay.canOpen}
onClick={handleOpen}
>
Edit image
</Button>
</Rows>
</div>
);
}
tsx

To verify that the code works:

  1. Open the app in the Canva editor.
  2. Add an image to the design.
  3. Select the image.
  4. In the app, click Open.

An iframe should appear on top of the selected image. This iframe will load the same JavaScript bundle that's loaded in the object panel, which means the same user interface will be rendered in both iframes.

This isn't what we ultimately want, but it's a step in the right direction.

At the moment, we're always running the same code, regardless of where that code is running. What we actually want is to run one code path in the object panel and a separate code path for the image overlay.

To run different code based on where the code is running:

  1. Import the appProcess object from the @canva/preview/platform package:

    import { appProcess } from "@canva/preview/platform";
    tsx

    This object has a current property that contains methods for interacting with the current process. It's beyond the scope of this topic to explain processes, but the key point is that each iframe has its own process.

  2. Call the getInfo method to retrieve the information about the current process:

    const context = appProcess.current.getInfo();
    tsx
  3. Use the surface property to identify the iframe in which the code is running:

    if (context.surface === "object_panel") {
    return (
    <div className={styles.scrollContainer}>
    <Rows spacing="2u">
    <Button
    variant="primary"
    disabled={!overlay.canOpen}
    onClick={handleOpen}
    >
    Edit image
    </Button>
    </Rows>
    </div>
    );
    }
    if (context.surface === "selected_image_overlay") {
    return <div>This is the selected image overlay.</div>;
    }
    tsx

For the sake of readability, extract the code for each surface into separate components:

import { Button, Rows } from "@canva/app-ui-kit";
import { appProcess } from "@canva/preview/platform";
import * as React from "react";
import styles from "styles/components.css";
import { useOverlay } from "utils/use_overlay_hook";
export function App() {
const context = appProcess.current.getInfo();
if (context.surface === "object_panel") {
return <ObjectPanel />;
}
if (context.surface === "selected_image_overlay") {
return <SelectedImageOverlay />;
}
throw new Error(`Invalid surface: ${context.surface}`);
}
function ObjectPanel() {
const overlay = useOverlay("image_selection");
function handleOpen() {
overlay.open();
}
return (
<div className={styles.scrollContainer}>
<Rows spacing="2u">
<Button
variant="primary"
disabled={!overlay.canOpen}
onClick={handleOpen}
>
Edit image
</Button>
</Rows>
</div>
);
}
function SelectedImageOverlay() {
return <div>This is the selected image overlay.</div>;
}
tsx

Based on these changes, the object panel and overlay iframes will contain different content.

It's often useful for an app to keep track of whether or not an overlay is open. For example, an app usually needs to render different controls in the object panel while an overlay is open.

To do this, use the isOpen property that's returned by the useOverlay hook:

if (overlay.isOpen) {
return <div className={styles.scrollContainer}>The overlay is open.</div>;
}
return (
<div className={styles.scrollContainer}>
<Rows spacing="2u">
<Button
variant="primary"
disabled={!overlay.canOpen}
onClick={handleOpen}
>
Edit image
</Button>
</Rows>
</div>
);
tsx

Based on this change, this is what the app looks like when the overlay is open:

An app can close an overlay at any point in its lifecycle, such as when a button is clicked, but when an overlay is closed, it's also important to consider the users intent:

  • Sometimes, the user wants to persist the changes they've made to the image.
  • In other cases, the user wants to reset the image to its original state.

Your app should account for both use-cases.

Here's what we recommend:

  1. When the overlay is open, render buttons that each call a function:

    function handleSave() {
    // TODO: Close the overlay and save changes
    }
    function handleClose() {
    // TODO: Close the overlay without saving changes
    }
    if (overlay.isOpen) {
    return (
    <div className={styles.scrollContainer}>
    <Rows spacing="1u">
    <Button variant="primary" onClick={handleSave}>
    Save and close
    </Button>
    <Button variant="secondary" onClick={handleClose}>
    Close without saving
    </Button>
    </Rows>
    </div>
    );
    }
    tsx

    (These buttons should be rendered in the object panel.)

  2. In each function, call the close method that's returned by the useOverlay hook:

    function handleSave() {
    overlay.close({ reason: "completed" });
    }
    function handleClose() {
    overlay.close({ reason: "aborted" });
    }
    tsx

    The reason property indicates why the overlay is being closed.

    For the time being, there is no difference between either option. In a later step though, we'll use the reason property to determine whether or not the app should persist changes to the image.

Based on these changes, the overlay can be opened and closed.

Generally speaking, an app needs to download the selected image, pass the image data into the overlay's iframe, and then render that image. It's this copy of the image that the app is then free to manipulate however it wants.

Let's explore how to do this.

  1. Import the useSelection hook from the utils directory in the starter kit:

    import { useSelection } from "utils/use_selection_hook";
    tsx
  2. Import the getTemporaryUrl method from the @canva/asset package:

    import { getTemporaryUrl } from "@canva/asset";
    tsx
  3. Call the useSelection hook from the ObjectPanel component:

    const selection = useSelection("image");
    tsx
  4. At the top of the handleOpen function, get the URL of the selected image:

    async function handleOpen() {
    // Get the URL of the selected image
    const draft = await selection.read();
    const [image] = draft.contents;
    const { url } = await getTemporaryUrl({
    type: "IMAGE",
    ref: image.ref,
    });
    // Open the overlay
    overlay.open();
    }
    tsx

    The Edit image button will only be active if a single image is selected, so we can assume the draft.contents array only contains a single element.

  5. Use the following function to download and get the dimensions of the image:

    async function downloadImage(url: string) {
    const response = await fetch(url, { mode: "cors" });
    const blob = await response.blob();
    const objectUrl = URL.createObjectURL(blob);
    const img = new Image();
    img.crossOrigin = "anonymous";
    await new Promise((resolve, reject) => {
    img.onload = resolve;
    img.onerror = () => reject(new Error("Image could not be loaded"));
    img.src = objectUrl;
    });
    URL.revokeObjectURL(objectUrl);
    return img;
    }
    tsx

    For example:

    async function handleOpen() {
    // Get the URL of the selected image
    const draft = await selection.read();
    const [image] = draft.contents;
    const { url } = await getTemporaryUrl({
    type: "IMAGE",
    ref: image.ref,
    });
    // Download the image
    const img = await downloadImage(url);
    // Get the image dimensions
    const { width, height } = img;
    // Open the overlay
    overlay.open();
    }
    tsx
  1. Create a LaunchParams type that defines the structure of the launch parameters:

    type LaunchParams = {
    img: {
    dataUrl: string;
    width: number;
    height: number;
    };
    };
    tsx

    The structure of the type and the names of the properties are arbitrary.

  2. Update the SelectedImageOverlay component to accept launchParams as a prop:

    function SelectedImageOverlay(props: { launchParams: LaunchParams }) {
    // ...
    }
    tsx
  3. In the App component, set the LaunchParams type as a type parameter for the getInfo method:

    const context = appProcess.current.getInfo<LaunchParams>();
    tsx
  4. Pass context.launchParams into the SelectedImageOverlay component:

    if (context.surface === "selected_image_overlay") {
    if (!context.launchParams) {
    throw new Error("`launchParams` is `undefined`");
    }
    return <SelectedImageOverlay launchParams={context.launchParams} />;
    }
    tsx
  5. In the handleOpen function, use the following toDataUrl function to convert the image into a data URL:

    function toDataUrl(img: HTMLImageElement) {
    const element = document.createElement("canvas");
    const { canvas, context } = getCanvas(element);
    const { width, height } = img;
    canvas.width = width;
    canvas.height = height;
    context.drawImage(img, 0, 0, width, height);
    return canvas.toDataURL();
    }
    function getCanvas(canvas: HTMLCanvasElement | null) {
    if (!canvas) {
    throw new Error("HTMLCanvasElement does not exist");
    }
    const context = canvas.getContext("2d");
    if (!context) {
    throw new Error("CanvasRenderingContext2D does not exist");
    }
    return { canvas, context };
    }
    tsx

    For example:

    // Download the image
    const img = await downloadImage(url);
    // Get the image dimensions
    const { width, height } = img;
    // Convert the image to a data URL
    const dataUrl = toDataUrl(img);
    tsx

    The getCanvas function checks for the existence of the canvas and its context. Without this check, TypeScript will complain. You'll need to perform this check at different times, so it's useful to wrap it in a function.

  6. Pass the image data, width, and height into the open method:

    overlay.open({
    launchParameters: {
    img: {
    dataUrl,
    width,
    height,
    },
    } satisfies LaunchParams,
    });
    tsx

    TypeScript's satisfies keyword ensures that the data conforms to the LaunchParams type.

The handleOpen function should look like this:

async function handleOpen() {
// Get the URL of the selected image
const draft = await selection.read();
const [image] = draft.contents;
const { url } = await getTemporaryUrl({
type: "IMAGE",
ref: image.ref,
});
// Download the image
const img = await downloadImage(url);
// Get the image dimensions
const { width, height } = img;
// Convert the image to a data URL
const dataUrl = toDataUrl(img);
// Open the overlay
overlay.open({
launchParameters: {
img: {
dataUrl,
width,
height,
},
} satisfies LaunchParams,
});
}
tsx

In the SelectedImageOverlay component:

  1. Use the useRef hook to create a ref for an HTMLCanvasElement:

    const canvasRef = React.useRef<HTMLCanvasElement>(null);
    tsx
  2. Return an HTMLCanvasElement that's connected to the ref:

    return <canvas ref={canvasRef} style={{ width: "100%", height: "100%" }} />;
    tsx

    The style properties are important. They ensure that the HTMLCanvasElement expands to fill the dimensions of the iframe, which means the element will grow or shrink based on the browser's zoom level.

  3. In a useEffect hook, draw the image into the HTMLCanvasElement:

    React.useEffect(() => {
    const { canvas, context } = getCanvas(canvasRef.current);
    const { dataUrl, width, height } = props.launchParams.img;
    const img = new Image();
    img.onload = function () {
    canvas.width = width;
    canvas.height = height;
    context.drawImage(img, 0, 0, width, height);
    };
    img.src = dataUrl;
    }, []);
    tsx

    Be sure to set the correct dimensions for the image.

The code for the SelectedImageOverlay component should look like this:

function SelectedImageOverlay(props: { launchParams: LaunchParams }) {
const canvasRef = React.useRef<HTMLCanvasElement>(null);
React.useEffect(() => {
const { canvas, context } = getCanvas(canvasRef.current);
const { dataUrl, width, height } = props.launchParams.img;
const img = new Image();
img.onload = function () {
canvas.width = width;
canvas.height = height;
context.drawImage(img, 0, 0, width, height);
};
img.src = dataUrl;
}, []);
return <canvas ref={canvasRef} style={{ width: "100%", height: "100%" }} />;
}
tsx

Based on these changes, the selected image will be rendered within the overlay when it opens.

Apps have a lot of freedom in terms of how they transform the selected image, but these are some common options:

  • Transform the image as soon as it's rendered in the overlay iframe.
  • Transform the image when a user interacts with a control in the object panel.
  • Transform the image in response to pointer events, such as touches.

The second option allows us to talk about an important feature of the Apps SDK — the ability for iframes to broadcast messages that other iframes can listen for — so that's what we'll explore.

Here's the plan:

When an overlay is open, we'll render an Invert button. When this button is clicked, it will invert the colors of the image in the overlay, thereby providing an instant preview of the effect.

This is a simple concept, but the same pattern can be used for more complex use-cases, such as creating sliders or dropdowns that transform the image in different ways based on their values.

In the ObjectPanel component:

  1. Create an Invert button that calls a handleInvert function:

    function handleInvert() {
    // TODO: Broadcast a message
    }
    return (
    <div className={styles.scrollContainer}>
    <Rows spacing="2u">
    <Rows spacing="1u">
    <Button variant="primary" onClick={handleInvert}>
    Invert
    </Button>
    </Rows>
    <Rows spacing="1u">
    <Button variant="primary" onClick={handleSave}>
    Save and close
    </Button>
    <Button variant="secondary" onClick={handleClose}>
    Close without saving
    </Button>
    </Rows>
    </Rows>
    </div>
    );
    tsx
  2. Call the broadcastMessage method from the invert function:

    function handleInvert() {
    appProcess.broadcastMessage("invert");
    }
    tsx

    This method accepts a single argument. The value of this argument is sent to all of the app's active processes, such as the process for the overlay's iframe. The app's processes can listen for and access that value.

    In this case, the argument is a string, but more complex data types are supported, such as objects.

This is what the app looks like with the Invert button:

In the SelectedImageOverlay component:

  1. Register a callback with the registerOnMessage method:

    React.useEffect(() => {
    appProcess.registerOnMessage((sender, message) => {
    console.log(message);
    });
    }, []);
    tsx

    This callback receives information about the process that sent the message and the message itself. Be sure to register the callback in a useEffect hook.

  2. If message is a string of "invert", invert the colors of the image:

    if (message === "invert") {
    const { canvas, context } = getCanvas(canvasRef.current);
    const { width, height } = canvas;
    context.filter = "invert(100%)";
    context.drawImage(canvas, 0, 0, width, height);
    }
    tsx

Based on these changes, clicking the Invert button inverts the colors of the image.

The last feature our app needs is the ability to save changes to the selected image.

All of these steps should be done in the SelectedImageOverlay component.

An overlay may close for various reasons. For example, a user may click outside of the overlay or tap the Escape key on their keyboard. To handle all of these possibilites, register a callback with the setOnDispose method:

React.useEffect(() => {
return void appProcess.current.setOnDispose(async (context) => {
console.log(context.reason);
});
}, []);
tsx

This callback runs when the current process closes — in this case, the process for the overlay's iframe. It receives a reason property the indicates why the overlay is closing. This property can be either of the following values:

  • "completed"
  • "aborted"

We can use this callback to always take the most appropriate action, whatever the user's intent.

  1. Check if context.reason is "completed":

    if (context.reason === "completed") {
    // code goes here
    }
    tsx
  2. Convert the HTMLCanvasElement into a data URL:

    const { canvas } = getCanvas(canvasRef.current);
    const dataUrl = canvas.toDataURL();
    tsx
  3. Import the upload method from the @canva/asset package:

    import { getTemporaryUrl, upload } from "@canva/asset";
    tsx
  4. Upload the result to the user's media library:

    const asset = await upload({
    type: "IMAGE",
    mimeType: "image/png",
    url: dataUrl,
    thumbnailUrl: dataUrl,
    });
    tsx
  1. Call the useSelection hook:

    const selection = useSelection("image");
    tsx
  2. Set the current selection as a dependency of the useEffect hook:

    React.useEffect(() => {
    // ...
    }, [selection]);
    tsx

    Be sure to set selection as a dependency for the hook.

  3. Get the original image:

    const draft = await selection.read();
    tsx
  4. Replace the ref property of the original image with the ref property of the new image:

    draft.contents[0].ref = asset.ref;
    tsx
  5. Save the change to the original image:

    await draft.save();
    tsx

This is the complete code for the useEffect hook:

React.useEffect(() => {
return void appProcess.current.setOnDispose(async (context) => {
if (context.reason === "completed") {
const { canvas } = getCanvas(canvasRef.current);
const dataUrl = canvas.toDataURL();
const asset = await upload({
type: "IMAGE",
mimeType: "image/png",
url: dataUrl,
thumbnailUrl: dataUrl,
});
const draft = await selection.read();
draft.contents[0].ref = asset.ref;
await draft.save();
}
});
}, [selection]);
tsx

Based on this change, the app can persist changes to the image.

If a user opens an overlay and clicks the Invert button before the image in the overlay has rendered, the effect won't be applied. This happens because, at the moment, the app is only aware of whether the overlay is open or not. It's not aware of whether or not the image within the overlay has finished rendering.

This is something we should fix to ensure a consistent user experience.

In the SelectedImageOverlay component:

  1. Broadcast a message after the image has rendered:

    React.useEffect(() => {
    const { canvas, context } = getCanvas(canvasRef.current);
    const { dataUrl, width, height } = props.launchParams.img;
    const img = new Image();
    img.onload = function () {
    canvas.width = width;
    canvas.height = height;
    context.drawImage(img, 0, 0, width, height);
    + appProcess.broadcastMessage({ isImageReady: true });
    };
    img.src = dataUrl;
    }, []);
    diff
  2. Broadcast a message after overlay has closed:

    React.useEffect(() => {
    return void appProcess.current.setOnDispose(async (context) => {
    if (context.reason === "completed") {
    const { canvas } = getCanvas(canvasRef.current);
    const dataUrl = canvas.toDataURL();
    const asset = await upload({
    type: "IMAGE",
    mimeType: "image/png",
    url: dataUrl,
    thumbnailUrl: dataUrl,
    });
    const draft = await selection.read();
    draft.contents[0].ref = asset.ref;
    await draft.save();
    }
    + appProcess.broadcastMessage({ isImageReady: false });
    });
    }, [selection]);
    diff

In the ObjectPanel component:

  1. Create a state variable to track if the image is ready:

    const [isImageReady, setIsImageReady] = React.useState(false);
    tsx
  2. Register a callback with the registerOnMessage method that updates the state variable:

    React.useEffect(() => {
    appProcess.registerOnMessage((sender, message) => {
    const isImageReady = Boolean(message.isImageReady);
    setIsImageReady(isImageReady);
    });
    }, []);
    tsx
  3. Disable the buttons if the image is not ready:

    <Rows spacing="2u">
    <Rows spacing="1u">
    <Button
    variant="primary"
    disabled={!isImageReady}
    onClick={handleInvert}
    >
    Invert
    </Button>
    </Rows>
    <Rows spacing="1u">
    <Button variant="primary" disabled={!isImageReady} onClick={handleSave}>
    Save and close
    </Button>
    <Button
    variant="secondary"
    disabled={!isImageReady}
    onClick={handleClose}
    >
    Close without saving
    </Button>
    </Rows>
    </Rows>
    tsx

Based on these changes, the app waits until the image is ready before the controls become active.

  • An overlay's iframe must adhere to the same Content Security Policy as the rest of the app.
  • Overlays are only compatible with image content, such as image elements or page backgrounds.
  • Overlays are not compatible with app elements, even if that element contains image content.
import { Button, Rows } from "@canva/app-ui-kit";
import { getTemporaryUrl, upload } from "@canva/asset";
import { appProcess } from "@canva/preview/platform";
import * as React from "react";
import styles from "styles/components.css";
import { useOverlay } from "utils/use_overlay_hook";
import { useSelection } from "utils/use_selection_hook";
type LaunchParams = {
img: {
dataUrl: string;
width: number;
height: number;
};
};
export function App() {
const context = appProcess.current.getInfo<LaunchParams>();
if (context.surface === "object_panel") {
return <ObjectPanel />;
}
if (context.surface === "selected_image_overlay") {
if (!context.launchParams) {
throw new Error("`launchParams` is `undefined`");
}
return <SelectedImageOverlay launchParams={context.launchParams} />;
}
throw new Error(`Invalid surface: ${context.surface}`);
}
function ObjectPanel() {
const overlay = useOverlay("image_selection");
const selection = useSelection("image");
const [isImageReady, setIsImageReady] = React.useState(false);
React.useEffect(() => {
appProcess.registerOnMessage((sender, message) => {
// Listen for when the image has been rendered
const isImageReady = Boolean(message.isImageReady);
setIsImageReady(isImageReady);
});
}, []);
async function handleOpen() {
// Get the URL of the selected image
const draft = await selection.read();
const [image] = draft.contents;
const { url } = await getTemporaryUrl({
type: "IMAGE",
ref: image.ref,
});
// Download the image
const img = await downloadImage(url);
// Get the image dimensions
const { width, height } = img;
// Convert the image to a data URL
const dataUrl = toDataUrl(img);
// Open the overlay
overlay.open({
launchParameters: {
img: {
dataUrl,
width,
height,
},
} satisfies LaunchParams,
});
}
function handleInvert() {
appProcess.broadcastMessage("invert");
}
function handleSave() {
overlay.close({ reason: "completed" });
}
function handleClose() {
overlay.close({ reason: "aborted" });
}
if (overlay.isOpen) {
return (
<div className={styles.scrollContainer}>
<Rows spacing="2u">
<Rows spacing="1u">
<Button
variant="primary"
disabled={!isImageReady}
onClick={handleInvert}
>
Invert
</Button>
</Rows>
<Rows spacing="1u">
<Button
variant="primary"
disabled={!isImageReady}
onClick={handleSave}
>
Save and close
</Button>
<Button
variant="secondary"
disabled={!isImageReady}
onClick={handleClose}
>
Close without saving
</Button>
</Rows>
</Rows>
</div>
);
}
return (
<div className={styles.scrollContainer}>
<Rows spacing="1u">
<Button
variant="primary"
disabled={!overlay.canOpen}
onClick={handleOpen}
>
Edit image
</Button>
</Rows>
</div>
);
}
function SelectedImageOverlay(props: { launchParams: LaunchParams }) {
const selection = useSelection("image");
const canvasRef = React.useRef<HTMLCanvasElement>(null);
// When the overlay opens, render the image
React.useEffect(() => {
const { canvas, context } = getCanvas(canvasRef.current);
const { dataUrl, width, height } = props.launchParams.img;
const img = new Image();
img.onload = function () {
canvas.width = width;
canvas.height = height;
context.drawImage(img, 0, 0, width, height);
// Set the `isImageReady` state
appProcess.broadcastMessage({ isImageReady: true });
};
img.src = dataUrl;
}, []);
React.useEffect(() => {
appProcess.registerOnMessage((sender, message) => {
// Invert the colors of the image
if (message === "invert") {
const { canvas, context } = getCanvas(canvasRef.current);
const { width, height } = canvas;
context.filter = "invert(100%)";
context.drawImage(canvas, 0, 0, width, height);
}
});
}, []);
React.useEffect(() => {
return void appProcess.current.setOnDispose(async (context) => {
// Save changes to the user's image
if (context.reason === "completed") {
// Get the data URL of the image
const { canvas } = getCanvas(canvasRef.current);
const dataUrl = canvas.toDataURL();
// Upload the new image
const asset = await upload({
type: "IMAGE",
mimeType: "image/png",
url: dataUrl,
thumbnailUrl: dataUrl,
});
// Replace the original image with the new image
const draft = await selection.read();
draft.contents[0].ref = asset.ref;
await draft.save();
}
// Reset the `isImageReady` state
appProcess.broadcastMessage({ isImageReady: false });
});
}, [selection]);
return <canvas ref={canvasRef} style={{ width: "100%", height: "100%" }} />;
}
async function downloadImage(url: string) {
const response = await fetch(url, { mode: "cors" });
const blob = await response.blob();
const objectUrl = URL.createObjectURL(blob);
const img = new Image();
img.crossOrigin = "anonymous";
await new Promise((resolve, reject) => {
img.onload = resolve;
img.onerror = () => reject(new Error("Image could not be loaded"));
img.src = objectUrl;
});
URL.revokeObjectURL(objectUrl);
return img;
}
function toDataUrl(img: HTMLImageElement) {
const element = document.createElement("canvas");
const { canvas, context } = getCanvas(element);
const { width, height } = img;
canvas.width = width;
canvas.height = height;
context.drawImage(img, 0, 0, width, height);
return canvas.toDataURL();
}
function getCanvas(canvas: HTMLCanvasElement | null) {
if (!canvas) {
throw new Error("HTMLCanvasElement does not exist");
}
const context = canvas.getContext("2d");
if (!context) {
throw new Error("CanvasRenderingContext2D does not exist");
}
return { canvas, context };
}
tsx