0

Consider this JSFiddle. In it I select photos which I then want to base64 encode using canvas so I can store them in sessionStorage for deferred uploading. Because I have it set for multiple files, I loop through each one and create an image and a canvas, but no matter what it just seems to output the exact same base64 encoded image every time. Through testing I know that on each loop iteration the image is different and does indeed point to a different file blob, but the canvas is just outputting the same thing over and over, which I think is also the last file in the files list. Sometimes it will also just output a "data," string and that's it. I'd love it if someone can point me in the right direction.

Code is show below:

HTML

<style type="text/css">
    input[type="file"] {
        display: none;
    }

    a {
        display: inline-block;
        margin: 6px;
    }
</style>
<form action="#" method="post">
    <input type="file" accept="image/*" multiple />
    <button type="button">Select Photos</button>
</form>
<nav></nav>

JavaScript

console.clear();

$(function () {
  $("button[type=button]").on("click", function (e) {
    $("input[type=file]").trigger("click");
  });
  
  $("input[type=file]").on("change", function (e) {
    var nav = $("nav").empty();
    
    for (var i = 0, l = this.files.length; i < l; i++) {
      var file = this.files[i],
          image = new Image();
          
      $(image).on("load", i, function (e) {
        var canvas = document.createElement("canvas"),
            context = canvas.getContext("2d");
        
       canvas.height = this.height;
       canvas.width = this.width;
       
        context.drawImage(this, 0, 0);
        
        nav.append("<a href=\"" + canvas.toDataURL("image/jpeg") + "\" target=\"_blank\">" + e.data + "</a>");
        
        URL.revokeObjectURL(this.src);
      });
      
      image.src = URL.createObjectURL(file);
    }
  });
});
Saranga A
  • 1,091
  • 16
  • 22
Gup3rSuR4c
  • 9,145
  • 10
  • 68
  • 126
  • 3
    That's because your `.on("load")` handler is asynchronous – adeneo Mar 25 '16 at 01:05
  • I am not sure what you are asking. It seems to be working correctly on my side. – Chris Mar 25 '16 at 01:12
  • When I display the canvas it shows each image and the inline image sources are correct too, you will need to clarify what you want, else I can't help you. – Chris Mar 25 '16 at 01:25
  • Is `"image/jpeg"` at `.toDataURL()` the issue ? If uploaded image is not `.jpeg` image may not be rendered as expected ? – guest271314 Mar 25 '16 at 01:33
  • Sigh, I swear that as I was leaving the office it wasn't working. Instead, I was just getting the last image uploaded for each link no matter how many there were. I guess I was in a parallel dimension or something... The `image/jpeg` is not an issue because it pulls up as I expect it to with it. Plus the images were coming from an iPad so they would always be jpegs. – Gup3rSuR4c Mar 25 '16 at 02:27
  • @Gup3rSuR4c _"I was just getting the last image uploaded for each link no matter how many there were."_ See http://stackoverflow.com/questions/33898423/jquery-get-within-for-loop-scope/ , http://stackoverflow.com/questions/14220321/how-do-i-return-the-response-from-an-asynchronous-call – guest271314 Mar 25 '16 at 02:51

1 Answers1

3

but no matter what it just seems to output the exact same base64 encoded image every time.

.load() event is asynchronous. You can use $.when() , $.Deferred(), substitute $.map() for for loop to handle asynchronously loaded img elements. The caveat that the displayed a element text may not be in numerical order; which can be adjusted by sorting the elements at .then(); though not addressed at Question, if required, the listing or loading of images sequentially can also be achieved.

$("input[type=file]").on("change", function(e) {
    var nav = $("nav").empty();
    var file = this.files
    $.when.apply($, $.map(file, function(img, i) {    
      return new $.Deferred(function(dfd) {
        var image = new Image();
        $(image).on("load", i, function(e) {
          var canvas = document.createElement("canvas")
          , context = canvas.getContext("2d");   
          canvas.height = this.height;
          canvas.width = this.width;    
          context.drawImage(this, 0, 0);    
          nav.append("<a href=\"" 
                    + canvas.toDataURL() 
                    + "\" target=\"_blank\">" 
                    + e.data + "</a>");
          dfd.resolve()
          URL.revokeObjectURL(this.src);
        });
        image.src = URL.createObjectURL(img);
        return dfd.promise()
      })
    })).then(function() {
      nav.find("a").each(function() {
        console.log(this.href + "\n");
      })
    })
  });
})

jsfiddle https://jsfiddle.net/bc6x3s02/19/

