5

I am trying to download xlsx spreadsheet with javascript. I have tested base64 data. I decode it like so:

var data = atob(validBase64Data);

After that, I do:

save(name, data, type) {
  const blob = new Blob([data], {type: type});
  let objectURL = window.URL.createObjectURL(blob);
  let anchor = document.createElement('a');

  anchor.href = objectURL;
  anchor.download = name;
  anchor.click();

  URL.revokeObjectURL(objectURL);
}

Where name is a filename.xlsx, data is the decoded data and type is a mime-type string.

The excel file is downloaded but would not open as excel. Data is corrupted somehow.

In addition: I tested the same data with a unix terminal command to base64 decode and write the xlsx directly into that file, and that produced working file. Test was done like so:

  1. I saved base64 data to test_excel.txt`
  2. Ran command base64 -D -i test_excel.txt -o test_excel.xlsx
  3. test_excel.xlsx is recognized by excel.

What am I doing wrong with the code?

Lex Podgorny
  • 2,598
  • 1
  • 23
  • 40
  • @RolandStarke I tested btoa, which, I assume, reverts atob with same Unix interface, and it worked. – Lex Podgorny Feb 17 '18 at 00:03
  • Thinking about it further, my suspicion is now that the original file string was not utf-8-encoded, but `atob()` assumes `dataFromServer` was a base64-encoded string that came from a utf-8 encoded string. – Patrick Roberts Feb 17 '18 at 00:05
  • @PatrickRoberts this is not relevant, after I receive it, I test it as I explain in my question. – Lex Podgorny Feb 17 '18 at 00:05
  • What is the value of the type parameter? – James Feb 17 '18 at 00:06
  • @James I tried 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' and 'octet/stream' – Lex Podgorny Feb 17 '18 at 00:07
  • @PatrickRoberts "I tested the same data with a unix terminal command to base64 decode and write the xlsx directly into that file, and that produced working file." I assume this excludes invalid data, right? Would you suggest I try to utf8 decode it anyway? – Lex Podgorny Feb 17 '18 at 00:08
  • @DivisionbyZero I can read, thank you. I'm saying that your terminal-based base64 decoder generates a binary string, whereas `atob()` generates a utf-8 string. So no. If I am correct, your corrupted file will have a larger byte size than the valid file. – Patrick Roberts Feb 17 '18 at 00:10
  • @PatrickRoberts done the edits you proposed. – Lex Podgorny Feb 17 '18 at 00:12
  • Before I make an assertion that I'm correct, try running `base64` without `-i`. If that _doesn't_ work, then the problem is that your `atob()` isn't RFC 3548 compliant, which would have a different solution that is much simpler. – Patrick Roberts Feb 17 '18 at 00:15
  • Your save function is fine - [quick fiddle using a xlsx file from the client](https://jsfiddle.net/fde3emas/) - so the data you're getting must be mangled a bit as @PatrickRoberts is suggesting. – James Feb 17 '18 at 00:27
  • @PatrickRoberts tried without `-i` option. Still works fine. – Lex Podgorny Feb 17 '18 at 00:29
  • @James that's not really a good test. [This would be a more accurate test](https://jsfiddle.net/fde3emas/2/). – Patrick Roberts Feb 17 '18 at 00:29
  • Can you propose a test to compare the data I produce with the one uploaded through the input? – Lex Podgorny Feb 17 '18 at 00:45
  • @PatrickRoberts Ah ok. I was just trying to provide a code-based working example that could be used as a starting point to eliminate the parts that work. – James Feb 17 '18 at 00:49

2 Answers2

4

Here is the code that solved it:

export default {
 save(name, data, type, isBinary) {
  if (isBinary) {
    var bytes = new Array(data.length);
    for (var i = 0; i < data.length; i++) {
        bytes[i] = data.charCodeAt(i);
    }
    data = new Uint8Array(bytes);
  }

  var blob = new Blob([data], {type: type});
  let objectURL = window.URL.createObjectURL(blob);
  let anchor = document.createElement('a');

  anchor.href = objectURL;
  anchor.download = name;
  anchor.click();

  URL.revokeObjectURL(objectURL);
 }
}

Thanks to everyone who participated in resolving. Also, credits to: Creating a Blob from a base64 string in JavaScript

Lex Podgorny
  • 2,598
  • 1
  • 23
  • 40
1

Okay, so let's clarify a few things before anyone tries to "explain" the problem incorrectly.

The original .xlsx is a binary-encoded file, meaning that the data will contain bytes in the full range of 0x00 to 0xFF.

In the question, it is assumed that this string has been successfully encoded into a valid base64 string, with no extraneous characters (as indicated by the success of the test using base64 without the -i flag), and stored to validBase64Data.

The problem is that atob(validBase64Data) generates a string decoded into utf-8, not binary. And as I said before, the original binary string contains non-ASCII bytes in the range 0x80 to 0xFF. In utf-8, these code points are stored as two bytes instead of one, so the solution, as described in Creating a Blob from a base64 string in JavaScript, is to convert the code points of each character in the utf-8 string data into bytes stored as a Uint8Array, and then construct a Blob from that.

A naive solution might look like this, though please refer to Creating a Blob from a base64 string in JavaScript for more performant solutions:

const blob = new Blob([Uint8Array.from(data, c => c.charCodeAt(0))], { type });
//...

This uses TypedArray.from(iterable, mapFn).

Patrick Roberts
  • 49,224
  • 10
  • 102
  • 153