diff --git a/CHANGELOG.md b/CHANGELOG.md
index 237f011d81..6244d0adf9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,10 @@ and this project adheres to
## [Unreleased]
+### Removed
+
+- 🔥(backend) remove easy-thumbnails and Pillow #2909
+
## [v6.0.4] - 2026-10-26
### Changed
diff --git a/UPGRADE.md b/UPGRADE.md
index abe77a42f0..04693737ea 100644
--- a/UPGRADE.md
+++ b/UPGRADE.md
@@ -16,6 +16,20 @@ the following command inside your docker container:
## [Unreleased]
+- The backend no longer depends on `easy-thumbnails`, nor on Pillow, which
+ came with it: nothing used them anymore. No migration drops the tables of
+ `easy-thumbnails`, they stay in the database and the backend reads none of
+ them: `easy_thumbnails_source`, `easy_thumbnails_thumbnail` and
+ `easy_thumbnails_thumbnaildimensions`. They are empty: nothing ever
+ created a thumbnail. Keep them or drop them; to drop them, along with
+ their entries in `django_migrations`:
+
+ ```sql
+ DROP TABLE easy_thumbnails_thumbnaildimensions, easy_thumbnails_thumbnail,
+ easy_thumbnails_source;
+ DELETE FROM django_migrations WHERE app = 'easy_thumbnails';
+ ```
+
## [v6.0.2] - 2026-10-21
- ⚠️ **The collaboration server moves to `@y/hub` 0.10.1: run its schema
diff --git a/src/backend/core/management/commands/anonymize_database.py b/src/backend/core/management/commands/anonymize_database.py
index 507dc3f815..05e1da14e6 100644
--- a/src/backend/core/management/commands/anonymize_database.py
+++ b/src/backend/core/management/commands/anonymize_database.py
@@ -13,7 +13,7 @@
(emails, names, the OIDC ``sub``), user-generated content (document titles and
excerpts, comment bodies, thread/comment metadata) and secrets (password
hashes), and TRUNCATEs the operational tables that embed identity (sessions,
-admin log, thumbnail cache). Structural columns that make
+admin log). Structural columns that make
the data realistic for load testing — the tree ``path``/``depth``/``numchild``,
``link_reach``, roles, foreign keys, row counts, timestamps — are deliberately
preserved, so the result pairs naturally with ``profile_volumetry`` /
@@ -129,8 +129,6 @@
TRUNCATES = [
"django_session", # session_data can carry the authenticated identity
"django_admin_log", # object_repr / change_message leak titles and emails
- "easy_thumbnails_source", # thumbnail cache: media file paths
- "easy_thumbnails_thumbnail",
]
diff --git a/src/backend/core/templatetags/__init__.py b/src/backend/core/templatetags/__init__.py
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/src/backend/core/templatetags/extra_tags.py b/src/backend/core/templatetags/extra_tags.py
deleted file mode 100644
index 273459ff89..0000000000
--- a/src/backend/core/templatetags/extra_tags.py
+++ /dev/null
@@ -1,58 +0,0 @@
-"""Custom template tags for the core application of Docs."""
-
-import base64
-
-from django import template
-from django.contrib.staticfiles import finders
-
-from PIL import ImageFile as PillowImageFile
-
-register = template.Library()
-
-
-def image_to_base64(file_or_path, close=False):
- """
- Return the src string of the base64 encoding of an image represented by its path
- or file opened or not.
-
- Inspired by Django's "get_image_dimensions"
- """
- pil_parser = PillowImageFile.Parser()
- if hasattr(file_or_path, "read"):
- file = file_or_path
- if file.closed and hasattr(file, "open"):
- file_or_path.open()
- file_pos = file.tell()
- file.seek(0)
- else:
- try:
- # pylint: disable=consider-using-with
- file = open(file_or_path, "rb")
- except OSError:
- return ""
- close = True
-
- try:
- image_data = file.read()
- if not image_data:
- return ""
- pil_parser.feed(image_data)
- if pil_parser.image:
- mime_type = pil_parser.image.get_format_mimetype()
- encoded_string = base64.b64encode(image_data)
- return f"data:{mime_type:s};base64, {encoded_string.decode('utf-8'):s}"
- return ""
- finally:
- if close:
- file.close()
- else:
- file.seek(file_pos)
-
-
-@register.simple_tag
-def base64_static(path):
- """Return a static file into a base64."""
- full_path = finders.find(path)
- if full_path:
- return image_to_base64(full_path, True)
- return ""
diff --git a/src/backend/impress/settings.py b/src/backend/impress/settings.py
index 8d07c2dfad..45b91ef243 100755
--- a/src/backend/impress/settings.py
+++ b/src/backend/impress/settings.py
@@ -399,7 +399,6 @@ class Base(Configuration):
"rest_framework",
"parler",
"treebeard",
- "easy_thumbnails",
"waffle",
# Django
"django.contrib.admin",
@@ -704,12 +703,6 @@ class Base(Configuration):
"https://eu.i.posthog.com", environ_name="POSTHOG_HOST", environ_prefix=None
)
- # Easy thumbnails
- THUMBNAIL_EXTENSION = "webp"
- THUMBNAIL_TRANSPARENCY_EXTENSION = "webp"
- THUMBNAIL_DEFAULT_STORAGE_ALIAS = "default"
- THUMBNAIL_ALIASES = {}
-
# Celery
CELERY_BROKER_URL = values.Value("redis://redis:6379/0")
CELERY_BROKER_TRANSPORT_OPTIONS = values.DictValue({})
diff --git a/src/backend/pyproject.toml b/src/backend/pyproject.toml
index 24bbc3267d..830e5522c7 100644
--- a/src/backend/pyproject.toml
+++ b/src/backend/pyproject.toml
@@ -46,7 +46,6 @@ dependencies = [
"django-waffle==5.0.0",
"drf_spectacular==0.30.0",
"dockerflow==2026.3.4",
- "easy_thumbnails==2.10.1",
"emoji==2.16.0",
"factory_boy==3.3.3",
"gunicorn==26.2.0",
diff --git a/src/backend/uv.lock b/src/backend/uv.lock
index 2e35f1bfe4..3eaf9223e4 100644
--- a/src/backend/uv.lock
+++ b/src/backend/uv.lock
@@ -737,19 +737,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/55/09/13509939a12e953759fa2f281b28dc14813c8c331addf3838fccdc71b364/drf_spectacular_sidecar-2026.10.1-py3-none-any.whl", hash = "sha256:fe1d005d8ac9b5d0472f2fe5deeb2e7d6dcffaa24118044f22578b4b8c4b31f2", size = 2656316, upload-time = "2026-10-01T17:20:33.813Z" },
]
-[[package]]
-name = "easy-thumbnails"
-version = "2.10.1"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "django" },
- { name = "pillow" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/04/2f/71f17c3ce65b25dd8fc8e513738c8496eadbd8bab94f39509a511ff99704/easy_thumbnails-2.10.1.tar.gz", hash = "sha256:a50aa5f99387546c35ab5ba1ea9b3cbbc5658e65601cd34949f62137c32c222e", size = 81446, upload-time = "2025-08-17T20:49:11.308Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/b0/90/be137d0592501ec08d4a00c8430121e95dfe6c8114a1d4436b5cdd6b9803/easy_thumbnails-2.10.1-py3-none-any.whl", hash = "sha256:24462d63dd31543ef1585538b2bfefe0db96d3409bb431c70b81548fb2cfc5be", size = 79695, upload-time = "2025-08-17T20:49:09.119Z" },
-]
-
[[package]]
name = "emoji"
version = "2.16.0"
@@ -994,7 +981,6 @@ dependencies = [
{ name = "djangorestframework" },
{ name = "dockerflow" },
{ name = "drf-spectacular" },
- { name = "easy-thumbnails" },
{ name = "emoji" },
{ name = "factory-boy" },
{ name = "gunicorn" },
@@ -1070,7 +1056,6 @@ requires-dist = [
{ name = "dockerflow", specifier = "==2026.3.4" },
{ name = "drf-spectacular", specifier = "==0.30.0" },
{ name = "drf-spectacular-sidecar", marker = "extra == 'dev'", specifier = "==2026.10.1" },
- { name = "easy-thumbnails", specifier = "==2.10.1" },
{ name = "emoji", specifier = "==2.16.0" },
{ name = "factory-boy", specifier = "==3.3.3" },
{ name = "freezegun", marker = "extra == 'dev'", specifier = "==1.5.5" },
@@ -1701,35 +1686,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523", size = 63772, upload-time = "2023-11-25T06:56:14.81Z" },
]
-[[package]]
-name = "pillow"
-version = "13.0.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/ce/a4/9cc30690a394a8305e8be4c9036aa16d0dfddadd09079860b582f562496d/pillow-13.0.0.tar.gz", hash = "sha256:cdcd1ba258178f7356d74170c9a81ba4eab6a5d55748bc402a5a2a6fe6be3510", size = 47058631, upload-time = "2026-10-16T22:18:09.718Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/8e/45/fbad9c7f5e2f1745c373a434d46072bc84eddfefaa28b852425ac957fda6/pillow-13.0.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:44629d4498c9cb6b2051806155f22141df0d8a929c56134b166655232dfb2f8c", size = 4190721, upload-time = "2026-10-16T22:16:27.524Z" },
- { url = "https://files.pythonhosted.org/packages/a2/bf/2074902a259a90692c5dbf132885d21671644cbbd3f690047516243a55db/pillow-13.0.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:bf97ba1b5542050e9b0c74e632323b6d8136cc99d64e193abebee22fe6013c76", size = 4290236, upload-time = "2026-10-16T22:16:29.562Z" },
- { url = "https://files.pythonhosted.org/packages/ff/a5/3d1bb93b8ea38d15d21356c1b8e9f50971ac592aaa248f7fb71739094c0e/pillow-13.0.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:aa694c49787040943570d61c9b8fc01271bd2a2cfdd55f0d896f8aecdce1a981", size = 3723093, upload-time = "2026-10-16T22:16:31.249Z" },
- { url = "https://files.pythonhosted.org/packages/84/a1/be060135d85187b25fa8b2573786c0e45c62ae27527fff5257675eb97f4a/pillow-13.0.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:49c12af515073e9bf511e4ef736743bf70d531a643668722279314b937742ba4", size = 5572253, upload-time = "2026-10-16T22:16:33.103Z" },
- { url = "https://files.pythonhosted.org/packages/b5/f4/0bf6f1ac9ba7ee2ffe9914aeb410a1035a0c1a3c42f122388d7fc950bc4b/pillow-13.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d9b0a2bee3f3e03e9f29057aedb807dbf1e1d96ee1b9d0f422d9db6c8583271d", size = 4852269, upload-time = "2026-10-16T22:16:35.102Z" },
- { url = "https://files.pythonhosted.org/packages/83/16/b603274617b3e6728da3a0133d4fba1ea8d7fbe0c4ea5fef9cbffa398b8b/pillow-13.0.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5933fcea5a5ba64c686f55c886a115cbfd92f2900460784a1e79909fdbafd7b6", size = 6516058, upload-time = "2026-10-16T22:16:37.228Z" },
- { url = "https://files.pythonhosted.org/packages/1d/f2/1371707ce438ba9a43f9d24089828494ec6f9396e28cd7903aa704dd6306/pillow-13.0.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e91c2b1cbeae768c98752e67e717e283d68dae45717b41f2d91eaee648e37b7d", size = 7142574, upload-time = "2026-10-16T22:16:39.172Z" },
- { url = "https://files.pythonhosted.org/packages/b9/a9/62cf68587f910bc87c5951bcb6e9430c7643a65076adf5f18b4bf350e2bd/pillow-13.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f6299e494e46253d40c34653cac369412f44ca53b5f85d848d3f95b1ed0546c", size = 6614295, upload-time = "2026-10-16T22:16:41.117Z" },
- { url = "https://files.pythonhosted.org/packages/6c/c9/9e9b48178649730299be0bafbf5f810a5b6977538f5a92d47e4ba5f5d608/pillow-13.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f9bd7d260d986f3553518c5d723544346fa48031f8aef3265d786aeb59845bc0", size = 7224841, upload-time = "2026-10-16T22:16:43.118Z" },
- { url = "https://files.pythonhosted.org/packages/8f/48/c44e91384b1b6c072a714a63d00608acc16a6a968735e478e4be4384e5ac/pillow-13.0.0-cp314-cp314-win32.whl", hash = "sha256:2e31c4e68cffd7b1f116127626813b82fcba19edf6b940a7b0c266d6484d0055", size = 6508694, upload-time = "2026-10-16T22:16:45.883Z" },
- { url = "https://files.pythonhosted.org/packages/50/e2/7ad17747517e4c813dca78257d6bb593466f8231baf1384bc98d92745edb/pillow-13.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:f7cbb7a64e7ec664775195e4f81923bc8a93d9451b963f844e59118f63147a7a", size = 7281830, upload-time = "2026-10-16T22:16:47.725Z" },
- { url = "https://files.pythonhosted.org/packages/41/9f/e5b51f2ce8ef4563215334035403ce61a244f93a2e65c24a940131618f87/pillow-13.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:a6aaa7871aebca081eb5c45f83e845bbd9712ae99fa9286dbfdcee835e63f4b4", size = 2695468, upload-time = "2026-10-16T22:16:49.515Z" },
- { url = "https://files.pythonhosted.org/packages/97/73/a53502f731f6c84805a4ce87a8881e86d07a65aee64728b900d112dc703e/pillow-13.0.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7b999816b5850a35ada08443e6e03945f1f6488144a83afe2d37b240a552aafa", size = 5578196, upload-time = "2026-10-16T22:16:51.291Z" },
- { url = "https://files.pythonhosted.org/packages/21/50/7e949959a56b5559c78ef172b70fc6025706acb289f3f648a15329e4fa6f/pillow-13.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:67871a13cf47275201553b536c025da777f997fa562a19eb798af4ad4b97c18f", size = 4857276, upload-time = "2026-10-16T22:16:53.399Z" },
- { url = "https://files.pythonhosted.org/packages/c9/52/43ded0ac08e77e39d17bd6d894162572201b5e8fdc4a022b14c55a1960a4/pillow-13.0.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e8ca8f69b7c77fa96dad99f8638c1fc4f1c0bed4acaa1baf7a3c31bdb80a0ab1", size = 6607008, upload-time = "2026-10-16T22:16:55.525Z" },
- { url = "https://files.pythonhosted.org/packages/0d/2e/4a57ed216d5bf877387cba4d44c02aa87abe51ad2e06404aa6b597c4da39/pillow-13.0.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6bce6a29c3898191f4c9998e4d4e8f69790caeabde46e54da4bc235ce5bde683", size = 7224281, upload-time = "2026-10-16T22:16:57.738Z" },
- { url = "https://files.pythonhosted.org/packages/5c/3d/2929b248e135752d63aaeabf53f6de797e905b7b6263f9c1e2adade20aba/pillow-13.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:816752a5ef76a80484c8c441c85593b7ea9304f6231fc5776ad1fbff7e773058", size = 6695600, upload-time = "2026-10-16T22:16:59.842Z" },
- { url = "https://files.pythonhosted.org/packages/bc/33/fb0da992cf084ed1ee6ee05e96b6191e35c351eb707ca2df874feb24ce47/pillow-13.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5bdb38ba220b61b4ec31dd1046dea6b016d3c6850a23f43ed8b58332b2fc668e", size = 7300589, upload-time = "2026-10-16T22:17:01.686Z" },
- { url = "https://files.pythonhosted.org/packages/bb/af/c30dd9ef6a3dce84ecb1b2393cac629047a32e66279056c6cb46c3ffd65c/pillow-13.0.0-cp314-cp314t-win32.whl", hash = "sha256:06903aac074d00c988e77a1fc435286dc498506d4ca8d6f69df9babddf4ecc15", size = 6517204, upload-time = "2026-10-16T22:17:03.722Z" },
- { url = "https://files.pythonhosted.org/packages/85/d5/ef3a8ae7609c8ab26570abf18194882534b480d245a9148b895a2262c004/pillow-13.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:9a635068d9793c7f0d929f39f18b58f1bd86c236e8e7b64167b86af9232ebca0", size = 7286695, upload-time = "2026-10-16T22:17:05.543Z" },
- { url = "https://files.pythonhosted.org/packages/84/3c/0861db0c69a03689ebe3e7599d35668a414deb5aaafd8255215e4e62e3d3/pillow-13.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0d1a9cf7bc6917538b20f8d06b9ab2147e542f68849e96fc58eb66bdf6acc692", size = 2698098, upload-time = "2026-10-16T22:17:07.674Z" },
-]
-
[[package]]
name = "platformdirs"
version = "4.9.6"
diff --git a/src/mail/mjml/template.mjml b/src/mail/mjml/template.mjml
index 97e92ec4bf..6ef0eb545a 100644
--- a/src/mail/mjml/template.mjml
+++ b/src/mail/mjml/template.mjml
@@ -6,7 +6,7 @@
We load django tags here so they appear in the body of the HTML output.
This ensures html-to-text also includes them in the plain text template.
-->
- {% load i18n static extra_tags %}
+ {% load i18n static %}