Skip to content

Commit

Permalink
Change to storing an array of frames, instead of the string for data.
Browse files Browse the repository at this point in the history
  • Loading branch information
sl1-1 committed Jan 10, 2020
1 parent bbdf9fa commit 0a61c4f
Show file tree
Hide file tree
Showing 2 changed files with 87 additions and 101 deletions.
186 changes: 86 additions & 100 deletions libweasyl/libweasyl/images.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
"""
Image manipulation with Pillow.
"""
import copy
from io import BytesIO

from PIL import Image, ImageSequence
Expand All @@ -15,33 +14,20 @@
"The maximum height of a thumbnail, in pixels."


def process_gif(image, action, args):
image_frames = ImageSequence.Iterator(image)

# Wrap on-the-fly thumbnail generator
def thumbnails(frames):
for frame in frames:
thumbnail = frame.copy()
func = getattr(thumbnail, action)
thumbnail = func(**args)
yield thumbnail

thumbnail_frames = thumbnails(image_frames)

# Save output
om = next(thumbnail_frames) # Handle first frame separately
om.info = image.info # Copy sequence info
with BytesIO() as out:
om.save(out, format=image.format, save_all=True, append_images=list(thumbnail_frames))
image_data = out.getvalue()
return image_data, om.size
def gif_to_frames(image):
frames = ImageSequence.Iterator(image)
for frame in frames:
yield frame.copy()


class WeasylImage(object):
_file_format = None
_frames = []
image_data = bytes()
webp = None
_size = (0, 0)
is_animated = None
_info = None

def __init__(self, fp=None, string=None):
if string:
Expand All @@ -55,6 +41,8 @@ def __init__(self, fp=None, string=None):
image = Image.open(image_bytes)
self._file_format = image.format
self._size = image.size
self._info = image.info
self._frames = [x for x in gif_to_frames(image)]
self.is_animated = getattr(image, 'is_animated', False)

@property
Expand All @@ -80,13 +68,27 @@ def file_format(self):
def image_extension(self):
return ".{}".format(self.file_format)


def to_buffer(self):
return self.image_data

def save(self, fp):
@property
def frames(self):
return len(self._frames)

def _write(self, out, *args, **kwargs):
if 'format' not in kwargs:
kwargs['format'] = self._file_format
if self.is_animated:
kwargs['save_all'] = True
kwargs['append_images'] = self._frames[1:]
self._frames[0].info = self._info
self._frames[0].save(out, *args, **kwargs)

def to_buffer(self, *args, **kwargs):
with BytesIO() as image_bytes:
self._write(image_bytes, *args, **kwargs)
return image_bytes.getvalue()

def save(self, fp, *args, **kwargs):
with open(fp, 'wb') as out:
out.write(self.image_data)
self._write(out, *args, **kwargs)

def resize(self, size):
"""
Expand All @@ -96,37 +98,20 @@ def resize(self, size):
Parameters:
size: Tuple (width, height)
"""
with BytesIO(self.image_data) as image_bytes:
image = Image.open(image_bytes)
if image.size[0] > size[0] or image.size[1] > size[1]:
if not getattr(image, 'is_animated', False):
image.thumbnail(size)
self._size = image.size
with BytesIO() as out:
image.save(out, format=self._file_format)
self.image_data = out.getvalue()

else:
self.image_data, self._size = process_gif(image, 'resize', {'size': size})
if self._size[0] > size[0] or self._size[1] > size[1]:
for i in range(0, len(self._frames)):
self._frames[i] = self._frames[i].resize(size)
self._size = self._frames[0].size

def crop(self, bounds):
"""
Crops the image using the bounds provided
:param bounds: tuple of ( left, upper, right, lower)
:return: None
"""
with BytesIO(self.image_data) as image_bytes:
image = Image.open(image_bytes)
# resize only if we need to; return None if we don't
if not getattr(image, 'is_animated', False):
image = image.crop(bounds)
self._size = image.size
with BytesIO() as out:
image.save(out, format=self._file_format)
self.image_data = out.getvalue()

else:
self.image_data, self._size = process_gif(image, 'crop', {'box': bounds})
for i in range(0, len(self._frames)):
self._frames[i] = self._frames[i].crop(bounds)
self._size = self._frames[0].size

