25

I'm trying to render a PNG image that is stored in a javascript Uint8Array. The code that I originally tried was the following:

var imgByteStr = String.fromCharCode.apply(null, this.imgBytes);

var pageImg = new Image();
pageImg.onload = function(e) {

    // Draw onto the canvas
    canvasContext.drawImage(pageImg, 0, 0);

};

// Attempt to generate the Data URI from the binary
// 3rd-party library adds toBase64() as a string function
pageImg.src="data:image/png;base64,"+encodeURIComponent(imgByteStr.toBase64());

However, I found that for some reason the onload function was never running (I had breakpoints set up in chrome and it simply never hit). I found that if I set the src to a URL instead of Data URI, it worked correctly. However, due to some constraints, I cannot directly refer to the image's URL, i.e., it must be loaded from the UInt8Array.

Also, to complicate things slightly more, these images may be megabytes in size. From what I have read, there are compatibility issues with attempting to use Data URIs for very large images. Because of this, I am extremely hesitant to utilize them.

The question, therefore, is how do I render this byte array as a PNG onto a canvas context without using Data URIs or referring directly to the image URL?


I've also tried using something like the following to manipulate the image data directly using the putImageData function. Keep in mind that I don't 100% understand how this function works

var imgdata = canvasContext.createImageData(this.baseHeight, this.baseWidth);
var imgdatalen = imgdata.data.length;
for(var i=0; i<imgdatalen; i++) {
    imgdata.data[i] = _this.imgBytes[i];
}
canvasContext.putImageData(imgdata, 0, 0);

It was lifted from a blog whose tab I have since closed, so apologies to the inspiration for not giving appropriate credit.


Also, while slowly hammering away at this thing, I ran into an error in Chrome SECURITY_ERR: DOM Exception 18. Turns out that, as soon as an image is loaded inot a canvas using drawImage, it cannot be retrieved without some additional workarounds. A Chromium Blog post on the topic was especially useful

zashu
  • 747
  • 1
  • 7
  • 22
  • Did you look into `putImageData()`? – David Hellsing Dec 19 '12 at 11:05
  • Just for curiosity: how did the image data come in the array? – closure Dec 19 '12 at 11:40
  • 1
    @David: I had looked into it a little bit, but my understanding is that is for raw image data, not PNG-encoded image data. I'll give it a shot – zashu Dec 19 '12 at 11:46
  • @raghaw: It's downloaded via an async HTTP GET request. The reason why the URL can't be referenced directly here is because the next step (once I get this working) is to encrypt the results of the GET request and then decrypt in JS. Thus, in the long run, the image URL won't return an image at all, but an encrypted binary file. It's a long story, but one that I'm stuck with ;) – zashu Dec 19 '12 at 11:47
  • 1
    I know it may be out of your control, but re-implementing the browser's existing support for encryption seems... frustrating. You can't just use HTTPS? FWIW I've done a little work with image data in the browser and I'm not sure how you'll turn encoded PNG bytes into 32-bit ARGB data. Perhaps you can use a Blob, then assign it as the src of an Image, then draw the image to the canvas. This will only work in some browsers. Is that an issue for you? – Drew Noakes Dec 19 '12 at 12:01
  • @DrewNoakes: The rationale actually isn't attempting to recreate browser encryption (this all happens over HTTPS), but it's a classic case of not wanting the images to be downloadable *by the average user* (Forget about screenshots or manual decrypting). My main concern is that, if we use the url in the image, then Chrome will download the PNG, display the request in the net panel, and then allow a user to save that image easily. That, unfortunately, is the root requirement. – zashu Dec 19 '12 at 18:09
  • @zashu, ok I see. Good luck! Fighting the browser like this tends to cause failures for some users, but I applaud your efforts. Seems like you're close. – Drew Noakes Dec 19 '12 at 21:23
  • You may be able to skip the for-loop in this example by using imgdata.data.set(_this.imgBytes) instead. I don't know if imgBytes would need to be Uint8ClampedArray instead of a Uint8Array, though. – DDR Aug 28 '14 at 05:54

3 Answers3

32

If you already have a UInt8Array, you should consider using Blob and createObjectURL; createObjectURL allocates a special URL that allows the browser to access an internally created blob of binary data as though it were a file being loaded externally.

Where it is supported, createObjectURL is an excellent alternative to huge data URIs. You may still need to fall back to use of data URIs in Opera, IE<10, and all but the most recent versions of mobile browsers.

var myArray; //= your data in a UInt8Array
var blob = new Blob([myArray], {'type': 'image/png'});
var url = URL.createObjectURL(blob); //possibly `webkitURL` or another vendor prefix for old browsers.
ovg
  • 1,486
  • 1
  • 18
  • 30
ellisbben
  • 6,352
  • 26
  • 43
  • Excellent answer, of note I was having trouble with this until I wrapped my UInt8Array in an array like this code says. Sometimes you just have to follow instructions :). `var blob = new Blob([myArray], {'type': 'image/png'});` – Vigrant Nov 26 '20 at 01:29
26

