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
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
Tools
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
├── 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:
run.py
`from app import app
if name == “main“: app.run()`
Setting up the app
We’ll bring our app together in __init__.py
and define some new variables:
__init__.py
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 theredis-server
instance running locally (without auth)q = Queue(connection=r)
- Creates our task queue, usingr
as the message broker
The task queue function
We’ll use this file to create our task queue function:
tasks.py
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
views.py
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)
@app.route("/image/<dir>/<img>")
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:
upload_image.html
<!doctype html>
<html lang="en">
<head>
<!-- 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>
</head>
<body>
<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">×</span>
</button>
</div>
{% 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>
</div>
</div>
<button type="submit" class="btn btn-primary">Upload</button>
</form>
{% if message %}
<a href="{{ message }}" class="mt-3">View image</a>
{% endif %}
</div>
</div>
</div>
<!-- import bootstrap JS here -->
</body>
</html>
Viewing the resized images
We’ll use this template to render the image set to the browser:
view_image.html
<!doctype html>
<html lang="en">
<head>
<!-- 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>
</head>
<body>
<main>
<div class="container mt-3">
<div class="row">
<div class="col">
<picture>
<!-- 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" />
</picture>
<picture>
<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" />
</picture>
</div>
</div>
</div>
</main>
<!-- Import bootstrap JS here -->
</body>
</html>
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:
redis-server
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!
tip
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
Source : .