""" 图片处理工具模块 支持图片的格式转换、压缩、缩略图生成等功能 """ import base64 import io from PIL import ExifTags, Image from yuxi.utils import logger class ImageProcessor: """图片处理类""" # 支持的图片格式 SUPPORTED_FORMATS = {"JPEG", "PNG", "WebP", "GIF", "BMP"} # 最大文件大小(5MB) MAX_FILE_SIZE = 5 * 1024 * 1024 # 缩略图尺寸 THUMBNAIL_SIZE = (200, 200) def process_image(self, image_data: bytes, original_filename: str = "") -> dict: """ 处理上传的图片 Args: image_data: 图片二进制数据 original_filename: 原始文件名 Returns: dict: 包含处理结果的字典 """ try: # 验证图片格式 img_format, _ = self._validate_image_format(image_data) if img_format not in self.SUPPORTED_FORMATS: raise ValueError(f"不支持的图片格式: {img_format}") # 加载图片 with Image.open(io.BytesIO(image_data)) as img: # 处理EXIF方向信息 img = self._fix_image_orientation(img) # 生成缩略图 thumbnail_data = self._generate_thumbnail(img) # 压缩主图片(如果需要) processed_data, final_format = self._compress_image(img, img_format) # 转换为 base64 base64_data = base64.b64encode(processed_data).decode("utf-8") base64_thumbnail = base64.b64encode(thumbnail_data).decode("utf-8") # 获取图片信息 width, height = img.size mime_type = f"image/{final_format.lower()}" return { "success": True, "image_content": base64_data, "thumbnail_content": base64_thumbnail, "width": width, "height": height, "format": final_format, "mime_type": mime_type, "size_bytes": len(processed_data), "original_filename": original_filename, } except Exception as e: logger.error(f"图片处理失败: {str(e)}") return {"success": False, "error": str(e)} def _validate_image_format(self, image_data: bytes) -> tuple[str, str]: """验证图片格式并返回格式信息""" try: with Image.open(io.BytesIO(image_data)) as img: return img.format, img.mode except Exception as e: raise ValueError(f"无效的图片格式: {str(e)}") def _fix_image_orientation(self, img: Image.Image) -> Image.Image: """根据EXIF信息修正图片方向""" try: if hasattr(img, "_getexif"): exif = img._getexif() if exif is not None: for tag, value in exif.items(): if tag in ExifTags.TAGS and ExifTags.TAGS[tag] == "Orientation": if value == 3: img = img.rotate(180, expand=True) elif value == 6: img = img.rotate(270, expand=True) elif value == 8: img = img.rotate(90, expand=True) break except Exception as e: logger.warning(f"修正图片方向失败,使用原始方向: {str(e)}") return img def _generate_thumbnail(self, img: Image.Image) -> bytes: """生成缩略图""" try: thumbnail = self._convert_to_rgb_for_export(img) # 生成缩略图,保持宽高比 thumbnail.thumbnail(self.THUMBNAIL_SIZE, Image.Resampling.LANCZOS) # 转换为JPEG格式 with io.BytesIO() as output: thumbnail.save(output, format="JPEG", quality=85, optimize=True) return output.getvalue() except Exception as e: logger.error(f"生成缩略图失败: {str(e)}") # 如果缩略图生成失败,返回一个1x1的透明图片 with io.BytesIO() as output: empty_img = Image.new("RGB", (1, 1), color="white") empty_img.save(output, format="JPEG", quality=85) return output.getvalue() def _convert_to_rgb_for_export(self, img: Image.Image) -> Image.Image: """转换为 RGB,同时把透明像素按白底合成,避免隐藏颜色变成可见像素。""" if img.mode == "RGB": return img.copy() has_alpha = img.mode in ("RGBA", "LA") or (img.mode == "P" and "transparency" in img.info) if not has_alpha: return img.convert("RGB") rgba_img = img.convert("RGBA") background = Image.new("RGBA", rgba_img.size, (255, 255, 255, 255)) background.alpha_composite(rgba_img) return background.convert("RGB") def _compress_image(self, img: Image.Image, original_format: str) -> tuple[bytes, str]: """ 压缩图片,如果超过大小限制 Args: img: PIL Image对象 original_format: 原始格式 Returns: Tuple[bytes, str]: (压缩后的图片数据, 最终格式) """ processed_img = self._convert_to_rgb_for_export(img) # 尝试保持原始格式,但优先使用JPEG(更好的压缩) target_format = "JPEG" if original_format != "PNG" else "PNG" # 初始质量设置 quality = 85 with io.BytesIO() as output: # 第一次保存以检查大小 processed_img.save(output, format=target_format, quality=quality, optimize=True) compressed_data = output.getvalue() # 如果文件大小合适,直接返回 if len(compressed_data) <= self.MAX_FILE_SIZE: return compressed_data, target_format # 如果文件太大,逐步降低质量 while len(compressed_data) > self.MAX_FILE_SIZE and quality > 10: quality -= 10 output.seek(0) output.truncate(0) processed_img.save(output, format=target_format, quality=quality, optimize=True) compressed_data = output.getvalue() # 如果质量降到最低仍然太大,尝试缩小尺寸 if len(compressed_data) > self.MAX_FILE_SIZE: # 逐步缩小尺寸 scale_factor = 0.9 while len(compressed_data) > self.MAX_FILE_SIZE and scale_factor > 0.3: new_width = int(processed_img.width * scale_factor) new_height = int(processed_img.height * scale_factor) resized_img = processed_img.resize((new_width, new_height), Image.Resampling.LANCZOS) output.seek(0) output.truncate(0) resized_img.save(output, format=target_format, quality=85, optimize=True) compressed_data = output.getvalue() scale_factor -= 0.1 return compressed_data, target_format # 全局实例 image_processor = ImageProcessor() def process_uploaded_image(image_data: bytes, filename: str = "") -> dict: """ 处理上传的图片(便捷函数) Args: image_data: 图片二进制数据 filename: 文件名 Returns: dict: 处理结果 """ return image_processor.process_image(image_data, filename)