If you have the PNG data in memory (which I think is what you're describing) then you can create an image from it. In HTML it would look like:

 <img src="data:image/png;base64,KSjs9JdldhAlisflAkshf==" />

In JavaScript you can do the same:

var image = document.createElement('img');
    image.src = 'data:image/png;base64,' + base64Data;

Note that you can change the MIME type if you don't have PNG data.

You could then draw the image onto your canvas using context.drawImage(image, 0, 0) or similar.

So the remaining part of the puzzle is to encode the contents of your Uint8Array into Base 64 data.

var array = new Uint8Array(),
    base64Data = btoa(String.fromCharCode.apply(null, array));

The function btoa isn't standard, so some browsers might not support it. However it seems that most do. Otherwise you might find some code I use helpful.

I haven't tested any of this myself!

Samuel Katz
  • 24,066
  • 8
  • 71
  • 57
Drew Noakes
  • 300,895
  • 165
  • 679
  • 742
  • btoa is definitely a great alternative to the 3rd-party library I'm using as a way to encode using native functions. My biggest fear with Data URIs (which is the method you recommend) is size, specifically, on mobile browsers for which this is designed, a concern I had after looking into [this question](http://stackoverflow.com/questions/695151/data-protocol-url-size-limitations). – zashu Dec 19 '12 at 18:21
  • 2
    I've hit a limit in Chrome before when trying to display an image via a data URL. If it's greater than 2MB, Chrome faults the tab's process. Other browsers have higher limits. I used a workaround I found on this Chromium bug report: http://code.google.com/p/chromium/issues/detail?id=69227 – Drew Noakes Dec 19 '12 at 21:26
  • In the end, I opted to go with data URIs since it seemed no other solution fit the need. I just had to make sure that the images themselves were small enough. – zashu Jan 30 '13 at 21:51
  • The proposed solution for turning your Uint8Array to base64 data will throw call stack size errors for large images. See https://stackoverflow.com/a/38437518/3826852 (in short, use TextEncoder instead of String.fromCharCode.apply) – basemath Mar 14 '19 at 20:38
8

Full Working Example: (save as .html file and open)

1: Creates Uint8Array.

2: Fills it with debug pattern.

3: Pastes it to canvas element on page.

Output should like like below:

enter image description here

<!DOCTYPE HTML >
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title> U8A_TO_CANVAS </title>
    <meta name   ="author" 
          content="John Mark Isaac Madison">
    <!-- EMAIL: J4M4I5M7@HOTMAIL.com           -->
</head>
<body>
<canvas id="CANVAS_ID" 
         width ="512" 
         height="512">
</canvas>
<script>

var com     =  4 ; //:4 components( RGBA )
var wid     = 512;
var hig     = 512;
var tot_com = wid*hig*com;//:total#components
var u8a     = new Uint8Array( tot_com );

DrawDebugPattern  ( u8a , wid, hig              );
Uint8ArrayToCanvas( u8a , wid, hig, "CANVAS_ID" );

function Uint8ArrayToCanvas( 
    u8a, //:uint8Array
    wid, //:width__of_u8a_data_in_pixels
    hig, //:height_of_u8a_data_in_pixels
    nam, //:name_id_of_canvas_on_dom
){

    //:Get Canvas:
    var can = document.getElementById( nam );
    if(!can){ throw "[FailedToGetCanvas]"; }

    //:Get Canvas's 2D Context:
    var ctx = can.getContext("2d");
    if(!ctx){ throw "[FailedToGetContext]"; }

    //:Use U8A to create image data object:    
    var UAC = new Uint8ClampedArray( u8a,wid,hig);
    var DAT = new ImageData(UAC, wid, hig);

    //:Paste Data Into Canvas:     
    var ORG_X = 0;                         
    var ORG_Y = 0;                         
    ctx.putImageData( DAT, ORG_X, ORG_Y );  
}

function DrawDebugPattern(u8a,wid,hig){

    var com     = 4      ; //:RGBA==4components.
    var tot_pix = wid*hig;//:total#pixels

    //:Calculate point in center of canvas:
    var cen_x = wid/2;
    var cen_y = hig/2;

    //:Define a circle's radius:
    var rad_c = Math.min(wid,hig) / 2;

    //:Make a pattern on array:
    var d   = 0; //:d_is_for_distance
    var ci  = 0; //:Component_Index
    var pi  = 0; //:Pixel_Index
    var px  = 0; //:Pixel_X_Coord
    var py  = 0; //:Pixel_Y_Coord
    for( pi = 0; pi < tot_pix; pi++ ){

        //:Calculate index of first component
        //:of current pixel:
        ci = pi * com;

        //:Index_To_XY_Formula:
        px =  pi    % wid ;
        py = (pi-px)/ wid ;

        //:Decide if pixel is inside circle:
        var dx = (cen_x-px); //:delta_x
        var dy = (cen_y-py); //:delta_y
        d=Math.sqrt( (dx*dx)+(dy*dy) );
        if( d < rad_c ){
            //:INSIDE_CIRCLE:
            u8a[ ci + 0 ] = 0  ; //:Red
            u8a[ ci + 1 ] = 255; //:Green
            u8a[ ci + 2 ] = 0  ; //:Blue
            u8a[ ci + 3 ] = 255; //:Alpha
        }else{
            //:OUTSIDE_CIRCLE:
            u8a[ ci + 0 ] = 0  ; //:Red
            u8a[ ci + 1 ] = 0  ; //:Green
            u8a[ ci + 2 ] = 64 ; //:Blue
            u8a[ ci + 3 ] = 255; //:Alpha
        }

    }
}
</script>
</body>
<!-- In IE: Script cannot be outside of body.  -->
</html>
KANJICODER
  • 3,611
  • 30
  • 17