diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c6569c7..3070c08 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -46,18 +46,16 @@ jobs: - run: python -m pip install -r lint-requirements.txt - run: ${{ matrix.lint-command }} - dist: runs-on: ubuntu-latest steps: - - name: Install gettext - run: sudo apt install gettext -y - - uses: actions/setup-python@v3 - - run: python -m pip install --upgrade pip setuptools wheel twine readme-renderer - uses: actions/checkout@v3 - - run: python setup.py sdist bdist_wheel + - uses: actions/setup-python@v3 + - run: sudo apt install gettext -y + - run: python -m pip install --upgrade pip build wheel twine readme-renderer + - run: python -m build --sdist --wheel - run: python -m twine check dist/* - - uses: actions/upload-artifact@v2 + - uses: actions/upload-artifact@v3 with: path: dist/* @@ -73,11 +71,11 @@ jobs: - "3.9" - "3.10" django-version: - - "3.2a" - - "4.0a" + - "3.2" + - "4.0" extra: - - "" - - "progressbar" + - "test" + - "test,progressbar" steps: - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v3 @@ -85,10 +83,9 @@ jobs: python-version: ${{ matrix.python-version }} - run: sudo apt install gettext -y - uses: actions/checkout@v3 - - run: python -m pip install --upgrade pip setuptools codecov wheel - - run: python -m pip install .[${{ matrix.extra }}] + - run: python -m pip install --upgrade pip codecov + - run: python -m pip install -e .[${{ matrix.extra }}] if: ${{ matrix.extra }} - - run: python -m pip install django~=${{ matrix.django-version }} - - name: Test with pytest - run: python setup.py test + - run: python -m pip install django~=${{ matrix.django-version }}a + - run: python -m pytest - run: codecov diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ff70343..19f0b4e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,10 +1,12 @@ -name: PyPi Release +name: Release -on: [release] +on: + release: + types: [published] jobs: - build: + PyPi: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 @@ -14,5 +16,5 @@ jobs: - run: python -m build --sdist --wheel - run: python -m twine upload dist/* env: - TWINE_USERNAME: ${{ secrets.TWINE_USERNAME }} + TWINE_USERNAME: __token__ TWINE_PASSWORD: ${{ secrets.TWINE_PASSWORD }} diff --git a/README.md b/README.md index cc70313..77fe5f6 100644 --- a/README.md +++ b/README.md @@ -4,19 +4,110 @@ # Django Standardized Image Field -Django Field that implement the following features: +This package has been deprecated in favor of [django-pictures][django-pictures]. -* Django-Storages compatible (S3) -* Resize images to different sizes +## Migration Instructions + +First, make sure you understand the differences between the two packages and +how to serve images in a modern web application via the [picture][picture-tag]-Element. + +Next, follow the setup instructions for [django-pictures][django-pictures]. + +Once you are set up, change your models to use the new `PictureField` and provide the + `aspect_ratios` you'd like to serve. Do create migrations just yet. + +This step should be followed by changing your templates and frontend. +The new placeholders feature for local development should help you +to do this almost effortlessly. + +Finally, run `makemigrations` and replace the `AlterField` operation with +`AlterPictureField`. + +We highly recommend to use Django's `image_width` and `image_height` fields, to avoid +unnecessary IO. If you can add these fields to your model, you can use the following +snippet to populate them: + +```python +import django.core.files.storage +from django.db import migrations, models +import pictures.models +from pictures.migrations import AlterPictureField + +def forward(apps, schema_editor): + for obj in apps.get_model("my-app.MyModel").objects.all().iterator(): + obj.image_width = obj.logo.width + obj.image_height = obj.logo.height + obj.save(update_fields=["image_height", "image_width"]) + +def backward(apps, schema_editor): + apps.get_model("my-app.MyModel").objects.all().update( + image_width=None, + image_height=None, + ) + +class Migration(migrations.Migration): + dependencies = [ + ('my-app', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name="mymodel", + name="image_height", + field=models.PositiveIntegerField(editable=False, null=True), + ), + migrations.AddField( + model_name="mymodel", + name="image_width", + field=models.PositiveIntegerField(editable=False, null=True), + ), + migrations.RunPython(forward, backward), + AlterPictureField( + model_name="mymodel", + name="image", + field=pictures.models.PictureField( + aspect_ratios=["3/2", "3/1"], + breakpoints={"desktop": 1024, "mobile": 576}, + container_width=1200, + file_types=["WEBP"], + grid_columns=12, + height_field="image_height", + pixel_densities=[1, 2], + storage=django.core.files.storage.FileSystemStorage(), + upload_to="pictures/", + verbose_name="image", + width_field="image_width", + ), + ), + ] +``` + +[django-pictures]: https://github.com/codingjoe/django-pictures +[picture-tag]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/picture + +## Why would I want this? + +This is a drop-in replacement for the [Django ImageField](https://docs.djangoproject.com/en/1.8/ref/models/fields/#django.db.models.ImageField) that provides a standardized way to handle image uploads. +It is designed to be as easy to use as possible, and to provide a consistent interface for all image fields. +It allows images to be presented in various size variants (eg:thumbnails, mid, and hi-res versions) +and it provides a way to handle images that are too large with validators. + + +## Features + +Django Standardized Image Field implements the following features: + +* [Django-Storages](https://django-storages.readthedocs.io/en/latest/) compatible (eg: S3, Azure, Google Cloud Storage, etc) +* Resizes images to different sizes * Access thumbnails on model level, no template tags required -* Preserves original image -* Asynchronous rendering (Celery & Co) -* Restrict accepted image dimensions -* Rename files to a standardized name (using a callable upload_to) +* Preserves original images +* Can be rendered asynchronously (ie as a [Celery job](https://realpython.com/asynchronous-tasks-with-django-and-celery/)) +* Restricts acceptable image dimensions +* Renames a file to a standardized name format (using a callable `upload_to` function, see below) ## Installation -Simply install the latest stable package using the command +Simply install the latest stable package using the following command: ```bash pip install django-stdimage @@ -28,11 +119,13 @@ and add `'stdimage'` to `INSTALLED_APP`s in your settings.py, that's it! ## Usage +Now it's instally you can use either: `StdImageField` or `JPEGField`. + `StdImageField` works just like Django's own [ImageField](https://docs.djangoproject.com/en/dev/ref/models/fields/#imagefield) -except that you can specify different sized variations. +except that you can specify different size variations. -The `JPEGField` works similar to the `StdImageField` but all size variations are +The `JPEGField` is identical to the `StdImageField` but all images are converted to JPEGs, no matter what type the original file is. ### Variations @@ -58,7 +151,7 @@ class MyModel(models.Model): # is the same as dictionary-style call image = StdImageField(upload_to='path/to/img', variations={'thumbnail': (100, 75)}) - # variations are converted to JPEGs + # JPEGField variations are converted to JPEGs. jpeg = JPEGField( upload_to='path/to/img', variations={'full': (None, None), 'thumbnail': (100, 75)}, @@ -77,7 +170,7 @@ class MyModel(models.Model): }, delete_orphans=True) ``` -For using generated variations in templates use `myimagefield.variation_name`. +To use these variations in templates use `myimagefield.variation_name`. Example: @@ -85,15 +178,32 @@ Example: ``` -### Utils +### Upload to function -Since version 4 the custom `upload_to` utils have been dropped in favor of -[Django Dynamic Filenames][dynamic_filenames]. +You can use a function for the `upload_to` argument. Using [Django Dynamic Filenames][dynamic_filenames].[dynamic_filenames]: https://github.com/codingjoe/django-dynamic-filenames -[dynamic_filenames]: https://github.com/codingjoe/django-dynamic-filenames +This allows images to be given unique paths and filenames based on the model instance. + +Example + +```python +from django.db import models +from stdimage import StdImageField +from dynamic_filenames import FilePattern + +upload_to_pattern = FilePattern( + filename_pattern='my_model/{app_label:.25}/{model_name:.30}/{uuid:base32}{ext}', +) + + +class MyModel(models.Model): + # works just like django's ImageField + image = StdImageField(upload_to=upload_to_pattern) +``` ### Validators -The `StdImageField` doesn't implement any size validation. Validation can be specified using the validator attribute +The `StdImageField` doesn't implement any size validation out-of-the-box. +However, Validation can be specified using the validator attribute and using a set of validators shipped with this package. Validators can be used for both Forms and Models. @@ -120,9 +230,9 @@ Django [dropped support](https://docs.djangoproject.com/en/dev/releases/1.3/#del for automated deletions in version 1.3. Since version 5, this package supports a `delete_orphans` argument. It will delete -orphaned files, should a file be delete or replaced via Django form or and object with -a `StdImageField` be deleted. It will not be deleted if the field value is changed or -reassigned programatically. In those rare cases, you will need to handle proper deletion +orphaned files, should a file be deleted or replaced via a Django form and the object with +the `StdImageField` be deleted. It will not delete files if the field value is changed or +reassigned programatically. In these rare cases, you will need to handle proper deletion yourself. ```python @@ -141,10 +251,10 @@ class MyModel(models.Model): ### Async image processing Tools like celery allow to execute time-consuming tasks outside of the request. If you don't want -to wait for your variations to be rendered in request, StdImage provides your the option to pass a -async keyword and a util. -Note that the callback is not transaction save, but the file will be there. -This example is based on celery. +to wait for your variations to be rendered in request, StdImage provides you the option to pass an +async keyword and a 'render_variations' function that triggers the async task. +Note that the callback is not transaction save, but the file variations will be present. +The example below is based on celery. `tasks.py`: ```python @@ -177,19 +287,18 @@ def image_processor(file_name, variations, storage): class AsyncImageModel(models.Model): image = StdImageField( # above task definition can only handle one model object per image filename - upload_to='path/to/file/', + upload_to='path/to/file/', # or use a function render_variations=image_processor # pass boolean or callable ) processed = models.BooleanField(default=False) # flag that could be used for view querysets ``` ### Re-rendering variations -You might want to add new variations to a field. That means you need to render new variations for missing fields. +You might have added or changed variations to an existing field. That means you will need to render new variations. This can be accomplished using a management command. ```bash python manage.py rendervariations 'app_name.model_name.field_name' [--replace] [-i/--ignore-missing] ``` The `replace` option will replace all existing files. -The `ignore-missing` option will suspend missing source file errors and keep -rendering variations for other files. Othervise command will stop on first -missing file. +The `ignore-missing` option will suspend 'missing source file' errors and keep +rendering variations for other files. Otherwise, the command will stop on first missing file. diff --git a/setup.cfg b/setup.cfg index 10e8e2b..5262f42 100644 --- a/setup.cfg +++ b/setup.cfg @@ -9,7 +9,7 @@ url = https://github.com/codingjoe/django-stdimage license = MIT license_file = LICENSE classifier = - Development Status :: 5 - Production/Stable + Development Status :: 7 - Inactive Environment :: Web Environment Framework :: Django Topic :: Multimedia :: Graphics :: Graphics Conversion @@ -38,11 +38,6 @@ install_requires = setup_requires = setuptools_scm - pytest-runner -tests_require = - pytest - pytest-cov - pytest-django [options.package_data] * = *.txt, *.rst, *.html, *.po @@ -52,20 +47,23 @@ exclude = tests [options.extras_require] +test = + pytest + pytest-cov + pytest-django progressbar = progressbar2>=3.0.0 [bdist_wheel] universal = 1 -[aliases] -test = pytest - [tool:pytest] +testpaths = + tests norecursedirs=venv env .eggs DJANGO_SETTINGS_MODULE=tests.settings addopts = --cov=stdimage --nomigrations --tb=short filterwarnings = - error + ignore::DeprecationWarning [coverage:run] source = . diff --git a/stdimage/models.py b/stdimage/models.py index 97ff514..bed00d7 100644 --- a/stdimage/models.py +++ b/stdimage/models.py @@ -1,5 +1,6 @@ import logging import os +import warnings from io import BytesIO from django.core.files.base import ContentFile @@ -11,12 +12,21 @@ ImageFileDescriptor, ) from PIL import Image, ImageFile, ImageOps +from PIL.Image import Resampling from .validators import MinSizeValidator logger = logging.getLogger() +warnings.warn( + "The django-stdimage is deprecated in favor of django-pictures.\n" + "Migration instructions are available in the README:\n" + "https://github.com/codingjoe/django-stdimage#migration-instructions", + DeprecationWarning, +) + + class StdImageFileDescriptor(ImageFileDescriptor): """The variation property of the field is accessible in instance cases.""" @@ -186,7 +196,7 @@ class StdImageField(ImageField): "width": None, "height": None, "crop": False, - "resample": Image.ANTIALIAS, + "resample": Resampling.LANCZOS, } def __init__( @@ -306,6 +316,19 @@ def save_form_data(self, instance, data): file.delete(save=False) super().save_form_data(instance, data) + def deconstruct(self): + name, path, args, kwargs = super().deconstruct() + return ( + name, + path, + args, + { + **kwargs, + "variations": self._variations, + "force_min_size": self.force_min_size, + }, + ) + class JPEGFieldFile(StdImageFieldFile): @classmethod diff --git a/tests/test_utils.py b/tests/test_utils.py index 2ab2c68..7180204 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,7 +1,7 @@ import os import pytest -from PIL import Image +from PIL.Image import Resampling from stdimage.utils import render_variations from tests.models import ManualVariationsModel @@ -24,7 +24,7 @@ def test_render_variations(self, image_upload_file): "width": 150, "height": 150, "crop": True, - "resample": Image.ANTIALIAS, + "resample": Resampling.LANCZOS, } }, ) pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy