1
1
from __future__ import annotations
2
2
3
+ import abc
3
4
import dataclasses
4
5
import io
5
6
import math
12
13
from django .db .models import ImageField
13
14
from django .db .models .fields .files import ImageFieldFile
14
15
from django .urls import reverse
15
- from PIL import Image , ImageOps
16
-
17
- __all__ = ["PictureField" , "PictureFieldFile" ]
18
-
19
16
from django .utils .module_loading import import_string
17
+ from PIL import Image , ImageOps
20
18
21
19
from pictures import conf , utils
22
20
21
+ __all__ = ["PictureField" , "PictureFieldFile" , "Picture" ]
22
+
23
23
RGB_FORMATS = ["JPEG" ]
24
24
25
25
26
26
@dataclasses .dataclass
27
- class SimplePicture :
28
- """A simple picture class similar to Django's image class."""
27
+ class Picture (abc .ABC ):
28
+ """
29
+ An abstract picture class similar to Django's image class.
30
+
31
+ Subclasses will need to implement the `url` property.
32
+ """
29
33
30
34
parent_name : str
31
35
file_type : str
@@ -37,13 +41,35 @@ def __post_init__(self):
37
41
self .aspect_ratio = Fraction (self .aspect_ratio ) if self .aspect_ratio else None
38
42
39
43
def __hash__ (self ):
40
- return hash (self .name )
44
+ return hash (self .url )
41
45
42
46
def __eq__ (self , other ):
43
47
if not isinstance (other , type (self )):
44
48
return NotImplemented
45
49
return self .deconstruct () == other .deconstruct ()
46
50
51
+ def deconstruct (self ):
52
+ return (
53
+ f"{ self .__class__ .__module__ } .{ self .__class__ .__qualname__ } " ,
54
+ (
55
+ self .parent_name ,
56
+ self .file_type ,
57
+ str (self .aspect_ratio ) if self .aspect_ratio else None ,
58
+ self .storage .deconstruct (),
59
+ self .width ,
60
+ ),
61
+ {},
62
+ )
63
+
64
+ @property
65
+ @abc .abstractmethod
66
+ def url (self ) -> str :
67
+ """Return the URL of the picture."""
68
+
69
+
70
+ class PillowPicture (Picture ):
71
+ """Use the Pillow library to process images."""
72
+
47
73
@property
48
74
def url (self ) -> str :
49
75
if conf .get_settings ().USE_PLACEHOLDERS :
@@ -78,7 +104,7 @@ def name(self) -> str:
78
104
def path (self ) -> Path :
79
105
return Path (self .storage .path (self .name ))
80
106
81
- def process (self , image ) -> Image :
107
+ def process (self , image ) -> " Image" :
82
108
image = ImageOps .exif_transpose (image ) # crates a copy
83
109
height = self .height or self .width / Fraction (* image .size )
84
110
size = math .floor (self .width ), math .floor (height )
@@ -101,24 +127,11 @@ def save(self, image):
101
127
def delete (self ):
102
128
self .storage .delete (self .name )
103
129
104
- def deconstruct (self ):
105
- return (
106
- f"{ self .__class__ .__module__ } .{ self .__class__ .__qualname__ } " ,
107
- (
108
- self .parent_name ,
109
- self .file_type ,
110
- str (self .aspect_ratio ) if self .aspect_ratio else None ,
111
- self .storage .deconstruct (),
112
- self .width ,
113
- ),
114
- {},
115
- )
116
-
117
130
118
131
class PictureFieldFile (ImageFieldFile ):
119
132
120
- def __xor__ (self , other ) -> tuple [set [SimplePicture ], set [SimplePicture ]]:
121
- """Return the new and obsolete :class:`SimpleFile ` instances."""
133
+ def __xor__ (self , other ) -> tuple [set [Picture ], set [Picture ]]:
134
+ """Return the new and obsolete :class:`Picture ` instances."""
122
135
if not isinstance (other , PictureFieldFile ):
123
136
return NotImplemented
124
137
new = self .get_picture_files_list () - other .get_picture_files_list ()
@@ -179,7 +192,7 @@ def height(self):
179
192
return self ._get_image_dimensions ()[1 ]
180
193
181
194
@property
182
- def aspect_ratios (self ) -> {Fraction | None : {str : {int : SimplePicture }}}:
195
+ def aspect_ratios (self ) -> {Fraction | None : {str : {int : Picture }}}:
183
196
self ._require_file ()
184
197
return self .get_picture_files (
185
198
file_name = self .name ,
@@ -197,11 +210,12 @@ def get_picture_files(
197
210
img_height : int ,
198
211
storage : Storage ,
199
212
field : PictureField ,
200
- ) -> {Fraction | None : {str : {int : SimplePicture }}}:
213
+ ) -> {Fraction | None : {str : {int : Picture }}}:
214
+ PictureClass = import_string (conf .get_settings ().PICTURE_CLASS )
201
215
return {
202
216
ratio : {
203
217
file_type : {
204
- width : SimplePicture (file_name , file_type , ratio , storage , width )
218
+ width : PictureClass (file_name , file_type , ratio , storage , width )
205
219
for width in utils .source_set (
206
220
(img_width , img_height ),
207
221
ratio = ratio ,
@@ -214,7 +228,7 @@ def get_picture_files(
214
228
for ratio in field .aspect_ratios
215
229
}
216
230
217
- def get_picture_files_list (self ) -> set [SimplePicture ]:
231
+ def get_picture_files_list (self ) -> set [Picture ]:
218
232
return {
219
233
picture
220
234
for sources in self .aspect_ratios .values ()
0 commit comments