@@ -557,6 +557,13 @@ function canvasToBlob(canvas: HTMLCanvasElement): Promise<Blob> {
557557 try {
558558 canvas . toBlob ( ( blob ) => {
559559 if ( ! blob ) {
560+ try {
561+ canvas . toDataURL ( "image/png" ) ;
562+ } catch ( error ) {
563+ reject ( error ) ;
564+ return ;
565+ }
566+
560567 reject ( new Error ( "Failed to create image blob" ) ) ;
561568 return ;
562569 }
@@ -569,6 +576,101 @@ function canvasToBlob(canvas: HTMLCanvasElement): Promise<Blob> {
569576 } ) ;
570577}
571578
579+ /**
580+ * Fetch an image via the background service worker (privileged context)
581+ * which bypasses CORS restrictions that apply to content scripts in MV3.
582+ */
583+ function fetchImageViaBackground ( imageUrl : string ) : Promise < string | null > {
584+ return new Promise ( ( resolve ) => {
585+ try {
586+ chrome . runtime . sendMessage (
587+ { type : "fetch_image" , payload : { url : imageUrl } } ,
588+ ( response ) => {
589+ if ( chrome . runtime . lastError || ! response ?. success ) {
590+ resolve ( null ) ;
591+ return ;
592+ }
593+ resolve ( response . dataUrl ) ;
594+ }
595+ ) ;
596+ } catch {
597+ resolve ( null ) ;
598+ }
599+ } ) ;
600+ }
601+
602+ /**
603+ * Convert export-unsafe images to data URLs using the background service
604+ * worker's privileged fetch. This covers both explicit cross-origin images
605+ * and same-origin URLs that redirect to a different origin at load time,
606+ * such as GitHub's /raw/ image routes.
607+ */
608+ async function convertCrossOriginImages (
609+ originalRoot : HTMLElement ,
610+ clonedRoot : HTMLElement ,
611+ sourceOrigin : string ,
612+ baseUri : string
613+ ) : Promise < void > {
614+ const originalImages = collectElements < HTMLImageElement > ( originalRoot , "img" ) ;
615+ const clonedImages = collectElements < HTMLImageElement > ( clonedRoot , "img" ) ;
616+ const pairCount = Math . min ( originalImages . length , clonedImages . length ) ;
617+
618+ const conversions = Array . from ( { length : pairCount } , async ( _ , index ) => {
619+ const originalImage = originalImages [ index ] ;
620+ const clonedImage = clonedImages [ index ] ;
621+ const src =
622+ clonedImage . getAttribute ( "src" ) ||
623+ originalImage . currentSrc ||
624+ originalImage . getAttribute ( "src" ) ;
625+ if ( ! src ) return ;
626+
627+ const url = parseExportResourceUrl ( src , baseUri ) ;
628+ if ( ! url ) return ;
629+
630+ // Skip non-HTTP URLs – data:, blob:, extension: are already safe.
631+ if ( url . protocol !== "http:" && url . protocol !== "https:" ) return ;
632+
633+ const imageLoaded = Boolean (
634+ originalImage . complete &&
635+ originalImage . naturalWidth > 0 &&
636+ originalImage . naturalHeight > 0
637+ ) ;
638+
639+ if ( imageLoaded && isImageExportSafe ( originalImage ) ) {
640+ return ;
641+ }
642+
643+ // Avoid fetching ordinary same-origin images that simply haven't loaded
644+ // yet. If the already-loaded image is still unsafe, fetch it regardless
645+ // of origin to handle redirecting asset URLs.
646+ if ( ! imageLoaded && url . origin === sourceOrigin ) {
647+ return ;
648+ }
649+
650+ try {
651+ const dataUrl = await fetchImageViaBackground ( url . href ) ;
652+ if ( ! dataUrl ) return ;
653+
654+ clonedImage . src = dataUrl ;
655+
656+ // Clear srcset / <picture> <source>s so the browser uses the data URL.
657+ if ( clonedImage . srcset ) {
658+ clonedImage . removeAttribute ( "srcset" ) ;
659+ }
660+ if ( clonedImage . parentElement instanceof HTMLPictureElement ) {
661+ clonedImage . parentElement
662+ . querySelectorAll ( "source" )
663+ . forEach ( ( sourceNode ) => sourceNode . remove ( ) ) ;
664+ }
665+ } catch {
666+ // Fetch failed – leave the original src; the existing sanitize
667+ // fallback will replace it with a placeholder if needed.
668+ }
669+ } ) ;
670+
671+ await Promise . all ( conversions ) ;
672+ }
673+
572674function waitForNextPaint ( targetDocument : Document ) : Promise < void > {
573675 const view = targetDocument . defaultView ;
574676
@@ -846,6 +948,19 @@ export async function elementToCanvas(
846948 try {
847949 const targetDocument = exportRoot . ownerDocument ;
848950 const sourceDocument = element . ownerDocument ;
951+ const sourceOrigin =
952+ sourceDocument . location ?. origin || window . location . origin ;
953+ const sourceBaseUri = sourceDocument . baseURI ;
954+
955+ // Convert export-unsafe images to data URLs before html2canvas runs so
956+ // they can be rendered without tainting the canvas.
957+ await convertCrossOriginImages (
958+ element ,
959+ exportRoot ,
960+ sourceOrigin ,
961+ sourceBaseUri
962+ ) ;
963+
849964 await waitForStylesheets ( targetDocument ) ;
850965 await copyFontFaces ( sourceDocument , targetDocument ) ;
851966 await waitForFonts ( targetDocument ) ;
0 commit comments