Skip to main content

Image handling for Django. Includes upload code, repository and filter creation. Low on features, fast setup, modular.

Project description

Django-images

A drop and go app (as much as a Django app can be) to handle upload and display of images.

Plug-and-go means a default repository built into the app. It works in a Django way, and needs only a migration to start uploading and managing images. To show images, only a few filter classes are needed, and to place the main template tag. That's all.

The base code is concise and levers Django recommendations and facilities where possible. It may provide a base for others wishing to build their own app.

The distribution is called 'django-img', but internally the module is called 'image'.

Why you may not want this app

This may not be the app for you.

The app API does not let you,

  • write template tags with adjustable parameters
  • filter with runtime parameter processing
  • create dynamic filter chains
  • categorise/tag images

All these facilities could be built into the base code. But then the app will not be a plug and go solution.

The code API is a step back from the facilities mentioned above. It is small, concise, and fits good CSS/template-practice.

Also, I have not,

  • considered SVG images or movie files
  • tested cloud storage

Overview

overview diagram

Images are tracked in the database. The base model is called 'Image'.

Each original image can generate derivative images. These are tracked by a model called 'Reform'. Reforms are generated by filters. Filters can be defined in apps or centrally.

Image delivery is by template tag. The presence of a tag with a reference to an image and a filter will generate a reform automatically. The tags deliver images by URL.

File-based storage is in 'media/' with paths adjustable through attribute settings.

The app includes code to upload images. Some custom Django admin is provided, which is optional and easy to modify/replace.

If you have done this before

Quickstart

Depemdancies

Unidecode,

pip unidecode

Unidecode

Pillow,

pip pillow

Pillow

Optional

To use Wand filters, on Debian-based distros,

sudo apt-get install libmagickwand-dev

Then,

pip install wand

Install

Download the app code to Django.

Declare in Django settings,

    INSTALLED_APPS = [
        ...
        'image.apps.ImageConfig',
        ...
    ]

Migrate,

./manage.py makemigrations image
./manage.py migrate image

If you have Django Admin, you can now upload images.

Upload some images

In Django admin, go to Image upload and upload a few images.

I don't know about you, but if I have a new app I like to try with real data. If you have a collection of test images somewhere, try this management command,

./manage.py image_create_bulk pathToMyDirectory

You can create, meaning upload and register, fifteen or twenty images in a few seconds.

View some images

Ok, let's see an image. Two ways,

Use a view

Find a web view template. Nearly any template will do (maybe not a JSON REST interface, something visible).

Add this tag to the template,

{% load img_tags %}
...
{% imagequery image.Image "pk=1" image.Thumb %}

'image.Thumb' is a predefined filter. It makes a 64x64 pixel thumbnail. The tag we use here searches for an image by a very low method, "pk=1". This will do for now.

Visit the page. The app will generate the filtered 'reform' image automatically.

Don't have a view?

Yeh, new or experimental site, I know. Image has a builtin view. Goto 'urls.py', add this,

from image.views import ImageDetailView
...
path('image/<int:pk>/', ImageDetailView.as_view(), name='image-detail'),

Now visit (probably) http://localhost:8000/image/1/ To see some real web code.

(optional) See a broken image

Use the management command to remove reforms,

./manage.py reform_delete

No better way to make a truly broken file... Goto '/media/originals' and delete a file (maybe the file you are currently viewing as a reform).

Now refresh the view. The app will try to find the reform. When it fails, it will attempt to make a new reform. But the original file is missing, so it will fail to do that too. It will then display a generic 'broken' image.

(aside) Filters

Perhaps your first request will be how to make a new filter.