def shrinkcrop(self, size, bounds=None):
"""
Expand All @@ -152,58 +137,60 @@ def shrinkcrop(self, size, bounds=None):
self.crop(bounds)

def get_thumbnail(self, bounds=None):
save_kwargs = {}
with BytesIO(self.image_data) as image_bytes:
image = Image.open(image_bytes)
if image.mode in ('1', 'L', 'LA', 'I', 'P'):
image = image.convert(mode='RGBA' if image.mode == 'LA' or 'transparency' in image.info else 'RGB')

if bounds is None:
source_rect, result_size = get_thumbnail_spec(image.size, THUMB_HEIGHT)
else:
source_rect, result_size = get_thumbnail_spec_cropped(
_fit_inside(bounds, image.size),
THUMB_HEIGHT)
if source_rect == (0, 0, image.width, image.height):
image.draft(None, result_size)
image = image.resize(result_size, resample=Image.LANCZOS)
else:
# TODO: draft and adjust rectangle?
image = image.resize(result_size, resample=Image.LANCZOS, box=source_rect)

if self._file_format == 'JPEG':
with BytesIO() as f:
image.save(f, format='JPEG', quality=95, optimize=True, progressive=True, subsampling='4:2:2')
compatible = (f.getvalue(), 'JPG')

lossless = False
elif self._file_format in ('PNG', 'GIF'):
with BytesIO() as f:
image.save(f, format='PNG', optimize=True, **save_kwargs)
compatible = (f.getvalue(), 'PNG')

lossless = True
else:
raise Exception("Unexpected image format: %r" % (self._file_format,))

with BytesIO() as f:
image.save(f, format='WebP', lossless=lossless, quality=100 if lossless else 90, method=6,
**save_kwargs)
webp = (f.getvalue(), 'WEBP')

if not len(webp[0]) >= len(compatible[0]):
self.webp = webp[0]

self._file_format = compatible[1]
self.image_data = compatible[0]
self._size = image.size
image = self._frames[0]
self._frames = self._frames[:1]
if image.mode in ('1', 'L', 'LA', 'I', 'P'):
image = image.convert(mode='RGBA' if image.mode == 'LA' or 'transparency' in image.info else 'RGB')

if bounds is None:
source_rect, result_size = get_thumbnail_spec(image.size, THUMB_HEIGHT)
else:
source_rect, result_size = get_thumbnail_spec_cropped(
_fit_inside(bounds, image.size),
THUMB_HEIGHT)
if source_rect == (0, 0, image.width, image.height):
image.draft(None, result_size)
image = image.resize(result_size, resample=Image.LANCZOS)
else:
# TODO: draft and adjust rectangle?
image = image.resize(result_size, resample=Image.LANCZOS, box=source_rect)

if self._file_format == 'JPEG':
out = self.to_buffer(format='JPEG', quality=95, optimize=True, progressive=True, subsampling='4:2:2')
compatible = (out, 'JPG')

lossless = False
elif self._file_format in ('PNG', 'GIF'):
out = self.to_buffer(format='PNG', optimize=True)
compatible = (out, 'PNG')

lossless = True
else:
raise Exception("Unexpected image format: %r" % (self._file_format,))

out = self.to_buffer(format='WebP', lossless=lossless, quality=100 if lossless else 90, method=6)
webp = (out, 'WEBP')

if not len(webp[0]) >= len(compatible[0]):
self.webp = webp[0]

self._file_format = compatible[1]
self._size = image.size

def copy(self):
"""
Creates a deep copy of the image class
:return: WeasylImage()
"""
return copy.deepcopy(self)
n = self.__new__(WeasylImage)
for frame in self._frames:
n._frames.append(frame.copy())
n._file_format = self._file_format
n.webp = self.webp
n._size = self._size
n.is_animated = self.is_animated
n._info = self._info
return n


def get_thumbnail_spec(size, height):
Expand Down Expand Up @@ -259,6 +246,5 @@ def check_crop(dim, x1, y1, x2, y2):
Return True if the specified crop coordinates are valid, else False.
"""
return (
x1 >= 0 and y1 >= 0 and x2 >= 0 and y2 >= 0 and x1 <= dim[0] and
y1 <= dim[1] and x2 <= dim[0] and y2 <= dim[1] and x2 > x1 and y2 > y1)

x1 >= 0 and y1 >= 0 and x2 >= 0 and y2 >= 0 and x1 <= dim[0] and
y1 <= dim[1] and x2 <= dim[0] and y2 <= dim[1] and x2 > x1 and y2 > y1)
2 changes: 1 addition & 1 deletion weasyl/weasyl.tac
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ cache.region.configure(
arguments=dict(
reactor=reactor,
url=d.config_read_setting(
'servers', 'tcp:127.0.0.1:11211', 'memcached').split(),
'servers', 'tcp:memcached:11211', 'memcached').split(),
retryDelay=10,
timeOut=0.4,
),
Expand Down

0 comments on commit 0a61c4f

Please sign in to comment.