11from __future__ import annotations
22
3+ import abc
34import dataclasses
45import io
56import math
1213from django .db .models import ImageField
1314from django .db .models .fields .files import ImageFieldFile
1415from django .urls import reverse
15- from PIL import Image , ImageOps
16-
17- __all__ = ["PictureField" , "PictureFieldFile" ]
18-
1916from django .utils .module_loading import import_string
17+ from PIL import Image , ImageOps
2018
2119from pictures import conf , utils
2220
21+ __all__ = ["PictureField" , "PictureFieldFile" , "Picture" ]
22+
2323RGB_FORMATS = ["JPEG" ]
2424
2525
2626@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+ """
2933
3034 parent_name : str
3135 file_type : str
@@ -37,13 +41,35 @@ def __post_init__(self):
3741 self .aspect_ratio = Fraction (self .aspect_ratio ) if self .aspect_ratio else None
3842
3943 def __hash__ (self ):
40- return hash (self .name )
44+ return hash (self .url )
4145
4246 def __eq__ (self , other ):
4347 if not isinstance (other , type (self )):
4448 return NotImplemented
4549 return self .deconstruct () == other .deconstruct ()
4650
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+
4773 @property
4874 def url (self ) -> str :
4975 if conf .get_settings ().USE_PLACEHOLDERS :
@@ -78,7 +104,7 @@ def name(self) -> str:
78104 def path (self ) -> Path :
79105 return Path (self .storage .path (self .name ))
80106
81- def process (self , image ) -> Image :
107+ def process (self , image ) -> " Image" :
82108 image = ImageOps .exif_transpose (image ) # crates a copy
83109 height = self .height or self .width / Fraction (* image .size )
84110 size = math .floor (self .width ), math .floor (height )
@@ -101,24 +127,11 @@ def save(self, image):
101127 def delete (self ):
102128 self .storage .delete (self .name )
103129
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-
117130
118131class PictureFieldFile (ImageFieldFile ):
119132
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."""
122135 if not isinstance (other , PictureFieldFile ):
123136 return NotImplemented
124137 new = self .get_picture_files_list () - other .get_picture_files_list ()
@@ -179,7 +192,7 @@ def height(self):
179192 return self ._get_image_dimensions ()[1 ]
180193
181194 @property
182- def aspect_ratios (self ) -> {Fraction | None : {str : {int : SimplePicture }}}:
195+ def aspect_ratios (self ) -> {Fraction | None : {str : {int : Picture }}}:
183196 self ._require_file ()
184197 return self .get_picture_files (
185198 file_name = self .name ,
@@ -197,11 +210,12 @@ def get_picture_files(
197210 img_height : int ,
198211 storage : Storage ,
199212 field : PictureField ,
200- ) -> {Fraction | None : {str : {int : SimplePicture }}}:
213+ ) -> {Fraction | None : {str : {int : Picture }}}:
214+ PictureClass = import_string (conf .get_settings ().PICTURE_CLASS )
201215 return {
202216 ratio : {
203217 file_type : {
204- width : SimplePicture (file_name , file_type , ratio , storage , width )
218+ width : PictureClass (file_name , file_type , ratio , storage , width )
205219 for width in utils .source_set (
206220 (img_width , img_height ),
207221 ratio = ratio ,
@@ -214,7 +228,7 @@ def get_picture_files(
214228 for ratio in field .aspect_ratios
215229 }
216230
217- def get_picture_files_list (self ) -> set [SimplePicture ]:
231+ def get_picture_files_list (self ) -> set [Picture ]:
218232 return {
219233 picture
220234 for sources in self .aspect_ratios .values ()
0 commit comments