How to generate a PNG image in PIL and display it in Jinja2 template using FastAPI?

Question:

I have a FastAPI endpoint that is generating PIL images. I want to then send the resulting image as a stream to a Jinja2 TemplateResponse. This is a simplified version of what I am doing:

import io
from PIL import Image

@api.get("/test_image", status_code=status.HTTP_200_OK)
def test_image(request: Request):
    '''test displaying an image from a stream.
    '''
    test_img = Image.new('RGBA', (300,300), (0, 255, 0, 0))

    # I've tried with and without this:
    test_img = test_img.convert("RGB")

    test_img = test_img.tobytes()
    base64_encoded_image = base64.b64encode(test_img).decode("utf-8")

    return templates.TemplateResponse("display_image.html", {"request": request,  "myImage": base64_encoded_image})

With this simple html:

<html>
   <head>
      <title>Display Uploaded Image</title>
   </head>
   <body>
      <h1>My Image<h1>
      <img src="data:image/jpeg;base64,{{ myImage | safe }}">
   </body>
</html>

I’ve been working from these answers and have tried multiple permutations of these:

How to display uploaded image in HTML page using FastAPI & Jinja2?

How to convert PIL Image.image object to base64 string?

How can I display PIL image to html with render_template flask?

This seems like it ought to be very simple but all I get is the html icon for an image that didn’t render.

What am I doing wrong? Thank you.

I used Mark Setchell’s answer, which clearly shows what I was doing wrong, but still am not getting an image in html. My FastAPI is:

@api.get("/test_image", status_code=status.HTTP_200_OK)
def test_image(request: Request):
# Create image
    im = Image.new('RGB',(1000,1000),'red')

    im.save('red.png')

    print(im.tobytes())

    # Create buffer
    buffer = io.BytesIO()

    # Tell PIL to save as PNG into buffer
    im.save(buffer, 'PNG')

    # get the PNG-encoded image from buffer
    PNG = buffer.getvalue()

    print()
    print(PNG)

    base64_encoded_image = base64.b64encode(PNG)

    return templates.TemplateResponse("display_image.html", {"request": request,  "myImage": base64_encoded_image})

and my html:

<html>
   <head>
      <title>Display Uploaded Image</title>
   </head>
   <body>
      <h1>My Image 3<h1>
      <img src="data:image/png;base64,{{ myImage | safe }}">
   </body>
</html>

When I run, if I generate a 1×1 image I get the exact printouts in Mark’s answer. If I run this version, with 1000×1000 image, it saves a red.png that I can open and see. But in the end, the html page has the heading and the icon for no image rendered. I’m clearly doing something wrong now in how I send to html.

Asked By: Brad Allen

||

Answers:

There are a couple of issues here. I’ll make a new section for each to keep it clearly divided up.


If you want to send a base64-encoded PNG, you need to change your HTML to:

<img src="data:image/png;base64,{{ myImage | safe }}">

If you create an image of a single red pixel like this:

im = Image.new('RGB',(1,1),'red')
print(im.tobytes())

you’ll get:

b'xffx00x00'

That is not a PNG-encoded image, how could it be – you haven’t told PIL that you want a PNG, or a JPEG, or a TIFF, so it cannot know. It is just giving you the 3 raw RGB pixels as bytes #ff0000.

If you save that image to disk as a PNG and dump it you will get:

im.save('red.png')

Then dump it:

xxd red.png

00000000: 8950 4e47 0d0a 1a0a 0000 000d 4948 4452  .PNG........IHDR
00000010: 0000 0001 0000 0001 0802 0000 0090 7753  ..............wS
00000020: de00 0000 0c49 4441 5478 9c63 f8cf c000  .....IDATx.c....
00000030: 0003 0101 00c9 fe92 ef00 0000 0049 454e  .............IEN
00000040: 44ae 4260 82                             D.B`.

You can now see the PNG signature at the start. So we need to create that same thing, but just in memory without bothering the disk:

import io
import base64
from PIL import image

# Create image
im = Image.new('RGB',(1,1),'red')

# Create buffer
buffer = io.BytesIO()

# Tell PIL to save as PNG into buffer
im.save(buffer, 'PNG')

Now we can get the PNG-encoded image from the buffer:

PNG = buffer.getvalue()

And if we print it, it will look suspiciously identical to the PNG on disk:

b'x89PNGrnx1anx00x00x00rIHDRx00x00x00x01x00x00x00x01x08x02x00x00x00x90wSxdex00x00x00x0cIDATxx9ccxf8xcfxc0x00x00x03x01x01x00xc9xfex92xefx00x00x00x00IENDxaeB`x82'

Now you can base64-encode it and send it:

base64_encoded_image = base64.b64encode(PNG)

Note: I only made 1×1 for demonstration purposes so I could show you the whole file. Make it bigger than 1×1 when you test, or you’ll never see it

Answered By: Mark Setchell

I used Mark Setchell’s answer and comments to come up with this full code. I thought it useful to show what works:

import base64
from PIL import Image

@api.get("/test_image", status_code=status.HTTP_200_OK)
def test_image(request: Request):
# Create image
    im = Image.new('RGB',(1000,1000),'red')

    # Create buffer
    buffer = io.BytesIO()

    # Tell PIL to save as PNG into buffer
    im.save(buffer, 'PNG')

    # get the PNG-encoded image from buffer
    PNG = buffer.getvalue()

    # the only difference is the .decode("utf-8") added here:
    base64_encoded_image = base64.b64encode(PNG).decode("utf-8")

    return templates.TemplateResponse("display_image.html", {"request": request,  "myImage": base64_encoded_image})
<html>
   <head>
      <title>Display Uploaded Image</title>
   </head>
   <body>
      <h1>My Image 3<h1>
      <img src="data:image/png;base64,{{ myImage | safe }}">
   </body>
</html>

This included some troubleshooting from:
How to display a bytes type image in HTML/Jinja2 template using FastAPI?

Answered By: Brad Allen