Introduction

Many applications use digital images, and with this, there is usually a need to process the images used. If you are building your application with Python and need to add image processing features to it, there are various libraries you could use. Some popular ones are OpenCV, scikit-image, Python Imaging Library and Pillow.

We won't debate on which library is the best here; they all have their merits. This article will focus on Pillow, a powerful library that provides a wide array of image processing features and is simple to use.

Pillow is a fork of the Python Imaging Library (PIL). PIL is a library that offers several standard procedures for manipulating images. It's a powerful library but hasn't been updated since 2009 and doesn't support Python 3. Pillow builds on this, adding more features and support for Python 3. It supports a range of image file formats such as PNG, JPEG, PPM, GIF, TIFF, and BMP. We'll see how to perform various operations on images such as cropping, resizing, adding text to images, rotating, greyscaling, etc., using this library.

Installation and Project Setup

Before installing Pillow, you should be aware of the following:

  • Pillow and PIL cannot co-exist in the same environment, so in case you have PIL installed, uninstall it first before proceeding.
  • We'll be using the current stable version of Pillow in this article (version 8.0.1 at the time of writing). This version requires Python version 3.6 and above.

We give instructions on how to install Pillow below, but it is a good idea to check the installation guide in case later versions of Pillow happen to require some pre-requisite libraries installed first.

You can install Pillow with pip as shown:

python3 -m pip install --upgrade pip
python3 -m pip install --upgrade Pillow

To follow along, you can download the images (courtesy of Unsplash) that we'll use in the article. You can also use your own images.

All examples will assume the required images are in the same directory as the python script file being run.

The Image Object

A crucial class in the Python Imaging Library is the Image class. It's defined in the Image module and provides a PIL image on which manipulation operations can be carried out. An instance of this class can be created in several ways: by loading images from a file, creating images from scratch, or as a result of processing other images. We'll see all these in use.

To load an image from a file, we use the open() function in the Image module, passing it the path to the image.

from PIL import Image

image = Image.open('demo_image.jpg')

If successful, the above returns an Image object. If there was a problem opening the file, an OSError exception will be raised.

After obtaining an Image object, you can now use the methods and attributes defined by the class to process and manipulate it. Let's start by displaying the image. You can do this by calling the show() method on it. This displays the image on an external viewer (usually Preview on macOS, xv on Unix, and the Paint program on Windows).

image.show()

You can get some details about the image using the object's attributes.

# The file format of the source file.
print(image.format) # Output: JPEG

# The pixel format used by the image. Typical values are "1", "L", "RGB", or "CMYK."
print(image.mode) # Output: RGB

# Image size, in pixels. The size is given as a 2-tuple (width, height).
print(image.size) # Output: (1920, 1280)

# Colour palette table, if any.
print(image.palette) # Output: None

For more on what you can do with the Image class, check out the documentation.

Changing Image Type

When you are done processing an image, you can save it to a file with the save() method, passing in the name that will be used to label the image file. When saving an image, you can specify a different extension from its original, and the saved image will be converted to the specified format.

image = Image.open('demo_image.jpg')
image.save('new_image.png')

The above creates an Image object loaded with the demo_image.jpg image and saves it to a new file, new_image.png. Pillow sees the file extension has been specified as PNG and so it converts it to PNG before saving it to file. You can provide a second argument to save() to explicitly specify a file format. This image.save('new_image.png', 'PNG') will do the same thing as the previous save(). Usually, it's unnecessary to supply this second argument as Pillow will determine the file storage format to use from the filename extension, but if you're using non-standard extensions, then you should always specify the format this way.

Resizing Images

To resize an image, you call the resize() method on it, passing in a two-integer tuple argument representing the width and height of the resized image. The function doesn't modify the used image; it instead returns another Image with the new dimensions.

image = Image.open('demo_image.jpg')
new_image = image.resize((400, 400))
new_image.save('image_400.jpg')

print(image.size) # Output: (1920, 1280)
print(new_image.size) # Output: (400, 400)

The resize() method returns an image whose width and height exactly match the passed in value. This could be what you want, but at times you might find that the images returned by this function aren't ideal. This is mostly because the function doesn't account for the image's Aspect Ratio, so you might end up with an image that either looks stretched or squished.

You can see this in the newly created image from the above code: image_400.jpg. It looks a bit squished horizontally.

Resized Image

