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

This commit is contained in:
2026-06-07 20:36:54 +03:00
parent a6d9f963b3
commit 62fe0722e5
2 changed files with 183 additions and 134 deletions

View File

@@ -1,154 +1,175 @@
import os import os
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.storage import FileSystemStorage
from django.core.files.base import ContentFile from django.core.files.base import ContentFile
from django.db.models.signals import pre_save
from PIL import Image as PILImage from PIL import Image as PILImage
from lpon_site.settings import DEBUG, MEDIA_ROOT
# 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}'
from lpon_site.settings import DEBUG
# 1. # 1.
class FrontendConfig(AppConfig): class FrontendConfig(AppConfig):
name = 'frontend' name = 'frontend'
verbose_name = 'Сайт lpon.ru' verbose_name = 'Сайт lpon.ru'
# Переключаем на стандартный AutoField (до 2 млрд записей)
default_auto_field = 'django.db.models.AutoField' default_auto_field = 'django.db.models.AutoField'
def ready(self): def ready(self):
"""
Вызывается при инициализации приложения.
Настраиваем админ-сайт (заголовки и т.п.)
"""
from django.contrib import admin from django.contrib import admin
admin.site.site_header = 'Управление LPON' admin.site.site_header = 'Управление LPON'
admin.site.site_title = 'LPON Administrator' admin.site.site_title = 'LPON Administrator'
admin.site.index_title = 'Добро пожаловать в LPON' admin.site.index_title = 'Добро пожаловать в LPON'
## Если надо, импортируем сигналы при запуске приложения.
# 2. Создаем кастомное файловое хранилище специально для filer # 2.
class FilerWebPStorage(FileSystemStorage): class FilerWebPStorage(FileSystemStorage):
def _save(self, name, content): """
filename, ext = os.path.splitext(name) Кастомное хранилище для Filer. Основная логика конвертации перенесена
в патч для MultiStorageFieldFile.save, чтобы обновлять метаданные до сохранения.
# Обрабатываем только форматы картинок, исключая webp, svg и гифки """
if ext.lower() in [".jpg", ".jpeg", ".png", ".bmp", ".tiff"]: def _save(self, name: str, content):
try:
# Читаем исходные байты файла
content.seek(0)
image_bytes = content.read()
# Конвертируем в WebP через Pillow
img = PILImage.open(BytesIO(image_bytes))
buffer = BytesIO()
img.save(buffer, format="WEBP", quality=85)
buffer.seek(0)
# Подменяем имя и контент на честный WebP
name = f"{filename}.webp"
content = ContentFile(buffer.read(), name=os.path.basename(name))
except Exception as e:
print(f"[Filer Storage WebP Error]: {e}")
# Передаем управление стандартному сохранению на диск
return super()._save(name, content) return super()._save(name, content)
# Добавляем кастомный конфиг для filer
# Добавляем кастомный конфиг для filer, чтобы переименовать verbose_name и добавить конфиги
class CustomFilerConfig(AppConfig): class CustomFilerConfig(AppConfig):
name = 'filer' name = 'filer'
verbose_name = 'Медиафайлы' # Переименование вкладки в админке verbose_name = 'Медиафайлы'
# ======================================================================== # Конфигурация Django-Filer
# Конфигурация Django-Filer (для загрузки файлов через админку) FILER_ENABLE_PERMISSIONS = DEBUG
# ========================================================================
FILER_ENABLE_PERMISSIONS = DEBUG # В production установить True для ограничения доступа
# Разрешенные расширения файлов для FilerImageField
FILER_WHITELIST_FOR_PATH_ACCESS = ( FILER_WHITELIST_FOR_PATH_ACCESS = (
# ПОДУМАТЬ: поддержка '.heic' требует дополнительных пакетов (и разных для прода и дева) + обработчик сигналов '.jpg', '.jpeg', '.png', '.gif', '.svg', '.webp',
# для автоматической конвертации .heic в .webp при загрузке. Пока отключаем, и оставим на будущее '.doc', '.docx', '.pdf', '.txt', '.xls', '.xlsx', '.csv',
'.jpg', '.jpeg', '.png', '.gif', '.svg', '.webp', # Изображения
'.doc', '.docx', '.pdf', '.txt', '.xls', '.xlsx', '.csv', # Документы
) )
# Максимальный размер загружаемого файла (в байтах): 100 MB
FILER_MAX_UPLOAD_SIZE = 100 * 1024 * 1024 FILER_MAX_UPLOAD_SIZE = 100 * 1024 * 1024
# MIME-типы разрешенные для загрузки
MIME_TYPE_WHITELIST = ( MIME_TYPE_WHITELIST = (
'image/jpeg', 'image/jpeg', 'image/png', 'image/gif', 'image/svg+xml', 'image/webp',
'image/png', 'application/pdf', 'application/msword',
'image/gif', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'image/svg+xml', 'text/plain', 'application/vnd.ms-excel',
'image/webp', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'application/pdf',
'application/msword', # .doc
'application/vnd.openxmlformats-officedocument.wordprocessingml.document', # .docx
'text/plain',
'application/vnd.ms-excel', # .xls
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', # .xlsx
'text/csv', 'text/csv',
) )
# Tiny Image Specifications для автоматических вариантов изображений
THUMBNAIL_ALIASES = { THUMBNAIL_ALIASES = {
'': { '': {
# Для галерей и витрин 'admin_thumbnail': {'size': (64, 64), 'crop': True},
# Кратные 16х16 картинки для ускорения GPU-рендера браузеров, retina-дисплеев и "копеечная" экономии на сервере 'small': {'size': (256, 256), 'crop': True},
'admin_thumbnail': {'size': (64, 64), 'crop': True}, # Для админки 'medium': {'size': (512, 512), 'crop': True},
'small': {'size': (256, 256), 'crop': True}, # Квадрат 256x256 для миниатюр 'large': {'size': (1024, 1024), 'crop': 'smart'},
'medium': {'size': (512, 512), 'crop': True}, # Квадрат 512x512 для просмотра
'large': {'size': (1024, 1024), 'crop': 'smart'}, # Большое изображение с умным crop
}, },
} }
THUMBNAIL_PRESERVE_FORMAT = False
# Форматы сохранения для thumbnail'ов (миниатюр)
THUMBNAIL_PRESERVE_FORMAT = False # Не сохранять оригинальный формат для thumbnails
# THUMBNAIL_FORMAT = 'WEBP' # Конвертировать все thumbnails в WebP
THUMBNAIL_FORCE_FORMAT = 'WEBP' THUMBNAIL_FORCE_FORMAT = 'WEBP'
THUMBNAIL_WEBP_QUALITY = 80 # Качество WebP (достаточно 75-85 для thumbnails) THUMBNAIL_WEBP_QUALITY = 80
# Интерпретатор для обработки изображений
THUMBNAIL_ENGINE = 'easy_thumbnails.engines.pil_engine.PilEngine' THUMBNAIL_ENGINE = 'easy_thumbnails.engines.pil_engine.PilEngine'
THUMBNAIL_DEBUG = DEBUG
# Качество JPEG при сжатии (0-100, по умолчанию 85)
# THUMBNAIL_QUALITY = 85
# Источник кеша для миниатюр
THUMBNAIL_DEBUG = DEBUG # Показывать ошибки генерирования миниатюр в debug режиме
# Валидаторы файлов (пусто = без ограничений)
FILE_VALIDATORS = {} FILE_VALIDATORS = {}
def ready(self): def ready(self):
from django.db.models.signals import pre_save import sys
from filer import settings as filer_settings from filer.fields.multistorage_file import MultiStorageFieldFile
from filer.models.imagemodels import Image as FilerImage
# ХАК 1: Принудительно заставляем filer использовать наше кастомное хранилище. print("[CustomFilerConfig.ready] Patching MultiStorageFieldFile.save()...", file=sys.stderr)
# Теперь все оригиналы файлов будут сохраняться через класс FilerWebPStorage. original_save = MultiStorageFieldFile.save
filer_settings.FILER_STORAGES["public"]["main"] = { webp_converter = WebPConverter()
"ENGINE": "frontend.apps.FilerWebPStorage",
"OPTIONS": {},
}
# ХАК 2: Подключаем легкий сигнал pre_save только для того, чтобы def patched_save(self, name, content, save=True):
# прописать в базу данных правильный mime_type и расширение для админки. """
pre_save.connect(self.fix_meta_before_db_save, sender=FilerImage) Переопределенный save(), который преобразует изображение в WebP, обновляет
метаданные модели (MIME-тип, размер, SHA1) и затем сохраняет.
"""
new_content, new_name, converted = webp_converter.convert_to_webp_if_needed(name, content)
@staticmethod if converted:
def fix_meta_before_db_save(sender, instance, **kwargs): # Обновляем метаданные прямо в инстансе модели
if not instance.file or not instance.file.name: self.instance.mime_type = "image/webp"
return
if hasattr(self.instance, 'original_filename') and self.instance.original_filename:
original_filename, _ = os.path.splitext(self.instance.original_filename)
self.instance.original_filename = f"{original_filename}.webp"
filename, ext = os.path.splitext(instance.file.name) # Вручную пересчитываем размер и SHA1-хэш для нового файла .webp
if ext.lower() in [".webp", ".svg", ".gif"]: new_content.seek(0)
return file_bytes = new_content.read()
new_content.seek(0)
self.instance._file_size = len(file_bytes)
self.instance.sha1 = hashlib.sha1(file_bytes).hexdigest()
# Меняем метаданные для базы данных, так как на диске файл СТОПРОЦЕНТНО будет .webp # Вызываем оригинальный метод save() с (возможно) новым контентом и именем
instance.file.name = f"{filename}.webp" return original_save(self, new_name, new_content, save)
instance.mime_type = "image/webp"
MultiStorageFieldFile.save = patched_save
print("[CustomFilerConfig.ready] MultiStorageFieldFile.save() patched successfully", file=sys.stderr)
class WebPConverter:
"""
Класс, инкапсулирующий логику преобразования изображений в 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

@@ -160,36 +160,64 @@ MEDIA_ROOT = PUBLIC_DIR.joinpath('media')
STATICFILES_DIRS = [PUBLIC_DIR.joinpath('static')] STATICFILES_DIRS = [PUBLIC_DIR.joinpath('static')]
STATIC_ROOT = PUBLIC_DIR.joinpath('staticfiles') STATIC_ROOT = PUBLIC_DIR.joinpath('staticfiles')
# ============================================================================
# Django-Filer Configuration - WebP Conversion Storage
# ============================================================================
# Настройка хранилища filer для автоматического преобразования изображений в WebP
# ВАЖНО: эта конфигурация должна быть в settings.py (не в class attribute),
# т.к. Django-filer загружает её ДО инициализации app configs
# ВАЖНО: location должна быть равна MEDIA_ROOT, иначе Django не сможет
# сгенерировать правильные URL для файлов (URL генерируется как MEDIA_URL + relative_path)
#
# Структура папок:
# public/media/
# ├── flr/ <- основные загруженные файлы (картинки)
# ├── flrm/ <- миниатюры (thumbnails)
# ├── filer_public/ <- старая структура (больше не используется)
# └── filer_public_thumbnails/ <- старая структура (больше не используется)
FILER_STORAGES = {
'public': {
'main': {
# Используем кастомное хранилище FilerWebPStorage для преобразования в WebP
'ENGINE': 'frontend.apps.FilerWebPStorage',
'OPTIONS': {
# location должна быть MEDIA_ROOT для корректной генерации URL!
'location': str(MEDIA_ROOT),
},
# UPLOAD_TO функция добавляет 'flr/' префикс для более компактных путей в шаблонах
'UPLOAD_TO': 'frontend.apps.generate_upload_path_flr',
'UPLOAD_TO_PREFIX': '',
},
'thumbnails': {
'ENGINE': 'django.core.files.storage.FileSystemStorage',
'OPTIONS': {
'location': str(MEDIA_ROOT),
},
# Миниатюры идят в папку flrm через UPLOAD_TO функцию
'UPLOAD_TO': 'frontend.apps.generate_upload_path_flrm',
'THUMBNAIL_OPTIONS': {
'base_dir': 'flrm',
},
},
},
}
# ============================================================================ # ============================================================================
# Easy Thumbnails Configuration (для создания миниатюр в WebP) # Easy-Thumbnails Configuration - WebP Generation
# ============================================================================ # ============================================================================
# Настройка генерирования миниатюр в формате WebP вместо JPEG/PNG
THUMBNAIL_PRESERVE_FORMAT = False # Не сохранять оригинальный формат для миниатюр
THUMBNAIL_FORMAT = 'WEBP' # Конвертировать все миниатюры в WebP
THUMBNAIL_QUALITY = 80 # Качество WebP (достаточно 75-85 для миниатюр)
# Не сохранять оригинальный формат — все миниатюры в WebP # Размеры миниатюр для разных использований
# THUMBNAIL_PRESERVE_FORMAT = False THUMBNAIL_ALIASES = {
# '': {
# # Все миниатюры конвертировать в WebP # Для админ-интерфейса
# THUMBNAIL_FORMAT = 'WEBP' 'admin_thumbnail': {'size': (64, 64), 'crop': True},
# THUMBNAIL_PRESERVE_FORMAT = False # Для фронтенда
'small': {'size': (256, 256), 'crop': True},
# 'medium': {'size': (512, 512), 'crop': True},
# # Качество WebP (75-85 достаточно для миниатюр) 'large': {'size': (1024, 1024), 'crop': 'smart'},
# THUMBNAIL_QUALITY = 80 },
# }
# # Интерпретатор для обработки изображений (Pillow)
# THUMBNAIL_ENGINE = 'easy_thumbnails.engines.pil_engine.PilEngine'
#
# # Показывать ошибки при создании миниатюр в debug режиме
# THUMBNAIL_DEBUG = DEBUG
#
# # Размеры для миниатюр (для фронтенд-галереи)
# THUMBNAIL_ALIASES = {
# '': {
# 'admin_thumbnail': {'size': (64, 64), 'crop': True},
# 'small': {'size': (256, 256), 'crop': True},
# 'medium': {'size': (512, 512), 'crop': True},
# 'large': {'size': (1024, 1024), 'crop': 'smart'},
# },
# }