Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Failed to execute 'toDataURL' on 'HTMLCanvasElement': Tainted canvases may not be exported. #1614

Open
jamesmarva opened this issue Aug 2, 2018 · 17 comments

Comments

@jamesmarva
Copy link

var canvasPromise  = html2canvas(document.body, {
                allowTaint: true,
                useCORS: true
            });
canvasPromise.then(function(canvas) {
    document.body.appendChild(canvas);
    console.log(canvas);
    canvas.toDataURL('image/png');
});

Bug reports:

Uncaught (in promise) DOMException: Failed to execute 'toDataURL' on 'HTMLCanvasElement': Tainted canvases may not be exported.

  • html2canvas version tested with:
  • Chrome 67.0.3396.99
  • Windows 10
@samiahakmi
Copy link

I have also the same issues.
Did you find a solution or workaround?

@AkashaP
Copy link

AkashaP commented Aug 24, 2018

i'm doing the same thing in Firefox 61.0.2 and i'm getting a SecurityError despite setting allowTaint to true

@dorklord23
Copy link

No solution as far as I know, but I have a workaround: change every image to base64. That way, you could render it in canvas even though it's originally from different domain.

@fairyly
Copy link

fairyly commented Jan 10, 2019

Have you saw this: CORS_enabled_image

If the source of the foreign content is an HTML element, attempting to retrieve the contents of the canvas isn't allowed.

As soon as you draw into a canvas any data that was loaded from another origin without CORS approval, the canvas becomes tainted.

I use the configuration options like this:

html2canvas(document.body, {
    allowTaint: true,
    foreignObjectRendering: true
});

@sandinosaso
Copy link

sandinosaso commented Feb 28, 2019

Hi, we faced the same problem. We followed @dorklord23 suggestion because we already had a proxy url that did the conversion.

If someone found it helpful the solution was:

      html2canvas(document.body, {
        proxy: this._proxyURL,
        allowTaint: true,
        onclone: (cloned) => convertAllImagesToBase64(this._proxyURL, cloned),
      }).then((canvas) => {
        this._postmessageChannel.send(`get.screenshot:${canvas.toDataURL('image/png')}`);
      });

Where the helper function convertAllImagesToBase64 is:

const convertAllImagesToBase64 = (proxyURL, cloned) => {
  const pendingImagesPromises = [];
  const pendingPromisesData = [];

  const images = cloned.getElementsByTagName('img');

  for (let i = 0; i < images.length; i += 1) {
    // First we create an empty promise for each image
    const promise = new Promise((resolve, reject) => {
      pendingPromisesData.push({
        index: i, resolve, reject,
      });
    });
    // We save the promise for later resolve them
    pendingImagesPromises.push(promise);
  }

  for (let i = 0; i < images.length; i += 1) {
    // We fetch the current image
    fetch(`${proxyURL}?url=${images[i].src}`)
      .then((response) => response.json())
      .then((data) => {
        const pending = pendingPromisesData.find((p) => p.index === i);
        images[i].src = data;
        pending.resolve(data);
      })
      .catch((e) => {
        const pending = pendingPromisesData.find((p) => p.index === i);
        pending.reject(e);
      });
  }

  // This will resolve only when all the promises resolve
  return Promise.all(pendingImagesPromises);
};

export { convertAllImagesToBase64 };

By the way this are the tests for that helper function (we are using jest for wrting test and mockFetch packages):

import { convertAllImagesToBase64 } from '../images';

fetch.resetMocks();

// Mock fetch to respond different for each image so we can assert that the image return the correct response
// Also make one of the response be delayed (2 seconds) to simulate the response is not in the same order we do the call (network latency, image size, etc)
fetch.mockImplementation((url) => {
  if (url.includes('imagesrc1')) {
    return Promise.resolve(new Response(JSON.stringify('data:image/png;base64,1')));
  } else if (url.includes('imagesrc2')) {
    return new Promise((resolve) => setTimeout(resolve(new Response(JSON.stringify('data:image/png;base64,2'))), 2000));
  } else if (url.includes('imagesrc3')) {
    return Promise.resolve(new Response(JSON.stringify('data:image/png;base64,3')));
  }
  return Promise.resolve(new Response(JSON.stringify('')));
});

