tl;dr
This issue made me crazy and solved it by loading image with crossOrigin="anonymous" before rendering canvas.
Detailed and too-specific solution
For those who uses React + canvg + Amazon S3 and want to export svg as png via canvas, this could be useful.
First, create a React hook to detect preloading cross-origin images:
// useLoadCrossOriginImage.tsx
import { useReducer, useMemo } from 'react'
export function useLoadCrossOriginImage(imageUrls: string[]) {
const [count, increase] = useReducer((count) => count + 1, 0)
const render = () =>
imageUrls.map((url) => (
<img
src={url}
crossOrigin="anonymous"
onLoad={increase}
className="hidden"
/>
))
const loaded = useMemo(() => count === imageUrls.length, [count, imageUrls])
return {
render,
loaded,
}
}
Then, render svg lazily after loading images:
// ImagePreview.tsx
import { useLoadCrossOriginImage } from './useLoadCrossOriginImage'
// This is usually state from parent component
const imageUrls = [
'https://s3-ap-northeast-1.amazonaws.com/bucket/xxxxxxx.png',
'https://s3-ap-northeast-1.amazonaws.com/bucket/yyyyyyy.png',
]
export const ImagePreview = () => {
const { loaded, render } = useLoadCrossOriginImage(imageUrls)
return (
<div className="border border-slate-300" onClick={onClick}>
{render()}
{loaded && (
<svg xmlns="http://www.w3.org/2000/svg">
{imageUrls.map((imageUrl) => (
<image key={el.id} href={imageUrl} />
))}
</svg>
)}
<canvas className="hidden" />
</div>
)
}
Finally, you can convert the canvas element into png:
const canvas = document.querySelector('canvas')!
const ctx = canvas.getContext('2d')!
const svg = document.querySelector('svg')!
const v = Canvg.fromString(ctx, svg.outerHTML, { anonymousCrossOrigin: true })
Finally, the S3 cors policy should be like this:
{
"CORSRules": [
{
"ID": "s3-cors-policy",
"AllowedHeaders": ["*"],
"AllowedMethods": ["GET", "HEAD"],
"AllowedOrigins": ["*"],
"ExposeHeaders": []
}
]
}
Please leave "MaxAgeSeconds"
empty.
localhost
– Uncinus