Skip to content

Commit f11269e

Browse files
authored
Merge branch 'master' into fix/reverse
2 parents 057be4f + d893e2d commit f11269e

30 files changed

Lines changed: 1899 additions & 54 deletions

‎.github/workflows/test.yml‎

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,3 +64,42 @@ jobs:
6464
run: coverage run ./tests/settings.py
6565
- name: Upload Coverage to Codecov
6666
uses: codecov/codecov-action@v6
67+
68+
unit-tests-without-svg-renderer:
69+
# svglib and reportlab are the optional django-filer[svg] extra. The matrix
70+
# above always has them, so this job covers what a plain "pip install
71+
# django-filer" gets: filer reading and thumbnailing SVGs on its own.
72+
runs-on: ubuntu-latest
73+
steps:
74+
- uses: actions/checkout@v6
75+
- name: Set up Python
76+
uses: actions/setup-python@v6
77+
with:
78+
python-version: '3.13'
79+
- name: library prerequisites
80+
run: |
81+
sudo apt-get update
82+
sudo apt-get install python-dev-is-python3 libpq-dev libmagic1 gcc libxml2-dev libxslt1-dev libjpeg62 libopenjp2-7 -y
83+
- name: Install dependencies
84+
run: |
85+
python -m pip install --upgrade pip
86+
pip install -r tests/requirements/django-5.2.txt
87+
pip install -e .
88+
- name: Remove the optional SVG renderer
89+
run: pip uninstall -y svglib reportlab
90+
- name: Check that it is really gone
91+
# Without this the job would silently degrade into a duplicate of the
92+
# matrix above if anything ever pulls the renderer back in.
93+
run: |
94+
python - <<'EOF'
95+
import importlib.util
96+
import sys
97+
still_here = [name for name in ('svglib', 'reportlab')
98+
if importlib.util.find_spec(name) is not None]
99+
if still_here:
100+
sys.exit(f"{', '.join(still_here)} still installed: this job tests nothing")
101+
EOF
102+
- name: Run coverage
103+
run: coverage run ./tests/settings.py
104+
- name: Upload Coverage to Codecov
105+
uses: codecov/codecov-action@v6

‎.pre-commit-config.yaml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ repos:
2121
# args: [--target-version, "2.2"]
2222

2323
- repo: https://github.com/astral-sh/ruff-pre-commit
24-
rev: v0.16.3
24+
rev: v0.16.6
2525
hooks:
2626
- id: ruff-check
2727
args: [--fix]

‎CHANGELOG.rst‎

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,36 @@ CHANGELOG
55
3.6.0 (unreleased)
66
==================
77

