# ImageLite
An app for local filesystem image storage. In truth, it's not 'Lite', with custom repository creation from an abstract base, an easy to configure and massively capable filter stack, enhanced upload options, and powerful template tags for rendering. But it's a reduction of a more general app, to avoid Django annoyances, so 'Lite'.

This is [a rewrite of Wagtail's Image app](#credits).

## Why you would not want to use this app
- It's (currently) written for local filesystem storage only (no remote)
- Raster image files only, no SVG, movie files, documents, etc.
- Filters can not be applied to individual images
- No effort to present filters to users, either admin or visitors
- The user must manage filenames (see below)
- No builtin categorisation and/or tagging



## Why you would want to use this app
- Abstract base allows any number of repositories with custom configurations
- If you want to scale beyond the app, the data is unobtrusive and needs no rebuilding
- Auto-generates filtered images
- Filter declarations can travel with apps (like CSS)
- Two (primary) template tags


 
## The key point about this app---please read
If you wish to associate files with pages, the app can do this. But the app is not designed to use a Foreign field. It is designed to avoid use of foreign fields. 

So, if you have a model of, say, a product, how do you associate uploaded images with the model? You use a URL. The product is called 'BansheeX4000'. So an image of this product must have the filename 'BansheeX4000.png' (the URLs are easily configurable, but this is the most likely scheme).

There are downsides to this approach,
- It puts the burden on the user to create filenames as intended. A solution using foreign keys could upload any image, then automatically make the connection
- Since the models do not know (much) about the attached images, much editing of uploaded images is pushed back onto management commands 
- Django admin for the models can not display the upload form (no model field to render). 

But there are upsides,
- Django admin displays for foreign fields are very limited. As default they are select boxes. ImageLite's URL system is more pleasant.
- Without foreign fields, the syatem is easier to scale. If you wish to move image handling out of Django, there are no foreign fields to remove, and the image data is naturally organised

 
## Overview
There is a base model called AbstractImage. From this, sub-models can be constructed. Each sub-model is an image repository. It tracks uploads in a database table, and stores files in a location. Locations are in subfolders of MEDIA_ROOT.

Each original image can generate derivative images. These are untracked and generated on a sucessful upload. These derivative images are called 'reforms' and are generated by filters. Reforms can be thumbnails, watermarked etc. 

Image delivery is by template tag. The tags write HTML elements with an appropriate URL. The model tag constructs the URL by asking the model instances they recieve.

## If you have done this before
- [subclass the models for Image and Reform](#custom-image-repositories) to create a repository. Tweak settings.
- Migrate custom repository tables
- Set an [admin](#admin)
- Create 'image_filters.py' files in page apps, then [subclass a few filters](#filters)
- Insert [template tags](#rendering) into relevant templates


## Quickstart
### Depemdancies
Pillow,

    pip install pillow

[Pillow](https://pillow.readthedocs.io/en/stable/index.html)


#### Optional
To use Wand filters, on Debian-based distros,

    sudo apt-get install libmagickwand-dev

Then,

    pip install wand


### Install
PyPi,

    pip install django-imagelite

Or download the app code to Django.

Declare in Django settings,

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

Migrate,

    ./manage.py makemigrations image_lite
    ./manage.py migrate image_lite

Now you need to [declare a repository](#custom-image-repositories). Further examples assume the example repository created there.


### Make some filters
In the repository, create a flle image_filtetrs.py. Paste this into it,

    from image_lite import filters_pillow, register

    @register()
    class Thumb(filters_pillow.Resize):
        width=64
        height=64
        format='png'

Change as you wish.

### Upload some images
In Django admin, 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 news_article_images.NewsArticleImage pathToMyDirectory

Note you need to give a path to a Model. You can create, meaning upload and register, fifteen or twenty images in a few seconds.

### View some images
Ok, let's see an image. 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 %}
    ...
    {% image_fixed news_images.NewsImage riverbank-renforcement Thumb %}


'news_images.NewsImage' is the path to the repository model. 'riverbank-renforcement' is the filename. 'Thumb' is the filter we made earlier. 

Visit the page. You should see a thumbnail of the 'riverbank-renforcement' image.


## QuickStop
Don't like what you see?

- Remove any temporary code.
- Migrate backwards ('./manage.py migrate imagelite zero')
- Remove from 'apps.py'
- Remove the two media directories. Defaults are '/media/originals/' and '/media/reforms/'
- Remove the app folder, or uninstall

That's it, gone.

## Full documentation
Index, 
- [Image Repository Models](#image-repository-models)
- [Model Fields](#model-fields)
- [Auto Delete](#auto-delete)
- [Filters](#filters)
- [Admin](#admin)
- [Rendering](#rendering)
- [Management Commands](#management-commands)


## Image Repository models
### Overview
New repositories can be made by subclassing the the core model. Reasons you may want to customise repositories,

#### 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 an image repository attached to a main Article model, but also an image pool for general site use such as banners or icons. 
 

### Subclassing AbstractImage 
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. For a seperate app,

    ./manage.py startapp news_article_images

Declare the app in settings.py,

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

Here is a minimal subclass. In a 'models.py' file, do this,

    from django.db import models
    from image_lite.models import AbstractImage


    class NewsArticleImage(AbstractImage):
        upload_dir='news_originals'
        reform_dir='news_reforms'

        # AbstractImage has a file and upload_date
        # but add whatever new fields you want e.g.
        caption = models.CharField(_('Caption'),
            max_length=255,
        )

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

        etc.

Migrate,

    ./manage.py makemigrations news_article_images
    ./manage.py migrate news_article_images

You now have a new image upload app. It has it's own DB table. You can change it's configuration (see next section).


### 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):
        upload_dir='news_originals'
        filepath_length=55
        form_limit_filepath_length=True
        accept_formats = ['png']
        max_upload_size=2
        auto_delete_files=True
        reform_dir='news_reforms'
        ...

I hope what these attributes do is easy to understand. Nine of them are available through standard Django or, not in this simple way.

Migrate, and you are up and running.

### Blocking file renaming
It is impossible to have two files in the same directory with the same name (this applies also to remote storage). The default behaviour of Django uploading is to enable uploads if possible, so the standard FileSystemStorage module will rename suplicate filenames. It does this by adding a pseudo-random sequence of codepoints to the filename. 

For the general outlook of ImageLite, this is not always what is wanted. If a user has uploaded an image called 'Spin_X9000CA', a later attempt to add a  'Spin_X9000CA' image should be blocked, or replace the first image (standard behaviour would be to generate a new filename such as 'Spin_X9000_CA_WEN"£HDOE5'). 

Only you can decide if blocking is more appropriate than renaming. There is a builtin way to block duplicate filenames, using a mixin,

    ...
    from image_lite import ModelUniqueFilenameMixin

        class RevImage(ModelUniqueFilenameMixin, AbstractImage):
            ...
If an image repository uses this mixin, attempts to upload  duplicate filenames cause a ValidationError. The user sees a message 'filename already exists'.


### 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. Do not stack models or you will create unusable migrations.


### Can I create different repositories, then point them at the same storage paths?
No, you risk duplicate entries.


### Things to consider when subclassing models
#### Auto delete of files
Good to decide a deletion policy from the start. See [Auto Delete](#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'
                )
            indexes = [
                models.Index(fields=['upload_time']),
            ]

Note that the base Image does not apply an index to upload_time, so if you want that, you must specify it.

## Auto-delete
### Overview
I read somewhere that a long time ago, Django auto-deleted 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 files when the traacking model is deleted.

#### Auto-delete Image files
To auto-delete, set the Image model attribute 'auto_delete_files=True'.  This action is triggered by a signal, so will work for bulk deletes also. The code attempts to remove both the original image and reforms. Reform removal uses constructed filepaths, so is efficient for what it is, but can not clean orphaned reforms or other unsync issues.


## Filters
### Overview
Filters are used to describe how an uploaded image should be modified for display. On upload, the app will automatically generate reformed images.
 
You can use the provided filters. 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 should be placed in a repository app. Create a file called 'image_filters.py' and off you go.


### Base filters
The filter code is an in heriting stack of classes. There are 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.


### Filter declarations
All builtin filter bases accept these attributes,

- width
- height
- format

Most filter code demands 'width' and 'height' and 'format'.'format' defaults to 'jpg'. 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'
        #fill_color="Coral"
        #jpeg_quality=28
        # optional effects

    registry.register(MediumImage)


### 'Smart' filters
Crop and Resize will crop or stretch the sides of images that do not fit the declared aspect ratio. This is behaviour as stated, and can be useful in many situations.

However, especially for premium display of an assorment of images, it may be preferable that the image is always fully contained inside the declared dimensions. This is what the Smart filters do. Smart filters preserve aspect ratio, make the image fit in the dimensions, then fill surplus bounding areas with a fill colour,

    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_lite import ResizeSmart, registry

    ...

    registry.register(single_or_list_of_filters)

Or use the decorator,

    from image_lite 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,

<dl>
    <dt>pop</dt>
    <dd>
        Tightens leveling of black and white
    </dd>
    <dt>greyscale</dt>
    <dd>
        A fast imitation
    </dd>
    <dt>night</dt>
    <dd>
        Pretend the picture is from a movie
    </dd>
    <dt>warm</dt>
    <dd>
        A small shift in hue to compensate for a common photography white-balance issue. 
    </dd>
    <dt>strong</dt>
    <dd>
        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. 
    </dd>
    <dt>no</dt>
    <dd>
        Draw a red cross over the image
    </dd>
    <dt>watermark</dt>
    <dd>Accepts a URL to a watermark image template.
    </dd>
</dl>

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?
Because it makes life easy for coders and users. If you want to produce a front-end that can adjust filters, or chain them, that is another step. This is not that app.


## Admin
### Overview
Repositort apps start with stock Django admin. However, this is not always suited to the app, it's intended or possible uses. So there is a pre-configured admin.

#### ImageLiteAdmin
Significant changes from stock admin,

- changelist is tidier and includes filenames not paths
- changelist includes 'view' and 'delete' links
- changelist has searchable filenames

Easy as this,

    from django.contrib import admin
    from image_lite.admins import ImageLiteAdmin
    from news_images.models import NewsImage

 
    # Register your models here.
    class NewsImageAdmin(ImageLiteAdmin):
        pass
    
    
    admin.site.register(NewsImage, NewsImageAdmin)

##### Notes and alternatives for ImageLiteAdmin
You may provide no 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 admin, look at the code for ImageLiteAdmin in '/image/admins.py'. It provides some clues about how to do formfield overrides and other customisation. You may find it more maintainable to build the admin code, rather than import and override.


## Rendering
### Overview
You can render the images in HTML. Since filenaming is controlled, this will often be easy,

    <img src="/media/reforms/logo.png" alt="image of logo" class="main-logo">

or use template tags.

### Template Tags
Currently, the app has two template tags. Both build a full HTML 'img' tag. Both accept keyword parameters which become HTML attributes.

#### The 'image' tag
The main tag. It depends on calling a page creation model to construct a URL. So you need a callable on the page model (not the image model). This callable is usually very simple, and must return the entire reform filename. You can make a few callables, one for every filter e.g.

    class NewsArticle():  
        ...
        def image_main_url(self):
            url = self.title + '-Stock.png' 
            return url

        def image_teaser_url(self):
            url = self.title + '-Teaser.png' 
            return url


Ok, let's call the method from a template, with the image repository, template main variable, and callable name as parameters,

    {% load img_tags %}

    {% image news_images.NewsImage newsartricle image_main_url class="framed" %}
 
then visit the page. Should see the image? As HTML it should render something like,

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


#### The 'image-fixed' tag
The above tag is mainly to retrieve images associated with page data. This tag lets you call by filename. Add this to template code, with the image repository, filename, and filtername as parameters,

    {% load img_tags %}

    {% image_fixed news_images.NewsImage forest_fire Stock class="ruled" %}


then visit the page. Should see the image?

## Management Commands
They are,

- image_create_bulk
- image delete_bulk
- image_list
- image_sync
- reform_create
- reform_delete
- reform_list

All management commands must be pointed at subclasses of Image. Several have a common switch -c/--contains, which makes a basic string match. 'image_sync' can be particularly useful for half-broken repositories, it will attempt to make models for orphaned files, or delete orphaned files, or delete models with missing files. Use the -h/--help switch to discover what each command can do.



## Tests
I've not found a way to test this app without Python ducktape. There should be tests, but I don't expect them.


## Credits
This is a rewrite of the Image app from Wagtail CMS. It is now distant from that app, nd would not work in the Wagtail system. However, some core ideas remain, such as the replicable repositories.

[Wagtail documentation](https://docs.wagtail.io/en/v2.8.1/advanced_topics/images/index.html)
