fixes
This commit is contained in:
1 parent
bb941cff43
commit
6a821c7512
7 files changed
+1768
-326
No files matched your search
@@ -0,0 +1,153 @@
|
||||
"""Zachowawcza, bezstratna dla PNG / wysokiej jakosci dla JPEG, optymalizacja.
|
||||
|
||||
Ten modul NIGDY nie nadpisuje pliku zrodlowego: tworzy jedynie propozycje
|
||||
w pliku tymczasowym. main.py odpowiada za backup, podmiane i transakcje SQL.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
from PIL import Image, ImageOps, PngImagePlugin
|
||||
|
||||
|
||||
class ImageOptimizationError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def _image_save_kwargs(source, image_format):
|
||||
kwargs = {}
|
||||
if source.info.get('icc_profile'):
|
||||
kwargs['icc_profile'] = source.info['icc_profile']
|
||||
if source.info.get('dpi'):
|
||||
kwargs['dpi'] = source.info['dpi']
|
||||
if image_format == 'PNG':
|
||||
pnginfo = PngImagePlugin.PngInfo()
|
||||
text_chunks = getattr(source, 'text', {}) or {}
|
||||
for key, value in text_chunks.items():
|
||||
if isinstance(key, str) and isinstance(value, str):
|
||||
pnginfo.add_text(key, value)
|
||||
if text_chunks:
|
||||
kwargs['pnginfo'] = pnginfo
|
||||
return kwargs
|
||||
|
||||
|
||||
def create_optimized_candidate(
|
||||
path,
|
||||
*,
|
||||
max_width=2400,
|
||||
max_height=3200,
|
||||
max_bytes=2 * 1024 * 1024,
|
||||
jpeg_quality=92,
|
||||
min_jpeg_quality=85,
|
||||
min_gain_percent=5.0,
|
||||
temp_dir=None,
|
||||
):
|
||||
"""Zwraca {status,reason,before,after,width,height,quality,temp_path}.
|
||||
|
||||
Status: ready, skip, error. Wlasnosc pliku temp_path przekazywana wywolujacemu:
|
||||
wywolujacy MUSI usunac plik, takze przy bledzie.
|
||||
"""
|
||||
orig_size = os.path.getsize(path)
|
||||
result = {
|
||||
'status': 'skip', 'reason': '', 'before': orig_size,
|
||||
'after': orig_size, 'width': 0, 'height': 0, 'quality': None,
|
||||
'temp_path': None,
|
||||
}
|
||||
temp_path = None
|
||||
try:
|
||||
with Image.open(path) as source:
|
||||
fmt = source.format
|
||||
ext = Path(path).suffix.lower()
|
||||
if fmt not in ('JPEG', 'PNG'):
|
||||
result['reason'] = f'format {fmt or "nieznany"} nie jest bezpiecznie obslugiwany'
|
||||
return result
|
||||
if fmt == 'JPEG' and ext not in ('.jpg', '.jpeg'):
|
||||
result['reason'] = 'format i rozszerzenie pliku nie sa zgodne'
|
||||
return result
|
||||
if fmt == 'PNG' and ext != '.png':
|
||||
result['reason'] = 'format i rozszerzenie pliku nie sa zgodne'
|
||||
return result
|
||||
if getattr(source, 'is_animated', False) and getattr(source, 'n_frames', 1) > 1:
|
||||
result['reason'] = 'animacja (nie modyfikujemy)'
|
||||
return result
|
||||
|
||||
original_width, original_height = source.size
|
||||
result['width'], result['height'] = original_width, original_height
|
||||
if original_width <= max_width and original_height <= max_height and orig_size <= max_bytes:
|
||||
result['reason'] = 'obraz juz spelnia limity'
|
||||
return result
|
||||
|
||||
# EXIF Orientation ma znaczenie zwlaszcza dla pionowych skanow.
|
||||
working = ImageOps.exif_transpose(source)
|
||||
if (fmt == 'PNG' and working.mode == 'P' and
|
||||
(working.width > max_width or working.height > max_height)):
|
||||
# Skalowanie obrazu paletowego przed konwersja daje nearest-neighbor.
|
||||
# Konwertuj najpierw, aby LANCZOS pracowal na pelnych kolorach.
|
||||
working = working.convert('RGBA' if 'transparency' in working.info else 'RGB')
|
||||
working.thumbnail((max_width, max_height), Image.Resampling.LANCZOS)
|
||||
result['width'], result['height'] = working.size
|
||||
kwargs = _image_save_kwargs(source, fmt)
|
||||
|
||||
fd, temp_path = tempfile.mkstemp(suffix=ext, prefix='.d6-image-', dir=temp_dir)
|
||||
os.close(fd)
|
||||
|
||||
if fmt == 'JPEG':
|
||||
if working.mode not in ('RGB', 'L', 'CMYK'):
|
||||
if working.mode == 'RGBA' or 'transparency' in working.info:
|
||||
result['reason'] = 'JPEG z kanalem alfa (nie modyfikujemy)'
|
||||
return result
|
||||
working = working.convert('RGB')
|
||||
# Pobieraj EXIF juz po prawidlowej korekcji orientacji.
|
||||
try:
|
||||
exif = working.getexif()
|
||||
if exif:
|
||||
kwargs['exif'] = exif.tobytes()
|
||||
except (ValueError, TypeError, OSError):
|
||||
# Uszkodzone EXIF nie powinno blokowac zapisu obrazu.
|
||||
pass
|
||||
qualities = list(range(jpeg_quality, min_jpeg_quality - 1, -3))
|
||||
if min_jpeg_quality not in qualities:
|
||||
qualities.append(min_jpeg_quality)
|
||||
best_quality = None
|
||||
best_size = orig_size
|
||||
for quality in qualities:
|
||||
working.save(
|
||||
temp_path, format='JPEG', optimize=True,
|
||||
progressive=True, subsampling=0, quality=quality, **kwargs,
|
||||
)
|
||||
size = os.path.getsize(temp_path)
|
||||
if size < best_size:
|
||||
best_quality, best_size = quality, size
|
||||
if size <= max_bytes and 100.0 * (orig_size - size) / max(1, orig_size) >= min_gain_percent:
|
||||
break
|
||||
# Jezeli nie udalo sie zejsc pod limit, zastosuj najmniejsza
|
||||
# akceptowalna jakosc. Plik wynikowy bedzie ostatnim kandydatem.
|
||||
if best_quality != quality:
|
||||
working.save(
|
||||
temp_path, format='JPEG', optimize=True,
|
||||
progressive=True, subsampling=0, quality=best_quality or min_jpeg_quality,
|
||||
**kwargs,
|
||||
)
|
||||
result['quality'] = best_quality
|
||||
else:
|
||||
working.save(temp_path, format='PNG', optimize=True, compress_level=9, **kwargs)
|
||||
|
||||
result['after'] = os.path.getsize(temp_path)
|
||||
gain_percent = 100.0 * (orig_size - result['after']) / max(1, orig_size)
|
||||
if result['after'] >= orig_size or gain_percent < min_gain_percent:
|
||||
result['reason'] = f'za mala oszczednosc ({gain_percent:.1f}% < {min_gain_percent:.1f}%)'
|
||||
return result
|
||||
result['status'] = 'ready'
|
||||
result['reason'] = 'osiagnieto limit' if result['after'] <= max_bytes else 'zmniejszono; nadal powyzej limitu'
|
||||
result['temp_path'] = temp_path
|
||||
temp_path = None # przekazujemy plik wywolujacemu
|
||||
return result
|
||||
except Exception as exc:
|
||||
result['status'] = 'error'
|
||||
result['reason'] = f'{type(exc).__name__}: {exc}'
|
||||
return result
|
||||
finally:
|
||||
if temp_path and os.path.exists(temp_path):
|
||||
os.unlink(temp_path)
|
||||
Reference in new issue
Block a user