8+
* feat: Support avif images. ``*.avif`` and ``*.avifs`` uploads now become
9+
``Image`` objects (with dimensions, thumbnails and use in image fields) if Pillow
10+
can decode them: natively since Pillow 11.3, or through the optional
11+
``pillow-avif-plugin`` package (``pip install django-filer[avif]``) for older
12+
Pillow versions.
13+
14+
.. note::
15+
16+
avif files that were uploaded *before* upgrading remain regular ``File`` objects:
17+
filer picks the model class when a file is uploaded. Re-upload them to turn them
18+
into ``Image`` objects.
19+
20+
* feat: SVG support no longer requires an SVG renderer. filer reads an SVG's size
21+
from the document and thumbnails it by rewriting ``width``, ``height`` and
22+
``viewBox`` on the root element, which keeps the vector data intact instead of
23+
re-drawing it. ``easy-thumbnails[svg]`` - and with it svglib, reportlab and lxml,
24+
around 14 MB - moved from a hard dependency to the new ``django-filer[svg]``
25+
extra (#1547).
26+
* fix: SVG thumbnails without a ``viewBox`` were scaled by growing the canvas
27+
rather than the drawing.
28+
* fix: An SVG stating only one of ``width`` and ``height`` alongside a ``viewBox``
29+
now keeps the dimension it states and derives the other from the viewBox's
30+
aspect ratio, the way a browser sizes it. The renderer reported the viewBox's
31+
own height instead, which does not match how the image is drawn.
32+
* fix: When an image's dimensions could not be read, the file object was left at
33+
its end, so upload validators that inspect the content saw an empty file.
34+
* fix: SVG documents nesting elements more than
35+
``filer.utils.svg.MAX_NESTING_DEPTH`` (100) levels deep are refused. Writing
36+
such a document out recurses once per level and would exhaust the stack while
37+
a thumbnail is generated. Real documents nest a handful of levels.
838
* feat: Add support for Django 6.1. The admin breadcrumbs now render as
939
``<ol class="breadcrumbs">`` on Django 6.1 and later, matching the markup its
1040
element-qualified CSS selectors expect, and keep the legacy
@@ -17,10 +47,34 @@ CHANGELOG
1747
of a file now also links to its own admin's expand view instead of always linking to
1848
the image admin's. The expand view now checks read permissions, just like the icon and
1949
change views.
50+
* fix: Dropping a file on the admin file widget no longer stacks the upload preview
51+
on top of the widget's own markup, which left two files visible and covered the
52+
widget's buttons (#1573). The widget now shows the uploaded file itself - thumbnail,
53+
label, and working lookup, edit and clear buttons - as it does after a reload, and a
54+
failed upload leaves the previous selection untouched. The upload response gained a
55+
``change_url`` key for this.
56+
* fix: The file widget's clear button works again in widgets that are added to the page
57+
after it loaded, e.g. in Django admin inline formsets.
58+
* fix: A file dragged over the file widget now only recolors the widget's background in
59+
the admin's primary color. The widget used to hide its content and grow its border,
60+
which moved what was under the cursor and made the widget flicker between its drop
61+
state and the file it holds.
2062
* fix: Send the CSRF token with uploads started from the "Upload Files" button in the
2163
folder view. Since 3.5.1 the upload endpoint enforces CSRF, but this uploader had not
2264
been updated and every upload from it failed with "CSRF token missing" (#1617).
2365

66+
.. note::
67+
68+
Existing installations keep SVG support unchanged: upgrading does not always uninstall
69+
svglib or reportlab. Fresh installs and re-locked dependency files no longer get
70+
them. The only documents that still need them are those whose size cannot be read
71+
from the markup, for example ``width="100%"`` without a ``viewBox``; install
72+
``django-filer[svg]`` if your project has such files. Thumbnails generated from
73+
now on are the original document rescaled rather than a reportlab rendering of
74+
it - existing cached thumbnails keep their file names and are still served.
75+
Installing the extra only ever adds documents filer can size; it never accepts
76+
a document filer rejects, so it cannot weaken the SVG upload checks.
77+
2478
.. note::
2579

2680
Projects that override one of filer's breadcrumb templates

‎docs/installation.rst‎

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,41 @@ retrieved from iOS devices by airdrop) using an optional dependency::
1818

1919
$ pip install django-filer\[heif\]
2020

21+
.. _optional_svg_support:
22+
23+
Optional SVG renderer
24+
---------------------
25+
26+
django-filer reads the size of an SVG and scales or crops it by rewriting the
27+
document itself, so SVG uploads and thumbnails work out of the box. Documents
28+
that state their size only in relative units (``width="100%"`` without a
29+
``viewBox``, say) cannot be measured that way. If you have such files, install
30+
the optional SVG renderer, which derives their size by drawing them::
31+
32+
$ pip install django-filer\[svg\]
33+
34+
This pulls in `svglib`_ and `reportlab`_ through ``easy-thumbnails[svg]``.
35+
Before version 3.6, django-filer always installed them.
36+
37+
38+
Optional avif support
39+
---------------------
40+
41+
django-filer treats avif images (``*.avif``, ``*.avifs``) as images if Pillow can
42+
decode them. `Pillow`_ does so out of the box since Pillow 11.3 (its wheels bundle
43+
libaom and dav1d). With older Pillow versions install the optional
44+
`pillow-avif-plugin`_ dependency::
45+
46+
$ pip install django-filer\[avif\]
47+
48+
If neither is available, avif uploads are kept as regular files instead of images.
49+
50+
.. note::
51+
52+
avif files uploaded *before* avif support became available stay regular files:
53+
django-filer decides on the model class at upload time. Re-upload them to turn
54+
them into images.
55+
2156

2257
Dependencies
2358
------------
@@ -37,6 +72,14 @@ If heif support is chosen, django-filer also installs
3772

3873
* pillow-heif
3974

75+
If avif support is chosen, django-filer also installs
76+
77+
* pillow-avif-plugin (only needed for Pillow < 11.3)
78+
If the optional SVG renderer is chosen, django-filer also installs
79+
80+
* svglib
81+
* reportlab
82+
4083

4184
Configuration
4285
-------------
@@ -163,9 +206,12 @@ generation errors, two options are provided to help when working with ``django-
163206
.. _Django: http://djangoproject.com
164207
.. _django-polymorphic: https://github.com/bconstantin/django_polymorphic
165208
.. _easy_thumbnails: https://github.com/SmileyChris/easy-thumbnails
209+
.. _svglib: https://github.com/deeplook/svglib
210+
.. _reportlab: https://www.reportlab.com/
166211
.. _sorl.thumbnail: http://thumbnail.sorl.net/
167212
.. _Pillow: http://pypi.python.org/pypi/Pillow/
168213
.. _Pillow doc: https://pillow.readthedocs.io/en/latest/installation.html
169214
.. _PIL: http://www.pythonware.com/products/pil/
215+
.. _pillow-avif-plugin: https://pypi.org/project/pillow-avif-plugin/
170216
.. _pip: http://pypi.python.org/pypi/pip
171217
.. _South: http://south.aeracode.org/

‎docs/settings.rst‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,16 @@ Defaults to ``MAX_IMAGE_PIXELS``. But when set, should always be lower than the
193193

194194
This is useful setting to prevent decompression bomb DOS attack.
195195

196+
The limit applies to every image whose pixel size is known, a vector image (SVG)
197+
included: an SVG is measured by the size it declares, and is refused just like a
198+
raster image if that exceeds the limit.
199+
200+
Images whose size cannot be read are treated differently by type. For a raster
201+
image that is itself the signature of a decompression bomb, because Pillow
202+
reports no size for one, so it is refused. A vector image has no pixel count to
203+
compare against, so it is stored with unset dimensions and rendered with a
204+
generic icon in the admin.
205+
196206

197207
``FILER_ADD_FILE_VALIDATORS``
198208
-----------------------------

‎docs/upgrading.rst‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,34 @@ Usually upgrade procedure is straightforward: update the package and run migrati
77
require special attention from the developer and here we provide upgrade instructions for such cases.
88

99

10+
from 3.x to 3.6
11+
---------------
12+
13+
django-filer 3.6 no longer installs an SVG renderer. It reads an SVG's size from
14+
the document and thumbnails it by rewriting the root element, so svglib and
15+
reportlab (roughly 14 MB, pulled in through ``easy-thumbnails[svg]``) became the
16+
optional :ref:`optional_svg_support` extra.
17+
18+
Upgrading an existing environment changes nothing: ``pip install -U django-filer``
19+
does not uninstall packages that are already there. Fresh installs and re-locked
20+
dependency files (``uv.lock``, ``poetry.lock``, ``pip-compile`` output) do drop
21+
them, and that is where you may notice a difference.
22+
23+
Only documents whose size cannot be read from the markup still need the renderer,
24+
for example ``width="100%"`` without a ``viewBox``. Their dimensions stay unset
25+
and the admin shows a generic image icon instead of a preview. If your project has
26+
such files, install
27+
28+
.. code-block:: shell
29+
30+
$ pip install django-filer\[svg\]
31+
32+
SVG thumbnails generated from 3.6 on are the original document rescaled rather
33+
than a reportlab rendering of it, which preserves the vector data more faithfully.
34+
Thumbnail file names are unchanged, so thumbnails cached by earlier versions keep
35+
being served.
36+
37+
1038
from 3.x to 3.3
1139
---------------
1240

‎docs/validation.rst‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,11 @@ To activate it, register it for the image mime types you want sanitized:
257257
django-filer[heif]``) and register the sanitizer for ``image/heic`` /
258258
``image/heif``.
259259

