How to Build a JPG to PNG Converter Using Python

When I need to convert a batch of JPG images, opening each file and saving it manually quickly becomes repetitive. A small Python application makes this much easier, especially when you need to process several images at once.

For this project, I built a JPG to PNG Converter using Python with Streamlit for the interface and Pillow for image processing. The application accepts multiple JPG/JPEG files, validates them, converts them, and packages the results into one ZIP file.

We will build the converter step by step using the same code structure used in the working app.py project.

What We Are Building

The completed application provides a simple workflow:

  1. Select JPG or JPEG images.
  2. Validate the number and total size of uploaded files.
  3. Display the selected filenames.
  4. Convert each image to PNG.
  5. Correct EXIF orientation.
  6. Handle duplicate filenames.
  7. Add converted images to a ZIP file.
  8. Download the ZIP file.

The current application allows 20 images with a maximum combined upload size of 50 MB.

How to Build a JPG to PNG Converter Using Python

Import the Required Python Modules

The first part of the application imports the libraries required for image processing, file buffering, ZIP creation, and memory cleanup.

import streamlit as st
from PIL import Image, ImageOps, UnidentifiedImageError
from io import BytesIO
import zipfile
import gc

Streamlit creates the web interface. Pillow handles JPG and PNG images. BytesIO provides an in-memory buffer, while zipfile creates the downloadable ZIP file. The gc module helps release unused memory after processing.

For projects that work with images, Pillow is especially useful because it lets you open, verify, transform, and save images directly from Python.

Configure the Streamlit Application

Next, configure the page title, icon, and layout.

st.set_page_config(
    page_title="JPG to PNG Converter",
    page_icon="🖼️",
    layout="wide"
)

The layout="wide" setting gives the application more horizontal space. This works well for a file conversion interface.

If you are building other Python GUI projects, you may also want to explore Python GUI programming for different interface approaches.

Create the File Uploader Reset

The application includes a Clear Selected Images button. Streamlit’s uploader needs a small reset mechanism to remove the previously selected files.

if "uploader_key" not in st.session_state:
    st.session_state.uploader_key = 0


def clear_files():
    st.session_state.uploader_key += 1
    st.rerun()

The uploader_key changes whenever the user clicks the clear button. Streamlit then recreates the uploader with a new key, effectively clearing the selected files.

Pro Tip: I’ve found that using a changing uploader key is much cleaner than trying to manipulate the uploaded file objects directly.

Create the Application Header

The application displays a title and short description using Streamlit’s HTML support.

st.markdown(
    '<div class="main-title">🖼️ JPG to PNG Converter</div>',
    unsafe_allow_html=True
)

st.markdown(
    '<div class="subtitle">'
    'Convert JPG and JPEG images to PNG format quickly and easily'
    '</div>',
    unsafe_allow_html=True
)

The CSS classes control the appearance of the title and subtitle.

Add the Image Uploader

Now we need to allow users to select multiple JPG or JPEG files.

uploaded_files = st.file_uploader(
    "Choose JPG/JPEG images",
    type=["jpg", "jpeg"],
    accept_multiple_files=True,
    key=f"uploader_{st.session_state.uploader_key}"
)

The type parameter restricts the uploader to JPG and JPEG extensions, while accept_multiple_files=True enables batch conversion.

This is useful when you need to process a folder of photographs, screenshots, or other JPG images without converting them individually.

For applications that need traditional desktop file selection, you can also look at Tkinter filedialog in Python.

Limit the Number and Size of Images

The converter uses two limits:

MAX_FILES = 20
MAX_TOTAL_SIZE_MB = 50
MAX_TOTAL_SIZE_BYTES = MAX_TOTAL_SIZE_MB * 1024 * 1024

The application then checks the number of selected files.

if len(uploaded_files) > MAX_FILES:
    st.error(
        f"❌ You can upload a maximum of {MAX_FILES} images."
    )
    st.stop()

Next, it calculates the combined size of the uploaded files.

total_size = sum(
    uploaded_file.size
    for uploaded_file in uploaded_files
)

total_size_mb = total_size / (1024 * 1024)

Then the application stops the conversion if the total exceeds 50 MB.

