21

This prints base64 out to console:

function getBase64(file) {
    var reader = new FileReader();
    reader.readAsDataURL(file);
    reader.onload = function() {
        console.log(reader.result);
    };
    reader.onerror = function(error) {
        console.log('Error: ', error);
    };
}

var file = document.querySelector('#files > input[type="file"]').files[0];
getBase64(file); // prints the base64 string

Source: https://stackoverflow.com/a/36281449/1063287

jsFiddle: jsFiddle demo of the above working code

I want to be able to assign the base64 to a variable, so I tried the following, based on this answer:

function getBase64(file, onLoadCallback) {
    var reader = new FileReader();
    reader.readAsDataURL(file);
    reader.onload = onLoadCallback;
    reader.onerror = function(error) {
        console.log('Error when converting PDF file to base64: ', error);
    };
}

var my_pdf_file = document.querySelector("#my_pdf_file").files[0];
var my_pdf_file_as_base64 = "";
getBase64(my_pdf_file, function(e) {
    my_pdf_file_as_base64 = e.target.result
});

// print out the base64 to show i have captured the value correctly
console.log(my_pdf_file_as_base64);

It is currently printing nothing out to the console.

Question:

How can I save the base64 value as a variable?

Edit:

As requested, for context:

I am submitting a form in a Google Apps Script environment.

I have done this previously and passed a form object (which included a file) through to the Google Apps Script function.

However, one of the constraints of this approach is that if passing a form object as a parameter, it is the only parameter allowed.

A form element within the page is also legal as a parameter, but it must be the function’s only parameter

Source

In this instance, I am passing multiple parameters through, and one of the parameters will be a pdf file, converted to base64.

In response to @Aasmund's great answer, I would like the variable assignment to block further code execution:

var my_pdf_file = [ converted file here ];

// don't do this stuff until the above variable is assigned

Otherwise, I will have to refactor the remaining code to take place in the then block (suggested by @Aasmund), and that might be messy / impossible due to the amount of validation / variable preparation / conditional handling that is taking place before the form is submitted.

user1063287
  • 10,265
  • 25
  • 122
  • 218

2 Answers2

39

FileReader.readAsDataURL() is asynchronous - the download happens in the background while the rest of the code keeps executing. So the reason console.log(my_pdf_file_as_base64); prints an empty string is that the line my_pdf_file_as_base64 = e.target.result hasn't been executed yet: the call to getBase64() finishes almost immediately, and the subsequent statement is executed; only later (when the download is complete) will the callback be executed.

The way to handle this is to place the code that uses the downloaded file inside the callback:

getBase64(my_pdf_file, function(e) {
    my_pdf_file_as_base64 = e.target.result;
    console.log(my_pdf_file_as_base64);
});

Alternatively, you can repeatedly (e.g. inside a setTimeout callback or inside some DOM event handler) check if reader.readyState === FileReader.DONE - whenever this becomes true, reader.result will contain the file.

A more flexible approach is to use a Promise, which is an object that encapsulates an asynchronous computation:

function getBase64(file, onLoadCallback) {
    return new Promise(function(resolve, reject) {
        var reader = new FileReader();
        reader.onload = function() { resolve(reader.result); };
        reader.onerror = reject;
        reader.readAsDataURL(file);
    });
}

var promise = getBase64(my_pdf_file);
promise.then(function(result) {
    console.log(result);
});

So far, this looks pretty similar to the first solution, but the advantage is that promise is an object that you can pass around to other functions, so that you can start a computation in one place and decide in another place what should happen when it's finished.

As you have probably noticed, neither of these approaches allow you to block further code execution until the file content has been assigned to the global variable my_pdf_file_as_base64. This is by design; however, if you really really need to block on the download because you don't have time to refactor old code, see the discussion in https://stackoverflow.com/a/39914235/626853. If your users' browsers are sufficiently modern, you can use async/await:

$(document).on("click", ".clicker", async function() {
    var promise = getBase64(my_pdf_file);
    var my_pdf_file_as_base64 = await promise;
}

(Note that await only works inside async functions, so your click handler must be async. I also experimented with adding a busy waiting loop, but that caused my browser to hang.)

(Also note, from Charles Owen in a comment, that readAsDataURL actually returns a data: URL, so if you want to get only the Base64 data, you need to strip a prefix as per the linked documentation.)

Aasmund Eldhuset
  • 37,289
  • 4
  • 68
  • 81
  • As requested, have added context to original post, thank you. – user1063287 Nov 12 '17 at 22:52
  • The on click handler was just shown in demo to prompt example behaviour, the actual instance takes place in the success handler of a form validation library, I am assuming it is not async. Have accepted answer as it is very well explained and helpful, but if someone from the future sees post when synchronous assignment of FileReader base64 is possible, please let us know! Thanks! – user1063287 Nov 14 '17 at 09:44
  • @AasmundEldhuset How would I do this method using the setTimeout() can you provide an example as I'm having no joys using promises at the minute :( – Zabs Nov 16 '18 at 11:24
  • @Zabs: I haven't actually tried that approach myself, but I'm pretty sure that it should work to do ```var timeoutID = window.setTimeout(function() { if (reader.readyState === FileReader.DONE) { console.log(reader.result); window.clearTimeout(timeoutID); } else { console.log("Downoading..."); } }, 1000);``` after `reader.readAsDataURL(file);`. (Sorry about the formatting; code blocks don't seem to work in comments.) – Aasmund Eldhuset Nov 20 '18 at 18:44
  • how to solve `await is only valid in async function`? – Lei Yang Sep 20 '19 at 08:41
  • @LeiYang: I'm not sure what you mean - that's a language restriction and not something you can "solve". Either, use an async function, or go with the first version of the code. – Aasmund Eldhuset Sep 20 '19 at 21:00
  • 1
    I really like this promise example and agree this is an elegant way to handle this. I'd add that the result returned isn't just the base64 of the image, but it's actually the entire link you'd use for the src of the image. – Charles Owen Nov 21 '21 at 20:00
  • @CharlesOwen: Thank you - I added that. – Aasmund Eldhuset Nov 23 '21 at 22:20
3

I work by this Code

   async function checkFileBase64(el) {
        let inputEl = $(el).find('input[type=file]');
        let promise = getBase64(inputEl[0]);
        return await promise;
    }
    function getBase64(file) {
       return new Promise(function (resolve, reject) {
            let reader = new FileReader();
            reader.onload = function () { resolve(reader.result); };
            reader.onerror = reject;
            reader.readAsDataURL(file.files[0]);
        });
    }
Milad Jafari
  • 970
  • 10
  • 15