Make a new file called 'image filters'. Put it in the top level of any app (not in the site directory, that can be done but must be configured in a settings.py. Put something in like this (adapt if you wish),

from image import Resize, registry

class MediumImage(Resize)
    width=260
    height=350
    format='png'

registry.register(MediumImage)

Now adapt the template tag (or the tag in 'image/templates/image/image_detail.html') to point at the new filter,

{% imagequery image.Image "pk=1" someAppName.MediumImage %}

Visit the page again. Image uses the new filter definition to generate a new reform (filters the image) then displays it.

Ok, you changed the image size, and maybe the format. If you want to continue, you probably have questions. Goto the main documentation.

QuickStop

Don't like what you see?

  • Remove any temporary code.
  • Migrate backwards ('./manage.py migrate image zero')
  • Remove from 'apps.py'
  • Remove the two directories '/media/originals/', '/media/reforms/'
  • Remove the app folder.

That's it, gone.

Full documentation

Index,

Model Fields

Two ways,

Custom ImageRelationFieldMixin fields

There are two, ImageOneToOneField and ImageManyToOneField (for image pools),

from image.model_fields import ImageManyToOneField


class Page(models.Model):

    img = ImageManyToOneField(
        'page.Image'
        )
Auto-delete images

An ImageOneToOneField field can auto-delete associated Image models and files (and their reforms) with the model. See Auto Delete

Stock Django

You can also use a Django foreign key declaration,

from image.models import Image


class Page(models.Model):

    img = models.ForeignKey(
        'page.Image',
        null=True,
        blank=True,
        on_delete=models.SET_NULL,
        related_name='+'
        )

     etc.

null=True and blank=True means users can delay adding an image until later. And related_name='*' means that Images will not track the models you are creating. See Django documentation of model fields for more details.

Only use models.CASCADE if you are sure this is what you want. It means, if an image is deleted, the model that carries the image is deleted too. This is not usually what you want.

Choosing between the two

ImageOneToOneField/ImageManyToOneField

  • tidy
  • Works with preconfigured admin and auto-delete

Foreign Field

  • Django stock
  • explicit
  • flexible configuration

Custom Image Repositories

Overview

New repositories can be made by subclassing the the core models.

Reasons you may want to do this,

Custom repository behaviour

Custom repositories have new DB tables, and can operate with new configurations such as storing files in different directories, auto-deleting original files, not limiting filename sizes and more.

Associate data with images

You may want to associate data with an image. Many people's first thought would be to add a title, as the app does not provide titles by default. But other kinds of information can be attached to an image such as captions, credits, dates, and/or data for semantic/SEO rendering.

Split needs

For example, you may want a repository attached to a main Article model, and also an image pool for general site use such as banners or icons.

Subclassing Image/Reform

Custom Image repository code is placed in 'models.py' files and migrated. You decide how you want your namespacing to work. The code can be placed in an app handling one kind of model or, for general use, in a separate app.

Here is a minimal subclass. In the 'models.py' file in an app, do this,

from image.models import AbstractImage, AbstractReform

class NewsArticleImage(AbstractImage):
    reform_model = 'NewsArticleReform'
    upload_dir='news_originals'

    # AbstractImage has a file and upload_date
    caption = models.CharField(_('Caption'),
        max_length=255,
    )

    author = models.CharField(_('Author'),
        max_length=255,
        db_index=True
    )

    etc.



class NewsArticleReform(AbstractReform):
    image_model = NewsArticleImage
    upload_dir='news_reforms'

    # exactly the same in every subclass
    image = models.ForeignKey(image_model, related_name='+', on_delete=models.CASCADE)

Not the last word in DRY coding, but you should be able to work out what the code is for. Note that 'image_model' and 'reform_model' are explicitly declared. Note also that 'reform_model' is declared as a string, but 'image_model' is declared as a class.

Migrate,

./manage.py makemigrations NewsArticle
./manage.py migrate NewsArticle

You now have a new image upload app. It has it's own DB tables. Change it's configuration (see next section). Refer to it in other models,

class NewsArticle(models.Model):

    img = ImageManyToOneField(
        "news_article.NewssArticleImage"
        )

    etc.

Attributes

Subclasses accept some attributes. Note that some of these settings are radical alterations to a model class. To be sure a model setting will take effect, it is best to migrate the class.

An expanded version of the above,

from image.models import AbstractImage, AbstractReform

class NewsArticleImage(AbstractImage):
    reform_model = 'NewsArticleReform'
    upload_dir='news_originals'
    accept_formats = ['png']
    filepath_length=55
    max_upload_size=2
    form_limit_filepath_length=True
    auto_delete_files=True

    etc.



class NewsArticleReform(AbstractReform):
    image_model = NewsArticleImage
    upload_dir='news_reforms'
    file_format='png'
    jpeg_quality=28

    # exactly the same in every subclass
    image = models.ForeignKey(image_model, related_name='+', on_delete=models.CASCADE)

Some of these attributes introduce checks ('max_upload_size'), some set defaults('file_format'), some can be overridden ('file_format', 'jpeg_quality' can be overridden by filter settings). See Settings for details.

Now migrate, and you are up and running.

Inheritance! Can I build repositories using OOP techniques?

No! Python has been cautious about this kind of programming, and Django's solutions are a workround. Try stacking models of any kind and, unless you know the code line by line, the classes will create unusable migrations. In the current situation, for stability and maintainability, create models directly from the two abstract bases.

Can I create different repositories, then point them at the same storage paths?

The app tracks through the database tables, and the management commands work from them, so yes, you can. That said, when code offers opportunities for namespacing/ecapsulaytion, you need a good reason to ignore it.

Things to consider when subclassing models

Do I need to migrate the default 'image' for a custom repository?

No. Migrate the app containing the repository code. If you got here later, and already have the core repository migrated, but don't need the data, you can zero migrate and remove the file folders.

Auto delete of files

May be good to set up your deletion policy from the start. See Auto Delete

Add Meta information

You may want to configure a Meta class. If you added titles or slugs, for example, you may be interested in making them into unique constrained groups, or adding indexes,

class NewssArticleImage(AbstractImage):
    upload_dir='news_originals'
    filepath_length=100

    etc.

    class Meta:
        verbose_name = _('news_image')
        verbose_name_plural = _('news_images')
        indexes = [
            models.Index(fields=['author']),
        ]
        constraints = [
            models.UniqueConstraint(
                fields=['title', 'author'], 
                name='unique_newsarticle_reform_src'
            )

Auto-delete

Overview

I read somewhere that a long time ago, Django would auto-delete files. This probably happened in model fields. This behaviour is not true now. If objects and fields are deleted, files are left in the host system. However, it suits this application, and some of it's intended uses, to auto-delete models and files. If you would like this behaviour, the app provides some solutions.

There are aspects to this. First, deletion of image models when a supporting model is deleted. Then, deletion of reform models when the image model is deleted. Finally, deletion of files, Image or Reform, when an model is deleted.

Automatic deletion of image models when a supporting object is deleted

Place this in the ready() method of the application,

from image.signals import register_image_delete_handler


class NewsConfig(AppConfig):
    ...

    def ready(self):
        super().ready()
        from news_article.models import NewsArticle
        register_image_delete_handler(NewsArticle)

The image fields must be ImageOneToOneFields, and the field attribute must be 'auto_delete=True'.

Why must the code run on the custom field, and why not on a ImageOneToManyField?

The custom field means lightweight field identification.

An ImageOneToManyField implies an image pool. Many connections to one image. This is a classic computing problem of reference counting. Best avoided.

Automatic deletion of reform objects

Reforms are treated as objects generated automatically, so automatic deletion is not controversial. Moreover, the template tag accesses reforms through the Image model so if the model for original image is deleted, the reforms are useless. Reform models are deleted by default, and so are the files.

Automatic deletion of files

The application provides a signals solution.

Auto-delete Image files

Image file deletion is optional. To auto-delete, use a custom repository and set the Image model attribute 'auto_delete_files=True'.

Auto-delete Reform files

Reform files are deleted with the reform. There is nothing else to do.

Behaviour of the default repository

By default, the default repository will not auto-delete the original files associated with Images. It will auto-delete reform models and their files.

Filters

Overview

Filters are used to describe how an uploaded image should be modified for display. In the background, the app will automatically adjust the image to the filter specification (or use a cached version).

A few filters are predefined. One utility/test filter,

Thumb
A 64x64 pixel square

And some base filters, which you can configure. These are centre-anchored,

  • Crop
  • Resize
  • SmartCrop
  • SmartResize

If you only need different image sizes, you only need to configure these. But if you want to pass some time with image-processing code, you can add filters to generate ''PuddingColour' and other effects.

Filter placement and registration

Files of filter definitions can be placed in any app. Create a file called 'image_filters.py' and off you go.

If you would prefer to gather all filters in one place, define the settings to include,

IMAGES = [
    {
        'SEARCH_MODULES': [
                    "siteName",
        ],
    },
]

Then put a file 'image_filters.py' in the 'sitename' directory. If you use a central file, you should namespace the filters,

BlogPostLarge:
    width : 256
    height 256

Filter declarations

All builtin filter bases accept these attributes,

  • width
  • height
  • format

Most filter code demands 'width' and 'height', but 'format' is optional. Without a stated format, the image format stays as it was (unless another setting is in place). Formats accepted are conservative,

bmp, gif, ico, jpg, png, rgb, tiff, webp 

which should be written as above (lowercase, and 'jpg', not 'jpeg'). So,

from image import Resize, registry

class MediumImage(Resize)
    width=260
    height=350
    format='png'

registry.register(MediumImage)

Crop and Resize can/often result in images narrower in one dimension.

The Smart filters do a background fill in a chosen colour, so return the requested size,

from image import ResizeSmart, registry

class MediumImage(ResizeSmart):
    width=260
    height=350
    format='jpg'
    fill_color="Coral"

registry.register(MediumImage)

Fill color is defined however the image library handles it. Both Pillow and Wand can handle CSS style hex e.g. '#00FF00' (green), and HTML colour-names e.g. 'AliceWhite'.

Registering filters

Filters need to be registered. Registration style is like ModelAdmin, templates etc. Registration is to 'image.registry' (this is how templatetags find them).

You can use an explicit declaration,

from image import ResizeSmart, registry

...

registry.register(single_or_list_of_filters)

Or use the decorator,

from image import register, ResizeSmart

@register()
class MediumImage(ResizeSmart):
    width=260
    height=350
    format='jpg'
    fill_color="Coral"

Wand filters

The base filters in the Wand filter set have more attributes available. The 'wand' code needs Wand to be installed on the host computer. Assuming that, you gain these effects,

from image import filters_wand, register

@register()
class Medium(filters_wand.ResizeSmart):
    width=260
    height=350
    format='jpg'
    pop=False
    greyscale=False
    night=False
    warm=False
    strong=False
    no=False
    watermark='image/watermark.png'

If you enable more than one effect, they will chain, though you have no control over order.

I lost my way with the Wand effects. There is no 'blur', no 'rotate', no 'waves'. But there is,

pop
Tightens leveling of black and white
greyscale
A fast imitation
night
Pretend the picture is from a movie
warm
A small shift in hue to compensate for a common photography white-balance issue.
strong
Oversaturate image colors (like everyone does on the web). Unlike 'pop' this will not stress contrast so flatten blacks and whites. You may or may not prefer this.
no
Draw a red cross over the image
watermark
Accepts a URL to a watermark image template.

Watermark deserves some explanation. This does not draw on the image, as text metrics are tricky to handle. You configure a URL stub to an image, here's a builtin,

watermark = 'image/watermark.png'

The URL is Django static-aware, but will pass untouched if you give it a web-scheme URL (like the URLs in Django Media).

The template is scaled to the image-to-be-watermarked, then composited over the main image by 'dissolve'. So the watermark is customisable, can be used on most sizes of image, and is usually readable since aspect ratio is preserved.

It is probably worth saying again that you can not change the parameters, so the strengths of these effects, without creating a new filter.

Writing custom filter code

First bear in mind that Image uses fixed parameters. So your filter must work with fixed parameters across a broad range of uploaded images. I don't want anyone to dive into code, put in hours of work, then ask me how they can create an online image-editing app. Not going to happen.

However, while I can't make a case for 'waves' or 'pudding-colour' filters, I can see uses. For example, Wagtail CMS uses the OpenCV library to generate images that auto-focus on facial imagery (i.e. not centrally crop). There are uses for that.

Second, bear in mind that image editing is lunging into another world, rather like creating Django Forms without using models and classes. It will take time. But there is help available.

Inherit from 'image.Filter'. You will need to provide a 'process' method, which takes an open Python File and returns a ByteBufferIO and a file extension.

If you want the filter to work with the Pillow or Wand libraries, you can inherit from the PillowMixin or WandMixin. These cover filehandling for those libraries. Then you can provide a 'modify' method, which alters then returns an image in the format of those libraries.

See the code for details.

Why can filters not be chained or given parameters?

This app only enables creation of fixed filters intended for a broad range of images. You write a filter with parameters, the processing order is fixed, and it is set.

This is a deliberate decision. It makes life easy. If you want to produce a front-end that can adjust the filters, or chain them, that is another step. This is not that app.

Admin

Overview

Image ships with stock Django admin. However, this is not always suited to the app, it's intended or possible uses. So there are some additions.

The admin provided has an attitude about how to use the app. It assumes that each model instance is locked to one file. If a model exists, then the file exists. If the admin is given the same file, it duplicates the file and model.

In this system, models that refer to Image models can be null and blank, which represents 'image not yet uploaded'. And it is possible to build systems that reuse images. It is the Image_instance->file connection that is locked.

Package solutions

ImageCoreAdmin

For administration and maintenance of image collections. This is a specialised use, which would only be visible to end users if trusted.

The configuration is built into the image app, and can be used on custom repositories too.

Significant changes,

  • changelist is tidier and includes 'view' and 'delete' links
  • changelist has searchable filenames
  • change form has 'readonly' file data
Remove ImageCoreAdmin from central repository

Want Django stock admin? Change the comments in 'image/admin.py' from,

# Custom admin interface disallows deletion of files from models.
class ImageAdmin(ImageCoreAdmin):
    
# Stock admin interface.
#class ImageAdmin(admin.ModelAdmin):
    pass
        
        
admin.site.register(Image, ImageAdmin)

to,

# Custom admin interface disalows deletion of files from models.
#class ImageAdmin(ImageCoreAdmin):
    
# Stock admin interface.
class ImageAdmin(admin.ModelAdmin):
    pass
    
    
admin.site.register(Image, ImageAdmin)
Notes and alternatives for the core admin

You may provide no core admin at all. You can use the './manage.py' commands to do maintenance. The stock admin is provided to get you started.

If you prefer your own core admin, have a look at the code for ImageCoreAdmin in '/image/admins.py'. It provides some clues about how to do formfield overrides and other customisation.

If using subclassed Image/Reform models, you may find it more maintainable to duplicate and modify the admin code, rather than import and override.

LinkedImageAdmin

For administration of models that contain foreign key links to images.

This is a small override that should not interfere with other admin code. It disallows image editing once an image has been connected to a field (by upload or selection). e.g.

from image.addmins import LinkedImageAdmin

    class NewsArticleAdmin(LinkedImageAdmin, admin.ModelAdmin):
        pass
        
        
    admin.site.register(NewsArticle, NewsArticleAdmin)

Forms

Overview

There's nothing special about using this app in forms. Even the custom fields are mostly renames to tidy code, and namespace custom admin configurations.

That said, the fields used internally are not standard.

ImageFileField

If you are interested in the internals, this does a few extra jobs beyond an ImageFile,

Contains extra config attributes,
Most of which are gathered from the class configuration (e.g. max_upload size). The class can deconstruct these for migrations.
extra validators
Beyond the standard Django field, this field and it's formfield actively check filesizes and extensions. Calls to is_valid() will run these validations.

For references to images

When images are referenced from another model, this would usually use a Foreign Key on the referring model.

Template Tags

Overview

For most uses, the app has only one template tag. There is another, for testing and edge cases.

The 'image' tag

Let's say there is a filter named ''Large'. Add this to template code,

{% load img_tags %}

{% image report.img my_app.Large %}

then visit the page. The app will generate a ''Large' reform of 'report.image', to the spec given in the filter.

The tag can guess the app from the context. So,

 {% image report.img Large %}

Will assume the filter is in the app the context says the view comes from.

The tag accepts keyword parameters which become HTML attributes,

 {% image report.img Large class="report-image" %}

This renders similar to,

<img src="/media/reforms/eoy-report_large.png" alt="eoy-report image" class="report-image">

The 'query' tag

There is a tag to find images by a database query. Sometimes this will be useful, for fixed or temporary decoration/banners etc. It must be given the app/model path in full,

    {{ imagequery some_app_name.some_image_model_name "some_query" image.Large  }}

e.g.

    {{ imagequery image.Image "pk=1" image.Large  }}

or,

    {{ imagequery "src="taunton-skyscraper"" image.Large  }}

While this may be useful, especially for fixed logos or banners, if you are passing a model through a context it is unnecessary.

Filters from other apps

You can borrow filter collections from other apps. Use the module path and filter classname,

{% image "urban_decay" different_app.filter_name  %} 

But try not to create a tangle between your apps. You would not do that with CSS or other similar resources. Store general filters in a central location, and namespace them.

Management Commands

They are,

  • image_create_bulk
  • image_sync
  • image_list
  • reform_delete

All management commands can be pointed at subclasses of Image and Reform, as well as the default app models.

They do what they say. 'image_sync' is particularly useful, it will attempt to make models for orphaned files, or delete orphaned files, or delete models with missing files. These commands are useful for dev, too.

Settings

Overview

Image accepts settings in several places. The app has moved away from site-wide settings towards other placements. Here is a summary, in order of last placement wins,

Image

reform_model
default=AbstractReform
upload_dir
default='originals'
filepath_length
default=100
form_limit_filepath_length
default=True
accept_formats
default=None
max_upload_size
default=2MB
auto_delete_files
(if the Image is deleted, the file is deleted too) default=False

Reform

upload_dir
default='reforms'
image_model
default='image.Image'
file_format
(set the default format of reforms) default=original format, Reform attribute, filter attribute
jpeg_quality
(set the quality of JPEG reforms) default=80, Reform attribute, filter attribute

ImageOneToOneField

auto_delete
if True, and signals are enabled, deletion of this model will delete image models.
(the field naturally has many other settings, this setting is the only one related to this app)

Site-wide settings

Images accepts some site-wide settings,

IMAGES = [
    {
        'BROKEN': 'myapp/lonely.png',
        'SEARCH_APP_DIRS': True,
        'SEARCH_MODULES': [
                    "someSiteName",
        ],
    },
]
BROKEN
URL of a static file displayed in place of unreadable files. Takes a static-aware URL to a file. URLs with a web-based scheme pass untouched, relative URLs are assumed in an app's static folder. Default is 'image/unfound.png'.
SEARCH_APP_DIRS
Find 'image_filters.py' files in apps. If False, the app only uses filters defined in the core app and SEARCH_MODULES setting.
SEARCH_MODULES
Defines extra places to find 'image_filter.py' files. The above example suggests a site-wide filter collection in the site directory (most page-based Django sites have central collections of templates and CSS in the same directory). The setting takes module paths, not filepaths, because 'image_filter.py' files are live code.

Broken Images

The app throws a special error if images are broken i.e. files are missing or unreadable. In this case a stock 'broken' image is returned. Using the standard tags there is no need to configure or change in any way for this. The image can be redefined.

Utilities

The View

Image has a builtin view. It's main purpose is to test filter code. The view template reforms from images in the core repository. As a test and trial device, it is not enabled by default.

Goto urls.py, add this,

path('image/<int:pk>/', ImageDetailView.as_view(), name='image-detail'),

Now visit (probably),

http://localhost:8000/image/1/

The template is at,

image/templates/image/image_detail.html

In the template you can edit the tag to point at your own configurations. With visible results and basic image data, the view is often easier to use than the shell.

Notes

No SVG support: would require shadow code, Pillow especially can't handle them.

Widths, heights and bytesize of original images are recorded: in case the storage media is not local files but cloud provision.

The app generally uses URLs, not the filepaths: this can be confusing, but means the app is 'static' aware, and it should keep working if you swap storage backends.

Credits

The upload and storage code (particularly the replicable models) are ripped from the Wagtail CMS. Though I am responsible for what has happened after that.

Wagtail documentation

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

django-img-0.0.1.tar.gz (84.9 kB view hashes)

Uploaded Source

Built Distribution

django_img-0.0.1-py2.py3-none-any.whl (96.8 kB view hashes)

Uploaded Python 2 Python 3

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page