From 09174c9c4fef1e2b4363e8f0e43c854a82f3b7f6 Mon Sep 17 00:00:00 2001 From: Salvatore Iovene Date: Thu, 1 Oct 2020 21:14:46 +0200 Subject: [PATCH 1/2] Keep ICC profile when saving image, if present --- README.rst | 9 ++++++++- easy_thumbnails/engine.py | 3 +++ easy_thumbnails/files.py | 4 +++- easy_thumbnails/tests/test_engine.py | 13 ++++++++++++- 4 files changed, 26 insertions(+), 3 deletions(-) diff --git a/README.rst b/README.rst index 6218a4e4..70189f86 100644 --- a/README.rst +++ b/README.rst @@ -134,7 +134,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/engine.py b/easy_thumbnails/engine.py index e6426864..c6edcaff 100644 --- a/easy_thumbnails/engine.py +++ b/easy_thumbnails/engine.py @@ -1,4 +1,5 @@ import os + from io import BytesIO from PIL import Image @@ -57,6 +58,8 @@ def save_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 64399087..d38651f4 100644 --- a/easy_thumbnails/files.py +++ b/easy_thumbnails/files.py @@ -150,6 +150,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) @@ -400,7 +401,7 @@ def generate_thumbnail(self, thumbnail_options, high_resolution=False, img = engine.save_image( thumbnail_image, filename=filename, quality=quality, - subsampling=subsampling) + subsampling=subsampling, keep_icc_profile=thumbnail_options.get('keep_icc_profile', False)) data = img.read() thumbnail = ThumbnailFile( @@ -653,6 +654,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/tests/test_engine.py b/easy_thumbnails/tests/test_engine.py index 9fc6a4e3..7c5e99a5 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_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_image(source, filename='test.jpg', keep_icc_profile=True) + img = Image.open(data) + + self.assertNotEqual(img.info.get('icc_profile'), None) From 7bdbd7c1e25051ce3e5b0c2f9b20dfcda5fdfab6 Mon Sep 17 00:00:00 2001 From: Jacob Rief Date: Wed, 11 Sep 2024 14:02:59 +0200 Subject: [PATCH 2/2] prepare for version 2.10 --- .github/workflows/python.yml | 2 +- CHANGES.rst | 7 +++++++ easy_thumbnails/__init__.py | 2 +- easy_thumbnails/storage.py | 22 +++++++--------------- setup.py | 18 +++++++----------- tox.ini | 3 +-- 6 files changed, 24 insertions(+), 30 deletions(-) diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml index c9ecb939..d6397843 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 195ac020..18443866 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,6 +1,13 @@ Changes ======= +2.10 +---- +* Drop support for Python-3.8. +* Drop support for Django-4.1 and earlier. +* Add support for Django-5.1. + + 2.9 (2024-07-25) ---------------- * Add support for Django 4.2 storages (mandatory in Django 5.1). diff --git a/easy_thumbnails/__init__.py b/easy_thumbnails/__init__.py index 23f2801c..ff3e8b5d 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/storage.py b/easy_thumbnails/storage.py index b07db067..3ea667e7 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/setup.py b/setup.py index dc567c74..849b1bb6 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 0f761e73..cc5ac937 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 =