mirror of
https://github.com/Hopiu/django-imagekit.git
synced 2026-03-17 13:50:24 +00:00
Compare commits
309 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
238573051e | ||
|
|
417e33ff5a | ||
|
|
9d450a78b8 | ||
|
|
bc12a319b3 | ||
|
|
85f0741594 | ||
|
|
3317273401 | ||
|
|
94cc8ed9e4 | ||
|
|
60f35b0af5 | ||
|
|
2c85d5aafe | ||
|
|
f3c5f7cb16 | ||
|
|
66db460c24 | ||
|
|
6f7de35f79 | ||
|
|
de991d4048 | ||
|
|
595f7b35ef | ||
|
|
fc221335b7 | ||
|
|
58e44975c7 | ||
|
|
115b596a8d | ||
|
|
ea66e3d10d | ||
|
|
6319891697 | ||
|
|
6ee931398f | ||
|
|
7e23384145 | ||
|
|
d80f426d3c | ||
|
|
c95542ee2a | ||
|
|
de3047e73d | ||
|
|
a153812add | ||
|
|
364cd49278 | ||
|
|
2e1b574486 | ||
|
|
3819e61fdb | ||
|
|
845eeab3ce | ||
|
|
755bd34c3e | ||
|
|
2b04099dc4 | ||
|
|
c3dbb1edf0 | ||
|
|
681b85d7bf | ||
|
|
f96dadbfe0 | ||
|
|
499e9e1e07 | ||
|
|
36fa53e249 | ||
|
|
c74d8424b8 | ||
|
|
3d37fb3d3a | ||
|
|
c24455ef36 | ||
|
|
934a5283ad | ||
|
|
5281859d60 | ||
|
|
46d2a9e663 | ||
|
|
855c9a32b1 | ||
|
|
16ab0d2c99 | ||
|
|
96383451a0 | ||
|
|
755193699b | ||
|
|
d3369eec89 | ||
|
|
12fdee81dd | ||
|
|
dee14b6c22 | ||
|
|
2bc6241f55 | ||
|
|
3546c39178 | ||
|
|
4d1ee41f2e | ||
|
|
f6d3cbe4a1 | ||
|
|
48cf03b482 | ||
|
|
175904617e | ||
|
|
732f7045e4 | ||
|
|
95e484d073 | ||
|
|
47ff56cfe2 | ||
|
|
c354bb365a | ||
|
|
7e4bf0e3d8 | ||
|
|
f1f295e054 | ||
|
|
6457cf0c55 | ||
|
|
6a8fe5f83c | ||
|
|
3c0c47d8ed | ||
|
|
d86ec082f1 | ||
|
|
23a243c51e | ||
|
|
07d29b3bf7 | ||
|
|
5061679b17 | ||
|
|
5cde74e3e2 | ||
|
|
e9425df833 | ||
|
|
f98ee822a4 | ||
|
|
52ad8a0ace | ||
|
|
f2255a5d3a | ||
|
|
03a8d0d443 | ||
|
|
b460a66874 | ||
|
|
124b23ccc8 | ||
|
|
371a3bb376 | ||
|
|
b3084b43b2 | ||
|
|
4e370fdc59 | ||
|
|
7ddca36712 | ||
|
|
340e26cd67 | ||
|
|
5ce8b9f072 | ||
|
|
d280ad8989 | ||
|
|
0c435539df | ||
|
|
7903efd9b7 | ||
|
|
53fb3a8722 | ||
|
|
d1e877f07d | ||
|
|
cec8cd7780 | ||
|
|
e79d2ba60e | ||
|
|
97dc4b6cb2 | ||
|
|
6fabad9749 | ||
|
|
b475de7b48 | ||
|
|
820d2f00eb | ||
|
|
c89a63edbe | ||
|
|
ecf5e892e2 | ||
|
|
c858936e0c | ||
|
|
7f36f897f8 | ||
|
|
5855e97997 | ||
|
|
e155b632cd | ||
|
|
fbf15befb8 | ||
|
|
0a0708d2d6 | ||
|
|
673b95b4c4 | ||
|
|
75763b80f8 | ||
|
|
71e2a5b802 | ||
|
|
7cdda46070 | ||
|
|
eb81b9c88c | ||
|
|
9e5ef330fa | ||
|
|
458f80050c | ||
|
|
e455768352 | ||
|
|
db70f810ad | ||
|
|
561b5d7ab7 | ||
|
|
6bb45bc532 | ||
|
|
d6bbff47f0 | ||
|
|
41f45a4fe7 | ||
|
|
8ad3d1e8be | ||
|
|
f6e0033aae | ||
|
|
207849e48e | ||
|
|
c5738740fb | ||
|
|
a159e7c75b | ||
|
|
d9fe8d24b2 | ||
|
|
5275d613e6 | ||
|
|
1d5606b3d7 | ||
|
|
7f40d4fd4b | ||
|
|
94255855db | ||
|
|
1ac3399737 | ||
|
|
e56f8c5925 | ||
|
|
5f4f7070f4 | ||
|
|
3a2150e515 | ||
|
|
e2ae850866 | ||
|
|
b9b95717c6 | ||
|
|
78a1ccaf2f | ||
|
|
8d35dad5fc | ||
|
|
002b5bdac8 | ||
|
|
bbf48a7953 | ||
|
|
00b4388245 | ||
|
|
c92f53c1b0 | ||
|
|
9f4192a7c6 | ||
|
|
f5b23a67bd | ||
|
|
945a5623ef | ||
|
|
06b06dbced | ||
|
|
89b5666b02 | ||
|
|
d013b82c7f | ||
|
|
2f7bfe5dc7 | ||
|
|
5bb41bdccd | ||
|
|
1d80e83732 | ||
|
|
1448e7dddd | ||
|
|
3056b3efc0 | ||
|
|
f45491bddb | ||
|
|
b869f78b35 | ||
|
|
6aa99adf1c | ||
|
|
9be8507ebd | ||
|
|
5b0c789f6b | ||
|
|
c5a1be3b8e | ||
|
|
85d8cb15bb | ||
|
|
d3aabb0db3 | ||
|
|
169b594b28 | ||
|
|
df8d798551 | ||
|
|
3799f3c2f4 | ||
|
|
ffd3ba384e | ||
|
|
14c7979e4c | ||
|
|
1ac1a44fc5 | ||
|
|
f113fc7517 | ||
|
|
ce9a62c02c | ||
|
|
2ff015a89a | ||
|
|
452a9c1b31 | ||
|
|
3667c09d82 | ||
|
|
26aa19eeef | ||
|
|
8a600d30b3 | ||
|
|
45f10075b6 | ||
|
|
87983c5e6d | ||
|
|
c1e16696b1 | ||
|
|
fb947b1937 | ||
|
|
68cfcce3f1 | ||
|
|
43afb7c33d | ||
|
|
687884224c | ||
|
|
af3316278d | ||
|
|
260c6f5a10 | ||
|
|
2ed1855aa1 | ||
|
|
d6a024ed2d | ||
|
|
5a218e1465 | ||
|
|
1c26a2ea5c | ||
|
|
857b1e160e | ||
|
|
bf1b45c943 | ||
|
|
2e4d435f4f | ||
|
|
3732b2ee09 | ||
|
|
183efabca7 | ||
|
|
0575011529 | ||
|
|
453efac553 | ||
|
|
c78cbfc089 | ||
|
|
d647678c2e | ||
|
|
1a33c2be51 | ||
|
|
bc8fdd7ada | ||
|
|
144c388689 | ||
|
|
3be774bbf6 | ||
|
|
532188bd51 | ||
|
|
e36290b4ee | ||
|
|
8a709a845c | ||
|
|
05ec0c1b33 | ||
|
|
cae6677994 | ||
|
|
3444626084 | ||
|
|
a0c7b3f274 | ||
|
|
f684b4e4e0 | ||
|
|
14939faef6 | ||
|
|
3c04cb852f | ||
|
|
db6cfcb6ce | ||
|
|
637af70921 | ||
|
|
ada883c99f | ||
|
|
ef05e23b66 | ||
|
|
90e9d314a6 | ||
|
|
bb7e9e5891 | ||
|
|
01a6c555a1 | ||
|
|
3001069254 | ||
|
|
654867c3cf | ||
|
|
6582794408 | ||
|
|
d3882c34b2 | ||
|
|
02cd1bf7ff | ||
|
|
af73a93953 | ||
|
|
0e41675d1c | ||
|
|
0e0240085f | ||
|
|
09eea6edf6 | ||
|
|
33b8913031 | ||
|
|
761dcd20ae | ||
|
|
f9d91c7c4d | ||
|
|
535e68aea6 | ||
|
|
404fed58ec | ||
|
|
c6a0a13c45 | ||
|
|
a6ef72027b | ||
|
|
4ff55724dc | ||
|
|
397a79ba56 | ||
|
|
0d5bfe3751 | ||
|
|
80008aee11 | ||
|
|
32522114db | ||
|
|
6023e9216a | ||
|
|
34739819aa | ||
|
|
c89b18aa95 | ||
|
|
6db082bca2 | ||
|
|
906fbbd463 | ||
|
|
bc49f9cf8b | ||
|
|
06dd238993 | ||
|
|
d5d5fc0550 | ||
|
|
4efa05099d | ||
|
|
82eb69b3be | ||
|
|
92a9184ed3 | ||
|
|
0de80cf59c | ||
|
|
2304381b3d | ||
|
|
6b75822eb3 | ||
|
|
84dcebbefe | ||
|
|
676d49c605 | ||
|
|
2064434042 | ||
|
|
d94c4bb0a9 | ||
|
|
4a608caf3a | ||
|
|
deed81b963 | ||
|
|
eb36ae399e | ||
|
|
f173861b53 | ||
|
|
c704db2da5 | ||
|
|
bc0c17010e | ||
|
|
a3498c5aa1 | ||
|
|
f5b171979b | ||
|
|
3e019f5dac | ||
|
|
0a98eb0e79 | ||
|
|
13c92db760 | ||
|
|
7946fe54b5 | ||
|
|
90c4529d26 | ||
|
|
261f164127 | ||
|
|
c4431fe296 | ||
|
|
4b2e6341f9 | ||
|
|
6f9f99e86c | ||
|
|
091b2137d0 | ||
|
|
41ab2c0fa3 | ||
|
|
dafebc9a4d | ||
|
|
9891314b8e | ||
|
|
681527fa9d | ||
|
|
c0ce3b5209 | ||
|
|
28ffd444d6 | ||
|
|
fbf052ce5f | ||
|
|
2b10e85813 | ||
|
|
3ca8c53698 | ||
|
|
728368abf6 | ||
|
|
86cd23e906 | ||
|
|
3d6a9de9aa | ||
|
|
b061e135c2 | ||
|
|
1e129c5b70 | ||
|
|
70ff6dc788 | ||
|
|
c48c720f8a | ||
|
|
fc87c0497c | ||
|
|
54dda25adb | ||
|
|
ac92b77709 | ||
|
|
f181d30008 | ||
|
|
044a3625f9 | ||
|
|
aae6aeb142 | ||
|
|
d62d8a824e | ||
|
|
14be033b7f | ||
|
|
c64eee40b3 | ||
|
|
92a3c2688c | ||
|
|
61aa1c32e7 | ||
|
|
b33869a9f8 | ||
|
|
190153d068 | ||
|
|
74ae51f164 | ||
|
|
dff0182310 | ||
|
|
f5a078b68f | ||
|
|
e456e0fd99 | ||
|
|
896e289884 | ||
|
|
c33b6f2d98 | ||
|
|
5e4dc79f1f | ||
|
|
78a9018337 | ||
|
|
f438f63bf8 | ||
|
|
e68906ef56 | ||
|
|
0656292bef | ||
|
|
c16292e372 |
65 changed files with 1668 additions and 754 deletions
8
.gitignore
vendored
8
.gitignore
vendored
|
|
@ -4,8 +4,14 @@
|
|||
*.pyc
|
||||
.DS_Store
|
||||
.tox
|
||||
.idea
|
||||
.vscode
|
||||
MANIFEST
|
||||
build
|
||||
dist
|
||||
/tests/media/*
|
||||
!/tests/media/lenna.png
|
||||
!/tests/media/reference.png
|
||||
/venv
|
||||
/venv3
|
||||
/.env
|
||||
/tags
|
||||
|
|
|
|||
34
.travis.yml
34
.travis.yml
|
|
@ -1,7 +1,35 @@
|
|||
sudo: false
|
||||
|
||||
language: python
|
||||
python:
|
||||
- 2.7
|
||||
install: pip install tox --use-mirrors
|
||||
script: tox
|
||||
- "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"
|
||||
|
|
|
|||
8
AUTHORS
8
AUTHORS
|
|
@ -6,8 +6,8 @@ HZDG_.
|
|||
Maintainers
|
||||
-----------
|
||||
|
||||
* `Bryan Veloso`_
|
||||
* `Matthew Tretter`_
|
||||
* `Bryan Veloso`_
|
||||
* `Chris Drackett`_
|
||||
* `Greg Newman`_
|
||||
|
||||
|
|
@ -26,6 +26,9 @@ Contributors
|
|||
* `Jan Sagemüller`_
|
||||
* `Clay McClure`_
|
||||
* `Jannis Leidel`_
|
||||
* `Sean Bell`_
|
||||
* `Saul Shanabrook`_
|
||||
* `Venelin Stoykov`_
|
||||
|
||||
.. _Justin Driscoll: http://github.com/jdriscoll
|
||||
.. _HZDG: http://hzdg.com
|
||||
|
|
@ -45,3 +48,6 @@ Contributors
|
|||
.. _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
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ contributions merged as quickly as possible:
|
|||
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, use ``make test``. This will let you know about any errors or style
|
||||
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
|
||||
|
|
@ -21,4 +21,4 @@ __ 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/jdriscoll/django-imagekit/tree/develop/tests
|
||||
__ https://github.com/matthewwithanm/django-imagekit/tree/develop/tests
|
||||
|
|
|
|||
17
MANIFEST.in
17
MANIFEST.in
|
|
@ -1,5 +1,18 @@
|
|||
include AUTHORS
|
||||
include LICENSE
|
||||
include README.rst
|
||||
recursive-include docs *
|
||||
recursive-include imagekit/templates *
|
||||
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
|
||||
|
|
|
|||
96
README.rst
96
README.rst
|
|
@ -1,13 +1,22 @@
|
|||
|Build Status|_
|
||||
|
||||
.. |Build Status| image:: https://travis-ci.org/matthewwithanm/django-imagekit.svg?branch=develop
|
||||
.. _Build Status: https://travis-ci.org/matthewwithanm/django-imagekit
|
||||
|
||||
ImageKit is a Django app for processing images. Need a thumbnail? A
|
||||
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**
|
||||
`ImageKit on RTD`_. Our `changelog is also available`_.
|
||||
`ImageKit on RTD`_.
|
||||
|
||||
.. _`ImageKit on RTD`: http://django-imagekit.readthedocs.org
|
||||
.. _`changelog is also available`: http://django-imagekit.readthedocs.org/en/latest/changelog.html
|
||||
__ https://github.com/fish2000/instakit
|
||||
|
||||
|
||||
Installation
|
||||
|
|
@ -16,7 +25,6 @@ Installation
|
|||
1. Install `PIL`_ or `Pillow`_. (If you're using an ``ImageField`` in Django,
|
||||
you should have already done this.)
|
||||
2. ``pip install django-imagekit``
|
||||
(or clone the source and put the imagekit module on your path)
|
||||
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
|
||||
|
|
@ -31,6 +39,7 @@ Installation
|
|||
Usage Overview
|
||||
==============
|
||||
|
||||
.. _specs:
|
||||
|
||||
Specs
|
||||
-----
|
||||
|
|
@ -62,8 +71,8 @@ your model class:
|
|||
options={'quality': 60})
|
||||
|
||||
profile = Profile.objects.all()[0]
|
||||
print profile.avatar_thumbnail.url # > /media/CACHE/images/982d5af84cddddfd0fbf70892b4431e4.jpg
|
||||
print profile.avatar_thumbnail.width # > 100
|
||||
print(profile.avatar_thumbnail.url) # > /media/CACHE/images/982d5af84cddddfd0fbf70892b4431e4.jpg
|
||||
print(profile.avatar_thumbnail.width) # > 100
|
||||
|
||||
As you can probably tell, ImageSpecFields work a lot like Django's
|
||||
ImageFields. The difference is that they're automatically generated by
|
||||
|
|
@ -80,6 +89,7 @@ class:
|
|||
|
||||
from django.db import models
|
||||
from imagekit.models import ProcessedImageField
|
||||
from imagekit.processors import ResizeToFill
|
||||
|
||||
class Profile(models.Model):
|
||||
avatar_thumbnail = ProcessedImageField(upload_to='avatars',
|
||||
|
|
@ -88,8 +98,8 @@ class:
|
|||
options={'quality': 60})
|
||||
|
||||
profile = Profile.objects.all()[0]
|
||||
print profile.avatar_thumbnail.url # > /media/avatars/MY-avatar.jpg
|
||||
print profile.avatar_thumbnail.width # > 100
|
||||
print(profile.avatar_thumbnail.url) # > /media/avatars/MY-avatar.jpg
|
||||
print(profile.avatar_thumbnail.width) # > 100
|
||||
|
||||
This is pretty similar to our previous example. We don't need to specify a
|
||||
"source" any more since we're not processing another image field, but we do need
|
||||
|
|
@ -128,24 +138,29 @@ particularly when the processing being done depends on user input.
|
|||
format = 'JPEG'
|
||||
options = {'quality': 60}
|
||||
|
||||
It's probaby not surprising that this class is capable of processing an image
|
||||
It's probably not surprising that this class is capable of processing an image
|
||||
in the exact same way as our ImageSpecField above. However, unlike with the
|
||||
image spec model field, this class doesn't define what source the spec is acting
|
||||
on, or what should be done with the result; that's up to you:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
source_file = open('/path/to/myimage.jpg')
|
||||
source_file = open('/path/to/myimage.jpg', 'rb')
|
||||
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
|
||||
want—including a model's ``ImageField``.
|
||||
|
||||
The result of calling ``generate()`` on an image spec is a file-like object
|
||||
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
|
||||
|
||||
dest = open('/path/to/dest.jpg', 'w')
|
||||
dest = open('/path/to/dest.jpg', 'wb')
|
||||
dest.write(result.read())
|
||||
dest.close()
|
||||
|
||||
|
|
@ -172,7 +187,7 @@ to register it.
|
|||
|
||||
.. code-block:: python
|
||||
|
||||
from imagekit import ImageSpec
|
||||
from imagekit import ImageSpec, register
|
||||
from imagekit.processors import ResizeToFill
|
||||
|
||||
class Thumbnail(ImageSpec):
|
||||
|
|
@ -215,7 +230,7 @@ that's what we need to pass to use our thumbnail spec:
|
|||
|
||||
{% load imagekit %}
|
||||
|
||||
{% generateimage 'myapp:thumbnail' source=source_image %}
|
||||
{% generateimage 'myapp:thumbnail' source=source_file %}
|
||||
|
||||
This will output the following HTML:
|
||||
|
||||
|
|
@ -230,7 +245,7 @@ keyword args using two dashes:
|
|||
|
||||
{% load imagekit %}
|
||||
|
||||
{% generateimage 'myapp:thumbnail' source=source_image -- alt="A picture of Me" id="mypicture" %}
|
||||
{% 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:
|
||||
|
|
@ -239,7 +254,7 @@ assignment tag, providing access to the underlying file object:
|
|||
|
||||
{% load imagekit %}
|
||||
|
||||
{% generateimage 'myapp:thumbnail' source=source_image as th %}
|
||||
{% generateimage 'myapp:thumbnail' source=source_file as th %}
|
||||
<a href="{{ th.url }}">Click to download a cool {{ th.width }} x {{ th.height }} image!</a>
|
||||
|
||||
|
||||
|
|
@ -253,7 +268,7 @@ template tag:
|
|||
|
||||
{% load imagekit %}
|
||||
|
||||
{% thumbnail '100x50' source_image %}
|
||||
{% thumbnail '100x50' source_file %}
|
||||
|
||||
Like the generateimage tag, the thumbnail tag outputs an <img> tag:
|
||||
|
||||
|
|
@ -274,15 +289,15 @@ with the id "imagekit:thumbnail" which, by default, is
|
|||
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 generatethumbnail tag, you can also specify additional HTML
|
||||
attributes for the thumbnail tag, or use it as an assignment 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_image -- alt="A picture of Me" id="mypicture" %}
|
||||
{% thumbnail '100x50' source_image as th %}
|
||||
{% thumbnail '100x50' source_file -- alt="A picture of Me" id="mypicture" %}
|
||||
{% thumbnail '100x50' source_file as th %}
|
||||
|
||||
|
||||
Using Specs in Forms
|
||||
|
|
@ -365,6 +380,12 @@ it in your spec's ``processors`` list:
|
|||
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
|
||||
-----
|
||||
|
|
@ -386,6 +407,37 @@ Django admin classes:
|
|||
|
||||
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``.
|
||||
|
||||
|
|
@ -403,7 +455,7 @@ of generator ids in order to generate images selectively.
|
|||
Community
|
||||
=========
|
||||
|
||||
Please use `the GitHub issue tracker <https://github.com/jdriscoll/django-imagekit/issues>`_
|
||||
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.
|
||||
|
|
@ -425,5 +477,5 @@ Check out our `contributing guidelines`__ for more information about pitching in
|
|||
with ImageKit.
|
||||
|
||||
|
||||
__ https://github.com/jdriscoll/django-imagekit/issues?labels=contributor-friendly&state=open
|
||||
__ https://github.com/jdriscoll/django-imagekit/blob/master/CONTRIBUTING.rst
|
||||
__ https://github.com/matthewwithanm/django-imagekit/issues?labels=contributor-friendly&state=open
|
||||
__ https://github.com/matthewwithanm/django-imagekit/blob/develop/CONTRIBUTING.rst
|
||||
|
|
|
|||
|
|
@ -47,14 +47,13 @@ for creating an ``ImageSpec``, registering it, and associating it with an
|
|||
class Profile(models.Model):
|
||||
avatar = models.ImageField(upload_to='avatars')
|
||||
avatar_thumbnail = ImageSpecField(source='avatar',
|
||||
spec_id='myapp:profile:avatar_thumbnail')
|
||||
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
|
||||
|
|
@ -98,7 +97,7 @@ for getting this information.
|
|||
class Profile(models.Model):
|
||||
avatar = models.ImageField(upload_to='avatars')
|
||||
avatar_thumbnail = ImageSpecField(source='avatar',
|
||||
spec_id='myapp:profile:avatar_thumbnail')
|
||||
id='myapp:profile:avatar_thumbnail')
|
||||
thumbnail_width = models.PositiveIntegerField()
|
||||
thumbnail_height = models.PositiveIntegerField()
|
||||
|
||||
|
|
@ -109,107 +108,6 @@ 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.
|
||||
|
||||
|
||||
Optimizing
|
||||
==========
|
||||
|
||||
Unlike Django's ImageFields, ImageKit's ImageSpecFields and template tags don't
|
||||
persist any data in the database. Therefore, in order to know whether an image
|
||||
file needs to be generated, ImageKit needs to check if the file already exists
|
||||
(using the appropriate file storage object`__). The object responsible for
|
||||
performing these checks is called a *cache file backend*.
|
||||
|
||||
|
||||
Cache!
|
||||
------
|
||||
|
||||
By default, ImageKit checks for the existence of a cache file every time you
|
||||
attempt to use the file and, if it doesn't exist, creates it synchronously. This
|
||||
is a very safe behavior because it ensures that your ImageKit-generated images
|
||||
are always available. However, that's a lot of checking with storage and those
|
||||
kinds of operations can be slow—especially if you're using a remote storage—so
|
||||
you'll want to try to avoid them as much as possible.
|
||||
|
||||
Luckily, the default cache file backend makes use of Django's caching
|
||||
abilities to mitigate the number of checks it actually has to do; it will use
|
||||
the cache specified by the ``IMAGEKIT_CACHE_BACKEND`` to save the state of the
|
||||
generated file. If your Django project is running in debug mode
|
||||
(``settings.DEBUG`` is true), this will be a dummy cache by default. Otherwise,
|
||||
it will use your project's default cache.
|
||||
|
||||
In normal operation, your cache files will never be deleted; once they're
|
||||
created, they'll stay created. So the simplest optimization you can make is to
|
||||
set your ``IMAGEKIT_CACHE_BACKEND`` to a cache with a very long, or infinite,
|
||||
timeout.
|
||||
|
||||
|
||||
Even More Advanced
|
||||
------------------
|
||||
|
||||
For many applications—particularly those using local storage for generated image
|
||||
files—a cache with a long timeout is all the optimization you'll need. However,
|
||||
there may be times when that simply doesn't cut it. In these cases, you'll want
|
||||
to change when the generation is actually done.
|
||||
|
||||
The objects responsible for specifying when cache files are created are
|
||||
called *cache file strategies*. The default strategy can be set using the
|
||||
``IMAGEKIT_DEFAULT_CACHEFILE_STRATEGY`` setting, and its default value is
|
||||
`'imagekit.cachefiles.strategies.JustInTime'`. As we've already seen above,
|
||||
the "just in time" strategy determines whether a file needs to be generated each
|
||||
time it's accessed and, if it does, generates it synchronously (that is, as part
|
||||
of the request-response cycle).
|
||||
|
||||
Another strategy is to simply assume the file exists. This requires the fewest
|
||||
number of checks (zero!), so we don't have to worry about expensive IO. The
|
||||
strategy that takes this approach is
|
||||
``imagekit.cachefiles.strategies.Optimistic``. In order to use this
|
||||
strategy, either set the ``IMAGEKIT_DEFAULT_CACHEFILE_STRATEGY`` setting or,
|
||||
to use it on a per-generator basis, set the ``cachefile_strategy`` attribute
|
||||
of your spec or generator. Avoiding checking for file existence can be a real
|
||||
boon to performance, but it also means that ImageKit has no way to know when a
|
||||
file needs to be generated—well, at least not all the time.
|
||||
|
||||
With image specs, we can know at least some of the times that a new file needs
|
||||
to be generated: whenever the source image is created or changed. For this
|
||||
reason, the optimistic strategy defines callbacks for these events. Every
|
||||
source registered with ImageKit will automatically cause its specs' files to be
|
||||
generated when it is created or changed.
|
||||
|
||||
.. note::
|
||||
|
||||
In order to understand source registration, read :ref:`source-groups`
|
||||
|
||||
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.
|
||||
|
||||
|
||||
.. _source-groups:
|
||||
|
||||
Source Groups
|
||||
|
|
@ -224,7 +122,7 @@ 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 created, changed, or deleted, and
|
||||
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()``),
|
||||
|
|
@ -265,7 +163,7 @@ A simple example of a custom source group class is as follows:
|
|||
def files(self):
|
||||
os.chdir(self.dir)
|
||||
for name in glob.glob('*.jpg'):
|
||||
yield open(name)
|
||||
yield open(name, 'rb')
|
||||
|
||||
Instances of this class could then be registered with one or more spec id:
|
||||
|
||||
|
|
@ -279,6 +177,6 @@ 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_created` or
|
||||
`source_changed` signals, the corresponding cache file strategy callbacks
|
||||
would not be called for them.
|
||||
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
Normal file
256
docs/caching.rst
Normal file
|
|
@ -0,0 +1,256 @@
|
|||
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.
|
||||
|
|
@ -1,142 +0,0 @@
|
|||
Changelog
|
||||
=========
|
||||
|
||||
v2.0.2
|
||||
------
|
||||
|
||||
- Fixed the pickling of ImageSpecFieldFile.
|
||||
- Signals are now connected without specifying the class and non-IK models
|
||||
are filitered out in the receivers. This is necessary beacuse of a bug
|
||||
with how Django handles abstract models.
|
||||
- Fixed a `ZeroDivisionError` in the Reflection processor.
|
||||
- `cStringIO` is now used if it's available.
|
||||
- Reflections on images now use RGBA instead of RGB.
|
||||
|
||||
v2.0.1
|
||||
------
|
||||
|
||||
- Fixed a file descriptor leak in the `utils.quiet()` context manager.
|
||||
|
||||
|
||||
v2.0.0
|
||||
------
|
||||
|
||||
- Added the concept of image cache backends. Image cache backends assume
|
||||
control of validating and invalidating the cached images from `ImageSpec` in
|
||||
versions past. The default backend maintins the current behavior: invalidating
|
||||
an image deletes it, while validating checks whether the file exists and
|
||||
creates the file if it doesn't. One can create custom image cache backends to
|
||||
control how their images are cached (e.g., Celery, etc.).
|
||||
|
||||
ImageKit ships with three built-in backends:
|
||||
|
||||
- ``imagekit.imagecache.PessimisticImageCacheBackend`` - A very safe image
|
||||
cache backend. Guarantees that files will always be available, but at the
|
||||
cost of hitting the storage backend.
|
||||
- ``imagekit.imagecache.NonValidatingImageCacheBackend`` - A backend that is
|
||||
super optimistic about the existence of spec files. It will hit your file
|
||||
storage much less frequently than the pessimistic backend, but it is
|
||||
technically possible for a cache file to be missing after validation.
|
||||
- ``imagekit.imagecache.celery.CeleryImageCacheBackend`` - A pessimistic cache
|
||||
state backend that uses celery to generate its spec images. Like
|
||||
``PessimisticCacheStateBackend``, this one checks to see if the file
|
||||
exists on validation, so the storage is hit fairly frequently, but an
|
||||
image is guaranteed to exist. However, while validation guarantees the
|
||||
existence of *an* image, it does not necessarily guarantee that you will
|
||||
get the correct image, as the spec may be pending regeneration. In other
|
||||
words, while there are ``generate`` tasks in the queue, it is possible to
|
||||
get a stale spec image. The tradeoff is that calling ``invalidate()``
|
||||
won't block to interact with file storage.
|
||||
|
||||
- Some of the processors have been renamed and several new ones have been added:
|
||||
|
||||
- ``imagekit.processors.ResizeToFill`` - (previously
|
||||
``imagekit.processors.resize.Crop``) Scales the image to fill the provided
|
||||
dimensions and then trims away the excess.
|
||||
- ``imagekit.processors.ResizeToFit`` - (previously
|
||||
``imagekit.processors.resize.Fit``) Scale to fit the provided dimensions.
|
||||
- ``imagekit.processors.SmartResize`` - Like ``ResizeToFill``, but crops using
|
||||
entroy (``SmartCrop``) instead of an anchor argument.
|
||||
- ``imagekit.processors.BasicCrop`` - Crop using provided box.
|
||||
- ``imagekit.processors.SmartCrop`` - (previously
|
||||
``imagekit.processors.resize.SmartCrop``) Crop to provided size, trimming
|
||||
based on entropy.
|
||||
- ``imagekit.processors.TrimBorderColor`` - Trim the specified color from the
|
||||
specified sides.
|
||||
- ``imagekit.processors.AddBorder`` - Add a border of specific color and
|
||||
thickness to an image.
|
||||
- ``imagekit.processors.Resize`` - Scale to the provided dimensions (can distort).
|
||||
- ``imagekit.processors.ResizeToCover`` - Scale to the smallest size that will
|
||||
cover the specified dimensions. Used internally by ``Fill`` and
|
||||
``SmartFill``.
|
||||
- ``imagekit.processors.ResizeCanvas`` - Takes an image an resizes the canvas,
|
||||
using a specific background color if the new size is larger than the current
|
||||
image.
|
||||
|
||||
- ``mat_color`` has been added as an arguemnt to ``ResizeToFit``. If set, the
|
||||
the target image size will be enforced and the specified color will be
|
||||
used as background color to pad the image.
|
||||
|
||||
- We now use `Tox`_ to automate testing.
|
||||
|
||||
.. _`Tox`: http://pypi.python.org/pypi/tox
|
||||
|
||||
|
||||
v1.1.0
|
||||
------
|
||||
|
||||
- A ``SmartCrop`` resize processor was added. This allows an image to be
|
||||
cropped based on the amount of entropy in the target image's histogram.
|
||||
|
||||
- The ``quality`` argument was removed in favor of an ``options`` dictionary.
|
||||
This is a more general solution which grants access to PIL's format-specific
|
||||
options (including "quality", "progressive", and "optimize" for JPEGs).
|
||||
|
||||
- The ``TrimColor`` processor was renamed to ``TrimBorderColor``.
|
||||
|
||||
- The private ``_Resize`` class has been removed.
|
||||
|
||||
|
||||
v1.0.3
|
||||
------
|
||||
|
||||
- ``ImageSpec._create()`` was renamed ``ImageSpec.generate()`` and is now
|
||||
available in the public API.
|
||||
|
||||
- Added an ``AutoConvert`` processor to encapsulate the transparency
|
||||
handling logic.
|
||||
|
||||
- Refactored transparency handling to be smarter, handling a lot more of
|
||||
the situations in which one would convert to or from formats that support
|
||||
transparency.
|
||||
|
||||
- Fixed PIL zeroing out files when write mode is enabled.
|
||||
|
||||
|
||||
v1.0.2
|
||||
------
|
||||
|
||||
- Added this changelog.
|
||||
|
||||
- Enhanced extension detection, format detection, and conversion between the
|
||||
two. This eliminates the reliance on an image being loaded into memory
|
||||
beforehand in order to detect said image's extension.
|
||||
|
||||
- Fixed a regression from the 0.4.x series in which ImageKit was unable to
|
||||
convert a PNG file in ``P`` or "palette" mode to JPEG.
|
||||
|
||||
|
||||
v1.0.1
|
||||
------
|
||||
|
||||
- Minor fixes related to the rendering of ``README.rst`` as a reStructured
|
||||
text file.
|
||||
|
||||
- Fixed the included admin template not being found when ImageKit was and
|
||||
the packaging of the included admin templates.
|
||||
|
||||
|
||||
v1.0
|
||||
----
|
||||
|
||||
- Initial release of the *new* field-based ImageKit API.
|
||||
|
|
@ -54,7 +54,7 @@ execfile(os.path.join(os.path.dirname(__file__), '..', 'imagekit',
|
|||
# built documents.
|
||||
#
|
||||
# The short X.Y version.
|
||||
version = re.match('\d+\.\d+', pkgmeta['__version__']).group()
|
||||
version = re.match(r'\d+\.\d+', pkgmeta['__version__']).group()
|
||||
# The full version, including alpha/beta/rc tags.
|
||||
release = pkgmeta['__version__']
|
||||
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ Settings
|
|||
|
||||
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, the storage of the source file
|
||||
and none is specified by the spec definition, `your default file storage`__
|
||||
will be used.
|
||||
|
||||
|
||||
|
|
@ -44,11 +44,24 @@ Settings
|
|||
|
||||
.. attribute:: IMAGEKIT_CACHE_BACKEND
|
||||
|
||||
:default: If ``DEBUG`` is ``True``, ``'django.core.cache.backends.dummy.DummyCache'``.
|
||||
Otherwise, ``'default'``.
|
||||
:default: ``'default'``
|
||||
|
||||
The Django cache backend to be used to store information like the state of
|
||||
cached images (i.e. validated or not).
|
||||
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
|
||||
|
|
@ -72,3 +85,6 @@ Settings
|
|||
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
|
||||
|
|
|
|||
|
|
@ -20,5 +20,5 @@ Indices and tables
|
|||
|
||||
configuration
|
||||
advanced_usage
|
||||
changelog
|
||||
caching
|
||||
upgrading
|
||||
|
|
|
|||
|
|
@ -79,12 +79,9 @@ IK3 provides analogous settings for cache file backends and strategies:
|
|||
IMAGEKIT_DEFAULT_CACHEFILE_BACKEND = 'path.to.MyCacheFileBackend'
|
||||
IMAGEKIT_DEFAULT_CACHEFILE_STRATEGY = 'path.to.MyCacheFileStrategy'
|
||||
|
||||
See the documentation on `cache file backends`_ and `cache file strategies`_
|
||||
See the documentation on :ref:`cache file backends <cache-file-backend>` and :ref:`cache file strategies <cache-file-strategy>`
|
||||
for more details.
|
||||
|
||||
.. _`cache file backends`:
|
||||
.. _`cache file strategies`:
|
||||
|
||||
|
||||
Conditional model ``processors``
|
||||
--------------------------------
|
||||
|
|
@ -93,9 +90,7 @@ 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 `advanced usage`_ documentation for more.
|
||||
|
||||
.. _`advanced usage`:
|
||||
solution: the custom ``spec``. See the :doc:`advanced usage <advanced_usage>` documentation for more.
|
||||
|
||||
|
||||
Conditonal ``cache_to`` file names
|
||||
|
|
@ -109,6 +104,14 @@ 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 `specs`_ for more.
|
||||
unique file names. See the documentation on :ref:`specs` for more.
|
||||
|
||||
.. _`specs`:
|
||||
|
||||
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,5 +1,4 @@
|
|||
# flake8: noqa
|
||||
from . import importers
|
||||
from . import conf
|
||||
from . import generatorlibrary
|
||||
from .specs import ImageSpec
|
||||
|
|
|
|||
|
|
@ -1,9 +1,12 @@
|
|||
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 LazyObject
|
||||
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 before_access
|
||||
from ..signals import content_required, existence_required
|
||||
from ..utils import get_logger, get_singleton, generate, get_by_qname
|
||||
|
||||
|
||||
|
|
@ -15,7 +18,7 @@ class ImageCacheFile(BaseIKFile, ImageFile):
|
|||
to be deferred until the time that the cache file strategy requires it.
|
||||
|
||||
"""
|
||||
def __init__(self, generator, name=None, storage=None, cachefile_backend=None):
|
||||
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
|
||||
|
|
@ -23,33 +26,72 @@ class ImageCacheFile(BaseIKFile, ImageFile):
|
|||
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
|
||||
|
||||
name = name or getattr(generator, 'cachefile_name', None)
|
||||
if not name:
|
||||
fn = get_by_qname(settings.IMAGEKIT_CACHEFILE_NAMER, 'namer')
|
||||
name = fn(generator)
|
||||
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)
|
||||
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):
|
||||
before_access.send(sender=self, file=self)
|
||||
return super(ImageCacheFile, self)._require_file()
|
||||
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):
|
||||
if force:
|
||||
self._generate()
|
||||
else:
|
||||
self.cachefile_backend.ensure_exists(self)
|
||||
"""
|
||||
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
|
||||
|
|
@ -57,39 +99,85 @@ class ImageCacheFile(BaseIKFile, ImageFile):
|
|||
|
||||
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'
|
||||
' ensure_exists() instead of generate(), or there may be a'
|
||||
' race condition in the file backend %s. The saved file'
|
||||
' will not be used.' % (self.storage,
|
||||
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))
|
||||
self.cachefile_backend
|
||||
)
|
||||
)
|
||||
|
||||
def __bool__(self):
|
||||
if not self.name:
|
||||
return False
|
||||
|
||||
class LazyImageCacheFile(LazyObject):
|
||||
def __init__(self, generator_id, *args, **kwargs):
|
||||
super(LazyImageCacheFile, self).__init__()
|
||||
# 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)
|
||||
|
||||
def setup():
|
||||
generator = generator_registry.get(generator_id, *args, **kwargs)
|
||||
self._wrapped = ImageCacheFile(generator)
|
||||
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
|
||||
|
||||
self.__dict__['_setup'] = setup
|
||||
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):
|
||||
if self._wrapped is None:
|
||||
self._setup()
|
||||
return '<%s: %s>' % (self.__class__.__name__, self or 'None')
|
||||
return smart_str("<%s: %s>" % (
|
||||
self.__class__.__name__, self if self.name else "None")
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
if self._wrapped is None:
|
||||
self._setup()
|
||||
return str(self._wrapped)
|
||||
|
||||
def __unicode__(self):
|
||||
if self._wrapped is None:
|
||||
self._setup()
|
||||
return unicode(self._wrapped)
|
||||
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,22 +0,0 @@
|
|||
def generate(file):
|
||||
file.generate()
|
||||
|
||||
|
||||
try:
|
||||
from celery.task import task
|
||||
except ImportError:
|
||||
pass
|
||||
else:
|
||||
generate_task = task(generate)
|
||||
|
||||
|
||||
def generate_deferred(file):
|
||||
try:
|
||||
import celery # NOQA
|
||||
except:
|
||||
raise ImportError("Deferred validation requires the the 'celery' library")
|
||||
generate_task.delay(file)
|
||||
|
||||
|
||||
def clear_now(file):
|
||||
file.clear()
|
||||
|
|
@ -1,6 +1,14 @@
|
|||
from ..utils import get_singleton
|
||||
from django.core.cache import get_cache
|
||||
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():
|
||||
|
|
@ -10,55 +18,178 @@ def get_default_cachefile_backend():
|
|||
"""
|
||||
from django.conf import settings
|
||||
return get_singleton(settings.IMAGEKIT_DEFAULT_CACHEFILE_BACKEND,
|
||||
'file 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):
|
||||
from django.conf import settings
|
||||
self._cache = get_cache(settings.IMAGEKIT_CACHE_BACKEND)
|
||||
self._cache = get_cache()
|
||||
return self._cache
|
||||
|
||||
def get_key(self, file):
|
||||
from django.conf import settings
|
||||
return '%s%s-exists' % (settings.IMAGEKIT_CACHE_PREFIX, file.name)
|
||||
return sanitize_cache_key('%s%s-state' %
|
||||
(settings.IMAGEKIT_CACHE_PREFIX, file.name))
|
||||
|
||||
def file_exists(self, file):
|
||||
def get_state(self, file, check_if_unknown=True):
|
||||
key = self.get_key(file)
|
||||
exists = self.cache.get(key)
|
||||
if exists is None:
|
||||
exists = self._file_exists(file)
|
||||
self.cache.set(key, exists)
|
||||
return exists
|
||||
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 ensure_exists(self, file):
|
||||
if self.file_exists(file):
|
||||
self.create(file)
|
||||
self.cache.set(self.get_key(file), True)
|
||||
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.
|
||||
exists. Files are generated synchronously.
|
||||
|
||||
"""
|
||||
|
||||
def _file_exists(self, file):
|
||||
if not getattr(file, '_file', None):
|
||||
# No file on object. Have to check storage.
|
||||
return not file.storage.exists(file.name)
|
||||
return False
|
||||
def generate(self, file, force=False):
|
||||
self.generate_now(file, force=force)
|
||||
|
||||
def create(self, file):
|
||||
"""
|
||||
Generates a new image by running the processors on the source file.
|
||||
def _exists(self, file):
|
||||
return bool(getattr(file, '_file', None)
|
||||
or (file.name and file.storage.exists(file.name)))
|
||||
|
||||
"""
|
||||
file.generate(force=True)
|
||||
|
||||
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,4 +1,7 @@
|
|||
import six
|
||||
|
||||
from django.utils.functional import LazyObject
|
||||
from ..lib import force_text
|
||||
from ..utils import get_singleton
|
||||
|
||||
|
||||
|
|
@ -8,7 +11,10 @@ class JustInTime(object):
|
|||
|
||||
"""
|
||||
|
||||
def before_access(self, file):
|
||||
def on_existence_required(self, file):
|
||||
file.generate()
|
||||
|
||||
def on_content_required(self, file):
|
||||
file.generate()
|
||||
|
||||
|
||||
|
|
@ -20,11 +26,11 @@ class Optimistic(object):
|
|||
|
||||
"""
|
||||
|
||||
def on_source_created(self, file):
|
||||
def on_source_saved(self, file):
|
||||
file.generate()
|
||||
|
||||
def on_source_changed(self, file):
|
||||
file.generate()
|
||||
def should_verify_existence(self, file):
|
||||
return False
|
||||
|
||||
|
||||
class DictStrategy(object):
|
||||
|
|
@ -33,24 +39,11 @@ class DictStrategy(object):
|
|||
setattr(self, k, v)
|
||||
|
||||
|
||||
class StrategyWrapper(LazyObject):
|
||||
def __init__(self, strategy):
|
||||
if isinstance(strategy, basestring):
|
||||
strategy = get_singleton(strategy, 'cache file strategy')
|
||||
elif isinstance(strategy, dict):
|
||||
strategy = DictStrategy(strategy)
|
||||
elif callable(strategy):
|
||||
strategy = strategy()
|
||||
self._wrapped = strategy
|
||||
|
||||
def __getstate__(self):
|
||||
return {'_wrapped': self._wrapped}
|
||||
|
||||
def __setstate__(self, state):
|
||||
self._wrapped = state['_wrapped']
|
||||
|
||||
def __unicode__(self):
|
||||
return unicode(self._wrapped)
|
||||
|
||||
def __str__(self):
|
||||
return str(self._wrapped)
|
||||
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,5 +1,6 @@
|
|||
from appconf import AppConf
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
|
||||
|
||||
class ImageKitConf(AppConf):
|
||||
|
|
@ -9,15 +10,31 @@ class ImageKitConf(AppConf):
|
|||
DEFAULT_CACHEFILE_BACKEND = 'imagekit.cachefiles.backends.Simple'
|
||||
DEFAULT_CACHEFILE_STRATEGY = 'imagekit.cachefiles.strategies.JustInTime'
|
||||
|
||||
DEFAULT_FILE_STORAGE = 'django.core.files.storage.FileSystemStorage'
|
||||
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:
|
||||
if getattr(settings, 'CACHES', None):
|
||||
value = 'django.core.cache.backends.dummy.DummyCache' if settings.DEBUG else 'default'
|
||||
else:
|
||||
value = 'dummy://' if settings.DEBUG else settings.CACHE_BACKEND
|
||||
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
|
||||
|
|
|
|||
|
|
@ -13,6 +13,10 @@ class MissingGeneratorId(Exception):
|
|||
pass
|
||||
|
||||
|
||||
class MissingSource(ValueError):
|
||||
pass
|
||||
|
||||
|
||||
# Aliases for backwards compatibility
|
||||
UnknownExtensionError = UnknownExtension
|
||||
UnknownFormatError = UnknownFormat
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
from django.core.files.base import File, ContentFile
|
||||
from django.utils.encoding import smart_str, smart_unicode
|
||||
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
|
||||
|
||||
|
||||
|
|
@ -46,14 +49,25 @@ class BaseIKFile(File):
|
|||
|
||||
def _get_size(self):
|
||||
self._require_file()
|
||||
if not self._committed:
|
||||
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()
|
||||
self.file.open(mode)
|
||||
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)
|
||||
|
|
@ -92,4 +106,5 @@ class IKContentFile(ContentFile):
|
|||
return smart_str(self.file.name or '')
|
||||
|
||||
def __unicode__(self):
|
||||
return smart_unicode(self.file.name or u'')
|
||||
# Python 2
|
||||
return smart_text(self.file.name or '')
|
||||
|
|
|
|||
|
|
@ -22,8 +22,12 @@ class ProcessedImageField(ImageField, SpecHost):
|
|||
def clean(self, data, initial=None):
|
||||
data = super(ProcessedImageField, self).clean(data, initial)
|
||||
|
||||
if data:
|
||||
if data and data != initial:
|
||||
spec = self.get_spec(source=data)
|
||||
data = generate(spec)
|
||||
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
|
||||
|
|
|
|||
|
|
@ -4,9 +4,9 @@ from .specs import ImageSpec
|
|||
|
||||
|
||||
class Thumbnail(ImageSpec):
|
||||
def __init__(self, width=None, height=None, anchor=None, crop=None, **kwargs):
|
||||
def __init__(self, width=None, height=None, anchor=None, crop=None, upscale=None, **kwargs):
|
||||
self.processors = [ThumbnailProcessor(width, height, anchor=anchor,
|
||||
crop=crop)]
|
||||
crop=crop, upscale=upscale)]
|
||||
super(Thumbnail, self).__init__(**kwargs)
|
||||
|
||||
|
||||
|
|
|
|||
35
imagekit/hashers.py
Normal file
35
imagekit/hashers.py
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
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,29 +0,0 @@
|
|||
from django.utils.importlib import import_module
|
||||
import re
|
||||
import sys
|
||||
|
||||
|
||||
class ProcessorImporter(object):
|
||||
"""
|
||||
The processors were moved to the PILKit project so they could be used
|
||||
separtely from ImageKit (which has a bunch of Django dependencies). However,
|
||||
there's no real need to expose this fact (and we want to maintain backwards
|
||||
compatibility), so we proxy all "imagekit.processors" imports to
|
||||
"pilkit.processors" using this object.
|
||||
|
||||
"""
|
||||
pattern = re.compile(r'^imagekit\.processors((\..*)?)$')
|
||||
|
||||
def find_module(self, name, path=None):
|
||||
if self.pattern.match(name):
|
||||
return self
|
||||
|
||||
def load_module(self, name):
|
||||
if name in sys.modules:
|
||||
return sys.modules[name]
|
||||
|
||||
new_name = self.pattern.sub(r'pilkit.processors\1', name)
|
||||
return import_module(new_name)
|
||||
|
||||
|
||||
sys.meta_path.append(ProcessorImporter())
|
||||
|
|
@ -19,6 +19,34 @@ 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.')
|
||||
|
||||
try:
|
||||
from cStringIO import StringIO
|
||||
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 StringIO import StringIO
|
||||
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']
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
from django.core.management.base import BaseCommand
|
||||
import re
|
||||
from ...registry import generator_registry, cachefile_registry
|
||||
from ...exceptions import MissingSource
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
|
|
@ -13,23 +14,28 @@ 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()
|
||||
|
||||
if args:
|
||||
patterns = self.compile_patterns(args)
|
||||
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 file in cachefile_registry.get(generator_id):
|
||||
self.stdout.write(' %s\n' % file)
|
||||
try:
|
||||
# TODO: Allow other validation actions through command option
|
||||
file.generate()
|
||||
except Exception, err:
|
||||
# TODO: How should we handle failures? Don't want to error, but should call it out more than this.
|
||||
self.stdout.write(' FAILED: %s\n' % err)
|
||||
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]
|
||||
|
|
|
|||
|
|
@ -1,4 +1,8 @@
|
|||
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
|
||||
|
|
@ -7,16 +11,18 @@ from ...registry import register
|
|||
|
||||
|
||||
class SpecHostField(SpecHost):
|
||||
def set_spec_id(self, cls, name):
|
||||
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 getattr(self, 'spec_id', None):
|
||||
spec_id = (u'%s:%s:%s' % (cls._meta.app_label,
|
||||
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)
|
||||
# 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):
|
||||
|
|
@ -37,16 +43,42 @@ class ImageSpecField(SpecHostField):
|
|||
cachefile_strategy=cachefile_strategy, spec=spec,
|
||||
spec_id=id)
|
||||
|
||||
# TODO: Allow callable for source. See https://github.com/jdriscoll/django-imagekit/issues/158#issuecomment-10921664
|
||||
# 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):
|
||||
setattr(cls, name, ImageSpecFileDescriptor(self, name))
|
||||
self.set_spec_id(cls, name)
|
||||
# If the source field name isn't defined, figure it out.
|
||||
|
||||
# Add the model and field as a source for this spec id
|
||||
register.source_group(self.spec_id,
|
||||
ImageFieldSourceGroup(cls, self.source))
|
||||
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):
|
||||
|
|
@ -61,7 +93,7 @@ class ProcessedImageField(models.ImageField, SpecHostField):
|
|||
|
||||
def __init__(self, processors=None, format=None, options=None,
|
||||
verbose_name=None, name=None, width_field=None, height_field=None,
|
||||
autoconvert=True, spec=None, spec_id=None, **kwargs):
|
||||
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
|
||||
|
|
@ -69,6 +101,10 @@ class ProcessedImageField(models.ImageField, SpecHostField):
|
|||
: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)
|
||||
|
|
@ -76,13 +112,15 @@ class ProcessedImageField(models.ImageField, SpecHostField):
|
|||
height_field, **kwargs)
|
||||
|
||||
def contribute_to_class(self, cls, name):
|
||||
self.set_spec_id(cls, name)
|
||||
self._set_spec_id(cls, name)
|
||||
return super(ProcessedImageField, self).contribute_to_class(cls, name)
|
||||
|
||||
|
||||
try:
|
||||
from south.modelsinspector import add_introspection_rules
|
||||
except ImportError:
|
||||
pass
|
||||
else:
|
||||
add_introspection_rules([], [r'^imagekit\.models\.fields\.ProcessedImageField$'])
|
||||
# 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,34 +1,17 @@
|
|||
from ...cachefiles import ImageCacheFile
|
||||
from django.db.models.fields.files import ImageField
|
||||
|
||||
|
||||
class ImageSpecFileDescriptor(object):
|
||||
def __init__(self, field, attname):
|
||||
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:
|
||||
field_name = getattr(self.field, 'source', None)
|
||||
if field_name:
|
||||
source = getattr(instance, field_name)
|
||||
else:
|
||||
image_fields = [getattr(instance, f.attname) for f in
|
||||
instance.__class__._meta.fields if
|
||||
isinstance(f, ImageField)]
|
||||
if len(image_fields) == 0:
|
||||
raise Exception('%s does not define any ImageFields, so your'
|
||||
' %s ImageSpecField has no image to act on.' %
|
||||
(instance.__class__.__name__, self.attname))
|
||||
elif len(image_fields) > 1:
|
||||
raise Exception('%s defines multiple ImageFields, but you'
|
||||
' have not specified a source for your %s'
|
||||
' ImageSpecField.' % (instance.__class__.__name__,
|
||||
self.attname))
|
||||
else:
|
||||
source = image_fields[0]
|
||||
source = getattr(instance, self.source_field_name)
|
||||
spec = self.field.get_spec(source=source)
|
||||
file = ImageCacheFile(spec)
|
||||
instance.__dict__[self.attname] = file
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
__title__ = 'django-imagekit'
|
||||
__author__ = 'Justin Driscoll, Bryan Veloso, Greg Newman, Chris Drackett, Matthew Tretter, Eric Eldredge'
|
||||
__version__ = '3.0a3'
|
||||
__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__']
|
||||
|
|
|
|||
12
imagekit/processors/__init__.py
Normal file
12
imagekit/processors/__init__.py
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
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'
|
||||
]
|
||||
7
imagekit/processors/base.py
Normal file
7
imagekit/processors/base.py
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
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']
|
||||
7
imagekit/processors/crop.py
Normal file
7
imagekit/processors/crop.py
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
import warnings
|
||||
|
||||
from pilkit.processors.crop import *
|
||||
|
||||
warnings.warn('imagekit.processors.crop is deprecated use imagekit.processors instead', DeprecationWarning)
|
||||
|
||||
__all__ = ['TrimBorderColor', 'Crop', 'SmartCrop']
|
||||
7
imagekit/processors/resize.py
Normal file
7
imagekit/processors/resize.py
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
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']
|
||||
5
imagekit/processors/utils.py
Normal file
5
imagekit/processors/utils.py
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import warnings
|
||||
|
||||
from pilkit.processors.utils import *
|
||||
|
||||
warnings.warn('imagekit.processors.utils is deprecated use pilkit.processors.utils instead', DeprecationWarning)
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
from .exceptions import AlreadyRegistered, NotRegistered
|
||||
from .signals import before_access, source_created, source_changed, source_deleted
|
||||
from .utils import call_strategy_method
|
||||
from .signals import content_required, existence_required, source_saved
|
||||
from .utils import autodiscover, call_strategy_method
|
||||
|
||||
|
||||
class GeneratorRegistry(object):
|
||||
|
|
@ -12,16 +12,17 @@ class GeneratorRegistry(object):
|
|||
"""
|
||||
def __init__(self):
|
||||
self._generators = {}
|
||||
before_access.connect(self.before_access_receiver)
|
||||
content_required.connect(self.content_required_receiver)
|
||||
existence_required.connect(self.existence_required_receiver)
|
||||
|
||||
def register(self, id, generator):
|
||||
if id in self._generators:
|
||||
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, generator):
|
||||
# TODO: Either don't require the generator, or--if we do--assert that it's registered with the provided id
|
||||
def unregister(self, id):
|
||||
try:
|
||||
del self._generators[id]
|
||||
except KeyError:
|
||||
|
|
@ -29,6 +30,8 @@ class GeneratorRegistry(object):
|
|||
' registered' % id)
|
||||
|
||||
def get(self, id, **kwargs):
|
||||
autodiscover()
|
||||
|
||||
try:
|
||||
generator = self._generators[id]
|
||||
except KeyError:
|
||||
|
|
@ -40,15 +43,22 @@ class GeneratorRegistry(object):
|
|||
return generator
|
||||
|
||||
def get_ids(self):
|
||||
autodiscover()
|
||||
return self._generators.keys()
|
||||
|
||||
def before_access_receiver(self, sender, file, **kwargs):
|
||||
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(generator, 'before_access', file=file)
|
||||
call_strategy_method(file, callback)
|
||||
|
||||
|
||||
class SourceGroupRegistry(object):
|
||||
|
|
@ -62,9 +72,7 @@ class SourceGroupRegistry(object):
|
|||
|
||||
"""
|
||||
_signals = {
|
||||
source_created: 'on_source_created',
|
||||
source_changed: 'on_source_changed',
|
||||
source_deleted: 'on_source_deleted',
|
||||
source_saved: 'on_source_saved',
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
|
|
@ -105,7 +113,7 @@ class SourceGroupRegistry(object):
|
|||
|
||||
for spec in specs:
|
||||
file = ImageCacheFile(spec)
|
||||
call_strategy_method(spec, callback_name, file=file)
|
||||
call_strategy_method(file, callback_name)
|
||||
|
||||
|
||||
class CacheFileRegistry(object):
|
||||
|
|
@ -176,8 +184,8 @@ class Unregister(object):
|
|||
Unregister generators and generated files.
|
||||
|
||||
"""
|
||||
def generator(self, id, generator):
|
||||
generator_registry.unregister(id, generator)
|
||||
def generator(self, id):
|
||||
generator_registry.unregister(id)
|
||||
|
||||
def cachefiles(self, generator_id, cachefiles):
|
||||
cachefile_registry.unregister(generator_id, cachefiles)
|
||||
|
|
|
|||
|
|
@ -2,9 +2,8 @@ from django.dispatch import Signal
|
|||
|
||||
|
||||
# Generated file signals
|
||||
before_access = Signal()
|
||||
content_required = Signal()
|
||||
existence_required = Signal()
|
||||
|
||||
# Source group signals
|
||||
source_created = Signal()
|
||||
source_changed = Signal()
|
||||
source_deleted = Signal()
|
||||
source_saved = Signal()
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
from copy import copy
|
||||
from django.conf import settings
|
||||
from django.db.models.fields.files import ImageFieldFile
|
||||
from hashlib import md5
|
||||
import pickle
|
||||
from ..cachefiles.backends import get_default_cachefile_backend
|
||||
from ..cachefiles.strategies import StrategyWrapper
|
||||
from ..processors import ProcessorPipeline
|
||||
from ..utils import open_image, img_to_fobj, get_by_qname
|
||||
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
|
||||
|
||||
|
||||
|
|
@ -36,11 +36,18 @@ class BaseImageSpec(object):
|
|||
|
||||
def __init__(self):
|
||||
self.cachefile_backend = self.cachefile_backend or get_default_cachefile_backend()
|
||||
self.cachefile_strategy = StrategyWrapper(self.cachefile_strategy)
|
||||
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):
|
||||
"""
|
||||
|
|
@ -82,51 +89,81 @@ class ImageSpec(BaseImageSpec):
|
|||
|
||||
@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 = self.__dict__
|
||||
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 __setstate__(self, state):
|
||||
field_data = state.pop('_field_data', None)
|
||||
self.__dict__ = state
|
||||
if field_data:
|
||||
self.source = getattr(field_data['instance'], field_data['attname'])
|
||||
|
||||
def get_hash(self):
|
||||
return md5(pickle.dumps([
|
||||
return hashers.pickle([
|
||||
self.source.name,
|
||||
self.processors,
|
||||
self.format,
|
||||
self.options,
|
||||
self.autoconvert,
|
||||
])).hexdigest()
|
||||
])
|
||||
|
||||
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.)
|
||||
img = open_image(self.source)
|
||||
original_format = img.format
|
||||
|
||||
# Run the processors
|
||||
processors = self.processors
|
||||
img = ProcessorPipeline(processors or []).process(img)
|
||||
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()
|
||||
|
||||
options = dict(self.options or {})
|
||||
format = self.format or img.format or original_format or 'JPEG'
|
||||
content = img_to_fobj(img, format, **options)
|
||||
return content
|
||||
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):
|
||||
|
|
@ -188,7 +225,17 @@ class SpecHost(object):
|
|||
|
||||
"""
|
||||
self.spec_id = id
|
||||
register.generator(id, self._original_spec)
|
||||
|
||||
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):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -2,20 +2,19 @@
|
|||
Source groups are the means by which image spec sources are identified. They
|
||||
have two responsibilities:
|
||||
|
||||
1. To dispatch ``source_created``, ``source_changed``, and ``source_deleted``
|
||||
signals. (These will be relayed to the corresponding specs' cache file
|
||||
strategies.)
|
||||
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, post_delete
|
||||
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_created, source_changed, source_deleted
|
||||
from ..signals import source_saved
|
||||
from ..utils import get_nonabstract_descendants
|
||||
|
||||
|
||||
|
|
@ -48,7 +47,7 @@ class ModelSignalRouter(object):
|
|||
``ImageFieldSourceGroup``s. This class encapsulates that functionality.
|
||||
|
||||
Related:
|
||||
https://github.com/jdriscoll/django-imagekit/issues/126
|
||||
https://github.com/matthewwithanm/django-imagekit/issues/126
|
||||
https://code.djangoproject.com/ticket/9318
|
||||
|
||||
"""
|
||||
|
|
@ -58,7 +57,6 @@ class ModelSignalRouter(object):
|
|||
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)
|
||||
post_delete.connect(self.post_delete_receiver, dispatch_uid=uid)
|
||||
|
||||
def add(self, source_group):
|
||||
self._source_groups.append(source_group)
|
||||
|
|
@ -74,41 +72,45 @@ class ModelSignalRouter(object):
|
|||
|
||||
"""
|
||||
self.init_instance(instance)
|
||||
instance._ik['source_hashes'] = dict((attname, hash(file_field))
|
||||
for attname, file_field in self.get_field_dict(instance).items())
|
||||
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_field_dict(self, instance):
|
||||
def get_source_fields(self, instance):
|
||||
"""
|
||||
Returns the source fields for the given instance, in a dictionary whose
|
||||
keys are the field names and values are the fields themselves.
|
||||
Returns a list of the source fields for the given instance.
|
||||
|
||||
"""
|
||||
return dict((src.image_field, getattr(instance, src.image_field)) for
|
||||
src in self._source_groups if isinstance(instance, src.model_class))
|
||||
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, raw=False, **kwargs):
|
||||
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, file in self.get_field_dict(instance).items():
|
||||
if created:
|
||||
self.dispatch_signal(source_created, file, sender, instance,
|
||||
attname)
|
||||
elif old_hashes[attname] != new_hashes[attname]:
|
||||
self.dispatch_signal(source_changed, file, sender, instance,
|
||||
attname)
|
||||
for attname in self.get_source_fields(instance):
|
||||
if update_fields and attname not in update_fields:
|
||||
continue
|
||||
|
||||
@ik_model_receiver
|
||||
def post_delete_receiver(self, sender, instance=None, **kwargs):
|
||||
for attname, file in self.get_field_dict(instance).items():
|
||||
self.dispatch_signal(source_deleted, file, sender, instance, attname)
|
||||
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.update_source_hashes(instance)
|
||||
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):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -1,9 +1,13 @@
|
|||
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 ..compat import parse_bits
|
||||
from ..cachefiles import ImageCacheFile
|
||||
from ..registry import generator_registry
|
||||
from ..lib import force_text
|
||||
|
||||
|
||||
register = template.Library()
|
||||
|
|
@ -28,7 +32,7 @@ def parse_dimensions(dimensions):
|
|||
will be None for that value.
|
||||
|
||||
"""
|
||||
width, height = [d.strip() or None for d in dimensions.split('x')]
|
||||
width, height = [d.strip() and int(d) or None for d in dimensions.split('x')]
|
||||
return dict(width=width, height=height)
|
||||
|
||||
|
||||
|
|
@ -40,12 +44,9 @@ class GenerateImageAssignmentNode(template.Node):
|
|||
self._variable_name = variable_name
|
||||
|
||||
def get_variable_name(self, context):
|
||||
return unicode(self._variable_name)
|
||||
return force_text(self._variable_name)
|
||||
|
||||
def render(self, context):
|
||||
from ..utils import autodiscover
|
||||
autodiscover()
|
||||
|
||||
variable_name = self.get_variable_name(context)
|
||||
context[variable_name] = get_cachefile(context, self._generator_id,
|
||||
self._generator_kwargs)
|
||||
|
|
@ -60,9 +61,6 @@ class GenerateImageTagNode(template.Node):
|
|||
self._html_attrs = html_attrs
|
||||
|
||||
def render(self, context):
|
||||
from ..utils import autodiscover
|
||||
autodiscover()
|
||||
|
||||
file = get_cachefile(context, self._generator_id,
|
||||
self._generator_kwargs)
|
||||
attrs = dict((k, v.resolve(context)) for k, v in
|
||||
|
|
@ -76,7 +74,7 @@ class GenerateImageTagNode(template.Node):
|
|||
attrs['src'] = file.url
|
||||
attr_str = ' '.join('%s="%s"' % (escape(k), escape(v)) for k, v in
|
||||
attrs.items())
|
||||
return mark_safe(u'<img %s />' % attr_str)
|
||||
return mark_safe('<img %s />' % attr_str)
|
||||
|
||||
|
||||
class ThumbnailAssignmentNode(template.Node):
|
||||
|
|
@ -89,12 +87,9 @@ class ThumbnailAssignmentNode(template.Node):
|
|||
self._generator_kwargs = generator_kwargs
|
||||
|
||||
def get_variable_name(self, context):
|
||||
return unicode(self._variable_name)
|
||||
return force_text(self._variable_name)
|
||||
|
||||
def render(self, context):
|
||||
from ..utils import autodiscover
|
||||
autodiscover()
|
||||
|
||||
variable_name = self.get_variable_name(context)
|
||||
|
||||
generator_id = self._generator_id.resolve(context) if self._generator_id else DEFAULT_THUMBNAIL_GENERATOR
|
||||
|
|
@ -119,9 +114,6 @@ class ThumbnailImageTagNode(template.Node):
|
|||
self._html_attrs = html_attrs
|
||||
|
||||
def render(self, context):
|
||||
from ..utils import autodiscover
|
||||
autodiscover()
|
||||
|
||||
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
|
||||
|
|
@ -143,7 +135,7 @@ class ThumbnailImageTagNode(template.Node):
|
|||
attrs['src'] = file.url
|
||||
attr_str = ' '.join('%s="%s"' % (escape(k), escape(v)) for k, v in
|
||||
attrs.items())
|
||||
return mark_safe(u'<img %s />' % attr_str)
|
||||
return mark_safe('<img %s />' % attr_str)
|
||||
|
||||
|
||||
def parse_ik_tag_bits(parser, bits):
|
||||
|
|
|
|||
|
|
@ -1,27 +1,23 @@
|
|||
from __future__ import unicode_literals
|
||||
import logging
|
||||
import re
|
||||
from tempfile import NamedTemporaryFile
|
||||
from hashlib import md5
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.core.files import File
|
||||
from django.db.models.loading import cache
|
||||
from django.utils.importlib import import_module
|
||||
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 get_spec_files(instance):
|
||||
try:
|
||||
return instance._ik.spec_files
|
||||
except AttributeError:
|
||||
return []
|
||||
|
||||
|
||||
def _get_models(apps):
|
||||
models = []
|
||||
for app_label in apps or []:
|
||||
app = cache.get_app(app_label)
|
||||
models += [m for m in cache.get_models(app)]
|
||||
return models
|
||||
bad_memcached_key_chars = re.compile('[\u0000-\u001f\\s]+')
|
||||
|
||||
_autodiscovered = False
|
||||
|
||||
def get_nonabstract_descendants(model):
|
||||
""" Returns all non-abstract descendants of the model. """
|
||||
|
|
@ -40,7 +36,7 @@ def get_by_qname(path, desc):
|
|||
module, objname = path[:dot], path[dot + 1:]
|
||||
try:
|
||||
mod = import_module(module)
|
||||
except ImportError, e:
|
||||
except ImportError as e:
|
||||
raise ImproperlyConfigured('Error importing %s module %s: "%s"' %
|
||||
(desc, module, e))
|
||||
try:
|
||||
|
|
@ -71,28 +67,60 @@ def autodiscover():
|
|||
|
||||
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
|
||||
from django.utils.importlib import import_module
|
||||
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:
|
||||
mod = import_module(app)
|
||||
# Attempt to import the app's admin module.
|
||||
# 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:
|
||||
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
|
||||
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(logging.NullHandler())
|
||||
logger.addHandler(NullHandler())
|
||||
return logger
|
||||
|
||||
|
||||
|
|
@ -122,20 +150,42 @@ def generate(generator):
|
|||
|
||||
"""
|
||||
content = generator.generate()
|
||||
|
||||
# If the file doesn't have a name, Django will raise an Exception while
|
||||
# trying to save it, so we create a named temporary file.
|
||||
if not getattr(content, 'name', None):
|
||||
f = NamedTemporaryFile()
|
||||
f.write(content.read())
|
||||
f.seek(0)
|
||||
content = f
|
||||
|
||||
return File(content)
|
||||
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(generator, method_name, *args, **kwargs):
|
||||
strategy = getattr(generator, 'cachefile_strategy', None)
|
||||
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(*args, **kwargs)
|
||||
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
|
||||
|
|
|
|||
2
setup.cfg
Normal file
2
setup.cfg
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
[bdist_wheel]
|
||||
universal = 1
|
||||
47
setup.py
47
setup.py
|
|
@ -1,4 +1,4 @@
|
|||
#/usr/bin/env python
|
||||
#!/usr/bin/env python
|
||||
import codecs
|
||||
import os
|
||||
from setuptools import setup, find_packages
|
||||
|
|
@ -7,23 +7,27 @@ import sys
|
|||
|
||||
# Workaround for multiprocessing/nose issue. See http://bugs.python.org/msg170215
|
||||
try:
|
||||
import multiprocessing
|
||||
import multiprocessing # NOQA
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
|
||||
if 'publish' in sys.argv:
|
||||
os.system('python setup.py sdist upload')
|
||||
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 = {}
|
||||
execfile(os.path.join(os.path.dirname(__file__),
|
||||
'imagekit', 'pkgmeta.py'), pkgmeta)
|
||||
exec_file(os.path.join(os.path.dirname(__file__),
|
||||
'imagekit', 'pkgmeta.py'), pkgmeta)
|
||||
|
||||
|
||||
setup(
|
||||
|
|
@ -31,27 +35,33 @@ setup(
|
|||
version=pkgmeta['__version__'],
|
||||
description='Automated image processing for Django models.',
|
||||
long_description=read(os.path.join(os.path.dirname(__file__), 'README.rst')),
|
||||
author='Justin Driscoll',
|
||||
author_email='justin@driscolldev.com',
|
||||
author='Matthew Tretter',
|
||||
author_email='m@tthewwithanm.com',
|
||||
maintainer='Bryan Veloso',
|
||||
maintainer_email='bryan@revyver.com',
|
||||
license='BSD',
|
||||
url='http://github.com/jdriscoll/django-imagekit/',
|
||||
packages=find_packages(),
|
||||
url='http://github.com/matthewwithanm/django-imagekit/',
|
||||
packages=find_packages(exclude=['*.tests', '*.tests.*', 'tests.*', 'tests']),
|
||||
zip_safe=False,
|
||||
include_package_data=True,
|
||||
tests_require=[
|
||||
'beautifulsoup4==4.1.3',
|
||||
'nose==1.2.1',
|
||||
'nose-progressive==1.3',
|
||||
'django-nose==1.1',
|
||||
'Pillow==1.7.8',
|
||||
'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',
|
||||
'pilkit>=0.2.0',
|
||||
'six',
|
||||
],
|
||||
extras_require={
|
||||
'async': ['django-celery>=3.0'],
|
||||
'async_rq': ['django-rq>=0.6.0'],
|
||||
},
|
||||
classifiers=[
|
||||
'Development Status :: 5 - Production/Stable',
|
||||
'Environment :: Web Environment',
|
||||
|
|
@ -59,9 +69,12 @@ setup(
|
|||
'Intended Audience :: Developers',
|
||||
'License :: OSI Approved :: BSD License',
|
||||
'Operating System :: OS Independent',
|
||||
'Programming Language :: Python :: 2.5',
|
||||
'Programming Language :: Python :: 2.6',
|
||||
'Programming Language :: Python :: 2',
|
||||
'Programming Language :: Python :: 2.7',
|
||||
'Programming Language :: Python :: 3',
|
||||
'Programming Language :: Python :: 3.3',
|
||||
'Programming Language :: Python :: 3.4',
|
||||
'Programming Language :: Python :: 3.5',
|
||||
'Topic :: Utilities'
|
||||
],
|
||||
)
|
||||
|
|
|
|||
|
|
@ -16,4 +16,7 @@ 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)
|
||||
|
|
|
|||
Binary file not shown.
|
Before Width: | Height: | Size: 463 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 198 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 206 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 463 KiB |
BIN
tests/media/reference.png
Normal file
BIN
tests/media/reference.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 117 KiB |
|
|
@ -1,10 +1,17 @@
|
|||
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')
|
||||
|
||||
|
|
@ -12,9 +19,10 @@ class ImageModel(models.Model):
|
|||
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)], source='original_image', format='JPEG',
|
||||
options={'quality': 90})
|
||||
ResizeToFill(50, 50)], format='JPEG',
|
||||
options={'quality': 90})
|
||||
|
||||
smartcropped_thumbnail = ImageSpecField([Adjust(contrast=1.2,
|
||||
sharpness=1.1), SmartCrop(50, 50)], source='original_image',
|
||||
|
|
@ -26,20 +34,24 @@ class ProcessedImageFieldModel(models.Model):
|
|||
options={'quality': 90}, upload_to='p')
|
||||
|
||||
|
||||
class ProcessedImageFieldWithSpecModel(models.Model):
|
||||
processed = ProcessedImageField(spec=Thumbnail, upload_to='p')
|
||||
|
||||
|
||||
class CountingCacheFileStrategy(object):
|
||||
def __init__(self):
|
||||
self.before_access_count = 0
|
||||
self.on_source_changed_count = 0
|
||||
self.on_source_created_count = 0
|
||||
self.on_existence_required_count = 0
|
||||
self.on_content_required_count = 0
|
||||
self.on_source_saved_count = 0
|
||||
|
||||
def before_access(self, file):
|
||||
self.before_access_count += 1
|
||||
def on_existence_required(self, file):
|
||||
self.on_existence_required_count += 1
|
||||
|
||||
def on_source_changed(self, file):
|
||||
self.on_source_changed_count += 1
|
||||
def on_content_required(self, file):
|
||||
self.on_content_required_count += 1
|
||||
|
||||
def on_source_created(self, file):
|
||||
self.on_source_created_count += 1
|
||||
def on_source_saved(self, file):
|
||||
self.on_source_saved_count += 1
|
||||
|
||||
|
||||
class AbstractImageModel(models.Model):
|
||||
|
|
|
|||
|
|
@ -21,6 +21,8 @@ DATABASES = {
|
|||
},
|
||||
}
|
||||
|
||||
SECRET_KEY = '_uobce43e5osp8xgzle*yag2_16%y$sf*5(12vfg25hpnxik_*'
|
||||
|
||||
INSTALLED_APPS = [
|
||||
'django.contrib.auth',
|
||||
'django.contrib.contenttypes',
|
||||
|
|
@ -32,7 +34,6 @@ INSTALLED_APPS = [
|
|||
TEST_RUNNER = 'django_nose.NoseTestSuiteRunner'
|
||||
NOSE_ARGS = [
|
||||
'-s',
|
||||
'--with-progressive',
|
||||
|
||||
# When the tests are run --with-coverage, these args configure coverage
|
||||
# reporting (requires coverage to be installed).
|
||||
|
|
@ -43,6 +44,26 @@ NOSE_ARGS = [
|
|||
'--cover-html-dir=%s' % os.path.join(BASE_PATH, 'cover')
|
||||
]
|
||||
|
||||
DEBUG = True
|
||||
TEMPLATE_DEBUG = DEBUG
|
||||
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,27 +1,7 @@
|
|||
from django.core.files import File
|
||||
from imagekit.signals import source_created
|
||||
from imagekit.specs.sourcegroups import ImageFieldSourceGroup
|
||||
from imagekit.utils import get_nonabstract_descendants
|
||||
from nose.tools import eq_
|
||||
from . models import (AbstractImageModel, ConcreteImageModel,
|
||||
ConcreteImageModelSubclass)
|
||||
from .utils import get_image_file
|
||||
|
||||
|
||||
def test_source_created_signal():
|
||||
source_group = ImageFieldSourceGroup(AbstractImageModel, 'original_image')
|
||||
count = [0]
|
||||
|
||||
def receiver(sender, *args, **kwargs):
|
||||
if sender is source_group:
|
||||
count[0] += 1
|
||||
|
||||
source_created.connect(receiver, dispatch_uid='test_source_created')
|
||||
instance = ConcreteImageModel()
|
||||
img = File(get_image_file())
|
||||
instance.original_image.save('test_source_created_signal.jpg', img)
|
||||
|
||||
eq_(count[0], 1)
|
||||
ConcreteImageModelSubclass)
|
||||
|
||||
|
||||
def test_nonabstract_descendants_generator():
|
||||
|
|
|
|||
115
tests/test_cachefiles.py
Normal file
115
tests/test_cachefiles.py
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
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>')
|
||||
25
tests/test_closing_fieldfiles.py
Normal file
25
tests/test_closing_fieldfiles.py
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
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()
|
||||
|
|
@ -5,7 +5,9 @@ from imagekit import forms as ikforms
|
|||
from imagekit.processors import SmartCrop
|
||||
from nose.tools import eq_
|
||||
from . import imagegenerators # noqa
|
||||
from .models import ProcessedImageFieldModel, ImageModel
|
||||
from .models import (ProcessedImageFieldModel,
|
||||
ProcessedImageFieldWithSpecModel,
|
||||
ImageModel)
|
||||
from .utils import get_image_file
|
||||
|
||||
|
||||
|
|
@ -19,6 +21,16 @@ def test_model_processedimagefield():
|
|||
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',
|
||||
|
|
@ -26,6 +38,7 @@ def test_form_processedimagefield():
|
|||
|
||||
class Meta:
|
||||
model = ImageModel
|
||||
fields = 'image',
|
||||
|
||||
upload_file = get_image_file()
|
||||
file_dict = {'image': SimpleUploadedFile('abc.jpg', upload_file.read())}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
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
|
||||
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)
|
||||
|
|
@ -15,6 +16,7 @@ def test_img_tag():
|
|||
|
||||
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')
|
||||
|
||||
|
|
@ -28,7 +30,7 @@ def test_dangling_html_attrs_delimiter():
|
|||
@raises(TemplateSyntaxError)
|
||||
def test_html_attrs_assignment():
|
||||
"""
|
||||
You can either use generateimage as an assigment tag or specify html attrs,
|
||||
You can either use generateimage as an assignment tag or specify html attrs,
|
||||
but not both.
|
||||
|
||||
"""
|
||||
|
|
@ -42,11 +44,13 @@ def test_single_dimension_attr():
|
|||
|
||||
"""
|
||||
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 }}"""
|
||||
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(), '')
|
||||
|
|
|
|||
16
tests/test_no_extra_queries.py
Normal file
16
tests/test_no_extra_queries.py
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
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)
|
||||
49
tests/test_optimistic_strategy.py
Normal file
49
tests/test_optimistic_strategy.py
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
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)
|
||||
|
|
@ -4,10 +4,40 @@ deserialized. This is important when using IK with Celery.
|
|||
|
||||
"""
|
||||
|
||||
from .utils import create_photo, pickleback
|
||||
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
|
||||
|
|
|
|||
55
tests/test_sourcegroups.py
Normal file
55
tests/test_sourcegroups.py
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
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,11 +1,12 @@
|
|||
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
|
||||
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)
|
||||
|
|
@ -15,6 +16,7 @@ def test_img_tag():
|
|||
|
||||
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')
|
||||
|
||||
|
|
@ -40,7 +42,7 @@ def test_too_many_args():
|
|||
@raises(TemplateSyntaxError)
|
||||
def test_html_attrs_assignment():
|
||||
"""
|
||||
You can either use thumbnail as an assigment tag or specify html attrs,
|
||||
You can either use thumbnail as an assignment tag or specify html attrs,
|
||||
but not both.
|
||||
|
||||
"""
|
||||
|
|
@ -50,17 +52,20 @@ def test_html_attrs_assignment():
|
|||
|
||||
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')
|
||||
|
|
|
|||
|
|
@ -1,23 +0,0 @@
|
|||
from imagekit.exceptions import UnknownFormatError, UnknownExtensionError
|
||||
from imagekit.utils import extension_to_format, format_to_extension
|
||||
from nose.tools import eq_, raises
|
||||
|
||||
|
||||
def test_extension_to_format():
|
||||
eq_(extension_to_format('.jpeg'), 'JPEG')
|
||||
eq_(extension_to_format('.rgba'), 'SGI')
|
||||
|
||||
|
||||
def test_format_to_extension_no_init():
|
||||
eq_(format_to_extension('PNG'), '.png')
|
||||
eq_(format_to_extension('ICO'), '.ico')
|
||||
|
||||
|
||||
@raises(UnknownFormatError)
|
||||
def test_unknown_format():
|
||||
format_to_extension('TXT')
|
||||
|
||||
|
||||
@raises(UnknownExtensionError)
|
||||
def test_unknown_extension():
|
||||
extension_to_format('.txt')
|
||||
|
|
@ -1,10 +1,15 @@
|
|||
from bs4 import BeautifulSoup
|
||||
import os
|
||||
from django.conf import settings
|
||||
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
|
||||
|
||||
|
||||
|
|
@ -14,12 +19,19 @@ def get_image_file():
|
|||
|
||||
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, 'lenna.png')
|
||||
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())
|
||||
|
||||
|
|
@ -52,4 +64,43 @@ def render_tag(ttag):
|
|||
|
||||
|
||||
def get_html_attrs(ttag):
|
||||
return BeautifulSoup(render_tag(ttag)).img.attrs
|
||||
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)
|
||||
|
|
|
|||
41
tox.ini
41
tox.ini
|
|
@ -1,37 +1,18 @@
|
|||
[tox]
|
||||
envlist =
|
||||
py27-django14, py27-django13, py27-django12,
|
||||
py26-django14, py26-django13, py26-django12
|
||||
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
|
||||
|
||||
[testenv:py27-django14]
|
||||
basepython = python2.7
|
||||
deps =
|
||||
Django>=1.4,<1.5
|
||||
|
||||
[testenv:py27-django13]
|
||||
basepython = python2.7
|
||||
deps =
|
||||
Django>=1.3,<1.4
|
||||
|
||||
[testenv:py27-django12]
|
||||
basepython = python2.7
|
||||
deps =
|
||||
Django>=1.2,<1.3
|
||||
|
||||
[testenv:py26-django14]
|
||||
basepython = python2.6
|
||||
deps =
|
||||
Django>=1.4,<1.5
|
||||
|
||||
[testenv:py26-django13]
|
||||
basepython = python2.6
|
||||
deps =
|
||||
Django>=1.3,<1.4
|
||||
|
||||
[testenv:py26-django12]
|
||||
basepython = python2.6
|
||||
deps =
|
||||
Django>=1.2,<1.3
|
||||
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