92

Using the python module fastAPI, I can't figure out how to return an image. In flask I would do something like this:

@app.route("/vector_image", methods=["POST"])
def image_endpoint():
    # img = ... # Create the image here
    return Response(img, mimetype="image/png")

what's the corresponding call in this module?

Hooked
  • 84,485
  • 43
  • 192
  • 261

10 Answers10

97

If you already have the bytes of the image in memory

Return a fastapi.responses.Response with your custom content and media_type.

You'll also need to muck with the endpoint decorator to get FastAPI to put the correct media type in the OpenAPI specification.

@app.get(
    "/image",

    # Set what the media type will be in the autogenerated OpenAPI specification.
    # fastapi.tiangolo.com/advanced/additional-responses/#additional-media-types-for-the-main-response
    responses = {
        200: {
            "content": {"image/png": {}}
        }
    },

    # Prevent FastAPI from adding "application/json" as an additional
    # response media type in the autogenerated OpenAPI specification.
    # https://github.com/tiangolo/fastapi/issues/3258
    response_class=Response
)
def get_image()
    image_bytes: bytes = generate_cat_picture()
    # media_type here sets the media type of the actual response sent to the client.
    return Response(content=image_bytes, media_type="image/png")

See the Response documentation.

If your image exists only on the filesystem

Return a fastapi.responses.FileResponse.

See the FileResponse documentation.


Be careful with StreamingResponse

Other answers suggest StreamingResponse. StreamingResponse is harder to use correctly, so I don't recommend it unless you're sure you can't use Response or FileResponse.

In particular, code like this is pointless. It will not "stream" the image in any useful way.

@app.get("/image")
def get_image()
    image_bytes: bytes = generate_cat_picture()
    # ❌ Don't do this.
    image_stream = io.BytesIO(image_bytes)
    return StreamingResponse(content=image_stream, media_type="image/png")

First of all, StreamingResponse(content=my_iterable) streams by iterating over the chunks provided by my_iterable. But when that iterable is a BytesIO, the chunks will be \n-terminated lines, which won't make sense for a binary image.

And even if the chunk divisions made sense, chunking is pointless here because we had the whole image_bytes bytes object available from the start. We may as well have just passed the whole thing into a Response from the beginning. We don't gain anything by holding data back from FastAPI.

