How to save an uploaded image to FastAPI using Python Imaging Library (PIL)?

Question:

I am using image compression to reduce the image size. When submitting the post request, I am not getting any error, but can’t figure out why the images do not get saved. Here is my code:

@app.post("/post_ads")
async def create_upload_files(title: str = Form(),body: str = Form(), 
    db: Session = Depends(get_db), files: list[UploadFile] = File(description="Multiple files as UploadFile")):
    for file in files:
        im = Image.open(file.file)
        im = im.convert("RGB")
        im_io = BytesIO()
        im = im.save(im_io, 'JPEG', quality=50) 
Asked By: hawaj

||

Answers:

PIL.Image.open() takes as fp argumnet the following:

fp – A filename (string), pathlib.Path object or a file object. The
file object must implement file.read(), file.seek(), and
file.tell() methods, and be opened in binary mode.

Using a BytesIO stream, you would need to have something like the below (as shown in client side of this answer):

Image.open(io.BytesIO(file.file.read()))

However, you don’t really have to use an in-memory bytes buffer, as you can get the the actual file object using the .file attribute of UploadFile. As per the documentation:

file: A SpooledTemporaryFile (a file-like object).
This is the actual Python file that you can pass directly to other
functions or libraries that expect a "file-like" object.

Example – Saving image to disk:

# ...
from fastapi import HTTPException
from PIL import Image

@app.post("/upload")
def upload(file: UploadFile = File()):
    try:        
        im = Image.open(file.file)
        if im.mode in ("RGBA", "P"): 
            im = im.convert("RGB")
        im.save('out.jpg', 'JPEG', quality=50) 
    except Exception:
        raise HTTPException(status_code=500, detail='Something went wrong')
    finally:
        file.file.close()
        im.close()

Example – Saving image to an in-memory bytes buffer (see this answer):

# ...
from fastapi import HTTPException
from PIL import Image

@app.post("/upload")
def upload(file: UploadFile = File()):
    try:        
        im = Image.open(file.file)
        if im.mode in ("RGBA", "P"): 
            im = im.convert("RGB")
        buf = io.BytesIO()
        im.save(buf, 'JPEG', quality=50)
        # to get the entire bytes of the buffer use:
        contents = buf.getvalue()
        # or, to read from `buf` (which is a file-like object), call this first:
        buf.seek(0)  # to rewind the cursor to the start of the buffer
    except Exception:
        raise HTTPException(status_code=500, detail='Something went wrong')
    finally:
        file.file.close()
        buf.close()
        im.close()

For more details and code examples on how to upload files/images using FastAPI, please have a look at this answer and this answer. Also, please have a look at this answer for more information on defining your endpoint with def or async def.

Answered By: Chris

I assume you are writing to a BytesIO to get an "in memory" JPEG without slowing yourself down by writing to disk and cluttering your filesystem.

If so, you want:

from PIL import Image
from io import BytesIO

im = Image.open(file.file)
im = im.convert("RGB")
im_io = BytesIO()
# create in-memory JPEG in RAM (not disk)
im.save(im_io, 'JPEG', quality=50)

# get the JPEG image in a variable called JPEG
JPEG = im_io.get_value()
Answered By: Mark Setchell