jdriscoll / django-imagekit
Automates image processing for Django models. Resize, process and cache multiple versions of your image files. Access newly created files with a standard API. Supports alternate storage schemes such as Amazon S3.
$ hg clone http://hg.driscolldev.com/django-imagekit
| commit 70: | 120f9bb88318 |
| parent 69: | 2cfa7f099dd8 |
| branch: | default |
Changed (Δ319 bytes):
imagekit/defaults.py (4 lines added, 4 lines removed)
imagekit/lib.py (binary file changed)
imagekit/models.py (10 lines added, 10 lines removed)
imagekit/options.py (3 lines added, 3 lines removed)
imagekit/processors.py (12 lines added, 12 lines removed)
imagekit/specs.py (11 lines added, 11 lines removed)
imagekit/tests.py (13 lines added, 13 lines removed)
Up to file-list imagekit/defaults.py:
2 |
2 |
|
3 |
3 |
from imagekit.specs import ImageSpec |
4 |
4 |
from imagekit import processors |
5 |
||
5 |
||
6 |
6 |
class ResizeThumbnail(processors.Resize): |
7 |
7 |
width = 100 |
8 |
8 |
height = 50 |
9 |
9 |
crop = True |
10 |
||
10 |
||
11 |
11 |
class EnhanceSmall(processors.Adjustment): |
12 |
12 |
contrast = 1.2 |
13 |
13 |
sharpness = 1.1 |
14 |
||
14 |
||
15 |
15 |
class SampleReflection(processors.Reflection): |
16 |
16 |
size = 0.5 |
17 |
17 |
background_color = "#000000" |
| … | … | @@ -19,7 +19,7 @@ class SampleReflection(processors.Reflec |
19 |
19 |
class PNGFormat(processors.Format): |
20 |
20 |
format = 'PNG' |
21 |
21 |
extension = 'png' |
22 |
||
22 |
||
23 |
23 |
class DjangoAdminThumbnail(ImageSpec): |
24 |
24 |
access_as = 'admin_thumbnail' |
25 |
25 |
processors = [ResizeThumbnail, EnhanceSmall, SampleReflection, PNGFormat] |
Up to file-list imagekit/lib.py:
Up to file-list imagekit/models.py:
| … | … | @@ -32,10 +32,10 @@ CROP_VERT_CHOICES = ( |
32 |
32 |
|
33 |
33 |
class ImageModelBase(ModelBase): |
34 |
34 |
""" ImageModel metaclass |
35 |
||
35 |
||
36 |
36 |
This metaclass parses IKOptions and loads the specified specification |
37 |
37 |
module. |
38 |
||
38 |
||
39 |
39 |
""" |
40 |
40 |
def __init__(cls, name, bases, attrs): |
41 |
41 |
parents = [b for b in bases if isinstance(b, ImageModelBase)] |
| … | … | @@ -47,7 +47,7 @@ class ImageModelBase(ModelBase): |
47 |
47 |
module = __import__(opts.spec_module, {}, {}, ['']) |
48 |
48 |
except ImportError: |
49 |
49 |
raise ImportError('Unable to load imagekit config module: %s' % \ |
50 |
opts.spec_module) |
|
50 |
opts.spec_module) |
|
51 |
51 |
for spec in [spec for spec in module.__dict__.values() \ |
52 |
52 |
if isinstance(spec, type) \ |
53 |
53 |
and issubclass(spec, specs.ImageSpec) \ |
| … | … | @@ -59,20 +59,20 @@ class ImageModelBase(ModelBase): |
59 |
59 |
|
60 |
60 |
class ImageModel(models.Model): |
61 |
61 |
""" Abstract base class implementing all core ImageKit functionality |
62 |
||
62 |
||
63 |
63 |
Subclasses of ImageModel are augmented with accessors for each defined |
64 |
64 |
image specification and can override the inner IKOptions class to customize |
65 |
65 |
storage locations and other options. |
66 |
||
66 |
||
67 |
67 |
""" |
68 |
68 |
__metaclass__ = ImageModelBase |
69 |
69 |
|
70 |
70 |
class Meta: |
71 |
71 |
abstract = True |
72 |
||
72 |
||
73 |
73 |
class IKOptions: |
74 |
74 |
pass |
75 |
||
75 |
||
76 |
76 |
def admin_thumbnail_view(self): |
77 |
77 |
if not self._imgfield: |
78 |
78 |
return None |
| … | … | @@ -89,11 +89,11 @@ class ImageModel(models.Model): |
89 |
89 |
(escape(self._imgfield.url), escape(prop.url)) |
90 |
90 |
admin_thumbnail_view.short_description = _('Thumbnail') |
91 |
91 |
admin_thumbnail_view.allow_tags = True |
92 |
||
92 |
||
93 |
93 |
@property |
94 |
94 |
def _imgfield(self): |
95 |
95 |
return getattr(self, self._ik.image_field) |
96 |
||
96 |
||
97 |
97 |
@property |
98 |
98 |
def _storage(self): |
99 |
99 |
return getattr(self._ik, 'storage', self._imgfield.storage) |
| … | … | @@ -108,7 +108,7 @@ class ImageModel(models.Model): |
108 |
108 |
if spec.pre_cache: |
109 |
109 |
prop = getattr(self, spec.name()) |
110 |
110 |
prop._create() |
111 |
||
111 |
||
112 |
112 |
def save_image(self, name, image, save=True, replace=True): |
113 |
113 |
if self._imgfield and replace: |
114 |
114 |
self._imgfield.delete(save=False) |
Up to file-list imagekit/options.py:
1 |
1 |
# Imagekit options |
2 |
2 |
from imagekit import processors |
3 |
3 |
from imagekit.specs import ImageSpec |
4 |
||
4 |
||
5 |
5 |
|
6 |
6 |
class Options(object): |
7 |
7 |
""" Class handling per-model imagekit options |
| … | … | @@ -17,8 +17,8 @@ class Options(object): |
17 |
17 |
admin_thumbnail_spec = 'admin_thumbnail' |
18 |
18 |
spec_module = 'imagekit.defaults' |
19 |
19 |
#storage = defaults to image_field.storage |
20 |
||
21 |
def __init__(self, opts): |
|
20 |
||
21 |
def __init__(self, opts): |
|
22 |
22 |
for key, value in opts.__dict__.iteritems(): |
23 |
23 |
setattr(self, key, value) |
24 |
24 |
self.specs = [] |
Up to file-list imagekit/processors.py:
1 |
1 |
""" Imagekit Image "ImageProcessors" |
2 |
2 |
|
3 |
A processor defines a set of class variables (optional) and a |
|
3 |
A processor defines a set of class variables (optional) and a |
|
4 |
4 |
class method named "process" which processes the supplied image using |
5 |
5 |
the class properties as settings. The process method can be overridden as well allowing user to define their |
6 |
6 |
own effects/processes entirely. |
| … | … | @@ -10,11 +10,11 @@ from imagekit.lib import * |
10 |
10 |
|
11 |
11 |
class ImageProcessor(object): |
12 |
12 |
""" Base image processor class """ |
13 |
||
13 |
||
14 |
14 |
@classmethod |
15 |
15 |
def process(cls, img, fmt, obj): |
16 |
16 |
return img, fmt |
17 |
||
17 |
||
18 |
18 |
|
19 |
19 |
class Adjustment(ImageProcessor): |
20 |
20 |
color = 1.0 |
| … | … | @@ -38,7 +38,7 @@ class Adjustment(ImageProcessor): |
38 |
38 |
class Format(ImageProcessor): |
39 |
39 |
format = 'JPEG' |
40 |
40 |
extension = 'jpg' |
41 |
||
41 |
||
42 |
42 |
@classmethod |
43 |
43 |
def process(cls, img, fmt, obj): |
44 |
44 |
return img, cls.format |
| … | … | @@ -48,7 +48,7 @@ class Reflection(ImageProcessor): |
48 |
48 |
background_color = '#FFFFFF' |
49 |
49 |
size = 0.0 |
50 |
50 |
opacity = 0.6 |
51 |
||
51 |
||
52 |
52 |
@classmethod |
53 |
53 |
def process(cls, img, fmt, obj): |
54 |
54 |
# convert bgcolor string to rgb value |
| … | … | @@ -92,7 +92,7 @@ class Resize(ImageProcessor): |
92 |
92 |
height = None |
93 |
93 |
crop = False |
94 |
94 |
upscale = False |
95 |
||
95 |
||
96 |
96 |
@classmethod |
97 |
97 |
def process(cls, img, fmt, obj): |
98 |
98 |
cur_width, cur_height = img.size |
| … | … | @@ -133,10 +133,10 @@ class Resize(ImageProcessor): |
133 |
133 |
img = img.resize(new_dimensions, Image.ANTIALIAS) |
134 |
134 |
return img, fmt |
135 |
135 |
|
136 |
||
136 |
||
137 |
137 |
class Transpose(ImageProcessor): |
138 |
138 |
""" Rotates or flips the image |
139 |
||
139 |
||
140 |
140 |
Method should be one of the following strings: |
141 |
141 |
- FLIP_LEFT RIGHT |
142 |
142 |
- FLIP_TOP_BOTTOM |
| … | … | @@ -144,10 +144,10 @@ class Transpose(ImageProcessor): |
144 |
144 |
- ROTATE_270 |
145 |
145 |
- ROTATE_180 |
146 |
146 |
- auto |
147 |
||
147 |
||
148 |
148 |
If method is set to 'auto' the processor will attempt to rotate the image |
149 |
149 |
according to the EXIF Orientation data. |
150 |
||
150 |
||
151 |
151 |
""" |
152 |
152 |
EXIF_ORIENTATION_STEPS = { |
153 |
153 |
1: [], |
| … | … | @@ -159,9 +159,9 @@ class Transpose(ImageProcessor): |
159 |
159 |
7: ['ROTATE_90', 'FLIP_LEFT_RIGHT'], |
160 |
160 |
8: ['ROTATE_90'], |
161 |
161 |
} |
162 |
||
162 |
||
163 |
163 |
method = 'auto' |
164 |
||
164 |
||
165 |
165 |
@classmethod |
166 |
166 |
def process(cls, img, fmt, obj): |
167 |
167 |
if cls.method == 'auto': |
Up to file-list imagekit/specs.py:
| … | … | @@ -18,11 +18,11 @@ class ImageSpec(object): |
18 |
18 |
quality = 70 |
19 |
19 |
increment_count = False |
20 |
20 |
processors = [] |
21 |
||
21 |
||
22 |
22 |
@classmethod |
23 |
23 |
def name(cls): |
24 |
24 |
return getattr(cls, 'access_as', cls.__name__.lower()) |
25 |
||
25 |
||
26 |
26 |
@classmethod |
27 |
27 |
def process(cls, image, obj): |
28 |
28 |
fmt = image.format |
| … | … | @@ -31,7 +31,7 @@ class ImageSpec(object): |
31 |
31 |
img, fmt = proc.process(img, fmt, obj) |
32 |
32 |
img.format = fmt |
33 |
33 |
return img, fmt |
34 |
||
34 |
||
35 |
35 |
|
36 |
36 |
class Accessor(object): |
37 |
37 |
def __init__(self, obj, spec): |
| … | … | @@ -39,7 +39,7 @@ class Accessor(object): |
39 |
39 |
self._fmt = None |
40 |
40 |
self._obj = obj |
41 |
41 |
self.spec = spec |
42 |
||
42 |
||
43 |
43 |
def _get_imgfile(self): |
44 |
44 |
format = self._img.format or 'JPEG' |
45 |
45 |
if format != 'JPEG': |
| … | … | @@ -49,7 +49,7 @@ class Accessor(object): |
49 |
49 |
quality=int(self.spec.quality), |
50 |
50 |
optimize=True) |
51 |
51 |
return imgfile |
52 |
||
52 |
||
53 |
53 |
def _create(self): |
54 |
54 |
if self._exists(): |
55 |
55 |
return |
| … | … | @@ -57,14 +57,14 @@ class Accessor(object): |
57 |
57 |
try: |
58 |
58 |
fp = self._obj._imgfield.storage.open(self._obj._imgfield.name) |
59 |
59 |
except IOError: |
60 |
return |
|
60 |
return |
|
61 |
61 |
fp.seek(0) |
62 |
62 |
fp = StringIO(fp.read()) |
63 |
63 |
self._img, self._fmt = self.spec.process(Image.open(fp), self._obj) |
64 |
64 |
# save the new image to the cache |
65 |
65 |
content = ContentFile(self._get_imgfile().read()) |
66 |
66 |
self._obj._storage.save(self.name, content) |
67 |
||
67 |
||
68 |
68 |
def _delete(self): |
69 |
69 |
self._obj._storage.delete(self.name) |
70 |
70 |
|
| … | … | @@ -99,12 +99,12 @@ class Accessor(object): |
99 |
99 |
setattr(self._obj, fieldname, current_count + 1) |
100 |
100 |
self._obj.save(clear_cache=False) |
101 |
101 |
return self._obj._storage.url(self.name) |
102 |
||
102 |
||
103 |
103 |
@property |
104 |
104 |
def file(self): |
105 |
105 |
self._create() |
106 |
106 |
return self._obj._storage.open(self.name) |
107 |
||
107 |
||
108 |
108 |
@property |
109 |
109 |
def image(self): |
110 |
110 |
if self._img is None: |
| … | … | @@ -112,11 +112,11 @@ class Accessor(object): |
112 |
112 |
if self._img is None: |
113 |
113 |
self._img = Image.open(self.file) |
114 |
114 |
return self._img |
115 |
||
115 |
||
116 |
116 |
@property |
117 |
117 |
def width(self): |
118 |
118 |
return self.image.size[0] |
119 |
||
119 |
||
120 |
120 |
@property |
121 |
121 |
def height(self): |
122 |
122 |
return self.image.size[1] |
Up to file-list imagekit/tests.py:
| … | … | @@ -14,14 +14,14 @@ from imagekit.lib import Image |
14 |
14 |
|
15 |
15 |
class ResizeToWidth(processors.Resize): |
16 |
16 |
width = 100 |
17 |
||
17 |
||
18 |
18 |
class ResizeToHeight(processors.Resize): |
19 |
19 |
height = 100 |
20 |
||
20 |
||
21 |
21 |
class ResizeToFit(processors.Resize): |
22 |
22 |
width = 100 |
23 |
23 |
height = 100 |
24 |
||
24 |
||
25 |
25 |
class ResizeCropped(ResizeToFit): |
26 |
26 |
crop = ('center', 'center') |
27 |
27 |
|
| … | … | @@ -32,7 +32,7 @@ class TestResizeToWidth(ImageSpec): |
32 |
32 |
class TestResizeToHeight(ImageSpec): |
33 |
33 |
access_as = 'to_height' |
34 |
34 |
processors = [ResizeToHeight] |
35 |
||
35 |
||
36 |
36 |
class TestResizeCropped(ImageSpec): |
37 |
37 |
access_as = 'cropped' |
38 |
38 |
processors = [ResizeCropped] |
| … | … | @@ -40,10 +40,10 @@ class TestResizeCropped(ImageSpec): |
40 |
40 |
class TestPhoto(ImageModel): |
41 |
41 |
""" Minimal ImageModel class for testing """ |
42 |
42 |
image = models.ImageField(upload_to='images') |
43 |
||
43 |
||
44 |
44 |
class IKOptions: |
45 |
45 |
spec_module = 'imagekit.tests' |
46 |
||
46 |
||
47 |
47 |
|
48 |
48 |
class IKTest(TestCase): |
49 |
49 |
""" Base TestCase class """ |
| … | … | @@ -52,14 +52,14 @@ class IKTest(TestCase): |
52 |
52 |
Image.new('RGB', (800, 600)).save(tmp, 'JPEG') |
53 |
53 |
tmp.seek(0) |
54 |
54 |
return tmp |
55 |
||
55 |
||
56 |
56 |
def setUp(self): |
57 |
57 |
self.p = TestPhoto() |
58 |
58 |
img = self.generate_image() |
59 |
59 |
self.p.save_image('test.jpeg', ContentFile(img.read())) |
60 |
60 |
self.p.save() |
61 |
61 |
img.close() |
62 |
||
62 |
||
63 |
63 |
def test_save_image(self): |
64 |
64 |
img = self.generate_image() |
65 |
65 |
path = self.p.image.path |
| … | … | @@ -70,19 +70,19 @@ class IKTest(TestCase): |
70 |
70 |
self.p.save_image('test.jpeg', ContentFile(img.read())) |
71 |
71 |
self.failIf(os.path.isfile(path)) |
72 |
72 |
img.close() |
73 |
||
73 |
||
74 |
74 |
def test_setup(self): |
75 |
75 |
self.assertEqual(self.p.image.width, 800) |
76 |
76 |
self.assertEqual(self.p.image.height, 600) |
77 |
||
77 |
||
78 |
78 |
def test_to_width(self): |
79 |
79 |
self.assertEqual(self.p.to_width.width, 100) |
80 |
80 |
self.assertEqual(self.p.to_width.height, 75) |
81 |
||
81 |
||
82 |
82 |
def test_to_height(self): |
83 |
83 |
self.assertEqual(self.p.to_height.width, 133) |
84 |
84 |
self.assertEqual(self.p.to_height.height, 100) |
85 |
||
85 |
||
86 |
86 |
def test_crop(self): |
87 |
87 |
self.assertEqual(self.p.cropped.width, 100) |
88 |
88 |
self.assertEqual(self.p.cropped.height, 100) |
| … | … | @@ -91,7 +91,7 @@ class IKTest(TestCase): |
91 |
91 |
tup = (settings.MEDIA_URL, self.p._ik.cache_dir, |
92 |
92 |
'images/test_to_width.jpeg') |
93 |
93 |
self.assertEqual(self.p.to_width.url, "%s%s/%s" % tup) |
94 |
||
94 |
||
95 |
95 |
def tearDown(self): |
96 |
96 |
# make sure image file is deleted |
97 |
97 |
path = self.p.image.path |
