Skip to main content

html2Canvas: Avoid Tainted Canvas

Intro

info

As far as I know this is a completely original solution to the problem of tainted canvases. I have not seen this solution anywhere else, so I'm a bit chuffed about it if i'm being honest.

When using the HTML2Canvas library to take a screenshot of a webpage, you may run into an issue where the canvas is "tainted" and you can't use it. The canvas is considered tainted if it contains an image that is loaded from a different domain than the webpage itself. This is a security feature to prevent cross-origin data leakage.

It is however a massive pain if I trust the source of the images and I don't want to go through the process of setting up a proxy server just to change a header in my image. It's on my computer already, why can't I use it?

Well you can, you just need to convert the image to a base64 string and then use that string as the source of the image. This way the image is considered to be from the same domain as the webpage and the canvas won't be tainted.

In my instance I have a bunch of Amazon product images where I am pulling the URL's from the Amazon API and then using those URL's as the source of the images to populate a datatable, which I am then taking a screenshot of with html2canvas for PDF export.

All of these examples are in React. If you are using something else, just adopt the concepts to your framework.

Storing and Converting images

My application has a DataTable component which gets fed data as a prop. The point is that all of the images which the table normally receives are just URL's, and it then fetches and renders the images on the fly.

We have to create a sort of middleware here however, where we take all the image URL's being fed into the table, fetch the images, convert them to base64, and then store them locally.

DataTable.js
//📸 fetch images which will cause CORS issues for PDF canvas generation and store
// them locally in IndexedDB
// sets printImagesStored state to true when all images have been fetched and stored
// when printing a PDF the table should not be rendered until this state is true
useEffect(() => {
const fetchProductImageConvertImageToBase64AndStoreLocally = async (
product
) => {
try {
// Check if we already have the image in localforage for this ASIN
const existingImage = await localforage.getItem(product.asin);

if (!existingImage) {
const response = await fetch(product.imageUrl);
if (!response.ok) console.warn("Network response was not ok");
const blob = await response.blob();
const base64data = await new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onloadend = () => resolve(reader.result);
reader.onerror = reject;
reader.readAsDataURL(blob);
});

// Store image in local storage (IndexedDB) using ASIN as unique ID
await localforage.setItem(product.asin, base64data);
}
} catch (error) {
console.error("Error converting image to Base64:", error);
}
};

const fetchAndStoreAllImages = async () => {
if (props.PrintFormat) {
switch (props.Context) {
case "product-performance":
// Create a promise for each product to fetch and store its image
const promises = table.data.map(
fetchProductImageConvertImageToBase64AndStoreLocally
);

// Wait for all promises to resolve
await Promise.all(promises);

// Set state after all images have been fetched and stored
setPrintImagesStored(true);
break;
// in all other instances, we don't need to fetch images
default:
setPrintImagesStored(true);
}
}
};

// Call the function to fetch and store all images
fetchAndStoreAllImages();
}, [props.PrintFormat, props.Context, table]);

Why localForage?

We have used the localforage library to store the images in IndexedDB as opposed to just localStorage. This is because there is a limit to the amount of data you can store in localStorage, and IndexedDB is a more robust solution for storing larger amounts of data.

You can try to simplify this process by using localStorage if you are only storing a few images, but I would recommend using localForage for a more robust solution.

Rendering the images

In my instance I am rendering the image into a cell component in a table. There are a few things happening in this component.

  • First, we check if we are in print format. If we are, we retrieve the image from local storage where it should be stored in base64 format to avoid CORS issues for printing.
  • We use useEffect to handle the image retrieval because localForage is async by nature.
  • While we wait for the image to load, we can render a placeholder.
Cell.jsx
const [imageSrc, setImageSrc] = useState(null);
const [imageLoaded, setImageLoaded] = useState(false);

// because localforage is async by nature we have to use useEffect to handle the image retrieval
useEffect(() => {
// if we are in print format, we need to retrieve the image from local storage
// where it should be stored in base64 format to avoid CORS issues for printing
const determineImageSrc = async () => {
if (!print_format) {
setImageSrc(row.original.imageUrl);
} else {
// get image from local storage where it should be stored in base64 format
const src = await localforage.getItem(value); // value is asin
setImageSrc(src);
}
};

determineImageSrc();
}, [value, row, print_format]);

// while we wait for the image to load, we can render a placeholder
const renderProductImage = () => {
const placeholder = (
<div className="dt-product-placeholder-container mr-point5">
<CubeTransparent Class="icon-small" Color="var(--gray-3)" />
</div>
);

if (imageSrc) {
return (
<>
{/* Main Image */}
<img
onLoad={() => setImageLoaded(true)}
src={imageSrc}
alt={row.original.title}
className="dt-product mr-point5"
/>
{/* Placeholder Image */}
{!imageLoaded && placeholder}
</>
);
}
// there is no product image to fetch and display
else {
return placeholder;
}
};

Conclusion

I am just so pleased with this solution. Not only does it solve the tainted canvas issue, but it also speeds up the rendering of the table on all subsequent page loads because the images are already stored locally. It's a win-win.

Recent Work

Free desktop AI Chat client, designed for developers and businesses. Unlocks advanced model settings only available in the API. Includes quality of life features like custom syntax highlighting.

Learn More
slide-6
slide-5
slide-2
slide-1
slide-3
slide-4
Technologies Used
TypeScript
Electron
React

BidBear

bidbear.io

Bidbear is a report automation tool. It downloads Amazon Seller and Advertising reports, daily, to a private database. It then merges and formats the data into beautiful, on demand, exportable performance reports.

Learn More
slide-1
slide-2
slide-5
slide-3
slide-4

Technologies Used

Front End
JavaScript
Docker
React
Redux
Vite
Next
Docusaurus
Stripe
Sentry
D3
React-Flow
TipTap
Back End
JavaScript
Python
AWS CognitoCognito
AWS API GatewayAPI Gateway
AWS LambdaLambda
AWS AthenaAthena
AWS GlueGlue
AWS Step FunctionsStep Functions
AWS SQSSQS
AWS DynamoDBDynamo DB
AWS S3S3
AWS CloudwatchCloudWatch
AWS CloudFrontCloudFront
AWS Route 53Route 53
AWS EventBridgeEventBridge