260+
``strip_exif`` keeps the encoding parameters of JPEG and WebP images. For
261+
other lossy formats — ``image/avif`` and ``image/heic`` among them — it
262+
re-encodes with Pillow's defaults, which costs image quality. Only register
263+
it for those MIME types if that trade-off is acceptable.
264+
260265
Block other MIME types
261266
----------------------
262267

‎eslint.config.js‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ export default [
2727
setInterval: 'readonly',
2828
clearInterval: 'readonly',
2929
NodeList: 'readonly',
30+
DataTransfer: 'readonly',
31+
DragEvent: 'readonly',
32+
File: 'readonly',
3033
HTMLCollection: 'readonly',
3134
URL: 'readonly',
3235
navigator: 'readonly',

‎filer/admin/clipboardadmin.py‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from django.core.exceptions import ValidationError
55
from django.forms.models import modelform_factory
66
from django.http import JsonResponse
7-
from django.urls import path, reverse
7+
from django.urls import NoReverseMatch, path, reverse
88
from django.utils.translation import gettext_lazy as _
99

1010
from .. import settings as filer_settings
@@ -160,6 +160,12 @@ def ajax_upload(request, folder_id=None):
160160
'label': str(file_obj),
161161
'file_id': file_obj.pk,
162162
}
163+
try:
164+
# Lets the file widget link its edit button to the new file
165+
data['change_url'] = file_obj.get_admin_change_url()
166+
except NoReverseMatch:
167+
# Custom file models are not necessarily registered in the admin
168+
data['change_url'] = ''
163169
# prepare preview thumbnail
164170
if isinstance(file_obj, Image):
165171
data['thumbnail_180'] = reverse(

‎filer/apps.py‎

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,38 @@ def register_optional_heif_supprt(self):
2929
# No heif support installed
3030
pass
3131

32+
def register_optional_avif_support(self):
33+
"""Pillow decodes AVIF itself since Pillow 11.3 (its wheels bundle libaom and
34+
dav1d). For older Pillow versions the optional pillow-avif-plugin package
35+
provides the same plugin."""
36+
from PIL import Image
37+
38+
try: # pragma: no cover
39+
import pillow_avif # noqa: F401
40+
except (ModuleNotFoundError, ImportError):
41+
# No pillow-avif-plugin installed: Pillow's own AVIF plugin (if any) is used
42+
pass
43+
44+
# registered_extensions() imports Pillow's plugins. Pillow only registers the
45+
# extensions if the AVIF codec actually is available.
46+
if ".avif" not in Image.registered_extensions(): # pragma: no cover
47+
# No AVIF support: leave avif files as plain files, they could not be
48+
# thumbnailed anyway
49+
return
50+
51+
from .settings import IMAGE_EXTENSIONS, IMAGE_MIME_TYPES
52+
53+
AVIF_EXTENSIONS = [".avif", ".avifs"]
54+
# Add extensions to python mimetypes which filer uses
55+
for ext in AVIF_EXTENSIONS:
56+
mimetypes.add_type("image/avif", ext)
57+
# Mark them as images
58+
for ext in AVIF_EXTENSIONS:
59+
if ext not in IMAGE_EXTENSIONS:
60+
IMAGE_EXTENSIONS.append(ext)
61+
if "avif" not in IMAGE_MIME_TYPES:
62+
IMAGE_MIME_TYPES.append("avif")
63+
3264
def resolve_validators(self):
3365
"""Resolve dotted path file validators"""
3466

@@ -62,6 +94,9 @@ def resolve_validators(self):
6294
def ready(self):
6395
# Make webp MIME type known to python (needed for python < 3.11)
6496
mimetypes.add_type("image/webp", ".webp")
97+
# Make avif MIME type known to python (needed for older python versions)
98+
mimetypes.add_type("image/avif", ".avif")
6599
#
66100
self.resolve_validators()
67101
self.register_optional_heif_supprt()
102+
self.register_optional_avif_support()

0 commit comments

Comments
 (0)