def pil_image(source, exif_orientation=True, **options): """ Try to open the source file directly using PIL, ignoring any errors. exif_orientation If EXIF orientation data is present, perform any required reorientation before passing the data along the processing pipeline. """ # Use a BytesIO wrapper because if the source is an incomplete file like # object, PIL may have problems with it. For example, some image types # require tell and seek methods that are not present on all storage # File objects. if not source: return source = BytesIO(source.read()) try: image = Image.open(source) # Fully load the image now to catch any problems with the image # contents. image.load() except Exception: return if exif_orientation: image = utils.exif_orientation(image) return image
def dquery_drupal_update_recommended_release(project, compatibility, cache=True): data = dquery_drupal_update_info_data(compatibility, project) f = StringIO(data) tree = lxml.etree.parse(f) error = tree.xpath('/error/text()') version_data = None #with context manager possible for f? try: if len(error): message = 'error requesting update information for {0!r}: {1!r}' raise DQueryException(message.format(project, error[0])) project_status = tree.xpath('/project/project_status/text()')[0] #just hacking this together for now if project_status == 'unsupported': message = 'project {0!r} is unsupported' raise DQueryException(message.format(project)) #Check drupal logic for this recommended_major = tree.xpath('/project/recommended_major/text()') if len(recommended_major): major = recommended_major[0] else: default_major = tree.xpath('/project/default_major/text()') major = default_major[0] recommended_release = tree.xpath('/project/releases/release[version_major = $major][1]', major = major)[0] version = recommended_release.xpath('version/text()')[0] tag = recommended_release.xpath('tag/text()')[0] version_data = {'version' : str(version), 'tag' : str(tag)} except DQueryException as e: warnings.warn(e.message, DQueryWarning) finally: f.close() return version_data
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 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 pil_image(source, exif_orientation=True, **options): """ Try to open the source file directly using PIL, ignoring any errors. exif_orientation If EXIF orientation data is present, perform any required reorientation before passing the data along the processing pipeline. """ # Use a BytesIO wrapper because if the source is an incomplete file like # object, PIL may have problems with it. For example, some image types # require tell and seek methods that are not present on all storage # File objects. if not source: return source = BytesIO(source.read()) image = Image.open(source) # Fully load the image now to catch any problems with the image # contents. image.load() if exif_orientation: image = utils.exif_orientation(image) return image
def pil_image(source, exif_orientation=True, **options): """ Try to open the source file directly using PIL, ignoring any errors. exif_orientation If EXIF orientation data is present, perform any required reorientation before passing the data along the processing pipeline. """ # Use a BytesIO wrapper because if the source is an incomplete file like # object, PIL may have problems with it. For example, some image types # require tell and seek methods that are not present on all storage # File objects. if not source: return source = BytesIO(source.read()) image = Image.open(source) # Fully load the image now to catch any problems with the image contents. try: # An "Image file truncated" exception can occur for some images that # are still mostly valid -- we'll swallow the exception. image.load() except IOError: pass # Try a second time to catch any other potential exceptions. image.load() if exif_orientation: image = utils.exif_orientation(image) return image
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 to_string(self, indent="", newl="", addindent=""): ''' Returns a string representation of the XMLi element. @return: str ''' buf = StringIO() self.to_xml().writexml(buf, indent=indent, addindent=addindent, newl=newl) return buf.getvalue()
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 run(self): while True: try: self.loop() except SystemExit: raise except Exception as e: S=StringIO() traceback.print_exc(S) self.send(error=str(e), stack=S.getvalue(), success=False) time.sleep(1)
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)
class FFmpegInput(BaseInput, AbstractOpus): def __init__(self, source='-', command='avconv', streaming=False, **kwargs): super(FFmpegInput, self).__init__(**kwargs) if source: self.source = source self.streaming = streaming self.command = command self._buffer = None self._proc = None def read(self, sz): if self.streaming: raise TypeError('Cannot read from a streaming FFmpegInput') # First read blocks until the subprocess finishes if not self._buffer: data, _ = self.proc.communicate() self._buffer = BufferedIO(data) # Subsequent reads can just do dis thang return self._buffer.read(sz) def fileobj(self): if self.streaming: return self.proc.stdout else: return self @property def proc(self): if not self._proc: if callable(self.source): self.source = self.source(self) if isinstance(self.source, (tuple, list)): self.source, self.metadata = self.source args = [ self.command, '-i', str(self.source), '-f', 's16le', '-ar', str(self.sampling_rate), '-ac', str(self.channels), '-loglevel', 'warning', 'pipe:1', ] self._proc = subprocess.Popen(args, stdin=None, stdout=subprocess.PIPE) return self._proc
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 read(self, sz): if self.streaming: raise TypeError('Cannot read from a streaming FFmpegInput') # First read blocks until the subprocess finishes if not self._buffer: data, _ = self.proc.communicate() self._buffer = BufferedIO(data) # Subsequent reads can just do dis thang return self._buffer.read(sz)
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 gzip_bytes(bytestring, level=6): """Turn some bytes into some compressed bytes. >>> len(gzip_bytes(b'a' * 10000)) 46 Args: bytestring (bytes): Bytes to be compressed level (int): An integer, 1-9, controlling the speed/compression. 1 is fastest, least compressed, 9 is slowest, but most compressed. Note that all levels of gzip are pretty fast these days, though it's not really a competitor in compression, at any level. """ out = StringIO() f = GzipFile(fileobj=out, mode='wb', compresslevel=level) f.write(bytestring) f.close() return out.getvalue()
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 render_claims_to_pdf(request, slug, claim_group, deferred_awards): """Currently hard-coded to print to Avery 22805 labels""" metrics = dict( page_width=(8.5 * inch), page_height=(11.0 * inch), top_margin=(0.5 * inch), left_margin=((25.0 / 32.0) * inch), qr_overlap=((1.0 / 32.0) * inch), padding=((1.0 / 16.0) * inch), horizontal_spacing=((5.0 / 16.0) * inch), vertical_spacing=((13.0 / 64.0) * inch), width=(1.5 * inch), height=(1.5 * inch), ) debug = request.GET.get("debug", False) is not False pagesize = (metrics["page_width"], metrics["page_height"]) cols = int((metrics["page_width"] - metrics["left_margin"]) / (metrics["width"] + metrics["horizontal_spacing"])) rows = int((metrics["page_height"] - metrics["top_margin"]) / (metrics["height"] + metrics["vertical_spacing"])) per_page = cols * rows label_ct = len(deferred_awards) page_ct = math.ceil(label_ct / per_page) pages = [deferred_awards[x : x + (per_page)] for x in range(0, label_ct, per_page)] response = HttpResponse(content_type="application/pdf; charset=utf-8") if not debug: # If debugging, don't force download. response["Content-Disposition"] = 'attachment; filename="%s-%s.pdf"' % ( slug.encode("utf-8", "replace"), claim_group, ) badge_img = None fout = StringIO() c = canvas.Canvas(fout, pagesize=pagesize) for page in pages: c.translate(metrics["left_margin"], metrics["page_height"] - metrics["top_margin"]) for row in range(0, rows, 1): c.translate(0.0, 0 - metrics["height"]) c.saveState() for col in range(0, cols, 1): try: da = page.pop(0) except IndexError: continue if not badge_img: image_fin = da.badge.image.file image_fin.open() badge_img = ImageReader(StringIO(image_fin.read())) c.saveState() render_label(request, c, metrics, da, badge_img, debug) c.restoreState() dx = metrics["width"] + metrics["horizontal_spacing"] c.translate(dx, 0.0) c.restoreState() c.translate(0.0, 0 - metrics["vertical_spacing"]) c.showPage() c.save() response.write(fout.getvalue()) return 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 render_claims_to_pdf(request, slug, claim_group, deferred_awards): """Currently hard-coded to print to Avery 22805 labels""" metrics = dict( page_width=(8.5 * inch), page_height=(11.0 * inch), top_margin=(0.5 * inch), left_margin=((25.0/32.0) * inch), qr_overlap=((1.0/32.0) * inch), padding=((1.0/16.0) * inch), horizontal_spacing=((5.0/16.0) * inch), vertical_spacing=((13.0/64.0) * inch), width=(1.5 * inch), height=(1.5 * inch), ) debug = (request.GET.get('debug', False) is not False) pagesize = (metrics['page_width'], metrics['page_height']) cols = int((metrics['page_width'] - metrics['left_margin']) / (metrics['width'] + metrics['horizontal_spacing'])) rows = int((metrics['page_height'] - metrics['top_margin']) / (metrics['height'] + metrics['vertical_spacing'])) per_page = (cols * rows) label_ct = len(deferred_awards) page_ct = math.ceil(label_ct / per_page) pages = [deferred_awards[x:x+(per_page)] for x in range(0, label_ct, per_page)] response = HttpResponse(content_type='application/pdf; charset=utf-8') if not debug: # If debugging, don't force download. response['Content-Disposition'] = ('attachment; filename="%s-%s.pdf"' % (slug.encode('utf-8', 'replace'), claim_group)) badge_img = None fout = StringIO() c = canvas.Canvas(fout, pagesize=pagesize) for page in pages: c.translate(metrics['left_margin'], metrics['page_height']-metrics['top_margin']) for row in range(0, rows, 1): c.translate(0.0, 0 - metrics['height']) c.saveState() for col in range(0, cols, 1): try: da = page.pop(0) except IndexError: continue if not badge_img: image_fin = da.badge.image.file image_fin.open() badge_img = ImageReader(StringIO(image_fin.read())) c.saveState() render_label(request, c, metrics, da, badge_img, debug) c.restoreState() dx = (metrics['width'] + metrics['horizontal_spacing']) c.translate(dx, 0.0) c.restoreState() c.translate(0.0, 0 - metrics['vertical_spacing']) c.showPage() c.save() response.write(fout.getvalue()) return 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 render_claims_to_pdf(request, slug, claim_group, deferred_awards): """Currently hard-coded to print to Avery 22805 labels""" metrics = dict( page_width=(8.5 * inch), page_height=(11.0 * inch), top_margin=(0.5 * inch), left_margin=((25.0/32.0) * inch), horizontal_spacing=((5.0/16.0) * inch), vertical_spacing=((13.0/64.0) * inch), width=(1.5 * inch), height=(1.5 * inch), ) debug = (request.GET.get('debug', False) is not False) pagesize = (metrics['page_width'], metrics['page_height']) cols = int((metrics['page_width'] - metrics['left_margin']) / (metrics['width'] + metrics['horizontal_spacing'])) rows = int((metrics['page_height'] - metrics['top_margin']) / (metrics['height'] + metrics['vertical_spacing'])) per_page = (cols * rows) label_ct = len(deferred_awards) page_ct = math.ceil(label_ct / per_page) pages = [deferred_awards[x:x+(per_page)] for x in range(0, label_ct, per_page)] response = HttpResponse(content_type='application/pdf; charset=utf-8') if not debug: # If debugging, don't force download. response['Content-Disposition'] = ('attachment; filename="%s-%s.pdf"' % (slug.encode('utf-8', 'replace'), claim_group)) fout = StringIO() c = canvas.Canvas(fout, pagesize=pagesize) for page in pages: c.translate(metrics['left_margin'], metrics['page_height']-metrics['top_margin']) for row in range(0, rows, 1): c.translate(0.0, 0 - metrics['height']) c.saveState() for col in range(0, cols, 1): try: da = page.pop(0) except IndexError: continue c.saveState() render_label(request, c, metrics, da) c.restoreState() dx = (metrics['width'] + metrics['horizontal_spacing']) c.translate(dx, 0.0) c.restoreState() c.translate(0.0, 0 - metrics['vertical_spacing']) c.showPage() c.save() response.write(fout.getvalue()) return response
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