Home Articles Categories Series
Pythonise Just now

Pillow, task queues & the HTML picture element | Learning Flask Ep. 22

Using Pillow and task queues to offload image resizing to a worker process and using the HTML picture tag to increase image rendering performance

Article Posted on by in Flask
Julian Nash · 8 months ago in Flask

Images on the web are important and we now have access to a plethora of devices to view them - from mobile to desktop and tablet to TV screens, to get the most performance from our images, we should consider showing a peoperly sized image based on their device.

In this guide, we're going to:

  • Create a form for uploading images
  • Create a function that creates several duplicates of the image at different sizes whilst maintaining the original aspect ratio
  • Offload said function to a task queue to be handled by a separate worker
  • Render an HTML template using the HTML picture tag to see the different images being loaded as the browser viewport is resized


We're going to be using the following Python packages:

  • Flask (Web framework)
  • Redis (Message broker)
  • RQ (Task queue/worker service)
  • Pillow (Copying & resizing images)

App structure

Our application structure:

├── app
│   ├── __init__.py
│   ├── static
│   │   └── img
│   │       └── uploads        <- Image uploads directory
│   ├── tasks.py               <- Task queue functions
│   ├── templates
│   │   ├── upload_image.html  <- Uploading an image
│   │   └── view_image.html    <- Viewing images with the picture tag
│   └── views.py               <- Appliaction routes
└── run.py                     <- appliation entry point

Getting started

We're going to start off creating a virtual environment for our project:

python -m venv env

Where env is the name of our virtual environment.

Next, activate the env:

source env/bin/activate

Install the required packages:

pip install flask redis rq Pillow

Go ahead and build out the appliaction structure and add the following:

Flask entry point

run.py will be the entry point to our application:


from app import app

if __name__ == "__main__":

Setting up the app

We'll bring our app together in __init__.py and define some new variables:


from flask import Flask
import redis
from rq import Queue

app = Flask(__name__)

r = redis.Redis()
q = Queue(connection=r)

from app import views
from app import tasks
  • r = redis.Redis() - Creates a connection to the redis-server instance running locally (without auth)
  • q = Queue(connection=r) - Creates our task queue, using r as the message broker

The task queue function

We'll use this file to create our task queue function:


from PIL import Image

import os
import time

def create_image_set(image_dir, image_name):

    start = time.time()

    thumb = 30, 30
    small = 540, 540
    medium = 768, 786
    large = 1080, 1080
    xl = 1200, 1200

    image = Image.open(os.path.join(image_dir, image_name))

    image_ext = image_name.split(".")[-1]
    image_name = image_name.split(".")[0]

    ### THUMBNAIL ###
    thumbnail_image = image.copy()
    thumbnail_image.thumbnail(thumb, Image.LANCZOS)
    thumbnail_image.save(f"{os.path.join(image_dir, image_name)}-thumbnail.{image_ext}", optimize=True, quality=95)

    ### SMALL ###
    small_image = image.copy()
    small_image.thumbnail(small, Image.LANCZOS)
    small_image.save(f"{os.path.join(image_dir, image_name)}-540.{image_ext}", optimize=True, quality=95)

    ### MEDIUM ###
    medium_image = image.copy()
    medium_image.thumbnail(medium, Image.LANCZOS)
    medium_image.save(f"{os.path.join(image_dir, image_name)}-768.{image_ext}", optimize=True, quality=95)

    ### LARGE ###
    large_image = image.copy()
    large_image.thumbnail(large, Image.LANCZOS)
    large_image.save(f"{os.path.join(image_dir, image_name)}-1080.{image_ext}", optimize=True, quality=95)

    ### XL ###
    xl_image = image.copy()
    xl_image.thumbnail(xl, Image.LANCZOS)
    xl_image.save(f"{os.path.join(image_dir, image_name)}-1200.{image_ext}", optimize=True, quality=95)

    end = time.time()

    time_elapsed = end - start

    print(f"Task complete in: {time_elapsed}")

    return True

The routes

We're going to have 2 routes in our application:

  • /upload-image - Renders a template and handles the upload of a new image
  • /image/<dir>/<img> - Will just be used to view an image after resizing


from app.tasks import create_image_set

from flask import render_template, request, flash

import os
import secrets

app.config["SECRET_KEY"] = "liruhfoi34uhfo8734yot8234h"
app.config["UPLOAD_DIRECTORY"] = "/mnt/c/wsl/projects/pythonise/tutorials/flask_series/ep_21_background_tasks/app/static/img/uploads"

@app.route("/upload-image", methods=["GET", "POST"])
def upload_image():

    message = None

    if request.method == "POST":

        image = request.files["image"]

        image_dir_name = secrets.token_hex(16)

        os.mkdir(os.path.join(app.config["UPLOAD_DIRECTORY"], image_dir_name))

        image.save(os.path.join(app.config["UPLOAD_DIRECTORY"], image_dir_name, image.filename))

        image_dir = os.path.join(app.config["UPLOAD_DIRECTORY"], image_dir_name)

        q.enqueue(create_image_set, image_dir, image.filename)

        flash("Image uploaded and sent for resizing", "success")

        message = f"/image/{image_dir_name}/{image.filename.split('.')[0]}"

    return render_template("upload_image.html", message=message)    

