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.

Clone this repository (size: 135.6 KB): HTTPS / SSH
$ hg clone http://hg.driscolldev.com/django-imagekit

Changed (Δ319 bytes):

raw changeset »

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:

Binary file has changed or diff was empty.

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