lokesh / color-thief

Grab the color palette from an image using just Javascript. Works in the browser and in Node.
https://lokeshdhakar.com/projects/color-thief/
MIT License
12.6k stars 1.31k forks source link

Use ColorThief with XMLHttpRequest and without canvas #86

Open loretoparisi opened 9 years ago

loretoparisi commented 9 years ago

I'm trying to use Color-Thief without making use of the <canvas/> element.

So, given a image url, width and height I have tried this

function getColorsNoCanvas(imageURL, imageHeight, imageWidth, done, error) {

  console.log(imageURL,imageHeight,imageWidth)

  var xhr = new XMLHttpRequest();
  xhr.open('GET', imageURL, true);
  xhr.responseType = 'arraybuffer';
  xhr.onload = function(e) {
    if (this.status == 200) {
      var uInt8Array = new Uint8Array(this.response);
      var i = uInt8Array.length;
      var biStr = new Array(i);
      while (i--)
      { biStr[i] = String.fromCharCode(uInt8Array[i]);
      }
      //var data = biStr.join('');
      //var base64 = window.btoa(data);
      //$("#myImage").attr("src", "data:image/jpeg;base64,"+base64);

      // Store the RGB values in an array format suitable for quantize function
      var threshold = 0.15;
      var pixels=uInt8Array
      var pixelCount=uInt8Array.length;
      var pixelArray = [];
      var bgPixelArray = [];
      for (var i = 0, offset, r, g, b, a; i < pixelCount; i++) {
          offset = i * 4;
          r = pixels[offset + 0];
          g = pixels[offset + 1];
          b = pixels[offset + 2];
          a = pixels[offset + 3];
          // If pixel is mostly opaque and not white
          if (a >= 125) {
              if (!(r > 250 && g > 250 && b > 250)) {
                  pixelArray.push([r, g, b]);

                  if ((i < pixelCount * threshold) || (i % imageHeight < imageWidth * threshold / 2)) {
                      bgPixelArray.push([r, g, b]);
                  }
              }
          }
      }
      var cmap = MMCQ.quantize(pixelArray, 5);
      var palette = cmap.palette();
      var bgCmap = MMCQ.quantize(bgPixelArray, 5);
      var bgPalette = bgCmap.palette();

      done.apply(this,[ [palette, bgPalette[0]] ])

    } // 200
  };
  xhr.send();
}

The result is anyways not the same as the getColors function. The reason I think it that I'm wrong when converting the uInt8Array:

var uInt8Array = new Uint8Array(this.response);
var i = uInt8Array.length;

to the rgba format:

// Store the RGB values in an array format suitable for quantize function
      var threshold = 0.15;
      var pixels=uInt8Array
      var pixelCount=uInt8Array.length;
      var pixelArray = [];
      var bgPixelArray = [];
      for (var i = 0, offset, r, g, b, a; i < pixelCount; i++) {
          offset = i * 4;
          r = pixels[offset + 0];
          g = pixels[offset + 1];
          b = pixels[offset + 2];
          a = pixels[offset + 3];
          // If pixel is mostly opaque and not white
          if (a >= 125) {
              if (!(r > 250 && g > 250 && b > 250)) {
                  pixelArray.push([r, g, b]);

                  if ((i < pixelCount * threshold) || (i % imageHeight < imageWidth * threshold / 2)) {
                      bgPixelArray.push([r, g, b]);
                  }
              }
          }
      }

Any hint?

loretoparisi commented 9 years ago

I have adapted the code to the latest version of Color-Thief:

ColorThief.prototype.getColorNoCanvas = function(sourceImage, quality, done) {
    this.getPaletteNoCanvas(sourceImage, 5, quality, function(palette) {
        done.apply(this, [palette[0]])
    });
};

where

ColorThief.prototype.getPaletteNoCanvas = function(sourceImageURL, colorCount, quality, done) {
  var xhr = new XMLHttpRequest();
  xhr.open('GET', sourceImageURL, true);
  xhr.responseType = 'arraybuffer';
  xhr.onload = function(e) {
    if (this.status == 200) {

      var uInt8Array = new Uint8Array(this.response);
      var i = uInt8Array.length;
      var biStr = new Array(i);
      while (i--)
      { biStr[i] = String.fromCharCode(uInt8Array[i]);
      }

      if (typeof colorCount === 'undefined') {
          colorCount = 10;
      }
      if (typeof quality === 'undefined' || quality < 1) {
          quality = 10;
      }

      var pixels     = uInt8Array;
      var pixelCount = 152 * 152 * 4 // this should be width*height*4

      // Store the RGB values in an array format suitable for quantize function
      var pixelArray = [];
      for (var i = 0, offset, r, g, b, a; i < pixelCount; i = i + quality) {
          offset = i * 4;
          r = pixels[offset + 0];
          g = pixels[offset + 1];
          b = pixels[offset + 2];
          a = pixels[offset + 3];
          // If pixel is mostly opaque and not white
          if (a >= 125) {
              if (!(r > 250 && g > 250 && b > 250)) {
                  pixelArray.push([r, g, b]);
              }
          }
      }

      // Send array to quantize function which clusters values
      // using median cut algorithm
      var cmap    = MMCQ.quantize(pixelArray, colorCount);
      var palette = cmap? cmap.palette() : null;
      done.apply(this,[ palette ])

    } // 200
  };
  xhr.send();
}

