PNG to JPEG Conversion Flask Web Application [Python]

Do not miss this exclusive book on Binary Tree Problems. Get it now for free.

In this article, we have developed a Web Application using Python based web framework that is Flask that will provide an option to upload a PNG image file, convert it to JPEG format from server side and give option to download the converted JPEG file.

Go through this Flask application to do Prime Factorization.

PNG VS JPEG

PNG (Portable Graphics Format) and JPEG (Joint Photograhic Experts Group) are both popular image formats for storing images. A question of 'which one you should use' has no right or wrong answer as the answer is subjective to the use case.

Before we dive into the details of the application, it's necessary to understand why one would want to build such an application.

JPEG

When images are converted to JPEG, their quality is compromised in favor of a smaller file size. The reason is because compression in jpeg images is lossy. This implies that some data in the image is permanently deleted. Even when some details are lost during compression, it might take quite a number of compressions to notice the degradation in quality.

The small file size nature of jpeg images is a desired characteristic for the web, as the smaller the size, the less latency experienced during page loads.

PNG

One notable benefit of PNG over JPEG is support for transparency or transparent backgrounds. PNG can exist in both RGB and RGBA modes - A is the alpha channel for tuning transparency.

File compression in PNG is lossless i.e. PNG images retain their data even after compression. For high constrast images like logos, where retention of details is necessary for visual clarity, PNG is your go-to option.

The other use case scenario of PNG is when images are still in the editing process. Before you commit to a lossy format like JPEG, a lossless format like PNG is a better choice in order to keep your changes intact during editing.

I hope after reading preceding the paragraphs, you find sufficient reasons for why an application like this should exist.

Source code

Download the source code of the application from https://github.com/kiraboibrahim/png-to-jpeg-converter

Creating the virtual environment

Using python venv module

python -m venv flask-sandbox

The above command creates a virtual environment with the name 'flask-sandbox'. This works for python >= 3.3

Using virtualenv

virtualenv --python=/usr/bin/python3 flask-sandbox

Installing the requirments

Activating the environment

source flask-sandbox/bin/activate

flask-sandbox/bin/activate path is relative to wherever you created the virtual environment.

pip install -r requirements.txt

Application structure

png_to_jpg/
├─ forms.py
├─ config/
│  ├─ __init__.py
│  ├─ config.py
│  ├─ .env
│  ├─ .env.template
├─ templates/
│  ├─ index.html
├─ __init__.py
├─ app.py

We shall start with building the form that prompts for the png image to be converted.

import io
import base64
from PIL import Image

from flask_wtf import FlaskForm
from flask_wtf.file import FileAllowed, FileRequired, FileSize, FileField

from .config.config import Config


class PngToJpegConverterForm(FlaskForm):
    png_image = FileField("Image", validators=[FileRequired(), FileAllowed(["png"], "Only png images"),
                                               FileSize(Config.MAX_FILE_UPLOAD_SIZE)])
    def convert(self) -> str:
        """Convert the png_image to jpeg in memory
        Should only be invoked when the form is valid

        Returns:
            str: base64 representation of the jpeg image
        """
        png_image_file = self.png_image.data  # This is a werkzeug FileStorage object
        in_buffer = io.BytesIO(png_image_file.read())
        data_uri = "data:image/jpeg;base64,{data}"
        with io.BytesIO() as out_buffer:
            with Image.open(in_buffer) as in_image:
                if in_image.mode in ("RGBA", "P"):
                    in_image = in_image.convert("RGB")  # Convert to RGB mode
                in_image.save(out_buffer, format="jpeg")
            out_buffer.seek(0)  # Position file cursor back to beginning of file since saving image moves cursor to end
            data_uri = data_uri.format(data=base64.b64encode(out_buffer.read()).decode("utf-8"))
        
        png_image_file.close()
        return data_uri

