mod: django-filer настройка (04) преобразование в webp

This commit is contained in:
2026-06-07 20:57:46 +03:00
parent 62fe0722e5
commit f99bbb5c6b
2 changed files with 69 additions and 137 deletions

View File

@@ -2,47 +2,12 @@ import os
import hashlib import hashlib
from io import BytesIO from io import BytesIO
from django.apps import AppConfig from django.apps import AppConfig
from django.core.files.storage import FileSystemStorage
from django.core.files.base import ContentFile from django.core.files.base import ContentFile
from PIL import Image as PILImage from PIL import Image as PILImage
from lpon_site.settings import DEBUG, MEDIA_ROOT from lpon_site.settings import DEBUG, THUMBNAIL_WEBP_QUALITY
# 0. Кастомная функция генерирования имен файлов специально для WebP
def generate_filename_webp(instance, filename):
"""
Генерирует имя файла для сохранения. Если исходный файл был конвертирован в WebP,
гарантирует что расширение файла будет .webp.
"""
_, ext = os.path.splitext(filename)
if ext.lower() in [".jpg", ".jpeg", ".png", ".bmp", ".tiff"]:
base_path = filename.rsplit(ext, 1)[0]
return f"{base_path}.webp"
return filename
# 0.1. Функция для генерирования путей с префиксом 'flr/' для основных файлов
def generate_upload_path_flr(instance, filename):
"""
Генерирует путь для сохранения файла с префиксом 'flr/'.
"""
from filer.utils.generate_filename import randomized
base_path = randomized(instance, filename)
return f'flr/{base_path}'
# 0.2. Функция для генерирования путей с префиксом 'flrm/' для миниатюр
def generate_upload_path_flrm(instance, filename):
"""
Генерирует путь для сохранения миниатюр с префиксом 'flrm/'.
"""
from filer.utils.generate_filename import randomized
base_path = randomized(instance, filename)
return f'flrm/{base_path}'
# 1.
class FrontendConfig(AppConfig): class FrontendConfig(AppConfig):
name = 'frontend' name = 'frontend'
verbose_name = 'Сайт lpon.ru' verbose_name = 'Сайт lpon.ru'
@@ -55,50 +20,43 @@ class FrontendConfig(AppConfig):
admin.site.index_title = 'Добро пожаловать в LPON' admin.site.index_title = 'Добро пожаловать в LPON'
# 2.
class FilerWebPStorage(FileSystemStorage):
"""
Кастомное хранилище для Filer. Основная логика конвертации перенесена
в патч для MultiStorageFieldFile.save, чтобы обновлять метаданные до сохранения.
"""
def _save(self, name: str, content):
return super()._save(name, content)
# Добавляем кастомный конфиг для filer
class CustomFilerConfig(AppConfig): class CustomFilerConfig(AppConfig):
name = 'filer' name = 'filer'
verbose_name = 'Медиафайлы' verbose_name = 'Медиафайлы'
# Конфигурация Django-Filer @staticmethod
FILER_ENABLE_PERMISSIONS = DEBUG def generate_upload_path_flr(instance, filename):
FILER_WHITELIST_FOR_PATH_ACCESS = ( from filer.utils.generate_filename import randomized
'.jpg', '.jpeg', '.png', '.gif', '.svg', '.webp', base_path = randomized(instance, filename)
'.doc', '.docx', '.pdf', '.txt', '.xls', '.xlsx', '.csv', return f'flr/{base_path}'
)
FILER_MAX_UPLOAD_SIZE = 100 * 1024 * 1024 @staticmethod
MIME_TYPE_WHITELIST = ( def generate_upload_path_flrm(instance, filename):
'image/jpeg', 'image/png', 'image/gif', 'image/svg+xml', 'image/webp', from filer.utils.generate_filename import randomized
'application/pdf', 'application/msword', base_path = randomized(instance, filename)
'application/vnd.openxmlformats-officedocument.wordprocessingml.document', return f'flrm/{base_path}'
'text/plain', 'application/vnd.ms-excel',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', class WebPConverter:
'text/csv', def convert_to_webp_if_needed(self, name: str, content):
) _, original_ext = os.path.splitext(name)
THUMBNAIL_ALIASES = { if original_ext.lower() in [".jpg", ".jpeg", ".png", ".bmp", ".tiff"]:
'': { try:
'admin_thumbnail': {'size': (64, 64), 'crop': True}, content.seek(0)
'small': {'size': (256, 256), 'crop': True}, img = PILImage.open(BytesIO(content.read()))
'medium': {'size': (512, 512), 'crop': True}, if img.mode == 'CMYK':
'large': {'size': (1024, 1024), 'crop': 'smart'}, img = img.convert('RGB')
}, buffer = BytesIO()
} img.save(buffer, format="WEBP", quality=THUMBNAIL_WEBP_QUALITY)
THUMBNAIL_PRESERVE_FORMAT = False buffer.seek(0)
THUMBNAIL_FORCE_FORMAT = 'WEBP' new_name = name.rsplit(original_ext, 1)[0] + ".webp"
THUMBNAIL_WEBP_QUALITY = 80 return ContentFile(buffer.read()), new_name, True
THUMBNAIL_ENGINE = 'easy_thumbnails.engines.pil_engine.PilEngine' except Exception as e:
THUMBNAIL_DEBUG = DEBUG import sys
FILE_VALIDATORS = {} print(f"[WebPConverter] ERROR converting {name}: {e}", file=sys.stderr)
content.seek(0)
return content, name, False
content.seek(0)
return content, name, False
def ready(self): def ready(self):
import sys import sys
@@ -106,70 +64,25 @@ class CustomFilerConfig(AppConfig):
print("[CustomFilerConfig.ready] Patching MultiStorageFieldFile.save()...", file=sys.stderr) print("[CustomFilerConfig.ready] Patching MultiStorageFieldFile.save()...", file=sys.stderr)
original_save = MultiStorageFieldFile.save original_save = MultiStorageFieldFile.save
webp_converter = WebPConverter() webp_converter = self.WebPConverter()
def patched_save(self, name, content, save=True): def patched_save(self_instance, name, content, save=True):
"""
Переопределенный save(), который преобразует изображение в WebP, обновляет
метаданные модели (MIME-тип, размер, SHA1) и затем сохраняет.
"""
new_content, new_name, converted = webp_converter.convert_to_webp_if_needed(name, content) new_content, new_name, converted = webp_converter.convert_to_webp_if_needed(name, content)
if converted: if converted:
# Обновляем метаданные прямо в инстансе модели self_instance.instance.mime_type = "image/webp"
self.instance.mime_type = "image/webp" if hasattr(self_instance.instance, 'original_filename') and self_instance.instance.original_filename:
original_filename, _ = os.path.splitext(self_instance.instance.original_filename)
if hasattr(self.instance, 'original_filename') and self.instance.original_filename: self_instance.instance.original_filename = f"{original_filename}.webp"
original_filename, _ = os.path.splitext(self.instance.original_filename)
self.instance.original_filename = f"{original_filename}.webp"
# Вручную пересчитываем размер и SHA1-хэш для нового файла .webp
new_content.seek(0) new_content.seek(0)
file_bytes = new_content.read() file_bytes = new_content.read()
new_content.seek(0) new_content.seek(0)
self_instance.instance._file_size = len(file_bytes)
self.instance._file_size = len(file_bytes) self_instance.instance.sha1 = hashlib.sha1(file_bytes).hexdigest()
self.instance.sha1 = hashlib.sha1(file_bytes).hexdigest() return original_save(self_instance, new_name, new_content, save)
# Вызываем оригинальный метод save() с (возможно) новым контентом и именем
return original_save(self, new_name, new_content, save)
MultiStorageFieldFile.save = patched_save MultiStorageFieldFile.save = patched_save
print("[CustomFilerConfig.ready] MultiStorageFieldFile.save() patched successfully", file=sys.stderr) print("[CustomFilerConfig.ready] MultiStorageFieldFile.save() patched successfully", file=sys.stderr)
# Создаем псевдонимы на уровне модуля для функций, чтобы их мог найти Django
class WebPConverter: generate_upload_path_flr = CustomFilerConfig.generate_upload_path_flr
""" generate_upload_path_flrm = CustomFilerConfig.generate_upload_path_flrm
Класс, инкапсулирующий логику преобразования изображений в WebP.
"""
def convert_to_webp_if_needed(self, name: str, content):
"""
Проверяет формат файла и, если это необходимо, преобразует его в WebP.
Возвращает кортеж (новое_содержимое, новое_имя, флагонвертации).
"""
_, original_ext = os.path.splitext(name)
if original_ext.lower() in [".jpg", ".jpeg", ".png", ".bmp", ".tiff"]:
try:
content.seek(0)
img = PILImage.open(BytesIO(content.read()))
if img.mode == 'CMYK':
img = img.convert('RGB')
buffer = BytesIO()
img.save(buffer, format="WEBP", quality=80)
buffer.seek(0)
new_name = name.rsplit(original_ext, 1)[0] + ".webp"
return ContentFile(buffer.read()), new_name, True
except Exception as e:
import sys
print(f"[WebPConverter] ERROR converting {name}: {e}", file=sys.stderr)
content.seek(0)
return content, name, False
content.seek(0)
return content, name, False