If you want to resize images and keep their aspect ratios, then you should instead use the thumbnail() function to resize them. This also takes a two-integer tuple argument representing the maximum width and maximum height of the thumbnail.

image = Image.open('demo_image.jpg')
image.thumbnail((400, 400))
image.save('image_thumbnail.jpg')

print(image.size) # Output: (400, 267)

The above will result in an image sized 400x267, having kept the aspect ratio of the original image. As you can see below, this results in a better-looking image.

Thumbnail Image

Another significant difference between the resize() and thumbnail() functions is that the resize() function 'blows up' an image if given parameters that are larger than the original image, while the thumbnail() function doesn't. For example, given an image of size 400x200, a call to resize((1200, 600)) will create a larger-sized image 1200x600; thus, the image will have lost some definition and is likely to be blurry compared to the original. On the other hand, a call to thumbnail((1200, 600)) using the original image will result in an image that keeps its size 400x200 since both the width and height are less than the specified maximum width and height.

Cropping

When an image is cropped, a rectangular region inside the image is selected and retained while everything else outside the region is removed. With the Pillow library, you can crop an image with the crop() method of the Image class. The method takes a box tuple that defines the position and size of the cropped region and returns an Image object representing the cropped image. The coordinates for the box are (left, upper, right, lower). The cropped section includes the left column and the upper row of pixels and goes up to (but doesn't include) the right column and bottom row of pixels. This is better explained with an example.

image = Image.open('demo_image.jpg')
box = (200, 300, 700, 600)
cropped_image = image.crop(box)
cropped_image.save('cropped_image.jpg')

# Print size of cropped image
print(cropped_image.size) # Output: (500, 300)

This is the resulting image:

Cropped Image

The Python Imaging Library uses a coordinate system that starts with (0, 0) in the upper left corner. The first two values of the box tuple specify the upper left starting position of the crop box. The third and fourth values specify the distance in pixels from this starting position towards the right and bottom direction, respectively. The coordinates refer to positions between the pixels, so the region in the above example is exactly 500x300 pixels.

Pasting an Image onto Another Image

Pillow enables you to paste an image onto another one. Some example use cases where this could be useful is in the protection of publicly available images by adding watermarks on them, the branding of images by adding a company logo, and in any other case where there is a need to merge two images.

Pasting is done with the paste() function. This modifies the Image object in place, unlike the other processing functions we've looked at so far that return a new Image object. Because of this, we'll first make a copy of our demo image before performing the paste so that we can continue with the other examples with an unmodified image.

image = Image.open('demo_image.jpg')
logo = Image.open('logo.png')
image_copy = image.copy()
position = ((image_copy.width - logo.width), (image_copy.height - logo.height))
image_copy.paste(logo, position)
image_copy.save('pasted_image.jpg')

In the above, we load in two images, unsplash_01.jpg and logo.png, then make a copy of the former with copy(). We want to paste the logo image onto the copied image, and we want it to be placed on the bottom right corner. This is calculated and saved in a tuple. The tuple can either be a 2-tuple giving the upper left corner, a 4-tuple defining the left, upper, right, and lower pixel coordinate, or None (same as (0, 0)). We then pass this tuple to paste() together with the image that will be pasted.

You can see the result below.

Pasted Image With Solid Pixels

That's not the result we were expecting.

By default, when you perform a paste, transparent pixels are pasted as solid pixels, thus the black (white on some OSs) box surrounding the logo. Most of the time, this isn't what you want. You can't have your watermark covering the underlying image's content. We would rather have transparent pixels appear as such.

To achieve this, you need to pass in a third argument to the paste() function. This argument is the transparency mask Image object. A mask is an Image object where the alpha value is significant, but its green, red, and blue values are ignored. If a mask is given, paste() updates only the regions indicated by the mask. You can use either 1, L, or RGBA images for masks. Pasting an RGBA image and also using it as the mask would paste the opaque portion of the image but not its transparent background. If you modify the paste as shown below, you should have a pasted logo with transparent pixels.

image_copy.paste(logo, position, logo)

Pasted Image With Transparent Pixels

Rotating Images

You can rotate images with Pillow using the rotate() method. This takes an integer or float argument representing the degrees to rotate an image and returns a new Image object of the rotated image. The rotation is done counterclockwise.

image = Image.open('demo_image.jpg')

image_rot_90 = image.rotate(90)
image_rot_90.save('image_rot_90.jpg')

image_rot_180 = image.rotate(180)
image_rot_180.save('image_rot_180.jpg')

In the above, we save two images to disk: one rotated at 90 degrees, the other at 180. The resulting images are shown below.

90 Degrees Rotation

180 Degrees Rotation

By default, the rotated image keeps the dimensions of the original image. This means that for angles other than multiples of 180, the image will be cut and/or padded to fit the original dimensions. If you look closely at the first image above, you'll notice that some of it has been cut to fit the original height, and its sides have been padded with black background (transparent pixels on some OSs) to fit the original width. The example below shows this more clearly.

image.rotate(18).save('image_rot_18.jpg')

The resulting image is shown below:

18 Degrees Rotation

To expand the dimensions of the rotated image to fit the entire view, you pass a second argument to rotate() as shown below.

image.rotate(18, expand=True).save('image_rot_18.jpg')

Now the contents of the image will be fully visible, and the dimensions of the image will have increased to account for this.

18 Degrees Rotation With Expanded Edges

Flipping Images

You can also flip images to get their mirror version. This is done with the transpose() function. It takes one of the following options: PIL.Image.FLIP_LEFT_RIGHT, PIL.Image.FLIP_TOP_BOTTOM, PIL.Image.ROTATE_90, PIL.Image.ROTATE_180, PIL.Image.ROTATE_270 PIL.Image.TRANSPOSE or PIL.Image.TRANSVERSE.

image = Image.open('demo_image.jpg')

image_flip = image.transpose(Image.FLIP_LEFT_RIGHT)
image_flip.save('image_flip.jpg')

The resulting image can be seen below.

Flipped Image

Drawing on Images

With Pillow, you can also draw on an image using the ImageDraw module. You can draw lines, points, ellipses, rectangles, arcs, bitmaps, chords, pie slices, polygons, shapes, and text.

from PIL import Image, ImageDraw

canvas = Image.new('RGB', (400, 300), 'white')
img_draw = ImageDraw.Draw(canvas)
img_draw.rectangle((70, 50, 270, 200), outline='red', fill='blue')
img_draw.text((70, 250), 'Hello World', fill='green')
canvas.save('drawn_image.jpg')

In the example, we create an Image object with the new() method. This returns an Image object with no loaded image. We then add a rectangle and some text to the image before saving it.

Drawing on Image

Color Transforms

Converting between modes

The Pillow library enables you to convert images between different pixel representations using the convert() method. It supports conversions between L (greyscale), RGB, and CMYK modes.

In the example below, we convert the image from RGB to L (luminance) mode, which will result in a greyscale image.

image = Image.open('demo_image.jpg')

greyscale_image = image.convert('L')
greyscale_image.save('greyscale_image.jpg')

print(image.mode) # Output: RGB
print(greyscale_image.mode) # Output: L

Greyscale Image

Splitting and Merging Bands

You can also split a multi-band image (such as an RGB) into individual bands using the split() method. split() creates new images, each containing one band from the original image.

You can merge a set of single band images into a new multi-band image using the merge() function. merge() takes a mode and a tuple of images and combines them into a new image.

image = Image.open('demo_image.jpg')

red, green, blue = image.split()

print(image.mode) # Output: RGB
print(red.mode) # Output: L
print(green.mode) # Output: L
print(blue.mode) # Output: L

new_image = Image.merge("RGB", (green, red, blue))
new_image.save('new_image.jpg')

print(new_image.mode) # Output: RGB

In the above code, we split an RGB image into individual bands, swap them, and then merge them. Below is the resulting image.

Swapped bands

Image enhancements

Pillow allows you to enhance an image by adjusting its contrast, color, brightness, and sharpness using classes in the ImageEnhance module.

from PIL import Image, ImageEnhance

image = Image.open('demo_image.jpg')

contrast = ImageEnhance.Contrast(image)
contrast.enhance(1.5).save('contrast.jpg')

In the above, we adjust the image contrast by a factor of 1.5. The factor used in the enhancement classes is a floating-point value that determines the level of enhancement. A factor of 1.0 returns a copy of the original image; lower factors mean less of the particular enhancement and higher values more. There is no restriction to this value.

You can see the enhanced image below.

Contrast

Below, we increase the color of the image. If we used a factor of 0.0, we would get a black and white image.

color = ImageEnhance.Color(image)
color.enhance(1.5).save('color.jpg')

Color

Below we make the image brighter. A factor of 0.0 would produce a black image.

brightness = ImageEnhance.Brightness(image)
brightness.enhance(1.5).save('brightness.jpg')

Brightness

Below we make the image sharper. An enhancement factor of 0.0 would produce a blurred image, and a factor of 2.0 would give a sharpened image.

sharpness = ImageEnhance.Sharpness(image)
sharpness.enhance(1.5).save('sharpness.jpg')

Sharpness

Aside: Adding Auth0 Authentication to a Python Application

Before concluding the article, let's take a look at how you can add authentication using Auth0 to a Python application. The application we'll look at is made with Flask, but the process is similar for other Python web frameworks.

Instead of creating an application from scratch, I've put together a simple app that you can download to follow along. It is a simple gallery application that enables the user to upload images to a server and view the uploaded images.

If you downloaded the project files, you would find two folders inside the main directory: complete_without_auth0 and complete_with_auth0. As the name implies, complete_without_auth0 is the project we'll start with and add Auth0 to.

To run the code, it's better to create a virtual environment and install the needed packages there. This prevents package clutter and version conflicts in the system's global Python interpreter.

We'll cover creating a virtual environment with Python 3. This version supports virtual environments natively and doesn't require downloading an external utility (virtualenv), as is the case with Python 2.7.

After downloading the code files, change your Terminal to point to the completed_without_auth0/gallery_demo folder.

$ cd path/to/complete_without_auth0/gallery_demo

Create the virtual environment with the following command.

$ python3 -m venv venv

Then activate it with (on macOS and Linux):

$ source venv/bin/activate

On Windows:

$ venv\Scripts\activate

To complete the setup, install the packages listed in the requirements.txt file with:

$ pip install -r requirements.txt

This will install flask, flask-bootstrap, python-dotenv, pillow, authlib, requests packages, and their dependencies. When flask-bootstrap is getting installed, you might get an error message in your Terminal reading ERROR: Failed building wheel for visitor. From what I've seen, the necessary packages will be installed, and setup is done without you needing to do anything (you should see the message Running setup.py install for visitor ... done in the Terminal). flask-bootstrap will be installed successfully and the demo project will run fine. You can read more about the error message here

Then finally, run the app.

$ Python app.py

Open http://localhost:3000/ in your browser, and you should see the following page.

Index page

When you head over to http://localhost:3000/gallery, you'll see a blank page. You can head over to http://localhost:3000/upload and upload some images that will then appear in the Gallery.

Upload page

Gallery page

When an image is uploaded, a smaller copy of it is made with the thumbnail() function we looked at earlier, then the two images are saved — the original to the images folder and the thumbnail to the thumbnails folder.

The Gallery displays the smaller sized thumbnails and only shows the larger image (inside a modal) when a thumbnail is clicked.

As the app stands, any user can upload an image. This might not be ideal. It might be better to put some protection over this action to prevent abuse or to at least track user uploads. This is where Auth0 comes in. With Auth0, we'll be able to add authentication to the app with a minimum amount of work.

For the simplicity of the app, most of its functionality is in the app.py file. Here, you can see the set route handlers. The upload() function handles calls to /upload. This is where images are processed before getting saved. We'll secure this route with Auth0.

from flask import Flask, render_template, redirect, url_for, send_from_directory, request
from flask_bootstrap import Bootstrap
from PIL import Image
from werkzeug.utils import secure_filename
import os

app = Flask(__name__)
Bootstrap(app)

APP_ROOT = os.path.dirname(os.path.abspath(__file__))
images_directory = os.path.join(APP_ROOT, 'images')
thumbnails_directory = os.path.join(APP_ROOT, 'thumbnails')
if not os.path.isdir(images_directory):
    os.mkdir(images_directory)
if not os.path.isdir(thumbnails_directory):
    os.mkdir(thumbnails_directory)


@app.route('/')
def index():
    return render_template('index.html')


@app.route('/gallery')
def gallery():
    thumbnail_names = os.listdir('./thumbnails')
    return render_template('gallery.html', thumbnail_names=thumbnail_names)


@app.route('/thumbnails/<filename>')
def thumbnails(filename):
    return send_from_directory('thumbnails', filename)


@app.route('/images/<filename>')
def images(filename):
    return send_from_directory('images', filename)


@app.route('/public/<path:filename>')
def static_files(filename):
    return send_from_directory('./public', filename)


@app.route('/upload', methods=['GET', 'POST'])
def upload():
    if request.method == 'POST':
        for upload in request.files.getlist('images'):
            filename = upload.filename
            # Always a good idea to secure a filename before storing it
            filename = secure_filename(filename)
            # This is to verify files are supported
            ext = os.path.splitext(filename)[1][1:].strip().lower()
            if ext in {'jpg', 'jpeg', 'png'}:
                print('File supported moving on...')
            else:
                return render_template('error.html', message='Uploaded files are not supported...')
            destination = '/'.join([images_directory, filename])
            # Save original image
            upload.save(destination)
            # Save a copy of the thumbnail image
            image = Image.open(destination)
            image.thumbnail((300, 170))
            image.save('/'.join([thumbnails_directory, filename]))
        return redirect(url_for('gallery'))
    return render_template('upload.html')


if __name__ == '__main__':
    app.run(host='0.0.0.0', port=os.environ.get('PORT', 3000))

Setting up Auth0

To set up the app with Auth0, first sign up for an Auth0 account, then navigate to the Dashboard. Click on the Create Application button and fill in the name of the application (or leave it at its default). Select Regular Web Applications from the application type list, then Create the application.

Create Application

Please note:
If you are taken to the Getting Started screen, click on the Create Application button, which is in the area labeled Integrate Auth0 into your application. You will be taken to the What technology are you using for your project? screen, in here just click the Skip Integration button, which will take you to the Settings tab for the application, where you can access the client ID, client secret, and domain.

After the app has been created, select the Settings tab where the client ID, Client Secret, and Domain can be retrieved. Set the Allowed Callback URLs to http://localhost:3000/callback and Allowed Logout URLs to http://localhost:3000 then save the changes with the button at the bottom of the page.

Back in your project, create a file labeled .env and save it at the root of the project. Add your Auth0 client credentials to this file. If you are using versioning, remember to not put this file under versioning. We'll use the value of SECRET_KEY as the app's secret key. You can/should change it.

AUTH0_CLIENT_ID=YOUR_AUTH0_CLIENT_ID
AUTH0_DOMAIN=YOUR_AUTH0_DOMAIN
AUTH0_CLIENT_SECRET=YOUR_AUTH0_CLIENT_SECRET
AUTH0_CALLBACK_URL=http://localhost:3000/callback
SECRET_KEY=F12ZMr47j\3yXgR~X@H!jmM]6Lwf/,4?KT