At the top of the file, we import the following modules:

  • io: We shall use this to create buffers for the input png image and the output image. The essence is to do all the conversion in memory and refrain from saving any users' files on the servers. The fundermental and honorable reason behind this implementation is to observe and respect user privacy.
  • base64: As a way to send back the converted image to the user, the image data is encoded in base64 and appended as a payload in a data uri
  • PIL(Python Imaging Library): This module contains the facilities for working with images. We shall use this module to convert images.

The forms package used is flask-wft, which is a flask extension for wtf-forms. wtf-forms package is framework agnostic,it could be used without any framework extension but the goal here is to focus on the major problem(image conversion) without getting frustrated by the intricate details of form handling and not to mention, reinvent the wheel.

Our form is encapsulated in a class that inherits from FlaskForm. We define a FileField with a label - Image. We then assign validators to the field to mark it as required, restrict input to only png images and the maximum file size to 5MB(defined in configuration file, which will be looked at later).

It isn't uncommon to have business logic encapsulated in forms because forms tend to habour functionality(converting png to jpeg) that may be required in other parts of the application. If you have quite an experience with Django, you may have used model forms which simplify performing CRUD(CREATE, READ, UPDATE, DELETE) operations with respect to the underlying model and with less code.

def convert(self) -> str:
        """Convert the png_image to jpeg in memory
        Should only be invoked when the form is valid

        Returns:
            str: base64 representation of the jpeg image
        """
        png_image_file = self.png_image.data  # This is a werkzeug FileStorage object
        in_buffer = io.BytesIO(png_image_file.read())
        data_uri = "data:image/jpeg;base64,{data}"
        with io.BytesIO() as out_buffer:
            with Image.open(in_buffer) as in_image:
                if in_image.mode in ("RGBA", "P"):
                    in_image = in_image.convert("RGB")  # Convert to RGB mode
                in_image.save(out_buffer, format="jpeg")
            out_buffer.seek(0)  # Position file cursor back to beginning of file since saving image moves cursor to end
            data_uri = data_uri.format(data=base64.b64encode(out_buffer.read()).decode("utf-8"))
        
        png_image_file.close()
        return data_uri

The convert() method does the actual conversion, and with this abstraction, there are benefits because we are separating concerns i.e the form does the conversion and the view function renders the template. The view is not aware of how the conversion is done.

Files sent over the network are represented as FileStrorage objects and these objects behave like File like objects supporting common file API operations like reading, seeking e.t.c.
So self.png_image.data is a reference to a FileStorage object.

We then create a buffer for holding the input image data. BytesIO objects or bytes like objects behave like file like objects though they don't have any backing file on the filesystem and their data resides in memory.

