Your frame may already do this
picframe3 does this by itself, so on a Raspberry Pi 4 or 5 you no longer need anything below. The article stays here for frames still running the older software.
One of our Pi3D PictureFrame fans recently encountered an issue where very large images, such as panorama photos, caused their Raspberry Pi Zero running Pi3D PictureFrame to display a black screen.
Paddy suggested this is likely due to the Pi Zero’s limited memory.
A simple solution seems to be ensuring that all images are resized to fit within the display’s maximum width and height.
Tested on a Raspberry Pi 5 running OS Bookworm, February 2025. The following instructions assume you have either used the one-click install script or followed the same setup steps manually. Works with jpg, png, tiff, and heic files.
If your frame runs picframe3 on a Pi 4 or Pi 5, you do not need this script — and I would not run it. picframe3 handles oversized pictures itself: shrink_oversized is on by default, so a picture larger than the decode limit is opened once in a separate process and a screen-sized copy is kept in the cache. The photograph on your wall comes from that copy, and the original is never moved, changed or deleted. That last part is the important difference. The script below overwrites your files in place. If HEIC photographs are being skipped on picframe3, run picframe3 doctor — it prints the exact command to add HEIC support for your install. The 2026 build guide has the rest.
This article stays here for Pi Zero 2 W, Pi 2 and Pi 3 frames, which run pi3d and where a panorama really can take the frame down. That is the case it was written for.
So here is a small Python script that does exactly that:
- Scans the Pictures folder and all subdirectories.
- Resizes oversized images proportionally (no cropping) while preserving quality and retaining metadata.
- Fixes potential color issues by ensuring RGB mode.
- Detects newly added files and automatically checks if they need resizing.
Note: The image files are overwritten with the reduced size. There is no undo. Keep your originals somewhere else — on your computer, on a NAS, in whatever cloud you use — before you point this at a folder you care about. Run it once by hand on a copy of a few photographs and look at the result before you set it running as a service.
The Python script installation
Before running the script, install the necessary Python libraries with
pip install pillow watchdog pillow-heif
sudo apt install libheif1
Note: Before you do any “pip” stuff, don’t forget to always activate your Python virtual directory, in our case with source venv_picframe/bin/activate. Since Bookworm, a pip install outside a virtual environment is refused outright with error: externally-managed-environment, so this is no longer optional. (apt-get still works, but apt is the command Raspberry Pi OS documents now.)
This ensures that your Pi has the tools to recognize and process new images as they are added.
Create a new script with
nano resize_images.py
(No sudo — the file belongs in your own home directory.)
Copy and paste the following content into this script:
Set MAX_WIDTH and MAX_HEIGHT before you run this. The defaults below are 1080 × 768, which is not a size any frame I know of uses — it is neither 1080p (1920 × 1080) nor 4K (3840 × 2160). Leave them alone and every photograph in the folder is permanently shrunk to postcard size. Use your panel’s actual resolution:
- a 1080p screen:
MAX_WIDTH = 1920,MAX_HEIGHT = 1080 - a 4K screen:
MAX_WIDTH = 3840,MAX_HEIGHT = 2160 - a portrait panel: swap the two numbers
The whole point of the script is to stop a photograph being far larger than anything the screen can show. Below the panel's own resolution you are throwing away quality for nothing.
import os
import time
import threading
import queue
from pathlib import Path
from PIL import Image
import pillow_heif
from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler
# Register HEIF opener with PIL
pillow_heif.register_heif_opener()
# Default configuration
PICTURES_FOLDER = os.path.expanduser("~/Pictures")
MAX_WIDTH = 1920 # your panel's width - CHANGE THIS
MAX_HEIGHT = 1080 # your panel's height - CHANGE THIS
FILE_CHECK_RETRIES = 10
FILE_CHECK_INTERVAL = 0.1 # 100ms between checks
MAX_QUEUE_RETRIES = 5 # Maximum number of times to requeue a file
# Thread-safe queue to store new file paths
file_queue = queue.Queue()
def wait_for_file_stability(file_path, max_retries=FILE_CHECK_RETRIES, check_interval=FILE_CHECK_INTERVAL):
"""Wait for file to be completely written by checking if its size remains stable"""
last_size = -1
for _ in range(max_retries):
try:
current_size = os.path.getsize(file_path)
if current_size == last_size and current_size > 0:
return True
last_size = current_size
time.sleep(check_interval)
except (OSError, FileNotFoundError):
time.sleep(check_interval)
continue
return False
def resize_image(image_path, attempt=1):
""" Resize image while maintaining aspect ratio and quality """
try:
print(f"Checking if file is ready: {image_path} (attempt {attempt})")
if not wait_for_file_stability(image_path):
if attempt < MAX_QUEUE_RETRIES:
print(f"File not ready yet, requeueing: {image_path}")
file_queue.put((image_path, attempt + 1))
else:
print(f"Max retries reached, skipping file: {image_path}")
return
print(f"Processing image: {image_path}")
with Image.open(image_path) as img:
# Preserve EXIF and other metadata if available
exif = img.info.get('exif', None) # Explicitly handle None case
icc_profile = img.info.get('icc_profile', None) # Same for ICC profile
# Convert to RGB if needed, preserving alpha channel if present
if img.mode == 'RGBA':
img = img
elif img.mode != 'RGB':
img = img.convert('RGB')
# Check if resizing is needed
width, height = img.size
if width <= MAX_WIDTH and height <= MAX_HEIGHT:
# If it's a HEIC file, we still need to convert it to JPEG
if image_path.lower().endswith(('.heic', '.heif')):
new_path = os.path.splitext(image_path)[0] + '.jpg'
save_kwargs = {'quality': 100, 'optimize': False}
if exif is not None: # Only include exif if it exists
save_kwargs['exif'] = exif
if icc_profile is not None: # Only include ICC if it exists
save_kwargs['icc_profile'] = icc_profile
img.save(new_path, 'JPEG', **save_kwargs)
try:
os.remove(image_path) # Remove original HEIC file
print(f"Converted HEIC to JPEG: {new_path}")
except Exception as e:
print(f"Error removing original HEIC file: {str(e)}")
else:
print(f"No resize needed for {image_path} ({width}x{height})")
return
print(f"Current image size: {width}x{height}")
# Calculate memory requirements and use progressive loading if needed
memory_estimate = (width * height * 3) / (1024 * 1024) # In MB
if memory_estimate > 500: # If image might use more than 500MB
print(f"Large image detected ({memory_estimate:.2f}MB), using progressive loading")
img.draft('RGB', (width//2, height//2))
# Resize while maintaining aspect ratio
img.thumbnail((MAX_WIDTH, MAX_HEIGHT), Image.LANCZOS)
new_width, new_height = img.size
print(f"Resizing image from {width}x{height} to {new_width}x{new_height}")
# Handle HEIC/HEIF files specially
file_ext = image_path.lower().split('.')[-1]
if file_ext in ['heic', 'heif']:
new_path = os.path.splitext(image_path)[0] + '.jpg'
save_kwargs = {'quality': 100, 'optimize': False}
if exif is not None: # Only include exif if it exists
save_kwargs['exif'] = exif
if icc_profile is not None: # Only include ICC if it exists
save_kwargs['icc_profile'] = icc_profile
img.save(new_path, 'JPEG', **save_kwargs)
try:
os.remove(image_path) # Remove original HEIC file
print(f"Converted and resized HEIC to JPEG: {new_path}")
except Exception as e:
print(f"Error removing original HEIC file: {str(e)}")
else:
# For other formats, use original settings
save_kwargs = {
'jpg': {'format': 'JPEG', 'quality': 100, 'optimize': False},
'jpeg': {'format': 'JPEG', 'quality': 100, 'optimize': False},
'png': {'format': 'PNG', 'optimize': False},
'tiff': {'format': 'TIFF', 'compression': None}
}
kwargs = save_kwargs.get(file_ext, {})
if exif is not None: # Only include exif if it exists
kwargs['exif'] = exif
if icc_profile is not None: # Only include ICC if it exists
kwargs['icc_profile'] = icc_profile
img.save(image_path, **kwargs)
print(f"Successfully saved resized image: {image_path}")
except Exception as e:
print(f"Error processing {image_path}: {str(e)}")
if attempt < MAX_QUEUE_RETRIES:
print(f"Requeueing due to error: {image_path}")
file_queue.put((image_path, attempt + 1))
def is_image_file(filename):
"""Check if a file is an image based on its extension"""
return filename.lower().endswith(('.png', '.jpg', '.jpeg', '.tiff', '.bmp', '.gif', '.heic', '.heif'))
def scan_existing_files():
"""Scan for existing images in the PICTURES_FOLDER and process them"""
print(f"\nScanning existing files in {PICTURES_FOLDER}")
count = 0
for root, _, files in os.walk(PICTURES_FOLDER):
for filename in files:
if is_image_file(filename):
file_path = os.path.join(root, filename)
print(f"Found existing image: {file_path}")
file_queue.put((file_path, 1)) # Start with attempt 1
count += 1
print(f"Initial scan completed. Found {count} images to process.\n")
def process_new_files():
""" Processes new files from the queue """
while True:
try:
item = file_queue.get() # Get file from queue
if isinstance(item, tuple):
image_path, attempt = item
else:
image_path, attempt = item, 1 # Handle old-style queue items
resize_image(image_path, attempt)
file_queue.task_done()
except Exception as e:
print(f"Error in process_new_files: {str(e)}")
class ImageWatcher(FileSystemEventHandler):
""" Watches for new image files and adds them to the queue """
def on_created(self, event):
if not event.is_directory and is_image_file(event.src_path):
print(f"\nNew image detected: {event.src_path}")
file_queue.put((event.src_path, 1)) # Start with attempt 1
def start_watching():
""" Watches the PICTURES_FOLDER for new files """
# First scan existing files
scan_existing_files()
# Then start watching for new files
observer = Observer()
event_handler = ImageWatcher()
observer.schedule(event_handler, PICTURES_FOLDER, recursive=True)
observer.start()
print(f"Watching for new images in {PICTURES_FOLDER}")
try:
while True:
time.sleep(1)
except KeyboardInterrupt:
print("\nStopping image watcher...")
observer.stop()
observer.join()
if __name__ == "__main__":
# Start the worker thread that processes images in the queue
worker_thread = threading.Thread(target=process_new_files, daemon=True)
worker_thread.start()
start_watching()
Save with CTRL+o and exit with CTRL+x.
Make the file executable with
chmod +x resize_images.py
Add a system file to launch automatically
If you want the script to run in the background, create a service file to start it automatically at boot.
Run the following command to create the service file:
sudo nano /etc/systemd/system/image-resizer.service
Then, add this content, replacing your-username with your actual user name (Raspberry Pi OS no longer creates a default pi account):
[Unit]
Description=Automatic Image Resizer for Digital Picture Frame
After=network.target
[Service]
ExecStart=/home/your-username/venv_picframe/bin/python3 /home/your-username/resize_images.py
WorkingDirectory=/home/your-username/
Restart=always
User=your-username
Group=your-username
[Install]
WantedBy=multi-user.target
After saving the file, reload systems:
sudo systemctl daemon-reload
Then, enable the service so it starts at boot:
sudo systemctl enable image-resizer.service
To start it immediately without rebooting:
sudo systemctl start image-resizer.service
To check if it’s running:
sudo systemctl status image-resizer.service
Conclusion
With this script running in the background, your images will always be resized correctly for your digital picture frame. This is especially recommended if you’re using a Pi Zero, but it’s a good idea regardless.
Just be sure to keep your original files.
And think about that last point before you start: if you shrink everything to the size of the screen you have now, the photographs will not be ready for the bigger screen you buy next. That is the strongest argument for letting the frame do the scaling in memory — which is what picframe3 does — rather than editing your library.
Was this article helpful?
Thank you for your support and motivation.