and so I'm using it like

var colorThief = new ColorThief();
colorThief.getColorNoCanvas("/colors2/"+$image.attr('src'), 8, function(colors) {
                                 console.log( "getColorNoCanvas", colors )
                                 styleBackground(colors, $image.parent().parent().attr('id'));
                            styleText(colors, colors,$image.parent().parent().attr('id'));
                             })

I have tried the simplest as possibile when getting the r,g,b,a array like:

for (var pxIndex = 0; pxIndex<pixels.length; pxIndex+=4 ) {
          var r = pixels[pxIndex+0];
          var g = pixels[pxIndex+1];
          var b = pixels[pxIndex+2];
          var a = pixels[pxIndex+3];
          if (a >= 125) {
              if (!(r > 250 && g > 250 && b > 250)) {
                  pixelArray.push([r, g, b]);
              }
          }
      }

But the result is wrong.

kosir commented 8 years ago

Hi, I'm highly interested in this :) ... want to use colorThief on remote images (flickr, etc). Please let me know if you succeed to make it work.

loretoparisi commented 8 years ago

@kosir At this time, I think I've solved 70% of this issue. The remaining 30% is the key part:

When you go through the bytearray, every 4 pixels, I'm not sure I'm collecting the right R-G-B pixels here:

for (var pxIndex = 0; pxIndex<pixels.length; pxIndex+=4 ) {
          var r = pixels[pxIndex+0];
          var g = pixels[pxIndex+1];
          var b = pixels[pxIndex+2];
          var a = pixels[pxIndex+3];
          if (a >= 125) {
              if (!(r > 250 && g > 250 && b > 250)) {
                  pixelArray.push([r, g, b]);
              }
          }
      }

so when passed to the MCQQ

// Send array to quantize function which clusters values
      // using median cut algorithm
      var cmap    = MMCQ.quantize(pixelArray, colorCount);
      var palette = cmap? cmap.palette() : null;

I get back the wrong colors palette now. I have also submitted the question to StackOverflow, maybe you can spread the question and/or +1 it in order to make it more visibile:

http://stackoverflow.com/questions/33312362/dominant-color-for-an-rgb-image-via-xmlhttprequest

If we take this sample images schermata 2015-11-09 alle 13 21 30

I have different results from the Canvas method

getColor [215, 232, 236]
(index):70 getColor [195, 178, 147]
(index):70 getColor [241, 149, 53]
(index):70 getColor [233, 207, 128]
(index):70 getColor [191, 188, 145]
(index):70 getColor [89, 89, 63]
(index):70 getColor [46, 53, 69]
(index):70 getColor [197, 201, 197]

and without Canvas using the XMLHttpRequest object and the byte array:

(index):65 getColorNoCanvas [188, 167, 165]
(index):65 getColorNoCanvas [186, 164, 163]
(index):65 getColorNoCanvas [186, 164, 166]
(index):65 getColorNoCanvas [186, 161, 160]
(index):65 getColorNoCanvas [186, 164, 164]
(index):65 getColorNoCanvas [120, 97, 161]
(index):65 getColorNoCanvas [131, 163, 160]
(index):65 getColorNoCanvas [184, 163, 162]
loretoparisi commented 8 years ago

I finally came out with a solution, so now the MCQQ and Color-Thief works without any canvas, just using XMLHttpRequest object.

I'm using jpg.js as JPEG image decoder - details described here: https://github.com/notmasteryet/jpgjs/issues/40

A online demo is here: http://www.parisilabs.com/colors2/

This is the final code:

ColorThief.prototype.getColorNoCanvas = function(sourceImage, quality, done) {
    this.getPaletteNoCanvas(sourceImage, 5, quality, function(palette) {
        done.apply(this, [palette[0]])
    });
};

ColorThief.prototype.getPaletteNoCanvas = function(sourceImageURL, colorCount, quality, done) {
  var j = new JpegImage();
    j.onload = function() {

      // Image Data
      var d = new Object();
      d.height=350;
      d.width=350;
      d.data = new Array();

      j.copyToImageData(d);

      var pixels = d.data;
      var pixelArray = [];
      var quality = 10;
      var pixelCount = d.height * d.width;

      for (var i = 0, offset, r, g, b, a; i < pixelCount; i = i + quality) {
          offset = i * 4;
          r = pixels[offset + 0];
          g = pixels[offset + 1];
          b = pixels[offset + 2];
          a = pixels[offset + 3];
          // If pixel is mostly opaque and not white
          if (a >= 125) {
              if (!(r > 250 && g > 250 && b > 250)) {
                  pixelArray.push([r, g, b]);
              }
          }
      }

      var cmap    = MMCQ.quantize(pixelArray, colorCount);
      var palette = cmap? cmap.palette() : null;
      done.apply(this, [palette]);

    };//onload
    j.load(sourceImageURL);
}

and you can call it like

var colorThief = new ColorThief();
var colors = colorThief.getColorNoCanvas(imageURL, 8, function(colors) {

                                ColorsHelper.styleColors($image, colors);

                                $image.bind('click', function(event) {
                                                                             ColorsHelper.styleColors($image, colors);

                                });

It works fine on SafariMobile too

screen shot 2015-11-19 at 14 03 04

A possibile improvement is to add png.js to decode PNG images: https://github.com/arian/pngjs.

teles commented 8 years ago

Very nice idea, would be great if we could use color thief on remote images!

loretoparisi commented 8 years ago

@teles yes! next step is to add the same using png.js, it's almost the same approach, the byte array will consider r,g,b,a - while for jpeg I'm ignoring the alpha channel, but everything should work in the same way. Currently I'm making further tests to bring this to Apple's TVML - https://github.com/notmasteryet/jpgjs/issues/41 as soon as I'm done with this I'm going to check the png.js if someone else will not.

ghost commented 8 years ago

@loretoparisi Right RGB colors, from an XHR image, Amazing, That's what I was looking for, But jpg.js is 190KB!!! that's so big, Can we avoid using it or something?

loretoparisi commented 8 years ago

@ManarKamel you can play with online demo here http://www.parisilabs.com/colors2/ and check the sources here: https://github.com/loretoparisi/dominant-colors-xmlhttprequest-example. Looking at jpg.js I think you can definitively shrink it using a minimizer like Yahoo YUI, http://yui.github.io/yuicompressor/ This is easier than making changes to the code, that I do not suggest.

loretoparisi commented 8 years ago

Yes, of course it would be possibile to use Base64 conversion, but you need jpg.js to decode a JPEG image byte array, then you can convert to Base64. This is what jpg.js does normally in their examples. Thanks for sharing the experiment, I will take a look.

2015-12-09 12:11 GMT+01:00 Manar Kamel notifications@github.com:

@loretoparisi https://github.com/loretoparisi No that's not what I meant, What amazing about color thief is the size, It's just 6kb minified, so you can use it for websites, but unfortunately it doesn't support XHR. You found a solution by using a JPG decoder because you couldn't use canvas for XHR images and get the right colors, right?

But how about another solution, like converting XHR images to base64, then we pass the data to the normal colorThief.getColor(), Did you try that?

If you're interested in some tvOS-like web project, check this experiment: http://codepen.io/ManarKamel/pen/zvqgZa

— Reply to this email directly or view it on GitHub https://github.com/lokesh/color-thief/issues/86#issuecomment-163190936.

Dott. Ing. Loreto Parisi Parisi Labs

Company: info@parisilabs.com Personal: loretoparisi@gmail.com Twitter: @loretoparisi Web: http://parisilabs.com http://blog.parisilabs.com LinkedIn: http://www.linkedin.com/in/loretoparisi

ghost commented 8 years ago

@loretoparisi I deleted my older comment because I wanted to post solutions instead of questions.

And No, you don't need JPG decoder (jpg.js) at ALL, After many tests, I was able to create other methods a lot better than using jpg.js/png.js decoders (more lightweight also production-ready) (Using Color Thief with XHR and with canvas).

1) Using img crossOrigin attribute (Chrome/Firefox) (Not supported in all browsers):

var img = document.createElement('img');
img.crossOrigin = 'Anonymous';
img.onload = function () {
   var colorThief = new ColorThief();
   var color = colorThief.getColor(img);
   console.log('rgb(' + color + ')')
};
img.src = 'https://crossOriginImageUrl.com/image.jpg';

2) Using XHR2 with responseType="blob" and FileReader() (Modern browsers/IE10+)