Using a context manager, we open the output buffer and the input buffer. The if in_image.mode in ("RGBA", "P") checks for unsupported modes i.e RGBA(remember jpeg doesn't support transparency) and P, which if the unsupported modes are detected, the input image is converted to RGB mode.

Converting an image with PIL is more like saving the image on a hard disk which entails calling the save() method on the image to be saved. The only difference is that the output filename should have a file extension corresponding to the destination image format. Since we are using bytes like objects and not filenames(strings), we have to explicitly specify the output format - jpeg using the format parameter.

After saving the converted image to the output buffer, we encode the output buffer in a base64 string and create a data uri with the base64 encoded data string. After which we close the FileStorage object i.e the png file that was submitted with the request and then return the data uri of the converted image. Closing the rest of the file objects is taken care of by the context managers.

Configuration

Environment Variables File(.env)

SECRET_KEY=[YOUR SECRET KEY]

Config File(config.py)

from environs import Env

env = Env()
env.read_env(recurse=False)


class Config(object):
    SECRET_KEY = env("SECRET_KEY")
    MAX_FILE_UPLOAD_SIZE = 5242880 # 5MB

By default, CSRF (Cross Site Request Forgery) protection is turned on by default in flask wtf forms and the SECRET_KEY or alternatively WTF_CSRF_SECRET_KEY is used to securely sign the csrf token.
The SECRET_KEY should be stored securely and because of this, It has been defined in an environment file(.env) separately from source files to protect it from accidental leakage and also encourage collaboration without compromising security.

MAX_FILE_UPLOAD_SIZE is a size limit to the uploaded png image in order to better handle the memory resource.

Environment Variables Template(.env.template)

SECRET_KEY=[PUT YOUR SECRET KEY HERE]

The essence of this file is to list the prerequisite sensitive variables the application requires to run.

Template(index.html)

...
<div class="container d-flex">
        <div class="bg-white d-flex flex-column form-and-privacy-policy-container">
            <h2 class="text-center form-title">Upload Image</h2>
            <form class="png-to-jpeg-form" enctype="multipart/form-data" method="post" action="/convert/png-to-jpeg">
                {{ png_to_jpeg_form.csrf_token }}
                <div class="file-container">
                    {{  png_to_jpeg_form.png_image(accept="images/png") }}
                    {%  if png_to_jpeg_form.png_image.errors %}
                        <ul class="errors">
                            {%  for error in png_to_jpeg_form.png_image.errors %}
                                <li class="errors__error">{{ error }}</li>
                            {%  endfor %}
                        </ul>
                    {%  endif %}
                </div>
                <button class="btn btn-success w-100" type="submit">CONVERT</button>
            </form>
            <div class="privacy-policy mt-auto">
                <p class="text-muted text-sm">We don't store any of the uploaded and converted files</p>
            </div>
        </div>
        <div class="preview-container d-flex flex-column align-items-center justify-content-center">
            <div id="preview" class="preview" {% if out_image_uri %} style="background-image: url('{{ out_image_uri }}');" {% endif %}></div>
            {%  if out_image_uri %}
                <a id="download-converted" class="btn btn-success download-converted" download="converted.jpeg" href="{{ out_image_uri }}">Download</a>
            {% endif %}
        </div>
    </div>
...

For brevity, an excerpt of the template has been shown above. The template renders the form - png_to_jpeg_form that prompts the user for the image to convert. In addition to rendering the form field - png_image, the errors pertaining to the png_image field are rendered below it in an unordered list. Take note of the enctype attribute of the form, since our form will include files, the enctype attribute is set to multipart/form-data.

The initial requirements of this application didn't include previewing the selected image or the converted image. As the application took shape, I found it relevant to allow users preview their selected and converted image simply for clarity(selected the right image) and comparison between the input image and the output image.

The last div with class preview-container houses this feature. And most notably, It provides a download link to the converted image when the out_image_uri is defined in the context variables.

Views And Routes

from flask import Flask, render_template

from .config.config import Config
from .forms import PngToJpegConverterForm

app = Flask(__name__)
app.config.from_object(Config())


@app.route("/")
def index():
    png_to_jpeg_converter_form = PngToJpegConverterForm()
    return render_template("index.html", png_to_jpeg_form=png_to_jpeg_converter_form)


@app.route("/convert/png-to-jpeg", methods=("POST",))
def convert_png_to_jpeg():
    png_to_jpeg_converter_form = PngToJpegConverterForm()
    if png_to_jpeg_converter_form.validate_on_submit():
        out_image_uri = png_to_jpeg_converter_form.convert()
        return render_template("index.html", png_to_jpeg_form=png_to_jpeg_converter_form,  out_image_uri=out_image_uri)
    return render_template("index.html", png_to_jpeg_form=png_to_jpeg_converter_form)


if __name__ == "__main__":
    app.run(debug=True)

There are two routes defined here.

  • / - This route renders the unbound(not associated with user data) png to jpeg form.
  • /convert/png-to-jpeg - Recieves the POST data of the form on submission, converts the image to jpeg, passes the form and output image data uri to the template - index.html to be rendered.

You may have noticed that after instantiating the Flask object, we set up the application configuration. As you saw ealier on in the configuration section, there are two configuration variables that were defined i.e SECRET_KEY and MAX_FILE_UPLOAD_SIZE. It is advisable configure the application earlier on to allow extensions access the configuration values when starting up and thats why I configure the application immediately after instantiation.

Thank you for reading, Happy Learning 😄.

Sign up for FREE 3 months of Amazon Music. YOU MUST NOT MISS.