Put Filter back together

pull/477/head
Karl Hobley 2014-07-22 16:04:58 +01:00
rodzic b1d412f0f6
commit 9ebddf2389
6 zmienionych plików z 88 dodań i 115 usunięć

Wyświetl plik

@ -1,23 +0,0 @@
from wagtail.wagtailimages.backends import get_image_backend
from wagtail.wagtailimages.utils import parse_filter_spec
def process_image(input_file, output_file, filter_spec, backend_name='default'):
# Get the image backend
backend = get_image_backend(backend_name)
# Parse the filter spec
method_name, method_arg = parse_filter_spec(filter_spec)
# Load image
image = backend.open_image(input_file)
file_format = image.format
# Call method
method = getattr(backend, method_name)
image = method(image, method_arg)
# Save image
backend.save_image(image, output_file, file_format)
return output_file

Wyświetl plik

@ -1,4 +1,5 @@
import os.path
import re
from six import BytesIO
@ -14,6 +15,7 @@ from django.utils.html import escape, format_html_join
from django.conf import settings
from django.utils.translation import ugettext_lazy as _
from django.utils.encoding import python_2_unicode_compatible
from django.utils.functional import cached_property
from unidecode import unidecode
@ -21,7 +23,6 @@ from wagtail.wagtailadmin.taggable import TagSearchable
from wagtail.wagtailimages.backends import get_image_backend
from wagtail.wagtailsearch import indexed
from wagtail.wagtailimages.utils import validate_image_format
from wagtail.wagtailimages import image_processor
@python_2_unicode_compatible
@ -70,7 +71,16 @@ class AbstractImage(models.Model, TagSearchable):
# If we have a backend attribute then pass it to process
# image - else pass 'default'
backend_name = getattr(self, 'backend', 'default')
generated_image_file = filter.process_image(file_field.file, backend_name=backend_name)
generated_image = filter.process_image(file_field.file, backend_name=backend_name)
# generate new filename derived from old one, inserting the filter spec string before the extension
input_filename_parts = os.path.basename(file_field.file.name).split('.')
filename_without_extension = '.'.join(input_filename_parts[:-1])
filename_without_extension = filename_without_extension[:60] # trim filename base so that we're well under 100 chars
output_filename_parts = [filename_without_extension, filter.spec] + input_filename_parts[-1:]
output_filename = '.'.join(output_filename_parts)
generated_image_file = File(generated_image, name=output_filename)
rendition, created = self.renditions.get_or_create(
filter=filter, defaults={'file': generated_image_file})
@ -144,29 +154,84 @@ class Filter(models.Model):
"""
spec = models.CharField(max_length=255, db_index=True)
def process_image(self, input_file, backend_name='default'):
OPERATION_NAMES = {
'max': 'resize_to_max',
'min': 'resize_to_min',
'width': 'resize_to_width',
'height': 'resize_to_height',
'fill': 'resize_to_fill',
'original': 'no_operation',
}
class InvalidFilterSpecError(ValueError):
pass
def _parse_spec_string(self):
# parse the spec string and return the method name and method arg.
# There are various possible formats to match against:
# 'original'
# 'width-200'
# 'max-320x200'
if self.spec == 'original':
return Filter.OPERATION_NAMES['original'], None
match = re.match(r'(width|height)-(\d+)$', self.spec)
if match:
return Filter.OPERATION_NAMES[match.group(1)], int(match.group(2))
match = re.match(r'(max|min|fill)-(\d+)x(\d+)$', self.spec)
if match:
width = int(match.group(2))
height = int(match.group(3))
return Filter.OPERATION_NAMES[match.group(1)], (width, height)
# Spec is not one of our recognised patterns
raise Filter.InvalidFilterSpecError("Invalid image filter spec: %r" % self.spec)
@cached_property
def _method(self):
return self._parse_spec_string()
def is_valid(self):
try:
self._parse_spec_string()
return True
except Filter.InvalidFilterSpecError:
return False
def process_image(self, input_file, output_file=None, backend_name='default'):
"""
Given an input image file as a django.core.files.File object,
generate an output image with this filter applied, returning it
as another django.core.files.File object
"""
# If file is closed, open it
# Get backend
backend = get_image_backend(backend_name)
# Parse spec string
method_name, method_arg = self._method
# Open image
input_file.open('rb')
image = backend.open_image(input_file)
file_format = image.format
# Process the image
output = image_processor.process_image(input_file, BytesIO(), self.spec, backend_name=backend_name)
# Process image
method = getattr(backend, method_name)
image = method(image, method_arg)
# and then close the input file
# Make sure we have an output file
if output_file is None:
output_file = BytesIO()
# Write output
backend.save_image(image, output_file, file_format)
# Close the input file
input_file.close()
# generate new filename derived from old one, inserting the filter spec string before the extension
input_filename_parts = os.path.basename(input_file.name).split('.')
filename_without_extension = '.'.join(input_filename_parts[:-1])
filename_without_extension = filename_without_extension[:60] # trim filename base so that we're well under 100 chars
output_filename_parts = [filename_without_extension, self.spec] + input_filename_parts[-1:]
output_filename = '.'.join(output_filename_parts)
return File(output, name=output_filename)
return output_file
class AbstractRendition(models.Model):

Wyświetl plik

@ -23,7 +23,7 @@ from wagtail.wagtailimages.formats import (
from wagtail.wagtailimages.backends import get_image_backend
from wagtail.wagtailimages.backends.pillow import PillowBackend
from wagtail.wagtailimages.utils import parse_filter_spec, InvalidFilterSpecError, generate_signature, verify_signature
from wagtail.wagtailimages.utils import generate_signature, verify_signature
def get_test_image_file():
@ -480,31 +480,6 @@ class TestFormat(TestCase):
self.assertEqual(result, self.format)
class TestFilterSpecParsing(TestCase):
good = {
'original': ('no_operation', None),
'min-800x600': ('resize_to_min', (800, 600)),
'max-800x600': ('resize_to_max', (800, 600)),
'fill-800x600': ('resize_to_fill', (800, 600)),
'width-800': ('resize_to_width', 800),
'height-600': ('resize_to_height', 600),
}
bad = [
'original-800x600', # Shouldn't have parameters
'abcdefg', # Filter doesn't exist
'min', 'max', 'fill', 'width', 'height' , # Should have parameters
]
def test_good(self):
for filter_spec, expected_result in self.good.items():
self.assertEqual(parse_filter_spec(filter_spec), expected_result)
def test_bad(self):
for filter_spec in self.bad:
self.assertRaises(InvalidFilterSpecError, parse_filter_spec, filter_spec)
class TestSignatureGeneration(TestCase):
def test_signature_generation(self):
self.assertEqual(generate_signature(100, 'fill-800x600'), b'xnZOzQyUg6pkfciqcfRJRosOrGg=')

Wyświetl plik

@ -34,47 +34,6 @@ def validate_image_format(f):
raise ValidationError(_("Not a valid %s image. Please use a gif, jpeg or png file with the correct file extension.") % (extension.upper()))
class InvalidFilterSpecError(RuntimeError):
pass
# TODO: Cache results from this method in something like Python 3.2s LRU cache (available in Django 1.7 as django.utils.lru_cache)
def parse_filter_spec(filter_spec):
# parse the spec string and save the results to
# self.method_name and self.method_arg. There are various possible
# formats to match against:
# 'original'
# 'width-200'
# 'max-320x200'
OPERATION_NAMES = {
'max': 'resize_to_max',
'min': 'resize_to_min',
'width': 'resize_to_width',
'height': 'resize_to_height',
'fill': 'resize_to_fill',
'original': 'no_operation',
}
# original
if filter_spec == 'original':
return OPERATION_NAMES['original'], None
# width/height
match = re.match(r'(width|height)-(\d+)$', filter_spec)
if match:
return OPERATION_NAMES[match.group(1)], int(match.group(2))
# max/min/fill
match = re.match(r'(max|min|fill)-(\d+)x(\d+)$', filter_spec)
if match:
width = int(match.group(2))
height = int(match.group(3))
return OPERATION_NAMES[match.group(1)], (width, height)
raise InvalidFilterSpecError(filter_spec)
def generate_signature(image_id, filter_spec):
# Based on libthumbor hmac generation
# https://github.com/thumbor/libthumbor/blob/b19dc58cf84787e08c8e397ab322e86268bb4345/libthumbor/crypto.py#L50

Wyświetl plik

@ -3,9 +3,8 @@ from django.http import HttpResponse
from django.core.exceptions import PermissionDenied
from django.views.decorators.cache import cache_page
from wagtail.wagtailimages.models import get_image_model
from wagtail.wagtailimages.utils import InvalidFilterSpecError, verify_signature
from wagtail.wagtailimages import image_processor
from wagtail.wagtailimages.models import get_image_model, Filter
from wagtail.wagtailimages.utils import verify_signature
@cache_page(60 * 60 * 24 * 60) # Cache for 60 days
@ -16,6 +15,6 @@ def serve(request, signature, image_id, filter_spec):
raise PermissionDenied
try:
return image_processor.process_image(image.file.file, HttpResponse(content_type='image/jpeg'), filter_spec)
except InvalidFilterSpecError:
return Filter(spec=filter_spec).process_image(image.file.file, HttpResponse(content_type='image/jpeg'))
except Filter.InvalidFilterSpecError:
return HttpResponse("Invalid filter spec: " + filter_spec, content_type='text/plain', status=400)

Wyświetl plik

@ -13,9 +13,9 @@ from django.http import HttpResponse
from wagtail.wagtailcore.models import Site
from wagtail.wagtailadmin.forms import SearchForm
from wagtail.wagtailimages.models import get_image_model
from wagtail.wagtailimages.models import get_image_model, Filter
from wagtail.wagtailimages.forms import get_image_form, URLGeneratorForm
from wagtail.wagtailimages.utils import parse_filter_spec, InvalidFilterSpecError, generate_signature
from wagtail.wagtailimages.utils import generate_signature
@permission_required('wagtailimages.add_image')
@ -159,9 +159,7 @@ def generate_url(request, image_id, filter_spec):
}, status=403)
# Parse the filter spec to make sure its valid
try:
parse_filter_spec(filter_spec)
except InvalidFilterSpecError:
if not Filter(spec=filter_spec).is_valid():
return json_response({
'error': "Invalid filter spec."
}, status=400)