def view_image(dir, img):
    return render_template("view_image.html", dir=dir, img=img)

Uploading an image

This wile contains a section to display any flashed messages, along with a form for uploading a file:


<!doctype html>
<html lang="en">

  <!-- Required meta tags -->
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">

  <!-- Bootstrap CSS -->
  <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">

  <title>Upload image</title>


  <div class="container">
    {% with messages = get_flashed_messages(with_categories=true) %}
    {% if messages %}
    {% for category, message in messages %}
    <div class="alert alert-{{ category }} alert-dismissible fade show mt-3" role="alert">
      {{ message }}
      <button type="button" class="close" data-dismiss="alert" aria-label="Close">
        <span aria-hidden="true">&times;</span>
    {% endfor %}
    {% endif %}
    {% endwith %}
    <div class="row">
      <div class="col">
        <div class="mb-3 mt-3">

          <h2 class="mb-3" style="font-weight: 300">Upload image</h2>

          <form action="/upload-image" method="POST" enctype="multipart/form-data" class="mb-3">
            <div class="form-group mb-3">
              <div class="custom-file">
                <input type="file" class="custom-file-input" name="image">
                <label id="file_input_label" class="custom-file-label" for="image">Select file</label>
                <small class="text-muted">Images should be 2560px x 1440px and not contain a dot in the filename</small>

            <button type="submit" class="btn btn-primary">Upload</button>

          {% if message %}
          <a href="{{ message }}" class="mt-3">View image</a>
          {% endif %}


    <!-- import bootstrap JS here -->



Viewing the resized images

We'll use this template to render the image set to the browser:


<!doctype html>
<html lang="en">

  <!-- Required meta tags -->
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">

  <!-- Bootstrap CSS -->
  <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">

  <title>View image</title>



    <div class="container mt-3">
      <div class="row">
        <div class="col">

            <!-- thumbnail -->
            <source class="img-fluid" srcset="/static/img/uploads/{{dir}}/{{img}}-thumbnail.png" media="(max-width: 150px)">
            <!-- small -->
            <source class="img-fluid" srcset="/static/img/uploads/{{dir}}/{{img}}-540.png" media="(max-width: 540px)">
            <!-- medium -->
            <source class="img-fluid" srcset="/static/img/uploads/{{dir}}/{{img}}-768.png" media="(max-width: 768px)">
            <!-- large -->
            <source class="img-fluid" srcset="/static/img/uploads/{{dir}}/{{img}}-1080.png" media="(max-width: 1080px)">
            <!-- xl -->
            <source class="img-fluid" srcset="/static/img/uploads/{{dir}}/{{img}}-1200.png" media="(max-width: 1200px)">
            <!-- original -->
            <img class="img-fluid" src="/static/img/uploads/{{dir}}/{{img}}.png" />

            <source srcset="example-thumbnail.png" media="(max-width: 150px)">
            <source srcset="example-540.png" media="(max-width: 540px)">
            <source srcset="example-768.png" media="(max-width: 768px)">
            <source srcset="example-1080.png" media="(max-width: 1080px)">
            <source srcset="example-1200.png" media="(max-width: 1200px)">
            <img src="/static/img/uploads/example.png" />



  <!-- Import bootstrap JS here -->



HTML picture tag

THe HTML picture tag allows us to provide a set of images, of which the browser will render the relevant version based on the viewport size of the users device.

In our example, we've provided 5 options using the source tag, along with a fallback using the img tag.

Each source tag contains a srcset attribute; The path to the image we'd like to render.

However, the source tag will only be rendered if the browser viewport falls wihin the max-width query in the media attribute.

Running the application

Before we run the app, you'll need to start redis.

You can do so (depending on your OS and installation of Redis), by running the following command:


This should start Redis and you should see a message in your terminal.

To run the application, you'll need 2 more terminals open. From the root of the application (in the same directory as run.py), run the following:

rq worker

You should see the RQ worker process start and a message like the following:

16:29:55 RQ worker 'rq:worker:jnwt.8633' started, version 0.13.0
16:29:55 *** Listening on default...
16:29:55 Cleaning registries for queue: default

In another terminal (from the same directory), run the following:

export FLASK_APP=run.py
export FLASK_ENV=development
flask run

Testing the application

Open up a browser and head to /upload-image.

You should be able to select and upload an image and your terminal running the rq worker should also print out some information to say it's running a task.

Head to the link returned by /upload-image and resize your browser viewport to see the different images being rendered by the picture tag!


Open the developer tools, select the network tab and select the IMG filter to only show images being requested.

As you resize the viewport, you'll see the dirrerent images being requested and rendered!

Last modified · 13 Mar 2019
Did you find this article useful?
This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License