import gradio as gr from PIL import Image import pillow_heif import os import shutil # 1. Register HEIF opener with Pillow so it can handle .heic/.heif files pillow_heif.register_heif_opener() def format_bytes(size): # Helper to make file sizes readable power = 2**10 n = 0 power_labels = {0 : '', 1: 'KB', 2: 'MB', 3: 'GB', 4: 'TB'} while size > power: size /= power n += 1 return f"{size:.2f} {power_labels[n]}" def compress_image(image_path): if image_path is None: return None, "No image uploaded." try: # Get original file info original_size = os.path.getsize(image_path) filename = os.path.basename(image_path) name_without_ext = os.path.splitext(filename)[0] # Define output path output_filename = f"{name_without_ext}_compressed.png" output_path = os.path.join(os.path.dirname(image_path), output_filename) # Open the image # pillow-heif handles the HEIC/HEIF format automatically here with Image.open(image_path) as img: # Create a copy of the image to remove specific metadata that might # conflict with saving (though we keep pixel data lossless) # We convert to RGBA if it supports transparency, or RGB otherwise if img.mode in ('RGBA', 'LA') or (img.mode == 'P' and 'transparency' in img.info): img = img.convert('RGBA') else: img = img.convert('RGB') # SAVE AS PNG # compress_level=9 is maximum compression (slowest, smallest size) # optimize=True performs additional passes to reduce size img.save(output_path, "PNG", compress_level=9, optimize=True) # Get new file info new_size = os.path.getsize(output_path) # Calculate stats diff = new_size - original_size status = "Reduced" if diff < 0 else "Increased" percent = (abs(diff) / original_size) * 100 info_text = ( f"Original Size: {format_bytes(original_size)}\n" f"Compressed Size: {format_bytes(new_size)}\n" f"Difference: {status} by {format_bytes(abs(diff))} ({percent:.1f}%)\n\n" f"Note: Converting JPEGs/HEICs (lossy) to PNG (lossless) may increase file size " f"because PNG preserves the exact pixel data without compression artifacts." ) return output_path, info_text except Exception as e: return None, f"Error processing image: {str(e)}" # 2. Create the Gradio Interface with gr.Blocks(title="Lossless PNG Compressor") as app: gr.Markdown("## 🗜️ Lossless Image Compressor (Max PNG Level)") gr.Markdown( "Upload any image (JPG, PNG, HEIC, WEBP). It will be converted to **PNG** " "saved at **Compression Level 9** (Maximum). \n\n" "*Note: This preserves pixel quality perfectly. However, converting a lossy photo (JPG/HEIC) " "to lossless PNG often results in a larger file size.*" ) with gr.Row(): with gr.Column(): # Input: Expects a file path image_input = gr.Image(type="filepath", label="Upload Image") submit_btn = gr.Button("Compress / Convert", variant="primary") with gr.Column(): # Output: Returns a downloadable file and text stats file_output = gr.File(label="Download PNG") stats_output = gr.Textbox(label="Compression Stats") # Bind function submit_btn.click( fn=compress_image, inputs=image_input, outputs=[file_output, stats_output] ) if __name__ == "__main__": app.launch()