将 Blob 反应为通过节点端点上传文件 - 文件已损坏

React Blob to File upload via Node endpoint - file is corrupt

我已经构建了一个 Slack 风格的头像图像上传和裁剪功能,但我无法在不损坏的情况下保存裁剪后的图像。使用相同端点和方法的原始文件上传工作正常。这是从 Blob 手动创建的裁剪副本,它总是损坏的。

步骤非常简单:

  1. Select 并上传图像文件
  2. 裁剪(react-image-crop)出现
  3. Select 区域,点击保存

第一步,文件输入改变时上传文件。文件被发送到流端点,该端点将文件上传到 s3 存储桶。

<button type="button">Upload</button>
<input 
    type="file" 
    accept="image/*"
    onChange={onFileChange} 
/>

...

const onFileChange = async e => {
    e.preventDefault();
    let uploadedFile = e.target.files[0];
    await onSave(uploadedFile);
};

uploadedFile 变量是从输入控件返回的文件对象。这很好用!还没有问题。

在第 3 步中,一旦您选择了图像的一个区域,就会通过 react-image-crop 生成一个 Blob。

const getCroppedImage = (source, config) => {
    const canvas = document.createElement("canvas");
    const scaleX = source.naturalWidth / source.width;
    const scaleY = source.naturalHeight / source.height;
    canvas.width = config.width;
    canvas.height = config.height;
    const ctx = canvas.getContext("2d");

    ctx.drawImage(
        source,
        config.x * scaleX,
        config.y * scaleY,
        config.width * scaleX,
        config.height * scaleY,
        0,
        0,
        config.width,
        config.height
    );

    let mimeType = mime.lookup(userProfile.image_file.split(".").at(-1));

    return new Promise((resolve, reject) => {
        canvas.toBlob(blob => {
            if (!blob) {
                reject(new Error("Canvas is empty"));
                return;
            }
            resolve(blob); //***THIS BLOB...
        }, mimeType);
    });
};

这个Blob是有效的,因为我在保存之前在屏幕上显示了选择的区域:

const AvatarPreview = () => {
    if (activeAvatar) {
        return <ImageCropper imageToCrop={activeAvatar} onImageCropped={onImageCropped} />;
    }
    return <Icon icon="bi:person" />;
};

我将 react-image-crop 生成的 Blob 填充到一个 File 对象中,因为这是我的代码所期望的,就像第 1 步一样。

const onImageCropped = croppedBlob => { //***IS PASSED IN HERE
    let croppedImg = URL.createObjectURL(croppedBlob);
    setActiveAvatar(croppedImg);
    const reader = new FileReader();
    reader.readAsDataURL(croppedBlob);
    reader.addEventListener("load", () => {
        let { result } = reader;
        let resultMimeType = result.split(";")[0].split(":")[1];
        let croppedFile = new File([result], userProfile.image_file, { type: resultMimeType }); //***NEW File object, from Blob
        setCroppedAvatar(croppedFile);
    }, false);
};

<button type="button" onClick={() => onSave(croppedAvatar)}>Save</button>

上面FileReader加载的“结果”是base64图片数据:

'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/4gIoSUNDX1BST0ZJTEUAAQEAAAIYAAAAAAQwAABtbnRyUkdCIFhZWiAAAAAAAAAAAAAAAABhY3NwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAA9tYAAQAAAADTLQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAlkZXNjAAAA8AAAAHRyWFlaAAABZAAAABRnWFlaAAABeAAAABRiWFlaAAABjAAAABRyVFJDAAABoAAAAChnVFJDAAABoAAAAChiVFJDAAABoAAAACh3dHB0AAAByAAAABRjcHJ0AAAB3AAAADxtbHVjAAAAAAAAAAEAAAAMZW5VUwAAAFgAAAAcAHMAUgBHAEIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA…QCMFWeFZ9u3fPsWY3hh9uVHWllbSwtGASrIOQc1t6rt0OBqG6x7aVfdY859pnIwfDDhCfpwBUyaSus4VZO+susGGM60zrB1TuEjx5Ov76FBKUgNTltIQlP4UpQghKQMAAAADHApt6lv1x1HeHrzdny9KkJbDrpHLhQhKNx91EJBJ9SSaQkLS+4HTwvalKvmQMZ/PFa8hBCRntnirYUDqVYtEWlQ8JYJSrv8vnTy6W2h5/qBYmCgqbcmtgLSMg+YUyYD4aUUqTlJ4I+td+FLethRhai0sgoUOCg+hFPBwcxjruUie1k/VDOmenKkIcS2iJCzyeAAmvF7qjqFzVWurzfHV7jJluKBJ/hBwP6VL1u+KvqlD0lL0VeLt+2rZLjmO25MyqSwNuPK7nKh8l7vlioFejqU8tx453EnIPcmrWrvW5VFf6zK0GifTWs1n6TRoUrIaDZBSeDSVUzyMTY7gBIORR/EcXwTnFEozfc0xVwOYT/2Q=='

新的 File 对象对我来说似乎是合法的:

然后我再次通过端点发送图像,并上传。它出现在存储桶中,文件大小似乎是合法的(不是像断流所指示的 0KB。)

但是,在下载并尝试打开文件时,文件已损坏。我想我在某处遗漏了一个选项......一些小的调整可以使这项工作?文件对象的格式不正确吗?我该如何进一步解决这个问题?

已修复。当然,只需一行代码即可使一切正常。我所要做的就是改变 FileReader 加载结果的方式:

const onImageCropped = async croppedBlob => {
    let croppedImg = URL.createObjectURL(croppedBlob);
    let mimeType = croppedBlob.type;
    setActiveAvatar(croppedImg);
    let reader = new FileReader();
    reader.readAsArrayBuffer(croppedBlob); //Fixed the issue
    reader.addEventListener("load", () => {
        let { result } = reader;
        let croppedFile = new File([result], userProfile.image_file, { type: mimeType });
        setCroppedAvatar(croppedFile);
    }, false);
};

无论如何,结果更优雅。改为使用 FileReader 的 readAsArrayBuffer。我猜在使用 readAsDataUrl 时缓冲区中有额外的信息,破坏了图像。