From 5dd41928ebdc2e51a16372aa755ead21e8edfd0b Mon Sep 17 00:00:00 2001 From: Cagri Sarigoz Date: Sat, 31 Jan 2026 12:05:36 +0000 Subject: [PATCH 1/4] [charlie] feat: Add AVIF support to image optimization options --- README.md | 1 + cloudfront_provider.py | 4 ++-- upload_files.py | 32 ++++++++++++++++++++++++++------ 3 files changed, 29 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index ceefef5..4b64641 100644 --- a/README.md +++ b/README.md @@ -177,6 +177,7 @@ deactivate ### Requirements - **Python 3.9+** (Tested on 3.9, 3.10, 3.11, 3.12, 3.13) +- **AVIF support (optional)**: For CloudFront/S3 smart format conversion to AVIF, install `pillow-avif-plugin` (included in `requirements.txt`) and ensure your environment has AVIF codecs available (common on most modern Linux distributions). - **Provider Account**: Choose one or both: - **Cloudinary account** (recommended for most users) - **AWS account** with S3 and CloudFront access diff --git a/cloudfront_provider.py b/cloudfront_provider.py index ea1a89f..a7f6d86 100644 --- a/cloudfront_provider.py +++ b/cloudfront_provider.py @@ -229,7 +229,7 @@ def upload_image( """Upload an image file with optimization""" try: # Check if it's an image file - if file_name.lower().endswith((".jpg", ".jpeg", ".png", ".gif", ".webp")): + if file_name.lower().endswith((".jpg", ".jpeg", ".png", ".gif", ".webp", ".avif")): # Optimize the image success, optimized_path = self._optimize_image( file_path, max_width, quality, smart_format @@ -282,7 +282,7 @@ def upload_from_url( # Create headers headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", - "Accept": "image/webp,image/apng,image/*,*/*;q=0.8", + "Accept": "image/avif,image/webp,image/apng,image/*,*/*;q=0.8", "Accept-Language": "en-US,en;q=0.9", "Accept-Encoding": "gzip, deflate, br", "Referer": "https://citizenshipper.com/", diff --git a/upload_files.py b/upload_files.py index 98362e9..52f7277 100644 --- a/upload_files.py +++ b/upload_files.py @@ -14,6 +14,12 @@ from flask import Flask, jsonify, request from PIL import Image +# Optional: enable AVIF read/write support when pillow-avif-plugin is installed +try: + import pillow_avif # noqa: F401 +except Exception: + pass + # Load environment variables load_dotenv() @@ -82,8 +88,8 @@ def save_uploaded_files(uploaded_files): def get_best_format(img, original_format, original_path, quality=DEFAULT_QUALITY): - """Determine the best format (JPEG, PNG, WebP) based on file size""" - formats_to_try = ["JPEG", "PNG", "WEBP"] + """Determine the best format (JPEG, PNG, WebP, AVIF) based on file size""" + formats_to_try = ["JPEG", "PNG", "WEBP", "AVIF"] # Skip testing if the image has transparency and we're considering JPEG has_transparency = img.mode in ("RGBA", "LA") or ( @@ -116,6 +122,10 @@ def get_best_format(img, original_format, original_path, quality=DEFAULT_QUALITY test_img.save(temp_file, format=fmt, optimize=True) elif fmt == "WEBP": test_img.save(temp_file, format=fmt, quality=quality, method=6) + elif fmt == "AVIF": + # AVIF uses a quality parameter similar to WebP. + # pillow-avif-plugin adds support for saving AVIF via Pillow. + test_img.save(temp_file, format=fmt, quality=quality) # Get file size file_size = os.path.getsize(temp_file) @@ -142,6 +152,8 @@ def get_best_format(img, original_format, original_path, quality=DEFAULT_QUALITY extension = ".png" elif fmt_name == "WEBP": extension = ".webp" + elif fmt_name == "AVIF": + extension = ".avif" else: extension = os.path.splitext(original_path)[1] @@ -162,6 +174,8 @@ def get_best_format(img, original_format, original_path, quality=DEFAULT_QUALITY save_img.save(new_path, format=fmt_name, optimize=True) elif fmt_name == "WEBP": save_img.save(new_path, format=fmt_name, quality=quality, method=6) + elif fmt_name == "AVIF": + save_img.save(new_path, format=fmt_name, quality=quality) print(f"Converted image from {original_format} to {fmt_name}") @@ -193,6 +207,8 @@ def optimize_image( original_format = "GIF" elif ext == ".webp": original_format = "WEBP" + elif ext == ".avif": + original_format = "AVIF" else: original_format = "JPEG" # Default @@ -218,6 +234,8 @@ def optimize_image( format_name = "GIF" elif file_ext == ".webp": format_name = "WEBP" + elif file_ext == ".avif": + format_name = "AVIF" else: format_name = "JPEG" # Default to JPEG @@ -241,6 +259,8 @@ def optimize_image( img.save(image_path, format=format_name, optimize=True) elif format_name == "WEBP": img.save(image_path, format=format_name, quality=quality, method=6) + elif format_name == "AVIF": + img.save(image_path, format=format_name, quality=quality) elif format_name == "GIF": # GIFs are saved as-is to preserve animation pass @@ -331,7 +351,7 @@ def upload_files( file_path = os.path.join(UPLOAD_FOLDER, file_name) # Check if it's an image file - if file_name.lower().endswith((".jpg", ".jpeg", ".png", ".gif", ".webp")): + if file_name.lower().endswith((".jpg", ".jpeg", ".png", ".gif", ".webp", ".avif")): # Optimize the image success, new_path = optimize_image( file_path, max_width, quality, smart_format @@ -520,7 +540,7 @@ def download_and_upload_from_csv( # Optimize the image if it's an image file if file_name.lower().endswith( - (".jpg", ".jpeg", ".png", ".gif", ".webp") + (".jpg", ".jpeg", ".png", ".gif", ".webp", ".avif") ): print( f"Optimizing image with max_width={max_width}, quality={quality}, smart_format={smart_format}..." @@ -619,7 +639,7 @@ def download_and_upload_from_csv( # Continue with optimization and upload if file_name.lower().endswith( - (".jpg", ".jpeg", ".png", ".gif", ".webp") + (".jpg", ".jpeg", ".png", ".gif", ".webp", ".avif") ): print( f"Optimizing image with max_width={max_width}, quality={quality}, smart_format={smart_format}..." @@ -735,7 +755,7 @@ def upload_file_api(): # Optimize the image if it's an image file new_filename = filename - if filename.lower().endswith((".jpg", ".jpeg", ".png", ".gif", ".webp")): + if filename.lower().endswith((".jpg", ".jpeg", ".png", ".gif", ".webp", ".avif")): success, new_path = optimize_image(file_path, max_width, quality, smart_format) if success and new_path != file_path: # If the file path changed (due to format conversion), update the file name From c2cb73694c27890acec67fe6d4b29bb9e772903b Mon Sep 17 00:00:00 2001 From: Cagri Sarigoz Date: Sat, 31 Jan 2026 12:03:09 +0000 Subject: [PATCH 2/4] [charlie] feat: Add AVIF support for smart format optimization --- README.md | 2 +- cloudfront_provider.py | 14 ++++++++++++-- requirements.txt | 3 ++- 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 4b64641..3863c32 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ Choose between two powerful image delivery platforms: - **🔄 Multi-Provider Support**: Choose between Cloudinary and AWS CloudFront/S3 - **📦 Batch Image Processing**: Download images from URLs and upload to your chosen provider - **🎯 Smart Optimization**: Automatic resizing, quality adjustment, and format conversion -- **🎨 Format Intelligence**: Automatically chooses the best format (JPEG, PNG, WebP) for optimal file size +- **🎨 Format Intelligence**: Automatically chooses the best format (JPEG, PNG, WebP, AVIF) for optimal file size - **⚡ Unique URLs**: Adds timestamps to prevent filename conflicts and ensure cache busting - **🤖 AI Alt Text Generation**: Generate descriptive alt text using AltText.ai API - **🔌 REST API**: HTTP endpoints for programmatic access diff --git a/cloudfront_provider.py b/cloudfront_provider.py index a7f6d86..9405c75 100644 --- a/cloudfront_provider.py +++ b/cloudfront_provider.py @@ -20,6 +20,12 @@ from botocore.exceptions import ClientError from PIL import Image + # Optional: enable AVIF read/write support when pillow-avif-plugin is installed + try: + import pillow_avif # noqa: F401 + except Exception: + pass + BOTO3_AVAILABLE = True except ImportError: BOTO3_AVAILABLE = False @@ -129,8 +135,8 @@ def _optimize_image( def _get_best_format( self, img: Image.Image, original_path: str, quality: int = 82 ) -> Tuple[bool, str]: - """Determine the best format (JPEG, PNG, WebP) based on file size""" - formats_to_try = ["JPEG", "PNG", "WEBP"] + """Determine the best format (JPEG, PNG, WebP, AVIF) based on file size""" + formats_to_try = ["JPEG", "PNG", "WEBP", "AVIF"] # Skip testing if the image has transparency and we're considering JPEG has_transparency = img.mode in ("RGBA", "LA") or ( @@ -164,6 +170,10 @@ def _get_best_format( test_img.save(temp_file, format=fmt, optimize=True) elif fmt == "WEBP": test_img.save(temp_file, format=fmt, quality=quality, method=6) + elif fmt == "AVIF": + # AVIF uses a quality parameter similar to WebP. + # pillow-avif-plugin adds support for saving AVIF via Pillow. + test_img.save(temp_file, format=fmt, quality=quality) # Get file size file_size = os.path.getsize(temp_file) diff --git a/requirements.txt b/requirements.txt index dd3adc9..56f0029 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,4 +5,5 @@ Werkzeug==3.1.3 requests==2.32.3 pillow==11.2.1 python-dotenv==1.1.0 -cloudinary==1.40.0 \ No newline at end of file +cloudinary==1.40.0 +pillow-avif-plugin>=1.4.0 From 1f73fdd6adeb50c31661393e5373367840c9b6d7 Mon Sep 17 00:00:00 2001 From: Cagri Sarigoz Date: Thu, 5 Feb 2026 07:55:20 +0000 Subject: [PATCH 3/4] [charlie] style: fix Black formatting for long endswith lines --- cloudfront_provider.py | 4 +++- upload_files.py | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/cloudfront_provider.py b/cloudfront_provider.py index 9405c75..3d8ede3 100644 --- a/cloudfront_provider.py +++ b/cloudfront_provider.py @@ -239,7 +239,9 @@ def upload_image( """Upload an image file with optimization""" try: # Check if it's an image file - if file_name.lower().endswith((".jpg", ".jpeg", ".png", ".gif", ".webp", ".avif")): + if file_name.lower().endswith( + (".jpg", ".jpeg", ".png", ".gif", ".webp", ".avif") + ): # Optimize the image success, optimized_path = self._optimize_image( file_path, max_width, quality, smart_format diff --git a/upload_files.py b/upload_files.py index 52f7277..200622a 100644 --- a/upload_files.py +++ b/upload_files.py @@ -351,7 +351,9 @@ def upload_files( file_path = os.path.join(UPLOAD_FOLDER, file_name) # Check if it's an image file - if file_name.lower().endswith((".jpg", ".jpeg", ".png", ".gif", ".webp", ".avif")): + if file_name.lower().endswith( + (".jpg", ".jpeg", ".png", ".gif", ".webp", ".avif") + ): # Optimize the image success, new_path = optimize_image( file_path, max_width, quality, smart_format From 1d86d8f51b47b317e2f1cf44c0c33607bb806ac5 Mon Sep 17 00:00:00 2001 From: Cagri Sarigoz Date: Thu, 5 Feb 2026 08:05:20 +0000 Subject: [PATCH 4/4] [charlie] fix: update vulnerable deps and fix safety CLI syntax MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Bump pillow 11.2.1 → 11.3.0 (CVE-2025-48379, buffer overflow in DDS) - Bump werkzeug 3.1.3 → 3.1.5 (CVE-2025-66221, DoS) - Bump requests 2.32.3 → 2.32.5 (CVE-2024-47081, .netrc credential leak) - Fix safety check --output flag (takes format type, not filename) - Remove redundant second safety check call that caused exit code 64 --- .github/workflows/ci.yml | 3 +-- requirements.txt | 6 +++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ad187de..f7513ab 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -57,8 +57,7 @@ jobs: - name: Check for known security vulnerabilities run: | - safety check --json --output safety-report.json || true - safety check + safety check --output json 2>&1 | tee safety-report.json || true - name: Test import functionality run: | diff --git a/requirements.txt b/requirements.txt index 56f0029..354ebf2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,9 +1,9 @@ boto3==1.38.25 botocore==1.38.25 Flask==3.1.1 -Werkzeug==3.1.3 -requests==2.32.3 -pillow==11.2.1 +Werkzeug==3.1.5 +requests==2.32.5 +pillow==11.3.0 python-dotenv==1.1.0 cloudinary==1.40.0 pillow-avif-plugin>=1.4.0