Skip to main content
Glama
image_optimizer.py7.89 kB
""" Image optimization utility for reducing file size while maintaining quality. """ from io import BytesIO from typing import Tuple, Optional from PIL import Image class ImageOptimizer: """Optimize images for reduced file size while maintaining readability.""" def __init__( self, max_width: int = 1920, max_height: int = 1080, jpeg_quality: int = 85, webp_quality: int = 80, prefer_webp: bool = True, size_threshold_kb: int = 100, ): """ Initialize the image optimizer. Args: max_width: Maximum image width (larger images will be resized) max_height: Maximum image height (larger images will be resized) jpeg_quality: JPEG compression quality (0-100) webp_quality: WebP compression quality (0-100) prefer_webp: Use WebP format when possible (better compression) size_threshold_kb: Compress images larger than this size (in KB) """ self.max_width = max_width self.max_height = max_height self.jpeg_quality = jpeg_quality self.webp_quality = webp_quality self.prefer_webp = prefer_webp self.size_threshold_kb = size_threshold_kb def optimize_image( self, image: Image.Image, name_hint: str = "", force_format: Optional[str] = None, ) -> Tuple[bytes, str]: """ Optimize an image for reduced size. Args: image: PIL Image object name_hint: Hint for the image name (for logging) force_format: Force specific output format ("png", "jpg", "webp") Returns: Tuple of (optimized_bytes, extension) """ original_mode = image.mode original_size = image.size # Convert RGBA to RGB for JPEG (JPEG doesn't support transparency) has_transparency = original_mode in ("RGBA", "LA", "P") # Resize if needed image = self._resize_if_needed(image) # Determine best format if force_format: output_format = force_format.lower() else: output_format = self._choose_format(image, has_transparency) # Optimize based on format if output_format == "webp": return self._save_as_webp(image, has_transparency) elif output_format == "png": return self._save_as_png(image) else: return self._save_as_jpeg(image, has_transparency) def _resize_if_needed(self, image: Image.Image) -> Image.Image: """Resize image if it exceeds maximum dimensions.""" width, height = image.size if width <= self.max_width and height <= self.max_height: return image # Calculate new size maintaining aspect ratio ratio = min(self.max_width / width, self.max_height / height) new_width = int(width * ratio) new_height = int(height * ratio) # Use high-quality resampling return image.resize((new_width, new_height), Image.Resampling.LANCZOS) def _choose_format(self, image: Image.Image, has_transparency: bool) -> str: """ Choose optimal format based on image characteristics. Strategy: - Screenshots/graphics with transparency -> PNG or WebP - Photos -> JPEG or WebP - Small images with few colors -> PNG """ # Check if image is likely a photo or graphics is_photo = self._is_photo_like(image) if has_transparency: # Need format that supports transparency return "webp" if self.prefer_webp else "png" if is_photo: # Photos compress better with lossy formats return "webp" if self.prefer_webp else "jpg" # Graphics/screenshots if self.prefer_webp: return "webp" # For simple graphics, PNG might be better return "png" def _is_photo_like(self, image: Image.Image) -> bool: """ Determine if image is photo-like (continuous tones) or graphics-like. Photos have many unique colors, graphics tend to have fewer. """ # Convert to RGB for color counting if image.mode != "RGB": test_image = image.convert("RGB") else: test_image = image # Sample a portion of the image for speed width, height = test_image.size sample_size = min(100, width) * min(100, height) # Get colors (with limit) colors = test_image.getcolors(maxcolors=1000) if colors is None: # More than 1000 colors - likely a photo return True # Many unique colors relative to size suggests photo unique_ratio = len(colors) / sample_size if sample_size > 0 else 0 return unique_ratio > 0.3 def _save_as_webp( self, image: Image.Image, has_transparency: bool ) -> Tuple[bytes, str]: """Save image as WebP format.""" buffer = BytesIO() if has_transparency: # Ensure RGBA mode for transparency if image.mode != "RGBA": image = image.convert("RGBA") image.save( buffer, format="WEBP", quality=self.webp_quality, lossless=False, method=4, # Balanced compression ) else: if image.mode == "RGBA": image = image.convert("RGB") image.save( buffer, format="WEBP", quality=self.webp_quality, lossless=False, method=4, ) return buffer.getvalue(), ".webp" def _save_as_png(self, image: Image.Image) -> Tuple[bytes, str]: """Save image as PNG format with optimization.""" buffer = BytesIO() # PNG is lossless, use compression image.save( buffer, format="PNG", optimize=True, compress_level=6, # Balanced speed/size ) return buffer.getvalue(), ".png" def _save_as_jpeg( self, image: Image.Image, has_transparency: bool ) -> Tuple[bytes, str]: """Save image as JPEG format.""" buffer = BytesIO() # JPEG doesn't support transparency - convert to RGB if image.mode in ("RGBA", "LA"): # Create white background and paste image background = Image.new("RGB", image.size, (255, 255, 255)) if image.mode == "RGBA": background.paste(image, mask=image.split()[3]) else: background.paste(image) image = background elif image.mode != "RGB": image = image.convert("RGB") image.save( buffer, format="JPEG", quality=self.jpeg_quality, optimize=True, progressive=True, # Better for web ) return buffer.getvalue(), ".jpg" def optimize_bytes( self, image_bytes: bytes, name_hint: str = "", force_format: Optional[str] = None, ) -> Tuple[bytes, str]: """ Optimize image from bytes. Args: image_bytes: Raw image bytes name_hint: Hint for logging force_format: Force specific format Returns: Tuple of (optimized_bytes, extension) """ image = Image.open(BytesIO(image_bytes)) return self.optimize_image(image, name_hint, force_format) def get_compression_ratio( self, original_bytes: bytes, optimized_bytes: bytes ) -> float: """Calculate compression ratio.""" if len(original_bytes) == 0: return 1.0 return len(optimized_bytes) / len(original_bytes)

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Asunainlove/OfficeReader-MCP'

If you have feedback or need assistance with the MCP directory API, please join our Discord server