if total_size > MAX_TOTAL_SIZE_BYTES:
    st.error(
        f"❌ Total file size is {total_size_mb:.2f} MB. "
        f"Maximum allowed size is {MAX_TOTAL_SIZE_MB} MB."
    )
    st.stop()

This prevents users from starting a conversion that exceeds the application’s configured limits.

Display the Selected Files

Once the files pass validation, the application displays their names inside an expandable section.

if uploaded_files:

    with st.expander(
        f"📋 View Selected Files ({len(uploaded_files)})"
    ):

        for index, uploaded_file in enumerate(
            uploaded_files,
            start=1
        ):

            st.write(
                f"**{index}.** {uploaded_file.name}"
            )

This gives the user a quick way to confirm which files they selected before starting the conversion.

Add the Convert Button

The conversion starts only after the user clicks the button.

st.markdown("###")
convert_button = st.button(
    "🔄 Convert to PNG",
    type="primary",
    use_container_width=True
)
Python JPG to PNG Converter

The actual conversion code runs inside the button condition:

if convert_button:

This prevents the application from converting files immediately after upload.

Prepare the ZIP File

Before processing the images, the application creates an in-memory ZIP buffer and initializes the required variables.

zip_buffer = BytesIO()
successful_conversions = 0
used_filenames = set()
failed_files = []

total_files = len(uploaded_files)

The BytesIO object keeps the ZIP file in memory rather than requiring a temporary file on disk.

The used_filenames set prevents duplicate PNG filenames inside the ZIP.

Add Conversion Progress

The application uses Streamlit placeholders for progress information.

progress_text = st.empty()
progress_bar = st.progress(0)

During conversion, the current filename and progress percentage are updated for each image.

Create the ZIP File

The ZIP creation itself is wrapped in exception handling.

try:

    with zipfile.ZipFile(
        zip_buffer,
        "w",
        zipfile.ZIP_DEFLATED
    ) as zip_file:

This provides an additional layer of protection if something unexpected happens while creating the ZIP file.

Process Each Uploaded Image

The application loops through every uploaded file.

for index, uploaded_file in enumerate(
    uploaded_files,
    start=1
):

It updates the progress message before processing the current image.

progress_text.write(
    f"🔄 Converting image {index} "
    f"of {total_files}: "
    f"{uploaded_file.name}"
)

This makes batch processing easier to follow, especially when converting many images.

Validate and Open the JPG Image

Pillow opens the uploaded file first.

image = Image.open(
    uploaded_file
)

The application then verifies that the image is valid.

image.verify()

Because verify() checks the image without fully processing it, the file needs to be opened again afterward.

uploaded_file.seek(0)

image = Image.open(
    uploaded_file
)

The application also corrects the image’s EXIF orientation.

image = ImageOps.exif_transpose(
    image
)

These lines are important when users upload photographs that contain orientation information from a camera or phone.

Convert the Image to RGB

Before saving the image as PNG, the application converts images that are not already RGB.

if image.mode != "RGB":
    image = image.convert(
        "RGB"
    )

This gives the output a consistent color mode before the PNG is created.

Create the PNG in Memory

Instead of saving a temporary PNG file to the computer, the application creates a memory buffer.

png_buffer = BytesIO()

image.save(
    png_buffer,
    format="PNG"
)

png_buffer.seek(0)

This approach keeps the conversion workflow inside memory and makes it straightforward to pass the resulting data directly to the ZIP file.

If you work with images in Python more generally, posts about saving Matplotlib charts as PNG can also help explain Python’s image output workflow.

Generate the PNG Filename

The original extension is removed and replaced with .png.

base_name = uploaded_file.name.rsplit(
    ".",
    1
)[0]

png_filename = (
    base_name + ".png"
)

For example, an uploaded file named vacation.jpg becomes vacation.png.

Handle Duplicate Filenames

Duplicate filenames can occur when users upload images with the same name. The application handles this automatically.

counter = 1

while png_filename in used_filenames:
    png_filename = (
        f"{base_name}_{counter}.png"
    )

    counter += 1

used_filenames.add(
    png_filename
)

So duplicate files can become:

photo.png
photo_1.png
photo_2.png
Build a JPG to PNG Converter Using Python

This prevents one converted file from overwriting another inside the ZIP.