Chrome FileReader returns 大文件的空字符串 (>= 300MB)
Chrome FileReader returns empty string for big files (>= 300MB)
目标:
- 在浏览器中,从用户文件系统读取一个文件作为base64字符串
- 这些文件最大为 1.5GB
问题:
- 以下脚本在 Firefox 上运行良好。无论文件大小如何。
- 在 Chrome 上,该脚本适用于较小的文件(我测试过大小约为 5MB 的文件)
- 如果您选择一个更大的文件(例如 400MB),FileReader 会在没有错误或异常的情况下完成,但是returns一个空字符串而不是 base64 字符串
问题:
- 这是 chrome 错误吗?
- 为什么既没有错误也没有异常?
- 我该如何解决或解决这个问题?
重要提示:
请注意,分块 对我来说不是一个选项,因为我需要通过 'POST' 将完整的 base64 字符串发送到 API不支持块。
代码:
'use strict';
var filePickerElement = document.getElementById('filepicker');
filePickerElement.onchange = (event) => {
const selectedFile = event.target.files[0];
console.log('selectedFile', selectedFile);
readFile(selectedFile);
};
function readFile(selectedFile) {
console.log('START READING FILE');
const reader = new FileReader();
reader.onload = (e) => {
const fileBase64 = reader.result.toString();
console.log('ONLOAD','base64', fileBase64);
if (fileBase64 === '') {
alert('Result string is EMPTY :(');
} else {
alert('It worked as expected :)');
}
};
reader.onprogress = (e) => {
console.log('Progress', ~~((e.loaded / e.total) * 100 ), '%');
};
reader.onerror = (err) => {
console.error('Error reading the file.', err);
};
reader.readAsDataURL(selectedFile);
}
<!doctype html>
<html lang="en">
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- Bootstrap CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0/dist/css/bootstrap.min.css" rel="stylesheet"
integrity="sha384-wEmeIV1mKuiNpC+IOBjI7aAzPcEZeedi5yW5f2yOq55WWLwNGmvvx4Um1vskeMj0" crossorigin="anonymous">
<title>FileReader issue example</title>
</head>
<body>
<div class="container">
<h1>FileReader issue example</h1>
<div class="card">
<div class="card-header">
Select File:
</div>
<div class="card-body">
<input type="file" id="filepicker" />
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0/dist/js/bootstrap.bundle.min.js"
integrity="sha384-p34f1UUtsS3wqzfto5wAAmdvj+osOnFyQFpp4Ua3gs/ZVWx6oOypYoCJhGGScy+8"
crossorigin="anonymous"></script>
<script src="main.js"></script>
</body>
</html>
这是一个部分解决方案,它将块中的 blob 转换为 base64 blob...将所有内容连接成一个 json blob,其中包含 json 的 pre/suffix 部分和 base64中间的块
将其保存为 blob 允许浏览器优化内存分配并在需要时将其卸载到磁盘。
您可以尝试将 chunkSize 更改为更大的值,浏览器喜欢在内存中保留较小的 blob 块(一个桶)
// get some dummy gradient file (blob)
var a=document.createElement("canvas"),b=a.getContext("2d"),c=b.createLinearGradient(0,0,3000,3000);a.width=a.height=3000;c.addColorStop(0,"red");c.addColorStop(1,"blue");b.fillStyle=c;b.fillRect(0,0,a.width,a.height);a.toBlob(main);
async function main (blob) {
var fr = new FileReader()
// Best to add 2 so it strips == from all chunks
// except from the last chunk
var chunkSize = (1 << 16) + 2
var pos = 0
var b64chunks = []
while (pos < blob.size) {
await new Promise(rs => {
fr.readAsDataURL(blob.slice(pos, pos + chunkSize))
fr.onload = () => {
const b64 = fr.result.split(',')[1]
// Keeping it as a blob allaws browser to offload memory to disk
b64chunks.push(new Blob([b64]))
rs()
}
pos += chunkSize
})
}
// How you concatinate all chunks to json is now up to you.
// this solution/answer is more of a guideline of what you need to do
// There are some ways to do it more automatically but here is the most
// simpliest form
// (fyi: this new blob won't create so much data in memory, it will only keep references points to other blobs locations)
const jsonBlob = new Blob([
'{"data": "', ...b64chunks, '"}'
], { type: 'application/json' })
/*
// strongly advice you to tell the api developers
// to add support for binary/file upload (multipart-formdata)
// base64 is roughly ~33% larger and streaming
// this data on the server to the disk is almost impossible
fetch('./upload-files-to-bad-json-only-api', {
method: 'POST',
body: jsonBlob
})
*/
// Just a test that it still works
//
// new Response(jsonBlob).json().then(console.log)
fetch('data:image/png;base64,' + await new Blob(b64chunks).text()).then(r => r.blob()).then(b => console.log(URL.createObjectURL(b)))
}
我避免制作 base64 += fr.result.split(',')[1]
和 JSON.stringify
因为 GiB 的数据很多而且 json 无论如何都不应该处理二进制数据
Is this a chrome bug?
正如我在对 Chrome, FileReader API, event.target.result === "" 的回答中所说,这是一个 V8(Chrome 但也有 node-js 和其他人的 JavaScript JS 引擎)限制.
这是故意的,因此不能真正定义为“错误”。
技术细节是,这里实际上失败的是在 64 位系统上构建超过 512MB(小于 header)的字符串,因为在 V8 中所有堆 objects 必须适合 Smi(小整数),(比照 this commit).
Why is there neither an error nor an exception?
那,可能是一个错误...正如我在链接的答案中也显示的那样,直接创建这样的字符串时我们会得到一个 RangeError:
const header = 24;
const bytes = new Uint8Array( (512 * 1024 * 1024) - header );
let txt = new TextDecoder().decode( bytes );
console.log( txt.length ); // 536870888
txt += "f"; // RangeError
并且在 FileReader::readOperation 的第 3 步中,UA 必须
If package data threw an exception error:
- Set fr’s error to error.
- Fire a progress event called error at fr.
但是在这里,我们没有那个错误。
const bytes = Uint32Array.from( { length: 600 * 1024 * 1024 / 4 }, (_) => Math.random() * 0xFFFFFFFF );
const blob = new Blob( [ bytes ] );
const fr = new FileReader();
fr.onerror = console.error;
fr.onload = (evt) => console.log( "success", fr.result.length, fr.error );
fr.readAsDataURL( blob );
我将打开一个关于此的问题,因为您应该能够处理来自 FileReader 的错误。
How can I fix or work around this issue?
最好的肯定是让你的 API end-point 直接接受二进制资源而不是 data:// URLs,无论如何都应该避免。
如果这不可行,“未来”的解决方案是 POST 一个 ReadableStream 到您的 end-point,然后进行 data:// URL 转换你自己,来自 Blob 的流。
class base64StreamEncoder {
constructor( header ) {
if( header ) {
this.header = new TextEncoder().encode( header );
}
this.tail = [];
}
transform( chunk, controller ) {
const encoded = this.encode( chunk );
if( this.header ) {
controller.enqueue( this.header );
this.header = null;
}
controller.enqueue( encoded );
}
encode( bytes ) {
let binary = Array.from( this.tail )
.reduce( (bin, byte) => bin + String.fromCharCode( byte ), "" );
const tail_length = bytes.length % 3;
const last_index = bytes.length - tail_length;
this.tail = bytes.subarray( last_index );
for( let i = 0; i<last_index; i++ ) {
binary += String.fromCharCode( bytes[ i ] );
}
const b64String = window.btoa( binary );
return new TextEncoder().encode( b64String );
}
flush( controller ) {
// force the encoding of the tail
controller.enqueue( this.encode( new Uint8Array() ) );
}
}
实例:https://base64streamencoder.glitch.me/
现在,您必须将 base64 表示的块存储在 Blob 中,如 Endless 的回答所示。
但是请注意,由于这是 V8 的限制,即使是 server-side 也可能会遇到这么大的字符串问题,所以无论如何,您应该联系 API 的维护者。
目标:
- 在浏览器中,从用户文件系统读取一个文件作为base64字符串
- 这些文件最大为 1.5GB
问题:
- 以下脚本在 Firefox 上运行良好。无论文件大小如何。
- 在 Chrome 上,该脚本适用于较小的文件(我测试过大小约为 5MB 的文件)
- 如果您选择一个更大的文件(例如 400MB),FileReader 会在没有错误或异常的情况下完成,但是returns一个空字符串而不是 base64 字符串
问题:
- 这是 chrome 错误吗?
- 为什么既没有错误也没有异常?
- 我该如何解决或解决这个问题?
重要提示:
请注意,分块 对我来说不是一个选项,因为我需要通过 'POST' 将完整的 base64 字符串发送到 API不支持块。
代码:
'use strict';
var filePickerElement = document.getElementById('filepicker');
filePickerElement.onchange = (event) => {
const selectedFile = event.target.files[0];
console.log('selectedFile', selectedFile);
readFile(selectedFile);
};
function readFile(selectedFile) {
console.log('START READING FILE');
const reader = new FileReader();
reader.onload = (e) => {
const fileBase64 = reader.result.toString();
console.log('ONLOAD','base64', fileBase64);
if (fileBase64 === '') {
alert('Result string is EMPTY :(');
} else {
alert('It worked as expected :)');
}
};
reader.onprogress = (e) => {
console.log('Progress', ~~((e.loaded / e.total) * 100 ), '%');
};
reader.onerror = (err) => {
console.error('Error reading the file.', err);
};
reader.readAsDataURL(selectedFile);
}
<!doctype html>
<html lang="en">
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- Bootstrap CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0/dist/css/bootstrap.min.css" rel="stylesheet"
integrity="sha384-wEmeIV1mKuiNpC+IOBjI7aAzPcEZeedi5yW5f2yOq55WWLwNGmvvx4Um1vskeMj0" crossorigin="anonymous">
<title>FileReader issue example</title>
</head>
<body>
<div class="container">
<h1>FileReader issue example</h1>
<div class="card">
<div class="card-header">
Select File:
</div>
<div class="card-body">
<input type="file" id="filepicker" />
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0/dist/js/bootstrap.bundle.min.js"
integrity="sha384-p34f1UUtsS3wqzfto5wAAmdvj+osOnFyQFpp4Ua3gs/ZVWx6oOypYoCJhGGScy+8"
crossorigin="anonymous"></script>
<script src="main.js"></script>
</body>
</html>
这是一个部分解决方案,它将块中的 blob 转换为 base64 blob...将所有内容连接成一个 json blob,其中包含 json 的 pre/suffix 部分和 base64中间的块
将其保存为 blob 允许浏览器优化内存分配并在需要时将其卸载到磁盘。
您可以尝试将 chunkSize 更改为更大的值,浏览器喜欢在内存中保留较小的 blob 块(一个桶)
// get some dummy gradient file (blob)
var a=document.createElement("canvas"),b=a.getContext("2d"),c=b.createLinearGradient(0,0,3000,3000);a.width=a.height=3000;c.addColorStop(0,"red");c.addColorStop(1,"blue");b.fillStyle=c;b.fillRect(0,0,a.width,a.height);a.toBlob(main);
async function main (blob) {
var fr = new FileReader()
// Best to add 2 so it strips == from all chunks
// except from the last chunk
var chunkSize = (1 << 16) + 2
var pos = 0
var b64chunks = []
while (pos < blob.size) {
await new Promise(rs => {
fr.readAsDataURL(blob.slice(pos, pos + chunkSize))
fr.onload = () => {
const b64 = fr.result.split(',')[1]
// Keeping it as a blob allaws browser to offload memory to disk
b64chunks.push(new Blob([b64]))
rs()
}
pos += chunkSize
})
}
// How you concatinate all chunks to json is now up to you.
// this solution/answer is more of a guideline of what you need to do
// There are some ways to do it more automatically but here is the most
// simpliest form
// (fyi: this new blob won't create so much data in memory, it will only keep references points to other blobs locations)
const jsonBlob = new Blob([
'{"data": "', ...b64chunks, '"}'
], { type: 'application/json' })
/*
// strongly advice you to tell the api developers
// to add support for binary/file upload (multipart-formdata)
// base64 is roughly ~33% larger and streaming
// this data on the server to the disk is almost impossible
fetch('./upload-files-to-bad-json-only-api', {
method: 'POST',
body: jsonBlob
})
*/
// Just a test that it still works
//
// new Response(jsonBlob).json().then(console.log)
fetch('data:image/png;base64,' + await new Blob(b64chunks).text()).then(r => r.blob()).then(b => console.log(URL.createObjectURL(b)))
}
我避免制作 base64 += fr.result.split(',')[1]
和 JSON.stringify
因为 GiB 的数据很多而且 json 无论如何都不应该处理二进制数据
Is this a chrome bug?
正如我在对 Chrome, FileReader API, event.target.result === "" 的回答中所说,这是一个 V8(Chrome 但也有 node-js 和其他人的 JavaScript JS 引擎)限制.
这是故意的,因此不能真正定义为“错误”。
技术细节是,这里实际上失败的是在 64 位系统上构建超过 512MB(小于 header)的字符串,因为在 V8 中所有堆 objects 必须适合 Smi(小整数),(比照 this commit).
Why is there neither an error nor an exception?
那,可能是一个错误...正如我在链接的答案中也显示的那样,直接创建这样的字符串时我们会得到一个 RangeError:
const header = 24;
const bytes = new Uint8Array( (512 * 1024 * 1024) - header );
let txt = new TextDecoder().decode( bytes );
console.log( txt.length ); // 536870888
txt += "f"; // RangeError
并且在 FileReader::readOperation 的第 3 步中,UA 必须
If package data threw an exception error:
- Set fr’s error to error.
- Fire a progress event called error at fr.
但是在这里,我们没有那个错误。
const bytes = Uint32Array.from( { length: 600 * 1024 * 1024 / 4 }, (_) => Math.random() * 0xFFFFFFFF );
const blob = new Blob( [ bytes ] );
const fr = new FileReader();
fr.onerror = console.error;
fr.onload = (evt) => console.log( "success", fr.result.length, fr.error );
fr.readAsDataURL( blob );
我将打开一个关于此的问题,因为您应该能够处理来自 FileReader 的错误。
How can I fix or work around this issue?
最好的肯定是让你的 API end-point 直接接受二进制资源而不是 data:// URLs,无论如何都应该避免。
如果这不可行,“未来”的解决方案是 POST 一个 ReadableStream 到您的 end-point,然后进行 data:// URL 转换你自己,来自 Blob 的流。
class base64StreamEncoder {
constructor( header ) {
if( header ) {
this.header = new TextEncoder().encode( header );
}
this.tail = [];
}
transform( chunk, controller ) {
const encoded = this.encode( chunk );
if( this.header ) {
controller.enqueue( this.header );
this.header = null;
}
controller.enqueue( encoded );
}
encode( bytes ) {
let binary = Array.from( this.tail )
.reduce( (bin, byte) => bin + String.fromCharCode( byte ), "" );
const tail_length = bytes.length % 3;
const last_index = bytes.length - tail_length;
this.tail = bytes.subarray( last_index );
for( let i = 0; i<last_index; i++ ) {
binary += String.fromCharCode( bytes[ i ] );
}
const b64String = window.btoa( binary );
return new TextEncoder().encode( b64String );
}
flush( controller ) {
// force the encoding of the tail
controller.enqueue( this.encode( new Uint8Array() ) );
}
}
实例:https://base64streamencoder.glitch.me/
现在,您必须将 base64 表示的块存储在 Blob 中,如 Endless 的回答所示。
但是请注意,由于这是 V8 的限制,即使是 server-side 也可能会遇到这么大的字符串问题,所以无论如何,您应该联系 API 的维护者。