Skip to content

Commit 6f04d22

Browse files
committed
Completely migrated to PIL, the crop functinality has been simplified, #112
1 parent 0747d3c commit 6f04d22

File tree

6 files changed

+69
-99
lines changed

6 files changed

+69
-99
lines changed

sqlalchemy_media/attachments.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from typing import Hashable, Tuple, List, Iterable
88

99
from sqlalchemy.ext.mutable import MutableList, MutableDict
10+
from PIL import Image as PilImage
1011

1112
from .constants import MB, KB
1213
from .descriptors import AttachableDescriptor
@@ -808,15 +809,14 @@ def generate_thumbnail(
808809
width, height, ratio
809810
)
810811

811-
CompatibleImage = get_image_factory()
812812
# opening the original file
813813
thumbnail_buffer = io.BytesIO()
814814
store = self.get_store()
815+
format_ = 'jpeg'
815816
with store.open(self.path) as original_file:
816817

817818
# generating thumbnail and storing in buffer
818-
img = CompatibleImage(file=original_file)
819-
img.format = 'jpg'
819+
img = PilImage.open(original_file)
820820

821821
with img:
822822
original_size = img.size
@@ -829,8 +829,8 @@ def generate_thumbnail(
829829
width = int(width)
830830
height = int(height)
831831

832-
img.resize(width, height)
833-
img.save(file=thumbnail_buffer)
832+
thumbnail_image = img.resize((width, height))
833+
thumbnail_image.save(thumbnail_buffer, format_)
834834

835835
thumbnail_buffer.seek(0)
836836
if self.thumbnails is None:

sqlalchemy_media/descriptors.py

Lines changed: 3 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
from .constants import KB
88
from .exceptions import MaximumLengthIsReachedError, \
9-
MinimumLengthIsNotReachedError, DescriptorOperationError
9+
DescriptorOperationError
1010
from .helpers import is_uri, copy_stream
1111
from .mimetypes_ import guess_extension, guess_type
1212
from .typing_ import FileLike, Attachable
@@ -233,21 +233,9 @@ def get_header_buffer(self) -> bytes:
233233

234234
def close(self, check_length=True) -> None:
235235
"""
236-
Closes the underlying file-object. and check for ``min_length``.
237-
238-
:param check_length: Check the minimum length of the stream and
239-
:class:`MinimumLengthIsNotReachedError` may be
240-
raised during close. default is `True`.
241-
242-
.. versionadded:: 0.8
243-
244-
- `check_length`
245-
236+
Do nothing here.
246237
"""
247-
if check_length:
248-
pos = self.tell()
249-
if self.min_length and self.min_length > pos:
250-
raise MinimumLengthIsNotReachedError(self.min_length, pos)
238+
pass
251239

252240
def seekable(self) -> bool:
253241
"""

sqlalchemy_media/exceptions.py

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -16,18 +16,6 @@ def __init__(self, max_length: int):
1616
)
1717

1818

19-
class MinimumLengthIsNotReachedError(SqlAlchemyMediaException):
20-
"""
21-
Indicates the minimum allowed length is not reached.
22-
"""
23-
24-
def __init__(self, min_length, length=None):
25-
super().__init__(
26-
f'Cannot store files smaller than: {min_length: d} bytes,'
27-
f'but the file length is: {length}'
28-
)
29-
30-
3119
class ContextError(SqlAlchemyMediaException):
3220
"""
3321
Exception related to :class:`.StoreManager`.

sqlalchemy_media/processors.py

Lines changed: 22 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from .exceptions import ContentTypeValidationError, DimensionValidationError, \
88
AspectRatioValidationError, AnalyzeError
99
from .helpers import validate_width_height_ratio, deprecated
10-
from .mimetypes_ import guess_extension
10+
from .mimetypes_ import guess_extension, guess_type
1111
from .optionals import magic_mime_from_buffer, ensure_wand
1212
from .typing_ import Dimension
1313

@@ -427,72 +427,56 @@ def __init__(self, fmt: str = None, width: int = None, height: int = None, crop=
427427
# k: v if isinstance(v, str) else str(v) for k, v in crop.items()
428428
# }
429429

430+
def _update_context(self, img: PilImage, format_, context: dict):
431+
mimetype = guess_type(f'a.{format_}'.lower())
432+
context.update(
433+
content_type=mimetype,
434+
width=img.width,
435+
height=img.height,
436+
extension=guess_extension(mimetype)
437+
)
438+
430439
def process(self, descriptor: StreamDescriptor, context: dict):
431440

432-
# FIXME: rewrite this method
433-
import pudb; pudb.set_trace() # XXX BREAKPOINT
434441
# Copy the original info
435442
# generating thumbnail and storing in buffer
436443
img = PilImage.open(descriptor)
437444

438-
is_invalid_format = self.format is None or img.format == self.format
439-
is_invalid_size = (
445+
format_unchanged = self.format is None or img.format == self.format
446+
size_unchanged = (
440447
(self.width is None or img.width == self.width) and
441448
(self.height is None or img.height == self.height)
442449
)
443450

444-
if self.crop is None and is_invalid_format and is_invalid_size:
445-
img.close()
451+
if self.crop is None and format_unchanged and size_unchanged:
452+
self._update_context(img, img.format, context)
446453
descriptor.prepare_to_read(backend='memory')
447454
return
448455

456+
# Preserving format
457+
format_ = self.format or img.format
458+
449459
if 'length' in context:
450460
del context['length']
451461

452462
# opening the original file
453463
output_buffer = io.BytesIO()
454464

455465
# Changing dimension if required.
456-
if self.width or self.height:
466+
if not size_unchanged:
457467
width, height, _ = \
458468
validate_width_height_ratio(self.width, self.height, None)
459-
img.resize((
469+
img = img.resize((
460470
width(img.size) if callable(width) else width,
461471
height(img.size) if callable(height) else height
462472
))
463473

464474
# Cropping
465475
if self.crop:
466-
def get_key_for_crop_item(key, value):
467-
crop_width_keys = ('width', 'left', 'right')
468-
crop_keys = (
469-
'left', 'top', 'right', 'bottom', 'width', 'height'
470-
)
471-
472-
if key in crop_keys and isinstance(value, str) \
473-
and '%' in value:
474-
return int(int(value[:-1]) / 100 * (
475-
img.width if key in crop_width_keys else img.height
476-
))
477-
478-
return value
479-
480-
img.crop(**{
481-
key: get_key_for_crop_item(key, value)
482-
for key, value in self.crop.items()
483-
})
484-
485-
img.save(output_buffer, format=self.format)
486-
487-
mimetype = img.get_format_mimetype()
488-
489-
context.update(
490-
content_type=mimetype,
491-
width=img.width,
492-
height=img.height,
493-
extension=guess_extension(mimetype)
494-
)
476+
img = img.crop(self.crop)
495477

478+
img.save(output_buffer, format=format_)
479+
self._update_context(img, format_, context)
496480
output_buffer.seek(0)
497481
descriptor.replace(output_buffer, position=0, **context)
498482

sqlalchemy_media/tests/test_file.py

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from sqlalchemy_media import File, StoreManager, ContentTypeValidator, \
88
MagicAnalyzer
99
from sqlalchemy_media.exceptions import MaximumLengthIsReachedError, \
10-
MinimumLengthIsNotReachedError, ContentTypeValidationError
10+
ContentTypeValidationError
1111
from sqlalchemy_media.tests.helpers import Json, TempStoreTestCase
1212

1313

@@ -162,12 +162,6 @@ class Person(self.Base):
162162
person1.cv = LimitedFile()
163163

164164
with StoreManager(session):
165-
166-
# MaximumLengthIsReachedError, MinimumLengthIsNotReachedError
167-
self.assertRaises(
168-
MinimumLengthIsNotReachedError,
169-
person1.cv.attach, BytesIO(b'less than 20 chars!')
170-
)
171165
self.assertRaises(
172166
MaximumLengthIsReachedError,
173167
person1.cv.attach,

sqlalchemy_media/tests/test_image_processors.py

Lines changed: 38 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -37,25 +37,57 @@ def test_resize_reformat(self):
3737
# Checking when not modifying stream.
3838
ctx = dict()
3939
ImageProcessor().process(d, ctx)
40-
self.assertFalse(len(ctx))
40+
self.assertEqual(
41+
{
42+
'content_type': 'image/jpeg',
43+
'width': 640,
44+
'height': 480,
45+
'extension': '.jpg'
46+
}.items(),
47+
ctx.items()
48+
)
4149

4250
# Checking when not modifying stream.
4351
ImageProcessor(fmt='jpeg').process(d, ctx)
44-
self.assertFalse(len(ctx))
52+
self.assertEqual(
53+
{
54+
'content_type': 'image/jpeg',
55+
'width': 640,
56+
'height': 480,
57+
'extension': '.jpg'
58+
}.items(),
59+
ctx.items()
60+
)
4561

4662
ImageProcessor(fmt='jpeg', width=640).process(d, ctx)
47-
self.assertFalse(len(ctx))
63+
self.assertEqual(
64+
{
65+
'content_type': 'image/jpeg',
66+
'width': 640,
67+
'height': 480,
68+
'extension': '.jpg'
69+
}.items(),
70+
ctx.items()
71+
)
72+
4873

4974
ImageProcessor(fmt='jpeg', height=480).process(d, ctx)
50-
self.assertFalse(len(ctx))
75+
self.assertEqual(
76+
{
77+
'content_type': 'image/jpeg',
78+
'width': 640,
79+
'height': 480,
80+
'extension': '.jpg'
81+
}.items(),
82+
ctx.items()
83+
)
5184

52-
"""
5385
def test_crop(self):
5486
with AttachableDescriptor(self.cat_jpeg) as d:
5587
# Checking when not modifying stream.
5688
ctx = dict()
5789
ImageProcessor(
58-
crop=dict(width='50%', height='50%', gravity='center')
90+
crop=(160, 120, 480, 360)
5991
).process(d, ctx)
6092
ctx = dict()
6193
ImageAnalyzer().process(d, ctx)
@@ -68,22 +100,6 @@ def test_crop(self):
68100
}
69101
)
70102

71-
# With integer values
72-
with AttachableDescriptor(self.cat_jpeg) as d:
73-
# Checking when not modifying stream.
74-
ctx = dict()
75-
ImageProcessor(crop=dict(width=100)).process(d, ctx)
76-
ctx = dict()
77-
ImageAnalyzer().process(d, ctx)
78-
self.assertDictEqual(
79-
ctx,
80-
{
81-
'content_type': 'image/jpeg',
82-
'width': 100,
83-
'height': 480,
84-
}
85-
)
86103

87-
"""
88104
if __name__ == '__main__': # pragma: no cover
89105
unittest.main()

0 commit comments

Comments
 (0)