mirror of
https://github.com/Hopiu/django-imagekit.git
synced 2026-03-17 22:00:24 +00:00
Compare commits
No commits in common. "develop" and "0.3.6" have entirely different histories.
88 changed files with 809 additions and 6178 deletions
18
.gitignore
vendored
18
.gitignore
vendored
|
|
@ -1,17 +1,7 @@
|
||||||
*.db
|
|
||||||
*.egg*
|
|
||||||
*.orig
|
|
||||||
*.pyc
|
*.pyc
|
||||||
|
*.db
|
||||||
|
*.orig
|
||||||
.DS_Store
|
.DS_Store
|
||||||
.tox
|
|
||||||
.idea
|
|
||||||
.vscode
|
|
||||||
MANIFEST
|
|
||||||
build
|
|
||||||
dist
|
dist
|
||||||
/tests/media/*
|
build
|
||||||
!/tests/media/reference.png
|
MANIFEST
|
||||||
/venv
|
|
||||||
/venv3
|
|
||||||
/.env
|
|
||||||
/tags
|
|
||||||
|
|
|
||||||
35
.travis.yml
35
.travis.yml
|
|
@ -1,35 +0,0 @@
|
||||||
sudo: false
|
|
||||||
|
|
||||||
language: python
|
|
||||||
python:
|
|
||||||
- "3.8"
|
|
||||||
- "3.7"
|
|
||||||
- "3.6"
|
|
||||||
- "3.5"
|
|
||||||
env:
|
|
||||||
- DJANGO="master"
|
|
||||||
- DJANGO="30"
|
|
||||||
- DJANGO="22"
|
|
||||||
- DJANGO="21"
|
|
||||||
- DJANGO="21"
|
|
||||||
- DJANGO="20"
|
|
||||||
- DJANGO="111"
|
|
||||||
|
|
||||||
install:
|
|
||||||
- pip install tox
|
|
||||||
|
|
||||||
script:
|
|
||||||
- tox -e py$(python -c 'import sys;print("".join(map(str, sys.version_info[:2])))')-django${DJANGO}
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
fast_finish: true
|
|
||||||
allow_failures:
|
|
||||||
- env: DJANGO="master"
|
|
||||||
exclude:
|
|
||||||
- python: "3.5"
|
|
||||||
env: DJANGO="30"
|
|
||||||
- python: "3.5"
|
|
||||||
env: DJANGO="master"
|
|
||||||
|
|
||||||
notifications:
|
|
||||||
irc: "irc.freenode.org#imagekit"
|
|
||||||
56
AUTHORS
56
AUTHORS
|
|
@ -1,53 +1,15 @@
|
||||||
ImageKit was originally written by `Justin Driscoll`_.
|
Original Author:
|
||||||
|
|
||||||
The field-based API and other post-1.0 stuff was written by the bright people at
|
* Justin Driscoll (jdriscoll)
|
||||||
HZDG_.
|
|
||||||
|
|
||||||
Maintainers
|
|
||||||
-----------
|
|
||||||
|
|
||||||
* `Matthew Tretter`_
|
Maintainers:
|
||||||
* `Bryan Veloso`_
|
|
||||||
* `Chris Drackett`_
|
|
||||||
* `Greg Newman`_
|
|
||||||
|
|
||||||
Contributors
|
* Bryan Veloso (bryanveloso)
|
||||||
------------
|
* Chris Drackett (chrisdrackett)
|
||||||
|
* Greg Newman (gregnewman)
|
||||||
|
|
||||||
* `Josh Ourisman`_
|
|
||||||
* `Jonathan Slenders`_
|
|
||||||
* `Eric Eldredge`_
|
|
||||||
* `Chris McKenzie`_
|
|
||||||
* `Markus Kaiserswerth`_
|
|
||||||
* `Ryan Bagwell`_
|
|
||||||
* `Alexander Bohn`_
|
|
||||||
* `Timothée Peignier`_
|
|
||||||
* `Madis Väin`_
|
|
||||||
* `Jan Sagemüller`_
|
|
||||||
* `Clay McClure`_
|
|
||||||
* `Jannis Leidel`_
|
|
||||||
* `Sean Bell`_
|
|
||||||
* `Saul Shanabrook`_
|
|
||||||
* `Venelin Stoykov`_
|
|
||||||
|
|
||||||
.. _Justin Driscoll: http://github.com/jdriscoll
|
Contributors:
|
||||||
.. _HZDG: http://hzdg.com
|
|
||||||
.. _Bryan Veloso: http://github.com/bryanveloso
|
* Jonathan Slenders (jonathanslenders)
|
||||||
.. _Chris Drackett: http://github.com/chrisdrackett
|
|
||||||
.. _Greg Newman: http://github.com/gregnewman
|
|
||||||
.. _Josh Ourisman: http://github.com/joshourisman
|
|
||||||
.. _Jonathan Slenders: http://github.com/jonathanslenders
|
|
||||||
.. _Matthew Tretter: http://github.com/matthewwithanm
|
|
||||||
.. _Eric Eldredge: http://github.com/lettertwo
|
|
||||||
.. _Chris McKenzie: http://github.com/kenzic
|
|
||||||
.. _Ryan Bagwell: http://github.com/ryanbagwell
|
|
||||||
.. _Markus Kaiserswerth: http://github.com/mkai
|
|
||||||
.. _Alexander Bohn: http://github.com/fish2000
|
|
||||||
.. _Timothée Peignier: http://github.com/cyberdelia
|
|
||||||
.. _Madis Väin: http://github.com/madisvain
|
|
||||||
.. _Jan Sagemüller: https://github.com/version2
|
|
||||||
.. _Clay McClure: https://github.com/claymation
|
|
||||||
.. _Jannis Leidel: https://github.com/jezdez
|
|
||||||
.. _Sean Bell: https://github.com/seanbell
|
|
||||||
.. _Saul Shanabrook: https://github.com/saulshanabrook
|
|
||||||
.. _Venelin Stoykov: https://github.com/vstoykov
|
|
||||||
|
|
|
||||||
|
|
@ -1,24 +0,0 @@
|
||||||
Contributing
|
|
||||||
------------
|
|
||||||
|
|
||||||
We love contributions! These guidelines will help make sure we can get your
|
|
||||||
contributions merged as quickly as possible:
|
|
||||||
|
|
||||||
1. Write `good commit messages`__!
|
|
||||||
2. If you want to add a new feature, talk to us on the `mailing list`__ or
|
|
||||||
`IRC`__ first. We might already have plans, or be able to offer some advice.
|
|
||||||
3. Make sure your code passes the tests that ImageKit already has. To run the
|
|
||||||
tests, first install tox, ``pip install tox``, then use ``tox``. This will let you know about any errors or style
|
|
||||||
issues.
|
|
||||||
4. While we're talking about tests, creating new ones for your code makes it
|
|
||||||
much easier for us to merge your code quickly. ImageKit uses nose_, so
|
|
||||||
writing tests is painless. Check out `ours`__ for examples.
|
|
||||||
5. It's a good idea to do your work in a branch; that way, you can work on more
|
|
||||||
than one contribution at a time without making them interdependent.
|
|
||||||
|
|
||||||
|
|
||||||
__ http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html
|
|
||||||
__ https://groups.google.com/forum/#!forum/django-imagekit
|
|
||||||
__ irc://irc.freenode.net/imagekit
|
|
||||||
.. _nose: https://nose.readthedocs.org/en/latest/
|
|
||||||
__ https://github.com/matthewwithanm/django-imagekit/tree/develop/tests
|
|
||||||
16
MANIFEST.in
16
MANIFEST.in
|
|
@ -1,18 +1,2 @@
|
||||||
include AUTHORS
|
include AUTHORS
|
||||||
include LICENSE
|
include LICENSE
|
||||||
include README.rst
|
|
||||||
include testrunner.py
|
|
||||||
include setup.cfg
|
|
||||||
include tests/*.py
|
|
||||||
include tests/assets/Lenna.png
|
|
||||||
include tests/assets/lenna-*.jpg
|
|
||||||
include tests/media/lenna.png
|
|
||||||
prune tests/media/CACHE
|
|
||||||
prune tests/media/b
|
|
||||||
prune tests/media/photos
|
|
||||||
include docs/Makefile
|
|
||||||
include docs/conf.py
|
|
||||||
include docs/make.bat
|
|
||||||
include docs/*.rst
|
|
||||||
recursive-include docs/_themes LICENSE README.rst flask_theme_support.py theme.conf *.css_t *.css *.html
|
|
||||||
recursive-include imagekit/templates *.html
|
|
||||||
|
|
|
||||||
534
README.rst
534
README.rst
|
|
@ -1,481 +1,143 @@
|
||||||
|Build Status|_
|
===============
|
||||||
|
django-imagekit
|
||||||
|
===============
|
||||||
|
|
||||||
.. |Build Status| image:: https://travis-ci.org/matthewwithanm/django-imagekit.svg?branch=develop
|
ImageKit In 7 Steps
|
||||||
.. _Build Status: https://travis-ci.org/matthewwithanm/django-imagekit
|
===================
|
||||||
|
|
||||||
ImageKit is a Django app for processing images. Need a thumbnail? A
|
Step 1
|
||||||
black-and-white version of a user-uploaded image? ImageKit will make them for
|
******
|
||||||
you. If you need to programatically generate one image from another, you need
|
|
||||||
ImageKit.
|
|
||||||
|
|
||||||
ImageKit comes with a bunch of image processors for common tasks like resizing
|
::
|
||||||
and cropping, but you can also create your own. For an idea of what's possible,
|
|
||||||
check out the `Instakit`__ project.
|
|
||||||
|
|
||||||
**For the complete documentation on the latest stable version of ImageKit, see**
|
$ pip install django-imagekit
|
||||||
`ImageKit on RTD`_.
|
|
||||||
|
|
||||||
.. _`ImageKit on RTD`: http://django-imagekit.readthedocs.org
|
(or clone the source and put the imagekit module on your path)
|
||||||
__ https://github.com/fish2000/instakit
|
|
||||||
|
|
||||||
|
Step 2
|
||||||
|
******
|
||||||
|
|
||||||
Installation
|
Add ImageKit to your models.
|
||||||
============
|
|
||||||
|
|
||||||
1. Install `PIL`_ or `Pillow`_. (If you're using an ``ImageField`` in Django,
|
::
|
||||||
you should have already done this.)
|
|
||||||
2. ``pip install django-imagekit``
|
|
||||||
3. Add ``'imagekit'`` to your ``INSTALLED_APPS`` list in your project's settings.py
|
|
||||||
|
|
||||||
.. note:: If you've never seen Pillow before, it considers itself a
|
# myapp/models.py
|
||||||
more-frequently updated "friendly" fork of PIL that's compatible with
|
|
||||||
setuptools. As such, it shares the same namespace as PIL does and is a
|
|
||||||
drop-in replacement.
|
|
||||||
|
|
||||||
.. _`PIL`: http://pypi.python.org/pypi/PIL
|
|
||||||
.. _`Pillow`: http://pypi.python.org/pypi/Pillow
|
|
||||||
|
|
||||||
|
|
||||||
Usage Overview
|
|
||||||
==============
|
|
||||||
|
|
||||||
.. _specs:
|
|
||||||
|
|
||||||
Specs
|
|
||||||
-----
|
|
||||||
|
|
||||||
You have one image and you want to do something to it to create another image.
|
|
||||||
But how do you tell ImageKit what to do? By defining an image spec.
|
|
||||||
|
|
||||||
An **image spec** is a type of **image generator** that generates a new image
|
|
||||||
from a source image.
|
|
||||||
|
|
||||||
|
|
||||||
Defining Specs In Models
|
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^
|
|
||||||
|
|
||||||
The easiest way to use define an image spec is by using an ImageSpecField on
|
|
||||||
your model class:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from imagekit.models import ImageSpecField
|
from imagekit.models import ImageModel
|
||||||
from imagekit.processors import ResizeToFill
|
|
||||||
|
|
||||||
class Profile(models.Model):
|
class Photo(ImageModel):
|
||||||
avatar = models.ImageField(upload_to='avatars')
|
name = models.CharField(max_length=100)
|
||||||
avatar_thumbnail = ImageSpecField(source='avatar',
|
original_image = models.ImageField(upload_to='photos')
|
||||||
processors=[ResizeToFill(100, 50)],
|
num_views = models.PositiveIntegerField(editable=False, default=0)
|
||||||
format='JPEG',
|
|
||||||
options={'quality': 60})
|
|
||||||
|
|
||||||
profile = Profile.objects.all()[0]
|
class IKOptions:
|
||||||
print(profile.avatar_thumbnail.url) # > /media/CACHE/images/982d5af84cddddfd0fbf70892b4431e4.jpg
|
# This inner class is where we define the ImageKit options for the model
|
||||||
print(profile.avatar_thumbnail.width) # > 100
|
spec_module = 'myapp.specs'
|
||||||
|
cache_dir = 'photos'
|
||||||
|
image_field = 'original_image'
|
||||||
|
save_count_as = 'num_views'
|
||||||
|
|
||||||
As you can probably tell, ImageSpecFields work a lot like Django's
|
Step 3
|
||||||
ImageFields. The difference is that they're automatically generated by
|
******
|
||||||
ImageKit based on the instructions you give. In the example above, the avatar
|
|
||||||
thumbnail is a resized version of the avatar image, saved as a JPEG with a
|
|
||||||
quality of 60.
|
|
||||||
|
|
||||||
Sometimes, however, you don't need to keep the original image (the avatar in
|
Create your specifications.
|
||||||
the above example); when the user uploads an image, you just want to process it
|
|
||||||
and save the result. In those cases, you can use the ``ProcessedImageField``
|
|
||||||
class:
|
|
||||||
|
|
||||||
.. code-block:: python
|
::
|
||||||
|
|
||||||
from django.db import models
|
# myapp/specs.py
|
||||||
from imagekit.models import ProcessedImageField
|
|
||||||
from imagekit.processors import ResizeToFill
|
|
||||||
|
|
||||||
class Profile(models.Model):
|
from imagekit.specs import ImageSpec
|
||||||
avatar_thumbnail = ProcessedImageField(upload_to='avatars',
|
from imagekit import processors
|
||||||
processors=[ResizeToFill(100, 50)],
|
|
||||||
format='JPEG',
|
|
||||||
options={'quality': 60})
|
|
||||||
|
|
||||||
profile = Profile.objects.all()[0]
|
# first we define our thumbnail resize processor
|
||||||
print(profile.avatar_thumbnail.url) # > /media/avatars/MY-avatar.jpg
|
class ResizeThumb(processors.Resize):
|
||||||
print(profile.avatar_thumbnail.width) # > 100
|
width = 100
|
||||||
|
height = 75
|
||||||
|
crop = True
|
||||||
|
|
||||||
This is pretty similar to our previous example. We don't need to specify a
|
# now we define a display size resize processor
|
||||||
"source" any more since we're not processing another image field, but we do need
|
class ResizeDisplay(processors.Resize):
|
||||||
to pass an "upload_to" argument. This behaves exactly as it does for Django
|
width = 600
|
||||||
ImageFields.
|
|
||||||
|
|
||||||
.. note::
|
# now let's create an adjustment processor to enhance the image at small sizes
|
||||||
|
class EnchanceThumb(processors.Adjustment):
|
||||||
You might be wondering why we didn't need an "upload_to" argument for our
|
contrast = 1.2
|
||||||
ImageSpecField. The reason is that ProcessedImageFields really are just like
|
sharpness = 1.1
|
||||||
ImageFields—they save the file path in the database and you need to run
|
|
||||||
syncdb (or create a migration) when you add one to your model.
|
|
||||||
|
|
||||||
ImageSpecFields, on the other hand, are virtual—they add no fields to your
|
|
||||||
database and don't require a database. This is handy for a lot of reasons,
|
|
||||||
but it means that the path to the image file needs to be programmatically
|
|
||||||
constructed based on the source image and the spec.
|
|
||||||
|
|
||||||
|
|
||||||
Defining Specs Outside of Models
|
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
||||||
|
|
||||||
Defining specs as models fields is one very convenient way to process images,
|
|
||||||
but it isn't the only way. Sometimes you can't (or don't want to) add fields to
|
|
||||||
your models, and that's okay. You can define image spec classes and use them
|
|
||||||
directly. This can be especially useful for doing image processing in views—
|
|
||||||
particularly when the processing being done depends on user input.
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
from imagekit import ImageSpec
|
|
||||||
from imagekit.processors import ResizeToFill
|
|
||||||
|
|
||||||
|
# now we can define our thumbnail spec
|
||||||
class Thumbnail(ImageSpec):
|
class Thumbnail(ImageSpec):
|
||||||
processors = [ResizeToFill(100, 50)]
|
access_as = 'thumbnail_image'
|
||||||
format = 'JPEG'
|
pre_cache = True
|
||||||
options = {'quality': 60}
|
processors = [ResizeThumb, EnchanceThumb]
|
||||||
|
|
||||||
It's probably not surprising that this class is capable of processing an image
|
# and our display spec
|
||||||
in the exact same way as our ImageSpecField above. However, unlike with the
|
class Display(ImageSpec):
|
||||||
image spec model field, this class doesn't define what source the spec is acting
|
increment_count = True
|
||||||
on, or what should be done with the result; that's up to you:
|
processors = [ResizeDisplay]
|
||||||
|
|
||||||
.. code-block:: python
|
Step 4
|
||||||
|
******
|
||||||
|
|
||||||
source_file = open('/path/to/myimage.jpg', 'rb')
|
Flush the cache and pre-generate thumbnails (ImageKit has to be added to ``INSTALLED_APPS`` for management command to work).
|
||||||
image_generator = Thumbnail(source=source_file)
|
|
||||||
result = image_generator.generate()
|
|
||||||
|
|
||||||
.. note::
|
::
|
||||||
|
|
||||||
You don't have to use ``open``! You can use whatever File-like object you
|
$ python manage.py ikflush myapp
|
||||||
want—including a model's ``ImageField``.
|
|
||||||
|
|
||||||
The result of calling ``generate()`` on an image spec is a file-like object
|
Step 5
|
||||||
containing our resized image, with which you can do whatever you want. For
|
******
|
||||||
example, if you wanted to save it to disk:
|
|
||||||
|
|
||||||
.. code-block:: python
|
Use your new model in templates.
|
||||||
|
|
||||||
dest = open('/path/to/dest.jpg', 'wb')
|
::
|
||||||
dest.write(result.read())
|
|
||||||
dest.close()
|
|
||||||
|
|
||||||
|
<div class="original">
|
||||||
|
<img src="{{ photo.original_image.url }}" alt="{{ photo.name }}">
|
||||||
|
</div>
|
||||||
|
|
||||||
Using Specs In Templates
|
<div class="display">
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^
|
<img src="{{ photo.display.url }}" alt="{{ photo.name }}">
|
||||||
|
</div>
|
||||||
|
|
||||||
If you have a model with an ImageSpecField or ProcessedImageField, you can
|
<div class="thumbs">
|
||||||
easily use those processed image just as you would a normal image field:
|
{% for p in photos %}
|
||||||
|
<img src="{{ p.thumbnail_image.url }}" alt="{{ p.name }}">
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
|
||||||
.. code-block:: html
|
Step 6
|
||||||
|
******
|
||||||
|
|
||||||
<img src="{{ profile.avatar_thumbnail.url }}" />
|
Play with the API.
|
||||||
|
|
||||||
(This is assuming you have a view that's setting a context variable named
|
::
|
||||||
"profile" to an instance of our Profile model.)
|
|
||||||
|
|
||||||
But you can also generate processed image files directly in your template—from
|
>>> from myapp.models import Photo
|
||||||
any image—without adding anything to your model. In order to do this, you'll
|
>>> p = Photo.objects.all()[0]
|
||||||
first have to define an image generator class (remember, specs are a type of
|
<Photo: MyPhoto>
|
||||||
generator) in your app somewhere, just as we did in the last section. You'll
|
>>> p.display.url
|
||||||
also need a way of referring to the generator in your template, so you'll need
|
u'/static/photos/myphoto_display.jpg'
|
||||||
to register it.
|
>>> p.display.width
|
||||||
|
600
|
||||||
|
>>> p.display.height
|
||||||
|
420
|
||||||
|
>>> p.display.image
|
||||||
|
<JpegImagePlugin.JpegImageFile instance at 0xf18990>
|
||||||
|
>>> p.display.file
|
||||||
|
<File: /path/to/media/photos/myphoto_display.jpg>
|
||||||
|
>>> p.display.spec
|
||||||
|
<class 'myapp.specs.Display'>
|
||||||
|
|
||||||
.. code-block:: python
|
Step 7
|
||||||
|
******
|
||||||
|
|
||||||
from imagekit import ImageSpec, register
|
Enjoy a nice beverage.
|
||||||
from imagekit.processors import ResizeToFill
|
|
||||||
|
|
||||||
class Thumbnail(ImageSpec):
|
::
|
||||||
processors = [ResizeToFill(100, 50)]
|
|
||||||
format = 'JPEG'
|
|
||||||
options = {'quality': 60}
|
|
||||||
|
|
||||||
register.generator('myapp:thumbnail', Thumbnail)
|
from refrigerator import beer
|
||||||
|
|
||||||
.. note::
|
beer.enjoy()
|
||||||
|
|
||||||
You can register your generator with any id you want, but choose wisely!
|
|
||||||
If you pick something too generic, you could have a conflict with another
|
|
||||||
third-party app you're using. For this reason, it's a good idea to prefix
|
|
||||||
your generator ids with the name of your app. Also, ImageKit recognizes
|
|
||||||
colons as separators when doing pattern matching (e.g. in the generateimages
|
|
||||||
management command), so it's a good idea to use those too!
|
|
||||||
|
|
||||||
.. warning::
|
|
||||||
|
|
||||||
This code can go in any file you want—but you need to make sure it's loaded!
|
|
||||||
In order to keep things simple, ImageKit will automatically try to load an
|
|
||||||
module named "imagegenerators" in each of your installed apps. So why don't
|
|
||||||
you just save yourself the headache and put your image specs in there?
|
|
||||||
|
|
||||||
Now that we've created an image generator class and registered it with ImageKit,
|
|
||||||
we can use it in our templates!
|
|
||||||
|
|
||||||
|
|
||||||
generateimage
|
|
||||||
"""""""""""""
|
|
||||||
|
|
||||||
The most generic template tag that ImageKit gives you is called "generateimage".
|
|
||||||
It requires at least one argument: the id of a registered image generator.
|
|
||||||
Additional keyword-style arguments are passed to the registered generator class.
|
|
||||||
As we saw above, image spec constructors expect a source keyword argument, so
|
|
||||||
that's what we need to pass to use our thumbnail spec:
|
|
||||||
|
|
||||||
.. code-block:: html
|
|
||||||
|
|
||||||
{% load imagekit %}
|
|
||||||
|
|
||||||
{% generateimage 'myapp:thumbnail' source=source_file %}
|
|
||||||
|
|
||||||
This will output the following HTML:
|
|
||||||
|
|
||||||
.. code-block:: html
|
|
||||||
|
|
||||||
<img src="/media/CACHE/images/982d5af84cddddfd0fbf70892b4431e4.jpg" width="100" height="50" />
|
|
||||||
|
|
||||||
You can also add additional HTML attributes; just separate them from your
|
|
||||||
keyword args using two dashes:
|
|
||||||
|
|
||||||
.. code-block:: html
|
|
||||||
|
|
||||||
{% load imagekit %}
|
|
||||||
|
|
||||||
{% generateimage 'myapp:thumbnail' source=source_file -- alt="A picture of Me" id="mypicture" %}
|
|
||||||
|
|
||||||
Not generating HTML image tags? No problem. The tag also functions as an
|
|
||||||
assignment tag, providing access to the underlying file object:
|
|
||||||
|
|
||||||
.. code-block:: html
|
|
||||||
|
|
||||||
{% load imagekit %}
|
|
||||||
|
|
||||||
{% generateimage 'myapp:thumbnail' source=source_file as th %}
|
|
||||||
<a href="{{ th.url }}">Click to download a cool {{ th.width }} x {{ th.height }} image!</a>
|
|
||||||
|
|
||||||
|
|
||||||
thumbnail
|
|
||||||
"""""""""
|
|
||||||
|
|
||||||
Because it's such a common use case, ImageKit also provides a "thumbnail"
|
|
||||||
template tag:
|
|
||||||
|
|
||||||
.. code-block:: html
|
|
||||||
|
|
||||||
{% load imagekit %}
|
|
||||||
|
|
||||||
{% thumbnail '100x50' source_file %}
|
|
||||||
|
|
||||||
Like the generateimage tag, the thumbnail tag outputs an <img> tag:
|
|
||||||
|
|
||||||
.. code-block:: html
|
|
||||||
|
|
||||||
<img src="/media/CACHE/images/982d5af84cddddfd0fbf70892b4431e4.jpg" width="100" height="50" />
|
|
||||||
|
|
||||||
Comparing this syntax to the generateimage tag above, you'll notice a few
|
|
||||||
differences.
|
|
||||||
|
|
||||||
First, we didn't have to specify an image generator id; unless we tell it
|
|
||||||
otherwise, thumbnail tag uses the generator registered with the id
|
|
||||||
"imagekit:thumbnail". **It's important to note that this tag is *not* using the
|
|
||||||
Thumbnail spec class we defined earlier**; it's using the generator registered
|
|
||||||
with the id "imagekit:thumbnail" which, by default, is
|
|
||||||
``imagekit.generatorlibrary.Thumbnail``.
|
|
||||||
|
|
||||||
Second, we're passing two positional arguments (the dimensions and the source
|
|
||||||
image) as opposed to the keyword arguments we used with the generateimage tag.
|
|
||||||
|
|
||||||
Like with the generateimage tag, you can also specify additional HTML attributes
|
|
||||||
for the thumbnail tag, or use it as an assignment tag:
|
|
||||||
|
|
||||||
.. code-block:: html
|
|
||||||
|
|
||||||
{% load imagekit %}
|
|
||||||
|
|
||||||
{% thumbnail '100x50' source_file -- alt="A picture of Me" id="mypicture" %}
|
|
||||||
{% thumbnail '100x50' source_file as th %}
|
|
||||||
|
|
||||||
|
|
||||||
Using Specs in Forms
|
|
||||||
^^^^^^^^^^^^^^^^^^^^
|
|
||||||
|
|
||||||
In addition to the model field above, there's also a form field version of the
|
|
||||||
``ProcessedImageField`` class. The functionality is basically the same (it
|
|
||||||
processes an image once and saves the result), but it's used in a form class:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
from django import forms
|
|
||||||
from imagekit.forms import ProcessedImageField
|
|
||||||
from imagekit.processors import ResizeToFill
|
|
||||||
|
|
||||||
class ProfileForm(forms.Form):
|
|
||||||
avatar_thumbnail = ProcessedImageField(spec_id='myapp:profile:avatar_thumbnail',
|
|
||||||
processors=[ResizeToFill(100, 50)],
|
|
||||||
format='JPEG',
|
|
||||||
options={'quality': 60})
|
|
||||||
|
|
||||||
The benefit of using ``imagekit.forms.ProcessedImageField`` (as opposed to
|
|
||||||
``imagekit.models.ProcessedImageField`` above) is that it keeps the logic for
|
|
||||||
creating the image outside of your model (in which you would use a normal Django
|
|
||||||
ImageField). You can even create multiple forms, each with their own
|
|
||||||
ProcessedImageField, that all store their results in the same image field.
|
|
||||||
|
|
||||||
|
|
||||||
Processors
|
|
||||||
----------
|
|
||||||
|
|
||||||
So far, we've only seen one processor: ``imagekit.processors.ResizeToFill``. But
|
|
||||||
ImageKit is capable of far more than just resizing images, and that power comes
|
|
||||||
from its processors.
|
|
||||||
|
|
||||||
Processors take a PIL image object, do something to it, and return a new one.
|
|
||||||
A spec can make use of as many processors as you'd like, which will all be run
|
|
||||||
in order.
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
from imagekit import ImageSpec
|
|
||||||
from imagekit.processors import TrimBorderColor, Adjust
|
|
||||||
|
|
||||||
class MySpec(ImageSpec):
|
|
||||||
processors = [
|
|
||||||
TrimBorderColor(),
|
|
||||||
Adjust(contrast=1.2, sharpness=1.1),
|
|
||||||
]
|
|
||||||
format = 'JPEG'
|
|
||||||
options = {'quality': 60}
|
|
||||||
|
|
||||||
The ``imagekit.processors`` module contains processors for many common
|
|
||||||
image manipulations, like resizing, rotating, and color adjustments. However,
|
|
||||||
if they aren't up to the task, you can create your own. All you have to do is
|
|
||||||
define a class that implements a ``process()`` method:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
class Watermark(object):
|
|
||||||
def process(self, image):
|
|
||||||
# Code for adding the watermark goes here.
|
|
||||||
return image
|
|
||||||
|
|
||||||
That's all there is to it! To use your fancy new custom processor, just include
|
|
||||||
it in your spec's ``processors`` list:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
from imagekit import ImageSpec
|
|
||||||
from imagekit.processors import TrimBorderColor, Adjust
|
|
||||||
from myapp.processors import Watermark
|
|
||||||
|
|
||||||
class MySpec(ImageSpec):
|
|
||||||
processors = [
|
|
||||||
TrimBorderColor(),
|
|
||||||
Adjust(contrast=1.2, sharpness=1.1),
|
|
||||||
Watermark(),
|
|
||||||
]
|
|
||||||
format = 'JPEG'
|
|
||||||
options = {'quality': 60}
|
|
||||||
|
|
||||||
Note that when you import a processor from ``imagekit.processors``, imagekit
|
|
||||||
in turn imports the processor from `PILKit`_. So if you are looking for
|
|
||||||
available processors, look at PILKit.
|
|
||||||
|
|
||||||
.. _`PILKit`: https://github.com/matthewwithanm/pilkit
|
|
||||||
|
|
||||||
|
|
||||||
Admin
|
|
||||||
-----
|
|
||||||
|
|
||||||
ImageKit also contains a class named ``imagekit.admin.AdminThumbnail``
|
|
||||||
for displaying specs (or even regular ImageFields) in the
|
|
||||||
`Django admin change list`_. AdminThumbnail is used as a property on
|
|
||||||
Django admin classes:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
from django.contrib import admin
|
|
||||||
from imagekit.admin import AdminThumbnail
|
|
||||||
from .models import Photo
|
|
||||||
|
|
||||||
class PhotoAdmin(admin.ModelAdmin):
|
|
||||||
list_display = ('__str__', 'admin_thumbnail')
|
|
||||||
admin_thumbnail = AdminThumbnail(image_field='thumbnail')
|
|
||||||
|
|
||||||
admin.site.register(Photo, PhotoAdmin)
|
|
||||||
|
|
||||||
To use specs defined outside of models:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
from django.contrib import admin
|
|
||||||
from imagekit.admin import AdminThumbnail
|
|
||||||
from imagekit import ImageSpec
|
|
||||||
from imagekit.processors import ResizeToFill
|
|
||||||
from imagekit.cachefiles import ImageCacheFile
|
|
||||||
|
|
||||||
from .models import Photo
|
|
||||||
|
|
||||||
class AdminThumbnailSpec(ImageSpec):
|
|
||||||
processors = [ResizeToFill(100, 30)]
|
|
||||||
format = 'JPEG'
|
|
||||||
options = {'quality': 60 }
|
|
||||||
|
|
||||||
def cached_admin_thumb(instance):
|
|
||||||
# `image` is the name of the image field on the model
|
|
||||||
cached = ImageCacheFile(AdminThumbnailSpec(instance.image))
|
|
||||||
# only generates the first time, subsequent calls use cache
|
|
||||||
cached.generate()
|
|
||||||
return cached
|
|
||||||
|
|
||||||
class PhotoAdmin(admin.ModelAdmin):
|
|
||||||
list_display = ('__str__', 'admin_thumbnail')
|
|
||||||
admin_thumbnail = AdminThumbnail(image_field=cached_admin_thumb)
|
|
||||||
|
|
||||||
admin.site.register(Photo, PhotoAdmin)
|
|
||||||
|
|
||||||
|
|
||||||
AdminThumbnail can even use a custom template. For more information, see
|
|
||||||
``imagekit.admin.AdminThumbnail``.
|
|
||||||
|
|
||||||
.. _`Django admin change list`: https://docs.djangoproject.com/en/dev/intro/tutorial02/#customize-the-admin-change-list
|
|
||||||
|
|
||||||
|
|
||||||
Management Commands
|
|
||||||
-------------------
|
|
||||||
|
|
||||||
ImageKit has one management command—`generateimages`—which will generate cache
|
|
||||||
files for all of your registered image generators. You can also pass it a list
|
|
||||||
of generator ids in order to generate images selectively.
|
|
||||||
|
|
||||||
|
|
||||||
Community
|
|
||||||
=========
|
|
||||||
|
|
||||||
Please use `the GitHub issue tracker <https://github.com/matthewwithanm/django-imagekit/issues>`_
|
|
||||||
to report bugs with django-imagekit. `A mailing list <https://groups.google.com/forum/#!forum/django-imagekit>`_
|
|
||||||
also exists to discuss the project and ask questions, as well as the official
|
|
||||||
`#imagekit <irc://irc.freenode.net/imagekit>`_ channel on Freenode.
|
|
||||||
|
|
||||||
|
|
||||||
Contributing
|
|
||||||
============
|
|
||||||
|
|
||||||
We love contributions! And you don't have to be an expert with the library—or
|
|
||||||
even Django—to contribute either: ImageKit's processors are standalone classes
|
|
||||||
that are completely separate from the more intimidating internals of Django's
|
|
||||||
ORM. If you've written a processor that you think might be useful to other
|
|
||||||
people, open a pull request so we can take a look!
|
|
||||||
|
|
||||||
You can also check out our list of `open, contributor-friendly issues`__ for
|
|
||||||
ideas.
|
|
||||||
|
|
||||||
Check out our `contributing guidelines`__ for more information about pitching in
|
|
||||||
with ImageKit.
|
|
||||||
|
|
||||||
|
|
||||||
__ https://github.com/matthewwithanm/django-imagekit/issues?labels=contributor-friendly&state=open
|
|
||||||
__ https://github.com/matthewwithanm/django-imagekit/blob/develop/CONTRIBUTING.rst
|
|
||||||
|
|
|
||||||
153
docs/Makefile
153
docs/Makefile
|
|
@ -1,153 +0,0 @@
|
||||||
# Makefile for Sphinx documentation
|
|
||||||
#
|
|
||||||
|
|
||||||
# You can set these variables from the command line.
|
|
||||||
SPHINXOPTS =
|
|
||||||
SPHINXBUILD = python $(shell which sphinx-build)
|
|
||||||
PAPER =
|
|
||||||
BUILDDIR = build
|
|
||||||
|
|
||||||
# Internal variables.
|
|
||||||
PAPEROPT_a4 = -D latex_paper_size=a4
|
|
||||||
PAPEROPT_letter = -D latex_paper_size=letter
|
|
||||||
ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
|
|
||||||
# the i18n builder cannot share the environment and doctrees with the others
|
|
||||||
I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
|
|
||||||
|
|
||||||
.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext
|
|
||||||
|
|
||||||
help:
|
|
||||||
@echo "Please use \`make <target>' where <target> is one of"
|
|
||||||
@echo " html to make standalone HTML files"
|
|
||||||
@echo " dirhtml to make HTML files named index.html in directories"
|
|
||||||
@echo " singlehtml to make a single large HTML file"
|
|
||||||
@echo " pickle to make pickle files"
|
|
||||||
@echo " json to make JSON files"
|
|
||||||
@echo " htmlhelp to make HTML files and a HTML help project"
|
|
||||||
@echo " qthelp to make HTML files and a qthelp project"
|
|
||||||
@echo " devhelp to make HTML files and a Devhelp project"
|
|
||||||
@echo " epub to make an epub"
|
|
||||||
@echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
|
|
||||||
@echo " latexpdf to make LaTeX files and run them through pdflatex"
|
|
||||||
@echo " text to make text files"
|
|
||||||
@echo " man to make manual pages"
|
|
||||||
@echo " texinfo to make Texinfo files"
|
|
||||||
@echo " info to make Texinfo files and run them through makeinfo"
|
|
||||||
@echo " gettext to make PO message catalogs"
|
|
||||||
@echo " changes to make an overview of all changed/added/deprecated items"
|
|
||||||
@echo " linkcheck to check all external links for integrity"
|
|
||||||
@echo " doctest to run all doctests embedded in the documentation (if enabled)"
|
|
||||||
|
|
||||||
clean:
|
|
||||||
-rm -rf $(BUILDDIR)/*
|
|
||||||
|
|
||||||
html:
|
|
||||||
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
|
|
||||||
@echo
|
|
||||||
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
|
|
||||||
|
|
||||||
dirhtml:
|
|
||||||
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
|
|
||||||
@echo
|
|
||||||
@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
|
|
||||||
|
|
||||||
singlehtml:
|
|
||||||
$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
|
|
||||||
@echo
|
|
||||||
@echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
|
|
||||||
|
|
||||||
pickle:
|
|
||||||
$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
|
|
||||||
@echo
|
|
||||||
@echo "Build finished; now you can process the pickle files."
|
|
||||||
|
|
||||||
json:
|
|
||||||
$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
|
|
||||||
@echo
|
|
||||||
@echo "Build finished; now you can process the JSON files."
|
|
||||||
|
|
||||||
htmlhelp:
|
|
||||||
$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
|
|
||||||
@echo
|
|
||||||
@echo "Build finished; now you can run HTML Help Workshop with the" \
|
|
||||||
".hhp project file in $(BUILDDIR)/htmlhelp."
|
|
||||||
|
|
||||||
qthelp:
|
|
||||||
$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
|
|
||||||
@echo
|
|
||||||
@echo "Build finished; now you can run "qcollectiongenerator" with the" \
|
|
||||||
".qhcp project file in $(BUILDDIR)/qthelp, like this:"
|
|
||||||
@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/ImageKit.qhcp"
|
|
||||||
@echo "To view the help file:"
|
|
||||||
@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/ImageKit.qhc"
|
|
||||||
|
|
||||||
devhelp:
|
|
||||||
$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
|
|
||||||
@echo
|
|
||||||
@echo "Build finished."
|
|
||||||
@echo "To view the help file:"
|
|
||||||
@echo "# mkdir -p $$HOME/.local/share/devhelp/ImageKit"
|
|
||||||
@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/ImageKit"
|
|
||||||
@echo "# devhelp"
|
|
||||||
|
|
||||||
epub:
|
|
||||||
$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
|
|
||||||
@echo
|
|
||||||
@echo "Build finished. The epub file is in $(BUILDDIR)/epub."
|
|
||||||
|
|
||||||
latex:
|
|
||||||
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
|
|
||||||
@echo
|
|
||||||
@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
|
|
||||||
@echo "Run \`make' in that directory to run these through (pdf)latex" \
|
|
||||||
"(use \`make latexpdf' here to do that automatically)."
|
|
||||||
|
|
||||||
latexpdf:
|
|
||||||
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
|
|
||||||
@echo "Running LaTeX files through pdflatex..."
|
|
||||||
$(MAKE) -C $(BUILDDIR)/latex all-pdf
|
|
||||||
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
|
|
||||||
|
|
||||||
text:
|
|
||||||
$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
|
|
||||||
@echo
|
|
||||||
@echo "Build finished. The text files are in $(BUILDDIR)/text."
|
|
||||||
|
|
||||||
man:
|
|
||||||
$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
|
|
||||||
@echo
|
|
||||||
@echo "Build finished. The manual pages are in $(BUILDDIR)/man."
|
|
||||||
|
|
||||||
texinfo:
|
|
||||||
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
|
|
||||||
@echo
|
|
||||||
@echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
|
|
||||||
@echo "Run \`make' in that directory to run these through makeinfo" \
|
|
||||||
"(use \`make info' here to do that automatically)."
|
|
||||||
|
|
||||||
info:
|
|
||||||
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
|
|
||||||
@echo "Running Texinfo files through makeinfo..."
|
|
||||||
make -C $(BUILDDIR)/texinfo info
|
|
||||||
@echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
|
|
||||||
|
|
||||||
gettext:
|
|
||||||
$(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
|
|
||||||
@echo
|
|
||||||
@echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
|
|
||||||
|
|
||||||
changes:
|
|
||||||
$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
|
|
||||||
@echo
|
|
||||||
@echo "The overview file is in $(BUILDDIR)/changes."
|
|
||||||
|
|
||||||
linkcheck:
|
|
||||||
$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
|
|
||||||
@echo
|
|
||||||
@echo "Link check complete; look for any errors in the above output " \
|
|
||||||
"or in $(BUILDDIR)/linkcheck/output.txt."
|
|
||||||
|
|
||||||
doctest:
|
|
||||||
$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
|
|
||||||
@echo "Testing of doctests in the sources finished, look at the " \
|
|
||||||
"results in $(BUILDDIR)/doctest/output.txt."
|
|
||||||
45
docs/_themes/LICENSE
vendored
45
docs/_themes/LICENSE
vendored
|
|
@ -1,45 +0,0 @@
|
||||||
Modifications:
|
|
||||||
|
|
||||||
Copyright (c) 2010 Kenneth Reitz.
|
|
||||||
|
|
||||||
|
|
||||||
Original Project:
|
|
||||||
|
|
||||||
Copyright (c) 2010 by Armin Ronacher.
|
|
||||||
|
|
||||||
|
|
||||||
Some rights reserved.
|
|
||||||
|
|
||||||
Redistribution and use in source and binary forms of the theme, with or
|
|
||||||
without modification, are permitted provided that the following conditions
|
|
||||||
are met:
|
|
||||||
|
|
||||||
* Redistributions of source code must retain the above copyright
|
|
||||||
notice, this list of conditions and the following disclaimer.
|
|
||||||
|
|
||||||
* Redistributions in binary form must reproduce the above
|
|
||||||
copyright notice, this list of conditions and the following
|
|
||||||
disclaimer in the documentation and/or other materials provided
|
|
||||||
with the distribution.
|
|
||||||
|
|
||||||
* The names of the contributors may not be used to endorse or
|
|
||||||
promote products derived from this software without specific
|
|
||||||
prior written permission.
|
|
||||||
|
|
||||||
We kindly ask you to only use these themes in an unmodified manner just
|
|
||||||
for Flask and Flask-related products, not for unrelated projects. If you
|
|
||||||
like the visual style and want to use it for your own projects, please
|
|
||||||
consider making some larger changes to the themes (such as changing
|
|
||||||
font faces, sizes, colors or margins).
|
|
||||||
|
|
||||||
THIS THEME IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
||||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
||||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
|
||||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
|
|
||||||
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
||||||
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
||||||
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
||||||
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
||||||
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
||||||
ARISING IN ANY WAY OUT OF THE USE OF THIS THEME, EVEN IF ADVISED OF THE
|
|
||||||
POSSIBILITY OF SUCH DAMAGE.
|
|
||||||
25
docs/_themes/README.rst
vendored
25
docs/_themes/README.rst
vendored
|
|
@ -1,25 +0,0 @@
|
||||||
krTheme Sphinx Style
|
|
||||||
====================
|
|
||||||
|
|
||||||
This repository contains sphinx styles Kenneth Reitz uses in most of
|
|
||||||
his projects. It is a drivative of Mitsuhiko's themes for Flask and Flask related
|
|
||||||
projects. To use this style in your Sphinx documentation, follow
|
|
||||||
this guide:
|
|
||||||
|
|
||||||
1. put this folder as _themes into your docs folder. Alternatively
|
|
||||||
you can also use git submodules to check out the contents there.
|
|
||||||
|
|
||||||
2. add this to your conf.py: ::
|
|
||||||
|
|
||||||
sys.path.append(os.path.abspath('_themes'))
|
|
||||||
html_theme_path = ['_themes']
|
|
||||||
html_theme = 'kr'
|
|
||||||
|
|
||||||
The following themes exist:
|
|
||||||
|
|
||||||
**kr**
|
|
||||||
the standard flask documentation theme for large projects
|
|
||||||
|
|
||||||
**kr_small**
|
|
||||||
small one-page theme. Intended to be used by very small addon libraries.
|
|
||||||
|
|
||||||
86
docs/_themes/flask_theme_support.py
vendored
86
docs/_themes/flask_theme_support.py
vendored
|
|
@ -1,86 +0,0 @@
|
||||||
# flasky extensions. flasky pygments style based on tango style
|
|
||||||
from pygments.style import Style
|
|
||||||
from pygments.token import Keyword, Name, Comment, String, Error, \
|
|
||||||
Number, Operator, Generic, Whitespace, Punctuation, Other, Literal
|
|
||||||
|
|
||||||
|
|
||||||
class FlaskyStyle(Style):
|
|
||||||
background_color = "#f8f8f8"
|
|
||||||
default_style = ""
|
|
||||||
|
|
||||||
styles = {
|
|
||||||
# No corresponding class for the following:
|
|
||||||
#Text: "", # class: ''
|
|
||||||
Whitespace: "underline #f8f8f8", # class: 'w'
|
|
||||||
Error: "#a40000 border:#ef2929", # class: 'err'
|
|
||||||
Other: "#000000", # class 'x'
|
|
||||||
|
|
||||||
Comment: "italic #8f5902", # class: 'c'
|
|
||||||
Comment.Preproc: "noitalic", # class: 'cp'
|
|
||||||
|
|
||||||
Keyword: "bold #004461", # class: 'k'
|
|
||||||
Keyword.Constant: "bold #004461", # class: 'kc'
|
|
||||||
Keyword.Declaration: "bold #004461", # class: 'kd'
|
|
||||||
Keyword.Namespace: "bold #004461", # class: 'kn'
|
|
||||||
Keyword.Pseudo: "bold #004461", # class: 'kp'
|
|
||||||
Keyword.Reserved: "bold #004461", # class: 'kr'
|
|
||||||
Keyword.Type: "bold #004461", # class: 'kt'
|
|
||||||
|
|
||||||
Operator: "#582800", # class: 'o'
|
|
||||||
Operator.Word: "bold #004461", # class: 'ow' - like keywords
|
|
||||||
|
|
||||||
Punctuation: "bold #000000", # class: 'p'
|
|
||||||
|
|
||||||
# because special names such as Name.Class, Name.Function, etc.
|
|
||||||
# are not recognized as such later in the parsing, we choose them
|
|
||||||
# to look the same as ordinary variables.
|
|
||||||
Name: "#000000", # class: 'n'
|
|
||||||
Name.Attribute: "#c4a000", # class: 'na' - to be revised
|
|
||||||
Name.Builtin: "#004461", # class: 'nb'
|
|
||||||
Name.Builtin.Pseudo: "#3465a4", # class: 'bp'
|
|
||||||
Name.Class: "#000000", # class: 'nc' - to be revised
|
|
||||||
Name.Constant: "#000000", # class: 'no' - to be revised
|
|
||||||
Name.Decorator: "#888", # class: 'nd' - to be revised
|
|
||||||
Name.Entity: "#ce5c00", # class: 'ni'
|
|
||||||
Name.Exception: "bold #cc0000", # class: 'ne'
|
|
||||||
Name.Function: "#000000", # class: 'nf'
|
|
||||||
Name.Property: "#000000", # class: 'py'
|
|
||||||
Name.Label: "#f57900", # class: 'nl'
|
|
||||||
Name.Namespace: "#000000", # class: 'nn' - to be revised
|
|
||||||
Name.Other: "#000000", # class: 'nx'
|
|
||||||
Name.Tag: "bold #004461", # class: 'nt' - like a keyword
|
|
||||||
Name.Variable: "#000000", # class: 'nv' - to be revised
|
|
||||||
Name.Variable.Class: "#000000", # class: 'vc' - to be revised
|
|
||||||
Name.Variable.Global: "#000000", # class: 'vg' - to be revised
|
|
||||||
Name.Variable.Instance: "#000000", # class: 'vi' - to be revised
|
|
||||||
|
|
||||||
Number: "#990000", # class: 'm'
|
|
||||||
|
|
||||||
Literal: "#000000", # class: 'l'
|
|
||||||
Literal.Date: "#000000", # class: 'ld'
|
|
||||||
|
|
||||||
String: "#4e9a06", # class: 's'
|
|
||||||
String.Backtick: "#4e9a06", # class: 'sb'
|
|
||||||
String.Char: "#4e9a06", # class: 'sc'
|
|
||||||
String.Doc: "italic #8f5902", # class: 'sd' - like a comment
|
|
||||||
String.Double: "#4e9a06", # class: 's2'
|
|
||||||
String.Escape: "#4e9a06", # class: 'se'
|
|
||||||
String.Heredoc: "#4e9a06", # class: 'sh'
|
|
||||||
String.Interpol: "#4e9a06", # class: 'si'
|
|
||||||
String.Other: "#4e9a06", # class: 'sx'
|
|
||||||
String.Regex: "#4e9a06", # class: 'sr'
|
|
||||||
String.Single: "#4e9a06", # class: 's1'
|
|
||||||
String.Symbol: "#4e9a06", # class: 'ss'
|
|
||||||
|
|
||||||
Generic: "#000000", # class: 'g'
|
|
||||||
Generic.Deleted: "#a40000", # class: 'gd'
|
|
||||||
Generic.Emph: "italic #000000", # class: 'ge'
|
|
||||||
Generic.Error: "#ef2929", # class: 'gr'
|
|
||||||
Generic.Heading: "bold #000080", # class: 'gh'
|
|
||||||
Generic.Inserted: "#00A000", # class: 'gi'
|
|
||||||
Generic.Output: "#888", # class: 'go'
|
|
||||||
Generic.Prompt: "#745334", # class: 'gp'
|
|
||||||
Generic.Strong: "bold #000000", # class: 'gs'
|
|
||||||
Generic.Subheading: "bold #800080", # class: 'gu'
|
|
||||||
Generic.Traceback: "bold #a40000", # class: 'gt'
|
|
||||||
}
|
|
||||||
16
docs/_themes/kr/layout.html
vendored
16
docs/_themes/kr/layout.html
vendored
|
|
@ -1,16 +0,0 @@
|
||||||
{%- extends "basic/layout.html" %}
|
|
||||||
{%- block extrahead %}
|
|
||||||
{{ super() }}
|
|
||||||
{% if theme_touch_icon %}
|
|
||||||
<link rel="apple-touch-icon" href="{{ pathto('_static/' ~ theme_touch_icon, 1) }}" />
|
|
||||||
{% endif %}
|
|
||||||
<link media="only screen and (max-device-width: 480px)" href="{{
|
|
||||||
pathto('_static/small_flask.css', 1) }}" type= "text/css" rel="stylesheet" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=0.9, maximum-scale=0.9">
|
|
||||||
{% endblock %}
|
|
||||||
{%- block relbar2 %}{% endblock %}
|
|
||||||
{%- block footer %}
|
|
||||||
<div class="footer">
|
|
||||||
© Copyright {{ copyright }}.
|
|
||||||
</div>
|
|
||||||
{%- endblock %}
|
|
||||||
19
docs/_themes/kr/relations.html
vendored
19
docs/_themes/kr/relations.html
vendored
|
|
@ -1,19 +0,0 @@
|
||||||
<h3>Related Topics</h3>
|
|
||||||
<ul>
|
|
||||||
<li><a href="{{ pathto(master_doc) }}">Documentation overview</a><ul>
|
|
||||||
{%- for parent in parents %}
|
|
||||||
<li><a href="{{ parent.link|e }}">{{ parent.title }}</a><ul>
|
|
||||||
{%- endfor %}
|
|
||||||
{%- if prev %}
|
|
||||||
<li>Previous: <a href="{{ prev.link|e }}" title="{{ _('previous chapter')
|
|
||||||
}}">{{ prev.title }}</a></li>
|
|
||||||
{%- endif %}
|
|
||||||
{%- if next %}
|
|
||||||
<li>Next: <a href="{{ next.link|e }}" title="{{ _('next chapter')
|
|
||||||
}}">{{ next.title }}</a></li>
|
|
||||||
{%- endif %}
|
|
||||||
{%- for parent in parents %}
|
|
||||||
</ul></li>
|
|
||||||
{%- endfor %}
|
|
||||||
</ul></li>
|
|
||||||
</ul>
|
|
||||||
480
docs/_themes/kr/static/flasky.css_t
vendored
480
docs/_themes/kr/static/flasky.css_t
vendored
|
|
@ -1,480 +0,0 @@
|
||||||
/*
|
|
||||||
* flasky.css_t
|
|
||||||
* ~~~~~~~~~~~~
|
|
||||||
*
|
|
||||||
* :copyright: Copyright 2010 by Armin Ronacher. Modifications by Kenneth Reitz.
|
|
||||||
* :license: Flask Design License, see LICENSE for details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
{% set page_width = '940px' %}
|
|
||||||
{% set sidebar_width = '220px' %}
|
|
||||||
|
|
||||||
@import url("basic.css");
|
|
||||||
|
|
||||||
/* -- page layout ----------------------------------------------------------- */
|
|
||||||
|
|
||||||
body {
|
|
||||||
font-family: 'goudy old style', 'minion pro', 'bell mt', Georgia, 'Hiragino Mincho Pro';
|
|
||||||
font-size: 17px;
|
|
||||||
background-color: white;
|
|
||||||
color: #000;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.document {
|
|
||||||
width: {{ page_width }};
|
|
||||||
margin: 30px auto 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.documentwrapper {
|
|
||||||
float: left;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.bodywrapper {
|
|
||||||
margin: 0 0 0 {{ sidebar_width }};
|
|
||||||
}
|
|
||||||
|
|
||||||
div.sphinxsidebar {
|
|
||||||
width: {{ sidebar_width }};
|
|
||||||
}
|
|
||||||
|
|
||||||
hr {
|
|
||||||
border: 1px solid #B1B4B6;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.body {
|
|
||||||
background-color: #ffffff;
|
|
||||||
color: #3E4349;
|
|
||||||
padding: 0 30px 0 30px;
|
|
||||||
}
|
|
||||||
|
|
||||||
img.floatingflask {
|
|
||||||
padding: 0 0 10px 10px;
|
|
||||||
float: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.footer {
|
|
||||||
width: {{ page_width }};
|
|
||||||
margin: 20px auto 30px auto;
|
|
||||||
font-size: 14px;
|
|
||||||
color: #888;
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.footer a {
|
|
||||||
color: #888;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.related {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.sphinxsidebar a {
|
|
||||||
color: #444;
|
|
||||||
text-decoration: none;
|
|
||||||
border-bottom: 1px dotted #999;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.sphinxsidebar a:hover {
|
|
||||||
border-bottom: 1px solid #999;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.sphinxsidebar {
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 1.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.sphinxsidebarwrapper {
|
|
||||||
padding: 18px 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.sphinxsidebarwrapper p.logo {
|
|
||||||
padding: 0;
|
|
||||||
margin: -10px 0 0 -20px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.sphinxsidebar h3,
|
|
||||||
div.sphinxsidebar h4 {
|
|
||||||
font-family: 'Garamond', 'Georgia', serif;
|
|
||||||
color: #444;
|
|
||||||
font-size: 24px;
|
|
||||||
font-weight: normal;
|
|
||||||
margin: 0 0 5px 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.sphinxsidebar h4 {
|
|
||||||
font-size: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.sphinxsidebar h3 a {
|
|
||||||
color: #444;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.sphinxsidebar p.logo a,
|
|
||||||
div.sphinxsidebar h3 a,
|
|
||||||
div.sphinxsidebar p.logo a:hover,
|
|
||||||
div.sphinxsidebar h3 a:hover {
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.sphinxsidebar p {
|
|
||||||
color: #555;
|
|
||||||
margin: 10px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.sphinxsidebar ul {
|
|
||||||
margin: 10px 0;
|
|
||||||
padding: 0;
|
|
||||||
color: #000;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.sphinxsidebar input {
|
|
||||||
border: 1px solid #ccc;
|
|
||||||
font-family: 'Georgia', serif;
|
|
||||||
font-size: 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* -- body styles ----------------------------------------------------------- */
|
|
||||||
|
|
||||||
a {
|
|
||||||
color: #004B6B;
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
|
|
||||||
a:hover {
|
|
||||||
color: #6D4100;
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.body h1,
|
|
||||||
div.body h2,
|
|
||||||
div.body h3,
|
|
||||||
div.body h4,
|
|
||||||
div.body h5,
|
|
||||||
div.body h6 {
|
|
||||||
font-family: 'Garamond', 'Georgia', serif;
|
|
||||||
font-weight: normal;
|
|
||||||
margin: 30px 0px 10px 0px;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.body h1 { margin-top: 0; padding-top: 0; font-size: 240%; }
|
|
||||||
div.body h2 { font-size: 180%; }
|
|
||||||
div.body h3 { font-size: 150%; }
|
|
||||||
div.body h4 { font-size: 130%; }
|
|
||||||
div.body h5 { font-size: 100%; }
|
|
||||||
div.body h6 { font-size: 100%; }
|
|
||||||
|
|
||||||
a.headerlink {
|
|
||||||
color: #ddd;
|
|
||||||
padding: 0 4px;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
a.headerlink:hover {
|
|
||||||
color: #444;
|
|
||||||
background: #eaeaea;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.body p, div.body dd, div.body li {
|
|
||||||
line-height: 1.4em;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.admonition {
|
|
||||||
background: #fafafa;
|
|
||||||
margin: 20px -30px;
|
|
||||||
padding: 10px 30px;
|
|
||||||
border-top: 1px solid #ccc;
|
|
||||||
border-bottom: 1px solid #ccc;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.admonition tt.xref, div.admonition a tt {
|
|
||||||
border-bottom: 1px solid #fafafa;
|
|
||||||
}
|
|
||||||
|
|
||||||
dd div.admonition {
|
|
||||||
margin-left: -60px;
|
|
||||||
padding-left: 60px;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.admonition p.admonition-title {
|
|
||||||
font-family: 'Garamond', 'Georgia', serif;
|
|
||||||
font-weight: normal;
|
|
||||||
font-size: 24px;
|
|
||||||
margin: 0 0 10px 0;
|
|
||||||
padding: 0;
|
|
||||||
line-height: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.admonition p.last {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.highlight {
|
|
||||||
background-color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
dt:target, .highlight {
|
|
||||||
background: #FAF3E8;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.note {
|
|
||||||
background-color: #eee;
|
|
||||||
border: 1px solid #ccc;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.seealso {
|
|
||||||
background-color: #ffc;
|
|
||||||
border: 1px solid #ff6;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.topic {
|
|
||||||
background-color: #eee;
|
|
||||||
}
|
|
||||||
|
|
||||||
p.admonition-title {
|
|
||||||
display: inline;
|
|
||||||
}
|
|
||||||
|
|
||||||
p.admonition-title:after {
|
|
||||||
content: ":";
|
|
||||||
}
|
|
||||||
|
|
||||||
pre, tt {
|
|
||||||
font-family: 'Consolas', 'Menlo', 'Deja Vu Sans Mono', 'Bitstream Vera Sans Mono', monospace;
|
|
||||||
font-size: 0.9em;
|
|
||||||
}
|
|
||||||
|
|
||||||
img.screenshot {
|
|
||||||
}
|
|
||||||
|
|
||||||
tt.descname, tt.descclassname {
|
|
||||||
font-size: 0.95em;
|
|
||||||
}
|
|
||||||
|
|
||||||
tt.descname {
|
|
||||||
padding-right: 0.08em;
|
|
||||||
}
|
|
||||||
|
|
||||||
img.screenshot {
|
|
||||||
-moz-box-shadow: 2px 2px 4px #eee;
|
|
||||||
-webkit-box-shadow: 2px 2px 4px #eee;
|
|
||||||
box-shadow: 2px 2px 4px #eee;
|
|
||||||
}
|
|
||||||
|
|
||||||
table.docutils {
|
|
||||||
border: 1px solid #888;
|
|
||||||
-moz-box-shadow: 2px 2px 4px #eee;
|
|
||||||
-webkit-box-shadow: 2px 2px 4px #eee;
|
|
||||||
box-shadow: 2px 2px 4px #eee;
|
|
||||||
}
|
|
||||||
|
|
||||||
table.docutils td, table.docutils th {
|
|
||||||
border: 1px solid #888;
|
|
||||||
padding: 0.25em 0.7em;
|
|
||||||
}
|
|
||||||
|
|
||||||
table.field-list, table.footnote {
|
|
||||||
border: none;
|
|
||||||
-moz-box-shadow: none;
|
|
||||||
-webkit-box-shadow: none;
|
|
||||||
box-shadow: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
table.footnote {
|
|
||||||
margin: 15px 0;
|
|
||||||
width: 100%;
|
|
||||||
border: 1px solid #eee;
|
|
||||||
background: #fdfdfd;
|
|
||||||
font-size: 0.9em;
|
|
||||||
}
|
|
||||||
|
|
||||||
table.footnote + table.footnote {
|
|
||||||
margin-top: -15px;
|
|
||||||
border-top: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
table.field-list th {
|
|
||||||
padding: 0 0.8em 0 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
table.field-list td {
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
table.footnote td.label {
|
|
||||||
width: 0px;
|
|
||||||
padding: 0.3em 0 0.3em 0.5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
table.footnote td {
|
|
||||||
padding: 0.3em 0.5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
dl {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
dl dd {
|
|
||||||
margin-left: 30px;
|
|
||||||
}
|
|
||||||
|
|
||||||
blockquote {
|
|
||||||
margin: 0 0 0 30px;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
ul, ol {
|
|
||||||
margin: 10px 0 10px 30px;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
pre {
|
|
||||||
background: #eee;
|
|
||||||
padding: 7px 30px;
|
|
||||||
margin: 15px -30px;
|
|
||||||
line-height: 1.3em;
|
|
||||||
}
|
|
||||||
|
|
||||||
dl pre, blockquote pre, li pre {
|
|
||||||
margin-left: -60px;
|
|
||||||
padding-left: 60px;
|
|
||||||
}
|
|
||||||
|
|
||||||
dl dl pre {
|
|
||||||
margin-left: -90px;
|
|
||||||
padding-left: 90px;
|
|
||||||
}
|
|
||||||
|
|
||||||
tt {
|
|
||||||
background-color: #ecf0f3;
|
|
||||||
color: #222;
|
|
||||||
/* padding: 1px 2px; */
|
|
||||||
}
|
|
||||||
|
|
||||||
tt.xref, a tt {
|
|
||||||
background-color: #FBFBFB;
|
|
||||||
border-bottom: 1px solid white;
|
|
||||||
}
|
|
||||||
|
|
||||||
a.reference {
|
|
||||||
text-decoration: none;
|
|
||||||
border-bottom: 1px dotted #004B6B;
|
|
||||||
}
|
|
||||||
|
|
||||||
a.reference:hover {
|
|
||||||
border-bottom: 1px solid #6D4100;
|
|
||||||
}
|
|
||||||
|
|
||||||
a.footnote-reference {
|
|
||||||
text-decoration: none;
|
|
||||||
font-size: 0.7em;
|
|
||||||
vertical-align: top;
|
|
||||||
border-bottom: 1px dotted #004B6B;
|
|
||||||
}
|
|
||||||
|
|
||||||
a.footnote-reference:hover {
|
|
||||||
border-bottom: 1px solid #6D4100;
|
|
||||||
}
|
|
||||||
|
|
||||||
a:hover tt {
|
|
||||||
background: #EEE;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@media screen and (max-width: 600px) {
|
|
||||||
|
|
||||||
div.sphinxsidebar {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.document {
|
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
div.documentwrapper {
|
|
||||||
margin-left: 0;
|
|
||||||
margin-top: 0;
|
|
||||||
margin-right: 0;
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.bodywrapper {
|
|
||||||
margin-top: 0;
|
|
||||||
margin-right: 0;
|
|
||||||
margin-bottom: 0;
|
|
||||||
margin-left: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
ul {
|
|
||||||
margin-left: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.document {
|
|
||||||
width: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer {
|
|
||||||
width: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bodywrapper {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer {
|
|
||||||
width: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.github {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* scrollbars */
|
|
||||||
|
|
||||||
::-webkit-scrollbar {
|
|
||||||
width: 6px;
|
|
||||||
height: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
::-webkit-scrollbar-button:start:decrement,
|
|
||||||
::-webkit-scrollbar-button:end:increment {
|
|
||||||
display: block;
|
|
||||||
height: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
::-webkit-scrollbar-button:vertical:increment {
|
|
||||||
background-color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
::-webkit-scrollbar-track-piece {
|
|
||||||
background-color: #eee;
|
|
||||||
-webkit-border-radius: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb:vertical {
|
|
||||||
height: 50px;
|
|
||||||
background-color: #ccc;
|
|
||||||
-webkit-border-radius: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb:horizontal {
|
|
||||||
width: 50px;
|
|
||||||
background-color: #ccc;
|
|
||||||
-webkit-border-radius: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* misc. */
|
|
||||||
|
|
||||||
.revsys-inline {
|
|
||||||
display: none!important;
|
|
||||||
}
|
|
||||||
90
docs/_themes/kr/static/small_flask.css
vendored
90
docs/_themes/kr/static/small_flask.css
vendored
|
|
@ -1,90 +0,0 @@
|
||||||
/*
|
|
||||||
* small_flask.css_t
|
|
||||||
* ~~~~~~~~~~~~~~~~~
|
|
||||||
*
|
|
||||||
* :copyright: Copyright 2010 by Armin Ronacher.
|
|
||||||
* :license: Flask Design License, see LICENSE for details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
padding: 20px 30px;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.documentwrapper {
|
|
||||||
float: none;
|
|
||||||
background: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.sphinxsidebar {
|
|
||||||
display: block;
|
|
||||||
float: none;
|
|
||||||
width: 102.5%;
|
|
||||||
margin: 50px -30px -20px -30px;
|
|
||||||
padding: 10px 20px;
|
|
||||||
background: #333;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.sphinxsidebar h3, div.sphinxsidebar h4, div.sphinxsidebar p,
|
|
||||||
div.sphinxsidebar h3 a {
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.sphinxsidebar a {
|
|
||||||
color: #aaa;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.sphinxsidebar p.logo {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.document {
|
|
||||||
width: 100%;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.related {
|
|
||||||
display: block;
|
|
||||||
margin: 0;
|
|
||||||
padding: 10px 0 20px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.related ul,
|
|
||||||
div.related ul li {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.footer {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.bodywrapper {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.body {
|
|
||||||
min-height: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rtd_doc_footer {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.document {
|
|
||||||
width: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer {
|
|
||||||
width: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer {
|
|
||||||
width: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.github {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
7
docs/_themes/kr/theme.conf
vendored
7
docs/_themes/kr/theme.conf
vendored
|
|
@ -1,7 +0,0 @@
|
||||||
[theme]
|
|
||||||
inherit = basic
|
|
||||||
stylesheet = flasky.css
|
|
||||||
pygments_style = flask_theme_support.FlaskyStyle
|
|
||||||
|
|
||||||
[options]
|
|
||||||
touch_icon =
|
|
||||||
22
docs/_themes/kr_small/layout.html
vendored
22
docs/_themes/kr_small/layout.html
vendored
|
|
@ -1,22 +0,0 @@
|
||||||
{% extends "basic/layout.html" %}
|
|
||||||
{% block header %}
|
|
||||||
{{ super() }}
|
|
||||||
{% if pagename == 'index' %}
|
|
||||||
<div class=indexwrapper>
|
|
||||||
{% endif %}
|
|
||||||
{% endblock %}
|
|
||||||
{% block footer %}
|
|
||||||
{% if pagename == 'index' %}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
{% endblock %}
|
|
||||||
{# do not display relbars #}
|
|
||||||
{% block relbar1 %}{% endblock %}
|
|
||||||
{% block relbar2 %}
|
|
||||||
{% if theme_github_fork %}
|
|
||||||
<a href="http://github.com/{{ theme_github_fork }}"><img style="position: fixed; top: 0; right: 0; border: 0;"
|
|
||||||
src="http://s3.amazonaws.com/github/ribbons/forkme_right_darkblue_121621.png" alt="Fork me on GitHub" /></a>
|
|
||||||
{% endif %}
|
|
||||||
{% endblock %}
|
|
||||||
{% block sidebar1 %}{% endblock %}
|
|
||||||
{% block sidebar2 %}{% endblock %}
|
|
||||||
287
docs/_themes/kr_small/static/flasky.css_t
vendored
287
docs/_themes/kr_small/static/flasky.css_t
vendored
|
|
@ -1,287 +0,0 @@
|
||||||
/*
|
|
||||||
* flasky.css_t
|
|
||||||
* ~~~~~~~~~~~~
|
|
||||||
*
|
|
||||||
* Sphinx stylesheet -- flasky theme based on nature theme.
|
|
||||||
*
|
|
||||||
* :copyright: Copyright 2007-2010 by the Sphinx team, see AUTHORS.
|
|
||||||
* :license: BSD, see LICENSE for details.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
@import url("basic.css");
|
|
||||||
|
|
||||||
/* -- page layout ----------------------------------------------------------- */
|
|
||||||
|
|
||||||
body {
|
|
||||||
font-family: 'Georgia', serif;
|
|
||||||
font-size: 17px;
|
|
||||||
color: #000;
|
|
||||||
background: white;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.documentwrapper {
|
|
||||||
float: left;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.bodywrapper {
|
|
||||||
margin: 40px auto 0 auto;
|
|
||||||
width: 700px;
|
|
||||||
}
|
|
||||||
|
|
||||||
hr {
|
|
||||||
border: 1px solid #B1B4B6;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.body {
|
|
||||||
background-color: #ffffff;
|
|
||||||
color: #3E4349;
|
|
||||||
padding: 0 30px 30px 30px;
|
|
||||||
}
|
|
||||||
|
|
||||||
img.floatingflask {
|
|
||||||
padding: 0 0 10px 10px;
|
|
||||||
float: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.footer {
|
|
||||||
text-align: right;
|
|
||||||
color: #888;
|
|
||||||
padding: 10px;
|
|
||||||
font-size: 14px;
|
|
||||||
width: 650px;
|
|
||||||
margin: 0 auto 40px auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.footer a {
|
|
||||||
color: #888;
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.related {
|
|
||||||
line-height: 32px;
|
|
||||||
color: #888;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.related ul {
|
|
||||||
padding: 0 0 0 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.related a {
|
|
||||||
color: #444;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* -- body styles ----------------------------------------------------------- */
|
|
||||||
|
|
||||||
a {
|
|
||||||
color: #004B6B;
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
|
|
||||||
a:hover {
|
|
||||||
color: #6D4100;
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.body {
|
|
||||||
padding-bottom: 40px; /* saved for footer */
|
|
||||||
}
|
|
||||||
|
|
||||||
div.body h1,
|
|
||||||
div.body h2,
|
|
||||||
div.body h3,
|
|
||||||
div.body h4,
|
|
||||||
div.body h5,
|
|
||||||
div.body h6 {
|
|
||||||
font-family: 'Garamond', 'Georgia', serif;
|
|
||||||
font-weight: normal;
|
|
||||||
margin: 30px 0px 10px 0px;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
{% if theme_index_logo %}
|
|
||||||
div.indexwrapper h1 {
|
|
||||||
text-indent: -999999px;
|
|
||||||
background: url({{ theme_index_logo }}) no-repeat center center;
|
|
||||||
height: {{ theme_index_logo_height }};
|
|
||||||
}
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
div.body h2 { font-size: 180%; }
|
|
||||||
div.body h3 { font-size: 150%; }
|
|
||||||
div.body h4 { font-size: 130%; }
|
|
||||||
div.body h5 { font-size: 100%; }
|
|
||||||
div.body h6 { font-size: 100%; }
|
|
||||||
|
|
||||||
a.headerlink {
|
|
||||||
color: white;
|
|
||||||
padding: 0 4px;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
a.headerlink:hover {
|
|
||||||
color: #444;
|
|
||||||
background: #eaeaea;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.body p, div.body dd, div.body li {
|
|
||||||
line-height: 1.4em;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.admonition {
|
|
||||||
background: #fafafa;
|
|
||||||
margin: 20px -30px;
|
|
||||||
padding: 10px 30px;
|
|
||||||
border-top: 1px solid #ccc;
|
|
||||||
border-bottom: 1px solid #ccc;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.admonition p.admonition-title {
|
|
||||||
font-family: 'Garamond', 'Georgia', serif;
|
|
||||||
font-weight: normal;
|
|
||||||
font-size: 24px;
|
|
||||||
margin: 0 0 10px 0;
|
|
||||||
padding: 0;
|
|
||||||
line-height: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.admonition p.last {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.highlight{
|
|
||||||
background-color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
dt:target, .highlight {
|
|
||||||
background: #FAF3E8;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.note {
|
|
||||||
background-color: #eee;
|
|
||||||
border: 1px solid #ccc;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.seealso {
|
|
||||||
background-color: #ffc;
|
|
||||||
border: 1px solid #ff6;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.topic {
|
|
||||||
background-color: #eee;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.warning {
|
|
||||||
background-color: #ffe4e4;
|
|
||||||
border: 1px solid #f66;
|
|
||||||
}
|
|
||||||
|
|
||||||
p.admonition-title {
|
|
||||||
display: inline;
|
|
||||||
}
|
|
||||||
|
|
||||||
p.admonition-title:after {
|
|
||||||
content: ":";
|
|
||||||
}
|
|
||||||
|
|
||||||
pre, tt {
|
|
||||||
font-family: 'Consolas', 'Menlo', 'Deja Vu Sans Mono', 'Bitstream Vera Sans Mono', monospace;
|
|
||||||
font-size: 0.85em;
|
|
||||||
}
|
|
||||||
|
|
||||||
img.screenshot {
|
|
||||||
}
|
|
||||||
|
|
||||||
tt.descname, tt.descclassname {
|
|
||||||
font-size: 0.95em;
|
|
||||||
}
|
|
||||||
|
|
||||||
tt.descname {
|
|
||||||
padding-right: 0.08em;
|
|
||||||
}
|
|
||||||
|
|
||||||
img.screenshot {
|
|
||||||
-moz-box-shadow: 2px 2px 4px #eee;
|
|
||||||
-webkit-box-shadow: 2px 2px 4px #eee;
|
|
||||||
box-shadow: 2px 2px 4px #eee;
|
|
||||||
}
|
|
||||||
|
|
||||||
table.docutils {
|
|
||||||
border: 1px solid #888;
|
|
||||||
-moz-box-shadow: 2px 2px 4px #eee;
|
|
||||||
-webkit-box-shadow: 2px 2px 4px #eee;
|
|
||||||
box-shadow: 2px 2px 4px #eee;
|
|
||||||
}
|
|
||||||
|
|
||||||
table.docutils td, table.docutils th {
|
|
||||||
border: 1px solid #888;
|
|
||||||
padding: 0.25em 0.7em;
|
|
||||||
}
|
|
||||||
|
|
||||||
table.field-list, table.footnote {
|
|
||||||
border: none;
|
|
||||||
-moz-box-shadow: none;
|
|
||||||
-webkit-box-shadow: none;
|
|
||||||
box-shadow: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
table.footnote {
|
|
||||||
margin: 15px 0;
|
|
||||||
width: 100%;
|
|
||||||
border: 1px solid #eee;
|
|
||||||
}
|
|
||||||
|
|
||||||
table.field-list th {
|
|
||||||
padding: 0 0.8em 0 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
table.field-list td {
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
table.footnote td {
|
|
||||||
padding: 0.5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
dl {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
dl dd {
|
|
||||||
margin-left: 30px;
|
|
||||||
}
|
|
||||||
|
|
||||||
pre {
|
|
||||||
padding: 0;
|
|
||||||
margin: 15px -30px;
|
|
||||||
padding: 8px;
|
|
||||||
line-height: 1.3em;
|
|
||||||
padding: 7px 30px;
|
|
||||||
background: #eee;
|
|
||||||
border-radius: 2px;
|
|
||||||
-moz-border-radius: 2px;
|
|
||||||
-webkit-border-radius: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
dl pre {
|
|
||||||
margin-left: -60px;
|
|
||||||
padding-left: 60px;
|
|
||||||
}
|
|
||||||
|
|
||||||
tt {
|
|
||||||
background-color: #ecf0f3;
|
|
||||||
color: #222;
|
|
||||||
/* padding: 1px 2px; */
|
|
||||||
}
|
|
||||||
|
|
||||||
tt.xref, a tt {
|
|
||||||
background-color: #FBFBFB;
|
|
||||||
}
|
|
||||||
|
|
||||||
a:hover tt {
|
|
||||||
background: #EEE;
|
|
||||||
}
|
|
||||||
10
docs/_themes/kr_small/theme.conf
vendored
10
docs/_themes/kr_small/theme.conf
vendored
|
|
@ -1,10 +0,0 @@
|
||||||
[theme]
|
|
||||||
inherit = basic
|
|
||||||
stylesheet = flasky.css
|
|
||||||
nosidebar = true
|
|
||||||
pygments_style = flask_theme_support.FlaskyStyle
|
|
||||||
|
|
||||||
[options]
|
|
||||||
index_logo = ''
|
|
||||||
index_logo_height = 120px
|
|
||||||
github_fork = ''
|
|
||||||
|
|
@ -1,182 +0,0 @@
|
||||||
Advanced Usage
|
|
||||||
**************
|
|
||||||
|
|
||||||
|
|
||||||
Models
|
|
||||||
======
|
|
||||||
|
|
||||||
|
|
||||||
The ``ImageSpecField`` Shorthand Syntax
|
|
||||||
---------------------------------------
|
|
||||||
|
|
||||||
If you've read the README, you already know what an ``ImageSpecField`` is and
|
|
||||||
the basics of defining one:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
from django.db import models
|
|
||||||
from imagekit.models import ImageSpecField
|
|
||||||
from imagekit.processors import ResizeToFill
|
|
||||||
|
|
||||||
class Profile(models.Model):
|
|
||||||
avatar = models.ImageField(upload_to='avatars')
|
|
||||||
avatar_thumbnail = ImageSpecField(source='avatar',
|
|
||||||
processors=[ResizeToFill(100, 50)],
|
|
||||||
format='JPEG',
|
|
||||||
options={'quality': 60})
|
|
||||||
|
|
||||||
This will create an ``avatar_thumbnail`` field which is a resized version of the
|
|
||||||
image stored in the ``avatar`` image field. But this is actually just shorthand
|
|
||||||
for creating an ``ImageSpec``, registering it, and associating it with an
|
|
||||||
``ImageSpecField``:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
from django.db import models
|
|
||||||
from imagekit import ImageSpec, register
|
|
||||||
from imagekit.models import ImageSpecField
|
|
||||||
from imagekit.processors import ResizeToFill
|
|
||||||
|
|
||||||
class AvatarThumbnail(ImageSpec):
|
|
||||||
processors = [ResizeToFill(100, 50)]
|
|
||||||
format = 'JPEG'
|
|
||||||
options = {'quality': 60}
|
|
||||||
|
|
||||||
register.generator('myapp:profile:avatar_thumbnail', AvatarThumbnail)
|
|
||||||
|
|
||||||
class Profile(models.Model):
|
|
||||||
avatar = models.ImageField(upload_to='avatars')
|
|
||||||
avatar_thumbnail = ImageSpecField(source='avatar',
|
|
||||||
id='myapp:profile:avatar_thumbnail')
|
|
||||||
|
|
||||||
Obviously, the shorthand version is a lot, well…shorter. So why would you ever
|
|
||||||
want to go through the trouble of using the long form? The answer is that the
|
|
||||||
long form—creating an image spec class and registering it—gives you a lot more
|
|
||||||
power over the generated image.
|
|
||||||
|
|
||||||
.. _dynamic-specs:
|
|
||||||
|
|
||||||
Specs That Change
|
|
||||||
-----------------
|
|
||||||
|
|
||||||
As you'll remember from the README, an image spec is just a type of image
|
|
||||||
generator that generates a new image from a source image. How does the image
|
|
||||||
spec get access to the source image? Simple! It's passed to the constructor as
|
|
||||||
a keyword argument and stored as an attribute of the spec. Normally, we don't
|
|
||||||
have to concern ourselves with this; the ``ImageSpec`` knows what to do with the
|
|
||||||
source image and we're happy to let it do its thing. However, having access to
|
|
||||||
the source image in our spec class can be very useful…
|
|
||||||
|
|
||||||
Often, when using an ``ImageSpecField``, you may want the spec to vary based on
|
|
||||||
properties of a model. (For example, you might want to store image dimensions on
|
|
||||||
the model and then use them to generate your thumbnail.) Now that we know how to
|
|
||||||
access the source image from our spec, it's a simple matter to extract its model
|
|
||||||
and use it to create our processors list. In fact, ImageKit includes a utility
|
|
||||||
for getting this information.
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
:emphasize-lines: 11-14
|
|
||||||
|
|
||||||
from django.db import models
|
|
||||||
from imagekit import ImageSpec, register
|
|
||||||
from imagekit.models import ImageSpecField
|
|
||||||
from imagekit.processors import ResizeToFill
|
|
||||||
from imagekit.utils import get_field_info
|
|
||||||
|
|
||||||
class AvatarThumbnail(ImageSpec):
|
|
||||||
format = 'JPEG'
|
|
||||||
options = {'quality': 60}
|
|
||||||
|
|
||||||
@property
|
|
||||||
def processors(self):
|
|
||||||
model, field_name = get_field_info(self.source)
|
|
||||||
return [ResizeToFill(model.thumbnail_width, thumbnail.avatar_height)]
|
|
||||||
|
|
||||||
register.generator('myapp:profile:avatar_thumbnail', AvatarThumbnail)
|
|
||||||
|
|
||||||
class Profile(models.Model):
|
|
||||||
avatar = models.ImageField(upload_to='avatars')
|
|
||||||
avatar_thumbnail = ImageSpecField(source='avatar',
|
|
||||||
id='myapp:profile:avatar_thumbnail')
|
|
||||||
thumbnail_width = models.PositiveIntegerField()
|
|
||||||
thumbnail_height = models.PositiveIntegerField()
|
|
||||||
|
|
||||||
Now each avatar thumbnail will be resized according to the dimensions stored on
|
|
||||||
the model!
|
|
||||||
|
|
||||||
Of course, processors aren't the only thing that can vary based on the model of
|
|
||||||
the source image; spec behavior can change in any way you want.
|
|
||||||
|
|
||||||
|
|
||||||
.. _source-groups:
|
|
||||||
|
|
||||||
Source Groups
|
|
||||||
=============
|
|
||||||
|
|
||||||
When you run the ``generateimages`` management command, how does ImageKit know
|
|
||||||
which source images to use with which specs? Obviously, when you define an
|
|
||||||
ImageSpecField, the source image is being connected to a spec, but what's going
|
|
||||||
on underneath the hood?
|
|
||||||
|
|
||||||
The answer is that, when you define an ImageSpecField, ImageKit automatically
|
|
||||||
creates and registers an object called a *source group*. Source groups are
|
|
||||||
responsible for two things:
|
|
||||||
|
|
||||||
1. They dispatch signals when a source is saved, and
|
|
||||||
2. They expose a generator method that enumerates source files.
|
|
||||||
|
|
||||||
When these objects are registered (using ``imagekit.register.source_group()``),
|
|
||||||
their signals will trigger callbacks on the cache file strategies associated
|
|
||||||
with image specs that use the source. (So, for example, you can chose to
|
|
||||||
generate a file every time the source image changes.) In addition, the generator
|
|
||||||
method is used (indirectly) to create the list of files to generate with the
|
|
||||||
``generateimages`` management command.
|
|
||||||
|
|
||||||
Currently, there is only one source group class bundled with ImageKit—the one
|
|
||||||
used by ImageSpecFields. This source group
|
|
||||||
(``imagekit.specs.sourcegroups.ImageFieldSourceGroup``) represents an ImageField
|
|
||||||
on every instance of a particular model. In terms of the above description, the
|
|
||||||
instance ``ImageFieldSourceGroup(Profile, 'avatar')`` 1) dispatches a signal
|
|
||||||
every time the image in Profile's avatar ImageField changes, and 2) exposes a
|
|
||||||
generator method that iterates over every Profile's "avatar" image.
|
|
||||||
|
|
||||||
Chances are, this is the only source group you will ever need to use, however,
|
|
||||||
ImageKit lets you define and register custom source groups easily. This may be
|
|
||||||
useful, for example, if you're using the template tags "generateimage" and
|
|
||||||
"thumbnail" and the optimistic cache file strategy. Again, the purpose is
|
|
||||||
to tell ImageKit which specs are used with which sources (so the
|
|
||||||
"generateimages" management command can generate those files) and when the
|
|
||||||
source image has been created or changed (so that the strategy has the
|
|
||||||
opportunity to act on it).
|
|
||||||
|
|
||||||
A simple example of a custom source group class is as follows:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
import glob
|
|
||||||
import os
|
|
||||||
|
|
||||||
class JpegsInADirectory(object):
|
|
||||||
def __init__(self, dir):
|
|
||||||
self.dir = dir
|
|
||||||
|
|
||||||
def files(self):
|
|
||||||
os.chdir(self.dir)
|
|
||||||
for name in glob.glob('*.jpg'):
|
|
||||||
yield open(name, 'rb')
|
|
||||||
|
|
||||||
Instances of this class could then be registered with one or more spec id:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
from imagekit import register
|
|
||||||
|
|
||||||
register.source_group('myapp:profile:avatar_thumbnail', JpegsInADirectory('/path/to/some/pics'))
|
|
||||||
|
|
||||||
Running the "generateimages" management command would now cause thumbnails to be
|
|
||||||
generated (using the "myapp:profile:avatar_thumbnail" spec) for each of the
|
|
||||||
JPEGs in `/path/to/some/pics`.
|
|
||||||
|
|
||||||
Note that, since this source group doesnt send the `source_saved` signal, the
|
|
||||||
corresponding cache file strategy callbacks would not be called for them.
|
|
||||||
|
|
||||||
256
docs/caching.rst
256
docs/caching.rst
|
|
@ -1,256 +0,0 @@
|
||||||
Caching
|
|
||||||
*******
|
|
||||||
|
|
||||||
|
|
||||||
Default Backend Workflow
|
|
||||||
========================
|
|
||||||
|
|
||||||
|
|
||||||
``ImageSpec``
|
|
||||||
-------------
|
|
||||||
|
|
||||||
At the heart of ImageKit are image generators. These are classes with a
|
|
||||||
``generate()`` method which returns an image file. An image spec is a type of
|
|
||||||
image generator. The thing that makes specs special is that they accept a source
|
|
||||||
image. So an image spec is just an image generator that makes an image from some
|
|
||||||
other image.
|
|
||||||
|
|
||||||
|
|
||||||
``ImageCacheFile``
|
|
||||||
------------------
|
|
||||||
|
|
||||||
However, an image spec by itself would be vastly inefficient. Every time an
|
|
||||||
an image was accessed in some way, it would have be regenerated and saved.
|
|
||||||
Most of the time, you want to re-use a previously generated image, based on the
|
|
||||||
input image and spec, instead of generating a new one. That's where
|
|
||||||
``ImageCacheFile`` comes in. ``ImageCacheFile`` is a File-like object that
|
|
||||||
wraps an image generator. They look and feel just like regular file
|
|
||||||
objects, but they've got a little trick up their sleeve: they represent files
|
|
||||||
that may not actually exist!
|
|
||||||
|
|
||||||
|
|
||||||
.. _cache-file-strategy:
|
|
||||||
|
|
||||||
Cache File Strategy
|
|
||||||
-------------------
|
|
||||||
|
|
||||||
Each ``ImageCacheFile`` has a cache file strategy, which abstracts away when
|
|
||||||
image is actually generated. It can implement the following three methods:
|
|
||||||
|
|
||||||
* ``on_content_required`` - called by ``ImageCacheFile`` when it requires the
|
|
||||||
contents of the generated image. For example, when you call ``read()`` or
|
|
||||||
try to access information contained in the file.
|
|
||||||
* ``on_existence_required`` - called by ``ImageCacheFile`` when it requires the
|
|
||||||
generated image to exist but may not be concerned with its contents. For
|
|
||||||
example, when you access its ``url`` or ``path`` attribute.
|
|
||||||
* ``on_source_saved`` - called when the source of a spec is saved
|
|
||||||
|
|
||||||
The default strategy only defines the first two of these, as follows:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
class JustInTime(object):
|
|
||||||
def on_content_required(self, file):
|
|
||||||
file.generate()
|
|
||||||
|
|
||||||
def on_existence_required(self, file):
|
|
||||||
file.generate()
|
|
||||||
|
|
||||||
|
|
||||||
.. _cache-file-backend:
|
|
||||||
|
|
||||||
Cache File Backend
|
|
||||||
------------------
|
|
||||||
|
|
||||||
The ``generate`` method on the ``ImageCacheFile`` is further delegated to the
|
|
||||||
cache file backend, which abstracts away how an image is generated.
|
|
||||||
|
|
||||||
The cache file backend defaults to the setting
|
|
||||||
``IMAGEKIT_DEFAULT_CACHEFILE_BACKEND`` and can be set explicitly on a spec with
|
|
||||||
the ``cachefile_backend`` attribute.
|
|
||||||
|
|
||||||
The default works like this:
|
|
||||||
|
|
||||||
* Checks the file storage to see if a file exists
|
|
||||||
* If not, caches that information for 5 seconds
|
|
||||||
* If it does, caches that information in the ``IMAGEKIT_CACHE_BACKEND``
|
|
||||||
|
|
||||||
If file doesn't exist, generates it immediately and synchronously
|
|
||||||
|
|
||||||
|
|
||||||
That pretty much covers the architecture of the caching layer, and its default
|
|
||||||
behavior. I like the default behavior. When will an image be regenerated?
|
|
||||||
Whenever it needs to be! When will your storage backend get hit? Depending on
|
|
||||||
your ``IMAGEKIT_CACHE_BACKEND`` settings, as little as twice per file (once for the
|
|
||||||
existence check and once to save the generated file). What if you want to change
|
|
||||||
a spec? The generated file name (which is used as part of the cache keys) vary
|
|
||||||
with the source file name and spec attributes, so if you change any of those, a
|
|
||||||
new file will be generated. The default behavior is easy!
|
|
||||||
|
|
||||||
.. note::
|
|
||||||
|
|
||||||
Like regular Django ImageFields, IK doesn't currently cache width and height
|
|
||||||
values, so accessing those will always result in a read. That will probably
|
|
||||||
change soon though.
|
|
||||||
|
|
||||||
|
|
||||||
Optimizing
|
|
||||||
==========
|
|
||||||
|
|
||||||
There are several ways to improve the performance (reduce I/O operations) of
|
|
||||||
ImageKit. Each has its own pros and cons.
|
|
||||||
|
|
||||||
|
|
||||||
Caching Data About Generated Files
|
|
||||||
----------------------------------
|
|
||||||
|
|
||||||
Generally, once a file is generated, you will never be removing it, so by
|
|
||||||
default ImageKit will use default cache to cache the state of generated
|
|
||||||
files "forever" (or only 5 minutes when ``DEBUG = True``).
|
|
||||||
|
|
||||||
The time for which ImageKit will cache state is configured with
|
|
||||||
``IMAGEKIT_CACHE_TIMEOUT``. If set to ``None`` this means "never expire"
|
|
||||||
(default when ``DEBUG = False``). You can reduce this timeout if you want
|
|
||||||
or set it to some numeric value in seconds if your cache backend behaves
|
|
||||||
differently and for example do not cache values if timeout is ``None``.
|
|
||||||
|
|
||||||
If you clear your cache durring deployment or some other reason probably
|
|
||||||
you do not want to lose the cache for generated images especcialy if you
|
|
||||||
are using some slow remote storage (like Amazon S3). Then you can configure
|
|
||||||
seprate cache (for example redis) in your ``CACHES`` config and tell ImageKit
|
|
||||||
to use it instead of the default cache by setting ``IMAGEKIT_CACHE_BACKEND``.
|
|
||||||
|
|
||||||
|
|
||||||
Pre-Generating Images
|
|
||||||
---------------------
|
|
||||||
|
|
||||||
The default cache file backend generates images immediately and synchronously.
|
|
||||||
If you don't do anything special, that will be when they are first requested—as
|
|
||||||
part of request-response cycle. This means that the first visitor to your page
|
|
||||||
will have to wait for the file to be created before they see any HTML.
|
|
||||||
|
|
||||||
This can be mitigated, though, by simply generating the images ahead of time, by
|
|
||||||
running the ``generateimages`` management command.
|
|
||||||
|
|
||||||
.. note::
|
|
||||||
|
|
||||||
If using with template tags, be sure to read :ref:`source-groups`.
|
|
||||||
|
|
||||||
|
|
||||||
Deferring Image Generation
|
|
||||||
--------------------------
|
|
||||||
|
|
||||||
As mentioned above, image generation is normally done synchronously. through
|
|
||||||
the default cache file backend. However, you can also take advantage of
|
|
||||||
deferred generation. In order to do this, you'll need to do two things:
|
|
||||||
|
|
||||||
1) install `celery`__ (or `django-celery`__ if you are bound to Celery<3.1)
|
|
||||||
2) tell ImageKit to use the async cachefile backend.
|
|
||||||
To do this for all specs, set the ``IMAGEKIT_DEFAULT_CACHEFILE_BACKEND`` in
|
|
||||||
your settings
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
IMAGEKIT_DEFAULT_CACHEFILE_BACKEND = 'imagekit.cachefiles.backends.Async'
|
|
||||||
|
|
||||||
Images will now be generated asynchronously. But watch out! Asynchrounous
|
|
||||||
generation means you'll have to account for images that haven't been generated
|
|
||||||
yet. You can do this by checking the truthiness of your files; if an image
|
|
||||||
hasn't been generated, it will be falsy:
|
|
||||||
|
|
||||||
.. code-block:: html
|
|
||||||
|
|
||||||
{% if not profile.avatar_thumbnail %}
|
|
||||||
<img src="/path/to/placeholder.jpg" />
|
|
||||||
{% else %}
|
|
||||||
<img src="{{ profile.avatar_thumbnail.url }}" />
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
Or, in Python:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
profile = Profile.objects.all()[0]
|
|
||||||
if profile.avatar_thumbnail:
|
|
||||||
url = profile.avatar_thumbnail.url
|
|
||||||
else:
|
|
||||||
url = '/path/to/placeholder.jpg'
|
|
||||||
|
|
||||||
.. note::
|
|
||||||
|
|
||||||
If you are using an "async" backend in combination with the "optimistic"
|
|
||||||
cache file strategy (see `Removing Safeguards`_ below), checking for
|
|
||||||
thruthiness as described above will not work. The "optimistic" backend is
|
|
||||||
very optimistic so to say, and removes the check. Create and use the
|
|
||||||
following strategy to a) have images only created on save, and b) retain
|
|
||||||
the ability to check whether the images have already been created::
|
|
||||||
|
|
||||||
class ImagekitOnSaveStrategy(object):
|
|
||||||
def on_source_saved(self, file):
|
|
||||||
file.generate()
|
|
||||||
|
|
||||||
.. note::
|
|
||||||
|
|
||||||
If you use custom storage backend for some specs,
|
|
||||||
(storage passed to the field different than configured one)
|
|
||||||
it's required the storage to be pickleable
|
|
||||||
|
|
||||||
|
|
||||||
__ https://pypi.python.org/pypi/django-celery
|
|
||||||
__ http://www.celeryproject.org
|
|
||||||
|
|
||||||
|
|
||||||
Removing Safeguards
|
|
||||||
-------------------
|
|
||||||
|
|
||||||
Even with pre-generating images, ImageKit will still try to ensure that your
|
|
||||||
image exists when you access it by default. This is for your benefit: if you
|
|
||||||
forget to generate your images, ImageKit will see that and generate it for you.
|
|
||||||
If the state of the file is cached (see above), this is a pretty cheap
|
|
||||||
operation. However, if the state isn't cached, ImageKit will need to query the
|
|
||||||
storage backend.
|
|
||||||
|
|
||||||
For those who aren't willing to accept that cost (and who never want ImageKit
|
|
||||||
to generate images in the request-responce cycle), there's the "optimistic"
|
|
||||||
cache file strategy. This strategy only generates a new image when a spec's
|
|
||||||
source image is created or changed. Unlike with the "just in time" strategy,
|
|
||||||
accessing the file won't cause it to be generated, ImageKit will just assume
|
|
||||||
that it already exists.
|
|
||||||
|
|
||||||
To use this cache file strategy for all specs, set the
|
|
||||||
``IMAGEKIT_DEFAULT_CACHEFILE_STRATEGY`` in your settings:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
IMAGEKIT_DEFAULT_CACHEFILE_STRATEGY = 'imagekit.cachefiles.strategies.Optimistic'
|
|
||||||
|
|
||||||
If you have specs that :ref:`change based on attributes of the source
|
|
||||||
<dynamic-specs>`, that's not going to cut it, though; the file will also need to
|
|
||||||
be generated when those attributes change. Likewise, image generators that don't
|
|
||||||
have sources (i.e. generators that aren't specs) won't cause files to be
|
|
||||||
generated automatically when using the optimistic strategy. (ImageKit can't know
|
|
||||||
when those need to be generated, if not on access.) In both cases, you'll have
|
|
||||||
to trigger the file generation yourself—either by generating the file in code
|
|
||||||
when necessary, or by periodically running the ``generateimages`` management
|
|
||||||
command. Luckily, ImageKit makes this pretty easy:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
from imagekit.cachefiles import LazyImageCacheFile
|
|
||||||
|
|
||||||
file = LazyImageCacheFile('myapp:profile:avatar_thumbnail', source=source_file)
|
|
||||||
file.generate()
|
|
||||||
|
|
||||||
One final situation in which images won't be generated automatically when using
|
|
||||||
the optimistic strategy is when you use a spec with a source that hasn't been
|
|
||||||
registered with it. Unlike the previous two examples, this situation cannot be
|
|
||||||
rectified by running the ``generateimages`` management command, for the simple
|
|
||||||
reason that the command has no way of knowing it needs to generate a file for
|
|
||||||
that spec from that source. Typically, this situation would arise when using the
|
|
||||||
template tags. Unlike ImageSpecFields, which automatically register all the
|
|
||||||
possible source images with the spec you define, the template tags
|
|
||||||
("generateimage" and "thumbnail") let you use any spec with any source.
|
|
||||||
Therefore, in order to generate the appropriate files using the
|
|
||||||
``generateimages`` management command, you'll need to first register a source
|
|
||||||
group that represents all of the sources you wish to use with the corresponding
|
|
||||||
specs. See :ref:`source-groups` for more information.
|
|
||||||
249
docs/conf.py
249
docs/conf.py
|
|
@ -1,249 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
#
|
|
||||||
# ImageKit documentation build configuration file, created by
|
|
||||||
# sphinx-quickstart on Sun Sep 25 17:05:55 2011.
|
|
||||||
#
|
|
||||||
# This file is execfile()d with the current directory set to its containing dir.
|
|
||||||
#
|
|
||||||
# Note that not all possible configuration values are present in this
|
|
||||||
# autogenerated file.
|
|
||||||
#
|
|
||||||
# All configuration values have a default; values that are commented out
|
|
||||||
# serve to show the default.
|
|
||||||
|
|
||||||
import re, sys, os
|
|
||||||
|
|
||||||
# If extensions (or modules to document with autodoc) are in another directory,
|
|
||||||
# add these directories to sys.path here. If the directory is relative to the
|
|
||||||
# documentation root, use os.path.abspath to make it absolute, like shown here.
|
|
||||||
sys.path.insert(0, os.path.abspath('..'))
|
|
||||||
sys.path.append(os.path.abspath('_themes'))
|
|
||||||
os.environ['DJANGO_SETTINGS_MODULE'] = 'tests.settings'
|
|
||||||
|
|
||||||
# -- General configuration -----------------------------------------------------
|
|
||||||
|
|
||||||
# If your documentation needs a minimal Sphinx version, state it here.
|
|
||||||
#needs_sphinx = '1.0'
|
|
||||||
|
|
||||||
# Add any Sphinx extension module names here, as strings. They can be extensions
|
|
||||||
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
|
|
||||||
extensions = ['sphinx.ext.autodoc']
|
|
||||||
|
|
||||||
# Add any paths that contain templates here, relative to this directory.
|
|
||||||
templates_path = ['_templates']
|
|
||||||
|
|
||||||
# The suffix of source filenames.
|
|
||||||
source_suffix = '.rst'
|
|
||||||
|
|
||||||
# The encoding of source files.
|
|
||||||
#source_encoding = 'utf-8-sig'
|
|
||||||
|
|
||||||
# The master toctree document.
|
|
||||||
master_doc = 'index'
|
|
||||||
|
|
||||||
# General information about the project.
|
|
||||||
project = u'ImageKit'
|
|
||||||
copyright = u'2011, Justin Driscoll, Bryan Veloso, Greg Newman, Chris Drackett & Matthew Tretter'
|
|
||||||
|
|
||||||
pkgmeta = {}
|
|
||||||
execfile(os.path.join(os.path.dirname(__file__), '..', 'imagekit',
|
|
||||||
'pkgmeta.py'), pkgmeta)
|
|
||||||
|
|
||||||
# The version info for the project you're documenting, acts as replacement for
|
|
||||||
# |version| and |release|, also used in various other places throughout the
|
|
||||||
# built documents.
|
|
||||||
#
|
|
||||||
# The short X.Y version.
|
|
||||||
version = re.match(r'\d+\.\d+', pkgmeta['__version__']).group()
|
|
||||||
# The full version, including alpha/beta/rc tags.
|
|
||||||
release = pkgmeta['__version__']
|
|
||||||
|
|
||||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
|
||||||
# for a list of supported languages.
|
|
||||||
#language = None
|
|
||||||
|
|
||||||
# There are two options for replacing |today|: either, you set today to some
|
|
||||||
# non-false value, then it is used:
|
|
||||||
#today = ''
|
|
||||||
# Else, today_fmt is used as the format for a strftime call.
|
|
||||||
#today_fmt = '%B %d, %Y'
|
|
||||||
|
|
||||||
# List of patterns, relative to source directory, that match files and
|
|
||||||
# directories to ignore when looking for source files.
|
|
||||||
exclude_patterns = []
|
|
||||||
|
|
||||||
# The reST default role (used for this markup: `text`) to use for all documents.
|
|
||||||
#default_role = None
|
|
||||||
|
|
||||||
# If true, '()' will be appended to :func: etc. cross-reference text.
|
|
||||||
#add_function_parentheses = True
|
|
||||||
|
|
||||||
# If true, the current module name will be prepended to all description
|
|
||||||
# unit titles (such as .. function::).
|
|
||||||
#add_module_names = True
|
|
||||||
|
|
||||||
# If true, sectionauthor and moduleauthor directives will be shown in the
|
|
||||||
# output. They are ignored by default.
|
|
||||||
#show_authors = False
|
|
||||||
|
|
||||||
# The name of the Pygments (syntax highlighting) style to use.
|
|
||||||
pygments_style = 'sphinx'
|
|
||||||
|
|
||||||
# A list of ignored prefixes for module index sorting.
|
|
||||||
#modindex_common_prefix = []
|
|
||||||
|
|
||||||
|
|
||||||
# -- Options for HTML output ---------------------------------------------------
|
|
||||||
|
|
||||||
# The theme to use for HTML and HTML Help pages. See the documentation for
|
|
||||||
# a list of builtin themes.
|
|
||||||
html_theme = 'kr'
|
|
||||||
|
|
||||||
# Theme options are theme-specific and customize the look and feel of a theme
|
|
||||||
# further. For a list of options available for each theme, see the
|
|
||||||
# documentation.
|
|
||||||
#html_theme_options = {}
|
|
||||||
|
|
||||||
# Add any paths that contain custom themes here, relative to this directory.
|
|
||||||
html_theme_path = ['_themes']
|
|
||||||
|
|
||||||
# The name for this set of Sphinx documents. If None, it defaults to
|
|
||||||
# "<project> v<release> documentation".
|
|
||||||
#html_title = None
|
|
||||||
|
|
||||||
# A shorter title for the navigation bar. Default is the same as html_title.
|
|
||||||
#html_short_title = None
|
|
||||||
|
|
||||||
# The name of an image file (relative to this directory) to place at the top
|
|
||||||
# of the sidebar.
|
|
||||||
#html_logo = None
|
|
||||||
|
|
||||||
# The name of an image file (within the static path) to use as favicon of the
|
|
||||||
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
|
|
||||||
# pixels large.
|
|
||||||
#html_favicon = None
|
|
||||||
|
|
||||||
# Add any paths that contain custom static files (such as style sheets) here,
|
|
||||||
# relative to this directory. They are copied after the builtin static files,
|
|
||||||
# so a file named "default.css" will overwrite the builtin "default.css".
|
|
||||||
# html_static_path = ['_static']
|
|
||||||
|
|
||||||
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
|
|
||||||
# using the given strftime format.
|
|
||||||
#html_last_updated_fmt = '%b %d, %Y'
|
|
||||||
|
|
||||||
# If true, SmartyPants will be used to convert quotes and dashes to
|
|
||||||
# typographically correct entities.
|
|
||||||
#html_use_smartypants = True
|
|
||||||
|
|
||||||
# Custom sidebar templates, maps document names to template names.
|
|
||||||
#html_sidebars = {}
|
|
||||||
|
|
||||||
# Additional templates that should be rendered to pages, maps page names to
|
|
||||||
# template names.
|
|
||||||
#html_additional_pages = {}
|
|
||||||
|
|
||||||
# If false, no module index is generated.
|
|
||||||
#html_domain_indices = True
|
|
||||||
|
|
||||||
# If false, no index is generated.
|
|
||||||
#html_use_index = True
|
|
||||||
|
|
||||||
# If true, the index is split into individual pages for each letter.
|
|
||||||
#html_split_index = False
|
|
||||||
|
|
||||||
# If true, links to the reST sources are added to the pages.
|
|
||||||
#html_show_sourcelink = True
|
|
||||||
|
|
||||||
# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
|
|
||||||
#html_show_sphinx = True
|
|
||||||
|
|
||||||
# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
|
|
||||||
html_show_copyright = False
|
|
||||||
|
|
||||||
# If true, an OpenSearch description file will be output, and all pages will
|
|
||||||
# contain a <link> tag referring to it. The value of this option must be the
|
|
||||||
# base URL from which the finished HTML is served.
|
|
||||||
#html_use_opensearch = ''
|
|
||||||
|
|
||||||
# This is the file name suffix for HTML files (e.g. ".xhtml").
|
|
||||||
#html_file_suffix = None
|
|
||||||
|
|
||||||
# Output file base name for HTML help builder.
|
|
||||||
htmlhelp_basename = 'ImageKitdoc'
|
|
||||||
|
|
||||||
|
|
||||||
# -- Options for LaTeX output --------------------------------------------------
|
|
||||||
|
|
||||||
latex_elements = {
|
|
||||||
# The paper size ('letterpaper' or 'a4paper').
|
|
||||||
#'papersize': 'letterpaper',
|
|
||||||
|
|
||||||
# The font size ('10pt', '11pt' or '12pt').
|
|
||||||
#'pointsize': '10pt',
|
|
||||||
|
|
||||||
# Additional stuff for the LaTeX preamble.
|
|
||||||
#'preamble': '',
|
|
||||||
}
|
|
||||||
|
|
||||||
# Grouping the document tree into LaTeX files. List of tuples
|
|
||||||
# (source start file, target name, title, author, documentclass [howto/manual]).
|
|
||||||
latex_documents = [
|
|
||||||
('index', 'ImageKit.tex', u'ImageKit Documentation',
|
|
||||||
u'Justin Driscoll, Bryan Veloso, Greg Newman, Chris Drackett \\& Matthew Tretter', 'manual'),
|
|
||||||
]
|
|
||||||
|
|
||||||
# The name of an image file (relative to this directory) to place at the top of
|
|
||||||
# the title page.
|
|
||||||
#latex_logo = None
|
|
||||||
|
|
||||||
# For "manual" documents, if this is true, then toplevel headings are parts,
|
|
||||||
# not chapters.
|
|
||||||
#latex_use_parts = False
|
|
||||||
|
|
||||||
# If true, show page references after internal links.
|
|
||||||
#latex_show_pagerefs = False
|
|
||||||
|
|
||||||
# If true, show URL addresses after external links.
|
|
||||||
#latex_show_urls = False
|
|
||||||
|
|
||||||
# Documents to append as an appendix to all manuals.
|
|
||||||
#latex_appendices = []
|
|
||||||
|
|
||||||
# If false, no module index is generated.
|
|
||||||
#latex_domain_indices = True
|
|
||||||
|
|
||||||
|
|
||||||
# -- Options for manual page output --------------------------------------------
|
|
||||||
|
|
||||||
# One entry per manual page. List of tuples
|
|
||||||
# (source start file, name, description, authors, manual section).
|
|
||||||
man_pages = [
|
|
||||||
('index', 'imagekit', u'ImageKit Documentation',
|
|
||||||
[u'Justin Driscoll, Bryan Veloso, Greg Newman, Chris Drackett & Matthew Tretter'], 1)
|
|
||||||
]
|
|
||||||
|
|
||||||
# If true, show URL addresses after external links.
|
|
||||||
#man_show_urls = False
|
|
||||||
|
|
||||||
|
|
||||||
# -- Options for Texinfo output ------------------------------------------------
|
|
||||||
|
|
||||||
# Grouping the document tree into Texinfo files. List of tuples
|
|
||||||
# (source start file, target name, title, author,
|
|
||||||
# dir menu entry, description, category)
|
|
||||||
texinfo_documents = [
|
|
||||||
('index', 'ImageKit', u'ImageKit Documentation', u'Justin Driscoll, Bryan Veloso, Greg Newman, Chris Drackett & Matthew Tretter',
|
|
||||||
'ImageKit', 'One line description of project.', 'Miscellaneous'),
|
|
||||||
]
|
|
||||||
|
|
||||||
# Documents to append as an appendix to all manuals.
|
|
||||||
#texinfo_appendices = []
|
|
||||||
|
|
||||||
# If false, no module index is generated.
|
|
||||||
#texinfo_domain_indices = True
|
|
||||||
|
|
||||||
# How to display URL addresses: 'footnote', 'no', or 'inline'.
|
|
||||||
#texinfo_show_urls = 'footnote'
|
|
||||||
|
|
||||||
autoclass_content = 'both'
|
|
||||||
|
|
@ -1,90 +0,0 @@
|
||||||
.. _settings:
|
|
||||||
|
|
||||||
Configuration
|
|
||||||
=============
|
|
||||||
|
|
||||||
|
|
||||||
Settings
|
|
||||||
--------
|
|
||||||
|
|
||||||
.. currentmodule:: django.conf.settings
|
|
||||||
|
|
||||||
|
|
||||||
.. attribute:: IMAGEKIT_CACHEFILE_DIR
|
|
||||||
|
|
||||||
:default: ``'CACHE/images'``
|
|
||||||
|
|
||||||
The directory to which image files will be cached.
|
|
||||||
|
|
||||||
|
|
||||||
.. attribute:: IMAGEKIT_DEFAULT_FILE_STORAGE
|
|
||||||
|
|
||||||
:default: ``None``
|
|
||||||
|
|
||||||
The qualified class name of a Django storage backend to use to save the
|
|
||||||
cached images. If no value is provided for ``IMAGEKIT_DEFAULT_FILE_STORAGE``,
|
|
||||||
and none is specified by the spec definition, `your default file storage`__
|
|
||||||
will be used.
|
|
||||||
|
|
||||||
|
|
||||||
.. attribute:: IMAGEKIT_DEFAULT_CACHEFILE_BACKEND
|
|
||||||
|
|
||||||
:default: ``'imagekit.cachefiles.backends.Simple'``
|
|
||||||
|
|
||||||
Specifies the class that will be used to validate cached image files.
|
|
||||||
|
|
||||||
|
|
||||||
.. attribute:: IMAGEKIT_DEFAULT_CACHEFILE_STRATEGY
|
|
||||||
|
|
||||||
:default: ``'imagekit.cachefiles.strategies.JustInTime'``
|
|
||||||
|
|
||||||
The class responsible for specifying how and when cache files are
|
|
||||||
generated.
|
|
||||||
|
|
||||||
|
|
||||||
.. attribute:: IMAGEKIT_CACHE_BACKEND
|
|
||||||
|
|
||||||
:default: ``'default'``
|
|
||||||
|
|
||||||
The Django cache backend alias to retrieve the shared cache instance defined
|
|
||||||
in your settings, as described in the `Django cache section`_.
|
|
||||||
|
|
||||||
The cache is then used to store information like the state of cached
|
|
||||||
images (i.e. validated or not).
|
|
||||||
|
|
||||||
.. _`Django cache section`: https://docs.djangoproject.com/en/1.8/topics/cache/#accessing-the-cache
|
|
||||||
|
|
||||||
|
|
||||||
.. attribute:: IMAGEKIT_CACHE_TIMEOUT
|
|
||||||
|
|
||||||
:default: ``None``
|
|
||||||
|
|
||||||
Use when you need to override the timeout used to cache file state.
|
|
||||||
By default it is "cache forever".
|
|
||||||
It's highly recommended that you use a very high timeout.
|
|
||||||
|
|
||||||
|
|
||||||
.. attribute:: IMAGEKIT_CACHE_PREFIX
|
|
||||||
|
|
||||||
:default: ``'imagekit:'``
|
|
||||||
|
|
||||||
A cache prefix to be used when values are stored in ``IMAGEKIT_CACHE_BACKEND``
|
|
||||||
|
|
||||||
|
|
||||||
.. attribute:: IMAGEKIT_CACHEFILE_NAMER
|
|
||||||
|
|
||||||
:default: ``'imagekit.cachefiles.namers.hash'``
|
|
||||||
|
|
||||||
A function responsible for generating file names for non-spec cache files.
|
|
||||||
|
|
||||||
|
|
||||||
.. attribute:: IMAGEKIT_SPEC_CACHEFILE_NAMER
|
|
||||||
|
|
||||||
:default: ``'imagekit.cachefiles.namers.source_name_as_path'``
|
|
||||||
|
|
||||||
A function responsible for generating file names for cache files that
|
|
||||||
correspond to image specs. Since you will likely want to base the name of
|
|
||||||
your cache files on the name of the source, this extra setting is provided.
|
|
||||||
|
|
||||||
|
|
||||||
__ https://docs.djangoproject.com/en/dev/ref/settings/#default-file-storage
|
|
||||||
|
|
@ -1,24 +0,0 @@
|
||||||
.. include:: ../README.rst
|
|
||||||
|
|
||||||
|
|
||||||
Authors
|
|
||||||
=======
|
|
||||||
|
|
||||||
.. include:: ../AUTHORS
|
|
||||||
|
|
||||||
|
|
||||||
Indices and tables
|
|
||||||
==================
|
|
||||||
|
|
||||||
* :ref:`genindex`
|
|
||||||
* :ref:`modindex`
|
|
||||||
* :ref:`search`
|
|
||||||
|
|
||||||
.. toctree::
|
|
||||||
:glob:
|
|
||||||
:maxdepth: 2
|
|
||||||
|
|
||||||
configuration
|
|
||||||
advanced_usage
|
|
||||||
caching
|
|
||||||
upgrading
|
|
||||||
190
docs/make.bat
190
docs/make.bat
|
|
@ -1,190 +0,0 @@
|
||||||
@ECHO OFF
|
|
||||||
|
|
||||||
REM Command file for Sphinx documentation
|
|
||||||
|
|
||||||
if "%SPHINXBUILD%" == "" (
|
|
||||||
set SPHINXBUILD=sphinx-build
|
|
||||||
)
|
|
||||||
set BUILDDIR=build
|
|
||||||
set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% .
|
|
||||||
set I18NSPHINXOPTS=%SPHINXOPTS% .
|
|
||||||
if NOT "%PAPER%" == "" (
|
|
||||||
set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS%
|
|
||||||
set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS%
|
|
||||||
)
|
|
||||||
|
|
||||||
if "%1" == "" goto help
|
|
||||||
|
|
||||||
if "%1" == "help" (
|
|
||||||
:help
|
|
||||||
echo.Please use `make ^<target^>` where ^<target^> is one of
|
|
||||||
echo. html to make standalone HTML files
|
|
||||||
echo. dirhtml to make HTML files named index.html in directories
|
|
||||||
echo. singlehtml to make a single large HTML file
|
|
||||||
echo. pickle to make pickle files
|
|
||||||
echo. json to make JSON files
|
|
||||||
echo. htmlhelp to make HTML files and a HTML help project
|
|
||||||
echo. qthelp to make HTML files and a qthelp project
|
|
||||||
echo. devhelp to make HTML files and a Devhelp project
|
|
||||||
echo. epub to make an epub
|
|
||||||
echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter
|
|
||||||
echo. text to make text files
|
|
||||||
echo. man to make manual pages
|
|
||||||
echo. texinfo to make Texinfo files
|
|
||||||
echo. gettext to make PO message catalogs
|
|
||||||
echo. changes to make an overview over all changed/added/deprecated items
|
|
||||||
echo. linkcheck to check all external links for integrity
|
|
||||||
echo. doctest to run all doctests embedded in the documentation if enabled
|
|
||||||
goto end
|
|
||||||
)
|
|
||||||
|
|
||||||
if "%1" == "clean" (
|
|
||||||
for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i
|
|
||||||
del /q /s %BUILDDIR%\*
|
|
||||||
goto end
|
|
||||||
)
|
|
||||||
|
|
||||||
if "%1" == "html" (
|
|
||||||
%SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html
|
|
||||||
if errorlevel 1 exit /b 1
|
|
||||||
echo.
|
|
||||||
echo.Build finished. The HTML pages are in %BUILDDIR%/html.
|
|
||||||
goto end
|
|
||||||
)
|
|
||||||
|
|
||||||
if "%1" == "dirhtml" (
|
|
||||||
%SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml
|
|
||||||
if errorlevel 1 exit /b 1
|
|
||||||
echo.
|
|
||||||
echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml.
|
|
||||||
goto end
|
|
||||||
)
|
|
||||||
|
|
||||||
if "%1" == "singlehtml" (
|
|
||||||
%SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml
|
|
||||||
if errorlevel 1 exit /b 1
|
|
||||||
echo.
|
|
||||||
echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml.
|
|
||||||
goto end
|
|
||||||
)
|
|
||||||
|
|
||||||
if "%1" == "pickle" (
|
|
||||||
%SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle
|
|
||||||
if errorlevel 1 exit /b 1
|
|
||||||
echo.
|
|
||||||
echo.Build finished; now you can process the pickle files.
|
|
||||||
goto end
|
|
||||||
)
|
|
||||||
|
|
||||||
if "%1" == "json" (
|
|
||||||
%SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json
|
|
||||||
if errorlevel 1 exit /b 1
|
|
||||||
echo.
|
|
||||||
echo.Build finished; now you can process the JSON files.
|
|
||||||
goto end
|
|
||||||
)
|
|
||||||
|
|
||||||
if "%1" == "htmlhelp" (
|
|
||||||
%SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp
|
|
||||||
if errorlevel 1 exit /b 1
|
|
||||||
echo.
|
|
||||||
echo.Build finished; now you can run HTML Help Workshop with the ^
|
|
||||||
.hhp project file in %BUILDDIR%/htmlhelp.
|
|
||||||
goto end
|
|
||||||
)
|
|
||||||
|
|
||||||
if "%1" == "qthelp" (
|
|
||||||
%SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp
|
|
||||||
if errorlevel 1 exit /b 1
|
|
||||||
echo.
|
|
||||||
echo.Build finished; now you can run "qcollectiongenerator" with the ^
|
|
||||||
.qhcp project file in %BUILDDIR%/qthelp, like this:
|
|
||||||
echo.^> qcollectiongenerator %BUILDDIR%\qthelp\ImageKit.qhcp
|
|
||||||
echo.To view the help file:
|
|
||||||
echo.^> assistant -collectionFile %BUILDDIR%\qthelp\ImageKit.ghc
|
|
||||||
goto end
|
|
||||||
)
|
|
||||||
|
|
||||||
if "%1" == "devhelp" (
|
|
||||||
%SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp
|
|
||||||
if errorlevel 1 exit /b 1
|
|
||||||
echo.
|
|
||||||
echo.Build finished.
|
|
||||||
goto end
|
|
||||||
)
|
|
||||||
|
|
||||||
if "%1" == "epub" (
|
|
||||||
%SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub
|
|
||||||
if errorlevel 1 exit /b 1
|
|
||||||
echo.
|
|
||||||
echo.Build finished. The epub file is in %BUILDDIR%/epub.
|
|
||||||
goto end
|
|
||||||
)
|
|
||||||
|
|
||||||
if "%1" == "latex" (
|
|
||||||
%SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
|
|
||||||
if errorlevel 1 exit /b 1
|
|
||||||
echo.
|
|
||||||
echo.Build finished; the LaTeX files are in %BUILDDIR%/latex.
|
|
||||||
goto end
|
|
||||||
)
|
|
||||||
|
|
||||||
if "%1" == "text" (
|
|
||||||
%SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text
|
|
||||||
if errorlevel 1 exit /b 1
|
|
||||||
echo.
|
|
||||||
echo.Build finished. The text files are in %BUILDDIR%/text.
|
|
||||||
goto end
|
|
||||||
)
|
|
||||||
|
|
||||||
if "%1" == "man" (
|
|
||||||
%SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man
|
|
||||||
if errorlevel 1 exit /b 1
|
|
||||||
echo.
|
|
||||||
echo.Build finished. The manual pages are in %BUILDDIR%/man.
|
|
||||||
goto end
|
|
||||||
)
|
|
||||||
|
|
||||||
if "%1" == "texinfo" (
|
|
||||||
%SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo
|
|
||||||
if errorlevel 1 exit /b 1
|
|
||||||
echo.
|
|
||||||
echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo.
|
|
||||||
goto end
|
|
||||||
)
|
|
||||||
|
|
||||||
if "%1" == "gettext" (
|
|
||||||
%SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale
|
|
||||||
if errorlevel 1 exit /b 1
|
|
||||||
echo.
|
|
||||||
echo.Build finished. The message catalogs are in %BUILDDIR%/locale.
|
|
||||||
goto end
|
|
||||||
)
|
|
||||||
|
|
||||||
if "%1" == "changes" (
|
|
||||||
%SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes
|
|
||||||
if errorlevel 1 exit /b 1
|
|
||||||
echo.
|
|
||||||
echo.The overview file is in %BUILDDIR%/changes.
|
|
||||||
goto end
|
|
||||||
)
|
|
||||||
|
|
||||||
if "%1" == "linkcheck" (
|
|
||||||
%SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck
|
|
||||||
if errorlevel 1 exit /b 1
|
|
||||||
echo.
|
|
||||||
echo.Link check complete; look for any errors in the above output ^
|
|
||||||
or in %BUILDDIR%/linkcheck/output.txt.
|
|
||||||
goto end
|
|
||||||
)
|
|
||||||
|
|
||||||
if "%1" == "doctest" (
|
|
||||||
%SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest
|
|
||||||
if errorlevel 1 exit /b 1
|
|
||||||
echo.
|
|
||||||
echo.Testing of doctests in the sources finished, look at the ^
|
|
||||||
results in %BUILDDIR%/doctest/output.txt.
|
|
||||||
goto end
|
|
||||||
)
|
|
||||||
|
|
||||||
:end
|
|
||||||
|
|
@ -1,117 +0,0 @@
|
||||||
Upgrading from 2.x
|
|
||||||
==================
|
|
||||||
|
|
||||||
ImageKit 3.0 introduces new APIs and tools that augment, improve, and in some
|
|
||||||
cases entirely replace old IK workflows. Below, you'll find some useful guides
|
|
||||||
for migrating your ImageKit 2.0 apps over to the shiny new IK3.
|
|
||||||
|
|
||||||
|
|
||||||
Model Specs
|
|
||||||
-----------
|
|
||||||
|
|
||||||
IK3 is chock full of new features and better tools for even the most
|
|
||||||
sophisticated use cases. Despite this, not too much has changed when it
|
|
||||||
comes to the most common of use cases: processing an ``ImageField`` on a model.
|
|
||||||
|
|
||||||
In IK2, you may have used an ``ImageSpecField`` on a model to process an
|
|
||||||
existing ``ImageField``:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
class Profile(models.Model):
|
|
||||||
avatar = models.ImageField(upload_to='avatars')
|
|
||||||
avatar_thumbnail = ImageSpecField(image_field='avatar',
|
|
||||||
processors=[ResizeToFill(100, 50)],
|
|
||||||
format='JPEG',
|
|
||||||
options={'quality': 60})
|
|
||||||
|
|
||||||
In IK3, things look much the same:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
class Profile(models.Model):
|
|
||||||
avatar = models.ImageField(upload_to='avatars')
|
|
||||||
avatar_thumbnail = ImageSpecField(source='avatar',
|
|
||||||
processors=[ResizeToFill(100, 50)],
|
|
||||||
format='JPEG',
|
|
||||||
options={'quality': 60})
|
|
||||||
|
|
||||||
The major difference is that ``ImageSpecField`` no longer takes an
|
|
||||||
``image_field`` kwarg. Instead, you define a ``source``.
|
|
||||||
|
|
||||||
|
|
||||||
Image Cache Backends
|
|
||||||
--------------------
|
|
||||||
|
|
||||||
In IK2, you could gain some control over how your cached images were generated
|
|
||||||
by providing an ``image_cache_backend``:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
class Photo(models.Model):
|
|
||||||
...
|
|
||||||
thumbnail = ImageSpecField(..., image_cache_backend=MyImageCacheBackend())
|
|
||||||
|
|
||||||
This gave you great control over *how* your images are generated and stored,
|
|
||||||
but it could be difficult to control *when* they were generated and stored.
|
|
||||||
|
|
||||||
IK3 retains the image cache backend concept (now called cache file backends),
|
|
||||||
but separates the 'when' control out to cache file strategies:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
class Photo(models.Model):
|
|
||||||
...
|
|
||||||
thumbnail = ImageSpecField(...,
|
|
||||||
cachefile_backend=MyCacheFileBackend(),
|
|
||||||
cachefile_strategy=MyCacheFileStrategy())
|
|
||||||
|
|
||||||
If you are using the IK2 default image cache backend setting:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
IMAGEKIT_DEFAULT_IMAGE_CACHE_BACKEND = 'path.to.MyImageCacheBackend'
|
|
||||||
|
|
||||||
IK3 provides analogous settings for cache file backends and strategies:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
IMAGEKIT_DEFAULT_CACHEFILE_BACKEND = 'path.to.MyCacheFileBackend'
|
|
||||||
IMAGEKIT_DEFAULT_CACHEFILE_STRATEGY = 'path.to.MyCacheFileStrategy'
|
|
||||||
|
|
||||||
See the documentation on :ref:`cache file backends <cache-file-backend>` and :ref:`cache file strategies <cache-file-strategy>`
|
|
||||||
for more details.
|
|
||||||
|
|
||||||
|
|
||||||
Conditional model ``processors``
|
|
||||||
--------------------------------
|
|
||||||
|
|
||||||
In IK2, an ``ImageSpecField`` could take a ``processors`` callable instead of
|
|
||||||
an iterable, which allowed processing decisions to made based on other
|
|
||||||
properties of the model. IK3 does away with this feature for consistency's sake
|
|
||||||
(if one kwarg could be callable, why not all?), but provides a much more robust
|
|
||||||
solution: the custom ``spec``. See the :doc:`advanced usage <advanced_usage>` documentation for more.
|
|
||||||
|
|
||||||
|
|
||||||
Conditonal ``cache_to`` file names
|
|
||||||
----------------------------------
|
|
||||||
|
|
||||||
IK2 provided a means of specifying custom cache file names for your
|
|
||||||
image specs by passing a ``cache_to`` callable to an ``ImageSpecField``.
|
|
||||||
IK3 does away with this feature, again, for consistency.
|
|
||||||
|
|
||||||
There is a way to achieve custom file names by overriding your spec's
|
|
||||||
``cachefile_name``, but it is not recommended, as the spec's default
|
|
||||||
behavior is to hash the combination of ``source``, ``processors``, ``format``,
|
|
||||||
and other spec options to ensure that changes to the spec always result in
|
|
||||||
unique file names. See the documentation on :ref:`specs` for more.
|
|
||||||
|
|
||||||
|
|
||||||
Processors have moved to PILKit
|
|
||||||
-------------------------------
|
|
||||||
|
|
||||||
Processors have moved to a separate project: `PILKit`_. You should not have to
|
|
||||||
make any changes to an IK2 project to use PILKit--it should be installed with
|
|
||||||
IK3, and importing from ``imagekit.processors`` will still work.
|
|
||||||
|
|
||||||
.. _`PILKit`: https://github.com/matthewwithanm/pilkit
|
|
||||||
|
|
@ -1,6 +1,11 @@
|
||||||
# flake8: noqa
|
"""
|
||||||
from . import conf
|
Django ImageKit
|
||||||
from . import generatorlibrary
|
|
||||||
from .specs import ImageSpec
|
Author: Justin Driscoll <justin.driscoll@gmail.com>
|
||||||
from .pkgmeta import *
|
Version: 0.3.6
|
||||||
from .registry import register, unregister
|
|
||||||
|
"""
|
||||||
|
__author__ = 'Justin Driscoll, Bryan Veloso, Greg Newman, Chris Drackett'
|
||||||
|
__version__ = (0, 3, 6)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,40 +0,0 @@
|
||||||
from django.utils.translation import ugettext_lazy as _
|
|
||||||
from django.template.loader import render_to_string
|
|
||||||
|
|
||||||
|
|
||||||
class AdminThumbnail(object):
|
|
||||||
"""
|
|
||||||
A convenience utility for adding thumbnails to Django's admin change list.
|
|
||||||
|
|
||||||
"""
|
|
||||||
short_description = _('Thumbnail')
|
|
||||||
allow_tags = True
|
|
||||||
|
|
||||||
def __init__(self, image_field, template=None):
|
|
||||||
"""
|
|
||||||
:param image_field: The name of the ImageField or ImageSpecField on the
|
|
||||||
model to use for the thumbnail.
|
|
||||||
:param template: The template with which to render the thumbnail
|
|
||||||
|
|
||||||
"""
|
|
||||||
self.image_field = image_field
|
|
||||||
self.template = template
|
|
||||||
|
|
||||||
def __call__(self, obj):
|
|
||||||
if callable(self.image_field):
|
|
||||||
thumbnail = self.image_field(obj)
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
thumbnail = getattr(obj, self.image_field)
|
|
||||||
except AttributeError:
|
|
||||||
raise Exception('The property %s is not defined on %s.' %
|
|
||||||
(self.image_field, obj.__class__.__name__))
|
|
||||||
|
|
||||||
original_image = getattr(thumbnail, 'source', None) or thumbnail
|
|
||||||
template = self.template or 'imagekit/admin/thumbnail.html'
|
|
||||||
|
|
||||||
return render_to_string(template, {
|
|
||||||
'model': obj,
|
|
||||||
'thumbnail': thumbnail,
|
|
||||||
'original_image': original_image,
|
|
||||||
})
|
|
||||||
|
|
@ -1,183 +0,0 @@
|
||||||
from copy import copy
|
|
||||||
from django.conf import settings
|
|
||||||
from django.core.files import File
|
|
||||||
from django.core.files.images import ImageFile
|
|
||||||
from django.utils.functional import SimpleLazyObject
|
|
||||||
from django.utils.encoding import smart_str
|
|
||||||
from ..files import BaseIKFile
|
|
||||||
from ..registry import generator_registry
|
|
||||||
from ..signals import content_required, existence_required
|
|
||||||
from ..utils import get_logger, get_singleton, generate, get_by_qname
|
|
||||||
|
|
||||||
|
|
||||||
class ImageCacheFile(BaseIKFile, ImageFile):
|
|
||||||
"""
|
|
||||||
A file that represents the result of a generator. Creating an instance of
|
|
||||||
this class is not enough to trigger the generation of the file. In fact,
|
|
||||||
one of the main points of this class is to allow the creation of the file
|
|
||||||
to be deferred until the time that the cache file strategy requires it.
|
|
||||||
|
|
||||||
"""
|
|
||||||
def __init__(self, generator, name=None, storage=None, cachefile_backend=None, cachefile_strategy=None):
|
|
||||||
"""
|
|
||||||
:param generator: The object responsible for generating a new image.
|
|
||||||
:param name: The filename
|
|
||||||
:param storage: A Django storage object that will be used to save the
|
|
||||||
file.
|
|
||||||
:param cachefile_backend: The object responsible for managing the
|
|
||||||
state of the file.
|
|
||||||
:param cachefile_strategy: The object responsible for handling events
|
|
||||||
for this file.
|
|
||||||
|
|
||||||
"""
|
|
||||||
self.generator = generator
|
|
||||||
|
|
||||||
if not name:
|
|
||||||
try:
|
|
||||||
name = generator.cachefile_name
|
|
||||||
except AttributeError:
|
|
||||||
fn = get_by_qname(settings.IMAGEKIT_CACHEFILE_NAMER, 'namer')
|
|
||||||
name = fn(generator)
|
|
||||||
self.name = name
|
|
||||||
|
|
||||||
storage = storage or getattr(generator, 'cachefile_storage',
|
|
||||||
None) or get_singleton(settings.IMAGEKIT_DEFAULT_FILE_STORAGE,
|
|
||||||
'file storage backend')
|
|
||||||
self.cachefile_backend = (
|
|
||||||
cachefile_backend
|
|
||||||
or getattr(generator, 'cachefile_backend', None)
|
|
||||||
or get_singleton(settings.IMAGEKIT_DEFAULT_CACHEFILE_BACKEND,
|
|
||||||
'cache file backend'))
|
|
||||||
self.cachefile_strategy = (
|
|
||||||
cachefile_strategy
|
|
||||||
or getattr(generator, 'cachefile_strategy', None)
|
|
||||||
or get_singleton(settings.IMAGEKIT_DEFAULT_CACHEFILE_STRATEGY,
|
|
||||||
'cache file strategy')
|
|
||||||
)
|
|
||||||
|
|
||||||
super(ImageCacheFile, self).__init__(storage=storage)
|
|
||||||
|
|
||||||
def _require_file(self):
|
|
||||||
if getattr(self, '_file', None) is None:
|
|
||||||
content_required.send(sender=self, file=self)
|
|
||||||
self._file = self.storage.open(self.name, 'rb')
|
|
||||||
|
|
||||||
# The ``path`` and ``url`` properties are overridden so as to not call
|
|
||||||
# ``_require_file``, which is only meant to be called when the file object
|
|
||||||
# will be directly interacted with (e.g. when using ``read()``). These only
|
|
||||||
# require the file to exist; they do not need its contents to work. This
|
|
||||||
# distinction gives the user the flexibility to create a cache file
|
|
||||||
# strategy that assumes the existence of a file, but can still make the file
|
|
||||||
# available when its contents are required.
|
|
||||||
|
|
||||||
def _storage_attr(self, attr):
|
|
||||||
if getattr(self, '_file', None) is None:
|
|
||||||
existence_required.send(sender=self, file=self)
|
|
||||||
fn = getattr(self.storage, attr)
|
|
||||||
return fn(self.name)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def path(self):
|
|
||||||
return self._storage_attr('path')
|
|
||||||
|
|
||||||
@property
|
|
||||||
def url(self):
|
|
||||||
return self._storage_attr('url')
|
|
||||||
|
|
||||||
def generate(self, force=False):
|
|
||||||
"""
|
|
||||||
Generate the file. If ``force`` is ``True``, the file will be generated
|
|
||||||
whether the file already exists or not.
|
|
||||||
|
|
||||||
"""
|
|
||||||
if force or getattr(self, '_file', None) is None:
|
|
||||||
self.cachefile_backend.generate(self, force)
|
|
||||||
|
|
||||||
def _generate(self):
|
|
||||||
# Generate the file
|
|
||||||
content = generate(self.generator)
|
|
||||||
|
|
||||||
actual_name = self.storage.save(self.name, content)
|
|
||||||
|
|
||||||
# We're going to reuse the generated file, so we need to reset the pointer.
|
|
||||||
content.seek(0)
|
|
||||||
|
|
||||||
# Store the generated file. If we don't do this, the next time the
|
|
||||||
# "file" attribute is accessed, it will result in a call to the storage
|
|
||||||
# backend (in ``BaseIKFile._get_file``). Since we already have the
|
|
||||||
# contents of the file, what would the point of that be?
|
|
||||||
self.file = File(content)
|
|
||||||
|
|
||||||
if actual_name != self.name:
|
|
||||||
get_logger().warning(
|
|
||||||
'The storage backend %s did not save the file with the'
|
|
||||||
' requested name ("%s") and instead used "%s". This may be'
|
|
||||||
' because a file already existed with the requested name. If'
|
|
||||||
' so, you may have meant to call generate() instead of'
|
|
||||||
' generate(force=True), or there may be a race condition in the'
|
|
||||||
' file backend %s. The saved file will not be used.' % (
|
|
||||||
self.storage,
|
|
||||||
self.name, actual_name,
|
|
||||||
self.cachefile_backend
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
def __bool__(self):
|
|
||||||
if not self.name:
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Dispatch the existence_required signal before checking to see if the
|
|
||||||
# file exists. This gives the strategy a chance to create the file.
|
|
||||||
existence_required.send(sender=self, file=self)
|
|
||||||
|
|
||||||
try:
|
|
||||||
check = self.cachefile_strategy.should_verify_existence(self)
|
|
||||||
except AttributeError:
|
|
||||||
# All synchronous backends should have created the file as part of
|
|
||||||
# `existence_required` if they wanted to.
|
|
||||||
check = getattr(self.cachefile_backend, 'is_async', False)
|
|
||||||
return self.cachefile_backend.exists(self) if check else True
|
|
||||||
|
|
||||||
def __getstate__(self):
|
|
||||||
state = copy(self.__dict__)
|
|
||||||
|
|
||||||
# file is hidden link to "file" attribute
|
|
||||||
state.pop('_file', None)
|
|
||||||
|
|
||||||
# remove storage from state as some non-FileSystemStorage can't be
|
|
||||||
# pickled
|
|
||||||
settings_storage = get_singleton(
|
|
||||||
settings.IMAGEKIT_DEFAULT_FILE_STORAGE,
|
|
||||||
'file storage backend'
|
|
||||||
)
|
|
||||||
if state['storage'] == settings_storage:
|
|
||||||
state.pop('storage')
|
|
||||||
return state
|
|
||||||
|
|
||||||
def __setstate__(self, state):
|
|
||||||
if 'storage' not in state:
|
|
||||||
state['storage'] = get_singleton(
|
|
||||||
settings.IMAGEKIT_DEFAULT_FILE_STORAGE,
|
|
||||||
'file storage backend'
|
|
||||||
)
|
|
||||||
self.__dict__.update(state)
|
|
||||||
|
|
||||||
def __nonzero__(self):
|
|
||||||
# Python 2 compatibility
|
|
||||||
return self.__bool__()
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return smart_str("<%s: %s>" % (
|
|
||||||
self.__class__.__name__, self if self.name else "None")
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class LazyImageCacheFile(SimpleLazyObject):
|
|
||||||
def __init__(self, generator_id, *args, **kwargs):
|
|
||||||
def setup():
|
|
||||||
generator = generator_registry.get(generator_id, *args, **kwargs)
|
|
||||||
return ImageCacheFile(generator)
|
|
||||||
super(LazyImageCacheFile, self).__init__(setup)
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return '<%s: %s>' % (self.__class__.__name__, str(self) or 'None')
|
|
||||||
|
|
@ -1,195 +0,0 @@
|
||||||
from ..utils import get_singleton, get_cache, sanitize_cache_key
|
|
||||||
import warnings
|
|
||||||
from copy import copy
|
|
||||||
from django.core.exceptions import ImproperlyConfigured
|
|
||||||
from django.conf import settings
|
|
||||||
|
|
||||||
|
|
||||||
class CacheFileState(object):
|
|
||||||
EXISTS = 'exists'
|
|
||||||
GENERATING = 'generating'
|
|
||||||
DOES_NOT_EXIST = 'does_not_exist'
|
|
||||||
|
|
||||||
|
|
||||||
def get_default_cachefile_backend():
|
|
||||||
"""
|
|
||||||
Get the default file backend.
|
|
||||||
|
|
||||||
"""
|
|
||||||
from django.conf import settings
|
|
||||||
return get_singleton(settings.IMAGEKIT_DEFAULT_CACHEFILE_BACKEND,
|
|
||||||
'file backend')
|
|
||||||
|
|
||||||
|
|
||||||
class InvalidFileBackendError(ImproperlyConfigured):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class AbstractCacheFileBackend(object):
|
|
||||||
"""
|
|
||||||
An abstract cache file backend. This isn't used by any internal classes and
|
|
||||||
is included simply to illustrate the minimum interface of a cache file
|
|
||||||
backend for users who wish to implement their own.
|
|
||||||
|
|
||||||
"""
|
|
||||||
def generate(self, file, force=False):
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
def exists(self, file):
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
|
|
||||||
class CachedFileBackend(object):
|
|
||||||
existence_check_timeout = 5
|
|
||||||
"""
|
|
||||||
The number of seconds to wait before rechecking to see if the file exists.
|
|
||||||
If the image is found to exist, that information will be cached using the
|
|
||||||
timeout specified in your CACHES setting (which should be very high).
|
|
||||||
However, when the file does not exist, you probably want to check again
|
|
||||||
in a relatively short amount of time. This attribute allows you to do that.
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
@property
|
|
||||||
def cache(self):
|
|
||||||
if not getattr(self, '_cache', None):
|
|
||||||
self._cache = get_cache()
|
|
||||||
return self._cache
|
|
||||||
|
|
||||||
def get_key(self, file):
|
|
||||||
from django.conf import settings
|
|
||||||
return sanitize_cache_key('%s%s-state' %
|
|
||||||
(settings.IMAGEKIT_CACHE_PREFIX, file.name))
|
|
||||||
|
|
||||||
def get_state(self, file, check_if_unknown=True):
|
|
||||||
key = self.get_key(file)
|
|
||||||
state = self.cache.get(key)
|
|
||||||
if state is None and check_if_unknown:
|
|
||||||
exists = self._exists(file)
|
|
||||||
state = CacheFileState.EXISTS if exists else CacheFileState.DOES_NOT_EXIST
|
|
||||||
self.set_state(file, state)
|
|
||||||
return state
|
|
||||||
|
|
||||||
def set_state(self, file, state):
|
|
||||||
key = self.get_key(file)
|
|
||||||
if state == CacheFileState.DOES_NOT_EXIST:
|
|
||||||
self.cache.set(key, state, self.existence_check_timeout)
|
|
||||||
else:
|
|
||||||
self.cache.set(key, state, settings.IMAGEKIT_CACHE_TIMEOUT)
|
|
||||||
|
|
||||||
def __getstate__(self):
|
|
||||||
state = copy(self.__dict__)
|
|
||||||
# Don't include the cache when pickling. It'll be reconstituted based
|
|
||||||
# on the settings.
|
|
||||||
state.pop('_cache', None)
|
|
||||||
return state
|
|
||||||
|
|
||||||
def exists(self, file):
|
|
||||||
return self.get_state(file) == CacheFileState.EXISTS
|
|
||||||
|
|
||||||
def generate(self, file, force=False):
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
def generate_now(self, file, force=False):
|
|
||||||
if force or self.get_state(file) not in (CacheFileState.GENERATING, CacheFileState.EXISTS):
|
|
||||||
self.set_state(file, CacheFileState.GENERATING)
|
|
||||||
file._generate()
|
|
||||||
self.set_state(file, CacheFileState.EXISTS)
|
|
||||||
file.close()
|
|
||||||
|
|
||||||
|
|
||||||
class Simple(CachedFileBackend):
|
|
||||||
"""
|
|
||||||
The most basic file backend. The storage is consulted to see if the file
|
|
||||||
exists. Files are generated synchronously.
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
def generate(self, file, force=False):
|
|
||||||
self.generate_now(file, force=force)
|
|
||||||
|
|
||||||
def _exists(self, file):
|
|
||||||
return bool(getattr(file, '_file', None)
|
|
||||||
or (file.name and file.storage.exists(file.name)))
|
|
||||||
|
|
||||||
|
|
||||||
def _generate_file(backend, file, force=False):
|
|
||||||
backend.generate_now(file, force=force)
|
|
||||||
|
|
||||||
|
|
||||||
class BaseAsync(Simple):
|
|
||||||
"""
|
|
||||||
Base class for cache file backends that generate files asynchronously.
|
|
||||||
"""
|
|
||||||
is_async = True
|
|
||||||
|
|
||||||
def generate(self, file, force=False):
|
|
||||||
# Schedule the file for generation, unless we know for sure we don't
|
|
||||||
# need to. If an already-generated file sneaks through, that's okay;
|
|
||||||
# ``generate_now`` will catch it. We just want to make sure we don't
|
|
||||||
# schedule anything we know is unnecessary--but we also don't want to
|
|
||||||
# force a costly existence check.
|
|
||||||
state = self.get_state(file, check_if_unknown=False)
|
|
||||||
if state not in (CacheFileState.GENERATING, CacheFileState.EXISTS):
|
|
||||||
self.schedule_generation(file, force=force)
|
|
||||||
|
|
||||||
def schedule_generation(self, file, force=False):
|
|
||||||
# overwrite this to have the file generated in the background,
|
|
||||||
# e. g. in a worker queue.
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
|
|
||||||
try:
|
|
||||||
from celery import task
|
|
||||||
except ImportError:
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
_celery_task = task(ignore_result=True, serializer='pickle')(_generate_file)
|
|
||||||
|
|
||||||
|
|
||||||
class Celery(BaseAsync):
|
|
||||||
"""
|
|
||||||
A backend that uses Celery to generate the images.
|
|
||||||
"""
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
try:
|
|
||||||
import celery # noqa
|
|
||||||
except ImportError:
|
|
||||||
raise ImproperlyConfigured('You must install celery to use'
|
|
||||||
' imagekit.cachefiles.backends.Celery.')
|
|
||||||
super(Celery, self).__init__(*args, **kwargs)
|
|
||||||
|
|
||||||
def schedule_generation(self, file, force=False):
|
|
||||||
_celery_task.delay(self, file, force=force)
|
|
||||||
|
|
||||||
|
|
||||||
# Stub class to preserve backwards compatibility and issue a warning
|
|
||||||
class Async(Celery):
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
message = '{path}.Async is deprecated. Use {path}.Celery instead.'
|
|
||||||
warnings.warn(message.format(path=__name__), DeprecationWarning)
|
|
||||||
super(Async, self).__init__(*args, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
try:
|
|
||||||
from django_rq import job
|
|
||||||
except ImportError:
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
_rq_job = job('default', result_ttl=0)(_generate_file)
|
|
||||||
|
|
||||||
|
|
||||||
class RQ(BaseAsync):
|
|
||||||
"""
|
|
||||||
A backend that uses RQ to generate the images.
|
|
||||||
"""
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
try:
|
|
||||||
import django_rq # noqa
|
|
||||||
except ImportError:
|
|
||||||
raise ImproperlyConfigured('You must install django-rq to use'
|
|
||||||
' imagekit.cachefiles.backends.RQ.')
|
|
||||||
super(RQ, self).__init__(*args, **kwargs)
|
|
||||||
|
|
||||||
def schedule_generation(self, file, force=False):
|
|
||||||
_rq_job.delay(self, file, force=force)
|
|
||||||
|
|
@ -1,91 +0,0 @@
|
||||||
"""
|
|
||||||
Functions responsible for returning filenames for the given image generator.
|
|
||||||
Users are free to define their own functions; these are just some some sensible
|
|
||||||
choices.
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
from django.conf import settings
|
|
||||||
import os
|
|
||||||
from ..utils import format_to_extension, suggest_extension
|
|
||||||
|
|
||||||
|
|
||||||
def source_name_as_path(generator):
|
|
||||||
"""
|
|
||||||
A namer that, given the following source file name::
|
|
||||||
|
|
||||||
photos/thumbnails/bulldog.jpg
|
|
||||||
|
|
||||||
will generate a name like this::
|
|
||||||
|
|
||||||
/path/to/generated/images/photos/thumbnails/bulldog/5ff3233527c5ac3e4b596343b440ff67.jpg
|
|
||||||
|
|
||||||
where "/path/to/generated/images/" is the value specified by the
|
|
||||||
``IMAGEKIT_CACHEFILE_DIR`` setting.
|
|
||||||
|
|
||||||
"""
|
|
||||||
source_filename = getattr(generator.source, 'name', None)
|
|
||||||
|
|
||||||
if source_filename is None or os.path.isabs(source_filename):
|
|
||||||
# Generally, we put the file right in the cache file directory.
|
|
||||||
dir = settings.IMAGEKIT_CACHEFILE_DIR
|
|
||||||
else:
|
|
||||||
# For source files with relative names (like Django media files),
|
|
||||||
# use the source's name to create the new filename.
|
|
||||||
dir = os.path.join(settings.IMAGEKIT_CACHEFILE_DIR,
|
|
||||||
os.path.splitext(source_filename)[0])
|
|
||||||
|
|
||||||
ext = suggest_extension(source_filename or '', generator.format)
|
|
||||||
return os.path.normpath(os.path.join(dir,
|
|
||||||
'%s%s' % (generator.get_hash(), ext)))
|
|
||||||
|
|
||||||
|
|
||||||
def source_name_dot_hash(generator):
|
|
||||||
"""
|
|
||||||
A namer that, given the following source file name::
|
|
||||||
|
|
||||||
photos/thumbnails/bulldog.jpg
|
|
||||||
|
|
||||||
will generate a name like this::
|
|
||||||
|
|
||||||
/path/to/generated/images/photos/thumbnails/bulldog.5ff3233527c5.jpg
|
|
||||||
|
|
||||||
where "/path/to/generated/images/" is the value specified by the
|
|
||||||
``IMAGEKIT_CACHEFILE_DIR`` setting.
|
|
||||||
|
|
||||||
"""
|
|
||||||
source_filename = getattr(generator.source, 'name', None)
|
|
||||||
|
|
||||||
if source_filename is None or os.path.isabs(source_filename):
|
|
||||||
# Generally, we put the file right in the cache file directory.
|
|
||||||
dir = settings.IMAGEKIT_CACHEFILE_DIR
|
|
||||||
else:
|
|
||||||
# For source files with relative names (like Django media files),
|
|
||||||
# use the source's name to create the new filename.
|
|
||||||
dir = os.path.join(settings.IMAGEKIT_CACHEFILE_DIR,
|
|
||||||
os.path.dirname(source_filename))
|
|
||||||
|
|
||||||
ext = suggest_extension(source_filename or '', generator.format)
|
|
||||||
basename = os.path.basename(source_filename)
|
|
||||||
return os.path.normpath(os.path.join(dir, '%s.%s%s' % (
|
|
||||||
os.path.splitext(basename)[0], generator.get_hash()[:12], ext)))
|
|
||||||
|
|
||||||
|
|
||||||
def hash(generator):
|
|
||||||
"""
|
|
||||||
A namer that, given the following source file name::
|
|
||||||
|
|
||||||
photos/thumbnails/bulldog.jpg
|
|
||||||
|
|
||||||
will generate a name like this::
|
|
||||||
|
|
||||||
/path/to/generated/images/5ff3233527c5ac3e4b596343b440ff67.jpg
|
|
||||||
|
|
||||||
where "/path/to/generated/images/" is the value specified by the
|
|
||||||
``IMAGEKIT_CACHEFILE_DIR`` setting.
|
|
||||||
|
|
||||||
"""
|
|
||||||
format = getattr(generator, 'format', None)
|
|
||||||
ext = format_to_extension(format) if format else ''
|
|
||||||
return os.path.normpath(os.path.join(settings.IMAGEKIT_CACHEFILE_DIR,
|
|
||||||
'%s%s' % (generator.get_hash(), ext)))
|
|
||||||
|
|
@ -1,49 +0,0 @@
|
||||||
import six
|
|
||||||
|
|
||||||
from django.utils.functional import LazyObject
|
|
||||||
from ..lib import force_text
|
|
||||||
from ..utils import get_singleton
|
|
||||||
|
|
||||||
|
|
||||||
class JustInTime(object):
|
|
||||||
"""
|
|
||||||
A strategy that ensures the file exists right before it's needed.
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
def on_existence_required(self, file):
|
|
||||||
file.generate()
|
|
||||||
|
|
||||||
def on_content_required(self, file):
|
|
||||||
file.generate()
|
|
||||||
|
|
||||||
|
|
||||||
class Optimistic(object):
|
|
||||||
"""
|
|
||||||
A strategy that acts immediately when the source file changes and assumes
|
|
||||||
that the cache files will not be removed (i.e. it doesn't ensure the
|
|
||||||
cache file exists when it's accessed).
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
def on_source_saved(self, file):
|
|
||||||
file.generate()
|
|
||||||
|
|
||||||
def should_verify_existence(self, file):
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
class DictStrategy(object):
|
|
||||||
def __init__(self, callbacks):
|
|
||||||
for k, v in callbacks.items():
|
|
||||||
setattr(self, k, v)
|
|
||||||
|
|
||||||
|
|
||||||
def load_strategy(strategy):
|
|
||||||
if isinstance(strategy, six.string_types):
|
|
||||||
strategy = get_singleton(strategy, 'cache file strategy')
|
|
||||||
elif isinstance(strategy, dict):
|
|
||||||
strategy = DictStrategy(strategy)
|
|
||||||
elif callable(strategy):
|
|
||||||
strategy = strategy()
|
|
||||||
return strategy
|
|
||||||
|
|
@ -1,161 +0,0 @@
|
||||||
# flake8: noqa
|
|
||||||
"""
|
|
||||||
This module contains code from django.template.base
|
|
||||||
(sha 90d3af380e8efec0301dd91600c6686232de3943). Bundling this code allows us to
|
|
||||||
support older versions of Django that did not contain it (< 1.4).
|
|
||||||
|
|
||||||
|
|
||||||
Copyright (c) Django Software Foundation and individual contributors.
|
|
||||||
All rights reserved.
|
|
||||||
|
|
||||||
Redistribution and use in source and binary forms, with or without modification,
|
|
||||||
are permitted provided that the following conditions are met:
|
|
||||||
|
|
||||||
1. Redistributions of source code must retain the above copyright notice,
|
|
||||||
this list of conditions and the following disclaimer.
|
|
||||||
|
|
||||||
2. Redistributions in binary form must reproduce the above copyright
|
|
||||||
notice, this list of conditions and the following disclaimer in the
|
|
||||||
documentation and/or other materials provided with the distribution.
|
|
||||||
|
|
||||||
3. Neither the name of Django nor the names of its contributors may be used
|
|
||||||
to endorse or promote products derived from this software without
|
|
||||||
specific prior written permission.
|
|
||||||
|
|
||||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
|
||||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
|
||||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
||||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
|
|
||||||
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
|
||||||
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
|
||||||
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
|
||||||
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
||||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
|
||||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
from django.template import TemplateSyntaxError
|
|
||||||
import re
|
|
||||||
|
|
||||||
|
|
||||||
# Regex for token keyword arguments
|
|
||||||
kwarg_re = re.compile(r"(?:(\w+)=)?(.+)")
|
|
||||||
|
|
||||||
|
|
||||||
def token_kwargs(bits, parser, support_legacy=False):
|
|
||||||
"""
|
|
||||||
A utility method for parsing token keyword arguments.
|
|
||||||
|
|
||||||
:param bits: A list containing remainder of the token (split by spaces)
|
|
||||||
that is to be checked for arguments. Valid arguments will be removed
|
|
||||||
from this list.
|
|
||||||
|
|
||||||
:param support_legacy: If set to true ``True``, the legacy format
|
|
||||||
``1 as foo`` will be accepted. Otherwise, only the standard ``foo=1``
|
|
||||||
format is allowed.
|
|
||||||
|
|
||||||
:returns: A dictionary of the arguments retrieved from the ``bits`` token
|
|
||||||
list.
|
|
||||||
|
|
||||||
There is no requirement for all remaining token ``bits`` to be keyword
|
|
||||||
arguments, so the dictionary will be returned as soon as an invalid
|
|
||||||
argument format is reached.
|
|
||||||
"""
|
|
||||||
if not bits:
|
|
||||||
return {}
|
|
||||||
match = kwarg_re.match(bits[0])
|
|
||||||
kwarg_format = match and match.group(1)
|
|
||||||
if not kwarg_format:
|
|
||||||
if not support_legacy:
|
|
||||||
return {}
|
|
||||||
if len(bits) < 3 or bits[1] != 'as':
|
|
||||||
return {}
|
|
||||||
|
|
||||||
kwargs = {}
|
|
||||||
while bits:
|
|
||||||
if kwarg_format:
|
|
||||||
match = kwarg_re.match(bits[0])
|
|
||||||
if not match or not match.group(1):
|
|
||||||
return kwargs
|
|
||||||
key, value = match.groups()
|
|
||||||
del bits[:1]
|
|
||||||
else:
|
|
||||||
if len(bits) < 3 or bits[1] != 'as':
|
|
||||||
return kwargs
|
|
||||||
key, value = bits[2], bits[0]
|
|
||||||
del bits[:3]
|
|
||||||
kwargs[key] = parser.compile_filter(value)
|
|
||||||
if bits and not kwarg_format:
|
|
||||||
if bits[0] != 'and':
|
|
||||||
return kwargs
|
|
||||||
del bits[:1]
|
|
||||||
return kwargs
|
|
||||||
|
|
||||||
|
|
||||||
def parse_bits(parser, bits, params, varargs, varkw, defaults,
|
|
||||||
takes_context, name):
|
|
||||||
"""
|
|
||||||
Parses bits for template tag helpers (simple_tag, include_tag and
|
|
||||||
assignment_tag), in particular by detecting syntax errors and by
|
|
||||||
extracting positional and keyword arguments.
|
|
||||||
"""
|
|
||||||
if takes_context:
|
|
||||||
if params[0] == 'context':
|
|
||||||
params = params[1:]
|
|
||||||
else:
|
|
||||||
raise TemplateSyntaxError(
|
|
||||||
"'%s' is decorated with takes_context=True so it must "
|
|
||||||
"have a first argument of 'context'" % name)
|
|
||||||
args = []
|
|
||||||
kwargs = {}
|
|
||||||
unhandled_params = list(params)
|
|
||||||
for bit in bits:
|
|
||||||
# First we try to extract a potential kwarg from the bit
|
|
||||||
kwarg = token_kwargs([bit], parser)
|
|
||||||
if kwarg:
|
|
||||||
# The kwarg was successfully extracted
|
|
||||||
param, value = list(kwarg.items())[0]
|
|
||||||
if param not in params and varkw is None:
|
|
||||||
# An unexpected keyword argument was supplied
|
|
||||||
raise TemplateSyntaxError(
|
|
||||||
"'%s' received unexpected keyword argument '%s'" %
|
|
||||||
(name, param))
|
|
||||||
elif param in kwargs:
|
|
||||||
# The keyword argument has already been supplied once
|
|
||||||
raise TemplateSyntaxError(
|
|
||||||
"'%s' received multiple values for keyword argument '%s'" %
|
|
||||||
(name, param))
|
|
||||||
else:
|
|
||||||
# All good, record the keyword argument
|
|
||||||
kwargs[str(param)] = value
|
|
||||||
if param in unhandled_params:
|
|
||||||
# If using the keyword syntax for a positional arg, then
|
|
||||||
# consume it.
|
|
||||||
unhandled_params.remove(param)
|
|
||||||
else:
|
|
||||||
if kwargs:
|
|
||||||
raise TemplateSyntaxError(
|
|
||||||
"'%s' received some positional argument(s) after some "
|
|
||||||
"keyword argument(s)" % name)
|
|
||||||
else:
|
|
||||||
# Record the positional argument
|
|
||||||
args.append(parser.compile_filter(bit))
|
|
||||||
try:
|
|
||||||
# Consume from the list of expected positional arguments
|
|
||||||
unhandled_params.pop(0)
|
|
||||||
except IndexError:
|
|
||||||
if varargs is None:
|
|
||||||
raise TemplateSyntaxError(
|
|
||||||
"'%s' received too many positional arguments" %
|
|
||||||
name)
|
|
||||||
if defaults is not None:
|
|
||||||
# Consider the last n params handled, where n is the
|
|
||||||
# number of defaults.
|
|
||||||
unhandled_params = unhandled_params[:-len(defaults)]
|
|
||||||
if unhandled_params:
|
|
||||||
# Some positional arguments were not supplied
|
|
||||||
raise TemplateSyntaxError(
|
|
||||||
"'%s' did not receive value(s) for the argument(s): %s" %
|
|
||||||
(name, ", ".join(["'%s'" % p for p in unhandled_params])))
|
|
||||||
return args, kwargs
|
|
||||||
|
|
@ -1,40 +0,0 @@
|
||||||
from appconf import AppConf
|
|
||||||
from django.conf import settings
|
|
||||||
from django.core.exceptions import ImproperlyConfigured
|
|
||||||
|
|
||||||
|
|
||||||
class ImageKitConf(AppConf):
|
|
||||||
CACHEFILE_NAMER = 'imagekit.cachefiles.namers.hash'
|
|
||||||
SPEC_CACHEFILE_NAMER = 'imagekit.cachefiles.namers.source_name_as_path'
|
|
||||||
CACHEFILE_DIR = 'CACHE/images'
|
|
||||||
DEFAULT_CACHEFILE_BACKEND = 'imagekit.cachefiles.backends.Simple'
|
|
||||||
DEFAULT_CACHEFILE_STRATEGY = 'imagekit.cachefiles.strategies.JustInTime'
|
|
||||||
|
|
||||||
DEFAULT_FILE_STORAGE = None
|
|
||||||
|
|
||||||
CACHE_BACKEND = None
|
|
||||||
CACHE_PREFIX = 'imagekit:'
|
|
||||||
CACHE_TIMEOUT = None
|
|
||||||
USE_MEMCACHED_SAFE_CACHE_KEY = True
|
|
||||||
|
|
||||||
def configure_cache_backend(self, value):
|
|
||||||
if value is None:
|
|
||||||
from django.core.cache import DEFAULT_CACHE_ALIAS
|
|
||||||
return DEFAULT_CACHE_ALIAS
|
|
||||||
|
|
||||||
if value not in settings.CACHES:
|
|
||||||
raise ImproperlyConfigured("{0} is not present in settings.CACHES".format(value))
|
|
||||||
|
|
||||||
return value
|
|
||||||
|
|
||||||
def configure_cache_timeout(self, value):
|
|
||||||
if value is None and settings.DEBUG:
|
|
||||||
# If value is not configured and is DEBUG set it to 5 minutes
|
|
||||||
return 300
|
|
||||||
# Otherwise leave it as is. If it is None then valies will never expire
|
|
||||||
return value
|
|
||||||
|
|
||||||
def configure_default_file_storage(self, value):
|
|
||||||
if value is None:
|
|
||||||
value = settings.DEFAULT_FILE_STORAGE
|
|
||||||
return value
|
|
||||||
25
imagekit/defaults.py
Normal file
25
imagekit/defaults.py
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
""" Default ImageKit configuration """
|
||||||
|
|
||||||
|
from imagekit.specs import ImageSpec
|
||||||
|
from imagekit import processors
|
||||||
|
|
||||||
|
class ResizeThumbnail(processors.Resize):
|
||||||
|
width = 100
|
||||||
|
height = 50
|
||||||
|
crop = True
|
||||||
|
|
||||||
|
class EnhanceSmall(processors.Adjustment):
|
||||||
|
contrast = 1.2
|
||||||
|
sharpness = 1.1
|
||||||
|
|
||||||
|
class SampleReflection(processors.Reflection):
|
||||||
|
size = 0.5
|
||||||
|
background_color = "#000000"
|
||||||
|
|
||||||
|
class PNGFormat(processors.Format):
|
||||||
|
format = 'PNG'
|
||||||
|
extension = 'png'
|
||||||
|
|
||||||
|
class DjangoAdminThumbnail(ImageSpec):
|
||||||
|
access_as = 'admin_thumbnail'
|
||||||
|
processors = [ResizeThumbnail, EnhanceSmall, SampleReflection, PNGFormat]
|
||||||
|
|
@ -1,22 +0,0 @@
|
||||||
from pilkit.exceptions import UnknownExtension, UnknownFormat
|
|
||||||
|
|
||||||
|
|
||||||
class AlreadyRegistered(Exception):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class NotRegistered(Exception):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class MissingGeneratorId(Exception):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class MissingSource(ValueError):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
# Aliases for backwards compatibility
|
|
||||||
UnknownExtensionError = UnknownExtension
|
|
||||||
UnknownFormatError = UnknownFormat
|
|
||||||
|
|
@ -1,110 +0,0 @@
|
||||||
from __future__ import unicode_literals
|
|
||||||
import os
|
|
||||||
|
|
||||||
from django.core.files.base import File, ContentFile
|
|
||||||
from django.utils.encoding import smart_str
|
|
||||||
from .lib import smart_text
|
|
||||||
from .utils import format_to_mimetype, extension_to_mimetype
|
|
||||||
|
|
||||||
|
|
||||||
class BaseIKFile(File):
|
|
||||||
"""
|
|
||||||
This class contains all of the methods we need from
|
|
||||||
django.db.models.fields.files.FieldFile, but with the model stuff ripped
|
|
||||||
out. It's only extended by one class, but we keep it separate for
|
|
||||||
organizational reasons.
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, storage):
|
|
||||||
self.storage = storage
|
|
||||||
|
|
||||||
def _require_file(self):
|
|
||||||
if not self:
|
|
||||||
raise ValueError()
|
|
||||||
|
|
||||||
def _get_file(self):
|
|
||||||
self._require_file()
|
|
||||||
if not hasattr(self, '_file') or self._file is None:
|
|
||||||
self._file = self.storage.open(self.name, 'rb')
|
|
||||||
return self._file
|
|
||||||
|
|
||||||
def _set_file(self, file):
|
|
||||||
self._file = file
|
|
||||||
|
|
||||||
def _del_file(self):
|
|
||||||
del self._file
|
|
||||||
|
|
||||||
file = property(_get_file, _set_file, _del_file)
|
|
||||||
|
|
||||||
def _get_path(self):
|
|
||||||
self._require_file()
|
|
||||||
return self.storage.path(self.name)
|
|
||||||
path = property(_get_path)
|
|
||||||
|
|
||||||
def _get_url(self):
|
|
||||||
self._require_file()
|
|
||||||
return self.storage.url(self.name)
|
|
||||||
url = property(_get_url)
|
|
||||||
|
|
||||||
def _get_size(self):
|
|
||||||
self._require_file()
|
|
||||||
if not getattr(self, '_committed', False):
|
|
||||||
return self.file.size
|
|
||||||
return self.storage.size(self.name)
|
|
||||||
size = property(_get_size)
|
|
||||||
|
|
||||||
def open(self, mode='rb'):
|
|
||||||
self._require_file()
|
|
||||||
try:
|
|
||||||
self.file.open(mode)
|
|
||||||
except ValueError:
|
|
||||||
# if the underlaying file can't be reopened
|
|
||||||
# then we will use the storage to try to open it again
|
|
||||||
if self.file.closed:
|
|
||||||
# clear cached file instance
|
|
||||||
del self.file
|
|
||||||
# Because file is a property we can acces it after
|
|
||||||
# we deleted it
|
|
||||||
return self.file.open(mode)
|
|
||||||
raise
|
|
||||||
|
|
||||||
def _get_closed(self):
|
|
||||||
file = getattr(self, '_file', None)
|
|
||||||
return file is None or file.closed
|
|
||||||
closed = property(_get_closed)
|
|
||||||
|
|
||||||
def close(self):
|
|
||||||
file = getattr(self, '_file', None)
|
|
||||||
if file is not None:
|
|
||||||
file.close()
|
|
||||||
|
|
||||||
|
|
||||||
class IKContentFile(ContentFile):
|
|
||||||
"""
|
|
||||||
Wraps a ContentFile in a file-like object with a filename and a
|
|
||||||
content_type. A PIL image format can be optionally be provided as a content
|
|
||||||
type hint.
|
|
||||||
|
|
||||||
"""
|
|
||||||
def __init__(self, filename, content, format=None):
|
|
||||||
self.file = ContentFile(content)
|
|
||||||
self.file.name = filename
|
|
||||||
mimetype = getattr(self.file, 'content_type', None)
|
|
||||||
if format and not mimetype:
|
|
||||||
mimetype = format_to_mimetype(format)
|
|
||||||
if not mimetype:
|
|
||||||
ext = os.path.splitext(filename or '')[1]
|
|
||||||
mimetype = extension_to_mimetype(ext)
|
|
||||||
self.file.content_type = mimetype
|
|
||||||
|
|
||||||
@property
|
|
||||||
def name(self):
|
|
||||||
return self.file.name
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return smart_str(self.file.name or '')
|
|
||||||
|
|
||||||
def __unicode__(self):
|
|
||||||
# Python 2
|
|
||||||
return smart_text(self.file.name or '')
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
# flake8: noqa
|
|
||||||
|
|
||||||
from .fields import ProcessedImageField
|
|
||||||
|
|
@ -1,33 +0,0 @@
|
||||||
from django.forms import ImageField
|
|
||||||
from ..specs import SpecHost
|
|
||||||
from ..utils import generate
|
|
||||||
|
|
||||||
|
|
||||||
class ProcessedImageField(ImageField, SpecHost):
|
|
||||||
|
|
||||||
def __init__(self, processors=None, format=None, options=None,
|
|
||||||
autoconvert=True, spec_id=None, spec=None, *args, **kwargs):
|
|
||||||
|
|
||||||
if spec_id is None:
|
|
||||||
# Unlike model fields, form fields are never told their field name.
|
|
||||||
# (Model fields are done so via `contribute_to_class()`.) Therefore
|
|
||||||
# we can't really generate a good spec id automatically.
|
|
||||||
raise TypeError('You must provide a spec_id')
|
|
||||||
|
|
||||||
SpecHost.__init__(self, processors=processors, format=format,
|
|
||||||
options=options, autoconvert=autoconvert, spec=spec,
|
|
||||||
spec_id=spec_id)
|
|
||||||
super(ProcessedImageField, self).__init__(*args, **kwargs)
|
|
||||||
|
|
||||||
def clean(self, data, initial=None):
|
|
||||||
data = super(ProcessedImageField, self).clean(data, initial)
|
|
||||||
|
|
||||||
if data and data != initial:
|
|
||||||
spec = self.get_spec(source=data)
|
|
||||||
f = generate(spec)
|
|
||||||
# Name is required in Django 1.4. When we drop support for it
|
|
||||||
# then we can dirrectly return the result from `generate(spec)`
|
|
||||||
f.name = data.name
|
|
||||||
return f
|
|
||||||
|
|
||||||
return data
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
from .registry import register
|
|
||||||
from .processors import Thumbnail as ThumbnailProcessor
|
|
||||||
from .specs import ImageSpec
|
|
||||||
|
|
||||||
|
|
||||||
class Thumbnail(ImageSpec):
|
|
||||||
def __init__(self, width=None, height=None, anchor=None, crop=None, upscale=None, **kwargs):
|
|
||||||
self.processors = [ThumbnailProcessor(width, height, anchor=anchor,
|
|
||||||
crop=crop, upscale=upscale)]
|
|
||||||
super(Thumbnail, self).__init__(**kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
register.generator('imagekit:thumbnail', Thumbnail)
|
|
||||||
|
|
@ -1,35 +0,0 @@
|
||||||
from copy import copy
|
|
||||||
from hashlib import md5
|
|
||||||
from pickle import MARK, DICT
|
|
||||||
try:
|
|
||||||
from pickle import _Pickler
|
|
||||||
except ImportError:
|
|
||||||
# Python 2 compatible
|
|
||||||
from pickle import Pickler as _Pickler
|
|
||||||
from .lib import StringIO
|
|
||||||
|
|
||||||
|
|
||||||
class CanonicalizingPickler(_Pickler):
|
|
||||||
dispatch = copy(_Pickler.dispatch)
|
|
||||||
|
|
||||||
def save_set(self, obj):
|
|
||||||
rv = obj.__reduce_ex__(0)
|
|
||||||
rv = (rv[0], (sorted(rv[1][0]),), rv[2])
|
|
||||||
self.save_reduce(obj=obj, *rv)
|
|
||||||
|
|
||||||
dispatch[set] = save_set
|
|
||||||
|
|
||||||
def save_dict(self, obj):
|
|
||||||
write = self.write
|
|
||||||
write(MARK + DICT)
|
|
||||||
|
|
||||||
self.memoize(obj)
|
|
||||||
self._batch_setitems(sorted(obj.items()))
|
|
||||||
|
|
||||||
dispatch[dict] = save_dict
|
|
||||||
|
|
||||||
|
|
||||||
def pickle(obj):
|
|
||||||
file = StringIO()
|
|
||||||
CanonicalizingPickler(file, 0).dump(obj)
|
|
||||||
return md5(file.getvalue()).hexdigest()
|
|
||||||
|
|
@ -1,52 +1,17 @@
|
||||||
# flake8: noqa
|
|
||||||
|
|
||||||
# Required PIL classes may or may not be available from the root namespace
|
# Required PIL classes may or may not be available from the root namespace
|
||||||
# depending on the installation method used.
|
# depending on the installation method used.
|
||||||
try:
|
try:
|
||||||
from PIL import Image, ImageColor, ImageChops, ImageEnhance, ImageFile, \
|
import Image
|
||||||
ImageFilter, ImageDraw, ImageStat
|
import ImageFile
|
||||||
|
import ImageFilter
|
||||||
|
import ImageEnhance
|
||||||
|
import ImageColor
|
||||||
except ImportError:
|
except ImportError:
|
||||||
try:
|
try:
|
||||||
import Image
|
from PIL import Image
|
||||||
import ImageColor
|
from PIL import ImageFile
|
||||||
import ImageChops
|
from PIL import ImageFilter
|
||||||
import ImageEnhance
|
from PIL import ImageEnhance
|
||||||
import ImageFile
|
from PIL import ImageColor
|
||||||
import ImageFilter
|
|
||||||
import ImageDraw
|
|
||||||
import ImageStat
|
|
||||||
except ImportError:
|
except ImportError:
|
||||||
raise ImportError('ImageKit was unable to import the Python Imaging Library. Please confirm it`s installed and available on your current Python path.')
|
raise ImportError('ImageKit was unable to import the Python Imaging Library. Please confirm it`s installed and available on your current Python path.')
|
||||||
|
|
||||||
try:
|
|
||||||
from io import BytesIO as StringIO
|
|
||||||
except:
|
|
||||||
try:
|
|
||||||
from cStringIO import StringIO
|
|
||||||
except ImportError:
|
|
||||||
from StringIO import StringIO
|
|
||||||
|
|
||||||
try:
|
|
||||||
from logging import NullHandler
|
|
||||||
except ImportError:
|
|
||||||
from logging import Handler
|
|
||||||
|
|
||||||
class NullHandler(Handler):
|
|
||||||
def emit(self, record):
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Try to import `force_text` available from Django 1.5
|
|
||||||
# This function will replace `unicode` used in the code
|
|
||||||
# If Django version is under 1.5 then use `force_unicde`
|
|
||||||
# It is used for compatibility between Python 2 and Python 3
|
|
||||||
try:
|
|
||||||
from django.utils.encoding import force_text, force_bytes, smart_text
|
|
||||||
except ImportError:
|
|
||||||
# Django < 1.5
|
|
||||||
from django.utils.encoding import (force_unicode as force_text,
|
|
||||||
smart_str as force_bytes,
|
|
||||||
smart_unicode as smart_text)
|
|
||||||
|
|
||||||
__all__ = ['Image', 'ImageColor', 'ImageChops', 'ImageEnhance', 'ImageFile',
|
|
||||||
'ImageFilter', 'ImageDraw', 'ImageStat', 'StringIO', 'NullHandler',
|
|
||||||
'force_text', 'force_bytes', 'smart_text']
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
|
||||||
|
|
@ -1,53 +0,0 @@
|
||||||
from django.core.management.base import BaseCommand
|
|
||||||
import re
|
|
||||||
from ...registry import generator_registry, cachefile_registry
|
|
||||||
from ...exceptions import MissingSource
|
|
||||||
|
|
||||||
|
|
||||||
class Command(BaseCommand):
|
|
||||||
help = ("""Generate files for the specified image generators (or all of them if
|
|
||||||
none was provided). Simple, glob-like wildcards are allowed, with *
|
|
||||||
matching all characters within a segment, and ** matching across
|
|
||||||
segments. (Segments are separated with colons.) So, for example,
|
|
||||||
"a:*:c" will match "a:b:c", but not "a:b:x:c", whereas "a:**:c" will
|
|
||||||
match both. Subsegments are always matched, so "a" will match "a" as
|
|
||||||
well as "a:b" and "a:b:c".""")
|
|
||||||
args = '[generator_ids]'
|
|
||||||
|
|
||||||
def add_arguments(self, parser):
|
|
||||||
parser.add_argument('generator_id', nargs='*', help='<app_name>:<model>:<field> for model specs')
|
|
||||||
|
|
||||||
def handle(self, *args, **options):
|
|
||||||
generators = generator_registry.get_ids()
|
|
||||||
|
|
||||||
generator_ids = options['generator_id'] if 'generator_id' in options else args
|
|
||||||
if generator_ids:
|
|
||||||
patterns = self.compile_patterns(generator_ids)
|
|
||||||
generators = (id for id in generators if any(p.match(id) for p in patterns))
|
|
||||||
|
|
||||||
for generator_id in generators:
|
|
||||||
self.stdout.write('Validating generator: %s\n' % generator_id)
|
|
||||||
for image_file in cachefile_registry.get(generator_id):
|
|
||||||
if image_file.name:
|
|
||||||
self.stdout.write(' %s\n' % image_file.name)
|
|
||||||
try:
|
|
||||||
image_file.generate()
|
|
||||||
except MissingSource as err:
|
|
||||||
self.stdout.write('\t No source associated with\n')
|
|
||||||
except Exception as err:
|
|
||||||
self.stdout.write('\tFailed %s\n' % (err))
|
|
||||||
|
|
||||||
def compile_patterns(self, generator_ids):
|
|
||||||
return [self.compile_pattern(id) for id in generator_ids]
|
|
||||||
|
|
||||||
def compile_pattern(self, generator_id):
|
|
||||||
parts = re.split(r'(\*{1,2})', generator_id)
|
|
||||||
pattern = ''
|
|
||||||
for part in parts:
|
|
||||||
if part == '*':
|
|
||||||
pattern += '[^:]*'
|
|
||||||
elif part == '**':
|
|
||||||
pattern += '.*'
|
|
||||||
else:
|
|
||||||
pattern += re.escape(part)
|
|
||||||
return re.compile('^%s(:.*)?$' % pattern)
|
|
||||||
36
imagekit/management/commands/ikflush.py
Normal file
36
imagekit/management/commands/ikflush.py
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
from django.db.models.loading import cache
|
||||||
|
from django.core.management.base import BaseCommand, CommandError
|
||||||
|
from optparse import make_option
|
||||||
|
from imagekit.models import ImageModel
|
||||||
|
from imagekit.specs import ImageSpec
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = ('Clears all ImageKit cached files.')
|
||||||
|
args = '[apps]'
|
||||||
|
requires_model_validation = True
|
||||||
|
can_import_settings = True
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
return flush_cache(args, options)
|
||||||
|
|
||||||
|
def flush_cache(apps, options):
|
||||||
|
""" Clears the image cache
|
||||||
|
|
||||||
|
"""
|
||||||
|
apps = [a.strip(',') for a in apps]
|
||||||
|
if apps:
|
||||||
|
for app_label in apps:
|
||||||
|
app = cache.get_app(app_label)
|
||||||
|
models = [m for m in cache.get_models(app) if issubclass(m, ImageModel)]
|
||||||
|
for model in models:
|
||||||
|
print 'Flushing cache for "%s.%s"' % (app_label, model.__name__)
|
||||||
|
for obj in model.objects.order_by('-id'):
|
||||||
|
for spec in model._ik.specs:
|
||||||
|
prop = getattr(obj, spec.name(), None)
|
||||||
|
if prop is not None:
|
||||||
|
prop._delete()
|
||||||
|
if spec.pre_cache:
|
||||||
|
prop._create()
|
||||||
|
else:
|
||||||
|
print 'Please specify on or more app names'
|
||||||
156
imagekit/models.py
Normal file
156
imagekit/models.py
Normal file
|
|
@ -0,0 +1,156 @@
|
||||||
|
import os
|
||||||
|
from datetime import datetime
|
||||||
|
from django.conf import settings
|
||||||
|
from django.core.files.base import ContentFile
|
||||||
|
from django.db import models
|
||||||
|
from django.db.models.base import ModelBase
|
||||||
|
from django.db.models.signals import post_delete
|
||||||
|
from django.utils.html import conditional_escape as escape
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
|
from imagekit import specs
|
||||||
|
from imagekit.lib import *
|
||||||
|
from imagekit.options import Options
|
||||||
|
from imagekit.utils import img_to_fobj
|
||||||
|
|
||||||
|
# Modify image file buffer size.
|
||||||
|
ImageFile.MAXBLOCK = getattr(settings, 'PIL_IMAGEFILE_MAXBLOCK', 256 * 2 ** 10)
|
||||||
|
|
||||||
|
# Choice tuples for specifying the crop origin.
|
||||||
|
# These are provided for convenience.
|
||||||
|
CROP_HORZ_CHOICES = (
|
||||||
|
(0, _('left')),
|
||||||
|
(1, _('center')),
|
||||||
|
(2, _('right')),
|
||||||
|
)
|
||||||
|
|
||||||
|
CROP_VERT_CHOICES = (
|
||||||
|
(0, _('top')),
|
||||||
|
(1, _('center')),
|
||||||
|
(2, _('bottom')),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ImageModelBase(ModelBase):
|
||||||
|
""" ImageModel metaclass
|
||||||
|
|
||||||
|
This metaclass parses IKOptions and loads the specified specification
|
||||||
|
module.
|
||||||
|
|
||||||
|
"""
|
||||||
|
def __init__(cls, name, bases, attrs):
|
||||||
|
parents = [b for b in bases if isinstance(b, ImageModelBase)]
|
||||||
|
if not parents:
|
||||||
|
return
|
||||||
|
user_opts = getattr(cls, 'IKOptions', None)
|
||||||
|
opts = Options(user_opts)
|
||||||
|
try:
|
||||||
|
module = __import__(opts.spec_module, {}, {}, [''])
|
||||||
|
except ImportError:
|
||||||
|
raise ImportError('Unable to load imagekit config module: %s' % \
|
||||||
|
opts.spec_module)
|
||||||
|
for spec in [spec for spec in module.__dict__.values() \
|
||||||
|
if isinstance(spec, type) \
|
||||||
|
and issubclass(spec, specs.ImageSpec) \
|
||||||
|
and spec != specs.ImageSpec]:
|
||||||
|
setattr(cls, spec.name(), specs.Descriptor(spec))
|
||||||
|
opts.specs.append(spec)
|
||||||
|
setattr(cls, '_ik', opts)
|
||||||
|
|
||||||
|
|
||||||
|
class ImageModel(models.Model):
|
||||||
|
""" Abstract base class implementing all core ImageKit functionality
|
||||||
|
|
||||||
|
Subclasses of ImageModel are augmented with accessors for each defined
|
||||||
|
image specification and can override the inner IKOptions class to customize
|
||||||
|
storage locations and other options.
|
||||||
|
|
||||||
|
"""
|
||||||
|
__metaclass__ = ImageModelBase
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
abstract = True
|
||||||
|
|
||||||
|
class IKOptions:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def admin_thumbnail_view(self):
|
||||||
|
if not self._imgfield:
|
||||||
|
return None
|
||||||
|
prop = getattr(self, self._ik.admin_thumbnail_spec, None)
|
||||||
|
if prop is None:
|
||||||
|
return 'An "%s" image spec has not been defined.' % \
|
||||||
|
self._ik.admin_thumbnail_spec
|
||||||
|
else:
|
||||||
|
if hasattr(self, 'get_absolute_url'):
|
||||||
|
return u'<a href="%s"><img src="%s"></a>' % \
|
||||||
|
(escape(self.get_absolute_url()), escape(prop.url))
|
||||||
|
else:
|
||||||
|
return u'<a href="%s"><img src="%s"></a>' % \
|
||||||
|
(escape(self._imgfield.url), escape(prop.url))
|
||||||
|
admin_thumbnail_view.short_description = _('Thumbnail')
|
||||||
|
admin_thumbnail_view.allow_tags = True
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _imgfield(self):
|
||||||
|
return getattr(self, self._ik.image_field)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _storage(self):
|
||||||
|
return getattr(self._ik, 'storage', self._imgfield.storage)
|
||||||
|
|
||||||
|
def _clear_cache(self):
|
||||||
|
for spec in self._ik.specs:
|
||||||
|
prop = getattr(self, spec.name())
|
||||||
|
prop._delete()
|
||||||
|
|
||||||
|
def _pre_cache(self):
|
||||||
|
for spec in self._ik.specs:
|
||||||
|
if spec.pre_cache:
|
||||||
|
prop = getattr(self, spec.name())
|
||||||
|
prop._create()
|
||||||
|
|
||||||
|
def save_image(self, name, image, save=True, replace=True):
|
||||||
|
if self._imgfield and replace:
|
||||||
|
self._imgfield.delete(save=False)
|
||||||
|
if hasattr(image, 'read'):
|
||||||
|
data = image.read()
|
||||||
|
else:
|
||||||
|
data = image
|
||||||
|
content = ContentFile(data)
|
||||||
|
self._imgfield.save(name, content, save)
|
||||||
|
|
||||||
|
def save(self, clear_cache=True, *args, **kwargs):
|
||||||
|
is_new_object = self._get_pk_val() is None
|
||||||
|
super(ImageModel, self).save(*args, **kwargs)
|
||||||
|
if is_new_object and self._imgfield:
|
||||||
|
clear_cache = False
|
||||||
|
spec = self._ik.preprocessor_spec
|
||||||
|
if spec is not None:
|
||||||
|
newfile = self._imgfield.storage.open(str(self._imgfield))
|
||||||
|
img = Image.open(newfile)
|
||||||
|
img, format = spec.process(img, self)
|
||||||
|
if format != 'JPEG':
|
||||||
|
imgfile = img_to_fobj(img, format)
|
||||||
|
else:
|
||||||
|
imgfile = img_to_fobj(img, format,
|
||||||
|
quality=int(spec.quality),
|
||||||
|
optimize=True)
|
||||||
|
content = ContentFile(imgfile.read())
|
||||||
|
newfile.close()
|
||||||
|
name = str(self._imgfield)
|
||||||
|
self._imgfield.storage.delete(name)
|
||||||
|
self._imgfield.storage.save(name, content)
|
||||||
|
if self._imgfield:
|
||||||
|
if clear_cache:
|
||||||
|
self._clear_cache()
|
||||||
|
self._pre_cache()
|
||||||
|
|
||||||
|
def clear_cache(self, **kwargs):
|
||||||
|
assert self._get_pk_val() is not None, "%s object can't be deleted because its %s attribute is set to None." % (self._meta.object_name, self._meta.pk.attname)
|
||||||
|
self._clear_cache()
|
||||||
|
|
||||||
|
|
||||||
|
post_delete.connect(ImageModel.clear_cache, sender=ImageModel)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1,4 +0,0 @@
|
||||||
# flake8: noqa
|
|
||||||
|
|
||||||
from .. import conf
|
|
||||||
from .fields import ImageSpecField, ProcessedImageField
|
|
||||||
|
|
@ -1,126 +0,0 @@
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from django.conf import settings
|
|
||||||
from django.db import models
|
|
||||||
from django.db.models.signals import class_prepared
|
|
||||||
from .files import ProcessedImageFieldFile
|
|
||||||
from .utils import ImageSpecFileDescriptor
|
|
||||||
from ...specs import SpecHost
|
|
||||||
from ...specs.sourcegroups import ImageFieldSourceGroup
|
|
||||||
from ...registry import register
|
|
||||||
|
|
||||||
|
|
||||||
class SpecHostField(SpecHost):
|
|
||||||
def _set_spec_id(self, cls, name):
|
|
||||||
spec_id = getattr(self, 'spec_id', None)
|
|
||||||
|
|
||||||
# Generate a spec_id to register the spec with. The default spec id is
|
|
||||||
# "<app>:<model>_<field>"
|
|
||||||
if not spec_id:
|
|
||||||
spec_id = ('%s:%s:%s' % (cls._meta.app_label,
|
|
||||||
cls._meta.object_name, name)).lower()
|
|
||||||
|
|
||||||
# Register the spec with the id. This allows specs to be overridden
|
|
||||||
# later, from outside of the model definition.
|
|
||||||
super(SpecHostField, self).set_spec_id(spec_id)
|
|
||||||
|
|
||||||
|
|
||||||
class ImageSpecField(SpecHostField):
|
|
||||||
"""
|
|
||||||
The heart and soul of the ImageKit library, ImageSpecField allows you to add
|
|
||||||
variants of uploaded images to your models.
|
|
||||||
|
|
||||||
"""
|
|
||||||
def __init__(self, processors=None, format=None, options=None,
|
|
||||||
source=None, cachefile_storage=None, autoconvert=None,
|
|
||||||
cachefile_backend=None, cachefile_strategy=None, spec=None,
|
|
||||||
id=None):
|
|
||||||
|
|
||||||
SpecHost.__init__(self, processors=processors, format=format,
|
|
||||||
options=options, cachefile_storage=cachefile_storage,
|
|
||||||
autoconvert=autoconvert,
|
|
||||||
cachefile_backend=cachefile_backend,
|
|
||||||
cachefile_strategy=cachefile_strategy, spec=spec,
|
|
||||||
spec_id=id)
|
|
||||||
|
|
||||||
# TODO: Allow callable for source. See https://github.com/matthewwithanm/django-imagekit/issues/158#issuecomment-10921664
|
|
||||||
self.source = source
|
|
||||||
|
|
||||||
def contribute_to_class(self, cls, name):
|
|
||||||
# If the source field name isn't defined, figure it out.
|
|
||||||
|
|
||||||
def register_source_group(source):
|
|
||||||
setattr(cls, name, ImageSpecFileDescriptor(self, name, source))
|
|
||||||
self._set_spec_id(cls, name)
|
|
||||||
|
|
||||||
# Add the model and field as a source for this spec id
|
|
||||||
register.source_group(self.spec_id, ImageFieldSourceGroup(cls, source))
|
|
||||||
|
|
||||||
if self.source:
|
|
||||||
register_source_group(self.source)
|
|
||||||
else:
|
|
||||||
# The source argument is not defined
|
|
||||||
# Then we need to see if there is only one ImageField in that model
|
|
||||||
# But we need to do that after full model initialization
|
|
||||||
def handle_model_preparation(sender, **kwargs):
|
|
||||||
|
|
||||||
image_fields = [f.attname for f in cls._meta.fields if
|
|
||||||
isinstance(f, models.ImageField)]
|
|
||||||
if len(image_fields) == 0:
|
|
||||||
raise Exception(
|
|
||||||
'%s does not define any ImageFields, so your %s'
|
|
||||||
' ImageSpecField has no image to act on.' %
|
|
||||||
(cls.__name__, name))
|
|
||||||
elif len(image_fields) > 1:
|
|
||||||
raise Exception(
|
|
||||||
'%s defines multiple ImageFields, but you have not'
|
|
||||||
' specified a source for your %s ImageSpecField.' %
|
|
||||||
(cls.__name__, name))
|
|
||||||
register_source_group(image_fields[0])
|
|
||||||
|
|
||||||
class_prepared.connect(handle_model_preparation, sender=cls, weak=False)
|
|
||||||
|
|
||||||
|
|
||||||
class ProcessedImageField(models.ImageField, SpecHostField):
|
|
||||||
"""
|
|
||||||
ProcessedImageField is an ImageField that runs processors on the uploaded
|
|
||||||
image *before* saving it to storage. This is in contrast to specs, which
|
|
||||||
maintain the original. Useful for coercing fileformats or keeping images
|
|
||||||
within a reasonable size.
|
|
||||||
|
|
||||||
"""
|
|
||||||
attr_class = ProcessedImageFieldFile
|
|
||||||
|
|
||||||
def __init__(self, processors=None, format=None, options=None,
|
|
||||||
verbose_name=None, name=None, width_field=None, height_field=None,
|
|
||||||
autoconvert=None, spec=None, spec_id=None, **kwargs):
|
|
||||||
"""
|
|
||||||
The ProcessedImageField constructor accepts all of the arguments that
|
|
||||||
the :class:`django.db.models.ImageField` constructor accepts, as well
|
|
||||||
as the ``processors``, ``format``, and ``options`` arguments of
|
|
||||||
:class:`imagekit.models.ImageSpecField`.
|
|
||||||
|
|
||||||
"""
|
|
||||||
# if spec is not provided then autoconvert will be True by default
|
|
||||||
if spec is None and autoconvert is None:
|
|
||||||
autoconvert = True
|
|
||||||
|
|
||||||
SpecHost.__init__(self, processors=processors, format=format,
|
|
||||||
options=options, autoconvert=autoconvert, spec=spec,
|
|
||||||
spec_id=spec_id)
|
|
||||||
models.ImageField.__init__(self, verbose_name, name, width_field,
|
|
||||||
height_field, **kwargs)
|
|
||||||
|
|
||||||
def contribute_to_class(self, cls, name):
|
|
||||||
self._set_spec_id(cls, name)
|
|
||||||
return super(ProcessedImageField, self).contribute_to_class(cls, name)
|
|
||||||
|
|
||||||
|
|
||||||
# If the project does not use south, then we will not try to add introspection
|
|
||||||
if 'south' in settings.INSTALLED_APPS:
|
|
||||||
try:
|
|
||||||
from south.modelsinspector import add_introspection_rules
|
|
||||||
except ImportError:
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
add_introspection_rules([], [r'^imagekit\.models\.fields\.ProcessedImageField$'])
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
from django.db.models.fields.files import ImageFieldFile
|
|
||||||
import os
|
|
||||||
from ...utils import suggest_extension, generate
|
|
||||||
|
|
||||||
|
|
||||||
class ProcessedImageFieldFile(ImageFieldFile):
|
|
||||||
def save(self, name, content, save=True):
|
|
||||||
filename, ext = os.path.splitext(name)
|
|
||||||
spec = self.field.get_spec(source=content)
|
|
||||||
ext = suggest_extension(name, spec.format)
|
|
||||||
new_name = '%s%s' % (filename, ext)
|
|
||||||
content = generate(spec)
|
|
||||||
return super(ProcessedImageFieldFile, self).save(new_name, content, save)
|
|
||||||
|
|
@ -1,21 +0,0 @@
|
||||||
from ...cachefiles import ImageCacheFile
|
|
||||||
|
|
||||||
|
|
||||||
class ImageSpecFileDescriptor(object):
|
|
||||||
def __init__(self, field, attname, source_field_name):
|
|
||||||
self.attname = attname
|
|
||||||
self.field = field
|
|
||||||
self.source_field_name = source_field_name
|
|
||||||
|
|
||||||
def __get__(self, instance, owner):
|
|
||||||
if instance is None:
|
|
||||||
return self.field
|
|
||||||
else:
|
|
||||||
source = getattr(instance, self.source_field_name)
|
|
||||||
spec = self.field.get_spec(source=source)
|
|
||||||
file = ImageCacheFile(spec)
|
|
||||||
instance.__dict__[self.attname] = file
|
|
||||||
return file
|
|
||||||
|
|
||||||
def __set__(self, instance, value):
|
|
||||||
instance.__dict__[self.attname] = value
|
|
||||||
24
imagekit/options.py
Normal file
24
imagekit/options.py
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
# Imagekit options
|
||||||
|
from imagekit import processors
|
||||||
|
from imagekit.specs import ImageSpec
|
||||||
|
|
||||||
|
|
||||||
|
class Options(object):
|
||||||
|
""" Class handling per-model imagekit options
|
||||||
|
|
||||||
|
"""
|
||||||
|
image_field = 'image'
|
||||||
|
crop_horz_field = 'crop_horz'
|
||||||
|
crop_vert_field = 'crop_vert'
|
||||||
|
preprocessor_spec = None
|
||||||
|
cache_dir = 'cache'
|
||||||
|
save_count_as = None
|
||||||
|
cache_filename_format = "%(filename)s_%(specname)s.%(extension)s"
|
||||||
|
admin_thumbnail_spec = 'admin_thumbnail'
|
||||||
|
spec_module = 'imagekit.defaults'
|
||||||
|
#storage = defaults to image_field.storage
|
||||||
|
|
||||||
|
def __init__(self, opts):
|
||||||
|
for key, value in opts.__dict__.iteritems():
|
||||||
|
setattr(self, key, value)
|
||||||
|
self.specs = []
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
__title__ = 'django-imagekit'
|
|
||||||
__author__ = 'Matthew Tretter, Venelin Stoykov, Eric Eldredge, Bryan Veloso, Greg Newman, Chris Drackett, Justin Driscoll'
|
|
||||||
__version__ = '4.0.2'
|
|
||||||
__license__ = 'BSD'
|
|
||||||
__all__ = ['__title__', '__author__', '__version__', '__license__']
|
|
||||||
177
imagekit/processors.py
Normal file
177
imagekit/processors.py
Normal file
|
|
@ -0,0 +1,177 @@
|
||||||
|
""" Imagekit Image "ImageProcessors"
|
||||||
|
|
||||||
|
A processor defines a set of class variables (optional) and a
|
||||||
|
class method named "process" which processes the supplied image using
|
||||||
|
the class properties as settings. The process method can be overridden as well allowing user to define their
|
||||||
|
own effects/processes entirely.
|
||||||
|
|
||||||
|
"""
|
||||||
|
from imagekit.lib import *
|
||||||
|
|
||||||
|
class ImageProcessor(object):
|
||||||
|
""" Base image processor class """
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def process(cls, img, fmt, obj):
|
||||||
|
return img, fmt
|
||||||
|
|
||||||
|
|
||||||
|
class Adjustment(ImageProcessor):
|
||||||
|
color = 1.0
|
||||||
|
brightness = 1.0
|
||||||
|
contrast = 1.0
|
||||||
|
sharpness = 1.0
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def process(cls, img, fmt, obj):
|
||||||
|
img = img.convert('RGB')
|
||||||
|
for name in ['Color', 'Brightness', 'Contrast', 'Sharpness']:
|
||||||
|
factor = getattr(cls, name.lower())
|
||||||
|
if factor != 1.0:
|
||||||
|
try:
|
||||||
|
img = getattr(ImageEnhance, name)(img).enhance(factor)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
return img, fmt
|
||||||
|
|
||||||
|
|
||||||
|
class Format(ImageProcessor):
|
||||||
|
format = 'JPEG'
|
||||||
|
extension = 'jpg'
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def process(cls, img, fmt, obj):
|
||||||
|
return img, cls.format
|
||||||
|
|
||||||
|
|
||||||
|
class Reflection(ImageProcessor):
|
||||||
|
background_color = '#FFFFFF'
|
||||||
|
size = 0.0
|
||||||
|
opacity = 0.6
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def process(cls, img, fmt, obj):
|
||||||
|
# convert bgcolor string to rgb value
|
||||||
|
background_color = ImageColor.getrgb(cls.background_color)
|
||||||
|
# handle palleted images
|
||||||
|
img = img.convert('RGB')
|
||||||
|
# copy orignial image and flip the orientation
|
||||||
|
reflection = img.copy().transpose(Image.FLIP_TOP_BOTTOM)
|
||||||
|
# create a new image filled with the bgcolor the same size
|
||||||
|
background = Image.new("RGB", img.size, background_color)
|
||||||
|
# calculate our alpha mask
|
||||||
|
start = int(255 - (255 * cls.opacity)) # The start of our gradient
|
||||||
|
steps = int(255 * cls.size) # the number of intermedite values
|
||||||
|
increment = (255 - start) / float(steps)
|
||||||
|
mask = Image.new('L', (1, 255))
|
||||||
|
for y in range(255):
|
||||||
|
if y < steps:
|
||||||
|
val = int(y * increment + start)
|
||||||
|
else:
|
||||||
|
val = 255
|
||||||
|
mask.putpixel((0, y), val)
|
||||||
|
alpha_mask = mask.resize(img.size)
|
||||||
|
# merge the reflection onto our background color using the alpha mask
|
||||||
|
reflection = Image.composite(background, reflection, alpha_mask)
|
||||||
|
# crop the reflection
|
||||||
|
reflection_height = int(img.size[1] * cls.size)
|
||||||
|
reflection = reflection.crop((0, 0, img.size[0], reflection_height))
|
||||||
|
# create new image sized to hold both the original image and the reflection
|
||||||
|
composite = Image.new("RGB", (img.size[0], img.size[1]+reflection_height), background_color)
|
||||||
|
# paste the orignal image and the reflection into the composite image
|
||||||
|
composite.paste(img, (0, 0))
|
||||||
|
composite.paste(reflection, (0, img.size[1]))
|
||||||
|
# Save the file as a JPEG
|
||||||
|
fmt = 'JPEG'
|
||||||
|
# return the image complete with reflection effect
|
||||||
|
return composite, fmt
|
||||||
|
|
||||||
|
|
||||||
|
class Resize(ImageProcessor):
|
||||||
|
width = None
|
||||||
|
height = None
|
||||||
|
crop = False
|
||||||
|
upscale = False
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def process(cls, img, fmt, obj):
|
||||||
|
cur_width, cur_height = img.size
|
||||||
|
if cls.crop:
|
||||||
|
crop_horz = getattr(obj, obj._ik.crop_horz_field, 1)
|
||||||
|
crop_vert = getattr(obj, obj._ik.crop_vert_field, 1)
|
||||||
|
ratio = max(float(cls.width)/cur_width, float(cls.height)/cur_height)
|
||||||
|
resize_x, resize_y = ((cur_width * ratio), (cur_height * ratio))
|
||||||
|
crop_x, crop_y = (abs(cls.width - resize_x), abs(cls.height - resize_y))
|
||||||
|
x_diff, y_diff = (int(crop_x / 2), int(crop_y / 2))
|
||||||
|
box_left, box_right = {
|
||||||
|
0: (0, cls.width),
|
||||||
|
1: (int(x_diff), int(x_diff + cls.width)),
|
||||||
|
2: (int(crop_x), int(resize_x)),
|
||||||
|
}[crop_horz]
|
||||||
|
box_upper, box_lower = {
|
||||||
|
0: (0, cls.height),
|
||||||
|
1: (int(y_diff), int(y_diff + cls.height)),
|
||||||
|
2: (int(crop_y), int(resize_y)),
|
||||||
|
}[crop_vert]
|
||||||
|
box = (box_left, box_upper, box_right, box_lower)
|
||||||
|
img = img.resize((int(resize_x), int(resize_y)), Image.ANTIALIAS).crop(box)
|
||||||
|
else:
|
||||||
|
if not cls.width is None and not cls.height is None:
|
||||||
|
ratio = min(float(cls.width)/cur_width,
|
||||||
|
float(cls.height)/cur_height)
|
||||||
|
else:
|
||||||
|
if cls.width is None:
|
||||||
|
ratio = float(cls.height)/cur_height
|
||||||
|
else:
|
||||||
|
ratio = float(cls.width)/cur_width
|
||||||
|
new_dimensions = (int(round(cur_width*ratio)),
|
||||||
|
int(round(cur_height*ratio)))
|
||||||
|
if new_dimensions[0] > cur_width or \
|
||||||
|
new_dimensions[1] > cur_height:
|
||||||
|
if not cls.upscale:
|
||||||
|
return img, fmt
|
||||||
|
img = img.resize(new_dimensions, Image.ANTIALIAS)
|
||||||
|
return img, fmt
|
||||||
|
|
||||||
|
|
||||||
|
class Transpose(ImageProcessor):
|
||||||
|
""" Rotates or flips the image
|
||||||
|
|
||||||
|
Method should be one of the following strings:
|
||||||
|
- FLIP_LEFT RIGHT
|
||||||
|
- FLIP_TOP_BOTTOM
|
||||||
|
- ROTATE_90
|
||||||
|
- ROTATE_270
|
||||||
|
- ROTATE_180
|
||||||
|
- auto
|
||||||
|
|
||||||
|
If method is set to 'auto' the processor will attempt to rotate the image
|
||||||
|
according to the EXIF Orientation data.
|
||||||
|
|
||||||
|
"""
|
||||||
|
EXIF_ORIENTATION_STEPS = {
|
||||||
|
1: [],
|
||||||
|
2: ['FLIP_LEFT_RIGHT'],
|
||||||
|
3: ['ROTATE_180'],
|
||||||
|
4: ['FLIP_TOP_BOTTOM'],
|
||||||
|
5: ['ROTATE_270', 'FLIP_LEFT_RIGHT'],
|
||||||
|
6: ['ROTATE_270'],
|
||||||
|
7: ['ROTATE_90', 'FLIP_LEFT_RIGHT'],
|
||||||
|
8: ['ROTATE_90'],
|
||||||
|
}
|
||||||
|
|
||||||
|
method = 'auto'
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def process(cls, img, fmt, obj):
|
||||||
|
if cls.method == 'auto':
|
||||||
|
try:
|
||||||
|
orientation = Image.open(obj._imgfield.file)._getexif()[0x0112]
|
||||||
|
ops = cls.EXIF_ORIENTATION_STEPS[orientation]
|
||||||
|
except:
|
||||||
|
ops = []
|
||||||
|
else:
|
||||||
|
ops = [cls.method]
|
||||||
|
for method in ops:
|
||||||
|
img = img.transpose(getattr(Image, method))
|
||||||
|
return img, fmt
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
from pilkit.processors import *
|
|
||||||
|
|
||||||
__all__ = [
|
|
||||||
# Base
|
|
||||||
'ProcessorPipeline', 'Adjust', 'Reflection', 'Transpose',
|
|
||||||
'Anchor', 'MakeOpaque',
|
|
||||||
# Crop
|
|
||||||
'TrimBorderColor', 'Crop', 'SmartCrop',
|
|
||||||
# Resize
|
|
||||||
'Resize', 'ResizeToCover', 'ResizeToFill', 'SmartResize',
|
|
||||||
'ResizeCanvas', 'AddBorder', 'ResizeToFit', 'Thumbnail'
|
|
||||||
]
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
import warnings
|
|
||||||
|
|
||||||
from pilkit.processors.base import *
|
|
||||||
|
|
||||||
warnings.warn('imagekit.processors.base is deprecated use imagekit.processors instead', DeprecationWarning)
|
|
||||||
|
|
||||||
__all__ = ['ProcessorPipeline', 'Adjust', 'Reflection', 'Transpose', 'Anchor', 'MakeOpaque']
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
import warnings
|
|
||||||
|
|
||||||
from pilkit.processors.crop import *
|
|
||||||
|
|
||||||
warnings.warn('imagekit.processors.crop is deprecated use imagekit.processors instead', DeprecationWarning)
|
|
||||||
|
|
||||||
__all__ = ['TrimBorderColor', 'Crop', 'SmartCrop']
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
import warnings
|
|
||||||
|
|
||||||
from pilkit.processors.resize import *
|
|
||||||
|
|
||||||
warnings.warn('imagekit.processors.resize is deprecated use imagekit.processors instead', DeprecationWarning)
|
|
||||||
|
|
||||||
__all__ = ['Resize', 'ResizeToCover', 'ResizeToFill', 'SmartResize', 'ResizeCanvas', 'AddBorder', 'ResizeToFit', 'Thumbnail']
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
import warnings
|
|
||||||
|
|
||||||
from pilkit.processors.utils import *
|
|
||||||
|
|
||||||
warnings.warn('imagekit.processors.utils is deprecated use pilkit.processors.utils instead', DeprecationWarning)
|
|
||||||
|
|
@ -1,201 +0,0 @@
|
||||||
from .exceptions import AlreadyRegistered, NotRegistered
|
|
||||||
from .signals import content_required, existence_required, source_saved
|
|
||||||
from .utils import autodiscover, call_strategy_method
|
|
||||||
|
|
||||||
|
|
||||||
class GeneratorRegistry(object):
|
|
||||||
"""
|
|
||||||
An object for registering generators. This registry provides
|
|
||||||
a convenient way for a distributable app to define default generators
|
|
||||||
without locking the users of the app into it.
|
|
||||||
|
|
||||||
"""
|
|
||||||
def __init__(self):
|
|
||||||
self._generators = {}
|
|
||||||
content_required.connect(self.content_required_receiver)
|
|
||||||
existence_required.connect(self.existence_required_receiver)
|
|
||||||
|
|
||||||
def register(self, id, generator):
|
|
||||||
registered_generator = self._generators.get(id)
|
|
||||||
if registered_generator and generator != self._generators[id]:
|
|
||||||
raise AlreadyRegistered('The generator with id %s is'
|
|
||||||
' already registered' % id)
|
|
||||||
self._generators[id] = generator
|
|
||||||
|
|
||||||
def unregister(self, id):
|
|
||||||
try:
|
|
||||||
del self._generators[id]
|
|
||||||
except KeyError:
|
|
||||||
raise NotRegistered('The generator with id %s is not'
|
|
||||||
' registered' % id)
|
|
||||||
|
|
||||||
def get(self, id, **kwargs):
|
|
||||||
autodiscover()
|
|
||||||
|
|
||||||
try:
|
|
||||||
generator = self._generators[id]
|
|
||||||
except KeyError:
|
|
||||||
raise NotRegistered('The generator with id %s is not'
|
|
||||||
' registered' % id)
|
|
||||||
if callable(generator):
|
|
||||||
return generator(**kwargs)
|
|
||||||
else:
|
|
||||||
return generator
|
|
||||||
|
|
||||||
def get_ids(self):
|
|
||||||
autodiscover()
|
|
||||||
return self._generators.keys()
|
|
||||||
|
|
||||||
def content_required_receiver(self, sender, file, **kwargs):
|
|
||||||
self._receive(file, 'on_content_required')
|
|
||||||
|
|
||||||
def existence_required_receiver(self, sender, file, **kwargs):
|
|
||||||
self._receive(file, 'on_existence_required')
|
|
||||||
|
|
||||||
def _receive(self, file, callback):
|
|
||||||
generator = file.generator
|
|
||||||
|
|
||||||
# FIXME: I guess this means you can't register functions?
|
|
||||||
if generator.__class__ in self._generators.values():
|
|
||||||
# Only invoke the strategy method for registered generators.
|
|
||||||
call_strategy_method(file, callback)
|
|
||||||
|
|
||||||
|
|
||||||
class SourceGroupRegistry(object):
|
|
||||||
"""
|
|
||||||
The source group registry is responsible for listening to source_* signals
|
|
||||||
on source groups, and relaying them to the image generated file strategies
|
|
||||||
of the appropriate generators.
|
|
||||||
|
|
||||||
In addition, registering a new source group also registers its generated
|
|
||||||
files with that registry.
|
|
||||||
|
|
||||||
"""
|
|
||||||
_signals = {
|
|
||||||
source_saved: 'on_source_saved',
|
|
||||||
}
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self._source_groups = {}
|
|
||||||
for signal in self._signals.keys():
|
|
||||||
signal.connect(self.source_group_receiver)
|
|
||||||
|
|
||||||
def register(self, generator_id, source_group):
|
|
||||||
from .specs.sourcegroups import SourceGroupFilesGenerator
|
|
||||||
generator_ids = self._source_groups.setdefault(source_group, set())
|
|
||||||
generator_ids.add(generator_id)
|
|
||||||
cachefile_registry.register(generator_id,
|
|
||||||
SourceGroupFilesGenerator(source_group, generator_id))
|
|
||||||
|
|
||||||
def unregister(self, generator_id, source_group):
|
|
||||||
from .specs.sourcegroups import SourceGroupFilesGenerator
|
|
||||||
generator_ids = self._source_groups.setdefault(source_group, set())
|
|
||||||
if generator_id in generator_ids:
|
|
||||||
generator_ids.remove(generator_id)
|
|
||||||
cachefile_registry.unregister(generator_id,
|
|
||||||
SourceGroupFilesGenerator(source_group, generator_id))
|
|
||||||
|
|
||||||
def source_group_receiver(self, sender, source, signal, **kwargs):
|
|
||||||
"""
|
|
||||||
Relay source group signals to the appropriate spec strategy.
|
|
||||||
|
|
||||||
"""
|
|
||||||
from .cachefiles import ImageCacheFile
|
|
||||||
source_group = sender
|
|
||||||
|
|
||||||
# Ignore signals from unregistered groups.
|
|
||||||
if source_group not in self._source_groups:
|
|
||||||
return
|
|
||||||
|
|
||||||
specs = [generator_registry.get(id, source=source) for id in
|
|
||||||
self._source_groups[source_group]]
|
|
||||||
callback_name = self._signals[signal]
|
|
||||||
|
|
||||||
for spec in specs:
|
|
||||||
file = ImageCacheFile(spec)
|
|
||||||
call_strategy_method(file, callback_name)
|
|
||||||
|
|
||||||
|
|
||||||
class CacheFileRegistry(object):
|
|
||||||
"""
|
|
||||||
An object for registering generated files with image generators. The two are
|
|
||||||
associated with each other via a string id. We do this (as opposed to
|
|
||||||
associating them directly by, for example, putting a ``cachefiles``
|
|
||||||
attribute on image generators) so that image generators can be overridden
|
|
||||||
without losing the associated files. That way, a distributable app can
|
|
||||||
define its own generators without locking the users of the app into it.
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self._cachefiles = {}
|
|
||||||
|
|
||||||
def register(self, generator_id, cachefiles):
|
|
||||||
"""
|
|
||||||
Associates generated files with a generator id
|
|
||||||
|
|
||||||
"""
|
|
||||||
if cachefiles not in self._cachefiles:
|
|
||||||
self._cachefiles[cachefiles] = set()
|
|
||||||
self._cachefiles[cachefiles].add(generator_id)
|
|
||||||
|
|
||||||
def unregister(self, generator_id, cachefiles):
|
|
||||||
"""
|
|
||||||
Disassociates generated files with a generator id
|
|
||||||
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
self._cachefiles[cachefiles].remove(generator_id)
|
|
||||||
except KeyError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def get(self, generator_id):
|
|
||||||
for k, v in self._cachefiles.items():
|
|
||||||
if generator_id in v:
|
|
||||||
for file in k():
|
|
||||||
yield file
|
|
||||||
|
|
||||||
|
|
||||||
class Register(object):
|
|
||||||
"""
|
|
||||||
Register generators and generated files.
|
|
||||||
|
|
||||||
"""
|
|
||||||
def generator(self, id, generator=None):
|
|
||||||
if generator is None:
|
|
||||||
# Return a decorator
|
|
||||||
def decorator(cls):
|
|
||||||
self.generator(id, cls)
|
|
||||||
return cls
|
|
||||||
return decorator
|
|
||||||
|
|
||||||
generator_registry.register(id, generator)
|
|
||||||
|
|
||||||
# iterable that returns kwargs or callable that returns iterable of kwargs
|
|
||||||
def cachefiles(self, generator_id, cachefiles):
|
|
||||||
cachefile_registry.register(generator_id, cachefiles)
|
|
||||||
|
|
||||||
def source_group(self, generator_id, source_group):
|
|
||||||
source_group_registry.register(generator_id, source_group)
|
|
||||||
|
|
||||||
|
|
||||||
class Unregister(object):
|
|
||||||
"""
|
|
||||||
Unregister generators and generated files.
|
|
||||||
|
|
||||||
"""
|
|
||||||
def generator(self, id):
|
|
||||||
generator_registry.unregister(id)
|
|
||||||
|
|
||||||
def cachefiles(self, generator_id, cachefiles):
|
|
||||||
cachefile_registry.unregister(generator_id, cachefiles)
|
|
||||||
|
|
||||||
def source_group(self, generator_id, source_group):
|
|
||||||
source_group_registry.unregister(generator_id, source_group)
|
|
||||||
|
|
||||||
|
|
||||||
generator_registry = GeneratorRegistry()
|
|
||||||
cachefile_registry = CacheFileRegistry()
|
|
||||||
source_group_registry = SourceGroupRegistry()
|
|
||||||
register = Register()
|
|
||||||
unregister = Unregister()
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
from django.dispatch import Signal
|
|
||||||
|
|
||||||
|
|
||||||
# Generated file signals
|
|
||||||
content_required = Signal()
|
|
||||||
existence_required = Signal()
|
|
||||||
|
|
||||||
# Source group signals
|
|
||||||
source_saved = Signal()
|
|
||||||
135
imagekit/specs.py
Normal file
135
imagekit/specs.py
Normal file
|
|
@ -0,0 +1,135 @@
|
||||||
|
""" ImageKit image specifications
|
||||||
|
|
||||||
|
All imagekit specifications must inherit from the ImageSpec class. Models
|
||||||
|
inheriting from ImageModel will be modified with a descriptor/accessor for each
|
||||||
|
spec found.
|
||||||
|
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
from StringIO import StringIO
|
||||||
|
from imagekit import processors
|
||||||
|
from imagekit.lib import *
|
||||||
|
from imagekit.utils import img_to_fobj
|
||||||
|
from django.core.files.base import ContentFile
|
||||||
|
|
||||||
|
|
||||||
|
class ImageSpec(object):
|
||||||
|
pre_cache = False
|
||||||
|
quality = 70
|
||||||
|
increment_count = False
|
||||||
|
processors = []
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def name(cls):
|
||||||
|
return getattr(cls, 'access_as', cls.__name__.lower())
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def process(cls, image, obj):
|
||||||
|
fmt = image.format
|
||||||
|
img = image.copy()
|
||||||
|
for proc in cls.processors:
|
||||||
|
img, fmt = proc.process(img, fmt, obj)
|
||||||
|
img.format = fmt
|
||||||
|
return img, fmt
|
||||||
|
|
||||||
|
|
||||||
|
class Accessor(object):
|
||||||
|
def __init__(self, obj, spec):
|
||||||
|
self._img = None
|
||||||
|
self._fmt = None
|
||||||
|
self._obj = obj
|
||||||
|
self.spec = spec
|
||||||
|
|
||||||
|
def _get_imgfile(self):
|
||||||
|
format = self._img.format or 'JPEG'
|
||||||
|
if format != 'JPEG':
|
||||||
|
imgfile = img_to_fobj(self._img, format)
|
||||||
|
else:
|
||||||
|
imgfile = img_to_fobj(self._img, format,
|
||||||
|
quality=int(self.spec.quality),
|
||||||
|
optimize=True)
|
||||||
|
return imgfile
|
||||||
|
|
||||||
|
def _create(self):
|
||||||
|
if self._obj._imgfield:
|
||||||
|
if self._exists():
|
||||||
|
return
|
||||||
|
# process the original image file
|
||||||
|
try:
|
||||||
|
fp = self._obj._imgfield.storage.open(self._obj._imgfield.name)
|
||||||
|
except IOError:
|
||||||
|
return
|
||||||
|
fp.seek(0)
|
||||||
|
fp = StringIO(fp.read())
|
||||||
|
self._img, self._fmt = self.spec.process(Image.open(fp), self._obj)
|
||||||
|
# save the new image to the cache
|
||||||
|
content = ContentFile(self._get_imgfile().read())
|
||||||
|
self._obj._storage.save(self.name, content)
|
||||||
|
|
||||||
|
def _delete(self):
|
||||||
|
if self._obj._imgfield:
|
||||||
|
self._obj._storage.delete(self.name)
|
||||||
|
|
||||||
|
def _exists(self):
|
||||||
|
if self._obj._imgfield:
|
||||||
|
return self._obj._storage.exists(self.name)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
if self._obj._imgfield.name:
|
||||||
|
filepath, basename = os.path.split(self._obj._imgfield.name)
|
||||||
|
filename, extension = os.path.splitext(basename)
|
||||||
|
for processor in self.spec.processors:
|
||||||
|
if issubclass(processor, processors.Format):
|
||||||
|
extension = processor.extension
|
||||||
|
cache_filename = self._obj._ik.cache_filename_format % \
|
||||||
|
{'filename': filename,
|
||||||
|
'specname': self.spec.name(),
|
||||||
|
'extension': extension.lstrip('.')}
|
||||||
|
if callable(self._obj._ik.cache_dir):
|
||||||
|
return self._obj._ik.cache_dir(self._obj, filepath,
|
||||||
|
cache_filename)
|
||||||
|
else:
|
||||||
|
return os.path.join(self._obj._ik.cache_dir, filepath,
|
||||||
|
cache_filename)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def url(self):
|
||||||
|
if not self.spec.pre_cache:
|
||||||
|
self._create()
|
||||||
|
if self.spec.increment_count:
|
||||||
|
fieldname = self._obj._ik.save_count_as
|
||||||
|
if fieldname is not None:
|
||||||
|
current_count = getattr(self._obj, fieldname)
|
||||||
|
setattr(self._obj, fieldname, current_count + 1)
|
||||||
|
self._obj.save(clear_cache=False)
|
||||||
|
return self._obj._storage.url(self.name)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def file(self):
|
||||||
|
self._create()
|
||||||
|
return self._obj._storage.open(self.name)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def image(self):
|
||||||
|
if not self._img:
|
||||||
|
self._create()
|
||||||
|
if not self._img:
|
||||||
|
self._img = Image.open(self.file)
|
||||||
|
return self._img
|
||||||
|
|
||||||
|
@property
|
||||||
|
def width(self):
|
||||||
|
return self.image.size[0]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def height(self):
|
||||||
|
return self.image.size[1]
|
||||||
|
|
||||||
|
|
||||||
|
class Descriptor(object):
|
||||||
|
def __init__(self, spec):
|
||||||
|
self._spec = spec
|
||||||
|
|
||||||
|
def __get__(self, obj, type=None):
|
||||||
|
return Accessor(obj, self._spec)
|
||||||
|
|
@ -1,250 +0,0 @@
|
||||||
from copy import copy
|
|
||||||
from django.conf import settings
|
|
||||||
from django.db.models.fields.files import ImageFieldFile
|
|
||||||
from ..cachefiles.backends import get_default_cachefile_backend
|
|
||||||
from ..cachefiles.strategies import load_strategy
|
|
||||||
from .. import hashers
|
|
||||||
from ..exceptions import AlreadyRegistered, MissingSource
|
|
||||||
from ..utils import open_image, get_by_qname, process_image
|
|
||||||
from ..registry import generator_registry, register
|
|
||||||
|
|
||||||
|
|
||||||
class BaseImageSpec(object):
|
|
||||||
"""
|
|
||||||
An object that defines how an new image should be generated from a source
|
|
||||||
image.
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
cachefile_storage = None
|
|
||||||
"""A Django storage system to use to save a cache file."""
|
|
||||||
|
|
||||||
cachefile_backend = None
|
|
||||||
"""
|
|
||||||
An object responsible for managing the state of cache files. Defaults to
|
|
||||||
an instance of ``IMAGEKIT_DEFAULT_CACHEFILE_BACKEND``
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
cachefile_strategy = settings.IMAGEKIT_DEFAULT_CACHEFILE_STRATEGY
|
|
||||||
"""
|
|
||||||
A dictionary containing callbacks that allow you to customize how and when
|
|
||||||
the image file is created. Defaults to
|
|
||||||
``IMAGEKIT_DEFAULT_CACHEFILE_STRATEGY``.
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self.cachefile_backend = self.cachefile_backend or get_default_cachefile_backend()
|
|
||||||
self.cachefile_strategy = load_strategy(self.cachefile_strategy)
|
|
||||||
|
|
||||||
def generate(self):
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
MissingSource = MissingSource
|
|
||||||
"""
|
|
||||||
Raised when an operation requiring a source is attempted on a spec that has
|
|
||||||
no source.
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class ImageSpec(BaseImageSpec):
|
|
||||||
"""
|
|
||||||
An object that defines how to generate a new image from a source file using
|
|
||||||
PIL-based processors. (See :mod:`imagekit.processors`)
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
processors = []
|
|
||||||
"""A list of processors to run on the original image."""
|
|
||||||
|
|
||||||
format = None
|
|
||||||
"""
|
|
||||||
The format of the output file. If not provided, ImageSpecField will try to
|
|
||||||
guess the appropriate format based on the extension of the filename and the
|
|
||||||
format of the input image.
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
options = None
|
|
||||||
"""
|
|
||||||
A dictionary that will be passed to PIL's ``Image.save()`` method as keyword
|
|
||||||
arguments. Valid options vary between formats, but some examples include
|
|
||||||
``quality``, ``optimize``, and ``progressive`` for JPEGs. See the PIL
|
|
||||||
documentation for others.
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
autoconvert = True
|
|
||||||
"""
|
|
||||||
Specifies whether automatic conversion using ``prepare_image()`` should be
|
|
||||||
performed prior to saving.
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, source):
|
|
||||||
self.source = source
|
|
||||||
super(ImageSpec, self).__init__()
|
|
||||||
|
|
||||||
@property
|
|
||||||
def cachefile_name(self):
|
|
||||||
if not self.source:
|
|
||||||
return None
|
|
||||||
fn = get_by_qname(settings.IMAGEKIT_SPEC_CACHEFILE_NAMER, 'namer')
|
|
||||||
return fn(self)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def source(self):
|
|
||||||
src = getattr(self, '_source', None)
|
|
||||||
if not src:
|
|
||||||
field_data = getattr(self, '_field_data', None)
|
|
||||||
if field_data:
|
|
||||||
src = self._source = getattr(field_data['instance'], field_data['attname'])
|
|
||||||
del self._field_data
|
|
||||||
return src
|
|
||||||
|
|
||||||
@source.setter
|
|
||||||
def source(self, value):
|
|
||||||
self._source = value
|
|
||||||
|
|
||||||
def __getstate__(self):
|
|
||||||
state = copy(self.__dict__)
|
|
||||||
|
|
||||||
# Unpickled ImageFieldFiles won't work (they're missing a storage
|
|
||||||
# object). Since they're such a common use case, we special case them.
|
|
||||||
# Unfortunately, this also requires us to add the source getter to
|
|
||||||
# lazily retrieve the source on the reconstructed object; simply trying
|
|
||||||
# to look up the source in ``__setstate__`` would require us to get the
|
|
||||||
# model instance but, if ``__setstate__`` was called as part of
|
|
||||||
# deserializing that model, the model wouldn't be fully reconstructed
|
|
||||||
# yet, preventing us from accessing the source field.
|
|
||||||
# (This is issue #234.)
|
|
||||||
if isinstance(self.source, ImageFieldFile):
|
|
||||||
field = getattr(self.source, 'field')
|
|
||||||
state['_field_data'] = {
|
|
||||||
'instance': getattr(self.source, 'instance', None),
|
|
||||||
'attname': getattr(field, 'name', None),
|
|
||||||
}
|
|
||||||
state.pop('_source', None)
|
|
||||||
return state
|
|
||||||
|
|
||||||
def get_hash(self):
|
|
||||||
return hashers.pickle([
|
|
||||||
self.source.name,
|
|
||||||
self.processors,
|
|
||||||
self.format,
|
|
||||||
self.options,
|
|
||||||
self.autoconvert,
|
|
||||||
])
|
|
||||||
|
|
||||||
def generate(self):
|
|
||||||
if not self.source:
|
|
||||||
raise MissingSource("The spec '%s' has no source file associated"
|
|
||||||
" with it." % self)
|
|
||||||
|
|
||||||
# TODO: Move into a generator base class
|
|
||||||
# TODO: Factor out a generate_image function so you can create a generator and only override the PIL.Image creating part. (The tricky part is how to deal with original_format since generator base class won't have one.)
|
|
||||||
|
|
||||||
closed = self.source.closed
|
|
||||||
if closed:
|
|
||||||
# Django file object should know how to reopen itself if it was closed
|
|
||||||
# https://code.djangoproject.com/ticket/13750
|
|
||||||
self.source.open()
|
|
||||||
|
|
||||||
try:
|
|
||||||
img = open_image(self.source)
|
|
||||||
new_image = process_image(img,
|
|
||||||
processors=self.processors,
|
|
||||||
format=self.format,
|
|
||||||
autoconvert=self.autoconvert,
|
|
||||||
options=self.options)
|
|
||||||
finally:
|
|
||||||
if closed:
|
|
||||||
# We need to close the file if it was opened by us
|
|
||||||
self.source.close()
|
|
||||||
return new_image
|
|
||||||
|
|
||||||
|
|
||||||
def create_spec_class(class_attrs):
|
|
||||||
|
|
||||||
class DynamicSpecBase(ImageSpec):
|
|
||||||
def __reduce__(self):
|
|
||||||
try:
|
|
||||||
getstate = self.__getstate__
|
|
||||||
except AttributeError:
|
|
||||||
state = self.__dict__
|
|
||||||
else:
|
|
||||||
state = getstate()
|
|
||||||
return (create_spec, (class_attrs, state))
|
|
||||||
|
|
||||||
return type('DynamicSpec', (DynamicSpecBase,), class_attrs)
|
|
||||||
|
|
||||||
|
|
||||||
def create_spec(class_attrs, state):
|
|
||||||
cls = create_spec_class(class_attrs)
|
|
||||||
instance = cls.__new__(cls) # Create an instance without calling the __init__ (which may have required args).
|
|
||||||
try:
|
|
||||||
setstate = instance.__setstate__
|
|
||||||
except AttributeError:
|
|
||||||
instance.__dict__ = state
|
|
||||||
else:
|
|
||||||
setstate(state)
|
|
||||||
return instance
|
|
||||||
|
|
||||||
|
|
||||||
class SpecHost(object):
|
|
||||||
"""
|
|
||||||
An object that ostensibly has a spec attribute but really delegates to the
|
|
||||||
spec registry.
|
|
||||||
|
|
||||||
"""
|
|
||||||
def __init__(self, spec=None, spec_id=None, **kwargs):
|
|
||||||
|
|
||||||
spec_attrs = dict((k, v) for k, v in kwargs.items() if v is not None)
|
|
||||||
|
|
||||||
if spec_attrs:
|
|
||||||
if spec:
|
|
||||||
raise TypeError('You can provide either an image spec or'
|
|
||||||
' arguments for the ImageSpec constructor, but not both.')
|
|
||||||
else:
|
|
||||||
spec = create_spec_class(spec_attrs)
|
|
||||||
|
|
||||||
self._original_spec = spec
|
|
||||||
|
|
||||||
if spec_id:
|
|
||||||
self.set_spec_id(spec_id)
|
|
||||||
|
|
||||||
def set_spec_id(self, id):
|
|
||||||
"""
|
|
||||||
Sets the spec id for this object. Useful for when the id isn't
|
|
||||||
known when the instance is constructed (e.g. for ImageSpecFields whose
|
|
||||||
generated `spec_id`s are only known when they are contributed to a
|
|
||||||
class). If the object was initialized with a spec, it will be registered
|
|
||||||
under the provided id.
|
|
||||||
|
|
||||||
"""
|
|
||||||
self.spec_id = id
|
|
||||||
|
|
||||||
if self._original_spec:
|
|
||||||
try:
|
|
||||||
register.generator(id, self._original_spec)
|
|
||||||
except AlreadyRegistered:
|
|
||||||
# Fields should not cause AlreadyRegistered exceptions. If a
|
|
||||||
# spec is already registered, that should be used. It is
|
|
||||||
# especially important that an error is not thrown here because
|
|
||||||
# of South, which will create duplicate models as part of its
|
|
||||||
# "fake orm," therefore re-registering specs.
|
|
||||||
pass
|
|
||||||
|
|
||||||
def get_spec(self, source):
|
|
||||||
"""
|
|
||||||
Look up the spec by the spec id. We do this (instead of storing the
|
|
||||||
spec as an attribute) so that users can override apps' specs--without
|
|
||||||
having to edit model definitions--simply by registering another spec
|
|
||||||
with the same id.
|
|
||||||
|
|
||||||
"""
|
|
||||||
if not getattr(self, 'spec_id', None):
|
|
||||||
raise Exception('Object %s has no spec id.' % self)
|
|
||||||
return generator_registry.get(self.spec_id, source=source)
|
|
||||||
|
|
@ -1,175 +0,0 @@
|
||||||
"""
|
|
||||||
Source groups are the means by which image spec sources are identified. They
|
|
||||||
have two responsibilities:
|
|
||||||
|
|
||||||
1. To dispatch ``source_saved`` signals. (These will be relayed to the
|
|
||||||
corresponding specs' cache file strategies.)
|
|
||||||
2. To provide the source files that they represent, via a generator method named
|
|
||||||
``files()``. (This is used by the generateimages management command for
|
|
||||||
"pre-caching" image files.)
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
from django.db.models.signals import post_init, post_save
|
|
||||||
from django.utils.functional import wraps
|
|
||||||
import inspect
|
|
||||||
from ..cachefiles import LazyImageCacheFile
|
|
||||||
from ..signals import source_saved
|
|
||||||
from ..utils import get_nonabstract_descendants
|
|
||||||
|
|
||||||
|
|
||||||
def ik_model_receiver(fn):
|
|
||||||
"""
|
|
||||||
A method decorator that filters out signals coming from models that don't
|
|
||||||
have fields that function as ImageFieldSourceGroup sources.
|
|
||||||
|
|
||||||
"""
|
|
||||||
@wraps(fn)
|
|
||||||
def receiver(self, sender, **kwargs):
|
|
||||||
if not inspect.isclass(sender):
|
|
||||||
return
|
|
||||||
for src in self._source_groups:
|
|
||||||
if issubclass(sender, src.model_class):
|
|
||||||
fn(self, sender=sender, **kwargs)
|
|
||||||
|
|
||||||
# If we find a match, return. We don't want to handle the signal
|
|
||||||
# more than once.
|
|
||||||
return
|
|
||||||
return receiver
|
|
||||||
|
|
||||||
|
|
||||||
class ModelSignalRouter(object):
|
|
||||||
"""
|
|
||||||
Normally, ``ImageFieldSourceGroup`` would be directly responsible for
|
|
||||||
watching for changes on the model field it represents. However, Django does
|
|
||||||
not dispatch events for abstract base classes. Therefore, we must listen for
|
|
||||||
the signals on all models and filter out those that aren't represented by
|
|
||||||
``ImageFieldSourceGroup``s. This class encapsulates that functionality.
|
|
||||||
|
|
||||||
Related:
|
|
||||||
https://github.com/matthewwithanm/django-imagekit/issues/126
|
|
||||||
https://code.djangoproject.com/ticket/9318
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self._source_groups = []
|
|
||||||
uid = 'ik_spec_field_receivers'
|
|
||||||
post_init.connect(self.post_init_receiver, dispatch_uid=uid)
|
|
||||||
post_save.connect(self.post_save_receiver, dispatch_uid=uid)
|
|
||||||
|
|
||||||
def add(self, source_group):
|
|
||||||
self._source_groups.append(source_group)
|
|
||||||
|
|
||||||
def init_instance(self, instance):
|
|
||||||
instance._ik = getattr(instance, '_ik', {})
|
|
||||||
|
|
||||||
def update_source_hashes(self, instance):
|
|
||||||
"""
|
|
||||||
Stores hashes of the source image files so that they can be compared
|
|
||||||
later to see whether the source image has changed (and therefore whether
|
|
||||||
the spec file needs to be regenerated).
|
|
||||||
|
|
||||||
"""
|
|
||||||
self.init_instance(instance)
|
|
||||||
instance._ik['source_hashes'] = dict(
|
|
||||||
(attname, hash(getattr(instance, attname)))
|
|
||||||
for attname in self.get_source_fields(instance))
|
|
||||||
return instance._ik['source_hashes']
|
|
||||||
|
|
||||||
def get_source_fields(self, instance):
|
|
||||||
"""
|
|
||||||
Returns a list of the source fields for the given instance.
|
|
||||||
|
|
||||||
"""
|
|
||||||
return set(src.image_field
|
|
||||||
for src in self._source_groups
|
|
||||||
if isinstance(instance, src.model_class))
|
|
||||||
|
|
||||||
@ik_model_receiver
|
|
||||||
def post_save_receiver(self, sender, instance=None, created=False, update_fields=None, raw=False, **kwargs):
|
|
||||||
if not raw:
|
|
||||||
self.init_instance(instance)
|
|
||||||
old_hashes = instance._ik.get('source_hashes', {}).copy()
|
|
||||||
new_hashes = self.update_source_hashes(instance)
|
|
||||||
for attname in self.get_source_fields(instance):
|
|
||||||
if update_fields and attname not in update_fields:
|
|
||||||
continue
|
|
||||||
|
|
||||||
file = getattr(instance, attname)
|
|
||||||
if file and old_hashes.get(attname) != new_hashes[attname]:
|
|
||||||
self.dispatch_signal(source_saved, file, sender, instance,
|
|
||||||
attname)
|
|
||||||
|
|
||||||
@ik_model_receiver
|
|
||||||
def post_init_receiver(self, sender, instance=None, **kwargs):
|
|
||||||
self.init_instance(instance)
|
|
||||||
source_fields = self.get_source_fields(instance)
|
|
||||||
local_fields = dict((field.name, field)
|
|
||||||
for field in instance._meta.local_fields
|
|
||||||
if field.name in source_fields)
|
|
||||||
instance._ik['source_hashes'] = dict(
|
|
||||||
(attname, hash(file_field))
|
|
||||||
for attname, file_field in local_fields.items())
|
|
||||||
|
|
||||||
def dispatch_signal(self, signal, file, model_class, instance, attname):
|
|
||||||
"""
|
|
||||||
Dispatch the signal for each of the matching source groups. Note that
|
|
||||||
more than one source can have the same model and image_field; it's
|
|
||||||
important that we dispatch the signal for each.
|
|
||||||
|
|
||||||
"""
|
|
||||||
for source_group in self._source_groups:
|
|
||||||
if issubclass(model_class, source_group.model_class) and source_group.image_field == attname:
|
|
||||||
signal.send(sender=source_group, source=file)
|
|
||||||
|
|
||||||
|
|
||||||
class ImageFieldSourceGroup(object):
|
|
||||||
"""
|
|
||||||
A source group that repesents a particular field across all instances of a
|
|
||||||
model and its subclasses.
|
|
||||||
|
|
||||||
"""
|
|
||||||
def __init__(self, model_class, image_field):
|
|
||||||
self.model_class = model_class
|
|
||||||
self.image_field = image_field
|
|
||||||
signal_router.add(self)
|
|
||||||
|
|
||||||
def files(self):
|
|
||||||
"""
|
|
||||||
A generator that returns the source files that this source group
|
|
||||||
represents; in this case, a particular field of every instance of a
|
|
||||||
particular model and its subclasses.
|
|
||||||
|
|
||||||
"""
|
|
||||||
for model in get_nonabstract_descendants(self.model_class):
|
|
||||||
for instance in model.objects.all().iterator():
|
|
||||||
yield getattr(instance, self.image_field)
|
|
||||||
|
|
||||||
|
|
||||||
class SourceGroupFilesGenerator(object):
|
|
||||||
"""
|
|
||||||
A Python generator that yields cache file objects for source groups.
|
|
||||||
|
|
||||||
"""
|
|
||||||
def __init__(self, source_group, generator_id):
|
|
||||||
self.source_group = source_group
|
|
||||||
self.generator_id = generator_id
|
|
||||||
|
|
||||||
def __eq__(self, other):
|
|
||||||
return (isinstance(other, self.__class__)
|
|
||||||
and self.__dict__ == other.__dict__)
|
|
||||||
|
|
||||||
def __ne__(self, other):
|
|
||||||
return not self.__eq__(other)
|
|
||||||
|
|
||||||
def __hash__(self):
|
|
||||||
return hash((self.source_group, self.generator_id))
|
|
||||||
|
|
||||||
def __call__(self):
|
|
||||||
for source_file in self.source_group.files():
|
|
||||||
yield LazyImageCacheFile(self.generator_id,
|
|
||||||
source=source_file)
|
|
||||||
|
|
||||||
|
|
||||||
signal_router = ModelSignalRouter()
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
{% if thumbnail %}
|
|
||||||
<a href="{{ model.get_absolute_url|default:original_image.url }}">
|
|
||||||
<img src="{{ thumbnail.url }}">
|
|
||||||
</a>
|
|
||||||
{% endif %}
|
|
||||||
|
|
@ -1,285 +0,0 @@
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from django import template
|
|
||||||
from django.utils.html import escape
|
|
||||||
from django.utils.safestring import mark_safe
|
|
||||||
|
|
||||||
from ..compat import parse_bits
|
|
||||||
from ..cachefiles import ImageCacheFile
|
|
||||||
from ..registry import generator_registry
|
|
||||||
from ..lib import force_text
|
|
||||||
|
|
||||||
|
|
||||||
register = template.Library()
|
|
||||||
|
|
||||||
|
|
||||||
ASSIGNMENT_DELIMETER = 'as'
|
|
||||||
HTML_ATTRS_DELIMITER = '--'
|
|
||||||
DEFAULT_THUMBNAIL_GENERATOR = 'imagekit:thumbnail'
|
|
||||||
|
|
||||||
|
|
||||||
def get_cachefile(context, generator_id, generator_kwargs, source=None):
|
|
||||||
generator_id = generator_id.resolve(context)
|
|
||||||
kwargs = dict((k, v.resolve(context)) for k, v in generator_kwargs.items())
|
|
||||||
generator = generator_registry.get(generator_id, **kwargs)
|
|
||||||
return ImageCacheFile(generator)
|
|
||||||
|
|
||||||
|
|
||||||
def parse_dimensions(dimensions):
|
|
||||||
"""
|
|
||||||
Parse the width and height values from a dimension string. Valid values are
|
|
||||||
'1x1', '1x', and 'x1'. If one of the dimensions is omitted, the parse result
|
|
||||||
will be None for that value.
|
|
||||||
|
|
||||||
"""
|
|
||||||
width, height = [d.strip() and int(d) or None for d in dimensions.split('x')]
|
|
||||||
return dict(width=width, height=height)
|
|
||||||
|
|
||||||
|
|
||||||
class GenerateImageAssignmentNode(template.Node):
|
|
||||||
|
|
||||||
def __init__(self, variable_name, generator_id, generator_kwargs):
|
|
||||||
self._generator_id = generator_id
|
|
||||||
self._generator_kwargs = generator_kwargs
|
|
||||||
self._variable_name = variable_name
|
|
||||||
|
|
||||||
def get_variable_name(self, context):
|
|
||||||
return force_text(self._variable_name)
|
|
||||||
|
|
||||||
def render(self, context):
|
|
||||||
variable_name = self.get_variable_name(context)
|
|
||||||
context[variable_name] = get_cachefile(context, self._generator_id,
|
|
||||||
self._generator_kwargs)
|
|
||||||
return ''
|
|
||||||
|
|
||||||
|
|
||||||
class GenerateImageTagNode(template.Node):
|
|
||||||
|
|
||||||
def __init__(self, generator_id, generator_kwargs, html_attrs):
|
|
||||||
self._generator_id = generator_id
|
|
||||||
self._generator_kwargs = generator_kwargs
|
|
||||||
self._html_attrs = html_attrs
|
|
||||||
|
|
||||||
def render(self, context):
|
|
||||||
file = get_cachefile(context, self._generator_id,
|
|
||||||
self._generator_kwargs)
|
|
||||||
attrs = dict((k, v.resolve(context)) for k, v in
|
|
||||||
self._html_attrs.items())
|
|
||||||
|
|
||||||
# Only add width and height if neither is specified (to allow for
|
|
||||||
# proportional in-browser scaling).
|
|
||||||
if not 'width' in attrs and not 'height' in attrs:
|
|
||||||
attrs.update(width=file.width, height=file.height)
|
|
||||||
|
|
||||||
attrs['src'] = file.url
|
|
||||||
attr_str = ' '.join('%s="%s"' % (escape(k), escape(v)) for k, v in
|
|
||||||
attrs.items())
|
|
||||||
return mark_safe('<img %s />' % attr_str)
|
|
||||||
|
|
||||||
|
|
||||||
class ThumbnailAssignmentNode(template.Node):
|
|
||||||
|
|
||||||
def __init__(self, variable_name, generator_id, dimensions, source, generator_kwargs):
|
|
||||||
self._variable_name = variable_name
|
|
||||||
self._generator_id = generator_id
|
|
||||||
self._dimensions = dimensions
|
|
||||||
self._source = source
|
|
||||||
self._generator_kwargs = generator_kwargs
|
|
||||||
|
|
||||||
def get_variable_name(self, context):
|
|
||||||
return force_text(self._variable_name)
|
|
||||||
|
|
||||||
def render(self, context):
|
|
||||||
variable_name = self.get_variable_name(context)
|
|
||||||
|
|
||||||
generator_id = self._generator_id.resolve(context) if self._generator_id else DEFAULT_THUMBNAIL_GENERATOR
|
|
||||||
kwargs = dict((k, v.resolve(context)) for k, v in
|
|
||||||
self._generator_kwargs.items())
|
|
||||||
kwargs['source'] = self._source.resolve(context)
|
|
||||||
kwargs.update(parse_dimensions(self._dimensions.resolve(context)))
|
|
||||||
generator = generator_registry.get(generator_id, **kwargs)
|
|
||||||
|
|
||||||
context[variable_name] = ImageCacheFile(generator)
|
|
||||||
|
|
||||||
return ''
|
|
||||||
|
|
||||||
|
|
||||||
class ThumbnailImageTagNode(template.Node):
|
|
||||||
|
|
||||||
def __init__(self, generator_id, dimensions, source, generator_kwargs, html_attrs):
|
|
||||||
self._generator_id = generator_id
|
|
||||||
self._dimensions = dimensions
|
|
||||||
self._source = source
|
|
||||||
self._generator_kwargs = generator_kwargs
|
|
||||||
self._html_attrs = html_attrs
|
|
||||||
|
|
||||||
def render(self, context):
|
|
||||||
generator_id = self._generator_id.resolve(context) if self._generator_id else DEFAULT_THUMBNAIL_GENERATOR
|
|
||||||
dimensions = parse_dimensions(self._dimensions.resolve(context))
|
|
||||||
kwargs = dict((k, v.resolve(context)) for k, v in
|
|
||||||
self._generator_kwargs.items())
|
|
||||||
kwargs['source'] = self._source.resolve(context)
|
|
||||||
kwargs.update(dimensions)
|
|
||||||
generator = generator_registry.get(generator_id, **kwargs)
|
|
||||||
|
|
||||||
file = ImageCacheFile(generator)
|
|
||||||
|
|
||||||
attrs = dict((k, v.resolve(context)) for k, v in
|
|
||||||
self._html_attrs.items())
|
|
||||||
|
|
||||||
# Only add width and height if neither is specified (to allow for
|
|
||||||
# proportional in-browser scaling).
|
|
||||||
if not 'width' in attrs and not 'height' in attrs:
|
|
||||||
attrs.update(width=file.width, height=file.height)
|
|
||||||
|
|
||||||
attrs['src'] = file.url
|
|
||||||
attr_str = ' '.join('%s="%s"' % (escape(k), escape(v)) for k, v in
|
|
||||||
attrs.items())
|
|
||||||
return mark_safe('<img %s />' % attr_str)
|
|
||||||
|
|
||||||
|
|
||||||
def parse_ik_tag_bits(parser, bits):
|
|
||||||
"""
|
|
||||||
Parses the tag name, html attributes and variable name (for assignment tags)
|
|
||||||
from the provided bits. The preceding bits may vary and are left to be
|
|
||||||
parsed by specific tags.
|
|
||||||
|
|
||||||
"""
|
|
||||||
varname = None
|
|
||||||
html_attrs = {}
|
|
||||||
tag_name = bits.pop(0)
|
|
||||||
|
|
||||||
if len(bits) >= 2 and bits[-2] == ASSIGNMENT_DELIMETER:
|
|
||||||
varname = bits[-1]
|
|
||||||
bits = bits[:-2]
|
|
||||||
|
|
||||||
if HTML_ATTRS_DELIMITER in bits:
|
|
||||||
|
|
||||||
if varname:
|
|
||||||
raise template.TemplateSyntaxError('Do not specify html attributes'
|
|
||||||
' (using "%s") when using the "%s" tag as an assignment'
|
|
||||||
' tag.' % (HTML_ATTRS_DELIMITER, tag_name))
|
|
||||||
|
|
||||||
index = bits.index(HTML_ATTRS_DELIMITER)
|
|
||||||
html_bits = bits[index + 1:]
|
|
||||||
bits = bits[:index]
|
|
||||||
|
|
||||||
if not html_bits:
|
|
||||||
raise template.TemplateSyntaxError('Don\'t use "%s" unless you\'re'
|
|
||||||
' setting html attributes.' % HTML_ATTRS_DELIMITER)
|
|
||||||
|
|
||||||
args, html_attrs = parse_bits(parser, html_bits, [], 'args',
|
|
||||||
'kwargs', None, False, tag_name)
|
|
||||||
if len(args):
|
|
||||||
raise template.TemplateSyntaxError('All "%s" tag arguments after'
|
|
||||||
' the "%s" token must be named.' % (tag_name,
|
|
||||||
HTML_ATTRS_DELIMITER))
|
|
||||||
|
|
||||||
return (tag_name, bits, html_attrs, varname)
|
|
||||||
|
|
||||||
|
|
||||||
#@register.tag
|
|
||||||
def generateimage(parser, token):
|
|
||||||
"""
|
|
||||||
Creates an image based on the provided arguments.
|
|
||||||
|
|
||||||
By default::
|
|
||||||
|
|
||||||
{% generateimage 'myapp:thumbnail' source=mymodel.profile_image %}
|
|
||||||
|
|
||||||
generates an ``<img>`` tag::
|
|
||||||
|
|
||||||
<img src="/path/to/34d944f200dd794bf1e6a7f37849f72b.jpg" width="100" height="100" />
|
|
||||||
|
|
||||||
You can add additional attributes to the tag using "--". For example,
|
|
||||||
this::
|
|
||||||
|
|
||||||
{% generateimage 'myapp:thumbnail' source=mymodel.profile_image -- alt="Hello!" %}
|
|
||||||
|
|
||||||
will result in the following markup::
|
|
||||||
|
|
||||||
<img src="/path/to/34d944f200dd794bf1e6a7f37849f72b.jpg" width="100" height="100" alt="Hello!" />
|
|
||||||
|
|
||||||
For more flexibility, ``generateimage`` also works as an assignment tag::
|
|
||||||
|
|
||||||
{% generateimage 'myapp:thumbnail' source=mymodel.profile_image as th %}
|
|
||||||
<img src="{{ th.url }}" width="{{ th.width }}" height="{{ th.height }}" />
|
|
||||||
|
|
||||||
"""
|
|
||||||
bits = token.split_contents()
|
|
||||||
|
|
||||||
tag_name, bits, html_attrs, varname = parse_ik_tag_bits(parser, bits)
|
|
||||||
|
|
||||||
args, kwargs = parse_bits(parser, bits, ['generator_id'], 'args', 'kwargs',
|
|
||||||
None, False, tag_name)
|
|
||||||
|
|
||||||
if len(args) != 1:
|
|
||||||
raise template.TemplateSyntaxError('The "%s" tag requires exactly one'
|
|
||||||
' unnamed argument (the generator id).' % tag_name)
|
|
||||||
|
|
||||||
generator_id = args[0]
|
|
||||||
|
|
||||||
if varname:
|
|
||||||
return GenerateImageAssignmentNode(varname, generator_id, kwargs)
|
|
||||||
else:
|
|
||||||
return GenerateImageTagNode(generator_id, kwargs, html_attrs)
|
|
||||||
|
|
||||||
|
|
||||||
#@register.tag
|
|
||||||
def thumbnail(parser, token):
|
|
||||||
"""
|
|
||||||
A convenient shortcut syntax for generating a thumbnail. The following::
|
|
||||||
|
|
||||||
{% thumbnail '100x100' mymodel.profile_image %}
|
|
||||||
|
|
||||||
is equivalent to::
|
|
||||||
|
|
||||||
{% generateimage 'imagekit:thumbnail' source=mymodel.profile_image width=100 height=100 %}
|
|
||||||
|
|
||||||
The thumbnail tag supports the "--" and "as" bits for adding html
|
|
||||||
attributes and assigning to a variable, respectively. It also accepts the
|
|
||||||
kwargs "anchor", and "crop".
|
|
||||||
|
|
||||||
To use "smart cropping" (the ``SmartResize`` processor)::
|
|
||||||
|
|
||||||
{% thumbnail '100x100' mymodel.profile_image %}
|
|
||||||
|
|
||||||
To crop, anchoring the image to the top right (the ``ResizeToFill``
|
|
||||||
processor)::
|
|
||||||
|
|
||||||
{% thumbnail '100x100' mymodel.profile_image anchor='tr' %}
|
|
||||||
|
|
||||||
To resize without cropping (using the ``ResizeToFit`` processor)::
|
|
||||||
|
|
||||||
{% thumbnail '100x100' mymodel.profile_image crop=0 %}
|
|
||||||
|
|
||||||
"""
|
|
||||||
bits = token.split_contents()
|
|
||||||
|
|
||||||
tag_name, bits, html_attrs, varname = parse_ik_tag_bits(parser, bits)
|
|
||||||
|
|
||||||
args, kwargs = parse_bits(parser, bits, [], 'args', 'kwargs',
|
|
||||||
None, False, tag_name)
|
|
||||||
|
|
||||||
if len(args) < 2:
|
|
||||||
raise template.TemplateSyntaxError('The "%s" tag requires at least two'
|
|
||||||
' unnamed arguments: the dimensions and the source image.'
|
|
||||||
% tag_name)
|
|
||||||
elif len(args) > 3:
|
|
||||||
raise template.TemplateSyntaxError('The "%s" tag accepts at most three'
|
|
||||||
' unnamed arguments: a generator id, the dimensions, and the'
|
|
||||||
' source image.' % tag_name)
|
|
||||||
|
|
||||||
dimensions, source = args[-2:]
|
|
||||||
generator_id = args[0] if len(args) > 2 else None
|
|
||||||
|
|
||||||
if varname:
|
|
||||||
return ThumbnailAssignmentNode(varname, generator_id, dimensions,
|
|
||||||
source, kwargs)
|
|
||||||
else:
|
|
||||||
return ThumbnailImageTagNode(generator_id, dimensions, source, kwargs,
|
|
||||||
html_attrs)
|
|
||||||
|
|
||||||
|
|
||||||
generateimage = register.tag(generateimage)
|
|
||||||
thumbnail = register.tag(thumbnail)
|
|
||||||
99
imagekit/tests.py
Normal file
99
imagekit/tests.py
Normal file
|
|
@ -0,0 +1,99 @@
|
||||||
|
import os
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
from django.conf import settings
|
||||||
|
from django.core.files.base import ContentFile
|
||||||
|
from django.db import models
|
||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
from imagekit import processors
|
||||||
|
from imagekit.models import ImageModel
|
||||||
|
from imagekit.specs import ImageSpec
|
||||||
|
from imagekit.lib import Image
|
||||||
|
|
||||||
|
|
||||||
|
class ResizeToWidth(processors.Resize):
|
||||||
|
width = 100
|
||||||
|
|
||||||
|
class ResizeToHeight(processors.Resize):
|
||||||
|
height = 100
|
||||||
|
|
||||||
|
class ResizeToFit(processors.Resize):
|
||||||
|
width = 100
|
||||||
|
height = 100
|
||||||
|
|
||||||
|
class ResizeCropped(ResizeToFit):
|
||||||
|
crop = ('center', 'center')
|
||||||
|
|
||||||
|
class TestResizeToWidth(ImageSpec):
|
||||||
|
access_as = 'to_width'
|
||||||
|
processors = [ResizeToWidth]
|
||||||
|
|
||||||
|
class TestResizeToHeight(ImageSpec):
|
||||||
|
access_as = 'to_height'
|
||||||
|
processors = [ResizeToHeight]
|
||||||
|
|
||||||
|
class TestResizeCropped(ImageSpec):
|
||||||
|
access_as = 'cropped'
|
||||||
|
processors = [ResizeCropped]
|
||||||
|
|
||||||
|
class TestPhoto(ImageModel):
|
||||||
|
""" Minimal ImageModel class for testing """
|
||||||
|
image = models.ImageField(upload_to='images')
|
||||||
|
|
||||||
|
class IKOptions:
|
||||||
|
spec_module = 'imagekit.tests'
|
||||||
|
|
||||||
|
|
||||||
|
class IKTest(TestCase):
|
||||||
|
""" Base TestCase class """
|
||||||
|
def generate_image(self):
|
||||||
|
tmp = tempfile.TemporaryFile()
|
||||||
|
Image.new('RGB', (800, 600)).save(tmp, 'JPEG')
|
||||||
|
tmp.seek(0)
|
||||||
|
return tmp
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.p = TestPhoto()
|
||||||
|
img = self.generate_image()
|
||||||
|
self.p.save_image('test.jpeg', ContentFile(img.read()))
|
||||||
|
self.p.save()
|
||||||
|
img.close()
|
||||||
|
|
||||||
|
def test_save_image(self):
|
||||||
|
img = self.generate_image()
|
||||||
|
path = self.p.image.path
|
||||||
|
self.p.save_image('test2.jpeg', ContentFile(img.read()))
|
||||||
|
self.failIf(os.path.isfile(path))
|
||||||
|
path = self.p.image.path
|
||||||
|
img.seek(0)
|
||||||
|
self.p.save_image('test.jpeg', ContentFile(img.read()))
|
||||||
|
self.failIf(os.path.isfile(path))
|
||||||
|
img.close()
|
||||||
|
|
||||||
|
def test_setup(self):
|
||||||
|
self.assertEqual(self.p.image.width, 800)
|
||||||
|
self.assertEqual(self.p.image.height, 600)
|
||||||
|
|
||||||
|
def test_to_width(self):
|
||||||
|
self.assertEqual(self.p.to_width.width, 100)
|
||||||
|
self.assertEqual(self.p.to_width.height, 75)
|
||||||
|
|
||||||
|
def test_to_height(self):
|
||||||
|
self.assertEqual(self.p.to_height.width, 133)
|
||||||
|
self.assertEqual(self.p.to_height.height, 100)
|
||||||
|
|
||||||
|
def test_crop(self):
|
||||||
|
self.assertEqual(self.p.cropped.width, 100)
|
||||||
|
self.assertEqual(self.p.cropped.height, 100)
|
||||||
|
|
||||||
|
def test_url(self):
|
||||||
|
tup = (settings.MEDIA_URL, self.p._ik.cache_dir,
|
||||||
|
'images/test_to_width.jpeg')
|
||||||
|
self.assertEqual(self.p.to_width.url, "%s%s/%s" % tup)
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
# make sure image file is deleted
|
||||||
|
path = self.p.image.path
|
||||||
|
self.p.delete()
|
||||||
|
self.failIf(os.path.isfile(path))
|
||||||
|
|
@ -1,191 +1,9 @@
|
||||||
from __future__ import unicode_literals
|
""" ImageKit utility functions """
|
||||||
import logging
|
|
||||||
import re
|
|
||||||
from tempfile import NamedTemporaryFile
|
|
||||||
from hashlib import md5
|
|
||||||
|
|
||||||
from django.conf import settings
|
import tempfile
|
||||||
from django.core.exceptions import ImproperlyConfigured
|
|
||||||
from django.core.files import File
|
|
||||||
try:
|
|
||||||
from importlib import import_module
|
|
||||||
except ImportError:
|
|
||||||
from django.utils.importlib import import_module
|
|
||||||
from pilkit.utils import *
|
|
||||||
from .lib import NullHandler, force_bytes
|
|
||||||
|
|
||||||
|
def img_to_fobj(img, format, **kwargs):
|
||||||
bad_memcached_key_chars = re.compile('[\u0000-\u001f\\s]+')
|
tmp = tempfile.TemporaryFile()
|
||||||
|
img.save(tmp, format, **kwargs)
|
||||||
_autodiscovered = False
|
tmp.seek(0)
|
||||||
|
return tmp
|
||||||
def get_nonabstract_descendants(model):
|
|
||||||
""" Returns all non-abstract descendants of the model. """
|
|
||||||
if not model._meta.abstract:
|
|
||||||
yield model
|
|
||||||
for s in model.__subclasses__():
|
|
||||||
for m in get_nonabstract_descendants(s):
|
|
||||||
yield m
|
|
||||||
|
|
||||||
|
|
||||||
def get_by_qname(path, desc):
|
|
||||||
try:
|
|
||||||
dot = path.rindex('.')
|
|
||||||
except ValueError:
|
|
||||||
raise ImproperlyConfigured("%s isn't a %s module." % (path, desc))
|
|
||||||
module, objname = path[:dot], path[dot + 1:]
|
|
||||||
try:
|
|
||||||
mod = import_module(module)
|
|
||||||
except ImportError as e:
|
|
||||||
raise ImproperlyConfigured('Error importing %s module %s: "%s"' %
|
|
||||||
(desc, module, e))
|
|
||||||
try:
|
|
||||||
obj = getattr(mod, objname)
|
|
||||||
return obj
|
|
||||||
except AttributeError:
|
|
||||||
raise ImproperlyConfigured('%s module "%s" does not define "%s"'
|
|
||||||
% (desc[0].upper() + desc[1:], module, objname))
|
|
||||||
|
|
||||||
|
|
||||||
_singletons = {}
|
|
||||||
|
|
||||||
|
|
||||||
def get_singleton(class_path, desc):
|
|
||||||
global _singletons
|
|
||||||
cls = get_by_qname(class_path, desc)
|
|
||||||
instance = _singletons.get(cls)
|
|
||||||
if not instance:
|
|
||||||
instance = _singletons[cls] = cls()
|
|
||||||
return instance
|
|
||||||
|
|
||||||
|
|
||||||
def autodiscover():
|
|
||||||
"""
|
|
||||||
Auto-discover INSTALLED_APPS imagegenerators.py modules and fail silently
|
|
||||||
when not present. This forces an import on them to register any admin bits
|
|
||||||
they may want.
|
|
||||||
|
|
||||||
Copied from django.contrib.admin
|
|
||||||
"""
|
|
||||||
global _autodiscovered
|
|
||||||
|
|
||||||
if _autodiscovered:
|
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
|
||||||
from django.utils.module_loading import autodiscover_modules
|
|
||||||
except ImportError:
|
|
||||||
# Django<1.7
|
|
||||||
_autodiscover_modules_fallback()
|
|
||||||
else:
|
|
||||||
autodiscover_modules('imagegenerators')
|
|
||||||
_autodiscovered = True
|
|
||||||
|
|
||||||
|
|
||||||
def _autodiscover_modules_fallback():
|
|
||||||
"""
|
|
||||||
Auto-discover INSTALLED_APPS imagegenerators.py modules and fail silently
|
|
||||||
when not present. This forces an import on them to register any admin bits
|
|
||||||
they may want.
|
|
||||||
|
|
||||||
Copied from django.contrib.admin
|
|
||||||
|
|
||||||
Used for Django versions < 1.7
|
|
||||||
"""
|
|
||||||
from django.conf import settings
|
|
||||||
try:
|
|
||||||
from importlib import import_module
|
|
||||||
except ImportError:
|
|
||||||
from django.utils.importlib import import_module
|
|
||||||
from django.utils.module_loading import module_has_submodule
|
|
||||||
|
|
||||||
for app in settings.INSTALLED_APPS:
|
|
||||||
# As of Django 1.7, settings.INSTALLED_APPS may contain classes instead of modules, hence the try/except
|
|
||||||
# See here: https://docs.djangoproject.com/en/dev/releases/1.7/#introspecting-applications
|
|
||||||
try:
|
|
||||||
mod = import_module(app)
|
|
||||||
# Attempt to import the app's admin module.
|
|
||||||
try:
|
|
||||||
import_module('%s.imagegenerators' % app)
|
|
||||||
except:
|
|
||||||
# Decide whether to bubble up this error. If the app just
|
|
||||||
# doesn't have an imagegenerators module, we can ignore the error
|
|
||||||
# attempting to import it, otherwise we want it to bubble up.
|
|
||||||
if module_has_submodule(mod, 'imagegenerators'):
|
|
||||||
raise
|
|
||||||
except ImportError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def get_logger(logger_name='imagekit', add_null_handler=True):
|
|
||||||
logger = logging.getLogger(logger_name)
|
|
||||||
if add_null_handler:
|
|
||||||
logger.addHandler(NullHandler())
|
|
||||||
return logger
|
|
||||||
|
|
||||||
|
|
||||||
def get_field_info(field_file):
|
|
||||||
"""
|
|
||||||
A utility for easily extracting information about the host model from a
|
|
||||||
Django FileField (or subclass). This is especially useful for when you want
|
|
||||||
to alter processors based on a property of the source model. For example::
|
|
||||||
|
|
||||||
class MySpec(ImageSpec):
|
|
||||||
def __init__(self, source):
|
|
||||||
instance, attname = get_field_info(source)
|
|
||||||
self.processors = [SmartResize(instance.thumbnail_width,
|
|
||||||
instance.thumbnail_height)]
|
|
||||||
|
|
||||||
"""
|
|
||||||
return (
|
|
||||||
getattr(field_file, 'instance', None),
|
|
||||||
getattr(getattr(field_file, 'field', None), 'attname', None),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def generate(generator):
|
|
||||||
"""
|
|
||||||
Calls the ``generate()`` method of a generator instance, and then wraps the
|
|
||||||
result in a Django File object so Django knows how to save it.
|
|
||||||
|
|
||||||
"""
|
|
||||||
content = generator.generate()
|
|
||||||
f = File(content)
|
|
||||||
# The size of the File must be known or Django will try to open a file
|
|
||||||
# without a name and raise an Exception.
|
|
||||||
f.size = len(content.read())
|
|
||||||
# After getting the size reset the file pointer for future reads.
|
|
||||||
content.seek(0)
|
|
||||||
return f
|
|
||||||
|
|
||||||
|
|
||||||
def call_strategy_method(file, method_name):
|
|
||||||
strategy = getattr(file, 'cachefile_strategy', None)
|
|
||||||
fn = getattr(strategy, method_name, None)
|
|
||||||
if fn is not None:
|
|
||||||
fn(file)
|
|
||||||
|
|
||||||
|
|
||||||
def get_cache():
|
|
||||||
try:
|
|
||||||
from django.core.cache import caches
|
|
||||||
except ImportError:
|
|
||||||
# Django < 1.7
|
|
||||||
from django.core.cache import get_cache
|
|
||||||
return get_cache(settings.IMAGEKIT_CACHE_BACKEND)
|
|
||||||
|
|
||||||
return caches[settings.IMAGEKIT_CACHE_BACKEND]
|
|
||||||
|
|
||||||
|
|
||||||
def sanitize_cache_key(key):
|
|
||||||
if settings.IMAGEKIT_USE_MEMCACHED_SAFE_CACHE_KEY:
|
|
||||||
# Memcached keys can't contain whitespace or control characters.
|
|
||||||
new_key = bad_memcached_key_chars.sub('', key)
|
|
||||||
|
|
||||||
# The also can't be > 250 chars long. Since we don't know what the
|
|
||||||
# user's cache ``KEY_FUNCTION`` setting is like, we'll limit it to 200.
|
|
||||||
if len(new_key) >= 200:
|
|
||||||
new_key = '%s:%s' % (new_key[:200-33], md5(force_bytes(key)).hexdigest())
|
|
||||||
|
|
||||||
key = new_key
|
|
||||||
return key
|
|
||||||
|
|
|
||||||
|
|
@ -1,2 +0,0 @@
|
||||||
[bdist_wheel]
|
|
||||||
universal = 1
|
|
||||||
81
setup.py
81
setup.py
|
|
@ -1,80 +1,31 @@
|
||||||
#!/usr/bin/env python
|
#/usr/bin/env python
|
||||||
import codecs
|
from distutils.core import setup
|
||||||
import os
|
|
||||||
from setuptools import setup, find_packages
|
|
||||||
import sys
|
|
||||||
|
|
||||||
|
|
||||||
# Workaround for multiprocessing/nose issue. See http://bugs.python.org/msg170215
|
|
||||||
try:
|
|
||||||
import multiprocessing # NOQA
|
|
||||||
except ImportError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
if 'publish' in sys.argv:
|
|
||||||
os.system('python setup.py sdist bdist_wheel upload')
|
|
||||||
sys.exit()
|
|
||||||
|
|
||||||
|
|
||||||
read = lambda filepath: codecs.open(filepath, 'r', 'utf-8').read()
|
|
||||||
|
|
||||||
|
|
||||||
def exec_file(filepath, globalz=None, localz=None):
|
|
||||||
exec(read(filepath), globalz, localz)
|
|
||||||
|
|
||||||
|
|
||||||
# Load package meta from the pkgmeta module without loading imagekit.
|
|
||||||
pkgmeta = {}
|
|
||||||
exec_file(os.path.join(os.path.dirname(__file__),
|
|
||||||
'imagekit', 'pkgmeta.py'), pkgmeta)
|
|
||||||
|
|
||||||
|
|
||||||
setup(
|
setup(
|
||||||
name='django-imagekit',
|
name='django-imagekit',
|
||||||
version=pkgmeta['__version__'],
|
version='0.3.6',
|
||||||
description='Automated image processing for Django models.',
|
description='Automated image processing for Django models.',
|
||||||
long_description=read(os.path.join(os.path.dirname(__file__), 'README.rst')),
|
author='Justin Driscoll',
|
||||||
author='Matthew Tretter',
|
author_email='justin@driscolldev.com',
|
||||||
author_email='m@tthewwithanm.com',
|
|
||||||
maintainer='Bryan Veloso',
|
maintainer='Bryan Veloso',
|
||||||
maintainer_email='bryan@revyver.com',
|
maintainer_email='bryan@revyver.com',
|
||||||
license='BSD',
|
license='BSD',
|
||||||
url='http://github.com/matthewwithanm/django-imagekit/',
|
url='http://github.com/jdriscoll/django-imagekit/',
|
||||||
packages=find_packages(exclude=['*.tests', '*.tests.*', 'tests.*', 'tests']),
|
packages=[
|
||||||
zip_safe=False,
|
'imagekit',
|
||||||
include_package_data=True,
|
'imagekit.management',
|
||||||
tests_require=[
|
'imagekit.management.commands'
|
||||||
'beautifulsoup4>=4.4.0',
|
|
||||||
'nose>=1.3.6',
|
|
||||||
'nose-progressive>=1.5.1',
|
|
||||||
'django-nose>=1.4',
|
|
||||||
'Pillow',
|
|
||||||
'mock>=1.0.1',
|
|
||||||
],
|
],
|
||||||
test_suite='testrunner.run_tests',
|
|
||||||
install_requires=[
|
|
||||||
'django-appconf>=0.5',
|
|
||||||
'pilkit>=0.2.0',
|
|
||||||
'six',
|
|
||||||
],
|
|
||||||
extras_require={
|
|
||||||
'async': ['django-celery>=3.0'],
|
|
||||||
'async_rq': ['django-rq>=0.6.0'],
|
|
||||||
},
|
|
||||||
classifiers=[
|
classifiers=[
|
||||||
'Development Status :: 5 - Production/Stable',
|
'Development Status :: 3 - Alpha',
|
||||||
'Environment :: Web Environment',
|
'Environment :: Web Environment',
|
||||||
'Framework :: Django',
|
|
||||||
'Intended Audience :: Developers',
|
'Intended Audience :: Developers',
|
||||||
'License :: OSI Approved :: BSD License',
|
'License :: OSI Approved :: BSD License',
|
||||||
'Operating System :: OS Independent',
|
'Operating System :: OS Independent',
|
||||||
'Programming Language :: Python :: 2',
|
'Programming Language :: Python',
|
||||||
'Programming Language :: Python :: 2.7',
|
'Framework :: Django',
|
||||||
'Programming Language :: Python :: 3',
|
|
||||||
'Programming Language :: Python :: 3.3',
|
|
||||||
'Programming Language :: Python :: 3.4',
|
|
||||||
'Programming Language :: Python :: 3.5',
|
|
||||||
'Topic :: Utilities'
|
'Topic :: Utilities'
|
||||||
],
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,22 +0,0 @@
|
||||||
# A wrapper for Django's test runner.
|
|
||||||
# See http://ericholscher.com/blog/2009/jun/29/enable-setuppy-test-your-django-apps/
|
|
||||||
# and http://gremu.net/blog/2010/enable-setuppy-test-your-django-apps/
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
|
|
||||||
os.environ['DJANGO_SETTINGS_MODULE'] = 'tests.settings'
|
|
||||||
test_dir = os.path.dirname(__file__)
|
|
||||||
sys.path.insert(0, test_dir)
|
|
||||||
|
|
||||||
from django.test.utils import get_runner
|
|
||||||
from django.conf import settings
|
|
||||||
|
|
||||||
|
|
||||||
def run_tests():
|
|
||||||
cls = get_runner(settings)
|
|
||||||
runner = cls()
|
|
||||||
failures = runner.run_tests(['tests'])
|
|
||||||
# Clean autogenerated junk before exit
|
|
||||||
from tests.utils import clear_imagekit_test_files
|
|
||||||
clear_imagekit_test_files()
|
|
||||||
sys.exit(failures)
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
from imagekit import ImageSpec, register
|
|
||||||
from imagekit.processors import ResizeToFill
|
|
||||||
|
|
||||||
|
|
||||||
class TestSpec(ImageSpec):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class ResizeTo1PixelSquare(ImageSpec):
|
|
||||||
def __init__(self, width=None, height=None, anchor=None, crop=None, **kwargs):
|
|
||||||
self.processors = [ResizeToFill(1, 1)]
|
|
||||||
super(ResizeTo1PixelSquare, self).__init__(**kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
register.generator('testspec', TestSpec)
|
|
||||||
register.generator('1pxsq', ResizeTo1PixelSquare)
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 117 KiB |
|
|
@ -1,72 +0,0 @@
|
||||||
from django.db import models
|
|
||||||
|
|
||||||
from imagekit import ImageSpec
|
|
||||||
from imagekit.models import ProcessedImageField
|
|
||||||
from imagekit.models import ImageSpecField
|
|
||||||
from imagekit.processors import Adjust, ResizeToFill, SmartCrop
|
|
||||||
|
|
||||||
|
|
||||||
class Thumbnail(ImageSpec):
|
|
||||||
processors = [ResizeToFill(100, 60)]
|
|
||||||
format = 'JPEG'
|
|
||||||
options = {'quality': 60}
|
|
||||||
|
|
||||||
|
|
||||||
class ImageModel(models.Model):
|
|
||||||
image = models.ImageField(upload_to='b')
|
|
||||||
|
|
||||||
|
|
||||||
class Photo(models.Model):
|
|
||||||
original_image = models.ImageField(upload_to='photos')
|
|
||||||
|
|
||||||
# Implicit source field
|
|
||||||
thumbnail = ImageSpecField([Adjust(contrast=1.2, sharpness=1.1),
|
|
||||||
ResizeToFill(50, 50)], format='JPEG',
|
|
||||||
options={'quality': 90})
|
|
||||||
|
|
||||||
smartcropped_thumbnail = ImageSpecField([Adjust(contrast=1.2,
|
|
||||||
sharpness=1.1), SmartCrop(50, 50)], source='original_image',
|
|
||||||
format='JPEG', options={'quality': 90})
|
|
||||||
|
|
||||||
|
|
||||||
class ProcessedImageFieldModel(models.Model):
|
|
||||||
processed = ProcessedImageField([SmartCrop(50, 50)], format='JPEG',
|
|
||||||
options={'quality': 90}, upload_to='p')
|
|
||||||
|
|
||||||
|
|
||||||
class ProcessedImageFieldWithSpecModel(models.Model):
|
|
||||||
processed = ProcessedImageField(spec=Thumbnail, upload_to='p')
|
|
||||||
|
|
||||||
|
|
||||||
class CountingCacheFileStrategy(object):
|
|
||||||
def __init__(self):
|
|
||||||
self.on_existence_required_count = 0
|
|
||||||
self.on_content_required_count = 0
|
|
||||||
self.on_source_saved_count = 0
|
|
||||||
|
|
||||||
def on_existence_required(self, file):
|
|
||||||
self.on_existence_required_count += 1
|
|
||||||
|
|
||||||
def on_content_required(self, file):
|
|
||||||
self.on_content_required_count += 1
|
|
||||||
|
|
||||||
def on_source_saved(self, file):
|
|
||||||
self.on_source_saved_count += 1
|
|
||||||
|
|
||||||
|
|
||||||
class AbstractImageModel(models.Model):
|
|
||||||
original_image = models.ImageField(upload_to='photos')
|
|
||||||
abstract_class_spec = ImageSpecField(source='original_image',
|
|
||||||
format='JPEG',
|
|
||||||
cachefile_strategy=CountingCacheFileStrategy())
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
abstract = True
|
|
||||||
|
|
||||||
|
|
||||||
class ConcreteImageModel(AbstractImageModel):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class ConcreteImageModelSubclass(ConcreteImageModel):
|
|
||||||
pass
|
|
||||||
|
|
@ -1,69 +0,0 @@
|
||||||
import os
|
|
||||||
|
|
||||||
ADMINS = (
|
|
||||||
('test@example.com', 'TEST-R'),
|
|
||||||
)
|
|
||||||
|
|
||||||
BASE_PATH = os.path.abspath(os.path.dirname(__file__))
|
|
||||||
|
|
||||||
MEDIA_ROOT = os.path.normpath(os.path.join(BASE_PATH, 'media'))
|
|
||||||
|
|
||||||
# Django <= 1.2
|
|
||||||
DATABASE_ENGINE = 'sqlite3'
|
|
||||||
DATABASE_NAME = 'imagekit.db'
|
|
||||||
TEST_DATABASE_NAME = 'imagekit-test.db'
|
|
||||||
|
|
||||||
# Django >= 1.3
|
|
||||||
DATABASES = {
|
|
||||||
'default': {
|
|
||||||
'ENGINE': 'django.db.backends.sqlite3',
|
|
||||||
'NAME': 'imagekit.db',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
SECRET_KEY = '_uobce43e5osp8xgzle*yag2_16%y$sf*5(12vfg25hpnxik_*'
|
|
||||||
|
|
||||||
INSTALLED_APPS = [
|
|
||||||
'django.contrib.auth',
|
|
||||||
'django.contrib.contenttypes',
|
|
||||||
'imagekit',
|
|
||||||
'tests',
|
|
||||||
'django_nose',
|
|
||||||
]
|
|
||||||
|
|
||||||
TEST_RUNNER = 'django_nose.NoseTestSuiteRunner'
|
|
||||||
NOSE_ARGS = [
|
|
||||||
'-s',
|
|
||||||
|
|
||||||
# When the tests are run --with-coverage, these args configure coverage
|
|
||||||
# reporting (requires coverage to be installed).
|
|
||||||
# Without the --with-coverage flag, they have no effect.
|
|
||||||
'--cover-tests',
|
|
||||||
'--cover-html',
|
|
||||||
'--cover-package=imagekit',
|
|
||||||
'--cover-html-dir=%s' % os.path.join(BASE_PATH, 'cover')
|
|
||||||
]
|
|
||||||
|
|
||||||
if os.getenv('TERM'):
|
|
||||||
NOSE_ARGS.append('--with-progressive')
|
|
||||||
|
|
||||||
CACHE_BACKEND = 'locmem://'
|
|
||||||
|
|
||||||
# Django >= 1.8
|
|
||||||
TEMPLATES = [
|
|
||||||
{
|
|
||||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
|
||||||
'APP_DIRS': True,
|
|
||||||
'OPTIONS': {
|
|
||||||
'context_processors': [
|
|
||||||
'django.contrib.auth.context_processors.auth',
|
|
||||||
'django.template.context_processors.debug',
|
|
||||||
'django.template.context_processors.i18n',
|
|
||||||
'django.template.context_processors.media',
|
|
||||||
'django.template.context_processors.static',
|
|
||||||
'django.template.context_processors.tz',
|
|
||||||
'django.contrib.messages.context_processors.messages',
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
from imagekit.utils import get_nonabstract_descendants
|
|
||||||
from nose.tools import eq_
|
|
||||||
from . models import (AbstractImageModel, ConcreteImageModel,
|
|
||||||
ConcreteImageModelSubclass)
|
|
||||||
|
|
||||||
|
|
||||||
def test_nonabstract_descendants_generator():
|
|
||||||
descendants = list(get_nonabstract_descendants(AbstractImageModel))
|
|
||||||
eq_(descendants, [ConcreteImageModel, ConcreteImageModelSubclass])
|
|
||||||
|
|
@ -1,115 +0,0 @@
|
||||||
from unittest import mock
|
|
||||||
from django.conf import settings
|
|
||||||
from hashlib import md5
|
|
||||||
from imagekit.cachefiles import ImageCacheFile, LazyImageCacheFile
|
|
||||||
from imagekit.cachefiles.backends import Simple
|
|
||||||
from imagekit.lib import force_bytes
|
|
||||||
from nose.tools import raises, eq_
|
|
||||||
from .imagegenerators import TestSpec
|
|
||||||
from .utils import (assert_file_is_truthy, assert_file_is_falsy,
|
|
||||||
DummyAsyncCacheFileBackend, get_unique_image_file,
|
|
||||||
get_image_file)
|
|
||||||
|
|
||||||
|
|
||||||
def test_no_source_falsiness():
|
|
||||||
"""
|
|
||||||
Ensure cache files generated from sourceless specs are falsy.
|
|
||||||
|
|
||||||
"""
|
|
||||||
spec = TestSpec(source=None)
|
|
||||||
file = ImageCacheFile(spec)
|
|
||||||
assert_file_is_falsy(file)
|
|
||||||
|
|
||||||
|
|
||||||
def test_sync_backend_truthiness():
|
|
||||||
"""
|
|
||||||
Ensure that a cachefile with a synchronous cache file backend (the default)
|
|
||||||
is truthy.
|
|
||||||
|
|
||||||
"""
|
|
||||||
spec = TestSpec(source=get_unique_image_file())
|
|
||||||
file = ImageCacheFile(spec)
|
|
||||||
assert_file_is_truthy(file)
|
|
||||||
|
|
||||||
|
|
||||||
def test_async_backend_falsiness():
|
|
||||||
"""
|
|
||||||
Ensure that a cachefile with an asynchronous cache file backend is falsy.
|
|
||||||
|
|
||||||
"""
|
|
||||||
spec = TestSpec(source=get_unique_image_file())
|
|
||||||
file = ImageCacheFile(spec, cachefile_backend=DummyAsyncCacheFileBackend())
|
|
||||||
assert_file_is_falsy(file)
|
|
||||||
|
|
||||||
|
|
||||||
@raises(TestSpec.MissingSource)
|
|
||||||
def test_no_source_error():
|
|
||||||
spec = TestSpec(source=None)
|
|
||||||
file = ImageCacheFile(spec)
|
|
||||||
file.generate()
|
|
||||||
|
|
||||||
|
|
||||||
def test_repr_does_not_send_existence_required():
|
|
||||||
"""
|
|
||||||
Ensure that `__repr__` method does not send `existance_required` signal
|
|
||||||
|
|
||||||
Cachefile strategy may be configured to generate file on
|
|
||||||
`existance_required`.
|
|
||||||
To generate images, backend passes `ImageCacheFile` instance to worker.
|
|
||||||
Both celery and RQ calls `__repr__` method for each argument to enque call.
|
|
||||||
And if `__repr__` of object will send this signal, we will get endless
|
|
||||||
recursion
|
|
||||||
|
|
||||||
"""
|
|
||||||
with mock.patch('imagekit.cachefiles.existence_required') as signal:
|
|
||||||
# import here to apply mock
|
|
||||||
from imagekit.cachefiles import ImageCacheFile
|
|
||||||
|
|
||||||
spec = TestSpec(source=get_unique_image_file())
|
|
||||||
file = ImageCacheFile(
|
|
||||||
spec,
|
|
||||||
cachefile_backend=DummyAsyncCacheFileBackend()
|
|
||||||
)
|
|
||||||
file.__repr__()
|
|
||||||
eq_(signal.send.called, False)
|
|
||||||
|
|
||||||
|
|
||||||
def test_memcached_cache_key():
|
|
||||||
"""
|
|
||||||
Ensure the default cachefile backend is sanitizing its cache key for
|
|
||||||
memcached by default.
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
class MockFile(object):
|
|
||||||
def __init__(self, name):
|
|
||||||
self.name = name
|
|
||||||
|
|
||||||
backend = Simple()
|
|
||||||
extra_char_count = len('state-') + len(settings.IMAGEKIT_CACHE_PREFIX)
|
|
||||||
|
|
||||||
length = 199 - extra_char_count
|
|
||||||
filename = '1' * length
|
|
||||||
file = MockFile(filename)
|
|
||||||
eq_(backend.get_key(file), '%s%s-state' %
|
|
||||||
(settings.IMAGEKIT_CACHE_PREFIX, file.name))
|
|
||||||
|
|
||||||
length = 200 - extra_char_count
|
|
||||||
filename = '1' * length
|
|
||||||
file = MockFile(filename)
|
|
||||||
eq_(backend.get_key(file), '%s%s:%s' % (
|
|
||||||
settings.IMAGEKIT_CACHE_PREFIX,
|
|
||||||
'1' * (200 - len(':') - 32 - len(settings.IMAGEKIT_CACHE_PREFIX)),
|
|
||||||
md5(force_bytes('%s%s-state' % (settings.IMAGEKIT_CACHE_PREFIX, filename))).hexdigest()))
|
|
||||||
|
|
||||||
|
|
||||||
def test_lazyfile_stringification():
|
|
||||||
file = LazyImageCacheFile('testspec', source=None)
|
|
||||||
eq_(str(file), '')
|
|
||||||
eq_(repr(file), '<ImageCacheFile: None>')
|
|
||||||
|
|
||||||
source_file = get_image_file()
|
|
||||||
file = LazyImageCacheFile('testspec', source=source_file)
|
|
||||||
file.name = 'a.jpg'
|
|
||||||
eq_(str(file), 'a.jpg')
|
|
||||||
eq_(repr(file), '<ImageCacheFile: a.jpg>')
|
|
||||||
|
|
@ -1,25 +0,0 @@
|
||||||
from nose.tools import assert_false, assert_true
|
|
||||||
|
|
||||||
from .models import Thumbnail
|
|
||||||
from .utils import create_photo
|
|
||||||
|
|
||||||
|
|
||||||
def test_do_not_leak_open_files():
|
|
||||||
instance = create_photo('leak-test.jpg')
|
|
||||||
source_file = instance.original_image
|
|
||||||
# Ensure the FieldFile is closed before generation
|
|
||||||
source_file.close()
|
|
||||||
image_generator = Thumbnail(source=source_file)
|
|
||||||
image_generator.generate()
|
|
||||||
assert_true(source_file.closed)
|
|
||||||
|
|
||||||
|
|
||||||
def test_do_not_close_open_files_after_generate():
|
|
||||||
instance = create_photo('do-not-close-test.jpg')
|
|
||||||
source_file = instance.original_image
|
|
||||||
# Ensure the FieldFile is opened before generation
|
|
||||||
source_file.open()
|
|
||||||
image_generator = Thumbnail(source=source_file)
|
|
||||||
image_generator.generate()
|
|
||||||
assert_false(source_file.closed)
|
|
||||||
source_file.close()
|
|
||||||
|
|
@ -1,49 +0,0 @@
|
||||||
from django import forms
|
|
||||||
from django.core.files.base import File
|
|
||||||
from django.core.files.uploadedfile import SimpleUploadedFile
|
|
||||||
from imagekit import forms as ikforms
|
|
||||||
from imagekit.processors import SmartCrop
|
|
||||||
from nose.tools import eq_
|
|
||||||
from . import imagegenerators # noqa
|
|
||||||
from .models import (ProcessedImageFieldModel,
|
|
||||||
ProcessedImageFieldWithSpecModel,
|
|
||||||
ImageModel)
|
|
||||||
from .utils import get_image_file
|
|
||||||
|
|
||||||
|
|
||||||
def test_model_processedimagefield():
|
|
||||||
instance = ProcessedImageFieldModel()
|
|
||||||
file = File(get_image_file())
|
|
||||||
instance.processed.save('whatever.jpeg', file)
|
|
||||||
instance.save()
|
|
||||||
|
|
||||||
eq_(instance.processed.width, 50)
|
|
||||||
eq_(instance.processed.height, 50)
|
|
||||||
|
|
||||||
|
|
||||||
def test_model_processedimagefield_with_spec():
|
|
||||||
instance = ProcessedImageFieldWithSpecModel()
|
|
||||||
file = File(get_image_file())
|
|
||||||
instance.processed.save('whatever.jpeg', file)
|
|
||||||
instance.save()
|
|
||||||
|
|
||||||
eq_(instance.processed.width, 100)
|
|
||||||
eq_(instance.processed.height, 60)
|
|
||||||
|
|
||||||
|
|
||||||
def test_form_processedimagefield():
|
|
||||||
class TestForm(forms.ModelForm):
|
|
||||||
image = ikforms.ProcessedImageField(spec_id='tests:testform_image',
|
|
||||||
processors=[SmartCrop(50, 50)], format='JPEG')
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = ImageModel
|
|
||||||
fields = 'image',
|
|
||||||
|
|
||||||
upload_file = get_image_file()
|
|
||||||
file_dict = {'image': SimpleUploadedFile('abc.jpg', upload_file.read())}
|
|
||||||
form = TestForm({}, file_dict)
|
|
||||||
instance = form.save()
|
|
||||||
|
|
||||||
eq_(instance.image.width, 50)
|
|
||||||
eq_(instance.image.height, 50)
|
|
||||||
|
|
@ -1,56 +0,0 @@
|
||||||
from django.template import TemplateSyntaxError
|
|
||||||
from nose.tools import eq_, assert_false, raises, assert_not_equal
|
|
||||||
from . import imagegenerators # noqa
|
|
||||||
from .utils import render_tag, get_html_attrs, clear_imagekit_cache
|
|
||||||
|
|
||||||
|
|
||||||
def test_img_tag():
|
|
||||||
ttag = r"""{% generateimage 'testspec' source=img %}"""
|
|
||||||
clear_imagekit_cache()
|
|
||||||
attrs = get_html_attrs(ttag)
|
|
||||||
expected_attrs = set(['src', 'width', 'height'])
|
|
||||||
eq_(set(attrs.keys()), expected_attrs)
|
|
||||||
for k in expected_attrs:
|
|
||||||
assert_not_equal(attrs[k].strip(), '')
|
|
||||||
|
|
||||||
|
|
||||||
def test_img_tag_attrs():
|
|
||||||
ttag = r"""{% generateimage 'testspec' source=img -- alt="Hello" %}"""
|
|
||||||
clear_imagekit_cache()
|
|
||||||
attrs = get_html_attrs(ttag)
|
|
||||||
eq_(attrs.get('alt'), 'Hello')
|
|
||||||
|
|
||||||
|
|
||||||
@raises(TemplateSyntaxError)
|
|
||||||
def test_dangling_html_attrs_delimiter():
|
|
||||||
ttag = r"""{% generateimage 'testspec' source=img -- %}"""
|
|
||||||
render_tag(ttag)
|
|
||||||
|
|
||||||
|
|
||||||
@raises(TemplateSyntaxError)
|
|
||||||
def test_html_attrs_assignment():
|
|
||||||
"""
|
|
||||||
You can either use generateimage as an assignment tag or specify html attrs,
|
|
||||||
but not both.
|
|
||||||
|
|
||||||
"""
|
|
||||||
ttag = r"""{% generateimage 'testspec' source=img -- alt="Hello" as th %}"""
|
|
||||||
render_tag(ttag)
|
|
||||||
|
|
||||||
|
|
||||||
def test_single_dimension_attr():
|
|
||||||
"""
|
|
||||||
If you only provide one of width or height, the other should not be added.
|
|
||||||
|
|
||||||
"""
|
|
||||||
ttag = r"""{% generateimage 'testspec' source=img -- width="50" %}"""
|
|
||||||
clear_imagekit_cache()
|
|
||||||
attrs = get_html_attrs(ttag)
|
|
||||||
assert_false('height' in attrs)
|
|
||||||
|
|
||||||
|
|
||||||
def test_assignment_tag():
|
|
||||||
ttag = r"""{% generateimage 'testspec' source=img as th %}{{ th.url }}{{ th.height }}{{ th.width }}"""
|
|
||||||
clear_imagekit_cache()
|
|
||||||
html = render_tag(ttag)
|
|
||||||
assert_not_equal(html.strip(), '')
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
from nose.tools import assert_false
|
|
||||||
from unittest.mock import Mock, PropertyMock, patch
|
|
||||||
from .models import Photo
|
|
||||||
|
|
||||||
|
|
||||||
def test_dont_access_source():
|
|
||||||
"""
|
|
||||||
Touching the source may trigger an unneeded query.
|
|
||||||
See <https://github.com/matthewwithanm/django-imagekit/issues/295>
|
|
||||||
|
|
||||||
"""
|
|
||||||
pmock = PropertyMock()
|
|
||||||
pmock.__get__ = Mock()
|
|
||||||
with patch.object(Photo, 'original_image', pmock):
|
|
||||||
photo = Photo() # noqa
|
|
||||||
assert_false(pmock.__get__.called)
|
|
||||||
|
|
@ -1,49 +0,0 @@
|
||||||
from nose.tools import assert_true, assert_false
|
|
||||||
from imagekit.cachefiles import ImageCacheFile
|
|
||||||
from unittest.mock import Mock
|
|
||||||
from .utils import create_image
|
|
||||||
from django.core.files.storage import FileSystemStorage
|
|
||||||
from imagekit.cachefiles.backends import Simple as SimpleCFBackend
|
|
||||||
from imagekit.cachefiles.strategies import Optimistic as OptimisticStrategy
|
|
||||||
|
|
||||||
|
|
||||||
class ImageGenerator(object):
|
|
||||||
def generate(self):
|
|
||||||
return create_image()
|
|
||||||
|
|
||||||
def get_hash(self):
|
|
||||||
return 'abc123'
|
|
||||||
|
|
||||||
|
|
||||||
def get_image_cache_file():
|
|
||||||
storage = Mock(FileSystemStorage)
|
|
||||||
backend = SimpleCFBackend()
|
|
||||||
strategy = OptimisticStrategy()
|
|
||||||
generator = ImageGenerator()
|
|
||||||
return ImageCacheFile(generator, storage=storage,
|
|
||||||
cachefile_backend=backend,
|
|
||||||
cachefile_strategy=strategy)
|
|
||||||
|
|
||||||
|
|
||||||
def test_no_io_on_bool():
|
|
||||||
"""
|
|
||||||
When checking the truthiness of an ImageCacheFile, the storage shouldn't
|
|
||||||
peform IO operations.
|
|
||||||
|
|
||||||
"""
|
|
||||||
file = get_image_cache_file()
|
|
||||||
bool(file)
|
|
||||||
assert_false(file.storage.exists.called)
|
|
||||||
assert_false(file.storage.open.called)
|
|
||||||
|
|
||||||
|
|
||||||
def test_no_io_on_url():
|
|
||||||
"""
|
|
||||||
When getting the URL of an ImageCacheFile, the storage shouldn't be
|
|
||||||
checked.
|
|
||||||
|
|
||||||
"""
|
|
||||||
file = get_image_cache_file()
|
|
||||||
file.url
|
|
||||||
assert_false(file.storage.exists.called)
|
|
||||||
assert_false(file.storage.open.called)
|
|
||||||
|
|
@ -1,43 +0,0 @@
|
||||||
"""
|
|
||||||
Make sure that the various IK classes can be successfully serialized and
|
|
||||||
deserialized. This is important when using IK with Celery.
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
from imagekit.cachefiles import ImageCacheFile
|
|
||||||
from .imagegenerators import TestSpec
|
|
||||||
from .utils import create_photo, pickleback, get_unique_image_file, clear_imagekit_cache
|
|
||||||
|
|
||||||
|
|
||||||
def test_imagespecfield():
|
|
||||||
clear_imagekit_cache()
|
|
||||||
instance = create_photo('pickletest2.jpg')
|
|
||||||
thumbnail = pickleback(instance.thumbnail)
|
|
||||||
thumbnail.generate()
|
|
||||||
|
|
||||||
|
|
||||||
def test_circular_ref():
|
|
||||||
"""
|
|
||||||
A model instance with a spec field in its dict shouldn't raise a KeyError.
|
|
||||||
|
|
||||||
This corresponds to #234
|
|
||||||
|
|
||||||
"""
|
|
||||||
clear_imagekit_cache()
|
|
||||||
instance = create_photo('pickletest3.jpg')
|
|
||||||
instance.thumbnail # Cause thumbnail to be added to instance's __dict__
|
|
||||||
pickleback(instance)
|
|
||||||
|
|
||||||
|
|
||||||
def test_cachefiles():
|
|
||||||
clear_imagekit_cache()
|
|
||||||
spec = TestSpec(source=get_unique_image_file())
|
|
||||||
file = ImageCacheFile(spec)
|
|
||||||
file.url
|
|
||||||
# remove link to file from spec source generator
|
|
||||||
# test __getstate__ of ImageCacheFile
|
|
||||||
file.generator.source = None
|
|
||||||
restored_file = pickleback(file)
|
|
||||||
assert file is not restored_file
|
|
||||||
# Assertion for #437 and #451
|
|
||||||
assert file.storage is restored_file.storage
|
|
||||||
|
|
@ -1,55 +0,0 @@
|
||||||
from django.core.files import File
|
|
||||||
from imagekit.signals import source_saved
|
|
||||||
from imagekit.specs.sourcegroups import ImageFieldSourceGroup
|
|
||||||
from nose.tools import eq_
|
|
||||||
from . models import AbstractImageModel, ImageModel, ConcreteImageModel
|
|
||||||
from .utils import get_image_file
|
|
||||||
|
|
||||||
|
|
||||||
def make_counting_receiver(source_group):
|
|
||||||
def receiver(sender, *args, **kwargs):
|
|
||||||
if sender is source_group:
|
|
||||||
receiver.count += 1
|
|
||||||
receiver.count = 0
|
|
||||||
return receiver
|
|
||||||
|
|
||||||
|
|
||||||
def test_source_saved_signal():
|
|
||||||
"""
|
|
||||||
Creating a new instance with an image causes the source_saved signal to be
|
|
||||||
dispatched.
|
|
||||||
|
|
||||||
"""
|
|
||||||
source_group = ImageFieldSourceGroup(ImageModel, 'image')
|
|
||||||
receiver = make_counting_receiver(source_group)
|
|
||||||
source_saved.connect(receiver)
|
|
||||||
ImageModel.objects.create(image=File(get_image_file()))
|
|
||||||
eq_(receiver.count, 1)
|
|
||||||
|
|
||||||
|
|
||||||
def test_no_source_saved_signal():
|
|
||||||
"""
|
|
||||||
Creating a new instance without an image shouldn't cause the source_saved
|
|
||||||
signal to be dispatched.
|
|
||||||
|
|
||||||
https://github.com/matthewwithanm/django-imagekit/issues/214
|
|
||||||
|
|
||||||
"""
|
|
||||||
source_group = ImageFieldSourceGroup(ImageModel, 'image')
|
|
||||||
receiver = make_counting_receiver(source_group)
|
|
||||||
source_saved.connect(receiver)
|
|
||||||
ImageModel.objects.create()
|
|
||||||
eq_(receiver.count, 0)
|
|
||||||
|
|
||||||
|
|
||||||
def test_abstract_model_signals():
|
|
||||||
"""
|
|
||||||
Source groups created for abstract models must cause signals to be
|
|
||||||
dispatched on their concrete subclasses.
|
|
||||||
|
|
||||||
"""
|
|
||||||
source_group = ImageFieldSourceGroup(AbstractImageModel, 'original_image')
|
|
||||||
receiver = make_counting_receiver(source_group)
|
|
||||||
source_saved.connect(receiver)
|
|
||||||
ConcreteImageModel.objects.create(original_image=File(get_image_file()))
|
|
||||||
eq_(receiver.count, 1)
|
|
||||||
|
|
@ -1,71 +0,0 @@
|
||||||
from django.template import TemplateSyntaxError
|
|
||||||
from nose.tools import eq_, raises, assert_not_equal
|
|
||||||
from . import imagegenerators # noqa
|
|
||||||
from .utils import render_tag, get_html_attrs, clear_imagekit_cache
|
|
||||||
|
|
||||||
|
|
||||||
def test_img_tag():
|
|
||||||
ttag = r"""{% thumbnail '100x100' img %}"""
|
|
||||||
clear_imagekit_cache()
|
|
||||||
attrs = get_html_attrs(ttag)
|
|
||||||
expected_attrs = set(['src', 'width', 'height'])
|
|
||||||
eq_(set(attrs.keys()), expected_attrs)
|
|
||||||
for k in expected_attrs:
|
|
||||||
assert_not_equal(attrs[k].strip(), '')
|
|
||||||
|
|
||||||
|
|
||||||
def test_img_tag_attrs():
|
|
||||||
ttag = r"""{% thumbnail '100x100' img -- alt="Hello" %}"""
|
|
||||||
clear_imagekit_cache()
|
|
||||||
attrs = get_html_attrs(ttag)
|
|
||||||
eq_(attrs.get('alt'), 'Hello')
|
|
||||||
|
|
||||||
|
|
||||||
@raises(TemplateSyntaxError)
|
|
||||||
def test_dangling_html_attrs_delimiter():
|
|
||||||
ttag = r"""{% thumbnail '100x100' img -- %}"""
|
|
||||||
render_tag(ttag)
|
|
||||||
|
|
||||||
|
|
||||||
@raises(TemplateSyntaxError)
|
|
||||||
def test_not_enough_args():
|
|
||||||
ttag = r"""{% thumbnail '100x100' %}"""
|
|
||||||
render_tag(ttag)
|
|
||||||
|
|
||||||
|
|
||||||
@raises(TemplateSyntaxError)
|
|
||||||
def test_too_many_args():
|
|
||||||
ttag = r"""{% thumbnail 'generator_id' '100x100' img 'extra' %}"""
|
|
||||||
render_tag(ttag)
|
|
||||||
|
|
||||||
|
|
||||||
@raises(TemplateSyntaxError)
|
|
||||||
def test_html_attrs_assignment():
|
|
||||||
"""
|
|
||||||
You can either use thumbnail as an assignment tag or specify html attrs,
|
|
||||||
but not both.
|
|
||||||
|
|
||||||
"""
|
|
||||||
ttag = r"""{% thumbnail '100x100' img -- alt="Hello" as th %}"""
|
|
||||||
render_tag(ttag)
|
|
||||||
|
|
||||||
|
|
||||||
def test_assignment_tag():
|
|
||||||
ttag = r"""{% thumbnail '100x100' img as th %}{{ th.url }}"""
|
|
||||||
clear_imagekit_cache()
|
|
||||||
html = render_tag(ttag)
|
|
||||||
assert_not_equal(html, '')
|
|
||||||
|
|
||||||
|
|
||||||
def test_single_dimension():
|
|
||||||
ttag = r"""{% thumbnail '100x' img as th %}{{ th.width }}"""
|
|
||||||
clear_imagekit_cache()
|
|
||||||
html = render_tag(ttag)
|
|
||||||
eq_(html, '100')
|
|
||||||
|
|
||||||
|
|
||||||
def test_alternate_generator():
|
|
||||||
ttag = r"""{% thumbnail '1pxsq' '100x' img as th %}{{ th.width }}"""
|
|
||||||
clear_imagekit_cache()
|
|
||||||
html = render_tag(ttag)
|
|
||||||
eq_(html, '1')
|
|
||||||
106
tests/utils.py
106
tests/utils.py
|
|
@ -1,106 +0,0 @@
|
||||||
from bs4 import BeautifulSoup
|
|
||||||
import os
|
|
||||||
import shutil
|
|
||||||
from django.core.files import File
|
|
||||||
from django.template import Context, Template
|
|
||||||
from imagekit.cachefiles.backends import Simple, CacheFileState
|
|
||||||
from imagekit.conf import settings
|
|
||||||
from imagekit.lib import Image, StringIO
|
|
||||||
from imagekit.utils import get_cache
|
|
||||||
from nose.tools import assert_true, assert_false
|
|
||||||
import pickle
|
|
||||||
from tempfile import NamedTemporaryFile
|
|
||||||
from .models import Photo
|
|
||||||
|
|
||||||
|
|
||||||
def get_image_file():
|
|
||||||
"""
|
|
||||||
See also:
|
|
||||||
|
|
||||||
http://en.wikipedia.org/wiki/Lenna
|
|
||||||
http://sipi.usc.edu/database/database.php?volume=misc&image=12
|
|
||||||
https://lintian.debian.org/tags/license-problem-non-free-img-lenna.html
|
|
||||||
https://github.com/libav/libav/commit/8895bf7b78650c0c21c88cec0484e138ec511a4b
|
|
||||||
"""
|
|
||||||
path = os.path.join(settings.MEDIA_ROOT, 'reference.png')
|
|
||||||
return open(path, 'r+b')
|
|
||||||
|
|
||||||
|
|
||||||
def get_unique_image_file():
|
|
||||||
file = NamedTemporaryFile()
|
|
||||||
file.write(get_image_file().read())
|
|
||||||
return file
|
|
||||||
|
|
||||||
|
|
||||||
def create_image():
|
|
||||||
return Image.open(get_image_file())
|
|
||||||
|
|
||||||
|
|
||||||
def create_instance(model_class, image_name):
|
|
||||||
instance = model_class()
|
|
||||||
img = File(get_image_file())
|
|
||||||
instance.original_image.save(image_name, img)
|
|
||||||
instance.save()
|
|
||||||
img.close()
|
|
||||||
return instance
|
|
||||||
|
|
||||||
|
|
||||||
def create_photo(name):
|
|
||||||
return create_instance(Photo, name)
|
|
||||||
|
|
||||||
|
|
||||||
def pickleback(obj):
|
|
||||||
pickled = StringIO()
|
|
||||||
pickle.dump(obj, pickled)
|
|
||||||
pickled.seek(0)
|
|
||||||
return pickle.load(pickled)
|
|
||||||
|
|
||||||
|
|
||||||
def render_tag(ttag):
|
|
||||||
img = get_image_file()
|
|
||||||
template = Template('{%% load imagekit %%}%s' % ttag)
|
|
||||||
context = Context({'img': img})
|
|
||||||
return template.render(context)
|
|
||||||
|
|
||||||
|
|
||||||
def get_html_attrs(ttag):
|
|
||||||
return BeautifulSoup(render_tag(ttag), features="html.parser").img.attrs
|
|
||||||
|
|
||||||
|
|
||||||
def assert_file_is_falsy(file):
|
|
||||||
assert_false(bool(file), 'File is not falsy')
|
|
||||||
|
|
||||||
|
|
||||||
def assert_file_is_truthy(file):
|
|
||||||
assert_true(bool(file), 'File is not truthy')
|
|
||||||
|
|
||||||
|
|
||||||
class DummyAsyncCacheFileBackend(Simple):
|
|
||||||
"""
|
|
||||||
A cache file backend meant to simulate async generation.
|
|
||||||
|
|
||||||
"""
|
|
||||||
is_async = True
|
|
||||||
|
|
||||||
def generate(self, file, force=False):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def clear_imagekit_cache():
|
|
||||||
cache = get_cache()
|
|
||||||
cache.clear()
|
|
||||||
# Clear IMAGEKIT_CACHEFILE_DIR
|
|
||||||
cache_dir = os.path.join(settings.MEDIA_ROOT, settings.IMAGEKIT_CACHEFILE_DIR)
|
|
||||||
if os.path.exists(cache_dir):
|
|
||||||
shutil.rmtree(cache_dir)
|
|
||||||
|
|
||||||
|
|
||||||
def clear_imagekit_test_files():
|
|
||||||
clear_imagekit_cache()
|
|
||||||
for fname in os.listdir(settings.MEDIA_ROOT):
|
|
||||||
if fname != 'reference.png':
|
|
||||||
path = os.path.join(settings.MEDIA_ROOT, fname)
|
|
||||||
if os.path.isdir(path):
|
|
||||||
shutil.rmtree(path)
|
|
||||||
else:
|
|
||||||
os.remove(path)
|
|
||||||
18
tox.ini
18
tox.ini
|
|
@ -1,18 +0,0 @@
|
||||||
[tox]
|
|
||||||
envlist =
|
|
||||||
py38-django{master,30,22,21,20,111},
|
|
||||||
py37-django{master,30,22,21,20,111},
|
|
||||||
py36-django{master,30,22,21,20,111},
|
|
||||||
py35-django{21,20,111},
|
|
||||||
|
|
||||||
[testenv]
|
|
||||||
commands = python setup.py test
|
|
||||||
|
|
||||||
deps =
|
|
||||||
djangomaster: git+https://github.com/django/django.git@master#egg=Django
|
|
||||||
django30: Django>=3.0,<3.1
|
|
||||||
django22: Django>=2.2,<3.0
|
|
||||||
django21: Django>=2.1,<2.2
|
|
||||||
django20: Django>=2.0,<2.1
|
|
||||||
django111: Django>=1.11,<2.0
|
|
||||||
django{21,20,111}: django-nose==1.4.5
|
|
||||||
Loading…
Reference in a new issue