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:
- Select JPG or JPEG images.
- Validate the number and total size of uploaded files.
- Display the selected filenames.
- Convert each image to PNG.
- Correct EXIF orientation.
- Handle duplicate filenames.
- Add converted images to a ZIP file.
- Download the ZIP file.
The current application allows 20 images with a maximum combined upload size of 50 MB.

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 gcStreamlit 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 * 1024The 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
)
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
This prevents one converted file from overwriting another inside the ZIP.
Add the PNG to the ZIP
The converted image is added directly to the ZIP buffer.
zip_file.writestr(
png_filename,
png_buffer.getvalue()
)
successful_conversions += 1writestr() lets the application add the PNG data without first creating a physical PNG file.
This is particularly useful for batch tools because the user ultimately needs one ZIP download rather than multiple individual files.
Release Memory After Each Image
The application releases the image and PNG buffer after processing.
image.close()
png_buffer.close()
gc.collect()This is useful when processing multiple images because the application does not need to keep every processed image object in memory.
Pro Tip: I’ve found that releasing image objects after every conversion matters more when users process larger batches.
Handle Invalid or Corrupted Images
The application uses specific exception handling for image problems.
except UnidentifiedImageError:
failed_files.append(
uploaded_file.name
)
st.error(
f"❌ Invalid or corrupted JPG: "
f"{uploaded_file.name}"
)
It also handles operating-system-level file errors.
except OSError:
failed_files.append(
uploaded_file.name
)
st.error(
f"❌ Unable to read JPG file: "
f"{uploaded_file.name}"
)Finally, an additional exception catches unexpected conversion errors.
except Exception as e:
failed_files.append(
uploaded_file.name
)
st.error(
f"❌ Could not convert "
f"{uploaded_file.name}."
)
st.caption(
f"Technical details: {str(e)}"
)These handlers allow one problematic image to be recorded without necessarily stopping the processing of the remaining files.
Update the Progress Bar
After each file, the progress percentage is calculated.
progress = int(
(index / total_files) * 100
)
progress_bar.progress(
progress
)The user therefore gets continuous feedback while the batch conversion runs.
Finish the Conversion
After all files have been processed, the progress bar reaches 100 percent.
progress_bar.progress(100)
progress_text.success(
f"✅ Conversion completed! "
f"{successful_conversions} of "
f"{total_files} images converted."
)
The ZIP creation exception is handled outside the image-processing loop.
except Exception as e:
st.error(
"❌ Something went wrong while creating the ZIP file."
)
st.caption(
f"Technical details: {str(e)}"
)This separates individual image errors from errors involving the ZIP creation itself.
Prepare and Download the ZIP
Finally, reset the buffer position and provide the download button.
zip_buffer.seek(0)
if successful_conversions > 0:
st.download_button(
label="⬇️ Download All PNG Images (ZIP)",
data=zip_buffer,
file_name="converted_png_images.zip",
mime="application/zip",
use_container_width=True,
key="download_zip"
)
The user receives all successfully converted PNG files in one ZIP download.
Things to Keep in Mind
- Validate uploaded files: Do not assume every uploaded image is valid just because it has a JPG extension.
- Control upload size: Batch image processing can consume significant memory, so the 50 MB limit helps keep the application manageable.
- Handle duplicate names: Always generate unique output filenames when creating a ZIP.
- Reset the uploader correctly: Changing the Streamlit uploader key provides a reliable way to clear selected files.
- Release memory: Close processed images and temporary buffers after each conversion.
- Handle exceptions: Keep individual image errors separate from errors that affect the ZIP creation.
Frequently Asked Questions
Can Python convert JPG to PNG?
Yes. The Pillow library provides the image-processing functionality needed to open a JPG and save it as PNG. In this application, Pillow handles the conversion directly in memory.
Can I convert multiple JPG images to PNG with Python?
Yes. The application uses Streamlit’s accept_multiple_files=True option and loops through every uploaded image. Each successful conversion gets added to the same ZIP file.
How does the Python JPG to PNG converter handle corrupted files?
The application calls image.verify() before conversion. It also catches UnidentifiedImageError and OSError so invalid or unreadable images can be reported.
Why does the application use BytesIO?
BytesIO creates an in-memory file-like buffer. The converter uses it for both the PNG output and the final ZIP instead of creating temporary files on disk.
How are duplicate JPG filenames handled?
The application keeps converted filenames in a used_filenames set. When a duplicate appears, it adds _1, _2, and so on to create a unique filename.
Can the converted PNG files be downloaded together?
Yes. The application adds each converted PNG to a ZIP file and provides one Streamlit download button for the completed archive.
Building this JPG to PNG Converter using Python combines Streamlit’s file-upload features with Pillow’s image-processing capabilities, validation, memory management, and ZIP creation. Start with the working batch converter, then expand it only when you have a real requirement for additional image operations.
You May Also Like
- Remove background from an image using Python
- Save Matplotlib charts as PNG
- Read binary files in Python
- Get the file extension from a filename in Python
- Create a string with variables in Python

Bijay Kumar is an experienced Python and AI professional who enjoys helping developers learn modern technologies through practical tutorials and examples. His expertise includes Python development, Machine Learning, Artificial Intelligence, automation, and data analysis using libraries like Pandas, NumPy, TensorFlow, Matplotlib, SciPy, and Scikit-Learn. At PythonGuides.com, he shares in-depth guides designed for both beginners and experienced developers. More about us.