guest271314
  • 1
  • 15
  • 104
  • 177
  • I'm ok with the async and out of order caveats. Ultimately I'll be storing the images into `sessionStorage` where a background uploader will pick them up and continue so I won't be displaying any visual ques beyond a "x image remaining" thing. – Gup3rSuR4c Mar 25 '16 at 02:43
  • *"If the uploaded image does not have type "image/jpeg" parameter set the image may not be rendered correctly"* Wait, what !? The images drawn on the canvas are decoded images, no matter if they were jpeg gif png video frame or an other canvas, once on the canvas it's just a set of raw pixels. The second part of this sentence is indeed almost correct except that it **will be** be an inaccurate reflection of the correct data URI of the image file, because of the decoding-reencoding process. If OP wanted the dataURI of the file without reencoding it to jpeg, he should have used a `FileReader`. – Kaiido Mar 25 '16 at 11:44
  • @Kaiido What are you considering "non-sense" ? Is that term necesary to communicate the points you are making ? Tried initial version of jsfiddle with different images. The images did not render correctly until `"image/jpeg"` was removed. Feel free to edit answer, or convey what you believe the correct explanation should be to improve answer – guest271314 Mar 25 '16 at 11:56
  • @Kaiido _"The second part of this sentence is indeed almost correct except that it will be be an inaccurate reflection of the correct data URI of the image file, because of the decoding-reencoding process. If OP wanted the dataURI of the file"_ At jsfiddle at original Question, a `data URI` is used to render the image at the new tab. Have you try the original jsfiddle ? Again, feel free to edit the Answer if the contents are inaccurate; or suggest how the explanation or `js` could be improved – guest271314 Mar 25 '16 at 12:05
  • For example, try uploading a `.gif` at original jsfiddle https://jsfiddle.net/4ngsTrIb1DI5te5H/bc6x3s02/ . For that matter, canvas is still setting `type` to `"image/png"` at https://jsfiddle.net/bc6x3s02/19/ , negating the animation effect; though this was not addressed at OP. Though was addressed at comments _"The image/jpeg is not an issue because it pulls up as I expect it to with it. Plus the images were coming from an iPad so they would always be jpegs."_ http://stackoverflow.com/questions/36212533/html5-canvas-todataurl-is-always-the-same-in-a-for-loop/36212842#comment60059155_36212533 – guest271314 Mar 25 '16 at 12:13
  • @Kaiido This `.gif` https://upload.wikimedia.org/wikipedia/commons/d/d8/5-cell.gif would not be animated at either version of jsfiddle. That would actually be a good Question; or issue to address when using `.toDataURL()` on `.gif`; though vaguely recollect ability to set `type` at `.toDataURL()` ? – guest271314 Mar 25 '16 at 12:18
  • @guest271314 this is [per specs](https://html.spec.whatwg.org/multipage/scripting.html#image-sources-for-2d-rendering-contexts:canvasimagesource-3) and it totally goes in my way. Once drawn on the canvas, only pixels remain, maybe not even the same ones if you used some of the drawImage options. If you want a dataURI represenation of a file, use a FileReader and its `readAsDataURL()` method. The only advantage taht OP's code has on the FileReader is that he can actually compress the image as jpeg to store it in localstorage – Kaiido Mar 25 '16 at 12:18
  • Not addressing specs, but actual results; though which portion of specs are you quoting ? have you tried downloading and uploading the `.gif` ? OP does not address this – guest271314 Mar 25 '16 at 12:22
  • No, that won't be a good question at all. You need to understand that `canvas.toDataURL` method only extracts the content drawn on the canvas when called. So it would makes no sense to have any animated content from this method. In some UA, `image/gif` is a permitted type for toDataURL, but it defaults to ìmage/png` yes... – Kaiido Mar 25 '16 at 12:23
  • _"If you want a dataURI represenation of a file"_ ? This user does not want anything; did not ask actual Question. Not certain where this is going ? Which portion of Answer do you have issue with ? The issue with the Question was, as interpret here, `for` loop – guest271314 Mar 25 '16 at 12:25
  • @Kaiido _"In some UA, image/gif is a permitted type for toDataURL, but it defaults to ìmage/png` yes... "_ Are you saying that `.toDataURL()` of `canvas` context does not render correct `data URI` for `.gif` images ? – guest271314 Mar 25 '16 at 12:34
  • 1
    No, `toDataURL` is a method to extract the pixels drawn on the canvas. If you do use drawing methods like `fillRect`, it will also be part of the exported data. `drawImage` will draw the raw pixels of the ImageSource you pass in. No matter what was their type before, if the browser can decode it, it will be drawn. (you can even draw svg on canvas). But, once drawn on canvas, you just have the pixels, no more info about where they do come from, no encoding and no more vectors for svg. Now, `toDataURL` has options to set the reencoding type that will be used to extract all these raw pixels. – Kaiido Mar 25 '16 at 12:37
  • @Kaiido That is good to be aware of: Do not expect `.toDataURL()` of `canvas` context to return accurate `data URI` representation `.gif` images. – guest271314 Mar 25 '16 at 12:41
  • 1
    Not accurate for any image ! It will always decode+reencode it. But this can be usefull to convert huge png files in small jpeg ones or vectors svg to png. Ps, you could also extract the raw information of the pixels by using the `getImageData` method, which will give you a TypedArray containing each value for each r,g,b and alpha channel – Kaiido Mar 25 '16 at 12:43
  • @Kaiido, I saw your comment about using `FileReader`, so I poked around in it and I do like that it's a much shorter way to just get the base64 encoded version of the image without instancing a `canvas` element and messing around with it. That being said, I've updated the fiddle, and now it's doing the exact same thing that lead me to post here in the first place. If you select more than one file, only the last one will be base64 encoded, the other ones will be discarded even though I'm instancing a new `FileReader` for each one in the loop... – Gup3rSuR4c Mar 25 '16 at 15:39
  • ...Curiously, the files are correct on each loop iteration, but the reader is just ignoring them. How can I address this? My ultimate goal is just select a bunch of photos from an iPad, base64 encode them, save them into `sessionStorage` and then have a "background uploader" of some kind post them later on. – Gup3rSuR4c Mar 25 '16 at 15:41
  • 1
    @Gup3rSuR4c _"...Curiously, the files are correct on each loop iteration, but the reader is just ignoring them. How can I address this?"_ Note, `FileReader` `onload` event is also asynchronous. See http://stackoverflow.com/questions/33492797/slider-filereader-js-multiple-image-upload-incrementing-index/ , http://stackoverflow.com/questions/28856729/upload-multiple-image-using-ajax-php-and-jquery/ – guest271314 Mar 25 '16 at 17:16