const mocksImages = [
  { id: 1, src: 'imagesrc1' },
  { id: 2, src: 'imagesrc2' },
  { id: 3, src: 'imagesrc3' },
];

const mockClone = {
  getElementsByTagName: jest.fn(() => mocksImages),
};

describe('utils/images', () => {  
  it('convertAllImagesToBase64. Expect to call 3 times to the correct enpoint using the image source', async () => {
    const allPromises = convertAllImagesToBase64('http://localhost/fake_proxy', mockClone);

    // Expect the clone elements gets all the image tags
    expect(mockClone.getElementsByTagName).toBeCalledWith('img');

    allPromises.then(() => {
      // Expect to have done the 3 fetch calls and with the correct params
      expect(fetch).toBeCalledTimes(3);
      expect(fetch).toHaveBeenNthCalledWith(1, 'http://localhost/fake_proxy?url=imagesrc1');
      expect(fetch).toHaveBeenNthCalledWith(2, 'http://localhost/fake_proxy?url=imagesrc2');
      expect(fetch).toHaveBeenNthCalledWith(3, 'http://localhost/fake_proxy?url=imagesrc3');

      // Expect that our images where updated properly
      expect(mocksImages).toContainEqual({
        id: 1, src: 'data:image/png;base64,1',
      });
      expect(mocksImages).toContainEqual({
        id: 2, src: 'data:image/png;base64,2',
      });
      expect(mocksImages).toContainEqual({
        id: 3, src: 'data:image/png;base64,3',
      });
    });
  });
});

Ruby backend enpdoint:

require 'base64'
require 'net/http'

module Api
  module V1
    class ImageProxyController < ApiController
      def index
        url   = URI.parse(params[:url])
        image = Net::HTTP.get_response(url)

        render json: data_url(image).to_json, callback: params[:callback]
      end

      private

        def data_url(image)
          "data:#{image.content_type};base64,#{Base64.encode64(image.body)}"
        end

    end
  end
end

I hope someone find this helpful. I hope it helps someone not to invest as much time as we did to fix this properly.

If you can see any improvment please suggest.
Regards.

@zhaosaisai
Copy link

If you do like below, what will happen?

const TempImage = window.Image

 const Image = function() {
        const img = new TempImage()
        img.crossOrigin = 'anonymous'
        return img
 }

@NishantTeria
Copy link

Found a solution and it is working

  1. While calling html2canvas,pass useCORS true
    html2canvas(selectorElement,{useCORS:true}).then(canvas => {
    //do something
    });

  2. Correct html2canvas.js file. Theres typo mistake
    Change "anonymous" to "Anonymous" in this if block
    if (isInlineBase64Image(src) || useCORS) {
    img.crossOrigin = 'Anonymous';
    }

@RodrigoMotaSoares
Copy link

RodrigoMotaSoares commented Dec 23, 2019

var canvasPromise  = html2canvas(document.body, {
                allowTaint: true,
                useCORS: true
            });
canvasPromise.then(function(canvas) {
    document.body.appendChild(canvas);
    console.log(canvas);
    canvas.toDataURL('image/png');
});

Bug reports:

Uncaught (in promise) DOMException: Failed to execute 'toDataURL' on 'HTMLCanvasElement': Tainted canvases may not be exported.

  • html2canvas version tested with:
  • Chrome 67.0.3396.99
  • Windows 10

You will need to use just the property 'useCORS: true', if you use the property 'allowTaint: true' you give permssion to turn your canvas into a tainted canvas

USE THIS:

var canvasPromise  = html2canvas(document.body, {
                useCORS: true
            });
canvasPromise.then(function(canvas) {
    document.body.appendChild(canvas);
    console.log(canvas);
    canvas.toDataURL('image/png');
});

INSTEAD OF THIS:

var canvasPromise  = html2canvas(document.body, {
                allowTaint: true,
                useCORS: true
            });
canvasPromise.then(function(canvas) {
    document.body.appendChild(canvas);
    console.log(canvas);
    canvas.toDataURL('image/png');
});

@richardblondet
Copy link

Hello, nice work for html2canvas.

Sadly I'm facing the same issue, has anyone solved this?
Already tried @motarock and all before that, plus combinations, etc. The image is white with nothing painted.

downloadQRCode = (fileName) => {
    html2canvas(document.getElementById('generated-qr-code'), {
      useCORS: true,
      // allowTaint: false,
      // logging: true,
    }).then((canvas) => {
      // document.body.appendChild(canvas); // checking
      const data = canvas.toDataURL('image/jpeg');
      const element = document.createElement('a');
      element.setAttribute('href', data);
      element.setAttribute('download', fileName + '.jpeg');
      document.body.appendChild(element);
      element.click();
      // document.body.removeChild(element);
      console.log("%c data", "font-size:2em;", data, fileName);
      console.log("%c canvas", "font-size:2em;", canvas );
      this.setState({
        imageSrc: data // setting the src of a img tag to check the result. Nothing in it either..
      })
    });

Thanks in advance

@stevenshen1020
Copy link

I have also the same issues.
Any one can fix this issue?

@z1haze
Copy link

z1haze commented Apr 17, 2020

:( same issue. we have html with nested svg and it will not render

@hidajoned
Copy link

Already tried @motarock and all before that, plus combinations, etc. The image is white with nothing painted.

I have this issue when I don't use SSL, With SSL works perfect

@bharatsavani
Copy link

Hi, we faced the same problem. We followed @dorklord23 suggestion because we already had a proxy url that did the conversion.

If someone found it helpful the solution was:

      html2canvas(document.body, {
        proxy: this._proxyURL,
        allowTaint: true,
        onclone: (cloned) => convertAllImagesToBase64(this._proxyURL, cloned),
      }).then((canvas) => {
        this._postmessageChannel.send(`get.screenshot:${canvas.toDataURL('image/png')}`);
      });

Where the helper function convertAllImagesToBase64 is:

const convertAllImagesToBase64 = (proxyURL, cloned) => {
  const pendingImagesPromises = [];
  const pendingPromisesData = [];

  const images = cloned.getElementsByTagName('img');

  for (let i = 0; i < images.length; i += 1) {
    // First we create an empty promise for each image
    const promise = new Promise((resolve, reject) => {
      pendingPromisesData.push({
        index: i, resolve, reject,
      });
    });
    // We save the promise for later resolve them
    pendingImagesPromises.push(promise);
  }

  for (let i = 0; i < images.length; i += 1) {
    // We fetch the current image
    fetch(`${proxyURL}?url=${images[i].src}`)
      .then((response) => response.json())
      .then((data) => {
        const pending = pendingPromisesData.find((p) => p.index === i);
        images[i].src = data;
        pending.resolve(data);
      })
      .catch((e) => {
        const pending = pendingPromisesData.find((p) => p.index === i);
        pending.reject(e);
      });
  }

  // This will resolve only when all the promises resolve
  return Promise.all(pendingImagesPromises);
};

export { convertAllImagesToBase64 };

By the way this are the tests for that helper function (we are using jest for wrting test and mockFetch packages):

import { convertAllImagesToBase64 } from '../images';

fetch.resetMocks();

// Mock fetch to respond different for each image so we can assert that the image return the correct response
// Also make one of the response be delayed (2 seconds) to simulate the response is not in the same order we do the call (network latency, image size, etc)
fetch.mockImplementation((url) => {
  if (url.includes('imagesrc1')) {
    return Promise.resolve(new Response(JSON.stringify('data:image/png;base64,1')));
  } else if (url.includes('imagesrc2')) {
    return new Promise((resolve) => setTimeout(resolve(new Response(JSON.stringify('data:image/png;base64,2'))), 2000));
  } else if (url.includes('imagesrc3')) {
    return Promise.resolve(new Response(JSON.stringify('data:image/png;base64,3')));
  }
  return Promise.resolve(new Response(JSON.stringify('')));
});

const mocksImages = [
  { id: 1, src: 'imagesrc1' },
  { id: 2, src: 'imagesrc2' },
  { id: 3, src: 'imagesrc3' },
];

const mockClone = {
  getElementsByTagName: jest.fn(() => mocksImages),
};

describe('utils/images', () => {  
  it('convertAllImagesToBase64. Expect to call 3 times to the correct enpoint using the image source', async () => {
    const allPromises = convertAllImagesToBase64('http://localhost/fake_proxy', mockClone);

    // Expect the clone elements gets all the image tags
    expect(mockClone.getElementsByTagName).toBeCalledWith('img');

    allPromises.then(() => {
      // Expect to have done the 3 fetch calls and with the correct params
      expect(fetch).toBeCalledTimes(3);
      expect(fetch).toHaveBeenNthCalledWith(1, 'http://localhost/fake_proxy?url=imagesrc1');
      expect(fetch).toHaveBeenNthCalledWith(2, 'http://localhost/fake_proxy?url=imagesrc2');
      expect(fetch).toHaveBeenNthCalledWith(3, 'http://localhost/fake_proxy?url=imagesrc3');

      // Expect that our images where updated properly
      expect(mocksImages).toContainEqual({
        id: 1, src: 'data:image/png;base64,1',
      });
      expect(mocksImages).toContainEqual({
        id: 2, src: 'data:image/png;base64,2',
      });
      expect(mocksImages).toContainEqual({
        id: 3, src: 'data:image/png;base64,3',
      });
    });
  });
});

Ruby backend enpdoint:

require 'base64'
require 'net/http'

module Api
  module V1
    class ImageProxyController < ApiController
      def index
        url   = URI.parse(params[:url])
        image = Net::HTTP.get_response(url)

        render json: data_url(image).to_json, callback: params[:callback]
      end

      private

        def data_url(image)
          "data:#{image.content_type};base64,#{Base64.encode64(image.body)}"
        end

    end
  end
end

I hope someone find this helpful. I hope it helps someone not to invest as much time as we did to fix this properly.

If you can see any improvment please suggest.
Regards.

I have used your approach and get image from my backend as base64 and it works
thank you so much for this idea

@ghost
Copy link

ghost commented Dec 3, 2021

I had same problem.
It was solved using images from same domain.

@realdavidalad
Copy link

Setting foreignObjectRendering to true worked for me.
html2canvas(document.body, {
allowTaint: true,
foreignObjectRendering: true
});

@yhsang2
Copy link

yhsang2 commented Sep 18, 2023

foreignObjectRendering을 true로 설정하면 저에게 효과적이었습니다. html2canvas(document.body, { allowTaint: true, foreignObjectRendering: true });

var canvasPromise = html2canvas(document.body, { allowTaint: true, useCORS: true, foreignObjectRendering: true }); canvasPromise.then(function(canvas) { document.body.appendChild(canvas); console.log(canvas); canvas.toDataURL('image/png'); });

Oh, this worked for me. Thank you!

@hadihonarvar
Copy link

hadihonarvar commented Oct 26, 2023

this worked for my next.js app.

 html2canvas(canvas,{useCORS:true}).then(cnvs => {
        if (cnvs.toDataURL) {
                const dataURL = cnvs.toDataURL('image/png');
                console.log(dataURL)  
        }
});

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests