16

I would like to know a way to generate a single pixel in JavaScript converting it to base64. The ideal function would be:

function createPixel(hexColor, opacity){
   //...Calculate
   return base64DataURL;
}

I am not very familiar with image processing. Any format is fine (png, gif etc). I would like to use this to overlay background images, (yes I could use rgba css3 but I am trying to place it with a background-image only on one element so i am not overlaying an element on top of another to achieve the effect).

Thanks in advance.

Edit: I would like not to use canvas, I am sure you can use canvas to get the base64 dataURL but I am sure it is not as fast as a string manipulation. Also I am not worried about converting an image into base64 but more interested in creating the image.

user654628
  • 1,429
  • 1
  • 17
  • 37
  • Honestly, using canvas+toDataURL is likely the easiest solution. Otherwise, you're basically going to have to figure out how a 1x1 image of a given color and opacity is represented in whatever format (say, PNG) and then either use a JavaScript base64 encoder on that information, or figure out the pattern and shortcut the external encoding yourself. Is that really worth it? – Matt Ball Apr 30 '11 at 23:45
  • 2
    You might also find that using `` is actually faster than a JavaScript base64 encoder. – Matt Ball Apr 30 '11 at 23:53
  • Thanks I guess I will stick with canvas. – user654628 May 01 '11 at 00:17

4 Answers4

9

4+ years later, here's a simpler solution that generates a standard GIF so actually works in browsers (I couldn't get the PEM solution to work in anything) and is up to an order of magnitude faster than PEM/canvas. Only downside is GIF doesn't support alpha opacity - but that can be controlled via CSS.

It's based on this JSFiddle (unknown beautiful author), but with basic optimisation - reused keyStr and accepts both hex string ('#FF0000') and hex literal (0xFF0000) - latter much faster (thanks icktoofay).

<html>

<body onload="Test()">
  <script>
    function Test() {
      var img = new Image;
      img.src = createPixelGIF(0xff0000); // generate a red pixel data URI
      img.height = 100;
      img.width = 100; // optional: add dimensions
      document.body.appendChild(img); // add to the page
    }

    // ROUTINES =============

    var createPixelGIF = (function() {

      var keyStr = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";

      return function createPixelGIF(hexColor) {
        return "data:image/gif;base64,R0lGODlhAQABAPAA" + encodeHex(hexColor) + "/yH5BAAAAAAALAAAAAABAAEAAAICRAEAOw==";
      }

      function encodeHex(hexColor) {
        var rgb;
        if (typeof hexColor == 'string') {
          var s = hexColor.substring(1, 7);
          if (s.length < 6) s = s[0] + s[0] + s[1] + s[1] + s[2] + s[2];
          rgb = [parseInt(s[0] + s[1], 16), parseInt(s[2] + s[3], 16), parseInt(s[4] + s[5], 16)];
        } else
          rgb = [(hexColor & (0xFF << 16)) >> 16, (hexColor & (0xFF << 8)) >> 8, hexColor & 0xFF];

        return encodeRGB(rgb[0], rgb[1], rgb[2]);
      }

      function encodeRGB(r, g, b) {
        return encode_triplet(0, r, g) + encode_triplet(b, 255, 255);
      }

      function encode_triplet(e1, e2, e3) {
        enc1 = e1 >> 2;
        enc2 = ((e1 & 3) << 4) | (e2 >> 4);
        enc3 = ((e2 & 15) << 2) | (e3 >> 6);
        enc4 = e3 & 63;
        return keyStr.charAt(enc1) + keyStr.charAt(enc2) + keyStr.charAt(enc3) + keyStr.charAt(enc4);
      }

    })();
  </script>
</body>

</html>

Updated JSPerf results: http://jsperf.com/base64image/4 (code above is "createPixelGIF2"). You'll see I tried further optimisation (3+4), but it seems JS is happier with stack operations than hard-to-read combo functions :)

I also added an updated test for the canvas method, which for some reason excluded the instantiation of the canvas object - the biggest performance drag that would be seen in real world use.

styfle
  • 22,361
  • 27
  • 86
  • 128
Ben Johnson
  • 93
  • 1
  • 4
6

Here's a fully cross-browser-compatible implementation using <canvas> (demo @ jsfiddle).

var canvas = document.createElement('canvas');

// http://code.google.com/p/explorercanvas/wiki/Instructions#Dynamically_created_elements
if (!canvas.getContext) G_vmlCanvasManager.initElement(canvas);

var ctx = canvas.getContext('2d');
canvas.width = 1;
canvas.height = 1;

// for simplicity, assume the input is in rgba format
function createPixel(r, g, b, a) {
    ctx.fillStyle = 'rgba(' + [r,g,b,a].join() + ')';
    ctx.fillRect(0,0,1,1);
    // 'data:image/png;base64,'.length => 22
    return canvas.toDataURL('image/png','').substring(22);
}

I was curious to see how this stacks up against icktoofay's answer, performance-wise. Note, this will have to use excanvas for IE <9, which means that performance will almost certainly be worse there (but what's new).

Check out the jsperf: http://jsperf.com/base64image

Community
  • 1
  • 1
Matt Ball
  • 354,903
  • 100
  • 647
  • 710
  • I optimized my answer a little more so it's not creating functions and lookup tables every time. It would be interesting to see how the optimized one stands up to `canvas`, as well. – icktoofay May 01 '11 at 01:40
  • And, apparently, there's a native Base64 encoder in some browsers (`window.btoa`), so I just added that, too. – icktoofay May 01 '11 at 01:44
  • Ok great, I updated the jsperf. I did call your function correctly, right? Your optimized version (using native `btoa` when present) blows the other 2 out of the water. – Matt Ball May 01 '11 at 01:48
  • Yeah, it makes sense. Concatenating a few strings together and then Base64 encoding it is going to be a lot faster than creating a canvas, rendering into it, encoding it as a PNG, and then encoding it into Base64. Regardless, my JavaScript native version is fairly useless; PAM is a very rare format, and is unlikely to be of use to many people. – icktoofay May 03 '11 at 01:57
4
function createPlaceholder(w, h) {
    var img = document.createElement('img');
    img.setAttribute('style', 'width:'+w+'px;height:'+h+'px;border:none;display:block');
    img.src = 'data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==';
    return img;
}

(for transparent pixel)

jaboja
  • 2,178
  • 1
  • 21
  • 35
2

Try this. It uses a somewhat esoteric image format (PAM), but you said any format is fine, and it really does work! It's not optimized or anything, so it's probably pretty slow, but hey, it works.

Edit: Okay, I optimized it a little...

var createPixel=(function() {
    var table=[];
    for(var i=0;i<26;i++) {
        table.push(String.fromCharCode("A".charCodeAt(0)+i));
    }
    for(var i=0;i<26;i++) {
        table.push(String.fromCharCode("a".charCodeAt(0)+i));
    }
    for(var i=0;i<10;i++) {
        table.push(i.toString(10));
    }
    table.push("+");
    table.push("/");
    function b64encode(x) {
        var bits=[];
        for(var i=0;i<x.length;i++) {
            var byte=x.charCodeAt(i);
            for(var j=7;j>=0;j--) {
                bits.push(byte&(1<<j));
            }
        }
        var output=[];
        for(var i=0;i<bits.length;i+=6) {
            var section=bits.slice(i, i+6).map(
                function(bit) { return bit?1:0; });
            var required=6-section.length;
            while(section.length<6) section.push(0);
            section=(section[0]<<5)|
                (section[1]<<4)|
                (section[2]<<3)|
                (section[3]<<2)|
                (section[4]<<1)|
                section[5];
            output.push(table[section]);
            if(required==2) {
                output.push('=');
            }else if(required==4) {
                output.push('==');
            }
        }
        output=output.join("");
        return output;
    }
    if(window.btoa) {
        b64encode=window.btoa;
    }
    return function createPixel(hexColor, opacity) {
        var colorTuple=[
            (hexColor&(0xFF<<16))>>16,
            (hexColor&(0xFF<<8))>>8,
            hexColor&0xFF,
            Math.floor(opacity*255)
        ];
        var data="P7\nWIDTH 1\nHEIGHT 1\nDEPTH 4\nMAXVAL 255\nTUPLTYPE RGB_ALPHA\nENDHDR\n";
        colorTuple.forEach(function(tupleElement) {
            data+=String.fromCharCode(tupleElement);
        });
        var base64DataURL="data:image/pam;base64,"+b64encode(data);
        return base64DataURL;
    }
})();

...but really, canvas should work fine.

icktoofay
  • 126,289
  • 21
  • 250
  • 231
  • Cool idea, but output doesn't seem to display for me in firefox4 or IE8... :/ "Failed to load url". – aaaidan May 01 '11 at 22:15
  • @aaaidan: Of course; PAM is fairly uncommon. It makes sense Firefox and IE don't support it. – icktoofay May 01 '11 at 22:31
  • @aaaidan: You can prove it works though by decoding the Base64 content into a `.pam` file and converting it with ImageMagick. Not that that's helpful for what the OP needed. – icktoofay May 01 '11 at 22:33