View File

@@ -178,10 +178,9 @@ STATIC_ROOT = PUBLIC_DIR.joinpath('staticfiles')
FILER_STORAGES = { FILER_STORAGES = {
'public': { 'public': {
'main': { 'main': {
# Используем кастомное хранилище FilerWebPStorage для преобразования в WebP # Используем стандартное хранилище Django. Логика конвертации в apps.py
'ENGINE': 'frontend.apps.FilerWebPStorage', 'ENGINE': 'django.core.files.storage.FileSystemStorage',
'OPTIONS': { 'OPTIONS': {
# location должна быть MEDIA_ROOT для корректной генерации URL!
'location': str(MEDIA_ROOT), 'location': str(MEDIA_ROOT),
}, },
# UPLOAD_TO функция добавляет 'flr/' префикс для более компактных путей в шаблонах # UPLOAD_TO функция добавляет 'flr/' префикс для более компактных путей в шаблонах
@@ -209,6 +208,26 @@ FILER_STORAGES = {
THUMBNAIL_PRESERVE_FORMAT = False # Не сохранять оригинальный формат для миниатюр THUMBNAIL_PRESERVE_FORMAT = False # Не сохранять оригинальный формат для миниатюр
THUMBNAIL_FORMAT = 'WEBP' # Конвертировать все миниатюры в WebP THUMBNAIL_FORMAT = 'WEBP' # Конвертировать все миниатюры в WebP
THUMBNAIL_QUALITY = 80 # Качество WebP (достаточно 75-85 для миниатюр) THUMBNAIL_QUALITY = 80 # Качество WebP (достаточно 75-85 для миниатюр)
# Кастомная настройка для встроенного конвертора загружаемых файлов в WebP (см. apps.py)
THUMBNAIL_WEBP_QUALITY = 80 # Качество для WebP
FILER_ENABLE_PERMISSIONS = DEBUG
FILER_WHITELIST_FOR_PATH_ACCESS = (
'.jpg', '.jpeg', '.png', '.gif', '.svg', '.webp',
'.doc', '.docx', '.pdf', '.txt', '.xls', '.xlsx', '.csv',
)
FILER_MAX_UPLOAD_SIZE = 100 * 1024 * 1024
MIME_TYPE_WHITELIST = (
'image/jpeg', 'image/png', 'image/gif', 'image/svg+xml', 'image/webp',
'application/pdf', 'application/msword',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'text/plain', 'application/vnd.ms-excel',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'text/csv',
)
THUMBNAIL_ENGINE = 'easy_thumbnails.engines.pil_engine.PilEngine'
THUMBNAIL_DEBUG = DEBUG
FILE_VALIDATORS = {}
# Размеры миниатюр для разных использований # Размеры миниатюр для разных использований
THUMBNAIL_ALIASES = { THUMBNAIL_ALIASES = {
@@ -220,4 +239,4 @@ THUMBNAIL_ALIASES = {
'medium': {'size': (512, 512), 'crop': True}, 'medium': {'size': (512, 512), 'crop': True},
'large': {'size': (1024, 1024), 'crop': 'smart'}, 'large': {'size': (1024, 1024), 'crop': 'smart'},
}, },
} }