Second, StreamingResponse corresponds to HTTP chunked transfer encoding. (This might depend on your ASGI server, but it's the case for Uvicorn, at least.) And this isn't a good use case for chunked transfer encoding.

Chunked transfer encoding makes sense when you don't know the size of your output ahead of time, and you don't want to wait to collect it all to find out before you start sending it to the client. That can apply to stuff like serving the results of slow database queries, but it doesn't generally apply to serving images.

Unnecessary chunked transfer encoding can be harmful. For example, it means clients can't show progress bars when they're downloading the file. See:

phreakhead
  • 14,721
  • 5
  • 39
  • 40
Maxpm
  • 24,113
  • 33
  • 111
  • 170
  • 2
    Good answer, however with this, the OpenAPI document will still list `application/json` as a possible 200 response, in addition to `image/png`. It even lists this first, so it's the first possible response shown in the generated docs. Do you know how to make it only list `image/png`? See also my question about this in https://github.com/tiangolo/fastapi/issues/3258 – estan May 21 '21 at 07:29
  • 2
    @estan Good catch. It looks like you've already found a solution in that GitHub issue. I have an alternative approach; I've replied to that GitHub issue with it and added it to my answer here. – Maxpm May 21 '21 at 19:11
  • 3
    No StreamingResponse does not correspond to chunked encoding. FastAPI/starlette are not in control of this as per the WSGI specification ([see "Handling the Content-Length Header"](https://www.python.org/dev/peps/pep-0333/#handling-the-content-length-header)). Other response classes set the `Content-Length` header for you. The StreamingResponse doesn't. `StreamingResponse(content, headers={'Content-Length': str(content_length)})` is unlikely to be chunked. To the server (uvicorn), this would look the same as any other static response. – Philip Couling Aug 06 '21 at 09:07
  • 1
    @PhilipCouling "Corresponds" is maybe the wrong word, yeah. Would something like "`StreamingResponse()` is likely to be handled by the server with chunked transfer encoding" be better? – Maxpm Aug 06 '21 at 23:36
  • @Maxpm no I would actually make it clear in the answer clear that you need to set the content-length header manually (or it will likely be chunked). That's the fundamental issue you're referencing. There's also another issue with the accepted answer. There's a pretty big trip hazard passing back a file object. The api doesn't close it. So it's fine with a BytesIO but not any real file objects. – Philip Couling Aug 07 '21 at 07:13
  • 1
    Man, i faced this choice few days ago. First of all i did as in your example "pointless StreamingResponse ". I noticed that TTFB is not good and i got some problems when trying to send files more than 50 MB - it was very slow. After that i came to Response variant. It works great couse my servece send files 50 - 200 KB. Your post gave me a lot of useful information. Thanks! – Alpensin Feb 26 '22 at 14:31
  • I faced this issue as well and your answer helped a lot, this worked for me ```python image = np.ones((800, 800, 3)) encoded_jpg = cv2.imencode(".jpg", image)[1] return Response(encoded_jpg.tobytes(), media_type="image/jpeg") ``` – 24dinitrophenylhydrazine Jun 02 '22 at 07:47
  • @Maxpm I tried your solution to return an image. But it threw `AttributeError: '_io.BytesIO' object has no attribute 'encode'` But this was resolved by using StreamingResponse. Am I missing something? – MSS Jun 18 '22 at 11:34
  • @MSS Could you post a [minimal reproducible example](https://stackoverflow.com/help/minimal-reproducible-example) somewhere and either open a new question or a new chatroom for us to investigate that? – Maxpm Jun 21 '22 at 18:16
69

I had a similar issue but with a cv2 image. This may be useful for others. Uses the StreamingResponse.

import io
from starlette.responses import StreamingResponse

app = FastAPI()

@app.post("/vector_image")
def image_endpoint(*, vector):
    # Returns a cv2 image array from the document vector
    cv2img = my_function(vector)
    res, im_png = cv2.imencode(".png", cv2img)
    return StreamingResponse(io.BytesIO(im_png.tobytes()), media_type="image/png")
biophetik
  • 989
  • 8
  • 5
  • Thanks! I think this is a much better answer than my hack that required a temporary file. – Hooked Jan 06 '20 at 20:05
  • 15
    If you're using `BytesIO` especially with PIL/skimage, make sure to also do `img.seek(0)` before returning! – Hendy Irawan Apr 16 '20 at 04:49
  • 2
    This also works very well for returning GridFS objects ex: `val = grid_fs_file.read()` `return StreamingResponse(io.BytesIO(val), media_type="application/pdf")` Thank you very much! – BrettJ Dec 26 '20 at 08:32
  • 5
    Things might have changed since this answer was written, but the use of `StreamingResponse` in this answer seems wrong today. See [my answer](https://stackoverflow.com/a/67497103/497934). – Maxpm May 12 '21 at 03:52
  • 1
    @HendyIrawan Why it's important to use img.seek(0)? – Alpensin Feb 26 '22 at 14:25
40

All the other answer(s) is on point, but now it's so easy to return an image

from fastapi.responses import FileResponse

@app.get("/")
async def main():
    return FileResponse("your_image.jpeg")
Yagiz Degirmenci
  • 16,595
  • 7
  • 65
  • 85
35

It's not properly documented yet, but you can use anything from Starlette.

So, you can use a FileResponse if it's a file in disk with a path: https://www.starlette.io/responses/#fileresponse

If it's a file-like object created in your path operation, in the next stable release of Starlette (used internally by FastAPI) you will also be able to return it in a StreamingResponse.

tiangolo
  • 1,536
  • 1
  • 15
  • 29
  • 4
    Thanks for the response! I got it to work with your suggestion but it wasn't easy (and probably overkill!). See my solution below. Other than this issue, fastAPI was a pleasure to work with a very nicely documented, thanks for providing it! – Hooked Apr 29 '19 at 14:05
  • 3
    I also created a tag for your library in the question. Feel free to edit it, and "watch it" so you can see questions from other users. – Hooked Apr 29 '19 at 14:08
18

Thanks to @biophetik's answer, with an important reminder that caused me confusion: If you're using BytesIO especially with PIL/skimage, make sure to also do img.seek(0) before returning!

@app.get("/generate")
def generate(data: str):
  img = generate_image(data)
  print('img=%s' % (img.shape,))
  buf = BytesIO()
  imsave(buf, img, format='JPEG', quality=100)
  buf.seek(0) # important here!
  return StreamingResponse(buf, media_type="image/jpeg",
    headers={'Content-Disposition': 'inline; filename="%s.jpg"' %(data,)})
Hendy Irawan
  • 20,498
  • 11
  • 103
  • 114
13

The answer from @SebastiánRamírez pointed me in the right direction, but for those looking to solve the problem, I needed a few lines of code to make it work. I needed to import FileResponse from starlette (not fastAPI?), add CORS support, and return from a temporary file. Perhaps there is a better way, but I couldn't get streaming to work:

from starlette.responses import FileResponse
from starlette.middleware.cors import CORSMiddleware
import tempfile

app = FastAPI()
app.add_middleware(
    CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"]
)

@app.post("/vector_image")
def image_endpoint(*, vector):
    # Returns a raw PNG from the document vector (define here)
    img = my_function(vector)

    with tempfile.NamedTemporaryFile(mode="w+b", suffix=".png", delete=False) as FOUT:
        FOUT.write(img)
        return FileResponse(FOUT.name, media_type="image/png")
Davide Fiocco
  • 5,350
  • 5
  • 35
  • 72
Hooked
  • 84,485
  • 43
  • 192
  • 261
  • 2
    could you be more specific please? like where is the file name? what is the Item, where is the route? – Peko Chan Aug 11 '19 at 18:05
  • 2
    @PekoChan You're right, I was missing some parts. I was trying to adapt the code I actually used to a minimal example. I made it a bit too minimal, hopefully I've fixed it. – Hooked Aug 11 '19 at 21:57
6

My needs weren't quite met from the above because my image was built with PIL. My fastapi endpoint takes an image file name, reads it as a PIL image, and generates a thumbnail jpeg in memory that can be used in HTML like:

<img src="http://localhost:8000/images/thumbnail/bigimage.jpg">

import io
from PIL import Image
from fastapi.responses import StreamingResponse
@app.get('/images/thumbnail/{filename}',
  response_description="Returns a thumbnail image from a larger image",
  response_class="StreamingResponse",
  responses= {200: {"description": "an image", "content": {"image/jpeg": {}}}})
def thumbnail_image (filename: str):
  # read the high-res image file
  image = Image.open(filename)
  # create a thumbnail image
  image.thumbnail((100, 100))
  imgio = io.BytesIO()
  image.save(imgio, 'JPEG')
  imgio.seek(0)
  return StreamingResponse(content=imgio, media_type="image/jpeg")
dam
  • 353
  • 1
  • 7
  • 19
3

You can use a FileResponse if it's a file in disk with a path:

import os

from fastapi import FastAPI 
from fastapi.responses import FileResponse

app = FastAPI()

path = "/path/to/files"

@app.get("/")
def index():
    return {"Hello": "World"}

@app.get("/vector_image", responses={200: {"description": "A picture of a vector image.", "content" : {"image/jpeg" : {"example" : "No example available. Just imagine a picture of a vector image."}}}})
def image_endpoint():
    file_path = os.path.join(path, "files/vector_image.jpg")
    if os.path.exists(file_path):
        return FileResponse(file_path, media_type="image/jpeg", filename="vector_image_for_you.jpg")
    return {"error" : "File not found!"}
Milovan Tomašević
  • 6,823
  • 1
  • 50
  • 42
3

If when following the top answer and you are attempting to return a BytesIO object like this in your Response

    buffer = BytesIO(my_data)

    # Return file
    return Response(content=buffer, media_type="image/jpg")

You may receive an error that looks like this (as described in this comment)

AttributeError: '_io.BytesIO' object has no attribute 'encode'

This is caused by the render function in Response which explicitly checks for a bytes type here. Since BytesIO != bytes it attempts to encode the value and fails.

The solution is to get the bytes value from the BytesIO object with getvalue()

    buffer = BytesIO(my_data)

    # Return file
    return Response(content=buffer.getvalue(), media_type="image/jpg")
1

You can do something very similar in FastAPI

from fastapi import FastAPI, Response

app = FastAPI()

@app.post("/vector_image/")
async def image_endpoint():
    # img = ... # Create the image here
    return Response(content=img, media_type="image/png")
Jibin Mathew
  • 4,816
  • 4
  • 40
  • 68