如何将Base64编码的图像发送到FastAPI后端?

3

我正在使用这个那个答案中的代码将一个base64编码的图像发送到Python FastAPI后端。

客户端代码如下:

function toDataURL(src, callback, outputFormat) {
            var img = new Image();
            img.crossOrigin = 'Anonymous';
            img.onload = function() {
                var canvas = document.createElement('CANVAS');
                var ctx = canvas.getContext('2d');
                var dataURL;
                canvas.height = this.naturalHeight;
                canvas.width = this.naturalWidth;
                ctx.drawImage(this, 0, 0);
                dataURL = canvas.toDataURL(outputFormat);
                callback(dataURL);
            };
            img.src = src;
            if (img.complete || img.complete === undefined) {
                img.src = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///ywAAAAAAQABAAACAUwAOw==";
                img.src = src;
            }
        }

        function makeBlob(dataURL) {
            var BASE64_MARKER = ';base64,';
            if (dataURL.indexOf(BASE64_MARKER) == -1) {
                var parts = dataURL.split(',');
                var contentType = parts[0].split(':')[1];
                var raw = decodeURIComponent(parts[1]);
                return new Blob([raw], { type: contentType });
            }
            var parts = dataURL.split(BASE64_MARKER);
            var contentType = parts[0].split(':')[1];
            var raw = window.atob(parts[1]);
            var rawLength = raw.length;

            var uInt8Array = new Uint8Array(rawLength);

            for (var i = 0; i < rawLength; ++i) {
                uInt8Array[i] = raw.charCodeAt(i);
            }

            return new Blob([uInt8Array], { type: contentType });
        }

        ...

        toDataURL(
            images[0], // images is an array of paths to images
            function(dataUrl) {
                console.log('RESULT:', dataUrl);

                $.ajax({
                    url: "http://0.0.0.0:8000/check/",
                    type: 'POST',
                    processData: false,
                    contentType: 'application/octet-stream',
                    data: makeBlob(dataUrl)
                }).done(function(data) {console.log("success");}).fail(function() {console.log("error");});
            }
        );

服务器端如下:

@app.post("/check")
async def check(file: bytes = File(...)) -> Any:  
    // do something here

我只展示了端点的签名,因为目前它并没有发生太多事情。

这是当我按照上面所示的方式调用后端时的输出:

172.17.0.1:36464 - "OPTIONS /check/ HTTP/1.1" 200

172.17.0.1:36464 - "POST /check/ HTTP/1.1" 307

172.17.0.1:36464 - "OPTIONS /check HTTP/1.1" 200

172.17.0.1:36464 - "POST /check HTTP/1.1" 422

因此,简而言之,我不断收到422错误代码,这意味着我发送的内容与端点期望的内容不匹配,但是即使经过一些阅读,我仍然不清楚到底出了什么问题。任何帮助都将不胜感激!


针对故障隔离:当您使用TypedArray的缓冲区而不是TypedArray本身时会发生什么?像这样:new Blob(uInt8Array.buffer, { type: contentType }); 而不是这个 new Blob([uInt8Array], { type: contentType }); - Randy Casburn
你尝试过将文件参数的类型设置为 UploadFile 吗?https://fastapi.tiangolo.com/tutorial/request-files/?h=+file#file-parameters-with-uploadfile - lsabi
@RandyCasburn 如果我尝试那个,会出现以下错误:Uncaught TypeError: Blob constructor: Argument 1 can't be converted to a sequence. @lsabi 是的,我也尝试了,完全相同的行为!这让我觉得我的JavaScript发送的内容既不被识别为文件,也不是一个正确的字节流,这有意义吗? - jerorx
我无法使用您的确切代码和已知的良好图像重新创建问题。最有可能的原因是您服务器上的图像损坏或图像路径指向非图像文件(如伪装成图像的文本文件)。 - Randy Casburn
换句话说,你的代码按照原样能够正常运行(即使你没有提供toDataURL()方法的第三个参数,并且直接在代码中使用了未定义的第三个参数)。 - Randy Casburn
1个回答

1
该答案所述,上传的文件是作为form数据发送的。根据FastAPI文档

当不包含文件时,表单数据通常使用“媒体类型”application/x-www-form-urlencoded进行编码。

但是当表单包含文件时,它被编码为multipart/form-data。如果你使用File,FastAPI将知道它必须从正确的部分获取文件。

无论你使用的是bytes还是UploadFile,由于...

如果你将路径操作函数参数的类型声明为bytesFastAPI将为你读取文件,并将内容作为字节接收。

因此,出现了422 Unprocessable entity错误。在你的示例中,你使用application/octet-stream作为content-type来发送二进制数据,然而,你的API端点期望form数据(即multipart/form-data)。

选项1

不要发送base64编码的图像,而是将file原样上传,可以使用HTML form(如此处所示)或Javascript。正如其他人指出的,当使用JQuery时,必须将contentType选项设置为false。使用Fetch API,最好也不要设置它,并强制浏览器设置它(以及必需的multipart boundary)。有关FastAPI后端中的异步读/写,请查看此答案

app.py:

import uvicorn
from fastapi import File, UploadFile, Request, FastAPI
from fastapi.templating import Jinja2Templates

app = FastAPI()
templates = Jinja2Templates(directory="templates")

@app.post("/upload")
def upload(file: UploadFile = File(...)):
    try:
        contents = file.file.read()
        with open("uploaded_" + file.filename, "wb") as f:
            f.write(contents)
    except Exception:
        return {"message": "There was an error uploading the file"}
    finally:
        file.file.close()
        
    return {"message": f"Successfuly uploaded {file.filename}"}

@app.get("/")
def main(request: Request):
    return templates.TemplateResponse("index.html", {"request": request})

templates/index.html

<script>
function uploadFile(){
    var file = document.getElementById('fileInput').files[0];
    if(file){
        var formData = new FormData();
        formData.append('file', file);
        fetch('/upload', {
               method: 'POST',
               body: formData,
             })
             .then(response => {
               console.log(response);
             })
             .catch(error => {
               console.error(error);
             });
    }
}
</script>
<input type="file" id="fileInput" name="file"><br>
<input type="button" value="Upload File" onclick="uploadFile()">

如果您想使用Axios库进行上传,请参考这个答案
选项2
如果您仍需要上传base64编码的图像,可以将数据作为form数据发送,使用application/x-www-form-urlencoded作为content-type;在API端点中,您可以定义一个Form字段来接收数据。下面是一个完整的工作示例,其中发送了一个base64编码的图像,由服务器接收、解码并保存到磁盘。对于base64编码,客户端使用了readAsDataURL方法。请注意,文件写入磁盘是使用同步写入完成的。在需要保存多个(或大型)文件的情况下,最好使用异步写入,如此处所述。 app.py
from fastapi import Form, Request, FastAPI
from fastapi.templating import Jinja2Templates
import base64

app = FastAPI()
templates = Jinja2Templates(directory="templates")

@app.post("/upload")
def upload(filename: str = Form(...), filedata: str = Form(...)):
    image_as_bytes = str.encode(filedata)  # convert string to bytes
    img_recovered = base64.b64decode(image_as_bytes)  # decode base64string
    with open("uploaded_" + filename, "wb") as f:
        f.write(img_recovered)
    return {"message": f"Successfuly uploaded {filename}"}

@app.get("/")
def main(request: Request):
    return templates.TemplateResponse("index.html", {"request": request})

templates/index.html

<script type="text/javascript">
    function previewFile() {
        const preview = document.querySelector('img');
        const file = document.querySelector('input[type=file]').files[0];
        const reader = new FileReader();
        reader.addEventListener("load", function () { 
            preview.src = reader.result; //show image in <img tag>
            base64String = reader.result.replace("data:", "").replace(/^.+,/, "");
            uploadFile(file.name, base64String)
        }, false);

        if (file) {
            reader.readAsDataURL(file);
        }
    }
    function uploadFile(filename, filedata){
        var formData = new FormData();
        formData.append("filename", filename);
        formData.append("filedata", filedata);
         fetch('/upload', {
               method: 'POST',
               body: formData,
             })
             .then(response => {
               console.log(response);
             })
             .catch(error => {
               console.error(error);
             });
      }

</script>
<input type="file" onchange="previewFile()"><br>
<img src="" height="200" alt="Image preview...">

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接