add: Настройке Django-Filer и Easy-Thumbnails для поддержки HEIC/HEIF с автоконвертацией вWebP в проектах на Django

This commit is contained in:
2026-06-10 11:14:06 +03:00
parent 583b6943c8
commit a9dc0a08c2
2 changed files with 499 additions and 1 deletions
+2 -1
View File
@@ -28,9 +28,10 @@
* [ChartDB в K3s](kubernetes/k3s-chartdb.md) — графический редактор схем баз данных * [ChartDB в K3s](kubernetes/k3s-chartdb.md) — графический редактор схем баз данных
* [Развертывание Metabase в K3s](kubernetes/k3s-metabase.md) — BI-платформа для анализа данных * [Развертывание Metabase в K3s](kubernetes/k3s-metabase.md) — BI-платформа для анализа данных
## Python ## Python & Django
* [Устранение проблем при установке Python-коннектора mysqlclient (MySQL/MariaDB)](python/python-mysql.md) * [Устранение проблем при установке Python-коннектора mysqlclient (MySQL/MariaDB)](python/python-mysql.md)
* [Python-скрипт как служба Linux](python/python_as_service.md) * [Python-скрипт как служба Linux](python/python_as_service.md)
* [Как настроить поддержку HEIC/HEIF в Django-filer](python/enabling-heic-image-support-in-django-filer.md)
## Linux (возможно в специфике Orange Pi / Raspberry Pi) ## Linux (возможно в специфике Orange Pi / Raspberry Pi)
* [Установка (перенос) системы на NVMe или eMMC (для Orange Pi 5 Plus)](raspberry-and-orange-pi/opi5plus-move-system-to-nvme-or-emmc.md) * [Установка (перенос) системы на NVMe или eMMC (для Orange Pi 5 Plus)](raspberry-and-orange-pi/opi5plus-move-system-to-nvme-or-emmc.md)
@@ -0,0 +1,497 @@
# Настройке Django-Filer и Easy-Thumbnails с поддержкой HEIC/HEIF и автоконве­ртацией в WebP в проектах на Django
Django-Filer — популярная библиотека для управления файлами и изобра­жениями в Django. Она использует Easy-Thumbnails
для создания миниатюр изображений. Но, к сожалению, в документации не все описано. Но главное, что поддержка
изображений HEIC/HEIF вообще не преду­смотрена. С этими форматами вообще не все так просто в мире Python.
Наиболее популярная библиотека PIL (Pillow) из коробки **не поддерживает HEIC/HEIF**. Это форматы от Apple для iPhone и большинство приложекний для работы с графикой мира Wiindows, Linux и даже MacOS не поддерживают их.
Но есть сторонняя библиотека `pillow-heif`, которая добавляет поддержку HEIC/HEIF в Pillow. И мы можем использовать
её для обработки этих форматов в Django-Filer и Easy-Thumbnails.
## Содержание
1. [Установка пакетов](#установка-пакетов)
2. [Настройка FILER_STORAGES](#настройка-filer_storages)
3. [Конфигурация Thumbnails](#конфигурация-thumbnails)
4. [Автоко­нвертация JPEG/HEIC в WebP](#автоко­нвертация-jpegheic-в-webp)
5. [Решение проблем](#решение-проблем)
## Так же рекомендую сверяться с официальной докуме­нтацией:
- [Настройки Django-Filer](https://django-filer.readthedocs.io/en/latest/settings.html)
- [Настройки Easy-Thumbnails](https://easy-thumbnails.readthedocs.io/en/latest/ref/settings/)
- [Опции Pillow-Heif](https://pillow-heif.readthedocs.io/en/latest/options.html)
## Установка пакетов
### Системные пакеты
Для поддержки HEIC/HEIF в Pillow нам нужно установить в системк `libheif` и его зависимости. На разных системах это
делается по-разному:
- **Ubuntu/Debian**:
```bash
sudo apt-get update
sudo apt-get install -y libheif-dev libde265-dev
```
- **Fedora**:
```bash
sudo dnf install -y libheif-devel libde265-devel
```
- **MacOS (через Homebrew)**:
```bash
brew install libheif
```
### Установка django-filer и pillow-heif
При установке `django-filer` он автома­тически добавит `pillow` и `easy-thumbnails`. Для поддержки HEIC/HEIF нам
нужно дополни­тельно установить `pillow-heif`:
- **Установка через pip**:
```bash
pip install django-filer pillow-heif
```
- **Установка через Poetry**:
```bash
poetry add django-filer pillow-heif
```
### Почему нужен pillow-heif?
`pillow-heif` регистрирует себя как плагин к Pillow и добавляет эту поддержку:
```python
import pillow_heif
pillow_heif.register_heif_opener() # Теперь PIL может открывать HEIC/HEIF
```
## Настройка FILER_STORAGES
У django-filer много настроек который делаются в `settings.py` вашего django-проекта, но не все из них докуме­нтированы.
В частности `FILER_STORAGES` содержит не описаный ключ `base_dir`, котрый задает имя промежу­точного каталог для хранеия миниатюр.
Например:
```python
FILER_STORAGES = {
'public': {
'main': {
'ENGINE': 'filer.storage.PublicFileSystemStorage',
'OPTIONS': {
'location': MEDIA_ROOT / 'flr', # где хранятся загруженные файлы
'base_url': MEDIA_URL + 'flr/', # URL публичного доступа
},
'UPLOAD_TO': 'filer.utils.generate_filename.randomized',
'UPLOAD_TO_PREFIX': '',
},
'thumbnails': {
'ENGINE': 'filer.storage.PublicFileSystemStorage',
'OPTIONS': {
'location': MEDIA_ROOT / 'flrm', # где хранятся миниатюры
'base_url': MEDIA_URL + 'flrm/', # URL для миниатюр
},
# ВАЖНО: Переопре­деляем THUMBNAIL_OPTIONS
'THUMBNAIL_OPTIONS': {
'base_dir': '', # Отключаем default prefix 'filer_public_thumbnails'
},
},
},
}
```
### Проблема с `base_dir`
**Проблема:** В официальной документации filer **нет упоминания** о ключе `base_dir` в `THUMBNAIL_OPTIONS`.
**Почему это важно:** По умолчанию filer добавляет prefix-каталог `'filer_public_thumbnails'` ко всем миниатюрам:
```
flrm/
├── filer_public_thumbnails/
│ └── af
│ └── a3
│ └── 6aa36f698a9844149e9a-f3a849da1223
│ └── [миниатюры]
```
Пути и URL у миниатюр (thumbnails) и так длинные, и добавление еще одного уровня вложенности каталогов, да еще
с таким длинным именем, может нешуточно раздуть резуль­тирующий html-файл или вызвать проблемы с файловой системой
(особенно актуально для Windows).
**Решение:** Явно указываем `'base_dir': ''` чтобы миниатюры лежали прямо в `flrm/`:
```
flrm/
├── af
│ └── a3
│ └── 6aa36f698a9844149e9a-f3a849da1223
│ └── [миниатюры]
```
Это намного удобнее и экономит уровни вложенности папок. — ## Конфигурация Thumbnails
### Easy-Thumbnails настройки
```python
# Основные параметры
THUMBNAIL_PRESERVE_FORMAT = False # Не сохранять исходный формат
THUMBNAIL_FORMAT = 'WEBP' # Создавать миниатюры в WebP (экономия места на диске и трафика в 3–4 раза)
THUMBNAIL_QUALITY = 80 # Качество сжатия (0–100)
THUMBNAIL_WEBP_QUALITY = THUMBNAIL_QUALITY # Наша кастомная настройка… Качество сжатие исходных изображений в WebP
# Движок обработки
THUMBNAIL_ENGINE = 'easy_thumbnails.engines.pil_engine.PilEngine'
# КРИТИЧНО: Для этих файлов будет сохранен формат при генерации миниатюр
THUMBNAIL_PRESERVE_EXTENSIONS = ['png', 'gif', 'webp']
# Нейминг миниатюр… Формат, что-то типа: you-image.webp__40×40_q85_crop_subsampling-2.jpg
THUMBNAIL_NAMER = 'easy_thumbnails.namers.default'
# Отладка (устана­вливает в соответствии с уровнем отладки всего проекта django)
THUMBNAIL_VERBOSE = DEBUG
# Преду­становки размеров. Можно вообще не объявлять, т. к. алиасы миниатюр можно настроить из админки filer, а размеры
# миниатюр для самой filer-админки зашиты внутрь библиотекb и не изменяются (это 40×40, 210×210 и 420×420)
THUMBNAIL_ALIASES = {
'': {
'small': {'size': (256, 256), 'crop': True},
'medium': {'size': (512, 512), 'crop': True},
'large': {'size': (1024, 1024), 'crop': 'smart'},
},
}
```
### Важные параметры для Filer
Для поддержки HEIC/HEIF важно добавить в «белый список» MMIME-типы и равсширения файлов:
```python
# Типы файлов
MIME_TYPE_WHITELIST = (
'image/jpeg',
'image/png',
'image/gif',
'image/svg+xml',
'image/webp',
'image/heic', # HEIC поддержка
'image/heif', # HEIF поддержка
'application/pdf',
# … другие типы
)
# Расширения файлов
FILER_WHITELIST_FOR_PATH_ACCESS = (
'.jpg', '.jpeg', '.png', '.gif', '.svg', '.webp',
'.heic', '.heif', # HEIC/HEIF поддержка
'.doc', '.docx', '.pdf', '.xls', '.xlsx', '.txt', '.csv',
)
# Ограничения загрузки
FILER_UPLOADER_MAX_FILE_SIZE = 100 * 1024 * 1024 # 100 MB
FILER_MAX_IMAGE_PIXELS = 4096 * 4096 # Макс разрешение
```
## Автоко­нвертация JPEG/HEIC/HEIF в WebP
### Проблема
Когда пользователь загружает HEIC или JPEG:
1. Браузер отправляет оригинальный файл (тяжелый).
2. Filer сохраняет его как есть.
3. Результат: большие файлы в хранилище, медленная загрузка, много трафика.
4. Для HEIC/HEIF никаких не создаётся вообще, т. к. fille не считает их картинками (A), PIL не может их открыть (B) и,
браузеры их вообще не поддерживают ©.
### Решение: Патчинг MultiStorageFieldFile.save()
Переопре­деляем метод сохранения файлов filer в `apps.py`:
```python
# frontend/apps.py
import hashlib
import os
import pillow_heif
from io import BytesIO
from django.apps import AppConfig
from django.core.exceptions import ValidationError
from django.core.files.base import ContentFile, File
from PIL import Image as PILImage
from typing import Tuple
from НАШ_ПРОЕКТ.settings import (
THUMBNAIL_WEBP_QUALITY,
FILER_ENABLE_PERMISSIONS,
FILER_UPLOADER_MAX_FILE_SIZE,
FILER_WHITELIST_FOR_PATH_ACCESS,
MIME_TYPE_WHITELIST,
FILE_VALIDATORS,
)
# Регистрируем плагин HEIF в Pillow для поддержки HEIC/HEIF файлов
pillow_heif.register_heif_opener()
# Настройка парал­лельного декоди­рования HEIF/HEIC файлов
# DECODE_THREADS: количество потоков для декоди­рования
# - 0 = использовать количество ядер процессора (рекоме­ндуется)
# - N > 0 = использовать N потоков (больше потоков = быстрее, но больше памяти)
pillow_heif.options.DECODE_THREADS = 0
# Дополни­тельные опции Pillow для обработки изображений
# LOAD_TRUNCATED_IMAGES: загружать некорректно завершенные изображения
# (полезно для некачес­твенных или поврежденных файлов)
PILImage.LOAD_TRUNCATED_IMAGES = True
logger = logging.getLogger(__name__)
# … конфигурация основного django-приложения …
# …
# …
# Патчинг filer для автоко­нвертации в WebP и поддержки HEIC/HEIF
class CustomFilerConfig(AppConfig):
name = 'filer'
verbose_name = 'Медиафайлы' # Очень удобно, что можно переопре­делить название приложения в админке
@staticmethod
def _convert_to_webp_if_needed(
name: str, content: File
) -> Tuple[File, str, bool, int | None, int | None]:
"""
Конвертирует JPEG/PNG/BMP/TIFF/HEIC/HEIF в WebP.
Returns:
(content, new_name, was_converted, image_width, image_height)
"""
_, original_ext = os.path.splitext(name)
convertible_extensions = [".jpg», ".jpeg», ".png», ".bmp», ".tiff», ".heic», ".heif»]
if original_ext.lower() in convertible_extensions:
try:
# Открываем исходный файл
content.seek(0)
img = PILImage.open(BytesIO(content.read()))
# Получаем размеры ПЕРЕД конвертацией
# (они не меняются, меняется только качество)
image_width, image_height = img.size
# Конвертируем CMYK в RGB
if img.mode == 'CMYK':
img = img.convert('RGB')
# Сохраняем в WebP
webp_buffer = BytesIO()
img.save(webp_buffer, format="WEBP», quality=THUMBNAIL_WEBP_QUALITY)
webp_data = webp_buffer.getvalue()
new_name = os.path.splitext(name)[0] + ".webp»
return ContentFile(webp_data), new_name, True, image_width, image_height
except Exception as e:
# логируем и выбразывсаем ошибку валидации в админку django
error_msg = f"Ошибка при конвертации '{name}' в WebP: {str(e)}"
logger.error(error_msg, exc_info=True)
raise ValidationError(error_msg)
# Если не конвертируем, возвращаем исходный файл
content.seek(0)
return content, name, False, None, None
def ready(self) -> None:
""«Патчим MultiStorageFieldFile.save() для WebP конвертации.»""
from filer.fields.multistorage_file import MultiStorageFieldFile
from filer import settings as filer_settings
original_save = MultiStorageFieldFile.save
# ВАЖНО: Добавляем HEIC/HEIF в IMAGE_EXTENSIONS, чтобы filer распознавал их как изображения
filer_settings.IMAGE_EXTENSIONS = list(
set(filer_settings.IMAGE_EXTENSIONS + ['.heic', '.heif'])
)
filer_settings.IMAGE_MIME_TYPES = list(
set(filer_settings.IMAGE_MIME_TYPES + ['heic', 'heif'])
)
def patched_save(self_instance, name: str, content: File, save: bool = True) -> str | None:
""«Патчи­рованный save() с конвертацией в WebP.»""
new_content, new_name, converted, img_width, img_height = (
CustomFilerConfig._convert_to_webp_if_needed(name, content)
)
if converted:
# Устана­вливаем MIME тип WebP
self_instance.instance.mime_type = «image/webp»
# Обновляем original_filename
if hasattr(self_instance.instance, 'original_filename'):
if self_instance.instance.original_filename:
original_filename, _ = os.path.splitext(
self_instance.instance.original_filename
)
self_instance.instance.original_filename = f"{original_filename}.webp»
# КРИТИЧНО: Обновляем _file_size и sha1 на основе уже сконверти­рованного WebP, а не исходника
new_content.seek(0)
webp_bytes = new_content.read()
new_content.seek(0)
self_instance.instance._file_size = len(webp_bytes)
self_instance.instance.sha1 = hashlib.sha1(webp_bytes).hexdigest()
# Вызываем оригинальный save()
result = original_save(self_instance, new_name, new_content, save)
# Гарантируем что mime_type=image/webp в БД
if converted and new_name.lower().endswith('.webp'):
if self_instance.instance.id:
self_instance.instance.__class__.objects.filter(
id=self_instance.instance.id
).update(mime_type='image/webp')
return result
MultiStorageFieldFile.save = patched_save
```
## Почему это было непросто с атрибутами файла?
### Проблема 1: Instance.id = None при первом сохранении 
**Проблема:**
```python
# После original_save() instance.id все еще None!
original_save(self_instance, new_name, new_content, save=False)
if self_instance.instance.id: # ← None!
# Это не выполнится
```
**Причина:** При `save=False` файл не сохраняется в БД, поэтому `id` не присва­ивается.
**Решение:** Обновляем в отдельном запросе ПОСЛЕ `original_save()`:
```python
if converted:
# После ориги­нального save()
self_instance.instance.__class__.objects.filter(
id=self_instance.instance.id
).update(mime_type='image/webp')
```
### Проблема 2: mime_type должен быть установлен ДО сохранения
**Проблема:** Если установить `mime_type` после `original_save()`, filer уже решил создать `File` или `Image`.
**Решение:** Устана­вливаем ПЕРЕД вызовом `original_save()`:
```python
if converted:
self_instance.instance.mime_type = «image/webp» # ← ДО save()
# Теперь filer видит image/webp и создает Image запись
```
### Проблема 3: _file_size и sha1 из исходного файла
**Проблема:** Django-filer читает `_file_size` и `sha1` из исходных данных:
```python
# Это будет размер ИСХОДНОГО файла (тяжелого HEIC)
self_instance.instance._file_size = len(original_bytes)
self_instance.instance.sha1 = hash(original_bytes)
```
**Результат:** В БД записывается неправильный размер и хеш исходного файла, а не WebP.
**Решение:** Обновляем ПЕРЕД `original_save()`:
```python
if converted:
new_content.seek(0)
webp_bytes = new_content.read()
new_content.seek(0)
self_instance.instance._file_size = len(webp_bytes)
self_instance.instance.sha1 = hashlib.sha1(webp_bytes).hexdigest()
# Теперь original_save() будет использовать ПРАВИЛЬНЫЕ значения
```
### Проблема 4: Размеры изображения из преобра­зованного файла
**Проблема:** Обычно размеры не меняются при конвертации, но нужно быть осторожным. HEIС/HEIF могут содержать
несколько изображений (например, Live Photos), параметры трехмерной сцены и вообще не иметь «основного» разрешения
и переуста­навливать его динамически в зависимости от устройства или слоев редакти­рования. Из-за этого Pillow
может не всегда корректно считать размеры HEIС/HEIF-картинки. На праактике, ему это почти никогда не удается, и он просто возвращает None.
Поэтому мы не можем полагаться на размеры из исходного файла, а должны брать их из уже конверти­рованного WebP,
который гаранти­рованно будет иметь правильные размеры.
**Решение:** Получаем размеры из исходного PIL Image **перед** конвертацией:
```python
img = PILImage.open(BytesIO(content.read()))
image_width, image_height = img.size # Получаем размеры
# Теперь конвертируем
img.convert('RGB').save(webp_buffer, format="WEBP»)
# Размеры image_width и image_height останутся теми же
# (пиксели не меняются, меняется только качество)
```
### Проблема 5: IMAGE_EXTENSIONS - глобальные константы
**Проблема:** Нельзя переопре­делить через `settings.py`:
```python
# В settings.py это НЕ будет работать:
FILER_IMAGE_EXTENSIONS = ['.heic', '.heif'] # ← игнорируется
# Потому что filer делает:
from filer.settings import IMAGE_EXTENSIONS # ← импортирует, не читает из settings
```
**Решение:** Модифицируем после импорта в `ready()`:
```python
from filer import settings as filer_settings
filer_settings.IMAGE_EXTENSIONS = list(
set(filer_settings.IMAGE_EXTENSIONS + ['.heic', '.heif'])
)
```
## Полный результат
При загрузке HEIC файла в админ filer:
```
Исходный HEIC файл (2.5 MB)
_convert_to_webp_if_needed() преобразует в WebP (0.3 MB)
patched_save() обновляет метаданные:
- mime_type = «image/webp»
- _file_size = 0.3 MB (WebP, не исходный!)
- sha1 = хеш WebP (не исходного!)
- original_filename = IMG.webp
Filer сохраняет файл в БД
Миниатюры генерируются автома­тически (40×40, 210×210, 420×420)
В админе отображается миниатюра и размеры
Экономия места: 2.5 MB → 0.3 MB (~88% сокращение)
```
## Тестирование
В админе filer загрузить HEIC файл и проверить:
2. Файл сохранен как .webp
2. Миниатюра 40×40 видна в списке
3. Размеры (в пикселях и вес картинки) указаны правильно
4. При открытии картинки на редакти­рованече через админку filer видно:
* что это WebP (не HEIC) и отображается корректно;
* автома­тически создались миниатюры 210×210 и 420×420;
* SHA1 хеш из WebP данных (не исходного HEIC);
* MIME-тип установлен как `image/webp`.