You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

119 lines
4.1 KiB

import io
import logging
import sys
from pathlib import Path
from typing import Optional
from PIL import Image
logger = logging.getLogger(__name__)
DEBUG_PREFIX = "[image-compression]"
def _load_imgcompress():
try:
from imgcompress import ( # type: ignore
CompressionMode,
CompressionSettings,
ImageCompressor,
)
print(f"{DEBUG_PREFIX} imported imgcompress from installed environment")
return ImageCompressor, CompressionSettings, CompressionMode
except ImportError as exc:
print(f"{DEBUG_PREFIX} installed import failed: {exc}")
repo_root = Path(__file__).resolve().parents[2]
src_path = repo_root / "image-compressor" / "src"
if src_path.exists():
sys.path.insert(0, str(src_path))
print(f"{DEBUG_PREFIX} trying local source path: {src_path}")
from imgcompress import ( # type: ignore
CompressionMode,
CompressionSettings,
ImageCompressor,
)
print(f"{DEBUG_PREFIX} imported imgcompress from local source")
return ImageCompressor, CompressionSettings, CompressionMode
raise
def is_compressible_image(filename: str, content_type: Optional[str] = None) -> bool:
name = (filename or "").lower()
ctype = (content_type or "").lower()
if name.endswith(".gif") or name.endswith(".svg"):
return False
if ctype in {"image/gif", "image/svg+xml"}:
return False
compressible_exts = (".jpg", ".jpeg", ".png", ".webp", ".bmp", ".tif", ".tiff", ".avif")
return name.endswith(compressible_exts) or ctype.startswith("image/")
def compress_image_bytes(data: bytes, filename: str) -> bytes:
if not data:
return data
print(f"{DEBUG_PREFIX} compress_image_bytes start file={filename} size={len(data)}")
ImageCompressor, CompressionSettings, CompressionMode = _load_imgcompress()
settings = CompressionSettings(
mode=CompressionMode.VISUALLY_LOSSLESS,
allow_format_switch=False,
strip_metadata=False,
png_use_zopfli=False,
)
compressor = ImageCompressor(settings=settings)
result = compressor.compress_bytes(data, Path(filename))
final_bytes = result.payload or data
print(
f"{DEBUG_PREFIX} compress_image_bytes done file={filename} "
f"original={len(data)} final={len(final_bytes)} skipped={getattr(result, 'skipped', False)} "
f"note={getattr(result, 'note', '')}"
)
return final_bytes
def maybe_compress_uploaded_file(file_obj):
filename = getattr(file_obj, "name", "upload")
content_type = getattr(file_obj, "content_type", None)
print(
f"{DEBUG_PREFIX} received upload file={filename} "
f"content_type={content_type} size={getattr(file_obj, 'size', 'unknown')}"
)
if not is_compressible_image(filename, content_type):
print(f"{DEBUG_PREFIX} skip non-compressible file={filename}")
if hasattr(file_obj, "seek"):
file_obj.seek(0)
return None
try:
raw_bytes = file_obj.read()
if hasattr(file_obj, "seek"):
file_obj.seek(0)
if not raw_bytes:
print(f"{DEBUG_PREFIX} skip empty payload file={filename}")
return None
with Image.open(io.BytesIO(raw_bytes)) as img:
img.verify()
compressed = compress_image_bytes(raw_bytes, filename)
if compressed and len(compressed) <= len(raw_bytes):
print(
f"{DEBUG_PREFIX} accepted file={filename} "
f"original={len(raw_bytes)} compressed={len(compressed)}"
)
return compressed
print(
f"{DEBUG_PREFIX} keeping original file={filename} "
f"original={len(raw_bytes)} candidate={len(compressed) if compressed else 'none'}"
)
return raw_bytes
except Exception as exc:
print(f"{DEBUG_PREFIX} failed file={filename} error={exc}")
logger.warning("Image compression skipped for %s: %s", filename, exc)
if hasattr(file_obj, "seek"):
file_obj.seek(0)
return None