def save_image(image, destination=None, filename=None, **options): """ Save a PIL image. """ if destination is None: destination = BytesIO() filename = filename or '' # Ensure plugins are fully loaded so that Image.EXTENSION is populated. Image.init() format = Image.EXTENSION.get(os.path.splitext(filename)[1].lower(), 'JPEG') if format in ('JPEG', 'WEBP'): options.setdefault('quality', 85) saved = False if format == 'JPEG': if settings.THUMBNAIL_PROGRESSIVE and ( max(image.size) >= settings.THUMBNAIL_PROGRESSIVE): options['progressive'] = True try: image.save(destination, format=format, optimize=1, **options) saved = True except IOError: # Try again, without optimization (PIL can't optimize an image # larger than ImageFile.MAXBLOCK, which is 64k by default). This # shouldn't be triggered very often these days, as recent versions # of pillow avoid the MAXBLOCK limitation. pass if not saved: image.save(destination, format=format, **options) if hasattr(destination, 'seek'): destination.seek(0) return destination
def save_image(image, destination=None, filename=None, **options): """ Save a PIL image. """ if destination is None: destination = BytesIO() filename = filename or '' # Ensure plugins are fully loaded so that Image.EXTENSION is populated. Image.init() format = Image.EXTENSION.get(os.path.splitext(filename)[1].lower(), 'JPEG') if format in ('JPEG', 'WEBP'): options.setdefault('quality', 85) saved = False if format == 'JPEG': if settings.THUMBNAIL_PROGRESSIVE and (max(image.size) >= settings.THUMBNAIL_PROGRESSIVE): options['progressive'] = True try: image.save(destination, format=format, optimize=1, **options) saved = True except IOError: # Try again, without optimization (PIL can't optimize an image # larger than ImageFile.MAXBLOCK, which is 64k by default). This # shouldn't be triggered very often these days, as recent versions # of pillow avoid the MAXBLOCK limitation. pass if not saved: image.save(destination, format=format, **options) if hasattr(destination, 'seek'): destination.seek(0) return destination
def save(filename, im, options, destination=None): pil_options = options.copy() if destination is None: destination = BytesIO() # Ensure plugins are fully loaded so that Image.EXTENSION is populated. Image.init() fmt = Image.EXTENSION.get( os.path.splitext(filename)[1].lower(), 'JPEG') if fmt in ('JPEG', 'WEBP'): pil_options.setdefault('quality', 85) saved = False if fmt == 'JPEG': progressive = pil_options.pop('progressive', 100) if progressive: if progressive is True or max(im.size) >= int(progressive): pil_options['progressive'] = True try: im.save(destination, format=fmt, optimize=1, **pil_options) saved = True except IOError: # Try again, without optimization (PIL can't optimize an image # larger than ImageFile.MAXBLOCK, which is 64k by default). # This shouldn't be triggered very often these days, as recent # versions of pillow avoid the MAXBLOCK limitation. pass if not saved: im.save(destination, format=fmt, **pil_options) if hasattr(destination, 'seek'): destination.seek(0) return destination
def test_nearly_image(self): """ Broken images raise an exception. """ data = self.create_image(None, None) trunc_data = BytesIO() trunc_data.write(data.read()[:-10]) trunc_data.seek(0) self.assertRaises(IOError, source_generators.pil_image, data)
def test_nearly_image(self): """ Broken images raise an exception. """ data = self.create_image(None, None) trunc_data = BytesIO() trunc_data.write(data.read()[:-10]) trunc_data.seek(0) self.assertRaises(IOError, source_generators.pil_image, data)
def test_nearly_image(self): """ Broken images are passed silently. """ data = self.create_image(None, None) trunc_data = BytesIO() trunc_data.write(data.read()[:-10]) trunc_data.seek(0) self.assertEqual(source_generators.pil_image(data), None)
def test_nearly_image(self): """ Broken images are passed silently. """ data = self.create_image(None, None) trunc_data = BytesIO() trunc_data.write(data.read()[:-10]) trunc_data.seek(0) self.assertEqual(source_generators.pil_image(data), None)
def saveObscurement(image): """ Return a 1-bit PNG byte string from the PIL image """ output = StringIO() output.name = '__obscurement.png' image.save(output, optimize=True, transparency=TRANS, bits=1) output.seek(0) return output.read()
def saveObscurement(image): """ Return a 1-bit PNG byte string from the PIL image """ output = StringIO() output.name = '__obscurement.png' image.save(output, optimize=True, transparency=TRANS, bits=1) output.seek(0) return output.read()
def test_nearly_image(self): """ Truncated images *don't* raise an exception if they can still be read. """ data = self.create_image(None, None) reference = source_generators.pil_image(data) data.seek(0) trunc_data = BytesIO() trunc_data.write(data.read()[:-10]) trunc_data.seek(0) im = source_generators.pil_image(trunc_data) # im will be truncated, but it should be the same dimensions. self.assertEqual(im.size, reference.size)
def test_nearly_image(self): """ Truncated images *don't* raise an exception if they can still be read. """ data = self.create_image(None, None) reference = source_generators.pil_image(data) data.seek(0) trunc_data = BytesIO() trunc_data.write(data.read()[:-10]) trunc_data.seek(0) im = source_generators.pil_image(trunc_data) # im will be truncated, but it should be the same dimensions. self.assertEqual(im.size, reference.size)
def create_image(self, storage, filename, size=(800, 600), image_mode="RGB", image_format="JPEG"): """ Generate a test image, returning the filename that it was saved as. If ``storage`` is ``None``, the BytesIO containing the image data will be passed instead. """ data = BytesIO() Image.new(image_mode, size).save(data, image_format) data.seek(0) if not storage: return data image_file = ContentFile(data.read()) return storage.save(filename, image_file)
def create_image(self, storage, filename, size=(800, 600), image_mode='RGB', image_format='JPEG'): """ Generate a test image, returning the filename that it was saved as. If ``storage`` is ``None``, the BytesIO containing the image data will be passed instead. """ data = BytesIO() Image.new(image_mode, size).save(data, image_format) data.seek(0) if not storage: return data image_file = ContentFile(data.read()) return storage.save(filename, image_file)
def save_image(image, destination=None, filename=None, **options): """ Save a PIL image. """ if destination is None: destination = BytesIO() filename = filename or '' format = Image.EXTENSION.get(os.path.splitext(filename)[1], 'JPEG') if format == 'JPEG': options.setdefault('quality', 85) try: image.save(destination, format=format, optimize=1, **options) except IOError: # Try again, without optimization (PIL can't optimize an image # larger than ImageFile.MAXBLOCK, which is 64k by default) pass image.save(destination, format=format, **options) if hasattr(destination, 'seek'): destination.seek(0) return destination
def save_image(image, destination=None, filename=None, **options): """ Save a PIL image. """ if destination is None: destination = BytesIO() filename = filename or '' format = Image.EXTENSION.get(os.path.splitext(filename)[1], 'JPEG') if format == 'JPEG': options.setdefault('quality', 85) try: image.save(destination, format=format, optimize=1, **options) except IOError: # Try again, without optimization (PIL can't optimize an image # larger than ImageFile.MAXBLOCK, which is 64k by default) pass image.save(destination, format=format, **options) if hasattr(destination, 'seek'): destination.seek(0) return destination
def save_image(image, destination=None, filename=None, **options): """ Save a PIL image. """ if destination is None: destination = BytesIO() filename = filename or '' # Ensure plugins are fully loaded so that Image.EXTENSION is populated. Image.init() format = Image.EXTENSION.get(os.path.splitext(filename)[1].lower(), 'JPEG') if format in ('JPEG', 'WEBP'): options.setdefault('quality', 85) if format == 'JPEG': try: image.save(destination, format=format, optimize=1, **options) except IOError: # Try again, without optimization (PIL can't optimize an image # larger than ImageFile.MAXBLOCK, which is 64k by default) pass image.save(destination, format=format, **options) if hasattr(destination, 'seek'): destination.seek(0) return destination
def save_image(image, destination=None, filename=None, **options): """ Save a PIL image. """ if destination is None: destination = BytesIO() filename = filename or '' # Ensure plugins are fully loaded so that Image.EXTENSION is populated. Image.init() format = Image.EXTENSION.get(os.path.splitext(filename)[1].lower(), 'JPEG') if format in ('JPEG', 'WEBP'): options.setdefault('quality', 85) if format == 'JPEG': try: image.save(destination, format=format, optimize=1, **options) except IOError: # Try again, without optimization (PIL can't optimize an image # larger than ImageFile.MAXBLOCK, which is 64k by default) pass image.save(destination, format=format, **options) if hasattr(destination, 'seek'): destination.seek(0) return destination
def iiif_image_api(request, identifier_param, region_param, size_param, rotation_param, quality_param, format_param): """ Image repurposing endpoint for IIIF Image API 2.1 """ ik_image, image = _get_image_or_404(identifier_param, load_image=True) is_transparent = et_utils.is_transparent(image) is_grayscale = image.mode in ('L', 'LA') # Map format names used for IIIF URL path extension to proper name format_mapping = { 'jpg': 'jpeg', 'tif': 'tiff', } try: # Parse region x, y, r_width, r_height = parse_region(region_param, image.width, image.height) # Parse size s_width, s_height = parse_size(size_param, r_width, r_height) # Parse rotation is_mirrored, rotation_degrees = \ parse_rotation(rotation_param, s_width, s_height) # Parse quality quality = parse_quality(quality_param) # Parse format # TODO Add support for unsupported formats (see `parse_format`) image_format = os.path.splitext(ik_image.image.name)[1][1:].lower() output_format = parse_format(format_param, image_format) corrected_format = format_mapping.get(output_format, output_format) # Redirect to canonical URL if appropriate, per # http://iiif.io/api/image/2.1/#canonical-uri-syntax canonical_path = make_canonical_path( identifier_param, image.width, image.height, (x, y, r_width, r_height), # Region (s_width, s_height), # Size (is_mirrored, rotation_degrees), # Rotation quality, output_format) if request.path != canonical_path: return HttpResponseRedirect(canonical_path) # Determine storage file name for item if iiif_storage: storage_path = build_iiif_file_storage_path( canonical_path, ik_image, iiif_storage) else: storage_path = None # Load pre-generated image from storage if one exists and is up-to-date # with the original image (per timestampt info embedded in the storage # path) # TODO The exists lookup is slow for S3 storage, cache metadata? # TODO Detect when original image would be unchanged & use it directly? if (storage_path and iiif_storage.exists(storage_path)): if is_remote_storage(iiif_storage, storage_path): return HttpResponseRedirect(iiif_storage.url(storage_path)) else: return FileResponse( iiif_storage.open(storage_path), content_type='image/%s' % corrected_format, ) ################## # Generate image # ################## # Apply region if x or y or r_width != image.width or r_height != image.height: box = (x, y, x + r_width, y + r_height) image = image.crop(box) # Apply size if s_width != r_width or s_height != r_height: size = (s_width, s_height) image = image.resize(size) # TODO Apply rotation # Apply quality # Much of this is cribbed from easythumbnails' `colorspace` processor # TODO Replace with glamkit-imagetools' sRGB colour space converter? if quality in ('default', 'color') and not is_grayscale: if is_transparent: new_mode = 'RGBA' else: new_mode = 'RGB' elif is_grayscale or quality == 'gray': if is_transparent: new_mode = 'LA' else: new_mode = 'L' if new_mode != image.mode: image = image.convert(new_mode) # Apply format and "save" result_image = BytesIO() image.save(result_image, format=corrected_format) # Save generated image to storage if possible if storage_path: iiif_storage.save(storage_path, result_image) if iiif_storage and is_remote_storage(iiif_storage, storage_path): return HttpResponseRedirect(iiif_storage.url(storage_path)) else: result_image.seek(0) # Reset image file in case it's just created return FileResponse( result_image.read(), content_type='image/%s' % corrected_format, ) # Handle error conditions per iiif.io/api/image/2.1/#server-responses except ClientError, ex: return HttpResponseBadRequest(ex.message) # 400 response
def writeGif(images, duration=0.1, repeat=True, dither=False, nq=0, subRectangles=True, dispose=None): """ writeGif(images, duration=0.1, repeat=True, dither=False, nq=0, subRectangles=True, dispose=None) Write an animated gif from the specified images. Parameters ---------- images : list Should be a list consisting of PIL images or numpy arrays. The latter should be between 0 and 255 for integer types, and between 0 and 1 for float types. duration : scalar or list of scalars The duration for all frames, or (if a list) for each frame. repeat : bool or integer The amount of loops. If True, loops infinitetely. dither : bool Whether to apply dithering nq : integer If nonzero, applies the NeuQuant quantization algorithm to create the color palette. This algorithm is superior, but slower than the standard PIL algorithm. The value of nq is the quality parameter. 1 represents the best quality. 10 is in general a good tradeoff between quality and speed. When using this option, better results are usually obtained when subRectangles is False. subRectangles : False, True, or a list of 2-element tuples Whether to use sub-rectangles. If True, the minimal rectangle that is required to update each frame is automatically detected. This can give significant reductions in file size, particularly if only a part of the image changes. One can also give a list of x-y coordinates if you want to do the cropping yourself. The default is True. dispose : int How to dispose each frame. 1 means that each frame is to be left in place. 2 means the background color should be restored after each frame. 3 means the decoder should restore the previous frame. If subRectangles==False, the default is 2, otherwise it is 1. """ # Check PIL if PIL is None: raise RuntimeError("Need PIL to write animated gif files.") # Check images images = checkImages(images) # Instantiate writer object gifWriter = GifWriter() gifWriter.transparency = False # init transparency flag used in GifWriter functions # Check loops if repeat is True: loops = 0 # zero means infinite elif repeat is False or repeat == 1: loops = -1 else: loops = int(repeat-1) # Check duration if hasattr(duration, '__len__'): if len(duration) == len(images): duration = [d for d in duration] else: raise ValueError("len(duration) doesn't match amount of images.") else: duration = [duration for im in images] # Check subrectangles if subRectangles: images, xy, images_info = gifWriter.handleSubRectangles(images, subRectangles) defaultDispose = 1 # Leave image in place else: # Normal mode xy = [(0,0) for im in images] defaultDispose = 2 # Restore to background color. # Check dispose if dispose is None: dispose = defaultDispose if hasattr(dispose, '__len__'): if len(dispose) != len(images): raise ValueError("len(xy) doesn't match amount of images.") else: dispose = [dispose for im in images] # Make images in a format that we can write easy images = gifWriter.convertImagesToPIL(images, dither, nq) # Write try: from cStringIO import cStringIO as BytesIO except ImportError: from django.utils.six import BytesIO fp = BytesIO() gifWriter.writeGifToFile(fp, images, duration, loops, xy, dispose) fp.seek(0) return fp
def writeGif(images, duration=0.1, repeat=True, dither=False, nq=0, subRectangles=True, dispose=None): """ writeGif(images, duration=0.1, repeat=True, dither=False, nq=0, subRectangles=True, dispose=None) Write an animated gif from the specified images. Parameters ---------- images : list Should be a list consisting of PIL images or numpy arrays. The latter should be between 0 and 255 for integer types, and between 0 and 1 for float types. duration : scalar or list of scalars The duration for all frames, or (if a list) for each frame. repeat : bool or integer The amount of loops. If True, loops infinitetely. dither : bool Whether to apply dithering nq : integer If nonzero, applies the NeuQuant quantization algorithm to create the color palette. This algorithm is superior, but slower than the standard PIL algorithm. The value of nq is the quality parameter. 1 represents the best quality. 10 is in general a good tradeoff between quality and speed. When using this option, better results are usually obtained when subRectangles is False. subRectangles : False, True, or a list of 2-element tuples Whether to use sub-rectangles. If True, the minimal rectangle that is required to update each frame is automatically detected. This can give significant reductions in file size, particularly if only a part of the image changes. One can also give a list of x-y coordinates if you want to do the cropping yourself. The default is True. dispose : int How to dispose each frame. 1 means that each frame is to be left in place. 2 means the background color should be restored after each frame. 3 means the decoder should restore the previous frame. If subRectangles==False, the default is 2, otherwise it is 1. """ # Check PIL if PIL is None: raise RuntimeError("Need PIL to write animated gif files.") # Check images images = checkImages(images) # Instantiate writer object gifWriter = GifWriter() gifWriter.transparency = False # init transparency flag used in GifWriter functions # Check loops if repeat is True: loops = 0 # zero means infinite elif repeat is False or repeat == 1: loops = -1 else: loops = int(repeat - 1) # Check duration if hasattr(duration, '__len__'): if len(duration) == len(images): duration = [d for d in duration] else: raise ValueError("len(duration) doesn't match amount of images.") else: duration = [duration for im in images] # Check subrectangles if subRectangles: images, xy, images_info = gifWriter.handleSubRectangles( images, subRectangles) defaultDispose = 1 # Leave image in place else: # Normal mode xy = [(0, 0) for im in images] defaultDispose = 2 # Restore to background color. # Check dispose if dispose is None: dispose = defaultDispose if hasattr(dispose, '__len__'): if len(dispose) != len(images): raise ValueError("len(xy) doesn't match amount of images.") else: dispose = [dispose for im in images] # Make images in a format that we can write easy images = gifWriter.convertImagesToPIL(images, dither, nq) # Write try: from cStringIO import cStringIO as BytesIO except ImportError: from django.utils.six import BytesIO fp = BytesIO() gifWriter.writeGifToFile(fp, images, duration, loops, xy, dispose) fp.seek(0) return fp