4

I'm trying to write a browser extension that does some image processing but I need access to the image data. My approach was to create a hidden canvas element, draw images and video to it via drawImage, and then read the pixel data with getImageData. This works just fine but on many pages half the content is denied by CORS errors.

I'm still confused as to why CORS exists (something along the lined of not stealing data, but then if the data is on the client's computer isn't it "stolen" already? :S). All it seems to lead to is retarded hacks like JS script injection. So 1. it doesn't work because it's too complicated for every browser to police correctly and 2. devs are punished and have to write browser-specific workarounds. So I'm thinking I must have the wrong idea because this seems pretty stupid.

Taking a step back, I think the idea of an extension that can do some image processing is perfectly normal and not malicious so please do not reply with "no, you shouldn't be doing this for security reasons".

I suspect that the browser is treating the extension as something foreign that could be trying to do malicious things. How can I reassure the browser that the client wants these features and have it grant me access to the image and video content? I already have full access to the DOM, how is a little bit extra going to make any difference??

Is there another way to get image/video data from an extension?

jozxyqk
  • 16,424
  • 12
  • 91
  • 180
  • In a content script, or from the background page? – Rob W Mar 04 '14 at 12:28
  • @RobW I just want to know any straight forward method. I wasn't aware this could be done from a background page. How would you access image/video data within each page? – jozxyqk Mar 04 '14 at 13:00
  • @jozxyqk have you found a solution for getting data from a video the user is watching? I have ran into the same problems as you, and I can't even think of a way of having the video load in the background page, since eg youtube videos use the `src="blob:..."` attribute. – krookedking Aug 27 '16 at 15:22
  • @krookedking Sometimes setting [`crossOrigin = "Anonymous"`](http://stackoverflow.com/a/23123261/1888983) helps but if not I haven't seen another way. – jozxyqk Aug 28 '16 at 03:10
  • 1
    @jozxyqk Ok, thanks for the pointer! I have actually found a different way of achieving what I wanted, using chrome's `desktopCapture` functionality... quite overkill but does the job. – krookedking Aug 28 '16 at 08:07

2 Answers2

4

After adding the right permissions to the manifest file, you can deal with cross-origin data as without being hindered by the same origin policy. In a background page or any other page within the extension's process, you can get a working demo of your described logic as follows:

// background.js
var canvas = document.createElement('canvas');
var context = canvas.getContext('2d');
var image = document.createElement('img');
image.onload = function() {
    context.drawImage(image, 0, 0);
    // Demo: Show image
    window.open(canvas.toDataURL('image/png'));
};
image.src = 'https://stackoverflow.com/favicon.ico';

Here is a minimal manifest file for this specific demo:

{
    "name": "Get image data",
    "version": "1",
    "manifest_version": 2,
    "background": {
        "scripts": ["background.js"]
    },
    "permissions": [
        "https://stackoverflow.com/favicon.ico",
        "http://cdn.sstatic.net/stackoverflow/img/favicon.ico"
    ]
}

The second permission is necessary because https://stackoverflow.com/favicon.ico redirects to http://cdn.sstatic.net/stackoverflow/img/favicon.ico.

Note that the code does not work in content scripts, because the DOM is shared and Chrome cannot offer unrestricted origin access to content scripts only.

Depending on what you really want, you could also just try to get the image data in raw form using XMLHttpRequest, and process it in any way you desire. This method also works in content scripts, and their advantage is that it is more efficient, and also preserves the bytes of the image (with the canvas method, the image is parsed and processed by the browser before it is converted to a data URL).

var x = new XMLHttpRequest();
x.responseType = 'blob';
x.open('get', 'http://stackoverflow.com');
x.onload = function() {
    var fileReader = new FileReader();
    fileReader.onloadend = function() {
        // fileReader.result is a data-URL (string)
        window.open(fileReader.result);
    };
    // x.response is a Blob object
    fileReader.readAsDataURL(x.response);
};
x.send();
Community
  • 1
  • 1
Rob W
  • 341,306
  • 83
  • 791
  • 678
  • 1
    Thanks for such a detailed answer! The first method doesn't seem possible for me since I'd need to add all images/videos the user will ever browse to the permissions. Does the second method not hit CORS problems? If so why deny access to the DOM image if I can just get it myself? This would be great for images but video is still a problem as I'd have to sync my copy with the one playing in the page. – jozxyqk Mar 05 '14 at 05:14
  • 1
    Both methods require host permissions. If you want access to all http(s) sites, just use `*://*/*`as permission. CORS is not a problem, but a solution to bypass the restrictions of the [same origin policy](http://en.wikipedia.org/wiki/Same_origin_policy). – Rob W Mar 05 '14 at 08:01
  • Thanks @RobW for pointing out the code must be placed in background.js. It works for me – duckegg Apr 21 '15 at 13:04
  • In Manifest V3 declare your URLs in a new field `"host_permissions": []` (instead of "permissions") – Prid Jun 28 '22 at 19:49
0

A workaround to the problem is to use Chrome's desktopCapture feature. Note this means the user has to interact with a popup asking for the permission every time you want to record (which could be left on for a long time, but I guess it's not very resource efficient).

I'm putting the code here if it helps.

background.js

var pending_request_id = null;

var video = document.createElement('video');
var canvas = document.createElement('canvas');
var ctx = canvas.getContext("2d");
var theStream = null;

var CAPTURING = false;

/*
 * Setting up recording when the user clicks the extension icon
 */
chrome.browserAction.onClicked.addListener(function() {
  if (CAPTURING) {
    console.log('stop capturing');
    theStream.getVideoTracks()[0].stop();
    chrome.browserAction.setIcon({path: "icons/icon128.png"});
    CAPTURING = false;
  } else {
    pending_request_id = chrome.desktopCapture.chooseDesktopMedia(
      ["screen","tab"],
      onAccessApproved
    );
  }
});

function onAccessApproved(id) {
  console.log('onAccessApproved: ' +id)
  if (!id) {
    console.log("Access rejected.");
    return;
  }
  navigator.webkitGetUserMedia({
    audio:false,
    video: {
      mandatory: {
        chromeMediaSource: "desktop",
        chromeMediaSourceId: id,
        maxWidth: 4000,
        maxHeight: 4000
      }
    }
  }, gotStream, getUserMediaError);
}

function getUserMediaError(error) {
  console.log("getUserMedia() failed:", error);
}

function gotStream(stream) {
  console.log("gotStream", stream);
  theStream = stream;
  video.src = URL.createObjectURL(theStream);
  video.play();
}

video.addEventListener('loadedmetadata',function(){
  console.log('loadedmetadata')
  chrome.browserAction.setIcon({path: "icons/icon128_recording.png"});
  CAPTURING = true;
}, false);


/*
 * You can trigger this when you want; I am doing it on every mouse move over the video (triggered by the content_script.js)
 */
chrome.runtime.onMessage.addListener(
  function(request, sender, sendResponse) {
    if (!CAPTURING) {
      console.log('User needs to activate the recording of the screen')
      return false; // no response
    }

    ctx.drawImage(video, 0, 0);
    var url = canvas.toDataURL('image/png');
    window.open(url);
  }
);
krookedking
  • 2,203
  • 20
  • 21