Add another file named constants.py to the root directory of the project and add the following constants to it.

AUTH0_CLIENT_ID = 'AUTH0_CLIENT_ID'
AUTH0_CLIENT_SECRET = 'AUTH0_CLIENT_SECRET'
AUTH0_CALLBACK_URL = 'AUTH0_CALLBACK_URL'
AUTH0_DOMAIN = 'AUTH0_DOMAIN'
PROFILE_KEY = 'profile'
JWT_PAYLOAD = 'jwt_payload'

Next, modify the beginning of the app.py file as shown — from the first statement to the point just before the first route definition (@app.route('/')).

from flask import Flask, render_template, redirect, url_for, send_from_directory, request, session, jsonify
from flask_bootstrap import Bootstrap
from PIL import Image
from werkzeug.utils import secure_filename
from werkzeug.exceptions import HTTPException
from dotenv import load_dotenv, find_dotenv
from functools import wraps
from authlib.integrations.flask_client import OAuth
import urllib.parse
import os
import constants

# Load Env variables
ENV_FILE = find_dotenv()
if ENV_FILE:
    load_dotenv(ENV_FILE)

app = Flask(__name__)
app.secret_key = os.environ.get('SECRET_KEY')
Bootstrap(app)

AUTH0_CALLBACK_URL = os.environ.get(constants.AUTH0_CALLBACK_URL)
AUTH0_CLIENT_ID = os.environ.get(constants.AUTH0_CLIENT_ID)
AUTH0_CLIENT_SECRET = os.environ.get(constants.AUTH0_CLIENT_SECRET)
AUTH0_DOMAIN = os.environ.get(constants.AUTH0_DOMAIN)
AUTH0_BASE_URL = 'https://' + AUTH0_DOMAIN

APP_ROOT = os.path.dirname(os.path.abspath(__file__))
images_directory = os.path.join(APP_ROOT, 'images')
thumbnails_directory = os.path.join(APP_ROOT, 'thumbnails')
if not os.path.isdir(images_directory):
    os.mkdir(images_directory)
if not os.path.isdir(thumbnails_directory):
    os.mkdir(thumbnails_directory)


@app.errorhandler(Exception)
def handle_auth_error(ex):
    response = jsonify(message=str(ex))
    response.status_code = (ex.code if isinstance(ex, HTTPException) else 500)
    return response


oauth = OAuth(app)

auth0 = oauth.register(
    'auth0',
    client_id=AUTH0_CLIENT_ID,
    client_secret=AUTH0_CLIENT_SECRET,
    api_base_url=AUTH0_BASE_URL,
    access_token_url=AUTH0_BASE_URL + '/oauth/token',
    authorize_url=AUTH0_BASE_URL + '/authorize',
    client_kwargs={
        'scope': 'openid profile email',
    },
)

We use load_dotenv() to load environment variables from the .env file.

We then set the app's secret_key. The application will make use of sessions, which allows storing information specific to a user from one request to the next. This is implemented on top of cookies and signs the cookies cryptographically. What this means is that someone could look at the contents of your cookie but not be able to make out the underlying credentials or to successfully modify it unless they know the secret key used for signing.

Next, we save our Auth0 credentials in some constants that we'll use later and add an error handler (handle_auth_error). We use the @app.errorhandler decorator on our error handler, which configures Flask to call this function when exceptions of type Exception are raised. The error handler makes errors more readable by placing them in a JSON object.

We then initialize a Flask OAuth client and register our app.

Next, add the following functions to the app.py file before the route handler definitions. requires_auth() has to come before any route handler definitions otherwise the error NameError: name 'requires_auth' is not defined will be raised.

# Requires authentication decorator
def requires_auth(f):
    @wraps(f)
    def decorated(*args, **kwargs):
        if is_logged_in():
            return f(*args, **kwargs)
        return redirect('/')

    return decorated


def is_logged_in():
    return constants.PROFILE_KEY in session

Here we define a decorator that will ensure that a user is authenticated before they can access a specific route. The second function simply returns True or False depending on whether there is some user data from Auth0 stored in the session object.

Next, modify the index() and upload() functions as shown.

@app.route('/')
def index():
    return render_template('index.html', env=os.environ, logged_in=is_logged_in())


@app.route('/upload', methods=['GET', 'POST'])
@requires_auth
def upload():
    if request.method == 'POST':
        for upload in request.files.getlist('images'):
            filename = upload.filename
            # Always a good idea to secure a filename before storing it
            filename = secure_filename(filename)
            # This is to verify files are supported
            ext = os.path.splitext(filename)[1][1:].strip().lower()
            if ext in {'jpg', 'jpeg', 'png'}:
                print('File supported moving on...')
            else:
                return render_template('error.html', message='Uploaded files are not supported...')
            destination = '/'.join([images_directory, filename])
            # Save original image
            upload.save(destination)
            # Save a copy of the thumbnail image
            image = Image.open(destination)
            image.thumbnail((300, 170))
            image.save('/'.join([thumbnails_directory, filename]))
        return redirect(url_for('gallery'))
    return render_template('upload.html', user=session[constants.PROFILE_KEY])

