Skip to content

Instantly share code, notes, and snippets.

@Veejay
Last active September 10, 2021 17:59
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save Veejay/8497487 to your computer and use it in GitHub Desktop.
Save Veejay/8497487 to your computer and use it in GitHub Desktop.
Working around Javascript Canvas API CORS limitations

The problem

Manipulating images through the Javascript Canvas API is great. It allows the developer to apply powerful transformations to an image before displaying it on a web page. Despites its power, the Canvas API also has a limitation that can be extremely annoying: not respecting CORS "taints" the canvas, which severely ampers its most basic uses (see the MDN article about it)

An example of the problem

I have the URI to a Flickr photo (say the picture of a friend for example) and I want to apply a given transformation to the image before displaying it on my webpage. Two choices then:

  1. Cropping the image myself with some random Adobe tool or Picasa / Aviary / whatever
  2. Using the canvas API to do it on the fly in web page

That second solution is nice because I don't have to do anything once the transformation has been implemented. I just provide it with the image URI and it will just work (provided said photos are properly framed but oh well)

Now because Flickr doesn't have a header like Access-Control-Allow-Origin * associated to its photos, the canvas simply won't do its deeds.

Solution to the problem

The solution here is to have the image source URIs be rewritten to hit a server-side piece of code that will take care of the bounce to and from Flickr for us. It will simply take the original URI as a paramater, retrieve the photo on Flickr and return it to the web page as base64 encoded data with the proper headers set.

Server-side code (written in Go, using the excellent gorilla/mux library)

package main

import (
	"fmt"
	"github.com/gorilla/mux"
	"io/ioutil"
	"net/http"
	"net/url"
)

func main() {
	r := mux.NewRouter()
	r.HandleFunc("/images", GetImageFromFlickr)
	http.Handle("/", r)
	http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("static/"))))
	fmt.Println("Up and listening on port 12345")
	http.ListenAndServe(":12345", nil)
}

func GetImageFromFlickr(rw http.ResponseWriter, request *http.Request) {
	imageUrl := request.FormValue("url")
	url, err := url.Parse(imageUrl)
	if err != nil {
		fmt.Fprintf(rw, "Invalid URL")
	}
	fmt.Printf("Proxying request for %s\n", url.String())
	response, err := http.Get(url.String())
	if err != nil {
		fmt.Fprintf(rw, "Error fetching the image")
	}
	defer response.Body.Close()
	body, err := ioutil.ReadAll(response.Body)
	if err != nil {
		fmt.Fprintf(rw, "Error while reading the body of the response")
	}
	rw.Header().Set("Access-Control-Allow-Origin", "*")
	_, err = rw.Write(body)
	if err != nil {
		panic("Couldn't write the response. Exiting.")
	}
}

Now I'm not a Go expert by any stretch of the imagination, but the language does make it easy to to do that sort of things without having to get too involved. Goroutines would even allow us to have that kind of backend service scale nicely with the use of well-place goroutines.

It's kind of straightforward, if there's a request to the static directory, just serve it, if there's a request to images, take the parameter and fetch the image on Flickr before returning it as base64. The important bit here is that the Access-Control-Allow-Origin header is set.

Client-side code (using vanilla JS and Remy Sharp's code from the article that inspired me to try and write this code)

<html>
  <head>
  </head>
  <body>
    <div id="main-content">
      <img src="http://farm8.staticflickr.com/7381/11996251195_03b7ceccde_n_d.jpg" crossorigin=anonymous style="display: none">
    </div>
    <script>
      function circlify(source, width) {
        var canvas = document.createElement('canvas'),
        ctx = canvas.getContext('2d');

        var short = source.width < source.height ? source.width : source.height,
        x = (source.width - short) / 2,
        y = (source.height - short) / 2;
        canvas.width = canvas.height = width;  
        ctx.drawImage(source, x, y, short, short, 0, 0, width, width);

        // now cut out a circle using global composite op
        ctx.globalCompositeOperation = 'destination-in';

        ctx.beginPath();
        ctx.arc(width/2, width/2, width/2, 0, Math.PI * 2, true);
        ctx.closePath();
        ctx.fill();

        return ctx;
      }

      window.onload = function () {

        var img = document.querySelector('img');
        img.onload = function() {
          var smallImg = new Image();
          smallImg.src = circlify(img, 100).canvas.toDataURL('image/jpg');
          img.parentNode.replaceChild(smallImg, img);
        }
        img.setAttribute('src', 'http://armored.armadillo.dev:12345/images?url=' + img.getAttribute('src'));
      };
    </script>
  </body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment