def test_6_digit_hex(self): fil = Filter(spec='width-400|bgcolor-ffffff') image = Image.objects.create( title="Test image", file=get_test_image_file(), ) out = fil.run(image, BytesIO()) self.assertFalse(out.has_alpha())
def test_gif(self): fil = Filter(spec='width-400|format-gif') image = Image.objects.create( title="Test image", file=get_test_image_file(), ) out = fil.run(image, BytesIO()) self.assertEqual(out.format_name, 'gif')
def test_png(self): fil = Filter(spec='width-400|format-png') image = Image.objects.create( title="Test image", file=get_test_image_file(), ) out = fil.run(image, BytesIO()) self.assertEqual(out.format_name, 'png')
def test_original_has_alpha(self): # Checks that the test image we're using has alpha fil = Filter(spec='width-400') image = Image.objects.create( title="Test image", file=get_test_image_file(), ) out = fil.run(image, BytesIO()) self.assertTrue(out.has_alpha())
def test_override_webp_convert_to_png(self): """WAGTAILIMAGES_FORMAT_CONVERSIONS can be overridden to disable webp conversion""" fil = Filter(spec='width-400') image = Image.objects.create( title="Test image", file=get_test_image_file_webp(), ) out = fil.run(image, BytesIO()) self.assertEqual(out.format_name, 'webp')
def test_webp_convert_to_png(self): """by default, webp images will be converted to png""" fil = Filter(spec='width-400') image = Image.objects.create( title="Test image", file=get_test_image_file_webp(), ) out = fil.run(image, BytesIO()) self.assertEqual(out.format_name, 'png')
def test_webp_quality_setting(self): fil = Filter(spec="width-400|format-webp") image = Image.objects.create( title="Test image", file=get_test_image_file_jpeg(), ) f = BytesIO() with patch("PIL.Image.Image.save") as save: fil.run(image, f) save.assert_called_with(f, "WEBP", quality=50, lossless=False)
def test_jpeg_quality_filter_overrides_setting(self): fil = Filter(spec='width-400|jpegquality-40') image = Image.objects.create( title="Test image", file=get_test_image_file_jpeg(), ) f = BytesIO() with patch('PIL.Image.Image.save') as save: fil.run(image, f) save.assert_called_with(f, 'JPEG', quality=40, optimize=True, progressive=True)
def test_jpeg_quality_filter_overrides_setting(self): fil = Filter(spec='width-400|webpquality-40|format-webp') image = Image.objects.create( title="Test image", file=get_test_image_file_jpeg(), ) f = BytesIO() with patch('PIL.Image.Image.save') as save: fil.run(image, f) save.assert_called_with(f, 'WEBP', quality=40, lossless=False)
def render(self, context): try: image = self.image_expr.resolve(context) mode = self.mode_expr.resolve(context) # We call abs() just in case the user set one of the dimensions to a negative number. width = abs(int( self.width_expr.resolve(context))) if self.width_expr else 1 height = abs(int( self.height_expr.resolve(context))) if self.height_expr else 1 except template.VariableDoesNotExist: return '' if not image: return '' # Build a filter spec based on the specified mode, height, and width for the base rendition. if mode == 'width': base_spec = "width-{}".format(width) elif mode == 'height': base_spec = "height-{}".format(height) else: base_spec = "{}-{}x{}-c100".format(mode, width, height) base_rendition = get_rendition_or_not_found(image, Filter(spec=base_spec)) # Build the fallback <img> tag for browsers that don't support <picture>. custom_attrs = { attr_name: expression.resolve(context) for attr_name, expression in self.attrs.items() } img_tag = base_rendition.img_tag(custom_attrs) # If the image is wider than a phone, add an additional, smaller rendition with the same aspect ratio. small_width = 425 # Two notes here: # 1) We used 'from __future__ import division' to make this use floating point division. # 2) Filter specs don't accept floats, so we need to cast back to int at the end. small_height = int(height * (small_width / width)) if mode == 'width': small_spec = "width-{}".format(small_width) else: # TODO: If the mode is 'height', this might not look right. I'm not really sure, though. small_spec = "fill-{}x{}-c100".format(small_width, small_height) small_rendition = get_rendition_or_not_found(image, Filter(spec=small_spec)) return """ <picture> <source srcset="{small.url}" media="(max-width: 499px)"> <source srcset="{full.url}" media="(min-width: 500px)"> {img_tag} </picture> """.format(img_tag=img_tag, full=base_rendition, small=small_rendition)
def check_settings(app_configs, **kwargs): errors = [] default_filter = settings.WAGTAIL_GRAPHQL_DEFAULT_RENDITION_FILTER allowed_filters = settings.WAGTAIL_GRAPHQL_ALLOWED_RENDITION_FILTERS from wagtail.images.models import Filter from wagtail.images.exceptions import InvalidFilterSpecError try: Filter(spec=default_filter).operations except InvalidFilterSpecError: errors.append( checks.Error( 'WAGTAIL_GRAPHQL_DEFAULT_RENDITION_FILTER setting must be a ' 'valid Wagtail image filter.', hint='http://docs.wagtail.io/en/stable/topics/images.html', id='wagtail_graphql.E001', )) try: for rendition_filter in filter(lambda x: x != '*', allowed_filters): Filter(spec=rendition_filter).operations except InvalidFilterSpecError: errors.append( checks.Error( 'WAGTAIL_GRAPHQL_ALLOWED_RENDITION_FILTERS setting must ' 'be a collection of valid Wagtail image filters.', hint=( f'"{rendition_filter}" is an invalid filter. See for more ' 'information: ' 'http://docs.wagtail.io/en/stable/topics/images.html', ), id='wagtail_graphql.E002', )) if not isinstance(settings.WAGTAIL_GRAPHQL_ENABLE_IMAGES, bool): errors.append( checks.Warning( 'WAGTAIL_GRAPHQL_ENABLE_IMAGES setting must be a boolean, not ' f'{type(settings.WAGTAIL_GRAPHQL_ENABLE_IMAGES).__name__}.', id='wagtail_graphql.W001', )) if not isinstance(settings.WAGTAIL_GRAPHQL_ENABLE_DOCUMENTS, bool): errors.append( checks.Warning( 'WAGTAIL_GRAPHQL_ENABLE_DOCUMENTS setting must be a boolean, ' 'not {}.'.format( type(settings.WAGTAIL_GRAPHQL_ENABLE_DOCUMENTS).__name__), id='wagtail_graphql.W002', )) return errors
def test_webp_lossless(self): fil = Filter(spec='width-400|format-webp-lossless') image = Image.objects.create( title="Test image", file=get_test_image_file(), ) f = BytesIO() with patch('PIL.Image.Image.save') as save: fil.run(image, f) # quality=80 is default for Williw and PIL libs save.assert_called_with(f, 'WEBP', quality=80, lossless=True)
def test_cache_key_fill_filter_with_focal_point(self): image = Image( width=1000, height=1000, focal_point_width=100, focal_point_height=100, focal_point_x=500, focal_point_y=500, ) fil = Filter(spec='fill-100x100') cache_key = fil.get_cache_key(image) self.assertEqual(cache_key, '0bbe3b2f')
def get_mock_rendition(self, rendition_filter): """Create a mock rendition object that wraps the original image. Using the template tag {% image image 'original' %} will return an <img> tag linking to the original file (instead of a file copy, as is default Wagtail behavior). Template tags with Wagtail size-related filters (width, height, max, and min), e.g. {% image image 'max-165x165' %}, will generate an <img> tag with appropriate size parameters, following logic from wagtail.images.image_operations. """ if isinstance(rendition_filter, str): rendition_filter = Filter(spec=rendition_filter) width = self.width height = self.height for operation in rendition_filter.operations: if isinstance(operation, DoNothingOperation): continue if not any([ isinstance(operation, WidthHeightOperation), isinstance(operation, MinMaxOperation), ]): raise RuntimeError('non-size operations not supported on GIFs') width, height = self.apply_size_operation(operation, width, height) return CFGOVRendition(image=self, file=self.file, width=width, height=height)
def test_jpeg_quality_setting(self): fil = Filter(spec="width-400") image = Image.objects.create( title="Test image", file=get_test_image_file_jpeg(), ) f = BytesIO() with patch("PIL.Image.Image.save") as save: fil.run(image, f) save.assert_called_with(f, "JPEG", quality=50, optimize=True, progressive=True)
def render(self, context): try: image = self.image_expr.resolve(context) except template.VariableDoesNotExist: return '' if not image: if self.output_var_name: context[self.output_var_name] = None return '' if not hasattr(image, 'get_rendition'): raise ValueError( f"{self.tag_name} tag expected an Image object, got {image!r}") filters = [Filter(spec=f) for f in self.raw_filter_specs(context)] renditions = get_renditions_or_not_found(image, filters) if self.output_var_name: # return the rendition object in the given variable context[self.output_var_name] = renditions return '' else: # render the rendition's image tag now resolved_attrs = {} for key in self.attrs: resolved_attrs[key] = self.attrs[key].resolve(context) return render_to_string('img_srcset_wip.html', { "fallback_renditions": renditions, "attributes": resolved_attrs })
def test_runs_operations(self): run_mock = Mock() def run(willow, image, env): run_mock(willow, image, env) self.operation_instance.run = run fil = Filter(spec='operation1|operation2') image = Image.objects.create( title="Test image", file=get_test_image_file(), ) fil.run(image, BytesIO()) self.assertEqual(run_mock.call_count, 2)
def render(self, context): try: image = self.image_expr.resolve(context) mode = self.mode_expr.resolve(context) # We call abs() just in case the user set one of the dimensions to a negative number. width = abs(int( self.width_expr.resolve(context))) if self.width_expr else 0 height = abs(int( self.height_expr.resolve(context))) if self.height_expr else 0 except template.VariableDoesNotExist: return '' if not image: return '' # Build a filter spec based on the specified mode, height, and width for the base rendition. if mode == 'width': spec = "width-{}".format(width) elif mode == 'height': spec = "height-{}".format(height) else: spec = "{}-{}x{}-c100".format(mode, width, height) rendition = get_rendition_or_not_found(image, Filter(spec=spec)) if self.output_var_name: # Save the rendition into the context, instead of outputting a tag. context[self.output_var_name] = rendition return '' else: # Resolve custom attrs to their value in this context, then print them within this rendition's <img> tag. custom_attrs = { attr_name: expression.resolve(context) for attr_name, expression in self.attrs.items() } return rendition.img_tag(custom_attrs)
def test_invalid_length(self): fil = Filter(spec='width-400|bgcolor-1234') image = Image.objects.create( title="Test image", file=get_test_image_file(), ) self.assertRaises(ValueError, fil.run, image, BytesIO())
def test_invalid(self): fil = Filter(spec='width-400|format-foo') image = Image.objects.create( title="Test image", file=get_test_image_file(), ) self.assertRaises(InvalidFilterSpecError, fil.run, image, BytesIO())
def test_webp_quality_filter_too_big(self): fil = Filter(spec='width-400|webpquality-101|format-webp') image = Image.objects.create( title="Test image", file=get_test_image_file_jpeg(), ) self.assertRaises(InvalidFilterSpecError, fil.run, image, BytesIO())
def test_jpeg_quality_filter_no_value(self): fil = Filter(spec='width-400|jpegquality') image = Image.objects.create( title="Test image", file=get_test_image_file_jpeg(), ) self.assertRaises(InvalidFilterSpecError, fil.run, image, BytesIO())
def get_full_image_url(request, image_id): image = get_object_or_404(Image, id=image_id) if image: filter = Filter(spec='original') orig_rendition = image.get_rendition(filter) return HttpResponse(orig_rendition.img_tag()) else: return HttpResponse('')
def preview(request, image_id, filter_spec): image = get_object_or_404(get_image_model(), id=image_id) try: response = HttpResponse() image = Filter(spec=filter_spec).run(image, response) response['Content-Type'] = 'image/' + image.format_name return response except InvalidFilterSpecError: return HttpResponse("Invalid filter spec: " + filter_spec, content_type='text/plain', status=400)
def get_placeholder_rendition(self, rendition_filter): if isinstance(rendition_filter, str): rendition_filter = Filter(spec=rendition_filter) available_methods = [ "width", "height", "fill", "max", "min", "scale", "original" ] operation = next(filter(lambda x: x.method in available_methods, rendition_filter.operations)) return Placeholder(spec=operation, img=self)
def get_context(self, value, parent_context=None): context = super().get_context(value, parent_context=parent_context) if value: width = 1110 img0 = value[0]['image'] ratio = img0.width / img0.height filter = Filter(spec=f'fill-{width}x{int(width/ratio)}') context['images'] = [ (item, get_rendition_or_not_found(item['image'], filter)) for item in value ] context['carousel_id'] = f'carousel-{img0.pk}' return context
def webp_filter(self): """ Return a ``wagtail.images.models.Filter`` instance to use for webp ``source`` rendition. By default, lossy webp renditions are created, but you can add 'q-100' or 'quality-100' to use webp-lossless. """ target_format = "webp" spec_list = self.filter_spec.split("|") if self.quality: if self.quality == "100": target_format = "webp-lossless" elif "webpquality-" not in self.filter_spec: spec_list.append(f"webpquality-{self.quality}") spec_list.append(f"format-{target_format}") return Filter("|".join(spec_list))
def generate_url(request, image_id, filter_spec): # Get the image Image = get_image_model() try: image = Image.objects.get(id=image_id) except Image.DoesNotExist: return JsonResponse({"error": "Cannot find image."}, status=404) # Check if this user has edit permission on this image if not permission_policy.user_has_permission_for_instance( request.user, "change", image): return JsonResponse( { "error": "You do not have permission to generate a URL for this image." }, status=403, ) # Parse the filter spec to make sure its valid try: Filter(spec=filter_spec).operations except InvalidFilterSpecError: return JsonResponse({"error": "Invalid filter spec."}, status=400) # Generate url signature = generate_signature(image_id, filter_spec) url = reverse("wagtailimages_serve", args=(signature, image_id, filter_spec)) # Get site root url try: site_root_url = Site.objects.get(is_default_site=True).root_url except Site.DoesNotExist: site_root_url = Site.objects.first().root_url # Generate preview url preview_url = reverse("wagtailimages:preview", args=(image_id, filter_spec)) return JsonResponse( { "url": site_root_url + url, "preview_url": preview_url }, status=200)
def test_uniqueness_constraint(self): image = CFGOVImage.objects.create(title='test', file=get_test_image_file()) filt = Filter(spec='original') def create_rendition(image, filt): return CFGOVRendition.objects.create( filter_spec=filt.spec, image=image, file=image.file, width=100, height=100, focal_point_key=filt.get_cache_key(image)) create_rendition(image=image, filt=filt) with self.assertRaises(IntegrityError): create_rendition(image=image, filt=filt)
def filter(self): """ Return a ``wagtail.images.models.Filter`` instance to use for creating <img> tag rendition. This will serve as our IE11/Safari fallback, so needs to be a png, jpeg or gif (not webp). By default, webp images are convered to png in order to preserve transparency. Add 'format-jpeg' to convert to jpegs instead. """ target_format = None spec_list = self.filter_spec.split("|") if self.specified_format: target_format = self.specified_format if target_format in ("webp", "webp-lossless"): target_format = "png" elif self.native_format == "webp": target_format = "png" if target_format: spec_list.append(f"format-{target_format}") if target_format == "jpeg": if "jpegquality-" not in self.filter_spec: spec_list.append(f"jpegquality-{self.quality}") return Filter("|".join(spec_list))
def test_cache_key(self): image = Image(width=1000, height=1000) fil = Filter(spec='max-100x100') cache_key = fil.get_cache_key(image) self.assertEqual(cache_key, '')
def test_cache_key_fill_filter(self): image = Image(width=1000, height=1000) fil = Filter(spec='fill-100x100') cache_key = fil.get_cache_key(image) self.assertEqual(cache_key, '2e16d0ba')