8 changed files with 221 additions and 5 deletions
-
14apps/article/serializers_admin.py
-
16apps/blog/serializers_admin.py
-
14apps/library/serializers_admin.py
-
14apps/podcast/serializers_admin.py
-
14apps/video/serializers_admin.py
-
6requirements.txt
-
30utils/__init__.py
-
118utils/image_compression.py
@ -0,0 +1,118 @@ |
|||||
|
import io |
||||
|
import logging |
||||
|
import sys |
||||
|
from pathlib import Path |
||||
|
|
||||
|
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: str | None = 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 |
||||
Write
Preview
Loading…
Cancel
Save
Reference in new issue