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