In index(), we pass some variables to the index.html template. We'll use these later on.

We add the @requires_auth decorator to the upload() function. This will ensure that calls to /upload can only be successful if the user is logged in. Not only will an unauthenticated user not be able to access the upload.html page, but they also won't be able to POST data to the route.

At the end of the function, we pass a user variable to the upload.html template.

Next, add the following function to the file.

@app.route('/callback')
def callback_handling():
    auth0.authorize_access_token()
    resp = auth0.get('userinfo')
    userinfo = resp.json()

    session[constants.JWT_PAYLOAD] = userinfo
    session[constants.PROFILE_KEY] = {
        'user_id': userinfo['sub'],
        'name': userinfo['name'],
        'picture': userinfo['picture']
    }
    return redirect(url_for('upload'))

The above will be called by the Auth0 server after user authentication. It is the path that we added to Allowed Callback URLs on the Auth0 Dashboard. The handler exchanges the code that Auth0 sends to the callback URL for an Access Token and an ID Token. The Access Token is used to call the /userinfo endpoint to get the user profile. After user information is obtained, we store it in the session object. Check the documentation to see the other user information returned by /userinfo

Modify templates/index.html as shown below.

{% extends "base.html" %}
{% block content %}
  <div class="container">
    <div class="row">
      <h1>Hi There!!!</h1>
      <p>Welcome to The Gallery</p>
      {% if logged_in %}
        <p>You can <a href="{{ url_for('upload') }}">upload images</a> or head over to the <a
            href="{{ url_for('gallery') }}">gallery</a></p>
        <p><a href="{{ url_for('logout') }}">Logout</a></p>
      {% else %}
        <p><a href="{{ url_for('login') }}" class="login">Login</a> to upload images or head over to the <a
            href="{{ url_for('gallery') }}">gallery</a></p>
      {% endif %}
    </div>
  </div>
{% endblock %}

