Files
doc_memo/python/enabling-heic-image-support-in-django-filer.md
2026-06-10 14:24:59 +03:00

517 lines
24 KiB
Markdown
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Настройке 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 = 'Медиафайлы' # Очень удобно, что можно переопре­делить название приложения в админке
# Атрибуты конфигурации Django-Filer (импортированы из settings.py)
# ВАЖНО: Эти атрибуты должны быть доступны в настройках нашего кастомного filer-приложения
FILER_ENABLE_PERMISSIONS = FILER_ENABLE_PERMISSIONS
FILER_UPLOADER_MAX_FILE_SIZE = FILER_UPLOADER_MAX_FILE_SIZE
FILER_WHITELIST_FOR_PATH_ACCESS = FILER_WHITELIST_FOR_PATH_ACCESS
MIME_TYPE_WHITELIST = MIME_TYPE_WHITELIST
FILE_VALIDATORS = FILE_VALIDATORS
@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
```
*ВАЖНО:* В `settins.py` нам нужно убедиться, что `CustomFilerConfig` используется вместо стандартного `filer.apps.FilerConfig`:
```python
INSTALLED_APPS = [
# … …
'НАШ_ПРОЕКТ.filer.apps.CustomFilerConfig', # Используем наш кастомный конфиг
# 'filer' # НЕ НУЖНО, так как мы добавили свой кастомный конфиг, который наследует от стандартного
# … …
]
```
## Почему это было непросто с атрибутами файла?
### Проблема 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`.