var xhr = new XMLHttpRequest();
xhr.responseType = 'blob';
xhr.onload = function() {
  var reader = new FileReader();
  reader.onload = function() {
    var img = document.createElement('img');
    img.onload = function() {
      var colorThief = new ColorThief();
      var color = colorThief.getColor(img);
      console.log('rgb(' + color + ')')
    }
    img.src = reader.result;
  }
  reader.readAsDataURL(xhr.response);
};
xhr.open('GET', 'https://crossOriginImageUrl.com/image.jpg');
xhr.send();

3) Using XHR2 with responseType="blob" and createObjectURL() (Modern browsers/IE10+)

var xhr = new XMLHttpRequest();
xhr.responseType = "blob";
xhr.onload = function () {
     window.URL = window.URL || window.webkitURL; // support for Safari/old Chrome
     var img = document.createElement('img');
     img.src = window.URL.createObjectURL(xhr.response);
     img.onload = function() {
        var colorThief = new ColorThief();
        var color = colorThief.getColor(img);
        console.log('rgb(' + color + ')')

        // Use this after you're done with the image and no longer needed
        // window.URL.revokeObjectURL(img.src) 
      }
}
xhr.open('GET', 'https://i.scdn.co/image/1a3ca29c2f93fa50f72df85b1a94f62383dbc5cd');
xhr.send();

4) Using XHR2 with responseType="arraybuffer" and Blob() with fixed content type (Modern browsers/IE10+)

var xhr = new XMLHttpRequest();
xhr.responseType = 'arraybuffer';
xhr.onload = function () {
     window.URL = window.URL || window.webkitURL; // support for Safari/old Chrome
     var blob = new Blob([xhr.response], {type: "image/jpeg"}),
         img = document.createElement('img');
     img.src = window.URL.createObjectURL(blob);
     img.onload = function() {
        var colorThief = new ColorThief();
        var color = colorThief.getColor(img);
        console.log('rgb(' + color + ')')

        // Use this after you're done with the image and no longer needed
        // window.URL.revokeObjectURL(img.src) 
      }

}
xhr.open("GET", 'https://i.scdn.co/image/1a3ca29c2f93fa50f72df85b1a94f62383dbc5cd');
xhr.send();

5) Using XHR2 with responseType="arraybuffer" and Blob() with auto content type (Modern browsers/IE10+)

var xhr = new XMLHttpRequest();
xhr.responseType = 'arraybuffer';
xhr.onload = function () {
     window.URL = window.URL || window.webkitURL; // support for Safari/old Chrome
     var contentType = xhr.getResponseHeader('content-type'),
         blob = new Blob([xhr.response], {type: contentType}),
         img = document.createElement('img');
     img.src = window.URL.createObjectURL(blob);
     img.onload = function() {
        var colorThief = new ColorThief();
        var color = colorThief.getColor(img);
        console.log('rgb(' + color + ')')

        // Use this after you're done with the image and no longer needed
        // window.URL.revokeObjectURL(img.src) 
      }
}
xhr.open('GET', 'https://i.scdn.co/image/1a3ca29c2f93fa50f72df85b1a94f62383dbc5cd');
xhr.send();

A working fiddle: http://jsfiddle.net/manarkamel/bmvqavjf/

@kosir You might want to check this out, you could use one of these for Flickr images

loretoparisi commented 8 years ago

@ManarKamel :+1: nice work! Yes if you do not need any jpg.js encoding/decoding, there is no actual need to do that, of course you need to have a cross-browser support for the createObjectURL or the FileReader. Also, in my solution I was not able to make it working in TVML since I get an error from jpg.js during the decoding phase of the byte array - look here https://github.com/notmasteryet/jpgjs/issues/41

The main problem is that TVML/TVJS as you know is a subset of HTML / JavaScript, so we do not have access to all browser object but only the ones described in https://developer.apple.com/library/tvos/documentation/TVMLJS/Reference/TVJSFrameworkReference/index.html#//apple_ref/doc/uid/TP40016076.

ghost commented 8 years ago

@loretoparisi tvOS supports XMLHTTPRequest, so I guess there would be no problem, Anyway I have no such experience in tvOS JavaScript engine

EDIT: if there's no Blob() support or If something is missing, Try converting arraybuffer to base64 ( btoa() ) then pass to colorThif.getColor(), If there's no native btoa() support, Try using external base64.js encoding/decoding.

Best wishes

teles commented 8 years ago

@loretoparisi any updates on this?