In the above, we check for the user's logged-in status and display a different message accordingly. We also add a Logout link if the user is logged in.

For authentication, the app will use Auth0's universal login. This will present a ready-made, but customizable, login/signup form.

Add the following two routes to app.py

@app.route('/login')
def login():
    return auth0.authorize_redirect(redirect_uri=AUTH0_CALLBACK_URL)


@app.route('/logout')
def logout():
    session.clear()
    params = {'returnTo': url_for('index', _external=True), 'client_id': AUTH0_CLIENT_ID}
    return redirect(auth0.api_base_url + '/v2/logout?' + urllib.parse.urlencode(params))

In login(), we call the authorize_redirect() function which is used for logging in users via Universal Login. It takes a redirect URL, which Auth0 redirects the browser after authorization has been granted for the user.

We also add a route that will logout the user. When implementing logout functionality in an application, there are typically three layers of sessions you need to consider:

  • Application Session: The first is the session inside the application. Even though your application uses Auth0 to authenticate users, you will still need to keep track of the fact that the user has logged in to your application. In a normal web application, this is achieved by storing information inside a cookie. You need to log out the user from your application by clearing their session.
  • Auth0 session: Next, Auth0 will also keep a session and store the user's information inside a cookie. Next time when a user is redirected to the Auth0 login screen, the user's information will be remembered. In order to logout a user from Auth0, you need to clear the SSO cookie.
  • Identity Provider session: The last layer is the Identity Provider, for example, Facebook or Google. When you allow users to sign in with any of these providers, and they are already signed into the provider, they will not be prompted to sign in. They may simply be required to give permissions to share their information with Auth0 and, in turn, your application.

In the code above, we deal with the first two. If we had only cleared the session with session.clear(), then the user would be logged out of the app, but they won't be logged out of Auth0. On using the app again, authentication would be required to upload images. If they tried to log in, the login widget would show the user account that is logged in on Auth0, and the user would only have to click on the email to get Auth0 to send their credentials back to the app, which will then be saved to the session object. Here, the user will not be asked to reenter their password.

Lock Widget

You can see the problem here. After a user logs out of the app, another user can log in as them on that computer. Thus, it is also necessary to log the user out of Auth0. This is done with a redirect to https://<YOUR_AUTH0_DOMAIN>/v2/logout. Redirecting the user to this URL clears all single sign-on cookies set by Auth0 for the user.

Although not a common practice, you can force the user to also log out of their identity provider by adding a federated querystring parameter to the logout URL: https://<YOUR_AUTH0_DOMAIN>/v2/logout?federated.

We add a returnTo parameter to the URL whose value is a URL that Auth0 should redirect to after logging out the user. For this to work, the URL has to have been added to the Allowed Logout URLs on the Auth0 Dashboard, which we did earlier.

Finally, in templates/upload.html, you can add the following before the form tag.

<h2>Welcome {{user['name']}}</h2>

This will display the logged-in user's name. Look at the User Profile to see what other user information is available to you. The information available will be determined by what is saved on the server. For instance, if the user only uses the email/password authentication, then you won't be able to get their name (their name will be the value before the @ in their email) or picture, but if they used one of the available Identity Providers like Facebook or Google, then you might get this data.

Run the application. You won't be able to get the upload form by navigating to /upload. Head to the Home page and use the Login link to bring up the login widget.

Auth0 Lock Widget

After authentication, you will be redirected to the /upload.html page.

Conclusion

In this article, we've covered some of the more common image processing operations found in applications. Pillow is a powerful library, and we definitely haven't discussed all it can do. If you want to find out more, be sure to read the documentation.

If you're building a Python application that requires authentication, consider using Auth0 as it is bound to save you loads of time and effort. After signing up, setting up your application with Auth0 is fairly simple. If you need help, you can look through the documentation or post your question in the comment section below.