diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml index c9ecb93..d639784 100644 --- a/.github/workflows/python.yml +++ b/.github/workflows/python.yml @@ -13,7 +13,7 @@ jobs: strategy: max-parallel: 4 matrix: - python-version: ['3.8', '3.9', '3.10', '3.11', '3.12'] + python-version: ['3.9', '3.10', '3.11', '3.12'] steps: - uses: actions/checkout@v2 diff --git a/CHANGES.rst b/CHANGES.rst index b45c96a..1007e2c 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,9 +1,14 @@ Changes ======= -next ----- +2.10 (2024-09-11) +----------------- +* Drop support for Python-3.8. +* Drop support for Django-4.1 and earlier. +* Add support for Django-5.1. * Experimental support for animated image formats. See documentation for more infos. +* Fix #642: Do not scale images (SVG) without size information. +* Fix #366: Keep ICC profile when saving image, if present. 2.9 (2024-07-25) diff --git a/README.rst b/README.rst index ebf54bb..b0156bc 100644 --- a/README.rst +++ b/README.rst @@ -176,7 +176,14 @@ during scaling. Changes the quality of the output JPEG thumbnail. Defaults to ``85``. In Python code, this is given as a separate option to the ``get_thumbnail`` -method rather than just alter the other +method rather than just alter the other. + +``keep_icc_profile`` +-------------------- + +If `True`, when saving a thumbnail with the alias that defines this option, the +ICC profile of the image will be preserved in the thumbnail, if present in the first place. + Other options ------------- diff --git a/easy_thumbnails/__init__.py b/easy_thumbnails/__init__.py index 23f2801..ff3e8b5 100644 --- a/easy_thumbnails/__init__.py +++ b/easy_thumbnails/__init__.py @@ -1,4 +1,4 @@ -VERSION = (2, 9, 0, 'final', 0) +VERSION = (2, 10, 0, 'final', 0) def get_version(*args, **kwargs): diff --git a/easy_thumbnails/engine.py b/easy_thumbnails/engine.py index b67455c..b9d80da 100644 --- a/easy_thumbnails/engine.py +++ b/easy_thumbnails/engine.py @@ -1,4 +1,5 @@ import os + from io import BytesIO, StringIO from django.utils.module_loading import import_string @@ -59,6 +60,8 @@ def save_pil_image(image, destination=None, filename=None, **options): max(image.size) >= settings.THUMBNAIL_PROGRESSIVE): options['progressive'] = True try: + if options.pop('keep_icc_profile', False): + options['icc_profile'] = image.info.get('icc_profile') image.save(destination, format=format, optimize=1, **options) saved = True except IOError: diff --git a/easy_thumbnails/files.py b/easy_thumbnails/files.py index 853eaf3..5e6cd89 100644 --- a/easy_thumbnails/files.py +++ b/easy_thumbnails/files.py @@ -154,6 +154,7 @@ class ThumbnailFile(ImageFieldFile): This can be used just like a Django model instance's property for a file field (i.e. an ``ImageFieldFile`` object). """ + def __init__(self, name, file=None, storage=None, thumbnail_options=None, *args, **kwargs): fake_field = FakeField(storage=storage) @@ -399,7 +400,7 @@ def generate_thumbnail(self, thumbnail_options, silent_template_exception=False) else: img = engine.save_pil_image( thumbnail_image, filename=filename, quality=quality, - subsampling=subsampling) + subsampling=subsampling, keep_icc_profile=thumbnail_options.get('keep_icc_profile', False)) data = img.read() # S3 requires the data as bytes. @@ -631,6 +632,7 @@ class ThumbnailerFieldFile(FieldFile, Thumbnailer): A field file which provides some methods for generating (and returning) thumbnail images. """ + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.source_storage = self.field.storage diff --git a/easy_thumbnails/storage.py b/easy_thumbnails/storage.py index b07db06..3ea667e 100644 --- a/easy_thumbnails/storage.py +++ b/easy_thumbnails/storage.py @@ -1,26 +1,18 @@ from django.core.files.storage import FileSystemStorage from django.utils.deconstruct import deconstructible -from django.utils.functional import LazyObject from easy_thumbnails.conf import settings + def get_storage(): # If the user has specified a custom storage backend, use it. + from django.core.files.storage.handler import InvalidStorageError + from django.core.files.storage import storages, default_storage + try: - from django.core.files.storage.handler import InvalidStorageError - from django.core.files.storage import storages - try: - return storages[settings.THUMBNAIL_DEFAULT_STORAGE_ALIAS] - except (InvalidStorageError): - pass - except (ImportError, TypeError): - pass - from django.core.files.storage import get_storage_class - storage_class = get_storage_class(settings.THUMBNAIL_DEFAULT_STORAGE) - class ThumbnailDefaultStorage(LazyObject): - def _setup(self): - self._wrapped = storage_class() - return ThumbnailDefaultStorage() + return storages[settings.THUMBNAIL_DEFAULT_STORAGE_ALIAS] + except InvalidStorageError: + return default_storage @deconstructible diff --git a/easy_thumbnails/tests/test_engine.py b/easy_thumbnails/tests/test_engine.py index 44bae24..14bdeba 100644 --- a/easy_thumbnails/tests/test_engine.py +++ b/easy_thumbnails/tests/test_engine.py @@ -1,5 +1,5 @@ from unittest import TestCase -from PIL import Image +from PIL import Image, ImageCms from easy_thumbnails import engine @@ -17,3 +17,14 @@ def test_save_jpeg_la(self): data = engine.save_pil_image(source, filename='test.jpg') with Image.open(data) as img: self.assertEqual(img.mode, 'L') + + def test_save_with_icc_profile(self): + source = Image.new('RGB', (100, 100), (255, 255, 255)) + profile = ImageCms.createProfile('sRGB') + source.save('source.jpg', icc_profile=ImageCms.ImageCmsProfile(profile).tobytes()) + source = Image.open('source.jpg') + + data = engine.save_pil_image(source, filename='test.jpg', keep_icc_profile=True) + img = Image.open(data) + + self.assertNotEqual(img.info.get('icc_profile'), None) diff --git a/setup.py b/setup.py index dc567c7..849b1bb 100644 --- a/setup.py +++ b/setup.py @@ -32,7 +32,7 @@ def read_files(*filenames): packages=find_packages(), include_package_data=True, install_requires=[ - "django>=2.2", + "django>=4.2", "pillow", ], extras_require={ @@ -41,27 +41,23 @@ def read_files(*filenames): "reportlab", ], }, - python_requires=">=3.6", + python_requires=">=3.9", classifiers=[ "Development Status :: 5 - Production/Stable", "Environment :: Web Environment", "Framework :: Django", - "Framework :: Django :: 2.2", - "Framework :: Django :: 3.0", - "Framework :: Django :: 3.1", - "Framework :: Django :: 3.2", - "Framework :: Django :: 4.0", - "Framework :: Django :: 4.1", + "Framework :: Django :: 4.2", + "Framework :: Django :: 5.0", + "Framework :: Django :: 5.1", "Intended Audience :: Developers", "License :: OSI Approved :: BSD License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", "Topic :: Software Development :: Libraries :: Application Frameworks", "Topic :: Software Development :: Libraries :: Python Modules", ], diff --git a/tox.ini b/tox.ini index 0f761e7..cc5ac93 100644 --- a/tox.ini +++ b/tox.ini @@ -8,7 +8,6 @@ skip_missing_interpreters = True [gh-actions] python = - 3.8: py38 3.9: py39 3.10: py310 3.11: py311 @@ -23,7 +22,7 @@ extras = deps = django42: Django<4.3 django50: Django<5.1 - django51: Django>=5.1a1,<5.2 + django51: Django>=5.1,<5.2 testfixtures coverage commands =