From 6a1a22825bc33d89033176d229736e105a136f42 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Sun, 12 Feb 2012 00:06:52 -0500 Subject: [PATCH 001/213] Working proof of concept of spec tag --- imagekit/__init__.py | 2 + imagekit/base.py | 94 ++++++++++++++++++++++++ imagekit/templatetags/__init__.py | 1 + imagekit/templatetags/imagekit_tags.py | 99 ++++++++++++++++++++++++++ 4 files changed, 196 insertions(+) create mode 100644 imagekit/base.py create mode 100644 imagekit/templatetags/__init__.py create mode 100644 imagekit/templatetags/imagekit_tags.py diff --git a/imagekit/__init__.py b/imagekit/__init__.py index 1b2a4a3..d19b0f1 100644 --- a/imagekit/__init__.py +++ b/imagekit/__init__.py @@ -2,3 +2,5 @@ __title__ = 'django-imagekit' __author__ = 'Justin Driscoll, Bryan Veloso, Greg Newman, Chris Drackett, Matthew Tretter, Eric Eldredge' __version__ = (1, 1, 0, 'final', 0) __license__ = 'BSD' + +from .base import * diff --git a/imagekit/base.py b/imagekit/base.py new file mode 100644 index 0000000..2fc3d65 --- /dev/null +++ b/imagekit/base.py @@ -0,0 +1,94 @@ +import os + +from django.core.files.images import ImageFile +from django.db.models.fields.files import ImageFieldFile + +from .imagecache import get_default_image_cache_backend +from .generators import SpecFileGenerator +from .processors import ProcessorPipeline, AutoConvert + + +def autodiscover(): + """ + Auto-discover INSTALLED_APPS imagespecs.py modules and fail silently when + not present. This forces an import on them to register any admin bits they + may want. + + Copied from django.contrib.admin + """ + + import copy + from django.conf import settings + from django.utils.importlib import import_module + from django.utils.module_loading import module_has_submodule + from .templatetags import imagekit_tags + + for app in settings.INSTALLED_APPS: + mod = import_module(app) + # Attempt to import the app's admin module. + try: + import_module('%s.imagespecs' % app) + except: + # Decide whether to bubble up this error. If the app just + # doesn't have an admin module, we can ignore the error + # attempting to import it, otherwise we want it to bubble up. + if module_has_submodule(mod, 'imagespecs'): + raise + + +class ImageSpec(object): + def __init__(self, *args, **kwargs): + self._args = args + self._kwargs = kwargs + + def get_file(self, source_file, spec_id): + return ImageSpecFile(source_file, spec_id, *self._args, **self._kwargs) + self.image_cache_backend = getattr(spec, 'image_cache_backend', None) \ + or get_default_image_cache_backend() + + +class ImageSpecFile(ImageFieldFile): + def __init__(self, source_file, spec_id, processors=None, format=None, options={}, + autoconvert=True, storage=None, cache_state_backend=None): + self.generator = SpecFileGenerator(processors=processors, + format=format, options=options, autoconvert=autoconvert, + storage=storage) + self.storage = storage or source_file.storage + self.cache_state_backend = cache_state_backend or \ + get_default_cache_state_backend() + self.source_file = source_file + self.spec_id = spec_id + + @property + def url(self): + self.validate() + return super(ImageFieldFile, self).url + + def _get_file(self): + self.validate() + return super(ImageFieldFile, self).file + + file = property(_get_file, ImageFieldFile._set_file, ImageFieldFile._del_file) + + def clear(self): + return self.cache_state_backend.clear(self) + + def invalidate(self): + return self.cache_state_backend.invalidate(self) + + def validate(self): + return self.cache_state_backend.validate(self) + + @property + def name(self): + source_filename = self.source_file.name + filepath, basename = os.path.split(source_filename) + filename = os.path.splitext(basename)[0] + extension = self.generator.suggest_extension(source_filename) + new_name = '%s%s' % (filename, extension) + cache_filename = ['cache', 'iktt'] + self.spec_id.split(':') + \ + [filepath, new_name] + return os.path.join(*cache_filename) + + def generate(self, save=True): + return self.generator.generate_file(self.name, self.source_file, save) diff --git a/imagekit/templatetags/__init__.py b/imagekit/templatetags/__init__.py new file mode 100644 index 0000000..e0a23fc --- /dev/null +++ b/imagekit/templatetags/__init__.py @@ -0,0 +1 @@ +from .imagekit_tags import spec diff --git a/imagekit/templatetags/imagekit_tags.py b/imagekit/templatetags/imagekit_tags.py new file mode 100644 index 0000000..c9d6f55 --- /dev/null +++ b/imagekit/templatetags/imagekit_tags.py @@ -0,0 +1,99 @@ +from django import template +import os + +register = template.Library() + + +class AlreadyRegistered(Exception): + pass + + +class NotRegistered(Exception): + pass + + +class SpecRegistry(object): + def __init__(self): + self._specs = {} + + def register(self, id, spec): + if id in self._specs: + raise AlreadyRegistered('The spec with id %s is already registered' % id) + self._specs[id] = spec + + def unregister(self, id, spec): + try: + del self._specs[id] + except KeyError: + raise NotRegistered('The spec with id %s is not registered' % id) + + def get_spec(self, id): + try: + return self._specs[id] + except KeyError: + raise NotRegistered('The spec with id %s is not registered' % id) + + +spec_registry = SpecRegistry() + + +class SpecNode(template.Node): + def __init__(self, spec_id, source_image, variable_name): + self.spec_id = spec_id + self.source_image = source_image + self.variable_name = variable_name + + def _default_cache_to(self, instance, path, specname, extension): + """ + Determines the filename to use for the transformed image. Can be + overridden on a per-spec basis by setting the cache_to property on + the spec. + + """ + filepath, basename = os.path.split(path) + filename = os.path.splitext(basename)[0] + new_name = '%s_%s%s' % (filename, specname, extension) + return os.path.join(os.path.join('cache', filepath), new_name) + + def render(self, context): + spec_id = self.spec_id.resolve(context) + spec = spec_registry.get_spec(spec_id) + source_image = self.source_image.resolve(context) + variable_name = str(self.variable_name) + context[variable_name] = spec.get_file(source_image, spec_id) + return '' + + +#@register.tag +def spec(parser, token): + """ + Creates an image based on the provided spec and source image and sets it + as a context variable: + + {% spec 'myapp:thumbnail' mymodel.profile_image as th %} + + + """ + + args = token.split_contents() + + if len(args) != 5 or args[3] != 'as': + raise TemplateSyntaxError('\'spec\' tags must be in the form "{% spec spec_id image as varname %}"') + + return SpecNode(*[parser.compile_filter(arg) for arg in args[1:3] \ + + [args[4]]]) + + +spec = spec_tag = register.tag(spec) + + +def _register_spec(id, spec): + spec_registry.register(id, spec) + + +def _unregister_spec(id, spec): + spec_registry.unregister(id, spec) + + +spec_tag.register = _register_spec +spec_tag.unregister = _unregister_spec From 807beef4180c8eda60015674fc9f7c9fa1c5d810 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Sun, 12 Feb 2012 16:00:43 -0500 Subject: [PATCH 002/213] Centralized spec properies --- imagekit/base.py | 40 +++++++++++++++----------- imagekit/templatetags/imagekit_tags.py | 12 +++++--- 2 files changed, 31 insertions(+), 21 deletions(-) diff --git a/imagekit/base.py b/imagekit/base.py index 2fc3d65..b6865d7 100644 --- a/imagekit/base.py +++ b/imagekit/base.py @@ -36,26 +36,32 @@ def autodiscover(): raise -class ImageSpec(object): - def __init__(self, *args, **kwargs): - self._args = args - self._kwargs = kwargs +class SpecWrapper(object): + """ + Wraps a user-defined spec object so we can access properties that don't + exist without errors. - def get_file(self, source_file, spec_id): - return ImageSpecFile(source_file, spec_id, *self._args, **self._kwargs) + """ + def __init__(self, spec): + self.processors = getattr(spec, 'processors', None) + self.format = getattr(spec, 'format', None) + self.options = getattr(spec, 'options', None) + self.autoconvert = getattr(spec, 'autoconvert', True) + self.storage = getattr(spec, 'storage', None) self.image_cache_backend = getattr(spec, 'image_cache_backend', None) \ or get_default_image_cache_backend() class ImageSpecFile(ImageFieldFile): - def __init__(self, source_file, spec_id, processors=None, format=None, options={}, - autoconvert=True, storage=None, cache_state_backend=None): - self.generator = SpecFileGenerator(processors=processors, - format=format, options=options, autoconvert=autoconvert, - storage=storage) - self.storage = storage or source_file.storage - self.cache_state_backend = cache_state_backend or \ - get_default_cache_state_backend() + def __init__(self, spec, source_file, spec_id): + spec = SpecWrapper(spec) + + self.storage = spec.storage or source_file.storage + self.generator = SpecFileGenerator(processors=spec.processors, + format=spec.format, options=spec.options, + autoconvert=spec.autoconvert, storage=self.storage) + + self.spec = spec self.source_file = source_file self.spec_id = spec_id @@ -71,13 +77,13 @@ class ImageSpecFile(ImageFieldFile): file = property(_get_file, ImageFieldFile._set_file, ImageFieldFile._del_file) def clear(self): - return self.cache_state_backend.clear(self) + return self.spec.image_cache_backend.clear(self) def invalidate(self): - return self.cache_state_backend.invalidate(self) + return self.spec.image_cache_backend.invalidate(self) def validate(self): - return self.cache_state_backend.validate(self) + return self.spec.image_cache_backend.validate(self) @property def name(self): diff --git a/imagekit/templatetags/imagekit_tags.py b/imagekit/templatetags/imagekit_tags.py index c9d6f55..c8069a8 100644 --- a/imagekit/templatetags/imagekit_tags.py +++ b/imagekit/templatetags/imagekit_tags.py @@ -1,5 +1,7 @@ -from django import template import os +from django import template +from .. import ImageSpecFile + register = template.Library() @@ -56,11 +58,13 @@ class SpecNode(template.Node): return os.path.join(os.path.join('cache', filepath), new_name) def render(self, context): - spec_id = self.spec_id.resolve(context) - spec = spec_registry.get_spec(spec_id) source_image = self.source_image.resolve(context) variable_name = str(self.variable_name) - context[variable_name] = spec.get_file(source_image, spec_id) + spec_id = self.spec_id.resolve(context) + spec = spec_registry.get_spec(spec_id) + if callable(spec): + spec = spec() + context[variable_name] = ImageSpecFile(spec, source_image, spec_id) return '' From d275aaa3f70ad8103cb3068ac7a7dcd75237a990 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Mon, 13 Feb 2012 22:05:33 -0500 Subject: [PATCH 003/213] A little reorganization --- imagekit/__init__.py | 2 -- imagekit/{base.py => files.py} | 48 +------------------------- imagekit/templatetags/imagekit_tags.py | 3 +- imagekit/utils.py | 47 ++++++++++++++++++++++++- 4 files changed, 49 insertions(+), 51 deletions(-) rename imagekit/{base.py => files.py} (50%) diff --git a/imagekit/__init__.py b/imagekit/__init__.py index d19b0f1..1b2a4a3 100644 --- a/imagekit/__init__.py +++ b/imagekit/__init__.py @@ -2,5 +2,3 @@ __title__ = 'django-imagekit' __author__ = 'Justin Driscoll, Bryan Veloso, Greg Newman, Chris Drackett, Matthew Tretter, Eric Eldredge' __version__ = (1, 1, 0, 'final', 0) __license__ = 'BSD' - -from .base import * diff --git a/imagekit/base.py b/imagekit/files.py similarity index 50% rename from imagekit/base.py rename to imagekit/files.py index b6865d7..ddfa4a8 100644 --- a/imagekit/base.py +++ b/imagekit/files.py @@ -1,55 +1,9 @@ import os -from django.core.files.images import ImageFile from django.db.models.fields.files import ImageFieldFile -from .imagecache import get_default_image_cache_backend from .generators import SpecFileGenerator -from .processors import ProcessorPipeline, AutoConvert - - -def autodiscover(): - """ - Auto-discover INSTALLED_APPS imagespecs.py modules and fail silently when - not present. This forces an import on them to register any admin bits they - may want. - - Copied from django.contrib.admin - """ - - import copy - from django.conf import settings - from django.utils.importlib import import_module - from django.utils.module_loading import module_has_submodule - from .templatetags import imagekit_tags - - for app in settings.INSTALLED_APPS: - mod = import_module(app) - # Attempt to import the app's admin module. - try: - import_module('%s.imagespecs' % app) - except: - # Decide whether to bubble up this error. If the app just - # doesn't have an admin module, we can ignore the error - # attempting to import it, otherwise we want it to bubble up. - if module_has_submodule(mod, 'imagespecs'): - raise - - -class SpecWrapper(object): - """ - Wraps a user-defined spec object so we can access properties that don't - exist without errors. - - """ - def __init__(self, spec): - self.processors = getattr(spec, 'processors', None) - self.format = getattr(spec, 'format', None) - self.options = getattr(spec, 'options', None) - self.autoconvert = getattr(spec, 'autoconvert', True) - self.storage = getattr(spec, 'storage', None) - self.image_cache_backend = getattr(spec, 'image_cache_backend', None) \ - or get_default_image_cache_backend() +from .utils import SpecWrapper class ImageSpecFile(ImageFieldFile): diff --git a/imagekit/templatetags/imagekit_tags.py b/imagekit/templatetags/imagekit_tags.py index c8069a8..202118f 100644 --- a/imagekit/templatetags/imagekit_tags.py +++ b/imagekit/templatetags/imagekit_tags.py @@ -1,6 +1,7 @@ import os from django import template -from .. import ImageSpecFile + +from ..files import ImageSpecFile register = template.Library() diff --git a/imagekit/utils.py b/imagekit/utils.py index 305bdd4..4f695cf 100644 --- a/imagekit/utils.py +++ b/imagekit/utils.py @@ -4,7 +4,8 @@ import types from django.db.models.loading import cache from django.utils.functional import wraps -from imagekit.lib import Image, ImageFile +from .imagecache import get_default_image_cache_backend +from .lib import Image, ImageFile def img_to_fobj(img, format, **kwargs): @@ -165,3 +166,47 @@ def validate_app_cache(apps, force_revalidation=False): if force_revalidation: f.invalidate() f.validate() + + +def autodiscover(): + """ + Auto-discover INSTALLED_APPS imagespecs.py modules and fail silently when + not present. This forces an import on them to register any admin bits they + may want. + + Copied from django.contrib.admin + """ + + import copy + from django.conf import settings + from django.utils.importlib import import_module + from django.utils.module_loading import module_has_submodule + from .templatetags import imagekit_tags + + for app in settings.INSTALLED_APPS: + mod = import_module(app) + # Attempt to import the app's admin module. + try: + import_module('%s.imagespecs' % app) + except: + # Decide whether to bubble up this error. If the app just + # doesn't have an admin module, we can ignore the error + # attempting to import it, otherwise we want it to bubble up. + if module_has_submodule(mod, 'imagespecs'): + raise + + +class SpecWrapper(object): + """ + Wraps a user-defined spec object so we can access properties that don't + exist without errors. + + """ + def __init__(self, spec): + self.processors = getattr(spec, 'processors', None) + self.format = getattr(spec, 'format', None) + self.options = getattr(spec, 'options', None) + self.autoconvert = getattr(spec, 'autoconvert', True) + self.storage = getattr(spec, 'storage', None) + self.image_cache_backend = getattr(spec, 'image_cache_backend', None) \ + or get_default_image_cache_backend() From 722a5535011a2fd5f42bdcd9391abbe15dd83114 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Mon, 13 Feb 2012 22:12:34 -0500 Subject: [PATCH 004/213] Automatically autodiscover --- imagekit/templatetags/imagekit_tags.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/imagekit/templatetags/imagekit_tags.py b/imagekit/templatetags/imagekit_tags.py index 202118f..92eefb5 100644 --- a/imagekit/templatetags/imagekit_tags.py +++ b/imagekit/templatetags/imagekit_tags.py @@ -59,6 +59,8 @@ class SpecNode(template.Node): return os.path.join(os.path.join('cache', filepath), new_name) def render(self, context): + from ..utils import autodiscover + autodiscover() source_image = self.source_image.resolve(context) variable_name = str(self.variable_name) spec_id = self.spec_id.resolve(context) From 938acedcc7b5852da30b53c77fa52be01b4c9eda Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Mon, 13 Feb 2012 22:13:32 -0500 Subject: [PATCH 005/213] Decorator syntax for registering specs --- imagekit/templatetags/imagekit_tags.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/imagekit/templatetags/imagekit_tags.py b/imagekit/templatetags/imagekit_tags.py index 92eefb5..0cb21ed 100644 --- a/imagekit/templatetags/imagekit_tags.py +++ b/imagekit/templatetags/imagekit_tags.py @@ -94,7 +94,12 @@ def spec(parser, token): spec = spec_tag = register.tag(spec) -def _register_spec(id, spec): +def _register_spec(id, spec=None): + if not spec: + def decorator(cls): + spec_registry.register(id, cls) + return cls + return decorator spec_registry.register(id, spec) From 7ad5cf4db5cfb674bd53968cd00eba4b85e85f71 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Thu, 19 Jul 2012 21:53:50 -0400 Subject: [PATCH 006/213] Use hashes for generated image filenames While this change means users can no longer specify their own filenames, changing a property of a processor, for example, will now result in a new image. This solves a lot of the previous invalidation issues. --- imagekit/conf.py | 1 + imagekit/generators.py | 34 ++++++++++++++++++--- imagekit/models/fields/__init__.py | 20 ++---------- imagekit/models/fields/files.py | 49 ++---------------------------- 4 files changed, 35 insertions(+), 69 deletions(-) diff --git a/imagekit/conf.py b/imagekit/conf.py index 51ddf55..4f3b95c 100644 --- a/imagekit/conf.py +++ b/imagekit/conf.py @@ -3,3 +3,4 @@ from appconf import AppConf class ImageKitConf(AppConf): DEFAULT_IMAGE_CACHE_BACKEND = 'imagekit.imagecache.PessimisticImageCacheBackend' + CACHE_DIR = 'CACHE/images' diff --git a/imagekit/generators.py b/imagekit/generators.py index 046f0c0..344fc67 100644 --- a/imagekit/generators.py +++ b/imagekit/generators.py @@ -1,8 +1,11 @@ +from django.conf import settings +from hashlib import md5 import os +import pickle from .lib import StringIO from .processors import ProcessorPipeline from .utils import (img_to_fobj, open_image, IKContentFile, extension_to_format, - UnknownExtensionError) + suggest_extension, UnknownExtensionError) class SpecFileGenerator(object): @@ -14,14 +17,18 @@ class SpecFileGenerator(object): self.autoconvert = autoconvert self.storage = storage + def get_processors(self, source_file): + processors = self.processors + if callable(processors): + processors = processors(source_file) + return processors + def process_content(self, content, filename=None, source_file=None): img = open_image(content) original_format = img.format # Run the processors - processors = self.processors - if callable(processors): - processors = processors(source_file) + processors = self.get_processors(source_file) img = ProcessorPipeline(processors or []).process(img) options = dict(self.options or {}) @@ -42,6 +49,25 @@ class SpecFileGenerator(object): content = IKContentFile(filename, imgfile.read(), format=format) return img, content + def generate_filename(self, source_file): + source_filename = source_file.name + filename = None + if source_filename: + hash = md5(''.join([ + pickle.dumps(self.get_processors(source_file)), + self.format, + pickle.dumps(self.options), + str(self.autoconvert), + ])).hexdigest() + extension = suggest_extension(source_filename, self.format) + + filename = os.path.normpath(os.path.join( + settings.IMAGEKIT_CACHE_DIR, + os.path.splitext(source_filename)[0], + '%s%s' % (hash, extension))) + + return filename + def generate_file(self, filename, source_file, save=True): """ Generates a new image file by processing the source file and returns diff --git a/imagekit/models/fields/__init__.py b/imagekit/models/fields/__init__.py index b2cceda..faa5fb5 100644 --- a/imagekit/models/fields/__init__.py +++ b/imagekit/models/fields/__init__.py @@ -17,8 +17,8 @@ class ImageSpecField(object): """ def __init__(self, processors=None, format=None, options=None, - image_field=None, pre_cache=None, storage=None, cache_to=None, - autoconvert=True, image_cache_backend=None): + image_field=None, pre_cache=None, storage=None, autoconvert=True, + image_cache_backend=None): """ :param processors: A list of processors to run on the original image. :param format: The format of the output file. If not provided, @@ -33,21 +33,6 @@ class ImageSpecField(object): original image. :param storage: A Django storage system to use to save the generated image. - :param cache_to: Specifies the filename to use when saving the image - cache file. This is modeled after ImageField's ``upload_to`` and - can be either a string (that specifies a directory) or a - callable (that returns a filepath). Callable values should - accept the following arguments: - - - instance -- The model instance this spec belongs to - - path -- The path of the original image - - specname -- the property name that the spec is bound to on - the model instance - - extension -- A recommended extension. If the format of the - spec is set explicitly, this suggestion will be - based on that format. if not, the extension of the - original file will be passed. You do not have to use - this extension, it's only a recommendation. :param autoconvert: Specifies whether automatic conversion using ``prepare_image()`` should be performed prior to saving. :param image_cache_backend: An object responsible for managing the state @@ -71,7 +56,6 @@ class ImageSpecField(object): autoconvert=autoconvert, storage=storage) self.image_field = image_field self.storage = storage - self.cache_to = cache_to self.image_cache_backend = image_cache_backend or \ get_default_image_cache_backend() diff --git a/imagekit/models/fields/files.py b/imagekit/models/fields/files.py index d6f3dc2..c213ac3 100644 --- a/imagekit/models/fields/files.py +++ b/imagekit/models/fields/files.py @@ -1,10 +1,4 @@ -import os -import datetime - from django.db.models.fields.files import ImageField, ImageFieldFile -from django.utils.encoding import force_unicode, smart_str - -from ...utils import suggest_extension class ImageSpecFieldFile(ImageFieldFile): @@ -89,52 +83,13 @@ class ImageSpecFieldFile(ImageFieldFile): if save: self.instance.save() - def _default_cache_to(self, instance, path, specname, extension): - """ - Determines the filename to use for the transformed image. Can be - overridden on a per-spec basis by setting the cache_to property on - the spec. - - """ - filepath, basename = os.path.split(path) - filename = os.path.splitext(basename)[0] - new_name = '%s_%s%s' % (filename, specname, extension) - return os.path.join('cache', filepath, new_name) - @property def name(self): """ - Specifies the filename that the cached image will use. The user can - control this by providing a `cache_to` method to the ImageSpecField. + Specifies the filename that the cached image will use. """ - name = getattr(self, '_name', None) - if not name: - filename = self.source_file.name - new_filename = None - if filename: - cache_to = self.field.cache_to or self._default_cache_to - - if not cache_to: - raise Exception('No cache_to or default_cache_to value' - ' specified') - if callable(cache_to): - suggested_extension = suggest_extension( - self.source_file.name, self.field.generator.format) - new_filename = force_unicode( - datetime.datetime.now().strftime( - smart_str(cache_to(self.instance, - self.source_file.name, self.attname, - suggested_extension)))) - else: - dir_name = os.path.normpath( - force_unicode(datetime.datetime.now().strftime( - smart_str(cache_to)))) - filename = os.path.normpath(os.path.basename(filename)) - new_filename = os.path.join(dir_name, filename) - - self._name = new_filename - return self._name + return self.field.generator.generate_filename(self.source_file) @name.setter def name(self, value): From 197dfb3485112b25ace247d03955304957e88129 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Wed, 5 Sep 2012 21:32:57 -0400 Subject: [PATCH 007/213] Add VALIDATE_ON_ACCESS setting --- imagekit/conf.py | 1 + imagekit/models/fields/__init__.py | 7 ++++++- imagekit/models/fields/files.py | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/imagekit/conf.py b/imagekit/conf.py index 4f3b95c..3d8a651 100644 --- a/imagekit/conf.py +++ b/imagekit/conf.py @@ -3,4 +3,5 @@ from appconf import AppConf class ImageKitConf(AppConf): DEFAULT_IMAGE_CACHE_BACKEND = 'imagekit.imagecache.PessimisticImageCacheBackend' + VALIDATE_ON_ACCESS = True CACHE_DIR = 'CACHE/images' diff --git a/imagekit/models/fields/__init__.py b/imagekit/models/fields/__init__.py index faa5fb5..078f814 100644 --- a/imagekit/models/fields/__init__.py +++ b/imagekit/models/fields/__init__.py @@ -1,5 +1,6 @@ import os +from django.conf import settings from django.db import models from django.db.models.signals import post_init, post_save, post_delete @@ -18,7 +19,7 @@ class ImageSpecField(object): """ def __init__(self, processors=None, format=None, options=None, image_field=None, pre_cache=None, storage=None, autoconvert=True, - image_cache_backend=None): + image_cache_backend=None, validate_on_access=None): """ :param processors: A list of processors to run on the original image. :param format: The format of the output file. If not provided, @@ -38,6 +39,8 @@ class ImageSpecField(object): :param image_cache_backend: An object responsible for managing the state of cached files. Defaults to an instance of IMAGEKIT_DEFAULT_IMAGE_CACHE_BACKEND + :param validate_on_access: Should the image cache be validated when it's + accessed? """ @@ -58,6 +61,8 @@ class ImageSpecField(object): self.storage = storage self.image_cache_backend = image_cache_backend or \ get_default_image_cache_backend() + self.validate_on_access = settings.IMAGEKIT_VALIDATE_ON_ACCESS if \ + validate_on_access is None else validate_on_access def contribute_to_class(self, cls, name): setattr(cls, name, ImageSpecFileDescriptor(self, name)) diff --git a/imagekit/models/fields/files.py b/imagekit/models/fields/files.py index c213ac3..b46d813 100644 --- a/imagekit/models/fields/files.py +++ b/imagekit/models/fields/files.py @@ -31,7 +31,7 @@ class ImageSpecFieldFile(ImageFieldFile): def _require_file(self): if not self.source_file: raise ValueError("The '%s' attribute's image_field has no file associated with it." % self.attname) - else: + elif self.field.validate_on_access: self.validate() def clear(self): From 0fc29ee7cf220c99225b1256b718c9dc32e4862c Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Wed, 5 Sep 2012 21:50:11 -0400 Subject: [PATCH 008/213] Extract useful backend utils --- imagekit/imagecache/__init__.py | 34 ++++++-------------------------- imagekit/utils.py | 35 ++++++++++++++++++++++++++++++++- 2 files changed, 40 insertions(+), 29 deletions(-) diff --git a/imagekit/imagecache/__init__.py b/imagekit/imagecache/__init__.py index cf98a9d..93dfe1b 100644 --- a/imagekit/imagecache/__init__.py +++ b/imagekit/imagecache/__init__.py @@ -1,34 +1,12 @@ -from django.core.exceptions import ImproperlyConfigured -from django.utils.importlib import import_module - -from imagekit.imagecache.base import InvalidImageCacheBackendError, PessimisticImageCacheBackend, NonValidatingImageCacheBackend - -_default_image_cache_backend = None +from ..utils import get_singleton +from .base import InvalidImageCacheBackendError, PessimisticImageCacheBackend, NonValidatingImageCacheBackend def get_default_image_cache_backend(): """ - Get the default image cache backend. Uses the same method as - django.core.file.storage.get_storage_class + Get the default validation backend. """ - global _default_image_cache_backend - if not _default_image_cache_backend: - from django.conf import settings - import_path = settings.IMAGEKIT_DEFAULT_IMAGE_CACHE_BACKEND - try: - dot = import_path.rindex('.') - except ValueError: - raise ImproperlyConfigured("%s isn't an image cache backend module." % \ - import_path) - module, classname = import_path[:dot], import_path[dot + 1:] - try: - mod = import_module(module) - except ImportError, e: - raise ImproperlyConfigured('Error importing image cache backend module %s: "%s"' % (module, e)) - try: - cls = getattr(mod, classname) - _default_image_cache_backend = cls() - except AttributeError: - raise ImproperlyConfigured('Image cache backend module "%s" does not define a "%s" class.' % (module, classname)) - return _default_image_cache_backend + from django.conf import settings + return get_singleton(settings.IMAGEKIT_DEFAULT_IMAGE_CACHE_BACKEND, + 'validation backend') diff --git a/imagekit/utils.py b/imagekit/utils.py index 4ffb556..0fd1b5d 100644 --- a/imagekit/utils.py +++ b/imagekit/utils.py @@ -3,10 +3,12 @@ import mimetypes import sys import types +from django.core.exceptions import ImproperlyConfigured from django.core.files.base import ContentFile from django.db.models.loading import cache -from django.utils.functional import wraps from django.utils.encoding import smart_str, smart_unicode +from django.utils.functional import wraps +from django.utils.importlib import import_module from .lib import Image, ImageFile, StringIO @@ -371,3 +373,34 @@ def prepare_image(img, format): save_kwargs['optimize'] = True return img, save_kwargs + + +def get_class(path, desc): + try: + dot = path.rindex('.') + except ValueError: + raise ImproperlyConfigured("%s isn't a %s module." % (path, desc)) + module, classname = path[:dot], path[dot + 1:] + try: + mod = import_module(module) + except ImportError, e: + raise ImproperlyConfigured('Error importing %s module %s: "%s"' % + (desc, module, e)) + try: + cls = getattr(mod, classname) + return cls + except AttributeError: + raise ImproperlyConfigured('%s module "%s" does not define a "%s"' + ' class.' % (desc[0].upper() + desc[1:], module, classname)) + + +_singletons = {} + + +def get_singleton(class_path, desc): + global _singletons + cls = get_class(class_path, desc) + instance = _singletons.get(cls) + if not instance: + instance = _singletons[cls] = cls() + return instance From 3103ab29bd208545ddd2e9a70f36ec9581489b90 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Wed, 5 Sep 2012 21:54:41 -0400 Subject: [PATCH 009/213] Remove "non-validating" backend It's been superseded by the VALIDATE_ON_ACCESS setting --- imagekit/imagecache/__init__.py | 4 ++-- imagekit/imagecache/base.py | 27 --------------------------- 2 files changed, 2 insertions(+), 29 deletions(-) diff --git a/imagekit/imagecache/__init__.py b/imagekit/imagecache/__init__.py index 93dfe1b..4680104 100644 --- a/imagekit/imagecache/__init__.py +++ b/imagekit/imagecache/__init__.py @@ -1,5 +1,5 @@ from ..utils import get_singleton -from .base import InvalidImageCacheBackendError, PessimisticImageCacheBackend, NonValidatingImageCacheBackend +from .base import InvalidImageCacheBackendError, PessimisticImageCacheBackend def get_default_image_cache_backend(): @@ -9,4 +9,4 @@ def get_default_image_cache_backend(): """ from django.conf import settings return get_singleton(settings.IMAGEKIT_DEFAULT_IMAGE_CACHE_BACKEND, - 'validation backend') + 'image cache backend') diff --git a/imagekit/imagecache/base.py b/imagekit/imagecache/base.py index f06c9b5..c0ec566 100644 --- a/imagekit/imagecache/base.py +++ b/imagekit/imagecache/base.py @@ -31,30 +31,3 @@ class PessimisticImageCacheBackend(object): def clear(self, file): file.delete(save=False) - - -class NonValidatingImageCacheBackend(object): - """ - A backend that is super optimistic about the existence of spec files. It - will hit your file storage much less frequently than the pessimistic - backend, but it is technically possible for a cache file to be missing - after validation. - - """ - - def validate(self, file): - """ - NonValidatingImageCacheBackend has faith, so validate's a no-op. - - """ - pass - - def invalidate(self, file): - """ - Immediately generate a new spec file upon invalidation. - - """ - file.generate(save=True) - - def clear(self, file): - file.delete(save=False) From 2ad3791d9d5925868142cba26868ad2a16a3f810 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Wed, 5 Sep 2012 22:19:46 -0400 Subject: [PATCH 010/213] Reorganize image cache backends --- imagekit/conf.py | 2 +- imagekit/imagecache/__init__.py | 12 ------ imagekit/imagecache/backends/__init__.py | 2 + imagekit/imagecache/{ => backends}/base.py | 18 +++++++-- imagekit/imagecache/backends/celery.py | 35 ++++++++++++++++++ imagekit/imagecache/celery.py | 43 ---------------------- imagekit/models/fields/__init__.py | 2 +- 7 files changed, 54 insertions(+), 60 deletions(-) create mode 100644 imagekit/imagecache/backends/__init__.py rename imagekit/imagecache/{ => backends}/base.py (59%) create mode 100644 imagekit/imagecache/backends/celery.py delete mode 100644 imagekit/imagecache/celery.py diff --git a/imagekit/conf.py b/imagekit/conf.py index 3d8a651..5618bcb 100644 --- a/imagekit/conf.py +++ b/imagekit/conf.py @@ -2,6 +2,6 @@ from appconf import AppConf class ImageKitConf(AppConf): - DEFAULT_IMAGE_CACHE_BACKEND = 'imagekit.imagecache.PessimisticImageCacheBackend' + DEFAULT_IMAGE_CACHE_BACKEND = 'imagekit.imagecache.backends.Simple' VALIDATE_ON_ACCESS = True CACHE_DIR = 'CACHE/images' diff --git a/imagekit/imagecache/__init__.py b/imagekit/imagecache/__init__.py index 4680104..e69de29 100644 --- a/imagekit/imagecache/__init__.py +++ b/imagekit/imagecache/__init__.py @@ -1,12 +0,0 @@ -from ..utils import get_singleton -from .base import InvalidImageCacheBackendError, PessimisticImageCacheBackend - - -def get_default_image_cache_backend(): - """ - Get the default validation backend. - - """ - from django.conf import settings - return get_singleton(settings.IMAGEKIT_DEFAULT_IMAGE_CACHE_BACKEND, - 'image cache backend') diff --git a/imagekit/imagecache/backends/__init__.py b/imagekit/imagecache/backends/__init__.py new file mode 100644 index 0000000..a733a93 --- /dev/null +++ b/imagekit/imagecache/backends/__init__.py @@ -0,0 +1,2 @@ +from .base import * +from .celery import * diff --git a/imagekit/imagecache/base.py b/imagekit/imagecache/backends/base.py similarity index 59% rename from imagekit/imagecache/base.py rename to imagekit/imagecache/backends/base.py index c0ec566..91a319f 100644 --- a/imagekit/imagecache/base.py +++ b/imagekit/imagecache/backends/base.py @@ -1,14 +1,26 @@ +from ...utils import get_singleton from django.core.exceptions import ImproperlyConfigured +def get_default_image_cache_backend(): + """ + Get the default image cache backend. + + """ + from django.conf import settings + return get_singleton(settings.IMAGEKIT_DEFAULT_IMAGE_CACHE_BACKEND, + 'image cache backend') + + class InvalidImageCacheBackendError(ImproperlyConfigured): pass -class PessimisticImageCacheBackend(object): +class Simple(object): """ - A very safe image cache backend. Guarantees that files will always be - available, but at the cost of hitting the storage backend. + The most basic image cache backend. Files are considered valid if they + exist. To invalidate a file, it's deleted; to validate one, it's generated + immediately. """ diff --git a/imagekit/imagecache/backends/celery.py b/imagekit/imagecache/backends/celery.py new file mode 100644 index 0000000..f3c103d --- /dev/null +++ b/imagekit/imagecache/backends/celery.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import + +from .base import InvalidImageCacheBackendError, Simple as SimpleBackend + + +def generate(model, pk, attr): + try: + instance = model._default_manager.get(pk=pk) + except model.DoesNotExist: + pass # The model was deleted since the task was scheduled. NEVER MIND! + else: + field_file = getattr(instance, attr) + field_file.delete(save=False) + field_file.generate(save=True) + + +class CeleryBackend(SimpleBackend): + """ + An image cache backend that uses celery to generate images. + + """ + def __init__(self): + try: + from celery.task import task + except: + raise InvalidImageCacheBackendError("Celery validation backend requires the 'celery' library") + if not getattr(CeleryBackend, '_task', None): + CeleryBackend._task = task(generate) + + def invalidate(self, file): + self._task.delay(file.instance.__class__, file.instance.pk, file.attname) + + def clear(self, file): + file.delete(save=False) diff --git a/imagekit/imagecache/celery.py b/imagekit/imagecache/celery.py deleted file mode 100644 index 9dee5ca..0000000 --- a/imagekit/imagecache/celery.py +++ /dev/null @@ -1,43 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import - -from imagekit.imagecache import PessimisticImageCacheBackend, InvalidImageCacheBackendError - - -def generate(model, pk, attr): - try: - instance = model._default_manager.get(pk=pk) - except model.DoesNotExist: - pass # The model was deleted since the task was scheduled. NEVER MIND! - else: - field_file = getattr(instance, attr) - field_file.delete(save=False) - field_file.generate(save=True) - - -class CeleryImageCacheBackend(PessimisticImageCacheBackend): - """ - A pessimistic cache state backend that uses celery to generate its spec - images. Like PessimisticCacheStateBackend, this one checks to see if the - file exists on validation, so the storage is hit fairly frequently, but an - image is guaranteed to exist. However, while validation guarantees the - existence of *an* image, it does not necessarily guarantee that you will get - the correct image, as the spec may be pending regeneration. In other words, - while there are `generate` tasks in the queue, it is possible to get a - stale spec image. The tradeoff is that calling `invalidate()` won't block - to interact with file storage. - - """ - def __init__(self): - try: - from celery.task import task - except: - raise InvalidImageCacheBackendError("Celery image cache backend requires the 'celery' library") - if not getattr(CeleryImageCacheBackend, '_task', None): - CeleryImageCacheBackend._task = task(generate) - - def invalidate(self, file): - self._task.delay(file.instance.__class__, file.instance.pk, file.attname) - - def clear(self, file): - file.delete(save=False) diff --git a/imagekit/models/fields/__init__.py b/imagekit/models/fields/__init__.py index 078f814..49278dc 100644 --- a/imagekit/models/fields/__init__.py +++ b/imagekit/models/fields/__init__.py @@ -4,7 +4,7 @@ from django.conf import settings from django.db import models from django.db.models.signals import post_init, post_save, post_delete -from ...imagecache import get_default_image_cache_backend +from ...imagecache.backends import get_default_image_cache_backend from ...generators import SpecFileGenerator from .files import ImageSpecFieldFile, ProcessedImageFieldFile from .utils import ImageSpecFileDescriptor, ImageKitMeta, BoundImageKitMeta From 8a2738ca8ad28ed11cae842f1dec48da86a0a985 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Thu, 6 Sep 2012 00:07:40 -0400 Subject: [PATCH 011/213] Add backend for caching image state --- imagekit/conf.py | 2 ++ imagekit/generators.py | 16 +++++---- imagekit/imagecache/backends/base.py | 49 +++++++++++++++++++++++++--- imagekit/models/fields/files.py | 3 ++ 4 files changed, 58 insertions(+), 12 deletions(-) diff --git a/imagekit/conf.py b/imagekit/conf.py index 5618bcb..6c09232 100644 --- a/imagekit/conf.py +++ b/imagekit/conf.py @@ -3,5 +3,7 @@ from appconf import AppConf class ImageKitConf(AppConf): DEFAULT_IMAGE_CACHE_BACKEND = 'imagekit.imagecache.backends.Simple' + CACHE_BACKEND = None VALIDATE_ON_ACCESS = True CACHE_DIR = 'CACHE/images' + CACHE_PREFIX = 'ik-' diff --git a/imagekit/generators.py b/imagekit/generators.py index 344fc67..6c4a974 100644 --- a/imagekit/generators.py +++ b/imagekit/generators.py @@ -49,18 +49,20 @@ class SpecFileGenerator(object): content = IKContentFile(filename, imgfile.read(), format=format) return img, content + def get_hash(self, source_file): + return md5(''.join([ + pickle.dumps(self.get_processors(source_file)), + self.format, + pickle.dumps(self.options), + str(self.autoconvert), + ])).hexdigest() + def generate_filename(self, source_file): source_filename = source_file.name filename = None if source_filename: - hash = md5(''.join([ - pickle.dumps(self.get_processors(source_file)), - self.format, - pickle.dumps(self.options), - str(self.autoconvert), - ])).hexdigest() + hash = self.get_hash(source_file) extension = suggest_extension(source_filename, self.format) - filename = os.path.normpath(os.path.join( settings.IMAGEKIT_CACHE_DIR, os.path.splitext(source_filename)[0], diff --git a/imagekit/imagecache/backends/base.py b/imagekit/imagecache/backends/base.py index 91a319f..a489f6d 100644 --- a/imagekit/imagecache/backends/base.py +++ b/imagekit/imagecache/backends/base.py @@ -1,4 +1,6 @@ from ...utils import get_singleton +from django.core.cache import get_cache +from django.core.cache.backends.dummy import DummyCache from django.core.exceptions import ImproperlyConfigured @@ -16,7 +18,39 @@ class InvalidImageCacheBackendError(ImproperlyConfigured): pass -class Simple(object): +class CachedValidationBackend(object): + @property + def cache(self): + if not getattr(self, '_cache', None): + from django.conf import settings + alias = settings.IMAGEKIT_CACHE_BACKEND + self._cache = get_cache(alias) if alias else DummyCache(None, {}) + return self._cache + + def get_key(self, file): + from django.conf import settings + return '%s%s-valid' % (settings.IMAGEKIT_CACHE_PREFIX, file.get_hash()) + + def is_invalid(self, file): + key = self.get_key(file) + cached_value = self.cache.get(key) + if cached_value is None: + cached_value = self._is_invalid(file) + self.cache.set(key, cached_value) + return cached_value + + def validate(self, file): + if self.is_invalid(file): + self._validate(file) + self.cache.set(self.get_key(file), True) + + def invalidate(self, file): + if not self.is_invalid(file): + self._invalidate(file) + self.cache.set(self.get_key(file), False) + + +class Simple(CachedValidationBackend): """ The most basic image cache backend. Files are considered valid if they exist. To invalidate a file, it's deleted; to validate one, it's generated @@ -24,21 +58,26 @@ class Simple(object): """ - def is_invalid(self, file): + def _is_invalid(self, file): if not getattr(file, '_file', None): # No file on object. Have to check storage. return not file.storage.exists(file.name) return False - def validate(self, file): + def _validate(self, file): """ Generates a new image by running the processors on the source file. """ - if self.is_invalid(file): - file.generate(save=True) + file.generate(save=True) def invalidate(self, file): + """ + Invalidate the file by deleting it. We override ``invalidate()`` + instead of ``_invalidate()`` because we don't really care to check + whether the file is invalid or not. + + """ file.delete(save=False) def clear(self, file): diff --git a/imagekit/models/fields/files.py b/imagekit/models/fields/files.py index b46d813..687295f 100644 --- a/imagekit/models/fields/files.py +++ b/imagekit/models/fields/files.py @@ -6,6 +6,9 @@ class ImageSpecFieldFile(ImageFieldFile): super(ImageSpecFieldFile, self).__init__(instance, field, None) self.attname = attname + def get_hash(self): + return self.field.generator.get_hash(self.source_file) + @property def source_file(self): field_name = getattr(self.field, 'image_field', None) From ec9a1f1fda56bfea568e25ddc5eabda7e9d32a1a Mon Sep 17 00:00:00 2001 From: Eric Eldredge Date: Thu, 6 Sep 2012 15:29:57 -0400 Subject: [PATCH 012/213] Spec templatetag returns html by default ...if no 'as var' is provided or if the var is printed directly. --- imagekit/files.py | 4 +- imagekit/templatetags/imagekit_tags.py | 66 +++++++++++++++++--------- 2 files changed, 46 insertions(+), 24 deletions(-) diff --git a/imagekit/files.py b/imagekit/files.py index ddfa4a8..c2ec36c 100644 --- a/imagekit/files.py +++ b/imagekit/files.py @@ -3,7 +3,7 @@ import os from django.db.models.fields.files import ImageFieldFile from .generators import SpecFileGenerator -from .utils import SpecWrapper +from .utils import SpecWrapper, suggest_extension class ImageSpecFile(ImageFieldFile): @@ -44,7 +44,7 @@ class ImageSpecFile(ImageFieldFile): source_filename = self.source_file.name filepath, basename = os.path.split(source_filename) filename = os.path.splitext(basename)[0] - extension = self.generator.suggest_extension(source_filename) + extension = suggest_extension(source_filename, self.generator.format) new_name = '%s%s' % (filename, extension) cache_filename = ['cache', 'iktt'] + self.spec_id.split(':') + \ [filepath, new_name] diff --git a/imagekit/templatetags/imagekit_tags.py b/imagekit/templatetags/imagekit_tags.py index 0cb21ed..90a5584 100644 --- a/imagekit/templatetags/imagekit_tags.py +++ b/imagekit/templatetags/imagekit_tags.py @@ -1,5 +1,6 @@ import os from django import template +from django.utils.safestring import mark_safe from ..files import ImageSpecFile @@ -40,42 +41,55 @@ class SpecRegistry(object): spec_registry = SpecRegistry() +class ImageSpecFileHtmlWrapper(object): + def __init__(self, image_spec_file): + self._image_spec_file = image_spec_file + + def __getattr__(self, name): + return getattr(self._image_spec_file, name) + + def __unicode__(self): + return mark_safe(u'' % self.url) + + class SpecNode(template.Node): - def __init__(self, spec_id, source_image, variable_name): + def __init__(self, spec_id, source_image, variable_name=None): self.spec_id = spec_id self.source_image = source_image self.variable_name = variable_name - def _default_cache_to(self, instance, path, specname, extension): - """ - Determines the filename to use for the transformed image. Can be - overridden on a per-spec basis by setting the cache_to property on - the spec. - - """ - filepath, basename = os.path.split(path) - filename = os.path.splitext(basename)[0] - new_name = '%s_%s%s' % (filename, specname, extension) - return os.path.join(os.path.join('cache', filepath), new_name) - def render(self, context): from ..utils import autodiscover autodiscover() source_image = self.source_image.resolve(context) - variable_name = str(self.variable_name) spec_id = self.spec_id.resolve(context) spec = spec_registry.get_spec(spec_id) if callable(spec): spec = spec() - context[variable_name] = ImageSpecFile(spec, source_image, spec_id) - return '' + spec_file = ImageSpecFileHtmlWrapper(ImageSpecFile(spec, source_image, spec_id)) + if self.variable_name is not None: + variable_name = str(self.variable_name) + context[variable_name] = spec_file + return '' + + return spec_file #@register.tag +# TODO: Should this be renamed to something like 'process'? def spec(parser, token): """ - Creates an image based on the provided spec and source image and sets it - as a context variable: + Creates an image based on the provided spec and source image. + + By default:: + + {% spec 'myapp:thumbnail', mymodel.profile_image %} + + Generates an ````:: + + + + Storing it as a context variable allows more flexibility:: {% spec 'myapp:thumbnail' mymodel.profile_image as th %} @@ -83,12 +97,20 @@ def spec(parser, token): """ args = token.split_contents() + arg_count = len(args) - if len(args) != 5 or args[3] != 'as': - raise TemplateSyntaxError('\'spec\' tags must be in the form "{% spec spec_id image as varname %}"') + if (arg_count < 3 or arg_count > 5 + or (arg_count > 3 and arg_count < 5) + or (args == 5 and args[3] != 'as')): + raise template.TemplateSyntaxError('\'spec\' tags must be in the form' + ' "{% spec spec_id image %}" or' + ' "{% spec spec_id image' + ' as varname %}"') - return SpecNode(*[parser.compile_filter(arg) for arg in args[1:3] \ - + [args[4]]]) + spec_id = parser.compile_filter(args[1]) + source_image = parser.compile_filter(args[2]) + variable_name = arg_count > 3 and args[4] or None + return SpecNode(spec_id, source_image, variable_name) spec = spec_tag = register.tag(spec) From f43bd4ec28468c8e85859a1a4d00cf9d77c3486f Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Sat, 15 Sep 2012 15:09:58 -0400 Subject: [PATCH 013/213] Include source filename in hash --- imagekit/generators.py | 1 + 1 file changed, 1 insertion(+) diff --git a/imagekit/generators.py b/imagekit/generators.py index 6c4a974..515bcb7 100644 --- a/imagekit/generators.py +++ b/imagekit/generators.py @@ -51,6 +51,7 @@ class SpecFileGenerator(object): def get_hash(self, source_file): return md5(''.join([ + source_file.name, pickle.dumps(self.get_processors(source_file)), self.format, pickle.dumps(self.options), From ba9bf1f8771a4d5c89ce53b280a537ea67303059 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Wed, 3 Oct 2012 22:23:11 -0400 Subject: [PATCH 014/213] Add image cache strategies This new feature gives the user more control over *when* their images are validated. Image cache backends are now exclusively for controlling the *how*. This means you won't have to write a lot of code when you just want to change one or the other. --- imagekit/conf.py | 3 +- imagekit/generators.py | 2 +- imagekit/imagecache/actions.py | 23 ++++++++ .../{backends/base.py => backends.py} | 2 +- imagekit/imagecache/backends/__init__.py | 2 - imagekit/imagecache/backends/celery.py | 35 ------------ imagekit/imagecache/strategies.py | 57 +++++++++++++++++++ imagekit/models/fields/__init__.py | 24 +++++--- imagekit/models/fields/files.py | 3 +- 9 files changed, 100 insertions(+), 51 deletions(-) create mode 100644 imagekit/imagecache/actions.py rename imagekit/imagecache/{backends/base.py => backends.py} (98%) delete mode 100644 imagekit/imagecache/backends/__init__.py delete mode 100644 imagekit/imagecache/backends/celery.py create mode 100644 imagekit/imagecache/strategies.py diff --git a/imagekit/conf.py b/imagekit/conf.py index 6c09232..e43d04e 100644 --- a/imagekit/conf.py +++ b/imagekit/conf.py @@ -1,9 +1,10 @@ from appconf import AppConf +from .imagecache.actions import validate_now, clear_now class ImageKitConf(AppConf): DEFAULT_IMAGE_CACHE_BACKEND = 'imagekit.imagecache.backends.Simple' CACHE_BACKEND = None - VALIDATE_ON_ACCESS = True CACHE_DIR = 'CACHE/images' CACHE_PREFIX = 'ik-' + DEFAULT_SPEC_FIELD_IMAGE_CACHE_STRATEGY = 'imagekit.imagecache.strategies.Pessimistic' diff --git a/imagekit/generators.py b/imagekit/generators.py index 515bcb7..e848981 100644 --- a/imagekit/generators.py +++ b/imagekit/generators.py @@ -56,7 +56,7 @@ class SpecFileGenerator(object): self.format, pickle.dumps(self.options), str(self.autoconvert), - ])).hexdigest() + ]).encode('utf-8')).hexdigest() def generate_filename(self, source_file): source_filename = source_file.name diff --git a/imagekit/imagecache/actions.py b/imagekit/imagecache/actions.py new file mode 100644 index 0000000..11c0534 --- /dev/null +++ b/imagekit/imagecache/actions.py @@ -0,0 +1,23 @@ +def validate_now(file): + print 'validate now!' + file.validate() + + +try: + from celery.task import task +except ImportError: + pass +else: + validate_now_task = task(validate_now) + + +def deferred_validate(file): + try: + import celery + except: + raise ImportError("Deferred validation requires the the 'celery' library") + validate_now_task.delay(file) + + +def clear_now(file): + file.clear() diff --git a/imagekit/imagecache/backends/base.py b/imagekit/imagecache/backends.py similarity index 98% rename from imagekit/imagecache/backends/base.py rename to imagekit/imagecache/backends.py index a489f6d..cbb7f5d 100644 --- a/imagekit/imagecache/backends/base.py +++ b/imagekit/imagecache/backends.py @@ -1,4 +1,4 @@ -from ...utils import get_singleton +from ..utils import get_singleton from django.core.cache import get_cache from django.core.cache.backends.dummy import DummyCache from django.core.exceptions import ImproperlyConfigured diff --git a/imagekit/imagecache/backends/__init__.py b/imagekit/imagecache/backends/__init__.py deleted file mode 100644 index a733a93..0000000 --- a/imagekit/imagecache/backends/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -from .base import * -from .celery import * diff --git a/imagekit/imagecache/backends/celery.py b/imagekit/imagecache/backends/celery.py deleted file mode 100644 index f3c103d..0000000 --- a/imagekit/imagecache/backends/celery.py +++ /dev/null @@ -1,35 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import - -from .base import InvalidImageCacheBackendError, Simple as SimpleBackend - - -def generate(model, pk, attr): - try: - instance = model._default_manager.get(pk=pk) - except model.DoesNotExist: - pass # The model was deleted since the task was scheduled. NEVER MIND! - else: - field_file = getattr(instance, attr) - field_file.delete(save=False) - field_file.generate(save=True) - - -class CeleryBackend(SimpleBackend): - """ - An image cache backend that uses celery to generate images. - - """ - def __init__(self): - try: - from celery.task import task - except: - raise InvalidImageCacheBackendError("Celery validation backend requires the 'celery' library") - if not getattr(CeleryBackend, '_task', None): - CeleryBackend._task = task(generate) - - def invalidate(self, file): - self._task.delay(file.instance.__class__, file.instance.pk, file.attname) - - def clear(self, file): - file.delete(save=False) diff --git a/imagekit/imagecache/strategies.py b/imagekit/imagecache/strategies.py new file mode 100644 index 0000000..7ec7252 --- /dev/null +++ b/imagekit/imagecache/strategies.py @@ -0,0 +1,57 @@ +from .actions import validate_now, clear_now +from ..utils import get_singleton + + +class Pessimistic(object): + """ + A caching strategy that validates the file every time it's accessed. + + """ + + def on_access(self, file): + validate_now(file) + + def on_source_delete(self, file): + clear_now(file) + + def on_source_change(self, file): + validate_now(file) + + +class Optimistic(object): + """ + A caching strategy that validates when the source file changes and assumes + that the cached file will persist. + + """ + + def on_source_create(self, file): + validate_now(file) + + def on_source_delete(self, file): + clear_now(file) + + def on_source_change(self, file): + validate_now(file) + + +class DictStrategy(object): + def __init__(self, callbacks): + for k, v in callbacks.items(): + setattr(self, k, v) + + +class StrategyWrapper(object): + def __init__(self, strategy): + if isinstance(strategy, basestring): + strategy = get_singleton(strategy, 'image cache strategy') + elif isinstance(strategy, dict): + strategy = DictStrategy(strategy) + elif callable(strategy): + strategy = strategy() + self._wrapped = strategy + + def invoke_callback(self, name, *args, **kwargs): + func = getattr(self._wrapped, 'on_%s' % name, None) + if func: + func(*args, **kwargs) diff --git a/imagekit/models/fields/__init__.py b/imagekit/models/fields/__init__.py index 49278dc..fdb4475 100644 --- a/imagekit/models/fields/__init__.py +++ b/imagekit/models/fields/__init__.py @@ -5,6 +5,7 @@ from django.db import models from django.db.models.signals import post_init, post_save, post_delete from ...imagecache.backends import get_default_image_cache_backend +from ...imagecache.strategies import StrategyWrapper from ...generators import SpecFileGenerator from .files import ImageSpecFieldFile, ProcessedImageFieldFile from .utils import ImageSpecFileDescriptor, ImageKitMeta, BoundImageKitMeta @@ -19,7 +20,7 @@ class ImageSpecField(object): """ def __init__(self, processors=None, format=None, options=None, image_field=None, pre_cache=None, storage=None, autoconvert=True, - image_cache_backend=None, validate_on_access=None): + image_cache_backend=None, image_cache_strategy=None): """ :param processors: A list of processors to run on the original image. :param format: The format of the output file. If not provided, @@ -38,9 +39,10 @@ class ImageSpecField(object): ``prepare_image()`` should be performed prior to saving. :param image_cache_backend: An object responsible for managing the state of cached files. Defaults to an instance of - IMAGEKIT_DEFAULT_IMAGE_CACHE_BACKEND - :param validate_on_access: Should the image cache be validated when it's - accessed? + ``IMAGEKIT_DEFAULT_IMAGE_CACHE_BACKEND`` + :param image_cache_strategy: A dictionary containing callbacks that + allow you to customize how and when the image cache is validated. + Defaults to ``IMAGEKIT_DEFAULT_SPEC_FIELD_IMAGE_CACHE_STRATEGY`` """ @@ -61,8 +63,9 @@ class ImageSpecField(object): self.storage = storage self.image_cache_backend = image_cache_backend or \ get_default_image_cache_backend() - self.validate_on_access = settings.IMAGEKIT_VALIDATE_ON_ACCESS if \ - validate_on_access is None else validate_on_access + if image_cache_strategy is None: + image_cache_strategy = settings.IMAGEKIT_DEFAULT_SPEC_FIELD_IMAGE_CACHE_STRATEGY + self.image_cache_strategy = StrategyWrapper(image_cache_strategy) def contribute_to_class(self, cls, name): setattr(cls, name, ImageSpecFileDescriptor(self, name)) @@ -101,8 +104,11 @@ class ImageSpecField(object): old_hashes = instance._ik._source_hashes.copy() new_hashes = ImageSpecField._update_source_hashes(instance) for attname in instance._ik.spec_fields: - if old_hashes[attname] != new_hashes[attname]: - getattr(instance, attname).invalidate() + file = getattr(instance, attname) + if created: + file.field.image_cache_strategy.invoke_callback('source_create', file) + elif old_hashes[attname] != new_hashes[attname]: + file.field.image_cache_strategy.invoke_callback('source_change', file) @staticmethod def _update_source_hashes(instance): @@ -119,7 +125,7 @@ class ImageSpecField(object): @staticmethod def _post_delete_receiver(sender, instance=None, **kwargs): for spec_file in instance._ik.spec_files: - spec_file.clear() + spec_file.field.image_cache_strategy.invoke_callback('source_delete', spec_file) @staticmethod def _post_init_receiver(sender, instance, **kwargs): diff --git a/imagekit/models/fields/files.py b/imagekit/models/fields/files.py index 687295f..2d58845 100644 --- a/imagekit/models/fields/files.py +++ b/imagekit/models/fields/files.py @@ -34,8 +34,7 @@ class ImageSpecFieldFile(ImageFieldFile): def _require_file(self): if not self.source_file: raise ValueError("The '%s' attribute's image_field has no file associated with it." % self.attname) - elif self.field.validate_on_access: - self.validate() + self.field.image_cache_strategy.invoke_callback('access', self) def clear(self): return self.field.image_cache_backend.clear(self) From c8778b9cfb07eb1b955ff34a9fd185ffd155386b Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Wed, 3 Oct 2012 22:31:45 -0400 Subject: [PATCH 015/213] Remove print statement --- imagekit/imagecache/actions.py | 1 - 1 file changed, 1 deletion(-) diff --git a/imagekit/imagecache/actions.py b/imagekit/imagecache/actions.py index 11c0534..2e222b3 100644 --- a/imagekit/imagecache/actions.py +++ b/imagekit/imagecache/actions.py @@ -1,5 +1,4 @@ def validate_now(file): - print 'validate now!' file.validate() From d2087aa1685afe5947b40c186e4673dd401d7a74 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Thu, 4 Oct 2012 21:37:20 -0400 Subject: [PATCH 016/213] Create ImageSpecs; remove generators --- imagekit/__init__.py | 4 ++ imagekit/{generators.py => base.py} | 103 ++++++++++++++++++++-------- imagekit/conf.py | 2 +- imagekit/exceptions.py | 8 +++ imagekit/files.py | 8 +-- imagekit/models/__init__.py | 9 --- imagekit/models/fields/__init__.py | 81 +++++++--------------- imagekit/models/fields/files.py | 18 ++--- imagekit/utils.py | 9 +-- tests/core/tests.py | 3 +- 10 files changed, 127 insertions(+), 118 deletions(-) rename imagekit/{generators.py => base.py} (51%) create mode 100644 imagekit/exceptions.py diff --git a/imagekit/__init__.py b/imagekit/__init__.py index 55a5d04..ee903d9 100644 --- a/imagekit/__init__.py +++ b/imagekit/__init__.py @@ -1,3 +1,7 @@ +from . import conf +from .base import ImageSpec + + __title__ = 'django-imagekit' __author__ = 'Justin Driscoll, Bryan Veloso, Greg Newman, Chris Drackett, Matthew Tretter, Eric Eldredge' __version__ = (2, 0, 1, 'final', 0) diff --git a/imagekit/generators.py b/imagekit/base.py similarity index 51% rename from imagekit/generators.py rename to imagekit/base.py index e848981..30b34cc 100644 --- a/imagekit/generators.py +++ b/imagekit/base.py @@ -2,20 +2,26 @@ from django.conf import settings from hashlib import md5 import os import pickle +from .exceptions import UnknownExtensionError +from .imagecache.backends import get_default_image_cache_backend +from .imagecache.strategies import StrategyWrapper from .lib import StringIO from .processors import ProcessorPipeline -from .utils import (img_to_fobj, open_image, IKContentFile, extension_to_format, - suggest_extension, UnknownExtensionError) +from .utils import (open_image, extension_to_format, IKContentFile, img_to_fobj, + suggest_extension) -class SpecFileGenerator(object): - def __init__(self, processors=None, format=None, options=None, - autoconvert=True, storage=None): - self.processors = processors - self.format = format - self.options = options or {} - self.autoconvert = autoconvert - self.storage = storage +class BaseImageSpec(object): + processors = None + format = None + options = None + autoconvert = True + + def __init__(self, processors=None, format=None, options=None, autoconvert=None): + self.processors = processors or self.processors or [] + self.format = format or self.format + self.options = options or self.options + self.autoconvert = self.autoconvert if autoconvert is None else autoconvert def get_processors(self, source_file): processors = self.processors @@ -23,6 +29,28 @@ class SpecFileGenerator(object): processors = processors(source_file) return processors + def get_hash(self, source_file): + return md5(''.join([ + source_file.name, + pickle.dumps(self.get_processors(source_file)), + self.format, + pickle.dumps(self.options), + str(self.autoconvert), + ]).encode('utf-8')).hexdigest() + + def generate_filename(self, source_file): + source_filename = source_file.name + filename = None + if source_filename: + hash = self.get_hash(source_file) + extension = suggest_extension(source_filename, self.format) + filename = os.path.normpath(os.path.join( + settings.IMAGEKIT_CACHE_DIR, + os.path.splitext(source_filename)[0], + '%s%s' % (hash, extension))) + + return filename + def process_content(self, content, filename=None, source_file=None): img = open_image(content) original_format = img.format @@ -49,27 +77,44 @@ class SpecFileGenerator(object): content = IKContentFile(filename, imgfile.read(), format=format) return img, content - def get_hash(self, source_file): - return md5(''.join([ - source_file.name, - pickle.dumps(self.get_processors(source_file)), - self.format, - pickle.dumps(self.options), - str(self.autoconvert), - ]).encode('utf-8')).hexdigest() - def generate_filename(self, source_file): - source_filename = source_file.name - filename = None - if source_filename: - hash = self.get_hash(source_file) - extension = suggest_extension(source_filename, self.format) - filename = os.path.normpath(os.path.join( - settings.IMAGEKIT_CACHE_DIR, - os.path.splitext(source_filename)[0], - '%s%s' % (hash, extension))) +class ImageSpec(BaseImageSpec): + storage = None + image_cache_backend = None + image_cache_strategy = settings.IMAGEKIT_DEFAULT_IMAGE_CACHE_STRATEGY - return filename + def __init__(self, processors=None, format=None, options=None, + storage=None, autoconvert=None, image_cache_backend=None, + image_cache_strategy=None): + """ + :param processors: A list of processors to run on the original image. + :param format: The format of the output file. If not provided, + ImageSpecField will try to guess the appropriate format based on the + extension of the filename and the format of the input image. + :param options: A dictionary that will be passed to PIL's + ``Image.save()`` method as keyword arguments. Valid options vary + between formats, but some examples include ``quality``, + ``optimize``, and ``progressive`` for JPEGs. See the PIL + documentation for others. + :param autoconvert: Specifies whether automatic conversion using + ``prepare_image()`` should be performed prior to saving. + :param image_field: The name of the model property that contains the + original image. + :param storage: A Django storage system to use to save the generated + image. + :param image_cache_backend: An object responsible for managing the state + of cached files. Defaults to an instance of + ``IMAGEKIT_DEFAULT_IMAGE_CACHE_BACKEND`` + :param image_cache_strategy: A dictionary containing callbacks that + allow you to customize how and when the image cache is validated. + Defaults to ``IMAGEKIT_DEFAULT_SPEC_FIELD_IMAGE_CACHE_STRATEGY`` + + """ + super(ImageSpec, self).__init__(processors=processors, format=format, + options=options, autoconvert=autoconvert) + self.storage = storage or self.storage + self.image_cache_backend = image_cache_backend or self.image_cache_backend or get_default_image_cache_backend() + self.image_cache_strategy = StrategyWrapper(image_cache_strategy or self.image_cache_strategy) def generate_file(self, filename, source_file, save=True): """ diff --git a/imagekit/conf.py b/imagekit/conf.py index e43d04e..2767e6b 100644 --- a/imagekit/conf.py +++ b/imagekit/conf.py @@ -7,4 +7,4 @@ class ImageKitConf(AppConf): CACHE_BACKEND = None CACHE_DIR = 'CACHE/images' CACHE_PREFIX = 'ik-' - DEFAULT_SPEC_FIELD_IMAGE_CACHE_STRATEGY = 'imagekit.imagecache.strategies.Pessimistic' + DEFAULT_IMAGE_CACHE_STRATEGY = 'imagekit.imagecache.strategies.Pessimistic' diff --git a/imagekit/exceptions.py b/imagekit/exceptions.py new file mode 100644 index 0000000..d3595b4 --- /dev/null +++ b/imagekit/exceptions.py @@ -0,0 +1,8 @@ + + +class UnknownExtensionError(Exception): + pass + + +class UnknownFormatError(Exception): + pass diff --git a/imagekit/files.py b/imagekit/files.py index c2ec36c..0d9837f 100644 --- a/imagekit/files.py +++ b/imagekit/files.py @@ -2,7 +2,6 @@ import os from django.db.models.fields.files import ImageFieldFile -from .generators import SpecFileGenerator from .utils import SpecWrapper, suggest_extension @@ -11,9 +10,6 @@ class ImageSpecFile(ImageFieldFile): spec = SpecWrapper(spec) self.storage = spec.storage or source_file.storage - self.generator = SpecFileGenerator(processors=spec.processors, - format=spec.format, options=spec.options, - autoconvert=spec.autoconvert, storage=self.storage) self.spec = spec self.source_file = source_file @@ -44,11 +40,11 @@ class ImageSpecFile(ImageFieldFile): source_filename = self.source_file.name filepath, basename = os.path.split(source_filename) filename = os.path.splitext(basename)[0] - extension = suggest_extension(source_filename, self.generator.format) + extension = suggest_extension(source_filename, self.spec.format) new_name = '%s%s' % (filename, extension) cache_filename = ['cache', 'iktt'] + self.spec_id.split(':') + \ [filepath, new_name] return os.path.join(*cache_filename) def generate(self, save=True): - return self.generator.generate_file(self.name, self.source_file, save) + return self.spec.generate_file(self.name, self.source_file, save) diff --git a/imagekit/models/__init__.py b/imagekit/models/__init__.py index 4207987..4c3dad7 100644 --- a/imagekit/models/__init__.py +++ b/imagekit/models/__init__.py @@ -1,11 +1,2 @@ from .. import conf from .fields import ImageSpecField, ProcessedImageField -import warnings - - -class ImageSpec(ImageSpecField): - def __init__(self, *args, **kwargs): - warnings.warn('ImageSpec has been moved to' - ' imagekit.models.ImageSpecField. Please use that instead.', - DeprecationWarning) - super(ImageSpec, self).__init__(*args, **kwargs) diff --git a/imagekit/models/fields/__init__.py b/imagekit/models/fields/__init__.py index fdb4475..9685218 100644 --- a/imagekit/models/fields/__init__.py +++ b/imagekit/models/fields/__init__.py @@ -1,14 +1,11 @@ import os -from django.conf import settings from django.db import models from django.db.models.signals import post_init, post_save, post_delete -from ...imagecache.backends import get_default_image_cache_backend -from ...imagecache.strategies import StrategyWrapper -from ...generators import SpecFileGenerator -from .files import ImageSpecFieldFile, ProcessedImageFieldFile -from .utils import ImageSpecFileDescriptor, ImageKitMeta, BoundImageKitMeta +from .files import ProcessedImageFieldFile +from .utils import ImageSpecFileDescriptor, ImageKitMeta +from ...base import ImageSpec from ...utils import suggest_extension @@ -19,53 +16,31 @@ class ImageSpecField(object): """ def __init__(self, processors=None, format=None, options=None, - image_field=None, pre_cache=None, storage=None, autoconvert=True, + image_field=None, storage=None, autoconvert=True, image_cache_backend=None, image_cache_strategy=None): - """ - :param processors: A list of processors to run on the original image. - :param format: The format of the output file. If not provided, - ImageSpecField will try to guess the appropriate format based on the - extension of the filename and the format of the input image. - :param options: A dictionary that will be passed to PIL's - ``Image.save()`` method as keyword arguments. Valid options vary - between formats, but some examples include ``quality``, - ``optimize``, and ``progressive`` for JPEGs. See the PIL - documentation for others. - :param image_field: The name of the model property that contains the - original image. - :param storage: A Django storage system to use to save the generated - image. - :param autoconvert: Specifies whether automatic conversion using - ``prepare_image()`` should be performed prior to saving. - :param image_cache_backend: An object responsible for managing the state - of cached files. Defaults to an instance of - ``IMAGEKIT_DEFAULT_IMAGE_CACHE_BACKEND`` - :param image_cache_strategy: A dictionary containing callbacks that - allow you to customize how and when the image cache is validated. - Defaults to ``IMAGEKIT_DEFAULT_SPEC_FIELD_IMAGE_CACHE_STRATEGY`` - """ - - if pre_cache is not None: - raise Exception('The pre_cache argument has been removed in favor' - ' of cache state backends.') - - # The generator accepts a callable value for processors, but it + # The spec accepts a callable value for processors, but it # takes different arguments than the callable that ImageSpecField # expects, so we create a partial application and pass that instead. # TODO: Should we change the signatures to match? Even if `instance` is not part of the signature, it's accessible through the source file object's instance property. p = lambda file: processors(instance=file.instance, file=file) if \ callable(processors) else processors - self.generator = SpecFileGenerator(p, format=format, options=options, - autoconvert=autoconvert, storage=storage) + self.spec = ImageSpec( + processors=p, + format=format, + options=options, + storage=storage, + autoconvert=autoconvert, + image_cache_backend=image_cache_backend, + image_cache_strategy=image_cache_strategy, + ) + self.image_field = image_field - self.storage = storage - self.image_cache_backend = image_cache_backend or \ - get_default_image_cache_backend() - if image_cache_strategy is None: - image_cache_strategy = settings.IMAGEKIT_DEFAULT_SPEC_FIELD_IMAGE_CACHE_STRATEGY - self.image_cache_strategy = StrategyWrapper(image_cache_strategy) + + @property + def storage(self): + return self.spec.storage def contribute_to_class(self, cls, name): setattr(cls, name, ImageSpecFileDescriptor(self, name)) @@ -94,7 +69,7 @@ class ImageSpecField(object): # Register the field with the image_cache_backend try: - self.image_cache_backend.register_field(cls, self, name) + self.spec.image_cache_backend.register_field(cls, self, name) except AttributeError: pass @@ -106,9 +81,9 @@ class ImageSpecField(object): for attname in instance._ik.spec_fields: file = getattr(instance, attname) if created: - file.field.image_cache_strategy.invoke_callback('source_create', file) + file.field.spec.image_cache_strategy.invoke_callback('source_create', file) elif old_hashes[attname] != new_hashes[attname]: - file.field.image_cache_strategy.invoke_callback('source_change', file) + file.field.spec.image_cache_strategy.invoke_callback('source_change', file) @staticmethod def _update_source_hashes(instance): @@ -125,7 +100,7 @@ class ImageSpecField(object): @staticmethod def _post_delete_receiver(sender, instance=None, **kwargs): for spec_file in instance._ik.spec_files: - spec_file.field.image_cache_strategy.invoke_callback('source_delete', spec_file) + spec_file.field.spec.image_cache_strategy.invoke_callback('source_delete', spec_file) @staticmethod def _post_init_receiver(sender, instance, **kwargs): @@ -152,20 +127,16 @@ class ProcessedImageField(models.ImageField): :class:`imagekit.models.ImageSpecField`. """ - if 'quality' in kwargs: - raise Exception('The "quality" keyword argument has been' - """ deprecated. Use `options={'quality': %s}` instead.""" \ - % kwargs['quality']) models.ImageField.__init__(self, verbose_name, name, width_field, height_field, **kwargs) - self.generator = SpecFileGenerator(processors, format=format, - options=options, autoconvert=autoconvert) + self.spec = ImageSpec(processors, format=format, options=options, + autoconvert=autoconvert) def get_filename(self, filename): filename = os.path.normpath(self.storage.get_valid_name( os.path.basename(filename))) name, ext = os.path.splitext(filename) - ext = suggest_extension(filename, self.generator.format) + ext = suggest_extension(filename, self.spec.format) return u'%s%s' % (name, ext) diff --git a/imagekit/models/fields/files.py b/imagekit/models/fields/files.py index 2d58845..691ce6b 100644 --- a/imagekit/models/fields/files.py +++ b/imagekit/models/fields/files.py @@ -7,7 +7,7 @@ class ImageSpecFieldFile(ImageFieldFile): self.attname = attname def get_hash(self): - return self.field.generator.get_hash(self.source_file) + return self.field.spec.get_hash(self.source_file) @property def source_file(self): @@ -34,16 +34,16 @@ class ImageSpecFieldFile(ImageFieldFile): def _require_file(self): if not self.source_file: raise ValueError("The '%s' attribute's image_field has no file associated with it." % self.attname) - self.field.image_cache_strategy.invoke_callback('access', self) + self.field.spec.image_cache_strategy.invoke_callback('access', self) def clear(self): - return self.field.image_cache_backend.clear(self) + return self.field.spec.image_cache_backend.clear(self) def invalidate(self): - return self.field.image_cache_backend.invalidate(self) + return self.field.spec.image_cache_backend.invalidate(self) def validate(self): - return self.field.image_cache_backend.validate(self) + return self.field.spec.image_cache_backend.validate(self) def generate(self, save=True): """ @@ -51,7 +51,7 @@ class ImageSpecFieldFile(ImageFieldFile): the content of the result, ready for saving. """ - return self.field.generator.generate_file(self.name, self.source_file, + return self.field.spec.generate_file(self.name, self.source_file, save) def delete(self, save=False): @@ -91,7 +91,7 @@ class ImageSpecFieldFile(ImageFieldFile): Specifies the filename that the cached image will use. """ - return self.field.generator.generate_filename(self.source_file) + return self.field.spec.generate_filename(self.source_file) @name.setter def name(self, value): @@ -123,7 +123,7 @@ class ImageSpecFieldFile(ImageFieldFile): class ProcessedImageFieldFile(ImageFieldFile): def save(self, name, content, save=True): - new_filename = self.field.generate_filename(self.instance, name) - img, content = self.field.generator.process_content(content, + new_filename = self.field.spec.generate_filename(self.instance, name) + img, content = self.field.spec.process_content(content, new_filename, self) return super(ProcessedImageFieldFile, self).save(name, content, save) diff --git a/imagekit/utils.py b/imagekit/utils.py index ef549f3..fb60035 100644 --- a/imagekit/utils.py +++ b/imagekit/utils.py @@ -10,6 +10,7 @@ from django.utils.encoding import smart_str, smart_unicode from django.utils.functional import wraps from django.utils.importlib import import_module +from .exceptions import UnknownExtensionError, UnknownFormatError from .lib import Image, ImageFile, StringIO @@ -76,14 +77,6 @@ def _wrap_copy(f): return copy -class UnknownExtensionError(Exception): - pass - - -class UnknownFormatError(Exception): - pass - - _pil_init = 0 diff --git a/tests/core/tests.py b/tests/core/tests.py index b4884c8..4998ce5 100644 --- a/tests/core/tests.py +++ b/tests/core/tests.py @@ -5,6 +5,7 @@ import os from django.test import TestCase from imagekit import utils +from imagekit.exceptions import UnknownFormatError from .models import (Photo, AbstractImageModel, ConcreteImageModel1, ConcreteImageModel2) from .testutils import create_photo, pickleback @@ -59,7 +60,7 @@ class IKUtilsTest(TestCase): self.assertEqual(utils.format_to_extension('PNG'), '.png') self.assertEqual(utils.format_to_extension('ICO'), '.ico') - with self.assertRaises(utils.UnknownFormatError): + with self.assertRaises(UnknownFormatError): utils.format_to_extension('TXT') From 0cc79384000dcb115ed3841800f0aa00bf51925c Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Thu, 4 Oct 2012 21:44:55 -0400 Subject: [PATCH 017/213] Re-integrate receivers module Somewhere along the line, a change got merged that stopped using the receivers module. This re-integrates it and moves changes made to the old receivers (static methods on ImageSpecField) to them. --- imagekit/models/fields/__init__.py | 46 +++--------------------------- imagekit/models/receivers.py | 9 ++++-- 2 files changed, 10 insertions(+), 45 deletions(-) diff --git a/imagekit/models/fields/__init__.py b/imagekit/models/fields/__init__.py index 9685218..34af0c7 100644 --- a/imagekit/models/fields/__init__.py +++ b/imagekit/models/fields/__init__.py @@ -5,6 +5,7 @@ from django.db.models.signals import post_init, post_save, post_delete from .files import ProcessedImageFieldFile from .utils import ImageSpecFileDescriptor, ImageKitMeta +from ..receivers import configure_receivers from ...base import ImageSpec from ...utils import suggest_extension @@ -58,54 +59,12 @@ class ImageSpecField(object): setattr(cls, '_ik', ik) ik.spec_fields.append(name) - # Connect to the signals only once for this class. - uid = '%s.%s' % (cls.__module__, cls.__name__) - post_init.connect(ImageSpecField._post_init_receiver, sender=cls, - dispatch_uid=uid) - post_save.connect(ImageSpecField._post_save_receiver, sender=cls, - dispatch_uid=uid) - post_delete.connect(ImageSpecField._post_delete_receiver, sender=cls, - dispatch_uid=uid) - # Register the field with the image_cache_backend try: self.spec.image_cache_backend.register_field(cls, self, name) except AttributeError: pass - @staticmethod - def _post_save_receiver(sender, instance=None, created=False, raw=False, **kwargs): - if not raw: - old_hashes = instance._ik._source_hashes.copy() - new_hashes = ImageSpecField._update_source_hashes(instance) - for attname in instance._ik.spec_fields: - file = getattr(instance, attname) - if created: - file.field.spec.image_cache_strategy.invoke_callback('source_create', file) - elif old_hashes[attname] != new_hashes[attname]: - file.field.spec.image_cache_strategy.invoke_callback('source_change', file) - - @staticmethod - def _update_source_hashes(instance): - """ - Stores hashes of the source image files so that they can be compared - later to see whether the source image has changed (and therefore whether - the spec file needs to be regenerated). - - """ - instance._ik._source_hashes = dict((f.attname, hash(f.source_file)) \ - for f in instance._ik.spec_files) - return instance._ik._source_hashes - - @staticmethod - def _post_delete_receiver(sender, instance=None, **kwargs): - for spec_file in instance._ik.spec_files: - spec_file.field.spec.image_cache_strategy.invoke_callback('source_delete', spec_file) - - @staticmethod - def _post_init_receiver(sender, instance, **kwargs): - ImageSpecField._update_source_hashes(instance) - class ProcessedImageField(models.ImageField): """ @@ -146,3 +105,6 @@ except ImportError: pass else: add_introspection_rules([], [r'^imagekit\.models\.fields\.ProcessedImageField$']) + + +configure_receivers() diff --git a/imagekit/models/receivers.py b/imagekit/models/receivers.py index da93a69..67960f7 100644 --- a/imagekit/models/receivers.py +++ b/imagekit/models/receivers.py @@ -20,14 +20,17 @@ def post_save_receiver(sender, instance=None, created=False, raw=False, **kwargs old_hashes = instance._ik._source_hashes.copy() new_hashes = update_source_hashes(instance) for attname in instance._ik.spec_fields: - if old_hashes[attname] != new_hashes[attname]: - getattr(instance, attname).invalidate() + file = getattr(instance, attname) + if created: + file.field.spec.image_cache_strategy.invoke_callback('source_create', file) + elif old_hashes[attname] != new_hashes[attname]: + file.field.spec.image_cache_strategy.invoke_callback('source_change', file) @ik_model_receiver def post_delete_receiver(sender, instance=None, **kwargs): for spec_file in instance._ik.spec_files: - spec_file.clear() + spec_file.field.spec.image_cache_strategy.invoke_callback('source_delete', spec_file) @ik_model_receiver From 30ba1d890e4f810e06057d8f53383126aa1c13fe Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Thu, 4 Oct 2012 21:57:33 -0400 Subject: [PATCH 018/213] Move exceptions --- imagekit/exceptions.py | 6 ++++++ imagekit/templatetags/imagekit_tags.py | 9 +-------- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/imagekit/exceptions.py b/imagekit/exceptions.py index d3595b4..55cf144 100644 --- a/imagekit/exceptions.py +++ b/imagekit/exceptions.py @@ -1,3 +1,9 @@ +class AlreadyRegistered(Exception): + pass + + +class NotRegistered(Exception): + pass class UnknownExtensionError(Exception): diff --git a/imagekit/templatetags/imagekit_tags.py b/imagekit/templatetags/imagekit_tags.py index 90a5584..16fce13 100644 --- a/imagekit/templatetags/imagekit_tags.py +++ b/imagekit/templatetags/imagekit_tags.py @@ -2,20 +2,13 @@ import os from django import template from django.utils.safestring import mark_safe +from ..exceptions import AlreadyRegistered, NotRegistered from ..files import ImageSpecFile register = template.Library() -class AlreadyRegistered(Exception): - pass - - -class NotRegistered(Exception): - pass - - class SpecRegistry(object): def __init__(self): self._specs = {} From 116b0bc0c513a6603d459002dbb4faa89072c703 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Thu, 4 Oct 2012 21:58:22 -0400 Subject: [PATCH 019/213] Move spec classes to specs module --- imagekit/__init__.py | 2 +- imagekit/{base.py => specs.py} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename imagekit/{base.py => specs.py} (100%) diff --git a/imagekit/__init__.py b/imagekit/__init__.py index ee903d9..f4fd227 100644 --- a/imagekit/__init__.py +++ b/imagekit/__init__.py @@ -1,5 +1,5 @@ from . import conf -from .base import ImageSpec +from .specs import ImageSpec __title__ = 'django-imagekit' diff --git a/imagekit/base.py b/imagekit/specs.py similarity index 100% rename from imagekit/base.py rename to imagekit/specs.py From 99ba61d6053d12c5c02b0a28d0d8c8ffe21bd8b1 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Thu, 4 Oct 2012 22:02:29 -0400 Subject: [PATCH 020/213] Move spec registry --- imagekit/specs.py | 27 +++++++++++++++++++++++- imagekit/templatetags/imagekit_tags.py | 29 +------------------------- 2 files changed, 27 insertions(+), 29 deletions(-) diff --git a/imagekit/specs.py b/imagekit/specs.py index 30b34cc..a81b82c 100644 --- a/imagekit/specs.py +++ b/imagekit/specs.py @@ -2,7 +2,7 @@ from django.conf import settings from hashlib import md5 import os import pickle -from .exceptions import UnknownExtensionError +from .exceptions import UnknownExtensionError, AlreadyRegistered, NotRegistered from .imagecache.backends import get_default_image_cache_backend from .imagecache.strategies import StrategyWrapper from .lib import StringIO @@ -11,6 +11,31 @@ from .utils import (open_image, extension_to_format, IKContentFile, img_to_fobj, suggest_extension) +class SpecRegistry(object): + def __init__(self): + self._specs = {} + + def register(self, id, spec): + if id in self._specs: + raise AlreadyRegistered('The spec with id %s is already registered' % id) + self._specs[id] = spec + + def unregister(self, id, spec): + try: + del self._specs[id] + except KeyError: + raise NotRegistered('The spec with id %s is not registered' % id) + + def get_spec(self, id): + try: + return self._specs[id] + except KeyError: + raise NotRegistered('The spec with id %s is not registered' % id) + + +spec_registry = SpecRegistry() + + class BaseImageSpec(object): processors = None format = None diff --git a/imagekit/templatetags/imagekit_tags.py b/imagekit/templatetags/imagekit_tags.py index 16fce13..938a53b 100644 --- a/imagekit/templatetags/imagekit_tags.py +++ b/imagekit/templatetags/imagekit_tags.py @@ -1,39 +1,12 @@ -import os from django import template from django.utils.safestring import mark_safe - -from ..exceptions import AlreadyRegistered, NotRegistered from ..files import ImageSpecFile +from ..specs import spec_registry register = template.Library() -class SpecRegistry(object): - def __init__(self): - self._specs = {} - - def register(self, id, spec): - if id in self._specs: - raise AlreadyRegistered('The spec with id %s is already registered' % id) - self._specs[id] = spec - - def unregister(self, id, spec): - try: - del self._specs[id] - except KeyError: - raise NotRegistered('The spec with id %s is not registered' % id) - - def get_spec(self, id): - try: - return self._specs[id] - except KeyError: - raise NotRegistered('The spec with id %s is not registered' % id) - - -spec_registry = SpecRegistry() - - class ImageSpecFileHtmlWrapper(object): def __init__(self, image_spec_file): self._image_spec_file = image_spec_file From f289ff31996286cb992c869e92e7fe774b2c6328 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Thu, 4 Oct 2012 22:56:26 -0400 Subject: [PATCH 021/213] Back ImageSpecFields with spec registry This marks a major step towards centralizing some of the "spec" logic and creating a single access point for them. Because `ImageSpecFields` are just alternative interfaces for defining and registering specs, they can be accessed and overridden in the same manner as other specs (like those used by template tags): via the spec registry. --- imagekit/models/fields/__init__.py | 35 ++++++++++-------- imagekit/specs.py | 58 ++++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+), 14 deletions(-) diff --git a/imagekit/models/fields/__init__.py b/imagekit/models/fields/__init__.py index 34af0c7..2747ebf 100644 --- a/imagekit/models/fields/__init__.py +++ b/imagekit/models/fields/__init__.py @@ -8,34 +8,31 @@ from .utils import ImageSpecFileDescriptor, ImageKitMeta from ..receivers import configure_receivers from ...base import ImageSpec from ...utils import suggest_extension +from ...specs import SpecHost -class ImageSpecField(object): +class ImageSpecField(SpecHost): """ The heart and soul of the ImageKit library, ImageSpecField allows you to add variants of uploaded images to your models. """ def __init__(self, processors=None, format=None, options=None, - image_field=None, storage=None, autoconvert=True, - image_cache_backend=None, image_cache_strategy=None): + image_field=None, storage=None, autoconvert=None, + image_cache_backend=None, image_cache_strategy=None, spec=None, + id=None): # The spec accepts a callable value for processors, but it # takes different arguments than the callable that ImageSpecField # expects, so we create a partial application and pass that instead. # TODO: Should we change the signatures to match? Even if `instance` is not part of the signature, it's accessible through the source file object's instance property. - p = lambda file: processors(instance=file.instance, file=file) if \ - callable(processors) else processors + p = lambda file: processors(instance=file.instance, + file=file) if callable(processors) else processors - self.spec = ImageSpec( - processors=p, - format=format, - options=options, - storage=storage, - autoconvert=autoconvert, - image_cache_backend=image_cache_backend, - image_cache_strategy=image_cache_strategy, - ) + SpecHost.__init__(self, processors=p, format=format, + options=options, storage=storage, autoconvert=autoconvert, + image_cache_backend=image_cache_backend, + image_cache_strategy=image_cache_strategy, spec=spec, id=id) self.image_field = image_field @@ -59,6 +56,16 @@ class ImageSpecField(object): setattr(cls, '_ik', ik) ik.spec_fields.append(name) + # Generate a spec_id to register the spec with. The default spec id is + # ":_" + if not self.spec_id: + self.spec_id = (u'%s:%s_%s' % (cls._meta.app_label, + cls._meta.object_name, name)).lower() + + # Register the spec with the id. This allows specs to be overridden + # later, from outside of the model definition. + self.register_spec(self.spec_id) + # Register the field with the image_cache_backend try: self.spec.image_cache_backend.register_field(cls, self, name) diff --git a/imagekit/specs.py b/imagekit/specs.py index a81b82c..d2e0cba 100644 --- a/imagekit/specs.py +++ b/imagekit/specs.py @@ -164,3 +164,61 @@ class ImageSpec(BaseImageSpec): storage.save(filename, content) return content + + +class SpecHost(object): + """ + An object that ostensibly has a spec attribute but really delegates to the + spec registry. + + """ + def __init__(self, processors=None, format=None, options=None, + storage=None, autoconvert=None, image_cache_backend=None, + image_cache_strategy=None, spec=None, id=None): + + if spec: + if any([processors, format, options, storage, autoconvert, + image_cache_backend, image_cache_strategy]): + raise TypeError('You can provide either an image spec or' + ' arguments for the ImageSpec constructor, but not both.') + else: + spec = ImageSpec( + processors=processors, + format=format, + options=options, + storage=storage, + autoconvert=autoconvert, + image_cache_backend=image_cache_backend, + image_cache_strategy=image_cache_strategy, + ) + + self._original_spec = spec + self.spec_id = None + + if id: + # If an id is given, register the spec immediately. + self.register_spec(id) + + def register_spec(self, id): + """ + Registers the spec with the specified id. Useful for when the id isn't + known when the instance is constructed (e.g. for ImageSpecFields whose + generated `spec_id`s are only known when they are contributed to a + class) + + """ + self.spec_id = id + spec_registry.register(id, self._original_spec) + + @property + def spec(self): + """ + Look up the spec by the spec id. We do this (instead of storing the + spec as an attribute) so that users can override apps' specs--without + having to edit model definitions--simply by registering another spec + with the same id. + + """ + if not getattr(self, 'spec_id', None): + raise Exception('Object %s has no spec id.' % self) + return spec_registry.get_spec(self.spec_id) From 82d0e4be73993ed2fbb91769788fa0496989db14 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Thu, 4 Oct 2012 23:03:22 -0400 Subject: [PATCH 022/213] Remove unused imports --- imagekit/models/fields/__init__.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/imagekit/models/fields/__init__.py b/imagekit/models/fields/__init__.py index 2747ebf..73969dc 100644 --- a/imagekit/models/fields/__init__.py +++ b/imagekit/models/fields/__init__.py @@ -1,8 +1,6 @@ import os from django.db import models -from django.db.models.signals import post_init, post_save, post_delete - from .files import ProcessedImageFieldFile from .utils import ImageSpecFileDescriptor, ImageKitMeta from ..receivers import configure_receivers From 56c66f48833e5a36a6f65209d5b56a2391682882 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Thu, 4 Oct 2012 23:15:16 -0400 Subject: [PATCH 023/213] Back ProcessedImageField with spec registry --- imagekit/models/fields/__init__.py | 12 +++++++----- imagekit/specs.py | 6 +++--- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/imagekit/models/fields/__init__.py b/imagekit/models/fields/__init__.py index 73969dc..f0e10af 100644 --- a/imagekit/models/fields/__init__.py +++ b/imagekit/models/fields/__init__.py @@ -30,7 +30,8 @@ class ImageSpecField(SpecHost): SpecHost.__init__(self, processors=p, format=format, options=options, storage=storage, autoconvert=autoconvert, image_cache_backend=image_cache_backend, - image_cache_strategy=image_cache_strategy, spec=spec, id=id) + image_cache_strategy=image_cache_strategy, spec=spec, + spec_id=id) self.image_field = image_field @@ -71,7 +72,7 @@ class ImageSpecField(SpecHost): pass -class ProcessedImageField(models.ImageField): +class ProcessedImageField(models.ImageField, SpecHost): """ ProcessedImageField is an ImageField that runs processors on the uploaded image *before* saving it to storage. This is in contrast to specs, which @@ -83,7 +84,7 @@ class ProcessedImageField(models.ImageField): def __init__(self, processors=None, format=None, options=None, verbose_name=None, name=None, width_field=None, height_field=None, - autoconvert=True, **kwargs): + autoconvert=True, spec=None, spec_id=None, **kwargs): """ The ProcessedImageField constructor accepts all of the arguments that the :class:`django.db.models.ImageField` constructor accepts, as well @@ -91,10 +92,11 @@ class ProcessedImageField(models.ImageField): :class:`imagekit.models.ImageSpecField`. """ + SpecHost.__init__(self, processors=processors, format=format, + options=options, autoconvert=autoconvert, spec=spec, + spec_id=spec_id) models.ImageField.__init__(self, verbose_name, name, width_field, height_field, **kwargs) - self.spec = ImageSpec(processors, format=format, options=options, - autoconvert=autoconvert) def get_filename(self, filename): filename = os.path.normpath(self.storage.get_valid_name( diff --git a/imagekit/specs.py b/imagekit/specs.py index d2e0cba..b4ea37b 100644 --- a/imagekit/specs.py +++ b/imagekit/specs.py @@ -174,7 +174,7 @@ class SpecHost(object): """ def __init__(self, processors=None, format=None, options=None, storage=None, autoconvert=None, image_cache_backend=None, - image_cache_strategy=None, spec=None, id=None): + image_cache_strategy=None, spec=None, spec_id=None): if spec: if any([processors, format, options, storage, autoconvert, @@ -195,9 +195,9 @@ class SpecHost(object): self._original_spec = spec self.spec_id = None - if id: + if spec_id: # If an id is given, register the spec immediately. - self.register_spec(id) + self.register_spec(spec_id) def register_spec(self, id): """ From ce084482079b5397c9275b33d7b909beeb569af7 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Thu, 4 Oct 2012 23:22:25 -0400 Subject: [PATCH 024/213] Rename `register_spec()` to `set_spec_id()` --- imagekit/models/fields/__init__.py | 8 ++++---- imagekit/specs.py | 11 +++++------ 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/imagekit/models/fields/__init__.py b/imagekit/models/fields/__init__.py index f0e10af..ba1d87e 100644 --- a/imagekit/models/fields/__init__.py +++ b/imagekit/models/fields/__init__.py @@ -57,13 +57,13 @@ class ImageSpecField(SpecHost): # Generate a spec_id to register the spec with. The default spec id is # ":_" - if not self.spec_id: + if not getattr(self, 'spec_id', None): self.spec_id = (u'%s:%s_%s' % (cls._meta.app_label, cls._meta.object_name, name)).lower() - # Register the spec with the id. This allows specs to be overridden - # later, from outside of the model definition. - self.register_spec(self.spec_id) + # Register the spec with the id. This allows specs to be overridden + # later, from outside of the model definition. + self.set_spec_id(self.spec_id) # Register the field with the image_cache_backend try: diff --git a/imagekit/specs.py b/imagekit/specs.py index b4ea37b..49affad 100644 --- a/imagekit/specs.py +++ b/imagekit/specs.py @@ -193,18 +193,17 @@ class SpecHost(object): ) self._original_spec = spec - self.spec_id = None if spec_id: - # If an id is given, register the spec immediately. - self.register_spec(spec_id) + self.set_spec_id(spec_id) - def register_spec(self, id): + def set_spec_id(self, id): """ - Registers the spec with the specified id. Useful for when the id isn't + Sets the spec id for this object. Useful for when the id isn't known when the instance is constructed (e.g. for ImageSpecFields whose generated `spec_id`s are only known when they are contributed to a - class) + class). If the object was initialized with a spec, it will be registered + under the provided id. """ self.spec_id = id From 5a1dd0c459a32c57dc05b6872d92f83c2fe5f6e0 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Thu, 4 Oct 2012 23:27:19 -0400 Subject: [PATCH 025/213] Don't require spec to be created up-front --- imagekit/specs.py | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/imagekit/specs.py b/imagekit/specs.py index 49affad..2d4b400 100644 --- a/imagekit/specs.py +++ b/imagekit/specs.py @@ -176,21 +176,22 @@ class SpecHost(object): storage=None, autoconvert=None, image_cache_backend=None, image_cache_strategy=None, spec=None, spec_id=None): - if spec: - if any([processors, format, options, storage, autoconvert, - image_cache_backend, image_cache_strategy]): + spec_args = dict( + processors=processors, + format=format, + options=options, + storage=storage, + autoconvert=autoconvert, + image_cache_backend=image_cache_backend, + image_cache_strategy=image_cache_strategy, + ) + + if any(v is not None for v in spec_args.values()): + if spec: raise TypeError('You can provide either an image spec or' ' arguments for the ImageSpec constructor, but not both.') - else: - spec = ImageSpec( - processors=processors, - format=format, - options=options, - storage=storage, - autoconvert=autoconvert, - image_cache_backend=image_cache_backend, - image_cache_strategy=image_cache_strategy, - ) + else: + spec = ImageSpec(**spec_args) self._original_spec = spec From 436a73dc9a994ebbedff4c00ea3b1f403202d56a Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Thu, 4 Oct 2012 23:28:55 -0400 Subject: [PATCH 026/213] Remove image cache backend field registration --- imagekit/models/fields/__init__.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/imagekit/models/fields/__init__.py b/imagekit/models/fields/__init__.py index ba1d87e..f749040 100644 --- a/imagekit/models/fields/__init__.py +++ b/imagekit/models/fields/__init__.py @@ -65,12 +65,6 @@ class ImageSpecField(SpecHost): # later, from outside of the model definition. self.set_spec_id(self.spec_id) - # Register the field with the image_cache_backend - try: - self.spec.image_cache_backend.register_field(cls, self, name) - except AttributeError: - pass - class ProcessedImageField(models.ImageField, SpecHost): """ From 667f0cc08e85257d6fe38b19150e49bf926b2581 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Thu, 4 Oct 2012 23:41:20 -0400 Subject: [PATCH 027/213] Simplify IMAGEKIT_CACHE_BACKEND setting --- imagekit/conf.py | 6 ++++++ imagekit/imagecache/backends.py | 4 +--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/imagekit/conf.py b/imagekit/conf.py index 2767e6b..5780235 100644 --- a/imagekit/conf.py +++ b/imagekit/conf.py @@ -1,4 +1,5 @@ from appconf import AppConf +from django.conf import settings from .imagecache.actions import validate_now, clear_now @@ -8,3 +9,8 @@ class ImageKitConf(AppConf): CACHE_DIR = 'CACHE/images' CACHE_PREFIX = 'ik-' DEFAULT_IMAGE_CACHE_STRATEGY = 'imagekit.imagecache.strategies.Pessimistic' + + def configure_cache_backend(self, value): + if value is None: + value = 'django.core.cache.backends.dummy.DummyCache' if settings.DEBUG else 'default' + return value diff --git a/imagekit/imagecache/backends.py b/imagekit/imagecache/backends.py index cbb7f5d..c7339ab 100644 --- a/imagekit/imagecache/backends.py +++ b/imagekit/imagecache/backends.py @@ -1,6 +1,5 @@ from ..utils import get_singleton from django.core.cache import get_cache -from django.core.cache.backends.dummy import DummyCache from django.core.exceptions import ImproperlyConfigured @@ -23,8 +22,7 @@ class CachedValidationBackend(object): def cache(self): if not getattr(self, '_cache', None): from django.conf import settings - alias = settings.IMAGEKIT_CACHE_BACKEND - self._cache = get_cache(alias) if alias else DummyCache(None, {}) + self._cache = get_cache(settings.IMAGEKIT_CACHE_BACKEND) return self._cache def get_key(self, file): From 06e5f459046b1b8c4dd59fb4c3e13f9c1af0e668 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Thu, 4 Oct 2012 23:41:32 -0400 Subject: [PATCH 028/213] Remove unused imports --- imagekit/conf.py | 1 - imagekit/models/fields/__init__.py | 1 - 2 files changed, 2 deletions(-) diff --git a/imagekit/conf.py b/imagekit/conf.py index 5780235..d245b50 100644 --- a/imagekit/conf.py +++ b/imagekit/conf.py @@ -1,6 +1,5 @@ from appconf import AppConf from django.conf import settings -from .imagecache.actions import validate_now, clear_now class ImageKitConf(AppConf): diff --git a/imagekit/models/fields/__init__.py b/imagekit/models/fields/__init__.py index f749040..fb3f78d 100644 --- a/imagekit/models/fields/__init__.py +++ b/imagekit/models/fields/__init__.py @@ -4,7 +4,6 @@ from django.db import models from .files import ProcessedImageFieldFile from .utils import ImageSpecFileDescriptor, ImageKitMeta from ..receivers import configure_receivers -from ...base import ImageSpec from ...utils import suggest_extension from ...specs import SpecHost From fe803f898163856af13e6f85c8a3e81cce03ccb7 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Wed, 10 Oct 2012 00:18:54 -0400 Subject: [PATCH 029/213] Beginning to move functionality into "sources" Before this is applied, we're going to have to make it so that the image cache strategies are passed the source file, not the other file. --- imagekit/models/fields/__init__.py | 24 ++--- imagekit/models/fields/utils.py | 23 ----- imagekit/models/receivers.py | 51 ----------- imagekit/{specs.py => specs/__init__.py} | 19 ++++ imagekit/specs/signals.py | 5 ++ imagekit/specs/sources.py | 107 +++++++++++++++++++++++ imagekit/utils.py | 8 -- 7 files changed, 139 insertions(+), 98 deletions(-) delete mode 100644 imagekit/models/receivers.py rename imagekit/{specs.py => specs/__init__.py} (91%) create mode 100644 imagekit/specs/signals.py create mode 100644 imagekit/specs/sources.py diff --git a/imagekit/models/fields/__init__.py b/imagekit/models/fields/__init__.py index fb3f78d..92a25d3 100644 --- a/imagekit/models/fields/__init__.py +++ b/imagekit/models/fields/__init__.py @@ -1,11 +1,12 @@ import os from django.db import models -from .files import ProcessedImageFieldFile -from .utils import ImageSpecFileDescriptor, ImageKitMeta +from ..files import ProcessedImageFieldFile +from .utils import ImageSpecFileDescriptor from ..receivers import configure_receivers from ...utils import suggest_extension -from ...specs import SpecHost +from ...specs import SpecHost, spec_registry +from ...specs.sources import ImageFieldSpecSource class ImageSpecField(SpecHost): @@ -40,19 +41,6 @@ class ImageSpecField(SpecHost): def contribute_to_class(self, cls, name): setattr(cls, name, ImageSpecFileDescriptor(self, name)) - try: - # Make sure we don't modify an inherited ImageKitMeta instance - ik = cls.__dict__['ik'] - except KeyError: - try: - base = getattr(cls, '_ik') - except AttributeError: - ik = ImageKitMeta() - else: - # Inherit all the spec fields. - ik = ImageKitMeta(base.spec_fields) - setattr(cls, '_ik', ik) - ik.spec_fields.append(name) # Generate a spec_id to register the spec with. The default spec id is # ":_" @@ -64,6 +52,10 @@ class ImageSpecField(SpecHost): # later, from outside of the model definition. self.set_spec_id(self.spec_id) + # Register the model and field as a source for this spec id + spec_registry.add_source(self.spec_id, + ImageFieldSpecSource(cls, self.image_field)) + class ProcessedImageField(models.ImageField, SpecHost): """ diff --git a/imagekit/models/fields/utils.py b/imagekit/models/fields/utils.py index 1b3ccaa..3014b53 100644 --- a/imagekit/models/fields/utils.py +++ b/imagekit/models/fields/utils.py @@ -1,29 +1,6 @@ from .files import ImageSpecFieldFile -class BoundImageKitMeta(object): - def __init__(self, instance, spec_fields): - self.instance = instance - self.spec_fields = spec_fields - - @property - def spec_files(self): - return [getattr(self.instance, n) for n in self.spec_fields] - - -class ImageKitMeta(object): - def __init__(self, spec_fields=None): - self.spec_fields = list(spec_fields) if spec_fields else [] - - def __get__(self, instance, owner): - if instance is None: - return self - else: - ik = BoundImageKitMeta(instance, self.spec_fields) - setattr(instance, '_ik', ik) - return ik - - class ImageSpecFileDescriptor(object): def __init__(self, field, attname): self.attname = attname diff --git a/imagekit/models/receivers.py b/imagekit/models/receivers.py deleted file mode 100644 index 67960f7..0000000 --- a/imagekit/models/receivers.py +++ /dev/null @@ -1,51 +0,0 @@ -from django.db.models.signals import post_init, post_save, post_delete -from ..utils import ik_model_receiver - - -def update_source_hashes(instance): - """ - Stores hashes of the source image files so that they can be compared - later to see whether the source image has changed (and therefore whether - the spec file needs to be regenerated). - - """ - instance._ik._source_hashes = dict((f.attname, hash(f.source_file)) \ - for f in instance._ik.spec_files) - return instance._ik._source_hashes - - -@ik_model_receiver -def post_save_receiver(sender, instance=None, created=False, raw=False, **kwargs): - if not raw: - old_hashes = instance._ik._source_hashes.copy() - new_hashes = update_source_hashes(instance) - for attname in instance._ik.spec_fields: - file = getattr(instance, attname) - if created: - file.field.spec.image_cache_strategy.invoke_callback('source_create', file) - elif old_hashes[attname] != new_hashes[attname]: - file.field.spec.image_cache_strategy.invoke_callback('source_change', file) - - -@ik_model_receiver -def post_delete_receiver(sender, instance=None, **kwargs): - for spec_file in instance._ik.spec_files: - spec_file.field.spec.image_cache_strategy.invoke_callback('source_delete', spec_file) - - -@ik_model_receiver -def post_init_receiver(sender, instance, **kwargs): - update_source_hashes(instance) - - -def configure_receivers(): - # Connect the signals. We have to listen to every model (not just those - # with IK fields) and filter in our receivers because of a Django issue with - # abstract base models. - # Related: - # https://github.com/jdriscoll/django-imagekit/issues/126 - # https://code.djangoproject.com/ticket/9318 - uid = 'ik_spec_field_receivers' - post_init.connect(post_init_receiver, dispatch_uid=uid) - post_save.connect(post_save_receiver, dispatch_uid=uid) - post_delete.connect(post_delete_receiver, dispatch_uid=uid) diff --git a/imagekit/specs.py b/imagekit/specs/__init__.py similarity index 91% rename from imagekit/specs.py rename to imagekit/specs/__init__.py index 2d4b400..160612f 100644 --- a/imagekit/specs.py +++ b/imagekit/specs/__init__.py @@ -1,3 +1,4 @@ +from collections import defaultdict from django.conf import settings from hashlib import md5 import os @@ -7,6 +8,7 @@ from .imagecache.backends import get_default_image_cache_backend from .imagecache.strategies import StrategyWrapper from .lib import StringIO from .processors import ProcessorPipeline +from .signals import source_created, source_changed, source_deleted from .utils import (open_image, extension_to_format, IKContentFile, img_to_fobj, suggest_extension) @@ -14,12 +16,29 @@ from .utils import (open_image, extension_to_format, IKContentFile, img_to_fobj, class SpecRegistry(object): def __init__(self): self._specs = {} + self._sources = defaultdict(list) def register(self, id, spec): if id in self._specs: raise AlreadyRegistered('The spec with id %s is already registered' % id) self._specs[id] = spec + def add_source(self, id, source): + self._sources[id].append(source) + source_created.connect(receiver, sender, weak, dispatch_uid) + source_changed.connect(receiver, sender, weak, dispatch_uid) + source_deleted.connect(receiver, sender, weak, dispatch_uid) + + def source_receiver(self, source, source_file): + # Get a list of specs that use this source. + ids = (k for k, v in self._sources.items() if source in v) + specs = (self.get_spec(id) for id in ids) + for spec in specs: + spec.image_cache_strategy.invoke_callback(..., source_file) + + def get_sources(self, id): + return self._sources[id] + def unregister(self, id, spec): try: del self._specs[id] diff --git a/imagekit/specs/signals.py b/imagekit/specs/signals.py new file mode 100644 index 0000000..7c0888c --- /dev/null +++ b/imagekit/specs/signals.py @@ -0,0 +1,5 @@ +from django.dispatch import Signal + +source_created = Signal(providing_args=[]) +source_changed = Signal() +source_deleted = Signal() diff --git a/imagekit/specs/sources.py b/imagekit/specs/sources.py new file mode 100644 index 0000000..88e6156 --- /dev/null +++ b/imagekit/specs/sources.py @@ -0,0 +1,107 @@ +from django.db.models.signals import post_init, post_save, post_delete +from django.utils.functional import wraps +from .signals import source_created, source_changed, source_deleted + + +def ik_model_receiver(fn): + """ + A method decorator that filters out signals coming from models that don't + have fields that function as ImageFieldSpecSources + + """ + @wraps(fn) + def receiver(self, sender, **kwargs): + if sender in (src.model_class for src in self._sources): + fn(sender, **kwargs) + return receiver + + +class ModelSignalRouter(object): + def __init__(self): + self._sources = [] + uid = 'ik_spec_field_receivers' + post_init.connect(self.post_init_receiver, dispatch_uid=uid) + post_save.connect(self.post_save_receiver, dispatch_uid=uid) + post_delete.connect(self.post_delete_receiver, dispatch_uid=uid) + + def add(self, source): + self._sources.append(source) + + def init_instance(self, instance): + instance._ik = getattr(instance, '_ik', {}) + + def update_source_hashes(self, instance): + """ + Stores hashes of the source image files so that they can be compared + later to see whether the source image has changed (and therefore whether + the spec file needs to be regenerated). + + """ + self.init_instance(instance) + instance._ik['source_hashes'] = dict((k, hash(v.source_file)) + for k, v in self.get_field_dict(instance).items()) + return instance._ik['source_hashes'] + + def get_field_dict(self, instance): + """ + Returns the source fields for the given instance, in a dictionary whose + keys are the field names and values are the fields themselves. + + """ + return dict((src.image_field, getattr(instance, src.image_field)) for + src in self._sources if src.model_class is instance.__class__) + + @ik_model_receiver + def post_save_receiver(self, sender, instance=None, created=False, raw=False, **kwargs): + if not raw: + self.init_instance(instance) + old_hashes = instance._ik.get('source_hashes', {}).copy() + new_hashes = self.update_source_hashes(instance) + for attname, file in self.get_field_dict(instance).items(): + if created: + self.dispatch_signal(source_created, sender, file) + elif old_hashes[attname] != new_hashes[attname]: + self.dispatch_signal(source_changed, sender, file) + + @ik_model_receiver + def post_delete_receiver(self, sender, instance=None, **kwargs): + for attname, file in self.get_field_dict(instance): + self.dispatch_signal(source_deleted, sender, file) + + @classmethod + @ik_model_receiver + def post_init_receiver(self, sender, instance, **kwargs): + self.update_source_hashes(instance) + + def dispatch_signal(self, signal, model_class, file): + """ + Dispatch the signal for each of the matching sources. Note that more + than one source can have the same model and image_field; it's important + that we dispatch the signal for each. + + """ + for source in self._sources: + if source.model_class is model_class and source.image_field == file.attname: + signal.send(sender=source, source_file=file) + + +class ImageFieldSpecSource(object): + def __init__(self, model_class, image_field): + """ + Good design would dictate that this instance would be responsible for + watching for changes for the provided field. However, due to a bug in + Django, we can't do that without leaving abstract base models (which + don't trigger signals) in the lurch. So instead, we do all signal + handling through the signal router. + + Related: + https://github.com/jdriscoll/django-imagekit/issues/126 + https://code.djangoproject.com/ticket/9318 + + """ + self.model_class = model_class + self.image_field = image_field + signal_router.add(self) + + +signal_router = ModelSignalRouter() diff --git a/imagekit/utils.py b/imagekit/utils.py index fb60035..07f05e5 100644 --- a/imagekit/utils.py +++ b/imagekit/utils.py @@ -399,14 +399,6 @@ def get_singleton(class_path, desc): return instance -def ik_model_receiver(fn): - @wraps(fn) - def receiver(sender, **kwargs): - if getattr(sender, '_ik', None): - fn(sender, **kwargs) - return receiver - - def autodiscover(): """ Auto-discover INSTALLED_APPS imagespecs.py modules and fail silently when From 8a35b3a3ddaaa4e052da80f2b087487eeaace459 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Fri, 12 Oct 2012 22:36:13 -0400 Subject: [PATCH 030/213] Registration and connection of sources --- imagekit/models/fields/__init__.py | 15 +++---- imagekit/specs/__init__.py | 64 +++++++++++++++++------------- imagekit/specs/sources.py | 9 ++--- 3 files changed, 46 insertions(+), 42 deletions(-) diff --git a/imagekit/models/fields/__init__.py b/imagekit/models/fields/__init__.py index 92a25d3..517c94e 100644 --- a/imagekit/models/fields/__init__.py +++ b/imagekit/models/fields/__init__.py @@ -1,11 +1,11 @@ import os from django.db import models -from ..files import ProcessedImageFieldFile +from .files import ProcessedImageFieldFile from .utils import ImageSpecFileDescriptor -from ..receivers import configure_receivers +from ... import specs from ...utils import suggest_extension -from ...specs import SpecHost, spec_registry +from ...specs import SpecHost from ...specs.sources import ImageFieldSpecSource @@ -52,9 +52,9 @@ class ImageSpecField(SpecHost): # later, from outside of the model definition. self.set_spec_id(self.spec_id) - # Register the model and field as a source for this spec id - spec_registry.add_source(self.spec_id, - ImageFieldSpecSource(cls, self.image_field)) + # Add the model and field as a source for this spec id + specs.registry.add_source(ImageFieldSpecSource(cls, self.image_field), + self.spec_id) class ProcessedImageField(models.ImageField, SpecHost): @@ -97,6 +97,3 @@ except ImportError: pass else: add_introspection_rules([], [r'^imagekit\.models\.fields\.ProcessedImageField$']) - - -configure_receivers() diff --git a/imagekit/specs/__init__.py b/imagekit/specs/__init__.py index 160612f..eb98975 100644 --- a/imagekit/specs/__init__.py +++ b/imagekit/specs/__init__.py @@ -1,44 +1,35 @@ -from collections import defaultdict from django.conf import settings from hashlib import md5 import os import pickle -from .exceptions import UnknownExtensionError, AlreadyRegistered, NotRegistered -from .imagecache.backends import get_default_image_cache_backend -from .imagecache.strategies import StrategyWrapper -from .lib import StringIO -from .processors import ProcessorPipeline from .signals import source_created, source_changed, source_deleted -from .utils import (open_image, extension_to_format, IKContentFile, img_to_fobj, - suggest_extension) +from ..exceptions import UnknownExtensionError, AlreadyRegistered, NotRegistered +from ..imagecache.backends import get_default_image_cache_backend +from ..imagecache.strategies import StrategyWrapper +from ..lib import StringIO +from ..processors import ProcessorPipeline +from ..utils import (open_image, extension_to_format, IKContentFile, + img_to_fobj, suggest_extension) class SpecRegistry(object): + signals = { + source_created: 'source_created', + source_changed: 'source_changed', + source_deleted: 'source_deleted', + } + def __init__(self): self._specs = {} - self._sources = defaultdict(list) + self._sources = {} + for signal in self.signals.keys(): + signal.connect(lambda *a, **k: self.source_receiver(signal, *a, **k)) def register(self, id, spec): if id in self._specs: raise AlreadyRegistered('The spec with id %s is already registered' % id) self._specs[id] = spec - def add_source(self, id, source): - self._sources[id].append(source) - source_created.connect(receiver, sender, weak, dispatch_uid) - source_changed.connect(receiver, sender, weak, dispatch_uid) - source_deleted.connect(receiver, sender, weak, dispatch_uid) - - def source_receiver(self, source, source_file): - # Get a list of specs that use this source. - ids = (k for k, v in self._sources.items() if source in v) - specs = (self.get_spec(id) for id in ids) - for spec in specs: - spec.image_cache_strategy.invoke_callback(..., source_file) - - def get_sources(self, id): - return self._sources[id] - def unregister(self, id, spec): try: del self._specs[id] @@ -51,8 +42,22 @@ class SpecRegistry(object): except KeyError: raise NotRegistered('The spec with id %s is not registered' % id) + def add_source(self, source, spec_id): + """ + Associates a source with a spec id -spec_registry = SpecRegistry() + """ + if source not in self._sources: + self._sources[source] = set() + self._sources[source].add(spec_id) + + def source_receiver(self, signal, source, source_file): + if source not in self._sources: + return + + callback_name = self._signals[signal] + for spec in (self.get_spec(id) for id in self._sources[source]): + spec.image_cache_strategy.invoke_callback(callback_name, source_file) class BaseImageSpec(object): @@ -227,7 +232,7 @@ class SpecHost(object): """ self.spec_id = id - spec_registry.register(id, self._original_spec) + registry.register(id, self._original_spec) @property def spec(self): @@ -240,4 +245,7 @@ class SpecHost(object): """ if not getattr(self, 'spec_id', None): raise Exception('Object %s has no spec id.' % self) - return spec_registry.get_spec(self.spec_id) + return registry.get_spec(self.spec_id) + + +registry = SpecRegistry() diff --git a/imagekit/specs/sources.py b/imagekit/specs/sources.py index 88e6156..82b118b 100644 --- a/imagekit/specs/sources.py +++ b/imagekit/specs/sources.py @@ -12,7 +12,7 @@ def ik_model_receiver(fn): @wraps(fn) def receiver(self, sender, **kwargs): if sender in (src.model_class for src in self._sources): - fn(sender, **kwargs) + fn(self, sender=sender, **kwargs) return receiver @@ -38,8 +38,8 @@ class ModelSignalRouter(object): """ self.init_instance(instance) - instance._ik['source_hashes'] = dict((k, hash(v.source_file)) - for k, v in self.get_field_dict(instance).items()) + instance._ik['source_hashes'] = dict((attname, hash(file_field)) + for attname, file_field in self.get_field_dict(instance).items()) return instance._ik['source_hashes'] def get_field_dict(self, instance): @@ -68,9 +68,8 @@ class ModelSignalRouter(object): for attname, file in self.get_field_dict(instance): self.dispatch_signal(source_deleted, sender, file) - @classmethod @ik_model_receiver - def post_init_receiver(self, sender, instance, **kwargs): + def post_init_receiver(self, sender, instance=None, **kwargs): self.update_source_hashes(instance) def dispatch_signal(self, signal, model_class, file): From a59330cf2c17065b2d590f8e340b85cd72a514ab Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Fri, 12 Oct 2012 23:12:05 -0400 Subject: [PATCH 031/213] Add class description --- imagekit/specs/sources.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/imagekit/specs/sources.py b/imagekit/specs/sources.py index 82b118b..807ee2a 100644 --- a/imagekit/specs/sources.py +++ b/imagekit/specs/sources.py @@ -17,6 +17,12 @@ def ik_model_receiver(fn): class ModelSignalRouter(object): + """ + Handles signals dispatched by models and relays them to the spec sources + that represent those models. + + """ + def __init__(self): self._sources = [] uid = 'ik_spec_field_receivers' From 440fcb19efb34ceff1665740d5bb0a5cfa219c1a Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Fri, 12 Oct 2012 23:43:51 -0400 Subject: [PATCH 032/213] Correct signal relaying --- imagekit/specs/__init__.py | 7 ++++--- imagekit/specs/sources.py | 11 ++++++----- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/imagekit/specs/__init__.py b/imagekit/specs/__init__.py index eb98975..e29cdb6 100644 --- a/imagekit/specs/__init__.py +++ b/imagekit/specs/__init__.py @@ -23,7 +23,7 @@ class SpecRegistry(object): self._specs = {} self._sources = {} for signal in self.signals.keys(): - signal.connect(lambda *a, **k: self.source_receiver(signal, *a, **k)) + signal.connect(self.source_receiver) def register(self, id, spec): if id in self._specs: @@ -51,11 +51,12 @@ class SpecRegistry(object): self._sources[source] = set() self._sources[source].add(spec_id) - def source_receiver(self, signal, source, source_file): + def source_receiver(self, sender, source_file, signal, **kwargs): + source = sender if source not in self._sources: return - callback_name = self._signals[signal] + callback_name = self.signals[signal] for spec in (self.get_spec(id) for id in self._sources[source]): spec.image_cache_strategy.invoke_callback(callback_name, source_file) diff --git a/imagekit/specs/sources.py b/imagekit/specs/sources.py index 807ee2a..183180d 100644 --- a/imagekit/specs/sources.py +++ b/imagekit/specs/sources.py @@ -65,20 +65,20 @@ class ModelSignalRouter(object): new_hashes = self.update_source_hashes(instance) for attname, file in self.get_field_dict(instance).items(): if created: - self.dispatch_signal(source_created, sender, file) + self.dispatch_signal(source_created, file, sender) elif old_hashes[attname] != new_hashes[attname]: - self.dispatch_signal(source_changed, sender, file) + self.dispatch_signal(source_changed, file, sender) @ik_model_receiver def post_delete_receiver(self, sender, instance=None, **kwargs): for attname, file in self.get_field_dict(instance): - self.dispatch_signal(source_deleted, sender, file) + self.dispatch_signal(source_deleted, file, sender) @ik_model_receiver def post_init_receiver(self, sender, instance=None, **kwargs): self.update_source_hashes(instance) - def dispatch_signal(self, signal, model_class, file): + def dispatch_signal(self, signal, file, model_class): """ Dispatch the signal for each of the matching sources. Note that more than one source can have the same model and image_field; it's important @@ -86,7 +86,8 @@ class ModelSignalRouter(object): """ for source in self._sources: - if source.model_class is model_class and source.image_field == file.attname: + # TODO: Is it okay to require a field attribute on our file? + if source.model_class is model_class and source.image_field == file.field.attname: signal.send(sender=source, source_file=file) From cedd744e32a8e76681fb33f04d4847b2c81c7253 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Sat, 13 Oct 2012 00:22:30 -0400 Subject: [PATCH 033/213] Add description of registry --- imagekit/specs/__init__.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/imagekit/specs/__init__.py b/imagekit/specs/__init__.py index e29cdb6..e3a4407 100644 --- a/imagekit/specs/__init__.py +++ b/imagekit/specs/__init__.py @@ -13,6 +13,16 @@ from ..utils import (open_image, extension_to_format, IKContentFile, class SpecRegistry(object): + """ + An object for registering specs and sources. The two are associated with + eachother via a string id. We do this (as opposed to associating them + directly by, for example, putting a ``sources`` attribute on specs) so that + specs can be overridden without losing the associated sources. That way, + a distributable app can define its own specs without locking the users of + the app into it. + + """ + signals = { source_created: 'source_created', source_changed: 'source_changed', From 7447d147d4da1eff12138ba7df14e79a5f253176 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Sat, 13 Oct 2012 01:15:42 -0400 Subject: [PATCH 034/213] Wire up source events to specs (and image cache strategies) Also change signal names to past tense to match convention. --- imagekit/imagecache/strategies.py | 14 +++++------ imagekit/specs/__init__.py | 39 ++++++++++++++++++++++++------- 2 files changed, 38 insertions(+), 15 deletions(-) diff --git a/imagekit/imagecache/strategies.py b/imagekit/imagecache/strategies.py index 7ec7252..e44929a 100644 --- a/imagekit/imagecache/strategies.py +++ b/imagekit/imagecache/strategies.py @@ -8,13 +8,13 @@ class Pessimistic(object): """ - def on_access(self, file): + def on_accessed(self, file): validate_now(file) - def on_source_delete(self, file): + def on_source_deleted(self, file): clear_now(file) - def on_source_change(self, file): + def on_source_changed(self, file): validate_now(file) @@ -25,13 +25,13 @@ class Optimistic(object): """ - def on_source_create(self, file): + def on_source_created(self, file): validate_now(file) - def on_source_delete(self, file): + def on_source_deleted(self, file): clear_now(file) - def on_source_change(self, file): + def on_source_changed(self, file): validate_now(file) @@ -52,6 +52,6 @@ class StrategyWrapper(object): self._wrapped = strategy def invoke_callback(self, name, *args, **kwargs): - func = getattr(self._wrapped, 'on_%s' % name, None) + func = getattr(self._wrapped, name, None) if func: func(*args, **kwargs) diff --git a/imagekit/specs/__init__.py b/imagekit/specs/__init__.py index e3a4407..7b5b0df 100644 --- a/imagekit/specs/__init__.py +++ b/imagekit/specs/__init__.py @@ -4,6 +4,7 @@ import os import pickle from .signals import source_created, source_changed, source_deleted from ..exceptions import UnknownExtensionError, AlreadyRegistered, NotRegistered +from ..files import ImageSpecFile from ..imagecache.backends import get_default_image_cache_backend from ..imagecache.strategies import StrategyWrapper from ..lib import StringIO @@ -23,16 +24,16 @@ class SpecRegistry(object): """ - signals = { - source_created: 'source_created', - source_changed: 'source_changed', - source_deleted: 'source_deleted', - } + _source_signals = [ + source_created, + source_changed, + source_deleted, + ] def __init__(self): self._specs = {} self._sources = {} - for signal in self.signals.keys(): + for signal in self._source_signals: signal.connect(self.source_receiver) def register(self, id, spec): @@ -62,13 +63,21 @@ class SpecRegistry(object): self._sources[source].add(spec_id) def source_receiver(self, sender, source_file, signal, **kwargs): + """ + Redirects signals dispatched on sources to the appropriate specs. + + """ source = sender if source not in self._sources: return - callback_name = self.signals[signal] for spec in (self.get_spec(id) for id in self._sources[source]): - spec.image_cache_strategy.invoke_callback(callback_name, source_file) + event_name = { + source_created: 'source_created', + source_changed: 'source_changed', + source_deleted: 'source_deleted', + } + spec._handle_source_event(event_name, source_file) class BaseImageSpec(object): @@ -176,6 +185,20 @@ class ImageSpec(BaseImageSpec): self.image_cache_backend = image_cache_backend or self.image_cache_backend or get_default_image_cache_backend() self.image_cache_strategy = StrategyWrapper(image_cache_strategy or self.image_cache_strategy) + # TODO: Can we come up with a better name for this? "process" may cause confusion with processors' process() + def apply(self, source_file): + """ + Creates a file object that represents the combination of a spec and + source file. + + """ + return ImageSpecFile(self, source_file) + + # TODO: I don't like this interface. Is there a standard Python one? pubsub? + def _handle_source_event(self, event_name, source_file): + file = self.apply(source_file) + self.image_cache_strategy.invoke_callback('on_%s' % event_name, file) + def generate_file(self, filename, source_file, save=True): """ Generates a new image file by processing the source file and returns From 7ce05f468a3b6a7f0d9f325ec3a6b952f1da04a5 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Sun, 14 Oct 2012 15:41:35 -0400 Subject: [PATCH 035/213] Pass attname to dispatch_signal This allows any file object to be used--even those that don't have a `field` attribute. For example, you could theoretically use one spec field as a source for another. --- imagekit/specs/sources.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/imagekit/specs/sources.py b/imagekit/specs/sources.py index 183180d..389d7cb 100644 --- a/imagekit/specs/sources.py +++ b/imagekit/specs/sources.py @@ -65,20 +65,20 @@ class ModelSignalRouter(object): new_hashes = self.update_source_hashes(instance) for attname, file in self.get_field_dict(instance).items(): if created: - self.dispatch_signal(source_created, file, sender) + self.dispatch_signal(source_created, file, sender, attname) elif old_hashes[attname] != new_hashes[attname]: - self.dispatch_signal(source_changed, file, sender) + self.dispatch_signal(source_changed, file, sender, attname) @ik_model_receiver def post_delete_receiver(self, sender, instance=None, **kwargs): for attname, file in self.get_field_dict(instance): - self.dispatch_signal(source_deleted, file, sender) + self.dispatch_signal(source_deleted, file, sender, attname) @ik_model_receiver def post_init_receiver(self, sender, instance=None, **kwargs): self.update_source_hashes(instance) - def dispatch_signal(self, signal, file, model_class): + def dispatch_signal(self, signal, file, model_class, attname): """ Dispatch the signal for each of the matching sources. Note that more than one source can have the same model and image_field; it's important @@ -86,8 +86,7 @@ class ModelSignalRouter(object): """ for source in self._sources: - # TODO: Is it okay to require a field attribute on our file? - if source.model_class is model_class and source.image_field == file.field.attname: + if source.model_class is model_class and source.image_field == attname: signal.send(sender=source, source_file=file) From dc84144d6badb4cf24b35abfa7e01f0bf55a1859 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Sun, 14 Oct 2012 15:52:08 -0400 Subject: [PATCH 036/213] Pass additional info with source signal --- imagekit/specs/__init__.py | 2 +- imagekit/specs/sources.py | 17 ++++++++++++----- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/imagekit/specs/__init__.py b/imagekit/specs/__init__.py index 7b5b0df..a86eb95 100644 --- a/imagekit/specs/__init__.py +++ b/imagekit/specs/__init__.py @@ -62,7 +62,7 @@ class SpecRegistry(object): self._sources[source] = set() self._sources[source].add(spec_id) - def source_receiver(self, sender, source_file, signal, **kwargs): + def source_receiver(self, sender, source_file, signal, info, **kwargs): """ Redirects signals dispatched on sources to the appropriate specs. diff --git a/imagekit/specs/sources.py b/imagekit/specs/sources.py index 389d7cb..42f8c85 100644 --- a/imagekit/specs/sources.py +++ b/imagekit/specs/sources.py @@ -65,20 +65,22 @@ class ModelSignalRouter(object): new_hashes = self.update_source_hashes(instance) for attname, file in self.get_field_dict(instance).items(): if created: - self.dispatch_signal(source_created, file, sender, attname) + self.dispatch_signal(source_created, file, sender, instance, + attname) elif old_hashes[attname] != new_hashes[attname]: - self.dispatch_signal(source_changed, file, sender, attname) + self.dispatch_signal(source_changed, file, sender, instance, + attname) @ik_model_receiver def post_delete_receiver(self, sender, instance=None, **kwargs): for attname, file in self.get_field_dict(instance): - self.dispatch_signal(source_deleted, file, sender, attname) + self.dispatch_signal(source_deleted, file, sender, instance, attname) @ik_model_receiver def post_init_receiver(self, sender, instance=None, **kwargs): self.update_source_hashes(instance) - def dispatch_signal(self, signal, file, model_class, attname): + def dispatch_signal(self, signal, file, model_class, instance, attname): """ Dispatch the signal for each of the matching sources. Note that more than one source can have the same model and image_field; it's important @@ -87,7 +89,12 @@ class ModelSignalRouter(object): """ for source in self._sources: if source.model_class is model_class and source.image_field == attname: - signal.send(sender=source, source_file=file) + info = dict( + source=source, + instance=instance, + field_name=attname, + ) + signal.send(sender=source, source_file=file, info=info) class ImageFieldSpecSource(object): From 461fbaef1a524456f89b1839fec28bfb96ac3717 Mon Sep 17 00:00:00 2001 From: Eric Eldredge Date: Sun, 14 Oct 2012 18:48:17 -0400 Subject: [PATCH 037/213] Processors no longer callable --- imagekit/models/fields/__init__.py | 9 +-------- imagekit/specs/__init__.py | 10 ++-------- 2 files changed, 3 insertions(+), 16 deletions(-) diff --git a/imagekit/models/fields/__init__.py b/imagekit/models/fields/__init__.py index 517c94e..148212c 100644 --- a/imagekit/models/fields/__init__.py +++ b/imagekit/models/fields/__init__.py @@ -20,14 +20,7 @@ class ImageSpecField(SpecHost): image_cache_backend=None, image_cache_strategy=None, spec=None, id=None): - # The spec accepts a callable value for processors, but it - # takes different arguments than the callable that ImageSpecField - # expects, so we create a partial application and pass that instead. - # TODO: Should we change the signatures to match? Even if `instance` is not part of the signature, it's accessible through the source file object's instance property. - p = lambda file: processors(instance=file.instance, - file=file) if callable(processors) else processors - - SpecHost.__init__(self, processors=p, format=format, + SpecHost.__init__(self, processors=processors, format=format, options=options, storage=storage, autoconvert=autoconvert, image_cache_backend=image_cache_backend, image_cache_strategy=image_cache_strategy, spec=spec, diff --git a/imagekit/specs/__init__.py b/imagekit/specs/__init__.py index a86eb95..cdb5638 100644 --- a/imagekit/specs/__init__.py +++ b/imagekit/specs/__init__.py @@ -92,16 +92,10 @@ class BaseImageSpec(object): self.options = options or self.options self.autoconvert = self.autoconvert if autoconvert is None else autoconvert - def get_processors(self, source_file): - processors = self.processors - if callable(processors): - processors = processors(source_file) - return processors - def get_hash(self, source_file): return md5(''.join([ source_file.name, - pickle.dumps(self.get_processors(source_file)), + pickle.dumps(self.processors), self.format, pickle.dumps(self.options), str(self.autoconvert), @@ -125,7 +119,7 @@ class BaseImageSpec(object): original_format = img.format # Run the processors - processors = self.get_processors(source_file) + processors = self.processors img = ProcessorPipeline(processors or []).process(img) options = dict(self.options or {}) From 93409c8f05b160584296e639d9cf202a86e456bc Mon Sep 17 00:00:00 2001 From: Eric Eldredge Date: Sun, 14 Oct 2012 21:35:07 -0400 Subject: [PATCH 038/213] SpecRegistry's spec can be callable (spec factory) --- imagekit/specs/__init__.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/imagekit/specs/__init__.py b/imagekit/specs/__init__.py index cdb5638..d6271d7 100644 --- a/imagekit/specs/__init__.py +++ b/imagekit/specs/__init__.py @@ -47,11 +47,15 @@ class SpecRegistry(object): except KeyError: raise NotRegistered('The spec with id %s is not registered' % id) - def get_spec(self, id): + def get_spec(self, id, **kwargs): try: - return self._specs[id] + spec = self._specs[id] except KeyError: raise NotRegistered('The spec with id %s is not registered' % id) + if callable(spec): + return spec(**kwargs) + else: + return spec def add_source(self, source, spec_id): """ @@ -71,7 +75,8 @@ class SpecRegistry(object): if source not in self._sources: return - for spec in (self.get_spec(id) for id in self._sources[source]): + for spec in (self.get_spec(id, source_file=source_file, **info) + for id in self._sources[source]): event_name = { source_created: 'source_created', source_changed: 'source_changed', From 5fe5a73cb17a55c1351fee7d57e5c56ce77b58ea Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Sun, 14 Oct 2012 22:28:48 -0400 Subject: [PATCH 039/213] Update docs This will be great when 3.0 is ready, but it'll also serve as a nice guide for us as we develop. --- README.rst | 287 +++++++++++++++++++++++++++++++++++------------------ 1 file changed, 189 insertions(+), 98 deletions(-) diff --git a/README.rst b/README.rst index 041d02d..9b38119 100644 --- a/README.rst +++ b/README.rst @@ -1,6 +1,7 @@ -ImageKit is a Django app that helps you to add variations of uploaded images -to your models. These variations are called "specs" and can include things -like different sizes (e.g. thumbnails) and black and white versions. +ImageKit is a Django app for processing images. Need a thumbnail? A +black-and-white version of a user-uploaded image? ImageKit will make them for +you. If you need to programatically generate one image from another, you need +ImageKit. **For the complete documentation on the latest stable version of ImageKit, see** `ImageKit on RTD`_. Our `changelog is also available`_. @@ -10,10 +11,10 @@ like different sizes (e.g. thumbnails) and black and white versions. Installation ------------- +============ -1. Install `PIL`_ or `Pillow`_. If you're using an ``ImageField`` in Django, - you should have already done this. +1. Install `PIL`_ or `Pillow`_. (If you're using an ``ImageField`` in Django, + you should have already done this.) 2. ``pip install django-imagekit`` (or clone the source and put the imagekit module on your path) 3. Add ``'imagekit'`` to your ``INSTALLED_APPS`` list in your project's settings.py @@ -27,11 +28,90 @@ Installation .. _`Pillow`: http://pypi.python.org/pypi/Pillow -Adding Specs to a Model ------------------------ +Usage Overview +============== -Much like ``django.db.models.ImageField``, Specs are defined as properties -of a model class: + +Specs +----- + +You have one image and you want to do something to it to create another image. +That's the basic use case of ImageKit. But how do you tell ImageKit what to do? +By defining an "image spec." Specs are instructions for creating a new image +from an existing one, and there are a few ways to define one. The most basic +way is by defining an ``ImageSpec`` subclass: + +.. code-block:: python + + from imagekit import ImageSpec + from imagekit.processors import ResizeToFill + + class Thumbnail(ImageSpec): + processors = [ResizeToFill(100, 50)] + format = 'JPEG' + options = {'quality': 60} + +Now that you've defined a spec, it's time to use it. The nice thing about specs +is that they can be used in many different contexts. + +Sometimes, you may want to just use a spec to generate a new image file. This +might be useful, for example, in view code, or in scripts: + +.. code-block:: python + + ???????? + +More often, however, you'll want to register your spec with ImageKit: + +.. code-block:: python + + from imagekit import specs + specs.register('myapp:fancy_thumbnail', Thumbnail) + +Once a spec is registered with a unique name, you can start to take advantage of +ImageKit's powerful utilities to automatically generate images for you... + +.. note:: You might be wondering why we bother with the id string instead of + just passing the spec itself. The reason is that these ids allow users to + easily override specs defined in third party apps. That way, it doesn't + matter if "django-badblog" says its thumbnails are 200x200, you can just + register your own spec (using the same id the app uses) and have whatever + size thumbnails you want. + + +In Templates +^^^^^^^^^^^^ + +One utility ImageKit provides for processing images is a template tag: + +.. code-block:: html + + {% load imagekit %} + + {% spec 'myapp:fancy_thumbnail' source_image alt='A picture of me' %} + +Output: + +.. code-block:: html + + A picture of me + +Not generating HTML image tags? No problem. The tag also functions as an +assignment tag, providing access to the underlying file object: + +.. code-block:: html + + {% load imagekit %} + + {% spec 'myapp:fancy_thumbnail' source_image as th %} + Click to download a cool {{ th.width }} x {{ th.height }} image! + + +In Models +^^^^^^^^^ + +Specs can also be used to add ``ImageField``-like fields that expose the result +of applying a spec to another one of your model's fields: .. code-block:: python @@ -39,73 +119,111 @@ of a model class: from imagekit.models import ImageSpecField class Photo(models.Model): - original_image = models.ImageField(upload_to='photos') - formatted_image = ImageSpecField(image_field='original_image', format='JPEG', - options={'quality': 90}) - -Accessing the spec through a model instance will create the image and return -an ImageFile-like object (just like with a normal -``django.db.models.ImageField``): - -.. code-block:: python + avatar = models.ImageField(upload_to='avatars') + avatar_thumbnail = ImageSpecField(id='myapp:fancy_thumbnail', image_field='avatar') photo = Photo.objects.all()[0] - photo.original_image.url # > '/media/photos/birthday.tiff' - photo.formatted_image.url # > '/media/cache/photos/birthday_formatted_image.jpeg' + print photo.avatar_thumbnail.url # > /static/CACHE/ik/982d5af84cddddfd0fbf70892b4431e4.jpg + print photo.avatar_thumbnail.width # > 100 -Check out ``imagekit.models.ImageSpecField`` for more information. - -If you only want to save the processed image (without maintaining the original), -you can use a ``ProcessedImageField``: +Since defining a spec, registering it, and using it in a single model field is +such a common usage, ImakeKit provides a shortcut that allow you to skip +writing a subclass of ``ImageSpec``: .. code-block:: python from django.db import models - from imagekit.models.fields import ProcessedImageField + from imagekit.models import ImageSpecField + from imagekit.processors import ResizeToFill class Photo(models.Model): - processed_image = ProcessedImageField(format='JPEG', options={'quality': 90}) + avatar = models.ImageField(upload_to='avatars') + avatar_thumbnail = ImageSpecField(processors=[ResizeToFill(100, 50)], + format='JPEG', + options={'quality': 60}, + image_field='avatar') -See the class documentation for details. + photo = Photo.objects.all()[0] + print photo.avatar_thumbnail.url # > /static/CACHE/ik/982d5af84cddddfd0fbf70892b4431e4.jpg + print photo.avatar_thumbnail.width # > 100 + +This has the exact same behavior as before, but the spec definition is inlined. +Since no ``id`` is provided, one is automatically generated based on the app +name, model, and field. + +Specs can also be used in models to add ``ImageField``-like fields that process +a user-provided image without saving the original: + +.. code-block:: python + + from django.db import models + from imagekit.models import ProcessedImageField + + class Photo(models.Model): + avatar_thumbnail = ProcessedImageField(spec_id='myapp:fancy_thumbnail', + upload_to='avatars') + + photo = Photo.objects.all()[0] + print photo.avatar_thumbnail.url # > /static/avatars/MY-avatar_3.jpg + print photo.avatar_thumbnail.width # > 100 + +Like with ``ImageSpecField``, the ``ProcessedImageField`` constructor also +has a shortcut version that allows you to inline spec definitions. + + +In Forms +^^^^^^^^ + +In addition to the model field above, there's also a form field version of the +``ProcessedImageField`` class. The functionality is basically the same (it +processes an image once and saves the result), but it's used in a form class: + +.. code-block:: python + + from django import forms + from imagekit.forms import ProcessedImageField + + class AvatarForm(forms.Form): + avatar_thumbnail = ProcessedImageField(spec_id='myapp:fancy_thumbnail') + +The benefit of using ``imagekit.forms.ProcessedImageField`` (as opposed to +``imagekit.models.ProcessedImageField`` above) is that it keeps the logic for +creating the image outside of your model (in which you would use a normal +Django ``ImageField``). You can even create multiple forms, each with their own +``ProcessedImageField``, that all store their results in the same image field. + +As with the model field classes, ``imagekit.forms.ProcessedImageField`` also +has a shortcut version that allows you to inline spec definitions. Processors ---------- -The real power of ImageKit comes from processors. Processors take an image, do -something to it, and return the result. By providing a list of processors to -your spec, you can expose different versions of the original image: +So far, we've only seen one processor: ``imagekit.processors.ResizeToFill``. But +ImageKit is capable of far more than just resizing images, and that power comes +from its processors. + +Processors take a PIL image object, do something to it, and return a new one. +A spec can make use of as many processors as you'd like, which will all be run +in order. .. code-block:: python - from django.db import models - from imagekit.models import ImageSpecField - from imagekit.processors import ResizeToFill, Adjust + from imagekit import ImageSpec + from imagekit.processors import TrimBorderColor, Adjust - class Photo(models.Model): - original_image = models.ImageField(upload_to='photos') - thumbnail = ImageSpecField([Adjust(contrast=1.2, sharpness=1.1), - ResizeToFill(50, 50)], image_field='original_image', - format='JPEG', options={'quality': 90}) - -The ``thumbnail`` property will now return a cropped image: - -.. code-block:: python - - photo = Photo.objects.all()[0] - photo.thumbnail.url # > '/media/cache/photos/birthday_thumbnail.jpeg' - photo.thumbnail.width # > 50 - photo.original_image.width # > 1000 - -The original image is not modified; ``thumbnail`` is a new file that is the -result of running the ``imagekit.processors.ResizeToFill`` processor on the -original. (If you only need to save the processed image, and not the original, -pass processors to a ``ProcessedImageField`` instead of an ``ImageSpecField``.) + class MySpec(ImageSpec): + processors = [ + TrimBorderColor(), + Adjust(contrast=1.2, sharpness=1.1), + ] + format = 'JPEG' + options = {'quality': 60} The ``imagekit.processors`` module contains processors for many common image manipulations, like resizing, rotating, and color adjustments. However, if they aren't up to the task, you can create your own. All you have to do is -implement a ``process()`` method: +define a class that implements a ``process()`` method: .. code-block:: python @@ -114,10 +232,23 @@ implement a ``process()`` method: # Code for adding the watermark goes here. return image - class Photo(models.Model): - original_image = models.ImageField(upload_to='photos') - watermarked_image = ImageSpecField([Watermark()], image_field='original_image', - format='JPEG', options={'quality': 90}) +That's all there is to it! To use your fancy new custom processor, just include +it in your spec's ``processors`` list: + +.. code-block:: python + + from imagekit import ImageSpec + from imagekit.processors import TrimBorderColor, Adjust + from myapp.processors import Watermark + + class MySpec(ImageSpec): + processors = [ + TrimBorderColor(), + Adjust(contrast=1.2, sharpness=1.1), + Watermark(), + ] + format = 'JPEG' + options = {'quality': 60} Admin @@ -134,12 +265,10 @@ Django admin classes: from imagekit.admin import AdminThumbnail from .models import Photo - class PhotoAdmin(admin.ModelAdmin): list_display = ('__str__', 'admin_thumbnail') admin_thumbnail = AdminThumbnail(image_field='thumbnail') - admin.site.register(Photo, PhotoAdmin) AdminThumbnail can even use a custom template. For more information, see @@ -148,40 +277,6 @@ AdminThumbnail can even use a custom template. For more information, see .. _`Django admin change list`: https://docs.djangoproject.com/en/dev/intro/tutorial02/#customize-the-admin-change-list -Image Cache Backends --------------------- - -Whenever you access properties like ``url``, ``width`` and ``height`` of an -``ImageSpecField``, its cached image is validated; whenever you save a new image -to the ``ImageField`` your spec uses as a source, the spec image is invalidated. -The default way to validate a cache image is to check to see if the file exists -and, if not, generate a new one; the default way to invalidate the cache is to -delete the image. This is a very simple and straightforward way to handle cache -validation, but it has its drawbacks—for example, checking to see if the image -exists means frequently hitting the storage backend. - -Because of this, ImageKit allows you to define custom image cache backends. To -be a valid image cache backend, a class must implement three methods: -``validate``, ``invalidate``, and ``clear`` (which is called when the image is -no longer needed in any form, i.e. the model is deleted). Each of these methods -must accept a file object, but the internals are up to you. For example, you -could store the state (valid, invalid) of the cache in a database to avoid -filesystem access. You can then specify your image cache backend on a per-field -basis: - -.. code-block:: python - - class Photo(models.Model): - ... - thumbnail = ImageSpecField(..., image_cache_backend=MyImageCacheBackend()) - -Or in your ``settings.py`` file if you want to use it as the default: - -.. code-block:: python - - IMAGEKIT_DEFAULT_IMAGE_CACHE_BACKEND = 'path.to.MyImageCacheBackend' - - Community --------- @@ -199,7 +294,3 @@ even Django—to contribute either: ImageKit's processors are standalone classes that are completely separate from the more intimidating internals of Django's ORM. If you've written a processor that you think might be useful to other people, open a pull request so we can take a look! - -ImageKit's image cache backends are also fairly isolated from the ImageKit guts. -If you've fine-tuned one to work perfectly for a popular file storage backend, -let us take a look! Maybe other people could use it. From 2a33a2ad88ad2b636eb1fbd86ae672f93e6a6ab0 Mon Sep 17 00:00:00 2001 From: Eric Eldredge Date: Mon, 15 Oct 2012 21:17:58 -0400 Subject: [PATCH 040/213] Fix circular import utils > imagecache.backends --- imagekit/utils.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/imagekit/utils.py b/imagekit/utils.py index 07f05e5..35671ac 100644 --- a/imagekit/utils.py +++ b/imagekit/utils.py @@ -439,5 +439,8 @@ class SpecWrapper(object): self.options = getattr(spec, 'options', None) self.autoconvert = getattr(spec, 'autoconvert', True) self.storage = getattr(spec, 'storage', None) - self.image_cache_backend = getattr(spec, 'image_cache_backend', None) \ - or get_default_image_cache_backend() + self.image_cache_backend = getattr(spec, 'image_cache_backend', None) + + if not self.image_cache_backend: + from .imagecache.backends import get_default_image_cache_backend + self.image_cache_backend = get_default_image_cache_backend() From c0b79a227d893c464cb37c2e9171f799111ee2a0 Mon Sep 17 00:00:00 2001 From: Eric Eldredge Date: Mon, 15 Oct 2012 23:53:05 -0400 Subject: [PATCH 041/213] Remove ImageSpecFieldFile in favor of ImageSpecFile --- imagekit/files.py | 23 +++--- imagekit/models/fields/files.py | 122 +------------------------------- imagekit/models/fields/utils.py | 25 ++++++- imagekit/utils.py | 6 ++ 4 files changed, 44 insertions(+), 132 deletions(-) diff --git a/imagekit/files.py b/imagekit/files.py index 0d9837f..b3b543c 100644 --- a/imagekit/files.py +++ b/imagekit/files.py @@ -15,6 +15,9 @@ class ImageSpecFile(ImageFieldFile): self.source_file = source_file self.spec_id = spec_id + def get_hash(self): + return self.spec.get_hash(self.source_file) + @property def url(self): self.validate() @@ -37,14 +40,18 @@ class ImageSpecFile(ImageFieldFile): @property def name(self): - source_filename = self.source_file.name - filepath, basename = os.path.split(source_filename) - filename = os.path.splitext(basename)[0] - extension = suggest_extension(source_filename, self.spec.format) - new_name = '%s%s' % (filename, extension) - cache_filename = ['cache', 'iktt'] + self.spec_id.split(':') + \ - [filepath, new_name] - return os.path.join(*cache_filename) + name = self.spec.generate_filename(self.source_file) + if name is not None: + return name + else: + source_filename = self.source_file.name + filepath, basename = os.path.split(source_filename) + filename = os.path.splitext(basename)[0] + extension = suggest_extension(source_filename, self.spec.format) + new_name = '%s%s' % (filename, extension) + cache_filename = ['cache', 'iktt'] + self.spec_id.split(':') + \ + [filepath, new_name] + return os.path.join(*cache_filename) def generate(self, save=True): return self.spec.generate_file(self.name, self.source_file, save) diff --git a/imagekit/models/fields/files.py b/imagekit/models/fields/files.py index 691ce6b..f0fc542 100644 --- a/imagekit/models/fields/files.py +++ b/imagekit/models/fields/files.py @@ -1,124 +1,4 @@ -from django.db.models.fields.files import ImageField, ImageFieldFile - - -class ImageSpecFieldFile(ImageFieldFile): - def __init__(self, instance, field, attname): - super(ImageSpecFieldFile, self).__init__(instance, field, None) - self.attname = attname - - def get_hash(self): - return self.field.spec.get_hash(self.source_file) - - @property - def source_file(self): - field_name = getattr(self.field, 'image_field', None) - if field_name: - field_file = getattr(self.instance, field_name) - else: - image_fields = [getattr(self.instance, f.attname) for f in \ - self.instance.__class__._meta.fields if \ - isinstance(f, ImageField)] - if len(image_fields) == 0: - raise Exception('%s does not define any ImageFields, so your' \ - ' %s ImageSpecField has no image to act on.' % \ - (self.instance.__class__.__name__, self.attname)) - elif len(image_fields) > 1: - raise Exception('%s defines multiple ImageFields, but you' \ - ' have not specified an image_field for your %s' \ - ' ImageSpecField.' % (self.instance.__class__.__name__, - self.attname)) - else: - field_file = image_fields[0] - return field_file - - def _require_file(self): - if not self.source_file: - raise ValueError("The '%s' attribute's image_field has no file associated with it." % self.attname) - self.field.spec.image_cache_strategy.invoke_callback('access', self) - - def clear(self): - return self.field.spec.image_cache_backend.clear(self) - - def invalidate(self): - return self.field.spec.image_cache_backend.invalidate(self) - - def validate(self): - return self.field.spec.image_cache_backend.validate(self) - - def generate(self, save=True): - """ - Generates a new image file by processing the source file and returns - the content of the result, ready for saving. - - """ - return self.field.spec.generate_file(self.name, self.source_file, - save) - - def delete(self, save=False): - """ - Pulled almost verbatim from ``ImageFieldFile.delete()`` and - ``FieldFile.delete()`` but with the attempts to reset the instance - property removed. - - """ - # Clear the image dimensions cache - if hasattr(self, '_dimensions_cache'): - del self._dimensions_cache - - # Only close the file if it's already open, which we know by the - # presence of self._file. - if hasattr(self, '_file'): - self.close() - del self.file - - if self.name and self.storage.exists(self.name): - try: - self.storage.delete(self.name) - except NotImplementedError: - pass - - # Delete the filesize cache. - if hasattr(self, '_size'): - del self._size - self._committed = False - - if save: - self.instance.save() - - @property - def name(self): - """ - Specifies the filename that the cached image will use. - - """ - return self.field.spec.generate_filename(self.source_file) - - @name.setter - def name(self, value): - # TODO: Figure out a better way to handle this. We really don't want - # to allow anybody to set the name, but ``File.__init__`` (which is - # called by ``ImageSpecFieldFile.__init__``) does, so we have to allow - # it at least that one time. - pass - - @property - def storage(self): - return getattr(self, '_storage', None) or self.field.storage or self.source_file.storage - - @storage.setter - def storage(self, storage): - self._storage = storage - - def __getstate__(self): - return dict( - attname=self.attname, - instance=self.instance, - ) - - def __setstate__(self, state): - self.attname = state['attname'] - self.instance = state['instance'] - self.field = getattr(self.instance.__class__, self.attname) +from django.db.models.fields.files import ImageFieldFile class ProcessedImageFieldFile(ImageFieldFile): diff --git a/imagekit/models/fields/utils.py b/imagekit/models/fields/utils.py index 3014b53..dab6170 100644 --- a/imagekit/models/fields/utils.py +++ b/imagekit/models/fields/utils.py @@ -1,4 +1,5 @@ -from .files import ImageSpecFieldFile +from ...files import ImageSpecFile +from django.db.models.fields.files import ImageField class ImageSpecFileDescriptor(object): @@ -10,8 +11,26 @@ class ImageSpecFileDescriptor(object): if instance is None: return self.field else: - img_spec_file = ImageSpecFieldFile(instance, self.field, - self.attname) + field_name = getattr(self.field, 'image_field', None) + if field_name: + source_file = getattr(instance, field_name) + else: + image_fields = [getattr(instance, f.attname) for f in \ + instance.__class__._meta.fields if \ + isinstance(f, ImageField)] + if len(image_fields) == 0: + raise Exception('%s does not define any ImageFields, so your' \ + ' %s ImageSpecField has no image to act on.' % \ + (instance.__class__.__name__, self.attname)) + elif len(image_fields) > 1: + raise Exception('%s defines multiple ImageFields, but you' \ + ' have not specified an image_field for your %s' \ + ' ImageSpecField.' % (instance.__class__.__name__, + self.attname)) + else: + source_file = image_fields[0] + img_spec_file = ImageSpecFile(self.field.spec, source_file, + self.attname) instance.__dict__[self.attname] = img_spec_file return img_spec_file diff --git a/imagekit/utils.py b/imagekit/utils.py index 35671ac..d33d83d 100644 --- a/imagekit/utils.py +++ b/imagekit/utils.py @@ -440,6 +440,12 @@ class SpecWrapper(object): self.autoconvert = getattr(spec, 'autoconvert', True) self.storage = getattr(spec, 'storage', None) self.image_cache_backend = getattr(spec, 'image_cache_backend', None) + # TODO: get_hash default return value. + self.get_hash = getattr(spec, 'get_hash', lambda f: None) + # TODO: generate_filename default return value. + self.generate_filename = getattr(spec, 'generate_filename', lambda f: None) + # TODO: generate_file default return value. + self.generate_file = getattr(spec, 'generate_file', lambda f: None) if not self.image_cache_backend: from .imagecache.backends import get_default_image_cache_backend From 80b723b510ee5253e62cbac1b244a422c8e852de Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Tue, 16 Oct 2012 21:31:47 -0400 Subject: [PATCH 042/213] Move IKContentFile to imagekit.files --- imagekit/files.py | 34 ++++++++++++++++++++++++++++++---- imagekit/specs/__init__.py | 6 +++--- imagekit/utils.py | 27 --------------------------- 3 files changed, 33 insertions(+), 34 deletions(-) diff --git a/imagekit/files.py b/imagekit/files.py index b3b543c..98560fc 100644 --- a/imagekit/files.py +++ b/imagekit/files.py @@ -1,8 +1,9 @@ -import os - +from django.core.files.base import ContentFile from django.db.models.fields.files import ImageFieldFile - -from .utils import SpecWrapper, suggest_extension +from django.utils.encoding import smart_str, smart_unicode +import os +from .utils import (SpecWrapper, suggest_extension, format_to_mimetype, + extension_to_mimetype) class ImageSpecFile(ImageFieldFile): @@ -55,3 +56,28 @@ class ImageSpecFile(ImageFieldFile): def generate(self, save=True): return self.spec.generate_file(self.name, self.source_file, save) + + +class IKContentFile(ContentFile): + """ + Wraps a ContentFile in a file-like object with a filename and a + content_type. A PIL image format can be optionally be provided as a content + type hint. + + """ + def __init__(self, filename, content, format=None): + self.file = ContentFile(content) + self.file.name = filename + mimetype = getattr(self.file, 'content_type', None) + if format and not mimetype: + mimetype = format_to_mimetype(format) + if not mimetype: + ext = os.path.splitext(filename or '')[1] + mimetype = extension_to_mimetype(ext) + self.file.content_type = mimetype + + def __str__(self): + return smart_str(self.file.name or '') + + def __unicode__(self): + return smart_unicode(self.file.name or u'') diff --git a/imagekit/specs/__init__.py b/imagekit/specs/__init__.py index d6271d7..5d71228 100644 --- a/imagekit/specs/__init__.py +++ b/imagekit/specs/__init__.py @@ -4,13 +4,13 @@ import os import pickle from .signals import source_created, source_changed, source_deleted from ..exceptions import UnknownExtensionError, AlreadyRegistered, NotRegistered -from ..files import ImageSpecFile +from ..files import ImageSpecFile, IKContentFile from ..imagecache.backends import get_default_image_cache_backend from ..imagecache.strategies import StrategyWrapper from ..lib import StringIO from ..processors import ProcessorPipeline -from ..utils import (open_image, extension_to_format, IKContentFile, - img_to_fobj, suggest_extension) +from ..utils import (open_image, extension_to_format, img_to_fobj, + suggest_extension) class SpecRegistry(object): diff --git a/imagekit/utils.py b/imagekit/utils.py index d33d83d..cdd9b47 100644 --- a/imagekit/utils.py +++ b/imagekit/utils.py @@ -4,9 +4,7 @@ import sys import types from django.core.exceptions import ImproperlyConfigured -from django.core.files.base import ContentFile from django.db.models.loading import cache -from django.utils.encoding import smart_str, smart_unicode from django.utils.functional import wraps from django.utils.importlib import import_module @@ -18,31 +16,6 @@ RGBA_TRANSPARENCY_FORMATS = ['PNG'] PALETTE_TRANSPARENCY_FORMATS = ['PNG', 'GIF'] -class IKContentFile(ContentFile): - """ - Wraps a ContentFile in a file-like object with a filename and a - content_type. A PIL image format can be optionally be provided as a content - type hint. - - """ - def __init__(self, filename, content, format=None): - self.file = ContentFile(content) - self.file.name = filename - mimetype = getattr(self.file, 'content_type', None) - if format and not mimetype: - mimetype = format_to_mimetype(format) - if not mimetype: - ext = os.path.splitext(filename or '')[1] - mimetype = extension_to_mimetype(ext) - self.file.content_type = mimetype - - def __str__(self): - return smart_str(self.file.name or '') - - def __unicode__(self): - return smart_unicode(self.file.name or u'') - - def img_to_fobj(img, format, autoconvert=True, **options): return save_image(img, StringIO(), format, options, autoconvert) From ca391fbf0ab019d3463a3a8a4c477da6f9e578f8 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Tue, 16 Oct 2012 21:36:40 -0400 Subject: [PATCH 043/213] Change cache prefix --- imagekit/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/imagekit/conf.py b/imagekit/conf.py index d245b50..e99cfcd 100644 --- a/imagekit/conf.py +++ b/imagekit/conf.py @@ -6,7 +6,7 @@ class ImageKitConf(AppConf): DEFAULT_IMAGE_CACHE_BACKEND = 'imagekit.imagecache.backends.Simple' CACHE_BACKEND = None CACHE_DIR = 'CACHE/images' - CACHE_PREFIX = 'ik-' + CACHE_PREFIX = 'imagekit:' DEFAULT_IMAGE_CACHE_STRATEGY = 'imagekit.imagecache.strategies.Pessimistic' def configure_cache_backend(self, value): From 7f6e97a37a55ad0ff8a0e9210f8073a809d036fb Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Tue, 16 Oct 2012 21:46:23 -0400 Subject: [PATCH 044/213] Rename Pessimistic to JustInTime --- imagekit/conf.py | 2 +- imagekit/imagecache/strategies.py | 15 +++++---------- 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/imagekit/conf.py b/imagekit/conf.py index e99cfcd..d183f00 100644 --- a/imagekit/conf.py +++ b/imagekit/conf.py @@ -7,7 +7,7 @@ class ImageKitConf(AppConf): CACHE_BACKEND = None CACHE_DIR = 'CACHE/images' CACHE_PREFIX = 'imagekit:' - DEFAULT_IMAGE_CACHE_STRATEGY = 'imagekit.imagecache.strategies.Pessimistic' + DEFAULT_IMAGE_CACHE_STRATEGY = 'imagekit.imagecache.strategies.JustInTime' def configure_cache_backend(self, value): if value is None: diff --git a/imagekit/imagecache/strategies.py b/imagekit/imagecache/strategies.py index e44929a..f134673 100644 --- a/imagekit/imagecache/strategies.py +++ b/imagekit/imagecache/strategies.py @@ -2,26 +2,21 @@ from .actions import validate_now, clear_now from ..utils import get_singleton -class Pessimistic(object): +class JustInTime(object): """ - A caching strategy that validates the file every time it's accessed. + A caching strategy that validates the file right before it's needed. """ def on_accessed(self, file): validate_now(file) - def on_source_deleted(self, file): - clear_now(file) - - def on_source_changed(self, file): - validate_now(file) - class Optimistic(object): """ - A caching strategy that validates when the source file changes and assumes - that the cached file will persist. + A caching strategy that acts immediately when the source file chages and + assumes that the cache files will not be removed (i.e. doesn't revalidate + on access). """ From 1452f04cdad6b931ee3fffd5ff5ab422ee76f2a7 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Tue, 16 Oct 2012 22:03:50 -0400 Subject: [PATCH 045/213] Remove unused parameter --- imagekit/models/fields/files.py | 3 +-- imagekit/specs/__init__.py | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/imagekit/models/fields/files.py b/imagekit/models/fields/files.py index f0fc542..6e5c489 100644 --- a/imagekit/models/fields/files.py +++ b/imagekit/models/fields/files.py @@ -4,6 +4,5 @@ from django.db.models.fields.files import ImageFieldFile class ProcessedImageFieldFile(ImageFieldFile): def save(self, name, content, save=True): new_filename = self.field.spec.generate_filename(self.instance, name) - img, content = self.field.spec.process_content(content, - new_filename, self) + img, content = self.field.spec.process_content(content, new_filename) return super(ProcessedImageFieldFile, self).save(name, content, save) diff --git a/imagekit/specs/__init__.py b/imagekit/specs/__init__.py index 5d71228..279af0a 100644 --- a/imagekit/specs/__init__.py +++ b/imagekit/specs/__init__.py @@ -119,7 +119,7 @@ class BaseImageSpec(object): return filename - def process_content(self, content, filename=None, source_file=None): + def process_content(self, content, filename=None): img = open_image(content) original_format = img.format @@ -214,7 +214,7 @@ class ImageSpec(BaseImageSpec): fp.seek(0) fp = StringIO(fp.read()) - img, content = self.process_content(fp, filename, source_file) + img, content = self.process_content(fp, filename) if save: storage = self.storage or source_file.storage From 8ef1437beabc75ec01f0c12c4e58a31916f47730 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Tue, 16 Oct 2012 22:06:24 -0400 Subject: [PATCH 046/213] Remove apply() method --- imagekit/specs/__init__.py | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/imagekit/specs/__init__.py b/imagekit/specs/__init__.py index 279af0a..a1f331b 100644 --- a/imagekit/specs/__init__.py +++ b/imagekit/specs/__init__.py @@ -184,18 +184,9 @@ class ImageSpec(BaseImageSpec): self.image_cache_backend = image_cache_backend or self.image_cache_backend or get_default_image_cache_backend() self.image_cache_strategy = StrategyWrapper(image_cache_strategy or self.image_cache_strategy) - # TODO: Can we come up with a better name for this? "process" may cause confusion with processors' process() - def apply(self, source_file): - """ - Creates a file object that represents the combination of a spec and - source file. - - """ - return ImageSpecFile(self, source_file) - # TODO: I don't like this interface. Is there a standard Python one? pubsub? def _handle_source_event(self, event_name, source_file): - file = self.apply(source_file) + file = ImageSpecFile(self, source_file) self.image_cache_strategy.invoke_callback('on_%s' % event_name, file) def generate_file(self, filename, source_file, save=True): From 37e0de30696aff3d0628bd3270e19244d9450556 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Tue, 16 Oct 2012 22:10:52 -0400 Subject: [PATCH 047/213] Move signals module --- imagekit/{specs => }/signals.py | 0 imagekit/specs/__init__.py | 2 +- imagekit/specs/sources.py | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) rename imagekit/{specs => }/signals.py (100%) diff --git a/imagekit/specs/signals.py b/imagekit/signals.py similarity index 100% rename from imagekit/specs/signals.py rename to imagekit/signals.py diff --git a/imagekit/specs/__init__.py b/imagekit/specs/__init__.py index a1f331b..5a63b73 100644 --- a/imagekit/specs/__init__.py +++ b/imagekit/specs/__init__.py @@ -2,13 +2,13 @@ from django.conf import settings from hashlib import md5 import os import pickle -from .signals import source_created, source_changed, source_deleted from ..exceptions import UnknownExtensionError, AlreadyRegistered, NotRegistered from ..files import ImageSpecFile, IKContentFile from ..imagecache.backends import get_default_image_cache_backend from ..imagecache.strategies import StrategyWrapper from ..lib import StringIO from ..processors import ProcessorPipeline +from ..signals import source_created, source_changed, source_deleted from ..utils import (open_image, extension_to_format, img_to_fobj, suggest_extension) diff --git a/imagekit/specs/sources.py b/imagekit/specs/sources.py index 42f8c85..958fa61 100644 --- a/imagekit/specs/sources.py +++ b/imagekit/specs/sources.py @@ -1,6 +1,6 @@ from django.db.models.signals import post_init, post_save, post_delete from django.utils.functional import wraps -from .signals import source_created, source_changed, source_deleted +from ..signals import source_created, source_changed, source_deleted def ik_model_receiver(fn): From 3308c92a713e32d55bfce38c7e9671d7eb2422c9 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Tue, 16 Oct 2012 22:23:14 -0400 Subject: [PATCH 048/213] Remove SpecWrapper class We don't need it now that we have an ImageSpec class --- imagekit/files.py | 5 +---- imagekit/utils.py | 25 ------------------------- 2 files changed, 1 insertion(+), 29 deletions(-) diff --git a/imagekit/files.py b/imagekit/files.py index 98560fc..e3c1b87 100644 --- a/imagekit/files.py +++ b/imagekit/files.py @@ -2,16 +2,13 @@ from django.core.files.base import ContentFile from django.db.models.fields.files import ImageFieldFile from django.utils.encoding import smart_str, smart_unicode import os -from .utils import (SpecWrapper, suggest_extension, format_to_mimetype, +from .utils import (suggest_extension, format_to_mimetype, extension_to_mimetype) class ImageSpecFile(ImageFieldFile): def __init__(self, spec, source_file, spec_id): - spec = SpecWrapper(spec) - self.storage = spec.storage or source_file.storage - self.spec = spec self.source_file = source_file self.spec_id = spec_id diff --git a/imagekit/utils.py b/imagekit/utils.py index cdd9b47..1c76977 100644 --- a/imagekit/utils.py +++ b/imagekit/utils.py @@ -398,28 +398,3 @@ def autodiscover(): # attempting to import it, otherwise we want it to bubble up. if module_has_submodule(mod, 'imagespecs'): raise - - -class SpecWrapper(object): - """ - Wraps a user-defined spec object so we can access properties that don't - exist without errors. - - """ - def __init__(self, spec): - self.processors = getattr(spec, 'processors', None) - self.format = getattr(spec, 'format', None) - self.options = getattr(spec, 'options', None) - self.autoconvert = getattr(spec, 'autoconvert', True) - self.storage = getattr(spec, 'storage', None) - self.image_cache_backend = getattr(spec, 'image_cache_backend', None) - # TODO: get_hash default return value. - self.get_hash = getattr(spec, 'get_hash', lambda f: None) - # TODO: generate_filename default return value. - self.generate_filename = getattr(spec, 'generate_filename', lambda f: None) - # TODO: generate_file default return value. - self.generate_file = getattr(spec, 'generate_file', lambda f: None) - - if not self.image_cache_backend: - from .imagecache.backends import get_default_image_cache_backend - self.image_cache_backend = get_default_image_cache_backend() From a4ef8aa681842b6513510712d9a042b2827bc714 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Tue, 16 Oct 2012 22:30:36 -0400 Subject: [PATCH 049/213] Add before_access signal --- imagekit/files.py | 5 +++-- imagekit/imagecache/strategies.py | 9 ++++++++- imagekit/signals.py | 1 + imagekit/specs/__init__.py | 7 ++++++- 4 files changed, 18 insertions(+), 4 deletions(-) diff --git a/imagekit/files.py b/imagekit/files.py index e3c1b87..a443329 100644 --- a/imagekit/files.py +++ b/imagekit/files.py @@ -2,6 +2,7 @@ from django.core.files.base import ContentFile from django.db.models.fields.files import ImageFieldFile from django.utils.encoding import smart_str, smart_unicode import os +from .signals import before_access from .utils import (suggest_extension, format_to_mimetype, extension_to_mimetype) @@ -18,11 +19,11 @@ class ImageSpecFile(ImageFieldFile): @property def url(self): - self.validate() + before_access.send(sender=self, spec=self.spec, file=self) return super(ImageFieldFile, self).url def _get_file(self): - self.validate() + before_access.send(sender=self, spec=self.spec, file=self) return super(ImageFieldFile, self).file file = property(_get_file, ImageFieldFile._set_file, ImageFieldFile._del_file) diff --git a/imagekit/imagecache/strategies.py b/imagekit/imagecache/strategies.py index f134673..3eca747 100644 --- a/imagekit/imagecache/strategies.py +++ b/imagekit/imagecache/strategies.py @@ -8,7 +8,7 @@ class JustInTime(object): """ - def on_accessed(self, file): + def before_access(self, file): validate_now(file) @@ -50,3 +50,10 @@ class StrategyWrapper(object): func = getattr(self._wrapped, name, None) if func: func(*args, **kwargs) + + def __unicode__(self): + return unicode(self._wrapped) + + + def __str__(self): + return str(self._wrapped) diff --git a/imagekit/signals.py b/imagekit/signals.py index 7c0888c..4c7aefa 100644 --- a/imagekit/signals.py +++ b/imagekit/signals.py @@ -1,5 +1,6 @@ from django.dispatch import Signal +before_access = Signal() source_created = Signal(providing_args=[]) source_changed = Signal() source_deleted = Signal() diff --git a/imagekit/specs/__init__.py b/imagekit/specs/__init__.py index 5a63b73..41f95c5 100644 --- a/imagekit/specs/__init__.py +++ b/imagekit/specs/__init__.py @@ -8,7 +8,8 @@ from ..imagecache.backends import get_default_image_cache_backend from ..imagecache.strategies import StrategyWrapper from ..lib import StringIO from ..processors import ProcessorPipeline -from ..signals import source_created, source_changed, source_deleted +from ..signals import (before_access, source_created, source_changed, + source_deleted) from ..utils import (open_image, extension_to_format, img_to_fobj, suggest_extension) @@ -35,6 +36,7 @@ class SpecRegistry(object): self._sources = {} for signal in self._source_signals: signal.connect(self.source_receiver) + before_access.connect(self.before_access_receiver) def register(self, id, spec): if id in self._specs: @@ -66,6 +68,9 @@ class SpecRegistry(object): self._sources[source] = set() self._sources[source].add(spec_id) + def before_access_receiver(self, sender, spec, file, **kwargs): + spec.image_cache_strategy.invoke_callback('before_access', file) + def source_receiver(self, sender, source_file, signal, info, **kwargs): """ Redirects signals dispatched on sources to the appropriate specs. From 13b59ef85e28901aad5a78b6b185511fc3b7a3c4 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Tue, 16 Oct 2012 22:33:17 -0400 Subject: [PATCH 050/213] Reorder methods --- imagekit/files.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/imagekit/files.py b/imagekit/files.py index a443329..0079f36 100644 --- a/imagekit/files.py +++ b/imagekit/files.py @@ -28,15 +28,6 @@ class ImageSpecFile(ImageFieldFile): file = property(_get_file, ImageFieldFile._set_file, ImageFieldFile._del_file) - def clear(self): - return self.spec.image_cache_backend.clear(self) - - def invalidate(self): - return self.spec.image_cache_backend.invalidate(self) - - def validate(self): - return self.spec.image_cache_backend.validate(self) - @property def name(self): name = self.spec.generate_filename(self.source_file) @@ -52,6 +43,15 @@ class ImageSpecFile(ImageFieldFile): [filepath, new_name] return os.path.join(*cache_filename) + def clear(self): + return self.spec.image_cache_backend.clear(self) + + def invalidate(self): + return self.spec.image_cache_backend.invalidate(self) + + def validate(self): + return self.spec.image_cache_backend.validate(self) + def generate(self, save=True): return self.spec.generate_file(self.name, self.source_file, save) From fdc08aeeb0fb466d38c78c11661fa909c4491980 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Tue, 16 Oct 2012 22:52:01 -0400 Subject: [PATCH 051/213] Don't extend ImageFieldFile This file isn't just for fields anymore, so we want to get rid of all the ORM stuff. --- imagekit/files.py | 79 ++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 67 insertions(+), 12 deletions(-) diff --git a/imagekit/files.py b/imagekit/files.py index 0079f36..3499643 100644 --- a/imagekit/files.py +++ b/imagekit/files.py @@ -1,5 +1,5 @@ -from django.core.files.base import ContentFile -from django.db.models.fields.files import ImageFieldFile +from django.core.files.base import ContentFile, File +from django.core.files.images import ImageFile from django.utils.encoding import smart_str, smart_unicode import os from .signals import before_access @@ -7,7 +7,69 @@ from .utils import (suggest_extension, format_to_mimetype, extension_to_mimetype) -class ImageSpecFile(ImageFieldFile): +class BaseImageSpecFile(File): + """ + This class contains all of the methods we need from + django.db.models.fields.files.FieldFile, but with the model stuff ripped + out. It's only extended by one class, but we keep it separate for + organizational reasons. + + """ + + def __init__(self): + pass + + def _require_file(self): + if not self: + raise ValueError() + + def _get_file(self): + self._require_file() + if not hasattr(self, '_file') or self._file is None: + self._file = self.storage.open(self.name, 'rb') + return self._file + + def _set_file(self, file): + self._file = file + + def _del_file(self): + del self._file + + file = property(_get_file, _set_file, _del_file) + + def _get_path(self): + self._require_file() + return self.storage.path(self.name) + path = property(_get_path) + + def _get_url(self): + self._require_file() + return self.storage.url(self.name) + url = property(_get_url) + + def _get_size(self): + self._require_file() + if not self._committed: + return self.file.size + return self.storage.size(self.name) + size = property(_get_size) + + def open(self, mode='rb'): + self._require_file() + self.file.open(mode) + + def _get_closed(self): + file = getattr(self, '_file', None) + return file is None or file.closed + closed = property(_get_closed) + + def close(self): + file = getattr(self, '_file', None) + if file is not None: + file.close() + + +class ImageSpecFile(ImageFile, BaseImageSpecFile): def __init__(self, spec, source_file, spec_id): self.storage = spec.storage or source_file.storage self.spec = spec @@ -17,16 +79,9 @@ class ImageSpecFile(ImageFieldFile): def get_hash(self): return self.spec.get_hash(self.source_file) - @property - def url(self): + def _require_file(self): before_access.send(sender=self, spec=self.spec, file=self) - return super(ImageFieldFile, self).url - - def _get_file(self): - before_access.send(sender=self, spec=self.spec, file=self) - return super(ImageFieldFile, self).file - - file = property(_get_file, ImageFieldFile._set_file, ImageFieldFile._del_file) + return super(ImageSpecFile, self)._require_file() @property def name(self): From 5ca8b7f4ba990387bab58c36c4fe5e8fec2aa923 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Tue, 16 Oct 2012 22:59:40 -0400 Subject: [PATCH 052/213] Rename process_file; don't return image object This function will just be used to apply the spec and create a new file; useful for transforming images in views, etc. --- imagekit/models/fields/files.py | 2 +- imagekit/specs/__init__.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/imagekit/models/fields/files.py b/imagekit/models/fields/files.py index 6e5c489..f7f887a 100644 --- a/imagekit/models/fields/files.py +++ b/imagekit/models/fields/files.py @@ -4,5 +4,5 @@ from django.db.models.fields.files import ImageFieldFile class ProcessedImageFieldFile(ImageFieldFile): def save(self, name, content, save=True): new_filename = self.field.spec.generate_filename(self.instance, name) - img, content = self.field.spec.process_content(content, new_filename) + content = self.field.spec.apply(content, new_filename) return super(ProcessedImageFieldFile, self).save(name, content, save) diff --git a/imagekit/specs/__init__.py b/imagekit/specs/__init__.py index 41f95c5..e166218 100644 --- a/imagekit/specs/__init__.py +++ b/imagekit/specs/__init__.py @@ -124,7 +124,7 @@ class BaseImageSpec(object): return filename - def process_content(self, content, filename=None): + def apply(self, content, filename=None): img = open_image(content) original_format = img.format @@ -148,7 +148,7 @@ class BaseImageSpec(object): imgfile = img_to_fobj(img, format, **options) content = IKContentFile(filename, imgfile.read(), format=format) - return img, content + return content class ImageSpec(BaseImageSpec): @@ -210,7 +210,7 @@ class ImageSpec(BaseImageSpec): fp.seek(0) fp = StringIO(fp.read()) - img, content = self.process_content(fp, filename) + content = self.apply(fp, filename) if save: storage = self.storage or source_file.storage From 738bbfa9a19ac286f985fba4ab2e06ed2f1ee397 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Tue, 16 Oct 2012 23:38:44 -0400 Subject: [PATCH 053/213] Move cache file naming into ImageSpecFile --- imagekit/files.py | 27 +++++++++++----------- imagekit/models/fields/__init__.py | 37 +++++++++++++++--------------- imagekit/models/fields/files.py | 10 +++++--- imagekit/models/fields/utils.py | 3 +-- imagekit/specs/__init__.py | 13 ----------- 5 files changed, 40 insertions(+), 50 deletions(-) diff --git a/imagekit/files.py b/imagekit/files.py index 3499643..8b417c4 100644 --- a/imagekit/files.py +++ b/imagekit/files.py @@ -1,3 +1,4 @@ +from django.conf import settings from django.core.files.base import ContentFile, File from django.core.files.images import ImageFile from django.utils.encoding import smart_str, smart_unicode @@ -70,11 +71,10 @@ class BaseImageSpecFile(File): class ImageSpecFile(ImageFile, BaseImageSpecFile): - def __init__(self, spec, source_file, spec_id): + def __init__(self, spec, source_file): self.storage = spec.storage or source_file.storage self.spec = spec self.source_file = source_file - self.spec_id = spec_id def get_hash(self): return self.spec.get_hash(self.source_file) @@ -85,18 +85,17 @@ class ImageSpecFile(ImageFile, BaseImageSpecFile): @property def name(self): - name = self.spec.generate_filename(self.source_file) - if name is not None: - return name - else: - source_filename = self.source_file.name - filepath, basename = os.path.split(source_filename) - filename = os.path.splitext(basename)[0] - extension = suggest_extension(source_filename, self.spec.format) - new_name = '%s%s' % (filename, extension) - cache_filename = ['cache', 'iktt'] + self.spec_id.split(':') + \ - [filepath, new_name] - return os.path.join(*cache_filename) + source_filename = self.source_file.name + filename = None + if source_filename: + hash = self.spec.get_hash(self.source_file) + ext = suggest_extension(source_filename, self.spec.format) + filename = os.path.normpath(os.path.join( + settings.IMAGEKIT_CACHE_DIR, + os.path.splitext(source_filename)[0], + '%s%s' % (hash, ext))) + + return filename def clear(self): return self.spec.image_cache_backend.clear(self) diff --git a/imagekit/models/fields/__init__.py b/imagekit/models/fields/__init__.py index 148212c..ecc6bbe 100644 --- a/imagekit/models/fields/__init__.py +++ b/imagekit/models/fields/__init__.py @@ -9,7 +9,20 @@ from ...specs import SpecHost from ...specs.sources import ImageFieldSpecSource -class ImageSpecField(SpecHost): +class SpecHostField(SpecHost): + def set_spec_id(self, cls, name): + # Generate a spec_id to register the spec with. The default spec id is + # ":_" + if not getattr(self, 'spec_id', None): + spec_id = (u'%s:%s_%s' % (cls._meta.app_label, + cls._meta.object_name, name)).lower() + + # Register the spec with the id. This allows specs to be overridden + # later, from outside of the model definition. + super(SpecHostField, self).set_spec_id(spec_id) + + +class ImageSpecField(SpecHostField): """ The heart and soul of the ImageKit library, ImageSpecField allows you to add variants of uploaded images to your models. @@ -34,23 +47,14 @@ class ImageSpecField(SpecHost): def contribute_to_class(self, cls, name): setattr(cls, name, ImageSpecFileDescriptor(self, name)) - - # Generate a spec_id to register the spec with. The default spec id is - # ":_" - if not getattr(self, 'spec_id', None): - self.spec_id = (u'%s:%s_%s' % (cls._meta.app_label, - cls._meta.object_name, name)).lower() - - # Register the spec with the id. This allows specs to be overridden - # later, from outside of the model definition. - self.set_spec_id(self.spec_id) + self.set_spec_id(cls, name) # Add the model and field as a source for this spec id specs.registry.add_source(ImageFieldSpecSource(cls, self.image_field), self.spec_id) -class ProcessedImageField(models.ImageField, SpecHost): +class ProcessedImageField(models.ImageField, SpecHostField): """ ProcessedImageField is an ImageField that runs processors on the uploaded image *before* saving it to storage. This is in contrast to specs, which @@ -76,12 +80,9 @@ class ProcessedImageField(models.ImageField, SpecHost): models.ImageField.__init__(self, verbose_name, name, width_field, height_field, **kwargs) - def get_filename(self, filename): - filename = os.path.normpath(self.storage.get_valid_name( - os.path.basename(filename))) - name, ext = os.path.splitext(filename) - ext = suggest_extension(filename, self.spec.format) - return u'%s%s' % (name, ext) + def contribute_to_class(self, cls, name): + self.set_spec_id(cls, name) + return super(ProcessedImageField, self).contribute_to_class(cls, name) try: diff --git a/imagekit/models/fields/files.py b/imagekit/models/fields/files.py index f7f887a..3c9ffd6 100644 --- a/imagekit/models/fields/files.py +++ b/imagekit/models/fields/files.py @@ -1,8 +1,12 @@ from django.db.models.fields.files import ImageFieldFile +import os +from ...utils import suggest_extension class ProcessedImageFieldFile(ImageFieldFile): def save(self, name, content, save=True): - new_filename = self.field.spec.generate_filename(self.instance, name) - content = self.field.spec.apply(content, new_filename) - return super(ProcessedImageFieldFile, self).save(name, content, save) + filename, ext = os.path.splitext(name) + ext = suggest_extension(name, self.field.spec.format) + new_name = '%s%s' % (filename, ext) + content = self.field.spec.apply(content, new_name) + return super(ProcessedImageFieldFile, self).save(new_name, content, save) diff --git a/imagekit/models/fields/utils.py b/imagekit/models/fields/utils.py index dab6170..ce55a18 100644 --- a/imagekit/models/fields/utils.py +++ b/imagekit/models/fields/utils.py @@ -29,8 +29,7 @@ class ImageSpecFileDescriptor(object): self.attname)) else: source_file = image_fields[0] - img_spec_file = ImageSpecFile(self.field.spec, source_file, - self.attname) + img_spec_file = ImageSpecFile(self.field.spec, source_file) instance.__dict__[self.attname] = img_spec_file return img_spec_file diff --git a/imagekit/specs/__init__.py b/imagekit/specs/__init__.py index e166218..883b4e0 100644 --- a/imagekit/specs/__init__.py +++ b/imagekit/specs/__init__.py @@ -111,19 +111,6 @@ class BaseImageSpec(object): str(self.autoconvert), ]).encode('utf-8')).hexdigest() - def generate_filename(self, source_file): - source_filename = source_file.name - filename = None - if source_filename: - hash = self.get_hash(source_file) - extension = suggest_extension(source_filename, self.format) - filename = os.path.normpath(os.path.join( - settings.IMAGEKIT_CACHE_DIR, - os.path.splitext(source_filename)[0], - '%s%s' % (hash, extension))) - - return filename - def apply(self, content, filename=None): img = open_image(content) original_format = img.format From df8905f7e4ae2ec2d7e2032de9112c0576fe2ef2 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Tue, 16 Oct 2012 23:39:10 -0400 Subject: [PATCH 054/213] Remove unused imports --- imagekit/models/fields/__init__.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/imagekit/models/fields/__init__.py b/imagekit/models/fields/__init__.py index ecc6bbe..0cb6443 100644 --- a/imagekit/models/fields/__init__.py +++ b/imagekit/models/fields/__init__.py @@ -1,10 +1,7 @@ -import os - from django.db import models from .files import ProcessedImageFieldFile from .utils import ImageSpecFileDescriptor from ... import specs -from ...utils import suggest_extension from ...specs import SpecHost from ...specs.sources import ImageFieldSpecSource From 5c6d1aef5dcd48ae5ada88d685372673e8092657 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Tue, 16 Oct 2012 23:51:26 -0400 Subject: [PATCH 055/213] Rename ImageSpecFile You can generate other "spec" files (using apply will get you one). This one is for saving cache files and its name should reflect that. --- imagekit/files.py | 6 +++--- imagekit/models/fields/utils.py | 8 ++++---- imagekit/specs/__init__.py | 4 ++-- imagekit/templatetags/imagekit_tags.py | 8 ++++---- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/imagekit/files.py b/imagekit/files.py index 8b417c4..df4055e 100644 --- a/imagekit/files.py +++ b/imagekit/files.py @@ -8,7 +8,7 @@ from .utils import (suggest_extension, format_to_mimetype, extension_to_mimetype) -class BaseImageSpecFile(File): +class BaseIKFile(File): """ This class contains all of the methods we need from django.db.models.fields.files.FieldFile, but with the model stuff ripped @@ -70,7 +70,7 @@ class BaseImageSpecFile(File): file.close() -class ImageSpecFile(ImageFile, BaseImageSpecFile): +class ImageSpecCacheFile(ImageFile, BaseIKFile): def __init__(self, spec, source_file): self.storage = spec.storage or source_file.storage self.spec = spec @@ -81,7 +81,7 @@ class ImageSpecFile(ImageFile, BaseImageSpecFile): def _require_file(self): before_access.send(sender=self, spec=self.spec, file=self) - return super(ImageSpecFile, self)._require_file() + return super(ImageSpecCacheFile, self)._require_file() @property def name(self): diff --git a/imagekit/models/fields/utils.py b/imagekit/models/fields/utils.py index ce55a18..9490966 100644 --- a/imagekit/models/fields/utils.py +++ b/imagekit/models/fields/utils.py @@ -1,4 +1,4 @@ -from ...files import ImageSpecFile +from ...files import ImageSpecCacheFile from django.db.models.fields.files import ImageField @@ -29,9 +29,9 @@ class ImageSpecFileDescriptor(object): self.attname)) else: source_file = image_fields[0] - img_spec_file = ImageSpecFile(self.field.spec, source_file) - instance.__dict__[self.attname] = img_spec_file - return img_spec_file + file = ImageSpecCacheFile(self.field.spec, source_file) + instance.__dict__[self.attname] = file + return file def __set__(self, instance, value): instance.__dict__[self.attname] = value diff --git a/imagekit/specs/__init__.py b/imagekit/specs/__init__.py index 883b4e0..faf5594 100644 --- a/imagekit/specs/__init__.py +++ b/imagekit/specs/__init__.py @@ -3,7 +3,7 @@ from hashlib import md5 import os import pickle from ..exceptions import UnknownExtensionError, AlreadyRegistered, NotRegistered -from ..files import ImageSpecFile, IKContentFile +from ..files import ImageSpecCacheFile, IKContentFile from ..imagecache.backends import get_default_image_cache_backend from ..imagecache.strategies import StrategyWrapper from ..lib import StringIO @@ -178,7 +178,7 @@ class ImageSpec(BaseImageSpec): # TODO: I don't like this interface. Is there a standard Python one? pubsub? def _handle_source_event(self, event_name, source_file): - file = ImageSpecFile(self, source_file) + file = ImageSpecCacheFile(self, source_file) self.image_cache_strategy.invoke_callback('on_%s' % event_name, file) def generate_file(self, filename, source_file, save=True): diff --git a/imagekit/templatetags/imagekit_tags.py b/imagekit/templatetags/imagekit_tags.py index 938a53b..13230b4 100644 --- a/imagekit/templatetags/imagekit_tags.py +++ b/imagekit/templatetags/imagekit_tags.py @@ -1,6 +1,6 @@ from django import template from django.utils.safestring import mark_safe -from ..files import ImageSpecFile +from ..files import ImageSpecCacheFile from ..specs import spec_registry @@ -32,13 +32,13 @@ class SpecNode(template.Node): spec = spec_registry.get_spec(spec_id) if callable(spec): spec = spec() - spec_file = ImageSpecFileHtmlWrapper(ImageSpecFile(spec, source_image, spec_id)) + file = ImageSpecFileHtmlWrapper(ImageSpecCacheFile(spec, source_image, spec_id)) if self.variable_name is not None: variable_name = str(self.variable_name) - context[variable_name] = spec_file + context[variable_name] = file return '' - return spec_file + return file #@register.tag From 9b81acd10c96c4c0aeadfb9275f5cc3a4ae24576 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Tue, 16 Oct 2012 23:52:28 -0400 Subject: [PATCH 056/213] Don't pass removed argument --- imagekit/templatetags/imagekit_tags.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/imagekit/templatetags/imagekit_tags.py b/imagekit/templatetags/imagekit_tags.py index 13230b4..82055d6 100644 --- a/imagekit/templatetags/imagekit_tags.py +++ b/imagekit/templatetags/imagekit_tags.py @@ -32,7 +32,7 @@ class SpecNode(template.Node): spec = spec_registry.get_spec(spec_id) if callable(spec): spec = spec() - file = ImageSpecFileHtmlWrapper(ImageSpecCacheFile(spec, source_image, spec_id)) + file = ImageSpecFileHtmlWrapper(ImageSpecCacheFile(spec, source_image)) if self.variable_name is not None: variable_name = str(self.variable_name) context[variable_name] = file From a93832626a90471ab6c9efb44d461b355dce029c Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Tue, 16 Oct 2012 23:54:53 -0400 Subject: [PATCH 057/213] Return string from render method ...instead of wrapping the file with an object that has a __unicode__ method. If you want the actual file, you should use the assignment form. --- imagekit/templatetags/imagekit_tags.py | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/imagekit/templatetags/imagekit_tags.py b/imagekit/templatetags/imagekit_tags.py index 82055d6..9468a76 100644 --- a/imagekit/templatetags/imagekit_tags.py +++ b/imagekit/templatetags/imagekit_tags.py @@ -7,17 +7,6 @@ from ..specs import spec_registry register = template.Library() -class ImageSpecFileHtmlWrapper(object): - def __init__(self, image_spec_file): - self._image_spec_file = image_spec_file - - def __getattr__(self, name): - return getattr(self._image_spec_file, name) - - def __unicode__(self): - return mark_safe(u'' % self.url) - - class SpecNode(template.Node): def __init__(self, spec_id, source_image, variable_name=None): self.spec_id = spec_id @@ -32,13 +21,13 @@ class SpecNode(template.Node): spec = spec_registry.get_spec(spec_id) if callable(spec): spec = spec() - file = ImageSpecFileHtmlWrapper(ImageSpecCacheFile(spec, source_image)) + file = ImageSpecCacheFile(spec, source_image) if self.variable_name is not None: variable_name = str(self.variable_name) context[variable_name] = file return '' - return file + return mark_safe(u'' % file.url) #@register.tag From 63ad9e442191ebff7ebb40d607a78fb0f9a181a4 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Tue, 16 Oct 2012 23:59:13 -0400 Subject: [PATCH 058/213] Remove registration methods from template tag The registry isn't just for template tags anymore. --- imagekit/specs/__init__.py | 13 +++++++++++++ imagekit/templatetags/imagekit_tags.py | 17 ----------------- 2 files changed, 13 insertions(+), 17 deletions(-) diff --git a/imagekit/specs/__init__.py b/imagekit/specs/__init__.py index faf5594..f43c04b 100644 --- a/imagekit/specs/__init__.py +++ b/imagekit/specs/__init__.py @@ -265,3 +265,16 @@ class SpecHost(object): registry = SpecRegistry() + + +def register(id, spec=None): + if not spec: + def decorator(cls): + registry.register(id, cls) + return cls + return decorator + registry.register(id, spec) + + +def unregister(id, spec): + registry.unregister(id, spec) diff --git a/imagekit/templatetags/imagekit_tags.py b/imagekit/templatetags/imagekit_tags.py index 9468a76..2930d7b 100644 --- a/imagekit/templatetags/imagekit_tags.py +++ b/imagekit/templatetags/imagekit_tags.py @@ -69,20 +69,3 @@ def spec(parser, token): spec = spec_tag = register.tag(spec) - - -def _register_spec(id, spec=None): - if not spec: - def decorator(cls): - spec_registry.register(id, cls) - return cls - return decorator - spec_registry.register(id, spec) - - -def _unregister_spec(id, spec): - spec_registry.unregister(id, spec) - - -spec_tag.register = _register_spec -spec_tag.unregister = _unregister_spec From 4c4727fa9fb4d61752b64dbd5627a190048d481c Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Wed, 17 Oct 2012 00:00:12 -0400 Subject: [PATCH 059/213] Remove unused import --- imagekit/specs/__init__.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/imagekit/specs/__init__.py b/imagekit/specs/__init__.py index f43c04b..2a1b1ca 100644 --- a/imagekit/specs/__init__.py +++ b/imagekit/specs/__init__.py @@ -10,8 +10,7 @@ from ..lib import StringIO from ..processors import ProcessorPipeline from ..signals import (before_access, source_created, source_changed, source_deleted) -from ..utils import (open_image, extension_to_format, img_to_fobj, - suggest_extension) +from ..utils import open_image, extension_to_format, img_to_fobj class SpecRegistry(object): From 77b33f757cfe955f1c8948869025f43d4fe9302a Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Wed, 17 Oct 2012 00:00:53 -0400 Subject: [PATCH 060/213] Correct example --- imagekit/templatetags/imagekit_tags.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/imagekit/templatetags/imagekit_tags.py b/imagekit/templatetags/imagekit_tags.py index 2930d7b..c365c2f 100644 --- a/imagekit/templatetags/imagekit_tags.py +++ b/imagekit/templatetags/imagekit_tags.py @@ -38,7 +38,7 @@ def spec(parser, token): By default:: - {% spec 'myapp:thumbnail', mymodel.profile_image %} + {% spec 'myapp:thumbnail' mymodel.profile_image %} Generates an ````:: From 97d47c9c6cb781d9961b29fb35e62a31c2b852d2 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Wed, 17 Oct 2012 00:21:47 -0400 Subject: [PATCH 061/213] Remove generate_file. apply() does it all! There was a lot of garbage in that method and I don't know why. --- imagekit/files.py | 5 ++++- imagekit/specs/__init__.py | 24 ------------------------ 2 files changed, 4 insertions(+), 25 deletions(-) diff --git a/imagekit/files.py b/imagekit/files.py index df4055e..a52c8bf 100644 --- a/imagekit/files.py +++ b/imagekit/files.py @@ -107,7 +107,10 @@ class ImageSpecCacheFile(ImageFile, BaseIKFile): return self.spec.image_cache_backend.validate(self) def generate(self, save=True): - return self.spec.generate_file(self.name, self.source_file, save) + if self.source_file: # TODO: Should we error here or something if the source_file doesn't exist? + # Process the original image file. + content = self.spec.apply(self.source_file) + return self.storage.save(self.name, content) class IKContentFile(ContentFile): diff --git a/imagekit/specs/__init__.py b/imagekit/specs/__init__.py index 2a1b1ca..aa1126c 100644 --- a/imagekit/specs/__init__.py +++ b/imagekit/specs/__init__.py @@ -180,30 +180,6 @@ class ImageSpec(BaseImageSpec): file = ImageSpecCacheFile(self, source_file) self.image_cache_strategy.invoke_callback('on_%s' % event_name, file) - def generate_file(self, filename, source_file, save=True): - """ - Generates a new image file by processing the source file and returns - the content of the result, ready for saving. - - """ - if source_file: # TODO: Should we error here or something if the source_file doesn't exist? - # Process the original image file. - - try: - fp = source_file.storage.open(source_file.name) - except IOError: - return - fp.seek(0) - fp = StringIO(fp.read()) - - content = self.apply(fp, filename) - - if save: - storage = self.storage or source_file.storage - storage.save(filename, content) - - return content - class SpecHost(object): """ From a08edaca563833e367aaa2fc792983a5009c080b Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Wed, 17 Oct 2012 00:29:51 -0400 Subject: [PATCH 062/213] Handle storage in BaseIKFile --- imagekit/files.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/imagekit/files.py b/imagekit/files.py index a52c8bf..66680e0 100644 --- a/imagekit/files.py +++ b/imagekit/files.py @@ -17,8 +17,8 @@ class BaseIKFile(File): """ - def __init__(self): - pass + def __init__(self, storage): + self.storage = storage def _require_file(self): if not self: @@ -70,9 +70,10 @@ class BaseIKFile(File): file.close() -class ImageSpecCacheFile(ImageFile, BaseIKFile): +class ImageSpecCacheFile(BaseIKFile, ImageFile): def __init__(self, spec, source_file): - self.storage = spec.storage or source_file.storage + storage = spec.storage or source_file.storage + super(ImageSpecCacheFile, self).__init__(storage=storage) self.spec = spec self.source_file = source_file From 41fa1972129c104ed16e28612ceee55e0a0bdfd0 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Wed, 17 Oct 2012 00:31:38 -0400 Subject: [PATCH 063/213] Remove save kwarg--that's what generate() does! --- imagekit/files.py | 2 +- imagekit/imagecache/backends.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/imagekit/files.py b/imagekit/files.py index 66680e0..c7af4bb 100644 --- a/imagekit/files.py +++ b/imagekit/files.py @@ -107,7 +107,7 @@ class ImageSpecCacheFile(BaseIKFile, ImageFile): def validate(self): return self.spec.image_cache_backend.validate(self) - def generate(self, save=True): + def generate(self): if self.source_file: # TODO: Should we error here or something if the source_file doesn't exist? # Process the original image file. content = self.spec.apply(self.source_file) diff --git a/imagekit/imagecache/backends.py b/imagekit/imagecache/backends.py index c7339ab..922b1f4 100644 --- a/imagekit/imagecache/backends.py +++ b/imagekit/imagecache/backends.py @@ -67,7 +67,7 @@ class Simple(CachedValidationBackend): Generates a new image by running the processors on the source file. """ - file.generate(save=True) + file.generate() def invalidate(self, file): """ From ca324b7f529714f7c07779ae31fff92bba5f78c1 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Wed, 17 Oct 2012 00:32:31 -0400 Subject: [PATCH 064/213] Remove unused import --- imagekit/specs/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/imagekit/specs/__init__.py b/imagekit/specs/__init__.py index aa1126c..7aa8728 100644 --- a/imagekit/specs/__init__.py +++ b/imagekit/specs/__init__.py @@ -6,7 +6,6 @@ from ..exceptions import UnknownExtensionError, AlreadyRegistered, NotRegistered from ..files import ImageSpecCacheFile, IKContentFile from ..imagecache.backends import get_default_image_cache_backend from ..imagecache.strategies import StrategyWrapper -from ..lib import StringIO from ..processors import ProcessorPipeline from ..signals import (before_access, source_created, source_changed, source_deleted) From 806ebd75b63ae20e526d9fb0f44d9e05dd688768 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Wed, 17 Oct 2012 00:37:02 -0400 Subject: [PATCH 065/213] Don't return file from generate() The file is `self` --- imagekit/files.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/imagekit/files.py b/imagekit/files.py index c7af4bb..81db579 100644 --- a/imagekit/files.py +++ b/imagekit/files.py @@ -111,7 +111,7 @@ class ImageSpecCacheFile(BaseIKFile, ImageFile): if self.source_file: # TODO: Should we error here or something if the source_file doesn't exist? # Process the original image file. content = self.spec.apply(self.source_file) - return self.storage.save(self.name, content) + self.storage.save(self.name, content) class IKContentFile(ContentFile): From d8ce11e86e9d4fc01a667293d21a0d73f74545ca Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Wed, 17 Oct 2012 01:11:05 -0400 Subject: [PATCH 066/213] Log warning when filename doesn't match expected value --- imagekit/files.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/imagekit/files.py b/imagekit/files.py index 81db579..1f590f1 100644 --- a/imagekit/files.py +++ b/imagekit/files.py @@ -2,6 +2,7 @@ from django.conf import settings from django.core.files.base import ContentFile, File from django.core.files.images import ImageFile from django.utils.encoding import smart_str, smart_unicode +import logging import os from .signals import before_access from .utils import (suggest_extension, format_to_mimetype, @@ -111,7 +112,18 @@ class ImageSpecCacheFile(BaseIKFile, ImageFile): if self.source_file: # TODO: Should we error here or something if the source_file doesn't exist? # Process the original image file. content = self.spec.apply(self.source_file) - self.storage.save(self.name, content) + actual_name = self.storage.save(self.name, content) + + if actual_name != self.name: + # TODO: Use named logger? + logging.warning('The storage backend %s did not save the file' + ' with the requested name ("%s") and instead used' + ' "%s". This may be because a file already existed with' + ' the requested name. If so, you may have meant to call' + ' validate() instead of generate(), or there may be a' + ' race condition in the image cache backend %s. The' + ' saved file will not be used.' % (self.storage, + self.name, actual_name, self.spec.image_cache_backend)) class IKContentFile(ContentFile): From a265fd79e1c250d7e6a8215b06aff8732b1cfc19 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Wed, 17 Oct 2012 11:00:59 -0400 Subject: [PATCH 067/213] Correct example image URLs Generated files are stored in the media folder, not static --- README.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.rst b/README.rst index 9b38119..a616d75 100644 --- a/README.rst +++ b/README.rst @@ -94,7 +94,7 @@ Output: .. code-block:: html - A picture of me + A picture of me Not generating HTML image tags? No problem. The tag also functions as an assignment tag, providing access to the underlying file object: @@ -123,7 +123,7 @@ of applying a spec to another one of your model's fields: avatar_thumbnail = ImageSpecField(id='myapp:fancy_thumbnail', image_field='avatar') photo = Photo.objects.all()[0] - print photo.avatar_thumbnail.url # > /static/CACHE/ik/982d5af84cddddfd0fbf70892b4431e4.jpg + print photo.avatar_thumbnail.url # > /media/CACHE/ik/982d5af84cddddfd0fbf70892b4431e4.jpg print photo.avatar_thumbnail.width # > 100 Since defining a spec, registering it, and using it in a single model field is @@ -144,7 +144,7 @@ writing a subclass of ``ImageSpec``: image_field='avatar') photo = Photo.objects.all()[0] - print photo.avatar_thumbnail.url # > /static/CACHE/ik/982d5af84cddddfd0fbf70892b4431e4.jpg + print photo.avatar_thumbnail.url # > /media/CACHE/ik/982d5af84cddddfd0fbf70892b4431e4.jpg print photo.avatar_thumbnail.width # > 100 This has the exact same behavior as before, but the spec definition is inlined. @@ -164,7 +164,7 @@ a user-provided image without saving the original: upload_to='avatars') photo = Photo.objects.all()[0] - print photo.avatar_thumbnail.url # > /static/avatars/MY-avatar_3.jpg + print photo.avatar_thumbnail.url # > /media/avatars/MY-avatar_3.jpg print photo.avatar_thumbnail.width # > 100 Like with ``ImageSpecField``, the ``ProcessedImageField`` constructor also From ca1db05c4e600cce5d2b6f615b548c5fb2116279 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Wed, 17 Oct 2012 21:00:32 -0400 Subject: [PATCH 068/213] Use named logger --- imagekit/files.py | 6 ++---- imagekit/utils.py | 8 ++++++++ 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/imagekit/files.py b/imagekit/files.py index 1f590f1..12852cc 100644 --- a/imagekit/files.py +++ b/imagekit/files.py @@ -2,11 +2,10 @@ from django.conf import settings from django.core.files.base import ContentFile, File from django.core.files.images import ImageFile from django.utils.encoding import smart_str, smart_unicode -import logging import os from .signals import before_access from .utils import (suggest_extension, format_to_mimetype, - extension_to_mimetype) + extension_to_mimetype, get_logger) class BaseIKFile(File): @@ -115,8 +114,7 @@ class ImageSpecCacheFile(BaseIKFile, ImageFile): actual_name = self.storage.save(self.name, content) if actual_name != self.name: - # TODO: Use named logger? - logging.warning('The storage backend %s did not save the file' + get_logger().warning('The storage backend %s did not save the file' ' with the requested name ("%s") and instead used' ' "%s". This may be because a file already existed with' ' the requested name. If so, you may have meant to call' diff --git a/imagekit/utils.py b/imagekit/utils.py index 1c76977..282b739 100644 --- a/imagekit/utils.py +++ b/imagekit/utils.py @@ -1,3 +1,4 @@ +import logging import os import mimetypes import sys @@ -398,3 +399,10 @@ def autodiscover(): # attempting to import it, otherwise we want it to bubble up. if module_has_submodule(mod, 'imagespecs'): raise + + +def get_logger(logger_name='imagekit', add_null_handler=True): + logger = logging.getLogger(logger_name) + if add_null_handler: + logger.addHandler(logging.NullHandler()) + return logger From 98a6fff62dc9a4691770bf3b1d1729b600ff7d02 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Wed, 17 Oct 2012 21:09:04 -0400 Subject: [PATCH 069/213] Add DEFAULT_FILE_STORAGE Setting; Closes #153 --- imagekit/conf.py | 1 + imagekit/files.py | 6 ++++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/imagekit/conf.py b/imagekit/conf.py index d183f00..ed568db 100644 --- a/imagekit/conf.py +++ b/imagekit/conf.py @@ -8,6 +8,7 @@ class ImageKitConf(AppConf): CACHE_DIR = 'CACHE/images' CACHE_PREFIX = 'imagekit:' DEFAULT_IMAGE_CACHE_STRATEGY = 'imagekit.imagecache.strategies.JustInTime' + DEFAULT_FILE_STORAGE = None # Indicates that the source storage should be used def configure_cache_backend(self, value): if value is None: diff --git a/imagekit/files.py b/imagekit/files.py index 12852cc..e04198d 100644 --- a/imagekit/files.py +++ b/imagekit/files.py @@ -5,7 +5,7 @@ from django.utils.encoding import smart_str, smart_unicode import os from .signals import before_access from .utils import (suggest_extension, format_to_mimetype, - extension_to_mimetype, get_logger) + extension_to_mimetype, get_logger, get_singleton) class BaseIKFile(File): @@ -72,7 +72,9 @@ class BaseIKFile(File): class ImageSpecCacheFile(BaseIKFile, ImageFile): def __init__(self, spec, source_file): - storage = spec.storage or source_file.storage + storage = (spec.storage or + get_singleton(settings.IMAGEKIT_DEFAULT_FILE_STORAGE, 'file storage backend') if settings.IMAGEKIT_DEFAULT_FILE_STORAGE + else source_file.storage) super(ImageSpecCacheFile, self).__init__(storage=storage) self.spec = spec self.source_file = source_file From 4f52e401d2b68316b6a29175251ce26a3391b0b1 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Wed, 17 Oct 2012 21:24:57 -0400 Subject: [PATCH 070/213] Handle null format --- imagekit/specs/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/imagekit/specs/__init__.py b/imagekit/specs/__init__.py index 7aa8728..ccb4cb4 100644 --- a/imagekit/specs/__init__.py +++ b/imagekit/specs/__init__.py @@ -104,7 +104,7 @@ class BaseImageSpec(object): return md5(''.join([ source_file.name, pickle.dumps(self.processors), - self.format, + str(self.format), pickle.dumps(self.options), str(self.autoconvert), ]).encode('utf-8')).hexdigest() From e300ce36a442de833c670af9c056766202fd4a2b Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Wed, 17 Oct 2012 21:25:19 -0400 Subject: [PATCH 071/213] Use new registry name --- imagekit/templatetags/imagekit_tags.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/imagekit/templatetags/imagekit_tags.py b/imagekit/templatetags/imagekit_tags.py index c365c2f..1d4cf82 100644 --- a/imagekit/templatetags/imagekit_tags.py +++ b/imagekit/templatetags/imagekit_tags.py @@ -1,7 +1,7 @@ from django import template from django.utils.safestring import mark_safe from ..files import ImageSpecCacheFile -from ..specs import spec_registry +from .. import specs register = template.Library() @@ -18,7 +18,7 @@ class SpecNode(template.Node): autodiscover() source_image = self.source_image.resolve(context) spec_id = self.spec_id.resolve(context) - spec = spec_registry.get_spec(spec_id) + spec = specs.registry.get_spec(spec_id) if callable(spec): spec = spec() file = ImageSpecCacheFile(spec, source_image) From 770a8cebf4682dc83442671fdeafc441aae8ea26 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Wed, 17 Oct 2012 21:35:19 -0400 Subject: [PATCH 072/213] Fix iteration error --- imagekit/specs/sources.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/imagekit/specs/sources.py b/imagekit/specs/sources.py index 958fa61..15c852e 100644 --- a/imagekit/specs/sources.py +++ b/imagekit/specs/sources.py @@ -73,7 +73,7 @@ class ModelSignalRouter(object): @ik_model_receiver def post_delete_receiver(self, sender, instance=None, **kwargs): - for attname, file in self.get_field_dict(instance): + for attname, file in self.get_field_dict(instance).items(): self.dispatch_signal(source_deleted, file, sender, instance, attname) @ik_model_receiver From b0b466618f669f87ecfe66b9dd655415ba88ed83 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Wed, 17 Oct 2012 23:39:48 -0400 Subject: [PATCH 073/213] Separate two forms of tag; support additional html attrs Closes #154 --- imagekit/templatetags/imagekit_tags.py | 113 +++++++++++++++++++------ 1 file changed, 89 insertions(+), 24 deletions(-) diff --git a/imagekit/templatetags/imagekit_tags.py b/imagekit/templatetags/imagekit_tags.py index 1d4cf82..b092ca4 100644 --- a/imagekit/templatetags/imagekit_tags.py +++ b/imagekit/templatetags/imagekit_tags.py @@ -1,5 +1,6 @@ from django import template from django.utils.safestring import mark_safe +import re from ..files import ImageSpecCacheFile from .. import specs @@ -7,27 +8,86 @@ from .. import specs register = template.Library() -class SpecNode(template.Node): - def __init__(self, spec_id, source_image, variable_name=None): - self.spec_id = spec_id - self.source_image = source_image - self.variable_name = variable_name +html_attr_pattern = r""" + (?P\w+) # The attribute name + ( + \s*=\s* # an equals sign, that may or may not have spaces around it + (?P + ("[^"]*") # a double-quoted value + | # or + ('[^']*') # a single-quoted value + | # or + ([^"'<>=\s]+) # an unquoted value + ) + )? +""" - def render(self, context): +html_attr_re = re.compile(html_attr_pattern, re.VERBOSE) + + +class SpecResultNodeMixin(object): + def __init__(self, spec_id, source_file): + self._spec_id = spec_id + self._source_file = source_file + + def get_spec(self, context): from ..utils import autodiscover autodiscover() - source_image = self.source_image.resolve(context) - spec_id = self.spec_id.resolve(context) + spec_id = self._spec_id.resolve(context) spec = specs.registry.get_spec(spec_id) if callable(spec): spec = spec() - file = ImageSpecCacheFile(spec, source_image) - if self.variable_name is not None: - variable_name = str(self.variable_name) - context[variable_name] = file - return '' + return spec - return mark_safe(u'' % file.url) + def get_source_file(self, context): + return self._source_file.resolve(context) + + def get_file(self, context): + spec = self.get_spec(context) + source_file = self.get_source_file(context) + return ImageSpecCacheFile(spec, source_file) + + +class SpecResultAssignmentNode(template.Node, SpecResultNodeMixin): + def __init__(self, spec_id, source_file, variable_name): + super(SpecResultAssignmentNode, self).__init__(spec_id, source_file) + self._variable_name = variable_name + + def get_variable_name(self, context): + return unicode(self._variable_name) + + def render(self, context): + variable_name = self.get_variable_name(context) + context[variable_name] = self.get_file(context) + return '' + + +class SpecResultImgTagNode(template.Node, SpecResultNodeMixin): + def __init__(self, spec_id, source_file, html_attrs): + super(SpecResultImgTagNode, self).__init__(spec_id, source_file) + self._html_attrs = html_attrs + + def get_html_attrs(self, context): + attrs = [] + for attr in self._html_attrs: + match = html_attr_re.search(attr) + if match: + attrs.append((match.group('name'), match.group('value'))) + return attrs + + def get_attr_str(self, k, v): + return k if v is None else '%s=%s' % (k, v) + + def render(self, context): + file = self.get_file(context) + attrs = self.get_html_attrs(context) + attr_dict = dict(attrs) + if not 'width' in attr_dict and not 'height' in attr_dict: + attrs = attrs + [('width', '"%s"' % file.width), + ('height', '"%s"' % file.height)] + attrs = [('src', '"%s"' % file.url)] + attrs + attr_str = ' '.join(self.get_attr_str(k, v) for k, v in attrs) + return mark_safe(u'' % attr_str) #@register.tag @@ -51,21 +111,26 @@ def spec(parser, token): """ - args = token.split_contents() - arg_count = len(args) + bits = token.split_contents() + tag_name = bits.pop(0) - if (arg_count < 3 or arg_count > 5 - or (arg_count > 3 and arg_count < 5) - or (args == 5 and args[3] != 'as')): + if len(bits) == 4 and bits[2] == 'as': + return SpecResultAssignmentNode( + parser.compile_filter(bits[0]), # spec id + parser.compile_filter(bits[1]), # source file + parser.compile_filter(bits[3]), # var name + ) + elif len(bits) > 1: + return SpecResultImgTagNode( + parser.compile_filter(bits[0]), # spec id + parser.compile_filter(bits[1]), # source file + bits[2:], # html attributes + ) + else: raise template.TemplateSyntaxError('\'spec\' tags must be in the form' ' "{% spec spec_id image %}" or' ' "{% spec spec_id image' ' as varname %}"') - spec_id = parser.compile_filter(args[1]) - source_image = parser.compile_filter(args[2]) - variable_name = arg_count > 3 and args[4] or None - return SpecNode(spec_id, source_image, variable_name) - spec = spec_tag = register.tag(spec) From e796b4cc61f6268c231b44df911dabf94bf8883b Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Sat, 20 Oct 2012 22:15:25 -0400 Subject: [PATCH 074/213] Create ImageSpec subclasses Since the `ImageSpec` constructor will be accepting keyword arg hints, it can no longer accept the properties. --- imagekit/specs/__init__.py | 89 ++++++++++++++++++++------------------ 1 file changed, 48 insertions(+), 41 deletions(-) diff --git a/imagekit/specs/__init__.py b/imagekit/specs/__init__.py index ccb4cb4..f9f7920 100644 --- a/imagekit/specs/__init__.py +++ b/imagekit/specs/__init__.py @@ -89,16 +89,36 @@ class SpecRegistry(object): class BaseImageSpec(object): - processors = None - format = None - options = None - autoconvert = True - def __init__(self, processors=None, format=None, options=None, autoconvert=None): - self.processors = processors or self.processors or [] - self.format = format or self.format - self.options = options or self.options - self.autoconvert = self.autoconvert if autoconvert is None else autoconvert + processors = None + """A list of processors to run on the original image.""" + + format = None + """ + The format of the output file. If not provided, ImageSpecField will try to + guess the appropriate format based on the extension of the filename and the + format of the input image. + + """ + + options = None + """ + A dictionary that will be passed to PIL's ``Image.save()`` method as keyword + arguments. Valid options vary between formats, but some examples include + ``quality``, ``optimize``, and ``progressive`` for JPEGs. See the PIL + documentation for others. + + """ + + autoconvert = True + """ + Specifies whether automatic conversion using ``prepare_image()`` should be + performed prior to saving. + + """ + + def __init__(self): + self.processors = self.processors or [] def get_hash(self, source_file): return md5(''.join([ @@ -137,42 +157,29 @@ class BaseImageSpec(object): class ImageSpec(BaseImageSpec): + storage = None + """A Django storage system to use to save the generated image.""" + image_cache_backend = None + """ + An object responsible for managing the state of cached files. Defaults to an + instance of ``IMAGEKIT_DEFAULT_IMAGE_CACHE_BACKEND`` + + """ + image_cache_strategy = settings.IMAGEKIT_DEFAULT_IMAGE_CACHE_STRATEGY + """ + A dictionary containing callbacks that allow you to customize how and when + the image cache is validated. Defaults to + ``IMAGEKIT_DEFAULT_SPEC_FIELD_IMAGE_CACHE_STRATEGY``. - def __init__(self, processors=None, format=None, options=None, - storage=None, autoconvert=None, image_cache_backend=None, - image_cache_strategy=None): - """ - :param processors: A list of processors to run on the original image. - :param format: The format of the output file. If not provided, - ImageSpecField will try to guess the appropriate format based on the - extension of the filename and the format of the input image. - :param options: A dictionary that will be passed to PIL's - ``Image.save()`` method as keyword arguments. Valid options vary - between formats, but some examples include ``quality``, - ``optimize``, and ``progressive`` for JPEGs. See the PIL - documentation for others. - :param autoconvert: Specifies whether automatic conversion using - ``prepare_image()`` should be performed prior to saving. - :param image_field: The name of the model property that contains the - original image. - :param storage: A Django storage system to use to save the generated - image. - :param image_cache_backend: An object responsible for managing the state - of cached files. Defaults to an instance of - ``IMAGEKIT_DEFAULT_IMAGE_CACHE_BACKEND`` - :param image_cache_strategy: A dictionary containing callbacks that - allow you to customize how and when the image cache is validated. - Defaults to ``IMAGEKIT_DEFAULT_SPEC_FIELD_IMAGE_CACHE_STRATEGY`` + """ - """ - super(ImageSpec, self).__init__(processors=processors, format=format, - options=options, autoconvert=autoconvert) - self.storage = storage or self.storage - self.image_cache_backend = image_cache_backend or self.image_cache_backend or get_default_image_cache_backend() - self.image_cache_strategy = StrategyWrapper(image_cache_strategy or self.image_cache_strategy) + def __init__(self, **kwargs): + super(ImageSpec, self).__init__() + self.image_cache_backend = self.image_cache_backend or get_default_image_cache_backend() + self.image_cache_strategy = StrategyWrapper(self.image_cache_strategy) # TODO: I don't like this interface. Is there a standard Python one? pubsub? def _handle_source_event(self, event_name, source_file): @@ -205,7 +212,7 @@ class SpecHost(object): raise TypeError('You can provide either an image spec or' ' arguments for the ImageSpec constructor, but not both.') else: - spec = ImageSpec(**spec_args) + spec = type('Spec', (ImageSpec,), spec_args) # TODO: Base class name on spec id? self._original_spec = spec From aa91a70e4642ec2843de354a8df129c1999626b8 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Sat, 20 Oct 2012 22:53:55 -0400 Subject: [PATCH 075/213] Make specs know less about source files --- imagekit/files.py | 8 ++++++-- imagekit/specs/__init__.py | 3 +-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/imagekit/files.py b/imagekit/files.py index e04198d..567468a 100644 --- a/imagekit/files.py +++ b/imagekit/files.py @@ -2,6 +2,7 @@ from django.conf import settings from django.core.files.base import ContentFile, File from django.core.files.images import ImageFile from django.utils.encoding import smart_str, smart_unicode +from hashlib import md5 import os from .signals import before_access from .utils import (suggest_extension, format_to_mimetype, @@ -80,7 +81,10 @@ class ImageSpecCacheFile(BaseIKFile, ImageFile): self.source_file = source_file def get_hash(self): - return self.spec.get_hash(self.source_file) + return md5(''.join([ + self.source_file.name, + self.spec.get_hash(), + ]).encode('utf-8')).hexdigest() def _require_file(self): before_access.send(sender=self, spec=self.spec, file=self) @@ -91,7 +95,7 @@ class ImageSpecCacheFile(BaseIKFile, ImageFile): source_filename = self.source_file.name filename = None if source_filename: - hash = self.spec.get_hash(self.source_file) + hash = self.get_hash() ext = suggest_extension(source_filename, self.spec.format) filename = os.path.normpath(os.path.join( settings.IMAGEKIT_CACHE_DIR, diff --git a/imagekit/specs/__init__.py b/imagekit/specs/__init__.py index f9f7920..e64f976 100644 --- a/imagekit/specs/__init__.py +++ b/imagekit/specs/__init__.py @@ -120,9 +120,8 @@ class BaseImageSpec(object): def __init__(self): self.processors = self.processors or [] - def get_hash(self, source_file): + def get_hash(self): return md5(''.join([ - source_file.name, pickle.dumps(self.processors), str(self.format), pickle.dumps(self.options), From 12493b3a0da6cb5503aba11339d45456854155e8 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Sat, 20 Oct 2012 23:21:01 -0400 Subject: [PATCH 076/213] Spec host must support kwarg "hints" The registry's `get_spec()` was already supporting kwargs as a means to provide information about the source to the spec constructor/factory function, but the ``SpecHost`` class wasn't capable of accepting any. This commit rectifies that. The main goal purpose of this is to allow a bound field (the file attached by ``ImageSpecFileDescriptor``)--and the attached model instance--to be taken into account during the spec instance creation. Related: #156 --- imagekit/models/fields/__init__.py | 4 ---- imagekit/models/fields/files.py | 5 +++-- imagekit/models/fields/utils.py | 3 ++- imagekit/specs/__init__.py | 5 ++--- 4 files changed, 7 insertions(+), 10 deletions(-) diff --git a/imagekit/models/fields/__init__.py b/imagekit/models/fields/__init__.py index 0cb6443..771c81e 100644 --- a/imagekit/models/fields/__init__.py +++ b/imagekit/models/fields/__init__.py @@ -38,10 +38,6 @@ class ImageSpecField(SpecHostField): self.image_field = image_field - @property - def storage(self): - return self.spec.storage - def contribute_to_class(self, cls, name): setattr(cls, name, ImageSpecFileDescriptor(self, name)) self.set_spec_id(cls, name) diff --git a/imagekit/models/fields/files.py b/imagekit/models/fields/files.py index 3c9ffd6..26037fd 100644 --- a/imagekit/models/fields/files.py +++ b/imagekit/models/fields/files.py @@ -6,7 +6,8 @@ from ...utils import suggest_extension class ProcessedImageFieldFile(ImageFieldFile): def save(self, name, content, save=True): filename, ext = os.path.splitext(name) - ext = suggest_extension(name, self.field.spec.format) + spec = self.field.get_spec() # TODO: What "hints"? + ext = suggest_extension(name, spec.format) new_name = '%s%s' % (filename, ext) - content = self.field.spec.apply(content, new_name) + content = spec.apply(content, new_name) return super(ProcessedImageFieldFile, self).save(new_name, content, save) diff --git a/imagekit/models/fields/utils.py b/imagekit/models/fields/utils.py index 9490966..0695212 100644 --- a/imagekit/models/fields/utils.py +++ b/imagekit/models/fields/utils.py @@ -29,7 +29,8 @@ class ImageSpecFileDescriptor(object): self.attname)) else: source_file = image_fields[0] - file = ImageSpecCacheFile(self.field.spec, source_file) + spec = self.field.get_spec() # TODO: What "hints" should we pass here? + file = ImageSpecCacheFile(spec, source_file) instance.__dict__[self.attname] = file return file diff --git a/imagekit/specs/__init__.py b/imagekit/specs/__init__.py index e64f976..0e10c65 100644 --- a/imagekit/specs/__init__.py +++ b/imagekit/specs/__init__.py @@ -230,8 +230,7 @@ class SpecHost(object): self.spec_id = id registry.register(id, self._original_spec) - @property - def spec(self): + def get_spec(self, **kwargs): """ Look up the spec by the spec id. We do this (instead of storing the spec as an attribute) so that users can override apps' specs--without @@ -241,7 +240,7 @@ class SpecHost(object): """ if not getattr(self, 'spec_id', None): raise Exception('Object %s has no spec id.' % self) - return registry.get_spec(self.spec_id) + return registry.get_spec(self.spec_id, **kwargs) registry = SpecRegistry() From 0c4d9738c63e6dec565303488205b74074ea4de1 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Sat, 20 Oct 2012 23:44:13 -0400 Subject: [PATCH 077/213] Add TODO --- imagekit/templatetags/imagekit_tags.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/imagekit/templatetags/imagekit_tags.py b/imagekit/templatetags/imagekit_tags.py index b092ca4..959d479 100644 --- a/imagekit/templatetags/imagekit_tags.py +++ b/imagekit/templatetags/imagekit_tags.py @@ -34,7 +34,7 @@ class SpecResultNodeMixin(object): from ..utils import autodiscover autodiscover() spec_id = self._spec_id.resolve(context) - spec = specs.registry.get_spec(spec_id) + spec = specs.registry.get_spec(spec_id) # TODO: What "hints" here? if callable(spec): spec = spec() return spec From 3e2c3803ff1dbe353d2131ee6fcd7b13bb386e6d Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Sat, 20 Oct 2012 23:44:26 -0400 Subject: [PATCH 078/213] No need to call spec; the registry does that --- imagekit/templatetags/imagekit_tags.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/imagekit/templatetags/imagekit_tags.py b/imagekit/templatetags/imagekit_tags.py index 959d479..b0b981b 100644 --- a/imagekit/templatetags/imagekit_tags.py +++ b/imagekit/templatetags/imagekit_tags.py @@ -35,8 +35,6 @@ class SpecResultNodeMixin(object): autodiscover() spec_id = self._spec_id.resolve(context) spec = specs.registry.get_spec(spec_id) # TODO: What "hints" here? - if callable(spec): - spec = spec() return spec def get_source_file(self, context): From fa54b9b6ef7bdaf581e128b81013ebfdbadbe99c Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Sun, 21 Oct 2012 17:55:18 -0400 Subject: [PATCH 079/213] Document file-generation aspect of specs --- README.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index a616d75..94cf4c0 100644 --- a/README.rst +++ b/README.rst @@ -59,7 +59,8 @@ might be useful, for example, in view code, or in scripts: .. code-block:: python - ???????? + spec = Thumbnail() + new_file = spec.apply(source_file) More often, however, you'll want to register your spec with ImageKit: From 3dbb96ea40e8960bc524a1756919577df7f4e024 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Sun, 21 Oct 2012 17:57:53 -0400 Subject: [PATCH 080/213] Remove unused imports --- imagekit/utils.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/imagekit/utils.py b/imagekit/utils.py index 282b739..a4712e4 100644 --- a/imagekit/utils.py +++ b/imagekit/utils.py @@ -382,11 +382,9 @@ def autodiscover(): Copied from django.contrib.admin """ - import copy from django.conf import settings from django.utils.importlib import import_module from django.utils.module_loading import module_has_submodule - from .templatetags import imagekit_tags for app in settings.INSTALLED_APPS: mod = import_module(app) From d110b823470ccecc7b203238049ab21837821be6 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Sun, 21 Oct 2012 17:59:56 -0400 Subject: [PATCH 081/213] Rename imagekit_tags to imagekit --- imagekit/templatetags/__init__.py | 1 - imagekit/templatetags/{imagekit_tags.py => imagekit.py} | 0 2 files changed, 1 deletion(-) rename imagekit/templatetags/{imagekit_tags.py => imagekit.py} (100%) diff --git a/imagekit/templatetags/__init__.py b/imagekit/templatetags/__init__.py index e0a23fc..e69de29 100644 --- a/imagekit/templatetags/__init__.py +++ b/imagekit/templatetags/__init__.py @@ -1 +0,0 @@ -from .imagekit_tags import spec diff --git a/imagekit/templatetags/imagekit_tags.py b/imagekit/templatetags/imagekit.py similarity index 100% rename from imagekit/templatetags/imagekit_tags.py rename to imagekit/templatetags/imagekit.py From 606f59a102fb69bb93ed4636b63b46e140e4eae4 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Sun, 21 Oct 2012 21:52:59 -0400 Subject: [PATCH 082/213] Add docs page about configuration & optimization --- docs/configuration.rst | 137 +++++++++++++++++++++++++++++++++++++++++ docs/index.rst | 1 + 2 files changed, 138 insertions(+) create mode 100644 docs/configuration.rst diff --git a/docs/configuration.rst b/docs/configuration.rst new file mode 100644 index 0000000..18b3595 --- /dev/null +++ b/docs/configuration.rst @@ -0,0 +1,137 @@ +.. _settings: + +Configuration +============= + + +Settings +-------- + +.. currentmodule:: django.conf.settings + + +.. attribute:: IMAGEKIT_CACHE_DIR + + :default: ``'CACHE/images'`` + + The directory to which image files will be cached. + + +.. attribute:: IMAGEKIT_DEFAULT_FILE_STORAGE + + :default: ``None`` + + The qualified class name of a Django storage backend to use to save the + cached images. If no value is provided for ``IMAGEKIT_DEFAULT_FILE_STORAGE``, + and none is specified by the spec definition, the storage of the source file + will be used. + + +.. attribute:: IMAGEKIT_DEFAULT_IMAGE_CACHE_BACKEND + + :default: ``'imagekit.imagecache.backends.Simple'`` + + Specifies the class that will be used to validate cached image files. + + +.. attribute:: IMAGEKIT_DEFAULT_IMAGE_CACHE_STRATEGY + + :default: ``'imagekit.imagecache.strategies.JustInTime'`` + + The class responsible for specifying how and when cache files are + generated. + + +.. attribute:: IMAGEKIT_CACHE_BACKEND + + :default: If ``DEBUG`` is ``True``, ``'django.core.cache.backends.dummy.DummyCache'``. + Otherwise, ``'default'``. + + The Django cache backend to be used to store information like the state of + cached images (i.e. validated or not). + + +.. attribute:: IMAGEKIT_CACHE_PREFIX + + :default: ``'imagekit:'`` + + A cache prefix to be used when values are stored in ``IMAGEKIT_CACHE_BACKEND`` + + +Optimization +------------ + +Not surprisingly, the trick to getting the most out of ImageKit is to reduce the +number of I/O operations. This can be especially important if your source files +aren't stored on the same server as the application. + + +Image Cache Strategies +^^^^^^^^^^^^^^^^^^^^^^ + +An important way of reducing the number of I/O operations that ImageKit makes is +by controlling when cached images are validated. This is done through "image +cache strategies"—objects that associate signals dispatched on the source file +with file actions. The default image cache strategy is +``'imagekit.imagecache.strategies.JustInTime'``; it looks like this: + +.. code-block:: python + + class JustInTime(object): + def before_access(self, file): + validate_now(file) + +When this strategy is used, the cache file is validated only immediately before +it's required—for example, when you access its url, path, or contents. This +strategy is exceedingly safe: by guaranteeing the presence of the file before +accessing it, you run no risk of it not being there. However, this strategy can +also be costly: verifying the existence of the cache file every time you access +it can be slow—particularly if the file is on another server. For this reason, +ImageKit provides another strategy: ``imagekit.imagecache.strategies.Optimistic``. +Unlike the just-in-time strategy, it does not validate the cache file when it's +accessed, but rather only when the soure file is created or changed. Later, when +the cache file is accessed, it is presumed to still be present. + +If neither of these strategies suits your application, you can create your own +strategy class. For example, you may wish to validate the file immediately when +it's accessed, but schedule validation using Celery when the source file is +saved or changed: + +.. code-block:: python + + from imagekit.imagecache.actions import validate_now, deferred_validate + + class CustomImageCacheStrategy(object): + + def before_access(self, file): + validate_now(file) + + def on_source_created(self, file): + deferred_validate(file) + + def on_source_changed(self, file): + deferred_validate(file) + +To use this cache strategy, you need only set the ``IMAGEKIT_DEFAULT_IMAGE_CACHE_STRATEGY`` +setting, or set the ``image_cache_strategy`` attribute of your image spec. + + +Django Cache Backends +^^^^^^^^^^^^^^^^^^^^^ + +In the "Image Cache Strategies" section above, we said that the just-in-time +strategy verifies the existence of the cache file every time you access +it, however, that's not exactly true. Cache files are actually validated using +image cache backends, and the default (``imagekit.imagecache.backends.Simple``) +memoizes the cache state (valid or invalid) using Django's cache framework. By +default, ImageKit will use a dummy cache backend when your project is in debug +mode (``DEBUG = True``), and the "default" cache (from your ``CACHES`` setting) +when ``DEBUG`` is ``False``. Since other parts of your project may have +different cacheing needs, though, ImageKit has an ``IMAGEKIT_CACHE_BACKEND`` +setting, which allows you to specify a different cache. + +In most cases, you won't be deleting you cached files once they're created, so +using a cache with a large timeout is a great way to optimize your site. Using +a cache that never expires would essentially negate the cost of the just-in-time +strategy, giving you the benefit of generating images on demand without the cost +of unnecessary future filesystem checks. diff --git a/docs/index.rst b/docs/index.rst index 2c06385..5484351 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -31,6 +31,7 @@ Digging Deeper .. toctree:: + configuration apireference changelog From 8d3fcafcd9438c4897de568597cef77cf3d9a061 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Wed, 24 Oct 2012 21:50:53 -0400 Subject: [PATCH 083/213] Swap argument order for specs.register This will allow us to put the id in the inner Meta class. --- README.rst | 2 +- imagekit/specs/__init__.py | 14 +++----------- 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/README.rst b/README.rst index 94cf4c0..242562a 100644 --- a/README.rst +++ b/README.rst @@ -67,7 +67,7 @@ More often, however, you'll want to register your spec with ImageKit: .. code-block:: python from imagekit import specs - specs.register('myapp:fancy_thumbnail', Thumbnail) + specs.register(Thumbnail, 'myapp:fancy_thumbnail') Once a spec is registered with a unique name, you can start to take advantage of ImageKit's powerful utilities to automatically generate images for you... diff --git a/imagekit/specs/__init__.py b/imagekit/specs/__init__.py index 0e10c65..af627d2 100644 --- a/imagekit/specs/__init__.py +++ b/imagekit/specs/__init__.py @@ -36,7 +36,7 @@ class SpecRegistry(object): signal.connect(self.source_receiver) before_access.connect(self.before_access_receiver) - def register(self, id, spec): + def register(self, spec, id): if id in self._specs: raise AlreadyRegistered('The spec with id %s is already registered' % id) self._specs[id] = spec @@ -228,7 +228,7 @@ class SpecHost(object): """ self.spec_id = id - registry.register(id, self._original_spec) + registry.register(self._original_spec, id) def get_spec(self, **kwargs): """ @@ -244,15 +244,7 @@ class SpecHost(object): registry = SpecRegistry() - - -def register(id, spec=None): - if not spec: - def decorator(cls): - registry.register(id, cls) - return cls - return decorator - registry.register(id, spec) +register = registry.register def unregister(id, spec): From bdec396180643dc143e4fe358a7c77536ac7ed4d Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Wed, 24 Oct 2012 22:23:32 -0400 Subject: [PATCH 084/213] Support inner `Config` class for id, sources Closes #164 --- imagekit/exceptions.py | 4 ++++ imagekit/specs/__init__.py | 19 +++++++++++++++++-- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/imagekit/exceptions.py b/imagekit/exceptions.py index 55cf144..b453f16 100644 --- a/imagekit/exceptions.py +++ b/imagekit/exceptions.py @@ -12,3 +12,7 @@ class UnknownExtensionError(Exception): class UnknownFormatError(Exception): pass + + +class MissingSpecId(Exception): + pass diff --git a/imagekit/specs/__init__.py b/imagekit/specs/__init__.py index af627d2..3d5e0ca 100644 --- a/imagekit/specs/__init__.py +++ b/imagekit/specs/__init__.py @@ -2,7 +2,8 @@ from django.conf import settings from hashlib import md5 import os import pickle -from ..exceptions import UnknownExtensionError, AlreadyRegistered, NotRegistered +from ..exceptions import (UnknownExtensionError, AlreadyRegistered, + NotRegistered, MissingSpecId) from ..files import ImageSpecCacheFile, IKContentFile from ..imagecache.backends import get_default_image_cache_backend from ..imagecache.strategies import StrategyWrapper @@ -36,11 +37,25 @@ class SpecRegistry(object): signal.connect(self.source_receiver) before_access.connect(self.before_access_receiver) - def register(self, spec, id): + def register(self, spec, id=None): + config = getattr(spec, 'Config', None) + + if id is None: + id = getattr(config, 'id', None) + + if id is None: + raise MissingSpecId('No id provided for %s. You must either pass an' + ' id to the register function, or add an id' + ' attribute to the inner Config class of your' + ' spec.' % spec) + if id in self._specs: raise AlreadyRegistered('The spec with id %s is already registered' % id) self._specs[id] = spec + for source in getattr(config, 'sources', None) or []: + self.add_source(source, id) + def unregister(self, id, spec): try: del self._specs[id] From adf143edc5374a635024d09d08a445a82145a6fb Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Wed, 24 Oct 2012 22:29:21 -0400 Subject: [PATCH 085/213] `add_source` -> `add_sources`; switch argument order --- imagekit/models/fields/__init__.py | 4 ++-- imagekit/specs/__init__.py | 15 ++++++++------- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/imagekit/models/fields/__init__.py b/imagekit/models/fields/__init__.py index 771c81e..3f98f35 100644 --- a/imagekit/models/fields/__init__.py +++ b/imagekit/models/fields/__init__.py @@ -43,8 +43,8 @@ class ImageSpecField(SpecHostField): self.set_spec_id(cls, name) # Add the model and field as a source for this spec id - specs.registry.add_source(ImageFieldSpecSource(cls, self.image_field), - self.spec_id) + specs.registry.add_sources(self.spec_id, + [ImageFieldSpecSource(cls, self.image_field)]) class ProcessedImageField(models.ImageField, SpecHostField): diff --git a/imagekit/specs/__init__.py b/imagekit/specs/__init__.py index 3d5e0ca..3e761a6 100644 --- a/imagekit/specs/__init__.py +++ b/imagekit/specs/__init__.py @@ -53,8 +53,8 @@ class SpecRegistry(object): raise AlreadyRegistered('The spec with id %s is already registered' % id) self._specs[id] = spec - for source in getattr(config, 'sources', None) or []: - self.add_source(source, id) + sources = getattr(config, 'sources', None) or [] + self.add_sources(id, sources) def unregister(self, id, spec): try: @@ -72,14 +72,15 @@ class SpecRegistry(object): else: return spec - def add_source(self, source, spec_id): + def add_sources(self, spec_id, sources): """ - Associates a source with a spec id + Associates sources with a spec id """ - if source not in self._sources: - self._sources[source] = set() - self._sources[source].add(spec_id) + for source in sources: + if source not in self._sources: + self._sources[source] = set() + self._sources[source].add(spec_id) def before_access_receiver(self, sender, spec, file, **kwargs): spec.image_cache_strategy.invoke_callback('before_access', file) From d27836983abc2933158325138badf24f923a1752 Mon Sep 17 00:00:00 2001 From: Eric Eldredge Date: Wed, 24 Oct 2012 23:30:37 -0400 Subject: [PATCH 086/213] Use nose to run tests Closes #160 --- requirements.txt | 5 +++++ tests/settings.py | 15 +++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/requirements.txt b/requirements.txt index 5161716..23b38e9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,8 @@ Django>=1.3.1 django-appconf>=0.5 PIL>=1.1.7 + +# Required for tests +nose==1.2.1 +nose-progressive==1.3 +django-nose==1.1 diff --git a/tests/settings.py b/tests/settings.py index f034e9c..11113f6 100644 --- a/tests/settings.py +++ b/tests/settings.py @@ -26,6 +26,21 @@ INSTALLED_APPS = [ 'django.contrib.contenttypes', 'imagekit', 'core', + 'django_nose', +] + +TEST_RUNNER = 'django_nose.NoseTestSuiteRunner' +NOSE_ARGS = [ + '-s', + '--with-progressive', + + # When the tests are run --with-coverage, these args configure coverage + # reporting (requires coverage to be installed). + # Without the --with-coverage flag, they have no effect. + '--cover-tests', + '--cover-html', + '--cover-package=imagekit', + '--cover-html-dir=%s' % os.path.join(BASE_PATH, 'cover') ] DEBUG = True From 84f3b6475bce1890f6ba95907d1f40f6c681ee2f Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Wed, 24 Oct 2012 22:41:35 -0400 Subject: [PATCH 087/213] Add `files()` generator. Re: #165 --- imagekit/specs/sources.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/imagekit/specs/sources.py b/imagekit/specs/sources.py index 15c852e..3be4a7e 100644 --- a/imagekit/specs/sources.py +++ b/imagekit/specs/sources.py @@ -115,5 +115,9 @@ class ImageFieldSpecSource(object): self.image_field = image_field signal_router.add(self) + def files(self): + for instance in self.model_class.objects.all(): + yield getattr(instance, self.image_field) + signal_router = ModelSignalRouter() From fb8c411f751b8989f61dd1ca888cc9bca31216f8 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Wed, 24 Oct 2012 23:36:00 -0400 Subject: [PATCH 088/213] Create new cache warming command Replaces ikcachevalidate and ikcacheinvalidate, and uses the "sources" abstraction. Closes #165 --- .../management/commands/ikcacheinvalidate.py | 14 -------- .../management/commands/ikcachevalidate.py | 30 ---------------- .../management/commands/warmimagecache.py | 36 +++++++++++++++++++ imagekit/specs/__init__.py | 6 ++++ imagekit/utils.py | 22 ------------ 5 files changed, 42 insertions(+), 66 deletions(-) delete mode 100644 imagekit/management/commands/ikcacheinvalidate.py delete mode 100644 imagekit/management/commands/ikcachevalidate.py create mode 100644 imagekit/management/commands/warmimagecache.py diff --git a/imagekit/management/commands/ikcacheinvalidate.py b/imagekit/management/commands/ikcacheinvalidate.py deleted file mode 100644 index 2b6e915..0000000 --- a/imagekit/management/commands/ikcacheinvalidate.py +++ /dev/null @@ -1,14 +0,0 @@ -from django.core.management.base import BaseCommand -from django.db.models.loading import cache -from ...utils import invalidate_app_cache - - -class Command(BaseCommand): - help = ('Invalidates the image cache for a list of apps.') - args = '[apps]' - requires_model_validation = True - can_import_settings = True - - def handle(self, *args, **options): - apps = args or cache.app_models.keys() - invalidate_app_cache(apps) diff --git a/imagekit/management/commands/ikcachevalidate.py b/imagekit/management/commands/ikcachevalidate.py deleted file mode 100644 index 8e9fc6c..0000000 --- a/imagekit/management/commands/ikcachevalidate.py +++ /dev/null @@ -1,30 +0,0 @@ -from optparse import make_option -from django.core.management.base import BaseCommand -from django.db.models.loading import cache -from ...utils import validate_app_cache - - -class Command(BaseCommand): - help = ('Validates the image cache for a list of apps.') - args = '[apps]' - requires_model_validation = True - can_import_settings = True - - option_list = BaseCommand.option_list + ( - make_option('--force-revalidation', - dest='force_revalidation', - action='store_true', - default=False, - help='Invalidate each image file before validating it, thereby' - ' ensuring its revalidation. This is very similar to' - ' running ikcacheinvalidate and then running' - ' ikcachevalidate; the difference being that this option' - ' causes files to be invalidated and validated' - ' one-at-a-time, whereas running the two commands in series' - ' would invalidate all images before validating any.' - ), - ) - - def handle(self, *args, **options): - apps = args or cache.app_models.keys() - validate_app_cache(apps, options['force_revalidation']) diff --git a/imagekit/management/commands/warmimagecache.py b/imagekit/management/commands/warmimagecache.py new file mode 100644 index 0000000..8c9efcb --- /dev/null +++ b/imagekit/management/commands/warmimagecache.py @@ -0,0 +1,36 @@ +from optparse import make_option +from django.core.management.base import BaseCommand +import re +from ...files import ImageSpecCacheFile +from ...specs import registry + + +class Command(BaseCommand): + help = ('Warm the image cache for the specified specs (or all specs if none' + ' was provided). Simple wildcard matching (using asterisks) is' + ' supported.') + args = '[spec_ids]' + + def handle(self, *args, **options): + specs = registry.get_spec_ids() + + if args: + patterns = self.compile_patterns(args) + specs = (id for id in specs if any(p.match(id) for p in patterns)) + + for spec_id in specs: + self.stdout.write('Validating spec: %s\n' % spec_id) + spec = registry.get_spec(spec_id) # TODO: HINTS! (Probably based on source, so this will need to be moved into loop below.) + for source in registry.get_sources(spec_id): + for source_file in source.files(): + if source_file: + self.stdout.write(' %s\n' % source_file) + try: + # TODO: Allow other validation actions through command option + ImageSpecCacheFile(spec, source_file).validate() + except Exception, err: + # TODO: How should we handle failures? Don't want to error, but should call it out more than this. + self.stdout.write(' FAILED: %s\n' % err) + + def compile_patterns(self, spec_ids): + return [re.compile('%s$' % '.*'.join(re.escape(part) for part in id.split('*'))) for id in spec_ids] diff --git a/imagekit/specs/__init__.py b/imagekit/specs/__init__.py index 3e761a6..dc33593 100644 --- a/imagekit/specs/__init__.py +++ b/imagekit/specs/__init__.py @@ -72,6 +72,9 @@ class SpecRegistry(object): else: return spec + def get_spec_ids(self): + return self._specs.keys() + def add_sources(self, spec_id, sources): """ Associates sources with a spec id @@ -82,6 +85,9 @@ class SpecRegistry(object): self._sources[source] = set() self._sources[source].add(spec_id) + def get_sources(self, spec_id): + return [source for source in self._sources if spec_id in self._sources[source]] + def before_access_receiver(self, sender, spec, file, **kwargs): spec.image_cache_strategy.invoke_callback('before_access', file) diff --git a/imagekit/utils.py b/imagekit/utils.py index a4712e4..7495238 100644 --- a/imagekit/utils.py +++ b/imagekit/utils.py @@ -146,28 +146,6 @@ def _get_models(apps): return models -def invalidate_app_cache(apps): - for model in _get_models(apps): - print 'Invalidating cache for "%s.%s"' % (model._meta.app_label, model.__name__) - for obj in model._default_manager.order_by('-pk'): - for f in get_spec_files(obj): - f.invalidate() - - -def validate_app_cache(apps, force_revalidation=False): - for model in _get_models(apps): - for obj in model._default_manager.order_by('-pk'): - model_name = '%s.%s' % (model._meta.app_label, model.__name__) - if force_revalidation: - print 'Invalidating & validating cache for "%s"' % model_name - else: - print 'Validating cache for "%s"' % model_name - for f in get_spec_files(obj): - if force_revalidation: - f.invalidate() - f.validate() - - def suggest_extension(name, format): original_extension = os.path.splitext(name)[1] try: From 9973e80a37d775cda07868efe8b35a55da7e3b31 Mon Sep 17 00:00:00 2001 From: Eric Eldredge Date: Wed, 24 Oct 2012 23:45:00 -0400 Subject: [PATCH 089/213] Add test requirements to setup --- setup.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/setup.py b/setup.py index 6f662f9..9764b06 100644 --- a/setup.py +++ b/setup.py @@ -28,6 +28,11 @@ setup( packages=find_packages(), zip_safe=False, include_package_data=True, + tests_require=[ + 'nose==1.2.1', + 'nose-progressive==1.3', + 'django-nose==1.1', + ], install_requires=[ 'django-appconf>=0.5', ], From 006ff54fa865942d533298bf0eea8e71d12576b5 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Thu, 25 Oct 2012 20:01:11 -0400 Subject: [PATCH 090/213] Clean up version meta. --- imagekit/__init__.py | 37 +------------------------------------ imagekit/pkgmeta.py | 5 +++++ setup.py | 8 +++++--- 3 files changed, 11 insertions(+), 39 deletions(-) create mode 100644 imagekit/pkgmeta.py diff --git a/imagekit/__init__.py b/imagekit/__init__.py index f4fd227..74808d3 100644 --- a/imagekit/__init__.py +++ b/imagekit/__init__.py @@ -1,38 +1,3 @@ from . import conf from .specs import ImageSpec - - -__title__ = 'django-imagekit' -__author__ = 'Justin Driscoll, Bryan Veloso, Greg Newman, Chris Drackett, Matthew Tretter, Eric Eldredge' -__version__ = (2, 0, 1, 'final', 0) -__license__ = 'BSD' - - -def get_version(version=None): - """Derives a PEP386-compliant version number from VERSION.""" - if version is None: - version = __version__ - assert len(version) == 5 - assert version[3] in ('alpha', 'beta', 'rc', 'final') - - # Now build the two parts of the version number: - # main = X.Y[.Z] - # sub = .devN - for pre-alpha releases - # | {a|b|c}N - for alpha, beta and rc releases - - parts = 2 if version[2] == 0 else 3 - main = '.'.join(str(x) for x in version[:parts]) - - sub = '' - if version[3] == 'alpha' and version[4] == 0: - # At the toplevel, this would cause an import loop. - from django.utils.version import get_svn_revision - svn_revision = get_svn_revision()[4:] - if svn_revision != 'unknown': - sub = '.dev%s' % svn_revision - - elif version[3] != 'final': - mapping = {'alpha': 'a', 'beta': 'b', 'rc': 'c'} - sub = mapping[version[3]] + str(version[4]) - - return main + sub +from .pkgmeta import * diff --git a/imagekit/pkgmeta.py b/imagekit/pkgmeta.py new file mode 100644 index 0000000..20e62db --- /dev/null +++ b/imagekit/pkgmeta.py @@ -0,0 +1,5 @@ +__title__ = 'django-imagekit' +__author__ = 'Justin Driscoll, Bryan Veloso, Greg Newman, Chris Drackett, Matthew Tretter, Eric Eldredge' +__version__ = '3.0a1' +__license__ = 'BSD' +__all__ = ['__title__', '__author__', '__version__', '__license__'] diff --git a/setup.py b/setup.py index 9764b06..985e103 100644 --- a/setup.py +++ b/setup.py @@ -11,12 +11,14 @@ if 'publish' in sys.argv: read = lambda filepath: codecs.open(filepath, 'r', 'utf-8').read() -# Dynamically calculate the version based on imagekit.VERSION. -version = __import__('imagekit').get_version() +# Load package meta from the pkgmeta module without loading imagekit. +pkgmeta = {} +execfile(os.path.join(os.path.dirname(__file__), + 'imagekit', 'pkgmeta.py'), pkgmeta) setup( name='django-imagekit', - version=version, + version=pkgmeta['__version__'], description='Automated image processing for Django models.', long_description=read(os.path.join(os.path.dirname(__file__), 'README.rst')), author='Justin Driscoll', From 76b9ebbab455e2ec7c99b3dbdd2522f0b067f270 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Thu, 25 Oct 2012 22:25:18 -0400 Subject: [PATCH 091/213] Omit missing kwargs so as not to override defaults The default image cache strategy was being overridden, which prevented images from being generated. --- imagekit/specs/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/imagekit/specs/__init__.py b/imagekit/specs/__init__.py index dc33593..3878bc9 100644 --- a/imagekit/specs/__init__.py +++ b/imagekit/specs/__init__.py @@ -233,7 +233,7 @@ class SpecHost(object): raise TypeError('You can provide either an image spec or' ' arguments for the ImageSpec constructor, but not both.') else: - spec = type('Spec', (ImageSpec,), spec_args) # TODO: Base class name on spec id? + spec = type('Spec', (ImageSpec,), dict((k, v) for k, v in spec_args.items() if v is not None)) # TODO: Base class name on spec id? self._original_spec = spec From 570e7bd640b5b84aefb3ccb4c836cf5b6b08a22c Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Thu, 25 Oct 2012 22:31:37 -0400 Subject: [PATCH 092/213] Simplify SpecHost creation --- imagekit/specs/__init__.py | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/imagekit/specs/__init__.py b/imagekit/specs/__init__.py index 3878bc9..2d1ad2c 100644 --- a/imagekit/specs/__init__.py +++ b/imagekit/specs/__init__.py @@ -214,26 +214,16 @@ class SpecHost(object): spec registry. """ - def __init__(self, processors=None, format=None, options=None, - storage=None, autoconvert=None, image_cache_backend=None, - image_cache_strategy=None, spec=None, spec_id=None): + def __init__(self, spec=None, spec_id=None, **kwargs): - spec_args = dict( - processors=processors, - format=format, - options=options, - storage=storage, - autoconvert=autoconvert, - image_cache_backend=image_cache_backend, - image_cache_strategy=image_cache_strategy, - ) + spec_args = dict((k, v) for k, v in kwargs.items() if v is not None) - if any(v is not None for v in spec_args.values()): + if spec_args: if spec: raise TypeError('You can provide either an image spec or' ' arguments for the ImageSpec constructor, but not both.') else: - spec = type('Spec', (ImageSpec,), dict((k, v) for k, v in spec_args.items() if v is not None)) # TODO: Base class name on spec id? + spec = type('Spec', (ImageSpec,), spec_args) # TODO: Base class name on spec id? self._original_spec = spec From 6377f89e853b1c0beb29374f15c85105b255c3a0 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Thu, 25 Oct 2012 22:43:10 -0400 Subject: [PATCH 093/213] IKContentFile must have name attr --- imagekit/files.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/imagekit/files.py b/imagekit/files.py index 567468a..c781c35 100644 --- a/imagekit/files.py +++ b/imagekit/files.py @@ -148,6 +148,10 @@ class IKContentFile(ContentFile): mimetype = extension_to_mimetype(ext) self.file.content_type = mimetype + @property + def name(self): + return self.file.name + def __str__(self): return smart_str(self.file.name or '') From 56f8d1b8bcdc8f2fb837167a7b26ea816ddf95db Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Thu, 25 Oct 2012 22:46:28 -0400 Subject: [PATCH 094/213] Create form field class; re: #163 --- imagekit/forms/__init__.py | 1 + imagekit/forms/fields.py | 25 +++++++++++++++++++++++++ 2 files changed, 26 insertions(+) create mode 100644 imagekit/forms/__init__.py create mode 100644 imagekit/forms/fields.py diff --git a/imagekit/forms/__init__.py b/imagekit/forms/__init__.py new file mode 100644 index 0000000..8086e94 --- /dev/null +++ b/imagekit/forms/__init__.py @@ -0,0 +1 @@ +from .fields import ProcessedImageField diff --git a/imagekit/forms/fields.py b/imagekit/forms/fields.py new file mode 100644 index 0000000..19a4387 --- /dev/null +++ b/imagekit/forms/fields.py @@ -0,0 +1,25 @@ +from django.forms import ImageField +from ..specs import SpecHost + + +class ProcessedImageField(ImageField, SpecHost): + + def __init__(self, processors=None, format=None, options=None, + autoconvert=True, spec=None, spec_id=None, *args, **kwargs): + + if spec_id is None: + spec_id = '??????' # FIXME: Wher should we get this? + + SpecHost.__init__(self, processors=processors, format=format, + options=options, autoconvert=autoconvert, spec=spec, + spec_id=spec_id) + super(ProcessedImageField, self).__init__(*args, **kwargs) + + def clean(self, data, initial=None): + data = super(ProcessedImageField, self).clean(data, initial) + + if data: + spec = self.get_spec() # HINTS?!?!?!?!?! + data = spec.apply(data, data.name) + + return data From 64d95768f8192b11b8d390c198a2eee95ec82373 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Fri, 2 Nov 2012 00:27:29 -0400 Subject: [PATCH 095/213] Extract GeneratedImageCacheFile As mentioned in #167, we want to be forward thinking and allow for a hypothetical spec supertype which has the same functionality as an image spec but doesn't require a source file: a generator. To this end, I've renamed `ImageSpec.apply()` to `ImageSpec.generate()` and extracted a `GeneratedImageCacheFile` base class from `ImageSpecCacheFile`, which supports the more general interface of a generator--namely, a `generate()` method with arbitrary args and kwargs. --- imagekit/files.py | 119 ++++++++++++++++++++------------ imagekit/imagecache/backends.py | 2 +- imagekit/specs/__init__.py | 8 +-- 3 files changed, 81 insertions(+), 48 deletions(-) diff --git a/imagekit/files.py b/imagekit/files.py index c781c35..57b73d6 100644 --- a/imagekit/files.py +++ b/imagekit/files.py @@ -4,8 +4,9 @@ from django.core.files.images import ImageFile from django.utils.encoding import smart_str, smart_unicode from hashlib import md5 import os +import pickle from .signals import before_access -from .utils import (suggest_extension, format_to_mimetype, +from .utils import (suggest_extension, format_to_mimetype, format_to_extension, extension_to_mimetype, get_logger, get_singleton) @@ -71,63 +72,95 @@ class BaseIKFile(File): file.close() -class ImageSpecCacheFile(BaseIKFile, ImageFile): - def __init__(self, spec, source_file): - storage = (spec.storage or - get_singleton(settings.IMAGEKIT_DEFAULT_FILE_STORAGE, 'file storage backend') if settings.IMAGEKIT_DEFAULT_FILE_STORAGE - else source_file.storage) - super(ImageSpecCacheFile, self).__init__(storage=storage) - self.spec = spec - self.source_file = source_file +class GeneratedImageCacheFile(BaseIKFile, ImageFile): + """ + A cache file that represents the result of a generator. Creating an instance + of this class is not enough to trigger the creation of the cache file. In + fact, one of the main points of this class is to allow the creation of the + file to be deferred until the time that the image cache strategy requires + it. - def get_hash(self): - return md5(''.join([ - self.source_file.name, - self.spec.get_hash(), - ]).encode('utf-8')).hexdigest() + """ + def __init__(self, generator, *args, **kwargs): + """ + :param generator: The object responsible for generating a new image. + :param args: Positional arguments that will be passed to the generator's + ``generate()`` method when the generation is called for. + :param kwargs: Keyword arguments that will be apssed to the generator's + ``generate()`` method when the generation is called for. - def _require_file(self): - before_access.send(sender=self, spec=self.spec, file=self) - return super(ImageSpecCacheFile, self)._require_file() + """ + self.generator = generator + self.args = args + self.kwargs = kwargs + storage = getattr(generator, 'storage', None) + if not storage and settings.IMAGEKIT_DEFAULT_FILE_STORAGE: + storage = get_singleton(settings.IMAGEKIT_DEFAULT_FILE_STORAGE, + 'file storage backend') + super(GeneratedImageCacheFile, self).__init__(storage=storage) @property def name(self): - source_filename = self.source_file.name - filename = None - if source_filename: - hash = self.get_hash() - ext = suggest_extension(source_filename, self.spec.format) - filename = os.path.normpath(os.path.join( - settings.IMAGEKIT_CACHE_DIR, - os.path.splitext(source_filename)[0], - '%s%s' % (hash, ext))) + # FIXME: This won't work if args or kwargs contain a file object. It probably won't work in many other cases as well. Better option? + hash = md5(''.join([ + pickle.dumps(self.args), + pickle.dumps(self.kwargs), + self.generator.get_hash(), + ]).encode('utf-8')).hexdigest() + ext = format_to_extension(self.generator.format) + return os.path.join(settings.IMAGEKIT_CACHE_DIR, + '%s%s' % (hash, ext)) - return filename + def _require_file(self): + before_access.send(sender=self, generator=self.generator, file=self) + return super(GeneratedImageCacheFile, self)._require_file() def clear(self): - return self.spec.image_cache_backend.clear(self) + return self.generator.image_cache_backend.clear(self) def invalidate(self): - return self.spec.image_cache_backend.invalidate(self) + return self.generator.image_cache_backend.invalidate(self) def validate(self): - return self.spec.image_cache_backend.validate(self) + return self.generator.image_cache_backend.validate(self) def generate(self): - if self.source_file: # TODO: Should we error here or something if the source_file doesn't exist? - # Process the original image file. - content = self.spec.apply(self.source_file) - actual_name = self.storage.save(self.name, content) + # Generate the file + content = self.generator.generate(*self.args, **self.kwargs) + actual_name = self.storage.save(self.name, content) - if actual_name != self.name: - get_logger().warning('The storage backend %s did not save the file' - ' with the requested name ("%s") and instead used' - ' "%s". This may be because a file already existed with' - ' the requested name. If so, you may have meant to call' - ' validate() instead of generate(), or there may be a' - ' race condition in the image cache backend %s. The' - ' saved file will not be used.' % (self.storage, - self.name, actual_name, self.spec.image_cache_backend)) + if actual_name != self.name: + get_logger().warning('The storage backend %s did not save the file' + ' with the requested name ("%s") and instead used' + ' "%s". This may be because a file already existed with' + ' the requested name. If so, you may have meant to call' + ' validate() instead of generate(), or there may be a' + ' race condition in the image cache backend %s. The' + ' saved file will not be used.' % (self.storage, + self.name, actual_name, + self.generator.image_cache_backend)) + + +class ImageSpecCacheFile(GeneratedImageCacheFile): + def __init__(self, generator, source_file): + super(ImageSpecCacheFile, self).__init__(generator, + source_file=source_file) + if not self.storage: + self.storage = source_file.storage + + @property + def name(self): + source_filename = self.kwargs['source_file'].name + hash = md5(''.join([ + source_filename, + self.generator.get_hash(), + ]).encode('utf-8')).hexdigest() + # TODO: Since specs can now be dynamically generated using hints, can we move this into the spec constructor? i.e. set self.format if not defined. This would get us closer to making ImageSpecCacheFile == GeneratedImageCacheFile + ext = suggest_extension(source_filename, self.generator.format) + return os.path.normpath(os.path.join( + settings.IMAGEKIT_CACHE_DIR, + os.path.splitext(source_filename)[0], + '%s%s' % (hash, ext))) class IKContentFile(ContentFile): diff --git a/imagekit/imagecache/backends.py b/imagekit/imagecache/backends.py index 922b1f4..c31002f 100644 --- a/imagekit/imagecache/backends.py +++ b/imagekit/imagecache/backends.py @@ -27,7 +27,7 @@ class CachedValidationBackend(object): def get_key(self, file): from django.conf import settings - return '%s%s-valid' % (settings.IMAGEKIT_CACHE_PREFIX, file.get_hash()) + return '%s%s-valid' % (settings.IMAGEKIT_CACHE_PREFIX, file.name) def is_invalid(self, file): key = self.get_key(file) diff --git a/imagekit/specs/__init__.py b/imagekit/specs/__init__.py index 2d1ad2c..2d9b270 100644 --- a/imagekit/specs/__init__.py +++ b/imagekit/specs/__init__.py @@ -88,8 +88,8 @@ class SpecRegistry(object): def get_sources(self, spec_id): return [source for source in self._sources if spec_id in self._sources[source]] - def before_access_receiver(self, sender, spec, file, **kwargs): - spec.image_cache_strategy.invoke_callback('before_access', file) + def before_access_receiver(self, sender, generator, file, **kwargs): + generator.image_cache_strategy.invoke_callback('before_access', file) def source_receiver(self, sender, source_file, signal, info, **kwargs): """ @@ -150,8 +150,8 @@ class BaseImageSpec(object): str(self.autoconvert), ]).encode('utf-8')).hexdigest() - def apply(self, content, filename=None): - img = open_image(content) + def generate(self, source_file, filename=None): + img = open_image(source_file) original_format = img.format # Run the processors From e56d687bb0bd9c42c21507107c875ed6d1576453 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Fri, 2 Nov 2012 22:17:25 -0400 Subject: [PATCH 096/213] Name can be set explicitly on GeneratedImageCacheFile --- imagekit/files.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/imagekit/files.py b/imagekit/files.py index 57b73d6..fddcce2 100644 --- a/imagekit/files.py +++ b/imagekit/files.py @@ -81,7 +81,7 @@ class GeneratedImageCacheFile(BaseIKFile, ImageFile): it. """ - def __init__(self, generator, *args, **kwargs): + def __init__(self, generator, name=None, *args, **kwargs): """ :param generator: The object responsible for generating a new image. :param args: Positional arguments that will be passed to the generator's @@ -90,6 +90,7 @@ class GeneratedImageCacheFile(BaseIKFile, ImageFile): ``generate()`` method when the generation is called for. """ + self._name = name self.generator = generator self.args = args self.kwargs = kwargs @@ -99,8 +100,7 @@ class GeneratedImageCacheFile(BaseIKFile, ImageFile): 'file storage backend') super(GeneratedImageCacheFile, self).__init__(storage=storage) - @property - def name(self): + def get_default_filename(self): # FIXME: This won't work if args or kwargs contain a file object. It probably won't work in many other cases as well. Better option? hash = md5(''.join([ pickle.dumps(self.args), @@ -111,6 +111,14 @@ class GeneratedImageCacheFile(BaseIKFile, ImageFile): return os.path.join(settings.IMAGEKIT_CACHE_DIR, '%s%s' % (hash, ext)) + def _get_name(self): + return self._name or self.get_default_filename() + + def _set_name(self, value): + self._name = value + + name = property(_get_name, _set_name) + def _require_file(self): before_access.send(sender=self, generator=self.generator, file=self) return super(GeneratedImageCacheFile, self)._require_file() @@ -148,8 +156,7 @@ class ImageSpecCacheFile(GeneratedImageCacheFile): if not self.storage: self.storage = source_file.storage - @property - def name(self): + def get_default_filename(self): source_filename = self.kwargs['source_file'].name hash = md5(''.join([ source_filename, From 5494ee7fc148acd1e75714b130551632a7380373 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Sat, 3 Nov 2012 00:08:47 -0400 Subject: [PATCH 097/213] Clarify relationship between BaseImageSpec and ImageSpec --- imagekit/specs/__init__.py | 80 +++++++++++++++++++++++--------------- 1 file changed, 48 insertions(+), 32 deletions(-) diff --git a/imagekit/specs/__init__.py b/imagekit/specs/__init__.py index 2d9b270..36cb102 100644 --- a/imagekit/specs/__init__.py +++ b/imagekit/specs/__init__.py @@ -111,6 +111,52 @@ class SpecRegistry(object): class BaseImageSpec(object): + """ + An object that defines how an new image should be generated from a source + image. + + """ + + storage = None + """A Django storage system to use to save the generated image.""" + + image_cache_backend = None + """ + An object responsible for managing the state of cached files. Defaults to an + instance of ``IMAGEKIT_DEFAULT_IMAGE_CACHE_BACKEND`` + + """ + + image_cache_strategy = settings.IMAGEKIT_DEFAULT_IMAGE_CACHE_STRATEGY + """ + A dictionary containing callbacks that allow you to customize how and when + the image cache is validated. Defaults to + ``IMAGEKIT_DEFAULT_SPEC_FIELD_IMAGE_CACHE_STRATEGY``. + + """ + + def __init__(self, **kwargs): + self.image_cache_backend = self.image_cache_backend or get_default_image_cache_backend() + self.image_cache_strategy = StrategyWrapper(self.image_cache_strategy) + + def get_hash(self): + raise NotImplementedError + + def generate(self, source_file, filename=None): + raise NotImplementedError + + # TODO: I don't like this interface. Is there a standard Python one? pubsub? + def _handle_source_event(self, event_name, source_file): + file = ImageSpecCacheFile(self, source_file) + self.image_cache_strategy.invoke_callback('on_%s' % event_name, file) + + +class ImageSpec(BaseImageSpec): + """ + An object that defines how to generate a new image from a source file using + PIL-based processors. (See :mod:`imagekit.processors`) + + """ processors = None """A list of processors to run on the original image.""" @@ -139,8 +185,9 @@ class BaseImageSpec(object): """ - def __init__(self): + def __init__(self, **kwargs): self.processors = self.processors or [] + super(ImageSpec, self).__init__() def get_hash(self): return md5(''.join([ @@ -177,37 +224,6 @@ class BaseImageSpec(object): return content -class ImageSpec(BaseImageSpec): - - storage = None - """A Django storage system to use to save the generated image.""" - - image_cache_backend = None - """ - An object responsible for managing the state of cached files. Defaults to an - instance of ``IMAGEKIT_DEFAULT_IMAGE_CACHE_BACKEND`` - - """ - - image_cache_strategy = settings.IMAGEKIT_DEFAULT_IMAGE_CACHE_STRATEGY - """ - A dictionary containing callbacks that allow you to customize how and when - the image cache is validated. Defaults to - ``IMAGEKIT_DEFAULT_SPEC_FIELD_IMAGE_CACHE_STRATEGY``. - - """ - - def __init__(self, **kwargs): - super(ImageSpec, self).__init__() - self.image_cache_backend = self.image_cache_backend or get_default_image_cache_backend() - self.image_cache_strategy = StrategyWrapper(self.image_cache_strategy) - - # TODO: I don't like this interface. Is there a standard Python one? pubsub? - def _handle_source_event(self, event_name, source_file): - file = ImageSpecCacheFile(self, source_file) - self.image_cache_strategy.invoke_callback('on_%s' % event_name, file) - - class SpecHost(object): """ An object that ostensibly has a spec attribute but really delegates to the From f9e2ce864916dca725954113716955b9b23a313f Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Sat, 3 Nov 2012 00:27:03 -0400 Subject: [PATCH 098/213] Add TODO --- imagekit/specs/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/imagekit/specs/__init__.py b/imagekit/specs/__init__.py index 36cb102..ae1ba65 100644 --- a/imagekit/specs/__init__.py +++ b/imagekit/specs/__init__.py @@ -220,6 +220,7 @@ class ImageSpec(BaseImageSpec): format = format or img.format or original_format or 'JPEG' imgfile = img_to_fobj(img, format, **options) + # TODO: Is this the right place to wrap the file? Can we use a mixin instead? Is it even still having the desired effect? Re: #111 content = IKContentFile(filename, imgfile.read(), format=format) return content From 8266099ae8ba0ff3488acf30f93557da0681da3e Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Mon, 5 Nov 2012 21:33:05 -0500 Subject: [PATCH 099/213] Clean up tests dir --- Makefile | 6 ++++++ tests/{core => }/assets/Lenna.png | Bin .../assets/lenna-800x600-white-border.jpg | Bin tests/{core => }/assets/lenna-800x600.jpg | Bin tests/core/__init__.py | 0 tests/{core => }/models.py | 0 tests/run_tests.sh | 6 ------ tests/settings.py | 2 +- tests/{core => }/tests.py | 2 +- tests/{core/testutils.py => utils.py} | 0 tox.ini | 4 +--- 11 files changed, 9 insertions(+), 11 deletions(-) create mode 100644 Makefile rename tests/{core => }/assets/Lenna.png (100%) rename tests/{core => }/assets/lenna-800x600-white-border.jpg (100%) rename tests/{core => }/assets/lenna-800x600.jpg (100%) delete mode 100644 tests/core/__init__.py rename tests/{core => }/models.py (100%) delete mode 100755 tests/run_tests.sh rename tests/{core => }/tests.py (98%) rename tests/{core/testutils.py => utils.py} (100%) diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..65cd591 --- /dev/null +++ b/Makefile @@ -0,0 +1,6 @@ +test: + export PYTHONPATH=$(PWD):$(PYTHONPATH); \ + django-admin.py test --settings=tests.settings tests + + +.PHONY: test diff --git a/tests/core/assets/Lenna.png b/tests/assets/Lenna.png similarity index 100% rename from tests/core/assets/Lenna.png rename to tests/assets/Lenna.png diff --git a/tests/core/assets/lenna-800x600-white-border.jpg b/tests/assets/lenna-800x600-white-border.jpg similarity index 100% rename from tests/core/assets/lenna-800x600-white-border.jpg rename to tests/assets/lenna-800x600-white-border.jpg diff --git a/tests/core/assets/lenna-800x600.jpg b/tests/assets/lenna-800x600.jpg similarity index 100% rename from tests/core/assets/lenna-800x600.jpg rename to tests/assets/lenna-800x600.jpg diff --git a/tests/core/__init__.py b/tests/core/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/tests/core/models.py b/tests/models.py similarity index 100% rename from tests/core/models.py rename to tests/models.py diff --git a/tests/run_tests.sh b/tests/run_tests.sh deleted file mode 100755 index 6d3f37b..0000000 --- a/tests/run_tests.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/bin/bash -PYTHONPATH=$PWD:$PWD/..${PYTHONPATH:+:$PYTHONPATH} -export PYTHONPATH - -echo "Running django-imagekit tests..." -django-admin.py test core --settings=settings diff --git a/tests/settings.py b/tests/settings.py index 11113f6..3272aee 100644 --- a/tests/settings.py +++ b/tests/settings.py @@ -25,7 +25,7 @@ INSTALLED_APPS = [ 'django.contrib.auth', 'django.contrib.contenttypes', 'imagekit', - 'core', + 'tests', 'django_nose', ] diff --git a/tests/core/tests.py b/tests/tests.py similarity index 98% rename from tests/core/tests.py rename to tests/tests.py index 4998ce5..7afeb89 100644 --- a/tests/core/tests.py +++ b/tests/tests.py @@ -8,7 +8,7 @@ from imagekit import utils from imagekit.exceptions import UnknownFormatError from .models import (Photo, AbstractImageModel, ConcreteImageModel1, ConcreteImageModel2) -from .testutils import create_photo, pickleback +from .utils import create_photo, pickleback class IKTest(TestCase): diff --git a/tests/core/testutils.py b/tests/utils.py similarity index 100% rename from tests/core/testutils.py rename to tests/utils.py diff --git a/tox.ini b/tox.ini index 72bf206..383c1f8 100644 --- a/tox.ini +++ b/tox.ini @@ -4,9 +4,7 @@ envlist = py26-django14, py26-django13, py26-django12 [testenv] -changedir = tests -setenv = PYTHONPATH = {toxinidir}/tests -commands = django-admin.py test core --settings=settings +commands = make test [testenv:py27-django14] basepython = python2.7 From 56f1ccb8a8e7baa8f54b527a569688eb8431a4de Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Mon, 5 Nov 2012 21:54:15 -0500 Subject: [PATCH 100/213] Separate test_utils module --- tests/test_utils.py | 23 +++++++++++++++++++++++ tests/tests.py | 18 ------------------ 2 files changed, 23 insertions(+), 18 deletions(-) create mode 100644 tests/test_utils.py diff --git a/tests/test_utils.py b/tests/test_utils.py new file mode 100644 index 0000000..f4c777e --- /dev/null +++ b/tests/test_utils.py @@ -0,0 +1,23 @@ +from imagekit.exceptions import UnknownFormatError, UnknownExtensionError +from imagekit.utils import extension_to_format, format_to_extension +from nose.tools import eq_, raises + + +def test_extension_to_format(): + eq_(extension_to_format('.jpeg'), 'JPEG') + eq_(extension_to_format('.rgba'), 'SGI') + + +def test_format_to_extension_no_init(): + eq_(format_to_extension('PNG'), '.png') + eq_(format_to_extension('ICO'), '.ico') + + +@raises(UnknownFormatError) +def test_unknown_format(): + format_to_extension('TXT') + + +@raises(UnknownExtensionError) +def test_unknown_extension(): + extension_to_format('.txt') diff --git a/tests/tests.py b/tests/tests.py index 7afeb89..9dab0c8 100644 --- a/tests/tests.py +++ b/tests/tests.py @@ -4,8 +4,6 @@ import os from django.test import TestCase -from imagekit import utils -from imagekit.exceptions import UnknownFormatError from .models import (Photo, AbstractImageModel, ConcreteImageModel1, ConcreteImageModel2) from .utils import create_photo, pickleback @@ -48,22 +46,6 @@ class IKTest(TestCase): self.photo.thumbnail.source_file, self.photo.original_image) -class IKUtilsTest(TestCase): - def test_extension_to_format(self): - self.assertEqual(utils.extension_to_format('.jpeg'), 'JPEG') - self.assertEqual(utils.extension_to_format('.rgba'), 'SGI') - - with self.assertRaises(utils.UnknownExtensionError): - utils.extension_to_format('.txt') - - def test_format_to_extension_no_init(self): - self.assertEqual(utils.format_to_extension('PNG'), '.png') - self.assertEqual(utils.format_to_extension('ICO'), '.ico') - - with self.assertRaises(UnknownFormatError): - utils.format_to_extension('TXT') - - class PickleTest(TestCase): def test_model(self): ph = pickleback(create_photo('pickletest.jpg')) From 6255b93b78f9275f73b25c4ec0dd67192ed294ff Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Mon, 5 Nov 2012 21:56:05 -0500 Subject: [PATCH 101/213] Add some processor tests --- tests/test_processors.py | 31 +++++++++++++++++++++++++++++++ tests/utils.py | 16 +++++++++------- 2 files changed, 40 insertions(+), 7 deletions(-) create mode 100644 tests/test_processors.py diff --git a/tests/test_processors.py b/tests/test_processors.py new file mode 100644 index 0000000..316b57f --- /dev/null +++ b/tests/test_processors.py @@ -0,0 +1,31 @@ +from imagekit.lib import Image +from imagekit.processors import ResizeToFill, ResizeToFit, SmartCrop +from nose.tools import eq_ +from .utils import create_image + + +def test_smartcrop(): + img = SmartCrop(100, 100).process(create_image()) + eq_(img.size, (100, 100)) + + +def test_resizetofill(): + img = ResizeToFill(100, 100).process(create_image()) + eq_(img.size, (100, 100)) + + +def test_resizetofit(): + # First create an image with aspect ratio 2:1... + img = Image.new('RGB', (200, 100)) + + # ...then resize it to fit within a 100x100 canvas. + img = ResizeToFit(100, 100).process(img) + + # Assert that the image has maintained the aspect ratio. + eq_(img.size, (100, 50)) + + +def test_resizetofit_mat(): + img = Image.new('RGB', (200, 100)) + img = ResizeToFit(100, 100, mat_color=0x000000).process(img) + eq_(img.size, (100, 100)) diff --git a/tests/utils.py b/tests/utils.py index 4acc13c..746920f 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -1,5 +1,4 @@ import os -import tempfile from django.core.files.base import ContentFile @@ -8,7 +7,7 @@ from .models import Photo import pickle -def generate_lenna(): +def get_image_file(): """ See also: @@ -16,17 +15,20 @@ def generate_lenna(): http://sipi.usc.edu/database/database.php?volume=misc&image=12 """ - tmp = tempfile.TemporaryFile() - lennapath = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'assets', 'lenna-800x600-white-border.jpg') - with open(lennapath, "r+b") as lennafile: - Image.open(lennafile).save(tmp, 'JPEG') + path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'assets', 'lenna-800x600-white-border.jpg') + tmp = StringIO() + tmp.write(open(path, 'r+b').read()) tmp.seek(0) return tmp +def create_image(): + return Image.open(get_image_file()) + + def create_instance(model_class, image_name): instance = model_class() - img = generate_lenna() + img = get_image_file() file = ContentFile(img.read()) instance.original_image = file instance.original_image.save(image_name, file) From c752eea6a0b60955d13a042eb4124ed6d5d996a8 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Mon, 5 Nov 2012 22:22:34 -0500 Subject: [PATCH 102/213] Fix bug with black mat_color --- imagekit/processors/resize.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/imagekit/processors/resize.py b/imagekit/processors/resize.py index e4b747a..5b211b3 100644 --- a/imagekit/processors/resize.py +++ b/imagekit/processors/resize.py @@ -215,6 +215,6 @@ class ResizeToFit(object): self.upscale: img = Resize(new_dimensions[0], new_dimensions[1]).process(img) - if self.mat_color: + if self.mat_color is not None: img = ResizeCanvas(self.width, self.height, self.mat_color, anchor=self.anchor).process(img) return img From c3f7aeca544191aebf324345c9fb7199b3bb39de Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Mon, 5 Nov 2012 22:44:00 -0500 Subject: [PATCH 103/213] Separate serialization test --- tests/test_serialization.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 tests/test_serialization.py diff --git a/tests/test_serialization.py b/tests/test_serialization.py new file mode 100644 index 0000000..76d5391 --- /dev/null +++ b/tests/test_serialization.py @@ -0,0 +1,13 @@ +""" +Make sure that the various IK classes can be successfully serialized and +deserialized. This is important when using IK with Celery. + +""" + +from .utils import create_photo, pickleback + + +def test_imagespecfield(): + instance = create_photo('pickletest2.jpg') + thumbnail = pickleback(instance.thumbnail) + thumbnail.source_file From 9d310ba57e5dca3d3fd87434b799a698d211739d Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Mon, 5 Nov 2012 22:45:10 -0500 Subject: [PATCH 104/213] Remove old class-based tests --- tests/tests.py | 70 -------------------------------------------------- 1 file changed, 70 deletions(-) delete mode 100644 tests/tests.py diff --git a/tests/tests.py b/tests/tests.py deleted file mode 100644 index 9dab0c8..0000000 --- a/tests/tests.py +++ /dev/null @@ -1,70 +0,0 @@ -from __future__ import with_statement - -import os - -from django.test import TestCase - -from .models import (Photo, AbstractImageModel, ConcreteImageModel1, - ConcreteImageModel2) -from .utils import create_photo, pickleback - - -class IKTest(TestCase): - - def setUp(self): - self.photo = create_photo('test.jpg') - - def test_nodelete(self): - """Don't delete the spec file when the source image hasn't changed. - - """ - filename = self.photo.thumbnail.file.name - self.photo.save() - self.assertTrue(self.photo.thumbnail.storage.exists(filename)) - - def test_save_image(self): - photo = Photo.objects.get(id=self.photo.id) - self.assertTrue(os.path.isfile(photo.original_image.path)) - - def test_setup(self): - self.assertEqual(self.photo.original_image.width, 800) - self.assertEqual(self.photo.original_image.height, 600) - - def test_thumbnail_creation(self): - photo = Photo.objects.get(id=self.photo.id) - self.assertTrue(os.path.isfile(photo.thumbnail.file.name)) - - def test_thumbnail_size(self): - """ Explicit and smart-cropped thumbnail size """ - self.assertEqual(self.photo.thumbnail.width, 50) - self.assertEqual(self.photo.thumbnail.height, 50) - self.assertEqual(self.photo.smartcropped_thumbnail.width, 50) - self.assertEqual(self.photo.smartcropped_thumbnail.height, 50) - - def test_thumbnail_source_file(self): - self.assertEqual( - self.photo.thumbnail.source_file, self.photo.original_image) - - -class PickleTest(TestCase): - def test_model(self): - ph = pickleback(create_photo('pickletest.jpg')) - - # This isn't supposed to error. - ph.thumbnail.source_file - - def test_field(self): - thumbnail = pickleback(create_photo('pickletest2.jpg').thumbnail) - - # This isn't supposed to error. - thumbnail.source_file - - -class InheritanceTest(TestCase): - def test_abstract_base(self): - self.assertEqual(set(AbstractImageModel._ik.spec_fields), - set(['abstract_class_spec'])) - self.assertEqual(set(ConcreteImageModel1._ik.spec_fields), - set(['abstract_class_spec', 'first_spec'])) - self.assertEqual(set(ConcreteImageModel2._ik.spec_fields), - set(['abstract_class_spec', 'second_spec'])) From aaa823afd6772ac59414a95b79613c02220fac36 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Mon, 5 Nov 2012 23:34:32 -0500 Subject: [PATCH 105/213] Add flake8 linting --- Makefile | 1 + imagekit/__init__.py | 2 ++ imagekit/admin.py | 2 +- imagekit/forms/__init__.py | 2 ++ imagekit/imagecache/actions.py | 2 +- imagekit/imagecache/strategies.py | 1 - imagekit/lib.py | 2 ++ imagekit/management/commands/warmimagecache.py | 1 - imagekit/models/__init__.py | 2 ++ imagekit/models/fields/__init__.py | 10 +++++----- imagekit/models/fields/utils.py | 12 ++++++------ imagekit/processors/__init__.py | 2 ++ imagekit/processors/crop.py | 2 +- imagekit/processors/resize.py | 7 ++----- imagekit/templatetags/imagekit.py | 2 +- 15 files changed, 28 insertions(+), 22 deletions(-) diff --git a/Makefile b/Makefile index 65cd591..3ec6c46 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,5 @@ test: + flake8 --ignore=E501,E126,E127,E128 imagekit tests export PYTHONPATH=$(PWD):$(PYTHONPATH); \ django-admin.py test --settings=tests.settings tests diff --git a/imagekit/__init__.py b/imagekit/__init__.py index 74808d3..747ce27 100644 --- a/imagekit/__init__.py +++ b/imagekit/__init__.py @@ -1,3 +1,5 @@ +# flake8: noqa + from . import conf from .specs import ImageSpec from .pkgmeta import * diff --git a/imagekit/admin.py b/imagekit/admin.py index 4466e6e..7a9e145 100644 --- a/imagekit/admin.py +++ b/imagekit/admin.py @@ -27,7 +27,7 @@ class AdminThumbnail(object): try: thumbnail = getattr(obj, self.image_field) except AttributeError: - raise Exception('The property %s is not defined on %s.' % \ + raise Exception('The property %s is not defined on %s.' % (self.image_field, obj.__class__.__name__)) original_image = getattr(thumbnail, 'source_file', None) or thumbnail diff --git a/imagekit/forms/__init__.py b/imagekit/forms/__init__.py index 8086e94..f7310d1 100644 --- a/imagekit/forms/__init__.py +++ b/imagekit/forms/__init__.py @@ -1 +1,3 @@ +# flake8: noqa + from .fields import ProcessedImageField diff --git a/imagekit/imagecache/actions.py b/imagekit/imagecache/actions.py index 2e222b3..b829a5f 100644 --- a/imagekit/imagecache/actions.py +++ b/imagekit/imagecache/actions.py @@ -12,7 +12,7 @@ else: def deferred_validate(file): try: - import celery + import celery # NOQA except: raise ImportError("Deferred validation requires the the 'celery' library") validate_now_task.delay(file) diff --git a/imagekit/imagecache/strategies.py b/imagekit/imagecache/strategies.py index 3eca747..630b77c 100644 --- a/imagekit/imagecache/strategies.py +++ b/imagekit/imagecache/strategies.py @@ -54,6 +54,5 @@ class StrategyWrapper(object): def __unicode__(self): return unicode(self._wrapped) - def __str__(self): return str(self._wrapped) diff --git a/imagekit/lib.py b/imagekit/lib.py index 574e587..7137e08 100644 --- a/imagekit/lib.py +++ b/imagekit/lib.py @@ -1,3 +1,5 @@ +# flake8: noqa + # Required PIL classes may or may not be available from the root namespace # depending on the installation method used. try: diff --git a/imagekit/management/commands/warmimagecache.py b/imagekit/management/commands/warmimagecache.py index 8c9efcb..8c25aaa 100644 --- a/imagekit/management/commands/warmimagecache.py +++ b/imagekit/management/commands/warmimagecache.py @@ -1,4 +1,3 @@ -from optparse import make_option from django.core.management.base import BaseCommand import re from ...files import ImageSpecCacheFile diff --git a/imagekit/models/__init__.py b/imagekit/models/__init__.py index 4c3dad7..b13b38f 100644 --- a/imagekit/models/__init__.py +++ b/imagekit/models/__init__.py @@ -1,2 +1,4 @@ +# flake8: noqa + from .. import conf from .fields import ImageSpecField, ProcessedImageField diff --git a/imagekit/models/fields/__init__.py b/imagekit/models/fields/__init__.py index 3f98f35..d7e4d8a 100644 --- a/imagekit/models/fields/__init__.py +++ b/imagekit/models/fields/__init__.py @@ -26,9 +26,9 @@ class ImageSpecField(SpecHostField): """ def __init__(self, processors=None, format=None, options=None, - image_field=None, storage=None, autoconvert=None, - image_cache_backend=None, image_cache_strategy=None, spec=None, - id=None): + image_field=None, storage=None, autoconvert=None, + image_cache_backend=None, image_cache_strategy=None, spec=None, + id=None): SpecHost.__init__(self, processors=processors, format=format, options=options, storage=storage, autoconvert=autoconvert, @@ -58,8 +58,8 @@ class ProcessedImageField(models.ImageField, SpecHostField): attr_class = ProcessedImageFieldFile def __init__(self, processors=None, format=None, options=None, - verbose_name=None, name=None, width_field=None, height_field=None, - autoconvert=True, spec=None, spec_id=None, **kwargs): + verbose_name=None, name=None, width_field=None, height_field=None, + autoconvert=True, spec=None, spec_id=None, **kwargs): """ The ProcessedImageField constructor accepts all of the arguments that the :class:`django.db.models.ImageField` constructor accepts, as well diff --git a/imagekit/models/fields/utils.py b/imagekit/models/fields/utils.py index 0695212..3043016 100644 --- a/imagekit/models/fields/utils.py +++ b/imagekit/models/fields/utils.py @@ -15,16 +15,16 @@ class ImageSpecFileDescriptor(object): if field_name: source_file = getattr(instance, field_name) else: - image_fields = [getattr(instance, f.attname) for f in \ - instance.__class__._meta.fields if \ + image_fields = [getattr(instance, f.attname) for f in + instance.__class__._meta.fields if isinstance(f, ImageField)] if len(image_fields) == 0: - raise Exception('%s does not define any ImageFields, so your' \ - ' %s ImageSpecField has no image to act on.' % \ + raise Exception('%s does not define any ImageFields, so your' + ' %s ImageSpecField has no image to act on.' % (instance.__class__.__name__, self.attname)) elif len(image_fields) > 1: - raise Exception('%s defines multiple ImageFields, but you' \ - ' have not specified an image_field for your %s' \ + raise Exception('%s defines multiple ImageFields, but you' + ' have not specified an image_field for your %s' ' ImageSpecField.' % (instance.__class__.__name__, self.attname)) else: diff --git a/imagekit/processors/__init__.py b/imagekit/processors/__init__.py index c2c9320..6c8d0b7 100644 --- a/imagekit/processors/__init__.py +++ b/imagekit/processors/__init__.py @@ -1,3 +1,5 @@ +# flake8: noqa + """ Imagekit image processors. diff --git a/imagekit/processors/crop.py b/imagekit/processors/crop.py index da5c0fb..b039d30 100644 --- a/imagekit/processors/crop.py +++ b/imagekit/processors/crop.py @@ -1,4 +1,4 @@ -from .base import Anchor +from .base import Anchor # noqa from .utils import histogram_entropy from ..lib import Image, ImageChops, ImageDraw, ImageStat diff --git a/imagekit/processors/resize.py b/imagekit/processors/resize.py index 5b211b3..fea3d51 100644 --- a/imagekit/processors/resize.py +++ b/imagekit/processors/resize.py @@ -1,5 +1,4 @@ from imagekit.lib import Image -import warnings from .base import Anchor @@ -211,10 +210,8 @@ class ResizeToFit(object): ratio = float(self.width) / cur_width new_dimensions = (int(round(cur_width * ratio)), int(round(cur_height * ratio))) - if (cur_width > new_dimensions[0] or cur_height > new_dimensions[1]) or \ - self.upscale: - img = Resize(new_dimensions[0], - new_dimensions[1]).process(img) + if (cur_width > new_dimensions[0] or cur_height > new_dimensions[1]) or self.upscale: + img = Resize(new_dimensions[0], new_dimensions[1]).process(img) if self.mat_color is not None: img = ResizeCanvas(self.width, self.height, self.mat_color, anchor=self.anchor).process(img) return img diff --git a/imagekit/templatetags/imagekit.py b/imagekit/templatetags/imagekit.py index b0b981b..ade02d8 100644 --- a/imagekit/templatetags/imagekit.py +++ b/imagekit/templatetags/imagekit.py @@ -110,7 +110,7 @@ def spec(parser, token): """ bits = token.split_contents() - tag_name = bits.pop(0) + tag_name = bits.pop(0) # noqa if len(bits) == 4 and bits[2] == 'as': return SpecResultAssignmentNode( From 7532e5040bd9487ddc4d74dcac22209b2fee33de Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Tue, 6 Nov 2012 00:40:14 -0500 Subject: [PATCH 106/213] Add contributing guidelines --- CONTRIBUTING.rst | 24 ++++++++++++++++++++++++ README.rst | 10 ++++++++++ 2 files changed, 34 insertions(+) create mode 100644 CONTRIBUTING.rst diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst new file mode 100644 index 0000000..875ec2c --- /dev/null +++ b/CONTRIBUTING.rst @@ -0,0 +1,24 @@ +Contributing +------------ + +We love contributions! These guidelines will help make sure we can get your +contributions merged as quickly as possible: + +1. Write `good commit messages`__! +2. If you want to add a new feature, talk to us on the `mailing list`__ or + `IRC`__ first. We might already have plans, or be able to offer some advice. +3. Make sure your code passes the tests that ImageKit already has. To run the + tests, use ``make test``. This will let you know about any errors or style + issues. +4. While we're talking about tests, creating new ones for your code makes it + much easier for us to merge your code quickly. ImageKit uses nose_, so + writing tests is painless. Check out `ours`__ for examples. +5. It's a good idea to do your work in a branch; that way, you can work on more + than one contribution at a time without making them interdependent. + + +__ http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html +__ https://groups.google.com/forum/#!forum/django-imagekit +__ irc://irc.freenode.net/imagekit +.. _nose: https://nose.readthedocs.org/en/latest/ +__ https://github.com/jdriscoll/django-imagekit/tree/develop/tests diff --git a/README.rst b/README.rst index 242562a..1eea912 100644 --- a/README.rst +++ b/README.rst @@ -295,3 +295,13 @@ even Django—to contribute either: ImageKit's processors are standalone classes that are completely separate from the more intimidating internals of Django's ORM. If you've written a processor that you think might be useful to other people, open a pull request so we can take a look! + +You can also check out our list of `open, contributor-friendly issues`__ for +ideas. + +Check out our `contributing guidelines`__ for more information about pitching in +with ImageKit. + + +__ https://github.com/jdriscoll/django-imagekit/issues?labels=contributor-friendly&state=open +__ https://github.com/jdriscoll/django-imagekit/blob/master/CONTRIBUTING.rst From 5a414a3644006440e82d03b03057b39dc5e70037 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Tue, 6 Nov 2012 23:50:23 -0500 Subject: [PATCH 107/213] Rename spec_args to spec_attrs --- imagekit/specs/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/imagekit/specs/__init__.py b/imagekit/specs/__init__.py index ae1ba65..94810d0 100644 --- a/imagekit/specs/__init__.py +++ b/imagekit/specs/__init__.py @@ -233,14 +233,14 @@ class SpecHost(object): """ def __init__(self, spec=None, spec_id=None, **kwargs): - spec_args = dict((k, v) for k, v in kwargs.items() if v is not None) + spec_attrs = dict((k, v) for k, v in kwargs.items() if v is not None) - if spec_args: + if spec_attrs: if spec: raise TypeError('You can provide either an image spec or' ' arguments for the ImageSpec constructor, but not both.') else: - spec = type('Spec', (ImageSpec,), spec_args) # TODO: Base class name on spec id? + spec = type('Spec', (ImageSpec,), spec_attrs) # TODO: Base class name on spec id? self._original_spec = spec From 49a55d4763994dce12ddc7aab6e9d744e9432eb4 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Sat, 1 Dec 2012 14:11:04 -0500 Subject: [PATCH 108/213] Try adding __reduce__ --- imagekit/specs/__init__.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/imagekit/specs/__init__.py b/imagekit/specs/__init__.py index 94810d0..6791aef 100644 --- a/imagekit/specs/__init__.py +++ b/imagekit/specs/__init__.py @@ -225,6 +225,17 @@ class ImageSpec(BaseImageSpec): return content +class DynamicSpec(ImageSpec): + def __reduce__(self): + return (create_spec_class, (self._spec_attrs,)) + + +def create_spec_class(spec_attrs): + cls = type('Spec', (DynamicSpec,), spec_attrs) + cls._spec_attrs = spec_attrs + return cls + + class SpecHost(object): """ An object that ostensibly has a spec attribute but really delegates to the @@ -240,7 +251,8 @@ class SpecHost(object): raise TypeError('You can provide either an image spec or' ' arguments for the ImageSpec constructor, but not both.') else: - spec = type('Spec', (ImageSpec,), spec_attrs) # TODO: Base class name on spec id? + # spec = type('Spec', (ImageSpec,), spec_attrs) # TODO: Base class name on spec id? + spec = create_spec_class(spec_attrs) self._original_spec = spec From 5b1c5f7b4e505bfff0a5c834715881055700bb4a Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Sat, 1 Dec 2012 15:51:28 -0500 Subject: [PATCH 109/213] Rename source objects to source groups --- imagekit/models/fields/__init__.py | 4 ++-- imagekit/specs/__init__.py | 2 +- imagekit/specs/{sources.py => sourcegroups.py} | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) rename imagekit/specs/{sources.py => sourcegroups.py} (98%) diff --git a/imagekit/models/fields/__init__.py b/imagekit/models/fields/__init__.py index d7e4d8a..153e793 100644 --- a/imagekit/models/fields/__init__.py +++ b/imagekit/models/fields/__init__.py @@ -3,7 +3,7 @@ from .files import ProcessedImageFieldFile from .utils import ImageSpecFileDescriptor from ... import specs from ...specs import SpecHost -from ...specs.sources import ImageFieldSpecSource +from ...specs.sourcegroups import ImageFieldSourceGroup class SpecHostField(SpecHost): @@ -44,7 +44,7 @@ class ImageSpecField(SpecHostField): # Add the model and field as a source for this spec id specs.registry.add_sources(self.spec_id, - [ImageFieldSpecSource(cls, self.image_field)]) + [ImageFieldSourceGroup(cls, self.image_field)]) class ProcessedImageField(models.ImageField, SpecHostField): diff --git a/imagekit/specs/__init__.py b/imagekit/specs/__init__.py index 6791aef..7d5c0fc 100644 --- a/imagekit/specs/__init__.py +++ b/imagekit/specs/__init__.py @@ -53,7 +53,7 @@ class SpecRegistry(object): raise AlreadyRegistered('The spec with id %s is already registered' % id) self._specs[id] = spec - sources = getattr(config, 'sources', None) or [] + sources = getattr(config, 'source_groups', None) or [] self.add_sources(id, sources) def unregister(self, id, spec): diff --git a/imagekit/specs/sources.py b/imagekit/specs/sourcegroups.py similarity index 98% rename from imagekit/specs/sources.py rename to imagekit/specs/sourcegroups.py index 3be4a7e..551d859 100644 --- a/imagekit/specs/sources.py +++ b/imagekit/specs/sourcegroups.py @@ -6,7 +6,7 @@ from ..signals import source_created, source_changed, source_deleted def ik_model_receiver(fn): """ A method decorator that filters out signals coming from models that don't - have fields that function as ImageFieldSpecSources + have fields that function as ImageFieldSourceGroup """ @wraps(fn) @@ -97,7 +97,7 @@ class ModelSignalRouter(object): signal.send(sender=source, source_file=file, info=info) -class ImageFieldSpecSource(object): +class ImageFieldSourceGroup(object): def __init__(self, model_class, image_field): """ Good design would dictate that this instance would be responsible for From 4ead0b3002fd42f0d66e95a210081cc90278a50a Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Sat, 1 Dec 2012 15:51:40 -0500 Subject: [PATCH 110/213] Add note about nested config classes --- imagekit/specs/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/imagekit/specs/__init__.py b/imagekit/specs/__init__.py index 7d5c0fc..6361549 100644 --- a/imagekit/specs/__init__.py +++ b/imagekit/specs/__init__.py @@ -38,6 +38,7 @@ class SpecRegistry(object): before_access.connect(self.before_access_receiver) def register(self, spec, id=None): + # TODO: Should we really allow a nested Config class, since it's not necessarily associated with its container? config = getattr(spec, 'Config', None) if id is None: From 54baa4490015a10b4b89d65b1f02272a08a61b58 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Sat, 1 Dec 2012 15:56:37 -0500 Subject: [PATCH 111/213] Require spec id for form fields Closes #163 --- imagekit/forms/fields.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/imagekit/forms/fields.py b/imagekit/forms/fields.py index 19a4387..40bb5b5 100644 --- a/imagekit/forms/fields.py +++ b/imagekit/forms/fields.py @@ -5,10 +5,13 @@ from ..specs import SpecHost class ProcessedImageField(ImageField, SpecHost): def __init__(self, processors=None, format=None, options=None, - autoconvert=True, spec=None, spec_id=None, *args, **kwargs): + autoconvert=True, spec_id=None, spec=None, *args, **kwargs): if spec_id is None: - spec_id = '??????' # FIXME: Wher should we get this? + # Unlike model fields, form fields are never told their field name. + # (Model fields are done so via `contribute_to_class()`.) Therefore + # we can't really generate a good spec id automatically. + raise TypeError('You must provide a spec_id') SpecHost.__init__(self, processors=processors, format=format, options=options, autoconvert=autoconvert, spec=spec, From d253fe281ad55620e116f2cce9c7262e613de7a7 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Sat, 1 Dec 2012 16:46:10 -0500 Subject: [PATCH 112/213] Rename `image_field` argument; closes #158 --- README.rst | 4 ++-- imagekit/models/fields/__init__.py | 7 ++++--- imagekit/models/fields/utils.py | 4 ++-- tests/models.py | 4 ++-- 4 files changed, 10 insertions(+), 9 deletions(-) diff --git a/README.rst b/README.rst index 1eea912..db024c3 100644 --- a/README.rst +++ b/README.rst @@ -121,7 +121,7 @@ of applying a spec to another one of your model's fields: class Photo(models.Model): avatar = models.ImageField(upload_to='avatars') - avatar_thumbnail = ImageSpecField(id='myapp:fancy_thumbnail', image_field='avatar') + avatar_thumbnail = ImageSpecField(id='myapp:fancy_thumbnail', source='avatar') photo = Photo.objects.all()[0] print photo.avatar_thumbnail.url # > /media/CACHE/ik/982d5af84cddddfd0fbf70892b4431e4.jpg @@ -142,7 +142,7 @@ writing a subclass of ``ImageSpec``: avatar_thumbnail = ImageSpecField(processors=[ResizeToFill(100, 50)], format='JPEG', options={'quality': 60}, - image_field='avatar') + source='avatar') photo = Photo.objects.all()[0] print photo.avatar_thumbnail.url # > /media/CACHE/ik/982d5af84cddddfd0fbf70892b4431e4.jpg diff --git a/imagekit/models/fields/__init__.py b/imagekit/models/fields/__init__.py index 153e793..95d5914 100644 --- a/imagekit/models/fields/__init__.py +++ b/imagekit/models/fields/__init__.py @@ -26,7 +26,7 @@ class ImageSpecField(SpecHostField): """ def __init__(self, processors=None, format=None, options=None, - image_field=None, storage=None, autoconvert=None, + source=None, storage=None, autoconvert=None, image_cache_backend=None, image_cache_strategy=None, spec=None, id=None): @@ -36,7 +36,8 @@ class ImageSpecField(SpecHostField): image_cache_strategy=image_cache_strategy, spec=spec, spec_id=id) - self.image_field = image_field + # TODO: Allow callable for source. See https://github.com/jdriscoll/django-imagekit/issues/158#issuecomment-10921664 + self.source = source def contribute_to_class(self, cls, name): setattr(cls, name, ImageSpecFileDescriptor(self, name)) @@ -44,7 +45,7 @@ class ImageSpecField(SpecHostField): # Add the model and field as a source for this spec id specs.registry.add_sources(self.spec_id, - [ImageFieldSourceGroup(cls, self.image_field)]) + [ImageFieldSourceGroup(cls, self.source)]) class ProcessedImageField(models.ImageField, SpecHostField): diff --git a/imagekit/models/fields/utils.py b/imagekit/models/fields/utils.py index 3043016..efba303 100644 --- a/imagekit/models/fields/utils.py +++ b/imagekit/models/fields/utils.py @@ -11,7 +11,7 @@ class ImageSpecFileDescriptor(object): if instance is None: return self.field else: - field_name = getattr(self.field, 'image_field', None) + field_name = getattr(self.field, 'source', None) if field_name: source_file = getattr(instance, field_name) else: @@ -24,7 +24,7 @@ class ImageSpecFileDescriptor(object): (instance.__class__.__name__, self.attname)) elif len(image_fields) > 1: raise Exception('%s defines multiple ImageFields, but you' - ' have not specified an image_field for your %s' + ' have not specified a source for your %s' ' ImageSpecField.' % (instance.__class__.__name__, self.attname)) else: diff --git a/tests/models.py b/tests/models.py index 2c3d8e4..199fdc0 100644 --- a/tests/models.py +++ b/tests/models.py @@ -10,11 +10,11 @@ class Photo(models.Model): original_image = models.ImageField(upload_to='photos') thumbnail = ImageSpecField([Adjust(contrast=1.2, sharpness=1.1), - ResizeToFill(50, 50)], image_field='original_image', format='JPEG', + ResizeToFill(50, 50)], source='original_image', format='JPEG', options={'quality': 90}) smartcropped_thumbnail = ImageSpecField([Adjust(contrast=1.2, - sharpness=1.1), SmartCrop(50, 50)], image_field='original_image', + sharpness=1.1), SmartCrop(50, 50)], source='original_image', format='JPEG', options={'quality': 90}) From e0567e8fa726a616f52bef5b1eb5a6994dacb15a Mon Sep 17 00:00:00 2001 From: Eric Eldredge Date: Sat, 1 Dec 2012 17:03:51 -0500 Subject: [PATCH 113/213] Remove specs.SpecRegistry; add registry module The registry module splits the work that specs.SpecRegistry used to do into two classes: GeneratorRegistry and SourceGroupRegistry. These two registries are wrapped in Register and Unregister utilities for API convenience. --- imagekit/__init__.py | 1 + imagekit/exceptions.py | 2 +- .../management/commands/warmimagecache.py | 8 +- imagekit/models/fields/__init__.py | 4 +- imagekit/registry.py | 156 ++++++++++++++++++ imagekit/specs/__init__.py | 116 +------------ imagekit/templatetags/imagekit.py | 4 +- 7 files changed, 170 insertions(+), 121 deletions(-) create mode 100644 imagekit/registry.py diff --git a/imagekit/__init__.py b/imagekit/__init__.py index 747ce27..2c72bf4 100644 --- a/imagekit/__init__.py +++ b/imagekit/__init__.py @@ -3,3 +3,4 @@ from . import conf from .specs import ImageSpec from .pkgmeta import * +from .registry import register, unregister diff --git a/imagekit/exceptions.py b/imagekit/exceptions.py index b453f16..308c0a1 100644 --- a/imagekit/exceptions.py +++ b/imagekit/exceptions.py @@ -14,5 +14,5 @@ class UnknownFormatError(Exception): pass -class MissingSpecId(Exception): +class MissingGeneratorId(Exception): pass diff --git a/imagekit/management/commands/warmimagecache.py b/imagekit/management/commands/warmimagecache.py index 8c25aaa..db8ebaa 100644 --- a/imagekit/management/commands/warmimagecache.py +++ b/imagekit/management/commands/warmimagecache.py @@ -1,7 +1,7 @@ from django.core.management.base import BaseCommand import re from ...files import ImageSpecCacheFile -from ...specs import registry +from ...registry import generator_registry, source_group_registry class Command(BaseCommand): @@ -11,7 +11,7 @@ class Command(BaseCommand): args = '[spec_ids]' def handle(self, *args, **options): - specs = registry.get_spec_ids() + specs = generator_registry.get_ids() if args: patterns = self.compile_patterns(args) @@ -19,8 +19,8 @@ class Command(BaseCommand): for spec_id in specs: self.stdout.write('Validating spec: %s\n' % spec_id) - spec = registry.get_spec(spec_id) # TODO: HINTS! (Probably based on source, so this will need to be moved into loop below.) - for source in registry.get_sources(spec_id): + spec = generator_registry.get(spec_id) # TODO: HINTS! (Probably based on source, so this will need to be moved into loop below.) + for source in source_group_registry.get(spec_id): for source_file in source.files(): if source_file: self.stdout.write(' %s\n' % source_file) diff --git a/imagekit/models/fields/__init__.py b/imagekit/models/fields/__init__.py index 95d5914..6b3287c 100644 --- a/imagekit/models/fields/__init__.py +++ b/imagekit/models/fields/__init__.py @@ -1,9 +1,9 @@ from django.db import models from .files import ProcessedImageFieldFile from .utils import ImageSpecFileDescriptor -from ... import specs from ...specs import SpecHost from ...specs.sourcegroups import ImageFieldSourceGroup +from ...registry import register class SpecHostField(SpecHost): @@ -44,7 +44,7 @@ class ImageSpecField(SpecHostField): self.set_spec_id(cls, name) # Add the model and field as a source for this spec id - specs.registry.add_sources(self.spec_id, + register.sources(self.spec_id, [ImageFieldSourceGroup(cls, self.source)]) diff --git a/imagekit/registry.py b/imagekit/registry.py new file mode 100644 index 0000000..5952b4e --- /dev/null +++ b/imagekit/registry.py @@ -0,0 +1,156 @@ +from .exceptions import AlreadyRegistered, NotRegistered, MissingGeneratorId +from .signals import (before_access, source_created, source_changed, + source_deleted) + + +class GeneratorRegistry(object): + """ + An object for registering generators (specs). This registry provides + a convenient way for a distributable app to define default generators + without locking the users of the app into it. + + """ + def __init__(self): + self._generators = {} + + def register(self, generator, id=None): + # TODO: Should we really allow a nested Config class, since it's not necessarily associated with its container? + config = getattr(generator, 'Config', None) + + if id is None: + id = getattr(config, 'id', None) + + if id is None: + raise MissingGeneratorId('No id provided for %s. You must either' + ' pass an id to the register function, or add' + ' an id attribute to the inner Config class of' + ' your spec or generator.' % generator) + + if id in self._generators: + raise AlreadyRegistered('The spec or generator with id %s is' + ' already registered' % id) + self._generators[id] = generator + + source_groups = getattr(config, 'source_groups', None) or [] + source_group_registry.register(id, source_groups) + + def unregister(self, id, generator): + try: + del self._generators[id] + except KeyError: + raise NotRegistered('The spec or generator with id %s is not' + ' registered' % id) + + def get(self, id, **kwargs): + try: + generator = self._generators[id] + except KeyError: + raise NotRegistered('The spec or generator with id %s is not' + ' registered' % id) + if callable(generator): + return generator(**kwargs) + else: + return generator + + def get_ids(self): + return self._generators.keys() + + +class SourceGroupRegistry(object): + """ + An object for registering source groups with specs. The two are + associated with each other via a string id. We do this (as opposed to + associating them directly by, for example, putting a ``source_groups`` + attribute on specs) so that specs can be overridden without losing the + associated sources. That way, a distributable app can define its own + specs without locking the users of the app into it. + + """ + + _source_signals = [ + source_created, + source_changed, + source_deleted, + ] + + def __init__(self): + self._sources = {} + for signal in self._source_signals: + signal.connect(self.source_receiver) + before_access.connect(self.before_access_receiver) + + def register(self, spec_id, sources): + """ + Associates sources with a spec id + + """ + for source in sources: + if source not in self._sources: + self._sources[source] = set() + self._sources[source].add(spec_id) + + def unregister(self, spec_id, sources): + """ + Disassociates sources with a spec id + + """ + for source in sources: + try: + self._sources[source].remove(spec_id) + except KeyError: + continue + + def get(self, spec_id): + return [source for source in self._sources + if spec_id in self._sources[source]] + + def before_access_receiver(self, sender, generator, file, **kwargs): + generator.image_cache_strategy.invoke_callback('before_access', file) + + def source_receiver(self, sender, source_file, signal, info, **kwargs): + """ + Redirects signals dispatched on sources to the appropriate specs. + + """ + source = sender + if source not in self._sources: + return + + for spec in (generator_registry.get(id, source_file=source_file, **info) + for id in self._sources[source]): + event_name = { + source_created: 'source_created', + source_changed: 'source_changed', + source_deleted: 'source_deleted', + } + spec._handle_source_event(event_name, source_file) + + +class Register(object): + """ + Register specs and sources. + + """ + def spec(self, id, spec): + generator_registry.register(id, spec) + + def sources(self, spec_id, sources): + source_group_registry.register(spec_id, sources) + + +class Unregister(object): + """ + Unregister specs and sources. + + """ + def spec(self, id, spec): + generator_registry.unregister(id, spec) + + def sources(self, spec_id, sources): + source_group_registry.unregister(spec_id, sources) + + +generator_registry = GeneratorRegistry() +source_group_registry = SourceGroupRegistry() +register = Register() +unregister = Unregister() diff --git a/imagekit/specs/__init__.py b/imagekit/specs/__init__.py index 6361549..2f41676 100644 --- a/imagekit/specs/__init__.py +++ b/imagekit/specs/__init__.py @@ -2,113 +2,13 @@ from django.conf import settings from hashlib import md5 import os import pickle -from ..exceptions import (UnknownExtensionError, AlreadyRegistered, - NotRegistered, MissingSpecId) +from ..exceptions import UnknownExtensionError from ..files import ImageSpecCacheFile, IKContentFile from ..imagecache.backends import get_default_image_cache_backend from ..imagecache.strategies import StrategyWrapper from ..processors import ProcessorPipeline -from ..signals import (before_access, source_created, source_changed, - source_deleted) from ..utils import open_image, extension_to_format, img_to_fobj - - -class SpecRegistry(object): - """ - An object for registering specs and sources. The two are associated with - eachother via a string id. We do this (as opposed to associating them - directly by, for example, putting a ``sources`` attribute on specs) so that - specs can be overridden without losing the associated sources. That way, - a distributable app can define its own specs without locking the users of - the app into it. - - """ - - _source_signals = [ - source_created, - source_changed, - source_deleted, - ] - - def __init__(self): - self._specs = {} - self._sources = {} - for signal in self._source_signals: - signal.connect(self.source_receiver) - before_access.connect(self.before_access_receiver) - - def register(self, spec, id=None): - # TODO: Should we really allow a nested Config class, since it's not necessarily associated with its container? - config = getattr(spec, 'Config', None) - - if id is None: - id = getattr(config, 'id', None) - - if id is None: - raise MissingSpecId('No id provided for %s. You must either pass an' - ' id to the register function, or add an id' - ' attribute to the inner Config class of your' - ' spec.' % spec) - - if id in self._specs: - raise AlreadyRegistered('The spec with id %s is already registered' % id) - self._specs[id] = spec - - sources = getattr(config, 'source_groups', None) or [] - self.add_sources(id, sources) - - def unregister(self, id, spec): - try: - del self._specs[id] - except KeyError: - raise NotRegistered('The spec with id %s is not registered' % id) - - def get_spec(self, id, **kwargs): - try: - spec = self._specs[id] - except KeyError: - raise NotRegistered('The spec with id %s is not registered' % id) - if callable(spec): - return spec(**kwargs) - else: - return spec - - def get_spec_ids(self): - return self._specs.keys() - - def add_sources(self, spec_id, sources): - """ - Associates sources with a spec id - - """ - for source in sources: - if source not in self._sources: - self._sources[source] = set() - self._sources[source].add(spec_id) - - def get_sources(self, spec_id): - return [source for source in self._sources if spec_id in self._sources[source]] - - def before_access_receiver(self, sender, generator, file, **kwargs): - generator.image_cache_strategy.invoke_callback('before_access', file) - - def source_receiver(self, sender, source_file, signal, info, **kwargs): - """ - Redirects signals dispatched on sources to the appropriate specs. - - """ - source = sender - if source not in self._sources: - return - - for spec in (self.get_spec(id, source_file=source_file, **info) - for id in self._sources[source]): - event_name = { - source_created: 'source_created', - source_changed: 'source_changed', - source_deleted: 'source_deleted', - } - spec._handle_source_event(event_name, source_file) +from ..registry import generator_registry, register class BaseImageSpec(object): @@ -270,7 +170,7 @@ class SpecHost(object): """ self.spec_id = id - registry.register(self._original_spec, id) + register.spec(self._original_spec, id) def get_spec(self, **kwargs): """ @@ -282,12 +182,4 @@ class SpecHost(object): """ if not getattr(self, 'spec_id', None): raise Exception('Object %s has no spec id.' % self) - return registry.get_spec(self.spec_id, **kwargs) - - -registry = SpecRegistry() -register = registry.register - - -def unregister(id, spec): - registry.unregister(id, spec) + return generator_registry.get(self.spec_id, **kwargs) diff --git a/imagekit/templatetags/imagekit.py b/imagekit/templatetags/imagekit.py index ade02d8..eea4f8c 100644 --- a/imagekit/templatetags/imagekit.py +++ b/imagekit/templatetags/imagekit.py @@ -2,7 +2,7 @@ from django import template from django.utils.safestring import mark_safe import re from ..files import ImageSpecCacheFile -from .. import specs +from ..registry import generator_registry register = template.Library() @@ -34,7 +34,7 @@ class SpecResultNodeMixin(object): from ..utils import autodiscover autodiscover() spec_id = self._spec_id.resolve(context) - spec = specs.registry.get_spec(spec_id) # TODO: What "hints" here? + spec = generator_registry.get(spec_id) # TODO: What "hints" here? return spec def get_source_file(self, context): From 9188499965e37a38826d8b203e90ac3a0130ec68 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Sat, 1 Dec 2012 20:36:31 -0500 Subject: [PATCH 114/213] Rework template tag for generators --- imagekit/conf.py | 2 +- imagekit/files.py | 2 +- imagekit/registry.py | 2 +- imagekit/templatetags/compat.py | 160 ++++++++++++++++++++++++++++++ imagekit/templatetags/imagekit.py | 157 ++++++++++------------------- imagekit/utils.py | 2 +- 6 files changed, 215 insertions(+), 110 deletions(-) create mode 100644 imagekit/templatetags/compat.py diff --git a/imagekit/conf.py b/imagekit/conf.py index ed568db..288d366 100644 --- a/imagekit/conf.py +++ b/imagekit/conf.py @@ -8,7 +8,7 @@ class ImageKitConf(AppConf): CACHE_DIR = 'CACHE/images' CACHE_PREFIX = 'imagekit:' DEFAULT_IMAGE_CACHE_STRATEGY = 'imagekit.imagecache.strategies.JustInTime' - DEFAULT_FILE_STORAGE = None # Indicates that the source storage should be used + DEFAULT_FILE_STORAGE = 'django.core.files.storage.FileSystemStorage' def configure_cache_backend(self, value): if value is None: diff --git a/imagekit/files.py b/imagekit/files.py index fddcce2..d92389a 100644 --- a/imagekit/files.py +++ b/imagekit/files.py @@ -95,7 +95,7 @@ class GeneratedImageCacheFile(BaseIKFile, ImageFile): self.args = args self.kwargs = kwargs storage = getattr(generator, 'storage', None) - if not storage and settings.IMAGEKIT_DEFAULT_FILE_STORAGE: + if not storage: storage = get_singleton(settings.IMAGEKIT_DEFAULT_FILE_STORAGE, 'file storage backend') super(GeneratedImageCacheFile, self).__init__(storage=storage) diff --git a/imagekit/registry.py b/imagekit/registry.py index 5952b4e..4147f87 100644 --- a/imagekit/registry.py +++ b/imagekit/registry.py @@ -132,7 +132,7 @@ class Register(object): """ def spec(self, id, spec): - generator_registry.register(id, spec) + generator_registry.register(spec, id) def sources(self, spec_id, sources): source_group_registry.register(spec_id, sources) diff --git a/imagekit/templatetags/compat.py b/imagekit/templatetags/compat.py new file mode 100644 index 0000000..8334dec --- /dev/null +++ b/imagekit/templatetags/compat.py @@ -0,0 +1,160 @@ +""" +This module contains code from django.template.base +(sha 90d3af380e8efec0301dd91600c6686232de3943). Bundling this code allows us to +support older versions of Django that did not contain it (< 1.4). + + +Copyright (c) Django Software Foundation and individual contributors. +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + 3. Neither the name of Django nor the names of its contributors may be used + to endorse or promote products derived from this software without + specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +""" + +from django.template import TemplateSyntaxError +import re + + +# Regex for token keyword arguments +kwarg_re = re.compile(r"(?:(\w+)=)?(.+)") + + +def token_kwargs(bits, parser, support_legacy=False): + """ + A utility method for parsing token keyword arguments. + + :param bits: A list containing remainder of the token (split by spaces) + that is to be checked for arguments. Valid arguments will be removed + from this list. + + :param support_legacy: If set to true ``True``, the legacy format + ``1 as foo`` will be accepted. Otherwise, only the standard ``foo=1`` + format is allowed. + + :returns: A dictionary of the arguments retrieved from the ``bits`` token + list. + + There is no requirement for all remaining token ``bits`` to be keyword + arguments, so the dictionary will be returned as soon as an invalid + argument format is reached. + """ + if not bits: + return {} + match = kwarg_re.match(bits[0]) + kwarg_format = match and match.group(1) + if not kwarg_format: + if not support_legacy: + return {} + if len(bits) < 3 or bits[1] != 'as': + return {} + + kwargs = {} + while bits: + if kwarg_format: + match = kwarg_re.match(bits[0]) + if not match or not match.group(1): + return kwargs + key, value = match.groups() + del bits[:1] + else: + if len(bits) < 3 or bits[1] != 'as': + return kwargs + key, value = bits[2], bits[0] + del bits[:3] + kwargs[key] = parser.compile_filter(value) + if bits and not kwarg_format: + if bits[0] != 'and': + return kwargs + del bits[:1] + return kwargs + + +def parse_bits(parser, bits, params, varargs, varkw, defaults, + takes_context, name): + """ + Parses bits for template tag helpers (simple_tag, include_tag and + assignment_tag), in particular by detecting syntax errors and by + extracting positional and keyword arguments. + """ + if takes_context: + if params[0] == 'context': + params = params[1:] + else: + raise TemplateSyntaxError( + "'%s' is decorated with takes_context=True so it must " + "have a first argument of 'context'" % name) + args = [] + kwargs = {} + unhandled_params = list(params) + for bit in bits: + # First we try to extract a potential kwarg from the bit + kwarg = token_kwargs([bit], parser) + if kwarg: + # The kwarg was successfully extracted + param, value = list(kwarg.items())[0] + if param not in params and varkw is None: + # An unexpected keyword argument was supplied + raise TemplateSyntaxError( + "'%s' received unexpected keyword argument '%s'" % + (name, param)) + elif param in kwargs: + # The keyword argument has already been supplied once + raise TemplateSyntaxError( + "'%s' received multiple values for keyword argument '%s'" % + (name, param)) + else: + # All good, record the keyword argument + kwargs[str(param)] = value + if param in unhandled_params: + # If using the keyword syntax for a positional arg, then + # consume it. + unhandled_params.remove(param) + else: + if kwargs: + raise TemplateSyntaxError( + "'%s' received some positional argument(s) after some " + "keyword argument(s)" % name) + else: + # Record the positional argument + args.append(parser.compile_filter(bit)) + try: + # Consume from the list of expected positional arguments + unhandled_params.pop(0) + except IndexError: + if varargs is None: + raise TemplateSyntaxError( + "'%s' received too many positional arguments" % + name) + if defaults is not None: + # Consider the last n params handled, where n is the + # number of defaults. + unhandled_params = unhandled_params[:-len(defaults)] + if unhandled_params: + # Some positional arguments were not supplied + raise TemplateSyntaxError( + "'%s' did not receive value(s) for the argument(s): %s" % + (name, ", ".join(["'%s'" % p for p in unhandled_params]))) + return args, kwargs diff --git a/imagekit/templatetags/imagekit.py b/imagekit/templatetags/imagekit.py index eea4f8c..f114a80 100644 --- a/imagekit/templatetags/imagekit.py +++ b/imagekit/templatetags/imagekit.py @@ -1,134 +1,79 @@ from django import template -from django.utils.safestring import mark_safe -import re -from ..files import ImageSpecCacheFile +from .compat import parse_bits +from ..files import GeneratedImageCacheFile from ..registry import generator_registry register = template.Library() -html_attr_pattern = r""" - (?P\w+) # The attribute name - ( - \s*=\s* # an equals sign, that may or may not have spaces around it - (?P - ("[^"]*") # a double-quoted value - | # or - ('[^']*') # a single-quoted value - | # or - ([^"'<>=\s]+) # an unquoted value - ) - )? -""" +class GenerateImageAssignmentNode(template.Node): + _kwarg_map = { + 'from': 'source_file', + } -html_attr_re = re.compile(html_attr_pattern, re.VERBOSE) - - -class SpecResultNodeMixin(object): - def __init__(self, spec_id, source_file): - self._spec_id = spec_id - self._source_file = source_file - - def get_spec(self, context): - from ..utils import autodiscover - autodiscover() - spec_id = self._spec_id.resolve(context) - spec = generator_registry.get(spec_id) # TODO: What "hints" here? - return spec - - def get_source_file(self, context): - return self._source_file.resolve(context) - - def get_file(self, context): - spec = self.get_spec(context) - source_file = self.get_source_file(context) - return ImageSpecCacheFile(spec, source_file) - - -class SpecResultAssignmentNode(template.Node, SpecResultNodeMixin): - def __init__(self, spec_id, source_file, variable_name): - super(SpecResultAssignmentNode, self).__init__(spec_id, source_file) + def __init__(self, variable_name, generator_id, **kwargs): + self.generator_id = generator_id + self.kwargs = kwargs self._variable_name = variable_name def get_variable_name(self, context): return unicode(self._variable_name) + def get_kwargs(self, context): + return dict((self._kwarg_map.get(k, k), v.resolve(context)) for k, + v in self.kwargs.items()) + def render(self, context): + from ..utils import autodiscover + autodiscover() + variable_name = self.get_variable_name(context) - context[variable_name] = self.get_file(context) + generator_id = self.generator_id.resolve(context) + kwargs = self.get_kwargs(context) + generator = generator_registry.get(generator_id) + context[variable_name] = GeneratedImageCacheFile(generator, **kwargs) return '' -class SpecResultImgTagNode(template.Node, SpecResultNodeMixin): - def __init__(self, spec_id, source_file, html_attrs): - super(SpecResultImgTagNode, self).__init__(spec_id, source_file) - self._html_attrs = html_attrs - - def get_html_attrs(self, context): - attrs = [] - for attr in self._html_attrs: - match = html_attr_re.search(attr) - if match: - attrs.append((match.group('name'), match.group('value'))) - return attrs - - def get_attr_str(self, k, v): - return k if v is None else '%s=%s' % (k, v) - - def render(self, context): - file = self.get_file(context) - attrs = self.get_html_attrs(context) - attr_dict = dict(attrs) - if not 'width' in attr_dict and not 'height' in attr_dict: - attrs = attrs + [('width', '"%s"' % file.width), - ('height', '"%s"' % file.height)] - attrs = [('src', '"%s"' % file.url)] + attrs - attr_str = ' '.join(self.get_attr_str(k, v) for k, v in attrs) - return mark_safe(u'' % attr_str) - - #@register.tag -# TODO: Should this be renamed to something like 'process'? -def spec(parser, token): +def generateimage(parser, token): """ - Creates an image based on the provided spec and source image. - - By default:: - - {% spec 'myapp:thumbnail' mymodel.profile_image %} - - Generates an ````:: - - - - Storing it as a context variable allows more flexibility:: - - {% spec 'myapp:thumbnail' mymodel.profile_image as th %} - + Creates an image based on the provided arguments. """ bits = token.split_contents() - tag_name = bits.pop(0) # noqa + tag_name = bits.pop(0) - if len(bits) == 4 and bits[2] == 'as': - return SpecResultAssignmentNode( - parser.compile_filter(bits[0]), # spec id - parser.compile_filter(bits[1]), # source file - parser.compile_filter(bits[3]), # var name - ) - elif len(bits) > 1: - return SpecResultImgTagNode( - parser.compile_filter(bits[0]), # spec id - parser.compile_filter(bits[1]), # source file - bits[2:], # html attributes - ) + if bits[-2] == 'as': + varname = bits[-1] + bits = bits[:-2] + + # (params, varargs, varkwargs, defaults) = getargspec(g) + # (params, 'args', 'kwargs', defaults) = getargspec(g) + + args, kwargs = parse_bits(parser, bits, + ['generator_id'], 'args', 'kwargs', None, + False, tag_name) + if len(args) != 1: + raise template.TemplateSyntaxError("The 'generateimage' tag " + ' requires exactly one unnamed argument.') + generator_id = args[0] + return GenerateImageAssignmentNode(varname, generator_id, **kwargs) else: - raise template.TemplateSyntaxError('\'spec\' tags must be in the form' - ' "{% spec spec_id image %}" or' - ' "{% spec spec_id image' - ' as varname %}"') + raise Exception('!!') + # elif len(bits) > 1: + # return GenerateImageTagNode( + # parser.compile_filter(bits[0]), # spec id + # parser.compile_filter(bits[1]), # source file + # bits[2:], # html attributes + # ) + # else: + # raise template.TemplateSyntaxError('\'generateimage\' tags must be in the form' + # ' "{% generateimage id image %}" or' + # ' "{% generateimage id image' + # ' as varname %}"') -spec = spec_tag = register.tag(spec) +generateimage = register.tag(generateimage) diff --git a/imagekit/utils.py b/imagekit/utils.py index 7495238..ac93cf6 100644 --- a/imagekit/utils.py +++ b/imagekit/utils.py @@ -371,7 +371,7 @@ def autodiscover(): import_module('%s.imagespecs' % app) except: # Decide whether to bubble up this error. If the app just - # doesn't have an admin module, we can ignore the error + # doesn't have an imagespecs module, we can ignore the error # attempting to import it, otherwise we want it to bubble up. if module_has_submodule(mod, 'imagespecs'): raise From 1f06c9ac700b6bdbd816da4fe81b05bb135dc9c0 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Sat, 1 Dec 2012 20:41:08 -0500 Subject: [PATCH 115/213] Remove ImageSpecCacheFile --- imagekit/files.py | 21 ------------------- .../management/commands/warmimagecache.py | 4 ++-- imagekit/models/fields/utils.py | 4 ++-- imagekit/specs/__init__.py | 4 ++-- 4 files changed, 6 insertions(+), 27 deletions(-) diff --git a/imagekit/files.py b/imagekit/files.py index d92389a..869b9b7 100644 --- a/imagekit/files.py +++ b/imagekit/files.py @@ -149,27 +149,6 @@ class GeneratedImageCacheFile(BaseIKFile, ImageFile): self.generator.image_cache_backend)) -class ImageSpecCacheFile(GeneratedImageCacheFile): - def __init__(self, generator, source_file): - super(ImageSpecCacheFile, self).__init__(generator, - source_file=source_file) - if not self.storage: - self.storage = source_file.storage - - def get_default_filename(self): - source_filename = self.kwargs['source_file'].name - hash = md5(''.join([ - source_filename, - self.generator.get_hash(), - ]).encode('utf-8')).hexdigest() - # TODO: Since specs can now be dynamically generated using hints, can we move this into the spec constructor? i.e. set self.format if not defined. This would get us closer to making ImageSpecCacheFile == GeneratedImageCacheFile - ext = suggest_extension(source_filename, self.generator.format) - return os.path.normpath(os.path.join( - settings.IMAGEKIT_CACHE_DIR, - os.path.splitext(source_filename)[0], - '%s%s' % (hash, ext))) - - class IKContentFile(ContentFile): """ Wraps a ContentFile in a file-like object with a filename and a diff --git a/imagekit/management/commands/warmimagecache.py b/imagekit/management/commands/warmimagecache.py index db8ebaa..8515325 100644 --- a/imagekit/management/commands/warmimagecache.py +++ b/imagekit/management/commands/warmimagecache.py @@ -1,6 +1,6 @@ from django.core.management.base import BaseCommand import re -from ...files import ImageSpecCacheFile +from ...files import GeneratedImageCacheFile from ...registry import generator_registry, source_group_registry @@ -26,7 +26,7 @@ class Command(BaseCommand): self.stdout.write(' %s\n' % source_file) try: # TODO: Allow other validation actions through command option - ImageSpecCacheFile(spec, source_file).validate() + GeneratedImageCacheFile(spec, source_file=source_file).validate() except Exception, err: # TODO: How should we handle failures? Don't want to error, but should call it out more than this. self.stdout.write(' FAILED: %s\n' % err) diff --git a/imagekit/models/fields/utils.py b/imagekit/models/fields/utils.py index efba303..2fded11 100644 --- a/imagekit/models/fields/utils.py +++ b/imagekit/models/fields/utils.py @@ -1,4 +1,4 @@ -from ...files import ImageSpecCacheFile +from ...files import GeneratedImageCacheFile from django.db.models.fields.files import ImageField @@ -30,7 +30,7 @@ class ImageSpecFileDescriptor(object): else: source_file = image_fields[0] spec = self.field.get_spec() # TODO: What "hints" should we pass here? - file = ImageSpecCacheFile(spec, source_file) + file = GeneratedImageCacheFile(spec, source_file=source_file) instance.__dict__[self.attname] = file return file diff --git a/imagekit/specs/__init__.py b/imagekit/specs/__init__.py index 2f41676..3abcfbc 100644 --- a/imagekit/specs/__init__.py +++ b/imagekit/specs/__init__.py @@ -3,7 +3,7 @@ from hashlib import md5 import os import pickle from ..exceptions import UnknownExtensionError -from ..files import ImageSpecCacheFile, IKContentFile +from ..files import GeneratedImageCacheFile, IKContentFile from ..imagecache.backends import get_default_image_cache_backend from ..imagecache.strategies import StrategyWrapper from ..processors import ProcessorPipeline @@ -48,7 +48,7 @@ class BaseImageSpec(object): # TODO: I don't like this interface. Is there a standard Python one? pubsub? def _handle_source_event(self, event_name, source_file): - file = ImageSpecCacheFile(self, source_file) + file = GeneratedImageCacheFile(self, source_file=source_file) self.image_cache_strategy.invoke_callback('on_%s' % event_name, file) From 7ed404f096960a49faf56a6aebce72093631c751 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Sat, 1 Dec 2012 20:45:34 -0500 Subject: [PATCH 116/213] Switch args back to old order --- imagekit/registry.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/imagekit/registry.py b/imagekit/registry.py index 4147f87..ccc9b7e 100644 --- a/imagekit/registry.py +++ b/imagekit/registry.py @@ -131,7 +131,7 @@ class Register(object): Register specs and sources. """ - def spec(self, id, spec): + def spec(self, spec, id=None): generator_registry.register(spec, id) def sources(self, spec_id, sources): From 5ecb491e653d935a17b5d2c722b814ea1bf63e3b Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Sat, 1 Dec 2012 20:47:55 -0500 Subject: [PATCH 117/213] Remove unused import --- imagekit/files.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/imagekit/files.py b/imagekit/files.py index 869b9b7..1922ca6 100644 --- a/imagekit/files.py +++ b/imagekit/files.py @@ -6,7 +6,7 @@ from hashlib import md5 import os import pickle from .signals import before_access -from .utils import (suggest_extension, format_to_mimetype, format_to_extension, +from .utils import (format_to_mimetype, format_to_extension, extension_to_mimetype, get_logger, get_singleton) From 7bc82d3624f80ac3077f1a4c9434583f97039a66 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Sat, 1 Dec 2012 21:20:33 -0500 Subject: [PATCH 118/213] Remove arguments from generate() method Previously, we had two places where we were passing kwargs that affected the image generation: the ImageSpec constructor and the generate method. These were essentially partial applications. With this commit, there's only one partial application (when the spec is instantiated), and the generate method is called without arguments. Therefore, specs can now be treated as generic generators whose constructors just happen to accept a source_file. --- imagekit/files.py | 17 +++-------------- imagekit/management/commands/warmimagecache.py | 4 ++-- imagekit/models/fields/utils.py | 4 ++-- imagekit/specs/__init__.py | 8 ++++++-- imagekit/templatetags/imagekit.py | 4 ++-- 5 files changed, 15 insertions(+), 22 deletions(-) diff --git a/imagekit/files.py b/imagekit/files.py index 1922ca6..041cbd5 100644 --- a/imagekit/files.py +++ b/imagekit/files.py @@ -81,19 +81,13 @@ class GeneratedImageCacheFile(BaseIKFile, ImageFile): it. """ - def __init__(self, generator, name=None, *args, **kwargs): + def __init__(self, generator, name=None): """ :param generator: The object responsible for generating a new image. - :param args: Positional arguments that will be passed to the generator's - ``generate()`` method when the generation is called for. - :param kwargs: Keyword arguments that will be apssed to the generator's - ``generate()`` method when the generation is called for. """ self._name = name self.generator = generator - self.args = args - self.kwargs = kwargs storage = getattr(generator, 'storage', None) if not storage: storage = get_singleton(settings.IMAGEKIT_DEFAULT_FILE_STORAGE, @@ -101,12 +95,7 @@ class GeneratedImageCacheFile(BaseIKFile, ImageFile): super(GeneratedImageCacheFile, self).__init__(storage=storage) def get_default_filename(self): - # FIXME: This won't work if args or kwargs contain a file object. It probably won't work in many other cases as well. Better option? - hash = md5(''.join([ - pickle.dumps(self.args), - pickle.dumps(self.kwargs), - self.generator.get_hash(), - ]).encode('utf-8')).hexdigest() + hash = self.generator.get_hash() ext = format_to_extension(self.generator.format) return os.path.join(settings.IMAGEKIT_CACHE_DIR, '%s%s' % (hash, ext)) @@ -134,7 +123,7 @@ class GeneratedImageCacheFile(BaseIKFile, ImageFile): def generate(self): # Generate the file - content = self.generator.generate(*self.args, **self.kwargs) + content = self.generator.generate() actual_name = self.storage.save(self.name, content) if actual_name != self.name: diff --git a/imagekit/management/commands/warmimagecache.py b/imagekit/management/commands/warmimagecache.py index 8515325..21db47c 100644 --- a/imagekit/management/commands/warmimagecache.py +++ b/imagekit/management/commands/warmimagecache.py @@ -19,14 +19,14 @@ class Command(BaseCommand): for spec_id in specs: self.stdout.write('Validating spec: %s\n' % spec_id) - spec = generator_registry.get(spec_id) # TODO: HINTS! (Probably based on source, so this will need to be moved into loop below.) for source in source_group_registry.get(spec_id): for source_file in source.files(): if source_file: + spec = generator_registry.get(spec_id, source_file=source_file) # TODO: HINTS! (Probably based on source, so this will need to be moved into loop below.) self.stdout.write(' %s\n' % source_file) try: # TODO: Allow other validation actions through command option - GeneratedImageCacheFile(spec, source_file=source_file).validate() + GeneratedImageCacheFile(spec).validate() except Exception, err: # TODO: How should we handle failures? Don't want to error, but should call it out more than this. self.stdout.write(' FAILED: %s\n' % err) diff --git a/imagekit/models/fields/utils.py b/imagekit/models/fields/utils.py index 2fded11..07c6e9e 100644 --- a/imagekit/models/fields/utils.py +++ b/imagekit/models/fields/utils.py @@ -29,8 +29,8 @@ class ImageSpecFileDescriptor(object): self.attname)) else: source_file = image_fields[0] - spec = self.field.get_spec() # TODO: What "hints" should we pass here? - file = GeneratedImageCacheFile(spec, source_file=source_file) + spec = self.field.get_spec(source_file=source_file) # TODO: What "hints" should we pass here? + file = GeneratedImageCacheFile(spec) instance.__dict__[self.attname] = file return file diff --git a/imagekit/specs/__init__.py b/imagekit/specs/__init__.py index 3abcfbc..961e89f 100644 --- a/imagekit/specs/__init__.py +++ b/imagekit/specs/__init__.py @@ -48,7 +48,7 @@ class BaseImageSpec(object): # TODO: I don't like this interface. Is there a standard Python one? pubsub? def _handle_source_event(self, event_name, source_file): - file = GeneratedImageCacheFile(self, source_file=source_file) + file = GeneratedImageCacheFile(self) self.image_cache_strategy.invoke_callback('on_%s' % event_name, file) @@ -88,17 +88,21 @@ class ImageSpec(BaseImageSpec): def __init__(self, **kwargs): self.processors = self.processors or [] + self.kwargs = kwargs super(ImageSpec, self).__init__() def get_hash(self): return md5(''.join([ + pickle.dumps(self.kwargs), pickle.dumps(self.processors), str(self.format), pickle.dumps(self.options), str(self.autoconvert), ]).encode('utf-8')).hexdigest() - def generate(self, source_file, filename=None): + def generate(self): + source_file = self.kwargs['source_file'] + filename = self.kwargs.get('filename') img = open_image(source_file) original_format = img.format diff --git a/imagekit/templatetags/imagekit.py b/imagekit/templatetags/imagekit.py index f114a80..9c9f690 100644 --- a/imagekit/templatetags/imagekit.py +++ b/imagekit/templatetags/imagekit.py @@ -31,8 +31,8 @@ class GenerateImageAssignmentNode(template.Node): variable_name = self.get_variable_name(context) generator_id = self.generator_id.resolve(context) kwargs = self.get_kwargs(context) - generator = generator_registry.get(generator_id) - context[variable_name] = GeneratedImageCacheFile(generator, **kwargs) + generator = generator_registry.get(generator_id, **kwargs) + context[variable_name] = GeneratedImageCacheFile(generator) return '' From 20c900df4afcc5cba17a790eb663cb2818f9fad1 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Sat, 1 Dec 2012 21:52:23 -0500 Subject: [PATCH 119/213] Remove unused imports --- imagekit/files.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/imagekit/files.py b/imagekit/files.py index 041cbd5..5c7b5ee 100644 --- a/imagekit/files.py +++ b/imagekit/files.py @@ -2,9 +2,7 @@ from django.conf import settings from django.core.files.base import ContentFile, File from django.core.files.images import ImageFile from django.utils.encoding import smart_str, smart_unicode -from hashlib import md5 import os -import pickle from .signals import before_access from .utils import (format_to_mimetype, format_to_extension, extension_to_mimetype, get_logger, get_singleton) From 236eea8459975de8f0f5eec593fe2f302a6ee8e6 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Sat, 1 Dec 2012 22:09:34 -0500 Subject: [PATCH 120/213] Move filename generation to generator --- imagekit/files.py | 12 +++--------- imagekit/specs/__init__.py | 17 +++++++++++++---- 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/imagekit/files.py b/imagekit/files.py index 5c7b5ee..52836de 100644 --- a/imagekit/files.py +++ b/imagekit/files.py @@ -4,8 +4,8 @@ from django.core.files.images import ImageFile from django.utils.encoding import smart_str, smart_unicode import os from .signals import before_access -from .utils import (format_to_mimetype, format_to_extension, - extension_to_mimetype, get_logger, get_singleton) +from .utils import (format_to_mimetype, extension_to_mimetype, get_logger, + get_singleton) class BaseIKFile(File): @@ -92,14 +92,8 @@ class GeneratedImageCacheFile(BaseIKFile, ImageFile): 'file storage backend') super(GeneratedImageCacheFile, self).__init__(storage=storage) - def get_default_filename(self): - hash = self.generator.get_hash() - ext = format_to_extension(self.generator.format) - return os.path.join(settings.IMAGEKIT_CACHE_DIR, - '%s%s' % (hash, ext)) - def _get_name(self): - return self._name or self.get_default_filename() + return self._name or self.generator.get_filename() def _set_name(self, value): self._name = value diff --git a/imagekit/specs/__init__.py b/imagekit/specs/__init__.py index 961e89f..effd56e 100644 --- a/imagekit/specs/__init__.py +++ b/imagekit/specs/__init__.py @@ -7,7 +7,8 @@ from ..files import GeneratedImageCacheFile, IKContentFile from ..imagecache.backends import get_default_image_cache_backend from ..imagecache.strategies import StrategyWrapper from ..processors import ProcessorPipeline -from ..utils import open_image, extension_to_format, img_to_fobj +from ..utils import (open_image, extension_to_format, img_to_fobj, + suggest_extension) from ..registry import generator_registry, register @@ -40,9 +41,6 @@ class BaseImageSpec(object): self.image_cache_backend = self.image_cache_backend or get_default_image_cache_backend() self.image_cache_strategy = StrategyWrapper(self.image_cache_strategy) - def get_hash(self): - raise NotImplementedError - def generate(self, source_file, filename=None): raise NotImplementedError @@ -91,6 +89,17 @@ class ImageSpec(BaseImageSpec): self.kwargs = kwargs super(ImageSpec, self).__init__() + def get_filename(self): + source_filename = self.kwargs['source_file'].name + ext = suggest_extension(source_filename, self.format) + return os.path.normpath(os.path.join( + settings.IMAGEKIT_CACHE_DIR, + os.path.splitext(source_filename)[0], + '%s%s' % (self.get_hash(), ext))) + + return os.path.join(settings.IMAGEKIT_CACHE_DIR, + '%s%s' % (hash, ext)) + def get_hash(self): return md5(''.join([ pickle.dumps(self.kwargs), From 14d2193f8d00200e16de9dcf2aa5866a0b9e25ab Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Sat, 1 Dec 2012 22:23:25 -0500 Subject: [PATCH 121/213] Remove unused args --- imagekit/specs/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/imagekit/specs/__init__.py b/imagekit/specs/__init__.py index effd56e..60d4d9a 100644 --- a/imagekit/specs/__init__.py +++ b/imagekit/specs/__init__.py @@ -41,7 +41,7 @@ class BaseImageSpec(object): self.image_cache_backend = self.image_cache_backend or get_default_image_cache_backend() self.image_cache_strategy = StrategyWrapper(self.image_cache_strategy) - def generate(self, source_file, filename=None): + def generate(self): raise NotImplementedError # TODO: I don't like this interface. Is there a standard Python one? pubsub? From 848d7d7fa3399b33c9f37640226cbf2e4e6415b4 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Sat, 1 Dec 2012 23:18:49 -0500 Subject: [PATCH 122/213] Add TODOs --- imagekit/specs/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/imagekit/specs/__init__.py b/imagekit/specs/__init__.py index 60d4d9a..581453d 100644 --- a/imagekit/specs/__init__.py +++ b/imagekit/specs/__init__.py @@ -110,6 +110,8 @@ class ImageSpec(BaseImageSpec): ]).encode('utf-8')).hexdigest() def generate(self): + # TODO: Move into a generator base class + # TODO: Factor out a generate_image function so you can create a generator and only override the PIL.Image creating part. (The tricky part is how to deal with original_format since generator base class won't have one.) source_file = self.kwargs['source_file'] filename = self.kwargs.get('filename') img = open_image(source_file) From 4f81e14f58695fb3b3a505d27c61d71d56fd7882 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Mon, 3 Dec 2012 21:06:15 -0500 Subject: [PATCH 123/213] Re-add html attribute handling --- imagekit/templatetags/imagekit.py | 118 ++++++++++++++++++++---------- 1 file changed, 80 insertions(+), 38 deletions(-) diff --git a/imagekit/templatetags/imagekit.py b/imagekit/templatetags/imagekit.py index 9c9f690..3169481 100644 --- a/imagekit/templatetags/imagekit.py +++ b/imagekit/templatetags/imagekit.py @@ -1,4 +1,6 @@ from django import template +from django.utils.html import escape +from django.utils.safestring import mark_safe from .compat import parse_bits from ..files import GeneratedImageCacheFile from ..registry import generator_registry @@ -7,35 +9,70 @@ from ..registry import generator_registry register = template.Library() -class GenerateImageAssignmentNode(template.Node): - _kwarg_map = { - 'from': 'source_file', - } +ASSIGNMENT_DELIMETER = 'as' +HTML_ATTRS_DELIMITER = 'with' - def __init__(self, variable_name, generator_id, **kwargs): - self.generator_id = generator_id - self.kwargs = kwargs + +_kwarg_map = { + 'from': 'source_file', +} + + +def get_cache_file(context, generator_id, generator_kwargs): + generator_id = generator_id.resolve(context) + kwargs = dict((_kwarg_map.get(k, k), v.resolve(context)) for k, + v in generator_kwargs.items()) + generator = generator_registry.get(generator_id, **kwargs) + return GeneratedImageCacheFile(generator) + + +class GenerateImageAssignmentNode(template.Node): + + def __init__(self, variable_name, generator_id, generator_kwargs): + self._generator_id = generator_id + self._generator_kwargs = generator_kwargs self._variable_name = variable_name def get_variable_name(self, context): return unicode(self._variable_name) - def get_kwargs(self, context): - return dict((self._kwarg_map.get(k, k), v.resolve(context)) for k, - v in self.kwargs.items()) - def render(self, context): from ..utils import autodiscover autodiscover() variable_name = self.get_variable_name(context) - generator_id = self.generator_id.resolve(context) - kwargs = self.get_kwargs(context) - generator = generator_registry.get(generator_id, **kwargs) - context[variable_name] = GeneratedImageCacheFile(generator) + context[variable_name] = get_cache_file(context, self._generator_id, + self._generator_kwargs) return '' +class GenerateImageTagNode(template.Node): + + def __init__(self, generator_id, generator_kwargs, html_attrs): + self._generator_id = generator_id + self._generator_kwargs = generator_kwargs + self._html_attrs = html_attrs + + def render(self, context): + from ..utils import autodiscover + autodiscover() + + file = get_cache_file(context, self._generator_id, + self._generator_kwargs) + attrs = dict((k, v.resolve(context)) for k, v in + self._html_attrs.items()) + + # Only add width and height if neither is specified (for proportional + # scaling). + if not 'width' in attrs and not 'height' in attrs: + attrs.update(width=file.width, height=file.height) + + attrs['src'] = file.url + attr_str = ' '.join('%s="%s"' % (escape(k), escape(v)) for k, v in + attrs.items()) + return mark_safe(u'' % attr_str) + + #@register.tag def generateimage(parser, token): """ @@ -43,37 +80,42 @@ def generateimage(parser, token): """ + varname = None + html_bits = [] bits = token.split_contents() tag_name = bits.pop(0) - if bits[-2] == 'as': + if bits[-2] == ASSIGNMENT_DELIMETER: varname = bits[-1] bits = bits[:-2] + elif HTML_ATTRS_DELIMITER in bits: + index = bits.index(HTML_ATTRS_DELIMITER) + html_bits = bits[index + 1:] + bits = bits[:index] - # (params, varargs, varkwargs, defaults) = getargspec(g) - # (params, 'args', 'kwargs', defaults) = getargspec(g) + if not html_bits: + raise template.TemplateSyntaxError('Don\'t use "%s" unless you\'re' + ' setting html attributes.' % HTML_ATTRS_DELIMITER) - args, kwargs = parse_bits(parser, bits, - ['generator_id'], 'args', 'kwargs', None, - False, tag_name) - if len(args) != 1: - raise template.TemplateSyntaxError("The 'generateimage' tag " - ' requires exactly one unnamed argument.') - generator_id = args[0] - return GenerateImageAssignmentNode(varname, generator_id, **kwargs) + args, kwargs = parse_bits(parser, bits, ['generator_id'], 'args', 'kwargs', + None, False, tag_name) + + if len(args) != 1: + raise template.TemplateSyntaxError('The "%s" tag requires exactly one' + ' unnamed argument (the generator id).' % tag_name) + + generator_id = args[0] + + if varname: + return GenerateImageAssignmentNode(varname, generator_id, kwargs) else: - raise Exception('!!') - # elif len(bits) > 1: - # return GenerateImageTagNode( - # parser.compile_filter(bits[0]), # spec id - # parser.compile_filter(bits[1]), # source file - # bits[2:], # html attributes - # ) - # else: - # raise template.TemplateSyntaxError('\'generateimage\' tags must be in the form' - # ' "{% generateimage id image %}" or' - # ' "{% generateimage id image' - # ' as varname %}"') + html_args, html_kwargs = parse_bits(parser, html_bits, [], 'args', + 'kwargs', None, False, tag_name) + if len(html_args): + raise template.TemplateSyntaxError('All "%s" tag arguments after' + ' the "%s" token must be named.' % (tag_name, + HTML_ATTRS_DELIMITER)) + return GenerateImageTagNode(generator_id, kwargs, html_kwargs) generateimage = register.tag(generateimage) From 4f7ce6890416fa8d660561f7eeecd082d91f136d Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Mon, 3 Dec 2012 21:11:52 -0500 Subject: [PATCH 124/213] Add documentation for generateimage --- imagekit/templatetags/imagekit.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/imagekit/templatetags/imagekit.py b/imagekit/templatetags/imagekit.py index 3169481..a1731a5 100644 --- a/imagekit/templatetags/imagekit.py +++ b/imagekit/templatetags/imagekit.py @@ -78,6 +78,28 @@ def generateimage(parser, token): """ Creates an image based on the provided arguments. + By default:: + + {% generateimage 'myapp:thumbnail' from=mymodel.profile_image %} + + generates an ```` tag:: + + + + You can add additional attributes to the tag using "with". For example, + this:: + + {% generateimage 'myapp:thumbnail' from=mymodel.profile_image with alt="Hello!" %} + + will result in the following markup:: + + Hello! + + For more flexibility, ``generateimage`` also works as an assignment tag:: + + {% generateimage 'myapp:thumbnail' from=mymodel.profile_image as th %} + + """ varname = None From a499f5fbe63f03a3c404a28e0c1286af74382e09 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Mon, 3 Dec 2012 22:24:55 -0500 Subject: [PATCH 125/213] Add util for generating named image file --- tests/utils.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/tests/utils.py b/tests/utils.py index 746920f..89d2974 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -3,11 +3,12 @@ import os from django.core.files.base import ContentFile from imagekit.lib import Image, StringIO +from tempfile import NamedTemporaryFile from .models import Photo import pickle -def get_image_file(): +def _get_image_file(file_factory): """ See also: @@ -16,12 +17,20 @@ def get_image_file(): """ path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'assets', 'lenna-800x600-white-border.jpg') - tmp = StringIO() + tmp = file_factory() tmp.write(open(path, 'r+b').read()) tmp.seek(0) return tmp +def get_image_file(): + return _get_image_file(StringIO) + + +def get_named_image_file(): + return _get_image_file(NamedTemporaryFile) + + def create_image(): return Image.open(get_image_file()) From a5c33a4925668c0a5315ac67e5df433ee78216e7 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Mon, 3 Dec 2012 22:25:12 -0500 Subject: [PATCH 126/213] Add tests for generateimage template tag Currently, these will fail because the temporary file cannot be pickled in order to generate a hash. --- setup.py | 1 + tests/imagespecs.py | 8 ++++ tests/test_generateimage_tag.py | 65 +++++++++++++++++++++++++++++++++ 3 files changed, 74 insertions(+) create mode 100644 tests/imagespecs.py create mode 100644 tests/test_generateimage_tag.py diff --git a/setup.py b/setup.py index 985e103..7c1495e 100644 --- a/setup.py +++ b/setup.py @@ -31,6 +31,7 @@ setup( zip_safe=False, include_package_data=True, tests_require=[ + 'beautifulsoup4==4.1.3', 'nose==1.2.1', 'nose-progressive==1.3', 'django-nose==1.1', diff --git a/tests/imagespecs.py b/tests/imagespecs.py new file mode 100644 index 0000000..536db7a --- /dev/null +++ b/tests/imagespecs.py @@ -0,0 +1,8 @@ +from imagekit import ImageSpec, register + + +class TestSpec(ImageSpec): + pass + + +register.spec(TestSpec, 'testspec') diff --git a/tests/test_generateimage_tag.py b/tests/test_generateimage_tag.py new file mode 100644 index 0000000..a7214ae --- /dev/null +++ b/tests/test_generateimage_tag.py @@ -0,0 +1,65 @@ +from bs4 import BeautifulSoup +from django.template import Context, Template, TemplateSyntaxError +from nose.tools import eq_, assert_not_in, raises, assert_not_equal +from . import imagespecs +from .utils import get_named_image_file + + +def render_tag(ttag): + img = get_named_image_file() + img.name = 'tmp' # FIXME: If we don't do this, we get a SuspiciousOperation + template = Template('{%% load imagekit %%}%s' % ttag) + context = Context({'img': img}) + return template.render(context) + + +def get_html_attrs(ttag): + return BeautifulSoup(render_tag(ttag)).img.attrs + + +def test_img_tag(): + ttag = r"""{% generateimage 'testspec' from=img %}""" + attrs = get_html_attrs(ttag) + expected_attrs = set(['src', 'width', 'height']) + eq_(set(attrs.keys()), expected_attrs) + for k in expected_attrs: + assert_not_equal(attrs[k].strip(), '') + + +def test_img_tag_attrs(): + ttag = r"""{% generateimage 'testspec' from=img with alt="Hello" %}""" + attrs = get_html_attrs(ttag) + eq_(attrs.get('alt'), 'Hello') + + +@raises(TemplateSyntaxError) +def test_dangling_with(): + ttag = r"""{% generateimage 'testspec' from=img with %}""" + render_tag(ttag) + + +@raises(TemplateSyntaxError) +def test_with_assignment(): + """ + You can either use generateimage as an assigment tag or specify html attrs, + but not both. + + """ + ttag = r"""{% generateimage 'testspec' from=img with alt="Hello" as th %}""" + render_tag(ttag) + + +def test_single_dimension_attr(): + """ + If you only provide one of width or height, the other should not be added. + + """ + ttag = r"""{% generateimage 'testspec' from=img with width="50" %}""" + attrs = get_html_attrs(ttag) + assert_not_in('height', attrs) + + +def test_assignment_tag(): + ttag = r"""{% generateimage 'testspec' from=img as th %} {{ th.url }}""" + html = render_tag(ttag) + assert_not_equal(html.strip(), '') From a07bc49a25e2ef0a114077b1f2445f5984df3949 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Mon, 3 Dec 2012 22:41:02 -0500 Subject: [PATCH 127/213] Remove inner Config classes --- imagekit/registry.py | 19 ++----------------- 1 file changed, 2 insertions(+), 17 deletions(-) diff --git a/imagekit/registry.py b/imagekit/registry.py index ccc9b7e..5961298 100644 --- a/imagekit/registry.py +++ b/imagekit/registry.py @@ -1,4 +1,4 @@ -from .exceptions import AlreadyRegistered, NotRegistered, MissingGeneratorId +from .exceptions import AlreadyRegistered, NotRegistered from .signals import (before_access, source_created, source_changed, source_deleted) @@ -13,27 +13,12 @@ class GeneratorRegistry(object): def __init__(self): self._generators = {} - def register(self, generator, id=None): - # TODO: Should we really allow a nested Config class, since it's not necessarily associated with its container? - config = getattr(generator, 'Config', None) - - if id is None: - id = getattr(config, 'id', None) - - if id is None: - raise MissingGeneratorId('No id provided for %s. You must either' - ' pass an id to the register function, or add' - ' an id attribute to the inner Config class of' - ' your spec or generator.' % generator) - + def register(self, generator, id): if id in self._generators: raise AlreadyRegistered('The spec or generator with id %s is' ' already registered' % id) self._generators[id] = generator - source_groups = getattr(config, 'source_groups', None) or [] - source_group_registry.register(id, source_groups) - def unregister(self, id, generator): try: del self._generators[id] From 956601b5d0c8f3059b84657224f8b0d8f11e4f76 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Mon, 3 Dec 2012 22:48:14 -0500 Subject: [PATCH 128/213] Revert register.spec argument order Since we got rid of inner Config classes, we can put the order back and support decorators. --- imagekit/registry.py | 6 +++--- imagekit/specs/__init__.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/imagekit/registry.py b/imagekit/registry.py index 5961298..a547c69 100644 --- a/imagekit/registry.py +++ b/imagekit/registry.py @@ -13,7 +13,7 @@ class GeneratorRegistry(object): def __init__(self): self._generators = {} - def register(self, generator, id): + def register(self, id, generator): if id in self._generators: raise AlreadyRegistered('The spec or generator with id %s is' ' already registered' % id) @@ -116,8 +116,8 @@ class Register(object): Register specs and sources. """ - def spec(self, spec, id=None): - generator_registry.register(spec, id) + def spec(self, id, spec): + generator_registry.register(id, spec) def sources(self, spec_id, sources): source_group_registry.register(spec_id, sources) diff --git a/imagekit/specs/__init__.py b/imagekit/specs/__init__.py index 581453d..906f210 100644 --- a/imagekit/specs/__init__.py +++ b/imagekit/specs/__init__.py @@ -185,7 +185,7 @@ class SpecHost(object): """ self.spec_id = id - register.spec(self._original_spec, id) + register.spec(id, self._original_spec) def get_spec(self, **kwargs): """ From afc5900db65fb69f18ec5e0769218a82d568aadc Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Mon, 3 Dec 2012 22:49:32 -0500 Subject: [PATCH 129/213] Support decorator syntax for register.spec --- imagekit/registry.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/imagekit/registry.py b/imagekit/registry.py index a547c69..c6d0150 100644 --- a/imagekit/registry.py +++ b/imagekit/registry.py @@ -116,7 +116,14 @@ class Register(object): Register specs and sources. """ - def spec(self, id, spec): + def spec(self, id, spec=None): + if spec is None: + # Return a decorator + def decorator(cls): + self.spec(id, cls) + return cls + return decorator + generator_registry.register(id, spec) def sources(self, spec_id, sources): From ea962b6259f689995cbb3ceabae1f0edbc045122 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Tue, 4 Dec 2012 22:48:02 -0500 Subject: [PATCH 130/213] Correct argument order Related: 2cc72cd --- tests/imagespecs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/imagespecs.py b/tests/imagespecs.py index 536db7a..8a69975 100644 --- a/tests/imagespecs.py +++ b/tests/imagespecs.py @@ -5,4 +5,4 @@ class TestSpec(ImageSpec): pass -register.spec(TestSpec, 'testspec') +register.spec('testspec', TestSpec) From 7f11f44c67e0cf3890cfc64c15443a27ebc20441 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Tue, 4 Dec 2012 22:48:40 -0500 Subject: [PATCH 131/213] Special case source_file for specs It was already special. Why hide it? Closes #173 --- imagekit/specs/__init__.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/imagekit/specs/__init__.py b/imagekit/specs/__init__.py index 906f210..b884eac 100644 --- a/imagekit/specs/__init__.py +++ b/imagekit/specs/__init__.py @@ -84,13 +84,14 @@ class ImageSpec(BaseImageSpec): """ - def __init__(self, **kwargs): + def __init__(self, source_file, **kwargs): + self.source_file = source_file self.processors = self.processors or [] self.kwargs = kwargs super(ImageSpec, self).__init__() def get_filename(self): - source_filename = self.kwargs['source_file'].name + source_filename = self.source_file.name ext = suggest_extension(source_filename, self.format) return os.path.normpath(os.path.join( settings.IMAGEKIT_CACHE_DIR, @@ -102,6 +103,7 @@ class ImageSpec(BaseImageSpec): def get_hash(self): return md5(''.join([ + self.source_file.name, pickle.dumps(self.kwargs), pickle.dumps(self.processors), str(self.format), @@ -112,7 +114,7 @@ class ImageSpec(BaseImageSpec): def generate(self): # TODO: Move into a generator base class # TODO: Factor out a generate_image function so you can create a generator and only override the PIL.Image creating part. (The tricky part is how to deal with original_format since generator base class won't have one.) - source_file = self.kwargs['source_file'] + source_file = self.source_file filename = self.kwargs.get('filename') img = open_image(source_file) original_format = img.format From 938e2e178b103c50cfbf5a0a4c43167c889c15a8 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Tue, 4 Dec 2012 23:11:05 -0500 Subject: [PATCH 132/213] Clean up test utils; write to media dir --- .gitignore | 3 ++- tests/media/lenna.png | Bin 0 -> 473831 bytes tests/test_generateimage_tag.py | 5 ++--- tests/utils.py | 23 +++++------------------ 4 files changed, 9 insertions(+), 22 deletions(-) create mode 100644 tests/media/lenna.png diff --git a/.gitignore b/.gitignore index f013ec3..7380ca5 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,5 @@ MANIFEST build dist -/tests/media +/tests/media/* +!/tests/media/lenna.png diff --git a/tests/media/lenna.png b/tests/media/lenna.png new file mode 100644 index 0000000000000000000000000000000000000000..59ef68aabd033341028ccd2b1f6c5871c02b9d7b GIT binary patch literal 473831 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 z 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

Q*ie`|Cj&2i?YTA`Sqeun>bF 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 z zo1c8f^C>Ss{_yROE@^#moyOKFtg6)GsWDU7h&a&fY~k9~>0(>IO7%9q&U_By>%K<1 zIDGg>V=9Kr-R+y{_Ev{{`tT>r_2uaeNnE?B4_?a4UwuVa|M;hW`rSYMv*+{E$M?^d z_fJ2(eR_U-x%L2oq=z{Zr69mrTU-CTzxmDI{M%pt`t^3qcKMeNk3U{rjdi+tef#A< z{Pfk&J{S3y{JxLn4%Rv4)5cr2`R*{M*B2$T#!zX7_wR9hqvTwU+1I2^JaNsAW_Izc zR1eWF!39yA$jjs|Ntvz5iFqo_DK+gPN$2?CJ3JN*I%w0%psKsY+?Wd{A{F)?v@kV7 zP;v?^;qlgGGVhIJWe)2oqzvX?Ahj9bw_sA}V8Q%k2TDW4*n-JxtP0Gsb2+;5~!g zI3i1(bdges8mkkLyH$xjl{r&l=909p#!N{FCa!6SI46$0ZE%Asi)ZfPEJ5izS3)PQ zM&Sw(shLKRlpIJ3PFB*MQ7FOW!wYd|mh=@Yl#FDT%CHb-_JBmriAXuZuAG`dfsOel zQW#0-kRZ%L7nMX7wks(zgD5EH%vI6Agc0Gyx~oeCKwBc#%qV3Hqf{r>5Z1+Gb=D5% zrkVwO}>5sgAu$>HV4!l$7V5(n$^}nPwYGdA8a@Cc&=e9aWM5lU;=@DMFOJ zXD!70Zo=TGBci|};XENB!h@(zoNG!`-8>tSQ?9N}#~5Pmw8XZ%`HuPgN8i5xw%z^& zC1Hn@C!#}UK7U*tmCWjaJZI%JD@)6#RXE+nImgv!8DZv!c8z`2(&}f2qq%Nd-}{Ev zKqvmb^L*H@muY!V>-&$FyVJ?&1^Hm^k=xVz>%+sNfS>&Tc{$xZ?p5J5MG+;o!{@Jc zKCz)(@HbWp5=B~;k9An@adk8OG*<)-NOr`y{wJljrO zBR0~*vA*bbafi{KP(`L_p^%%Knb#^Ww2ZttlGMv~-Y+ht;9WN=9!}kzo!bG|?(r93 zX;u5*e)ZGe{GWgE_1Dw!3_V zNrHH;@IrDa<>=SQ=ikyt5S9Z*Y0M&0CZj7Txt_dqbWm}Rs#DK0BL&J7fNFB#(Zc~% zt{#*b$wF8ynTRSSCpjcVDl);uq8T6rjhTdj)QHiUK|!hwo=>>Fqfh(1_H~+4doz?X zBnIicrx{REo%3>j0?#Q|2^Pr5$MO03Lv2T#G}Iz0Dvk3rwjW%eg`d-ARP;fKUBQN0 zxt83M#@Gv~b8F!?NT#`P)G}Sqy}(I1&J^AwlvKbbn0&il&)c@|XoA0x03o(@6Q&*9 znM$E#nW;a=ihxSelx$O3h-4~Y=;_s6ouw{V#yMo_(TT_{fjEfc&}uNMsu$VExbU^x;dNe~x=tl_qJxxmY)8#4v57ABylF#%dPH^iQt8p7R_wvzb zo;;XJN#2D9EOQi_k>|5(BY#F!-AV1!)O*?8do!lEKJCytS!YqHjFEjqYfTXV#mdUp z;gzUu8wqW`%W?K=x_h#(Tq<}BUzm62xLJ6;c2d$It>x|k76As6l9^-lTF5Y%L@2V8 zRcV{+qT}VSTL1Ca<<0PW$WoFIRtu z%h|6Z-FKzs=H~g~Z8|?ZonQR&YEJ`vnW$x^YuVl5R*r4&2{=OfU;TW#YoFF{KR$lDuPfF2xqkk|%dd{hm&evVjJJ;tgE&!9ua&QJYdc~aOVsh> zemFcooTkBfQ(|j_D29VZ7Jhi|ep?U0>(-;m(lDCbY*u#dgLp!?h{8IhD6b+ySB!7| ziQaB}I=1TVG?AOPpowUd5D33R`4ZzfyK^}tCrC0O&Urjx?N*+s93|97{4EIrbxJ=Ce z>R<);ZnC7W9?T;%OHYSEl3+b4BPx1lr7WdLTJ1bQ0v|+3Ds$#Wup||yBncLRXOu|i zCVnLm4tHjRo@9FfDJmf&i!-1JmzuO?E*U$bWP;Ub&zvT+tN7$Jh%DIH#A7d#6m!@X zWuov*giVY!by6u2UH&g${Vj~hyBy}S?>oe24u?sRaZWiP)Kp4%Pv`Cw(sNG|8pa44 zkz)f9h4pSqnMI2fJ)K$x7m^}OoCMZMNW=prBS0){=a5;dq!GE16l$DQxKrd#e#>$p z;Sfm-@9JLl`4YVkldim5?$WN;bS57oO978O-jINim=~ER2E}g7EH)wpMbwN6Q9=-1NTpDYUAU3E6VUn|1O_V|>G<_O-v8^{ z?c3kQ6^)T9VTvZ`MhIzzO?&JiFv}RbK@2JgXg%Ctj=y*#^XE{{tl9Ucx0g@<~cDZ^Cnr1b#u!o2951&5$FzCdHK}b|*J4f0o<$)9?SKiew@O;x9n3kzgmG%v74H zes;6`>;LhWfBQF|zgo`gKfnLjm6_<8P7E(dWFrA3Nf8p1xw3%Q zZTtRT=#OWaZjLodLh48&P;y*S3eZTBv{Su-Z^}wi5Q9=PAB700InFsMO$|t7BXUkn z@6aG9cV!^ULI2}%Di70&CI_*1u`1bAFruk-ntSqG8ItBg!qN!tLZOU(_-Z>*1V?vK;%vlSMJO_BEdnXX z^#T*WBs&k4b1uMsuLsgkV z!Je66qB6|q7^Xbn5(S(bK|m)~{6$j2xhB%tiHMW;)JEhy21P)eeJ~nfM^v5~*%_?x z0GvtoJF8}k-f1{b9H78DOlG)a0v!Y9dI|-qvZkJ?XPEU{0G!3XFht-3O zUaItn)ZOxMkG@)w1`SSDQ~i{keKQ|B6$BBt!*yQ~Ot`kPoNn%< z)am&0`surNoId~THy{7}+s(XEk==sUmw)|B@#)iV|Jzf)mIALxC-rrYiG;#I+(h`Z z)AaBD-9P;1-@W{j*8PwDhqvp~nVIZ#_u}-c-yD81S6uSrx6#{fMvxRDQV4fCTwE`k z>NMTITyL5y*OmIn>2NpI#j=!SN;zri)i_5VTZBj0k$DrMWTYx+Xkq3;rE)*p_P{^> zz#pZ}G~K;a>MSLuBQvOw>phVWHq<+k24S$sex`$hC-E&yAy3oi-rBMv>Dba%( zr!z_DOzMy%R#JsCr6DDSi3_<&BncDE+%K6u49tC4p~3m^G`11GZpN1(9^?6$FT_zK z3xOk(n6h6J398zr8LO}R<7dD5`NTpwM5}iulDZoeDOR@i?wQNa^ieh>%5eC zO2^2J+=VrRlCzLd2^s1_oFdGrNu@}EZ&unABI>rMLKdRv;OL2PcO$7y)19k83_2gO zm%Tr&&P64iyYy47H;3i^i+MgAZ;ks8NuF%aG@H(%$5vL!c19-)zZ_VLDIa| z+>?-F&q5I?kzn-_DGQ=!9Le3a!nW87kW|>gG`5TbWY1t0LL~ko9lUrsldJnAHnNCv zjVO{B0?Nwfp4y5rm&&Eq3OVn%Y#dtv+5+p9BXb{q*{r~NW_m)k1d$?!OVjlkG~9g% zqdz5tsz$d+0mLcf{20L*9nws8N|mUg9a54<7-?KGRwhXkCU7Cr5yKKeNd!qR)D24T z%>b^~EXwfQ_p2Z(_Td~%*S#PKRofiH*Dj@EQraQeMGP)MG=jPakBw(c|M(AI{hM?9^Z)wvANzBt zOXjrr*b2x+#9RaxAh~9nv|u5ZDg`E-?Jzg^$A>)s;XB*jWtkE^(7Mxb9T!&(EVH>5 zvd2+oUzejhD7LV{y!(9j;&fYI`gz`d*yw&cTlTni2-=aS`7j?ofBo67KL7mJpTEAJ zKL6tI^0s_2tMeAmpJaVrAN^^whjD$+`rQ-#;k$?b&xh;t+Cw;jLz#pNv0CA?U$`=J zVdBiL(5V%IUdPtg>%M{`yKgwQx=d714qB#mTg$e4-?8tx?iUHby9RQM?(^lLKdom` zT9&)t{KLQf$>%>mym+y+Iv?ke!;>F>`19kZPkGbYtD9kQUV9i{N561mHPn`+l|@gL z|KZ>K>KFfb|CRRfxPCle#wa>-`}}nG4__}|-^;X)cTYi|MRArn8r#@YnZS0Kl{*WB z_9f_4{ZK!=hQF-OnS0ul;}X1-qn2b>2`ijC$t-nCD$WI>8AzH`67c}>^?asp&v=AT zooW@boR+9WK)Qox-HilH6PJpTEW}2x2VRM?Co{r>8VyDmXi9U$hD_)+6<`BqPv`Q? zFcKkQWKS`#O9&&A4Z-ksN}WL?%0#qqgkWxxoSPm_2kA;uZ182TAyvoWz)j!~8;)c$ zXUgl|FP|RAzAxqQ_Q&h`!&_wOE^F@Dsz@!^YHQQP04`5Y*XKv7zJ;YVvvvLSBJx7ueXj^x`8a-QfxOpgvZLup>5E^)mNJ<}( zo&uRw&Ez;ZBaGb0cV%V$LLg)`sm#N?gK=SYe(j zD7Kvs_rd|^U<|akJyPLAX}ErX-iwIRu>6L z!IpzMoKT__7rF?^G(jZl6o!~N&0$=$P#B@c+2?-(}67)ab#=A8$Ew2 ztD3fNWA8SB6t$1QW@&{>vz<=M&Ew;Fnp=c3Ja>;W=P=|}idK?t`}yOi2%p46{N>O7 zx|Tk_dbvz9ybCwm%EPzcP{!%?jVhaExI2*(4NT|jVDj5j{kQ-9um0}ee*X0r+jx6j ze+b)RhTqTa^-sU5U%!y~BmMX`TJf7I_Nyd@y9U9P3Ya*@@{SW^BiNQP2q)HGPpG20-nhu z3quM`$3!QR1mz^zreP!5lN<3Z7mGTjO0p9;t*0tVp{xQSHcmlr5#Wp@iBN{JBoibh z38c*Fgn$uQ!i}ILN{vyNW~VlBsS|Q2lE94l>B=4$dk!~?0T~peZD$^Y`E;b()(@Ye zo|=LN#GlhQQt&a3)6`hqlT8Nk^kR;MH7h2`Nt3q}31JF?jdaPqTMVdX#v%7IM8-l`sDS_n%W(@DV`5D)Ba zwg6*@LYLa6S}YT1p94HO3v>cX0(t;9P8`SqnSxmSfsUVt& zi$md{%0!ti~c=D3r za-f|9_17XB+=Nh4T+?SODC8cF(XpTzL{NPx;t8> zsjxA5dbFlVk!%C=X-XBCGm^xEJtZB*14Ngi;-H2YL0y6HR>g*gNON=5^saz+M7Xv( ziG!GwC*JPrSpU=I$KUn$QH@+O%Pi~ed*2YrLTWp>MiuAE5s^s&N{a}M9mC22x{h}b zasH2Q{roLdr5*9bm$Hm~L+HZSYB<@4e0aR2(|h)rebOy|ofKRs|8 zc6`H4hzhNbd<~6(?$2k6>pENtx_v@7uo4r(3<5bLQB?ePn-T0SWJyb243?E+`$zC^FT;VbAMI z%uT=~Wj?t^ky{_M@oU;n(#i672RrPZnOM)LxMParv= z9dbODn>pJJ;R3N9H9tK2bm>Y2r2Z@pv4q-OTGxeFcWCXep?V1dg)LR%4 z0!EYo4NnFeX(l6*nL3m+lMP9MbizhF0TG6z6%^hMm=zO4PE?h00v|X%n$()4^>8RG zfV=iEV9P!%oFq5Hz6Gzm&~bk8%{Pxq8y|0}H8V{o8@oSlJUkG`;~`1HU2i|1PxD5> zDatI)Jc8*!rG-0HO>NNtPA8`|lcy0@2Doo^H3C{~=Mk~zUPVnpOzX6VA%;jv+q_fk z9grAs;t{G7x6|=h>+Rjt7Mg3DMwg|4F3?3s7auydxp?8~m9%EoiM*u`PIi(ayTCiM zvp|JN3lniqUXn7}n;b*9f~vV7H=hcam3Tnnav>jdc$tIPj1Nht$DuO~Ydy(! zr>r2LSQJE1f&l0aU=gG{gPEM1TWpa$XfKva<}IbA3UvmyprjxYBnkD6IYBgNiUjL9 zybv<1J6RIjRX~!q0VzY||NfhQCCom8k`rJKHsofJsgz*HB}v7w&j@VcFouU0LS!O) z1XnQ(j#{<0dBR=_YYizXWl%sGBe_fyk|hzPOLBU!G7F{b#Es$6w`>QojleM{@5t6T zg9i|vmkyqsHg*JLM~=hIc3peO*m9t486~o5l~TwNS`-@86o|+WZI%0;wQ%F0EXtW- zUQ3G&odli@{w$e0k?*9_*v2qbfki?nMpDuMl&J+3rm37KmG2z z4-c2?$B*k88nssU47$0!Kfiz2saPa48kg~O?$}?xeqHZhefs8Kp4Y*JxHTX9bh^=` z=l#>=!xal^a+~JuecJx;V0fd zIv?p`-~aCSb*WMhG##5Bj;EV6ee=_wzxsz?NW(JSpMU)0zV<=n3D)JP_=n&8;qqa<%gfU^es=oo_3hujtgjAo{sE6?X^Pty7-uh&kEX(0wp?CH9YJ*s zxvUDwWo}AW&cRxgUBTLBotIQ?RekGm9@x)8Op^+)AaOI>ldMLK_WZG)d$Fjq5F58b zR7fU>qfMbqQbC+1LQkS6sDT#_PpT~R7o1E&@Eyz~oPHr;0z(+qSp))NN+y93lxgH( zsx9pb77hgrLV!|mW@iA|BAq!oIMZn79QzoSnH3b#$ zLcAN5HWkU!;T5g%+&}&DuYY#BJ$UyLXFt4o{O-H;-9t8h{4~V(Jt|w;<)Pf&9xX$7 z5*AhKBiR$I1f_x)I)defNvK(sqvU3#YS?JauWL*VmPxb^D~m=4Y9YjAmbDNd(z;~d zX(R@5;nK9s%&$IwbGzv2)qHc{QbZ^dEyg8MI8vB$x+@hbsZ*8GnNqo?qHNck<{VTL z2oSD;BxHK1Z-Wc>vq%hLvNc(WC2hEAVA_2rDV)v6BT)gbX#)&rNO~utjy}M?Cy_f* zsEzJJQ-f^>4u(up&P-8)B_G2t1Te@6l51KfuK=fSEFesj7kDGvjGHnRsvQh|c!^XCgg9i`g z`PFBvjr#CwMuPX0eS|T@;0}thJvi5dOP`S>nQ2C`k{<#W=a%hA`jl9r?67oIMo{i! zZV8H3BdzQn9$DQT_QQv%X6_xrBUWLSrD9=HBGGUP4+(e9Jho#MgR2%e5N>%L?rEUa z1JxPsg7Xk`PIj&>-OB+YX_2U9DhMtlD7CSRRPHFLA?-U+Yx4xL()GRWASSzuNi)j@s6GS7$*L)y$}pgqS?E zxosL{zU9h2Ho|(XeLdb6{^?7ZUfE6sqx#O%VYBu6;FtGrcPH$tS#CByY~zPVe|yP~ z;j6p%wH}w3KmVDJ(GdtwTC2{RS(hT^xUH*@*V|huO_#$om7Ck!R^{pW>G6HH^hBen zlFIHx%%pANf_=1$x8p4%k=80tx2H)5Q_MW|?Rt3}*N@xIeNExNjf-%g5)3=X639)%yv5`^Eh)|JC8E7dNFoJ^c%| z6{_bxzdF|K=C58&FOK^3XMFR_ML8@{!k3co_)fA3l^b5)hE~XA>jRf6I@YHZIs4{$ zrD1hvg$hY1Vf2T6Tf@BDBw$$)J#p!L-4Q}1mv7!JAKp)3UG9=O^gf+gh3Sketmyz* z(74dNV|opC>`u961lh*!Oayjt3HAVGa(2!^aA#~(TQWd^5Xc}&uoy;dCU6zii%zb!ia*np$U}pVja*oY(NX<*zx?6H^Z6VwH-y+Aq2-W1a)=*_%+nrD zdR=0a5wTvR2*w6g=I)haA2~*L=i$EYleU5o%fXEzEHW`_)|AM-i(~Jb?G_vp_S2i& zdUq;^6Hk%>i7i5Q@17pZZK0JRDM7r)irm%uko0Uv&YfwT(6Oul1bTTi6W-#F11vC$Ks#L<%Do zsi$S{OeAc*2{WRw(8v)kla>i+Oby_OZQItJLVSA~fsSIq#@J`gBAt}}qOt89Y8j(? zGBPE?35}8v3|4YaS?X}2%}|82@!mxWbV|Dnu32V8Z#*PRTplw#wI&HDmEkhGEA3k# zDo|nNYzQnd~QR@c!^zp+t=fN*t%IO$BY3S*0dinCz{ru+X z;oWtNq!6Wby;7}4mVxbJeUF>tXH%O#d-3w~moM)>|FU1UZM#MgL?+QD$IG|xAxWiB zAI2I}*^T4jfuSHN9JhxzV>XYDN*uyMmgV8;ZE{Un9zJ~+T9%2%vu*qJ-M4@G{cr!5 zfBF6Y>$iXU-+%wl|BvnYlMGxx6_F`Ph+1?1*{hd-_wT>@>5H2echmEquwJeY>w+D0 zQ`+DEV|y|1{Ea=VV?I%G@*&!q_0IWq;iA?JQ(_@Zi5{Et;TEIQ*p1X^GG?x$QiE0x z_sD%4c@%gSyI{)x(amaKh!dxvvYwJ-o=SDJRA&V?jsLiA^ypld6 zh4sjtKn(V%l9Z{*;;Az@QyEgh3Km#81;GJ@3#=qhLBiTtKp^G?4&q2jvWcV<0SdU{ zFDQr;#30y0UYgbfm0(q_6U7N3jc3Hmgi+IE0+LclQv{`zB+^o3Vn`VaWS&SBNwu`8<%?IxyEpUP>MhG5SB8xI z3v{g$lc*+$i?fEdgNFt$A>?ELoJYz@_j4|uEmAvbFl*LJs&WIWq`*{3Ea4`5t!B%i zsY#?x(8_^i9v$jCz#M@gR7i}487>FU9^et;iav^uOI)|ua_n0`hd1AMqR~@rs2P(Q z5}6=g$tsOde?mv}C36o0qJw;-W&yZIm}L+;s*;}>Tb7oR8IiGrn+=u!>({@@2xKTZ z928m1Vz?8PR8LOMC^dC>)>yaD;&7}E%}i#G5coCW>C9T^qONj~nT2SYYU&Wov?wJ+ zX4am}sVz&_-jN8Zqzsyvb_f%kVsRMnL1iUs+#jP(W4~fIW9ME1VbAA%>sPDY!?w|f zZv!=`vjHSMA;JenZEQ)Exi#)#k;$ZC$3orK@2a%RI>-Oz_-F2 zkugu$I<%s|iNY+Ii!hnbi_mAch65g+21Q~5k$C56vPDUf#vWQ4x#S(_$E z4L*(Yb@ZsyZfpJY{`nf+3eM;Kn{OU|`~GTrSl8~6bDQ6M{hQm@_f(GSWE@}JAD5{eUhtgfr>j`*cAfNC)K7Oe z*QaYZjj^ZNvEJX^-aS2iZ@qDvGpF~Km6vhzlb_0d{PQ3G6A06BDwz6M-+%Mr@p^r_ znyuN-AE%R?KL3rKQ}da zcPt7i_ZWs-|+t`%|$Ak2b?diGgyYh|l#*2W2qGxHM!m^W|NS&qSV2HCRCgn?_ zKIdsBQrL5r15pr9JS=IaF}Tj)V7sCmC_U2{3@z{k?GcJ}ApwMWcqHd$EE37YAsOie zAp}x@LIEO6C}BiIc7+4|n!ClmJEvvaFXz2m7TNZ=1O`N^fS5$IkV+e47mk~D-=Cg@ z;wS&=m#t=rVLJKw`NJQ-lUHxfAD-VouMu2T2~3=-Aykl#!7eGnl(N%MChI#Wp#_8> zAtB#vgWcZTZhMF7yxi{V>cSJXO*ajU;E6hEWg-$FJkD-|9YbU)O{alZua{RZZV$(H zvyha=(am@d^li{?QW-_cVIg*|gr)^oN>N$YzNiE+k_f5nm6(_i)6A;}qM#Y2NT4$3 zL^P^WiiR_liQ&Q86civmkQ-{-jp0|+C8Rb}5sOH$cCez+QeFZeb z6T}ph6(utR+8E&@vL<F(dh*T|7!61P;Yjx?CE66rDf?bABNMsSpYzKCb z6euSbYA2##5?>BcGV-k9u&2lgYC!-GB9VClHpJ>7*f$=an%I0GEfjT95{N6IASt0B zj_ zjGdS?hxCwjM+qYIZU#JBz1^SW#B`izR#cssMbvky-Rl`t77pYHsm$HO{HhTZ=510L$KIZp7{Cg0Qpwcx6)u9qo#Q^Oc zehw_?J4tem$Sp@B1G!UxN@L#xnKUOrlB5Be8Odp25cddU<=~lx6AnbC&_D))XDS8> z6bUXYMWP6^OhE|;l8EM69oRS2X+BQ1NGqjbs#U8TGMT#OzGios?;?7JEz{w_w(a=` z1}DcqmQudhj(T8ZX4UKnoF(0ApqLRVd!i@%FL&kjI>Z#`17+1A%=~CY7@7o zkI&R|zg(~H-*OesY2}qWnRAY9c#}rNk%mZdrmO8rGr40MGK0Y~m17;Xg7Z^`S0VXE;lqdDx()ySp$on7B$2uR>F_!n}wu4OJqJ zPR!ssOcXnh3l}BNLtUqwIoBe4U<5}foC)qdSShcq2whh>*c@ZP5Ifr5eapgbu{*iv z7s;=SN5SBo4&Lz*XU!#xoG0ccor_B9K+{4cDAAa_0 zxchR~&6vAHn$$R7cU3uY4i!?SEuFBubnA%WRe5ZdN_(d&xppLsTNPySdC_{?G}>If zNGG1Sfi;B*%#>YdhZA5-7Yc$dX=e4rI}=w4qf7*|102|mcnaT`%1EJMLYjWP%E_Ou z>v~x`nc;a04l+u>3RR&YoLEHMS~7qnO535e_nd=Y=tveSLE`kl|^U&1G_S<*ow^y5sjgp?-hR)^18)^EIw`X&0yGM?!Z)Ed< zN?a*~IHiE-@ciTV*Y}sT_isMze|m})?$dI6_gUYz=jR8Q$+Fq?liqzX<@o*&-ywwA z)5qb(sTHy7J~$QZ7O`)ATwcDq8(XexJYJt=dc$$PKiuEE_-sFac>4YmrH`E#o#D08nJl%fb@A`Ir`>+4%&CmYs)i1wT=+mdC?=I`p1?3>QwaEu03se;%bX&-U#V$AMH_K&%ftcLKqBcm7kDRTWF{%B zFoHOU=WtN*q$A28Oi3H3A*YnVWlCGqch-e+BnkN-;vi)gn1wXX9ULS}0>qhF2%U6- zfhmy;I0y*`T6lOycEe?h-qG#)bPk7^uOl|n4c@oxGOEh3Bw`y|%SyQ&U)|r|y!r9{ z4`04K-hTFib4=tG`0!8vobB~D-@i4=BwZ$o4bM+2R7ojlXha@Wu7i#f`PSx%c*z_p zOIV-@YXvKO^sA|&OUYW<&GuDG^TO3VDY;d)o=#-$A#T{Bdp3BVYI${gy8rCO-NMVU zAdRLe9E#OCK`P}A&rtpkg6*QwdBH$M`ls% zp2$#J1P0B>M3lpfiqDI2CqXjWw64lA5w!kh^DMm=|kTY_OkG!T`xgeZe~nyo{Evw?QcXapl!B0Q6mgrY|al0$k(2yxFW%8>utum2S> z4)QMJ91TJ1 zW!pUhyX1sK&(?7gt}8C zGAW`^Sk{7#WP*DQ3Pg$uDJ)gk%hJ8@#AlbrZ0j!Js>rTfDVarB`ZZPHT}7cm=`u|c z7MwyF8d`|kv8ZM7E%vc#|1lpw4j(z`*kqA`yBjGBr-jWgp$8j#k+R?abmQe}bw_#b zZ5_7t>3se4@zeVLig!=@4 zaXXdUyPx;Vd)r5y58|}jp5|LzV~o4cf4;u|*t2E-aC3M6*%x0FV!J+H;<8^m+}(Pu zGF-J{+j}Haj`?PK@%pRhAKpGaf4I5-^5v_$hYxSZa4q%e{6wtlzP>p9)O}lSZ*E@y z?BT=Pr>A|(F4O`!QVJ`9UcS8J-M_fI`|_*&>tEhZ_3iqtK0dACMQ5R#*RTHer-FCo z`Fkoehs1u#Nx~ZOnWj7FPVJDfvrQJj+$gtnxHlc)wyobkk(9gFuWLnJ4AYXvwm!yr z&8_dds}uG$TngLi@jKmGjmQG-NU23iR&k2R8`4lsFo-JrDMC0kRe&T+%o=IZj>KSE z5L?cLbOM}7;Y@AMI58Y50w-=hEaWJDO&87)QWH*OlpIKKmB^$5PH-WQ)I!vm2?PWs z457?K(ZQL)5uGkqw-J|N-F&z8y4ry$>tMcyZbS|peH0adozfx>%sJ)D+c($8=i4c6 ze)hGNNyL0FpT7IUr}yLB_jAwJFOTB9#|V`A;((FpqqfP@m`H{pl8WoK%XCohM9Cz8 zQHrOfDje&zXCx9jX{MHlL0(HKwIqSqBDnuWLZWDuLp$ETobSK9dG+Q^J?OD+7~~QW25oGs?8a1z&{#Vy2N^poU_(@QLQbG;kvuz zbS!I3zPe&eq`Yo2)!aH!lR~94Xf@&#@#`uQttu>-jg!618KZ`;DP*9OAn=ydq^t<{ zY{fjyCF+JvR6N4Hsp^z4-C;#VhK+!tM4u0>nAx%(d(O;`Z+TG`)NKXJ5y~T~j-@Lp|1I?t9m| z@HGg}FF*Tt)9wEF$@2DiXg8Pbdt~koeuz)~VQTLWw|6L)?)Jsc|8|*5|M=t6<Nq*@ZE++mRj|JR-$er`pOsw&--(e%x^bV_K#$&M+Bd%XHJGd0!)Gz4`Lx>mPsq z&7b~-WPlp&gMjtv^8R;!cszYk+y0CH@$X*QpO*UZ`aAl#^5r_FInG!Wb>u`+2-|VUP*{!~1FC3C?4M1`qby&2W~)bYFuP@QAKQ;{#=46z z(z_3qDs44y} z6a{6DL>hPr9-KyPD${k8pb|VY`l&^~s5Osb&f#vXBIusj8`y}95kyqt_IAD$<+7>F zNk)f5))z*XgG}Gy0?aYY_mQsl>EUKB`|X`z)h)pu$^dylhQGp-U2O!FG^-PoWc0z6 z4}vpn8>@6-CMMy`b%bPDV(}aAXILj@k;P%yosvUYrJi0L=Aw5m?o0JLld;a$dj^Jp z)#*?VCZz_iqBG^7S~V<2BoZt+Qg8_>#JleidQ7?yvXH8sy)2wFiFM<$2Z~E6K^d8v z9#SHV3Ui1gl2=~E>u?+hPb``+V9-UU+Zp{TC=*S&e`IazFeR!bsf*YZ#5`BeiXsv{ z$|SZTE)dB`L-AnGO3UfswH>IQIApP*taAAjkMZ) zOszB?vMjuW@fuZ0W|B2ffkgz!2cnt%N_C2IfCN&Y35=YV+Am4Q=-Qtn2dPHv;X4Wm znD(L9i%wA1PyxX=8SATv$jkRh1H#Zp={%sP?gS6&Db*iY8@(N;}aQ(M0AL zMHwV;iQ+0XXKLN@nNOQ6!%useVhx9$uW4g? z7}HZfZaj59j%ySxhgZEG-#=Z>|NO1|@Tvco|MdRy@v$6n9o!%HDtS1yQYBILz45xH z9xnFsZ1HxRHc;>&1tsuDsnime z4bpF4y?OD~i}(NS|GQnU{jx?}3(cwt#)QL*z`gDV$N9AACriBHcL16>}k&!0X_r*>*D=CTy!Q@g2Fe)!?fv2C}n z{;Ga{JlrjZdV(qS&Ed==xo+3}o8SH?t!VP++sU7P`|;Bg_NPxpntbtP`@6ro|CN>f zVLx+N%>+kAK_&){L&>Z}kK7{V$|)%eD7-ZAXo&+bAC{xW@u)SgQY7o*kB{;FV{Wc) z$YhdA*S5ypc2NW`_c}M4D%Zi`8JQ608g<6TQ74R!sltrGES^+^Um_LoER9G>2U(4n zSe?2WSN1(UGA&ay)+A3ZWKP{_JyVY4jmJo{*fRx@TZHm}J0T+-85cN3L?W4c{zZ7A z1S(}FM*3mwk$%q7Xuc&0ZH?MOff{{mdy7zSGdnRiTm5=9-$`h2ZDO3dzkl${N@w4H zc-}u=(}#`DbF$J_b%V$vi6B<$&fo8GJ60KOY=-*7QF%xw4px@aT=HeYwiTlj8M+!g@Fc!%kN!ec&LC~1#?$W*vs&Iy&36cVm#i@GpF;x$!jR@OSwkjj4G~8Ia0dVkYRar32)^1-ltbAn zae%`VZ8O?s!K)$dsFo37q_HsXauVI{Q=i6^Qn~TV=d!(4-%m8{<9vAh54XSnV?f@W zEBO9PZI^&~xVfw+R||^b$h|pGI_u5pmL6ArY%i*e8W8=6z7DBxrBB;*IhNg2*-RCI z?eg;Zc(LicRxl-?giSj{0dyjqcGu(ciFwGWoIiYXo~?D-4_EGV_2NaIc9)y$l6TG{ z#wbayfA~ur-R%CucYj>x8B3X;4&7tvCs+dzqgHo@1vQ?A%`~Mg!CajT#5KIb{-cjy ze*B4%zj*n@{`t%5Yt{Swd^djl>BG0b`S#oY6*oj_s;G$Q9Ew`D>E-jw3-;3=AK!ep zeE;qE=KJH?>;C17&1awQFExGm3h&PZ;4-;!$3d}>NwDj(*LpNfju{Lgizo2eqMK`K zQISb(PM4d@mnm(wn58fMe6EWQVZe^0p61gq+tdw7Id7y8Wg~U=jwMsxMq80CSOChF zyMyn-Pd*HwN*EAm(Ev0kIV=b(cn(L@CvQe>KrK>n6$GR>Iqe85Y6~ys6)XS=s(Bg! zEYb+-qC$qqgbZ$E=1vn}7!HJJFak(mLLjsV0|G{Oh%>~*l5yK;oFFIGRX7~;21&LV zbfyI4ZZv?9CZx!YZHcH65KS?iI0DwTnzJGf6oeCQ_m~kK>9H=@4Y@-pn`f7KNCmlD zO=Xi3o|X=jdB_-jttOIb3Iw;h#!65G(411FN~^CYXl)&N^5-Q9T79uDY zh|CfM3*k-)0xJ{tTp$c3TcQA9cY+02A$1aW6hO*Ap*0LRt$-lOp;k_%ry=DYVvd+t zhk%NmQbuhM+PDZ*Mv#;UoHH@>aMlhHqx)HLa0nZ+rLrHGNO_5I!4&EB> zdo<`4oY_in5F=tC_F$tx4rD~P!HpB4L-lM6RyWSVn|N432thisb0R`UMi69_fdNT7 zrO?bttp!^5z!tC>A?*k|0LHYjA;aJr7vSeEGb)94bAUiZ5D)~#Rya-&FTy88P%UAk z$pQe=#;gL6F@<-Di0XOi)H<{tNPQ|v7^-82gbDL zxkPaIy27f&UYnMVKrDph8r55EB1l;xBP3!3Hq+ohUlBc#kh%!ES?}o4P|+O`hwt@U%vSCpIoj# z`!ZdBGJN{O*bLbRTh^tfm9A@g^@!Ks`?&0r?~L=I`1{gsFW>&hm-_WSmK~fX$JE=? znUKdkY+nBO$J6$*KOEK$U}}wngGo{;A$c4IRNh}*E%SW$^|x!Qz!%eIyPl4&w7a|- z#?8&EKT>%3_J97jZ@+(Nra8%!_AWV1+wHXd;^yXy zzr4J7^%AIl^Y+8*)1j8#ba_2}{?X-6Zm`eoVO8JwW1q^J;3Co(usPjXFt@ zWME+_ksVP|L}n2hX?w-f#Yma3FLZim3Xv^v32y0lJ1v$p@sx4e;51+bURIOha)E#d zr-jDzyu`5Wk=$e3>^`bB?Sn1 zBA=@WoV|>x839E$7X>CU>V%9=Z0HX2=`jVJXF7iOjv{-jJO~drP$d$TapGZ{Ho&l* z5`jS(Vz{`XVW8A$C}l4R%-xF-^d6i*xl8h#HH><7GRP7^9HCB-$dGzWW<5xOL@4S3 zFq5((Q?CcieOqXAy~)Wir&1PBmrhQP+Lc$dXe|RI;Yy;3hD?f^hBM-F?3BPL=b?93^D)_1S2M((Pd8ph#M?94H68AM5!wxNptkE z9T_5EB33sN&zS*wb-C0)vH)}N3d$Z7%dz{WB}HON4P@8Z!wf=FcWlRq0bsH}69{y7 z@Rg%E*ow3fQj{t6CnO6cvIN*2C2&t>$t(e)%}vRXl2deuHoSo7Eu0d?>Y$p`Dcl@^ zEko_+Lo_#CDB0RmWk^9{QNTUAtLFypV2OMU!W6#ezPMTRy+ew;brlG67w2qIQ8Zuz zU(qXQ0B?aYJQG-iPz2xv5tg@_m{^P`6{3_31lMM*bq0*kz}lNPwzZAIniHNiC&3_N zVyAFm7Gr5NC}46YKoItow4`bkns)}0)C%!5RUiui2Mjfe?nqoEC|d*2Iu0n#XpvK| zmaJBgxkEG34q7;p2S`Yk@JK1TBXSrdhXC`XvTH0|#!@~%9e(m<`uyhQUwrY=^YN3L z@t1P`>2SX=Vm-wy&z{P~14&FwI8V#%R3C@a{pRpghD14JIrj>O z+q>U?yM9<)$2?tML(^fmQB+9TJ$I^%Daodvo^U8$bo=b{LCWFj_Bhv6WV_ooeV7-u zI&a2tb90lX^vUNRef*<;Fzx;D?cc_0&<& z{O;R(&jQxipe6{y{i_$mQ`Gu&f9CA_k3N6?qaXj~pZ$05KOCx6_A+g5%Cw~rUmriY zNgo(u5=JFA zmIxjaI1c-80?NHeLZZCXh4hXMB{3)TO-4y|5S+@nd-v?zcpL(tpy!JW-nVKjx)}#Y zGz|m}&KbIMvJ*91Ic+$RgRcOx%XZSfE@*&AJfvQ+9V^ZYVY49*FD|VMkftFIlpM&3 z-F*c_ps*AUn`zqbc6q~0;sGNrBn}enD{Jn7XIBCkhg~rv>cJAUnwDq?VMsfMimo&! z)(K64Mn4fQZ_snom5?kZLuBj-2*AqC^MKVI83EkU$-yv85_O~uWU)4-0jYKf(qRZg z$xM_XsX|}{+-NRnJ({UfhDUalfg+QKM|f~y2}2}g=h1o#Z!8-~k;oASP@;%nV`1+} z)z7^xG1gw{EZVzv18t#Xx3%H8VYTgzgS0T9|QmcjE-) z0Bd+xa^g_$A!#BSoy9N*I3Y9|6HW=53dJyDC1S-+-$Lkx+nlK4fsIz2gE_4XLpk0tJ%-Vh=W`W||A5;o1jv zaf&G8C>=&~jUfl3se%Kq)femD1xY~-t)~G*gRs^POiBrY#yDU5;iF%Spa0eN+4B!C zH;=mx@cOj7fA^6-d_29|e{qq9v1>i@^GBDb<1sRB<+yKsH@_YFF%x1cC2=U6SA2Xr z{{5TN+ZvsGls%~eLIrA1=e9mrGSLKK2#SorKqME>Uf!KvL!i&Mt=R~Ees`aEb8)R>M{T zu%<9S;+Va-D4foBsx86!N6)TrcHV#U zjo-rvbhDS2zxeF>uYQp5Xiv9*i8hk~=54^lF2;7+Y{N4kyQM2#_d) z5px)-mOV`q0Kz;|>B-i9_lNd}U)6vAeY`!|Y4#P`^3YE!*y_!|J!S(VfF6_}&?#aV znHno&4oDLP0yC&5hzNuNRFG3ALlFqt8CJpxN+zojrhW{TSP8Xb1Uo_pR09P7K~*Ml z%GggB1dS<{h=#oru0R~o$gA`@qD6qakpuVwp-2+o76bH9<7lgcLnIg&K}oVf0!Rw5 z*!0l3%MhXCSU9p9Ihtz#Lw`D``+9ru$7Oz8<{`(@Ju)ODz_c0bLH9TLG8b2e%xOD? z#P-Fu5y&v6>#R?0zJ2PwCG)%iUcqU}r8pXP_0}Uom;f=1l8fD)t*^ty&b>nBwBG>+ zO>If-?aCEwK)kR0De##DKOcbgT;ADH)ZF3}h8_WmjUNZ6KZjcG5Yp z_6Q^rG31`6fvHH$WZH&FV{3eh1TmDgYT#%&v9HA5K}sk&37nDl4#evdb?D~Z11Kf7 zt^km$wTNQQwV!J*G;s8Vy7x94b(P#GV>t9iYiDz@7633#QCnx0$N|0Mb|28(0>WS# zh%gX=040PG$UR@cdIm;p3o{5thkf{>c}zandDU%BXrUmqqKm^K53j%a_mw}Uby$xOS5nlPUTUnH*k6RDxx8qUf_>Cwz@zk@0s6AmXp>BIk(bkO zr>8%?US7}OC}Y__`}l`T`(3_yfy=|=hxcGS+OSHOaV)5<_rR9&_3`w5eKPNB8ZTf| z%vLW&oW}4nO#7Qp%BYLpAO84sembHI^Sj@#jD9+M4c7*Pa1FpfL&GPEl11!%$eVom z{POtvPwuSEL6i_cc2e;8;ZZrNBE&N6pKbS-@BY{SsEMYs8@A8p^>K!Im{X-Qmrh_rPJ3ZhrE`tDn6buT#8zACDk~Xp>0Qi-YVzm#6@gp{`l(jRyt~ znObYY7{Dy1<~$AVN(!M|28w=%GI+}U_Tl`8fBE>^m5;Xhwx`*NNY?PF z$m1|>1qHJ_~iZG$4lgUA|MX~PSFfE$Q=+v15g|c zAP~WQ4Cr760c7D)yIam<*NUVj7WQG z38O8E6Jkag2#%hUP$4@Lj$UU7?9`e5Z0q=BH95o))7Z_dWJ8M6$#+6aC?JXOE&)IT zr{=!f;^PUSIR&q)F<2xA@nz+RMlxVz#8^z@2`mRxCkl*I7|u_)*{C-NN~xP-qASN_ z^D&hRDkGpfT^d#yh6uuNODD83VkDa)j;_i=*w2tA=oHvu=AeM)PAtq-GR?O=h(d5( z!Nbj|2aqS#21y~(M5HC4Bc%k?S7ifZbabGk(8@xMd_ZH)(K`{zIN-8Kw+@aiL56NE zr);EysS&v$X7UAO%CxL7W#E)r6-Wj~gOJfe482oHzG@mQQx3_A)CG~U%>bA{fuaU% zq(`?3++VTS07FnSgZK+lCLMH?R5QYlCjjw zPjqFZJ_t@e5A88ott*)=+%&!1`P1XmacDC<9Ti}tESIl7*==L}u$GHgAO8MV$7OLD zgBc-&scg14SJ$vEk>vgFf7|;R(x$c3u8cVG7)gk(Z(dGf=MR5+r%%}IxYn|}PIy?J z=Ec@R2|5@cIgkYwP7I4vz?ylVHl@Q++qmAoUgw5HWI1;0<;Oqjx~A#Vh91VqZFlj* zBo!ll^zw(}=KB5JpXSGR=MK~4KYw}k`9FB}gOB$9@OQUw;%Fh;>BXnpXTSL2_+pbD zf8XzdMsUko6}JOf*tFW?43zxoEz)Rbq)o6z_r{QegCK#I#3|>bt3u{7h3gDPk(TrO zKYabq{{8#^xbRBjly@(-)IgS*BXcNUJ=^q^vjy&9ePn*3GNkS>4K#!efPO=O;Rjp= zH$a17lRyJWoTVkpTd8GzzL3g2aqa0;_dFQMUj$QwTfyd5`%myXBB+r#?PNrmVLd^rv?zu3~B?67K4FCk6bK!B-N zmCa7dW?62zDUOI;C=b?q{^VsAY3uj03qRE{?H=n938R~gn_3$(V;WklQ!bR*YN}|4 zKxImS1d@141jBY1M=P7su#JLUX%F$#W~EYap%~nkG>nuYxDpIz2+W}d)rc#(1t*7+ zaK-RIAZN10d2u0#l}O+mAcP3&7*P=gac#W8?i^r630uI9@$5XtDjh;j2?n&jI;fyT z3$$?93RJU%8BnoV8-yF6XA)%fj#;e_$sant;<6-D?GPkLsv%%=g27?#t{6hTSV`PL z-Gf{r2)ROMtd(w3F;efXW6Htm4O}qf&ols_IZ|(UCL<><)YO<{wZ>RyHIZ|L1m$kqiiehJ2S}Qo@4jsEB8W!(t(Xfp4w8nXfGPx(=Xdbc^O2m|G zt~3x>W>HL}oRWuUQY1O4LNpB^F`xam;`5E^3Ko-!j!Y{q6?gz&tw=E&*gBUHdLNKh z1M15-B_kyaNG|Sb=)PhWB5&OX4=9GgMjH=yBp7k~qh0<#{F7_{TfYD2@1FNI+41}1 zzxb#9Z|Ckje>g$LYA`VP7VFFF{GVM;*WYAdie18 zetuXE-_3DSo}P~kdH?c@yYK&5e)YpR-afqklctRVpe{LG4oRQge0KBs=ZLmT+r!hN zhnw}BXuP=a!+W5V#rpc;tyN#4YM?s+w$p)jH&Kc&wNInY6$zb?u6A*%9ev8KfxR}; zRVmi{5A(w#4oS2Cbm6kUd3L`4yXVh7%T+gmyt~{!yF7ikKdw*f`F_3sy|4bX_FVk= z#j~IPS3mu18w49<0$SFYF70rSjRU8_4 zrAS1*yAe4o=>6=c)gD?q&wX}%cx*?zeK^lu&pd`(kLE&nZfLznis-61U0!#equ^Kb@+A==}#w2bKZAV=?6r86Z)d=M^+> z_Va1>SX*Bv7DX0uXaH<*RBp>$9GyXjlcfr+`+9kauMAZXH>xV~T*883_Rg%$^cQSIF52 z%TT0O7%rh0MI)F190a)|=SCm_8nnYuzzQ}7DFUExT`Gk@WP&r=;6o(SirN!E>yFv6 zgLa|;VbK5vu-&A$k2K_%Hd@zQCJt93n9o*HU`p<-w=5+(h5%RU$drY9I0MSarPe5X znIlcfs(DTr>sC zA+-?3Za}=9;Q9w}^^w_EPp|*qcd!3&@`8W-#=f7u^SF4IP{+|RBJJmP`gC`hfBEIQ zTuv8P{Jf;$V*Gkczxh{{#137(Rvk*h^Euu>@i(`p?`@!An=oH~`bm*1v-Y06f?Aem<`jxD~>blBA~wL92D~7){k8=PjS^57ThfW%f3Q z8&M96zyOkQnnvcfAL|*r^&yYu6M_ys62Df5DsTm7{WA|?C z)e+fGFunj-&=nVR$}~5?LQp-#xp^n*J+N62U zA{>bVTJth?h-@CxB8sL}P+GSvJ^;8arX zfduQChHUk4Az529NK8GLaitZrfUbZgmNjS-)ilI$fImYZx1f?@P;WjAyD$W2$-Mw|o!fU7pbDROu>U=p;RatY8HsWkUUM!=n#!(b2%q(CHsj=-+M z16Vg09i6eKa6_aZ#ZF>i6a?(+k#rNj0CgbeaB~n-6Q={BH$18FAVhXPv*gIw){bQ` z&$)9Q*}122f~C5+D0^K@aux~G5Xs)Eo3m|}1t=;YiS&6DTOfnfnK z5({8(ced_D1US(q6gm4M+_jVQ$g&6vSa3#E2WBtXby?OiMhi!bmdHcSKpm%K#>)~`N*KHg^_#bwxl*3w`IjGE{_^v3F}K(6p`+(58(Aj6EhTUhC;-}Bn=2wVB__N? zG};K}K;Q~JK|$0@r>0ZXmX<9ezdEVr`TpV1k&s4T~W*QR@1EmGV(OX9hN7J@I9)UI(3qrDW zK^lP#mli2_b;}7&pm-P{79sO;4BgDrUqq5 z)j@kpN=G~Q-~3vC_@858EE!Xppl=6>jU>bQvGtMS)Fyf2s)wsBG>6)WP=T_63&c3I z`&p7$JsS#5*cU@UZCX+e9KeDAL%mK|?2kfFzHK8*w*6 zgkeH$D3sltqLf&}sUfIh0TfA`f(WOG7MMiLqktJ9I6#A*fQ-C&KO2n@XQrLQ=(388q$AWvkiF;5m7FKQ zW9B$YZgWjdjY_wboRbG|qH+P?dRk>85Euy+F_#d&f_91k=OV;?Gax2xYe)v`T}F=+ zbkv00&~o00dn#e*icrjT&Xf&TQSK3O3f=dOk1ZU*BQO#4?4wX$NpMjhC==Hf5|wgx zQzT@pAqbK(aY@itefs)TpVE(iWPkUElO&0>pl7m6NC$P+bz7$EA;0+O;@PL4JjQSd z`qB3L>Gb=0zTL#LW2X73-90StRy!M>-=3-tM>~7oc9dyXf}X(;YssJg^1qD3yYIgK zZlPi1RL_-|E}z}(Z(d=_y`87exBO;~QecS&ohGiae_GzBVdOM0Fm5(iU;NPEY3vCj~<*^9t5X6O{lo8!&#d-TmyZZoarkwm#l|H=gbeQ1aC$FE9V%=bP!# z-#*!yV3Q=8Z7!&Tm4$}uU;vW7o~$?G60E5s0P$wS;fSH`4ikVtG@u4Sc7oII{>SrQ zpZCjFUtk)iGUPOj-VVr<#oq03I_+USHqYxSjq6O8JhzzP{hPNDoQg7qFlM;D-lof| z>&-hXyK(n&kJs1iv#>GV6Q}5e&LLNX4Tz0|uv^R655~x`kycHFnnKw_Un8-Lpk{THU(RJ}W{>%ku3>HZjp~CP1>@AH#U=Yj@JI6v(bM3E0|G*&EgdmgedO z%5Lvmp@aD$Xe z9jFHrL5kif5Q){rWeP{|DaX+xB}RkpjS&=`-C4vSvPT1eZW7*+ks^EP;DTN#dK2W; z5fWf%2O2L_&l2R$eRZCcdLW2uNMUypzhxc+f!$VP$%>H?AWRj2OLH+ zmN2$W>*tUJ#tuA`WmODGsV|MN7jJ}?EQ3^+s@m=~u_NCG`c7QmwDNJz{P zvjhXrz&o!F&_lLNh!D^gVl=oSzlHcS5vn~H(21r7M@ZSY7%$=r0C`HF-fA16#|WG! zYSk#Hhl~*&z$!OWEZADNNJH*2GSDVY{pMHj@cB*fF_tod0 z{S~$`Ay?GuJalU7UGRtzrHs?F=k6L2RCG|0ElKb@RX3-n{(!tG{03?&|ZuLPgc zbky_J)%C@WfAWui`s!oeX87=Tx93G^EW1xWyZPxy<7YQ`_ge2O?I&ao*rSM*%t4rh z6WQ??OT<%EceES>%H`gr#V|M`n&!Qbt{vEoqTU_;-QW4|ZIG$lc;Uh$=0cE5q#Zg0 z3&M8Xf$6i}tS(LKX`TDMAQ14{+H8m8$u2JH)4YJ5+IYKsyPGbz_x0I4uX7v6VRMxe z?ujfFWQ4HUVF{VAI(SF(kj&v!GI*uFKqS~#h`@QJ!62nj4q@T!sK^sojZLBkf9499 zBRt3&he84ZiD=LqR(E_ft=gkL)%N{|)3O{t=+o2jab38kMbDY$+IT(Ix~z%AYY6Lg z;d_VL!)BY=a^l$mL6nJ-P=Ey=&Wo_Ev(-?pTiR36BhxTVbAo{rTf&O5OVb>V$93Ef zV;;+wCS`AWQCqaqay9J%b-Jo+%iI1mS#K_E!I0z$JTPpF0abXz{&{`cQ z8Pk~SnqFS-x8{u8H6Q~BwWT3D^)i%BbxH%5Oi*bxFqdNL?0scU?h#=S=zx^iy$@WQ z7SaV%SS=*(-XszPsR=naGx_W)l)0~#$XYlNVC#^do4ps%R*JiVQzt2X5hAe13#qhOgBmd-}C z(^fH1Py<>}Ql_x*00D5yl*HUdR9wx%6f>y0H5kSaAWUly_ti@&kkk#<=46BhoX5i0 zt48Xiq=3Y05z&S)5myY&fYM4`nBL?Xc?8cTf3Yj|1$ly#Dg)!jhePZk9Hy4tKwLc!aNC4;i!MQwMsGUQyzWK5lP+cYII}+&wk|KLl8*X`(NMxMx$m(G5Hd9mRtGj`YYJlA!9vqOj*+QvXZFczj{I7oR-#um6@VF^OVYpy3WI{igOkoD?A?+`u z6kDo`+HlbuR5(v8M~nldRd06l4FNXK$arf@&W;b4carLan zut`osN+T+HXV4?6qk3fFT8%J}4LM?xpprv)F=QTf(AE_Q15CR6i~*lv81iEvp3{mYp{KFUA}t*O1TscMUy%z z3|n0uxasb4r@k~-@1FBkYcud@J#_5L**VGxVT!~g!7zGXBdE7dAk2G@)_U_6Np|j@ zDHYPq^(OBw5OlOQs~a>`w`M7^B^jt?91S`1Vuskl8No_GP$EYpXdVK%VjU5yM_HVb zgf|V^TJj_dhN)e-Jd!nr0o1{hNkLx-ut`8TQVwSGv&hiVK#xo*U_)&MB1#;@0HI2y zcS=c-sB18VLm&_}nt+$yOo9NB0nXS9Ljr~ngXT^H%h8OSNyA{=hn^#Zh&zWXa0b_s zVC^wboTw!jHgG;_bm0t9gR{G%nW)2m!cYwdXu6^^2dvpKICv<*!5+36xH{Baa)kC^ z+5q%5QVMT?!9reJFoT&@hmjlrbd?Py8v1f%ZNbkXP3&uw&P^e6F9~Bo%GetM2yCh$ zVK9*sDvo5y`^pgk5>cIU0EHTr%-(Q8t(MkkDNz8YXTdABMTi&GZOn~)V4~&J*I^il zJtM}NcUsB(=a|iLF$r6s)K)RhE zxOxb3vIP;@rnZe?J=-wQqR^DR;j~G+Np_UihGT}>UF+d^ETP-$HOYMc_Vub4p3qD? zVSqzKHP?Qe%X1rtKDYjx@7K*UU*L?=o7KCyO&6Q7_Ib`k(UxYkv;r<=o1TC96Y=AM zH}i+@-+c8QQfjM+_jax?e)NN9pMDbUh}(<%uYX(TWx0Rcr_J-r-5;Nho1oskYzHi5 zcXPe@=o+B;8c|#9Uxdf={XTy5j86UL;*}#_?)MMpyTP`g9h@1BWut(ACt*eeU;0Yr z`)|K#yk2eo;)lQZFKP41{dfNcV9?rDgjYZO;_#af@7^EH{Ma6NC|_=NU;f3*S1&S+ z^D-;+6D4{6<)>FazM0a4fBSW^)Ni((fTtlT;V3X%6qZiO=abDw-PZMLpe4#ylF()c zn=MFCNsL`nk0q@iemDP5zk;Wdc|__$#7d|SJY7PlN`iW}1I_oho3hD>lrn9$rLDd0 z@>-KHXW!lYw4LtmZ!b32&zIBVm~x(v$fM-m=YE*mLeUO44`$qZEnx5J}O!ho*cDz8K>&qZAG&1P?~vQZw--+9W;hq!caLf3(MrXq^pa5=v)c{)Oy&IErIYj zAfec-;Wb}q9IX!BPLwl6$!RS-kgN$LBBZI0M4iF{y#-9v6-XjR9EchaWbo!R_@8CQ z)&;|tC3tY_2_+JQX8=TQ3?Q7HJG(jnsu@s9rsqXUF&mLP$^mc)p9z`c#^aV9!i$0k zRF?uNQ@gjckI_icvZE2W2dxRA7v?&HY|sExB2^(qF;eGD0V{BjXi_%7a1L)y4PoS_ zJ`_{Nb`AmN5u5eA%i1xl`NaRt4}J~-t_IzWbrjTI5Y%(-COl@U-2np?9U&8hL)GG7 z1Wv^yYxgj61y49~1j)20d1TOTra@PCug72i;ltaf=x&!glyTZ!P3IQp z20YQLtE*>Ud`uS7?&9LJr?gQpJPQ$>d@QyzH^5+*HUFgYAci(^a`>z){-hB4u?&>3)?f&*nv!4E}@cMKj zuiQ-U9vphr-sZYIwfk@1z^o8y`|O6vF0NnLygERg&*xfMNy9Tn1T&F`jXX*w2pE6x z>7w5x=Bv+sHh=Z|x9{!>&p76b;qv=$e!teGdlyVs*ZF_@SFgVO*|QhVQhmGV<6WDl z{P{;WKmBC?(aw(Fw>L&SKn_WyoNXvbD^D9kv}n3`JKWia1LVwTbh#54wod?fBZEcFfN9)*-vZ^X29ajS=7x>>zsghgH zPWzPoczXLVZnmg3-+ZJ4%5yBF)ep!J$>}fnMx)4 z+YfW~(i^A!&AAc{r+s##4^Kzwa+JVl^qA_Wo} z2ONcDvme_`sCRG) zM^v-gs-iDiNsRKU=rw>SqO--u2oiz-<-#0@X~=mEqX>@)SqlTc{0L$-C-ZKaSV0Ia z)JX`#92p@9Q~?$vfIv1g2jQq4hdgfkai%QLjazdP)Rn`{8V*P-LEJb|TNQWP=i6yJ zr3=WiWno0p2(IeP!~^l!zxeogb=_Y-aZ0Phmp}dV%fI-`=bzl@)A>{b>+*cqef+~u zb`+QGyYkU9J+7+IPv@`i4<#zC^Sk%UhvTvP_SkTBo%YYCF^`v5;X|IL$JehF)-T_W5?Z6=d&iIozMu7TPWPu*rZ`Qk#zm#4$y zkvBUC*o+g5#EdN9biKhmZFJd8BWRD1!re*h;qiF3)sLLDwGZb$gS91RCImkzVOSH&Q-;;k zmWJ^XHJ%e|F zHqu&!@gkm{R!|s+u7GG4*U!3na-dxS#aJ7FfmbZgpdKy62)4skgfBMf>ebC=SN0>N zQ35T3OjS5B+QdeiG;m1+pjwwe&k#qq=4`$OMqZ9Ktew| z?j%L{$$N@41~7R?1p`DTpx_*2g1m6=loNmR{LTn~mv~I2<;_8YS5FiHe3P^}Rk`XZ?9Js=msA^&M!XnDfm;q`;IU58)3&dL0 zrHL6#Oe^+=j{JZ8!7mNjOChUaLx@N>AJCFycbykV16qw#Vr{Z^ApcK9kBZwh9j1WB#5EJ>zdBbeN*)tVp>J6quO`DzoiQI)VK_3$41PEl6pbYL1y)z*@ zmR)SGFVYaq4v68^JP10Z39*xtS$p=$CcS#)pyTy*S?AC9`G^1Yk1l&1Te=Xw+Llj$ z^u@*Hm``t~wr(e)9aSFnY5A_N`>PjoU*8^&XG;%r7nYK@V@YWk$Ls6q*)u!d>S-3* zCZ@F8?JmlAvERO!&bRH`-~IZ-(>X_2kHosanDVrL{Qm2cMoG{#2z9#H@2)O+xH{jy zJN0Icn9vpSunBAXn~QB3P!NINzW%BzMkW^a)l(QCVHw#yv>Tydw^|dV<*^^%zBY_! zuYTy~bGxgDrHdp367bJ&cK_(V{>vYJa=|`7{LRCCEjVqje);3;AKav}uHU_l2C$6; z18|OA;O0sQLy(!xQ>-VtJNt4}xfC!QF8L~r%rG()L{7{7_wWDd|M2189BE!Su@F(O zy%_+9otx+I!)d8{d^Fb8<@n+K!?*AM!?%aR^HxS2r{0hCQBU99-#$D-$=LDxufO%a zxwuHQ8Oz1965S<@%on@u&|~K=n{i_{WEsL_lLRmq2J6wr=rfwKHzzQXAVe@kLY}b< zK}2puLIgwskQfr723H^f+5#&RI;5CqUssLR=9s^|eR_X)xSd-h7{@{O)1J1wbg@a( z&?y(a|3GWYu{<1)c{|DB{^{=A=fm9tet$eJv$h5&r@HX6gpcj=`lHi29|OnD6@YmQ z11KEhc01&Jdt5$#cJcBD&!($Ov(q|jEa&sdp5~`qcBfNqu|`fQqKECKX*pLVFS6d{qN03#&w5+K;mVMaa zxd!Ycbi@J3qcH%2dkVyi>q^5AA_(S$5zJ5^6h#mbdSOo9Rq`C`MBGET)sq7$5a!Wi zD|%8$P=u^v%-~^)kqWl%0FncN;A}pyw;o`Yg$Vra00YH#hwJLqJz=yJn2DLYS`0q+ zY~FbkoV7<9iM+dccgyOPmx*_(vlXHh5Fu0Xyv6<$sd1o6_SOtNwmU~ha}K7IB%&0? zp%Hr621a+-@oE687X~b}K++iQXtZU0GVzaH`rpFO)Sr%bf{EKYBZ z@7`^vMz{CHL5|BG-n={J@i%WijDWX~R(AW%xVzl!xojq48Y5p_*2 z7s!whkcKp5bEP=AnhqO}mMq*eU2(zOl4t2rw#Tpk^v(bMe|YoTzQ5cJdTQ#)Z3VP? zJgv>rxnNC6J&H(xKCN(m{II-xSC2v8-1e)72Y$81#}5zdj5R_CQJ{?W^A2bwnI!xjsmZP)X{!D0Yy)w{`dftz3+pe>HwEMqw9>bS)) zwymWzl`Gg5ObJ6A0|cXk8&e0=)#V9!FwhXALa~}X*>bMS$z!Ipy9kNRSnS;4c-K`d zOF!W0_QN0F&(~M``OR_d@Z#n9jQ+U5zQnt;ub$7$nb_v8T_fQJv&3vDvPNC+SM*YZ zainpizR)l+J8c0_J*9z%wJxrF@Jt};nF;A!JGAa3 z;eyQT)0)Bq0H+*c?1NE4)0_jkf=crRD1mqE3|xz(%`Oi`%IHZ-3{9(IjHnNQC4q?^ z#{CwD5osmIR?@JV8iEMw8s_K@DH#ah(cnU`iS`D;jHKNIp-n_5PZz#+C`ptc8s)$g zzVsy7xWllsd2w=K3j|ChdUc@yhMvLCfaWw|aQF(AJdJECIVFv5)zb#8nr{pe>3f5h zNlpkQEF@HbEg4r06elIGf)}wK!aB${+9{GKARvRSq{b}JiwPD5CBShCN%3=E7k zkdc?hki`#hHZ0uFYjR(Qk>*a4kQN@F#eCxM=n)K73T&AVryiUWc54b5Kr?WJXHir$ zH*k0GFjyBK25Avts*IaL0kgT&86brv7FFW8fzg>4#?%>%*OXXI%7qT#n?Wuna!>d` zvw8}W#bL{FaM=Ki(AA-Z6h^euS>|DH=WVXnKTd6)33o$*SI_!2)3MKg{QH0T`oXE{ znI48AzQ6T%$Mk(Q*j?Blhd+IQH=`gcl9Dg$ zSoKe(eDvbwkI!HK&cR*mi|xhdKYspfyFb4D?P+l*xw)BM{^CcQONDpu^bTmsxgOhi z8PF0`NCPAy<{IcS!E&}XfXvAf3@lEG$ui4+M3M8~|Lx!YpZ}-R`-YgyyevcZMMIpK zDrPPE{rf&zH@DhOn=3w69|yjBb6Sj&%l$dN{^oV)HO(thhiz)ON=Zh3a9}|dZuo!@at0S$E zGvtPS#9enMv<<*L(c}P;1|l}^hyggDE+GY}I)*B@s`zwP5W3u{M(GDBX*r(XzkhdE z$2`XAe!07I4LIE2I*TJ&JKvVH)Od#;8Dfbk1r9@Wg>W9mQG7l=)&s;K*pNG$>rgzb z!Z?cSpspw9AnbJ+Qq7xs_$K+yK>cw!CZw7K41Ew1ujdEEV1*Ik#3BLOz>QkB27L(W z#F9By>uz{zu`9kT%;M0YTm&pob9L*g6K@mX_IenuhmvBzKyp2YNfS(od3z1T zs2!vv!$k8rGLb?+3Nk`*>S`Nk)zUVqX(1#;&xk!J&>iyZD%dTBcW=?ihzwPXO|t6= z1#vbaA#rl|7#Yv1=s?gN3BeL&p^8-2+OtqgZFMOUYc*qGa)VId+4Bx@&*q3QcwJL6 z9l}=w8NjHkGX)RS4yF{55f?SZC>l#{-AZYlvn`5f9->CMG}8;lC%W){F4lTicC0X@ z!9+HD__N#sxa9%sS$kCmH;AaB66dp^getpxPB>&a_uhI(X2&iJ(483QEV^NKZb4|s zgdnL0(>?feXXR9Yk_9ky^9j?*NT6X1z>p9+rb<@8(W$|(#ip8pVoUSv2!<=rGfM|B zjLkOgKG0^5%R{l$XB%10 zw!8?q2QEs%uCAFtBO&H1(8SsRq~F(~Z`!G^wc4Q$SI_v9K{p7G^M|{`?|${w-@ZSj zXD|EVxYpXe)Id!h`9dJ3k3YNm@}tAA-tC^fgW)vKM>fyb zUxJ^e{f;tu?}xhw%m^!LppX^jT)w#f?%%cNpI=^FzJ2|DFbXOym4*~yi}uz36GR|a z$AUH1w!Gs%UVQOvH+@DM98%$&=T;Lj6Cr2oPHPW9=47E$*{W!u&`dS5&&OEL5BH53;qqqchNn61 zkx|6M-@dKPvL075HlsDnG=BYk`u@Ya+BmRu7^=}%k4-?booRj=`fl@=zr6qUn|Ar^ z>Z83(BgX}*BX!Ot*c=PhGXRi~(};L-6>JwoCz5SMSUVDp1OUx!mMFopoADTrT}Hv zp0RlpVUy%$ltkJRM#>#Ek-@-E@3b{X@F8IhY1Y^%&MRDVE~x@rwKdX>A9+?DcTG_? zR7#?^{_Oe1`==Thaq4^n!!U3j+j*H?%0}w>WEKD#PANn0r%WXbJsGJ8W37S83Bx*s zpOm@v?wv77=d?zT8Z4Bjfr`d(-JU)egbb5W#o8q8fgS+%#=?|>P|9E{V(MP@)K2b( znh{ULd*~0EuCShrfQelY9GT2x0ISH}vZTc+00;(mNVRfA%OgF0 z5Grn!9Bm|DAV7>NGNw8kg{MqjyOcN_V8~L}N(HoWNi-xuN?m-z!Oe*qBms?clQHvz zy|Lt(Iyo8>vRMnpyt5SCW>Dfzjt%HJ*#nqE4(uIu6nop%VFjqSpdS6mD5|Ew>re=3 zAhU->1q*ubjUpuuOqslM2gAp95J@5t2wf6tR|U$>0uJE7U{R8vEXbL0Ib(>YqTK_% zOeken><%ie#WvZ}r+Z{tkA)P0(z}h1j8BxWXJ?r z0U!V$mefhDuHXTTX023wE(jg`isxfO^t!-q$aU)2u{X{M zphp)d#chek5Y=EHOd;eFY0PYHBM=IB_mU9?00bx@1UMrDz=TwzW~0T|-p}Xw@>U=Q)EMFf+nt8fjHP ze0;a|?l$JcX<1FJAx_a3qTuj^R)CiC85m%xT&{-w16{o6^@xp?W89Yu#@i34%-|+{ zULddBGYL|ywBb0|kfm0Y2Fl0`?pB7at?Oz5)|n~Rh8IJMO^@ycMZI_9bpa{Eei}1F zbXa51?@x%-taY3ciP!*5oZNUA1$!7e@d_>uiIxve60(nePXyo#B_u!jP^d3K=rt(9 zBm$aj_gEJ(ap~+UK*oFmPwqpA(fSlT01hTX)(RWoR@|H|0cmh*-XpU|GpEt4G3bD- zZ4y~DGNCo>jZkVN4tEOc1Rf?A$xa|E*aYDr$>S7s1A6pGkOx~-l?^44szxa_#);6J!=(l=8_vh> zDf(`c*TnS@0U-%ceQwA@t&IzqdLL=%+GUdi7&Gbu!x(whmQzYV5V$gIk=+B5uLgii zid3MCsHXr5TRAXdbU3>Y5RV!=hhx|ZB#r>q!y;OX0wrDc`!|aLrJP-HOobssQV3dF zDM)DwG)z8tM3NvyPUuF679Q&4JzO%Z;1MiA4!A*>5m!tDrD)Bg-4pv9@S5BKBlW;}NS@Z{z4!lEtnlAlYSMo1Y(4o?clh*D7NdZ02}Pv5`&>iOl5 zUcP$uhxgx0%19{{%QgX+5P6Vn2bh=O;HU4EF3(F|&);4A_U}IW@-J>a&cpD(oBs6M zci;To?9|ONp)Xb+Z@F9BCe{QZy)PI<(>86U_4RoUOmO((;>8dC(HAfFS9Sg4v06mz zK7G0U@y|BbyY}5{gdx~*8eKaOa0Ud|L>P>uSVe7iHExU^$V^prJH|E%(R`%Kt566%GE88q zSlG|O8NIVIow{!fX%p%+3sQ@efB@MD8PXU?QWtkMZKlr#}~Mgwe~ykc!L^l=zgm^+No zTV;vfoe3_XdvP+MqegfH4w{;w(IE(geNLG=MKMCOvtJlECvs zr&lIM$&sbnijPL>Y1m!!f)EgcgC0V&1U{ zk>w52Z5Z^(pu~t6%`gQdWld`^HS2)6XHKRoctg2b8=0&~k|`j%38GT&O+#^!89Q4J z=#DIPWmIxxN0z8ry+^o0KOa5vEUIM)%Fw};a7L)u*B(i_6jHT*R?3>ER$*B{psdK& zytif@LmkM$|Nj)>SF>hYb{N=Ax7OO`Gson~lbJWKzvT@8f-O2lw<4sD=+LWP6#8`v zy~u^w4^l^nWH*U!0w4eaRmCcluj})sKjw3L?`7tkDvgod5%q)#O$eBn6Ig?iQ3XQc zrGW{gHL5CsAyTUV18D+pZWQ7Ks+=oX17AX^lsW>Z6x6XXAPIngR}U*=@s=wAN^tEK z$mRuCXn;UjbHzhF^_ZKMsV&VkC6|F~(-&_4Ie%F;vM}%x~b~UbrCIY4aj_QSZ>JzG(!dq11cI} zfv}X}@bdo6)Lu`^@1NcMkH4RP@~by*pWj)3<$%q!s_!$p0?^Qn-PMD4-udLu9)9?7 z--K%+YzSt4Gi)w7hUb6#7x%Zvc=<<{gGZ-$0;9Vzru59K;1j&ZwADvMT^UTbx511(6PZMWP~TRr>LFOKtG8%Ww+ zfA{0>{K>z#I)7-@mU+pltwK(MAQB<Z&fa-64jVfDmuL51 zzuj)T2OmG^Ke(XzbG&(u>Y|JL(fovQv$do*9I;yu ziLc&!cl%(p<4x!iyH@tlo$W0jq}D#Y-A%vw<(uFA?2G#^e={Gm55j!_fvT+yD{L8- z0?2gthNwBMARm+na)dk?v3oKHi|9CjM@wS8I|4z^{gym{5V*5f52-aY)iy7O+xhJu zKL70JU;OOXZ-4n5Zl#+c%}^l?OW$eg%rOsLw_0Q0RZQsONZdhzh^2`!>Ox_yo3&Z4 zhDwMb_-Q%5xK{&=+O0co3K1kk(1ivgg4w2WSO~D%lBWiin>CS|HfQoUkGRI3MV*rF483w7bTLUWy@hAPxtG4h6iu-SSQ zMR*u6ffpW(ykNnaF^&=`3|*g`V<$s!49bMkdoWl?s*vvq*PsibIgPa-cnstX8yZWz zud#QpOKEOoFklLP*joV~&TKw`qN6%=)+(0b%j8yBC#=n=XLM^B2+aW1n$Lvjv^Mu< zq6ppqt*RQxOaV=qOr3g5gdurD#$roE^GF@zQNbL)px|9?KaYPIfkQs`xW?T)dju=dj)*-Y>9YD?z z-%?p&D3Aq0B^xLnYEFRSoSQK#H)O00L#O71e`Hn2m!|E$+FDxX7$K27zG!_2CZdK={KHFPQJ@Vv;t&kT zbX(4=?~b0n-o5pPn~UlBt5jmj*V}&g z@>g%)+5-K9cOP%Q`(Y0h$4AJE(|9jid5< z|Jh&v{QvR!;SNh-pkkDkS$BfgVs0&$$ys8EV98-JN^Y~E;1KHR)Vz`gOO|PfSXx@2 zfB0_Oz4`e4vy1D8%X~GRy?XoN@OIwkiAtT+!#b?Xas=e(7U6kDTi&7l>cZ-TLJ zmRyWGis0tJog-6p#1vdPl>1wwPGun+ z%3QqeH`}vQy^H-&>pY#1y||f`%8J#qBOsAMg%pB2X;#Fz+LrwRqq}$MJT0?YHKtg! zMvAM1d`y`$wmPsA4>Y~J zxx4x1{!ko>w$_d3U#a~`rC(RpT2%`;OJ=>gE%?5 zHtwT)3lbYqsW$CiZyvl8Qt$bw@zf7b&xeg6;G7#aB@kk*zp#xsF!s?guuqnSM4=eU zarN~`^6fAtUl=byg%=GapbF`wIHP7!w}o4i&Cn1U*jX1C@B82d8kgl;XPaUm&T z#Ym`{co-!G@+S|^-u?3*pMU>5tIgT)_(`k_+HA{`v$LamiDb>Z5KjtCV%K*g?h-Q# zc`cv-P6D%_c^pM6T60D4LLN~BbQW(w1DRqsQV$Rlz>1`!WW)(z0Bf88vAULJKkq*M z=C}XTPyhOFUOmexGf$OsBX6K70EEYpd3{FSdn>}?{D^9-`&M4Lp(YZEt7b0&h^kQL% z66@*SOfYoNJdvjq&Ng&;_29w7^?BkjXvkH8uKrJ)P8j7|_(0SOqino&YGs=QzhRr&w)@ehQ#^wx+<+QJU))pDq6I}k)D2O{HEmMD8qXi}u*4`t4D1XDy##bL z_QV*4umTHMbVq_7Ven7`1@r*OAla+~il_>j+_NfB6?cP@XYJ5h&7~|QH?IpFYKchb zz7&mwiI}A^hUmZqf5h7rIzz*NN?jB!=pl9PixY)RXy6)=)gT3mj7vo`^9I~4wQ@0U zYMV|KxPn(hh{*ZFkK+gbyC1Cc)NXcz)b4?MJMKSwdGpf=qfMdV9h@$ZN`SONwsk<4UeDv->$Im~%zngM(RNGJ*Top~v&eM1Q>|b90 z=-ThzoL=01`m0~;?oM+%rVk#x`{)1b`QEBxEwwxuJrW#KRUm7e0X)&@Mz$uI1?v?qM&Xu zwLkm#!*Bny&GqW?{jD&_VKGZ&RS{quPtShwv;XVQUp~tnhgA?WkdAZe%3;1gO@c{9KpQYR8b|67mRz&~ z^$8mT%Uqm;M)!vwJq?PR!5_Wz;Od9(Kl=E=qfai^kDqjXlGQp&->){h+vQRg18Y{I zjAp^yv-YvA)^X?=Xa(g2TGfFOp&9Z>Sg;mLmqAb786as~3C$Q2upuz=f-+DDxQU2B zaiC~GjO=P{&UyFMm!JMGzx~Hw?4K8I*d|Dtt0HJ_;K1W~4|@6Z^6?M<+tsR`_PdY% zxBq+`Irg2PcSL0=`=Yfj?xL+2QwXC&C9O!S;Ehv>s}A1y34m;-yN5`)y3m{JXsWJ5jY2?i|rP$%?BgI#)XWl7F6&M zu@;QZ9Gws)HZ^3(3W2<~qQ=TdjdRl|QG_0xtsh>Gt99286cVxZDu!9XJR!>YHYP_# z4h4`Ty0!rB&dFTB|0p0eH**#TWKwWIcDCS{Q4z&B0}vnvYb6jXA+cjyIW=e~P%W!_ za~8lRp;cO;)n;8><{Ho|Q7eFg>O##xz@3_}mzmOl#Mlry0jrj6Z! zBuZ`YM~o9DF=RCnolQwFH#JS!l-;;O168PQLK>Sac?s_OJ@kEuL2piiBw)^tOQS$| zyoar~v<`4%bma-r+?^~XgazT~KB6|V0Fj|qq-s8RD+UF5B_&pL^a@6Z=;}@z^rM+M z1PNu%gbc()=o)LFuAU|_?1zEOO;kJ)8W6IgcEX1V9I< zyAFk5wgxSA$q8GoQHb2E(Q?Q^2{;nfT9KI6LCA5c(n^ghfVxayy}kRXw$M`mM}Vdx ztf&x$^~CBxL)Ce?PhY>>-{T>tu^|HGS3WwdKZ>V3L&GdYo9>4efZ+-K-Wh$j&1H)_% zu94tjT7UH6_%Lk>-5q{E<=kIgpI?0I1MVPy{g3rDqKqk3O&vi1bwgAc%+@IdWYuaY z;N^y8zvts}N!z>Do@JH35- z--*Q0qsW^>t|0Si&QYIiyRceM&z|RUOLKqOzqgKO$ljMRy#JuT7>6aY)$`9^eD?AN zh@Y%i56=6pzaXS^&*zJ>5*t{_f|$`?B24Mu5cz%@a669HcIj!Fq8}3R^$>(=+<=&wu$p z|M$J*VRb&6N>kbIueR|p`MowYa%5i`gd_O?Xb!VbDB1{s)~i;lSG9dtB#8N7T`!>oWA)DR zy?9@7ArNe}GDV#-g@6+yG^>hW8qu9(W{&{wIRZsY znj2E#K^Np)C0U^uG&>a#a7Pc&)EfxeJ`fo&qTrIXLxbQE0u0a=L$3zlgIHx@axev*vk2CS6Z>3{u|fo`21R`hqv4Wk0kXVj6v6^wsp{6HU{ete4%Uu~ z3vh!!3Z!9^Ec@=%kenGsa6z{P!nuiw1&WrzoD~*yQpoC*VD?(G3tDc9*oixpEJms z=X+||tT|$Jqou*Ac?6_@dBzlh9TK2}7@BzT!~+cI#Yza#mxDb%aJYK^ zpPqm8?&0&F-M)Hxd#k7WT%wQCz4L>A86&>@$uC~rO+YdOuG4^MUEjqqt{+^-JnfMI zZJMU>^vIX8dwVx$l3EO^i}#;Ap1yo_czbJsn>Wm!u5dninM{oRIJafqyz}Vt>SFh1 zANoNLM?k1noRiJjt*(dh1n!W@LcNk41umsv zDBpj7b@uUkmD1~9e}1#zxCxsd{>k{x8SKB&yBRhk0t0VYNJ~XZKmif>Vof1gs{n|u z?nF}2vc>E2=Cfb@>;LfbdAdMPF;r_gmwJNx<#emy!!SFLdhih7xL&UwoUi-TH7Ma& z`XO9gtgvcA$T>zGhQ5?OQwjiY9 z=w&+$V+5k1qc=eenpjJg4~#uC4|r?+Vo?jK6WAXg4oOhw-%g;Y@)o9 zXp}HcL{SZ)9Wiy_g{*5eM|8Kqg3=6SXjU|ha40RP#Atw2K-AFs=77%DDPRC72x3|V zDh_MX3^0h@TayN^97D+ntRW%0TLWVk3)YGg5V`jnLr_r^slLn^eKf6T9I$}Z1{*SF zKTg(11lJb&HY-sj!K^{ZLsK(^qxWX=g zEp98Ai`4*W<%h{AATG!PbIs1)Z?H|VPRzuSO`sz-wc1)(cgWC#ky>W9qErr)B%`b} zo0*Du1s6l7{cK5f4k<9{lASxR$0QL|87Nh?fXZqWooX}B$NJmP{QJLnx%%D}bg@ia zhI9S+OdjOTS1)Y;)(|lD=H#-*c5+iN0kp=Y+PWX*;Z^tGd>OsnzkuI-cK7w&{r8?c zl>~P$_P*NisX(sXW$Mn>>FmAD(?{j=-{1b>^Vj=@m>R+Ma=g5Hx9g{`fA+I7qZdJx zP|eW#Zb)k^lZAm`Y4c$(EKPTGwuP5B*0>9xUDsyPR+i7dL34F%_u0TW#F)!m6;lAS z`IwKd-d=s@^i%3VU9I?#~QnBccTT+;7J}hKa z*4uX<{UAVhxcxNo^yIsb9z{9be6jobo(eqp-n-X-`j~?2Y46@KF%3m404MH(B2~c9 zulk-59E{v2XOVtO<#is$ynFri|NAfAdUjdaO!aYsK+wb(xXgv4{1`J8MjTDCWzUj6txr*?Mtv^X)1H(j=G*d1+Hy*m@A# z7kT#gKilH=)KsUI2xS1m4d2m(r)^?gw8U7@lp{ZR)V_G;D1a^zy@6vWR;fAmK_=F)#vnbE~Ba8A2yMxoksjC=J3IS~d?z6(Bl+S2saliqV0H8ubQb zO97(R8YAR_01oU*jHna?#()Tc&4Y6@2#$%8EQVRg^sf8H8#F}wc0#OPeWT>tyaAGLtfGUp7yRJH7HPVSHGO?$Q;2vW@zlD`}CGeTr zNka$Pj2SyZDNfCuPzAxU*Oj}K1EMsCW-Xh>+8QWX1(8-MOc@zDbUx)FrVzw|lQtJY zAdb<8PMbPWpE?wb#?n#Z;10~G2E<|QF=zmV04$K0v+wSfXD9n=@3(igo?!m+`5%7$ zcW>YREv;_OKiZDp{;2=>`*d+dGQc10?=g&k=Vqw^XKcG^XBF=@7JMUaTG#YZ3eIq5M2loU{xT*7`!9w zbh{ezeu6IP(zIC1dV2LmX?5t&gol-ku#_Q6ZBXjci~xOEN<$6nEjM?~Iu~|OpJ^Nh zjCIQMRBJmlvYeNTX_AZ1zoj>fa-@kn4gX_x+HtLTWaN2e2Fpg3@)hVkk?)zdzF=lD{=Cz*0 zntE&6s=9bR0#XYM=m0`CF%L-1xdDJTAT0pJ9GdB2e|!78Q{C$+TUy6Q53ho=<#utt zIXgeQ5~S8vkH)VJ0s0CT}8$U2Prt63%l5bg#X z0wlNLavc!!JYg;}NULZV&`_gBCJ1cHeZAeO0(aIRfO^dZV^Bh-KDvvHK`dz=;))Xu z>?~cN(xmgiyxi%yA$25_Qe6d3V4RGSY2SijV-R+Z?%b#Y3m98VPl1SeLGD070XVx- z0G~YeWG7r^afZBe_o`71oP=v}XJj!Bs7Gy$SlJX=i+5&6~e8xgMte}o-+)qtVvY&y1Du2#dpjf>H_wa~QE=1rY=w@F=d; zTmYF6O`br0A>m9lL)l3K^hK~Mnmwx-H3DRl3T8=%9CT#crv;l513E+_Dg?laB&OHVgMa`jB1UKm$Ahr^;8WP8drmdAg!6~E3soMRies%N) z(v#)QuuuJ~7u_HJZf^AY;-i23;eYed)zcq$Pu|bBTe{i%i%;wJvE`GnnxHq?pRx{{ z`)}^P{LSxT*FOKkKfAG;Dc4A#fkOmOtu5oaAN&5@Kl^dOc6xPl-0kiT*(_h3t3Vxi*-4Z#{m2x(UTj+z00F(^S(9Aljjp(Qp$=cih^zWblyl@y-nB&l6u{lt0 zC$1LGX*?hEzU^w8@8+fk~^62LN{Nj>4KX~kh zCSdA33RzSra#;!ZN zx+)9rZ*C{Rei&1vz8}tqpi}KHW%Xc8&uBWd*ZZ5t?|#y6*Rf`7%B3NP9EHl^wEg+$n%GA`vi5f)K zMGQ(&M^xyb9)z2B%Rc)Ido=KtEPZq!O+i;2k1Mu3II$UwiEUVnl>|%!L`NK z)M6Oal!T(w(Zg1BcF4p+ZSO=?D~f5o#CoUPsaHpF7StmFRicK@tgr{F8Jlp&BtljU z8U^;L#x7`?yH4s5TXwHYi-L(m0BbZru`t$Ju?q&xvwKLwjj+K)+I3`zwy?G~rpT*V z6+!df52toF+f*F|-`>I9Z~o@$yB~!2KAC><#qHa+(38zTEkvGZaXh#^?Qvv_uPdH&$)0nT^F-R^kz z_SF}+BnUxLq_fK>((}u&ez%RQ%z?yNxj*kaS=G~lL*OLRr`7cp#6ZL4YIU5TTs*$+ z`!J!2^TOULS127yx9w$u>Q&o`a^taI%2GTSfi{bEHbb*7O+5JcCB9fkd~s9Ska*_(1_O^Lo;pcVM#V#@o>KV`tT>Sasr&3xRE5@Pkpp`b=rAOmrwe)={8+% z26e%umVkjkd+-b`uwe5vNU1^rpaCF?3m>t+fQBHamd;4g5_2C}+yhtxWvUh1>soIQ zEynZn5H2?6-FxbP_`_#q+6!-naa@DXV>ZS4?d%+C&646f-~IM_6`)RnRu!B(tJ?J; z)fqD#qJ|YA8Ad8tctS)Z@ws$mR8-6U@4VL z4Nc&(&jI!K10qL9}Xek{vmV}O~8ObQKH)IPIj_5rxRR{D;&h7!NT0S8q zr+djs`?ZmAm#Q@`1p*jGb#NvW2aW1e!U#}MQj3IT5{v+QlmWc4TVWw?#w+s4niwPw z#b-83kj&QR)RJh$&_}GKJzqO>3~=3@)+Af=8PZ zCxzJKu3D7X>{K~oL(43ZY^9q!RVjFrt&7nh~fj1GkYshfI3B7Xj4HR*mvlT48jgWsI%g! z$T~R~n2HmUu}V`HU$iL2NJz3&4cLhlig}qy6>DZe=|bV5ynWNHI*Ji%i_uhB#^~$r zaDO;GJHDD`fOM*5FI;-pd71X`pa1gk@Q1%09{%~m_ujdDckDj=5pKsw%~v|z+~2e>KEL^^pYLAGa{+{_LCQuV25a zEv3j_#e2SZeD(PKZ$121fAZw13^rkFM1*R@)tb}NU^>a^j`1`Mbaj26Us}T1y>wk? z=lyb9AYunXNm}Y%_cEVoH5w*?U{HV@2%{El9i57IgVTmfgYy;k9rP#1@9-_2F>XTg2~t_n%+?_>&Zjs#7I( z_tU{DL(S$1+7y+2-PX9C?vD~sjo2(>Q|E+ir>Qo;j_PEMy(h6j`_(z9RWEEwBODe1 zWX~q7>=Df(^~ZeN7KNG!CpL}7%d!WpYPAvLRG~q!!}{WC(Wd)@1gW)G7$b?b;#O73 z!HnaObAvc(8_bwpDY?x7-LWm-axl<*PDP;%rexP|f#RvcO za6PsiPFEp6)7O9X)4%;i%ImAVI0QAdkj7Sy4RVT6n-N9g5+OZ!_oS(w0-|S1=!3THo{h&9l7ovg}&Jayapv zV>iTM=$9~0bvZ3Dgt%I@!vP2qnUG+vlvZ}QO=$&fjxm%{mixPJe*O2$%`F+sSjpPh zugkJ0B!xy*F$JCG)InQy^!8AyS88emwODA9Mr4TLO4Iz5?Aw3-@h16>py+kjSYUk)6`byAe}=*@*ZH%VZ#m%4pE%EEEWjWQ6!hvoqqFg0AoO$zhD2= zeQSQP9uij|$1)3V2y`92#z0fS*iGDqNcRhsbts$en_Zc$S(ifB_bJJu0ChDi*N-2^ zus!bY_pc8Gz!#hCdbs}KpRT|6!Q&4%D>Ba}%>@<;2*qJ>u-V#4Zx*<3DZ#}>YIrg2 z-uPjb4TQL+aZ+@|A~85L6Y`EKnk+}$o&}#(2n{Mp03c`{4MQ;-C=p6>?4g^T*ed&! z%c0H-l^o?ItrU8SJVJI!QcDzK7Yo&^U~;G><(qv2h{I?GD2h%3SR4?lDG#6SusQ@PMLE0_g2tD(nEGuFVR*wTvBO)}_YK|S~qT&QCEMg`|lhL3q zPDLAVXAYg4bTK=3J}vC6hLM$o7_=ZHhslLn9vYZ9I=ITnGS|fb6NwuWlmZrr*j>@H zP;VNzRQKYT00Xm$K?_RFD)pu%CR6FFsxm`u#DQ8f_e?!faT5kD-k2GR84*}1VGFsK zR4Yu-fFIhO#yt~uAx%LN1{Ze)0PcdDAY`4FC=r9U z;trJs8VLrWsd%7T$w9+Xiu7%6Y|O0Kf^(}RI5q5=cH-<7R$gaHYo-O5&;@8AcQY1o zXyAb=3gP0!0jXILyG2c?Ym7T1LbXcCVHP6A6?-d~Fs-ySCPQ|wH9!}L;#6IX=3LVd z2ZEAQ45|Qdf|$gcRI?CEQ4p-BnmVo>dZj*~%~Dx~Vl0z;^0r{2NKo0d=DD=PoBE5t z-|e8!fZf?6o9<3WNHK5-m)rGb?5{rg$UA{cJb!Q&c5g0UOlx_u%&o1Tz@zWPrw>_g zAyv@*SO56;|N7tWcj?L5c+LCU({yA?fr=YuZ2{Sk29W@M{qeUSY%ce2zENOUot<4@ zH1D{>`R$wIG@+G#waL=2w#{s9m{*&tP)&Uvw-E=fy4>B~ZLdEnb3X2m2$y%qS808v zv!esA1E-ErdMS>S#+8<1Yidk2*NcsuUcGJ*hVxZDCt%07#;MzDVQ69$AIXDJ+ zupQr7Z9crdejFc!33Kp(nv9Tv6&e^AXw&C&xzD=0<6+x{!~Ea_|M{`zIV>%0`x-Ce z?p~WT4Z7@N-!VC2gGi=}o6vCqacvXf89>H<>rwD5Q936lifGs)y4$`$u=x%7DFE5q z`(@o1m-Fe(Iz;{Wtj5qS+x^Wx?=v~@&_BL@w0e4d@%;}Tyw|PEtyc507;tl__10hp z5*DMdt^FJ;W>03#Env|gj3EsHp~`eR0fl%L$U>Qb$rv4EsLQdPCgU+zJKmRi`!;Tc z073#tiTm@|UT7|Hj8)N*NoZ-)xan@*&SD|Y)uM$c<`9C^a^lvNIwcB~j|tI&xUmL{ zAUd_0h{)r6@3LqZQj7#8qf^5HY>Ii?asfPeM35E50u8*@21yXXGpe~M;;Quvk(*Lg z5-?>X45lY&?ldb&wCwB-Mi$*?2ZT*uvx#-Nxc4er00}TrLd%YEL|*{Z9XLhvMcasE zZbS1$Lb5n|4ctoBUT7+TLc{Tc((}V?Yp41^~=OL4^aI zg;JH8p|Ckus6YWr15QXrK!IwZgyzDnw#cN`GFp&OwW^zsXz!e%I*OR~7VZf*4N6dPoE!pbQ|~1>%9-cAmj%3X-M0ot;69=s38HAxOnE)h zG{vyh0mJD4<7gzTC*gs(2D9qmt#U$Cz#g<)w_GHV%*PzMjsm)MQHoBvOIg5=hp%4h zyq~60PV39tQwSk8Ilc4IyD|J|y1S#}eR#Zn@btmh4VMr5%ZIvY_x#?w(`SeM+d~k; z2ci4%pTtLk`ptB|zxn;&{MG;E@BZOG)EhrP8@ls%ujhPUrUL-hxwtrlq&1QR3ViYK z$>qhP(DT{&VLoP_@9%au)u+Q3_hPWjMx1?GovC}RtJPKNHAwV%p17ZmFFRl5*N5UN z37xx!VZFY*x!XS+u7F2bJ+zxo`|2n~OEHyd|*W8qFLYrDAtkM&ez z2rXCWIt9_PG_mz~HjE?XQp`gaLwAPDUAC$KfgMPU7&O*wswc!AY^luKvq$;<`THMk z-~C^G>(M3O|Mtx!0s9AMwJVt;3@P?FHq zd*UUIz2AJjh?#Xw`rxOI)K#=%_{o^a_w{D`=S(vJ{|Bgt0lX zxJ&Xzj%pgg7ift+wCFAuN=1rp6#*(JJGE0`tccKv5Od=dpAI!m*g_&KFi>gImKL~hMMrf($0bHv z++(1nR9I5ChO!&lSh^&5@VT;A18v~hh)Saf)E#pCfHFx#EBA9 z2p)kDnN5%?0F;W746#5HS4Hq@0@9dL)5-)s7XnKus3I&CV(=&kRZp(f04q?}$s|Br zG@d#4os$L-%qOQ#x|6}yD6?`-5@2$#)Yd>pVs#tLPXxV%p0Fqs2qZNdDrp5iX*h>c zI0P#v$K>@bbb}MLas+bD3pxVMtqqtGPm6S(h}Ehcs;z{bN`b)vgb;|AWh5_+&B;UX zmZ=gV+FE4uD8G68v(I1NRM=%%D$4$dNF9}6W_MO%FJgx91 z)gct!p5NI!PyP1y({_FNoeyNSKRx@&&1Wxu{y+Zwmw$8m>g8~I4BW@leDlsKUS3{* z^XlsgO&SO81eim=d2sdg@skH1f0S0FK2N(lz@XFV?&~imP+>WpN^2aVt~Y&ue)ZK) zf6}G#*FuMXYrGNZ6d* zLlVrEwiFYP13M`@oD7)w?_VEIw_kqmTOV%afDCqRi93}U)r(v?W59;a(ZrMt~aNAoDpM+7CPsWA)P^O5`y*sP?g+(V%a-`p+ncl zWrq@!CB(I(k+8=gBED1_D#8HnAjubWAoUKQUu^*VbSm76>DzGHPW`Q2T*+Cw+$Dz# z6bUlsZZ!|FACIIXX_?xz=V>W#UbojTr&ss)4w|Ri`#PgiT92Csr3_sxWSz5Vl@NQ> zW3GS+^MAg1|KOQ3H>!k6+la zHt2k|M0f(pu5hf9*p`X}08XGVSMF$bU?~Po5P>_VHERFNZzYG8@da&<+C3KoFC8a8X%@+#ALM{*x5qfdn8x$X)zt?dZmuuWhJ&11v*Q=PJ4}aR`}p~9esh?X6X@8T=c0iy z4XdzTb;D-Ao9o4J;hU1};4`wMcyU^E{qUk3nzdtv+wIx8-rP#Z-i2=65vFpx3!973 zjq_c|$635=AAf(n4!`-?Pppgc-GN$brX^qsJTEO~2sFnIRYiM)wjAc%tF*<7?b3P+ zhe=7RP4)9#xVR+La(`oxhkiWFr5H)`YNv6)kFUD(N7v)o=G9;PdYZI<=i&KxAD_K* zCbjU}-^9>)m|zebwjRu>Ckh||&z$A1-a?z+ zz%bzWaofEX8EPEV9i}(dKhl2YXM#?&(s!l;#@$f#Ez#E z$DXq48VqFQ*Y-eiPbQN{v|J zGW7vDFL-_$3ZQXd!sV8&ZwVJIj0?=Q?RTE6Ue$ByPPVja z!2&>|*cWpsU=YZg#S)*q*IvESq8!;ovjEI;%qR2>hpM_*9TfId`!3Y3w_E8VLOIlV zqajl4RA$`Gtv|z&&wPl7=WkFQ*c^Ji-1z-#17n~h*iuK!v4%cG(PK5AhQR5ZS1FAh zcCI}b1z?eE%06VSiDrl!Qh;Kz&aNvZQ|?2{iU#6lE3zY|h}le%x8jXGBrPDpO(%uq z+?{eBd&DV_EMiHc2LuDd2+o_XFYP!>Q^4rjS16HdXfelGB%lUE!cvWC>jMn*(P2t80^HnL#kf`I@)nwl4{MKMr_ga%Ei zk0=HcTQehd(2}inK_-RBc&Z{~3)4WLiy=sWS_#a9o4V!N05oO44_aNyNj>`F z*bxbGRM9Au>x|Y3_m0#bA;os`u|ou&UE_d+1eRy&`?h+7>Fm|3=X0J)82WayF35Q= zsf$;ew0?Yb_2JX>Xh;n0KF6h-`{Qz+s%V+J%)AY9unhx|J4`w&z?>5y(*qxfA9Xq zH@oT%62h44+;sy&l-Ras7je@=7poerxE@P@lx?E!0^O&(x0~%loR0Nw9liO2fPV@IQR@H#>1WTcxohQkH=b z&uE?yATm}-7S{|%BS8gf6%ffH(Bz(^TX&dqls@$W2>p7!j))~xJCy2dd6xbV?oMw{ zzxmDT-4D;lftX}F&FwW!1k%!z>pE$E^)fwt+5^Jd9##+C5w~Z)Eg`DF6y6SaXF8eB zpcy;_H^pv+$G0%9F|A>K1(tw3h{F1e#Fb1MnIK^^VI)KiPQefW3Asnykx>}Ob(nh$ zGd~rhW$S8PI|1S zyzWMOcy-yeQ{%x!rwU$M%i0ayiCU;qPCJxQ^nmVxiA!~gdTKV^a4ia;5?d*`P43Ox zA^@r6@w~%ViKls*m(9@ge3U-4#f)6A-oDzYLKnRCk($x|)(N0M&U2r7FpOyp6>`?5 z;d~f1@{|AWpTU3rqF*ypK&(J#HXW&+8cS*!f>^0ECh6RpgR7Z~-&I-%tnNMdY&KF+ z?F<%bo*)@Ek8D1>OR(l13-%e!sJp<1)qrhr?>!~Q0XE(8>a8=o3u{KI+?w{IGm)xV zva(0+oIMCQ1;=7lkOLc7n;iRA+!G<52>VW(mIWN<+Jl=3tOJ$OTmm-7-LcS$%Z$i1 zD*0HKBNt;2Ic}&xM%2LFdfnkM4U)zfRH0AOH-s1hku$S88L%=oQWRt&l zUgV2^_=|t{^S_>6?p)wv#cLgriXY|!jUj|JDL9?3zWd(x0JdC0w_S1`)?I>iJ$?P=?)PV#Cx#IF z-TLfox|_})o*ePw(b_lM@P4QC%cOe?JOu72@SC5VmOvo-J)5N9I_Z-NM+&LI7~1T(POwqw6^u z{{K-{>M3PGmiajHT%m3O+5}a!Z6jbtN%MmFG@pu4CHU;~;bMC@-Oa9NtIe0ceE!+1 zn*qAXfEpkNB)&Lb4Q?Fp0*>>rUdsV*Pe%nHVQ5|_H7MF@+VomZZ+BReaU=A8NMweg zmu{O4#47n&04xr%S?ez}K0{#^_H{q)PARUcdOyTi`m)~%MVJ~za&*rkU249gZX0ml zQ3_o!Qs_42{$!LR1_N1lzWl|1Ip17_fh7%qCt6<4yh+t-olr8u8F21d7=X+wc{K7S zC=HMi79=vQN{MPiUAnNwl8qI`RZ?Y>TCF7Bc*l%E$e23!6QVjt0z%9l2dG(X zAX*S2Tanlt02U^yMg$gJf1y)}#k3lKK|vZ(TLVQCA?`fy~;BGFaHK9Nae`CvwVtAZP$;+6iU_;ILHc#J~i@so9{dMeaH}HAAGh zXzd^(F416tyf8u#1`_Bz7jKKlrtDCyrB!bfu+bdp!rOv18e_vm1Ri>5)rBzeJfk~k zfT}i9gX7za_4Rf5-R&IysHf|*;qk{8mrovF%C23UU)iKd9X9;UO;bE*cdvbjo4UF0 zy7}zAetUU4|NbX`|8M^A_g~VntX8Yk4-Z;9)iR&vyUn-_fY`7i(S?Tc3whnt;>$hf+`{^Y~;!-rq|r+>FUPT4epV2Fmq!hOJ!PmpKyw%I;X z@p3$DFW=ElN51;v<*T}T_W1EfAi)xJyq;dXKHWYaR~Nu65~lmR=?IqBhnug?o_@6X z!MB0>7k~M;r^Dh}_ypKREDI(P3}w!-E9xK(5VR4oL>4BnoU=9eVei8wThznekV%(a zTar03&BlPGU=*@WyIBjUgPxJukQDb0j}P;xOj3VnDUEXLFs5sW`rR)1+S4- z0DuIpb_6&g$5!CYKm6OT{`*f$rpFJ@#Toh$R#6ZnkZ@F}7B)-^tvb_%_}owh7ocbr zA>`sh^T7R`)0uUvp!XE6sof${%c4VPN4+@5)O(#;91_$Px7=E-n=MwY>tUHcG(Uup z%6fIk_e|mPY>#gb`gYIbT6C|P4SL(Xur{`(*n*7)3zg=^;;u0!HI4?%r?;4T)gX0o zDo|I>#nXjbBSrMt%a+*PE4YEFgFtWqG6_f_PliqHkQI@ZgHR;R?k6kQ4EL}1(4*P) zako@|f46@|6z2F|?(BH-2j`pnyW7;o^UD!POb=KX6}su9$}+cvT`r}W_hTo;X`ZcB zN}Z5S>fDd!rH`X`=w27EBU3^nc;1hk1n;dZfI*=3+hN>}L((V*hy%9fZicUC zQ&J_N1qu}Dv4KW)1#C*(GaLcfBQeY_SUm=49$HX>SPdT_wNqZCvS2|~RIuqDoScLf zCcP2t+_IRv#9WU72^^pRl42<4j5a7Lq8kW-Q*CAfBqW+A^Uyp7@`)`qG)4eGh(iSi zuAyKoa$jNgTx3}H=B*v%xUJ$N^l=TM}ic9u>cQ5$k3w{qyTP& zilE>TKoJ_c1Fc>64cGjC{OA)00|cF2tU!nr#0ZVpNQpbErK#7j0x7L^n5#Fd0Tnnm zH-vIr)Tmes5kv%np9SWCUC$&QLUm$E4+(A&4W$7OD3QS-jBEhZK~TT}U?Pj;M9hEz zi*u)50F=E})2I?CA1#BI$vo72Txu%0xYlf-6^x0yj;Il$brb|?NIDk`!76r1i#a+u zc=o8xfQ?-pp`klszi4_O`xkn99ZEyvWT8OoCE8mt#8~7J%vc``?Y1 zE7TssX32{+zqo$)-EV(%{o{|@;pW93evdQ?VK1G5Mg*4BTFG@D*4L}Et5=`?Z2is$ zR;LIEV5q~gKfK*fw(N%Ow(HJ>k7VO=Y7im?UJmyGR?E%nw0?B<^ih94;zu7IfBnCF z`k#KiSS2P*-5~7bm;}0f@#ur)?yUog0Y*e*@?whG)U2VPJ5WKeg?d7a*;S0y8#y;L zHAak#cD9v2`?C+`xj>Nt|7_bG27I4Du4EKAKzBzDE3cZI0(371o zM9R(40GKG?04ysEhU8w{!N!q-BdTH3&}}cyL!4R+B~VD>m{ktD?fEzieK!n!iWe7T zHxoD{H(N5!+KRTNWyc(#Ptc=SV4BGpY$11G^u`EP!DG}9ZcPdzm#ik7Tz~echN$Om*kJN<(7^!I~ z%i~8qNXKZ=+SrGrH6l({0*VqXrOxwGIj}^5wm4Q4aEv2$>yjyp`C~Wq3 zxz3spuh#I+2Op-0<0ds}r=#OiPN(e0^7`%W?7WNI&HK|_rOq|<3;<0C#f3vaGlgZ& zee5|kNh?gS9NSXOTItftAg{Xa;_-uRs&Re3T?NLd=2!}_sKG!Im{Zp%^ayARvMZsB z5U|NoGKYvky%C~$bZrI&F(J05&1k~}QJXUYG)4~5xPT)$4bBE2)s?I<4#Z~AFeF3s zqE(W)OYjPesICFws4ajNYYAd&9DvcanhSMk>fp_pNr#5o00YhoZqbh32;1ZiZj7xu zI@KNZ1680}i6}%5iB~J`5`{H~;4vlcB}QZELWdlU1RYR(k&YZHdh(#i5v&0=_tB|} z2X`fQA!GnhW?qx3Ay~v?&3I}Y#LS%#09@xKU~Lv15d;YYNhJ{)cns=-QVSq1P8|U< zMsZ?q+G>!#13wv}NI(;45wSS|V1Zas&C9|SM>MAxK;?e2 z)l0wn@H^jzug1+oNicfcgu!jz)z#g-bCqp~{_xCyn7Q8N+dY1B_v!Cm-yT3#akaUg z-Lz%grmODq_V{dfcL)`0KD8QJGBiXYY&f-IGrfI%`_1RSfAF(K!{}TI)5W2(b+ah9qR67mp%lXA&a~bC)pPxT; z=`ODx+#RMCGmqP_>XyUtbpO?O{vf0j&L8Dho$g9^{$c8t_^_kZJ8ysWZ+`PX|7>oC zgyXpFyI7XVz;#}<;l4hHZrq?KTAd~jC?R?iYz0G5b0&pEfIa3J0tJKF+&Mv|Q`rl3 zOc+A@!FNBn{%CvtIG+CT^V`(W+SKBtr2l`C6ES* z+tFh0;!urfF;#EkkWtXp^_c6hiCEmAcvc9Y#|766M*z}58ZLF32euMBW)zn^3mJkm zgDxam^(rH=FQMNEpC}a!A)vDupdlCq2WM17t7L-(B}L>3Ik^Xnvu9IUz-O?PG++R5 zV$UaXEk1D}v+QA`Y*rU?v@V9)YE3Y(@0uxC@Mh+Yury@ETEUeTgXB$nWUCdSs12P9 zgR0fa!cGj}WW_Q(J@*sr}_Tk-KVkh&HD2CYQNjR43{g- z`S9w-3D@UOOv(5zT}XHJi0|Jv0_fxN%}+o5H-C4W-MbJ$S}P|neLN#KYp>n8CwFwU z0%{(~=cPjEyTQqo8)bwv5DT<2RSlM15|!mR*Fb@#M^6Oho&O(2_|Zhm>I?|%mCPUc4Q9bk*{ zEPRpl+$Q5($Xd`Ca(Mi!fBF~y&tJ52-jm+U{9BS|N)Dh=jYk2_Kr7OO=7_5^Qe`Mq z6Y{xoG2%Z!vL5Kece5qZ9; zQ%P`<{R@f#E+M{Oo&S?$GFqp%L28;w0 zOzM*$HIVF5ajeiPw9RV~=z$gBN|G6tn2Y5TKV5n~Z^pQ7WDTaSEc68SfvKyXS7)X}?bx*T&VE{c$3R#NrK)by}GZSzg!~`BR z$Lc|Zq$D9DjmV%YmC3;nH}jb!L339O5DpDRB7_OKt%-e+LaakG#)PO$CrQCL5M1mKGvKYS#Hc#ZA(l3lvt$6IDhDG-L`y2ua`-Wk8Hz0SfPfguwUUj<6t1 zm=<&=X3L%b4?p;WfWW{;Kn9&l3hzMZohS_*p&6l#b$DiI4glnRaOT1~EWosj8H{a6 za+>OnxF(vCCoV}4a>7KwJ4W+q;+%j5Ap`|$_a>Mfek&n^fMfEAm{2Z=bM#Gm!#WKI zxxP1WGg38Ib2B&S#;u1!Yzw zbqK^T8h8z3&c>59^b4M37YarX%6=+mA_2ZL@LRPqz^ZEa~#i*U0P5i;poU z+||0n@o*k-j#7DtF#)fa`!D}ys35v+qgl8h`msG@2{0@pTAuW8|LyPp;FH_CSJ%tWzd1i5 zh2Or|9sl_6Pp=lczrskgOQ{UVM-*~WJQS%!fgqbjdHUJE`OE+6pI^R&!<26d)tu>6 zr<9TpuGzgXlN3VBA_UXSxsmNjlu8j_ta3>F-DQ(^9#Cjici?AUifWbI%vX~~SmG`$?D-`C`1FHpl8y>2g5@!wuMP?^Q zOcnhqNl=}{S%blddJsbIfPpNbYgiF)oOUTdm~CH6sE~tXH^|ssO`BiVtp`a)L+l%@ zND+9*42+brxv_w=O>^}{AD=d(BEa)BzW1Y#rd)SZqk6kOUgG|#v#f95eR#O4_0x15 zrssFpT&Gw%muhi_bW_WL3A(Xl0)Z(f^WGNd+lFcC38$!z%Zt>u2a&>}Wc2_`ipg{UEPifGA#68Gj1j9Qymb@A}kzRgd>8u zk~QTcm`037C)Hk(185kaInYK>!xZzxele+m9>jtb8##1RF+joYhyhOCoT{4|7BpZy z4@@jYsKo%WZcdTBMD49>5Wzmi3bM!Shq^nQWU5haa>|gVz3f$3NrF>iR$^l+h)m%` z1c4d`MuCZ7fIA`}qXUs?U?niLBnJPpo&+omgCkbnp^R8A1|gAD+#6b|wsrtVuHl2! zyBc=tBpyvA^+vYET?Q9$Vn&n#h=Xm1iXi5@J&k~2QIW(o&o~n0h+beCJUCF$o=$=M8|)x5v`3l<&@-Y=Q%NHlDtK`igyam^ITzgm zl5s$g01`wN=uSa}WcUXtuYR-nKX|vi_{^_wz1~c7-kDRoT)*~**WZ5gRG!BCa6Vt3 zuDYA8uXT@lTR4nCiNcJM$A*Sx zT^%q*5UDZ!;BNlzr_(2|p4;o6Y;T7|(9Y}kzIXgVvJYP+VyjA?%u|3yI`M9w=E@XN zN7s3K{geOjfBPrzKi5N@4@nb5<|HNvoo9+pgxx6vQXp8G1Qv(FRe%e)yG=>2Lou<% zZo0^1GHnf{?Hb!>QXVJo-~Ax3G;m8PEZwfc5~7ND*KIfu~N)I)oiPV>h5H3tA?->X>)Z z7jkzrYLRyd`;*22VGXZIhA99u!bO}cQl@j`*nH06fEi}upwPvs==mnS&46vYg3`PnB2b>91L=ph_b~Pep za3fjIRs%7&03MsAgy=$U1)?L2jTsF|*X}~Tw9%uF-feZS(|KjiZN+{pmJ7SP4yq;D z3cmMPA`y_A5pAodK@o7=(;&bNsVGvo0hJ(UjDb6XjLZFy4k*xpQF}tXHUu;sDHEfw z3QYqhMj>B^GiPgNkdZ*WB@$;Nhg`M|cESe6BnCZ*ed#Gd56FdZ_y+mmk)QqSbAIt* zyZM;z4r6(;*N^?;Z@ziFk_FGn8X{f#dRYie&Ad!gJxsSBe=pjj>fxgI*XyzxLl2PL zfnVKC!N>h{yc{xIrs*)xrFK#Garg3jx4YfLPrr%77Xfpfrq)LI=i}ibX?pz4yT|1* zuCy)JSQ}&IHneb^ZZ2=14ySo|1sZJWdi7ubhd=rLAN>94S`bv9FGV4(I0;Q)r*rzd-|q5+ym5cJiMqkFR;Ic z^MtoS>k$PY1M-cefrXGPc+;52yDxw8U;V#-`B&Qy4ynKZfy^mabOtOG;pi*w?x2tl zMPs(}h*Z)Zb!F@$5>v8Z%;9{bHsa=maB*!SIl={I1;+%20A*)1bGigfM9JnItq4V; zko6m;AoSRO0(}H4!j8D3V`PlJK`GRUOg)MxLR$jGyFxe25|=9diWNL@N=862uEYf{ z-4Ftj&wIA!(~N+~6UGXo84=78;{w|=s^NLZ;eg^y?5pC8a3;*e$y1L@!)$)Y^0)x9 zi!mhD=2`&O1Qn!-axm?PD25Vk>sk&6SzDi zbgqD=22m)7t&}L1PNyT9ceG*KOgo4?+@ns`7L=__$8EOkH+pJGd6TKE6zwp9V`1{o;oT;Nfn5JiO|8(fC7Wj zgM_5_P8l#Lb0BG9bn<~Jax^pF)OMLPTEP$`W?$b~y3-TeA)>+i?4+lkam=8yL` z`%ga_%8yS^-+b{kbHV)~t*tM(VlPuYZ;u~+|5J+(JS3G`Qa;^$B*nK;PoErOd6;iM z{o=3w$NQCd&NMb_{nOw7TZN+Rp6{hx?te+Ss!3~40C=-Icps|)01zzwI@J4zLj%0OU+Z%bN^sV=J6^D*b8mBtUuCpahb4%hEDMuDbvY z%`!zZrJ5mfp0*!8tDk=VbbH6!FaPrLO%F~XIh~%R7q{*GFH=3>aLVlU2Jk9V22@Tv zNW&ua`|D5thkx|%|M^y)tA{{0a*2_0BWh{qiON`LAlB#oNMxToiusEga0jy_mG^s@9gD_)gAf7GmXnUf1 zW7Y*Q?AF3%DDWP`LpTD$86XpHq2jzis_1TX<;AIMOhU%gIuh!PXgkybdd50Y4@-qr zFh}-HGo&FKJgrc6{IuZh9{Yg(XlaL|XbTa)aC+o{vpPlTBciVN@-|W`bE5_ppb%ha(*OGE{ z=#~1r#W)*rAaeuEystT-XGxv`b!#BAKDNl#FZsAPl2jpuD`YnFFr$o27M-LfT0#eA zN1Hqh+oQ5 zdE_aE8j09>>$aWn;H!sJldHV2WG z1pxdypkH7(hT+otqYI#>lmQ4`9brIU`!-F- zfBW-jo1Q%r;lq5Z_LuXgzuzBT>%wEh!khq!8d{q^Hm{@EY@@Ba7G zT~2uhReiYHdO3gn_VW1Ha{qX$@BZ>vva6r{oquricm7WM=Fi{!>b{j7TOS^?q!cbW z^=-l2Gx(Yc12K^+7)X^e0tmQk+3oV$hAIGhAbPg3f@cXNfjI5(2Y>j{r+dW9hsUqR zvXLb&hr{j%KO}ia7YIpCOoc&=T_8nH3{%^1wCmxE|KI=RPyb&Zit|f?Jv%~V%4|-E zlnMrjYDfitqDaPbaFfkI5~8EC3q!j^Jy1^gwB^DOj>MiO^b2`0GSnf3F=dp+>;$Ud zj?13%MM!K4Pc!uZ?3i~H-4o&j4hi^5p<~0q!D$9+P>h!f+&!5q()vhh5dwtF64Yoq zAZ}oewyFaNL2+-2EDZ04+$FG3L&AWLc!+t=yNX633b7)PD-$AXGX}RnNtx6;0SP(*6SyusANq18$c)zX@mNlUBPBt!1TYpx9vb8+p%Qg3MS>J)oh%0IyeYhq?PUK28V^EkzV#*i>$T~*Ou1;Xbrdu27+KjOCrm=Mpz>C@xaE!+rR=2Xs%@vyc!nf>bi6Vs0nQ-pnq8Ab9rC2{lRzbAmJCjkAVM2?+D*up?^3 z-Vxb0NOcSbU1@-VyBtBEC|8Rnl#BxBAf>EM%#`{JSRzxdhZ{&HP4G8L@-+C3>WIFxy_3mVZe(srW#BY@9$&!^`nbn4@8e3{@? z?@z8N_lFOO>*;QO_3`i3dU*KeaeMss;j(oMS6DfwX;&iOzj@%PJiqxd?H}`8=9^bz zxeldR+QUP{%4>5_z#%afciqO|MK5l-rxY>^epZlOFljS+kfx*fAWLqGJ&X|3ZXf4M#T2mo z?(>^ZULEVJ+w-4)^`Y%CTiVO<)88vUxQpNX1>C*l+@RbIB*7<;yD%F}9z%V8^NWA{ zPyX+(4dhwMl*6Q!;WT@SE6FZVU}8k1lzh3sZo+cFNRTYpJcyZ7bRda-4V+^?ada3f z%rA+DQ^oBO$`Q0?A~#90&Ll7$UC0rydBRyPm4J~}4Af=!|M1a7M!8-UBIQbmRn+f3NKDL0us{_vvs~{nB&y@WL6{h{}{nOb15fH!{ zLhHl^{py&3UB>$%dohh*_aGpkAigr1^B(=tWrssS+#<1q^e_NONGmc?_q>a~dU3=< z^o6uJkxVk&T>~=`<~|gKRd(`tR>M@&*jh#iVZ@6W157|u;E3%SzC&0;8`K^5_okqX5dv-rL}FPR8IlKX zV3T!Al@RHeyADBO@)1Pj-k6dOy$02^EF_6A8iyZB+Kg?ZZ{O&&C(m!+FAwMY`S|L< zUhbaHx9RcA$8}#0LWdtcub;fw-R;!x1zaD-<;lUP%i~uHIAi6I4hyxk<6_N^0E0k$ zzw>`J#OCd#C5mU)cJj4};1q-KURV z9RA=tX*$~gmKL;&G5`-aPB1wj8=Cpy@|!>ZC;!KPIc zdXTdt7F$*+29&^D`hb)p3cGxc zZm_*&?*Rsp$TGl{QW`xH4w@LQ*1?mA`w(=cFobf5zNpHawym!O+chIyt6L{x=xcU! z-;6l{TZN(*;I_n!c_&POZ&t(bK_0Q*cmCw+3Qj#r%;&z4B_3s$huL^s=Lt1@L!Htr zBH9DY|1YU?N`(5IKPX=g@G**>f}Bf%TkeFy}@@J;CZQLAWDOAUkas zDTQkBeNT&8KeNmu1R1Pf5)zCI2=H|OB;{7++O=RH8(BiIP-)}Jlmo)I0Vx^*I*^cs z83=jDX-ZX3$28AS3#Ecgfhh|Le znJf7SU_=iVUb%0bSV^#~STQ`i2;gvQ z9Wc;4#ofr`>7EY4(=n1pREI9@0`6t*8rfv1zB*Vd) zQpQvQTrrDsC~`eUpN%@;vxmkHO?UN}^FDui^U?QSj!Fx4mgzZhi00drxs7v*ZGF6~ z?_U3UxjsReu`iq^*Ur0I>s(qHzPUHTIq%8O0*L);X}`w-c7AvK{@?1?ua*xN*K+&v z6J7P$_wSy6`boS0d3(RyKfPZrj|voNW6UXG?|mxK1CsAQeE8CtKuS8Kc2|NKAyhu?l#j@qXH z$)`RpyD66f6sR=8W=yLWp+zkdDiego5OfUml(0QQ5r=0`8MI*@v8iK43tR)!2n0_% zMDVN<>R6%ON4g7`FsfPl;CBc1|etftelmJ z)*%?IaLU*^F1DVxJnt2?6YkAqzvn3nUe6yc$K!Dj0?XJo&RA(rO`KLq6JrSqSUc`- zz;)=Ly1#ZOC%TozM*@xz2%UF#TQh;^eZgS{XhH#s1`5E5Fm`0^SmgAqR3vtkq#@TjzX{dm(0XPTGP2VTMK`d*J#O`1zkzu3~9_-!d1og?`Dh4AO zN?>qmtBEi&Sb$oLL@2~-z!BhzUIms|$*lMYrXHL<29TiSK01cRw43|;gJ!pDG_2Bs?kPww&8$2AZH+pfwd~8(J{7AzT!-jD_Vgak{Ccoeu=nvQg$|A zj!U2bxAnYJ!Q2 z6Nn??0P_~ieFV!`S!`hx!wCRQZo+eq=l1NFr=0`)oc9%LJxD!N$954(226~7yR=09 z?Ju^co8BIG&p$rge!9H>YkHP$cQ+boTtA%GZQahxzk9#C{mp0p+5Z;jj|zfOkEK2Q z)nEVoPyS?Czv!z9UMhU%hbs%~#)C!8t_=r@)$dygeaF>@UA_{Opf@|M~4+KfHc?$4|}YJ>*=DpMB=g>^{=5`Yt)os><9c zO*D0rx;^!u{&)Z7zxzp@Rp(;PJ8YL+S#m;R6HYpE$`AsBkV#xT5MuMqg4K%%R{{#S zAz4X}FccR=Mnq*~O$pl)a-ebnei`~EvI0QJ0UZUp#vbuXX$Oo%;iLDY(gRFF8pa$! zF^qQ=r2q}2%|>9ZV%^#WNH7vm;4t<)z#wZ_0tiu6TYbvBI=F@$f=jdqX2LW0Ol~rEr4e z6s^}I>XOG6Dh7xkB|-0i%#wtng?6bsdU_(BrC6;J2|!`q7gHK;INDVK&}Ay?`Q4OW zUB`o7nwmK$?dHB1AgzxhA7DJ0Y*9~;?w{|<+xyMNb-ic=1;Tas30rKVH|XqpL@rs9 z5y8433=I^-rK5JrDHl$j%K?dtpPQC}NP@*$1VK(X0Re(v)r0~;G%|%hV4hWa3ltwmo_gp$&7=TIsw-;pkD!EdoNxM3^EP z(4H$nnw*Ntg*ZC~AxCUv2ux1ENHG?`k02Hh0xuY=GeiRPEYXcCY6o-xN9&q73^5UQ z3+GKLt2sw|9E2TR2vDYkv{Toh9xK3(!z{N;(-cyz1?=wqPferu?jOu^+&uyqO8#`(H+_4IDu*019uo?kvo`%|i|NaAOoO;2Z7QG=K5 z-TQy}ub%z*)iRg$>gS(+{`J58`Q!QV_x|qRdht^B&tK~K;acqR?K=!jN6EQ-{fO4E zWa(&AqPAQjVVY(I!Z6IarnwJCH3)N*@PG)%0KiXf(r4d2$P^EMeR&!=6~_tW<#)<= zKZeI&^A2O56$_TdxH3;vM@Te<*Z=yz`EUQ>w=m|UMuqomyF8`iXq%=vPac$}NSW*$ zA%;%aITn%>0R#f3IGRrteFf)$9xjMy^yeTQz;OUg7yu4Qc=4Wm2|OG?H_U{6Cf}m3 zu`5wIObAqB@??}!jBMed#3=_-!p*uv$uU~Q3b6p37&a_!o1nWg3H2D+vOxezCA&fx zh)0w#*S?T3OcS6RX7Uzg2f>KwyodDy;c-l8rx=NodmoeqYycV)IGZtAXM#Zp5%(bz zRX_W zMl3XCcyxzUg@GwYyDkm%Sg;paLAM@#LvrgdrDSVQ=Ve#v9yyCCSRY}F>2@BB#az(x zd@S3;bsceJo{6`$r#)p&jsfmC(YoAZnRfG(4TKmKtRjvyw1XgYkcno=HiW^PB1Jer zrw9#ZM8}aFvf3G>l7?FeUyxDr4fxd+oDq5#XBGD$Ax3zKD3pUY5U`Oo?C5q?XJVt~ zk#{u5=r!8KLYTO=0fO#;sUgr90a+|sZFv<=}_$$WQ{)8-`tgs=y(0|jDk0keZD*Pt1R3D$tsZ3e&h%rx976QGVR+0+4S zXXC+uxuXR{Hx8qcifH2yO(29V0xboG2k;V9P?0fI0c97l$#kcjz`{Lk^|SQiv-HtT zJ{?Hf7;A!7;^EpH^*q1N@^qd~1NJYzxz~JKzxl;a{^lS5`m1xB^XdKLIVCwAU*3H3 zvM=x7y?xu&iMdcQ$po9&!F@1fo~Bt<<~h~<+LsF8vD^IJ{`9}F?dknb{v7bId-iho z>X@t7ydT?TdHr~~K3x4urH~b-K9^}MSFg@gVBqz`!<4A6VL&zIrsHw>=K1cs-+uA! zczg|c{@#y&oU^9dZclqSl<_hh+Rfu~|L^|4|K!7m54OQ*Q)Si_%qTcH>26K)MJ@lOwetMx<&=-6 z?-Sn8uCV7Cfcmg*15`9}7QMsTHDr0i9NH8-JGV|H1iVaRH`c^)(3!;OVV{i zU_OvIftm6^IFWZB9?7^(1e+6U9}s~#ODvGZ)M2F1Ko~@%(HziW3R-h$=Zw9C6GXFC ze9nRteNbEhIdf}k0SfS~E*Y3^oja2$zDf1|w=R zT!RW=SOgLWHUI@wf(1My1DJbe3^NEoV`S+L|1)puv+7%SCIZudMC@cON}+KXnQj6K3pB*T#L9>$JOdOl@dfo3(^8 zIKN1T{O}jQaX#LC?+34b|NHIzYflBo^6uAv^X~1t^Hu%&gjos%%PF)$C3IwTU`973 zK<8AmdH`bIF7IA{{^Is`c>kTt!pi zJ<5&|98dFHq(L0yW-bfFp+P>JuYUbzc{&r+#3Q!RQxgLhfOc*H>{z+Db&TKQ6oST} z`#Wq`!g+ocKm7FY;yWLe8-M)CMaxabl=BnTXZc0dr#E>jI16&6l$;jgk_&dE$EW-M z;s5%NUw@0x$4=N_kW7SFxJyovoooZyNt~TBMiO(Pfk3gFWvsp)@nk;KlSi7ft(Fs_ zqdCYFqfsgj3P~c|BjeU11rl1t7=ct^D1hT6LCL#=Uqfbxj!KSqJRXz;L%}B)!8AZ7 zvCh6PDdouz$aGC8ABL0laxmEk(ka^{qW1~LK8TVx0@fMhZOd~&$+ zV1yt<@<2EX?%h@%3>nb6p)&ztAXvf?OAqg0)!d+U0A&m7=47FaiDM)~Wnz>$qM>aG zyw4LVDOMb=GKD(t(HfTRe4bCZtZ;pddB)LV3=c~4p4M|{!QIX;#>G-e7-+%SV%sdK zr!wiX_3fT^CBs!4Z{6{vB>nbzmWLh2_RxQ8Lj-};x6VTm$9^i2GoVYvd25nn?H0IJ z1gBagr^0iVB%#~F^@iHH8L_F>ROCdq&@2w2iIc1Q#Kb`siFn-%cDbm!DKIKB4#kXo zg>J?(cp%N}fwH6Vm}YB%sSglHt{BI%E}tPU>40 zT&*7@x^fZ%^`3HprtSn?2RNa3a}j*F7?t83sm&>N^E1XvDoQ00DYGDW;7qgtC^$QI zbVHJ0Kmf-H?F=y>W~7a%1?@u>0RU^n&|Ww@03%I62q2C#K>^q>6~tlz5)uG_tC+V0 zK+{e)2P12Us@kL5$SKE*kM!Hu5gbU2z8QjJc!4swNq6HaDL`{D&!!LC@~j;_rJc>p zj6)`lVg!;7$%n}RpAm@0MrCjQff0Sn#0|r`6I5i-kfZg-fL-`i6CfZcLtkR? z^$kn|bJ3xtCyzGLzDD=VNMHj&-@gClJ2iC5d*(8gqpml*BiROP!`%e{iHG>0j@S41_YR5D=ueep zh#@hI_$_uaTye0VHw~0hnAxkJPT#2B--a!w=ffa{T^v5z(@coxfAsV zo1VeqXpOW)4h}b}xGjJQ5;<%rCxlK7VG_rRJ`e~nVY!4)OdBwRRY;DFIDG=e?mOjzN;EpOB~gn=k1Q}m$Ki(uO< z7J#GYv&1oM0{~wGrIQd^4l0J7U_$C(1rxfd6Mz)&26=#GTRP#Nt5~ z%xHm=^C*BJ-r%=FBnkA^LK+#mh6NIYOzZt38~pUpJg%jJ}O> zIwo;<9`=s9E+fnFbTxgHyKiCTn_BDgGOz1ssU+dNy!$!}%!g;!^S8V?P@8DRq)q!4 z6|W!MaR2I)eVssb{_KbMuYdmSSHGF3=jEBCdMKxI{q^UEkAArS{vEDgZ{J>Rg2!L~ zd%Hj6w7-rkU7j2hR>W;s@91O7bt8w!V_g%A_RUH1Ebg81j)hw1hsO`g_47}E{~vs3 z_ji8%*Z=0-yZ0QruW8Fq8NpMC^SbtMCEe=0d-iM+PkwpKr&%#Vp?6%L-YqhJ`iZ># z=3S8j;o~yO37bYLBqeDZgxmx;i*5ZHYC%MZ7#Z>Re)!_k?|fV7aY|afz#Telc+ZHYu4F`ZF@XZYn4(L}G1T&M>QIrD4 zfItWZ*-$f>Yatlq_JM&d5i|`1FSm&=m@Bbk^sqUUb<-a=P?9cmQKhSbDO3ok?ulr$hwg!sCIsdIXkJ0uK(Z8Nkpy7$I0} zjUsMy($=*ja7c#9p*td|J%l5A0W=061vCh3R6ern+i;4px)%&pUnO(XsUm0$%eQ2Y zwrznUk%MG1$H7V{^QIv&03}2umW8>xwt%6Q9U-MTJ9TV>PZXUu-3Bz;S9Sy-vg1A` zo|33wr36C2!Wafb5P+}-mKY1NB212}(-BEUKb5FDuXflphbhg$i^590&kz<{;Ts>AKLk9 z!?!9$vj_$TTYzs#Ap#T;IfzJu!RWU@Bz0hb z>$W_tPfz9Dum1gLU;p5D|Niam%K@lsD`EELf!1&zQ>_I#e5`gY6EU`SzL+CoM<_9d z!tOgi`0R9=!-gP-s(as-9)t#T9UWWmeN9v(5zKj@dsm>u6t`7B{{G$b{q54mW$gv0 z2?uFE{?U(bet3)h10PP9D;f;HYLpP`l;8jAum16W{uhsDtcZ6U^DHIN@J&h*N)jC< zWmT7n)tDz7V^DT8+<>g1qN_S%1R=F3NfSfAcw)$du()9ibrR!b>jlSfP(<*AkeOIr zdZ3Xuj0EAxR~JP*z@88%m25O1){q=ZE&z!DJz7KEK@3R4>nRdO>6QVLhj>KrMtHr@ zZdb|#NWcNpM1&|65h7Qt6Cl%Wa$#;xJs5-Xh)Ae`jeox#I3m;DOTW)X<}vy(a5o6r zDTMQ+G6kVyq9Agvpveb#;)PRSAcuNH3_33O=AsYLui0^5 zgKu7@8{^u!uuw)x2n~sl2_T1?IR%4931El>?w|;e$Tbkqx7flXMz=uI%a_8n!B!Xg6X^f!+fla3F7Jq5Xp9!Ygz` z83985>bwG6t*>MQn1~o+F-!Ja>uXe- zkH^z~e!5%+d7cu_^L#kYFJ2rzdRcDou=?`l&wE>`OkoQpP9NRmX`lAV9)5QJH(xw_ zbA9+~eYjWchjxA1J%7GW6CxC(^E!Mq@C=f0?F7g{Q~(SN*&Arv@~#T7w9Ql0odm>D zZeRcG&mOkF{`iyceRMdsrD5CBA*EfKIFoJ~jp=wn8j8#wxGQCvLZ(9sliNT1{{Ev% z_vg!&050Wrn(uD%;m+61hI4>R1$IEw>oS&e1_uh*)?LOw`qA!tuja$E_x;U%nDU`M zF228c_3XQ!5k93RFrOIR)11bJNvQ7EcklnjfBBDo`Zb^#0h;5otz>>U%#I~2!h#KB zo;WrEH9!PH+@juK5ig)%*c*TtnPaBjp;Lr%NMHZ}nL#-E>bivH5lAiE9lV1B&;*F6 z8!`}IDIB)We&^C6AK-YzoTw!iXa%2oOm~ zJ5T_4fFLM=VE0~2B*uOro;VGv$OPd7Z8#hHW`K+xsSIpEY-=ZNtvecZ4tHp^)CnZ# z&ZVg`bV?HIVy2PBgvp2OtM`szzO4wOMM6?o>Dp9v@gPDSoiIQSy?*-q#mzJ>(#5P@(wP9T}eoG5qa#!#k1bc67Du63rI1?Gbe!eW4GQuy8bOc?}g5!mk_h9w0? z+Fd`;H(?Ez61SC>xMnW%l6aX|plHnEb*!^i6=#doB6NQI%7fvaHPh~ou zBrhe;nG1mvUx+%$j`fMhKzIX?#Toz+5V`>vS%j_*5Gf&s!$drQ0uyo|Y3Gq}tiUrS zpi~70W{6S%cfkp;0+8VjFq04TfxT0>4`rTb!3@OUbKgc18U_l{ESs3Bqb(t>NM@#q zJSBPdi5sQ1!v2=0L)S8-<$RN${r-1< zZz{FL7K4W8%C(~G4|$#s_1UYK`Tp}?e)IY3wI^}c5vLbFOvg@#XAfU}@%k^mJfGd? zqaOEg^PH#H8gaf19lNr30oTn!VO!k|4LwFVB*_yO$P#IuC2=g9_hto4Kwr;gg3%t% zBYpjgUwrxHzc%o7Ykl-I?-D4Qv4WGV+Y;Iqqp3n@w6V0SMQ`i3kKN zh_WzeLr8lth)B4uzOLQ}wueC&uAmy`HWV7N3#5REkW(PyO5B^p6L$`I5u3sW9#^AV zghz&8-=IE$WWWs=eH$ncJv$&R481E33`Ts^1gL%3W0V=&!XvN*Q37a&0qo!yn8P-> zoP#6`!xWk!dJl>1V%g9$VF}+sIl6)^=7T&J7ocr}5nvih;KLzEDoR&dtmg1x!Ng8X z$Oc);kXX!-k-8JO*XV&sJk2~Lq!5UX4ou_{f-&z6opR~h=7zR*>&EI~He880u@FN< zazs}s7OWW@VRSRK-f?K?l|UL@0=Li^#6tm?)3%glJMflCjEOL~%texiY6K^=#5tiy z8yG2b#r-v9j*5;M(1MvrFbHGC@k$^CA zOW7DmkXZ)|h{OTPHUcxL!7%F0W(~>#*8s3h9WX|CGf*@@#>ub{JVWj74T(KZQJq)C zrSZ89&S6ds6>Ez4hdKusU?)N_ZqwnU~Z_6|;;QO0>Ii9A& zv%A~VXD?2lyrNUT|MKTweeu#GIZ-Kib^NVLt6n{^pIKfA)tz z{`TkpuFeO*xV(SAZNrheaIHJeCC?M+2(?T?TtZiOpp2L&JufKYklp*`cuW;l=5@?# zQ(#=KDJPzQ^62I`Oy|@7`QetAhqXgWxvde(AHI$0l|A>KGn_s>-tKnSrylF-Jv_3| z4s6pgPzNar5CLtA3KAo8E#Ldm&5P4#$>{MX4_9Veb8MqLKkWbLd!XN%DW)AKrqO{2 z_Is1N55NA`|M0)~)n!PnQ=V}@Pb7?zlMicwMAEtR1lPh`hFi% zMiGz*;s_wtm~Y7g)-BYcKZSWjw`jCoFoyJZ6o|B$UhujLY`8vo%7~1-qbbk?Tlc38 zJ}8Eg6UG_Y0X*aywOgHHo-hP*^37pufICs9)f{&*8eWpED|N-d*anWSK1_CwU}@(5 zWL|^92e!0b;;lSM3Q9>8w_PfVioTW@?>>^({P$5{e8X za$!gilE?uhAz`E$ddiJTnz&@|Kmf60I-Y_(htZ)PEf1%zEqO~Xo;y)MtZyGHA_jmq zUAI0KLL9bcPE*;{0yx#IlnN-A5dyIjc!*R6 zr0|5vyAU{I8UaXYhlB|wpmvi89V<9PkiqtCNz~yGNE#WUIfHw((bd&dAy5+lMnFmD zi^s92S)&q;&P5eX(G+`YGR}hzs69r5HcEC1V1<2O>&<>wdlpVRr<@Uul0dG6iO_^< z00*u}Bfudxf`SNv!Z3pNM2%svtsE61uqQMR=mCL6L<&d{4oZQ-;E5240+CYeiM0Z)Tt-26Qn!q{`BmfT zgARRqJH9$!5$i|4|Hp@u+Hc>=u4!H0{O0`j>moea_G~=vh*PdGm&06- zH~IAO>G09ZvO8?+>o5Q8-+ukm&)bLQ+kjyXHp}$xi=W(o@g?uD-h70Z{liuJ)2r|Q zaDSXY@%~r;;YrGy@9TqHCFBK43;6MnGpMsT^s1e4FnoN1zj^@z<|g_i>b%=eyG^-}&xmzw^;ypL`=B z?_=$Kd1$VVc_cuC`-cyz=4KYgg+DpeSD(o7Mg1@~btION+(>Uf`eZ7!-9NbRjKv~i zD!xDZ{$@Kr{^>vbi*I!ol)w@>71rcjBz5p%&WZrc;f4$bDQ#Q?C9J!vo1Ov4=6wd? zv49QIbNp6HfCV6BGN25-0nG$V7GUA2h>4EPdW-EsO@bF%6;T1Uu-^h&@2$#!fK&t6 zZG%*tj}Xj&l%C5PBnv1-Tzz!V0TmTTRK;w8BfL8RE{3qGB^w$T2!(tJoM1b9&ZyS_ zww8jrk(;;^L<<)rZ77qcDcERzh#q{&wq1f61ArTF!Ela2y8_k-x)zlb$Q`kxc}zzV zCq$GTI768N$;p+6(EwN2uA~8jqJwo;Oc6whi00+G4wo_Pu#?W%0yEc0s6KSHq-Z0m zLQdow9!jKW2Ef_c!1m#y2AgNW)N>M^XXpzxvz?T?XZ2RXV{jUd{>^((k^rcYYKD|~ zu9TA}NhXx{6Q=2e(=H#ToQeW?v6X@wm!h2}fiw|<3t%b86VU21MNS?tJQ3~Cd*y)W z=wOT?hN{wCk#O6>1Hv<)SK#DG$s=6MRsc^@_OuX2MmkFr%}xkLJIH*zQI5oq4s zfJ2P|cm(evo91TFVyCgydQZS29=Pqz1R9UWWmWO#F z*{(9KQF8W1h{~xjV5L0mft)NvN7N}2X*0h6;Fm4o1v3rQu4EcC9C4H0&5d!in6@@u z-;ImLv-!t2Kl*#S=ltr2pT7LtKRkVceEIg>Pk!>|=f7+^UcUOo^YrF{9^1ICiw$QP zFFyOu;p6Y^KRT_~hu{45FTQ&HZn40U_O~?=d)Qas{N(H3e0#aSJl?;nCzrHUoWU4_zrxOW!KnjPh+0m>oW8t|lF&QBeLP{FpBfq?n!;AYj zPfWhsKf8VLYN{Ds-aKyW+lMlPQ7Ht;^tK2-uC)q6A|7pROP??AzV2WBtekw#{OUW$ z&wlT-k3PPYR5r$RI;NW=*KC2Vv<2pT#5`rBLTMKG{wLE%&!&C(%D;L8fdX<8%C*+t z{T+fQ(Lfd~GXTS!$|)TA>p%OK|Kgv%^K*}@B3xvyd^n(AV!PxrF}4AtnL}HA>0lc= zL&=Bq>^eY3SuhKpr#^6NFb2y6BSJf4Qv_6u zK^6eZLpYJH=m0i23E2g~R0Ymgt1nFl;~E2Xu1;NzLQ}Z7kxd0JTda#CU}iu-3YhlZ zF*=12Z0^Z@bZDky;8;Q}Jk&;jJ0yn&o*j_#j2TBD=NS_4 z#bMEOjkcCKTXVrSYz>2J*S1~R5Te)?m?Z!m+=)`E+ne{49p~!1QpMYYsmf%1zB$YY z1E7$!Zf7$!Z2`oiL!M^2KCA-44123`@sc5tjVPcj=h+ zKmL!|I_L#`@>sAVu`xDB1GK2Kr~x8CDRd1kMvMYL2M<$ShXV9&!48Wug>gVQ@EBs= zsh!zU7xPtD<>-rT+ByYn4P$g`jV!$BN-1;7Ixe2)0U*sfh7>p9aPJhWOiw;_O3fpJ z=KZb~trONGCRU#IDU6^{Hu4q#XpTWhhCvjF(8GsQxEUHotf&iMjmY8XLO}<)=CaAOrTop?w}^Ur_T6&#eEw+v@cyeeKmXa&r3n+09CkM{v>Yd2)U~!xUs zyf0_fMh45%Hr`$Qytd2b_SMT*Klsjf|D(Ulxof*FPx|4_qR6Ev7Rn{Pe`vpY|M+-+ zl__D!^%1n+_Yd~XpFQ?3|83eIZ+`g6CqJye|A*iI!xyJ{o?VBHHq0Rgm1G>&&!aC> zs+I7jz{}q|9?M~Q)35Ib+Co8FS*A}u$uCah`pCRXB@+YHiZu_sdHV9F|NZ~@Up+MJ zy{j%BS=)+>Wo9BK>XlEPcF@%G43alU&^4L@56TluFkz5JbeBQh$)C^_2q7aOhP#uZ z_b7q9M;{pyL3dV^3BoP0^9ih*O)e$eJnvA=I}o8-4qE{nGg8cSaa0UY)GdJ76}L0{ zh7|yrSOExA1l=eV7+3H>GMsi873>Vq+!@tFGYyR?k%4wXRD>1FotHye9&o!1KCpM( zI&>Dea*9wPzv}e~9uysI3=G5HA|?n*?1CO1Iba##Y6&Q!hnfXNkS?4um4pBuB}gp- z6&Y4fW=_6sG&YckNZJjD`sTea1h8z|xkUuiL1I%HftUmmRrZ;JKS%u{hypp;5SO4t)yH|zrzKw`ClJ0t|bJmNU^ zG)=^VBxVCj{^Y+X0pY(&6dVnJr&5+RO|!-!Hy4tsJFOkn~ z)hUB*of4XJ+rT>1YB;g3h#72VbB$cw1u-K)-}KFUtPZ}HC!H=YcNd=z$Im4v?4-!h znv;sx?g>)3`mCcV`*!K=CEUEc{M~=^;lKXL)gK*mIo0!5?|=S_U;O)j_wzsb$K^d( zOvnA<%lB_Y6FzkGIc)}OUVt&t+%Y7psEf*_<&3-JA+8itf}jDB-@`D{4m z^6oL-{>_t2`@H<-h^LSL;D^8aFF*hBhc`K~SGTH--NA=coM=Ce!kBsf{KN73I839j zU!Am)cpS18*&ToQBN;0B1iQp3W63gf9Hw<`fBWzLpRXTAbcu)rzqMXoT~0YNI5qXH z^|t6bD_f%>5R*kGfv%!}wn7st2HU`p#a1eV0+=DeY58oV#AR^tMGeqS;NJ3js2Oti`a5Ei3D;hvw zpzMQkV0Q*|0GWVTNn<-}9;9v^yFpkDaN@0HBF~hGodQA$fjk1y*9~m7O=NE2VL${C zmf^Rwl2^!ud!}`hj#3evs1!+=7bTKt8aN^`CqWn>t)!WREMGWt*A666hK@8Bt;}9Vi0Mw8-fIdKpSvboyY-0%yeP_GJpt(f~-y>M-R$koMB+!+^5Ys z&9OG*=!OWP>Z`(Gg)>G-Y>tG%k(iy4!M&pw(j~hWKn!SY7cCU5yNYcA0BtkrYZW(| z#18|M-7)Pz%87D8QkTIOv?(sxHa(R9nd=@M`Old512AE1@$&zwTi>o_NkxT62Rqk`ry^> z!+mhdW)X%(LNq{j7ZN``^F%@F)zybo=!$w&qu#ee&%0|5ZBp+n@f;>!1H>uC84f zn~;3?y&usQIUO?l%d3wr#!F!y4lnmFK4#iw%6UBQcLNRc)%SmRIPABxu8Q88yYp^< zVKiiMCp)JAJ4b(SC z1vRL*XdTpn1Y^WS!w|+Jwk7r{Iv_Lv2c8HL+9tim+EELjhd9ugnSYH?z99Jw@HTSr1pmZFeNk5L&4i zlo%gFHF~dbtXS?b7VW3XPP<91#U;`Q=v(P5{j6*;_K@8|?0J~TOE4zykxuY^O z^o~$X(OAPdz#Sk`F$LA;bwrzu3P3w9+`9r!Kzj|dk~&T)W0suvNUSt80Fo?1XA~r8f=021KY;_b z4jqA3fHT65R6_>Ojd(yV0TPJ8>_UM8IUEPT{|%5J1ql-$Lk@_Y2?)WFC?eb_*5Cv< zxtov+3fw6ACsDAWM6! zPl^{na}ReG+Ugb-W3s!NSZK586tWSGLnasyA6KEQz+nys$;6$>l9WrjdX6`r;ESs= zWjdUEeZAh*@BZ@7e);9&GOKdJIVpnWIX~-}B`S0ZyrG0&u_E&-F;r1(+%t^lYyZ>^!6x2FhjI|N` zCLX@|^3xyx@y+v_Q(v{$d1-*6^9eNCxDc1k-mmpE?04(!qnY|SMPIsFM3nu`=f#0! zq_nvAy)IAv{xs#CzJ34V$giG1Bkz5!*7~_IH=oA+uK2TOmjeFSX#aBmLAev|=RqEM z{POkskAHHT$D1jyH-E6wNBN_Ni{JbBVqTu+`H>k1&|a6Ndmix&_Tfm!Pe0jB`1Ex+ z3~~f%TkKx#e)ue3k1^j0jgiRlw>FPqee>q8{`)_9tQ?LY?o z)?7rGsc#*N&MJj}qlS+qwv`iXhM=bFCdVC+lss`tByg10jSG7p zAiOHnvz7@Eq~TN3V$^2z^DX0)c3e*nRQ(~|?RNP%6iQ?rTXed(z>UBr$A|1GQ4u1+ z0^(r{s1SRQprW7Lhp zf(TtGHE_>5w?0trt<%*cWtWEuQm%Ot!pUW03StRe0ZKq1%+5C?w{9cA8l;g6J7VO3 za8k!7VgWNugX4yj9h8}ZNZf!VyfY;OLP&saQCQuvyH9`?PJoXTH||PU7z;B8mCPu- z<3S6iL_;`%Pst13ohaIG{Zb^vxi#7o-8g5F6sK8@%R7L_^hmmc+N{_0Eq>KDu3 z|MJ~0m#@E`pH4L@K^_XHad-Lb1(ETZI zuHK|k!HA*mKYjrt;<#f<^vAUZLJ%%>-LiKw;ynb3UzQ6F9WK?e^)}@$kXt=rh~og-{Ao z??ewReeq}i!=L|MRHZQ$1Vn<82S5@Ya^Y~a;BaV7-7JxVXHFIfo)LgELnw7AxPpzv zSpc&4FlGxvfWRFui&{l=7zi>`HSR(Egi#HXx9Yreo-B9V9enPpYi!lAxTDRhZ`npi zt)WcIBYA{TFf%67Ed;$NphY<13Uh@_C>>32);iS6tn0qq=r5wZ^I*{Ui@coSp$7hHAia@&yz>p9}s;fgD zyhm&^bydAh%)`t5I6;BN0%$8K0Z3Mdm@+UZ3DqEgfm$$7P^d_WvL~`=O3n!d$RLqQ zXIIpa)_NqLCJCEzV45g9$S!>GG&b4+Dx^KRqJuk9iV7KYC7D_hw+`BoAb?^}#ZlVg zi~)>c4gwOJN!peO0AivHM16~DIWTj`q$qp^S%xBMk~B^!0Sujy0Ss{j>_8qQ04(r8 z9SCOBOjZ#LC=6#O3+zY}cqZ8quE0Csig|L70D_c&AV?xs0tjpn9g{h?h>mdruiz&$ z@i+xIMYCw;HT20z5FEB>xMPt-W%Q1S3`Ew$I<9kdOu|Da^dZAI5I~mf%8pvt0~Ilt zvRfb;1gNh@#VLh4CYKJ-#D~g)pgN=jFoaR!h2#jNL=DRUeKRF@sL{vW>3*W2k0pXYiqb~TLzIQHdq70N-~DF)(X*S6Kk-mB!HJrZ$k3lQF|lfOZD|;Y11K1vt!t#>NENU#JG+rn zfeXP8ucn)uG)(?-pGHj6l&7nK82cOsaC1?fe{wxe<8qp#e6)Lh_()EJ4DsPb`Siu^ zqy4kVSJ=OQ+}_{z_dm1w7mxjyZ{|89prN^k`@Z$rz(wTw%iV{+|9ktJ>HPI$U){+@ z5k4FaH)(plgYDi%$4KZ8!CifK_p|@k|L4E|`aD;TfWCxQY_u(pSQyltC9@BOJsMH! zTlK9f2f26hZWw_;4%FBdBM1jsDquG@4o~60XlCF^y8DzpS~vu><`}JcjHI(mk+w1^ ztquAKr@f>xAcCvSixRp5)rEKn?W*o(UL0fg^8%{C4wj$?mh9+}Cj>$Wz%9IE0y3en zI6puagbjT~P>f3Ap%l!3U<#HwWC9ALfvY-jFhM{xb)q1TGLkk>i`Kx!M8MR~>e~_; z&IOQhLLR2DM5s79NHSmfQa{*iP=&fR87tbNVHoj?JB@N%aEpt z?qvuVeOtWFPijWJ6Imnmwovb0olTYV{`zuP_9xtG?8->YcoOO z0nIEy09(%kDnvLKfo;wSJVR^buDV%ZV75UR-7T~>DTX6O4p(&S5O6){@gO;`G!UmS zG5`x`ND#dN0fM5Uhq#8}7|1T}nA|q-9x8O{A z^0jUj;MxsZAg@)o8X(fos#XILun-{lnK~i5cLsJFiDQU36Z&FCPJr30SQt6GRyaRF zpBNj~3ItY!+=3?PO|nu{3il}j14`0R)_}|eVF|TXD-aE96S;eHl!xi%N8|DPG?VZ4 z<<$q}M?X3K_~q_+#}B_*{>vBN{oPmZUk9X{FYoHUmqVsf_>VdzzUj#Md0%4WqYbx@22tQ!$16; z{f!(it`5&%?mzxC&TW2kKEHdKPn(T2K6~XB^TU>2K7aP&W1k}La>)blCnrULx-LK( z5bok0-n&B(uzB@m(|YP^{b|E$yW#NsX1tlwFQdOq%D%&^PZ=GC@&MFfn)ZYrBT z%%}YFN0&eP=oQ~y;56Lu<&-Zk^X|pfl=Eq+-^}gq{^^T%x4+pwc-MAwbq_Njk2r60 z4;K$g>yN*8{NOTix{mq+R)zJ(9 zOA#y@ENEJaIU5V7SBmu~Q1IZYv00}TBFj&#lshtJfNif`- z@)n`lxKK!Nl9U;R$pKSrGYkXvjQ(Ui1`n?0lCV{VxmPfY@NgIayYvz@Kpc30mO;;f z9@WDlY;h2%4Ohp`&^L7nXDS7lh|KezOwGX?qAF_y5P+(Vwp9U#7;Q=cMB1FEP~~m0 z2#^kY4(t?+a0hzEc?mVQhD;=c$jC0c;(gQe3Qu>a9RuCBz=(ElLXN3xH}a4kIWdxL z!41hB0I_zUF$OWuer_Fo@ie4p?o~WpUn84vcvnL9rS&QCkV}zr^~{M7N}O0Uy;9y07w zr&0!Tw=C-lyfY~Rl36hAvKmHG-5I)5sB^EU*2pxIGzE1x28E?jO^g&+1KfHNT+}Q) zEYOCWb}nwj#%{z6umv>tK}kRuQ|>rSNWmKcQ&cCYycci5>`mR9x&*rtXIw+i?Y?y+ zqad`pdOe*{70_977LloDNtg%8fs71^h=Iq56KR;ah#rMBAy!c#(>F{hfzQnx#s{j2zEr0jL?KdZbvz`gILhIAtoLWvHGvQdh^CU0 z?3UMG*;ub$eZ0pJy)=7Vo}RE%y?+96GzyfeXB#k3+!}(5FqTAVpcIEO?{=~+3a8pP z8?L6~htCclUR_+5$zfZTr^l8^h{i=@UHJIf?>_tZdf>-#dxsy6z$G&Bh`S3JuSa8D z;o|+f^EUVJwIaWMcSI=MsgzIC#iE#H)FsGtr`zq1mX!$I}%zCw`h*)Xf-;(=ISZ72H=q^{@I0Y%P^{Pk!_{` zBp#4@Q)1SDSnjr^(cH&sl85PH8ZM4`N}Ptg8&EZ>1r=O~(HXGzd;kv6NLiyN!U}l+FOY!{p;uc+>s!wyprUoF(y|1Tv~DimYpbWQ7#&iU%gZ!!ff4W$VCbG#=e}i%DEdnaWtw2<5lZkWi9{ zI1o0#8}J4;g)=~nXaFg=5CMiePDqG$4jf%5D3A>)P#lo~R{#MpM-2o(hDd-IfB+OR zI%r^u2mlSr$QVQzK};@8l2{T$Hw|w>f)1j!c3i7ABcyFM>Hp6Pj3}Tpekj-NR-8`izd!#jjdK$b95UbS)7PJ(BkctJGAl4;dK)^6Z z6x;@a!YMIGhKDqa=q_~h!3e(%%&Ohl-n=T$_xkW~_w9EgyW{f@`qOKe zpTIC3*}%5P6F^c!MCWpqo{h(=PX%MU{cia5gXwteG22EuFUx7cZV=>>Oc!EcM~=!= zqBl{46}^zFs_ zFZ-8Y_WBg*3NW!3$ujNAAz*vFJ~=4o z!|99m`rYIxi>2s<B}a44{C z4qHx_k`R$hLVz{0b`Z*~vb2iiM2LBa`a~(kutT(1!kc3Pr05P*3fT(o7#ayw2UlZl z1q={vgP@A0j%qpK#=r~65q%>hpArNTZaskffgci`o^X2z+d&QM?tvU=wzKGA%@YA9 z+l?xKW{6WbDGM&|6#&!>tKy=r;0S`?07_vR-7u;KhdH=v?}h~K=o*oMm^^?8Kq4Hy zd4Sa(t;M!Rbpn7u=1iC{CCY-w?6}9wKpvq+90tIOXx<62Q?Rtzpb=M201X&A#7u;2 zR^3dai=P=f^rsDa1@>T<+$Bw{0PNLE38Yan^^V~>jmy%|ry+$mW6l{QIFQ94W}VR# z3>bWr1k0Af5rKDtMH0k}WF{mBaugyUfDDk4gGdv5LIsDQ2m;s|F*wuW>PqMYrrf=Q zVpfOPtVvtL0s_!I7DH1&<18y=)DuFA?FqLB=`$%3fEMR4%!kN@g`=&Ag1jRz4&@kq zVo^h2poB4mERY{GJd&7X-)0xKER+IhFy)j8fjb1TaVg`04b8klHrgJa?zhw5{!{t& z|MH@Jlcw)xOmDs!-nV>LWt-*xC){OkYS2cQ4$ zIJF#aj{RYM^Y}OaaK8PkKdotlw2$N|!}$EghX?GTi}9N${Q1wm`{HdoK{AR}SMhA% zLUJ{vSD)=KcSC!>woN(>vaU{j_r1@@?|+&KlOzx8flBGSzy1IH-~TUPJ)MHB6Ic@V zXfOatv28U2>zbH$dmVG8flDgE;JvbKXf0p^3IOWl6_jO2bxVD9L&`?rsDX_sS4-kF zI=V*$0_{2SApoo{E(Y$jtwG_4=HL-wy+zf)52pUQ0jT8*`SFyD;M$wh4(P49SSp)ku83JTL{46N8pV2R zZVjabln8CiBEZwoWTasVPDYU&GLs=PQUXv121tMjp)q^V#=!_`Gz1{P7F5u)ksumk zLg;{mj2r@#DFkq{AP54GKnVkk9#jH39FTSZRd^qizyPQ-C599LlE47KQ!;}zq`EE= z+?DefYd~6nv6c*~&ZS_0Z)-sb^krFvWz`EGV!RNPqJFlmz54YP^$5$UP3HF8qXB@V(t>1im{mGK{m)?K9m4XmKxSg8T zweMH36r%yH(oKduTLT1JgTW*t=W<>v?eg%^N9|p(-#-EU`ay?`!Z~BfmzN)YcKQ6@ ze1H4-JA8kSb3%Xj-P8H@9DnogfBj0g4}bhakA9lvZhg?TBj{Z>Uu=5~ON(ht?R?&G zGD?GPiS7Bzi|4PdcgKGE*1?Fz%(dEdGk*SJd^sk+0~mTddYT@;x%+qjr~misyD*4c zcA*Yc)|Gekus=m?0RXm*dz+9MiRoIG}F3-=X-yFy;O76eFa3v?qo=va_Pj5VRO z)+i(=*RrSOLG?@~bli)sHXTWkcO1wdL=2|DzBw9Ojl4r@zz`7SVyMw!GiMXY<|>+q zH?tk(X!!ykzjg!%^l>M5_f4F7I1>OUyJD)3O}nWbFM==j4m`5frsJvkpX$boWVE|1ei|6aS{XeP*fr^vIdj`2qAkvD-{<64IK~w zor$)&50^p=?gR+wGOVSfHK*7TMM*yQT8L)zokhw?jh%oU$i#|2VaBi(p|IUG9e@OA z2xEXrI9l0fxQ%sZNPsJXcvO~w=Oq|`C$lr}rZOmBaUL_}AOX%5{1PO>8-x<<&~Af{ z;0z8F2vmSktQqx)3qXR*2tD!+0{}B}LSSb@P+$lpfCvl<0&|ES=-}X9h#CeMPT@TW z19lWEN)BHsK*A6JtST6U83qc{n8|vSoC9YTQXoe&L|jhFM8MsfC)^a7L0noUn74pX z@+lYYjeKG^O2FDE2EqgB8BtCxMb-)=WOpVaN;Cog28Z~o%1WBPd755uWgaUkBBZSzU#0~x>;1l;Ely zFW>&=n|HlGOXJqw4HvSzmaFUhY5G$izP+n`-MKy0B%#c|c;9~ZZ-4Wnj~`F7-=Alr z%XdLT8CPBBH7?7P!5)_HIPLOv-tM-G{Uzw#CohhN9QyV>&c?&Y&|#RyS1+z!y%7C6 zuyns~%deN=Cx81N{_Rg+b8If)!CMgm^Lp+j?Hn3{X4(yd6SJj+!&n6G7r&Um+N%43 zwt%37ixv||0$>y#KszQ900K8Bu?!N#+ASk`LP^l(DEm0C)LN7sMla-s#W8ctU>dH- z;%ZJ9Sv)e)3Xo9G<_OeDQeZ+|k#`P;sL)+YA*3)snoys@cVWeaslGS3Nin-A4p6rO z4WOPYhHt?T00v$}fu+T8#AWM%iA(5;YK}-?7!r98x>?S&IY@QOP`AJxYzrln2Z|d2 z6o3dyh(gqZk|z*w_OntlNQl*HAZtM8<-N<^Z8pxXbKf)6FhKSSG9 zEa(@K9w@>9T!>b-K~S5ss7X*r?~Z_!y>H-Ar%`NiNbKP{3ui4y#&Zkayz zCy6xdgo{&dB0Cw0xF8K2cLoWO0oQ;6zB!Hn3m}Fp{3@A z3ByKFA{Ro%oV)90)Kiu`NfehYoI{|Ltu+*$%iz;s4&7(c20~#91|9?Bf|Rl()W+q~ z6wptK7_tEcwtXm|En(e}D4g;oUzqrUx(zC91${^P~^t6_U1rJjM#PyJr^t8mOd6p+j*dcA!( zzq>uX`{wTS=6JkvVQRWpX{E!(D}CAKW$6f))NoGiDQ}1_xkwV z7f<)Et*&m|Pmg`+ZC>iO!Z@0@v>P+tM#R2U7p^33H$xsFMh7~@3xAkCsiHB!*Ce-e!P%Be~n)} zY`3@D+qZAuJ%01XU!NN64_Dv&=jZWzcWu0DS8s3Y9J<=+u)Ffi7Mpo>a4A=8+>AaW+(m|VsL)R1$8 ziZ-VlyDZ@V5(qAdA^=c~kqKxF^rnhk11JC-b1()umeHF*-)swPCVfS4MIoGITc(H}#e3SEv3|1)D7^y5)*d9i@_@%Hem+gh`DAJ#*u1-qVSU-oOetpD zPq`!*MN>))nh2z{z=5nnDkxLLttF0bCW1>u*`arj5`G4ASSiv3WpY)AtYJxADTk6` zLLV#wwOe0#4aWoBf6E}Gn6M!g2q8yzv?z!JBN-IxbKpWj;F7m89+lpA_^Q33aKq> z3t9&ahcqH{PB=*s1_~C)d9XYY0JJ{r#ofh$A)qUG^Z+om*vHCs%UM6Um68J+(!fAGza7U4tn|j;AI=Y7= zVBkJQ(^#yx=*<Sp@j)r*VIpM`d&tGn;sw7HF!)A90>$3YT7Pp|*+&uOt|AAbLM zJ%KGxufKhId+uB9x?Ftx>EV+PPv3p9J)Q5rxqtKJS8br-aLBaFoNJw_VG0ma3gbNS zl!V5+-~1|$^y<@(g>rG$^`1R{@9@&MzL_T)df>&&XM9t3#~nP?tv~kDyKU=F>n(x9 z25(>g+_(JrO?~~)uJ)G{mh$y)cQ=cqP3o^3HX-`i=0 z{mXLr{Bx6~*Y)A>=(ABXt<|&IgLtnGrkY3ryi@ z0G5dG+I(A0DWW@u8nah{1TN4KTSr3zq7;;}mt8*`!h;=vDvG-h!P&=swAEr^N+1X< zlsLSz8iO(#VF03}j6?C-?7Z2s_*NYZoWmU2;xGU^k3m?IHwHjlQ5%F;+xq=|4CJP^ zYV8$d&pE(52!mVN31(W0M8Xsvn3*_V6anjuWt6V@@jSD8ZPlR*5XkPlowRqeTyw*6 z$*#E+Y&IM?dI6K`&tCE6{%R`L&epldjQJ2A7A`|fN7(Q8T$qRApxu}ig#m{kN{Yby z0U?DXj1I}2B!V2oFau}E?pB$!Cn0sD0!Zu)Nys|jOzO5gKqXlkTlJ8N#-ULLE6f5~ zI3p2Oa74I81V$DI^{{cY6hzj>scSBGDv4!JFPU}Byl*J^45V1Kz2E>R!W$7jo1SMN?=!H$5gCp zjH^asYu=3G6qekKB-{b?m~hrLMSzNNB5$VEC^P`NDl#jbz{MbNc`9R(-A*V3ij((5XYZiZC$aNzvAg=@Q?l*x+uOOKNaXR^Rhh+ zPMhKYyLLMN!(X3Hbe`|q(wDg#kOVjtBz{_-v=xkkQGfLP-~F9`_QRV3eE)7ZFOSli zr5>x(C8Jz%G`!n>{arih^6S)Yeo~NRcm3lZ{odQR@87=t78Wstu_T=PRHTKgS(t)x z9Dy{L5?jy2L((kBzlEwsJrQhklwIDjb}a~q!7PX!l-N2jV1-C4utb2kg98@?W~%6f zE>3CiSP>@E6>%55K<@wof@p-UkQlH!h`<^^ya8nObH`l)6%D33RXYtSunaB*iikDZllBa$AU=^_Lz-Z|V^f><*k;5MhNKFd z44KWLHRvafE6|()hQid(4pbC%!1iFNu%?0}kElfoFDrMM~ z<~-p#JMC;~YwhQ!wQ}mzDI`=F)-;Zo(NAvfh&Jpbwu)1ZRX1`$HwXH)*=Xnjx ziV#pl(!*8wZAOjASu|oyk`1%qe@gQ&ynRJmyuKAQnSI zB1{P2p~y`wnbFABkneT6rnXc80&^vXLzRs?fUsAyCZuhnD?`jA6y``u34F7lL>%9S^)<@0Na2fToC}mF~Vd*6ePiDp~g9>VmKle%m{4@C=M2&5(1DVqr-3Q2kVWQ zTnoU$kR4Kum0)z1yta-k3a*_M!Xt)I8YXv&Fk?w^oMS!J3oaPqSFuqpZaYzZF5ym_b(35CA(Y+?{}=UJiG_|QE$7|)Q4$I1Dw@K?!NfDPhS1e^PB6p zclScD8KaZzD35u6e5vc(^SrK&Gyy<9qV#zQm0ooBe0L+oA9x4-_`{zZp6B1Wd~HA4 zJ$!qyym|kh{_2a1fAothe)(_a7eD#Y_-Iex@~?mS_RY8F)BPOBtL4qZDZsW)lvGUWL6A?svH^{?xmf zT@5ns#g=uqn~*G_27u#08OhcdOZ4U#Oe7SF48RgVn8F*P;I_e(An3P3d$a~@L5Pk) zfK7cY*c<{W6gpFJ7!TlGa14G7WMuHVI)U+oX24_g#rY2U0D|Po6yajq0vO&L0wG~E z3o}*f>&D2KiLtVX*w%9)^284M6eKhy*E3x(t^kw~&Kj9?)r}xcsP{e;;sWj_m&JIeb7eW`Notz}(o~X>E`UHh#V8HIw zDQJ^)wb~ZiOD4AgdlgPhq%?32y1c~gJ<=t6cdxKUP(vwoSr9V#HcLL%=%j;4Nu0(h zC81ElASVDL2=@S7T{A{-H1`gva6AQZ7=xWb1Suh5h)3i=LJB}8Y!N`<9ua_OJxJUg z00J4Kfg9LnFc5pV0ZvFOf`Tbg@j$RZP($NDaUK9b10yHcDy9TML7j}G2t*g?y<~E8 z%vu|VtGO2K0yisLY(vtIwyIE_D3xBD&pv@ujmc759|@P0wv{WJ-*L z7kYYJ+v)Pv1x1Gff)<2+j=R@ief;TvJ(NB99j06u^2C?>=aS+6yDt^bwVP~!+AnVo zjQq5oTGZU-@%>$Z$?hXt-d=n-eENU*gWY#`Q~l!M*#Git{_dN1!|wXUOmF_}X?Oh} zcf*T*-@p3xyEk*3dz16M_6CkyZ#V$COArC~wiyyyjA{A(Klg~g~aJu6qa05bdvXn*(ExVyUoiQ{{e0TnW z!pt3qeL%u--~c$!NM+Ai`obich(~hbzG}{hkP}ub7xHvZSt8|tOke@P98jx~N-j`q zkU*yhLt#+G#6|;#xvz9K=K&26F|fM`w2f4JqO{JP40#~kl(U2pnS)UTU=89hLK?Bx zC|ym$5OepQJnbDeY}FZp5HiP8!zm*UDHM4JZb=M+Lxjwj$omXP3>HJ8rp*$pt7(Na zK)=TXK;nU50N%|dVMeXq3)jaUJF1U>6uK#5vLIOjBzUCORVcM?5oxPcGF!*M4kpfS zj5Y%k6w3hT#GLck9K)2?#Sqx0l*%XmM*y8mutGbkawqsK^o=&Lz=oNISK=(LU;l$ z43E(!9s=9ak==_0XpP92i+37S{x6LV!SUAIKj)T?IIW1(rZ! zhCl#}01g)f1H$l*4nY&z1|We6V>THG(9IYCIDs3}7LWkIREQg54p#6Ip%zKNIsgp9 z4TEH|ZB-%BFdobClx$rVgM94zNFg*ZCebo^60jB` z7@Od;u9y--A;X+JlQ=|<=m-fzBZlB7z!EBukWefhk%o>N2ji-!OT<3920-f*wv#KE zbe{>vv9DI5sA2iwa2yVDeYJn~eArKEJj27A{_g&Cf4_!~`G(3~x#dOiy0A=$DBFEi z>G`m|+t#H{(_wClUp(&*_oUh2aoWAwefA0R%)RL8{_d-<)9yMQ^89dG=LcOuF7o5k z+h_Yrq<%QQ$h!-Ee0n-n2kW-sIC@`dZ8zBgdLWklQ0KSzU;pCS&F^B)RCatZI0EwV z^xa+C%Zu;-!JBVxgY0s;8V{NKqHA4!A;s2WB-p>Wy1Ljcr?Z%oh!=W1_jPJ}J#A~h z{hPlXhoAoaSNC0~$8Y2Hqi>ONs>?mxUr!h9qpRy1;D_lc!THyK7t4BY%|RT3by;_b zfuWxso?6q`H-7wV|NO)M>iN~(edhxZ(t4(JbF-T-%XPhgYWvNvL?~Uv&C8R;?;OD)n_cAl!!%GizVZ^Wo(U zx48&pbCw;DIs}=SH=Ghs4rUyJH&AM9j<5$LNZ{TG%smq^2Vq;Bu3gVj1lo$_;LVUL zMvdtjT8*wG#ZsZVI_z*gvt*<|G&3b&3`guCH_^AChOoGIhNXHAu{xcfy>2!5_BX>-@;I zNrxGkIh6CLPS{#WVVEHTtfMC_fjETlrfN;|VFkBfa;!Bdb5?OoB}zi08r6G{BF}Rd zqP|rw8Ji6rU-Kk zAaQ*dlJ!=|JJ7_fo4f0VLKqMl5ugn4z0+OEL$3+qDgMp7#l`dG{6b1f-FdTH=;oC2MB_^8g*zdv!HN13 z!ocUyU08KV>=W6^WdQ8R3U*=g7`zWY(E8|_Va&d)3R}bksRAC-WI}Yzo6<@UP*CA+PuWqFdLF?n7)IZ8IJjy(`(`FU4Sn9)((6?A2tLYAj~Vzh zzq_Aqey}cIXT45)Tb}0HH-l2Qr9HjR`_cG=!}Z0@XMZqUO^<)`pSbe!^dwoVL3O^n z{qFhvSwB@`%p@C5+qd`U?>#=d{5V~dX}C^o}`x6r@&(^%FcS@xL{rTt9 zc(sT3Z(sfRg`Mtx`S!6Rb7G??BGn@8MyPR8-C{7{Ow0Fw@B6!{)_0{GhoH9rdpaC( zp&{nP`T1dhOiqD9((U1PeE8|xH}AHGyZ$uyRx(r<@rco*w`FEBVUsejCR`S$Aj41t zSf7^Flys@B+x|E)c>zm7-a1NWL4am(kqYDX2FeJj;Q0xMUGxckb;{ricw!*X6HN&d zfp%X<9PR@PrU_c3s&L%FykaiUJz5Jngg&5=QBdzNUIZ$*b7a>6>;cN)CDXb(0Xrd> z0YN>7Cud;o(Zg_c0a#Z$9Glmw3YtuTg_5XqOPQy$q02t>kbyB5Df0C znzrd#e*D^5oHhrqEe!nraeKX$hxX_lh`@1Yu5LgyHqN8OR-q81ElG=+rb|4Q>CL+} z*GL1U4$?f36M`^rhSan!eapI@*E8lL5IOPgh`es=chU~pfFuS*41<&=78URS?7o{u z*(GXq*hK{=LXZ`7Kr5DTl0=yU9keNr?n)kkXu)DNTJP?a?-XOwio+fRDk9?j-8S%;VhkYBO4};;dQ9W(`{{EL=KJl;q=BNMVH}%bG*uhM_E9x1y zP6?R@s|_E{u&kbnIy3b&jx_9XI#L7(yhf3EQmbc-t|_S8jW8h+VbT}`kRx@^UP}arx&S+^ z-m-6%njq|m+&v9na1G*Cd z&}9fAQA7y@B#h|50WJiFu(@Z49)V#L1Q2EpwQBb*xfkJ% z8KW{`7M7Hu2vo+4gyRc}_k;}636GG1Z8b`ubAr92f+O}b@PtT^0bBqoxnOER6Es2~ zvTDKNYsieXcv$TBY1^tc45i^2@w(og){6bjT^3d5G!d_sd7cbzkp z!4LHO4?mSc5C3pGq{Dok1N7$j(fYX1;?{LrPbec9^KrM6gT=}EfaG}^EF?jOAT{^P@Usn4~x9%dtl0JR8Ha)BFOo))%m zQWrlCWu&G7b=3Vy;`Q zT!S%M!nnGAb@O2vvcWC`8Tf&(#(ux%>B@(aON-Xe+QVVHi<9C%{ZD`Xt6%locGc>s zIhigAz$0&z zN4LryFezvyR1=o%WQCA-5>JtK#2P-rwqR_LkBD=`9%w~F0O*8K;&aZJ0hsNxVS)(nzBpMtMBoDmrx zJpdTlq0798H_utXbzBPQonPCs^m$%8iiv+_ezn8s$=HX9at#!)&sM!uFhrwy~BP? z#e@fjF4Pkt@j&1Kie^aasDOL7Gspn!-T@GhwgASIAv(zcFc1WT0Dr5yCx{i`5_}72 zfDxGBf8zuYkTDwKfS^EY#E2Woz>y)`HcP|U zNp$N>9PYUF(8{$UAc8UpBCc&KJD2gI(?ChgYr#AUhzxtCL6}E`Al9=DV+5lnuvLvc zESS-EY6T%+8XynO71Dt!I09k{?Uq(FAy|-R+Z8BbG6Wu+A|PRe)986u%XMSdMW33T z9&a&Y^B@nblqHzhkpnSPL>3-|k|VQd^L0gcdwhKQ&+lvE)8jjIJxtSBic6cuG+aJg zpB_)A+oABqZhv_*jfullN=H6SQ>n{Ia{>~y^>BGPy?kl!|8agh_x0)hyRS0kd_3%j zp+b|gv#L5jU>H!{?`KO2Al zk3asw4~G|N*xqgLDJtX{vSJQ`EW1P)5R(%iavlngj1tLy_}R7S&0o_N5+Th&11pv88tS9fqd-KS>VSoKTmJrS;jT}*c1z`}E9a)Nu>MS%W z8vq&a35nR$yKhnJ-MhQ_anq+}6avi0{X}NewdqD;LKIasDo|Ib$ruLAgtc)af+RpG zVG2h9^dRSSQ6d<7C{Ss)Ys}SV1U3>B=ytaC9LNXnoV;-!T*2f=v=kl0(`Z!U{H1vSXU?iC>v_eDj@|PmqANsbtmDNn zmFOEp)YdDp^T5z$2Zs!Ti0+hv(h(i8D{ui{gD7xBYzQrwfDC$P2t-0;zzi6Xt^q0- z01kl?Xy!Qp0w>Tl0D>9@2P-llr63JXK^ahhbA&tg00U$Ic2Fln0wRtuq>cfIhA5Qs zkn$j^7|tmh8x(1eb1apo1cv!=;HmJK(Zh+5z{3JjGGP$wqu_$J?FSqt7B&(u$x|Mg z0EC!Hm_Z0|aAwp@9&9bx|Nj)>*V8s#b|2>bR@i%Y_Z^<_hA++xW`F?*P^3hKvdRbk zBYor(hq9~e1M5J+qA1a2%Vb%l!~_8#FgbkjjZe7m?%rXol5Ha; zxI1rNYP7{OLKxNUIxspBpb}x2h_^cZVC%{rv?cAlxP=ZySVLIq zSGAg$toNM3F)oqbOYS<`$8VqCtWx??JTHaTQHu6@9X1k6n#%2q!((qgpJqwR^5v(aezX4m&E>kk|LUv0JL%Uu)cBg~8S-LO} zDfj!wwmq)2q|?j!=5WW;scxfQD$hkC@59Y3Wo_4MlDXS<`-4|sesb`qv2DE*rX?{E zf_kr#$uvZ`Ti(62=jS@!e*VKRKmFm~dvUlKy_Kk`{nq-g<#<@+oEMp9$y$g+X-pI) z=T&>_x>@XFW5L}yefjVGnLfRZ$BWn&@lH2b+C?sT7B(cs%-!D3|=Ac2mM;}8VNWz)Yi`!{FM%!~LS^`soV_Put> zF$g<#U#uKozkd1S^NSa=e)?*8b$>j5{;8GaJ#H?TuOG%&-+lb;chAoizCL&FP9}4|M0cz4t*%EC<2OD}=?~oQrQeSqw8=c8}hf18^NaUe?3I$KSQ};-?q)Km3tV;RItXDf(rLAC~S1#K#5YN zv9aDI>ZCm? zGbA_Svac=?Jk#}hTR6R#znpNjS#!x#Os6^bb?ZDDU002Kw)dCI`wwzIrNT9~$-H;R zhbI)3r&SNPv0mu?1JM+-Q@v)}ZCcnaRIjd+xnU43g@#8D=Bp2vSUrs(CA9Tndt2A{ zO-F%JX>E52%{eH#4@l%rB0MSwLlmJ)AwjHZDg9%|wYHbAMN;AP*f&BD%9KtRZ=OuC znMpSvZSVQyeuacd#**5uk;s<2hIcv7T4cm}-TUXSUmWP7x5p)?!~`gV;T2*bvy+&C zVU&{Z!JNI5@ilqs%sL*7Zk;b>xW|E7mz-2LjH!FLfSgCPlC%$~C!GfIk==y@W=_K! z6A;>s3Zr%eb!7s>hlyj5P#8-((0&;yleD&VC5}c)Tc1yP?*rmAB~CD>Mnj0fGAT`~ z;#J5~;tXck8sT7ZJd24TRCojcXYb$?#DXbk1{w2&2yO zhU>*Msj)ZWB$NhKbxjyOv2>`UJ=$^L>M5ZnX;;U>*d-c9-DWNeTG){f#7t1cmC{;|LF67@Dq9Z{q^7`|KZ+^W$)ymUz1tmi}^j>9*-5@x9^YFX#tIrOf{zz``e)Icp zKmN_*Z+?Hd)Mmkp=yE(ojM!}Nb#1BJD9hoTyun{BpPx_t^DoEgsn@;oY1ee#ZnkVM z#@D|~RK(rSNt!)(-X0Ghe)d;ie`ug10Y^~vp3_W4(S{22a2j=&78D!krtrG0#HVnb z)8Sa`eQ@J^=ws2?<}o(qG)9~BWS*#GR9@~fKl=uZ!QiBs{pwwq!JM7LiN!akBX}nX z@*`rP8t2SbAqT`N;=J*gIxG=)?rcfByRwI6o2usG^*Yi_R4@j{PE4pXyt#|0QEr-)t&%NTFt-(~2CfHNTJO-^j z9ho~}dvWLW35|R{vJV6CHPb;HC!IevkU#GxF8$d9PVA$>gESBYZO|CO%*M{y4#gio zaviL0>aCwH&reV5<=b~VBiuQ-@k})56m)&67F^7S2Q6%Ch)_;E`fi0-k}$avSgA|j zLJny>?&1LwHo}q$n{$p%k2Rj(KlvYh=9O~f7SH8;+FFITG&j0G&Tp^Nbv!=k2V6e? z^n946MWC5UGh+?!J~914FUk@=LumM*L_JtJ+^3id*Nxc)mo6vA9w@9p+g&Ffk&?rA z;#>D-T_cALb$1>YNTyNAbmI~f#BSk9;A0V~5sGLorW^*LW;R6IL(H&M5T1PZnW1KK z7#$p|cOo34?ZZjLcwTfeL1W2?m4#e8MKPUW4K~0Mq(l-Sunjsv5Rrm{1SCSJh#V{g za|U@L@04a3lbS~-H>hA#3!;<}L~bNg*dTC3fS6f5I)Vuea15d_4sjYDGbpH44JQC9 zVU=4nXDdEJk{grPa5PbAV^jyoGM7aAps`w}eWV;?mn=wv%5&ctOG;@8F zaKhjW->F4I=Mg(60$I^)cBL|BXDQ*^K$cM1hf2<3g^o8P6Jx~CcA(RAUVD4KK0J-) zJ(5J*qj^G)%kwtfpC9(8=lXR0=%L^2PrKxEvZv$e{DXh=?@s>x{#AQ=`hI(#r}HuU zJl(#E=jTdczAwx9xn3Tg+h@#t8q-$B)B0rF?fS%NLU6sVhZnEs*SGVpkJB<=e)-kQ zFaG3oJ|;%5cKPs8bIg1i-W!_z#}9w_!^``R>p6e=#jB}r zTi;&ZoKvBrj`bIRO~)T*c3U3hLwmGKlH*vOr|D&U^P!%8`r-QffB8@Ui@*EJRwE0I z)+$lXDwryc_nL^_yyw0$)KQ~{^Yc*g*uzegnAQu5PRyJWXp~D_rSbPiHJ3R>XC}+aT`8Ywd+#H;66e4lj#tyD?KV=NYnL zOgh~SUP+lpYf(8w>`^!=QOF{bo9%&O`JBg9gcP)w+@c0rHIi=!=hyG#oM)Uv3#D(sz^Z+ z5JEFyqKh{NMwcm#A-Q7@*eXdvrf^{H2t%8V&2>t=M|Zm+e=5j1Mg=0s$`Wl)u$UCg zd-X1yhgW8S?J*a+N-cf}a$CR_g;EqJa5K-@)<-SJ%}Pkm6ODSH>nf5CvP(I}Vj>VY zL18Y!;UAsv5gXzL;Xx1NFG!t)K?&%@3aP{mID`|Kfd-3)a73`XAw>+9K>Pu7G*Jo4 zh@AuBjsSv~oLn3mVg{`NaA-u2kN`lLqBBh%S<4^*wzVLl-DM!oIowRBch^>0TWLB% zHsp!-G2+4_A<~|z?#NSVbxUa?3H8n*2NmXk7onk)wXQ>Nr|}Te96Hzs*x8`5529{J zy1Nk3%OR`zdRd>=zK##{IMy*w1?fsG@FQ@kL>3o}xH=o73 zzgaae;p_89tQnEFb;JufmLKtFf8^9h=70Hr{J;DU|EkwZ@5UfgnTWWM#qE9h{KvL` z;QL$bR${Eb{jEn)pbrp@tc=6SQrWg|ug~j?(-J3g(tJ8LF>SrMwBAcwJaz9i&nH7z zctqCG0>yl#e3E)~7Pb+`SGKw4;I@;&D05>&Wj%VoGVe4M^d4?R0Ut5nV7~x@mL+#T z-UQrT85?tQQ3~<3an!CVN^oNawFnK7+t}WEW0F}kQ3H_%jHr+%9*?Fe=hdA&i~D`q z*Qh;Ay^f>_;nX&s4iO(6XE3Nc8Dv}ql7cKPdV7k~jU8{(?FqL9>dAu zYe2h9DMs|wBco(~u0a;6BNCy)hh-&6xIXoi_T7H<^|tpkup4VC=iZ+s1n#4eDktUI zx?8t~e8_(F!HIK}yBFilgZ3DEpKp&tg;xhB#qOj@s9whK2u&DyM1Ou*hZP{4gPq6l zMAV2<(q*1j&$su7`u@RJ-?QiQ?Hzf+Ms^~t-4BVbL??O)QA^*tz#eqBW7Kkf95tq7Np})jbN24el;Ul`s$_QNpV=6J%LO!r;U!QldJc z1X>N*#yE(tBy$II-Ela@2%GP?ubeClwqCh+WJXa4J6uR(5@Zf^W(o*gz^gOKxI%9)Gf{`o;4-h+rTaXe{#7+Sk2{91OQ$j@GS@;AR6eA)zL4q9{u>cm< zC?ByKg$HE>f(PR0aIk4h13;8ShPX*|xn#%HBQf z%F;S!%_ypg3^o-`L5H)&)$=ZNF-eNxND&>T;M{yeN`iUB&U;06LjvzE8a}b_q_^-j zz#tad0@`U7`QCUCN1!=_sJVIUTMM$mVcBEYLJVP6GcI`p4=~fZYDojSOzSw`6EN4W zLoU1C#lE+(T;D(b`e(nS@`?nREs&8^*O_7kAHagCB}!x z>-F;H7eCuBnQ0Zj-X71^#?!~AdAY-=r_a7@zyDx+ug`5Nk9B>RZch=-jc`tU)yH4_ z=JlWbc)Gv2|K!WZ?IFi~d;7@8(`P^Y!N=#nLKj|C$W<3vp@e2r}=RC+24Kh_E+cE|EnL<-7kLjKRamBEMcc| zIsE7~OP=Ly@Bd`}!Ek@| z)qnqA{*V9cS}koz%R$G*XksTsp138v`%S*Noo{a5|Lecb5m^;$?KaXfU9UW@E3PK0 zx*X)@mTy1t^{ahsI#1){JEEe!=9|KElF;rU?U`~3J+d?P)j7iu88$CTwiq7!)eEq8 zPXe6CJ0!f62tg3R4Bf#zoJ8CVx|uk|)Y+Lcl$ff|C%$%zkx_aIE{icIyCOVNw$V9^ zJYzi3eC99@C#_7yXQzDzx7e>Cg^cKsGo^5iQRDU#uRE^q-Q37|^azf7F?>b(cNWZpxf8Er6f zonl)fQ>$%Idi(bB-O!ybip9)Ze~@YE*V_8PvQX`Yk(EY7S1(mqB{5-#+OvQ5WqkKF zMR;xggweVr@=DI6;3sd&OH#FVkvO&;v92C#l4PmGs1!9CcDq#Pbj;Cfk`&%2f}FLf zH1UbHjwNq9=-Pc|DZ!?^GO~%low|`uL%wWzf^i})C&M=A)X6urY6(Sf>p zG;;P}V$Gaw@A-UG6&2HPmWfG-jeIj%%pj=nDG0zBQJpiTIS3?agdrH=ph9sCR`~xP z0HTC&W^#|9jiH1JV?aSIxKa;S3MKXk4A2xl0*#!b)__DX5rPKwfm8`OIIC3wU~2H3 zbc~bx1!N|+vy-&$;tp-poK19R24H-m<2$~fXyXq0W1MWpo79K_eoVv>?G*cLr zx%ccVz-3mvPd%r+}S_0D~{AOp09d5%KYxbhb!gS>hSGml>N4q6P$Nu z&FKK-?fU4X4PxYXck_?_{Z}`S#`f$?ZVL`80_b?U^|(w}vZq($<3kf-~5yRDRDV3 zW&Q9x`ce)HG96w{_dovWs~-@R{P5Lp{+IvrKYy;`5-F9aE!f=01mYSLPhn~7V|)KL z2)(t8(dG=4K<7-O?}>Bo7a}jpynjn_Tig4iYVT4O6b#rT+m;p%EpAC%B$|1nl*tAs zc+BcoXJ%z5(We>;XCp*p=3b@Epw$`SW7Jc^M=$q2J`$H07FJD`B)b|LC!$2d5ET|? z0ecQkLdnxalw;pVr=%8IiHT4>72id#Pp(sR!8F5(z?`|R#7${})Qah*?|Vwor?ZZ2 z9B=c(I|CyR=OPP8;pMf|ZDeAc>28|tkNHN@ZRRtXBB~=8Ik2)bQ{_-ZHBvUY;p;<) zfZUYf!dM|}VC;e1%_ic+^~0d$y4K&ly>65|O%cpdg^5`#45Zq6i^!@e5Sn$EQ1h%d z+Hy!X@bvV%JS);+(($xC=UIwJEQ%p%Lj9O@7FwGl_5FF>VL_wzTs@@)Gs_~G3v;(k$~Y4CdTNX22O}X1OoR6p#y9LIff8hup>CYyv%-CK?USQ1}_97 z%#lV=CLqW-&&V@UaHJ8{V;fIvi%iMP!+UKk$t%^wV4*SKkVG`APg-sB)}!xWgLtU znGR&I3>fO}A-rC%b@%J_4Tjq#n3QVDb<79);h+A)&;NrTO1Xag=I!r){mt{o=WY=4 zaM!jxKYZvLjY`+;shn=h-Rp8YonHTNdig?xIFrojc>DyV_sfHi`n|zWa^Jzrkq>NynwooJk6@ zmqYpVCqJ&={LL@__0N7=b>Z^#oA0yGpZ@WW{_ID$fAmM6-sze75N(y9bo`{8(&?vv z^5S!R`Q`LUG%q|YYNI7;$7R_Fn z*qOWN7EsxDEmc^H2rKDUEuCXI#C#WrGcR|1ILfD|>GRLhCol5-l%=J%3n@*?GzdZL zV22}8L0@@N(1CPz=jd061cgIF3i=v7LkQ7bsE35N#<|I|gTH(8^zPa$I)t5u5p4H9 zw!~Qzkds9B0K&MBq{5o0(sD@o=7c=^b&Pj))HWiEr(WmWy9XUkX*wQ6*hiNlQ=TFp z2Ic+Z3Cmv|~-3Z!%}3T~atAqfMGO(QrW!IW(E z5E^~7?hqbp$cgX=8zsdsL$W9syNqrXfQ48QN^atw5=nR@8lz$=tQKP(bqoty1den?;348%<}Q7A7SKxdls@ z&^m5s z6ciZZ-k1mNJBtw1kvtfA1VykrGLu>`5vkPypr=T22-}o+Q^xgry1V0u+}CicNsHS~ zgQ~eEE+Q@U-6iIsT{ugUp*ZA&2)O_<4=&Q(Pl;5+Iw^-k`oMG;=0uYf<9H&TAcd=q zLxK-jkQa(UNrxm2CA(luMmyJ@IffFNFgkgdkf3=MCya8y_ghG&llI#3`F7Ydr$oy{ zAD&%SH7_M2e6%io07pI&&By%tPiKBVkWa4``95ql7v*Idah=nRz&?DtLyCyNkCIE{ zZ$JN|fAs0U{}1x1UjO#p`*$B6pP#oTgZx0nsL#uxue(p3S%}N!vfcYSrQ`9F6Z>2r zzemc`3#P(i`SR}j>-z@_eSG^kIQS+nhnr|Ey?X8ST3#Ig=uiLb7k~F}tsUBU^h7)@ z^KlZeEqUNrdz~ zzy8UW#}_ca{rWHevw!+e$Cj58W5lT(F)22L4knl^K7w+RBFnzH*5{GU_DLp4=Kyo> zJmr~{op?G%6?WxAyUtA_&Xv z=B6-2o}vM?dRGzz67$6k07c2s9+MMF2@@=2*nOL638ExDhAc9!M8kL%37Ha|j=0lE zH-$%k+EU$k2B(3(`f`Ffd^j?r2OKG3G?J@tBf3Qns_F&~L*1MxY>Q4_VFX8jk3Alg z$_{*g;Wiz@Fj291V$F^nj%LHU;na9qD3$%`=^$-dvRIfIjLD750=F&nc%1TlsH4{J zw@R7zO%AE^urZ{h;T*p1L)vA%G{0#Py=m@Q$oHXbMOwmIw6<`*KaTTxN)b&(rlnZ- z^+Umiq_l3sgG*+0N+Ep=2StOc2~ls%#dCvSNUQg$JEdt@0}D~&7_P&Dno|a9*P<2z zx4Jn=vSFb@gD0tWlyPzPF(S#L!&30*SpX)YNxbvGvw94jQFelgr?AICC)hBqsk=$e zQDdYLBh6jn5Mn8WtTLxaiZO^g*lQV(r zf;@|OVkZi2o|7<|G>cfq*wHsD6RffiFSkeuQUi<1Rg5VXDncMnON3acOD4V@n1>u~ zx#LjJbJS>aYV0sjD%RHFEYt#%hxXsJihLt@+MYqc_5(Jv?&V z>)P7mJ7IFWs6|o>>pa2-g@+rgx*D5}-nkmt$4!DjGD{e1o@VZA@6NhV=OAK3sC$u^ z&Yp7%2^CgHPRJ>;L=UnHkf4e4L1reIxLHI44POpIEsQlN9MCLYQ8E%R3>H2NCC3Dz zFgb90PkaorK=Pc`+(F2UxzJccIVe*woEgI-56&?L)haAV#VlxCy)~yC+@lXdLs7+* z(=Ny3G^rjFofdgD^I_3Lk-L*F3@L;J(m(|<=>&IB^&U`$v-@zL!fPOLEIbxgBHS3X_UNu{Plv4M<9-D?|gd8SVA5^W?kGVd{yTqdUl>dSG}u zDe`pl;&v(}$h{dwvXp|!Z8A-f+>??$6NAI6TcRL_N2mi}gK$u$$OM8+ESU{JAs%c20w=IK zLbdA!0B^JPMus}TZYZ2oSP8KG+9Ms?^&*)>I#;E_y{@*BWo1l)4V{KuM@&1%67`9&IqpNBrBe7d ze1$X$b??I}^APWeqU~Vp%=v!0d2yKfIDKN5Zy&$= zFbYIpryq^NZul7xRmsoa$HKJb&}<>#v{Tbu%<@IwS)} zk|?xMoR+ffmCJE|ym%kSn`2lXvGByt5AVz2$IJ%qc|Q6lnxEco_PAebdC8sg^V>K2 z_ImUBG(JA$*URmTSCrGo^@%w~t2aOV;_SX!ukz~cufDw@Sx&cqMO(k=nC2IgEvNgO z`oo(y4?FeKG4HB54PhJOj-Te!i#my~oM#;Ga#>zXx8ZrC@~X{G3G(o7|IPbvCA~b2 z$H(oNbvaD5^Zju;-k$&BEJywLbop2R>3{wEcMDBZNST|A?8oCVFx=7WN=6}r5>(Xd z6x|NfvbE-x$SUe4V9Bzt-OJQ`6%SY+qY>P^rJSqKVGQn9)f@tY=WIk`GRO<{J#zLu zv2Pq^kpV{{!bG%lz*y7?!!>irfV1bE;89mkj$VBjF$IR_qtw0YL9;V!fBNR`rC@WKdL6 znP`w9I(&|~vuMsT!^H21!9k{D&@mB&q{4I=^@ z)ZLeckun%7AZ{XT(yJ@O>!^(o!O1K%W$_lkaAMDtGskWr$v5H%VIp!T_OL8k2d9yu z4X$Q0{3+4Yx5yI(AJ{q$L8Y=x%tu6XWg0aQK}@kYUBVVh7nc($8;pr5NmvlI-~e;{ zfid~6Ny#lVBACJ;0&fgZTmuvcfPjFtU~*CkCrXGGgot1?a1P$tJai$A@X=c+Dmbg` z_yfwL8PVl}l$A&&xjEMADf3iXUwv8F*AZHiaIZnxyrmXZT5<6q>;bl7hh9_(cF1t+ zF_JFqJ<_el#+uXsPSG1OLck=UnZ46kkqY_6*x3l&g<`!R@cg{KKP^)_eY(E?j*c(qc~%nN=;K$v zKHc4&kN5B2e2aQv+=LtZdbm0FZEb_+X${}*J1VXn z4`sU&SJwReakIKRrQ>qDP=9%M`245;ak@XvAAgzj2Db+}-f~&xXgLD=p3c$Rygj`C z^?&(iNa~wpw)7>pa(1E9q^Hw5{)4q(bpokoM+)NVZ6c&<*C*q*v>ybF0??&BR8M7jk zqm59OBtoP!l5j-y&C7v=M9p#VNEAJmiS|9Rf=XxT2+fOCXJI3v+PH^D5VNCrZyas- zEcMy5rqNu9+7tAiWal9v3waN9GK3S+gtj?FSoL&6-GdsG$i%D?8K^+TTZQI`5e8ua zSM(lS7(qrD5bRhE!7jvL_N8z>^1P6bC>+NLz4JsPOa)<;Tv<|#t_1@Nj==TFDS14F z&A~wiiH1nZGT7l_CM-^>7ENg4O2 z+wri|FsAizzEhyC)h}x}7LxXG5e-sHM9ih#*v&5JP2+3m7z`TBug-q5}= zO>tPX?K432t?>eRQg`QtUjuNckbad1Hv&VpJ*H0$4abIQQgvbfJ~7Sgg~9BBNFW5aUky? z3F^!eYG496NJs=RD9%9xL2w7LhXXAn{(y3Gg5(5uE@YuETZjhCYHc4eg9Js*fO%-YV9fc~Ugy03=zno`>&| z%+wR8d1&w5^LY5s5X2HG3k`uOJUFMspf z|LGSW*1c4$-@Mz!MM`%W49bOWZg@(L9xBVtqD;MRKqHU({IKq8M}K;HNJXFDzv=55 z=_wuOX@33fU;g#uS6|byXev*S8_ATXLv%aboRczT-N(S`rRjOTdHwSI1s!i`zKQew zQ;j~$b^GS~=MNv+zJgj01Zti|jyb1SuRr=SQ)6?{-mSQEB z78d1`QI55xZ+`yYKks^bT9{p=ED%O|G`oqzM+|KI&_J z65Ext^H#4rZCATq+S6nI?wj%O1`qG++c)(1pzZ4Q+3M5w@IGJOj^__y7rgt>zk7TA z?%V5|3;F7O@L~3+j}uEXGS4b!9F0&Q3G&@FE>w|y+4ym2o(VxSFwx+$>aQGvldnb4c!Qd5L z`WVz0)}vLjY@Hn)6z&2dbEDRbiBO3;`Wj;dPk@CGQH5B{Ge{ARG3-E;MHUi;uqsH( zSu&z``U9B=hhSm~76}%T9oXHRd?!AJHTM>UTH;|sg=sjvF_9V!Am2uft<$f*y-wpYJio(A*vDb^4Ba}^Wj!+9!g4^bP_4z!1KdUZ2P>|NQX*p(smBgHAx&e$P? z021;vfv#BB;8p0vQBhZS23GGKJ#{DbGsl&Nh*pZI?$RRMZKUBXhxS3;+b}du-uk+^ zdq{g|9hfBCM~f=iNrvj!u5KLGEXW8%@NCrEU>0zq;;D!dY$SoW0ec{2;^HfCh0R|CM0SLYl?Sv8mc(4c2L`X~k5g-&D?7_^`!6jHZ zfas3yND&ht;1nPV1X4r-6Xnd!$r`f?J0b`nPJ}S;Y_(hMASWU&L}58VoXCA2l?})g z(WupM8Y;aG;tI)ri+&xQBy_U76g=OsiSkXII z7r=BXW25e=9F}sP&-Xl?+K2Do|L)^6FZ$|JFk|bH^!}%REC=;b6VB1LJRd&&;~(7r z`2P5XI{R1u_OE{Rum19zzx&y$q_?l~U5?3>dAUC(jWW`xTNzDtUfa|8eB;439wbd| z_x1Adv_6()Qe&Q9(`<+GX{+PoH@{)-nr>|8x4-x$oy&_CKeYE>nf1e~m&}86pH4TU zM)V5V<)+P_o~A?Iulwcu?>@YHsLz*S?c>|$%acFt`&z3MD)TgfzDT%R%H6X4=+pZj z-8{V7zr9Um(lKQ|8m7~iepC4N3;u#Frtt6I{txeRqCD~bc84oXM8?zWqx|qkaiFK) z{CEHO|MZ&=kJz`^A0HBPo{m!omqgd6HaaD!a0*r)Sy+~nCe`zlPfL_Nds=3boBQWn zkeC6Cgi~U3TaL6WmJgJT4hxo}5+Wr|2U7{v zC?}boa5zJXG*BW+p&Fb58R|hDv~JX$88WVj>MZ797q8bI!&bMk2f{qOnZx#4o}S;n zxk%sE(J}sj5L(DdBVsz{V48?b9e*$ymppr~efU&;&(2HsaA*iy3q%lsY+4S_?K({f z!eYG>e zS40TF&54Y&A$fGd92`{@vM37KMYKRy^Lb3CQ4N~QEKOp_)TL9pb4{n%2m-g{yD^o8 zMw!}3Y8B22YfNP{WoaEjBAT8>Lt$uD{Sw>MeP=I0>NYH-OSUDg!d2zaYPRXuz*%jCRrGV35Q7}`sUs_1ggw?Yd%8B z)psN&6BUPJcU8$j5us4CUW2G1Y^Q}IF`@2*h`YnQTO*OO1YwLkev{ebAqEb9MZ^Qd)r)xd%Lc^n-nRB zscT~O?&LY^`GC9oHP9`}#ayd`@lJg%(!o459>H?Wb)tZL&K=ey&J zPhK2vfRKk@{px3b{daG_e*bvsqMjK$N8_R@r~w#FAtn2?_)a_7_3?V<8>x!E>8OWv zJLj}swldx1_W0qqKbv2_I8g5I*N-2+ZR0YRPam(f_jUf{(|Nwzp5E(nq~UU0<}`&p zuKV5|9u9|Pxu@gVTYY*M{`ld$k8eNH-bS~5Y}>VOZ`XR+j%G{V$IRE7J^6HO% zsC4mP{_Xbe(x#b6QQOd?oZgoxC99OmU3F807o}$V@G#TZt&M8t*Nd;!AJ$|hoX|%5u+sAb zk4PL-Dr0?&OJxlq@&>!?-S&Psjl<+KMI;D!<*05k7>&|2Ow0%N&>>)s2=Q*Ia5fh= zDtx(WYdpHpZHbM%$KJ_1d+>1C9#NlJGQaOhpo75E6iJ0L zn1T_Qi3r+U7!0t49D^XHRyzXb>dWNAohHjA8o50MTymyiP$7o1K?0n%5m&*_e|5dK z(K4ll#YbvmA%lme#4$J{g^)T&q*e~&`+Nk0(@cBg za4HKhhV=nupVU8c#AM*@BDOe-Bk>`RmV2VC8Qut6FG(TXAGx;>3vYM>q#c1R*XQgG0h2oFV`u#7qF86BVK(2=E9N zgp#FjMQ{jtP>=#flp`d(1#I^O1-KAXxKkLy$%lKwACR%EyR3WkEOSck0vhIhDxNbL zkM6KBMjs@-h3)I8qdP5Gn0Q%oa!PIz6qU7JsY6)Md(gr1Zu zcVa^(3=fK61`ObSN8#YnHYvF|1<@S~3y2 zDuLG8u{RrL9ivy!`FwXScSrd8?)P6`_i;+?h$Z!2*HPcSOOg-EAw{lr^U(P)op0_$ zE3DPW%ZK0nZheM#X;;}V_3rh{`#=9A2_5yY%!iWZHm;0}HB4%%vBigUUlM84SjY2v zd6?3I@sU`)jmw+wFfk>1^WD4JqxX-?&1bjsrzCbb*)kv6n@exI5e?rDKls7z@ul07 z#<>6N{^r%0F28@iK0RIchiCi#`SII_x1YQ|zc?OGrPaY4OmQ(%7!^RPN^Kw%q*mkD156 z_<#K8|MS0Y-CcW2szw^N(YhUGeSJSmxSpm10Z!eSJcL|RnvN$6BFTy*#W39up>BH! z+wgGl=uxY0&9>*5ln3{ZPduNhp3TfD^)b3iat;r#YqTl^o)xXaSU5Uo^c|d}LC|9= zCKSmH<~}iqv_g~_#3LYFl2`><4D;%gZa4uPZbYmMLq@m_qi{!5wk~keoA7W~ZpOxd zQ3NOqLfd0b!mLp=0>;(l6pGlcTww+oYs@pm-~ttG8_fktgPG<;I#Ze=cEFHRlv__^ zk;1$0D@9}PL^{Y_4Q&uRH|Hclm1?qHDGHNDnT8c38lk4tm(xINEk8XyfBO`jH8u05 zKGM#p*PX&Z78dMgqel3sklnPlE-C3WI;hTfhq-sl$D`GY1h|xJflEc*vr@MJ_mxD; z)?&G!LWyq;rq^#+?D5>Fr%c@n|Tu=2^&rnI>c%vlkO@GW_k$WNEu;4 z0uheEq<{rc&`MBnLR2ITG=y*x!Sei#@c(W|bjV22T zrpfK%J;M(e2zNvfU!zxsgd0W;4jwFQ;VSDRTr+}NZ(A*S<`iBnQz5j-tcPQv?7K=s zOfUHLZ9095cKQC_{-PU8yG$j|i*-Ao=WE zeQqBgE+b1yB`4oKp&3&eQ3S|6=;#{Wt&Szy9z3nT)E` zM=HytIG^)rKCzZstq9H2q!t%eB~L2LTx=8%qiE@ns8<`#jya*MRgrp-z<3JMJYpVg zY*!Dj<{WcSO3usv5(IB3GntYOWrv;^%xqMe)D63FQ(T|1 zRfLhcc!y_~LBhna?z9i@7j_2(G~kZDM^DvN3EnQ<1wr5b{==oZGxD(n49SSR2l6cF z7BN`CX(BNaB6gn2EZDnT*4=CMVa~%*Bbc z?CXBW#Z9fp0QeLopP#&Qq%;7QwCK5TI_Vtl1aFMq!?%$%cpB^`UwSq-vltbLIYcwU zX@Fr~dz8d^NE~BW3b!dlhS;D!ID*+FmxRgItKke$P#y!Bdh~7-ZiPrig-H@}Y?X46 zNe)GQWIqI!Ba7%*6OH!xn0chpGeh=XBc8XVciydI?0S^iGj-Q2p6^Jz`p%@pcL9o& z96K36?lOadSVM$Hht*&qzXCf^0&2vS5Hy@f!6o1x2x1@yEgs<5Jd}J24@VFxz%k$w z&KXR>K~e}A$N~#0Br8Q?Z;%`i8kMMl7)0zG&OxTZxgk{=9FrX8zIRd)5az7OSea8M zAv+{aX^q{4dK-3-SV}NgM;}a98;V!2d#4s;&z*!s9nLOr31woA7&6>R#E&E%-34wm zlQ4@qb*>lV+63q-iGo4u!ZTAsQev_5Ck7!74{|O{rT4*! znZ-Bz`fuL8`uxk}2`OmNay+@lR0?}f7jL_a`t@{kD#z(&K0R%Z7kcL7*~e(q(B}QS zceb?GpZx@>4eQU}zWes)|6Lt4Hl}(lhx-p-f4|lK^z>o0?fm)j#gBgU>Ia{`nD=S@ z?VGP2-tU)ZUr%@ELG#l^-@IM>_Tf+e-XH(@pZ&=Xe*D>TSNcA5^gZe`r6?4qb17q* z#a0V0x+vz?Bg1O@^*{M%A09X@iyu!^5)lw`e7)TMy+6JAkAC{_xBuCH^S}G~`;U8q zLNoTKZXw>4*U8O>K0j|T$ti1V9i?R2$W!8|l{H1<*0y{j$NRHJ)8nLv1sfa2L=$uH z7|zNIYZq9bdT7*iK3?mvK;N(X!-IHB%S6M&_C6gw5ST^{6B7aqj?RxYIulhA2~I!Ncx&!#OhGnSyBFG5?_*R7 zSPYOGnnz>D3M|pD+L{oPxhXkL&_ubRHVTAr05P4A;Rz8Kq2X$+lEDM)5w&}F<|zum zFF^;>BgRC_3+RltkuXHrsncxLX*5`+y@y3;G8(K1DcCu}K3wbXpRgV#vC%JUH~^6V z$Z+dIfcy*wsO z?jBqagH#)p;+;j;R$WITi)P?_oIEqrq#-#rbKi4VCYTD5fuxgyGV#rTgdYTt&5rIw zo>&&2vTL?$78SChf(s)E%u0jD$M3$9D-zqN-Zh5iBNoRvjKHrSI43JZZLlBxS&x zrESQ?H%G#yS?$y}qbM4@?ZhDib5cZwVL%5tSWv0Q+-X5~(l8Q0BQn8VDMZ+Ty1|&a zj8Nd2hEZum#j-@dC0PU*E}ERM4|sH=@aUb~l)I4GTbUOWPWwJy!-urdFJPjvVBmA`7FKdU8cTYM{*jPx!k>eHQg;^f7~8EZkBt%SGs)s zF7c@lN-50I_twxdFGZ;P!*#s~B$t~h&rkc63*w~n;l=y6E7%eSrR8ugacRT$raPoO zjmx%Kd;a?H^6u_*|M2w9)5pi}fAjpCfAcS{mj}JMUGDFfk}dH3Jlv2LIvr4$F(xA3bK?VW zSa{xj8z2AW|L3c}{>ZmS)C+yMU=LO~ob&OE)7^jg_vra=|MCC!U;p}@3*-S^(u&9GHU{g zDKl@?tX)?^(&NIYK~ehZBWMilmDr_-iEw}BWe#MlkC9RsS%ye=40j6JSO&RkCxRe` zxhaFJxq^~}P6+{KVD{Bra;N|aR06{*JGxWxwgF4DMr)-$7#^94Bskm=HoUsoi1q|4 z-k!zB7{|ySMn0T4!Yc1gf*66mdo0*{45w!$D$H!GL8xs&Zk*2;V);z-4b2N)y{7ZM zWTMG|ArnbDaNH1Q4`O?QrLaaxhfcBYVML%r!%!=HSoy^-zkWZ)cv_7J;3O2I`AG92 zPg$9hu&9Vos{4k0o9;6whm2~SdoA_ZOetiHI?QZu1Il$h#P>!48y z9<7Nx4Zl`rv;H_br^yF<8eWClbbi%s^C2lJTNlyZh9Sh2uE!NDL_hI$l{6k{`**uB_D-okW5 zg1M4*nlR1mv{kZ{U>peDcgdF0Avz?4y`@C#J$q~Fl#eaX@CyvgGckzZNh8>^1P>zN zU^0;a6r@biK!w9YGYAobsDTT{3a4OoiXb8%5TQuS0Sb>nB;*#fyA6sN5#dBUixSh- zB@oIGkOLYDCk~;IKqQ1yY-EjmCTc-MF}Oc({dsfSeT2toYZbFha$%wr17uJ!mlD}$ zZljs0C~4Lawxq0+J#99|W?{$M!KjrfoeCMIrg|*#*7G99-lmfF5XnSTu$Pybk2pDBIBXwms?^kal?Z)OZo$t=C zUQBnd?7Od?zI!Xvm|{=v3h524!_7yy8XxZS`TneP`S82nef6uq`S|#(=hOQ3{nNL< zd3y8V?s4|#s&e9(*%kkF3!l>_~ zK0bjtr5xV&>sqhtX5W7Plb@E5)_ZUG_^$r$7hgYp_(sqA`j7wOG~Wu2NJe)$fAae9 zJiqH z&42!%|Lyy!!aW>D+^rVeEOI^+@%>babBuL^+jecf?wi9Q)*87J%kKLeOo_X5ztqE| zI_14?4xy5qUBW4-U#itL#pqjxj0%!{j|z@Kje59s>h3a!5+!CDp+{^V z$$8N82*GIK9>K8iydck`H#0Rp1bGnmT0xyg3$N&k?H!sJGIj^=rQULCQPHj_W(T`-UIB_ortIm|nn4dFo978<>1Gs*R!y z17qU=b`Fl(ukYUX^=b@L5{%KgY0f0w=^}SWlTbHEHXwuzeLOwdP;<^|^Km&%DFi0O zCODp?4Kf!^XXeB2tPeR3hm>gh0`E5=kh5Y;OuE|IDvgs6ru*#B4z@RS(MB$Fq2%_*%`zfLETbd4!vMhm=EL-8!|^9w#*2Q z4^K7|4^j8%l(TZMvX^O|zIbuG|KWD|{+qAA^Xt>CS5Gs0>qp508DlQ0snqNAvmogHO({%rHA9+$``#zQF{i`aj;9yxc~4B~n8%)wT!xu=EN8w}uhx2- zC9Upr>)p!vWx){RXFQgy)}LF+soTWZI7J=s&3KkvbI`!V>D4*fmUfFnRv&ES&@f{o z$3)95^+&QnJSAw19!rew;W@8wNe^UeB*kbPOzybUE(h%mlZ2eYx-p|U%!3lBJ9?yv ztdTfD0tONH8kxeuu5ce9*wyn3etw`Dl}73mv|&;gb$3d~uu7x2vr=;_lroD^p2(l2 z+=^VlmHL9b%F{Q4oi@;iXPuIlx1g0?yw0C}#+9QzTg(!SH`}{H2HY$o#db%g^47#AQcD*qnvp(FrhZX zBp@5Bq?xugGK$SH_1+5i&Gjf`W_#ihNjQzMsgp>zt^-Uy6fq#MZISJ{CrwhA$8IJ* zXn-b;<~g=7@m4t(B$rV~B<7JpsHu_%HWL|jWG=he5{SZ~2@JLf9pEXvQ&MmSRi-KY z3O&MYFk{YK75xEh1P3du6A4o|<_G`*N{&uaU{~+~!5{%|gv>zD5~$(A&P<@p)+xlu zXL18E#pY3%Bl-hm5o%_}gn$!~q6+{fXjGdxkw)M5K2*dd&j*4JSN7`wo*C9UQGvOT zu(fSS&eI8a7QF{WQ0W|AJrV__gCn#dN*@;B6w&vMyt8AzUN2ol!Y{@$M$%|RgrHgudHMX+`Tmw?`}nh8 zkN4LZdrw;WZWAvy{Bmi$aQL>rCkJ}^`2Kf~PvhZoSu$?E_`+K!pA^xrPkEZ)IpwK8 ze>k4!B;~q3RE@|bwqeEda@)4&tC3HfA_oVcFk4_bM!VYH`~-`O7!CI^UVXsI)&!J zo$37I_+m+4rgloh#z79pG9ITdKer!$UaxJ`ef#a<^!ee%>+^Kn$NJIq1>|{s-+%c} z|Lb?Zzn;#o7QwrZA5o@hIW0?_Uf&%5{r`1qKmV`(_y6MUnwO$uaM)%;CkYA?#(bC# zWm=R+Z+*AK-21Q=oWlAROJU|xP7!@MvQUBcENSnS3VYPOSxTa$h)P7E`v0Q{&w6!B z(!?~;SL!Qja%o@9zWUFb)5@5p@2JC-ozy=IKFbw#@yikiYB}#0XYKp9$ zYsjpu%rl=oe8XBRBHqE}=fQf-&!6iLf9Pm(-a!faRCVpX2d`bju|A3bGBOf0gY*bw zR4 ztxMmHXpkc;ddzcN7BX7Ur`s&8;QJ2&nSc^oOu1jGgafz;xP|!Eoa^<9=`(Tm6=52% z!=-u}_J`Xt=^{fR@RXebodO2N9zJH2&QY*GIj0y8)K*9-yoHAWLS#%zfNN7E3FgQX z!lDDlS`3tAkGgq6lp{rTA#_ip_suGf1O(8bAZo4z7^{;5j|`g`pv>qCfq1Xxgo(h_ zi-0cRWH^EaP(#@_SOf-M7mqfkqA(rkBl8X&&^sv^GUMjKObihK3V}h45KclE1lg+x zu7Lvq1R#PDxnl4esIY{o0p`RJcHJaEjmjizP=?Twigf@2Mg&UC zZZ%D_NC=V)G^aElj|p2U3P?i0i6vFfJ48sfU`gN_VAsFaPk@*J-~S?+)j; zC%xo6#CVtUZMpsD|NQXf{8#_a|HuFGZ?9??x+N5^O(>9{m>nkm;=t*4xLnR3F3~na z^_-)*8Tm0!ZY?541cj8zKrI)l)v8;%)K#NG$>S7liyp*2W!djY%3*nWk3;7D1lvLZ z>(iN2&dEo{;KC$G4!Ovf0GKUN)EIiVh3O#tv~=ML!CgPU5U9 z6$m~`y{B$)URZ0K5}Z%*RQvg?E%_((uCX923A_8n{o3pr{$%#C`iITV-B-s)jTNG< z1_Zs@b+fj#r^n^fsV;;Ph>B%OQzkRd1){G8zy62se*5OUaw-hiOv(_bOpGb#jM~gm z#GHspFfeh>6PTKe^X#E>rY`BUtkh!OvDPgOlPy<@2&9B1Yg=0PNN$xS1B*AS9n`@9 zA{2wuFpaJ+fA;F-F=2|{%l(@VV%K4u>f`D`{|9K`6ixumsbFm20-=Ez%;-u;L4=5e3=n?=NN4~Q3>i=mHlP6B zMJSU63c9)*Vjv5Edl(W1N`xaYl0{$;BM_kjiV!paXoN(83Rr9PR^}a;Rn#@a<*EsVkPe|r0N9BxY<8Ei$f*yteRbT}_(22b;Fn7FPU zI5)t7^3(Y#?|1bKQLp!p-~Z@~-Dh9?Brf&+!=+(#sQFTFUwj?zb`3-1*l+jm%8oz# z#n1oQCH~?2zt+q7wovL%=5HMygZ^&+H=WiH@Dj#{mJ~xFTeSp{;&V$zkPFEwn0WfR9}T4k*j30 zb=mOad;d)0vQ@iw0IebeVuD=t>e~~RiEN9A!Ml_^lrj!}xpG&)8YqK7Az9a* z;5H<*kZn^^hCr{@!Hf{rFbzFf1LmCrbqra~eS9U$TO@5>@N$JR4B6v)2s4CB^SwAg zMm%rcHB@^GLU#(P(C5-4>h=8b@v%KA;Ixw@$7ezzNL|FVZc*Cl({H|eT&PHk7|8-v zYhkI{8P{%18r{}iN!!+l8A+^OI15zk<$OC0K>%G@f>Q>9)qGIn1VJzi^Dw4|KRg1G znKuKt*ly3&2*CfyB=03FF$fu~uGox5UpKmoa9x*n!%T^3`$LR4h88nUcB?)KJ=nA=$}$NhvT!XP55AGjISds^mfJ)fGrEl+6O& z!poS?G7QjHkO6RnNI;B40Sw516ykslV1PfuK(dH|!aPR64RA&b@H6rOY{L$YJLCod zNEzIW1PB8V7|`bcMXCTPT#0hPf;?b2+6FKYd3Zr`WJkn+goJ2Iz@DfBs$3ca0G6eBb*6*IdOgg#u66Y<|m5ix^ zVdOGBe*eRFzx#DMJ{xG9pS`-ic4yEzBS}VyaC8EPYRiZdPHAhk){AXOIMOJdVVF@2f5)9dY?YW!Tg7bbV}VP(#-XBe~C)Z`XJ0-@N$KfARUB{1+wt=EK8fgU6sT z-|n}E1q`=OZ}QF8_IP{#_22G({`FUX{wKH3zFHq1pOuF{{c0~ac{Msc?Tk)Wyyyn_ z^H+X3`(aKpP(SDDHRqPj_Q(%E{PjQltM{_s?REtcFYj8XG|sepIm3@$;eYjCZl8Yp zum3N4sC6O(WDXuUk7+#!I)@2S17A+P&5`Fl;_tQ%-7HQ4TqP7}L|! zmT2FuXFNzonVCQJCk!M;n{w&ndU~XI!Nbnch;v&c%g*ersI9{=2x*34yTY8RAqb~+ zL!M<02PTgQcgUhwH3;SihSe}3z+m;BB|Ax2Ky63{We5#P;OOk;gU~fOE2r!&^5E-~ z(zzj^Ue{}@?X*@dSv1ejL{}~w3Iztinqa67sb1VgZF39oib@tm+LIDvtA^}78Ght6 z_F;4H+8?cM;We`MXz8KK;_0w~q_`}0nC0X5F+b~Vt=CWWVL6?*v6P%SjdMTuv-0xc zbZz9rIHk@&Ik!Z}gvx=7SZlw1{pQkz#?)IQbr4M;*j7ntE~9$C7$DhoS%+crda2%a zd+7=w2)&m54aatRkIn{g%{<@|uU#2$2GO2xNrKy@YN7C7>fN0ffMy4xvNKjD9laMFa%WVN*h7~%}ES}NTUK4 zMB6GV3U*;H|dfFtE2xh8=c-qCF_LO9&%VXMk%(Gj56w62>;wgqzXQ^0wnlo$sb<`=g&hoAgX zOdnp?YrCjjz`2b#;>|RV#~nZae4aI?7xnZ*Z%s@E)8)D{!fuM2XTM}?-+lZ2b@lIm z^_!P>UxaS+F|C&Ma>5Rdo(4P|b{83(`WQg;)nMS7u|pMBjCa6Z=W7z)pi^> zjmMStPygu~PM6QV`jgN9$=8SJpOjZ5Z7G@X@BfDFxEpyq5F03tJ=UB??d>|>S$}Vr zbbj;kxBu_|KrKzrbJ@+O$8Jbvyeh!%-BSd)rJ-Fj=9vln0}JkBMZmk4oW+EfQ3 zm-X82lD5S%S1U0ckJoq3gFH3AB?=~YQkBAhX>=uWun9=BRwf4IDP(^ZyTrrqR+ zV=MU-@e0p3)~i`8Fgb5rs$MMN#QNfQJpd?lc`n+86*+3ZZwrKr?yt{Xnn~;&IzI7 zW_mX5O1(y!%C;$`pd=Q9g|^d@=c8kzTxQUvgI6aGb*^jgmundI7!zJ*zG?ir9FouD zq}R~KGDD7pW=2@coIn%dr8VCujX**gNXgY*IiV52b-_GR(s1_Z z5?w6?hSI@$By5s6${AOrg5l92&10tt*jrT_xS6z(1&iV;W*)*%8B9RiR8 zEb!oTBCQbuv4syP68;EsgHat7I6*WBiYOimAOscw5DCx#6Z%8Y2o?YVu>jo=nTH_| zpaCc)GcST^1OUeFRx~sZb7}09IM$k-X#%V=GOY&Q8wt8M6!gsk%Z?=i6OmF3BP2>C z7ivj_p*-ICO); z!o5QtM>IRF0COqBwqC|(&oGyK;MV={GY9gzdxq) zrltIT=7uH1ZKSXbbO^pjb;dgTD(OSug?vmSBePKmr+5k*5jS#jYr+qab-jOe0 z#KY{dA&@?;Td(E_-b_mC7hv61q#mUZP*1_*NRcHKh9v`Hu-UzLv+ePR^L_X*PuXw^ zm01cPdXmGyq66-dhcNGC*x!Jw`T$(rw!rec*B?Hd*46!X%Czamq;0*)BSFVtoWw6Z z0Y!$Phek-yL{?l{mV#FDK(L2WG1Xx|C&q@GvXrg2G~~%O8Fxszw6)r{g@7D!ev#ID z8+W>%M5rs5fx4k9^5TA7EA3Np@S=IQEA7(fW7Ma_40a73UAM?Y6_~RXGb83OR_xpn z)6L{+cwIwJP$D$JwnR=E1M(x%?7brZuoJQcAwBwXgkeQ-Fpp~unn|b!m>W9=^=L{q zf+(;uNsr9zJA~>cK89}2s*)KK_73Ar;OKQ}#f0kSfEn}@o*jqPaU>UD<^_jmC>vt| zFwaEJ5eS6nTgVYn9TR{ec8bx#fzUw_9RdS@!`;sTfN@3>M?+L2K+K?9@BrR{C`ibe z06ikG1B~da2O%&*15U^eK0wr<9N`!lVM7cC4zh4W%CH!*iX{gCZ#L#qH`ls)Xe!xU zq!hQUVA-|?DRiY|7Q#xoc(0h+s(6d5fL5fzun;a} zf^`7d7?&82ff2c20|*9Mk=5Ji*ulut0i&uBgn~zSkKnW|W}B)u9BO}Bg%>Bw5)FFu z!;6=9ub!1>`}Y0cUVb>mY=Ic&Vy{SpiM?1{(raf6}W-F(^VC!~Ehi{RcW zjU^#(@K6p%e0_o4PSe2C%W-~nxck!|t*3AQ^8fZPzx%i(%7Br* zQFBU>DClxcnwDw8u1r(|`h(Ve?8o`>5;b~wvgI-s$el$n2{n|#-6BdF(E5fpM+Yfl zDvZ85P|ujHt8=I~Q$WC&4z)k&rAg4{H43J#JaK2w=xv2EMWpUFAt&ezsaAKdk-^QB zg}NMNeS@_^xeIs68tqCsdyvy0dNB~s1N0ihj@p^I08M5OK{=R0-8>*fFzs({s2d$h zdVJbu*j*n1i*BoTUqY<*{}@N8FlUwul;C5DjWji}OKmYRZB znlJBv_~zG_OY1qXUz?{fyzb}GwHF@Pqnuj}+_2?)Y)CQoVaS=&wromqJC>aD_4*MX z!8Y8UK0W{J7BUkPWHdmBmYb?)y{;R;hLn5PN+5|w92#t}hGfo6kiy(N^`+5TbDEi9 zC}QK@u@Cz_cEoVsA$27(<{F_M*>P|eo6Pz?(v<+6JZcc3K_VWgK0(P5U4ptY=M(lD zU}MY{T~rh1m0cPu4dACfy&zR@R@knvH(0ZBCmg2G2<_ew%X+=$D3MfyNXyv4m<%gN zhEPT6p*aC{>|-vQuTO3{12CLAv9B8(GdaMz;4=Y0w1%(?6a;`6LJ%nfPj~-IU1y*ousW09;?QX+$xUfYi0riM!ZBv2;y7; z2y53vArltTEyj^Wqvw6L7JsBfSR)$>aKr6) zu#mDR03QREaOlB=n?o^L0SBgqVVCfUR$}M7XX*1_{`u`sU&Rl6qKnwhp^v9+2~4 zGVJqVxCjPE2wg6ZoKlTCKRaBuGuE`hip03Z3)uywae=a1jK`tv^<_v86K?0R0;XtG`&F1Dmk?=Qn1ZYIg&&L_qJ zZ)h#L9rLGsAIgK_f86~4{5T(9;Bc4VtaUQ1+Sk7Jhu{21KVS0g#1L=3djj8w>&<63 z!!N(s|Fb_q?7#ju|NUS771FRzGOtvOrEk$-a zBGbnUB^39LC_RBJO$uGNr*jOGG$6q%55qW3k8X_4JjSwZIiZkm3WJJS@L&-w3G5i_ zwPK!~wuep!A#vF@n+_?u=ghIh+C5)HN@znFz&xGrgGYyrg4nj;T_7>AyMZ-zN)WiN zG0eCvm>oo-8{ptFkX?dAY(q?)hnriuHNsSEejeAgPPv=;rEK1S0&HRl2;C0{=Ijc2 zB-@zWSc` zZAD&XKLe=Tjd1?xR0dMwluCCJ{KAPYt2tt}V~UL)m7h_JMBsF$mS z^dZ~;ym>VYC$pSD3ZWS~SL-Rm7%GYNsqS)dBs!fUGR({vJSM(Yg21tK>)T>Nh~|NT zYSorBO&*TAv}IiC_H0>FDU{Mm6)KE}Jq-n+l0P$B&@O?}^QZx!#JFIel3yJNQ0cOS z?ZP7{V>|Ucadfc>#G++|%#2|U0MU&S85?v^WbBWGgw_l!(&(`i-!y{r3BBNPU5WI*j~nKN7+cLdf)q`=S)&|5E(7If4=iX>z{WQdNb(1h3&%8X?IVUiBK z2Tq6`$RZHE0Rg}eaS8aN1Yv=~ZjKHC5CLrNg3uyHVh%9i0)&7D#(<6x$P#YhYfuVM zM5aK9K-35X0x$zf)XBXCARrTLphPU-0jS_YK(e$B06b0PO=H4AkhO!P9E?udToNJV z9k!;_D`3gkmNbsUo-1=s<355y1HBh8aSmw01LFpHG$T?%C{PTcgY6Kmp`!~xMyd_V z-q%K;0SJbi7MB~83n(TDmn!|J+s81W)`hIW9{hvLEy@az12I-gOyhib{*%w2{TDyS z<(oI(-G4ZLjII09Q%>$43~Ao)p3M@`G@U=(U+>epziQuBH}j{Kd`!o}@c!)w4cbcZ zF41Z=E^PB*7}vJ-`zP1vA4sx+L>c#OS(-Z~afVmVe)0Xo?{XW0y|$Hx10RO{?z!YW z**#M-0GVzLp=tG+y4}w?_WRE_Io!-;+G$^OrL80;Ib1)!9oGwVeg5JXr{$@|<$C>e zyZb8d_WPU9ZwCGB`ORm$%=M%JHcbAXP566FYbNClu=G*f2um6X?_-{YdN}-z!J)6pSpzG4Y^HAIr z0A*k7=_2NmSgqMstl2nl-TZnzAAuH#gP=?^Y?nw@J*Il$-DIs9W8G?Kp)qr2gn)=7 z;KN9^fGf_&cIt49lwx~Y38-E_7Ah^wBhr`xyqPy2;aW5I2=Wlj=v%kL0k@AHK-gGD z%f=nunwFAH(H)HaIC@(n#4Q4i&aN{-9)KE|dmacXC<6__CmcM*TmsRyjOW&~A8F9z z20qgo7}gip@RTG7B{H!K0jDUb;9w4tlb6{H83Tv_3YMi|iU=%6Fy`o?g)-(;APrn7 z(lJ9vMjkS7HgEpJ59VN!Iu#!PIbmO=jIc|23x^eqfl{=q+4%AC`(OWX={96Y#@M{& zY1Y~iXY!T?A8>!^1~3RAQcYZlvP6Z#I*ep11(Mmim?)W;4EsX3s6igr+SARm@C71) zq>TI&$T`NOG4bwjgn|$E=kA{KfMjG1a6sY!Hw(~!857EGdE(u(JkRiMBOBbugy-l* z$H|*Q9wQChH!uodpdLz{f>0xza`Qz(%{hP_ELhOgy)Y^P1twMmp_C@xHbWj9o3_R{ zcE+-K&zUjDZas~* zp386$I+1{IjEI&B zZA&!jJ#UKuPvvI3{p!!|e)-whe{=tjpFVtiy`9fqsX&0R$dCxe-ODjb><(&M|8QM% z;aayP(ma=OKlr)qrsez*7^gHc=WT^^eN4IR_s_P=bzM(oyhTad>6`|}+Q%bgFq z$NLZe_W%C3KYZ+j3CE0p)7`waEpSRgxGjxx7mKAw&z9>jO_f@YN&P&KBO;k;29eRC z8!FJi1JOmWqdwDsnvsr@3U0UG)@jcxOAD5 z)t~{8u(@P|V1!2E2>>JMlLI&fU?vYz3&;+_t11PBLD@mAZW-BQpfGmI?3ai_t3^p@ zn`4I5$Q7FFu$!r=SLh?GXJIe{vJ7`bpUI91e3|4~+r$BTX?%YmLzJO{7 zjfrsD!T8d=iXnJMnQ_cPk}Mlvr3|!x4$=JXF7-2}KrXi2(-Zk_4~roz?vd2{s4AIh zDte^?7+W8v*xE+vvb_J*-@N}AwE8CAF{VtpZ5vYzT~=s=A0maVjUw4#(ijuDb|P{h zBrGW7?CS^BUTvJn5jk{9NFdjHpN?bEb|nN%-s&VH4cqncYP|2ZJbc){d>IbHTqpsv zS0!tk<;``y-`Y=%FwYad8RmWMh)OvFI7u~RVIoyTZ#V$BB_h^{=;1&>oSGt2vc*vy z1_7C9eSnhGGjiAE%wr})^p-e_ZcUHHYBy(zjw9j5nw*9E$!UzfDk5iOJ-N#tIivuG zUWEc}g`_|`HpDszo>T_ePk6O#W(+xJSdpVTFzOj2aQzT>BgW_!4iHvxm;fesK{QGh zcmr^TWZ-TwB73NiV5o%`qy`p15deS#_$hDz4*+-Qo{!M`rg>xoN)bc&85ofg5J0$x zATnYPWbi%G3O+(~^d5{5>X8r}BhVbEB4RjFbO0A527qpY$!Tz{ktzJrV8{JxDU-pj zM};7)psoldJSzx&#a(Qt{pxD@+4*_3*GjGEeNAAvLE7CD0lm4O#8(Sib&h_vc^b^Y^FU z+`s$qULQACXGVu&2FWphc_`14PG!jb^v&A_UbeLnnnjLI0~Q@r-F>MYZjOcJwk@w4 zaoNpE+xoOFYmE$ieWVzyJ7P@p7^hvtr&t%$(!7>&XV-0`Hs+La$@>Gmf2T`hL1Fpf z!`qxkM}oQVJV%ltPid^$*Sg;OwXVwrIi-1bd3t>R?#Y|U7>8FMZ=e6Nz5MC#K77j< zcP~HNzuF)BwnH6xPH@JQ;p!x$$)jbv^oJiT|KqRe=FU8p+OKckKfU|#>DzB3Powzd zDtyS(K7IC+7v;}~DqqKT{oDWkfBd`eN#+qMkzn-drb8xZ)*w<&f{qo7;EHOR&bHl5 z`|ENU1Xt}MERLZr+=GRjcG*uIVt1&^g~kE1p?kaZ0tD0wGReq-Bm)8@wwPYl?J>;j zhFiic5H6CjnK0V9!cKAmbBd0_keaUo6Y1hmBmrm;5TJ(d0&C+zR)z7ZF{1!=^=-2` zp{|g`5h=(^AflrH0D^fiInL!rc`5(T4-1IX%3?VWh90M_u#e1~*twC_t~p z&!MD|&^4rFA2-Kj<_RiE!hWeZqn@{hE#l$(_pcwZc4X$D!~|}AYQK__v(mv#j#}RPv^+9NY=+%uNO!I-W~F~ zgn}0Y*I~%A+WmQ%u%=;`Br#)2BZRu_C<_%VnUf%xR~#}lFf7$6B;t4%rAcIDh)Xdu1r{@?g`bL<>IW*7++$G_rw4;8ugoZ-U37Shzwvgk6_hYvh zO7wv6&g=xmwgAMQIK0i7d1Svxziu%os|*M34KL?v0YnboObB{YAr5Gq&><44bKf8r z6r+qtfMSRqGQ!NTHSi%202qWp4FJ&^pagjYk)#M_4=01TL0Az7LqbU0n?sD5BLFa? zXv9bW5CNcG5;P192xegCH6nX7q=*25X3m5)A`u`U0uW+HaDXk~KmDE0wrJaietfP=jH{1ECIsq$E^i(OZ6+8NJ49AMzu+qF5Fn|@Mn-q(8m@Vb5T`tqOtDmC681(!=1 zhUc&H?Tf>!|HIwQJjmYffBV<}^I!a-n&GB-mxmtIx+#WXxDul^?VD57df6y-ho;pZ z9=3vD+Xg{L&gfxo-qlH$OKoIK*gt)QzDZarnNkep3GsrvjX$gM#Y)fbGxhPt(sA>fo(@l3t}-;zv*bml zOtGP_5UaPv+kzZM%htL@844%8xD*RPzg#!7*3ak5fBOCP{Z%cTdJk>Bxsh>Knf6_s zpmQ-!(obEh_S&LX>U}qdgqb?IL2pl&y0+GOmFNRQC!FU35ZPN^R+>3ujmiqt*H%xD zaR0>AZgxXIZ_&>(6hmfk<%Gm;o-DjWuPQtmRt7FHJ3DxXF&PHAY6zkM4uJ>vE23c% z>(*mn1ctMwBa2hsBF#-%slhO6ucQcB!Q7mvZM-X&@iU7cf5Ef+U5wQUT1|gaU5H=7bP{0b|;0szs05k_ZQ}i|Z8p;4w)zKQb z(ZHy{p@}V(;?mZ~D--r@p_HMnAPj3yg_CXJkP*SU^h#DqQie*7;lQDw?twrR5Y3!T z32%Z5B#odDlQ+Kotxb_R$D9>@Lj8TT{M;pzJi+w$a>lL1|H70EU0!@CD-UALj# zuRwhJlb;`c@~T}fqw}JtYMa^$p2E;A!c17o^jQ)f(kO(ub#m$M>9CWO*6os~38o`) zm`BWg16J zJEsKE!`!#Z>!Yj}T`Ph?+hBCJx~yw$+Pe4Nx9dvYm+;oBg|)UpKo~SH;it-Ow1?b7 z60cA045kplT-6f6M#zRkM6ihd^8+ zMFc`XN2+kBwJW%d6jSlC(}3^-TkDV^PO=8BXV#6hLJ3l(#|Ql88~Xm8JZN~c>sxyN z8s2`y?>_ji|Hkhh?T7dF=IWnb`-g{ark@^L6Kxn%u~J}snW&=>?{C8cqHj1>-mSy@ z&G#SQK1~)#IiZX=NJ9fJm1?wJsCZaJ0)!+1NXdhYD1{TbNM;)|@5-=tqj44o&`g76 zOjLG^(yK-0Vw!MBx(2RX2Ef9Jr`<@SY$vlwd6#w&IZqh~nt77$;66xFNE>xi+Whtq z4IwPM2^r*WxNqd3-AKd>GB^~IJqi;z;6a*UhX7=0Fb+-$$%IX9fyP8lLOH-7fR9nP zYE)<>Y(iw;h86yyh7$XX}Kqy265r6;=z>XG(g2+IE zG6Lj42h1jOq=JAz5J(saFaR@QCQxD|sK6MK2v&dyKyU}_fPladSHO3djLj!davlL`3FL?aB(^ZC%4!J2$4Sdv{ER7Lsc3=3dQxo(dt289i_; z&P;k%C);{(L+4x&Jd`{vfWh-@{e7$!mI7DQgZe4ZLyB!f(=!xA7|k3ZiE$X?A_Rak zlBZzA>8Hw{Z_D2Ejo#h%-7)v&;oDE&{`Fsd{Qe<&D3|~@EQ@hZBMws@Z;4Q*<8^zz zT!MH*gt{)^R#j%Nu_?eY;AGOb3q1HZ6LV6xx4%9 z;JTczPvcmS2aJBbe8g2^0fe08B-u;)qys;MabF=mHjacdHDL- z^wVF+*Uvif^6=Z=|Ih#V<9BU%aW@Y-5f))l z1q{Z6V05(>$*^0sB`0UUSkt~4VzoZ z2^ecgkmP6pK#knm=2}4`M$lzbOkRzwsj9J}WH5Aq@GUwv?Hfk)>gz(34FL@W7R);% zG7~6`@=VlXnrNCSiSq~wSX)e~|lk@WK4;nIA6xgz%eB{14yR_n+Vo_wk#Het-4%%{n%#G!-4#3k5w6gOG}= zV_PCq1SW{~;q7mKw>)kai%#LKJ`zL%;!w9h=+lh=psxYGK?`65HpFh~uwLW3DfrD@ zZV=tQuGekdl&n5&Nt6>Lb1ImISvb*N`iLtT9!nkpilm^+r)@s&3~C{9(#+Mzp;1S( z$O43yP5iQWE}?ZI_cH4iP-bBYnT=A8h~h{&U<>U5f%u4?SUliLvG^=uIeHUcB%y?Z zsSJ$Bk+5^Ia3uFufixFy-hhw?45e;{*-->Y!4yE!TreWRk?IOs4Et&n!~=I=4l`Ok zGGPT2F$$t0`s)lm`fCGR>!$X7v7Q{djlmikVWrzZZKo}xG6=4CnL7<3^ z0f<0BfP$JJ41hs9QTIR$LpU)CP#`LL3vdhr2E+|OA;!QQj_g-}A)+FNLqrfoCx65q z*ejX>LI{MnFeL9`XLAU3)GHuS3^|6Z0MX1<$yjUn(gKS{LKpywjRTa4B|}|(Q&SkS zbj}nOFi>yNi5z=xP_w&0&M7t2nWAn@5gJEj`e_(bZEIir@#*rzum0xUZ~n84GJSsg{EIL1&24KRr{mXOKL3o+rU`M?yH}_d=+K*di*gyelxsYAOHG4{M?{R7207gzJr;>WBR+R&BEM>})3qUX; zuy|akqBx7MV9nI5^|gfnN7v$z#*71-r`o!~^*9K6T(1i)Xr9?~xGfC<50uCKyhF)4 zWwWt$tLJSXK}@z<>FQ^Vz9e-i2okDS$AXRhd<}pIt4lRU4@NZYRjr_T2kAJ~ytA!A zNe*H_)K?&O&l@b}43(k^Xb)(b^EDNf41sKj*i}Y_z{(ih8-Oc01QT>-3rg-&^5dS; z)pJoc?;hvH*K>$?Jht5p6mtx;1yVC(*AyGm(-KRh)o|r_0(;!~ef5gqgI_hCioI8O z-TeK@J}q`pfBU+g7gx8e;Z@KEG>t7;1F%r9Hd^%Sdj96O@9xi)=1Cb_-3XFPpt~_J zQwHtVOAYAc-FTx&NkDQpFL@xz772Po8p)Q-Lk0@Kk+TdF0z;=@&@NP4)tY7C`35`r zb#sd{prq_ZOFbjscyFzCWFAWA0eGTFBU1Q2~nZfPkbhVBai>#Hy_Xd883} z#sbm|5ImmHAgCjD!8#=9ZKP!2ypjY&6aqww3E;tqT)^yPPHh8_6ugFZxdLKED%_2t z2MF*6=)nZYKq;akWb{OVfSIThIv@c$MtAK%T;?Fq2ClxIdEbiO@-mv!?`r#s+cpX$nb!5~X-d(k{bxe|&y^_2s9V{O$nT z@`t~_|M32F|8ZM=>l=gMLFfB3e&dqiEZar40B>3C?%^?a^a z+vD}(exflSzWAJbJ-E2rbhST}QPk#3N`IqO@ zY4JKgyKMvP$GdSq8%6uH_@g#@y#3|ZpFRIPTD6X+rw7T-JKRsNHiZwbUw`wrf2nv% zH@8!nz;OTa`IkTc7vykv`}siQ;q#xT-C=z8d6A$7`p>t#$>C4GUionE{QB>{`B#6@mW3FJ zhr-?ZdP3cT1V{`O?2oEz&t-@)VI)kG5n^^t2R!Vd48{dZN~JUwDnN+nL{x~XPEi^#2OVsGoB>qY|9F@WUuPVR2$ZZ}iWy2V5=rq#4EwzajjdqYLKxG%$` z&J6~Yv73cQ2X`ZiYv|^XU~AM@1~3bxj#KF=n7SE*C*PGA_T(1PoezB4csT=&Zkq*Y zLu(AZ_bh;lG(ZqAlDo478p=#O2(3s2LSXLL6=N0BxTto=%{Y7j&#B^~-sAK;^$v}p zgY^`rOI*~h{I2rO(XRWO*<}EI<+5PLc z|M(GC8P>HjW2nZpXVX@k_#%P-oVzMU#@VNWS*^SsG-@Y5twvs0tq33Y31vL z%##8p=|WBf4Ui*9gp^CS@U%mmAez&WeGOy>lK|4Nrf3=)*tY75fQSHyo|z*oB}iTa z_aqBBhlOe)?oN}$l}rK^+r4)R0;DXS)7TtRcu9bS6|zL-h78md!(w&>Br=sOD5)S3 z;@F0wEKZC`6f$5YTonWW!VM*%%uJa?5E&!^cu*xmzyxjqM3jIaFe3qwBLoDXH|!BO zLX^M>g9*2QIS?@v(TM_xkw>5zFaQl9BRW6>Q;cq=;pU(b0qmg)U}z`k&5;1zfhasY z0vzKhkcp58NdW^}%A_5oKr$z^0`6$m9grmRw&@mx0!E6|m;rjYZ#KEOZEfiWQ8$fd zwFhW~g$2P1K)D2y!;rGB_VoMHZ{F1997{*) zQnC+s*Y%-Y>M3;VQLDZF!>{vh7(P2dU)pusK72fVTHD&qDsc90fB@|Cyeo9ae6yV2 zo!2;1l}V5Dt3f8XY@vNZy8HUee4O<0)9LMfk8=6=QKtUlM?bmCH$qtwTVtUz-t1Dy z^#QM^`stf*1D5ILv+4QG;W#||C$G}l_n*Dm|KiW)FFzYz&WG`)ogeh{ah&r!$nA{N z?(V7jLI2-~6wa z6(=7{o(9Q9cH=Zoe0(;HrR?R8kU5PbaBy$bZ76~0DWOvW?4uwPRs}adUu(PeK$wFl zxrk*@y|nXv%eGQq9BnnC#EB!1f*A}Mh{J%!!l_drcPCRG73KuQv{he`(UtQGxb=2_ zRW&2qf?ZiUgkm(IZS4-#0E2pHkJQk*E1(nTrj${;HVa}f3#(zZ7YP;6#)l)ti0$Og zT|4F!b#Wy&g}_8eSbzcCfd#@5i-4h*gh=8j>}hfg&9}XI53?GT#oDqBiM{x4*Bklr z*g?ZL?T?kA>qfQlcJ+>MU7~eQfqk{Mfqm+W*{4sf)t(CMMRx~3Oyp}=A@5P8S8?sC z>C);`9}J~yl<`oJP(hj5Aq)XX0`fpY-T@PEYc7b{tOJm@GEb(R7>OOiLj(n6%4mq- z%)M}~{bZ_$U`f=oM4*~wEXQYq41}>R34FtVaW^H0wl)Mv;}`@>?V)Y6F2`M96VRa> zXXaW7S;q)RgJd)XiiThcpx{!S7KbCk>Nz_?AgE>|BGX!(rAm;4lM69p1G23!LOX#J zh?B(LsB?kv?yX}k0Y~)}A`Ewms%7o~%F(<4NFXmTHBZd06cHFP28}FER3omS9UZJ* z2>^V-+GDMNrH7l>aO|!uj65c6A_`oC8W08*#0*Fn6oC-|Xb}hw=!j4O3U~uxkU4-L z5g-6{N(2M|0qBGt4WhK(%prO+cUA~hCZl8l30#RgD1o#fWMJeA^vsE1SOHdIBv9xl z%Gm2;thHUNzPs;hgYDGTmAkI0BA%y0Aea*e)P(c2JAA$0-Fz|H!|D6$19I;H3^}vP zGmfBCbjXsVOb4&+;(mDfRhdRwJ7rwLdf%?Sp`r)KkhDgAe%Q^2`Q}!6-PT2&x8>9Q z(>oTuz4@}Ax{|`w_P0A!womuVmQ?xt_OgWU{`ALpUwySZet|~&!^`2tJl#&yG3};t z`OwzK`n&)2f9m-D`Kw=)jp^_IBaED19dCa8`Lv^9qVYJlPp9jL`?h?Lj&gG|{b=0U zr%Pk&Y|iY#>(eP;P9J66XnB9yr~S)kGEVdUr++fp+lZeI|7<>e`_0?GyZpmn{rzwL z&|7ynHxcHxZI8+u?;@38=#eytVQc}ygPJ!+ zb^2h%#Uf$pyAg zXlO8a53k-kL2&fedyB?k)nW@0%mS)iNOJdZvVfe43l1fR5&{ZAV&~Gq{IXgzt96wW z>sblS6!4SA+19mh=T6bVnumE)Xhv64vcB}**6pI}ht)5A7!X&?7ot}5N~VGCwMT@} z=48uOU%!2Ke)IHnt)VT}g~JFPbjvd4!jx2U#tdsKgE$O5jpOcsh`Fa!@<^=$CZq|R zx(BgE8R@z(0T2X%3N|3kU{=S-iJ_+@h63$u;0@K$sBc+yqzJt-=Q&|mpeY@0hFn03 zHN!Hwt;W$MWA2Tm#T7LxD=Gp$dajNIAOJcHq>ca=(+C(5ESj5i0;O@t#2%!Mlmr06 zgT+jWfj|+B49*=9%^^HoDHUIqfR)ogNQ9^rQCp&J9tbJC2L-V~T>L75j@US=wTo^w z%yl)#3_$_Wu^49{%Q2gtb64Pe_I+gbtAaI4F=Dpa*tvK)8f8^a$@@OPHed=n#EHTR|j9ruy!|O%ny{hG7H_2+17U3A9598M}euKp6~;F{3&NxF3}Z`4K5RAXLDcHw0y} z(d`m)h3wFbh6F4gCq>8p-nXj|dDAI$NU*H|;mt#X#b7+_#xm^6_3?La9v-%+7aAbA zapAO+1#EeER0q5Ic0QTWVYe?gDbMK8>(jNih$444GY!mzJKG>_0`XQarH&3s>zyJ0h|Ef)!fAOR5>u33Rmvdq`dp+e?reS;b?B~n*@x!nGaPmGCzbQRE ze7scOc0aK^^>KX4U-rw+rhUjfK>hkpo=smJKl{a>?7nz&ese?ryPx*0{Qf_@`H%mf zfB28T!fi;oWFCtGXu`D1IhZi0Qz}KK9F%A`2?l}<`xjWoOCaVk5C9WfW>{s+m@~0# zSY$fJn9^YuG|Kthh=4IICd_5r zO(J8SN)O;HeV8_qrt_lX;uM|KAv*OHtZ{QhaD=c90CVff(Sv5w9+je#N^vRx44k@F z(3MQpu7R`rl~^E03=@NT0wciMomnlI+`R+=q2x%dSH@!TM*xyMI0#{y1zniHsuQR{ z<;m)`FKy9l<=Sjn+POM*d+hqi@u>Z*e(g`c)5{Or>EUwzVY_~6y7sGg$vW-esyY&D`3f#6Wl&YmSzxn$&uj?k-Q?5wOR^qukIKXyZ!fR5G<%-Sxy0)h^&f!Uu zrzN_Wm*T1&%u~*#U|%({U;|?&U4jTow#ack2}8;$7{LrG0<3zLY;C_EwuRh#xYs;b z+jvT8WG@uf?cv@c_!*8N34)WP?%-C%9c0J>Nt_J=2q-%e$H-fNBMu2gwXXrl zMUw*d3EHU-DHze66o8S?X+)%O-v|U^b&;q&u((=I19V6kDYu|x(HbZN8Y)=l3@Ei> z4%p1WQqj^xaWmK;Tb|XR!>p84X^W;trd!`uh-g}ZMssZ>dVOTqK#Kv969^)x33z5; zMqh(Rq682?0!RTKj^Ki^0w5v*B?tv1a1TlmfyCf5F(EKA0W(Gi4+2D2hzB>t*xc>` z*ux;WV+&|u7*UWKppaG~@<@&ikN^gd(OrWRLI48+5`OjyKYP^)BjeT(b?fF0#E==u z=bapL3+@BE%niH;d6ModIb&h~rAbO^&|9q6rWgcM z<%mmUX8?vP`UZM+WK;q#kPD&*Fn}Q;YDe{@am9nUz}V2_*%E+lLS)lfjKzUXxH`O>1ny1R~&@OSTfyByKxwpKxyo)w@q7X zrWT&sx^3t4bz9~^ru}Wm`Fj1RZo@Qz_Vv21dd|3Ol+9Zr%(uJ5hGQ;wx3Tr*;txN( zIlq0#KicnK-0VKv<^4c3h1Og~pmpAj!myY9+rNK&`wxG=z?<^Z&kld~F9>yee7wH? zy*<7y`QiAJpG<^*`&Yku^UzNpPM2RV+o#hi<+^_^fS7mn?s=VdG9HAlH+b{qkCtco z@Zy*6*8hBd{=fgPhOvHn_uFs({lnk9gT*Nv!6CvZA`~&DP!$6iBnd|C;8nEe<0y&a z{sFp4tG=$poU+N#h*DVXx(ETlQtKsUIjv8iDUsSsOttWAw_Tej%6HVwTl7jL0p?(-Cd5!t9*dv9w~7S-km zshz8K0J81O9j!XJ#kPh;0;g70k-7#r&}i*sRd`cFGn#m5AvZ3Hh#r&l4qGQ8u7DMebH^{?eb|`*KIk~>*MzLre5yXWm~r8 zv|L)ia_*bebw1X4mtH8?9tT$DPU`hEeX^;B64q#W|kN;P17zKk@-A@ zj97aI6^)L`K+1MK73|tV$z;r47dtif;Du8FeK1l6oBvGQZ5RfI*EG%@H{f=G((dfiJ5Q}$BZrOM#Y-4L&5-4?Y_D} z>W|LY6Cz8m?CFsRF)P9jF(CwS0r$WR;OGI7ITR>4vO^ARjvg3*5&*!CEEEHh18cw^ zVMHf{z>MIa2GPMXySawPXnF+?2qZ8B0tA57AtAWS5H3ssm=FXIFbHIif+mMxLMjy3 zC9Vqv22w6llnI0xl2+9QX3O<@4PbAI)=49c0%0Ve=&2MLiF=p}1yM2rfVBYxM++qD z7lj#Gv#1&c-J~zbglWU>?gM*vA3AN83h)dn0b4{xSb~VHVO#_n^v)GfujrN8ID*%E z31sz<&07$LDTKD_gAbHAq>z+(x6AA64_5=CdRqLloUoYyp=e-u zeKPO3UA%84F@Q)=@eU>NxU<|j4dVps>+92Z-?sAw>!n}&y2>BE`{sl?%Z`Pf{p`=j z+Z{;bJe0Y#^GEd99+uO)_b|%*?58xa`fkOmqT{Nd=W!Qv~E%c8U_dAZ6mGy ze71h=_aC_G`fzXON3FmZ?bE$odz>#M(&`mjKv)pXdQ4E zQe+yFoO|8eGN`LtRdZBkSMRzJ<2d99tWPI5kL%i7&up#grPT$ZAv?L-x@@h3$GQ-9 zD;92O+L~H~ZmU7pMVUd2fTWEM+6)l1dvr4hWaO~O0qSZNEWY_PxeG0iF_dtN4u)e3 zR0M;;Q|{S)JW6oDK$Jj|IR-c&N3Eplf!Q+FjRo={(R9?q)KfXo2r5$dY5qU$?RNO;X zDxPr&ghWl0MSuYU=T6|zAtvvs6Qd<0&0T>|@ z2tgpX2u6qi0wN@W$Yd)(3ib#FL--@?;4Q2JsCx~N2nWo82?Br$fI=qE1w{Y_a3Ej_ zARHNi0*Hb7cIn$i&0JH&>KQR04M0JZs43{Ct#@-KDN^n z=;ob?EsuLHgfx{Ml{w|((XjST!5R@DBe%*p!I(&hIj3KI;VRAGVyS{N(&zIKfVBn# zt<@#a*XSD%%GEU(*_;Yc$LQfP_8LurL{u}3Lrgco*@Am>88XWtgJ@$=hQ@2t=3uU< zjiC!uFB!0hHLtL0hhCiq36G`#K`A3rC;&KR2@HgeEy6Hla0^622xN50Kqnn0k4mMx zh)dYkphd5%20N!9LrhRD9E=ZIl0^XSfTx&ma2_&bnWTjCy!U}16(g*}-1D(}re`Bh zBi!8NeU{-kz8vM%FUpH~f16~Otz9sWjOZ1-Tko#j;kwpV74p09?tlFh#H1D;of#ym z+Y*rFFoA^d5Dwh5Lr<9~r7SJju3H$KpFNuz2D{53=^zFwC}U!F8VjGTF_!LeK3!%hLQX@Ao!qt-bg6o#w`kE}Lq}Dv_cj8XDN1 z`tLO0kp~(XwxNjzwgCY&42u9IQe?4;tn4!*BjPsaoUhq?t@SoeKaY!}Fd7kvEm^Vg zav_}1?$oV-c;0pHqLlJTm5CXlB?V62oa>UdWT8|JZbVq6A+gXsN);3;fR#LoN<`{N zUDCV9QibP_BAiZz?iL%yoz0a(GLQqiCsE7}fQZe~=WPcMPR(F+lBR4VAZS8@6)50H zV!}vtP)ibOM#{;=xTCBDLS&L=a-vWkfB;FD6i)sIRKx|I$b0ghNt6V5QX~kH$w~wg z0}Is(E~G2xBs9(-4kCjPDI+@=M2iG5m4(W}(v-|oXcBWx(JFF3Ms(P=I%}E)aZnAX z%!-`6thxY8D~2KsLb08yD#^k^oXNRFu-$GU$Fwb|r!jKMaBu~Y_%oTqs@Eh1BB%wW zBi^#_+^WO)=uAB#lBNhd-yxNVm+$=yA4K@{)i(s(e*JxaoqYE5x6eQO<}YXZh~s#DmFKc-%LKOeq{)Q! z{P_Oi@r&*8>9j0=@zX#5=CA*@JkFP2{QCMb$LCK~xUR?YlZPx{efQ(?>GS!kcYn!7 zF}w5flBbV9yjl0xW7(&l`R%cxknDfFzu7kr|E%o)IKKKn{MY9G>;K#T_y7KX`S^P~ zn{h4Bbz#D?YI!;@UvAs7z=1_6Q#fNGl7;-p&IyYNM8$3A;Bs2CEL4PCYD_abUhZ_5 z%+BAvQop~A7&!_6p=TSaavNU*lZNv0cC^0o=xsXP)BXJqjj zlVam#)Ad}qwe#cY@!NV@+QZ{%J)a+Vd;hf7?cwqH;aMKuJ)VF1&2p;eQ=H}jLLM(T zkP&(CxbF$~T4Onznn&C3```V-jwqysIq!yCT9(?Ai;Q_(r8EI1f~uCX*3(wkMcO%Y zO+zfFqqT6%vRY;wzGt>$(T8MPpS=5fOMff zk&&JiPQ^TwtT*^7X4HiyELjc*G_Y@yC3@$dfd`F8KTaO0LU zP-5=~wE7IjoY&X=_Og%59P?p&@7p%AmtC*d(aM$1n?9-#_t$+VJ-N%XfAzP2eLMUQ zzxhw+ciZE)Kanq_N2Wh$SYH+;fGRcmI%=^6iRH0A-tq!D^#iRtrI&aCKMJ( zrI<4s5C+qR>z#d(>%G@g^%L=|H>FL*D0@5mxNBP!EKS7C9A!Hrk`~Ss!09BI;(~(8 z(9|-MXq3>2Jj0ReJINavFiVMHZKV_FM$wopY+iLabEMBBG6_Amv)^vaDTTw7iL;#2 zBB=1*e=UenRnFFUC$*_eA(b{^XZE^LoV*O zH`a}LWK-`qSILwg|Kabx|8T^@tny;mWzFM__;E?hIg&IGQX0p|;ZyhY)rjnRJFl`A z)^U#?>ss{9cBbZ|^9*3b(bGdvkUQoAnc2yGV7Se>kfu$I#NI_7$cfo9?q`W4rq@>= zphpfr*7G*Lf4d!%fg}z?Nah$`S|phZWJG=D>+eE6H;_%!4TvlyYZ^q3BWNu#S*|&Y zIYa{JL5+Mkf;fHLIZcZXMHS8-LEsY66X8-Sm*FVxkwk2MKorwLX)5Vtz~*FG;~;Js z2hYlEDn2B6PpBZ0si>Ki1PhC&1+;P;i*TR&b;LF$176&$tdoU3QLU4u<5fd%&j!TC&8bBSO9?Bl*LB*d@N!(`^f0psAJr#op<>`=5U8wSMlmQ1@eU$Z@nmd|Q_7RNGULI5yH#;V+&Zbe7xO=f2DBci+9-cM(koP)svY9E6f} zt;_|dQ>kkpBLUEI67F1tPUk;)zkKsha>k5l&gJp=Wb@O@`t$Xhzy6!|=WoCJ^>6N< zKh>|lsSitbFQDUkFY8GomUr9fXYXXO>u>+z(|5ahy?yxL58nx`S9`M z@4x@G9WU@s;ksUw1J7H2^)TOkdCzySb3K>$|Mc6R{_Q37r`tdI`R8B!#}EJd{`cSc zh`_M0-EH)^XTK%fue%*xXxjav^Nw+3zlgu34?}vNc755$th0NDk3RRXo^wu7%Am9^ z;j^0^cld5`<3;kYaU3x^`W(Hd?}5M`$IGQ(q3L?;u;}}pZ7`FMdGC%nM|Z=lRqw}c zp4ZPe&XnUOf%^!%TyC%adb{oW5xv{)Znus+_IbbEA{;((?{T|i9E>SJZjpBoS@y-4 z6Aqkfc!?e^7KyhTsKg8qK9W;mUV59=eoA-`LWfm&)=5yt`D0$JP?N#B5quSd`x}PIdOZ< zl+Z`@j^d;gg?z+*`S_oIwZA68@%*^lUp^>nD#Af}>Pf~rIuM;hhL=Nn4Mab*%!#FbBg!LTGV3!nS5&)4d zNvk$lF=V2HgtU34hwrmEV;))14G%~JJUSt=$HLZQf+(bDIrw#Jyv?^P-9-N7qRo1iJN=e3W|H@x<`ZEfRHDskJ1 zOshXFt0>Ru`xJ_Yhwb^a)nZ=C?egjJ`E$SDk1%Ov?r%1d3N;}ON^@x_pyPh;;izRI zmS`NV52w?&Lb+Uj_@$3s%G4~x2PS>`-FGj)`48{^eEF;Y>R&!A4uNA1bbfSr*h}pQ9OOx^}~l@Wcw$1`*BRJ z1=RsLRYX{_C+00T5>0NAFd9OfNH?NtqbqZ&l)BozW1lgPz*lggZ#hfQcH zETY^<;BsC(2a}jGvCbrpuraA1C#=yitW#D=4eGa?0aZm?Na>0?Q14+*b?EYPp>BQ{ zFCw4rwts>IqDijp{OF^xylm^nMe zJ+rustcA#k!7Hae*EEqJekc)KsY=Ls%T>`HNR`oPeX??5h0u!itPk&4wVp&*k_O(G z8?g)yr$$tjGb0(>xr*ty154zLvgc`Gk{G*@cje#w<3E0XRj#ay)C~+rUEXe&{nN#Q zOH?D&lagu<%1V+LLqbY~*9A(=DOx(AJhdnx?%)v-Jj|3x+md8W*8(S5!A6>fnETjY zj&V1qhZ@)Qaak0#Kr&SGL}Z!5&PHN=Q|Yz1W69bezB^xn$|9*ssaxYMxYUG6IT6BQ zjAUtq=|qJ=BAyxv4NEXw6T-}>0^)+CX{(tAiJ^Mv)FopVHdH-^nPq^d5Hb^0REe)C zi^n}nRxFUvoD)77L@C0>ERY*cb0+Z(b-_GEW{J_NPA{w^hYi$BNzjPGLb0^jdzRv) zGb|Y?&j=&b>^DTCoXJZ@CKHm81V>7PDoBYz(9E4;A>WZb1IV4?=0T|)=|mecGAmI_ z1XwaFaweLnvVeF4aS}KQB*e_Laty=-FAM`Lk{C=W$Tvy@I%uX7A$jmI_=w?BHEPw` zaL)ovsZ2B@4MN8Ikr;D=b9I^%5;+8wiOlJgbc?uPT?a}=_nH~!#KGg9!#$C6j|gf66&G0wD$_t5 z$HCQyd5`_mz;U_8Jh(Ew`^LXIGg_!|96rT72DEPN;r+6mPOY-vZ|QXX;WN%Er?ZGB zRcD%ewHA_cx{DN7~`(yXCE}3vwR6y6!LEefrh!`t6vv+w|+a$G+dEef8;& z{qv`P+`egl_WqymKfdlCKk`{bVII5?sjctM_V&8}@bdQKt(5g7wLY~)mQVlicdtKw zKMS=!mUaF1oAtl=YW>S?T^_d#T(8$({O)@y5^wjie>$(@`KRxzW;Q^v)&wsW3ZaqI>2iy z!@3K)IZf;LD{T}_f>eaC)Qyut=96NS+D6pw2XP^Rd4@{#A*DdX0AVjun36@bhjPIT z=)o32-7>vjY~H;(T7^`aCT2E_9DNIzA z5;=i!-p(nGq!Bv%%~Oai6Kf4p6i+f0jOBT$ja278y!YV|B)RdKgQ&^#`zJ3;AM7A1m8W`V9D>44b~uyaWtGiQKuDIOpc;Vwx`JOjC;J0;CoM!2VCsC6e5&Z!dE-P%$Ba;?o7aS#)9z#6g;LZ2=%EoNctIb$HX z0iksd&=L9iHnrrOyKgCl%@2yhh`kq&VM0O06H_V|mNY62n#>JgFaZg6q@XGhl0i`f zGt!WE2onp_N5(6H7qRh2qD!_>WT_J_YAS%mP_B zof4eRaicKABp0OQoSBngaA%rM(_Q6$>~ZuxG_~YX7cJAtizJhY(x$ndR76?M%c-KQ zN*d&GjgXsLjp)fClZkjvsVjP~;|@AZN=8qcN`>?qbS60vlLKi>#Hz9vUnL!5B9bW> z8|7VxQG%wCsq}lzV){hKRT(5Fwux?GRZF4DsM~s~i%|5Wb=jG&x3}wUxB}}}s*KxM zpDRb(zrSAlv{;DeCabpP{KdI$>#{xPv3DQESHE6HCv(rkMp~qRLpk9lEMs=JIfh@a zH%b;@^{7n}av$Tk+5L0B{Qkr7_Hw^}mMu>2|Mv1jzrOupeRpcUe*Wd}#(tF37rLxu zM#NI8y^Y(|s64G&6~*q~*}i-4mzTHY`RiYPdY$?F=fr>gsQ=|xUp_6BxY&OA^@rED zw~xv4;fLG(4}ZA)=D)YwA9VZ<4|acd`^SI%Km8B?*T4V!w|N~%cXX373%pfHAtGKD z)>`XVTM&5Fpn8lHH{>A_uwxdI*we@z^L|%Q5m`Vg!5bHwM$iQ%CdVv{f-Kayg!euA zb=+~k-^`7XGn@cB?iHaieRw^W*~4a+?LoL_(CjTUIHlIssfZ;HJ8kQoQSGpMy6+uf zwcJ&2H~sYaK5jOFo4HdC(lNS?p7d}NC5!=3F%Gk7^*G2ay;8U@YigBP0E1TIU^j|P zaafA9L{KoJ1DQ(=?8zV?#i*Xh1|wAfU2A13kE+Dm!hO(sE+a^ULCiym@c6*gof95l zVP0uDfe6io2`WL7lPGkOW>Azu*+8D*2~W)+3~e8N{o@}lL-rAV=fpAQ=i4r|L<)r| ziFXkSFZBov*JE0mucwpLR@-9Np*HGPpB^{uZ?BysY>ABhHhXX_nC5|vwI`(3wrwd( zu%K8E=0LKjR2(BFrnEe0#2_2YvVXX#sw@wOY0=ZVsxrrzUSwsBe&1&)VN^HoB2t5u zh$Tuu(iW}>w5&o=b<63*Scp7*MHf8oV6| z^`~oTzLQBkuP>i|y!hPf!}lNg_x~S#`PcvR<&VD|c>2x1`ZxdG|M7Q!_xn1*e5$*E zHi@xNw2Vl12IG!7-CZOygE!^YR;?umg)J{h|7A48B)qBrK)HR}~2CXUpQ&q~-Qn)T+Qxy+e_MW=71W4+=WA9$2I@Pi)owBuM z6eZ@e$|-`w9S!}BQkk{Z!Wb&eYh^0>_ztYYDM(!_X|9XbRKMQSj;6^R%Tg%VZ(}#N zG})FoJ!ia8`0PDV7E$I4o(_lrzXc&SP@!YH;<; zEAo_B2oKK0HY@2cg^8Q7XWEuXn8K$YHQ46>a+Wfe;Kq}HnyttZ2&o4Z;jDH{Py;!z za7L1-W;A$)5YJ+ylx3M-aokyDjYDLml_-;xk}9ERdS=8lL;?vW@`D6Ga{LKXzGW6f zBs}v-R>YA^V4y@4 zHwMdj!IfblLj8^4FthTYBEd+G^qE3ng?Pq24oORQqDU)iG){z*!IDym8aEE2goixQ z;wcmi=!QNmwbV5CuuO_{uw~I~G#Y_rkNsk|w~OTn_CYLV{o>?><j&fi{BP}7pY)9-Lmmh#3?oXL zOamOX=|nd(zh3y&LSpv&QQ-aU`1q-{#*3>^YG=PI?-pf7(EUP`%BDzOsD%d&xlbz% zdv@`)GLx0aj9TQ7g5b1c$LMTSA`4T5lF)wLg|uWy!iyvZv-EK|Q}ltj&@q-$``(L% zz=IGzb7VS%kP3_Ox%2CdUPtt|nb&=_?l3XSd8F-kJ3RZ~>~@dgo@U2=v`6wO<5D?U z5~MUEx|5hqr3v<#6wH$$BuX4Cl(i(7W|4VKTA+>0lMbTDB*7#tBqU`mo1mU^eFQO$ z!38c=wUH;xlHJFrV3mGPd7yX@l3btUc?t0>Pt=kloK8}N8Z#-|{qXYn(~p1rF-#Ff zV%S<$r3FT2P8CEH% zSm8l*OGqxk!X;DWz7zr-IW9XE%do6PNi?MKFn6s(N1~#P1kZ6RosKvqJ@hDC^PbI# zQ`SL|{hpAZC6&TWgULHlr0yX=*&r09kti>#pfHahr4%Az3FrnQBtQio#O}=0B*GIG z3BVJD(vp}nC?#nD0u<0Qm>D2iNWkGl2Aaqqio}Fnke!j7onoLOd7(&937F>sLZBpf z_FHC2X_+mfQzUaJhw{7!mBX#cfTkMd(IaB^E(ixAfNk+sGlq_Z%v)U!09qKQpY zl_(M&elRymmUKr6Y9(&zCFoDoO~c4}W>PRi;TDks&0?>}bc)OXe?Wl0C1y|Yj1Lhc ze3j7jPZ>K$A$Xcm9O%I$4}RrPW%r)l$#S9;@LExjKPPqfyfr>FaIjBy;i?_I%g>>C%e#$yxnje^T)}1(B#f zYY|&abx0)z+gWNbRc7T4)HYhrX%v(Smn;}J${v++IpPTf#kl3Y@UY;@j}lFd?VOsSNih5GMF! za%q^U`_=Ya#G<;;@ahT6F16;MKYsrz!i>XbmP*T3u$0PCx1b6^z%ud@lSst^8BW6` z6H1ogj3n{f-LD{XZpZetaeDW1o!3n~juc_8c{r;8X&CoT8g36qr1~wHF&36C z{YAVK&t*mt$Z;?tH1|LqnPV5*d+#!12KksYVkB(L7|o6y1n@G@jFglYx)&~7q1#|y zV`fwxGt3A%d6wDKm`rAwxAYguH!iZ_v)JU@N3o<*VhYWXfvFp(Vf)&>M%Pg`q9Nc1TjMdm;fa> zBM^~^jDw=sm+s!Qjnh>9UK!Xjvofz|> zBN^|>opYp;($wrkKo$&^@=S3#;gA38 zZ~pP${G0Fp;olkGp1%Ea%`f-2>2_Pc_^L$nJ9qn79?r7W`XCQar*r+1ELq=0GCiH= zm+hte^kQe*FJ)c6eDKW=K|ii}z5KyuZ`M545FfiKQFzirEmc?=mNSJY4%rXS)Iy}1~x+;)BTxf?`r_K3|9rLP}BI zjwlq#Nk*Ol#gT|`VRct`ltNlUt8Q8zx3o%J%zIEHT`(aehXAw$*X3M7Yev|uU*Fzh z+MFDNWgk`_1;|wyEi+JCNR{jy6Bvk0o6MfdgyE&27Ubb7)Wm~QcdCqWO+4_tVuX$x z0hFE|JODWZ!!DoQZfW-RcHF$*FGmUoOMCok z-nz~4X1M#I?@sM$+bQGvesH3Q`8Q_7gpy zM;}3Ga@NB2{CNKA-Q%}EV;`5BonWUERLe*OIJzxpSCaXQJ%_b-?I z8t?M$w}d_+|&=dTWDD~ z(kfcY(d}lo+x^Gh2I}tRa?kzh<7R`r97mFmJ`VAN*tL3bMnCWhyX7>(m{ zw}?be(#GuWoSm@_x43sc$)Zt&D7qd=N}O&K_WCmWSY?#fG{o$s?2cHONVTC#qSbB9 zkC#c9G*VA<2#N8^wVNodD&<_Gh%YEHI7ZIbkSx@dsnyn}`+kk?TtPNiE9E75=pmvQoDuHR zmD965npP{-rr@~ixWW>t-a|+v4o}K48L<{L0VjH`tXqbfG^Yi;^237TQbrq;KWKG#3UrUQVhZZ@`NN81SnU8hpdn!VbGr($c!{1 zPg*l3VdXHSgMvbnIc)+Z9GDi_!vnE%+$j&vga^PJJKTw#i5&sIlY4^0!g?p4>csey zo`@l>vT8qiQ@T$ycS2^|oTk)wCrX|-sl^V%)<zZLRVBE~Vhn_nq^B^`V|) z99`9-*H?bk_MHFlZ_hvf+54aVrId4g?$Lg)B6g!Ue0+X-FMO)BqFk9N*1z=qwO=m^ zU%uRAefL?`ty^ENU*YqA`+oiVb^Gx8!+rMs?GMMx$6s8Z?*HAt@`GNxFDL!xFP?t* zx4-=y1O}^yqDI9$&JX22qIbyj8AKYgl#Jlh3D+L(tLmJ(Y-%txs!c&s8 zir)vpwP|_y0@HE79>A%n@mPx3oT4?%8o9BuGV#e9srl^jaBkvsk2o@dv4V_b%{f9+ z0$N$z#w2Bw<~CIBt?=&o+T;0rp5rEk%kpsBuW@eo-RD3Z*WPUt-cD;-`SIX2k7=GS zLACbJ{fkoug;up0A&_7y!fTAE)o+~^)+#oWfLeo=df8iJ89lHO?ipf`ETG%qws5^; zc-BSS(g*3uR^rPWlI^teJ|G*2g)JsP3cxhgCkKVGG?^=L<#55>EfJOaZqg6z&moZ~4mBb_0LJ zlLS}R2-$Lep0QM)TxR4FQY!Xr!}@8}1r{pB;F%8ifu=cwY7UcS%;TJtykgoFxdL0z z-esxU(=rYiQHauX&rWhGIYv`S@B2|U;9yV3`dqG84k)IHPCp8aWK9)`oA4O|F#SnV zAQ?djLpZ1eX40BSz&Q3m0a2!d6bVoVF$WWIa#CiZB@(0w0+KQ!ndrt|AQL7O0q-bE zm1tJvJ>xN>6CGqbaYI_pME+5|1l7zPZea&GY=Vf-7%Pwo$I!> z?zZFUYkEE%!QCl_3&SL)u#17baN)QIp-rF&S7pymo66aP3>hRTOyNwyHZofJD{S#x zU@lIi4^ma~PuQ`sTgFOi(eD|v+#EsK!0MDi&Uv{KB~P>A!-wsT5t4AvBa9q+u7vsg z_51akEo%S%$J>X`w>ja5z20xP%heYhOVF~l^ExlT-K13to5Oq_udf`!MT({nlTGTj zzm9$0C#48m-}gB~8XwUSrVN`+>iYOtz~`qg9?oBF-~QyhKH_a382nh4ZK+u1rH{G2 z{pQE(Z-4Xh{Vz?#F6-sTS1Bhs(#hzw)W^0xY|B&8hljFm-~aB%{k~!RP`-Zu@E3pH zh52Lu`~UT~uit<7&AYFE{>`7Ume-5kxi0(k$&!{_nS_*6igTk@g6bOg?z$0sYMHHh&i2HW)E(lU`@s?A zp_1#gV-eMy=GlE@s_jhfV{~`RWJ(u~Yu`bRnZ1YPbWxjm=QT4uiQliBnKQUs%Nf)l zQW@liWca=J;HVf^R=1rH*6VsedMtN+xaDa-qIddy@#*~P>Q2)H3nM}cf zJffC>h8ZMF1_h(3dVrhmUDAEFpsw@EZaChOi&m$)#Iaklzy7!n#k>{Om|0JS&krws z4v2()34SfW`_0jg-y0YD-C<6vB7#Rhl z$vk#v2_GKekzT_l@<0k7Hq$jtkpO5$>U#wDm=34J3?go%E@}*Lb}cL0Th3X)$Sy(# ziW=OS-!A4?_{;&spog6-W)OoS!VkpJfyIWn5lHlWQdtOXdVJoeS@e0+>Am~W`wes-B+N8BSepp3 zCu^3~MOaqx1NB@hXB-+%RH%sb5;C1ih+AAyPl;smlxHfI*uo-+n3Vz<5p8#h)W+ag z7Ky;0(8^{&%qDRpB@(5!l?qtuKolE{P!D^1OO~aizkU48FFsDP89H;QAeg4>^=+@j zNO1?R1cD{o0yf_7 zbcB_)u=>hOV2*qcS`)%aP(nvz+b_CLvPjwIJbI7$c29H6Y^7C6^MN2<>q@%+a7^jM zd>=WkIhiMpQ+0Agyi6}tGtIzDVoWCzi5STh@F~$#!pL-IK7xHX8)oQgW9dmu3@uca zq+qNS#Ae_Cis+)E6G@br#9>R#S{6CNlB%#dd6RTc6uRA}h0LQIgmfD2Y17Nm!{^}{ zb_``o2#wD>N+jo?xCOh3n}7@%k8-C_uqfKefJNa|KYdt-#q=(WF|{UlXZnpCLaZvjF?JnG)06s z#X-K)NXnq1Rk>9xQ_7N=wX$hhzc?k8hiwfeAqK@PGjnZH1dR)V6<{V(D1n3`2`f)U zPUZ=|P_`5TAnD1fM%aa`Pz;Nb^MEflcAse(qnE?%atrHrpYhT&DJO9)OAv7lNPT|y zb(QEYq4{rchb{Em`u*N}+uaxgpZcg9IQr z&Q-9KB59VgMBy^64=OYdXK~`y&0xEhyxWqcM4~6i zq!VT2JbcXIlaq3&-gj|>lyZ0BRs$hwshP~Pn?XI<=P^CNAdpXooIJXDOygV{!J@GA zk+nu42s0UT!$6cI8(hxBZcgwcDl;`SNe`5Q(il!{(f#l`DVD^JAfkbEI5~CPkEz1@ zoV@1fkraxQxa6wGhyBg3pYxYj8%#nZ(j+W)|c)RZhWx%Gd+wxa`{dE53Tx`w#dmag-)SB?o zZB(wjMobG6_Y8rgcxFU+F-vhtGGfuuQ&x_Vx==;$qd>e?vKzEv9v~2vU`u3(u#<~O z9jaz@3bW}Xbz1i!C5e?L77~=q3?>7!QWA@tEq91WP;g~37+VS$?g7f6Y8FYYp%E0{pEJN((Tg^x8HvM`ImqA z-7j8#`-?aEZy*1f38WMf-1i^~N6dj~2;)hX#M${IS{HX_W%4PYvbI7-%uJNasaYCj z$a$;61*#3>LVTjCD#5yPEF>v~xU>{2)>4(T2aK~jS_DyJnhFP@aSL)JD+_`WS!DD; z$!QTI=5Xk7_oL6(?t8}D&CHVmvPicPJGL#?ckh??KmFq$e*e3VY|-6<%(3ev8036@ zP&lgp^w0m9+`hYhe67rHA8*%vca~+_2qB85r*Iq5-#vYm$K@a+;5ZJeAkaydsbmQ7 zYS-6x!q5KppO>eH96tA#^7P)1?$^Fg>$m$CUw%`U6;v$7x-2|n`t|9{zj*oW_Zhyv z`(_+hGm~2N;h`+=l(;;9dH(iirN;f!?tYi^gRIJ4-<9L(XPe~n55NEI51((NK(Wdg z5yPpZ!muH%AW@iSMi@h69w27Un1ukcf%eFD zWD%rfCJsm2n80bohJAv-xI{VOnqe;>%`~KdEFf_=fH4eaPM$+Zro(J{w@SC)$5k$K z{^BzeN-Y~TUYE89bs~VEqCkR11_?k)DiS?aDZD7(x)=9{QxTTy?d~GPMTFK(Z?`}o zkz9(D6LdYZs${5 z9@}mA1C&X7=<}2O{I9=y_p`sG{3GqI4m0vXcF4Hh+#`dd-@EIKG$slsi!el*M?*O0 z$T>w?s#7?K08NW-=^RWg($Wi46dD?uk2?Yqk%m{=NGbac^!IXQ_2|-A} zW)Aq6c72U~kLzWAy6(S!y?poK_4mK}^{>V+fBECz|6=_6|8)P&=czpI07eQ$bWrQ= z93V;M>SUBQ9F61ErS`HgoU)yI&b(f-2|cS-VSk;=+QOMS`76=87_&_9$%t!e1qMYR zm&`kPLja2c7qY@Maw?xJkTM!!&$6YIn9v;FJ*Dv8Lw2~QoC9}YhCbr*NthTJNi!4$ zOflQ)qf>3`lKk%Z^zE0I+ozA0%xPwG(Zvg2Y`SY(735!hu?|{R+&}%{?e){8Y7^!~ z>&?^0y=~ig>r~#>ZKX}q_FA`SG9#r`gW! z{Pc8wxyTl`{`$pFAJ!k~w%f}OzhATnsptOH`>#J-K3FaD<+yzKuzmH%cR&Bx^0W8# zE74_P@7M2tD-WCRdAnR%JC~=kE&A^L^3C7W-~5NKzZ+PV`Si^{+2lIke|$c@k6-M+ z`+=@ZYL*7+2%qAlO59z9$NXF)QDg+wR)r-@d0`A^+L{BlyI1px)#z@FIL=m=AfXG?6pY^cofmF$jk?# z-bH6uqO9D#L_ZRMm@?b#dhymCjgiCmpz`n_{f9U8AhKinPPuCH8P{p^zC2)L%BR;` z`||A9YYVAwpC8^o+HtQ7eYjqQRU5sVjHR!#?ekDET9)p2o3g%N*XzN#*`nM1R>C=u zZT0aP^%+HS4wfDCB>l6Y5G!3T{CKut2_o@uCOia=Oiyqe$p}jmq_63-qitN)=$^L` z&S{YplXQt|5AE(p28(lGx9d}>6X{ZQBQGuYo?}N_b(MLuXt|`hN8u^UMq@0RWnH#+ zkNf35T~m1ap;~lZqI%<$y7v3MfG!8n7tg5y|j z>Kj^%en*wWw48f$Datx-8P-Rh9fPxPz2Ee3WP!!=|n*p>6r{o0BfcJ0-AwD6pBRNfy`_aBMl*2;wro( z?(FK+$SgQ`Ts#rE`=IFE_c~tp*ysKAe)-}0dbxi3KL7Y(e7xU2cRmINYQS|G0~jO^ z+k?&CGYbWTDi^XOAr+(%aada$X7)U|YvY^AvTiXax2=%FeAXhyovGcAT$d?@xkN%0 zscXs%R+5!-XFaD~Q_o4kM><9l z2MGt|e)D3+zveG;>9jIp@}g+ov*jsI&W3W>G}NW54U4~eK@@j5GX4WDMVhjlfJy( zk0SMQ+y@JYA2_;riMCq2QAta;Ip*s}rxwA_bEVYEOb8^?_%Q-S%T- zU#Jqo63FsUvn1avJlAa*-B{J%Zs}AsrcF~VIk({H&Kv>djB&qpi;Zhk?>DPe`fgTD zgIkfj?=FqPdz4i3R)v$9WuH~sWaQ4%$67Xve!I2zU#!Qj#f*S=PfyolFdgl_c`Qan2 zr_{IlmgC{nOR2kwy>fkUy9X;|DZJpg1#d`;tJ5=$dnXlVPHN(}@Jy{iJ!Z50O|($n zJsZy#C#9V8a--p%JmV0)b!(uP;`iI=xZOVdIFA|3qO7A&E+yFeTdD?%wQT(`^Waiq zpR_E6S)6_E{phl-xBJbmw{eZ=W`M9@*!XzSBu5!%ySvc8)QT-4XvmX35BdJJ^iYF+~Y_rD`iXTDRA#geJ-o0@X!VRF;Td_pFRw0 z_e*zGa+yaEVNV4}ETRS|#x9GEv8wf~s)2|cMdfn9FgM9Kk}G+MG{BUU$)lVg#U>`d@7>5M+}(HIQDU|u|FL@et7xVKYsuA`yc4j_xlgG z{_h0^#KRf@~e?2um{@s5%SLWw0 zzx1weyQaQVx8vn=c+I@M-HxRes>j39LI6y^dCbw-o*$nsKmOq$ z4&?RvqL~O}39=Z6>t=f5GoGe@^rZ$4{@F}^z=|I@_N48E$N88 z%^xrH`2Mt=j&WPwo$B>;`}83)u9wgH?yLI;`oph&_4UvG#wpqvY|ou<>tjozAHO@S z^!DXDD)((|?fgjcFWTjI?Ogc3IRBT2+dux|htFo?P?qR11k%A*F(F}Y1V_qJ(t~v( zC1Oy}<0CWcBj#Jn=BmPdx>>|qbY{w&%MzSzEpH!p1jc;^f!qcUp)wq$7G=+#Va6tt z+{si5Gl9wxeX)kq;vQ5~W@M$^OxoJ4r&KJ&J48~pGtIp$ZRc!Bx$27ByByjz@8|3NK%+b zORM2mutdxpo`u*wOQKr_%=g0=(wexAzHh77=yA`_6Iiv(45B{!sXiV1-5hPxA+2&d zd^%VP=AtbkGlc|IlbpN+6Bmi~jPXi)6k!D?(S!lSB{Y`XK1XTmS)%u%1(K0M!GTrR zP4!1w7osTr$}vfoxF2TFR*p`nn1&UEr9!T!?UZhZSy{xpRCm!*2uOfkHEkGliEYKl z@6y}sK88%|bMBrhn9&OJ2EP;Oz@*6?2@(Wom}@O`bzW1tPom|K_9I0rOE_j`E$MfM za-lM{QBQ{M2bBS%@_(%4Oi|XeOX{x5%3WZrGCKNSSc+-?HDL^j=Py? z$vF0GyouCPDNn>}C=Zeb;*^ClkU#`5fd(G{BWvbhItZGqL>YOfXcUvk5|VVMJP<-z zFn?gyz|1H_2fUIb$L``Vch`)bgDpl34^uimb!+qTHeTlS!>?X$KmGCdufPBG>mT3z zVsahfA%HtTsstLMl&ob@0F=rG_~8~=Yv%Ca!q91>mI@8R@^I>Jug%?|^7x+cHe>WC z1-16P3zx~{AhXJ~9-pw)jNth~zQ9K=s>6UpBd#m*@VlTQCyR`^|F;fFwD4(;(0uVtMtzxvhB{^rl6s;mp4l&AIUzxw$YJmgh) ztEWOX#vZUf?)Z?mZ@*Y=-k-kwvmZYELH_c6{h{6FejM}q{bzpsaEzNQkDX#^LLSSi zi7E^G>+6TAId@r4%l+8i*Y@>OeES9eaQ1xNXnymzkLM5Hed>EzpN>7KI=Kf+Ah@ZJ zjN6?nBV~x7yEZXvZ#3U~DYXJ2R_vrSEVruD$L>a4kn+JJsmDPC~%7Sgx+o!NvPqoeQLmhHZriC_22blC&^pxo&k5A%GMNdUlYkgkM z$6ZFTvjEc6wZ?JO6QwAgaw%gb4UAIhfEI$2z3EM}EM>n2KcQsEow2$#q9#lT*wxB% z@?um&_+iFXW=EcLT+}Ej^H{SGQyupnoO$=%;o}3^Ic&HXvClg`yN+^x8&@o+?ER$b z?9o;NC{ z?PS)u3niI_(?s4f4R76B^1>v>jI6CJZrkFY?l?OW`7{)m(IDcH`I=khRs^=KRrxlZ(;E4Wm}YHb zV#DiaRms?G-AE?Q(=m*EFJ2r@y&sva0VbX%K8;o^P3H)jMVb;4Bcx*N0q&CmXPY-R z3uNZvNG9*xYQOrNJR_+x-Q3}_mPq(qN{SYy4T4O{bU}hTk}?QkhzKOx3})eiNN46u zV1hEBl$iudPa?`S;|hReO*m2#jdCZBv=W{Xo~W=R(rlO=9{W8ncfarVZr2}PKYefC z{b-+l`1|iJ^L5nyJ|^6IQllhJ>hLUX4OPVzVEl<0XH~-T{_Few6_Z5E-Y2VQdTP}< z=IblD5!5G*m)~zsm5ao6IEOuM>gjeRT_oZ6fp-$&VowlvTcj#*k6>CQrB9hmhw?k; zTjkAgFkVxVP(TtX89SAl@hU8Vnz%x?%r_^bx3F6drG85(IgjY+x+2TdCV%@c{>4xJ zo1Z=Xv!A~I`TLju_y6hR|L`&1{nfbt<}d&GPru^NAO843xY|BM7Algl-^SR^@8|wn zzy6CDzq8MP+$+!R`K!x*6p1#ftl7LJ(d~LGO9_{G-QjFBQ&y0=?X|44$eGu2(msxu znbZ3bwcfaX*}nSnn^<4|@P|RRJS@kD&u*_o#pPzbGynYl*0*o&58rNOskdz*@ayLb zozML1A4}cVr>}$O<@NVpy#F@WM`fMq0-=7H`z^NU*X#8+Km2*Tt$+Dm757-_;d#~h zH-G&v`}hwp$E|`YbUGD=8%HMQ&Q&77$8bhUw;`*r5KMDB6XTt|em--J#%xY8%fmDJ z3ui?R_GP@dz!RJ0Te{A9Rgz8qCJTcgoaH%&V~1T&mzaw|6Kc_n?wE_r$5 zO8W2(X=W%nRr2!w&%gmYpw17ch=U!(Y1+gtB&R4U@|-c#Q}`#z38GN#Ou&^v3Uc}P+gr_<+pGEz0L(GvZZIK?foglLei|2D*-%5GTr#ZfVYs=a9%W^*1 zbTgl?(auYhDw6HtsgK(fnFt}69@(_6G>q6B^B5=(u-C&Mmb!aXq2%KI@WK8tWnI+N zV_GX|$w&8P)uLlRScJ)NNiJnv^Wlq1lR{%2DdL;wA|PuG7J%C1^Ni64<^A%%Yz4ej8qGdZ}s0tmW9_9sDNc;Q8b+5T8o=#KHv;b72`tIBtVTYn=o# zbI%M@Vvf12vOP3rQ6;7V4#+7v$!g{SC`ZjW00*krbWSjFa=5cpq!C?-7Pu!R%}knz z05QcIxMZ59TJ(fO-qUvh9 z=j>zHsL2+wwa4I5N@GtdqRkS3@ni^Cua^%tePOwe$q9Fm6Xy*~oZ=Ck@*#r-3@SoF zodeMi3XRmtK07-h_Y5vs4qG1Td`)dM7|U4#A(iJIt-=OrWC?Wl2oT*dmig?q$9JDLGnfX(03ZkgAPF)^iC3gRNk2_@GQp5| zr3eKjkpUS9B0#|r1kjk7o}TVLr;piw@6WBZswy*Iqj{bTRiQhiEO-jYhAX;P^VX7w zB*u@gEW}=H|oQu+4~4)c{De1sEb)U&FjN zLOPm))z%I-NaOPEZYVTd?8CYp#+Tpx8$Dd0R>qVNrGWyy{`R}aPj|<6>HMRMKpC@q zcQ<3oXvxg>msi8Z=J3s*zy0d_WBMrJMu*J5c>A+?H~WyO}$y3Kf3yP zl#f59%g5>IXVluoXP^D9U%&eP=P%!X*X9OIj|!wJ5}g>GWJGmf^_B{S0f_amAPViR zA_FIDhcFg1X!jooi&^8%kcI-PwRuTqI;2dtwnWB+ZUB)jdP0JX1LQjQyk+-J&Vy%V z)fPAfglRKl7@MsWz>OT+m~A!OjM3b5NleA#qy$PHE+t!GUaNzFj0{H^M^9v0y;2Qx z$@_uDG@osgKptp&wZW+35RSx!yqC?C(*#@Eo{~hweuQX-z+U2NvQ_ zyOk&0UZfEjj%jC%8iZCg=P|D5fYtAVSTLV309r#r;%>e=p?CutEZ@6d1|h*J!_ajW zGzj#w9z=e4I`;16shME59G;iu{RNbp5BoQ-wjaJbrhz&wV6~GCmh5CMF_i3zz;2C? z4JMAhx>Ic(oymwdgxo~?;m$0$_h!gahITlBSzW`0{HQc0Z(tsr!z@~B9y#D-vE%(T zO{A5e7HR!yP)x{Dc=ORObX^XpzGSd1%yFx?{ zhWQjRb~o@CfI5%l&DR-m>PZ;9tu9GXBKZ=1b3g^06&dd-v(OXh&P|5m9Ptxi`JJ9j%!~NTbcdzG{AKJ@1e>ioM&8c^G zZ<jgC~FvmJ_%kCq+sO#z+)7 zMZ%JkNh&-E)m35V+Wnz+#t9@1!8s*nDwz=?V@iRZm;wYbfD%DxfRNA-#DIVXJOLqC zQ_bQYT-<_K0ucJiv*X&mLs+uYyr|7>=)IWnbawg4&pvzh@BiSVzy6c6bN|iX|KI(; z|Lb@6hv9f1wd!zDp8Vk-+1c5j{=fd&;dEM*h(o<|N`$uA?aKw~U9)w8G9WO@kU~+C zGlQ+eX3x@2hqbTjrA$cxb5L{*b4uvlkOq-9I8hRN{L!bEpFFMaKa{*(?+y^B;mMh; z`0(P*{fBq!d>m2^P(yBZj6my-(E%&Tu$)QN~$C<4qZNEd}xSK};ThQwl8DMAUEVI%>JXnh6|@5qn@pc`VE=gMvF8abd> zW6CbTLEs%*t80KO%tz%UX#HTd+C(@|+HQF>meO^<9d}#a5L|%oYafahri(nDQ=N9> zF6jo0|AJUyOx6KVN*QEMU>k1iCrQLeU~A^hHrf^IkUUwJ0CJ-klb)m`(MF3#N$83g z1_5mi6mW1O)Ck0G2Z1r#iPa$x>jzduVDwJwsThtM$a|SKGG6esiTsG|YNDq@dj5nj z9-m!(e7S#ewn+sNlFdlHtCu949CTSb%sp6`#2g^pT!EPbBUC&Mv_vH0+`Au>%>L9 zC&U$z13HAec=Qevsv3Dv4;eXn=OI+d!@UP@-I^0%uzColOm=a(zj%(*`A{;`uwgQY zKqT~ruzGg@1vfwdNba2hJWwGJjXAkDLdH-e#&`fkAah5sC2%0`02HQZi^9>W^;Nv8 zE)QMveAf>Lzj^!N?RO7vU-z$H>-!J=<|K#k<3l6JjXLs{34|ya4!dzjh~D-SBd5a3 z1M*+pf6fH7p*(IZBKH;1a+X>#0-z*8_XZ{VCJjiPz^FN9tgHKS!T`#A zwki3XB%O02fkuKQ?^h_y#GokP(-u1M7}TZnz)nF3Fd!IKg%DthC>+Xy!4{<22wbfJ ztgW|N8>}-{9Z$g1{*y<)^Mgpz|X8?RMHfyn45&B4HUM z=V==AdeW{chh_D+w(3ceE*GoOIPt?(@>44comN zuJr`bT3^}F@`v3EqXK=2J`Om=y_Jumj*!{Q8#4BElD^lIi5kc)*gW=t3~ z4yLZja)g>;SPE{y-n+7MWaKVhoDn&>fdesGasY4&7=|(xKNx_!)~2f1DOjB_2)X-s zvF|7ol1#+A5m48PY0T4(IiHWTstlHx$BBVL00&P*sUHkSjG&ky5ZKXOwgkOFGf)o% zstSS$#G+S89NnR#5&P;eL|ZX{inyx-p*cbTq1RO+$7~qgo0(}4qk{Dgow{(WHJW;4 z4~OGJ@I;B7QIuthWS&wf*r%-9pGi%i0h)YDWz60V|NF|NYiHUuHmJOASu$- zBer^fP(x8{B0>!9VU?joZ8%C6LTJ4YY2q>Qp#~0;fXgttD)n?&br|L9a`@u2Co-M) z_rIofPWx+H)|-clnrjPRPTMi%9-4%zQAbBX(AL9D$tm0mV+WSD29XDF)73V3eBW^J zaOknNiec#3x@Tp#YQRX?yt{9Dk5*4W4SneDLLN;qER;$g@if|63jEo2xNN5teLI7AoGyq1<5H)}(95GQs zFo3WK$54pDBLae;2PLuu7657{>K1iStI-EN-S!@R_4PO(Z`Qkaw{Pz3{hfYzKi^b0 zBXg1cWF*F9)`v&uadR_?<{ksv*$#JQrVO0AU3p6ojH2o1g=c1RC<} zAw(T8VGwY*MQ}oYz!-s7YCG`RfkGi{wIzD97TZ44DW8ut?SJp5mw)i-lfU-4(3`LS z+yCu9_&>fncK`JEeg<@^hk9yvfXjA#fA}|d-!Hv>xILZLG1iB9W}#l^>1=xP^hrCc z^Wk37P}@Ua8Hjc5+so_ycv+YGldj!_QC#bz=bt3Dhli6f$KnYiSg0eZdyd`t_R;50 z%Gnuqi`H{KTW{YBQa(S!X)7fs+#T=Vgeyo|j;Hp}!8$@-dsnyZqaQ-rp)K2IA6@_S zc^)x~AAkGi{jYyLpALDvT9>=s^<(q9G!DaVg5D^UH+D9Cn$j7cKHPk`pPOe4rk9VO zJl~(~hDSUOBFLj&+#O%8wUIh7Ix`80BQOE<4QEnJcw#Wz76+GzG2^=EvAPns@Dfsc z-|YF>`IHDhJXo!W;E*PQ4pOK{Yu$rFk`y8BZN62|=YoiGe5=v@n1c=)S z!i3$_fNYvZFrPIb+PD{}6nz0IjR8?X2>=Zs1jii~fZZ6qJJ#BbB=5FKhcrx%c_hF* z@mR)9PL#L}h@EyO*|aMn*G>$*r)=mLsff9UBd3rMF&g$p z#(m|8AW~vd05T*nBtzybhFh`^t_VzYnw)Fv41vAKe(DMKYo1vbU*I8pH9cB+ITCtx^oz?fpJG=@ZK4mB5JK%sMhGZ zT7#(!y;?OJcNc`byUJ31H6ahvb=+@Q*CLcgF1a8K^KoViCP}+pAlckg85dolAv5A> zZUNKa+1vd?%e8Gb*#l1x3r+c`n$#YBdiL}=m))*CyxxQbji;XG53e<+^>j}mEp4{G zmMt_N`m)OA40=ZnHtcysBCKn#iLgZ^LssBuYX^4DsIxmYv(WC^DNVDl&aQRstr6^E z{UC(Z5dqiOiXB4HR3QfS$k4ihBZ75i3Q#X^eb6dP`06IhvCZj~PagogO z^x3AImtkAdG$KzRhCzraw1#)k05|jsG+^`y1d4D(9uZcg0n9MSd4pic+1&yRVb8Wg zfOEK`S~p`PZwtq&y|JB^dXl=F?jGv>0Y7{=+`VrPRhzXweor8nc>mZ1~XH}rFCDx-Tp!@m^Z@74z~|Y zu?QwUU*mA~{E1*%mP4qUc##yk@^}V}pq^8?d~#7TreRCSm2p%D4hNBhsY`^Tsbd8P!j6F|n4BX?YIV(&6g#*h)1XF}j4Wx?)yRT7sr8Ft zoC!?3q|#13I3yRZU}huB5EM{L-T~4eF>PgEVh4-^PtkLhJmi!tqYyZ$0wM~vwMBDC z48ej+AcQBd2rL55t{$CFBAE!;R+c(15^LAb4IJU*GA;Id~&E~Y6CeB)jg6-jf2XSbT zM`nhm(G52PnMG4a0$Y<0NQmAN07|rKK_g?g+M^M!i$@|(W*Y7)nb*1c6w5uJuf_na zW9eAJYH(mfC(3}<(V!+F?u*BQSadxJA)+XIIB93lW&s>&lluM~H~VSavLqt}00UNt zOkTk$yoM_hK?uQujvfh+gP2f2PZ)%O1WG{=l|g_F+6e#x2+XY)#G$u}O1*=x*nF{- zj)%J3J>0!Net36(^Sa-j*4nYA-MY-A7UTo?;Fwcp@oD5iHs?FoP?ju-9M+!lKil96(Xh=AC5>v@*BWfrfmQEs50ptoATX1&=#x)> zfV1A+pN26_=bPq6)OA)ZiIu>64@=uIr>)sx94~vV>vB76$4o3Kl5P8Zym@zXYHRC` zGHiCc{q>`>tH&45uBXk2v84U8=}Eyn)b}4={PCZ6)BX9=-s{@#$Ni1dt*zIx;)3 zgbd>Vo&yr`z`cw`&2JXyvn}p~lsVjCJS$u)8QiR4f{f%{1DTO;R#*KoEl` zl%__k1eOXb83y%Q-HC++k#HhGw`M4mpim}BC8ae(RYB?&J6#fqN=Zjhi`T_v(?+)zUT~9FzcJ zKNw1KgCQ}P7eh#ph|`9oMH0Xi39&nf1TrRfa)CT0+-|nlXL0e#etNRIy!`U%{sN-= zFt}A5#xQqdVjecRD>I9FO2c40FlMb$%ZMQ|4N9EP&Kjp&iXh4~l=HJGxg_S4bB-7{ zdS4GqFAVOHlL7AX0J>C3f+&VGFy_rjZuiY{mc*htsSRGn<3ZDa&#up({`4b{h_`Pr zCF}j(N1gA((9=XhY1+CW3dyv$&BS9u1;WC}J&X+!01U)Hje;Oy2ZAMp)Tmn*1q+G3 zcmTN@li}#Nph@`ZC>cOo3wKBu2BtwyXx)~TC?Q8{5zNth#mbn&q5=o75t2+6t7-yp z4R?~zMrjM-n?Wv~P1BX#DBNm{OH+F|Kp$1ufF@!pMQ5hpVSZ?AT-qNSOrrlu+76D#mS!-vbOr5oUrx!a94&)Mh)|EoX9bdn;)n13vh$1<9K;|cQZe{ zPs0^ugOP{faXs99cyZ&2hk`?vtB)Q>djK}cH4WSRWIygxV0`%bufO}%w-kUQ4&!5= zPkK0TY-_JrMoLrK?E<#%U%zNtIm{Pf91f?$c9%pFoqt4U*Y&%X_xEr4Y(MSF_~_}d z+RHcJXlt?tuF>4H2d6yxeY*PG?LMJ{$k`dUqu(E6CD4FsO6p~p!g>nWZ^m~IZGZvE zs#89jAQXFzvcZNCW<(j-n(~lS9yp`pz_n}0P?9lA8A=fzMAS`O7egRn5KJC{0x%7_ znpkh$Ik_0GN&_HE>9( zIj7we7Z(@%CvEfO`t1DaWw2xKYirn;z=>9C0s#=1LY*x-W~PK;XvKkn358XwlDTk3 zF=^}B**=&AVYu1=Z+kzrx#yD9kTzMhGv(Xcd(N4un}I}~refx`ayJABM?p?hO09h> z@${qJ^WS-nYs14^%;a6#X{`?4It*i4=W;eUC&9k)wWmbtV8poMIJu1ED~W@Jfd?k9 z4?S(!BcQ5h7E!ARjI9ERD!N512L!3Dfhq{p2Ss0<21Cemt)SSkHnqg9f}a*_EtCYi zB8Pc`)gqZK;MsHuWe7#0fE-cXoRHWw<$U?r#?6#6FfkBPN&pxT6pO0^8kz)lHZ%@` z6zD7i5Fi%xV+6Y+b3g4IQv!$_2(u$MSDERlhHk=2YSh=4O6X3CA6C~c!H$Q(3+3%~-pw785&-m`A6;nUBjKm3X8-_8H> zAOHOS{;RKl>vwH1rpw0{!~Wvo?&0tdYIJ=z{QlqhcP>Bu=UrC;N?KT^!x5P+= zoN5>advF2&I z7{=Xj{xo0Z>3m}bhi`uS>Q8@(ushAQp6UeWI-{0;({@wHIA4q>V|7dgZ#dGq*;oH}69G9Cvm+R~8Cs!r5%ZInO_YaCe8HaJp zUK7$V6gizRO{hQ#i6!Q^?Oi!Rh<0~ob1R95ZQf1!(1nus#m!qJDk2RuYIoPfQq^Qg zLW$UUka_87>K2fYlAsx2#~$HeRzclClB*>Na~Q%~Xj{WfKp>B))L=GnXb1yC1lKMD zO@Jvokudc!C3e~{1VLN30~0cEgd;%+8=-SbSQUzx8gAHEN3>zb zK0}$rDWpZ-V~~WJ19l*akTJp0$}Z3`lnf97dkbq36lw5&L=kWY0MrVuhzJ>kkPtB; z!0c(Hl9&O}B4wl%b8C10x%i-p^fx6_cQoldG^>uh~4n+q)fOPmhJp~RLBS*;0MSjZ=n$sC)bnH6s?CI zjXj!?leYuYR7QMs3B$gO69@xvMyL^i>SiA16bS*s4L}eH#V|4efmPxWf&x1c2L%QY zt>J?~MI+PZRsYj>?S zjC^~n;KSi`%sc+_C(nNNH=ZMW|BHY8r?2jP(cb$}9DCGmEqa%xw8uR0fCD;V>$S$s zymp@@r8Jawx;d_uk;oTKgRC}A@aRWBOvl-l72FYnIPI@?5uL*U(Y!B*xi2^`H@p3A z+MGMC1ne^&-yfF4+wt-$mEswO-P!rm=VupJ`$r$`o_}06oG;F{pPuE>XqVgDhj0JI zzdU|84*61KWGPk;`;X7J@^oEKc_?+Q$u*apFB4hnxAp$PYg>jZ`N7}#_qI^7d^rE) zr+D*bxpx46`RA`*{N@+We)7ri=;##n4TGD1s1xIrS9)5Lp$&4N-)>b;oemK1oqmNi38kaT#Jj$R$NJ z&djMYiUDCaU;t-qGe`vQygi^M2hG|DRI~?@pgB&cE0IJXlT*ip=*Z}ek|L#GLI(r` zMyC|k-IS2q+5kEx^006SC!!#3Oc6j1D%c?nB!JNuJ1vD%+38{iEk+@??ZVJ3g%5!VJONsuICr z%i_%{D^nscaJ6PJBv6f{f?}LX5jh)(OFUZaaEx3%Ib>6dwB2a5$sMG=TH}B@XLsdPj%ZK~> zx8J}2?(Ws=4==x6U%b5kaNB}t^~eHQicsVYjT0llF063m5mQGZCuT_&0eUpgjMnL# zM#}v6{`%jQX+uo=eHMhYL7geoWMoE!jNU>5QOPvG8#0c}fa_2gF(AMqtZJ(OK#-k+ z0ne2oqq##^xKcBc(kckqrvl@M3Ah z<(jXy!_%W2zrOwd{n?9O%-`JiSgRfbTC;Jq+EY0@-<|jM?uTD|{Ok{Z{A9n}{Oj-k z`+xrS^e`{oGE4LZg$5bX!wnFquCs*ZvhCV2!*V$FplO-{6kC~lH^3Z|ttTm&&v`%X zbIDXTw$}T*gSY`^0axslMv8KK|E8@_ml=G%{PHJ5&R`7!wJzXncGu^%-ro!I?vV_W zj8{`0G4IQ8wdK(;cvXJ$FaF1Ge(}BgP&Q@z_`J||y}Q%?VSBzWY3!|GVuMZ`F)!9N zT8RB>9qx%-pvUZ3sPN5A)@a&b0%z}IhoTMvzb zsm_Q#l#H!5!?6epOba5KF?qg6iYW@jOa0>)p{scU7cLQwsKj3FDDT4Z=}@Hi8|M0~rIOut5=T zIPBB1*fcmdf&qI^F$V>t0hN+JAdd*m2+8 zA{YXPj8tieuo4(N5aC3?M1Ybh5DGv5;tF6$0xSj@rF)v*$ch%0s`5%OU4QQ-1C zT|eFLKf>|qlkIeMHPE`&Q=Pj2G6EQQG#0bdx`v@qxS5N!tuPimCoKC?gx7fyfz8?3 z>8>v8DjQ*zZq}o&{d$`}|KZi;lgIt= zz{{Sl=$Xp2S+!AxG=i&K|aJYQ0s*;?|%g(Xu%z=34g z1_G( zY`wQSxK$7K=-#?1VOv@2_F{W}RfZiWMji?PAdL_wfD!%Z$k761K!ZR+DHu5r9hkUB zU_b&8M<>T1L?y!Jm?P$h;x&0cG)q=tbv?a)|L)u4H{agB{9eC*wY<4+k)%pekBpey zi|{z*91&`a9zrQiDbtWSNzMe!q{K+34TwD3ZmDW(m=}*4oqw!=1!5lkvdCm6-@75n(ZaM64_rnm}r( z5IO~WLTDZ71hfq}Mn;Dc+EKQ^yWn%*J>u)@*uQW)(z_S)pWWSl+aBg)?bweBQRZ;T zV}w3>`a9$F9@F^6U;W+9)pGjP%Rm14tFLdCj?#n9MqwFp60L{8gu+{3ixway_f^WbLiR@UzUe-ammbW>8^L1M^DFnxx1N9 z_1HUM(#?5XeEI{Z2k%STZQJo?eptTwiRQ14xcv7QcPyU*M0_~atZ_v7Yt z{nKZcSG>IVlf$bIo~R%7?ykLg@y*4x?Vmp`XHV+4-`*XL(B>geq&;Vbx+2Rq0S~mv ztm?N%vtE0Enqtb5aRPmO23&T>w+|1si$lj$7`VFyh;?KhL~59JA!^bq(?G7^fsV%L zY9p}aKmyjRYC=qmn1-BpTqw!lVQETBRl-1|0dbz21+V}F5Tm+*2So_#u({~-%{ZN7 zTX`dSI7>-U3g%LYo3K4p*f3GbV4(o8296P{kWnxpLMT#RdoU!TM5DS42;u8OA*2;i zt(=jr&;Ugc6s#B|3JoOhVy#D+5KX!n5(L?H&w30_7?FGl92puQMoI*Qi-C+7$O04t zjEu#R7?{}!r~(a$0&XOP;NgHk5k>$WnJIw)djJg)4a|Tu12W2n?R*?BpTh9`iZ7mB zjL)~Q-1G{j-8^cTTNq$2=-n-nyC*smUb+nfkGbj*9hHO2qcdDj^ZQc(c#$MvRYme~ zZJPvOeV%bBYf8DDR$FY*UdD+e_1Q`uPIJ4s+I;fqZu{)oU;Gkev`EzLR8#f3a2BFrOT zJ@s&(&HG7Dhxu)N^Yx3@FAl%`M!$Zu-p$%A))k0S78nzZW7$odC-lIaDM=K`i zgIG9Q4~=NnaG>NNLfSBo{9pd<|CCY@7LuAIv1|x)^cjexm&BgZb|4G`0!K2ilm@@p zN;o4%B_$hdNYPtZ!choflXDp{l~RyLaY-YODNjsklaewG&f+*C0;WKaND?7}CyWu? z9W&wvkqyp~H$GkZZYxhI;a6}Q_T5drYs;Zo>(N@P0WBWt61_y$-Tt$WzWj8#{nh7x z=Rdgo@fp8<`Ky2R`l}a*Ms<#5=3@jR@qx2IqH{eRF_xcKDDNwz#C#a<@i zj!Z1$uJ*&(rx#_q3|%;H*LEu7jsVHEY+D%y2Ap5~^4Sj`PiwsVCqMu0yYJt8^G%dZ zim+w-@;ASk@cr3mAIW<5^{;;(iV~Uxr<8a~A!*3Yu4%KmpZootb_va6pv&ASd?>Nq zl`ZqT#p>!N9M&@t5^C##Lu-vj?lOk!6m7HHddb~8V4xbXMGQEA8ej!=g(SXsN*Y-b zIhl3ItV{3F6ideB5fCW@O>AEC2OtDDB7%c(Id2_Olh6@;pQO&LzW`mjY`fDre+*GM=vEr>3#+wTOG$u z(c|f~`grm9=Jw|4$LAkEeRT13?8gVgP2At~VXQ8_dpbKO(oEf9Z{SVOK}!MwR^7`lVb+SX`uKYloVcvHXo z`u>~m53la}+(iRm8ib<&jU3}t7;!rkFim4(#{!yUH)f5nQZi&Cm@#k;>fWQHQ^|AO~Uw>R8U0oa02{XnW>P$~w##jFQ5U9G#}dgA4_xn8q7Ez3$(f z`n?7&-8MGA!`7ODX?j>ym6z4hZu{}aKc@8UG!8%byMHvccYpTx|KXo}Grzy9s@l3a zwTJ{cI6%?J95sXk7_~yx6P00KhVgPcZqE0QpWeQE_5Q^N$1uWak_buTmdx^yb{}7O za94%q&5c4^Tc$_P&Ypd8^V?q^ZthGHxseeAFSqY^&z=mj^%x=6yczRwA=8e4ODa=7 zANKosSv%$Zd15|aUVixM-+qm(7tfyUFFsZ~aX55Zmvz`kPJ4kkefTYJ_rqpfXWNdc z*Ey#VlDl_S1&zC#7v-7{o89{1>9=3~>h|G7$!AM{znv~pT)lYtI&QwY{OKR=m+kF` z-&(+gMB;TpUrt##Z^!MB?r!P=ZXGddZ4K2DQXctoh`i13-rjoyH3xzrkG*$92@Op! zQfkHoBw+x^3}(y>Bm`w}GYSVsBP7Q##X@B>f-e$i9YBweM{xrMCztJRoWeA%>jPDw zA{uT%ija{Fam<|%38`eB`pi>d?_zFdq=bkg66Ow^Oq+*;2$)8Tz#!N%E@mrEn-n!1 zm>`^y!?i*l$K{?`z{SCsT0n#Zc8|>1XYAd}MB^@~djNGcb^swf24~2KU6BIw7D2%< zr4cg=R^%~?L!K}xO@t|X0dNID;O<_51_*QM937kx763$H3>1(EaO@O}5y&ar9Rs5i z4PfB_guozM!Ru`qpGkRgy*vN3DBLc$T7aBe?}40fZ~-(Q1tQ7$cI#k4J`82;^R&MP zqK6L$I@_4mVc^5kGecb*5HNbHaG48{&8~qYj$}qTH{{L0H+TKxC!3GHIGfI%^qW`E z6P7%~mTHUWvO6cz07MK27hsB0CITtedX$6?3IS=1v~dY>ifp8X^ud{B*g-qV2oOWI zQ{U`t?uec8fE*A=!-xTvD0(LnkMQn9Zs>&6ydh(l)d0%e9u_1atRaK-btqX<2en)X z4PdzkN9fuc`0?nyr#wt@*zX_jt~KqZoQO#PH-HK;0~H5HumB1ifdFwtf?y`_poG={ z1xQg-V24OysBU2H)vSk}a=X7d9M{|T@%HOi-@RPF`*uC9m08Flt8Q5K=Qsk5Y@9q( zMhsTxBtE4<6eSfj7Iq78bz0D4xK3mZa*a+Pjdxk#ZCQ>qAAFvd*|IJe4(rjXd(hhPxL9BP=!rMO_VS}o zzkGgt^{f5Qe*DQFemVd4*Z=+h<(F?Oc|ct|rWl5estULR1u;ZRoTT_tC4hT`nC6Lw zGHyS+CfeS7`a8P|cl+@gQLWm!Ez-=3*B8;ij%wQWEj;$@8+`9l!bJ_IOfP zU<8M#(Sq9HUD}T4&!0=0h+uoQ8>USb27=4SkL^%z-@m%NyxKn6^6v8H#aE{{emWe> z2DTqv?5;ntQv*iKA_dCXnA&uB|NZg)?(DNKhCC8dT~}a)MuOWk;kfzCsz0VXxtQ$Y z@g^>Dz1v+ronL%-`~Kzj>Z8le4`1DXd;OQD>mU6!ee?cwdm{;=lLt%YiyUZ%%(bo$ zm4H^mEn=Wzh-?I|_S}#4_O7dp*juR3Zw&8hplHJrZX^ z4bLNY00)4M%Zda&kN|*2lt9j89z0SSfhhrbNrXW;xgjQr zXzoOSh~Vk~Jutw5oWT!6A95a^Ys;uPcPu?>H;pk z-`yNmWFo{~8vz6Q*?#KSF3*RkZU~b&X&*L&F0~!z?v%hK%V}*ALLjwvla>>HP%;8`vR21@9n&So)LOP{P3Xm?cRNx_ksRamg89^%`Fp#+u zGIVn(&R|SBdKx<875+96-mSlk}^6nqX<#S&MDM8s6lopYG`2K z&STk3vYC>2Y%}D|kTP-_Qh~^$1Rz&%a!Tk&6b7$>BdH*rFentkO>9GSPn{h}=QYC7 zB3!`))_bF|ALjKTR*h3l4Wm4H^!&4*<^KBpcW?jTfBr|CC)3ye-9P>pzdE&MPGE%r z2M;IcOu1xMwMcn8OjZp%I%pz>aoS)g_RBAREKF~|`SrW|2S+IkCfFkq3jxvKPri6Q z4i{GY;oYrIYxuL~mYwT_aG2k_T_yScg<>>7dK7DZDRH*L1cY#wpV_y!Sd( zJq^2y%P&6KJbsLM`|=CfB&0blXfG9FhCx;rIBm))2$o%s%7m*YoQnkeAwm7i&9%0PpPvbBFF#(OM&L> zst}NeK_y!Rf$&g;Cb8&(F_>FJ!&Boloja*vSltcd4UncLPL++q*hG z+KOl@nMCg2&8=G@Lh`j!p2!ujHYU=D(Jbeo-z!l>$(%wDr*-vI($eZjA76g_=~d3+ zVz5My@U>PnWhr}TjhGZnmP+gDFRtqpCl7AI!i=HR6qeIcO(L&giHVlutWqJ0v0eJ4uBy7 z0U^TTbTV^2&3#$ge0%(0hZnD2y=>oob@Tex?&cgFSa2GL?N$cP8wMuG5lBK@nmaa2 zB?SX0rjalV8BrtS?iUN|q_xNTp<$#LmZ(NOtUz^H`h`foxD3Btn#+OtL`F z!GsR%4_GpS01%>KIHCrQ)Q-@=$uU;YGp|Q)s!AUGK&84Y6;qaRbMf&Xe*EHp{NtYO zcmMq#t?Tj6|Ih#E=ifdw1};0TYgmV{k~h#Tq(h_}iCUXO1AV{0+@D=#vaw9s4(FeJ z;t%)lzI%OtY8DNd)dd2}w95nR_7_jS{5(%vKRmp8`3-1>!^fA{*ESGwBS zP-;DKpmU58=NDHXr0+l6|K_XLU;Q%tHa|H>EQCcLUd8CwKly|G53Vs5efR3s*Dove z-R9C2AMS7Ze7kvmISfyGKjboncPgHJs<(IR>EZ10Zu9g>DqCDm-Ph@2E7_BA+}$m= zU*EmO!|mJQSxQ%5*!%kO^{dcQ*ZJ~{Kl+_NTtD1hUbph-C(Fxk-`?D74H~Ji?EXwB4(2zTL2~sW3pv7zyS6rV}gikE(KZ(${az~>qJT@o2@9; z)X5+hUwD)hW}OpJDGV_vCrVkY7pt5TY6Lo{6ZD*zI-o9{`n*=*VbYa73K$B8I!rr) z#%7iX86i0sK!lSM2U!Oowr&U*6q2L}NC7cjgKz*;lprS(2y4hCn30gC0E7@@0>Z+c zK`MpdR^Z))f!0i$3W#bi(|{4vn97zWT2sjk4NNmoldK`tqUuHJ(`UQQ<7pf* z4}_SI34tS+Fp#Q?KtjU8Oo)WhJu09CDM1A!0sss*kACd4+3nHhoAuu27dN+W?CW2? z_~vDN|Mqxuw?gl^Knb|mV9qHMus8wX=1dKu9?Bp{9D|IAoUk=URzxogUQ$%t|EEKE|vq_QwNEyY+8s`j|h)pPh6QH7kixCqLxb{?NIR%2& zhEZ?XU^@~BPaNZByWy6GDGlIxizQ0h06O9s;EDmkh;Ebvsz({X7qd)mkrI*x4_p^d z3CJKIQ~(w-FWwU;geNT<7d~i0$^P@85kmWTt7_j?W$~_ct%U{`LLciP9*AymMW8$>sX-^|Q}^ zkhDq~-@bWucZ2Qz^zi;2je{NTGST$t+4)%RhU$t_U8(ZrDs0rg40@ z2VBk`KTj;~r=0e-9&;H5ig$H@;Xin;emarI_;gaWkT5r-rU3YZ`b0v0(VMC2`~6AMOoQyW@MGXN4xW6O^f=;XV=eu@EF&7j{(}Pwi<2d7-ObXGUzgR*8ucmq#X-7b&O_~nIjw=LVM*Q zGl^LWBPI*A1WB<&xe%R$M(rozT~Kgz;ES*$h8iR(2E(3pwjM#ig>-2YVbHfb)||u8 zwE__|gB+XfP_ksxfo$3rVwPwLndV08ZRK5mIIbu4>SZiv7v;$p<8*#D?gW@PWgvnG zWC=6GjXMwv5`~P2hR6X#Xaoqr0pw6UdN_Dn^~VZ93r z@_+H~{$01gwgv)nfnh_43c_xV!g&k_K?Wc`KLb5s*=BK`m?+GV#bE&om}~7uY=}c1 zvP{KuPUq6LSwMuRop7Tp7y=^f3K;bM@2wd>rcX(O>(6d)R74P<~sIU=QGJuuMw zsdorM0`K#>2BdKcTAqFS?E2}G-Tv{gxs-fXJ1}eqoO%G4q44%>bNJzRx1ta5)vfDWX_3vJEp4*wIK7-f}4jU=ahR> z4Ihb`GJ{zJ5eYby6kYQ;stb7J0NQa$sa7=;$T_K}-K1dXmIvWHVacm%nNkUAO_SlK z5Fs+VBQ78qQJZst@SG62we6HWd^#^SM;?d~P;G(+4hT$xAYxDkCL!`vgdhai0|J10 zgdPzCcp7|fEie%wGX!!XhSn`1gkUIu<1kVsmyVc_5W9wK!W@IeR^-WdnGng%Q$cD# z1F!)zN=9ZFkPIjS$s_N{s|PT(5Dc%#lTbJcxPe=6MwlZDxFQ7*hhuO;=nxHa33n0$ z6f}p7hy|kojDU{nkTODtU|@_u03wJic{B9gCE(SQA@*|n{fBwwZo}5m%e+)FkCM0B zGEBK4)VpO>9j0V!H?t(957ooA1FdV%9t7!rUYS7=sndtkgVKH+5!U6)A76d+>>}=8 zF>bnpuC>d6({?MyW$5ix_9JQp4q$31AeJIUtVE;hEIbBBHWUc3Ln@d$*)S4yjskwb zyw%=V3i}L{T6M}~fB+PMF02YpC5;R{3xguCq?Duc$QfgeNHCP73d1ysE+yl*9ZJ%~ zkqQ+vbLhP+>L}bVeAU1C_Vmk_`tn4_m5dU3&xNKjonK5-;Y5&%WXeI^ zv?F^|Bv1+AY2@sLm?eOiC>T7zAcR!J9pK)I#ZOR~?%8-V0j!tFE z)QK~LJA-7U&u(-Rg@=AWpFjW1?p|O0?0Me& z@YOGWUKa;WI{~F+WppYlF(ye&7}$cwgqvwP->3OVJz(t?AetXOwsa?)Fo3|+U&Y32n-aR6ptru)-!R+ z8w7{7=YjW+Mi0o4_G#eIaXW6Jp@c;Wqzr~JgnOupW6pxi*i8xC0MwSmHS%WKjomN_ zcp+Sg3xhjF@DzeX4Wx*!h9lF1wXkuL!%d)|2S{>C9+eWJ7lGL+6IAgv@&*%7>fr-r z6JG!)M}#oZIUqwAlAvcNLbV+6tsP@GY;g9F7%K|mFN&^t4SJA$*Ed8`ou zkO<}gBu2o%P=t&+2PX;#B8-#`H+k9YlpkMmT(#rf!|6_Y6v}gG?`*o9Z89eCxu0&= z?!ai>0RybNt|zDD8olXQ1c=Q+tV>uTxj%L%8g`Sn!xw+$qw|kH@!Rhha<4~=44|q_ zwgXRSO~tZdV4o@Pn%*a5h)j%4JrFpts{@4R>?RqMRDYfLL%%KiXccmV)S@G01OHQa3jF3 z&5n2L&Hc^$_VU~M{mYx*zFJS);qLwFppG$ZHv1A6m*ak)!FwXYq;AbRhdW^)5gHTK zq>)4hH$r9)5aV%zZoV$wH7ElFb`xkAj0XI__?!O`1Xxs1~jvQJ4}kXRQlHCqe7nPls5KtsQH>pU%he zYC7NL?PS~QVfWE^_GsE(1s?9+{pPE0|LjFY8K*~MDW}sdXf*6u^00Yw^ZuKMn|sRX z>U{d~&;H=?XSWfXB6R*KvQ5V06YZ6bfMQaz3QproKCL1R|EHPy=h-S@V8Z@7q`3 zzq{#in#T|CfBXE&c=nfmFRl6g>)#&lS0o!G#@$(3f3yF?zd<)2zWeP9_qNR#L47`* z9Nnw|_H`j(Na)Cg{mJ#T_T}L)n@M=LB_y%d1ppxzox3nOvI9#8&xMx*Gf6lga2|YC zjSi!PS#F9N;H(VZJ7E(UCx&I#vgHw6c&OUOk<)H0YsaxX+$}`n0-;J^)?u^DXM=dv zf-az`Fb53)kO63uVZ{*QD!dU@CH9VWl z3Cr0Cb32~uIHdpphY!`!*c;^JKmFnJ>G3(^7rYPdoIFS;F z8AZ zdpSPL>Eegs(F~@B*Qdr2AGnM+cA;KFbV{fpt46?Bqwl(!i>!mQ+SX#AO%@;1PW3B14Ixv zXh0fa*gL3bH|rb*YGZo(B%ZeM@%o3}51dwBiT`tF1E2Vvz5X}O!< zzJBxF&FdGhKz3!DCFL{?_xHDJZ#|$p_I`i3pQqja>7(bL{P;)LUz`>3`|n^FSp@4x;2p+{?!v;6KK{pIUVcjLvg;X&WN z{eV^r!<4QLcQ2p+{^x^y^!itS(jPR9;u?y6&@hd*EV*Q>Mv_PaeSLnN+r25U=hP0p z@v_^L-Vj64oeBnqIknb|G53yI5o6pXBTNwnAWXe8qXK~iDF9*w4|(l8%0SSSQD|3S zUC1~trZO>#@P6aR6^;lgN!d;EDo-VA#nz2Rg49 zyI`kar;M-?q=@hc;*QAZEf@h3Q(#O)D`Ejg1Pew7Spx*veuzlCCP=U;sCuvFbYO=k4&B&6%%-OGqhffi5al1P=Wxx?7UShTPD-4 z;wlmZ-~`&B0t|UD04FqCazv#w>bJq82T>w`$%FwM6Sb}SqD=<&FJJ`Io0(q)pgWlT^?aKb={2+9yWC?h9?=pjS^ zfgBXx5!Ey((Cws}^l*QAGtYMqcQ^3%ZU6Qzj*D6ltaZ5BZOPA{?DqR%!-VQ6lLrei zCK4hH4xm6MDrrYU$sELqlPjl0S_9!PM40vj)?+miaK$v{g2w;(U;TH$D<;Nqf?$en zWkf<`LWh76bqNkC8MU+LpkP3UA@pP|RIAOP;0~}>?+UIpB{-jkvCzeshbbok!ht3c zK^CG?Fwc@VffbTD2pPK(MwPKpZw8HMKtRj`Bw}CO&H+wXhY*Qnw%J2Rn}+k>|McvK z!?fR?-hKP?|K^{)c-P84-u>pKyD#gKVWU}^zrOnDqaXh0^4az7(uM5d&DU>#^Y-y`;6?bE_+v!>}c15`z#)%ueSQ!{Kg839!@upCUZ{wQkGq!@S>U=A3J- zz4tliZEt_8B~_#tlZvI7Q4=Q!kS_uJCI4meF$fSif#BFMY^b3vJ2XYAsOoOr=55c} z&01?VV~kXu2iBS8_&iU;k5ep)b@kq9+pb2Ewy|~-?e{N^u;b|UgJT=wCaPm&5n4^F<_Vz_hm%T+Y# z`}2S#m3X+%|KN|lD$g>XKTtZ1F=~BuaGEg9X+$H=;alWPKH$9}h^3nmF_MFrklZIC zivWVVK~jm>Gr7TBw3>t&5e*K8SqP#h442uhxfawlJWGnfDP`)7;58o#I`yQgo;8E};ZXWOb?aRy%ww0lV1hSjp{yhEEdRwn`QHbK z=7Y2fo^*l^LL`^s1>^#9WEEmi0S&0YI$@C0kf7$*7BT!5=307lxXsJ_FzJ%%J}VKW zG--)6Wt|T!nQ3yLl^@Bj9HylH+&Y22Q+a+rVp{rht@W=l+i6FX6iM9d~jDl1zm2eh#xX3AM3 zrRntS@bLU3xvU@8$H$9vYSyzJra3Wqe{;L7J(@X^q`Y}E)9cfv)uA-sKTI#4eR_EL z^4ZJh)BT*5i17pV{o}X);oILlKE1#0qdwk-P)U<~`ICR})t`Qyzx-r3V%DgowFb*Sh*9C2J;? z;~{4y(xl*^N}}jP!~?JajuC05(I_Zt149@<0}y?uG&6}XAs-wLIjXFE`b>F)OkS7< zC)-JiiA2|!7;Uh(KmiQG90hVCB%*K+p#&M>PJ`J72?L-QP{kmqQh*TbjSw^faH19l z(8LIifF2Q@LO~!1k&|hd5lDk5*y^Rm^TWBa&`f63d{qXVq zxxe}C_08M*v34IcsXh}rzAA^Jcfv^mNya`HnK&Xm!o3C3eHt4lmE$ZWO%s<-|AC2urm#+^nzADsn@WI$V}Mv_0z6n_?9sbS z_XP8zXv2d@X;_#i?}pJ@<|WJNkaF@xM@~9Tnlf@ZutbvG_^$Y{VvNpEk``WqQ-lba zIzlNpAV-$Q0g7TI;Q`xXCu$(o;UyoQ@z1}`^VH+?tH1i2$IEp0>XZE0apF(f^~2wP zw^rEt)R_l)zy}d?$tB5jcb`BYnwC>JK9_M@4$qF|C>YK{3RLCNd7*_CD8*6Vir{rU3k_Tk&N-~8s|_rLr0n;+Km*lv9vak(_5 zq|W!RU%dMA_2H{emS?*E=KSI957!o*Z-=`V+zH&LXLs-(94z9^qql8bmb+s)-IXM5 z-Du?AHwF_5QZC0d9S;(0TaW9tEHfWG@nL;;vHo#-{(M|`f4rT~7ozFOaOe2sr$0Kq zbba}A{@u5a>&GvD_Gj(=`OWsbuYU1)I{oa2fB!!}ez*}F1AB8sgsLzaC90N-65{i_ za=EoW+5Y4T=OH9kiL6`7W=voYL3oValCV!CX%XF%6v?CxR==&zhm0_EibMi3W)%^x z&yMr?=^`8x$-eI|Pe<~&TXfNpit5C5YfsnR7^qzaZ$9Qi9G+M)>gnNrQQ>yuX-WgE zk!%~w9a-}rt9|dW2@NSH9fy(@37Ch>KqE@LD4D~=1H{5w9AI)wN6zp85@Ck%#2}j0 zDX|k#MP58B3r!Ir&Z1;hk~)E>AadrxJSiE{%-|@HW>k{9LuYUiZqb=oVIUurC+~y8 z13~O?MdF0!WR%(6L<=HhfZPFx2-qkRNheoFC3b`bm{NgsM`G&GLJ;!ZSqOjNa7vmE zlljBZ9><5r@9T#T0O`17=6*eYc)X?Pb=@`Ews-g2GKnT+3H5cmdFIh-rySYhcTZK7 zl>O`1`DcIni74y&`{vTaw{3tENV7UXI!u{Fj$xxa(8I_u*1^R@f+^gQTw?Nm1DjeM zk>CdRpkxqH=BPqy<&;X%m0du9)YSUzMqocgw@@6Mgl1Ro*3 z5J4t3k5MJWg^h7-JQnwZ#_#ek!ai6~sj0Wub?d8cbSXz~^~zC%_Apo0sGTr1Mj{C|HFHYA5GAr9 z0$`%Hhcm(@%tvJ!A%X-ohX8Bq*fH)|L1QCK0yt@#Pn1p~p zir9h#G_wXcXT^M|`PJwAE}q`LiIFy|2N`a%t!~{&Xv#FA+Ta49S4nxh;AtC&!|X~W zADa8yOO#;A{`IT!^`CqZ#CZ3nBOy(-n^HaG3BvFeu#S`Xx1*fYD~Tr#i#d_^y0T~M zk3uuC>lh;HE*hPP8i>P_1lK)CDBO%$I*5?TdySybg4TkR#oUm240sr^7)9|xHmtZe zO3DP8+4{`XNm;>Ovu4RINbDv=Nh3ChLM2|jz~SYwyqM*fB{3%^Jvs&u!4nwjOyNwO zAQ(v!h%KP*M(ANanxLNBI@;sfpKed@+T+{XyC3Z7wtJeBk7dSj$@h0Drd^ojzbZ8!u<}{jfm;du${8Jwz zTH|tXsv21}v5XGmU}t6!I9W(YWiK3-CW#o7Cb7+kW8Ep;Ut4TXdnd}Ua(A2;UdSco zxnL?J^PG-HWD-d-Rj`wuSR+N^v6?%@#14XhdsreeBw=IXWWKqc9H`{OjL663NyPeQS<}Ry1{O!V+F18!$y_ z37LsSkPl0*?S9S-O)>BNvU2CO&BvUM`S$L^<#y{8)#_7-n+YK=ix5!|D@iJCG@az~ zcx~q19XXShZQoCK%W|3m?BT?!Q;&n>l%C#vOCqJb`1tf)Th~2IQks_gRM6^{Jm;gf z)~D0KRY!y}?q0oeZ>lsM?)q(Qv`^EKG$qb@WCH`MOfR>`?~)IU>GJlw-Z3pxI?6OH zr}^PD9j^WD%P+r3Z{Fy`j=N|38-M%uckjOWzSWWJ-RFP#=jAf}{+EA!*;?zvwhA9) zJx5T6n$Tz`<_pXrwbiC8+-b=yOOcD)@0g4eKY_$f=LXvlnkT4M=nYP*~ z(K4O9G%6)c$+ugkoYQ!CcDimm6YVTenwI&LOD>672^gF4%dm*d$;4v%be7C zIizQIM^>bNpP!O0H7S3+Onvl(TFbalKau4CH zdlo7KH3;rB69y5%ln{vKC}3wF03k$-JXYcZiIaCG2aBqeg-F^8FKke1&_Ot~N#XcCC*tSr%QIJYr8k~8*I^7Mj z&ZBTL-V`~|6;y~D3HheN9dm>;YtHC6BFuu2LzKHw8l#6x?1p9H;l0ItVV}8vBsjYZ=4i$H&6v7;O<#kK6^er7dbp|%FxAW zaDa%39AU)8NdW^ER*%>a8X&{wLq=;cuA^~W_wCYdAGYgx{qTc*{IH&Dg4Hya(=p%A zbXcY-B}gFHmDPqOVYmlNH;{qAs)xII%3L(Flcp^_`y<-k=TV!n$AHXX7B#FfOQfX) z7bXIU{2#yihyAv>x%W+zst9;kB(m^;m}{OrdMd&Z2x8Gx60%b35j<*jq}${Bw%Xdq z-o3CCYsX3E`;t%Pq?Ge9OVgB@Mo1EINzXCu9HBj23P}o&m=xYUJCQn^Lr`Y+Yalav z_)KF(%*>al((Go&lEa^$&QJ1p|J}cR^CtcN{OdpY(JBA&SO4z+`v3mbd#92C%hMb( zN;yQdVMd%SIyr82Z>{t%@c(_|HSL?ew3EB3zueY0TyXNEk>^OtA z$IU!~JbX5Wv*)pn``1V1R_2HMaw5r3gKoe2%_v0r#p7?k`OU9?Fw5I``uRWpS^Dgc zp8odl-hcPjRifn3B9IXdp}tlqO^8pQ(fPcM-J+yMTMN3+c3GWl>#F*6cEB4(ZM1{U z66zpmateshQA*wR%dIg_78`f)3WPW@ zLM#T9QfCHdHhjqrpi7=Ri-IL13NRb#UfDwD3Fg#qKnK4R|{4g;8*uMSt`}bGd+Z6HP(sgVE zHWv#|S&q@?WpbB&Zz!o?Z0^b9%`JB5f$VgzU%ozk_Q#*v+U4@pBaN{)nljC@j4jY<8jwd;Maadj* z%JG@z5Mjzf%nSxbAd`b)gakN&dH@UpdyL^;A~ql4yZY@`SF(5SuaEo3vwVCv&YgU> zd??TEj>{rT;dxR*bJpI%Mz~R83kwSk33u|8l1M2&Q%*B$5*XX{BX!>+Rr7&jw!_`W z(2`~5l5!%)l$Dgv=iB*w+s>DIixJTcy~BnhtAK=ps8TM>6Va5)ayZWEc$e}*6N6a_ zu@SkeS7FaYHmvtfiKw&0pp+%CP`EgeCSoQrtXPR8z+ITdka(tO!a^jODZ!BmwjpJf z6w@NoL?i@a6F8?H9&x>?|L*(P-zI+ba3}3T|Nd{k{q4W`FMs!84;3Fxp>@A1R5c9) zN5b$nY?V12-D2ymBc9$p)tf2N4&8R_bu@5aD=D-4>ETtL z@|++1)}OYWMmP|jNr%CCq(RBhnE3hU&t8A|QW&RtJ>AAk0Xe-z5S@%iSrzyG`A zSI@rs(NE%-dr04E)M$J4z;+qY-TRhuuq-ji$m#kaQBJIN-)&Ux98dcI39}QRrCBG9 zsT7HlB1tD}R_>B{!<^OkG)7z=hk-VXx-rDr)(S%OFkmQX8YXQm;iseIIcKX&qD6#n zR@;z#441aM=cx#d535%qE);_;QQf)OsMJRrUXORvf%p_9HDdzm#wn3ZMuMDhSVA-T zjYzl$S@Y>+J}_d0(f$#U__iSuNI92|f~}z>h?6%0Sv5G3!wBSpw9vTe)|i5gsXK*H zt1uzY1RbD&^QdHU(M1F8Z@XkhHbBQ=~w(2ywlFLNl0g3Ri>_#LOsA(3C3U3aM zAdiAEQ3!~r5e1VAGRQc*Nemv1Jb=X3g9xNBqfo*0BJ0opEdAo2{qJ6X_FzN}=`byw zQtiF+Y8!QO^YOZ!=mD_e9)yx z2C)%?J(!adAr#@zL?YynXrK%P{Xr->F!tV)4hqbMnQ1|Mi1Ko|s9Y#%)5PN7DQTYhuH-}J zsZgac%vfStS~U|+dmAHq-G-|0!ik87hk%qOhr`6p_7E~`WMrhVOZP@;A|fe{0j|N< z?y;CA>0^N47s-Wb9m^-T-(5cbyZ@$5&(mpC=fD59{SW`oU;pMjIGlqy_T}(g3cGdI z)JEM~gsZljC}z@J9 z8gKg*YUSCp`FLVQ9cSyU#io2$->vVy|BX$o&kpHWKzXBKtx8IKI0m|RkPqsEK;{1a z*%v>Wj|ZxnOQGBj^XJz$-{jFiYkPlv_s#9{!|z}H2w(j3U;OdUz66nswEpt9&+f<9 z|M;iVDC2SotD`=S-Z?BB9&H*5tB#tKy;tQ-PZw$*b4r@v1_p~JW}(ZC{6?z47`L6; z-q+hyc$)KeyV<-XqwdmrGt)*e5jU|u5Dpei8NpMTTW^s$NC^fjNsg2R^Rv^vJRAs| zSb&JyMt9NE#Cl3~5AfIrNUl__yYB&D+%IXF^Sy!=SdQo%ZrskuENnDZL<@w|fC}kC z1ipTZGKGVX5fqn;=X+>|F`mw{1<^!M66Ku3Be`a(&D%O`@zJT-kPZ=w3N6wH z*-E`5f=2V4LPvB#C*cTbVTeYK4dFoHRlETl2%<4u9HM4yWX1`733BfeFz`e%T!Ohd zZNU-IEFw}1b~1Aq35Qd_gB%D-08xklLRiRHWPW|mKl&G6{rNxt&!+pd3k@+&+%#EO zP)Nvzu~#5qxA5V&9{Y0Kwu(-M%<$xL)B?)E96Vai;_B%l#HoHXQ_`z9@1{&J<0!qz zA_C9EG8KY_CSheuqMQ?Yj0pIgQXfW^meCa022+q(cxd8ii95wUQ4?p!&cdvxKCa>c z2@5zueTZ}Pkc&yqq^7V?fsty~FjL>mSDT()+D-S#s*9yF9rRb!MMVrv5}NIXfAGzwHnviRu4+hcG+A3RGgQ(8_OY)VqZh%JU!j&SXoAe5%(kH7k5uX{?$ZY(*^Dtv6W4?LctU(B7k*XtUor=tdS&f==LJLG(rUOfNe z%demRXc2p-e!gC&-~LbkfqwVjzWhA>#b5m8aY+$*du-3se*g8CpL}+VDUU`$R*FzQ zz_*BAVWS22Tisgc0L6eXCEj};Bovw4!_jvy*}FhlH4!O@oN}+-$Iw)yHovV=FBrxP zZ0ufpv)#?O?K)AiVg1^#*TbyGhcqoZ9rF}})D{l)YXYg$M8>1%Q3KuDX1%+8SRX&` zaY7zpeLG**uzZsHG9Sv}B#9_II;kQqKx!Va4HgiMbvI)&(Yl83eXuJ9CL|zg=;U0Y z?~qC!A)SxO6Czqf7$+hg@DOzddB?yAmlU4h92kgg0Ln5UK*$ja)nKK%!I-Z1uw z4~c@M?z_5!`Z7&&oCBn_p#|@od0?2NY0uB2e}qlFH6P~f#@XS?#)YX(u-(1Gn0Fvj z2>Y(o!NrslV}yzjS(QXcv&5`qAR*>ZfvK_?L>DHSQX6h&NG>Xzdv($PIf*3>x;r_L zyz>ZG0=pw1#ABpUXQ@0Lm@H4zuyD!bUW%s~kVb4_odqNa72*mK3JMY@3KM4un6n~! z{Qs>^H;&5GeYDZ6UiWqP^JUyR`p8rmSp=*v4*5_Dh``lztYoTD=E$s*L>A5^m7FNV zV_;%C%bc)erp-b|$KC)I<%Hqj7%+Y)v`{Dqazq*sG2wmP_jNyC&TSprdaKv&x7GKJ zdqXpalZvr3K>=k6GFESSp_I8u@QkwRl=H&`<*3wpi%1~|=+$G}&BfVijE%y{4LX8{ zClG*POi8=3MEHP~5RcH%Y7vM`1KtZlgikPP#^xjLUK-~R+|7^r^PkW(XZ3tX)8UZ% zDCIsA!K+egm#Q!9n*iZjZVpi{ z;A(z;3SC+qkKcSZEcN>F^RK^t@%n{O>X)s{4vv%;kYpX1Xrm`kAAS1p?)Up3n^8nK zm=pJNyZq{h>zg-YYwMevS-pL@1rIGt&RIyO<+Qwhe)wYf(I5TePu2AMcOM?N7XAUB z{Ja0#e+_$c_n-ar=Rf^Sr_;xeW4~SU7x!O&wy=_>4CiS3Xdekob54{3+y{D6;h9nz zQXo2}#dVINI?s~WbxKJ?SY-rp)5J?+qc%x^)?2F-l!?rknMmUU$(YTgzPaK&3SDv8TyY(k!CH7Ou~#J=G`O)?F~29-bd3IV@?W zP~JT;ca4z)fQakN4k8RNxR3#K^gIy;M+hstaR{OjHmIruTO%JtM{1VQ8Hn!OH{!0D z!n*G%z(f!|xvM*)N0`G0L`^p!Ty9~E7}3p$d4K}Zj5 zkc1XV9wi~WA-pGx4YqnHAq6K#x6W{nAz=(uvLnL4Mne$=@CXJoSOOFwFov-chX!aO z(s`Dze!l$dKmE(kpXV-cJ(4nVLX(^XJY{gZhMS|uxPgL_sFsBjrL2c39g?B0sFhhp zu$dywefuc&CRzfXnOO>@9LSmLQk2a|FnbRw5o;US<3^?UoQDQw8DVA}9FkS2alfcN z3MY_}rrP&ii*r$Dc%gocjG-+pr^ep)ebAkt6$Y9+2K8G`u;q@}DFW0IIPsPaB-uv` zM`O{D&eG$sNMX4<2$PaBtCkt!U}eef%# zxwSER8zp%+k*q0rI%+;7W(;yNAI$63)~j`^b>Fx1wwrab2yVk8E7Cv`VnU07$%#BC z;-orhb}E$EB_>g!EF~jYlT3yncP2_)@~nXgKAi8Po9Qz|f?2feNe~_3U`I0skvazx zHDVSmJXVWNlSEI--Ni{2KI7wiVvUM4_7|Vy;nNrR{_XnleI09W*O$*;>D`2BF^nW& z$l$)Wh%~B=)<+t_I#Z~qpxHbD?%4${VL_uuPpPoz zoXX6B^T!X{R=b$CwCndH>GdluCdWvpe0XuZt^IQC?;q>cE>G|3U;Wkl@BiEX*YQRA z<3IZOGs?SLzut=2{c^-+d%K0(0GW4$`EGmN`o0gcvAOzfLXB}g2)M6#{TTJ?zUt^` zThx!`Kz+NRtqS+YCso(2yM@M}T7yL?k3QIYpeAA1fII5GwH?H6zFx;&hAO8?r%a)V zY?zNBv$7O&V(p=9&3p7#$6l+dtBn16+rwne6PHw{Z7`9x8*L-);9iNGbe4!@)q}`g zYeh8TaCOr?Qo-S#gDD(A1a%WafRvzM4&o3PMzFG*bAn~p5)p##0+5DXd0eGBoTyep zgm+mxc@3YKHX;HB5r_7W%mgO_RkBK1q9ly7phxdwTf`vTX*B9&aBvG`j-A89Pwv9h z2`l*)$ehd(Pz}b0iU;3y2==G2P*MI)` zi`n}+dX?^7nvz{^*ITpA)=u6ki`~##INH8Ru$mvAD~|WF)#^6FQCpx}F_wqXHzGn< z_-Lc=R#NX)eWbhH)z=Oqp`G|tas?B-(Z(!}olu5=&1kf?%k1TTFpnUz5w#J`JZfXG zQqYjHz+?g&G#E&l^5}@3j?{+LqN9f=4|p@S&YYQB*ld-@YC(jZY#=8s{FeY|j54(DAKBD#B*7f>se_V6jBc?nZ^6_q- zQ%bC8t|G44REM+bOj8MSo|Z^S=cJxe=`;CVst2u<=7|uiwOj2Q)jE7%d-MQtFk(t& z#1LU)g6CPYg_L~;H@D_it2~{bdKIsba}tNUhmr(y5@ccnk6{o#kwr{Q zc}~5Fo1^V@wP64@(CS332i(#f(i-KI`W4tY-Nj(vHwPjI=0urEJ0l1g{Ro5)bIoBC z>rPsG?-8M?QFi8F-&qTO_t&Yve{om#^z8PIBKC6kFvh7*Bc0VbnAw zRz1Z@+^VbEFrE1M{e$?pokx)KG*RgF={ktYS3j|bWp7t4LNm`q)CV+|>2AFK?2EhOsanw>Sp`zzq?%CMOH~V)=Dz<$J_bC z6?L1B_bCZyt?T>!yWfTcrO@F7idpOZ@y%_zoNrfd;DEI^m-)Z{FaPa_U;Qug*(ayd z;t`J@gD5XQ`nup#Ev>Dd2sJM7rU9umY%;N$%LcuNE0FD5fb5)vWJg8wd)|4 z(d4@BLzhN0Jv@|W55@s40<*eN@TiZ|;gs%=C5aG^Mly7NU!!$qB#L=vUpo;~-{@GF zI?bAvOl3i*v2V#KcEf~q%>xhdcN%W9VclN6s#8 z`2_5+Vwi@`h;Uip1I{!?7=cC*fds2y=TPGPCUlA|0GJWXtyE`n3f~)82?bo7l;tW6 z_oE91XNeSYA96Ih!WNJq987@%YLFDx!z!T%QBV*W5zxRM2nv)S2^_M@tJnM2|M@@q z{OixAq;9f@h0nLH&gwE^UOX(QHCLJGcB_MV*d95jaLrhWM<2aRjq{M>G9s`JO(xkx z{4mcEWDN|Y8P|(q65=L;*42BZY%UIuv7u?=7F*>c$U=zRJMCAu5Dg#)YPMF$1Tt)d zQMeS^P>!+JWRl#n7*qDTi==d?oLx+ENt8xniV-w|eGni>;sLI#s!^ssq^N44XS1p! zJ-7!^IN(vp_wW%t*f(}32S5QOM2uv++rZ=Q>)Xp(taM>t!-_Kx@YRzS4qs? z2@$QqdtV!olT;>Sc5Y2ZO@ozNLL@c@qqF-u);?lv`?|WDF^nl?8o@B)mLsw-)y}qy z4H2Q*!<3o8gl4vTyIt1nt=rRPy|-J#8pM@2F%j%4irpK-*u-QaEJWx4a}FO@ zgcCNCooj6W;j;h7e{*^K{^8T*boZs|_WX~3asTtzeXpa9UT>=pkr>@br|uPfbhk;l ztuZcb(CzsrukT*GetPrGFt^?PwmH>I#hYE;z9Et744K4Tgz|w>F6b_jg*?YfPoez|P#KDs9@bEMtp!$u1cBD_Z*!+-rB+aFplAR^NDp?&-8T3 zGD^Wbl}=VkFW>K>BL=%rk~|*{nGMLOEi{YRh!mkAHQ2(;M_6k{@LfPG^$M>P8cyI5 zUP6R;3Ia7VRFP8pm3reKQ0Eb$X=o@D6d)O5YG z8*3)n$#-}sfVhT#63IHnOag4*7} z=7;I-KmSWIYR!#>6OYDwBnxF19=05nlD5_diMI+$s_y#=micf(RLVzR(RHY#;o+mZ zs!MQ=Lc>M66izPr0Jl*LnFi->v)AU8IilHci$rO3GgZi3qeUo>D>NsJjNa2^+bZ11 z&k^Eg+F55G$#FXyC$mdC@7}9BNm~(V=e$=mULqwaQ<|8UB!^NK(if^ZK?~Ck1}K6+ z)Pst+6C+eYoG=hO#|T6i*n5zRbp%IL-@C8%_I7`~*6Yn&MH%FnCZ5QaWttPYm36>_ z1`m=%GBZ&mSV|;JV5pLC&)Np6<+0&*S*sjdc=(8;V6CjfgVRz_k|t!OA%(QW7&)ad z;TSze$Ef4B-#)Il4{x7-c&zL0y@i(ObGNWE(R6SvPR>j!DP$p?p`1!_fu0UTNLj|% z#s2_nTU(liZ!H$@#5O`yqK8jGEnMMjRDvajm>x%)%{+`Lf{uvk=sL*UE7+OLm_zHJ za_5|bHjnVIx99ZTzx=mv|NimW7k~2nr$?dX)z>z!zbl6YQp_bWi%j`6wRSD@J&0N1 zBSN&yyyWHOtIs~X|IOvY*%FyJA&pB1^{N(FTKY4XOAN#FtPapQ%74A+0 zqwHSKmmPv>s)vW=`RmVNvR^MsbCPGv^YQGHXD>hd$;Mt~V>eS!}HaqX<-9kAQ{$xlv)JCb4}YK?Zid;9D2F4OC0Ukx59XhvT? z{j?+}yDg_rEhus(pM|)#pe1+0yrh{`5|u=j2qaBp?3)*fC~@)BszT7KJCr*iymkNZ zgtg|W_`WvsG#^r}>QjCCVT6V{@odzC=MMIe)7{Kv>hm&7jGP%7{% z25aaX>t0^IWG+O5&~@L^)|C;-I1-Bp53=E=G=!3nWN~nweVN!gWjE@?Mj7n4&;|Wt zgbto$XSdC10+Ew&N61hKS|;qqn1WPgcqwKU(Z~j9#9HCb`wrRBZ|;J=5xUD}c5rg; zo0Y>z$4CR_sKo0YjbIc>g8>dj1A|J!$_Ce884`yd&Le&^L z8jK>gK#pEz+pKMzs97FuP+d8*fy8QrkS3zo-GdXbc56fvsar66wc%Y;>NOGz6Vg3f zbD%MC?;+|;G;}WRbtGo8aPQYv$(=`}GU#xh=QJI4dU3xzm-*BC<@F?o7c3G0mbsiDhz)L~iULiiBLKc7gk} zP=0A;@;LnFZ`W~}Pygg$`P>!stAF|b`0aoH>)uzA5N7*j#kq$^hme zZyjT$2DUXbEh+Qu3Li5#JN4Vvhkgxx@|M*8=pO%N` z-Gx+pr{HOgUGqevN*kARx8a)&VHAnyFYZ0N1feiuQVs?OLc~bf_lq!sQ>3gE6s^n2 zf`Wx;cw-%15FCjGzC*%-5~s{1~k zZpViQnYb`dpc{Gul6Q9{iSuRr@OFFi-7l5n)lWb9+0TFW`RVoThwsYiJ{=}g-qw9R zU)Iapcfb4f=#k5^93C#0^M3tMPR~;|qCVBmQ@{JL;Qjp?Lhu8XnYaF-8r)cZ)EwY6EKcz5o?=jP3IQp{o`5V4M1r3j1+)bh zlsmv;bmJ7{-Uwq0ry%t(28B+<9Noo@!eki4!Eg*Xht&WRx9_Vu6s`19@K zn<7#zJdv5xu-N)YbamQC++4$=kQ{T)rvq&_CW`cI?YpKsQieCS9b-+B8Wz~fBVuel z3Rz9biFQES9F56%5)iXR5=p1vVCCXFmlClts?ZW3CL?o%5mBnwE(^CVaN#`|&?9>A zRKmkz4Cf(+wfC(BG%*E2II=SS7WFeY} zgRoHuc}Ea1$Zn2IVdO4YITYg-t-4KK!)~{}dtJAE_kFc}ATiBiNeXM>EKD4Pu9`sV zQDU3QP$}>#<2uI(Nes$H#6yIV!O;~%2gjuo91(O!U5L#@h+JG?F2t#&Bky%A#g=EN z8|8^TsV8NY;b?V)PcF!Z(Bo(2i^bAHnu6CR0K{GRkDj> z3>{A04|$QgCUOST@W3tfz&t!Rcc*L)74A$a+}Lg8yH2yEgr&vAA)pwvIa3R!t{Zt4 z7Dne};4r%;(zhSBcmLo2@%+O_DtD)c*S+WH!%3zC^Hjp!V{@f0L3=NUmwtXyqf!ob zd0ZbaG|d6#*ds`$(%Kd#Wr~Q}`{w58X4X29A%?S_a>~b~#YH_&mvuvHvG&7ZaUN4T zH1B=egr(In+=D|?5W^!~*ZS_fe*4`onfl$s{g=%Qg!rg1!IKw8^sXRTqRn_X*f+qOryG~XS6`sMtbuG({A zg-W-o(N$2E4DDODmZF0WGm%0|iDo_aTr!gZL0hm!W**&^xiCSAqTgh1-8Pcw*GN~jaTAMU%gc6nRhZ^EntQ$n_P%9c4aGYUwL5p6VP zVL428+ik0+S@xX}7(4AX_SKQ$x6a8hA_Ui~3A+<=9^HtUt+II_;c2J3Qz+hMJz`}*+K0gbV4}m9u2;mB7i4hz#1q-PPy?B*>`p^F9t9#yiO`=<^0T7qQ7W*(W zL`(DGmum1kLG`8&*gLYcI8a|B-;r(3rbJ`nm5SM=2YT4H0W7h<9~`4sa*s%4Mx#R`X7+ux z^+tCh+pR~X-KOn2-Q68dX+A8e+{sCFnR$AUCC7Y{WK7W1i4Dl$6f_eBr3@A#qY$tM z2n0ukOTZ#DXkdKwps`kay7s;HbF@9!X_}IxBz?^zG=kk^nu-I1lkY_*Y2iR{lHM&b zbN66^_@Gg_wcbWrBU;~nQvsv}M=^@B2tjel9<(Y_8 z=TNtX^Sc`Rx&G#Ta)*Nl3TNVjicra+sT5|eo?1Xi41KGY>+QqY+aso9Od4r&vPOP1 zC##{8NFYdb3v5^$O^ZV#_8^UzL5ZhCxH1dE&=A}Z6Z&fM0C#8?PfnX>APGc$ID2a5 z@8k5lfA#j)zkmAiFMjd)&whNqT`Pf{Y^%93CFW`3UWGM8>2fpMbfziMHg0{M5XR(` zRfhnr99}TD^QB2SjHUt8sfg61i<#`8?2zd$-5>R!G4^^rlWLmFcD)QACkE3vTKF!Z4&YO$Q&%SV9sPOd3 zr+1&e$_Jbsk593#k6-=t=U;vGqw7}B>wa!V)b#apE>rXv*R$>Y_W0H>d%IrN?HVDy zlTpeHzfrC5D%(JwX`b$!$Ra`tjlSgtB(HiR3H4mw&4RY{91 z$LaM$xqq&Q1d+^Hd7K^|4s%N2={_4&7v8Q<<7(};mts4hGwV43AX4YOdJyn+Zd};jL_7s zJ%^PT^D@nu9x@I~nonuDr)f#~be|@bbjnlW@&!*RFgb&Sh;|bP3d4axB;gWZFp(%b zhp~<3ng!W9^>z2w`eyq_?CWTeWhzn}(PAocAW?{qeQz>4(xAz5Vr2^3Q;-ytN}wzl z7TpYkyoy)Jol6}wh*MHM<#bRd2ajnI%+$LFb@S}o%Hh#FE0Y&5nQc@XwFN~kd#fXn zpoh|^&4-T|Hm>d-9H0-BmvANPohC`{anSiB5tfd%wz_$@95K8j<&u_3M!R{cY0lok z<~}fr??i{ATL_U*@luFCl0|rzSXezmf(AT@6eTNl@0qLxt7BrT+`5cl5t*hUTKMqp zO`Y`RkN)|eefrsLk_nW3Vj+#(L7>XWGrJ?gd=HO;`PR1mx+OPWcZ{k@k<40Ped=v! zNs?z0KRvuuN)2;2AK*EsXRq!L#|OPX4o6r|(>&_F)zzC*m)rU@gvT&dr)2{A8g_m1^J=La;{cVy)2w35Od*`ot!;<9 z10>8@xhyXpP74{SZ&w)2^Zj1ev9H9j=cGBtn`=G4r4S-HEXgWh-DGm6@My6I+WO76 z<164qig^p63!o~Yg;#^4KCMG2?_ zYmBB?J)LN5bUjPIDl?Udyt)$i@MYn84jUXhxUj5BnM#emYg@v1p?i-#j6H^e$Rq?* znJ0DcfyN-jfFhcNkjD-j10C}b=44@cj*;BEB_WSU8YqrLJb|?vf&x@Q0rf~6M&TW3 zE&^@{F;*i1dLTk_kXR7RK@y&fvS7(F?ms#Ge1Vj}b;7%aKgHFwz(imAeDAW@gOCoaXF}qp@`n zo~N?RK25>XAVlBrO1kp*op;|_&oCsvoq*E?=_M+hdWfT({mWM_|NfE#p)>>~7 zR&6+0xDCoh+lI@|hdCR|sBmTm84a?5+^?07he$GPLuVe2;YdtsgHocp?M~FO@6CW= z#Gc^|!<7oP9uk}cBIcq*%xw(cAI}y#N|O1`28gncjiW?TPXwVs15{Xwlc*N8O$~&B zBxz^k1BfAsRoIns;fN6D05vpw8hLe55Cz{LUwA3OMCG0cJo9w-oKc@2POl$6p8w`I zH)nZ%n)#s1LS9Cbp4c5$=Cmwwe=0BUBZqZsE*pjAM5&C1lrv#qE;7wt_i=riG}YTx z`bAUX}%Y% zkmB=oyUF|C{qA?a{pI)H{qXkb*Zap0^=Y*h?fU+B|KgPHx5slc&4n|TalKwHPdOi` z@9p*>X$kRlUl~ZmT!(eiDKwubL`>@CX;=yyba>I_>D}M{PxH$k@#I9051;;Ga^J9f z)_33khqvE+yLoqn#jUd2F&XlB+Sd=i*;U7I+ImTn$x1mAIiqK?=`d#mml+!dR1%@%WUsvnl$XX^WiRRfjh~N+$nE3>v z;Nh%7B5V=aX@m-cok~PTa&UGwp-jA^?ZlLrHDpMly$MsOINW5$*kbI2)Vn%(jDab+ zu~X)Hjb(C`&@AGBj&K?|w17JFNVK9)o|u~tcT7TcW8chLq!Li`iE{M`OwuyOYugYCHSxw1HSH&pKy;(sTwuQSh3CStin^cM!^MA_jqJdFKj;T9vZdEPcI6PK*? z2W+Vb+<3Aa)JN3aXQe;b_vlW?!m35J2sb@E3yR?y7JX|VL!;5!NW;fu1hUZFvJE)f zc5~~^W4~@TloM;7@{|G=XiP-I2sa1k-S}!4=8lNy)lni8knX6mXVO8)K=U};*ojw0 zC0C|wynC5+LMoGR3*!WV{TAU7MB5gz&aXec`_X69*PnCd>)-s(|HI$+ZM1mWL;GFg zgNoG~?G1)`xVf?S+{9~dw^eu+S1Eub60Ca*WXJBK<#{U8L^-EB9eMVhnr&R}B*%0- zm1i%4x!(46?L1}9#W89}3)I*pxXgK;o-ui(a>~>+SuMx54>1%`eyWmeUJOsrJpg?fdEr zd1pdczi{9Cbs#&Yw152l`@i~kxTW-BX7~2^R-cz2|Kb<MCdVuXs@v31>2{!EI$B(%e0QxHLSAL7@6!$Z4k2La0<%BW8%}2(PEIZoO}buD3oP9!N;)VZB$nUDQLnN z7G1>iv4(|jgh^7D*u#vMhuk9oD``~JKo2q}i|!a`=cY4n)US{39>Xi@sHrzb3lr0* zE&_MQ&g7$Aq*c#BV~KY07|cnAlR6h@!gTNXl=8hz&yr4wvO?}SoU$@)5)d@Tm06U@ zLzszUcQwKkLE&VwgaJ+=Oj4N7?riO2+qeC4-Jh`4=;6ss#vn)tn|DIwwzKuZoI*5t zwk;o-V#$evkD{)`6i$+8n708J4<9s)6Hh`#BuUIO5Ak5l5(E;8lww%(9HzlRd#I=j z$!Ye@Ijn}&ZEx-Rjzv<@bpK!oMvsVu(C%(VYjdK}6LTYgb#`6MZq9;OiO5?M>!@aJ zY;6rhtZf=jI#66vi)b21#h98q!Y9{h@YZ`nUo8&QCt?ZYxF#y$WTC{o2PW=jexS@2 z6JiQV7@e6Z@bG+p^(S8>oJZ5&|BZiht9+QjbqQzGOTBqFn7P6eALc_U&zyR{UW~Hi zO8W(?_1nd^26(?+se$T%gGNK!`n7ihWtGM`Q!4rJ{KtHt)<=)Z7--G5E8r{ zZFKqO_h#oG{q(1YqI=)F$YpP%Tie%BcO$~6ruzM_{`R}y{zo9l8^Xr9Lo@7}MA zb;b}`3FWq3B)h0>>&J@Zk5^pJlyZWJ4bnD|#cekm7M00qYcjjfdCydi%4G%3WMpv%}7PWpg~GOM93Hd>qG!Asw2!S<)G{y-6z1p2)m(oP(nD5o9!v6 zr|TKcxW5nIqDZhZGJ0iA@SP=DN`&GtHyhn}NK`TdCKX88yJsUA z&>kezg8SxD1*?cdJdAy9=mn&t2|~~et04fxiO3ub3WqDRQ;Uck=n(;D1V@mC!4Td^ zBM3wX7$Sh^Va*+g76fnvoQwrH9LDqiX9@A@j&>QWWB3@(#3|hoZ%GwWqIBD8GfE_~pBsjS{r^b9uO{2J^i0fq zKQqRdbFQ^AbMJljaph4c6l^<^O@<65GI*nZsUM)wm2{y{C^{%Hq;9G~HcSHzpu14! zoH}m1%v@`lF+QVW=y`bCQ|3&9Z;Z-2v5?lKMx>9Z&3h<{?*?(s0Z}#_F-LHyWM+4b zoQx*w2Rx!;ObyPG%rVWi*148f_wvyscNbY^o@Xrgq?~d|X^5IyGbyurh9i=v6nK;X zd+tO+9F()rK@1AdaU^CRw-{z0*0|9+IG#s8YDX1#Ic1Dc<>usOUK)mLX5kc_VpQaf zBUv0pHC#1@D5ab7(R1y{l$67gOHdjT849jR41r}L`j9cwx2%^?*2Z)y%9K)%JQz-J z3K#|L>(jokF-h+|y1OJ?Y(yq)cJD+-C#6W@(M5Xsgk)a~wODT5`_bu7h=d24dyvInZ^HOW2Xw(Os0{5l0 zsxM z%tD!K-`i~;@0~g8bWd}DsIt-+&P=6kkKg*H&ghw=9suz=D1x1cLrP^*7Dhq9K|<+)aA(p^C8Kc07DuooZ7WMbUnK}R z^7JZVW7?cLa}X0JoH`g>YMMmV^b`AYR@FhUtMEBuP~Xs$xw3lco+K=0 zl2WG$%aY#N4yQmr{PyTWA_k8wM)Ub36ofH~`k+?LXkp)^wLPcZZ4`FHA;tISgG-Da z!!!EQL@6XUC*5{uCisD1MoF%$r*O~6QoOHGx998|MKZ-aP4lZo&r6+8rQKImZd!_E zBPvyLrzH4e<^gP=07&6Zo>C>4a?W%lGj5;&DSW4#d|ab>E80Xj$1&L;GYZlw7^T;1hKF5}!ubpxUcrM!8dv8&f^vXJQ}BQ^ zAz^xoj94o3RTm>C&Mg(lpxyyFo}Ts&yS@oz9(?ShWY*HQ!%CY}wMCvvoiAqc8uLKE0?ldzW0a1Y>yvufY$fa zYgiG4*=?F7B_0o7_vkXZD~VKP5t*h_9uSs#S`4NGYtryE2xgRXRbr;3lSoiEY)Kxn2Qo=u=#Dbj z1Co76H-Ke2VHj;LKmr&h`=C0b58nk)X-3t%ToSh+1T4S_Cl*SNbm8y}P^v?NN92JlGRlD?LMPgv37$-8Krlr> zf;ck*?v#?GfC3X$1v8wI0%{cD2!z8dz?SStfC!l|3Z{spkw`KSG>go4`Rc#@m!Ez* zV~pe27jFR$LqsACBxl0hUJWDROEm9ZZ*j<6v};sdct zZCNL@HnlpUXi^cHYndMAdQWXW*IJ}mEO*oj>I|7#0hzr$BvZmb6QX2B!YDzp5(e=` zREQ!{BRF#pNca{bu)D7uPrhCK>E^q;MY3BIN*1D`AY{0U0DPL8^wQJ%POD9Pw6SWE zbCzR}A7sQN+wOW^oH$v+gMBAqI!}CBC{1*#pg~ep2RM}vp+MSoCEWq!Ex`?x!AX={Pee72n{ST;Im|?ozyt?8+&9m` zjYO5B_y%Lb!L)}N^`0_QY!EG;eQwg8pDn-xA59(ar8dGv_9=YVCYUO#p zJv|-QeeB!w+c(}fo!a{JczgF{kKntHkWXaw^y%vtpIq)fc^N~FZM^yGfBz4E|F5pM zZ>HDf)t~+JkN)L<^RqwruYd0+fAs2d_xk?f_0Ruw^uB+M>#x50_R9}H`{)0=AN_a# zsW8)`!JM;@7Me8X`{}gAauO*jO~{>>OX`&AAaT)zXOTT~ zut3qYV#Itd13B+&7=fv5yO&ARL6n|J0+}6NK&eDZT`4=aN;L^;!-$IZ zN9GvqrE{g+f*(*4yQ@~F;NUn|lJ&F%?&;W4;q65xFgm2UG=dC^1js>tC7ER$lr@?l zJSky4S)-&!;Vm={SWrfWL^RGsj1=b}QXxc=yC{J%0Ced+!~v!ZED2pf|h2~RIpQP! z>v0{cM|G8*tb>cn!(5%7qgT&WAYyPQJ`T;Au^(|tk{rctljE6#eLM?T`xZ=NOB+V} zkrwF6V{r7e8Z@Ucj?6p&5LE}sZeXsyvnsQdwrw_ic#vZM&~K|s8K7>hleS@`CLY;1BHRMVW-O2hp=1hj#~q6r`hu7FE*B z3=sT=oYrR^mNE{;Foye}N{L#+7_CwkZmrg)#IYcF0#QPEU_!Xpl z^$*{D{l%}}|NV3BnTEu*RVq?SQPBwz{=D_E?lG{&_Tfn=lr)Yj)xwmrheQsG%elt7 z_hSdCYLQZ5Z2kG8yU#!Vy-$}{EQRdYyzfTIcV^J__Kt2V9=ksu>+RY4u*YW+YIjmX z)XlF~goPbTsS~1qxbnCrYCP}9*s4x7L+2tO_g+dJ`@Z!@+Pm-L`t)ws=N;|w`D(tO z?#{xH3!feyj@#q=Z@#`h-~9R%J`A=Gn(khP_2bF-axaW_;`yOyeNGzd*YVX~e)X$= z{coOr{oC-{^u2ca-u&60{)<2S7ytF||L6aQ`#<=8T=(N>{p;JOpZ)Cn|Ll){^84-6 z@1H+?_0c>x#eTVaT^erd%GYfid5fK_@BZY6?t?68svZ47tBw&RVCXeO&7_ErOv{1Wh!HyUrv?94>{y(6GW~ zA}=CzQR{TuyGFNZYDFPsOaQU_5lmddP*@74Iobry!4q$h?1T*!*(Nc^a2x^LNSD~2 z4xVg&CG-K>NN3J>*^j6iX`Gugp$U2T?OCvaW=iIMNF?Y02}h@38P5!6e@jxbKQo0S z$H+kbFXRM1QdPtv$Vh1G!A|IUtZ+-Jg&4n+W-RH0r3D*^5;Zk50uYW7euSLJ2-v|b z`$%MFP^=^pNy(nUNu9_kCP-4cqmxJy6AU&;kjEO4$&@)VGIND0K`Erb9$w8q{NWFm zLYg+vS8Qh9EfRw?mb)35L3@k}8d3U4^dPxsZ}*LMQuaEWg;<@a1>+c2iybLU)VG`$ zqJ%d(&7?YF&73KcRTxZuXe1(&2&D)$u`Utjl2(LD3TX)`Q}|KbiuLE~(fkQovr31KQp+(?C_a-LF|Wn%8cEZm7Ip&$U7gTaNEiHRA= z(6pPc7PoF2?fd8-j&WoeTqr6}MI(9!5(ru?j$MK!Mp%YuHlZxo9%C?#E~AGb>F|!e z4d-fnWHJ|rOctz`v@D`vncV_|xwgWb^F;~NCoSEk)8Lvp>^x_o{n$COM|L_ayYIGU z92OBdjpz4e@)+EYBdJaVB`JOgI+KX)86n%AzKW=5EmKV@OeLcnZhq`$iOxu4nt37_ z&)_MuQ9(Jb$YfN?002QICIqFq^Mn|P2pFL-D!CE2Ov;qlg|;=1l5cOh z#hN9@R@%&U>1bKY{BW?gnIEIG?JdWH9Em)`h z`Kp2lWQLSzMb4M|X%d}vaGhGIt)!fW<=9519)$@#jk2lb@Zh;M?|af{lZ+5!`W>+$ z9>G8aYbGc6XQJW42Eja09%E|KHzsvU_zq4aD1lmnf;1$qNkn5HiMCtHd4PcfqL2y> zHxHROju<0lczVDPPL?5@EJ;puN+}u5q$STG5#nhk6d42trHW;^BvWEGa}wn&t`crU zYevtdWGHPr2<}^&5h_Yj?unU3jG|7JZ3ozg2Xm%VloSv|P1hpafA zd76}W!c<6UIQ65YsLq1on;z0*nsSWNszlB6CG8se5%4zlO0jBct~W54LnQ~7ao9lE z`N2X_Ba))j5Sg13Bm-IRg$6LA6;{d=k3GlE^OQl=hBK9jOsejBTJ$t=YfvU?qy#>v z2nZPhiu750gYF3iFA|logC~H54U`l+<%S5tk%Y|1v1fO)BR+iCc7EQvZNTOvspM5d zMkGbrQHMKBe9*pz=K(7PWsC$z%04U$C18Vw*Sz(Q)*%EU8mXJsG6$5h?6<=Z4%7G#(F(2Me+!!Q5g>tFt6-Jkcj z*T*;S-@bbTEzz@xdW<*@rrE-6k8R(|UAzC}uGaGiJ61n#S68W#X~T@4N9_A5+Qz=6 zTX`{|a+Bs`lhH?7nJdh+%I(cpPw$U4^_Tgy64V}_wx_Gh zmF}Ehtizwa_|12}`SXAM+i$O2=x!;k_*{!(5BB2oy6)uc#{1Qdj?o`U>Ov5%skMh8 zvv9DKg-Ah6Bd4R%hWj+u|Xi%u1KlO zM;EftOB#Gbf4|nkqSNy7R15Rz%-v<0CkG2mrs&4~=H-;zm9{JtK_RB1o~_CZ-X0xu zU@m-QtHI(RsM8o6I}rz;^!5g-GJ>XsGYU&&MYPO7cX5)VvlFAhLIUg&R8f^&DH6Vr zB8Q|oS&vBYiICGI>37NlOmOpkK-CLF4o1yAgI5W#6mWr0yfeUrq@$uRlM*|^A|{NT zR6rt07*nJrGOQS>s)WVlV?S&)dHBcv^XjvG4|jXs z_NP^$31FCkfj})ayHRRJ0~u*+XpRJ-@Iv4y%hIh2lK2v*_ewgY^gGabJ0-A;gA^p$TrW$jm9<%?t@8#Y219H zxciv;4Ki7n##QD8$H5x3?`)s~ry03P=d||}C*+|ZFG4bbRzgdFGKB%wAP#_SB@8MQ z4&;gvScrL22KgFXbnAIhg9}j@QI4!9vDIaY*jetf@BaFgJg%%72}z=2i3C@RB-(*U z6j*0igk`qe!^3f^$&xxDR*XU#2>~pjkYp#e$eM@->410)b_X{|2di@K!rUe1|>x*;K`HH6-kx=oKZ4^@&sCQ5GBKgrBV-%-fvS} zzx5--)yBa{s#&>=ooCo!=4g4WWiHMX-24dHpUT477G$cJhkB&<4TTNRf^8>I^kc9J zgd$X$lEZ~ql}$pN(}PRoNaC!TPpjwEM*)goZN$}&=vqKS=`a8+!o0W*Z6y+|P9f<4 z2wW1`HO@jwBBHD30^iu#(?DmGoBPV8%!R=iHX1P@JEOa;HeSh0MJP&c1HMw~>E zdnJynO(LRH-cY6)W=RL(oY}dLOrer2C$mm|gwE)S3=m71`Wlw(@7MkTh082@*ikFb zRTXW`y+8GRpeR5KO(aQ0-Tiub^>Tan+%#qcA7QM^!?xe zC+ClTuswYN?cDYL^Sg5Tr|Z+Vp;=V+r)MdN+&Mr+c|*wG&Ny_8(vFK7U#N zESdLiQ~9KM&r=A1$6woA`0tLUxO?cMv4G)h?>8bOqW zFq(zWQwC(_2%$h~laV6}ajdCfSbUYR)EwFk5UL>M3w7B364Sx`t3fvf6R zb1AeNc%lPVDhF6Q#zN{*8mA>QLy2m}GqUpg_ugjqEkzxNj4DiK2LsveMiP%JWO4Rl zO3?x37P)&)0((v&?hbdV93@GDDgl;t3KT-eC?uII2_rgy3hit}G9!f&2uVRExG)@< zKmwF0Gr`goNRT4-)H7vAQaV8ewfDw_P*Z6 zO6%H3S8g&nx@MW@Yf0%muia|NRu?~_)Pgk^lu~JQ5ihMkQY2Dld%lLu`-l-eX^vRg zVMyV|X*9EzMG=xQAXF2hOCj^hw)F^X;OeO|hHrb?=yG0KJ2A*X4v^Uj=Jj?<%KdnU z4756Y%P}O%;ZJF!qGJdWBP^Y{Hga|qniuF~hzLlnn(oYOGK3wP6fq*0(JC#4GJCf2;R%h$c-QeZoo?3= zq|T+KbPrpNhB(3dG1nz6wV@l1qH&DODQ-g@n&85vzTUWNILcDT_5!@j z7W=J477UE66pe^{P$o4YaUNZf9!1cUQ>BqPeb~q2BR5bYA}F9dGR+5%C^Cc;KqV>J zKq5q&!qOAwJ~-nsoQ>|83o!>zM2snEN{$F8qD-GC24#{PlSb?*B_Vtr85V5W8q+|I zz-;A0*~MJ~$?5UrB5-%kOhlx+xje9?nRyaOz>4E2K2rpkrR3lp1~jK=#Hq#hq1RI@1gPT|N%R$?J`LP93u zks-(`AoyU1hk>1-z>$Q6frP=W?028M{@Ke}d^ZH;3i%zK<|b!JkHMQT1d4braY$0% z!(6C%%A94^vGPC>uGBmWv1XA{nr`=fKhbV3hTka z*$_x*1c3+yranlJ1V$DCH_9Mvo)$yK-tBhFUE|PX4yq}U36U(t>#!+0Ed+Af_GokU zCpV@nzVFhcOsq;mp?-vq&caY4t4&9YoYaO@OO0KVC`<>0I8;J3SO!UqY3pN*NJMaA zr`S6Am1?+p+_rf4EI%pVPZFqbEZw)ps)T8}%rEYy9P_82w{sySl?eqzN|n+P4kTqi z{9yBStZN+h^wo#)G3Q(RmK147cDuV&9HudwJdZusBS^4p8SRifJEP--cU;??bY9 z7(sz7Q~-80NeTyo&!n&%HqTseI?debbh)c~ zrt#FyGaS}Oy;nM)+-}#eul-uab+i8d{Q8yHWqST(A9+lK zaXWfM*1``D?dL!L>Db?W^~L+=(O$hSoalYt0z~E9>itK}uJ(Sl(^8G0(nM*qES07T z8g+Q!IHs9vF+|z+@V+u}C3u96p-JbeH7QHXQ#rNiaxQ1dR)~ej!^$Ow5rL3k_F6L$ zK=S_eTX{Z?y<;eU`tiI7=jf$8ywW~|F2ua3KuHQ)QVn3nq`U?5h&(}>mL77?xzjL6 zBL->BNVO3$BX&ZgsnF=osaZG~<0(i}Cb6Co1j@S5`b>xj2P=^1x3mI=BZ+2VQ<`!- zlM@iENogpjG$ZjWEh&*E#~`lI#(4xJQm7y~NGGpM6roIm05FsE zPWQ(ze)SiB{|$kXnM2aY_ZRuuk6xr|v970SY=ft}UX@6~&=^TG^)0d(RW#5DsfSx| zc#nm(({7$4LFhXvN4sywhtwkZY}r^ks4=@zI)O7&jAMdbi2$g_MlmrR0SRO#0hyGA z3DVel=;UaG8|Ie{`*f<)!;5wy(UTUBGD$e6Wg6S+9_&G$j5YKEcH$FgFfT#`I3tr( za8UHb@P6c2{W{{h?r+xPN%|PK4LCfBC@cYFc1tW|ehTSIs+BUCgyE!;(N42+bO%}z zJE>3(Cn$F1P zJ%Fy|j@>-=dh2>_fgv;@4`Rx$1Oisq2I=TPQm$Lz2vY)~a>U?6<2Z&{T7`4;aQ)cv zhHB;VaUUY{i;o`kx_^5!5-t7R&C8mu$rQWuLM6z`G|^Db!91uFltc>ASo<2+hnJr} z##@f^`myhesHoK5%=f`;8|4@*>d2BD;R2|XoOEB`(bh}Cas=_{DU#E}%jNmQ zuw%Eqzw^iS-LW_U>${!rmf2<|Ek(=BIh7$VeB0!F55IEOhPHombG!|) z!K1p!WuXIp))qCj8(kJWub3@j4nU6{6r@d@p_Ipx<#oD~udWzQb<#HC`5bWfdf1m<(z zcH&M-G1zJgQsFgXCiX;v2B@KUIbbEymFWly#|Ba)MQUMkIiL%&6IybB8?eE;(ITv= zmM9GNh2ieXGMLERDMcxmU?3*S41h{vM6V=H@PuJ-QZh*qok}I%=QBR~;rB1~ zFP^*^B0wkPdEu$ZU~aXdaUUb(PO^w z)0*Xsc+@`9iEtyi^Ac?cm6fdCj2Q??Pw|ng$U)9U$e{(jWmt?8S93Q>3tBQ9FqqUB zr4W~k^hceb3lTS{CNswlUWNtdnt_PqAW#nxVs%O(gD?TfP98*(xu-L{BP(ELtM7Gx zes+_w-NqIfC0ooE2RSLz+6!0gQDW0UOnbLEDVbcRl1bB8#~3^nwQg9PtOhC8{gAL8 z9_gICrPd4{VO(b^1m8kR90>&i?9rcZt|PPaB(x64n$x1|MwuPW;wnF{-)EL28abMt z5uN3v1gT~D=>G2hRH)Qxf$~xr6imT7A{NJnZUEVce!Wtb<9T~}x8^?FtW0fgO55x$G++L!S}w8 zXj0_N^Q#99eD}?xTu!yl!?&aF!pUb2+=X}ReVSV1iHN3|dAfi8=G*mYesw8sW^do3-sK$Yhe1u?v0Ro| z$F@I()ARPl&Qd@9Oit(i{O;`+@AUrV-RGCr({ca0+%H8R9`5fx`oTxv|Kacdw7vWC zi*La+ooD{|#Y=G9u6CoKB(|Ix-`zq~&xK(=-IwKIxoEv!*BXAX(mIehy|`qz5i6Wv zK`LV%k|dJ%^VEvtpwgsDIZK|Jw>FU!r68%vLhFt1E_7&yHDNb%UJl=X`^#;Q_7s?x z^6GAxrfAyciJJKRM3r13(kvSj5vXunQ-wp2oe0tHgAZO_72i_`Wd?{DbUa19%kiwo zf#wj6GH2hB8}^i15eLG=HL|mCW)cB$gjS}2LP$~yVi%;7aI8!d+wKBQ5GAZfDeOep z$b;%6I8p?%P#o-rFe6Cc5|#HO#3&||BVdM!(PL9MK{nJviX@OEXOBY6JA$$#5o184 z5Rc>ni6o(PCZ;To6wV}JNl*cVsV9&CadHwUiG_qI8D@z>2&AzJ1tlV4yY>0&zxlVuMyf$m6wN(_@~J7`Wf(Nczq^5~sv z*z^&O4=DFz-&B&wIMp&#(kTiL$ii}UTO7TlN7XdXgM3(OhPqOf(ep5&Yf@th4pWcD zUfMV>m+8Khhlw;%n5ZO-i(v?PA`=k^A}SPyl1vb0Fe5dC0-f9lls;l?7@KW!e0URY z`~JA@Pc$}lMzbizLtq;x=^&unx*y!ovhJRth079s1Q2OuYN-_jOA95JGi~e5DMvVn ztOHWO5T~h9ktNfCutE$j)wTHTI#QA|iAXQjJxQG@oFNm2p^|zse#b13hd-aQLN_O9 zdnNlmp5DE9{S!n-sE?66mqa)d9~39Z2(pa9?G(19@6>n5q&^~s?av!(`S?7%4}P)a zjd0?gP~v(f+=%9^4KNC1TH@rSUN-Jze1VBnL5(aUkCwF3TxfOXZg4%eh*+;3^GT)O zef03r_vB9IcgZx0>@{VOUg<03Lid)^F;-w&Zc6l^@eD0nzT&8$gANKKnY7hDT z>6_M>@`t)Fs~~+wb0fyFXsF^~-NA_n-gdq5RW&h=s*B_cV37lfzI z#GE6Mfl6^G;vi@SAtH%8r`Cza$|xfy6_bR!=p@LXG%A{XFjb}oy+xTchSv)RoHix6 z1|Q<>RAV)2)CW&%GDieWg{(S-@CYsxJ#?eEupF{dS61Pih^9t1Pzoq>z`+!fNg2*0 z$si^t{vD8FBoqViW z^D;@ohtP7#eiMH~QrE7HgeWwcQS_*cgE^gwp|ihKc^5osE*Q@#?P#bGTYyG{neoKa zU{iowZmh~oNnLbKX{o^pPU287F)7T5u7MNL?pYW?5m_gS2y%fsgo3z=A4UYYrLAsb z^pe*vefPd~+tmQqY-2Yxa3Sj%Zj`|6W>UBZvnI(bc@-&se^?xHQ-~&jCgSa!DfoErpq(Z@mx`?ESG%rHz;szw*1NlkPotcc>Z(@=S zvn+d~nos(8?fvmQe~wdqdVcR)gcH-`wl@|YNY{XNzk?&5N+nbUzE@AJwi5<6c z+}am!|LV=#CpYEl%2mRJ+0(h?4v#8&KB3Bp6d&7!`F<(yNB6_6gnpP$cbas4dfZl% zBBMsX?u8WNnO>agJlnSF!6{1Jo9fY{e{+3#=MNu!^!~$BSHB!a_m_FOuk+>GuYdD+ z{qCYKv{n1=@gS|jV|!fUtYx`9emne=8MR=v`F?-*e%toT>(_O6naJ0{1V6kT<>~h8 zk3adNZ*%_ci(i;E)bjl1?c0}M{q&#wH*A-``B(oFpZ`iKefr{czgoocpZypA+5PM3 z7ysdze(#T$A8?}|*J}I}ZN96g@4tBUcGRhz&iUaREx$P3e{y-4a(rh!e))G_DU&jr z%RY>IMjJ_ODkD2}E$+dCL^8tXsg#IObRk(v<>ENqXX{0ndT?cUc&&^H73mdBWxWN} zlpaAbRW;>^%Snh)xGqz{v2wc$VnkAz!w2z2)CUEz5oS#?9CBQ}ea467oX{HLNVN!Y zi*}*)UDoPCY?EO3+YV(urX&weq?LrHyPXQaREa4K-b(IkPLgPBdk}~jJkljo8I!P_E6d`fkNE}jG*`shC;D$JeDso>oM8oO8GL_5QW&`gbWrz zW`-mu!ojn{H5_}PvIQ?}p5o-UNFfq1QH;z&nP8_7L}E8kW=@En6kIsW7%kbHxKN~L zf~bh{?fkm@{?9%>|J$#7mT>1RsVXXLN4EPh4$cS-qH++XFiG*F=jr31t!nJy2qsI^ z$YUX*GFOwc@OP@8?EM4Dnfpy?a3wyBV=>9}uG6Wc?XHMEP2y@fW=ZOHQ(f%X)cPR{ z`q2vZ<2{!blHDjK$;^^2G?%iz(ejZ@SqrfMtN{}YM4`wCCx1!`K_onw5YM0ssAf7K zi7V{HSvjAB2vT|1*x&Zsu*W`D#MZmD+K=5ri;#~{N`VkFMNS+>rIj3}qxY`GZjJV1 z(y3CYl;f!nkQQ>!?tX+-w988)Zp3WHwqO0~SAQQv zPIsq$y%C>Yo?d0;>0I6qB|0K*Dw-G4t1A`UdRt1lfBF9Xcjvov@B6kN9l}j_AAH@y zIj((OfX_eyrQRkMm5I z^T+%8{_&T;J%99Zd$`!?=kI>`ueOKJUwrz5^2@KT=(9zS#@pDgZ$5hchu87%*0;#y zH(!0F?&ZGz?A!nFSO50SZ{FzgKmLC&cXvPfvwtRwhFrKU)9mH`lgkgkr!P+9`UmIV z{z1g^c>K*bfA{?1-TUX~<9v6ypX=A}zMC!=PP0zY3pE~W=9cP3m+V_@Wu`cwx<|Qa zJ5{xDs&T?dK9wSgBo(m;CPu(0P8Z3Qb{|Fu1(juNU*)KxBCVj??#BdFyV3l>s3?YV zCvXRtCP#9vqt1LCC^hW~dLT5cTXy9JnP8#Eqv@=f7$YjfDk%|V5=aa>k&lon;~LSk z5UgXGD4o|EibyY^M`{U-970G$hs{(X5pK}jKn3W?nqeNy!$}g_K$E#=WYU^erEsFq zWZ+~wAX9Jy1x-DbV%?+^_)dqW4$;NJxr1`!s);Gb#&t>7ECa!TEvb?pil!`)fdFNs zB*_2)nTSM=%n}&T%9${Vs6+#v5cXtY2N7IAGR@QF$G`uA|KjV>StrqbdU$1IZ7Klln;7?p;NAhvQw2b}A(;e(7sEE5$mywyC&l_iwaU5Mb z9}{wLIw?=K1-H_-P7|}9fMa%}rqHml0w0sZ51WoRMYAGVDx|BJq%5+(li|BGW zP2nTCRB1#A5yIY+=YT}S*!O+q+q^=+dU;zCN6%Qwx!^&WZ`Y^EJR+J^yk<-XSk;q%`$@21nqC9{1-EZHn`-*oT zJ{+mDZEx2>6!XJL3g^BJbFQ+mn_fw_4KgRq3gHr5o5i6sC;L3T`mhb_Oa^u zq_Dm{lB4fqyFPokQ(l&(m|Xo?{j}OTEpmT3-M_p)cK_zrzr5~;mRRn8q!HKaRVKYW ze5^ndp6F8am|mQvOt*jh;)}oioB5O1%ZvNc@c0kEd3^uGX?LH!miZ;Ew5#`A|?|Ko?_ z!*BoQFTVWc7mvUFyVs{LU;SRorEXvT<@)BA^Nr*5@%6)MTJD;F_STH2(i<-!G7tIU-+tw=kSIG2#qteze&Ml>~1c2SQR;+*MPv-hvRIo|aC zysDn`;}?a&)BR}{YR>10+q9HM@G1Ji3Q%(Ht}i(|@^=`ek`z>>$RuG@QYOD4DNvJ@ z;|60sI`NbhY)9yl=;2930YEq-Aem7TFtcNh(C@ z;|jTCUYRV~iEA1Yse9a%3sTzEMWxPSg<3)tRIjt_5cg z6~AS4FU-c2&H-wrd!iMFM>Z+hg90^}+0zBuX7a)CtoL+%%IVIIYt*}tO4C+#K`%TN zA}+ZV7LwZLY_C3U7jUgy99)?q`GVLpoJS`tnUCNLkPM_UX_2Y2j|?R~(svlA?R-4< z4!idL_Hn$AW9$8funcn#h+_yRYgig03XvG5c3TgpL4-+5l`XX@1*fSL+crb99!DiU z+=kI%kpsqL&Sh#9)Kr-Wg~8CtGE!)4iESN9+L}cY@fZh$5mJ{BFX3Q6xZQE%(zs~a zEG^JIY}n@UbX%`JJpAe#$FbXRN@l;M6`1&OweuZMrSX+P#&=^}gQo@{cJeCyhT&wl z8eL*gM97gqTBfF$F-6*NR4#K4BvBOdvVj5_G=hJJT*e^wL?UifO3@|tAc5b{UhY=& z9pmcl*sbsD0YCbF^#kdsWgfzZ%3RK;lf^+cmiyV+QOZ6nr5&jzYL*_J$?ec!ij9bx?#>(k?A``zUfo}zYZen6*b8M#+( znN}v%dD@?z#{1`Sb*N@gJ6}fPa(@9aRSwBA%OaC)yQ)T&^loE6hJ1MUe%rqK?0bLw zYI!k<>ew!)yCO|bQ`#8o6U;V$dEH}wej5@!J@t;`{_S^9@4oznzSv&>g9a5{r!Kk@1l4%mJ6x(ZQOdk`SSg@Z??DB z4_`f(zxwyT{pk;q>J4BUk z8jKCd_wDmt?ybdq#PB^Rxva z>%OjY!g}X1n%VJq)DkLM$ub>WDidW$DpF}YCN-j>RkM_=34qk4HRgfnP)%Zlh;PUS zrK~5CHM6ouhEwkx8^=b(@S+-t#zd(}#M)-pDcmS%&>Wr?or8TtEpGh+~WxLVX2FMort0pqv>(#FCPd z5t&(?DMJzs(UYbD=VWF92XPgr90Mas7=RNcXF?c_vJwS?H4@N3Ne(#3k{yNn>8^e9 z>5Ggwa7?98-^G~uKzh2sw(TfRlAJO*1cZ_+XgYH?HV#y&u3DMEG%Ds1X1NgZj>+r9nX9}SLvc!Vn}}{$KBX0&*q`zx0EI3 znes+q;ZKog*oMB-U?2!aJdhDGMcyo?@D9r03TUF?7$A@w={=$(RwO%R96Pr6zDp+O zksMHFuQMlE6|1vtN0#Jt*rq*g@|S_)~VPtzdJ z7$beVJ|`jiwa9FHfB)@=>+=&bxK1eoh(#|izb~>-EFa!}Ma_Tsr~mxb{rAr2`PAn5 z#o5`3+xpF$>(f&?m&@xv(7gELb-a5&ee&YrCqG`!-}~ktzPkSAJ9j_ym&+Ythu0-a~F}Cl2avEP>ue)~nBPIP{kjW+b&Qa9{cOiErB@UO8KxF3lkezw#$)Ip74ynNw3GgDp zrh|yARI4?^VmE3+ty6Hy5hXc$I$+Kq1Ts1ptd(eG8YzQfPqwrpaUfibD37EgO)`wZ zNsLS=Aq+~%97Kth6!;wjM@G*XESNa5^4F^MBG71j7m zj*&`H6NewcOzvsZMS^$N<7P2NQSVBel)hC?I_wmyplI}R`wp_?=xoKGzs2E61O!aU zZ!tQ%a}EkBk&?zZm@^1O9jHi$BjJLCr^mpE%QYk^=*~8QZLCAuK`TI52oe-R^0T$8PRH$fd(9(EholKjg_%ZSz z;RzUgK1*dcm&&FsoH#@GwF%Sxg>a*D;Yz3kN2a;hjoE9d)HkVh+4g_5>k%L%i?IlTj+Ew;Mm~pBa0-Ha zh1Zm1nSh;r2MuC4rSL{RBTUV41OhP*?0c0{uT#rx$c4C2ZAs^Onz^x&vrg?iPgC#D z;X|YyHvEV-sd^E#QJWoi8WnLd+`DZ9fDVZ7MQqX_xadZtsMr z?md(0GIi&^uHXIYKRsMN|Mbi0S8x9E;qzCY{oWr_k{3@;N#`gD5R3yXSBJ;XiH9->V)z_ceBSWEhZJt^7yN-Dn(YmemMT)zx(Dl zfBtt9|MGHLn>|Y*4!vEs+{^-d??x(>MR~b9%_uR3EVZ?knN?@G*&T-{F)31&YiSyk zT@=I)86gC+!Tr#qAA|I^b~03z#;DwYVxm+UWO60;o=F6kJaB-hQVFiXv85>X4F?Bj zX_;~-ga8qUT#}F!JPD&x-f~aq+C5=!lJ|j{F*{qOVUd+4F2c6uav>dz?m`^TY)0uR z9z3{^AC}=5PeRR^IVNTT2FCzVnr9|k^4`Ij#$bdRQz1x*1QU{g2ax5qQ;Jm<1U&q( z2nZ#C@jKaEj~tdvP6Mt9p*Dq33cyN|oDpteh!_Z>DozJ;I7<4S=?DiCOOiT92NXzx zayY@#$%#-3m=Ios&OiRaLt%kmnxL`Yp3yj{q!iZzRI0;y3$+rCVkXGz%$ zNk>K-?>Z95PSc%m@~{J0it!*SjWfA%C?D)BCBT&q5l%V*$<>Tn-QGA=CZ9^|v1H6; zA83=x#7$D}##C8^{F!v7HbJWGAFL0v%;9c(+6U@8BEg5#2+5!T9CU&oir`G5gJOV$ zVXw$;F*&s($4;k% zWL?eEkRYL)4AWGEWs=H6%B0;gCOdX`9wc4LNIz%~6h4OPgl@5yS=3QuDsgIkY%~t= zBEhUxwW^Tn+@#i4&n+bgF|5o&CB|0c@Z!|>5vy@M^=IrOV_@U?kr>O8Q8;Ng7Z+cNB8%DN0ghaJo^7kt8O*-4H~)5EE)4YC;u} zIbEVGQc`BkJqX9CG#cGX*?GtaU5aP#eJ_fUN~uZET1jL~)i8pR!LhC5w!0^i&0K3c z&GqH|{oRG8LhjXi^kcNvl3|K#dyLiqW|<#&qUqz8W^dLHZ#^u>SIJ7}iRjh-c-(&d>$h+I zPR_#GuJ7&o+c(m9IxVM9mLL7;pS0=hJ>&g;^yBV(uL?=Zm?m8xkMC}y5Mg^;rg3-Q z7MMM4{qe&<=EDL-lQH`dYnT0)Ckh&_@lMab`YyisAO6#u@7_c`P2@*T1L0agb=;hW z=h1T?ErNJs1~*o5Ntz_5xrj5D7TJ?SS`**Pv2!Nslxb4JWH_Yze!ZFXanz~`5_fP> zG7Vy>M8yvr2CC^(FnZ1?B(0*JLCFYOF4RqM|*USRhBQ`7nER^BoI3mYRt*~0SlWL~Y zdd;M06?-S~dMIonOEx4e49{TXnwb!xame)+Ay6hIFsE}414d$CV>tQP7zSZ+$_^P@ zMqv=a!;^u?J;KtD%tUH97(F0>vvX#N?7kDY0!+%l0NA5~0^x{AH>RmHyL|ukpL}wm zBxbR6ViKK6H_lqB5{=YE7L@zJGIENs*l^*S7FE5P7{Wy*+QZuu(LcKQd(b=ueiZrax=H%<1 z2l={=^%)k!GY-#fqn>Pm=#=j9f^$u-$qiCK4NT#yQH#j%1RTDj-;yJ3*v?1P5ealI zcWgmq*>R=F9#rl-=T;;#Gd#{qOl61@5$q-;b>e*;&ri3*!#&6CmgYG?f%&q?G&3g?<8J9I|v?57(Q&wr~9BJA+D66A}Nri);68c_gKd^Mrp*oQCQe|!DnfBwsF{_$_OcWe9jp5~XR$mR##51W7c@BjSGmw)TAgFmw8mD`E& z_~Pe(@YA3FtP#c3)^BbY2T6cu%;kCYT_@+5mL|?~9+@(D+_vYw?vo&nLCMFlF{Hj& z8lzvG_HlgsA%nko7r*_ApFX@VkzEGPbIS1JDU?V>h~}D{qn#dVYiF2b(6FL9seyWq zB(MZ$gpEunPwcj#o-n%J+}3W}?p;Us9$Zy1#jFw8OT9a@FJe-Z3Z->YA~WKdSi?6F z#cj|ruzyIXqa+JBSrWyPOR7_4Drd?YZ$ao(Cd}t3HFX(VW=>42l?i?%F$H2ڵ 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 B Date: Wed, 5 Dec 2012 00:22:39 -0500 Subject: [PATCH 133/213] Correct pickling/unpickling of dynamic specs Previously, __reduce__ was returning a reduction of the class, not the instance. --- imagekit/specs/__init__.py | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/imagekit/specs/__init__.py b/imagekit/specs/__init__.py index b884eac..c839aff 100644 --- a/imagekit/specs/__init__.py +++ b/imagekit/specs/__init__.py @@ -143,15 +143,22 @@ class ImageSpec(BaseImageSpec): return content +def create_spec_class(class_attrs): + cls = type('Spec', (DynamicSpec,), class_attrs) + cls._spec_attrs = class_attrs + return cls + + +def create_spec(class_attrs, kwargs): + cls = create_spec_class(class_attrs) + return cls(**kwargs) + + class DynamicSpec(ImageSpec): def __reduce__(self): - return (create_spec_class, (self._spec_attrs,)) - - -def create_spec_class(spec_attrs): - cls = type('Spec', (DynamicSpec,), spec_attrs) - cls._spec_attrs = spec_attrs - return cls + kwargs = dict(self.kwargs) + kwargs['source_file'] = self.source_file + return (create_spec, (self._spec_attrs, kwargs)) class SpecHost(object): @@ -169,7 +176,6 @@ class SpecHost(object): raise TypeError('You can provide either an image spec or' ' arguments for the ImageSpec constructor, but not both.') else: - # spec = type('Spec', (ImageSpec,), spec_attrs) # TODO: Base class name on spec id? spec = create_spec_class(spec_attrs) self._original_spec = spec From 7578903307470fdf5dbac9ff3f3cda7cf4fbdcd4 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Wed, 5 Dec 2012 00:44:16 -0500 Subject: [PATCH 134/213] Fix test --- tests/test_serialization.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_serialization.py b/tests/test_serialization.py index 76d5391..cd7b6d7 100644 --- a/tests/test_serialization.py +++ b/tests/test_serialization.py @@ -10,4 +10,4 @@ from .utils import create_photo, pickleback def test_imagespecfield(): instance = create_photo('pickletest2.jpg') thumbnail = pickleback(instance.thumbnail) - thumbnail.source_file + thumbnail.generate() From 2a6199b8040332d8727006a8bbbdc2dc96e6fa2e Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Wed, 5 Dec 2012 23:16:07 -0500 Subject: [PATCH 135/213] Simplify get_hash implementation --- imagekit/specs/__init__.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/imagekit/specs/__init__.py b/imagekit/specs/__init__.py index c839aff..9db70d0 100644 --- a/imagekit/specs/__init__.py +++ b/imagekit/specs/__init__.py @@ -102,14 +102,14 @@ class ImageSpec(BaseImageSpec): '%s%s' % (hash, ext)) def get_hash(self): - return md5(''.join([ - self.source_file.name, - pickle.dumps(self.kwargs), - pickle.dumps(self.processors), - str(self.format), - pickle.dumps(self.options), - str(self.autoconvert), - ]).encode('utf-8')).hexdigest() + return md5(pickle.dumps([ + self.source_file, + self.kwargs, + self.processors, + self.format, + self.options, + self.autoconvert, + ])).hexdigest() def generate(self): # TODO: Move into a generator base class From c45876f95c2c5e2343966c2b1eb7d90ccaaa6372 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Wed, 5 Dec 2012 23:16:34 -0500 Subject: [PATCH 136/213] Ignore some style errors --- imagekit/templatetags/compat.py | 1 + tests/test_generateimage_tag.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/imagekit/templatetags/compat.py b/imagekit/templatetags/compat.py index 8334dec..f26e8b8 100644 --- a/imagekit/templatetags/compat.py +++ b/imagekit/templatetags/compat.py @@ -1,3 +1,4 @@ +# flake8: noqa """ This module contains code from django.template.base (sha 90d3af380e8efec0301dd91600c6686232de3943). Bundling this code allows us to diff --git a/tests/test_generateimage_tag.py b/tests/test_generateimage_tag.py index 82cb3ce..95face7 100644 --- a/tests/test_generateimage_tag.py +++ b/tests/test_generateimage_tag.py @@ -1,7 +1,7 @@ from bs4 import BeautifulSoup from django.template import Context, Template, TemplateSyntaxError from nose.tools import eq_, assert_not_in, raises, assert_not_equal -from . import imagespecs +from . import imagespecs # noqa from .utils import get_image_file From 042bdcefb61e47ebb37897c64ffd86d47f43a3fd Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Wed, 5 Dec 2012 23:38:10 -0500 Subject: [PATCH 137/213] Simplify dynamic spec definitions Use a closure instead of an attribute to store the class attrs. --- imagekit/specs/__init__.py | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/imagekit/specs/__init__.py b/imagekit/specs/__init__.py index 9db70d0..df99721 100644 --- a/imagekit/specs/__init__.py +++ b/imagekit/specs/__init__.py @@ -144,9 +144,14 @@ class ImageSpec(BaseImageSpec): def create_spec_class(class_attrs): - cls = type('Spec', (DynamicSpec,), class_attrs) - cls._spec_attrs = class_attrs - return cls + + class DynamicSpecBase(ImageSpec): + def __reduce__(self): + kwargs = dict(self.kwargs) + kwargs['source_file'] = self.source_file + return (create_spec, (class_attrs, kwargs)) + + return type('DynamicSpec', (DynamicSpecBase,), class_attrs) def create_spec(class_attrs, kwargs): @@ -154,13 +159,6 @@ def create_spec(class_attrs, kwargs): return cls(**kwargs) -class DynamicSpec(ImageSpec): - def __reduce__(self): - kwargs = dict(self.kwargs) - kwargs['source_file'] = self.source_file - return (create_spec, (self._spec_attrs, kwargs)) - - class SpecHost(object): """ An object that ostensibly has a spec attribute but really delegates to the From 12307c97aaf9e46b152366d7253ea7fd9743b2fa Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Wed, 5 Dec 2012 23:51:30 -0500 Subject: [PATCH 138/213] Use state--not constructor args--to recreate dynamic specs Previously, we were relying on `__init__`'s arguments to recreate specs. Now we do it the proper way, using the dict returned by `__getstate__` (which may or may not include those arguments). --- imagekit/specs/__init__.py | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/imagekit/specs/__init__.py b/imagekit/specs/__init__.py index df99721..908ecba 100644 --- a/imagekit/specs/__init__.py +++ b/imagekit/specs/__init__.py @@ -147,16 +147,27 @@ def create_spec_class(class_attrs): class DynamicSpecBase(ImageSpec): def __reduce__(self): - kwargs = dict(self.kwargs) - kwargs['source_file'] = self.source_file - return (create_spec, (class_attrs, kwargs)) + try: + getstate = self.__getstate__ + except AttributeError: + state = self.__dict__ + else: + state = getstate() + return (create_spec, (class_attrs, state)) return type('DynamicSpec', (DynamicSpecBase,), class_attrs) -def create_spec(class_attrs, kwargs): +def create_spec(class_attrs, state): cls = create_spec_class(class_attrs) - return cls(**kwargs) + instance = cls.__new__(cls) # Create an instance without calling the __init__ (which may have required args). + try: + setstate = instance.__setstate__ + except AttributeError: + instance.__dict__ = state + else: + setstate(state) + return instance class SpecHost(object): From 8c80ba3b4f703a3be5c209593bd28a93ae928598 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Thu, 6 Dec 2012 19:54:26 -0500 Subject: [PATCH 139/213] GeneratedImageCacheFile stores file manipulation attributes Everything for dealing with files should be part of GeneratedImageCacheFile--not the generator. The fact that GeneratedImageCacheFile can get this information (storage, filename, etc.) is a convenience so that the user only has to define one class (the generator) to fully specify their functionality, but handling the cache file is not part of the core responsibility of the generator. This is also the reason for the renaming of `get_filename` and `storage` to `cache_file_name` and `cache_file_storage`: the generator is just as useful for those who want to generate persistent files. But the original attribute names didn't indicate that they were used only for cache files. The new ones do, and don't preclude the addition of other versions that would be used by another `File` subclass for specifying file names or storage classes. --- imagekit/files.py | 36 +++++++++++++++--------------- imagekit/models/fields/__init__.py | 5 +++-- imagekit/specs/__init__.py | 7 +++--- 3 files changed, 25 insertions(+), 23 deletions(-) diff --git a/imagekit/files.py b/imagekit/files.py index 52836de..9916c2c 100644 --- a/imagekit/files.py +++ b/imagekit/files.py @@ -79,39 +79,39 @@ class GeneratedImageCacheFile(BaseIKFile, ImageFile): it. """ - def __init__(self, generator, name=None): + def __init__(self, generator, name=None, storage=None, image_cache_backend=None): """ :param generator: The object responsible for generating a new image. + :param name: The filename + :param storage: A Django storage object that will be used to save the + file. + :param image_cache_backend: The object responsible for managing the + state of the cache file. """ - self._name = name self.generator = generator - storage = getattr(generator, 'storage', None) - if not storage: - storage = get_singleton(settings.IMAGEKIT_DEFAULT_FILE_STORAGE, - 'file storage backend') + + self.name = name or getattr(generator, 'cache_file_name', None) + storage = storage or getattr(generator, 'cache_file_storage', + None) or get_singleton(settings.IMAGEKIT_DEFAULT_FILE_STORAGE, + 'file storage backend') + self.image_cache_backend = image_cache_backend or getattr(generator, + 'image_cache_backend', None) + super(GeneratedImageCacheFile, self).__init__(storage=storage) - def _get_name(self): - return self._name or self.generator.get_filename() - - def _set_name(self, value): - self._name = value - - name = property(_get_name, _set_name) - def _require_file(self): before_access.send(sender=self, generator=self.generator, file=self) return super(GeneratedImageCacheFile, self)._require_file() def clear(self): - return self.generator.image_cache_backend.clear(self) + return self.image_cache_backend.clear(self) def invalidate(self): - return self.generator.image_cache_backend.invalidate(self) + return self.image_cache_backend.invalidate(self) def validate(self): - return self.generator.image_cache_backend.validate(self) + return self.image_cache_backend.validate(self) def generate(self): # Generate the file @@ -127,7 +127,7 @@ class GeneratedImageCacheFile(BaseIKFile, ImageFile): ' race condition in the image cache backend %s. The' ' saved file will not be used.' % (self.storage, self.name, actual_name, - self.generator.image_cache_backend)) + self.image_cache_backend)) class IKContentFile(ContentFile): diff --git a/imagekit/models/fields/__init__.py b/imagekit/models/fields/__init__.py index 6b3287c..131ff33 100644 --- a/imagekit/models/fields/__init__.py +++ b/imagekit/models/fields/__init__.py @@ -26,12 +26,13 @@ class ImageSpecField(SpecHostField): """ def __init__(self, processors=None, format=None, options=None, - source=None, storage=None, autoconvert=None, + source=None, cache_file_storage=None, autoconvert=None, image_cache_backend=None, image_cache_strategy=None, spec=None, id=None): SpecHost.__init__(self, processors=processors, format=format, - options=options, storage=storage, autoconvert=autoconvert, + options=options, cache_file_storage=cache_file_storage, + autoconvert=autoconvert, image_cache_backend=image_cache_backend, image_cache_strategy=image_cache_strategy, spec=spec, spec_id=id) diff --git a/imagekit/specs/__init__.py b/imagekit/specs/__init__.py index 908ecba..fdf2044 100644 --- a/imagekit/specs/__init__.py +++ b/imagekit/specs/__init__.py @@ -19,8 +19,8 @@ class BaseImageSpec(object): """ - storage = None - """A Django storage system to use to save the generated image.""" + cache_file_storage = None + """A Django storage system to use to save a generated cache file.""" image_cache_backend = None """ @@ -90,7 +90,8 @@ class ImageSpec(BaseImageSpec): self.kwargs = kwargs super(ImageSpec, self).__init__() - def get_filename(self): + @property + def cache_file_name(self): source_filename = self.source_file.name ext = suggest_extension(source_filename, self.format) return os.path.normpath(os.path.join( From 1fb1d83c5666fbb992c37427db8422f8092038c8 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Thu, 6 Dec 2012 23:17:12 -0500 Subject: [PATCH 140/213] Add Thumbnail processor --- imagekit/processors/resize.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/imagekit/processors/resize.py b/imagekit/processors/resize.py index fea3d51..624ccf6 100644 --- a/imagekit/processors/resize.py +++ b/imagekit/processors/resize.py @@ -215,3 +215,30 @@ class ResizeToFit(object): if self.mat_color is not None: img = ResizeCanvas(self.width, self.height, self.mat_color, anchor=self.anchor).process(img) return img + + +class Thumbnail(object): + """ + Resize the image for use as a thumbnail. Wraps ``ResizeToFill``, + ``ResizeToFit``, and ``SmartResize``. + + Note: while it doesn't currently, in the future this processor may also + sharpen based on the amount of reduction. + + """ + + def __init__(self, width=None, height=None, anchor='auto', crop=True): + self.width = width + self.height = height + self.anchor = anchor + self.crop = crop + + def process(self, img): + if self.crop: + if self.anchor == 'auto': + processor = SmartResize(self.width, self.height) + else: + processor = ResizeToFill(self.width, self.height, self.anchor) + else: + processor = ResizeToFit(self.width, self.height) + return processor.process(img) From c69c2d087e7481b4d36ff43d0eae240181cb75db Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Thu, 6 Dec 2012 23:48:09 -0500 Subject: [PATCH 141/213] Create Thumbnail spec; closes #175 --- imagekit/__init__.py | 1 + imagekit/generatorlibrary.py | 13 +++++++++++++ imagekit/processors/resize.py | 3 +++ 3 files changed, 17 insertions(+) create mode 100644 imagekit/generatorlibrary.py diff --git a/imagekit/__init__.py b/imagekit/__init__.py index 2c72bf4..e65c270 100644 --- a/imagekit/__init__.py +++ b/imagekit/__init__.py @@ -1,6 +1,7 @@ # flake8: noqa from . import conf +from . import generatorlibrary from .specs import ImageSpec from .pkgmeta import * from .registry import register, unregister diff --git a/imagekit/generatorlibrary.py b/imagekit/generatorlibrary.py new file mode 100644 index 0000000..1786022 --- /dev/null +++ b/imagekit/generatorlibrary.py @@ -0,0 +1,13 @@ +from .registry import register +from .processors import Thumbnail as ThumbnailProcessor +from .specs import ImageSpec + + +class Thumbnail(ImageSpec): + def __init__(self, width=None, height=None, anchor='auto', crop=True, **kwargs): + self.processors = [ThumbnailProcessor(width, height, anchor=anchor, + crop=crop)] + super(Thumbnail, self).__init__(**kwargs) + + +register.spec('ik:thumbnail', Thumbnail) diff --git a/imagekit/processors/resize.py b/imagekit/processors/resize.py index 624ccf6..f241ca6 100644 --- a/imagekit/processors/resize.py +++ b/imagekit/processors/resize.py @@ -235,6 +235,9 @@ class Thumbnail(object): def process(self, img): if self.crop: + if not self.width or not self.height: + raise Exception('You must provide both a width and height when' + ' cropping.') if self.anchor == 'auto': processor = SmartResize(self.width, self.height) else: From 30e40b4916418356fbde6c36f48ab5833a92006a Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Fri, 7 Dec 2012 00:17:35 -0500 Subject: [PATCH 142/213] Add TODO for unregistration --- imagekit/registry.py | 1 + 1 file changed, 1 insertion(+) diff --git a/imagekit/registry.py b/imagekit/registry.py index c6d0150..160c31e 100644 --- a/imagekit/registry.py +++ b/imagekit/registry.py @@ -20,6 +20,7 @@ class GeneratorRegistry(object): self._generators[id] = generator def unregister(self, id, generator): + # TODO: Either don't require the generator, or--if we do--assert that it's registered with the provided id try: del self._generators[id] except KeyError: From 52fb4e24bec4dd7accb67994d0165a4d5278f2c1 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Fri, 7 Dec 2012 00:36:11 -0500 Subject: [PATCH 143/213] Add thumbnail templatetag Finally! --- imagekit/templatetags/imagekit.py | 102 +++++++++++++++++++++--------- 1 file changed, 72 insertions(+), 30 deletions(-) diff --git a/imagekit/templatetags/imagekit.py b/imagekit/templatetags/imagekit.py index a1731a5..c917011 100644 --- a/imagekit/templatetags/imagekit.py +++ b/imagekit/templatetags/imagekit.py @@ -73,38 +73,9 @@ class GenerateImageTagNode(template.Node): return mark_safe(u'' % attr_str) -#@register.tag -def generateimage(parser, token): - """ - Creates an image based on the provided arguments. - - By default:: - - {% generateimage 'myapp:thumbnail' from=mymodel.profile_image %} - - generates an ```` tag:: - - - - You can add additional attributes to the tag using "with". For example, - this:: - - {% generateimage 'myapp:thumbnail' from=mymodel.profile_image with alt="Hello!" %} - - will result in the following markup:: - - Hello! - - For more flexibility, ``generateimage`` also works as an assignment tag:: - - {% generateimage 'myapp:thumbnail' from=mymodel.profile_image as th %} - - - """ - +def _generateimage(parser, bits): varname = None html_bits = [] - bits = token.split_contents() tag_name = bits.pop(0) if bits[-2] == ASSIGNMENT_DELIMETER: @@ -140,4 +111,75 @@ def generateimage(parser, token): return GenerateImageTagNode(generator_id, kwargs, html_kwargs) +#@register.tag +def generateimage(parser, token): + """ + Creates an image based on the provided arguments. + + By default:: + + {% generateimage 'myapp:thumbnail' from=mymodel.profile_image %} + + generates an ```` tag:: + + + + You can add additional attributes to the tag using "with". For example, + this:: + + {% generateimage 'myapp:thumbnail' from=mymodel.profile_image with alt="Hello!" %} + + will result in the following markup:: + + Hello! + + For more flexibility, ``generateimage`` also works as an assignment tag:: + + {% generateimage 'myapp:thumbnail' from=mymodel.profile_image as th %} + + + """ + bits = token.split_contents() + return _generateimage(parser, bits) + + +#@register.tag +def thumbnail(parser, token): + """ + A convenient alias for the ``generateimage`` tag with the generator id + ``'ik:thumbnail'``. The following:: + + {% thumbnail from=mymodel.profile_image width=100 height=100 %} + + is equivalent to:: + + {% generateimage 'ik:thumbnail' from=mymodel.profile_image width=100 height=100 %} + + The thumbnail tag supports the "with" and "as" bits for adding html + attributes and assigning to a variable, respectively. It also accepts the + kwargs "width", "height", "anchor", and "crop". + + To use "smart cropping" (the ``SmartResize`` processor):: + + {% thumbnail from=mymodel.profile_image width=100 height=100 %} + + To crop, anchoring the image to the top right (the ``ResizeToFill`` + processor):: + + {% thumbnail from=mymodel.profile_image width=100 height=100 anchor='tr' %} + + To resize without cropping (using the ``ResizeToFit`` processor):: + + {% thumbnail from=mymodel.profile_image width=100 height=100 crop=0 %} + + """ + # TODO: Support positional arguments for this tag for "from", "width" and "height". + # Example: + # {% thumbnail mymodel.profile_image 100 100 anchor='tl' %} + bits = token.split_contents() + bits.insert(1, "'ik:thumbnail'") + return _generateimage(parser, bits) + + generateimage = register.tag(generateimage) +thumbnail = register.tag(thumbnail) From 184c13dd4e51c2453c6b6abeea1acdf49ffa7cd3 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Tue, 11 Dec 2012 22:33:33 -0500 Subject: [PATCH 144/213] More source_group renaming --- .../management/commands/warmimagecache.py | 4 +- imagekit/registry.py | 38 +++++++++---------- imagekit/specs/sourcegroups.py | 28 +++++++------- 3 files changed, 35 insertions(+), 35 deletions(-) diff --git a/imagekit/management/commands/warmimagecache.py b/imagekit/management/commands/warmimagecache.py index 21db47c..35695af 100644 --- a/imagekit/management/commands/warmimagecache.py +++ b/imagekit/management/commands/warmimagecache.py @@ -19,8 +19,8 @@ class Command(BaseCommand): for spec_id in specs: self.stdout.write('Validating spec: %s\n' % spec_id) - for source in source_group_registry.get(spec_id): - for source_file in source.files(): + for source_group in source_group_registry.get(spec_id): + for source_file in source_group.files(): if source_file: spec = generator_registry.get(spec_id, source_file=source_file) # TODO: HINTS! (Probably based on source, so this will need to be moved into loop below.) self.stdout.write(' %s\n' % source_file) diff --git a/imagekit/registry.py b/imagekit/registry.py index 160c31e..bdf3fd6 100644 --- a/imagekit/registry.py +++ b/imagekit/registry.py @@ -53,57 +53,57 @@ class SourceGroupRegistry(object): """ - _source_signals = [ + _signals = [ source_created, source_changed, source_deleted, ] def __init__(self): - self._sources = {} - for signal in self._source_signals: - signal.connect(self.source_receiver) + self._source_groups = {} + for signal in self._signals: + signal.connect(self.source_group_receiver) before_access.connect(self.before_access_receiver) - def register(self, spec_id, sources): + def register(self, spec_id, source_groups): """ - Associates sources with a spec id + Associates source groups with a spec id """ - for source in sources: - if source not in self._sources: - self._sources[source] = set() - self._sources[source].add(spec_id) + for source_group in source_groups: + if source_group not in self._source_groups: + self._source_groups[source_group] = set() + self._source_groups[source_group].add(spec_id) - def unregister(self, spec_id, sources): + def unregister(self, spec_id, source_groups): """ Disassociates sources with a spec id """ - for source in sources: + for source_group in source_groups: try: - self._sources[source].remove(spec_id) + self._source_groups[source_group].remove(spec_id) except KeyError: continue def get(self, spec_id): - return [source for source in self._sources - if spec_id in self._sources[source]] + return [source_group for source_group in self._source_groups + if spec_id in self._source_groups[source_group]] def before_access_receiver(self, sender, generator, file, **kwargs): generator.image_cache_strategy.invoke_callback('before_access', file) - def source_receiver(self, sender, source_file, signal, info, **kwargs): + def source_group_receiver(self, sender, source_file, signal, info, **kwargs): """ Redirects signals dispatched on sources to the appropriate specs. """ - source = sender - if source not in self._sources: + source_group = sender + if source_group not in self._source_groups: return for spec in (generator_registry.get(id, source_file=source_file, **info) - for id in self._sources[source]): + for id in self._sources_groups[source_group]): event_name = { source_created: 'source_created', source_changed: 'source_changed', diff --git a/imagekit/specs/sourcegroups.py b/imagekit/specs/sourcegroups.py index 551d859..fe0b366 100644 --- a/imagekit/specs/sourcegroups.py +++ b/imagekit/specs/sourcegroups.py @@ -11,27 +11,27 @@ def ik_model_receiver(fn): """ @wraps(fn) def receiver(self, sender, **kwargs): - if sender in (src.model_class for src in self._sources): + if sender in (src.model_class for src in self._source_groups): fn(self, sender=sender, **kwargs) return receiver class ModelSignalRouter(object): """ - Handles signals dispatched by models and relays them to the spec sources - that represent those models. + Handles signals dispatched by models and relays them to the spec source + groups that represent those models. """ def __init__(self): - self._sources = [] + self._source_groups = [] uid = 'ik_spec_field_receivers' post_init.connect(self.post_init_receiver, dispatch_uid=uid) post_save.connect(self.post_save_receiver, dispatch_uid=uid) post_delete.connect(self.post_delete_receiver, dispatch_uid=uid) - def add(self, source): - self._sources.append(source) + def add(self, source_group): + self._source_groups.append(source_group) def init_instance(self, instance): instance._ik = getattr(instance, '_ik', {}) @@ -55,7 +55,7 @@ class ModelSignalRouter(object): """ return dict((src.image_field, getattr(instance, src.image_field)) for - src in self._sources if src.model_class is instance.__class__) + src in self._source_groups if src.model_class is instance.__class__) @ik_model_receiver def post_save_receiver(self, sender, instance=None, created=False, raw=False, **kwargs): @@ -82,19 +82,19 @@ class ModelSignalRouter(object): def dispatch_signal(self, signal, file, model_class, instance, attname): """ - Dispatch the signal for each of the matching sources. Note that more - than one source can have the same model and image_field; it's important - that we dispatch the signal for each. + Dispatch the signal for each of the matching source groups. Note that + more than one source can have the same model and image_field; it's + important that we dispatch the signal for each. """ - for source in self._sources: - if source.model_class is model_class and source.image_field == attname: + for source_group in self._source_groups: + if source_group.model_class is model_class and source_group.image_field == attname: info = dict( - source=source, + source_group=source_group, instance=instance, field_name=attname, ) - signal.send(sender=source, source_file=file, info=info) + signal.send(sender=source_group, source_file=file, info=info) class ImageFieldSourceGroup(object): From d80f2f26a99fbedd8f8bae6bd46cef51efe16ebf Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Tue, 11 Dec 2012 22:53:13 -0500 Subject: [PATCH 145/213] "source" now refers to the file itself --- imagekit/admin.py | 2 +- imagekit/management/commands/warmimagecache.py | 8 ++++---- imagekit/models/fields/utils.py | 6 +++--- imagekit/registry.py | 6 +++--- imagekit/specs/__init__.py | 14 +++++++------- imagekit/specs/sourcegroups.py | 2 +- imagekit/templatetags/imagekit.py | 2 +- 7 files changed, 20 insertions(+), 20 deletions(-) diff --git a/imagekit/admin.py b/imagekit/admin.py index 7a9e145..6b37a4c 100644 --- a/imagekit/admin.py +++ b/imagekit/admin.py @@ -30,7 +30,7 @@ class AdminThumbnail(object): raise Exception('The property %s is not defined on %s.' % (self.image_field, obj.__class__.__name__)) - original_image = getattr(thumbnail, 'source_file', None) or thumbnail + original_image = getattr(thumbnail, 'source', None) or thumbnail template = self.template or 'imagekit/admin/thumbnail.html' return render_to_string(template, { diff --git a/imagekit/management/commands/warmimagecache.py b/imagekit/management/commands/warmimagecache.py index 35695af..6c9822d 100644 --- a/imagekit/management/commands/warmimagecache.py +++ b/imagekit/management/commands/warmimagecache.py @@ -20,10 +20,10 @@ class Command(BaseCommand): for spec_id in specs: self.stdout.write('Validating spec: %s\n' % spec_id) for source_group in source_group_registry.get(spec_id): - for source_file in source_group.files(): - if source_file: - spec = generator_registry.get(spec_id, source_file=source_file) # TODO: HINTS! (Probably based on source, so this will need to be moved into loop below.) - self.stdout.write(' %s\n' % source_file) + for source in source_group.files(): + if source: + spec = generator_registry.get(spec_id, source=source) # TODO: HINTS! (Probably based on source, so this will need to be moved into loop below.) + self.stdout.write(' %s\n' % source) try: # TODO: Allow other validation actions through command option GeneratedImageCacheFile(spec).validate() diff --git a/imagekit/models/fields/utils.py b/imagekit/models/fields/utils.py index 07c6e9e..39dbc52 100644 --- a/imagekit/models/fields/utils.py +++ b/imagekit/models/fields/utils.py @@ -13,7 +13,7 @@ class ImageSpecFileDescriptor(object): else: field_name = getattr(self.field, 'source', None) if field_name: - source_file = getattr(instance, field_name) + source = getattr(instance, field_name) else: image_fields = [getattr(instance, f.attname) for f in instance.__class__._meta.fields if @@ -28,8 +28,8 @@ class ImageSpecFileDescriptor(object): ' ImageSpecField.' % (instance.__class__.__name__, self.attname)) else: - source_file = image_fields[0] - spec = self.field.get_spec(source_file=source_file) # TODO: What "hints" should we pass here? + source = image_fields[0] + spec = self.field.get_spec(source=source) # TODO: What "hints" should we pass here? file = GeneratedImageCacheFile(spec) instance.__dict__[self.attname] = file return file diff --git a/imagekit/registry.py b/imagekit/registry.py index bdf3fd6..6ff9210 100644 --- a/imagekit/registry.py +++ b/imagekit/registry.py @@ -93,7 +93,7 @@ class SourceGroupRegistry(object): def before_access_receiver(self, sender, generator, file, **kwargs): generator.image_cache_strategy.invoke_callback('before_access', file) - def source_group_receiver(self, sender, source_file, signal, info, **kwargs): + def source_group_receiver(self, sender, source, signal, info, **kwargs): """ Redirects signals dispatched on sources to the appropriate specs. @@ -102,14 +102,14 @@ class SourceGroupRegistry(object): if source_group not in self._source_groups: return - for spec in (generator_registry.get(id, source_file=source_file, **info) + for spec in (generator_registry.get(id, source=source, **info) for id in self._sources_groups[source_group]): event_name = { source_created: 'source_created', source_changed: 'source_changed', source_deleted: 'source_deleted', } - spec._handle_source_event(event_name, source_file) + spec._handle_source_event(event_name, source) class Register(object): diff --git a/imagekit/specs/__init__.py b/imagekit/specs/__init__.py index fdf2044..de1db8d 100644 --- a/imagekit/specs/__init__.py +++ b/imagekit/specs/__init__.py @@ -45,7 +45,7 @@ class BaseImageSpec(object): raise NotImplementedError # TODO: I don't like this interface. Is there a standard Python one? pubsub? - def _handle_source_event(self, event_name, source_file): + def _handle_source_event(self, event_name, source): file = GeneratedImageCacheFile(self) self.image_cache_strategy.invoke_callback('on_%s' % event_name, file) @@ -84,15 +84,15 @@ class ImageSpec(BaseImageSpec): """ - def __init__(self, source_file, **kwargs): - self.source_file = source_file + def __init__(self, source, **kwargs): + self.source = source self.processors = self.processors or [] self.kwargs = kwargs super(ImageSpec, self).__init__() @property def cache_file_name(self): - source_filename = self.source_file.name + source_filename = self.source.name ext = suggest_extension(source_filename, self.format) return os.path.normpath(os.path.join( settings.IMAGEKIT_CACHE_DIR, @@ -104,7 +104,7 @@ class ImageSpec(BaseImageSpec): def get_hash(self): return md5(pickle.dumps([ - self.source_file, + self.source, self.kwargs, self.processors, self.format, @@ -115,9 +115,9 @@ class ImageSpec(BaseImageSpec): def generate(self): # TODO: Move into a generator base class # TODO: Factor out a generate_image function so you can create a generator and only override the PIL.Image creating part. (The tricky part is how to deal with original_format since generator base class won't have one.) - source_file = self.source_file + source = self.source filename = self.kwargs.get('filename') - img = open_image(source_file) + img = open_image(source) original_format = img.format # Run the processors diff --git a/imagekit/specs/sourcegroups.py b/imagekit/specs/sourcegroups.py index fe0b366..b36b211 100644 --- a/imagekit/specs/sourcegroups.py +++ b/imagekit/specs/sourcegroups.py @@ -94,7 +94,7 @@ class ModelSignalRouter(object): instance=instance, field_name=attname, ) - signal.send(sender=source_group, source_file=file, info=info) + signal.send(sender=source_group, source=file, info=info) class ImageFieldSourceGroup(object): diff --git a/imagekit/templatetags/imagekit.py b/imagekit/templatetags/imagekit.py index c917011..d5aa9c6 100644 --- a/imagekit/templatetags/imagekit.py +++ b/imagekit/templatetags/imagekit.py @@ -14,7 +14,7 @@ HTML_ATTRS_DELIMITER = 'with' _kwarg_map = { - 'from': 'source_file', + 'from': 'source', } From faee0fa5371a6779550b56803e848869285a0455 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Tue, 8 Jan 2013 20:36:17 -0500 Subject: [PATCH 146/213] Correct typo --- imagekit/registry.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/imagekit/registry.py b/imagekit/registry.py index 6ff9210..8d082cd 100644 --- a/imagekit/registry.py +++ b/imagekit/registry.py @@ -103,7 +103,7 @@ class SourceGroupRegistry(object): return for spec in (generator_registry.get(id, source=source, **info) - for id in self._sources_groups[source_group]): + for id in self._source_groups[source_group]): event_name = { source_created: 'source_created', source_changed: 'source_changed', From c2dedaa2b87e293697be7fea9c808e95e20c0bb7 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Tue, 8 Jan 2013 20:57:19 -0500 Subject: [PATCH 147/213] Use file name; not file, which can't be pickled --- imagekit/specs/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/imagekit/specs/__init__.py b/imagekit/specs/__init__.py index de1db8d..79ff107 100644 --- a/imagekit/specs/__init__.py +++ b/imagekit/specs/__init__.py @@ -104,7 +104,7 @@ class ImageSpec(BaseImageSpec): def get_hash(self): return md5(pickle.dumps([ - self.source, + self.source.name, self.kwargs, self.processors, self.format, From 658bb22c783fe9e50db66773782e3a7fcdbfbaba Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Tue, 8 Jan 2013 21:52:56 -0500 Subject: [PATCH 148/213] Special case serialization of ImageFieldFiles Closes #168 --- imagekit/specs/__init__.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/imagekit/specs/__init__.py b/imagekit/specs/__init__.py index 79ff107..e9e72cf 100644 --- a/imagekit/specs/__init__.py +++ b/imagekit/specs/__init__.py @@ -1,4 +1,5 @@ from django.conf import settings +from django.db.models.fields.files import ImageFieldFile from hashlib import md5 import os import pickle @@ -102,6 +103,25 @@ class ImageSpec(BaseImageSpec): return os.path.join(settings.IMAGEKIT_CACHE_DIR, '%s%s' % (hash, ext)) + def __getstate__(self): + state = self.__dict__ + + # Unpickled ImageFieldFiles won't work (they're missing a storage + # object). Since they're such a common use case, we special case them. + if isinstance(self.source, ImageFieldFile): + field = getattr(self.source, 'field') + state['_field_data'] = { + 'instance': getattr(self.source, 'instance', None), + 'attname': getattr(field, 'name', None), + } + return state + + def __setstate__(self, state): + field_data = state.pop('_field_data', None) + self.__dict__ = state + if field_data: + self.source = getattr(field_data['instance'], field_data['attname']) + def get_hash(self): return md5(pickle.dumps([ self.source.name, From 11d511f9ccfdc83927e7c8ed9917f7c195d2b295 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Tue, 8 Jan 2013 22:39:02 -0500 Subject: [PATCH 149/213] Extract util for parsing common bits. In preparation for new thumbnail and placeholder tag syntaxes (#177 and #176) which share some (but not all) syntax with the generateimage tag. --- imagekit/templatetags/imagekit.py | 31 ++++++++++++++++++++++--------- 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/imagekit/templatetags/imagekit.py b/imagekit/templatetags/imagekit.py index d5aa9c6..c2c6363 100644 --- a/imagekit/templatetags/imagekit.py +++ b/imagekit/templatetags/imagekit.py @@ -73,9 +73,15 @@ class GenerateImageTagNode(template.Node): return mark_safe(u'' % attr_str) -def _generateimage(parser, bits): +def parse_ik_tag_bits(parser, bits): + """ + Parses the tag name, html attributes and variable name (for assignment tags) + from the provided bits. The preceding bits may vary and are left to be + parsed by specific tags. + + """ varname = None - html_bits = [] + html_attrs = {} tag_name = bits.pop(0) if bits[-2] == ASSIGNMENT_DELIMETER: @@ -90,6 +96,19 @@ def _generateimage(parser, bits): raise template.TemplateSyntaxError('Don\'t use "%s" unless you\'re' ' setting html attributes.' % HTML_ATTRS_DELIMITER) + args, html_attrs = parse_bits(parser, html_bits, [], 'args', + 'kwargs', None, False, tag_name) + if len(args): + raise template.TemplateSyntaxError('All "%s" tag arguments after' + ' the "%s" token must be named.' % (tag_name, + HTML_ATTRS_DELIMITER)) + + return (tag_name, bits, html_attrs, varname) + + +def _generateimage(parser, bits): + tag_name, bits, html_attrs, varname = parse_ik_tag_bits(parser, bits) + args, kwargs = parse_bits(parser, bits, ['generator_id'], 'args', 'kwargs', None, False, tag_name) @@ -102,13 +121,7 @@ def _generateimage(parser, bits): if varname: return GenerateImageAssignmentNode(varname, generator_id, kwargs) else: - html_args, html_kwargs = parse_bits(parser, html_bits, [], 'args', - 'kwargs', None, False, tag_name) - if len(html_args): - raise template.TemplateSyntaxError('All "%s" tag arguments after' - ' the "%s" token must be named.' % (tag_name, - HTML_ATTRS_DELIMITER)) - return GenerateImageTagNode(generator_id, kwargs, html_kwargs) + return GenerateImageTagNode(generator_id, kwargs, html_attrs) #@register.tag From 3177eb8e192d96396095547c0a075995796e3e1c Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Tue, 8 Jan 2013 23:36:22 -0500 Subject: [PATCH 150/213] Extract utils for use in other modules --- tests/test_generateimage_tag.py | 16 ++-------------- tests/utils.py | 13 +++++++++++++ 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/tests/test_generateimage_tag.py b/tests/test_generateimage_tag.py index 95face7..35c29d8 100644 --- a/tests/test_generateimage_tag.py +++ b/tests/test_generateimage_tag.py @@ -1,19 +1,7 @@ -from bs4 import BeautifulSoup -from django.template import Context, Template, TemplateSyntaxError +from django.template import TemplateSyntaxError from nose.tools import eq_, assert_not_in, raises, assert_not_equal from . import imagespecs # noqa -from .utils import get_image_file - - -def render_tag(ttag): - img = get_image_file() - template = Template('{%% load imagekit %%}%s' % ttag) - context = Context({'img': img}) - return template.render(context) - - -def get_html_attrs(ttag): - return BeautifulSoup(render_tag(ttag)).img.attrs +from .utils import render_tag, get_html_attrs def test_img_tag(): diff --git a/tests/utils.py b/tests/utils.py index 3093b7d..1a4e3fc 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -1,6 +1,8 @@ +from bs4 import BeautifulSoup import os from django.conf import settings from django.core.files.base import ContentFile +from django.template import Context, Template from imagekit.lib import Image, StringIO import pickle from .models import Photo @@ -42,3 +44,14 @@ def pickleback(obj): pickle.dump(obj, pickled) pickled.seek(0) return pickle.load(pickled) + + +def render_tag(ttag): + img = get_image_file() + template = Template('{%% load imagekit %%}%s' % ttag) + context = Context({'img': img}) + return template.render(context) + + +def get_html_attrs(ttag): + return BeautifulSoup(render_tag(ttag)).img.attrs From 43a1f494984491701770589b6d3ff906d086ba5a Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Wed, 9 Jan 2013 00:25:08 -0500 Subject: [PATCH 151/213] New thumbnail tag syntax! Closes #177 --- imagekit/generatorlibrary.py | 2 +- imagekit/processors/resize.py | 17 +++- imagekit/templatetags/imagekit.py | 138 ++++++++++++++++++++++++++---- 3 files changed, 137 insertions(+), 20 deletions(-) diff --git a/imagekit/generatorlibrary.py b/imagekit/generatorlibrary.py index 1786022..bd9da9a 100644 --- a/imagekit/generatorlibrary.py +++ b/imagekit/generatorlibrary.py @@ -4,7 +4,7 @@ from .specs import ImageSpec class Thumbnail(ImageSpec): - def __init__(self, width=None, height=None, anchor='auto', crop=True, **kwargs): + def __init__(self, width=None, height=None, anchor=None, crop=None, **kwargs): self.processors = [ThumbnailProcessor(width, height, anchor=anchor, crop=crop)] super(Thumbnail, self).__init__(**kwargs) diff --git a/imagekit/processors/resize.py b/imagekit/processors/resize.py index f241ca6..0bae122 100644 --- a/imagekit/processors/resize.py +++ b/imagekit/processors/resize.py @@ -227,11 +227,24 @@ class Thumbnail(object): """ - def __init__(self, width=None, height=None, anchor='auto', crop=True): + def __init__(self, width=None, height=None, anchor=None, crop=None): self.width = width self.height = height - self.anchor = anchor + if anchor: + if crop is False: + raise Exception("You can't specify an anchor point if crop is False.") + else: + crop = True + elif crop is None: + # Assume we are cropping if both a width and height are provided. If + # only one is, we must be resizing to fit. + crop = width is not None and height is not None + + # A default anchor if cropping. + if crop and anchor is None: + anchor = 'auto' self.crop = crop + self.anchor = anchor def process(self, img): if self.crop: diff --git a/imagekit/templatetags/imagekit.py b/imagekit/templatetags/imagekit.py index c2c6363..457d13b 100644 --- a/imagekit/templatetags/imagekit.py +++ b/imagekit/templatetags/imagekit.py @@ -11,6 +11,7 @@ register = template.Library() ASSIGNMENT_DELIMETER = 'as' HTML_ATTRS_DELIMITER = 'with' +DEFAULT_THUMBNAIL_GENERATOR = 'ik:thumbnail' _kwarg_map = { @@ -18,7 +19,7 @@ _kwarg_map = { } -def get_cache_file(context, generator_id, generator_kwargs): +def get_cache_file(context, generator_id, generator_kwargs, source=None): generator_id = generator_id.resolve(context) kwargs = dict((_kwarg_map.get(k, k), v.resolve(context)) for k, v in generator_kwargs.items()) @@ -26,6 +27,17 @@ def get_cache_file(context, generator_id, generator_kwargs): return GeneratedImageCacheFile(generator) +def parse_dimensions(dimensions): + """ + Parse the width and height values from a dimension string. Valid values are + '1x1', '1x', and 'x1'. If one of the dimensions is omitted, the parse result + will be None for that value. + + """ + width, height = [d.strip() or None for d in dimensions.split('x')] + return dict(width=width, height=height) + + class GenerateImageAssignmentNode(template.Node): def __init__(self, variable_name, generator_id, generator_kwargs): @@ -62,8 +74,75 @@ class GenerateImageTagNode(template.Node): attrs = dict((k, v.resolve(context)) for k, v in self._html_attrs.items()) - # Only add width and height if neither is specified (for proportional - # scaling). + # Only add width and height if neither is specified (to allow for + # proportional in-browser scaling). + if not 'width' in attrs and not 'height' in attrs: + attrs.update(width=file.width, height=file.height) + + attrs['src'] = file.url + attr_str = ' '.join('%s="%s"' % (escape(k), escape(v)) for k, v in + attrs.items()) + return mark_safe(u'' % attr_str) + + +class ThumbnailAssignmentNode(template.Node): + + def __init__(self, variable_name, generator_id, dimensions, source, generator_kwargs): + self._variable_name = variable_name + self._generator_id = generator_id + self._dimensions = dimensions + self._source = source + self._generator_kwargs = generator_kwargs + + def get_variable_name(self, context): + return unicode(self._variable_name) + + def render(self, context): + from ..utils import autodiscover + autodiscover() + + variable_name = self.get_variable_name(context) + + generator_id = self._generator_id.resolve(context) if self._generator_id else DEFAULT_THUMBNAIL_GENERATOR + kwargs = dict((_kwarg_map.get(k, k), v.resolve(context)) for k, + v in self._generator_kwargs.items()) + kwargs['source'] = self._source.resolve(context) + kwargs.update(parse_dimensions(self._dimensions.resolve(context))) + generator = generator_registry.get(generator_id, **kwargs) + + context[variable_name] = GeneratedImageCacheFile(generator) + + return '' + + +class ThumbnailImageTagNode(template.Node): + + def __init__(self, generator_id, dimensions, source, generator_kwargs, html_attrs): + self._generator_id = generator_id + self._dimensions = dimensions + self._source = source + self._generator_kwargs = generator_kwargs + self._html_attrs = html_attrs + + def render(self, context): + from ..utils import autodiscover + autodiscover() + + generator_id = self._generator_id.resolve(context) if self._generator_id else DEFAULT_THUMBNAIL_GENERATOR + dimensions = parse_dimensions(self._dimensions.resolve(context)) + kwargs = dict((_kwarg_map.get(k, k), v.resolve(context)) for k, + v in self._generator_kwargs.items()) + kwargs['source'] = self._source.resolve(context) + kwargs.update(dimensions) + generator = generator_registry.get(generator_id, **kwargs) + + file = GeneratedImageCacheFile(generator) + + attrs = dict((k, v.resolve(context)) for k, v in + self._html_attrs.items()) + + # Only add width and height if neither is specified (to allow for + # proportional in-browser scaling). if not 'width' in attrs and not 'height' in attrs: attrs.update(width=file.width, height=file.height) @@ -84,10 +163,17 @@ def parse_ik_tag_bits(parser, bits): html_attrs = {} tag_name = bits.pop(0) - if bits[-2] == ASSIGNMENT_DELIMETER: + if len(bits) >= 2 and bits[-2] == ASSIGNMENT_DELIMETER: varname = bits[-1] bits = bits[:-2] - elif HTML_ATTRS_DELIMITER in bits: + + if HTML_ATTRS_DELIMITER in bits: + + if varname: + raise template.TemplateSyntaxError('Do not specify html attributes' + ' (using "%s") when using the "%s" tag as an assignment' + ' tag.' % (HTML_ATTRS_DELIMITER, tag_name)) + index = bits.index(HTML_ATTRS_DELIMITER) html_bits = bits[index + 1:] bits = bits[:index] @@ -159,10 +245,9 @@ def generateimage(parser, token): #@register.tag def thumbnail(parser, token): """ - A convenient alias for the ``generateimage`` tag with the generator id - ``'ik:thumbnail'``. The following:: + A convenient shortcut syntax for generating a thumbnail. The following:: - {% thumbnail from=mymodel.profile_image width=100 height=100 %} + {% thumbnail '100x100' mymodel.profile_image %} is equivalent to:: @@ -170,28 +255,47 @@ def thumbnail(parser, token): The thumbnail tag supports the "with" and "as" bits for adding html attributes and assigning to a variable, respectively. It also accepts the - kwargs "width", "height", "anchor", and "crop". + kwargs "anchor", and "crop". To use "smart cropping" (the ``SmartResize`` processor):: - {% thumbnail from=mymodel.profile_image width=100 height=100 %} + {% thumbnail '100x100' mymodel.profile_image %} To crop, anchoring the image to the top right (the ``ResizeToFill`` processor):: - {% thumbnail from=mymodel.profile_image width=100 height=100 anchor='tr' %} + {% thumbnail '100x100' mymodel.profile_image anchor='tr' %} To resize without cropping (using the ``ResizeToFit`` processor):: - {% thumbnail from=mymodel.profile_image width=100 height=100 crop=0 %} + {% thumbnail '100x100' mymodel.profile_image crop=0 %} """ - # TODO: Support positional arguments for this tag for "from", "width" and "height". - # Example: - # {% thumbnail mymodel.profile_image 100 100 anchor='tl' %} bits = token.split_contents() - bits.insert(1, "'ik:thumbnail'") - return _generateimage(parser, bits) + + tag_name, bits, html_attrs, varname = parse_ik_tag_bits(parser, bits) + + args, kwargs = parse_bits(parser, bits, [], 'args', 'kwargs', + None, False, tag_name) + + if len(args) < 2: + raise template.TemplateSyntaxError('The "%s" tag requires at least two' + ' unnamed arguments: the dimensions and the source image.' + % tag_name) + elif len(args) > 3: + raise template.TemplateSyntaxError('The "%s" tag accepts at most three' + ' unnamed arguments: a generator id, the dimensions, and the' + ' source image.' % tag_name) + + dimensions, source = args[-2:] + generator_id = args[0] if len(args) > 2 else None + + if varname: + return ThumbnailAssignmentNode(varname, generator_id, dimensions, + source, kwargs) + else: + return ThumbnailImageTagNode(generator_id, dimensions, source, kwargs, + html_attrs) generateimage = register.tag(generateimage) From 219b8507ad35f91917fcd367a5637ea8d25793fe Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Wed, 9 Jan 2013 00:25:28 -0500 Subject: [PATCH 152/213] Add thumbnail tag tests --- tests/imagespecs.py | 8 +++++ tests/test_thumbnail_tag.py | 66 +++++++++++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+) create mode 100644 tests/test_thumbnail_tag.py diff --git a/tests/imagespecs.py b/tests/imagespecs.py index 8a69975..06d2dab 100644 --- a/tests/imagespecs.py +++ b/tests/imagespecs.py @@ -1,8 +1,16 @@ from imagekit import ImageSpec, register +from imagekit.processors import ResizeToFill class TestSpec(ImageSpec): pass +class ResizeTo1PixelSquare(ImageSpec): + def __init__(self, width=None, height=None, anchor=None, crop=None, **kwargs): + self.processors = [ResizeToFill(1, 1)] + super(ResizeTo1PixelSquare, self).__init__(**kwargs) + + register.spec('testspec', TestSpec) +register.spec('1pxsq', ResizeTo1PixelSquare) diff --git a/tests/test_thumbnail_tag.py b/tests/test_thumbnail_tag.py new file mode 100644 index 0000000..70f3f4d --- /dev/null +++ b/tests/test_thumbnail_tag.py @@ -0,0 +1,66 @@ +from django.template import TemplateSyntaxError +from nose.tools import eq_, assert_not_in, raises, assert_not_equal +from . import imagespecs # noqa +from .utils import render_tag, get_html_attrs + + +def test_img_tag(): + ttag = r"""{% thumbnail '100x100' img %}""" + attrs = get_html_attrs(ttag) + expected_attrs = set(['src', 'width', 'height']) + eq_(set(attrs.keys()), expected_attrs) + for k in expected_attrs: + assert_not_equal(attrs[k].strip(), '') + + +def test_img_tag_attrs(): + ttag = r"""{% thumbnail '100x100' img with alt="Hello" %}""" + attrs = get_html_attrs(ttag) + eq_(attrs.get('alt'), 'Hello') + + +@raises(TemplateSyntaxError) +def test_dangling_with(): + ttag = r"""{% thumbnail '100x100' img with %}""" + render_tag(ttag) + + +@raises(TemplateSyntaxError) +def test_not_enough_args(): + ttag = r"""{% thumbnail '100x100' %}""" + render_tag(ttag) + + +@raises(TemplateSyntaxError) +def test_too_many_args(): + ttag = r"""{% thumbnail 'generator_id' '100x100' img 'extra' %}""" + render_tag(ttag) + + +@raises(TemplateSyntaxError) +def test_with_assignment(): + """ + You can either use thumbnail as an assigment tag or specify html attrs, + but not both. + + """ + ttag = r"""{% thumbnail '100x100' img with alt="Hello" as th %}""" + render_tag(ttag) + + +def test_assignment_tag(): + ttag = r"""{% thumbnail '100x100' img as th %}{{ th.url }}""" + html = render_tag(ttag) + assert_not_equal(html, '') + + +def test_single_dimension(): + ttag = r"""{% thumbnail '100x' img as th %}{{ th.width }}""" + html = render_tag(ttag) + eq_(html, '100') + + +def test_alternate_generator(): + ttag = r"""{% thumbnail '1pxsq' '100x' img as th %}{{ th.width }}""" + html = render_tag(ttag) + eq_(html, '1') From 5acce98223df75b2e482ed754ffa8325e286fa08 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Wed, 9 Jan 2013 00:26:47 -0500 Subject: [PATCH 153/213] Remove extra space --- tests/test_generateimage_tag.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_generateimage_tag.py b/tests/test_generateimage_tag.py index 35c29d8..76ad2a5 100644 --- a/tests/test_generateimage_tag.py +++ b/tests/test_generateimage_tag.py @@ -47,6 +47,6 @@ def test_single_dimension_attr(): def test_assignment_tag(): - ttag = r"""{% generateimage 'testspec' from=img as th %} {{ th.url }}""" + ttag = r"""{% generateimage 'testspec' from=img as th %}{{ th.url }}""" html = render_tag(ttag) assert_not_equal(html.strip(), '') From e5b15d09bdc74f3c67dd37fa197644456ba62ae7 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Wed, 9 Jan 2013 00:28:29 -0500 Subject: [PATCH 154/213] Remove _generateimage utility. --- imagekit/templatetags/imagekit.py | 35 ++++++++++++++----------------- 1 file changed, 16 insertions(+), 19 deletions(-) diff --git a/imagekit/templatetags/imagekit.py b/imagekit/templatetags/imagekit.py index 457d13b..9daacc2 100644 --- a/imagekit/templatetags/imagekit.py +++ b/imagekit/templatetags/imagekit.py @@ -192,24 +192,6 @@ def parse_ik_tag_bits(parser, bits): return (tag_name, bits, html_attrs, varname) -def _generateimage(parser, bits): - tag_name, bits, html_attrs, varname = parse_ik_tag_bits(parser, bits) - - args, kwargs = parse_bits(parser, bits, ['generator_id'], 'args', 'kwargs', - None, False, tag_name) - - if len(args) != 1: - raise template.TemplateSyntaxError('The "%s" tag requires exactly one' - ' unnamed argument (the generator id).' % tag_name) - - generator_id = args[0] - - if varname: - return GenerateImageAssignmentNode(varname, generator_id, kwargs) - else: - return GenerateImageTagNode(generator_id, kwargs, html_attrs) - - #@register.tag def generateimage(parser, token): """ @@ -239,7 +221,22 @@ def generateimage(parser, token): """ bits = token.split_contents() - return _generateimage(parser, bits) + + tag_name, bits, html_attrs, varname = parse_ik_tag_bits(parser, bits) + + args, kwargs = parse_bits(parser, bits, ['generator_id'], 'args', 'kwargs', + None, False, tag_name) + + if len(args) != 1: + raise template.TemplateSyntaxError('The "%s" tag requires exactly one' + ' unnamed argument (the generator id).' % tag_name) + + generator_id = args[0] + + if varname: + return GenerateImageAssignmentNode(varname, generator_id, kwargs) + else: + return GenerateImageTagNode(generator_id, kwargs, html_attrs) #@register.tag From 8c5a571293d7b799df027d1e8664840c09638d8a Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Sun, 13 Jan 2013 23:35:19 -0500 Subject: [PATCH 155/213] Remove unused import Fixes flake8 error --- tests/test_thumbnail_tag.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_thumbnail_tag.py b/tests/test_thumbnail_tag.py index 70f3f4d..5fdbbb2 100644 --- a/tests/test_thumbnail_tag.py +++ b/tests/test_thumbnail_tag.py @@ -1,5 +1,5 @@ from django.template import TemplateSyntaxError -from nose.tools import eq_, assert_not_in, raises, assert_not_equal +from nose.tools import eq_, raises, assert_not_equal from . import imagespecs # noqa from .utils import render_tag, get_html_attrs From 4ecfa5d35e962af1cebf4bf70bc2491b6cf34e4b Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Sun, 13 Jan 2013 23:40:26 -0500 Subject: [PATCH 156/213] Don't rely on source filename being relative path Closes #180 --- imagekit/specs/__init__.py | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/imagekit/specs/__init__.py b/imagekit/specs/__init__.py index e9e72cf..9bfab2c 100644 --- a/imagekit/specs/__init__.py +++ b/imagekit/specs/__init__.py @@ -93,15 +93,20 @@ class ImageSpec(BaseImageSpec): @property def cache_file_name(self): - source_filename = self.source.name - ext = suggest_extension(source_filename, self.format) - return os.path.normpath(os.path.join( - settings.IMAGEKIT_CACHE_DIR, - os.path.splitext(source_filename)[0], - '%s%s' % (self.get_hash(), ext))) + source_filename = getattr(self.source, 'name', None) - return os.path.join(settings.IMAGEKIT_CACHE_DIR, - '%s%s' % (hash, ext)) + if source_filename is None or os.path.isabs(source_filename): + # Generally, we put the file right in the cache directory. + dir = settings.IMAGEKIT_CACHE_DIR + else: + # For source files with relative names (like Django media files), + # use the source's name to create the new filename. + dir = os.path.join(settings.IMAGEKIT_CACHE_DIR, + os.path.splitext(source_filename)[0]) + + ext = suggest_extension(source_filename or '', self.format) + return os.path.normpath(os.path.join(dir, + '%s%s' % (self.get_hash(), ext))) def __getstate__(self): state = self.__dict__ From d632fc70fab17d962f26369b4f2e0460430f4298 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Wed, 23 Jan 2013 21:27:21 -0500 Subject: [PATCH 157/213] Copy contents to NamedTemporaryFile if generated file has no name --- imagekit/files.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/imagekit/files.py b/imagekit/files.py index 9916c2c..b4efd84 100644 --- a/imagekit/files.py +++ b/imagekit/files.py @@ -3,6 +3,7 @@ from django.core.files.base import ContentFile, File from django.core.files.images import ImageFile from django.utils.encoding import smart_str, smart_unicode import os +from tempfile import NamedTemporaryFile from .signals import before_access from .utils import (format_to_mimetype, extension_to_mimetype, get_logger, get_singleton) @@ -116,7 +117,16 @@ class GeneratedImageCacheFile(BaseIKFile, ImageFile): def generate(self): # Generate the file content = self.generator.generate() - actual_name = self.storage.save(self.name, content) + + # If the file doesn't have a name, Django will raise an Exception while + # trying to save it, so we create a named temporary file. + if not getattr(content, 'name', None): + f = NamedTemporaryFile() + f.write(content.read()) + f.seek(0) + content = f + + actual_name = self.storage.save(self.name, File(content)) if actual_name != self.name: get_logger().warning('The storage backend %s did not save the file' From eef1e41448ad11e0a0e78f3515123226388fde46 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Wed, 23 Jan 2013 21:28:23 -0500 Subject: [PATCH 158/213] Remove code that used old filename kwarg --- imagekit/specs/__init__.py | 29 ++++++----------------------- 1 file changed, 6 insertions(+), 23 deletions(-) diff --git a/imagekit/specs/__init__.py b/imagekit/specs/__init__.py index 9bfab2c..e05aac8 100644 --- a/imagekit/specs/__init__.py +++ b/imagekit/specs/__init__.py @@ -1,15 +1,14 @@ from django.conf import settings +from django.core.files import File from django.db.models.fields.files import ImageFieldFile from hashlib import md5 import os import pickle -from ..exceptions import UnknownExtensionError -from ..files import GeneratedImageCacheFile, IKContentFile +from ..files import GeneratedImageCacheFile from ..imagecache.backends import get_default_image_cache_backend from ..imagecache.strategies import StrategyWrapper from ..processors import ProcessorPipeline -from ..utils import (open_image, extension_to_format, img_to_fobj, - suggest_extension) +from ..utils import open_image, img_to_fobj, suggest_extension from ..registry import generator_registry, register @@ -140,9 +139,7 @@ class ImageSpec(BaseImageSpec): def generate(self): # TODO: Move into a generator base class # TODO: Factor out a generate_image function so you can create a generator and only override the PIL.Image creating part. (The tricky part is how to deal with original_format since generator base class won't have one.) - source = self.source - filename = self.kwargs.get('filename') - img = open_image(source) + img = open_image(self.source) original_format = img.format # Run the processors @@ -150,22 +147,8 @@ class ImageSpec(BaseImageSpec): img = ProcessorPipeline(processors or []).process(img) options = dict(self.options or {}) - - # Determine the format. - format = self.format - if filename and not format: - # Try to guess the format from the extension. - extension = os.path.splitext(filename)[1].lower() - if extension: - try: - format = extension_to_format(extension) - except UnknownExtensionError: - pass - format = format or img.format or original_format or 'JPEG' - - imgfile = img_to_fobj(img, format, **options) - # TODO: Is this the right place to wrap the file? Can we use a mixin instead? Is it even still having the desired effect? Re: #111 - content = IKContentFile(filename, imgfile.read(), format=format) + format = self.format or img.format or original_format or 'JPEG' + content = img_to_fobj(img, format, **options) return content From 6ff1d35fbea771a692fe7e650ed6889e922fe819 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Wed, 23 Jan 2013 21:31:53 -0500 Subject: [PATCH 159/213] Remove unused import --- imagekit/specs/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/imagekit/specs/__init__.py b/imagekit/specs/__init__.py index e05aac8..062f239 100644 --- a/imagekit/specs/__init__.py +++ b/imagekit/specs/__init__.py @@ -1,5 +1,4 @@ from django.conf import settings -from django.core.files import File from django.db.models.fields.files import ImageFieldFile from hashlib import md5 import os From 4737ac64c43a2af4a892f93fe6df19b4c9288407 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Wed, 23 Jan 2013 21:35:38 -0500 Subject: [PATCH 160/213] Specs no longer accept arbitrary kwargs Only the source. --- imagekit/management/commands/warmimagecache.py | 2 +- imagekit/models/fields/utils.py | 2 +- imagekit/registry.py | 2 +- imagekit/specs/__init__.py | 10 ++++------ 4 files changed, 7 insertions(+), 9 deletions(-) diff --git a/imagekit/management/commands/warmimagecache.py b/imagekit/management/commands/warmimagecache.py index 6c9822d..2d147f5 100644 --- a/imagekit/management/commands/warmimagecache.py +++ b/imagekit/management/commands/warmimagecache.py @@ -22,7 +22,7 @@ class Command(BaseCommand): for source_group in source_group_registry.get(spec_id): for source in source_group.files(): if source: - spec = generator_registry.get(spec_id, source=source) # TODO: HINTS! (Probably based on source, so this will need to be moved into loop below.) + spec = generator_registry.get(spec_id, source=source) self.stdout.write(' %s\n' % source) try: # TODO: Allow other validation actions through command option diff --git a/imagekit/models/fields/utils.py b/imagekit/models/fields/utils.py index 39dbc52..2324a33 100644 --- a/imagekit/models/fields/utils.py +++ b/imagekit/models/fields/utils.py @@ -29,7 +29,7 @@ class ImageSpecFileDescriptor(object): self.attname)) else: source = image_fields[0] - spec = self.field.get_spec(source=source) # TODO: What "hints" should we pass here? + spec = self.field.get_spec(source=source) file = GeneratedImageCacheFile(spec) instance.__dict__[self.attname] = file return file diff --git a/imagekit/registry.py b/imagekit/registry.py index 8d082cd..9c0f4b8 100644 --- a/imagekit/registry.py +++ b/imagekit/registry.py @@ -102,7 +102,7 @@ class SourceGroupRegistry(object): if source_group not in self._source_groups: return - for spec in (generator_registry.get(id, source=source, **info) + for spec in (generator_registry.get(id, source=source) for id in self._source_groups[source_group]): event_name = { source_created: 'source_created', diff --git a/imagekit/specs/__init__.py b/imagekit/specs/__init__.py index 062f239..d2b154b 100644 --- a/imagekit/specs/__init__.py +++ b/imagekit/specs/__init__.py @@ -36,7 +36,7 @@ class BaseImageSpec(object): """ - def __init__(self, **kwargs): + def __init__(self): self.image_cache_backend = self.image_cache_backend or get_default_image_cache_backend() self.image_cache_strategy = StrategyWrapper(self.image_cache_strategy) @@ -83,10 +83,9 @@ class ImageSpec(BaseImageSpec): """ - def __init__(self, source, **kwargs): + def __init__(self, source): self.source = source self.processors = self.processors or [] - self.kwargs = kwargs super(ImageSpec, self).__init__() @property @@ -128,7 +127,6 @@ class ImageSpec(BaseImageSpec): def get_hash(self): return md5(pickle.dumps([ self.source.name, - self.kwargs, self.processors, self.format, self.options, @@ -212,7 +210,7 @@ class SpecHost(object): self.spec_id = id register.spec(id, self._original_spec) - def get_spec(self, **kwargs): + def get_spec(self, source): """ Look up the spec by the spec id. We do this (instead of storing the spec as an attribute) so that users can override apps' specs--without @@ -222,4 +220,4 @@ class SpecHost(object): """ if not getattr(self, 'spec_id', None): raise Exception('Object %s has no spec id.' % self) - return generator_registry.get(self.spec_id, **kwargs) + return generator_registry.get(self.spec_id, source=source) From d52b9c810006c3551e286739c0238f268c958a6f Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Wed, 23 Jan 2013 21:47:54 -0500 Subject: [PATCH 161/213] Add utility for extracting field info --- imagekit/utils.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/imagekit/utils.py b/imagekit/utils.py index ac93cf6..e6c2943 100644 --- a/imagekit/utils.py +++ b/imagekit/utils.py @@ -382,3 +382,22 @@ def get_logger(logger_name='imagekit', add_null_handler=True): if add_null_handler: logger.addHandler(logging.NullHandler()) return logger + + +def get_field_info(field_file): + """ + A utility for easily extracting information about the host model from a + Django FileField (or subclass). This is especially useful for when you want + to alter processors based on a property of the source model. For example:: + + class MySpec(ImageSpec): + def __init__(self, source): + instance, attname = get_field_info(source) + self.processors = [SmartResize(instance.thumbnail_width, + instance.thumbnail_height)] + + """ + return ( + getattr(field_file, 'instance', None), + getattr(getattr(field_file, 'field', None), 'attname', None), + ) From 9dd7bef709a53cd8757fb04ee07eaa61740d7fed Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Wed, 23 Jan 2013 22:07:31 -0500 Subject: [PATCH 162/213] Simplify import --- tests/models.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/models.py b/tests/models.py index 199fdc0..1aa55e4 100644 --- a/tests/models.py +++ b/tests/models.py @@ -1,9 +1,7 @@ from django.db import models from imagekit.models import ImageSpecField -from imagekit.processors import Adjust -from imagekit.processors import ResizeToFill -from imagekit.processors import SmartCrop +from imagekit.processors import Adjust, ResizeToFill, SmartCrop class Photo(models.Model): From 234082e63cba8b86066d63554b85b9f3d594d411 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Wed, 23 Jan 2013 22:34:29 -0500 Subject: [PATCH 163/213] Extract generate() util, to make files Django likes --- imagekit/files.py | 15 +++------------ imagekit/utils.py | 21 +++++++++++++++++++++ 2 files changed, 24 insertions(+), 12 deletions(-) diff --git a/imagekit/files.py b/imagekit/files.py index b4efd84..1dd8976 100644 --- a/imagekit/files.py +++ b/imagekit/files.py @@ -3,10 +3,9 @@ from django.core.files.base import ContentFile, File from django.core.files.images import ImageFile from django.utils.encoding import smart_str, smart_unicode import os -from tempfile import NamedTemporaryFile from .signals import before_access from .utils import (format_to_mimetype, extension_to_mimetype, get_logger, - get_singleton) + get_singleton, generate) class BaseIKFile(File): @@ -116,17 +115,9 @@ class GeneratedImageCacheFile(BaseIKFile, ImageFile): def generate(self): # Generate the file - content = self.generator.generate() + content = generate(self.generator) - # If the file doesn't have a name, Django will raise an Exception while - # trying to save it, so we create a named temporary file. - if not getattr(content, 'name', None): - f = NamedTemporaryFile() - f.write(content.read()) - f.seek(0) - content = f - - actual_name = self.storage.save(self.name, File(content)) + actual_name = self.storage.save(self.name, content) if actual_name != self.name: get_logger().warning('The storage backend %s did not save the file' diff --git a/imagekit/utils.py b/imagekit/utils.py index e6c2943..82a2c02 100644 --- a/imagekit/utils.py +++ b/imagekit/utils.py @@ -2,9 +2,11 @@ import logging import os import mimetypes import sys +from tempfile import NamedTemporaryFile import types from django.core.exceptions import ImproperlyConfigured +from django.core.files import File from django.db.models.loading import cache from django.utils.functional import wraps from django.utils.importlib import import_module @@ -401,3 +403,22 @@ def get_field_info(field_file): getattr(field_file, 'instance', None), getattr(getattr(field_file, 'field', None), 'attname', None), ) + + +def generate(generator): + """ + Calls the ``generate()`` method of a generator instance, and then wraps the + result in a Django File object so Django knows how to save it. + + """ + content = generator.generate() + + # If the file doesn't have a name, Django will raise an Exception while + # trying to save it, so we create a named temporary file. + if not getattr(content, 'name', None): + f = NamedTemporaryFile() + f.write(content.read()) + f.seek(0) + content = f + + return File(content) From c6f2c2e7a7377eec830e30680ce69a23c5e1c89c Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Wed, 23 Jan 2013 22:35:57 -0500 Subject: [PATCH 164/213] Add test for ProcessedImageField --- tests/models.py | 6 ++++++ tests/test_fields.py | 15 +++++++++++++++ 2 files changed, 21 insertions(+) create mode 100644 tests/test_fields.py diff --git a/tests/models.py b/tests/models.py index 1aa55e4..0b997ea 100644 --- a/tests/models.py +++ b/tests/models.py @@ -1,5 +1,6 @@ from django.db import models +from imagekit.models import ProcessedImageField from imagekit.models import ImageSpecField from imagekit.processors import Adjust, ResizeToFill, SmartCrop @@ -16,6 +17,11 @@ class Photo(models.Model): format='JPEG', options={'quality': 90}) +class ProcessedImageFieldModel(models.Model): + processed = ProcessedImageField([SmartCrop(50, 50)], format='JPEG', + options={'quality': 90}, upload_to='p') + + class AbstractImageModel(models.Model): original_image = models.ImageField(upload_to='photos') abstract_class_spec = ImageSpecField() diff --git a/tests/test_fields.py b/tests/test_fields.py new file mode 100644 index 0000000..ee7301c --- /dev/null +++ b/tests/test_fields.py @@ -0,0 +1,15 @@ +from django.core.files.base import File +from nose.tools import eq_ +from . import imagespecs # noqa +from .models import ProcessedImageFieldModel +from .utils import get_image_file + + +def test_model_processedimagefield(): + instance = ProcessedImageFieldModel() + file = File(get_image_file()) + instance.processed.save('whatever.jpeg', file) + instance.save() + + eq_(instance.processed.width, 50) + eq_(instance.processed.height, 50) From 84b30e990fdb6a24aa48f8d36b998d2df8b07b89 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Wed, 23 Jan 2013 22:37:00 -0500 Subject: [PATCH 165/213] Fix imagekit.models.fields.ProcessedImageField --- imagekit/models/fields/files.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/imagekit/models/fields/files.py b/imagekit/models/fields/files.py index 26037fd..0fbbad6 100644 --- a/imagekit/models/fields/files.py +++ b/imagekit/models/fields/files.py @@ -1,13 +1,13 @@ from django.db.models.fields.files import ImageFieldFile import os -from ...utils import suggest_extension +from ...utils import suggest_extension, generate class ProcessedImageFieldFile(ImageFieldFile): def save(self, name, content, save=True): filename, ext = os.path.splitext(name) - spec = self.field.get_spec() # TODO: What "hints"? + spec = self.field.get_spec(source=content) ext = suggest_extension(name, spec.format) new_name = '%s%s' % (filename, ext) - content = spec.apply(content, new_name) + content = generate(spec) return super(ProcessedImageFieldFile, self).save(new_name, content, save) From f4917ab7ca267c446420209b98812c5ae8b59d55 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Wed, 23 Jan 2013 22:39:14 -0500 Subject: [PATCH 166/213] Clean up util method --- tests/utils.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/tests/utils.py b/tests/utils.py index 1a4e3fc..2763f8f 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -1,7 +1,7 @@ from bs4 import BeautifulSoup import os from django.conf import settings -from django.core.files.base import ContentFile +from django.core.files import File from django.template import Context, Template from imagekit.lib import Image, StringIO import pickle @@ -26,10 +26,8 @@ def create_image(): def create_instance(model_class, image_name): instance = model_class() - img = get_image_file() - file = ContentFile(img.read()) - instance.original_image = file - instance.original_image.save(image_name, file) + img = File(get_image_file()) + instance.original_image.save(image_name, img) instance.save() img.close() return instance From a8855d4c27ce0df8cf8c0b40d2c1c368b2befebe Mon Sep 17 00:00:00 2001 From: Eric Eldredge Date: Wed, 23 Jan 2013 22:46:57 -0500 Subject: [PATCH 167/213] Change spec/source registry to generator/cacheable --- imagekit/generatorlibrary.py | 2 +- imagekit/imagecache/strategies.py | 12 +- .../management/commands/warmimagecache.py | 34 ++--- imagekit/models/fields/__init__.py | 4 +- imagekit/registry.py | 116 +++++++++--------- imagekit/signals.py | 6 +- imagekit/specs/__init__.py | 2 +- imagekit/specs/sourcegroups.py | 18 +-- tests/imagespecs.py | 4 +- 9 files changed, 104 insertions(+), 94 deletions(-) diff --git a/imagekit/generatorlibrary.py b/imagekit/generatorlibrary.py index bd9da9a..977e960 100644 --- a/imagekit/generatorlibrary.py +++ b/imagekit/generatorlibrary.py @@ -10,4 +10,4 @@ class Thumbnail(ImageSpec): super(Thumbnail, self).__init__(**kwargs) -register.spec('ik:thumbnail', Thumbnail) +register.generator('ik:thumbnail', Thumbnail) diff --git a/imagekit/imagecache/strategies.py b/imagekit/imagecache/strategies.py index 630b77c..a0b8f76 100644 --- a/imagekit/imagecache/strategies.py +++ b/imagekit/imagecache/strategies.py @@ -14,19 +14,19 @@ class JustInTime(object): class Optimistic(object): """ - A caching strategy that acts immediately when the source file chages and - assumes that the cache files will not be removed (i.e. doesn't revalidate - on access). + A caching strategy that acts immediately when the cacheable file changes + and assumes that the cache files will not be removed (i.e. doesn't + revalidate on access). """ - def on_source_created(self, file): + def on_cacheable_created(self, file): validate_now(file) - def on_source_deleted(self, file): + def on_cacheable_deleted(self, file): clear_now(file) - def on_source_changed(self, file): + def on_cacheable_changed(self, file): validate_now(file) diff --git a/imagekit/management/commands/warmimagecache.py b/imagekit/management/commands/warmimagecache.py index 6c9822d..42a005d 100644 --- a/imagekit/management/commands/warmimagecache.py +++ b/imagekit/management/commands/warmimagecache.py @@ -1,35 +1,35 @@ from django.core.management.base import BaseCommand import re from ...files import GeneratedImageCacheFile -from ...registry import generator_registry, source_group_registry +from ...registry import generator_registry, cacheable_registry class Command(BaseCommand): - help = ('Warm the image cache for the specified specs (or all specs if none' - ' was provided). Simple wildcard matching (using asterisks) is' - ' supported.') - args = '[spec_ids]' + help = ('Warm the image cache for the specified generators' + ' (or all generators if none was provided).' + ' Simple wildcard matching (using asterisks) is supported.') + args = '[generator_ids]' def handle(self, *args, **options): - specs = generator_registry.get_ids() + generators = generator_registry.get_ids() if args: patterns = self.compile_patterns(args) - specs = (id for id in specs if any(p.match(id) for p in patterns)) + generators = (id for id in generators if any(p.match(id) for p in patterns)) - for spec_id in specs: - self.stdout.write('Validating spec: %s\n' % spec_id) - for source_group in source_group_registry.get(spec_id): - for source in source_group.files(): - if source: - spec = generator_registry.get(spec_id, source=source) # TODO: HINTS! (Probably based on source, so this will need to be moved into loop below.) - self.stdout.write(' %s\n' % source) + for generator_id in generators: + self.stdout.write('Validating generator: %s\n' % generator_id) + for cacheables in cacheable_registry.get(generator_id): + for cacheable in cacheables.files(): + if cacheable: + generator = generator_registry.get(generator_id, cacheable=cacheable) # TODO: HINTS! (Probably based on cacheable, so this will need to be moved into loop below.) + self.stdout.write(' %s\n' % cacheable) try: # TODO: Allow other validation actions through command option - GeneratedImageCacheFile(spec).validate() + GeneratedImageCacheFile(generator).validate() except Exception, err: # TODO: How should we handle failures? Don't want to error, but should call it out more than this. self.stdout.write(' FAILED: %s\n' % err) - def compile_patterns(self, spec_ids): - return [re.compile('%s$' % '.*'.join(re.escape(part) for part in id.split('*'))) for id in spec_ids] + def compile_patterns(self, generator_ids): + return [re.compile('%s$' % '.*'.join(re.escape(part) for part in id.split('*'))) for id in generator_ids] diff --git a/imagekit/models/fields/__init__.py b/imagekit/models/fields/__init__.py index 131ff33..449faac 100644 --- a/imagekit/models/fields/__init__.py +++ b/imagekit/models/fields/__init__.py @@ -45,8 +45,8 @@ class ImageSpecField(SpecHostField): self.set_spec_id(cls, name) # Add the model and field as a source for this spec id - register.sources(self.spec_id, - [ImageFieldSourceGroup(cls, self.source)]) + register.cacheables(self.spec_id, + ImageFieldSourceGroup(cls, self.source)) class ProcessedImageField(models.ImageField, SpecHostField): diff --git a/imagekit/registry.py b/imagekit/registry.py index 8d082cd..93e491f 100644 --- a/imagekit/registry.py +++ b/imagekit/registry.py @@ -1,11 +1,11 @@ from .exceptions import AlreadyRegistered, NotRegistered -from .signals import (before_access, source_created, source_changed, - source_deleted) +from .signals import (before_access, cacheable_created, cacheable_changed, + cacheable_deleted) class GeneratorRegistry(object): """ - An object for registering generators (specs). This registry provides + An object for registering generators. This registry provides a convenient way for a distributable app to define default generators without locking the users of the app into it. @@ -15,7 +15,7 @@ class GeneratorRegistry(object): def register(self, id, generator): if id in self._generators: - raise AlreadyRegistered('The spec or generator with id %s is' + raise AlreadyRegistered('The generator with id %s is' ' already registered' % id) self._generators[id] = generator @@ -24,14 +24,14 @@ class GeneratorRegistry(object): try: del self._generators[id] except KeyError: - raise NotRegistered('The spec or generator with id %s is not' + raise NotRegistered('The generator with id %s is not' ' registered' % id) def get(self, id, **kwargs): try: generator = self._generators[id] except KeyError: - raise NotRegistered('The spec or generator with id %s is not' + raise NotRegistered('The generator with id %s is not' ' registered' % id) if callable(generator): return generator(**kwargs) @@ -42,108 +42,114 @@ class GeneratorRegistry(object): return self._generators.keys() -class SourceGroupRegistry(object): +class CacheableRegistry(object): """ - An object for registering source groups with specs. The two are + An object for registering cacheables with generators. The two are associated with each other via a string id. We do this (as opposed to - associating them directly by, for example, putting a ``source_groups`` - attribute on specs) so that specs can be overridden without losing the - associated sources. That way, a distributable app can define its own - specs without locking the users of the app into it. + associating them directly by, for example, putting a ``cacheables`` + attribute on generators) so that generators can be overridden without + losing the associated cacheables. That way, a distributable app can define + its own generators without locking the users of the app into it. """ _signals = [ - source_created, - source_changed, - source_deleted, + cacheable_created, + cacheable_changed, + cacheable_deleted, ] def __init__(self): - self._source_groups = {} + self._cacheables = {} for signal in self._signals: - signal.connect(self.source_group_receiver) + signal.connect(self.cacheable_receiver) before_access.connect(self.before_access_receiver) - def register(self, spec_id, source_groups): + def register(self, generator_id, cacheables): """ - Associates source groups with a spec id + Associates cacheables with a generator id """ - for source_group in source_groups: - if source_group not in self._source_groups: - self._source_groups[source_group] = set() - self._source_groups[source_group].add(spec_id) + for cacheable in cacheables: + if cacheable not in self._cacheables: + self._cacheables[cacheable] = set() + self._cacheables[cacheable].add(generator_id) - def unregister(self, spec_id, source_groups): + def unregister(self, generator_id, cacheables): """ - Disassociates sources with a spec id + Disassociates cacheables with a generator id """ - for source_group in source_groups: + for cacheable in cacheables: try: - self._source_groups[source_group].remove(spec_id) + self._cacheables[cacheable].remove(generator_id) except KeyError: continue - def get(self, spec_id): - return [source_group for source_group in self._source_groups - if spec_id in self._source_groups[source_group]] + def get(self, generator_id): + return [cacheable for cacheable in self._cacheables + if generator_id in self._cacheables[cacheable]] - def before_access_receiver(self, sender, generator, file, **kwargs): - generator.image_cache_strategy.invoke_callback('before_access', file) + def before_access_receiver(self, sender, generator, cacheable, **kwargs): + generator.image_cache_strategy.invoke_callback('before_access', cacheable) - def source_group_receiver(self, sender, source, signal, info, **kwargs): + def cacheable_receiver(self, sender, cacheable, signal, info, **kwargs): """ - Redirects signals dispatched on sources to the appropriate specs. + Redirects signals dispatched on cacheables + to the appropriate generators. """ - source_group = sender - if source_group not in self._source_groups: + cacheable = sender + if cacheable not in self._cacheables: return - for spec in (generator_registry.get(id, source=source, **info) - for id in self._source_groups[source_group]): + for generator in (generator_registry.get(id, cacheable=cacheable, **info) + for id in self._cacheables[cacheable]): event_name = { - source_created: 'source_created', - source_changed: 'source_changed', - source_deleted: 'source_deleted', + cacheable_created: 'cacheable_created', + cacheable_changed: 'cacheable_changed', + cacheable_deleted: 'cacheable_deleted', } - spec._handle_source_event(event_name, source) + generator._handle_cacheable_event(event_name, cacheable) class Register(object): """ - Register specs and sources. + Register generators and cacheables. """ - def spec(self, id, spec=None): - if spec is None: + def generator(self, id, generator=None): + if generator is None: # Return a decorator def decorator(cls): - self.spec(id, cls) + self.generator(id, cls) return cls return decorator - generator_registry.register(id, spec) + generator_registry.register(id, generator) - def sources(self, spec_id, sources): - source_group_registry.register(spec_id, sources) + # iterable that returns kwargs or callable that returns iterable of kwargs + def cacheables(self, generator_id, cacheables): + if callable(cacheables): + cacheables = cacheables() + cacheable_registry.register(generator_id, cacheables) class Unregister(object): """ - Unregister specs and sources. + Unregister generators and cacheables. """ - def spec(self, id, spec): - generator_registry.unregister(id, spec) + def generator(self, id, generator): + generator_registry.unregister(id, generator) - def sources(self, spec_id, sources): - source_group_registry.unregister(spec_id, sources) + def cacheables(self, generator_id, cacheables): + if callable(cacheables): + cacheables = cacheables() + cacheable_registry.unregister(generator_id, cacheables) generator_registry = GeneratorRegistry() -source_group_registry = SourceGroupRegistry() +cacheable_registry = CacheableRegistry() register = Register() unregister = Unregister() diff --git a/imagekit/signals.py b/imagekit/signals.py index 4c7aefa..97a9d9c 100644 --- a/imagekit/signals.py +++ b/imagekit/signals.py @@ -1,6 +1,6 @@ from django.dispatch import Signal before_access = Signal() -source_created = Signal(providing_args=[]) -source_changed = Signal() -source_deleted = Signal() +cacheable_created = Signal(providing_args=[]) +cacheable_changed = Signal() +cacheable_deleted = Signal() diff --git a/imagekit/specs/__init__.py b/imagekit/specs/__init__.py index 9bfab2c..015d4a6 100644 --- a/imagekit/specs/__init__.py +++ b/imagekit/specs/__init__.py @@ -228,7 +228,7 @@ class SpecHost(object): """ self.spec_id = id - register.spec(id, self._original_spec) + register.generator(id, self._original_spec) def get_spec(self, **kwargs): """ diff --git a/imagekit/specs/sourcegroups.py b/imagekit/specs/sourcegroups.py index b36b211..6bd1eed 100644 --- a/imagekit/specs/sourcegroups.py +++ b/imagekit/specs/sourcegroups.py @@ -1,6 +1,6 @@ from django.db.models.signals import post_init, post_save, post_delete from django.utils.functional import wraps -from ..signals import source_created, source_changed, source_deleted +from ..signals import cacheable_created, cacheable_changed, cacheable_deleted def ik_model_receiver(fn): @@ -58,23 +58,25 @@ class ModelSignalRouter(object): src in self._source_groups if src.model_class is instance.__class__) @ik_model_receiver - def post_save_receiver(self, sender, instance=None, created=False, raw=False, **kwargs): + def post_save_receiver(self, sender, instance=None, created=False, + raw=False, **kwargs): if not raw: self.init_instance(instance) old_hashes = instance._ik.get('source_hashes', {}).copy() new_hashes = self.update_source_hashes(instance) for attname, file in self.get_field_dict(instance).items(): if created: - self.dispatch_signal(source_created, file, sender, instance, - attname) + self.dispatch_signal(cacheable_created, file, sender, + instance, attname) elif old_hashes[attname] != new_hashes[attname]: - self.dispatch_signal(source_changed, file, sender, instance, - attname) + self.dispatch_signal(cacheable_changed, file, sender, + instance, attname) @ik_model_receiver def post_delete_receiver(self, sender, instance=None, **kwargs): for attname, file in self.get_field_dict(instance).items(): - self.dispatch_signal(source_deleted, file, sender, instance, attname) + self.dispatch_signal(cacheable_deleted, file, sender, instance, + attname) @ik_model_receiver def post_init_receiver(self, sender, instance=None, **kwargs): @@ -119,5 +121,7 @@ class ImageFieldSourceGroup(object): for instance in self.model_class.objects.all(): yield getattr(instance, self.image_field) + def __call__(self): + return self.files() signal_router = ModelSignalRouter() diff --git a/tests/imagespecs.py b/tests/imagespecs.py index 06d2dab..11e87b3 100644 --- a/tests/imagespecs.py +++ b/tests/imagespecs.py @@ -12,5 +12,5 @@ class ResizeTo1PixelSquare(ImageSpec): super(ResizeTo1PixelSquare, self).__init__(**kwargs) -register.spec('testspec', TestSpec) -register.spec('1pxsq', ResizeTo1PixelSquare) +register.generator('testspec', TestSpec) +register.generator('1pxsq', ResizeTo1PixelSquare) From b45a22abe6f472b6fc875b442fdc90e3a4330284 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Wed, 23 Jan 2013 22:54:08 -0500 Subject: [PATCH 168/213] Add test for imagekit.forms.fields.ProcessedImageField --- tests/models.py | 4 ++++ tests/test_fields.py | 23 ++++++++++++++++++++++- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/tests/models.py b/tests/models.py index 0b997ea..17e887b 100644 --- a/tests/models.py +++ b/tests/models.py @@ -5,6 +5,10 @@ from imagekit.models import ImageSpecField from imagekit.processors import Adjust, ResizeToFill, SmartCrop +class ImageModel(models.Model): + image = models.ImageField(upload_to='b') + + class Photo(models.Model): original_image = models.ImageField(upload_to='photos') diff --git a/tests/test_fields.py b/tests/test_fields.py index ee7301c..bce294f 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -1,7 +1,11 @@ +from django import forms from django.core.files.base import File +from django.core.files.uploadedfile import SimpleUploadedFile +from imagekit import forms as ikforms +from imagekit.processors import SmartCrop from nose.tools import eq_ from . import imagespecs # noqa -from .models import ProcessedImageFieldModel +from .models import ProcessedImageFieldModel, ImageModel from .utils import get_image_file @@ -13,3 +17,20 @@ def test_model_processedimagefield(): eq_(instance.processed.width, 50) eq_(instance.processed.height, 50) + + +def test_form_processedimagefield(): + class TestForm(forms.ModelForm): + image = ikforms.ProcessedImageField(spec_id='tests:testform_image', + processors=[SmartCrop(50, 50)], format='JPEG') + + class Meta: + model = ImageModel + + upload_file = get_image_file() + file_dict = {'image': SimpleUploadedFile('abc.jpg', upload_file.read())} + form = TestForm({}, file_dict) + instance = form.save() + + eq_(instance.image.width, 50) + eq_(instance.image.height, 50) From c202234e827b294f7a7d418d868f0c6f95715929 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Wed, 23 Jan 2013 22:54:25 -0500 Subject: [PATCH 169/213] Fix imagekit.forms.fields.ProcessedImageField --- imagekit/forms/fields.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/imagekit/forms/fields.py b/imagekit/forms/fields.py index 40bb5b5..903f6ae 100644 --- a/imagekit/forms/fields.py +++ b/imagekit/forms/fields.py @@ -1,5 +1,6 @@ from django.forms import ImageField from ..specs import SpecHost +from ..utils import generate class ProcessedImageField(ImageField, SpecHost): @@ -22,7 +23,7 @@ class ProcessedImageField(ImageField, SpecHost): data = super(ProcessedImageField, self).clean(data, initial) if data: - spec = self.get_spec() # HINTS?!?!?!?!?! - data = spec.apply(data, data.name) + spec = self.get_spec(source=data) + data = generate(spec) return data From eb9089e0c8966adea727c2bb9460b371360c79b1 Mon Sep 17 00:00:00 2001 From: Eric Eldredge Date: Thu, 24 Jan 2013 00:04:43 -0500 Subject: [PATCH 170/213] Register cacheables as generators instead of items --- .../management/commands/warmimagecache.py | 21 +++++++-------- imagekit/registry.py | 26 ++++++++----------- imagekit/specs/sourcegroups.py | 7 ++--- 3 files changed, 23 insertions(+), 31 deletions(-) diff --git a/imagekit/management/commands/warmimagecache.py b/imagekit/management/commands/warmimagecache.py index 42a005d..dfe5d46 100644 --- a/imagekit/management/commands/warmimagecache.py +++ b/imagekit/management/commands/warmimagecache.py @@ -19,17 +19,16 @@ class Command(BaseCommand): for generator_id in generators: self.stdout.write('Validating generator: %s\n' % generator_id) - for cacheables in cacheable_registry.get(generator_id): - for cacheable in cacheables.files(): - if cacheable: - generator = generator_registry.get(generator_id, cacheable=cacheable) # TODO: HINTS! (Probably based on cacheable, so this will need to be moved into loop below.) - self.stdout.write(' %s\n' % cacheable) - try: - # TODO: Allow other validation actions through command option - GeneratedImageCacheFile(generator).validate() - except Exception, err: - # TODO: How should we handle failures? Don't want to error, but should call it out more than this. - self.stdout.write(' FAILED: %s\n' % err) + for kwargs in cacheable_registry.get(generator_id): + if kwargs: + generator = generator_registry.get(generator_id, **kwargs) # TODO: HINTS! (Probably based on cacheable, so this will need to be moved into loop below.) + self.stdout.write(' %s\n' % generator) + try: + # TODO: Allow other validation actions through command option + GeneratedImageCacheFile(generator).validate() + except Exception, err: + # TODO: How should we handle failures? Don't want to error, but should call it out more than this. + self.stdout.write(' FAILED: %s\n' % err) def compile_patterns(self, generator_ids): return [re.compile('%s$' % '.*'.join(re.escape(part) for part in id.split('*'))) for id in generator_ids] diff --git a/imagekit/registry.py b/imagekit/registry.py index 93e491f..258f252 100644 --- a/imagekit/registry.py +++ b/imagekit/registry.py @@ -70,25 +70,25 @@ class CacheableRegistry(object): Associates cacheables with a generator id """ - for cacheable in cacheables: - if cacheable not in self._cacheables: - self._cacheables[cacheable] = set() - self._cacheables[cacheable].add(generator_id) + if cacheables not in self._cacheables: + self._cacheables[cacheables] = set() + self._cacheables[cacheables].add(generator_id) def unregister(self, generator_id, cacheables): """ Disassociates cacheables with a generator id """ - for cacheable in cacheables: - try: - self._cacheables[cacheable].remove(generator_id) - except KeyError: - continue + try: + self._cacheables[cacheables].remove(generator_id) + except KeyError: + pass def get(self, generator_id): - return [cacheable for cacheable in self._cacheables - if generator_id in self._cacheables[cacheable]] + for k, v in self._cacheables.items(): + if generator_id in v: + for cacheable in k(): + yield cacheable def before_access_receiver(self, sender, generator, cacheable, **kwargs): generator.image_cache_strategy.invoke_callback('before_access', cacheable) @@ -130,8 +130,6 @@ class Register(object): # iterable that returns kwargs or callable that returns iterable of kwargs def cacheables(self, generator_id, cacheables): - if callable(cacheables): - cacheables = cacheables() cacheable_registry.register(generator_id, cacheables) @@ -144,8 +142,6 @@ class Unregister(object): generator_registry.unregister(id, generator) def cacheables(self, generator_id, cacheables): - if callable(cacheables): - cacheables = cacheables() cacheable_registry.unregister(generator_id, cacheables) diff --git a/imagekit/specs/sourcegroups.py b/imagekit/specs/sourcegroups.py index 6bd1eed..04c30c3 100644 --- a/imagekit/specs/sourcegroups.py +++ b/imagekit/specs/sourcegroups.py @@ -117,11 +117,8 @@ class ImageFieldSourceGroup(object): self.image_field = image_field signal_router.add(self) - def files(self): - for instance in self.model_class.objects.all(): - yield getattr(instance, self.image_field) - def __call__(self): - return self.files() + for instance in self.model_class.objects.all(): + yield {'source': getattr(instance, self.image_field)} signal_router = ModelSignalRouter() From a3e9a080d44b07c78dd00afb8b5a6a2ec9cb103c Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Mon, 28 Jan 2013 21:07:35 -0500 Subject: [PATCH 171/213] Revert signal names --- imagekit/imagecache/strategies.py | 6 +++--- imagekit/registry.py | 16 ++++++++-------- imagekit/signals.py | 6 +++--- imagekit/specs/sourcegroups.py | 8 ++++---- 4 files changed, 18 insertions(+), 18 deletions(-) diff --git a/imagekit/imagecache/strategies.py b/imagekit/imagecache/strategies.py index a0b8f76..56efbd7 100644 --- a/imagekit/imagecache/strategies.py +++ b/imagekit/imagecache/strategies.py @@ -20,13 +20,13 @@ class Optimistic(object): """ - def on_cacheable_created(self, file): + def on_source_created(self, file): validate_now(file) - def on_cacheable_deleted(self, file): + def on_source_deleted(self, file): clear_now(file) - def on_cacheable_changed(self, file): + def on_source_changed(self, file): validate_now(file) diff --git a/imagekit/registry.py b/imagekit/registry.py index 258f252..6f48f9c 100644 --- a/imagekit/registry.py +++ b/imagekit/registry.py @@ -1,6 +1,6 @@ from .exceptions import AlreadyRegistered, NotRegistered -from .signals import (before_access, cacheable_created, cacheable_changed, - cacheable_deleted) +from .signals import (before_access, source_created, source_changed, + source_deleted) class GeneratorRegistry(object): @@ -54,9 +54,9 @@ class CacheableRegistry(object): """ _signals = [ - cacheable_created, - cacheable_changed, - cacheable_deleted, + source_created, + source_changed, + source_deleted, ] def __init__(self): @@ -106,9 +106,9 @@ class CacheableRegistry(object): for generator in (generator_registry.get(id, cacheable=cacheable, **info) for id in self._cacheables[cacheable]): event_name = { - cacheable_created: 'cacheable_created', - cacheable_changed: 'cacheable_changed', - cacheable_deleted: 'cacheable_deleted', + source_created: 'source_created', + source_changed: 'source_changed', + source_deleted: 'source_deleted', } generator._handle_cacheable_event(event_name, cacheable) diff --git a/imagekit/signals.py b/imagekit/signals.py index 97a9d9c..4c7aefa 100644 --- a/imagekit/signals.py +++ b/imagekit/signals.py @@ -1,6 +1,6 @@ from django.dispatch import Signal before_access = Signal() -cacheable_created = Signal(providing_args=[]) -cacheable_changed = Signal() -cacheable_deleted = Signal() +source_created = Signal(providing_args=[]) +source_changed = Signal() +source_deleted = Signal() diff --git a/imagekit/specs/sourcegroups.py b/imagekit/specs/sourcegroups.py index 04c30c3..825276e 100644 --- a/imagekit/specs/sourcegroups.py +++ b/imagekit/specs/sourcegroups.py @@ -1,6 +1,6 @@ from django.db.models.signals import post_init, post_save, post_delete from django.utils.functional import wraps -from ..signals import cacheable_created, cacheable_changed, cacheable_deleted +from ..signals import source_created, source_changed, source_deleted def ik_model_receiver(fn): @@ -66,16 +66,16 @@ class ModelSignalRouter(object): new_hashes = self.update_source_hashes(instance) for attname, file in self.get_field_dict(instance).items(): if created: - self.dispatch_signal(cacheable_created, file, sender, + self.dispatch_signal(source_created, file, sender, instance, attname) elif old_hashes[attname] != new_hashes[attname]: - self.dispatch_signal(cacheable_changed, file, sender, + self.dispatch_signal(source_changed, file, sender, instance, attname) @ik_model_receiver def post_delete_receiver(self, sender, instance=None, **kwargs): for attname, file in self.get_field_dict(instance).items(): - self.dispatch_signal(cacheable_deleted, file, sender, instance, + self.dispatch_signal(source_deleted, file, sender, instance, attname) @ik_model_receiver From 5b4456431814b9a892b97547ea3c3c29e17b41fb Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Mon, 28 Jan 2013 21:31:38 -0500 Subject: [PATCH 172/213] Add LazyGeneratedImageCacheFile --- imagekit/files.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/imagekit/files.py b/imagekit/files.py index 1dd8976..c025af3 100644 --- a/imagekit/files.py +++ b/imagekit/files.py @@ -2,7 +2,9 @@ from django.conf import settings from django.core.files.base import ContentFile, File from django.core.files.images import ImageFile from django.utils.encoding import smart_str, smart_unicode +from django.utils.functional import LazyObject import os +from .registry import generator_registry from .signals import before_access from .utils import (format_to_mimetype, extension_to_mimetype, get_logger, get_singleton, generate) @@ -158,3 +160,14 @@ class IKContentFile(ContentFile): def __unicode__(self): return smart_unicode(self.file.name or u'') + + +class LazyGeneratedImageCacheFile(LazyObject): + def __init__(self, generator_id, *args, **kwargs): + super(LazyGeneratedImageCacheFile, self).__init__() + + def setup(): + generator = generator_registry.get(generator_id, *args, **kwargs) + self._wrapped = GeneratedImageCacheFile(generator) + + self.__dict__['_setup'] = setup From 3931b552a0924fa34b3333c4e6605d81c9f83204 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Tue, 29 Jan 2013 01:40:00 -0500 Subject: [PATCH 173/213] Separate source groups and cacheables. This allows a sensible specialized interface for source groups, but also for ImageKit to interact with specs using the generalized image generator interface. --- imagekit/files.py | 2 +- imagekit/imagecache/strategies.py | 5 -- imagekit/models/fields/__init__.py | 4 +- imagekit/registry.py | 104 ++++++++++++++++++++--------- imagekit/signals.py | 6 +- imagekit/specs/__init__.py | 6 -- imagekit/specs/sourcegroups.py | 102 +++++++++++++++++++--------- imagekit/utils.py | 7 ++ 8 files changed, 157 insertions(+), 79 deletions(-) diff --git a/imagekit/files.py b/imagekit/files.py index c025af3..150cfa0 100644 --- a/imagekit/files.py +++ b/imagekit/files.py @@ -103,7 +103,7 @@ class GeneratedImageCacheFile(BaseIKFile, ImageFile): super(GeneratedImageCacheFile, self).__init__(storage=storage) def _require_file(self): - before_access.send(sender=self, generator=self.generator, file=self) + before_access.send(sender=self, file=self) return super(GeneratedImageCacheFile, self)._require_file() def clear(self): diff --git a/imagekit/imagecache/strategies.py b/imagekit/imagecache/strategies.py index 56efbd7..4a3d618 100644 --- a/imagekit/imagecache/strategies.py +++ b/imagekit/imagecache/strategies.py @@ -46,11 +46,6 @@ class StrategyWrapper(object): strategy = strategy() self._wrapped = strategy - def invoke_callback(self, name, *args, **kwargs): - func = getattr(self._wrapped, name, None) - if func: - func(*args, **kwargs) - def __unicode__(self): return unicode(self._wrapped) diff --git a/imagekit/models/fields/__init__.py b/imagekit/models/fields/__init__.py index 449faac..2aea3ca 100644 --- a/imagekit/models/fields/__init__.py +++ b/imagekit/models/fields/__init__.py @@ -45,8 +45,8 @@ class ImageSpecField(SpecHostField): self.set_spec_id(cls, name) # Add the model and field as a source for this spec id - register.cacheables(self.spec_id, - ImageFieldSourceGroup(cls, self.source)) + register.source_group(self.spec_id, + ImageFieldSourceGroup(cls, self.source)) class ProcessedImageField(models.ImageField, SpecHostField): diff --git a/imagekit/registry.py b/imagekit/registry.py index 6f48f9c..d74f6b1 100644 --- a/imagekit/registry.py +++ b/imagekit/registry.py @@ -1,6 +1,6 @@ from .exceptions import AlreadyRegistered, NotRegistered -from .signals import (before_access, source_created, source_changed, - source_deleted) +from .signals import before_access, source_created, source_changed, source_deleted +from .utils import call_strategy_method class GeneratorRegistry(object): @@ -12,6 +12,7 @@ class GeneratorRegistry(object): """ def __init__(self): self._generators = {} + before_access.connect(self.before_access_receiver) def register(self, id, generator): if id in self._generators: @@ -41,6 +42,67 @@ class GeneratorRegistry(object): def get_ids(self): return self._generators.keys() + def before_access_receiver(self, sender, file, **kwargs): + generator = file.generator + if generator in self._generators.values(): + # Only invoke the strategy method for registered generators. + call_strategy_method(generator, 'before_access', file=file) + + +class SourceGroupRegistry(object): + """ + The source group registry is responsible for listening to source_* signals + on source groups, and relaying them to the image cache strategies of the + appropriate generators. + + In addition, registering a new source group also registers its cacheables + generator with the cacheable registry. + + """ + _signals = { + source_created: 'on_source_created', + source_changed: 'on_source_changed', + source_deleted: 'on_source_deleted', + } + + def __init__(self): + self._source_groups = {} + for signal in self._signals.keys(): + signal.connect(self.source_group_receiver) + + def register(self, generator_id, source_group): + from .specs.sourcegroups import SourceGroupCacheablesGenerator + generator_ids = self._source_groups.setdefault(source_group, set()) + generator_ids.add(generator_id) + cacheable_registry.register(generator_id, + SourceGroupCacheablesGenerator(source_group, generator_id)) + + def unregister(self, generator_id, source_group): + from .specs.sourcegroups import SourceGroupCacheablesGenerator + generator_ids = self._source_groups.setdefault(source_group, set()) + if generator_id in generator_ids: + generator_ids.remove(generator_id) + cacheable_registry.unregister(generator_id, + SourceGroupCacheablesGenerator(source_group, generator_id)) + + def source_group_receiver(self, sender, source, signal, **kwargs): + """ + Relay source group signals to the appropriate spec strategy. + + """ + source_group = sender + + # Ignore signals from unregistered groups. + if source_group not in self._source_groups: + return + + specs = [generator_registry.get(id, source=source) for id in + self._source_groups[source_group]] + callback_name = self._signals[signal] + + for spec in specs: + call_strategy_method(spec, callback_name, file=source) + class CacheableRegistry(object): """ @@ -53,17 +115,8 @@ class CacheableRegistry(object): """ - _signals = [ - source_created, - source_changed, - source_deleted, - ] - def __init__(self): self._cacheables = {} - for signal in self._signals: - signal.connect(self.cacheable_receiver) - before_access.connect(self.before_access_receiver) def register(self, generator_id, cacheables): """ @@ -90,28 +143,6 @@ class CacheableRegistry(object): for cacheable in k(): yield cacheable - def before_access_receiver(self, sender, generator, cacheable, **kwargs): - generator.image_cache_strategy.invoke_callback('before_access', cacheable) - - def cacheable_receiver(self, sender, cacheable, signal, info, **kwargs): - """ - Redirects signals dispatched on cacheables - to the appropriate generators. - - """ - cacheable = sender - if cacheable not in self._cacheables: - return - - for generator in (generator_registry.get(id, cacheable=cacheable, **info) - for id in self._cacheables[cacheable]): - event_name = { - source_created: 'source_created', - source_changed: 'source_changed', - source_deleted: 'source_deleted', - } - generator._handle_cacheable_event(event_name, cacheable) - class Register(object): """ @@ -132,6 +163,9 @@ class Register(object): def cacheables(self, generator_id, cacheables): cacheable_registry.register(generator_id, cacheables) + def source_group(self, generator_id, source_group): + source_group_registry.register(generator_id, source_group) + class Unregister(object): """ @@ -144,8 +178,12 @@ class Unregister(object): def cacheables(self, generator_id, cacheables): cacheable_registry.unregister(generator_id, cacheables) + def source_group(self, generator_id, source_group): + source_group_registry.unregister(generator_id, source_group) + generator_registry = GeneratorRegistry() cacheable_registry = CacheableRegistry() +source_group_registry = SourceGroupRegistry() register = Register() unregister = Unregister() diff --git a/imagekit/signals.py b/imagekit/signals.py index 4c7aefa..c01e30e 100644 --- a/imagekit/signals.py +++ b/imagekit/signals.py @@ -1,6 +1,10 @@ from django.dispatch import Signal + +# "Cacheables" (cache file) signals before_access = Signal() -source_created = Signal(providing_args=[]) + +# Source group signals +source_created = Signal() source_changed = Signal() source_deleted = Signal() diff --git a/imagekit/specs/__init__.py b/imagekit/specs/__init__.py index 01c7131..7b92845 100644 --- a/imagekit/specs/__init__.py +++ b/imagekit/specs/__init__.py @@ -3,7 +3,6 @@ from django.db.models.fields.files import ImageFieldFile from hashlib import md5 import os import pickle -from ..files import GeneratedImageCacheFile from ..imagecache.backends import get_default_image_cache_backend from ..imagecache.strategies import StrategyWrapper from ..processors import ProcessorPipeline @@ -43,11 +42,6 @@ class BaseImageSpec(object): def generate(self): raise NotImplementedError - # TODO: I don't like this interface. Is there a standard Python one? pubsub? - def _handle_source_event(self, event_name, source): - file = GeneratedImageCacheFile(self) - self.image_cache_strategy.invoke_callback('on_%s' % event_name, file) - class ImageSpec(BaseImageSpec): """ diff --git a/imagekit/specs/sourcegroups.py b/imagekit/specs/sourcegroups.py index 825276e..3c23d64 100644 --- a/imagekit/specs/sourcegroups.py +++ b/imagekit/specs/sourcegroups.py @@ -1,12 +1,26 @@ +""" +Source groups are the means by which image spec sources are identified. They +have two responsibilities: + +1. To dispatch ``source_created``, ``source_changed``, and ``source_deleted`` + signals. (These will be relayed to the corresponding specs' image cache + strategies.) +2. To provide the source files that they represent, via a generator method named + ``files()``. (This is used by the warmimagecache management command for + "pre-caching" image files.) + +""" + from django.db.models.signals import post_init, post_save, post_delete from django.utils.functional import wraps +from ..files import LazyGeneratedImageCacheFile from ..signals import source_created, source_changed, source_deleted def ik_model_receiver(fn): """ A method decorator that filters out signals coming from models that don't - have fields that function as ImageFieldSourceGroup + have fields that function as ImageFieldSourceGroup sources. """ @wraps(fn) @@ -18,8 +32,15 @@ def ik_model_receiver(fn): class ModelSignalRouter(object): """ - Handles signals dispatched by models and relays them to the spec source - groups that represent those models. + Normally, ``ImageFieldSourceGroup`` would be directly responsible for + watching for changes on the model field it represents. However, Django does + not dispatch events for abstract base classes. Therefore, we must listen for + the signals on all models and filter out those that aren't represented by + ``ImageFieldSourceGroup``s. This class encapsulates that functionality. + + Related: + https://github.com/jdriscoll/django-imagekit/issues/126 + https://code.djangoproject.com/ticket/9318 """ @@ -58,25 +79,23 @@ class ModelSignalRouter(object): src in self._source_groups if src.model_class is instance.__class__) @ik_model_receiver - def post_save_receiver(self, sender, instance=None, created=False, - raw=False, **kwargs): + def post_save_receiver(self, sender, instance=None, created=False, raw=False, **kwargs): if not raw: self.init_instance(instance) old_hashes = instance._ik.get('source_hashes', {}).copy() new_hashes = self.update_source_hashes(instance) for attname, file in self.get_field_dict(instance).items(): if created: - self.dispatch_signal(source_created, file, sender, - instance, attname) + self.dispatch_signal(source_created, file, sender, instance, + attname) elif old_hashes[attname] != new_hashes[attname]: - self.dispatch_signal(source_changed, file, sender, - instance, attname) + self.dispatch_signal(source_changed, file, sender, instance, + attname) @ik_model_receiver def post_delete_receiver(self, sender, instance=None, **kwargs): for attname, file in self.get_field_dict(instance).items(): - self.dispatch_signal(source_deleted, file, sender, instance, - attname) + self.dispatch_signal(source_deleted, file, sender, instance, attname) @ik_model_receiver def post_init_receiver(self, sender, instance=None, **kwargs): @@ -91,34 +110,55 @@ class ModelSignalRouter(object): """ for source_group in self._source_groups: if source_group.model_class is model_class and source_group.image_field == attname: - info = dict( - source_group=source_group, - instance=instance, - field_name=attname, - ) - signal.send(sender=source_group, source=file, info=info) + signal.send(sender=source_group, source=file) class ImageFieldSourceGroup(object): + """ + A source group that repesents a particular field across all instances of a + model. + + """ def __init__(self, model_class, image_field): - """ - Good design would dictate that this instance would be responsible for - watching for changes for the provided field. However, due to a bug in - Django, we can't do that without leaving abstract base models (which - don't trigger signals) in the lurch. So instead, we do all signal - handling through the signal router. - - Related: - https://github.com/jdriscoll/django-imagekit/issues/126 - https://code.djangoproject.com/ticket/9318 - - """ self.model_class = model_class self.image_field = image_field signal_router.add(self) - def __call__(self): + def files(self): + """ + A generator that returns the source files that this source group + represents; in this case, a particular field of every instance of a + particular model. + + """ for instance in self.model_class.objects.all(): - yield {'source': getattr(instance, self.image_field)} + yield getattr(instance, self.image_field) + + +class SourceGroupCacheablesGenerator(object): + """ + A cacheables generator for source groups. The purpose of this class is to + generate cacheables (cache files) from a source group. + + """ + def __init__(self, source_group, generator_id): + self.source_group = source_group + self.generator_id = generator_id + + def __eq__(self, other): + return (isinstance(other, self.__class__) + and self.__dict__ == other.__dict__) + + def __ne__(self, other): + return not self.__eq__(other) + + def __hash__(self): + return hash((self.source_group, self.generator_id)) + + def __call__(self): + for source_file in self.source_group.files(): + yield LazyGeneratedImageCacheFile(self.generator_id, + source=source_file) + signal_router = ModelSignalRouter() diff --git a/imagekit/utils.py b/imagekit/utils.py index 82a2c02..adeb377 100644 --- a/imagekit/utils.py +++ b/imagekit/utils.py @@ -422,3 +422,10 @@ def generate(generator): content = f return File(content) + + +def call_strategy_method(generator, method_name, *args, **kwargs): + strategy = getattr(generator, 'image_cache_strategy', None) + fn = getattr(strategy, method_name, None) + if fn is not None: + fn(*args, **kwargs) From ca4f090e63f7415905db8a71f8ae39ff00bb52a1 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Tue, 29 Jan 2013 01:48:06 -0500 Subject: [PATCH 174/213] Fix source callbacks on strategies --- imagekit/imagecache/strategies.py | 3 ++- imagekit/registry.py | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/imagekit/imagecache/strategies.py b/imagekit/imagecache/strategies.py index 4a3d618..cb7edd4 100644 --- a/imagekit/imagecache/strategies.py +++ b/imagekit/imagecache/strategies.py @@ -1,3 +1,4 @@ +from django.utils.functional import LazyObject from .actions import validate_now, clear_now from ..utils import get_singleton @@ -36,7 +37,7 @@ class DictStrategy(object): setattr(self, k, v) -class StrategyWrapper(object): +class StrategyWrapper(LazyObject): def __init__(self, strategy): if isinstance(strategy, basestring): strategy = get_singleton(strategy, 'image cache strategy') diff --git a/imagekit/registry.py b/imagekit/registry.py index d74f6b1..764ba24 100644 --- a/imagekit/registry.py +++ b/imagekit/registry.py @@ -90,6 +90,7 @@ class SourceGroupRegistry(object): Relay source group signals to the appropriate spec strategy. """ + from .files import GeneratedImageCacheFile source_group = sender # Ignore signals from unregistered groups. @@ -101,7 +102,8 @@ class SourceGroupRegistry(object): callback_name = self._signals[signal] for spec in specs: - call_strategy_method(spec, callback_name, file=source) + file = GeneratedImageCacheFile(spec) + call_strategy_method(spec, callback_name, file=file) class CacheableRegistry(object): From e48817a5ecbe72bb2f6e3b6c6d3d5dd44c9ee2d6 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Tue, 29 Jan 2013 01:53:23 -0500 Subject: [PATCH 175/213] Update warmimagecache to use new cacheable registry --- .../management/commands/warmimagecache.py | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/imagekit/management/commands/warmimagecache.py b/imagekit/management/commands/warmimagecache.py index dfe5d46..b07c7ec 100644 --- a/imagekit/management/commands/warmimagecache.py +++ b/imagekit/management/commands/warmimagecache.py @@ -1,6 +1,5 @@ from django.core.management.base import BaseCommand import re -from ...files import GeneratedImageCacheFile from ...registry import generator_registry, cacheable_registry @@ -19,16 +18,14 @@ class Command(BaseCommand): for generator_id in generators: self.stdout.write('Validating generator: %s\n' % generator_id) - for kwargs in cacheable_registry.get(generator_id): - if kwargs: - generator = generator_registry.get(generator_id, **kwargs) # TODO: HINTS! (Probably based on cacheable, so this will need to be moved into loop below.) - self.stdout.write(' %s\n' % generator) - try: - # TODO: Allow other validation actions through command option - GeneratedImageCacheFile(generator).validate() - except Exception, err: - # TODO: How should we handle failures? Don't want to error, but should call it out more than this. - self.stdout.write(' FAILED: %s\n' % err) + for cacheable in cacheable_registry.get(generator_id): + self.stdout.write(' %s\n' % cacheable) + try: + # TODO: Allow other validation actions through command option + cacheable.validate() + except Exception, err: + # TODO: How should we handle failures? Don't want to error, but should call it out more than this. + self.stdout.write(' FAILED: %s\n' % err) def compile_patterns(self, generator_ids): return [re.compile('%s$' % '.*'.join(re.escape(part) for part in id.split('*'))) for id in generator_ids] From e0ffb246ae8f253b511e31803d2dcd566a4b9bbb Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Tue, 29 Jan 2013 02:17:52 -0500 Subject: [PATCH 176/213] Always use colon as segment separator --- imagekit/models/fields/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/imagekit/models/fields/__init__.py b/imagekit/models/fields/__init__.py index 2aea3ca..35c78da 100644 --- a/imagekit/models/fields/__init__.py +++ b/imagekit/models/fields/__init__.py @@ -11,7 +11,7 @@ class SpecHostField(SpecHost): # Generate a spec_id to register the spec with. The default spec id is # ":_" if not getattr(self, 'spec_id', None): - spec_id = (u'%s:%s_%s' % (cls._meta.app_label, + spec_id = (u'%s:%s:%s' % (cls._meta.app_label, cls._meta.object_name, name)).lower() # Register the spec with the id. This allows specs to be overridden From 54ca5da15d177291585f395ad08c89343e01931c Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Tue, 29 Jan 2013 02:18:21 -0500 Subject: [PATCH 177/213] Improve generator id pattern matching This behavior allows users to easy generate images by app, model, or field. --- .../management/commands/warmimagecache.py | 23 +++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/imagekit/management/commands/warmimagecache.py b/imagekit/management/commands/warmimagecache.py index b07c7ec..9f2a8c8 100644 --- a/imagekit/management/commands/warmimagecache.py +++ b/imagekit/management/commands/warmimagecache.py @@ -4,9 +4,12 @@ from ...registry import generator_registry, cacheable_registry class Command(BaseCommand): - help = ('Warm the image cache for the specified generators' - ' (or all generators if none was provided).' - ' Simple wildcard matching (using asterisks) is supported.') + help = ("""Warm the image cache for the specified generators (or all generators if +none was provided). Simple, fnmatch-like wildcards are allowed, with * +matching all characters within a segment, and ** matching across segments. +(Segments are separated with colons.) So, for example, "a:*:c" will match +"a:b:c", but not "a:b:x:c", whereas "a:**:c" will match both. Subsegments +are always matched, so "a" will match "a" as well as "a:b" and "a:b:c".""") args = '[generator_ids]' def handle(self, *args, **options): @@ -28,4 +31,16 @@ class Command(BaseCommand): self.stdout.write(' FAILED: %s\n' % err) def compile_patterns(self, generator_ids): - return [re.compile('%s$' % '.*'.join(re.escape(part) for part in id.split('*'))) for id in generator_ids] + return [self.compile_pattern(id) for id in generator_ids] + + def compile_pattern(self, generator_id): + parts = re.split(r'(\*{1,2})', generator_id) + pattern = '' + for part in parts: + if part == '*': + pattern += '[^:]*' + elif part == '**': + pattern += '.*' + else: + pattern += re.escape(part) + return re.compile('^%s(:.*)?$' % pattern) From f0dbe32f7a9fb1781eba214c4eb92b9b9ff6f8f7 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Tue, 29 Jan 2013 02:27:03 -0500 Subject: [PATCH 178/213] Fix pickling error --- imagekit/imagecache/strategies.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/imagekit/imagecache/strategies.py b/imagekit/imagecache/strategies.py index cb7edd4..28068f0 100644 --- a/imagekit/imagecache/strategies.py +++ b/imagekit/imagecache/strategies.py @@ -47,6 +47,12 @@ class StrategyWrapper(LazyObject): strategy = strategy() self._wrapped = strategy + def __getstate__(self): + return {'_wrapped': self._wrapped} + + def __setstate__(self, state): + self._wrapped = state['_wrapped'] + def __unicode__(self): return unicode(self._wrapped) From 04aa72c1f9f8f7e84a0e5a6ef15fb7f74d81ecef Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Thu, 31 Jan 2013 03:51:29 -0500 Subject: [PATCH 179/213] Rename cache things (it isn't cachine) https://twitter.com/alex_gaynor/statuses/257558176965206016 --- imagekit/conf.py | 6 +- imagekit/files.py | 53 ++++++------ .../{imagecache => generators}/__init__.py | 0 .../{imagecache => generators}/actions.py | 10 +-- imagekit/generators/filebackends.py | 64 +++++++++++++++ .../{imagecache => generators}/strategies.py | 19 ++--- imagekit/imagecache/backends.py | 82 ------------------- .../{warmimagecache.py => generateimages.py} | 21 ++--- imagekit/models/fields/__init__.py | 10 +-- imagekit/models/fields/utils.py | 4 +- imagekit/registry.py | 72 ++++++++-------- imagekit/signals.py | 2 +- imagekit/specs/__init__.py | 32 ++++---- imagekit/specs/sourcegroups.py | 13 ++- imagekit/templatetags/imagekit.py | 14 ++-- imagekit/utils.py | 2 +- 16 files changed, 189 insertions(+), 215 deletions(-) rename imagekit/{imagecache => generators}/__init__.py (100%) rename imagekit/{imagecache => generators}/actions.py (60%) create mode 100644 imagekit/generators/filebackends.py rename imagekit/{imagecache => generators}/strategies.py (68%) delete mode 100644 imagekit/imagecache/backends.py rename imagekit/management/commands/{warmimagecache.py => generateimages.py} (68%) diff --git a/imagekit/conf.py b/imagekit/conf.py index 288d366..7eb3afc 100644 --- a/imagekit/conf.py +++ b/imagekit/conf.py @@ -3,11 +3,11 @@ from django.conf import settings class ImageKitConf(AppConf): - DEFAULT_IMAGE_CACHE_BACKEND = 'imagekit.imagecache.backends.Simple' + DEFAULT_GENERATEDFILE_BACKEND = 'imagekit.generators.filebackends.Simple' CACHE_BACKEND = None - CACHE_DIR = 'CACHE/images' + GENERATED_FILE_DIR = 'generated/images' CACHE_PREFIX = 'imagekit:' - DEFAULT_IMAGE_CACHE_STRATEGY = 'imagekit.imagecache.strategies.JustInTime' + DEFAULT_IMAGE_GENERATOR_STRATEGY = 'imagekit.generators.strategies.JustInTime' DEFAULT_FILE_STORAGE = 'django.core.files.storage.FileSystemStorage' def configure_cache_backend(self, value): diff --git a/imagekit/files.py b/imagekit/files.py index 150cfa0..9b07659 100644 --- a/imagekit/files.py +++ b/imagekit/files.py @@ -72,48 +72,41 @@ class BaseIKFile(File): file.close() -class GeneratedImageCacheFile(BaseIKFile, ImageFile): +class GeneratedImageFile(BaseIKFile, ImageFile): """ - A cache file that represents the result of a generator. Creating an instance - of this class is not enough to trigger the creation of the cache file. In - fact, one of the main points of this class is to allow the creation of the - file to be deferred until the time that the image cache strategy requires - it. + A file that represents the result of a generator. Creating an instance of + this class is not enough to trigger the generation of the file. In fact, + one of the main points of this class is to allow the creation of the file + to be deferred until the time that the image generator strategy requires it. """ - def __init__(self, generator, name=None, storage=None, image_cache_backend=None): + def __init__(self, generator, name=None, storage=None, generatedfile_backend=None): """ :param generator: The object responsible for generating a new image. :param name: The filename :param storage: A Django storage object that will be used to save the file. - :param image_cache_backend: The object responsible for managing the - state of the cache file. + :param generatedfile_backend: The object responsible for managing the + state of the file. """ self.generator = generator - self.name = name or getattr(generator, 'cache_file_name', None) - storage = storage or getattr(generator, 'cache_file_storage', + self.name = name or getattr(generator, 'generatedfile_name', None) + storage = storage or getattr(generator, 'generatedfile_storage', None) or get_singleton(settings.IMAGEKIT_DEFAULT_FILE_STORAGE, 'file storage backend') - self.image_cache_backend = image_cache_backend or getattr(generator, - 'image_cache_backend', None) + self.generatedfile_backend = generatedfile_backend or getattr(generator, + 'generatedfile_backend', None) - super(GeneratedImageCacheFile, self).__init__(storage=storage) + super(GeneratedImageFile, self).__init__(storage=storage) def _require_file(self): before_access.send(sender=self, file=self) - return super(GeneratedImageCacheFile, self)._require_file() + return super(GeneratedImageFile, self)._require_file() - def clear(self): - return self.image_cache_backend.clear(self) - - def invalidate(self): - return self.image_cache_backend.invalidate(self) - - def validate(self): - return self.image_cache_backend.validate(self) + def ensure_exists(self): + return self.generatedfile_backend.ensure_exists(self) def generate(self): # Generate the file @@ -126,11 +119,11 @@ class GeneratedImageCacheFile(BaseIKFile, ImageFile): ' with the requested name ("%s") and instead used' ' "%s". This may be because a file already existed with' ' the requested name. If so, you may have meant to call' - ' validate() instead of generate(), or there may be a' - ' race condition in the image cache backend %s. The' - ' saved file will not be used.' % (self.storage, + ' ensure_exists() instead of generate(), or there may be a' + ' race condition in the file backend %s. The saved file' + ' will not be used.' % (self.storage, self.name, actual_name, - self.image_cache_backend)) + self.generatedfile_backend)) class IKContentFile(ContentFile): @@ -162,12 +155,12 @@ class IKContentFile(ContentFile): return smart_unicode(self.file.name or u'') -class LazyGeneratedImageCacheFile(LazyObject): +class LazyGeneratedImageFile(LazyObject): def __init__(self, generator_id, *args, **kwargs): - super(LazyGeneratedImageCacheFile, self).__init__() + super(LazyGeneratedImageFile, self).__init__() def setup(): generator = generator_registry.get(generator_id, *args, **kwargs) - self._wrapped = GeneratedImageCacheFile(generator) + self._wrapped = GeneratedImageFile(generator) self.__dict__['_setup'] = setup diff --git a/imagekit/imagecache/__init__.py b/imagekit/generators/__init__.py similarity index 100% rename from imagekit/imagecache/__init__.py rename to imagekit/generators/__init__.py diff --git a/imagekit/imagecache/actions.py b/imagekit/generators/actions.py similarity index 60% rename from imagekit/imagecache/actions.py rename to imagekit/generators/actions.py index b829a5f..dc56576 100644 --- a/imagekit/imagecache/actions.py +++ b/imagekit/generators/actions.py @@ -1,5 +1,5 @@ -def validate_now(file): - file.validate() +def ensure_exists(file): + file.ensure_exists() try: @@ -7,15 +7,15 @@ try: except ImportError: pass else: - validate_now_task = task(validate_now) + ensure_exists_task = task(ensure_exists) -def deferred_validate(file): +def ensure_exists_deferred(file): try: import celery # NOQA except: raise ImportError("Deferred validation requires the the 'celery' library") - validate_now_task.delay(file) + ensure_exists_task.delay(file) def clear_now(file): diff --git a/imagekit/generators/filebackends.py b/imagekit/generators/filebackends.py new file mode 100644 index 0000000..b80aa8e --- /dev/null +++ b/imagekit/generators/filebackends.py @@ -0,0 +1,64 @@ +from ..utils import get_singleton +from django.core.cache import get_cache +from django.core.exceptions import ImproperlyConfigured + + +def get_default_generatedfile_backend(): + """ + Get the default file backend. + + """ + from django.conf import settings + return get_singleton(settings.IMAGEKIT_DEFAULT_GENERATEDFILE_BACKEND, + 'file backend') + + +class InvalidFileBackendError(ImproperlyConfigured): + pass + + +class CachedFileBackend(object): + @property + def cache(self): + if not getattr(self, '_cache', None): + from django.conf import settings + self._cache = get_cache(settings.IMAGEKIT_CACHE_BACKEND) + return self._cache + + def get_key(self, file): + from django.conf import settings + return '%s%s-exists' % (settings.IMAGEKIT_CACHE_PREFIX, file.name) + + def file_exists(self, file): + key = self.get_key(file) + exists = self.cache.get(key) + if exists is None: + exists = self._file_exists(file) + self.cache.set(key, exists) + return exists + + def ensure_exists(self, file): + if self.file_exists(file): + self.create(file) + self.cache.set(self.get_key(file), True) + + +class Simple(CachedFileBackend): + """ + The most basic file backend. The storage is consulted to see if the file + exists. + + """ + + def _file_exists(self, file): + if not getattr(file, '_file', None): + # No file on object. Have to check storage. + return not file.storage.exists(file.name) + return False + + def create(self, file): + """ + Generates a new image by running the processors on the source file. + + """ + file.generate() diff --git a/imagekit/imagecache/strategies.py b/imagekit/generators/strategies.py similarity index 68% rename from imagekit/imagecache/strategies.py rename to imagekit/generators/strategies.py index 28068f0..7def2a6 100644 --- a/imagekit/imagecache/strategies.py +++ b/imagekit/generators/strategies.py @@ -1,34 +1,33 @@ from django.utils.functional import LazyObject -from .actions import validate_now, clear_now from ..utils import get_singleton class JustInTime(object): """ - A caching strategy that validates the file right before it's needed. + A strategy that ensures the file exists right before it's needed. """ def before_access(self, file): - validate_now(file) + file.ensure_exists() class Optimistic(object): """ - A caching strategy that acts immediately when the cacheable file changes - and assumes that the cache files will not be removed (i.e. doesn't - revalidate on access). + A strategy that acts immediately when the source file changes and assumes + that the generated files will not be removed (i.e. it doesn't ensure the + generated file exists when it's accessed). """ def on_source_created(self, file): - validate_now(file) + file.ensure_exists() def on_source_deleted(self, file): - clear_now(file) + file.delete() def on_source_changed(self, file): - validate_now(file) + file.ensure_exists() class DictStrategy(object): @@ -40,7 +39,7 @@ class DictStrategy(object): class StrategyWrapper(LazyObject): def __init__(self, strategy): if isinstance(strategy, basestring): - strategy = get_singleton(strategy, 'image cache strategy') + strategy = get_singleton(strategy, 'generator strategy') elif isinstance(strategy, dict): strategy = DictStrategy(strategy) elif callable(strategy): diff --git a/imagekit/imagecache/backends.py b/imagekit/imagecache/backends.py deleted file mode 100644 index c31002f..0000000 --- a/imagekit/imagecache/backends.py +++ /dev/null @@ -1,82 +0,0 @@ -from ..utils import get_singleton -from django.core.cache import get_cache -from django.core.exceptions import ImproperlyConfigured - - -def get_default_image_cache_backend(): - """ - Get the default image cache backend. - - """ - from django.conf import settings - return get_singleton(settings.IMAGEKIT_DEFAULT_IMAGE_CACHE_BACKEND, - 'image cache backend') - - -class InvalidImageCacheBackendError(ImproperlyConfigured): - pass - - -class CachedValidationBackend(object): - @property - def cache(self): - if not getattr(self, '_cache', None): - from django.conf import settings - self._cache = get_cache(settings.IMAGEKIT_CACHE_BACKEND) - return self._cache - - def get_key(self, file): - from django.conf import settings - return '%s%s-valid' % (settings.IMAGEKIT_CACHE_PREFIX, file.name) - - def is_invalid(self, file): - key = self.get_key(file) - cached_value = self.cache.get(key) - if cached_value is None: - cached_value = self._is_invalid(file) - self.cache.set(key, cached_value) - return cached_value - - def validate(self, file): - if self.is_invalid(file): - self._validate(file) - self.cache.set(self.get_key(file), True) - - def invalidate(self, file): - if not self.is_invalid(file): - self._invalidate(file) - self.cache.set(self.get_key(file), False) - - -class Simple(CachedValidationBackend): - """ - The most basic image cache backend. Files are considered valid if they - exist. To invalidate a file, it's deleted; to validate one, it's generated - immediately. - - """ - - def _is_invalid(self, file): - if not getattr(file, '_file', None): - # No file on object. Have to check storage. - return not file.storage.exists(file.name) - return False - - def _validate(self, file): - """ - Generates a new image by running the processors on the source file. - - """ - file.generate() - - def invalidate(self, file): - """ - Invalidate the file by deleting it. We override ``invalidate()`` - instead of ``_invalidate()`` because we don't really care to check - whether the file is invalid or not. - - """ - file.delete(save=False) - - def clear(self, file): - file.delete(save=False) diff --git a/imagekit/management/commands/warmimagecache.py b/imagekit/management/commands/generateimages.py similarity index 68% rename from imagekit/management/commands/warmimagecache.py rename to imagekit/management/commands/generateimages.py index 9f2a8c8..d25426e 100644 --- a/imagekit/management/commands/warmimagecache.py +++ b/imagekit/management/commands/generateimages.py @@ -1,15 +1,16 @@ from django.core.management.base import BaseCommand import re -from ...registry import generator_registry, cacheable_registry +from ...registry import generator_registry, generatedfile_registry class Command(BaseCommand): - help = ("""Warm the image cache for the specified generators (or all generators if -none was provided). Simple, fnmatch-like wildcards are allowed, with * -matching all characters within a segment, and ** matching across segments. -(Segments are separated with colons.) So, for example, "a:*:c" will match -"a:b:c", but not "a:b:x:c", whereas "a:**:c" will match both. Subsegments -are always matched, so "a" will match "a" as well as "a:b" and "a:b:c".""") + help = ("""Generate files for the specified image generators (or all of them if +none was provided). Simple, glob-like wildcards are allowed, with * +matching all characters within a segment, and ** matching across +segments. (Segments are separated with colons.) So, for example, +"a:*:c" will match "a:b:c", but not "a:b:x:c", whereas "a:**:c" will +match both. Subsegments are always matched, so "a" will match "a" as +well as "a:b" and "a:b:c".""") args = '[generator_ids]' def handle(self, *args, **options): @@ -21,11 +22,11 @@ are always matched, so "a" will match "a" as well as "a:b" and "a:b:c".""") for generator_id in generators: self.stdout.write('Validating generator: %s\n' % generator_id) - for cacheable in cacheable_registry.get(generator_id): - self.stdout.write(' %s\n' % cacheable) + for file in generatedfile_registry.get(generator_id): + self.stdout.write(' %s\n' % file) try: # TODO: Allow other validation actions through command option - cacheable.validate() + file.ensure_exists() except Exception, err: # TODO: How should we handle failures? Don't want to error, but should call it out more than this. self.stdout.write(' FAILED: %s\n' % err) diff --git a/imagekit/models/fields/__init__.py b/imagekit/models/fields/__init__.py index 35c78da..0cfcf5b 100644 --- a/imagekit/models/fields/__init__.py +++ b/imagekit/models/fields/__init__.py @@ -26,15 +26,15 @@ class ImageSpecField(SpecHostField): """ def __init__(self, processors=None, format=None, options=None, - source=None, cache_file_storage=None, autoconvert=None, - image_cache_backend=None, image_cache_strategy=None, spec=None, + source=None, generatedfile_storage=None, autoconvert=None, + generatedfile_backend=None, generator_strategy=None, spec=None, id=None): SpecHost.__init__(self, processors=processors, format=format, - options=options, cache_file_storage=cache_file_storage, + options=options, generatedfile_storage=generatedfile_storage, autoconvert=autoconvert, - image_cache_backend=image_cache_backend, - image_cache_strategy=image_cache_strategy, spec=spec, + generatedfile_backend=generatedfile_backend, + generator_strategy=generator_strategy, spec=spec, spec_id=id) # TODO: Allow callable for source. See https://github.com/jdriscoll/django-imagekit/issues/158#issuecomment-10921664 diff --git a/imagekit/models/fields/utils.py b/imagekit/models/fields/utils.py index 2324a33..e549a6b 100644 --- a/imagekit/models/fields/utils.py +++ b/imagekit/models/fields/utils.py @@ -1,4 +1,4 @@ -from ...files import GeneratedImageCacheFile +from ...files import GeneratedImageFile from django.db.models.fields.files import ImageField @@ -30,7 +30,7 @@ class ImageSpecFileDescriptor(object): else: source = image_fields[0] spec = self.field.get_spec(source=source) - file = GeneratedImageCacheFile(spec) + file = GeneratedImageFile(spec) instance.__dict__[self.attname] = file return file diff --git a/imagekit/registry.py b/imagekit/registry.py index 764ba24..e746f0c 100644 --- a/imagekit/registry.py +++ b/imagekit/registry.py @@ -52,11 +52,11 @@ class GeneratorRegistry(object): class SourceGroupRegistry(object): """ The source group registry is responsible for listening to source_* signals - on source groups, and relaying them to the image cache strategies of the + on source groups, and relaying them to the image generator strategies of the appropriate generators. - In addition, registering a new source group also registers its cacheables - generator with the cacheable registry. + In addition, registering a new source group also registers its generated + files with that registry. """ _signals = { @@ -71,26 +71,26 @@ class SourceGroupRegistry(object): signal.connect(self.source_group_receiver) def register(self, generator_id, source_group): - from .specs.sourcegroups import SourceGroupCacheablesGenerator + from .specs.sourcegroups import SourceGroupFilesGenerator generator_ids = self._source_groups.setdefault(source_group, set()) generator_ids.add(generator_id) - cacheable_registry.register(generator_id, - SourceGroupCacheablesGenerator(source_group, generator_id)) + generatedfile_registry.register(generator_id, + SourceGroupFilesGenerator(source_group, generator_id)) def unregister(self, generator_id, source_group): - from .specs.sourcegroups import SourceGroupCacheablesGenerator + from .specs.sourcegroups import SourceGroupFilesGenerator generator_ids = self._source_groups.setdefault(source_group, set()) if generator_id in generator_ids: generator_ids.remove(generator_id) - cacheable_registry.unregister(generator_id, - SourceGroupCacheablesGenerator(source_group, generator_id)) + generatedfile_registry.unregister(generator_id, + SourceGroupFilesGenerator(source_group, generator_id)) def source_group_receiver(self, sender, source, signal, **kwargs): """ Relay source group signals to the appropriate spec strategy. """ - from .files import GeneratedImageCacheFile + from .files import GeneratedImageFile source_group = sender # Ignore signals from unregistered groups. @@ -102,53 +102,53 @@ class SourceGroupRegistry(object): callback_name = self._signals[signal] for spec in specs: - file = GeneratedImageCacheFile(spec) + file = GeneratedImageFile(spec) call_strategy_method(spec, callback_name, file=file) -class CacheableRegistry(object): +class GeneratedFileRegistry(object): """ - An object for registering cacheables with generators. The two are + An object for registering generated files with image generators. The two are associated with each other via a string id. We do this (as opposed to - associating them directly by, for example, putting a ``cacheables`` - attribute on generators) so that generators can be overridden without - losing the associated cacheables. That way, a distributable app can define - its own generators without locking the users of the app into it. + associating them directly by, for example, putting a ``generatedfiles`` + attribute on image generators) so that image generators can be overridden + without losing the associated files. That way, a distributable app can + define its own generators without locking the users of the app into it. """ def __init__(self): - self._cacheables = {} + self._generatedfiles = {} - def register(self, generator_id, cacheables): + def register(self, generator_id, generatedfiles): """ - Associates cacheables with a generator id + Associates generated files with a generator id """ - if cacheables not in self._cacheables: - self._cacheables[cacheables] = set() - self._cacheables[cacheables].add(generator_id) + if generatedfiles not in self._generatedfiles: + self._generatedfiles[generatedfiles] = set() + self._generatedfiles[generatedfiles].add(generator_id) - def unregister(self, generator_id, cacheables): + def unregister(self, generator_id, generatedfiles): """ - Disassociates cacheables with a generator id + Disassociates generated files with a generator id """ try: - self._cacheables[cacheables].remove(generator_id) + self._generatedfiles[generatedfiles].remove(generator_id) except KeyError: pass def get(self, generator_id): - for k, v in self._cacheables.items(): + for k, v in self._generatedfiles.items(): if generator_id in v: - for cacheable in k(): - yield cacheable + for file in k(): + yield file class Register(object): """ - Register generators and cacheables. + Register generators and generated files. """ def generator(self, id, generator=None): @@ -162,8 +162,8 @@ class Register(object): generator_registry.register(id, generator) # iterable that returns kwargs or callable that returns iterable of kwargs - def cacheables(self, generator_id, cacheables): - cacheable_registry.register(generator_id, cacheables) + def generatedfiles(self, generator_id, generatedfiles): + generatedfile_registry.register(generator_id, generatedfiles) def source_group(self, generator_id, source_group): source_group_registry.register(generator_id, source_group) @@ -171,21 +171,21 @@ class Register(object): class Unregister(object): """ - Unregister generators and cacheables. + Unregister generators and generated files. """ def generator(self, id, generator): generator_registry.unregister(id, generator) - def cacheables(self, generator_id, cacheables): - cacheable_registry.unregister(generator_id, cacheables) + def generatedfiles(self, generator_id, generatedfiles): + generatedfile_registry.unregister(generator_id, generatedfiles) def source_group(self, generator_id, source_group): source_group_registry.unregister(generator_id, source_group) generator_registry = GeneratorRegistry() -cacheable_registry = CacheableRegistry() +generatedfile_registry = GeneratedFileRegistry() source_group_registry = SourceGroupRegistry() register = Register() unregister = Unregister() diff --git a/imagekit/signals.py b/imagekit/signals.py index c01e30e..36c915b 100644 --- a/imagekit/signals.py +++ b/imagekit/signals.py @@ -1,7 +1,7 @@ from django.dispatch import Signal -# "Cacheables" (cache file) signals +# Generated file signals before_access = Signal() # Source group signals diff --git a/imagekit/specs/__init__.py b/imagekit/specs/__init__.py index 7b92845..c156b52 100644 --- a/imagekit/specs/__init__.py +++ b/imagekit/specs/__init__.py @@ -3,8 +3,8 @@ from django.db.models.fields.files import ImageFieldFile from hashlib import md5 import os import pickle -from ..imagecache.backends import get_default_image_cache_backend -from ..imagecache.strategies import StrategyWrapper +from ..generators.filebackends import get_default_generatedfile_backend +from ..generators.strategies import StrategyWrapper from ..processors import ProcessorPipeline from ..utils import open_image, img_to_fobj, suggest_extension from ..registry import generator_registry, register @@ -17,27 +17,27 @@ class BaseImageSpec(object): """ - cache_file_storage = None - """A Django storage system to use to save a generated cache file.""" + generatedfile_storage = None + """A Django storage system to use to save a generated file.""" - image_cache_backend = None + generatedfile_backend = None """ - An object responsible for managing the state of cached files. Defaults to an - instance of ``IMAGEKIT_DEFAULT_IMAGE_CACHE_BACKEND`` + An object responsible for managing the state of generated files. Defaults to + an instance of ``IMAGEKIT_DEFAULT_GENERATEDFILE_BACKEND`` """ - image_cache_strategy = settings.IMAGEKIT_DEFAULT_IMAGE_CACHE_STRATEGY + generator_strategy = settings.IMAGEKIT_DEFAULT_IMAGE_GENERATOR_STRATEGY """ A dictionary containing callbacks that allow you to customize how and when - the image cache is validated. Defaults to - ``IMAGEKIT_DEFAULT_SPEC_FIELD_IMAGE_CACHE_STRATEGY``. + the image file is created. Defaults to + ``IMAGEKIT_DEFAULT_IMAGE_GENERATOR_STRATEGY``. """ def __init__(self): - self.image_cache_backend = self.image_cache_backend or get_default_image_cache_backend() - self.image_cache_strategy = StrategyWrapper(self.image_cache_strategy) + self.generatedfile_backend = self.generatedfile_backend or get_default_generatedfile_backend() + self.generator_strategy = StrategyWrapper(self.generator_strategy) def generate(self): raise NotImplementedError @@ -83,16 +83,16 @@ class ImageSpec(BaseImageSpec): super(ImageSpec, self).__init__() @property - def cache_file_name(self): + def generatedfile_name(self): source_filename = getattr(self.source, 'name', None) if source_filename is None or os.path.isabs(source_filename): - # Generally, we put the file right in the cache directory. - dir = settings.IMAGEKIT_CACHE_DIR + # Generally, we put the file right in the generated file directory. + dir = settings.IMAGEKIT_GENERATED_FILE_DIR else: # For source files with relative names (like Django media files), # use the source's name to create the new filename. - dir = os.path.join(settings.IMAGEKIT_CACHE_DIR, + dir = os.path.join(settings.IMAGEKIT_GENERATED_FILE_DIR, os.path.splitext(source_filename)[0]) ext = suggest_extension(source_filename or '', self.format) diff --git a/imagekit/specs/sourcegroups.py b/imagekit/specs/sourcegroups.py index 3c23d64..5e82144 100644 --- a/imagekit/specs/sourcegroups.py +++ b/imagekit/specs/sourcegroups.py @@ -3,17 +3,17 @@ Source groups are the means by which image spec sources are identified. They have two responsibilities: 1. To dispatch ``source_created``, ``source_changed``, and ``source_deleted`` - signals. (These will be relayed to the corresponding specs' image cache + signals. (These will be relayed to the corresponding specs' generator strategies.) 2. To provide the source files that they represent, via a generator method named - ``files()``. (This is used by the warmimagecache management command for + ``files()``. (This is used by the generateimages management command for "pre-caching" image files.) """ from django.db.models.signals import post_init, post_save, post_delete from django.utils.functional import wraps -from ..files import LazyGeneratedImageCacheFile +from ..files import LazyGeneratedImageFile from ..signals import source_created, source_changed, source_deleted @@ -135,10 +135,9 @@ class ImageFieldSourceGroup(object): yield getattr(instance, self.image_field) -class SourceGroupCacheablesGenerator(object): +class SourceGroupFilesGenerator(object): """ - A cacheables generator for source groups. The purpose of this class is to - generate cacheables (cache files) from a source group. + A Python generator that yields generated file objects for source groups. """ def __init__(self, source_group, generator_id): @@ -157,7 +156,7 @@ class SourceGroupCacheablesGenerator(object): def __call__(self): for source_file in self.source_group.files(): - yield LazyGeneratedImageCacheFile(self.generator_id, + yield LazyGeneratedImageFile(self.generator_id, source=source_file) diff --git a/imagekit/templatetags/imagekit.py b/imagekit/templatetags/imagekit.py index 9daacc2..555c693 100644 --- a/imagekit/templatetags/imagekit.py +++ b/imagekit/templatetags/imagekit.py @@ -2,7 +2,7 @@ from django import template from django.utils.html import escape from django.utils.safestring import mark_safe from .compat import parse_bits -from ..files import GeneratedImageCacheFile +from ..files import GeneratedImageFile from ..registry import generator_registry @@ -19,12 +19,12 @@ _kwarg_map = { } -def get_cache_file(context, generator_id, generator_kwargs, source=None): +def get_generatedfile(context, generator_id, generator_kwargs, source=None): generator_id = generator_id.resolve(context) kwargs = dict((_kwarg_map.get(k, k), v.resolve(context)) for k, v in generator_kwargs.items()) generator = generator_registry.get(generator_id, **kwargs) - return GeneratedImageCacheFile(generator) + return GeneratedImageFile(generator) def parse_dimensions(dimensions): @@ -53,7 +53,7 @@ class GenerateImageAssignmentNode(template.Node): autodiscover() variable_name = self.get_variable_name(context) - context[variable_name] = get_cache_file(context, self._generator_id, + context[variable_name] = get_generatedfile(context, self._generator_id, self._generator_kwargs) return '' @@ -69,7 +69,7 @@ class GenerateImageTagNode(template.Node): from ..utils import autodiscover autodiscover() - file = get_cache_file(context, self._generator_id, + file = get_generatedfile(context, self._generator_id, self._generator_kwargs) attrs = dict((k, v.resolve(context)) for k, v in self._html_attrs.items()) @@ -110,7 +110,7 @@ class ThumbnailAssignmentNode(template.Node): kwargs.update(parse_dimensions(self._dimensions.resolve(context))) generator = generator_registry.get(generator_id, **kwargs) - context[variable_name] = GeneratedImageCacheFile(generator) + context[variable_name] = GeneratedImageFile(generator) return '' @@ -136,7 +136,7 @@ class ThumbnailImageTagNode(template.Node): kwargs.update(dimensions) generator = generator_registry.get(generator_id, **kwargs) - file = GeneratedImageCacheFile(generator) + file = GeneratedImageFile(generator) attrs = dict((k, v.resolve(context)) for k, v in self._html_attrs.items()) diff --git a/imagekit/utils.py b/imagekit/utils.py index adeb377..59ef235 100644 --- a/imagekit/utils.py +++ b/imagekit/utils.py @@ -425,7 +425,7 @@ def generate(generator): def call_strategy_method(generator, method_name, *args, **kwargs): - strategy = getattr(generator, 'image_cache_strategy', None) + strategy = getattr(generator, 'generator_strategy', None) fn = getattr(strategy, method_name, None) if fn is not None: fn(*args, **kwargs) From 01fad6e4c6faa2cd8ad443d39b0b1da8f503d532 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Thu, 31 Jan 2013 04:07:57 -0500 Subject: [PATCH 180/213] Fix registration bug --- imagekit/registry.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/imagekit/registry.py b/imagekit/registry.py index e746f0c..3a998f0 100644 --- a/imagekit/registry.py +++ b/imagekit/registry.py @@ -44,7 +44,9 @@ class GeneratorRegistry(object): def before_access_receiver(self, sender, file, **kwargs): generator = file.generator - if generator in self._generators.values(): + + # FIXME: I guess this means you can't register functions? + if generator.__class__ in self._generators.values(): # Only invoke the strategy method for registered generators. call_strategy_method(generator, 'before_access', file=file) From 8e6abc1e6578d467ad0391ef89570c3801ac8252 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Thu, 31 Jan 2013 04:20:21 -0500 Subject: [PATCH 181/213] Remove ensure_exists `generate()` now plays double duty --- imagekit/files.py | 9 ++++++--- imagekit/generators/actions.py | 10 +++++----- imagekit/generators/filebackends.py | 2 +- imagekit/generators/strategies.py | 6 +++--- imagekit/management/commands/generateimages.py | 2 +- 5 files changed, 16 insertions(+), 13 deletions(-) diff --git a/imagekit/files.py b/imagekit/files.py index 9b07659..441d2cb 100644 --- a/imagekit/files.py +++ b/imagekit/files.py @@ -105,10 +105,13 @@ class GeneratedImageFile(BaseIKFile, ImageFile): before_access.send(sender=self, file=self) return super(GeneratedImageFile, self)._require_file() - def ensure_exists(self): - return self.generatedfile_backend.ensure_exists(self) + def generate(self, force=False): + if force: + self._generate() + else: + self.generatedfile_backend.ensure_exists(self) - def generate(self): + def _generate(self): # Generate the file content = generate(self.generator) diff --git a/imagekit/generators/actions.py b/imagekit/generators/actions.py index dc56576..634bcbe 100644 --- a/imagekit/generators/actions.py +++ b/imagekit/generators/actions.py @@ -1,5 +1,5 @@ -def ensure_exists(file): - file.ensure_exists() +def generate(file): + file.generate() try: @@ -7,15 +7,15 @@ try: except ImportError: pass else: - ensure_exists_task = task(ensure_exists) + generate_task = task(generate) -def ensure_exists_deferred(file): +def generate_deferred(file): try: import celery # NOQA except: raise ImportError("Deferred validation requires the the 'celery' library") - ensure_exists_task.delay(file) + generate_task.delay(file) def clear_now(file): diff --git a/imagekit/generators/filebackends.py b/imagekit/generators/filebackends.py index b80aa8e..ed375c1 100644 --- a/imagekit/generators/filebackends.py +++ b/imagekit/generators/filebackends.py @@ -61,4 +61,4 @@ class Simple(CachedFileBackend): Generates a new image by running the processors on the source file. """ - file.generate() + file.generate(force=True) diff --git a/imagekit/generators/strategies.py b/imagekit/generators/strategies.py index 7def2a6..b1c6f56 100644 --- a/imagekit/generators/strategies.py +++ b/imagekit/generators/strategies.py @@ -9,7 +9,7 @@ class JustInTime(object): """ def before_access(self, file): - file.ensure_exists() + file.generate() class Optimistic(object): @@ -21,13 +21,13 @@ class Optimistic(object): """ def on_source_created(self, file): - file.ensure_exists() + file.generate() def on_source_deleted(self, file): file.delete() def on_source_changed(self, file): - file.ensure_exists() + file.generate() class DictStrategy(object): diff --git a/imagekit/management/commands/generateimages.py b/imagekit/management/commands/generateimages.py index d25426e..569761f 100644 --- a/imagekit/management/commands/generateimages.py +++ b/imagekit/management/commands/generateimages.py @@ -26,7 +26,7 @@ well as "a:b" and "a:b:c".""") self.stdout.write(' %s\n' % file) try: # TODO: Allow other validation actions through command option - file.ensure_exists() + file.generate() except Exception, err: # TODO: How should we handle failures? Don't want to error, but should call it out more than this. self.stdout.write(' FAILED: %s\n' % err) From d6b73b8da7f0bcab1f5aa179583c73d833d41302 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Thu, 31 Jan 2013 10:03:42 -0500 Subject: [PATCH 182/213] Renaming/repackaging of generated file related classes --- imagekit/conf.py | 4 +- imagekit/files.py | 78 +------------------ imagekit/generatedfiles/__init__.py | 75 ++++++++++++++++++ .../{generators => generatedfiles}/actions.py | 0 .../backends.py} | 0 .../strategies.py | 2 +- imagekit/generators/__init__.py | 0 imagekit/models/fields/__init__.py | 4 +- imagekit/models/fields/utils.py | 2 +- imagekit/registry.py | 6 +- imagekit/specs/__init__.py | 10 +-- imagekit/specs/sourcegroups.py | 4 +- imagekit/templatetags/imagekit.py | 2 +- imagekit/utils.py | 2 +- 14 files changed, 95 insertions(+), 94 deletions(-) create mode 100644 imagekit/generatedfiles/__init__.py rename imagekit/{generators => generatedfiles}/actions.py (100%) rename imagekit/{generators/filebackends.py => generatedfiles/backends.py} (100%) rename imagekit/{generators => generatedfiles}/strategies.py (95%) delete mode 100644 imagekit/generators/__init__.py diff --git a/imagekit/conf.py b/imagekit/conf.py index 7eb3afc..4c39edc 100644 --- a/imagekit/conf.py +++ b/imagekit/conf.py @@ -3,11 +3,11 @@ from django.conf import settings class ImageKitConf(AppConf): - DEFAULT_GENERATEDFILE_BACKEND = 'imagekit.generators.filebackends.Simple' + DEFAULT_GENERATEDFILE_BACKEND = 'imagekit.generatedfiles.backends.Simple' CACHE_BACKEND = None GENERATED_FILE_DIR = 'generated/images' CACHE_PREFIX = 'imagekit:' - DEFAULT_IMAGE_GENERATOR_STRATEGY = 'imagekit.generators.strategies.JustInTime' + DEFAULT_GENERATEDFILE_STRATEGY = 'imagekit.generatedfiles.strategies.JustInTime' DEFAULT_FILE_STORAGE = 'django.core.files.storage.FileSystemStorage' def configure_cache_backend(self, value): diff --git a/imagekit/files.py b/imagekit/files.py index 441d2cb..fb4375c 100644 --- a/imagekit/files.py +++ b/imagekit/files.py @@ -1,13 +1,7 @@ -from django.conf import settings -from django.core.files.base import ContentFile, File -from django.core.files.images import ImageFile +from django.core.files.base import File, ContentFile from django.utils.encoding import smart_str, smart_unicode -from django.utils.functional import LazyObject import os -from .registry import generator_registry -from .signals import before_access -from .utils import (format_to_mimetype, extension_to_mimetype, get_logger, - get_singleton, generate) +from .utils import format_to_mimetype, extension_to_mimetype class BaseIKFile(File): @@ -72,63 +66,6 @@ class BaseIKFile(File): file.close() -class GeneratedImageFile(BaseIKFile, ImageFile): - """ - A file that represents the result of a generator. Creating an instance of - this class is not enough to trigger the generation of the file. In fact, - one of the main points of this class is to allow the creation of the file - to be deferred until the time that the image generator strategy requires it. - - """ - def __init__(self, generator, name=None, storage=None, generatedfile_backend=None): - """ - :param generator: The object responsible for generating a new image. - :param name: The filename - :param storage: A Django storage object that will be used to save the - file. - :param generatedfile_backend: The object responsible for managing the - state of the file. - - """ - self.generator = generator - - self.name = name or getattr(generator, 'generatedfile_name', None) - storage = storage or getattr(generator, 'generatedfile_storage', - None) or get_singleton(settings.IMAGEKIT_DEFAULT_FILE_STORAGE, - 'file storage backend') - self.generatedfile_backend = generatedfile_backend or getattr(generator, - 'generatedfile_backend', None) - - super(GeneratedImageFile, self).__init__(storage=storage) - - def _require_file(self): - before_access.send(sender=self, file=self) - return super(GeneratedImageFile, self)._require_file() - - def generate(self, force=False): - if force: - self._generate() - else: - self.generatedfile_backend.ensure_exists(self) - - def _generate(self): - # Generate the file - content = generate(self.generator) - - actual_name = self.storage.save(self.name, content) - - if actual_name != self.name: - get_logger().warning('The storage backend %s did not save the file' - ' with the requested name ("%s") and instead used' - ' "%s". This may be because a file already existed with' - ' the requested name. If so, you may have meant to call' - ' ensure_exists() instead of generate(), or there may be a' - ' race condition in the file backend %s. The saved file' - ' will not be used.' % (self.storage, - self.name, actual_name, - self.generatedfile_backend)) - - class IKContentFile(ContentFile): """ Wraps a ContentFile in a file-like object with a filename and a @@ -156,14 +93,3 @@ class IKContentFile(ContentFile): def __unicode__(self): return smart_unicode(self.file.name or u'') - - -class LazyGeneratedImageFile(LazyObject): - def __init__(self, generator_id, *args, **kwargs): - super(LazyGeneratedImageFile, self).__init__() - - def setup(): - generator = generator_registry.get(generator_id, *args, **kwargs) - self._wrapped = GeneratedImageFile(generator) - - self.__dict__['_setup'] = setup diff --git a/imagekit/generatedfiles/__init__.py b/imagekit/generatedfiles/__init__.py new file mode 100644 index 0000000..5249451 --- /dev/null +++ b/imagekit/generatedfiles/__init__.py @@ -0,0 +1,75 @@ +from django.conf import settings +from django.core.files.images import ImageFile +from django.utils.functional import LazyObject +from ..files import BaseIKFile +from ..registry import generator_registry +from ..signals import before_access +from ..utils import get_logger, get_singleton, generate + + +class GeneratedImageFile(BaseIKFile, ImageFile): + """ + A file that represents the result of a generator. Creating an instance of + this class is not enough to trigger the generation of the file. In fact, + one of the main points of this class is to allow the creation of the file + to be deferred until the time that the generated file strategy requires it. + + """ + def __init__(self, generator, name=None, storage=None, generatedfile_backend=None): + """ + :param generator: The object responsible for generating a new image. + :param name: The filename + :param storage: A Django storage object that will be used to save the + file. + :param generatedfile_backend: The object responsible for managing the + state of the file. + + """ + self.generator = generator + + self.name = name or getattr(generator, 'generatedfile_name', None) + storage = storage or getattr(generator, 'generatedfile_storage', + None) or get_singleton(settings.IMAGEKIT_DEFAULT_FILE_STORAGE, + 'file storage backend') + self.generatedfile_backend = generatedfile_backend or getattr(generator, + 'generatedfile_backend', None) + + super(GeneratedImageFile, self).__init__(storage=storage) + + def _require_file(self): + before_access.send(sender=self, file=self) + return super(GeneratedImageFile, self)._require_file() + + def generate(self, force=False): + if force: + self._generate() + else: + self.generatedfile_backend.ensure_exists(self) + + def _generate(self): + # Generate the file + content = generate(self.generator) + + actual_name = self.storage.save(self.name, content) + + if actual_name != self.name: + get_logger().warning('The storage backend %s did not save the file' + ' with the requested name ("%s") and instead used' + ' "%s". This may be because a file already existed with' + ' the requested name. If so, you may have meant to call' + ' ensure_exists() instead of generate(), or there may be a' + ' race condition in the file backend %s. The saved file' + ' will not be used.' % (self.storage, + self.name, actual_name, + self.generatedfile_backend)) + + +class LazyGeneratedImageFile(LazyObject): + def __init__(self, generator_id, *args, **kwargs): + super(LazyGeneratedImageFile, self).__init__() + + def setup(): + generator = generator_registry.get(generator_id, *args, **kwargs) + self._wrapped = GeneratedImageFile(generator) + + self.__dict__['_setup'] = setup diff --git a/imagekit/generators/actions.py b/imagekit/generatedfiles/actions.py similarity index 100% rename from imagekit/generators/actions.py rename to imagekit/generatedfiles/actions.py diff --git a/imagekit/generators/filebackends.py b/imagekit/generatedfiles/backends.py similarity index 100% rename from imagekit/generators/filebackends.py rename to imagekit/generatedfiles/backends.py diff --git a/imagekit/generators/strategies.py b/imagekit/generatedfiles/strategies.py similarity index 95% rename from imagekit/generators/strategies.py rename to imagekit/generatedfiles/strategies.py index b1c6f56..72bc1df 100644 --- a/imagekit/generators/strategies.py +++ b/imagekit/generatedfiles/strategies.py @@ -39,7 +39,7 @@ class DictStrategy(object): class StrategyWrapper(LazyObject): def __init__(self, strategy): if isinstance(strategy, basestring): - strategy = get_singleton(strategy, 'generator strategy') + strategy = get_singleton(strategy, 'generated file strategy') elif isinstance(strategy, dict): strategy = DictStrategy(strategy) elif callable(strategy): diff --git a/imagekit/generators/__init__.py b/imagekit/generators/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/imagekit/models/fields/__init__.py b/imagekit/models/fields/__init__.py index 0cfcf5b..ee9a789 100644 --- a/imagekit/models/fields/__init__.py +++ b/imagekit/models/fields/__init__.py @@ -27,14 +27,14 @@ class ImageSpecField(SpecHostField): """ def __init__(self, processors=None, format=None, options=None, source=None, generatedfile_storage=None, autoconvert=None, - generatedfile_backend=None, generator_strategy=None, spec=None, + generatedfile_backend=None, generatedfile_strategy=None, spec=None, id=None): SpecHost.__init__(self, processors=processors, format=format, options=options, generatedfile_storage=generatedfile_storage, autoconvert=autoconvert, generatedfile_backend=generatedfile_backend, - generator_strategy=generator_strategy, spec=spec, + generatedfile_strategy=generatedfile_strategy, spec=spec, spec_id=id) # TODO: Allow callable for source. See https://github.com/jdriscoll/django-imagekit/issues/158#issuecomment-10921664 diff --git a/imagekit/models/fields/utils.py b/imagekit/models/fields/utils.py index e549a6b..d9a2976 100644 --- a/imagekit/models/fields/utils.py +++ b/imagekit/models/fields/utils.py @@ -1,4 +1,4 @@ -from ...files import GeneratedImageFile +from ...generatedfiles import GeneratedImageFile from django.db.models.fields.files import ImageField diff --git a/imagekit/registry.py b/imagekit/registry.py index 3a998f0..309bb54 100644 --- a/imagekit/registry.py +++ b/imagekit/registry.py @@ -54,8 +54,8 @@ class GeneratorRegistry(object): class SourceGroupRegistry(object): """ The source group registry is responsible for listening to source_* signals - on source groups, and relaying them to the image generator strategies of the - appropriate generators. + on source groups, and relaying them to the image generated file strategies + of the appropriate generators. In addition, registering a new source group also registers its generated files with that registry. @@ -92,7 +92,7 @@ class SourceGroupRegistry(object): Relay source group signals to the appropriate spec strategy. """ - from .files import GeneratedImageFile + from .generatedfiles import GeneratedImageFile source_group = sender # Ignore signals from unregistered groups. diff --git a/imagekit/specs/__init__.py b/imagekit/specs/__init__.py index c156b52..7c096a2 100644 --- a/imagekit/specs/__init__.py +++ b/imagekit/specs/__init__.py @@ -3,8 +3,8 @@ from django.db.models.fields.files import ImageFieldFile from hashlib import md5 import os import pickle -from ..generators.filebackends import get_default_generatedfile_backend -from ..generators.strategies import StrategyWrapper +from ..generatedfiles.backends import get_default_generatedfile_backend +from ..generatedfiles.strategies import StrategyWrapper from ..processors import ProcessorPipeline from ..utils import open_image, img_to_fobj, suggest_extension from ..registry import generator_registry, register @@ -27,17 +27,17 @@ class BaseImageSpec(object): """ - generator_strategy = settings.IMAGEKIT_DEFAULT_IMAGE_GENERATOR_STRATEGY + generatedfile_strategy = settings.IMAGEKIT_DEFAULT_GENERATEDFILE_STRATEGY """ A dictionary containing callbacks that allow you to customize how and when the image file is created. Defaults to - ``IMAGEKIT_DEFAULT_IMAGE_GENERATOR_STRATEGY``. + ``IMAGEKIT_DEFAULT_GENERATEDFILE_STRATEGY``. """ def __init__(self): self.generatedfile_backend = self.generatedfile_backend or get_default_generatedfile_backend() - self.generator_strategy = StrategyWrapper(self.generator_strategy) + self.generatedfile_strategy = StrategyWrapper(self.generatedfile_strategy) def generate(self): raise NotImplementedError diff --git a/imagekit/specs/sourcegroups.py b/imagekit/specs/sourcegroups.py index 5e82144..e828f91 100644 --- a/imagekit/specs/sourcegroups.py +++ b/imagekit/specs/sourcegroups.py @@ -3,7 +3,7 @@ Source groups are the means by which image spec sources are identified. They have two responsibilities: 1. To dispatch ``source_created``, ``source_changed``, and ``source_deleted`` - signals. (These will be relayed to the corresponding specs' generator + signals. (These will be relayed to the corresponding specs' generated file strategies.) 2. To provide the source files that they represent, via a generator method named ``files()``. (This is used by the generateimages management command for @@ -13,7 +13,7 @@ have two responsibilities: from django.db.models.signals import post_init, post_save, post_delete from django.utils.functional import wraps -from ..files import LazyGeneratedImageFile +from ..generatedfiles import LazyGeneratedImageFile from ..signals import source_created, source_changed, source_deleted diff --git a/imagekit/templatetags/imagekit.py b/imagekit/templatetags/imagekit.py index 555c693..8a59242 100644 --- a/imagekit/templatetags/imagekit.py +++ b/imagekit/templatetags/imagekit.py @@ -2,7 +2,7 @@ from django import template from django.utils.html import escape from django.utils.safestring import mark_safe from .compat import parse_bits -from ..files import GeneratedImageFile +from ..generatedfiles import GeneratedImageFile from ..registry import generator_registry diff --git a/imagekit/utils.py b/imagekit/utils.py index 59ef235..e210576 100644 --- a/imagekit/utils.py +++ b/imagekit/utils.py @@ -425,7 +425,7 @@ def generate(generator): def call_strategy_method(generator, method_name, *args, **kwargs): - strategy = getattr(generator, 'generator_strategy', None) + strategy = getattr(generator, 'generatedfile_strategy', None) fn = getattr(strategy, method_name, None) if fn is not None: fn(*args, **kwargs) From 75962976d02592c4aa75471f6aaaed0bf0874a77 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Thu, 31 Jan 2013 19:41:54 -0500 Subject: [PATCH 183/213] Add stringify methods to LazyGeneratedImageFile --- imagekit/generatedfiles/__init__.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/imagekit/generatedfiles/__init__.py b/imagekit/generatedfiles/__init__.py index 5249451..8b1c9cd 100644 --- a/imagekit/generatedfiles/__init__.py +++ b/imagekit/generatedfiles/__init__.py @@ -73,3 +73,18 @@ class LazyGeneratedImageFile(LazyObject): self._wrapped = GeneratedImageFile(generator) self.__dict__['_setup'] = setup + + def __repr__(self): + if self._wrapped is None: + self._setup() + return '<%s: %s>' % (self.__class__.__name__, self or 'None') + + def __str__(self): + if self._wrapped is None: + self._setup() + return str(self._wrapped) + + def __unicode__(self): + if self._wrapped is None: + self._setup() + return unicode(self._wrapped) From bf1685dbfbbd16b6310b588f8c0bbc7712bb3698 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Thu, 31 Jan 2013 22:01:01 -0500 Subject: [PATCH 184/213] Generalize get_class util --- imagekit/utils.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/imagekit/utils.py b/imagekit/utils.py index e210576..2ed14de 100644 --- a/imagekit/utils.py +++ b/imagekit/utils.py @@ -322,23 +322,23 @@ def prepare_image(img, format): return img, save_kwargs -def get_class(path, desc): +def get_by_qname(path, desc): try: dot = path.rindex('.') except ValueError: raise ImproperlyConfigured("%s isn't a %s module." % (path, desc)) - module, classname = path[:dot], path[dot + 1:] + module, objname = path[:dot], path[dot + 1:] try: mod = import_module(module) except ImportError, e: raise ImproperlyConfigured('Error importing %s module %s: "%s"' % (desc, module, e)) try: - cls = getattr(mod, classname) - return cls + obj = getattr(mod, objname) + return obj except AttributeError: - raise ImproperlyConfigured('%s module "%s" does not define a "%s"' - ' class.' % (desc[0].upper() + desc[1:], module, classname)) + raise ImproperlyConfigured('%s module "%s" does not define "%s"' + % (desc[0].upper() + desc[1:], module, objname)) _singletons = {} @@ -346,7 +346,7 @@ _singletons = {} def get_singleton(class_path, desc): global _singletons - cls = get_class(class_path, desc) + cls = get_by_qname(class_path, desc) instance = _singletons.get(cls) if not instance: instance = _singletons[cls] = cls() From e1c819e9b4b7477f5291559a44fee2ed39cb4a64 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Thu, 31 Jan 2013 22:37:09 -0500 Subject: [PATCH 185/213] Allow default generatedfile name configuration w/namers --- imagekit/conf.py | 2 ++ imagekit/generatedfiles/__init__.py | 9 ++++-- imagekit/generatedfiles/namers.py | 45 +++++++++++++++++++++++++++++ imagekit/specs/__init__.py | 19 ++---------- 4 files changed, 57 insertions(+), 18 deletions(-) create mode 100644 imagekit/generatedfiles/namers.py diff --git a/imagekit/conf.py b/imagekit/conf.py index 4c39edc..7bbb5b9 100644 --- a/imagekit/conf.py +++ b/imagekit/conf.py @@ -5,6 +5,8 @@ from django.conf import settings class ImageKitConf(AppConf): DEFAULT_GENERATEDFILE_BACKEND = 'imagekit.generatedfiles.backends.Simple' CACHE_BACKEND = None + GENERATEDFILE_NAMER = 'imagekit.generatedfiles.namers.hash' + SPEC_GENERATEDFILE_NAMER = 'imagekit.generatedfiles.namers.source_name_as_path' GENERATED_FILE_DIR = 'generated/images' CACHE_PREFIX = 'imagekit:' DEFAULT_GENERATEDFILE_STRATEGY = 'imagekit.generatedfiles.strategies.JustInTime' diff --git a/imagekit/generatedfiles/__init__.py b/imagekit/generatedfiles/__init__.py index 8b1c9cd..5bc93f3 100644 --- a/imagekit/generatedfiles/__init__.py +++ b/imagekit/generatedfiles/__init__.py @@ -4,7 +4,7 @@ from django.utils.functional import LazyObject from ..files import BaseIKFile from ..registry import generator_registry from ..signals import before_access -from ..utils import get_logger, get_singleton, generate +from ..utils import get_logger, get_singleton, generate, get_by_qname class GeneratedImageFile(BaseIKFile, ImageFile): @@ -27,7 +27,12 @@ class GeneratedImageFile(BaseIKFile, ImageFile): """ self.generator = generator - self.name = name or getattr(generator, 'generatedfile_name', None) + name = name or getattr(generator, 'generatedfile_name', None) + if not name: + fn = get_by_qname(settings.IMAGEKIT_GENERATEDFILE_NAMER, 'namer') + name = fn(generator) + self.name = name + storage = storage or getattr(generator, 'generatedfile_storage', None) or get_singleton(settings.IMAGEKIT_DEFAULT_FILE_STORAGE, 'file storage backend') diff --git a/imagekit/generatedfiles/namers.py b/imagekit/generatedfiles/namers.py new file mode 100644 index 0000000..a20aa5a --- /dev/null +++ b/imagekit/generatedfiles/namers.py @@ -0,0 +1,45 @@ +from django.conf import settings +import os +from ..utils import format_to_extension, suggest_extension + + +def source_name_as_path(generator): + source_filename = getattr(generator.source, 'name', None) + + if source_filename is None or os.path.isabs(source_filename): + # Generally, we put the file right in the generated file directory. + dir = settings.IMAGEKIT_GENERATED_FILE_DIR + else: + # For source files with relative names (like Django media files), + # use the source's name to create the new filename. + dir = os.path.join(settings.IMAGEKIT_GENERATED_FILE_DIR, + os.path.splitext(source_filename)[0]) + + ext = suggest_extension(source_filename or '', generator.format) + return os.path.normpath(os.path.join(dir, + '%s%s' % (generator.get_hash(), ext))) + + +def source_name_dot_hash(generator): + source_filename = getattr(generator.source, 'name', None) + + if source_filename is None or os.path.isabs(source_filename): + # Generally, we put the file right in the generated file directory. + dir = settings.IMAGEKIT_GENERATED_FILE_DIR + else: + # For source files with relative names (like Django media files), + # use the source's name to create the new filename. + dir = os.path.join(settings.IMAGEKIT_GENERATED_FILE_DIR, + os.path.dirname(source_filename)) + + ext = suggest_extension(source_filename or '', generator.format) + basename = os.path.basename(source_filename) + return os.path.normpath(os.path.join(dir, '%s.%s%s' % ( + os.path.splitext(basename)[0], generator.get_hash()[:12], ext))) + + +def hash(generator): + format = getattr(generator, 'format', None) + ext = format_to_extension(format) if format else '' + return os.path.normpath(os.path.join(settings.IMAGEKIT_GENERATED_FILE_DIR, + '%s%s' % (generator.get_hash(), ext))) diff --git a/imagekit/specs/__init__.py b/imagekit/specs/__init__.py index 7c096a2..cf37b6f 100644 --- a/imagekit/specs/__init__.py +++ b/imagekit/specs/__init__.py @@ -1,12 +1,11 @@ from django.conf import settings from django.db.models.fields.files import ImageFieldFile from hashlib import md5 -import os import pickle from ..generatedfiles.backends import get_default_generatedfile_backend from ..generatedfiles.strategies import StrategyWrapper from ..processors import ProcessorPipeline -from ..utils import open_image, img_to_fobj, suggest_extension +from ..utils import open_image, img_to_fobj, get_by_qname from ..registry import generator_registry, register @@ -84,20 +83,8 @@ class ImageSpec(BaseImageSpec): @property def generatedfile_name(self): - source_filename = getattr(self.source, 'name', None) - - if source_filename is None or os.path.isabs(source_filename): - # Generally, we put the file right in the generated file directory. - dir = settings.IMAGEKIT_GENERATED_FILE_DIR - else: - # For source files with relative names (like Django media files), - # use the source's name to create the new filename. - dir = os.path.join(settings.IMAGEKIT_GENERATED_FILE_DIR, - os.path.splitext(source_filename)[0]) - - ext = suggest_extension(source_filename or '', self.format) - return os.path.normpath(os.path.join(dir, - '%s%s' % (self.get_hash(), ext))) + fn = get_by_qname(settings.IMAGEKIT_SPEC_GENERATEDFILE_NAMER, 'namer') + return fn(self) def __getstate__(self): state = self.__dict__ From 933ff79ac16578e546710369864d681df338589a Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Thu, 31 Jan 2013 22:38:48 -0500 Subject: [PATCH 186/213] Make settings consistent --- imagekit/conf.py | 2 +- imagekit/generatedfiles/namers.py | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/imagekit/conf.py b/imagekit/conf.py index 7bbb5b9..abeee93 100644 --- a/imagekit/conf.py +++ b/imagekit/conf.py @@ -7,7 +7,7 @@ class ImageKitConf(AppConf): CACHE_BACKEND = None GENERATEDFILE_NAMER = 'imagekit.generatedfiles.namers.hash' SPEC_GENERATEDFILE_NAMER = 'imagekit.generatedfiles.namers.source_name_as_path' - GENERATED_FILE_DIR = 'generated/images' + GENERATEDFILE_DIR = 'generated/images' CACHE_PREFIX = 'imagekit:' DEFAULT_GENERATEDFILE_STRATEGY = 'imagekit.generatedfiles.strategies.JustInTime' DEFAULT_FILE_STORAGE = 'django.core.files.storage.FileSystemStorage' diff --git a/imagekit/generatedfiles/namers.py b/imagekit/generatedfiles/namers.py index a20aa5a..062fb7a 100644 --- a/imagekit/generatedfiles/namers.py +++ b/imagekit/generatedfiles/namers.py @@ -8,11 +8,11 @@ def source_name_as_path(generator): if source_filename is None or os.path.isabs(source_filename): # Generally, we put the file right in the generated file directory. - dir = settings.IMAGEKIT_GENERATED_FILE_DIR + dir = settings.IMAGEKIT_GENERATEDFILE_DIR else: # For source files with relative names (like Django media files), # use the source's name to create the new filename. - dir = os.path.join(settings.IMAGEKIT_GENERATED_FILE_DIR, + dir = os.path.join(settings.IMAGEKIT_GENERATEDFILE_DIR, os.path.splitext(source_filename)[0]) ext = suggest_extension(source_filename or '', generator.format) @@ -25,11 +25,11 @@ def source_name_dot_hash(generator): if source_filename is None or os.path.isabs(source_filename): # Generally, we put the file right in the generated file directory. - dir = settings.IMAGEKIT_GENERATED_FILE_DIR + dir = settings.IMAGEKIT_GENERATEDFILE_DIR else: # For source files with relative names (like Django media files), # use the source's name to create the new filename. - dir = os.path.join(settings.IMAGEKIT_GENERATED_FILE_DIR, + dir = os.path.join(settings.IMAGEKIT_GENERATEDFILE_DIR, os.path.dirname(source_filename)) ext = suggest_extension(source_filename or '', generator.format) @@ -41,5 +41,5 @@ def source_name_dot_hash(generator): def hash(generator): format = getattr(generator, 'format', None) ext = format_to_extension(format) if format else '' - return os.path.normpath(os.path.join(settings.IMAGEKIT_GENERATED_FILE_DIR, + return os.path.normpath(os.path.join(settings.IMAGEKIT_GENERATEDFILE_DIR, '%s%s' % (generator.get_hash(), ext))) From 0947c1403fa1873bca27116ded1370f84adcd0af Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Thu, 31 Jan 2013 22:40:54 -0500 Subject: [PATCH 187/213] Organize settings --- imagekit/conf.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/imagekit/conf.py b/imagekit/conf.py index abeee93..c81e228 100644 --- a/imagekit/conf.py +++ b/imagekit/conf.py @@ -3,15 +3,17 @@ from django.conf import settings class ImageKitConf(AppConf): - DEFAULT_GENERATEDFILE_BACKEND = 'imagekit.generatedfiles.backends.Simple' - CACHE_BACKEND = None GENERATEDFILE_NAMER = 'imagekit.generatedfiles.namers.hash' SPEC_GENERATEDFILE_NAMER = 'imagekit.generatedfiles.namers.source_name_as_path' GENERATEDFILE_DIR = 'generated/images' - CACHE_PREFIX = 'imagekit:' + DEFAULT_GENERATEDFILE_BACKEND = 'imagekit.generatedfiles.backends.Simple' DEFAULT_GENERATEDFILE_STRATEGY = 'imagekit.generatedfiles.strategies.JustInTime' + DEFAULT_FILE_STORAGE = 'django.core.files.storage.FileSystemStorage' + CACHE_BACKEND = None + CACHE_PREFIX = 'imagekit:' + def configure_cache_backend(self, value): if value is None: value = 'django.core.cache.backends.dummy.DummyCache' if settings.DEBUG else 'default' From 92b11f83495bcbe7f7323d42d6ff0f5cf47aaf53 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Fri, 1 Feb 2013 00:30:15 -0500 Subject: [PATCH 188/213] Use imagegenerators module, not imagespecs --- imagekit/utils.py | 12 ++++++------ tests/{imagespecs.py => imagegenerators.py} | 0 tests/test_fields.py | 2 +- tests/test_generateimage_tag.py | 2 +- tests/test_thumbnail_tag.py | 2 +- 5 files changed, 9 insertions(+), 9 deletions(-) rename tests/{imagespecs.py => imagegenerators.py} (100%) diff --git a/imagekit/utils.py b/imagekit/utils.py index 2ed14de..2ac4f56 100644 --- a/imagekit/utils.py +++ b/imagekit/utils.py @@ -355,9 +355,9 @@ def get_singleton(class_path, desc): def autodiscover(): """ - Auto-discover INSTALLED_APPS imagespecs.py modules and fail silently when - not present. This forces an import on them to register any admin bits they - may want. + Auto-discover INSTALLED_APPS imagegenerators.py modules and fail silently + when not present. This forces an import on them to register any admin bits + they may want. Copied from django.contrib.admin """ @@ -370,12 +370,12 @@ def autodiscover(): mod = import_module(app) # Attempt to import the app's admin module. try: - import_module('%s.imagespecs' % app) + import_module('%s.imagegenerators' % app) except: # Decide whether to bubble up this error. If the app just - # doesn't have an imagespecs module, we can ignore the error + # doesn't have an imagegenerators module, we can ignore the error # attempting to import it, otherwise we want it to bubble up. - if module_has_submodule(mod, 'imagespecs'): + if module_has_submodule(mod, 'imagegenerators'): raise diff --git a/tests/imagespecs.py b/tests/imagegenerators.py similarity index 100% rename from tests/imagespecs.py rename to tests/imagegenerators.py diff --git a/tests/test_fields.py b/tests/test_fields.py index bce294f..df513ee 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -4,7 +4,7 @@ from django.core.files.uploadedfile import SimpleUploadedFile from imagekit import forms as ikforms from imagekit.processors import SmartCrop from nose.tools import eq_ -from . import imagespecs # noqa +from . import imagegenerators # noqa from .models import ProcessedImageFieldModel, ImageModel from .utils import get_image_file diff --git a/tests/test_generateimage_tag.py b/tests/test_generateimage_tag.py index 76ad2a5..12f8d4b 100644 --- a/tests/test_generateimage_tag.py +++ b/tests/test_generateimage_tag.py @@ -1,6 +1,6 @@ from django.template import TemplateSyntaxError from nose.tools import eq_, assert_not_in, raises, assert_not_equal -from . import imagespecs # noqa +from . import imagegenerators # noqa from .utils import render_tag, get_html_attrs diff --git a/tests/test_thumbnail_tag.py b/tests/test_thumbnail_tag.py index 5fdbbb2..235bb1b 100644 --- a/tests/test_thumbnail_tag.py +++ b/tests/test_thumbnail_tag.py @@ -1,6 +1,6 @@ from django.template import TemplateSyntaxError from nose.tools import eq_, raises, assert_not_equal -from . import imagespecs # noqa +from . import imagegenerators # noqa from .utils import render_tag, get_html_attrs From 08ebcbcbf316a25efe72e3057c9f09243168e993 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Fri, 1 Feb 2013 00:56:29 -0500 Subject: [PATCH 189/213] Change html attrs delimiter to -- --- imagekit/templatetags/imagekit.py | 8 ++++---- tests/test_generateimage_tag.py | 12 ++++++------ tests/test_thumbnail_tag.py | 10 +++++----- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/imagekit/templatetags/imagekit.py b/imagekit/templatetags/imagekit.py index 8a59242..67d2438 100644 --- a/imagekit/templatetags/imagekit.py +++ b/imagekit/templatetags/imagekit.py @@ -10,7 +10,7 @@ register = template.Library() ASSIGNMENT_DELIMETER = 'as' -HTML_ATTRS_DELIMITER = 'with' +HTML_ATTRS_DELIMITER = '--' DEFAULT_THUMBNAIL_GENERATOR = 'ik:thumbnail' @@ -205,10 +205,10 @@ def generateimage(parser, token): - You can add additional attributes to the tag using "with". For example, + You can add additional attributes to the tag using "--". For example, this:: - {% generateimage 'myapp:thumbnail' from=mymodel.profile_image with alt="Hello!" %} + {% generateimage 'myapp:thumbnail' from=mymodel.profile_image -- alt="Hello!" %} will result in the following markup:: @@ -250,7 +250,7 @@ def thumbnail(parser, token): {% generateimage 'ik:thumbnail' from=mymodel.profile_image width=100 height=100 %} - The thumbnail tag supports the "with" and "as" bits for adding html + The thumbnail tag supports the "--" and "as" bits for adding html attributes and assigning to a variable, respectively. It also accepts the kwargs "anchor", and "crop". diff --git a/tests/test_generateimage_tag.py b/tests/test_generateimage_tag.py index 12f8d4b..18b4168 100644 --- a/tests/test_generateimage_tag.py +++ b/tests/test_generateimage_tag.py @@ -14,25 +14,25 @@ def test_img_tag(): def test_img_tag_attrs(): - ttag = r"""{% generateimage 'testspec' from=img with alt="Hello" %}""" + ttag = r"""{% generateimage 'testspec' from=img -- alt="Hello" %}""" attrs = get_html_attrs(ttag) eq_(attrs.get('alt'), 'Hello') @raises(TemplateSyntaxError) -def test_dangling_with(): - ttag = r"""{% generateimage 'testspec' from=img with %}""" +def test_dangling_html_attrs_delimiter(): + ttag = r"""{% generateimage 'testspec' from=img -- %}""" render_tag(ttag) @raises(TemplateSyntaxError) -def test_with_assignment(): +def test_html_attrs_assignment(): """ You can either use generateimage as an assigment tag or specify html attrs, but not both. """ - ttag = r"""{% generateimage 'testspec' from=img with alt="Hello" as th %}""" + ttag = r"""{% generateimage 'testspec' from=img -- alt="Hello" as th %}""" render_tag(ttag) @@ -41,7 +41,7 @@ def test_single_dimension_attr(): If you only provide one of width or height, the other should not be added. """ - ttag = r"""{% generateimage 'testspec' from=img with width="50" %}""" + ttag = r"""{% generateimage 'testspec' from=img -- width="50" %}""" attrs = get_html_attrs(ttag) assert_not_in('height', attrs) diff --git a/tests/test_thumbnail_tag.py b/tests/test_thumbnail_tag.py index 235bb1b..e31304a 100644 --- a/tests/test_thumbnail_tag.py +++ b/tests/test_thumbnail_tag.py @@ -14,14 +14,14 @@ def test_img_tag(): def test_img_tag_attrs(): - ttag = r"""{% thumbnail '100x100' img with alt="Hello" %}""" + ttag = r"""{% thumbnail '100x100' img -- alt="Hello" %}""" attrs = get_html_attrs(ttag) eq_(attrs.get('alt'), 'Hello') @raises(TemplateSyntaxError) -def test_dangling_with(): - ttag = r"""{% thumbnail '100x100' img with %}""" +def test_dangling_html_attrs_delimiter(): + ttag = r"""{% thumbnail '100x100' img -- %}""" render_tag(ttag) @@ -38,13 +38,13 @@ def test_too_many_args(): @raises(TemplateSyntaxError) -def test_with_assignment(): +def test_html_attrs_assignment(): """ You can either use thumbnail as an assigment tag or specify html attrs, but not both. """ - ttag = r"""{% thumbnail '100x100' img with alt="Hello" as th %}""" + ttag = r"""{% thumbnail '100x100' img -- alt="Hello" as th %}""" render_tag(ttag) From 50d83745bce1f4ee07436f4095f3435b59a59a4a Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Fri, 1 Feb 2013 01:02:20 -0500 Subject: [PATCH 190/213] Remove unnecessary complexity of kwarg mapping --- imagekit/templatetags/imagekit.py | 24 +++++++++--------------- tests/test_generateimage_tag.py | 12 ++++++------ 2 files changed, 15 insertions(+), 21 deletions(-) diff --git a/imagekit/templatetags/imagekit.py b/imagekit/templatetags/imagekit.py index 67d2438..956f592 100644 --- a/imagekit/templatetags/imagekit.py +++ b/imagekit/templatetags/imagekit.py @@ -14,15 +14,9 @@ HTML_ATTRS_DELIMITER = '--' DEFAULT_THUMBNAIL_GENERATOR = 'ik:thumbnail' -_kwarg_map = { - 'from': 'source', -} - - def get_generatedfile(context, generator_id, generator_kwargs, source=None): generator_id = generator_id.resolve(context) - kwargs = dict((_kwarg_map.get(k, k), v.resolve(context)) for k, - v in generator_kwargs.items()) + kwargs = dict((k, v.resolve(context)) for k, v in generator_kwargs.items()) generator = generator_registry.get(generator_id, **kwargs) return GeneratedImageFile(generator) @@ -104,8 +98,8 @@ class ThumbnailAssignmentNode(template.Node): variable_name = self.get_variable_name(context) generator_id = self._generator_id.resolve(context) if self._generator_id else DEFAULT_THUMBNAIL_GENERATOR - kwargs = dict((_kwarg_map.get(k, k), v.resolve(context)) for k, - v in self._generator_kwargs.items()) + kwargs = dict((k, v.resolve(context)) for k, v in + self._generator_kwargs.items()) kwargs['source'] = self._source.resolve(context) kwargs.update(parse_dimensions(self._dimensions.resolve(context))) generator = generator_registry.get(generator_id, **kwargs) @@ -130,8 +124,8 @@ class ThumbnailImageTagNode(template.Node): generator_id = self._generator_id.resolve(context) if self._generator_id else DEFAULT_THUMBNAIL_GENERATOR dimensions = parse_dimensions(self._dimensions.resolve(context)) - kwargs = dict((_kwarg_map.get(k, k), v.resolve(context)) for k, - v in self._generator_kwargs.items()) + kwargs = dict((k, v.resolve(context)) for k, v in + self._generator_kwargs.items()) kwargs['source'] = self._source.resolve(context) kwargs.update(dimensions) generator = generator_registry.get(generator_id, **kwargs) @@ -199,7 +193,7 @@ def generateimage(parser, token): By default:: - {% generateimage 'myapp:thumbnail' from=mymodel.profile_image %} + {% generateimage 'myapp:thumbnail' source=mymodel.profile_image %} generates an ```` tag:: @@ -208,7 +202,7 @@ def generateimage(parser, token): You can add additional attributes to the tag using "--". For example, this:: - {% generateimage 'myapp:thumbnail' from=mymodel.profile_image -- alt="Hello!" %} + {% generateimage 'myapp:thumbnail' source=mymodel.profile_image -- alt="Hello!" %} will result in the following markup:: @@ -216,7 +210,7 @@ def generateimage(parser, token): For more flexibility, ``generateimage`` also works as an assignment tag:: - {% generateimage 'myapp:thumbnail' from=mymodel.profile_image as th %} + {% generateimage 'myapp:thumbnail' source=mymodel.profile_image as th %} """ @@ -248,7 +242,7 @@ def thumbnail(parser, token): is equivalent to:: - {% generateimage 'ik:thumbnail' from=mymodel.profile_image width=100 height=100 %} + {% generateimage 'ik:thumbnail' source=mymodel.profile_image width=100 height=100 %} The thumbnail tag supports the "--" and "as" bits for adding html attributes and assigning to a variable, respectively. It also accepts the diff --git a/tests/test_generateimage_tag.py b/tests/test_generateimage_tag.py index 18b4168..e7ea091 100644 --- a/tests/test_generateimage_tag.py +++ b/tests/test_generateimage_tag.py @@ -5,7 +5,7 @@ from .utils import render_tag, get_html_attrs def test_img_tag(): - ttag = r"""{% generateimage 'testspec' from=img %}""" + ttag = r"""{% generateimage 'testspec' source=img %}""" attrs = get_html_attrs(ttag) expected_attrs = set(['src', 'width', 'height']) eq_(set(attrs.keys()), expected_attrs) @@ -14,14 +14,14 @@ def test_img_tag(): def test_img_tag_attrs(): - ttag = r"""{% generateimage 'testspec' from=img -- alt="Hello" %}""" + ttag = r"""{% generateimage 'testspec' source=img -- alt="Hello" %}""" attrs = get_html_attrs(ttag) eq_(attrs.get('alt'), 'Hello') @raises(TemplateSyntaxError) def test_dangling_html_attrs_delimiter(): - ttag = r"""{% generateimage 'testspec' from=img -- %}""" + ttag = r"""{% generateimage 'testspec' source=img -- %}""" render_tag(ttag) @@ -32,7 +32,7 @@ def test_html_attrs_assignment(): but not both. """ - ttag = r"""{% generateimage 'testspec' from=img -- alt="Hello" as th %}""" + ttag = r"""{% generateimage 'testspec' source=img -- alt="Hello" as th %}""" render_tag(ttag) @@ -41,12 +41,12 @@ def test_single_dimension_attr(): If you only provide one of width or height, the other should not be added. """ - ttag = r"""{% generateimage 'testspec' from=img -- width="50" %}""" + ttag = r"""{% generateimage 'testspec' source=img -- width="50" %}""" attrs = get_html_attrs(ttag) assert_not_in('height', attrs) def test_assignment_tag(): - ttag = r"""{% generateimage 'testspec' from=img as th %}{{ th.url }}""" + ttag = r"""{% generateimage 'testspec' source=img as th %}{{ th.url }}""" html = render_tag(ttag) assert_not_equal(html.strip(), '') From f94b7276b3a8464e82929c8cdcf048f77cefe9dc Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Fri, 1 Feb 2013 01:21:01 -0500 Subject: [PATCH 191/213] Use "imagekit" instead of "ik" for built-in generator prefix --- imagekit/generatorlibrary.py | 2 +- imagekit/templatetags/imagekit.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/imagekit/generatorlibrary.py b/imagekit/generatorlibrary.py index 977e960..bc5a0f8 100644 --- a/imagekit/generatorlibrary.py +++ b/imagekit/generatorlibrary.py @@ -10,4 +10,4 @@ class Thumbnail(ImageSpec): super(Thumbnail, self).__init__(**kwargs) -register.generator('ik:thumbnail', Thumbnail) +register.generator('imagekit:thumbnail', Thumbnail) diff --git a/imagekit/templatetags/imagekit.py b/imagekit/templatetags/imagekit.py index 956f592..fbad511 100644 --- a/imagekit/templatetags/imagekit.py +++ b/imagekit/templatetags/imagekit.py @@ -11,7 +11,7 @@ register = template.Library() ASSIGNMENT_DELIMETER = 'as' HTML_ATTRS_DELIMITER = '--' -DEFAULT_THUMBNAIL_GENERATOR = 'ik:thumbnail' +DEFAULT_THUMBNAIL_GENERATOR = 'imagekit:thumbnail' def get_generatedfile(context, generator_id, generator_kwargs, source=None): @@ -242,7 +242,7 @@ def thumbnail(parser, token): is equivalent to:: - {% generateimage 'ik:thumbnail' source=mymodel.profile_image width=100 height=100 %} + {% generateimage 'imagekit:thumbnail' source=mymodel.profile_image width=100 height=100 %} The thumbnail tag supports the "--" and "as" bits for adding html attributes and assigning to a variable, respectively. It also accepts the From 58e1c7f7e0ceaebd7d2515380271dcf2c2428fa8 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Fri, 1 Feb 2013 01:27:54 -0500 Subject: [PATCH 192/213] Some docs --- README.rst | 269 +++++++++++++++++++++--------- imagekit/generatedfiles/namers.py | 46 +++++ 2 files changed, 233 insertions(+), 82 deletions(-) diff --git a/README.rst b/README.rst index db024c3..3e6e97b 100644 --- a/README.rst +++ b/README.rst @@ -36,10 +36,87 @@ Specs ----- You have one image and you want to do something to it to create another image. -That's the basic use case of ImageKit. But how do you tell ImageKit what to do? -By defining an "image spec." Specs are instructions for creating a new image -from an existing one, and there are a few ways to define one. The most basic -way is by defining an ``ImageSpec`` subclass: +But how do you tell ImageKit what to do? By defining an image spec. + +An **image spec** is a type of **image generator** that generates a new image +from a source image. + + +Defining Specs In Models +^^^^^^^^^^^^^^^^^^^^^^^^ + +The easiest way to use define an image spec is by using an ImageSpecField on +your model class: + +.. code-block:: python + + from django.db import models + from imagekit.models import ImageSpecField + from imagekit.processors import ResizeToFill + + class Profile(models.Model): + avatar = models.ImageField(upload_to='avatars') + avatar_thumbnail = ImageSpecField(source='avatar', + processors=[ResizeToFill(100, 50)], + format='JPEG', + options={'quality': 60}) + + profile = Profile.objects.all()[0] + print profile.avatar_thumbnail.url # > /media/generated/images/982d5af84cddddfd0fbf70892b4431e4.jpg + print profile.avatar_thumbnail.width # > 100 + +As you can probably tell, ImageSpecFields work a lot like Django's +ImageFields. The difference is that they're automatically generated by +ImageKit based on the instructions you give. In the example above, the avatar +thumbnail is a resized version of the avatar image, saved as a JPEG with a +quality of 60. + +Sometimes, however, you don't need to keep the original image (the avatar in +the above example); when the user uploads an image, you just want to process it +and save the result. In those cases, you can use the ``ProcessedImageField`` +class: + +.. code-block:: python + + from django.db import models + from imagekit.models import ProcessedImageField + + class Profile(models.Model): + avatar_thumbnail = ProcessedImageField(upload_to='avatars', + processors=[ResizeToFill(100, 50)], + format='JPEG', + options={'quality': 60}) + + profile = Profile.objects.all()[0] + print profile.avatar_thumbnail.url # > /media/avatars/MY-avatar.jpg + print profile.avatar_thumbnail.width # > 100 + +This is pretty similar to our previous example. We don't need to specify a +"source" any more since we're not processing another image field, but we do need +to pass an "upload_to" argument. This behaves exactly as it does for Django +``ImageField``s. + +.. note:: + + You might be wondering why we didn't need an "upload_to" argument for our + ImageSpecField. The reason is that ProcessedImageFields really are just like + ImageFields—they save the file path in the database and you need to run + syncdb (or create a migration) when you add one to your model. + + ImageSpecFields, on the other hand, are virtual—they add no fields to your + database and don't require a database. This is handy for a lot of reasons, + but it means that the path to the image file needs to be programmatically + constructed based on the source image and the spec. + + +Defining Specs Outside of Models +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Defining specs as models fields is one very convenient way to process images, +but it isn't the only way. Sometimes you can't (or don't want to) add fields to +your models, and that's okay. You can define image spec classes and use them +directly. This can be especially useful for doing image processing in views— +particularly when the processing being done depends on user input. .. code-block:: python @@ -51,51 +128,109 @@ way is by defining an ``ImageSpec`` subclass: format = 'JPEG' options = {'quality': 60} -Now that you've defined a spec, it's time to use it. The nice thing about specs -is that they can be used in many different contexts. - -Sometimes, you may want to just use a spec to generate a new image file. This -might be useful, for example, in view code, or in scripts: +It's probaby not surprising that this class is capable of processing an image +in the exact same way as our ImageSpecField above. However, unlike with the +image spec model field, this class doesn't define what source the spec is acting +on, or what should be done with the result; that's up to you: .. code-block:: python - spec = Thumbnail() - new_file = spec.apply(source_file) + source_file = open('/path/to/myimage.jpg') + image_generator = Thumbnail(source=source_file) + result = image_generator.generate() -More often, however, you'll want to register your spec with ImageKit: +The result of calling ``generate()`` on an image spec is a file-like object +containing our resized image, with which you can do whatever you want. For +example, if you wanted to save it to disk: .. code-block:: python - from imagekit import specs - specs.register(Thumbnail, 'myapp:fancy_thumbnail') - -Once a spec is registered with a unique name, you can start to take advantage of -ImageKit's powerful utilities to automatically generate images for you... - -.. note:: You might be wondering why we bother with the id string instead of - just passing the spec itself. The reason is that these ids allow users to - easily override specs defined in third party apps. That way, it doesn't - matter if "django-badblog" says its thumbnails are 200x200, you can just - register your own spec (using the same id the app uses) and have whatever - size thumbnails you want. + dest = open('/path/to/dest.jpg', 'w') + dest.write(result.read()) + dest.close() -In Templates -^^^^^^^^^^^^ +Using Specs In Templates +^^^^^^^^^^^^^^^^^^^^^^^^ -One utility ImageKit provides for processing images is a template tag: +If you have a model with an ImageSpecField or ProcessedImageField, you can +easily use those processed image just as you would a normal image field: + +.. code-block:: html + + + +(This is assuming you have a view that's setting a context variable named +"profile" to an instance of our Profile model.) + +But you can also generate processed image files directly in your template—from +any image—without adding anything to your model. In order to do this, you'll +first have to define an image generator class (remember, specs are a type of +generator) in your app somewhere, just as we did in the last section. You'll +also need a way of referring to the generator in your template, so you'll need +to register it. + +.. code-block:: python + + from imagekit import ImageSpec + from imagekit.processors import ResizeToFill + + class Thumbnail(ImageSpec): + processors = [ResizeToFill(100, 50)] + format = 'JPEG' + options = {'quality': 60} + + register.generator('myapp:thumbnail', Thumbnail) + +.. note:: + + You can register your generator with any id you want, but choose wisely! + If you pick something too generic, you could have a conflict with another + third-party app you're using. For this reason, it's a good idea to prefix + your generator ids with the name of your app. Also, ImageKit recognizes + colons as separators when doing pattern matching (e.g. in the generateimages + management command), so it's a good idea to use those too! + +.. warning:: + + This code can go in any file you want—but you need to make sure it's loaded! + In order to keep things simple, ImageKit will automatically try to load an + module named "imagegenerators" in each of your installed apps. So why don't + you just save yourself the headache and put your image specs in there? + +Now that we've created an image generator class and registered it with ImageKit, +we can use it in our templates! + + +generateimage +""""""""""""" + +The most generic template tag that ImageKit gives you is called "generateimage". +It requires at least one argument: the id of a registered image generator. +Additional keyword-style arguments are passed to the registered generator class. +As we saw above, image spec constructors expect a source keyword argument, so +that's what we need to pass to use our thumbnail spec: .. code-block:: html {% load imagekit %} - {% spec 'myapp:fancy_thumbnail' source_image alt='A picture of me' %} + {% generateimage 'myapp:thumbnail' source=source_image %} -Output: +This will output the following HTML: .. code-block:: html - A picture of me + + +You can also add additional HTML attributes; just separate them from your +keyword args using two dashes: + +.. code-block:: html + + {% load imagekit %} + + {% generateimage 'myapp:thumbnail' source=source_image -- alt="A picture of Me" id="mypicture" %} Not generating HTML image tags? No problem. The tag also functions as an assignment tag, providing access to the underlying file object: @@ -104,72 +239,42 @@ assignment tag, providing access to the underlying file object: {% load imagekit %} - {% spec 'myapp:fancy_thumbnail' source_image as th %} + {% generateimage 'myapp:thumbnail' source=source_image as th %} Click to download a cool {{ th.width }} x {{ th.height }} image! -In Models -^^^^^^^^^ +thumbnail +""""""""" -Specs can also be used to add ``ImageField``-like fields that expose the result -of applying a spec to another one of your model's fields: +Because it's such a common use case, ImageKit also provides a "thumbnail" +template tag. -.. code-block:: python +.. code-block:: html - from django.db import models - from imagekit.models import ImageSpecField + {% load imagekit %} - class Photo(models.Model): - avatar = models.ImageField(upload_to='avatars') - avatar_thumbnail = ImageSpecField(id='myapp:fancy_thumbnail', source='avatar') + {% thumbnail '100x50' source_image %} - photo = Photo.objects.all()[0] - print photo.avatar_thumbnail.url # > /media/CACHE/ik/982d5af84cddddfd0fbf70892b4431e4.jpg - print photo.avatar_thumbnail.width # > 100 +.. note:: -Since defining a spec, registering it, and using it in a single model field is -such a common usage, ImakeKit provides a shortcut that allow you to skip -writing a subclass of ``ImageSpec``: + Comparing this syntax to the generateimage tag above, you'll notice a few + differences. -.. code-block:: python + First, we didn't have to specify an image generator id; unless we tell it + otherwise, thumbnail tag uses the generator registered with the id + "imagekit:thumbnail". (A custom id can be specified by passing an argument + before the dimensions.) **It's important to note that this tag is *not* + using the Thumbnail spec class we defined earlier**; it's using the + generator registered with the id "imagekit:thumbnail" which, by default, is + ``imagekit.generatorlibrary.Thumbnail``. - from django.db import models - from imagekit.models import ImageSpecField - from imagekit.processors import ResizeToFill + Second, we're passing two positional arguments (the dimensions and the + source image) as opposed to the keyword arguments we used with the + generateimage tag. Interally, however, the tag is parsing our positional + arguments and passing them as keyword arguments to our generator class. - class Photo(models.Model): - avatar = models.ImageField(upload_to='avatars') - avatar_thumbnail = ImageSpecField(processors=[ResizeToFill(100, 50)], - format='JPEG', - options={'quality': 60}, - source='avatar') - photo = Photo.objects.all()[0] - print photo.avatar_thumbnail.url # > /media/CACHE/ik/982d5af84cddddfd0fbf70892b4431e4.jpg - print photo.avatar_thumbnail.width # > 100 -This has the exact same behavior as before, but the spec definition is inlined. -Since no ``id`` is provided, one is automatically generated based on the app -name, model, and field. - -Specs can also be used in models to add ``ImageField``-like fields that process -a user-provided image without saving the original: - -.. code-block:: python - - from django.db import models - from imagekit.models import ProcessedImageField - - class Photo(models.Model): - avatar_thumbnail = ProcessedImageField(spec_id='myapp:fancy_thumbnail', - upload_to='avatars') - - photo = Photo.objects.all()[0] - print photo.avatar_thumbnail.url # > /media/avatars/MY-avatar_3.jpg - print photo.avatar_thumbnail.width # > 100 - -Like with ``ImageSpecField``, the ``ProcessedImageField`` constructor also -has a shortcut version that allows you to inline spec definitions. In Forms diff --git a/imagekit/generatedfiles/namers.py b/imagekit/generatedfiles/namers.py index 062fb7a..2a9f9b7 100644 --- a/imagekit/generatedfiles/namers.py +++ b/imagekit/generatedfiles/namers.py @@ -1,9 +1,29 @@ +""" +Functions responsible for returning filenames for the given image generator. +Users are free to define their own functions; these are just some some sensible +choices. + +""" + from django.conf import settings import os from ..utils import format_to_extension, suggest_extension def source_name_as_path(generator): + """ + A namer that, given the following source file name:: + + photos/thumbnails/bulldog.jpg + + will generate a name like this:: + + /path/to/generated/images/photos/thumbnails/bulldog/5ff3233527c5ac3e4b596343b440ff67.jpg + + where "/path/to/generated/images/" is the value specified by the + ``IMAGEKIT_GENERATEDFILE_DIR`` setting. + + """ source_filename = getattr(generator.source, 'name', None) if source_filename is None or os.path.isabs(source_filename): @@ -21,6 +41,19 @@ def source_name_as_path(generator): def source_name_dot_hash(generator): + """ + A namer that, given the following source file name:: + + photos/thumbnails/bulldog.jpg + + will generate a name like this:: + + /path/to/generated/images/photos/thumbnails/bulldog.5ff3233527c5.jpg + + where "/path/to/generated/images/" is the value specified by the + ``IMAGEKIT_GENERATEDFILE_DIR`` setting. + + """ source_filename = getattr(generator.source, 'name', None) if source_filename is None or os.path.isabs(source_filename): @@ -39,6 +72,19 @@ def source_name_dot_hash(generator): def hash(generator): + """ + A namer that, given the following source file name:: + + photos/thumbnails/bulldog.jpg + + will generate a name like this:: + + /path/to/generated/images/5ff3233527c5ac3e4b596343b440ff67.jpg + + where "/path/to/generated/images/" is the value specified by the + ``IMAGEKIT_GENERATEDFILE_DIR`` setting. + + """ format = getattr(generator, 'format', None) ext = format_to_extension(format) if format else '' return os.path.normpath(os.path.join(settings.IMAGEKIT_GENERATEDFILE_DIR, From c9205e588ea758400aa49ac6cbecfab54ae7102a Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Fri, 1 Feb 2013 23:27:36 -0500 Subject: [PATCH 193/213] More docs --- README.rst | 65 +++++++++++++++++++++++++++++++----------------------- 1 file changed, 37 insertions(+), 28 deletions(-) diff --git a/README.rst b/README.rst index 3e6e97b..4c902f0 100644 --- a/README.rst +++ b/README.rst @@ -247,7 +247,7 @@ thumbnail """"""""" Because it's such a common use case, ImageKit also provides a "thumbnail" -template tag. +template tag: .. code-block:: html @@ -255,30 +255,38 @@ template tag. {% thumbnail '100x50' source_image %} -.. note:: +Like the generateimage tag, the thumbnail tag outputs an tag: - Comparing this syntax to the generateimage tag above, you'll notice a few - differences. +.. code-block:: html - First, we didn't have to specify an image generator id; unless we tell it - otherwise, thumbnail tag uses the generator registered with the id - "imagekit:thumbnail". (A custom id can be specified by passing an argument - before the dimensions.) **It's important to note that this tag is *not* - using the Thumbnail spec class we defined earlier**; it's using the - generator registered with the id "imagekit:thumbnail" which, by default, is - ``imagekit.generatorlibrary.Thumbnail``. + - Second, we're passing two positional arguments (the dimensions and the - source image) as opposed to the keyword arguments we used with the - generateimage tag. Interally, however, the tag is parsing our positional - arguments and passing them as keyword arguments to our generator class. +Comparing this syntax to the generateimage tag above, you'll notice a few +differences. + +First, we didn't have to specify an image generator id; unless we tell it +otherwise, thumbnail tag uses the generator registered with the id +"imagekit:thumbnail". **It's important to note that this tag is *not* using the +Thumbnail spec class we defined earlier**; it's using the generator registered +with the id "imagekit:thumbnail" which, by default, is +``imagekit.generatorlibrary.Thumbnail``. + +Second, we're passing two positional arguments (the dimensions and the source +image) as opposed to the keyword arguments we used with the generateimage tag. + +Like with the generatethumbnail tag, you can also specify additional HTML +attributes for the thumbnail tag, or use it as an assignment tag: + +.. code-block:: html + + {% load imagekit %} + + {% thumbnail '100x50' source_image -- alt="A picture of Me" id="mypicture" %} + {% thumbnail '100x50' source_image as th %} - - - -In Forms -^^^^^^^^ +Using Specs in Forms +^^^^^^^^^^^^^^^^^^^^ In addition to the model field above, there's also a form field version of the ``ProcessedImageField`` class. The functionality is basically the same (it @@ -288,18 +296,19 @@ processes an image once and saves the result), but it's used in a form class: from django import forms from imagekit.forms import ProcessedImageField + from imagekit.processors import ResizeToFill - class AvatarForm(forms.Form): - avatar_thumbnail = ProcessedImageField(spec_id='myapp:fancy_thumbnail') + class ProfileForm(forms.Form): + avatar_thumbnail = ProcessedImageField(spec_id='myapp:profile:avatar_thumbnail', + processors=[ResizeToFill(100, 50)], + format='JPEG', + options={'quality': 60}) The benefit of using ``imagekit.forms.ProcessedImageField`` (as opposed to ``imagekit.models.ProcessedImageField`` above) is that it keeps the logic for -creating the image outside of your model (in which you would use a normal -Django ``ImageField``). You can even create multiple forms, each with their own -``ProcessedImageField``, that all store their results in the same image field. - -As with the model field classes, ``imagekit.forms.ProcessedImageField`` also -has a shortcut version that allows you to inline spec definitions. +creating the image outside of your model (in which you would use a normal Django +ImageField). You can even create multiple forms, each with their own +ProcessedImageField, that all store their results in the same image field. Processors From 218f5690056c4c5b3d04d02988b1e30a02abef0b Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Sat, 2 Feb 2013 19:21:32 -0500 Subject: [PATCH 194/213] Don't assign processors, so properties will work This way, a subclass can add a @property without a setter and not worry about an error. --- imagekit/specs/__init__.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/imagekit/specs/__init__.py b/imagekit/specs/__init__.py index cf37b6f..c286a44 100644 --- a/imagekit/specs/__init__.py +++ b/imagekit/specs/__init__.py @@ -49,7 +49,7 @@ class ImageSpec(BaseImageSpec): """ - processors = None + processors = [] """A list of processors to run on the original image.""" format = None @@ -78,7 +78,6 @@ class ImageSpec(BaseImageSpec): def __init__(self, source): self.source = source - self.processors = self.processors or [] super(ImageSpec, self).__init__() @property From 36c075741716dab043c1b0eb9d8fb06674b18347 Mon Sep 17 00:00:00 2001 From: Eric Eldredge Date: Sat, 2 Feb 2013 20:47:05 -0500 Subject: [PATCH 195/213] Some upgrading docs --- docs/upgrading.rst | 113 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100644 docs/upgrading.rst diff --git a/docs/upgrading.rst b/docs/upgrading.rst new file mode 100644 index 0000000..a6189af --- /dev/null +++ b/docs/upgrading.rst @@ -0,0 +1,113 @@ +Upgrading from 2.x +================== + +ImageKit 3.0 introduces new APIs and tools that augment, improve, and in some +cases entirely replace old IK workflows. Below, you'll find some useful guides +for migrating your ImageKit 2.0 apps over to the shiny new IK3. + +Model Specs +----------- + +IK3 is chock full of new features and better tools for even the most +sophisticated use cases. Despite this, not too much has changed when it +comes to the most common of use cases: processing an ``ImageField`` on a model. + +In IK2, you may have used an ``ImageSpecField`` on a model to process an +existing ``ImageField``: + +.. code-block:: python + + class Profile(models.Model): + avatar = models.ImageField(upload_to='avatars') + avatar_thumbnail = ImageSpecField(image_field='avatar', + processors=[ResizeToFill(100, 50)], + format='JPEG', + options={'quality': 60}) + +In IK3, things look much the same: + +.. code-block:: python + + class Profile(models.Model): + avatar = models.ImageField(upload_to='avatars') + avatar_thumbnail = ImageSpecField(source='avatar', + processors=[ResizeToFill(100, 50)], + format='JPEG', + options={'quality': 60}) + +The major difference is that ``ImageSpecField`` no longer takes an +``image_field`` kwarg. Instead, you define a ``source``. + + +Image Cache Backends +-------------------- + +In IK2, you could gain some control over how your cached images were generated +by providing an ``image_cache_backend``: + +.. code-block:: python + + class Photo(models.Model): + ... + thumbnail = ImageSpecField(..., image_cache_backend=MyImageCacheBackend()) + +This gave you great control over *how* your images are generated and stored, +but it could be difficult to control *when* they were generated and stored. + +IK3 retains the image cache backend concept (now called generated file backends), +but separates the 'when' control out to generated file strategies: + +.. code-block:: python + + class Photo(models.Model): + ... + thumbnail = ImageSpecField(..., + generatedfile_backend=MyGeneratedFileBackend(), + generatedfile_strategy=MyGeneratedFileStrategy()) + +If you are using the IK2 default image cache backend setting: + +.. code-block:: python + + IMAGEKIT_DEFAULT_IMAGE_CACHE_BACKEND = 'path.to.MyImageCacheBackend' + +IK3 provides analogous settings for generated file backends and strategies: + +.. code-block:: python + + IMAGEKIT_DEFAULT_GENERATEDFILE_BACKEND = 'path.to.MyGeneratedFileBackend' + IMAGEKIT_DEFAULT_GENERATEDFILE_STRATEGY = 'path.to.MyGeneratedFileStrategy' + +See the documentation on `generated file backends`_ and `generated file strategies`_ +for more details. + +.. _`generated file backends`: +.. _`generated file strategies`: + + +Conditional model ``processors`` +-------------------------------- + +In IK2, an ``ImageSpecField`` could take a ``processors`` callable instead of +an iterable, which allowed processing decisions to made based on other +properties of the model. IK3 does away with this feature for consistency's sake +(if one kwarg could be callable, why not all?), but provides a much more robust +solution: the custom ``spec``. See the `advanced usage`_ documentation for more. + +.. _`advanced usage`: + + +Conditonal ``cache_to`` file names +---------------------------------- + +IK2 provided a means of specifying custom generated file names for your +image specs by passing a ``cache_to`` callable to an ``ImageSpecField``. +IK3 does away with this feature, again, for consistency. + +There is a way to achieve custom file names by overriding your spec's +``generatedfile_name``, but it is not recommended, as the spec's default +behavior is to hash the combination of ``source``, ``processors``, ``format``, +and other spec options to ensure that changes to the spec always result in +unique file names. See the documentation on `specs`_ for more. + +.. _`specs`: From 1a7c0627df062e597ed915d672ec397246b56b0c Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Sat, 2 Feb 2013 21:48:41 -0500 Subject: [PATCH 196/213] Starting advanced usage docs --- docs/advanced_usage/models.rst | 99 ++++++++++++++++++++++++++++++ docs/advanced_usage/optimizing.rst | 48 +++++++++++++++ 2 files changed, 147 insertions(+) create mode 100644 docs/advanced_usage/models.rst create mode 100644 docs/advanced_usage/optimizing.rst diff --git a/docs/advanced_usage/models.rst b/docs/advanced_usage/models.rst new file mode 100644 index 0000000..2ccd70d --- /dev/null +++ b/docs/advanced_usage/models.rst @@ -0,0 +1,99 @@ +The ``ImageSpecField`` Shorthand Syntax +--------------------------------------- + +If you've read the README, you already know what an ``ImageSpecField`` is and +the basics of defining one: + +.. code-block:: python + + from django.db import models + from imagekit.models import ImageSpecField + from imagekit.processors import ResizeToFill + + class Profile(models.Model): + avatar = models.ImageField(upload_to='avatars') + avatar_thumbnail = ImageSpecField(source='avatar', + processors=[ResizeToFill(100, 50)], + format='JPEG', + options={'quality': 60}) + +This will create an ``avatar_thumbnail`` field which is a resized version of the +image stored in the ``avatar`` image field. But this is actually just shorthand +for creating an ``ImageSpec``, registering it, and associating it with an +``ImageSpecField``: + +.. code-block:: python + + from django.db import models + from imagekit import ImageSpec, register + from imagekit.models import ImageSpecField + from imagekit.processors import ResizeToFill + + class AvatarThumbnail(ImageSpec): + processors = [ResizeToFill(100, 50)] + format = 'JPEG' + options = {'quality': 60} + + register.generator('myapp:profile:avatar_thumbnail', AvatarThumbnail) + + class Profile(models.Model): + avatar = models.ImageField(upload_to='avatars') + avatar_thumbnail = ImageSpecField(source='avatar', + spec_id='myapp:profile:avatar_thumbnail') + +Obviously, the shorthand version is a lot, well…shorter. So why would you ever +want to go through the trouble of using the long form? The answer is that the +long form—creating an image spec class and registering it—gives you a lot more +power over the generated image. + + +Specs That Change +----------------- + +As you'll remember from the README, an image spec is just a type of image +generator that generates a new image from a source image. How does the image +spec get access to the source image? Simple! It's passed to the constructor as +a keyword argument and stored as an attribute of the spec. Normally, we don't +have to concern ourselves with this; the ``ImageSpec`` knows what to do with the +source image and we're happy to let it do its thing. However, having access to +the source image in our spec class can be very useful… + +Often, when using an ``ImageSpecField``, you may want the spec to vary based on +properties of a model. (For example, you might want to store image dimensions on +the model and then use them to generate your thumbnail.) Now that we know how to +access the source image from our spec, it's a simple matter to extract its model +and use it to create our processors list. In fact, ImageKit includes a utility +for getting this information. + +.. code-block:: python + :emphasize-lines: 11-14 + + from django.db import models + from imagekit import ImageSpec, register + from imagekit.models import ImageSpecField + from imagekit.processors import ResizeToFill + from imagekit.utils import get_field_info + + class AvatarThumbnail(ImageSpec): + format = 'JPEG' + options = {'quality': 60} + + @property + def processors(self): + model, field_name = get_field_info(self.source) + return [ResizeToFill(model.thumbnail_width, thumbnail.avatar_height)] + + register.generator('myapp:profile:avatar_thumbnail', AvatarThumbnail) + + class Profile(models.Model): + avatar = models.ImageField(upload_to='avatars') + avatar_thumbnail = ImageSpecField(source='avatar', + spec_id='myapp:profile:avatar_thumbnail') + thumbnail_width = models.PositiveIntegerField() + thumbnail_height = models.PositiveIntegerField() + +Now each avatar thumbnail will be resized according to the dimensions stored on +the model! + +Of course, processors aren't the only thing that can vary based on the model of +the source image; spec behavior can change in any way you want. diff --git a/docs/advanced_usage/optimizing.rst b/docs/advanced_usage/optimizing.rst new file mode 100644 index 0000000..ca5ef85 --- /dev/null +++ b/docs/advanced_usage/optimizing.rst @@ -0,0 +1,48 @@ +Unlike Django's ImageFields, ImageKit's ImageSpecFields and template tags don't +persist any data in the database. Therefore, in order to know whether an image +file needs to be generated, ImageKit needs to check if the file already exists +(using the appropriate file storage object`__). The object responsible for +performing these checks is called a *generated file backend*. + + +Cache! +------ + +By default, ImageKit checks for the existence of a generated file every time you +attempt to use the file and, if it doesn't exist, creates it synchronously. This +is a very safe behavior because it ensures that your ImageKit-generated images +are always available. However, that's a lot of checking with storage and those +kinds of operations can be slow—especially if you're using a remote storage—so +you'll want to try to avoid them as much as possible. + +Luckily, the default generated file backend makes use of Django's caching +abilities to mitigate the number of checks it actually has to do; it will use +the cache specified by the ``IMAGEKIT_CACHE_BACKEND`` to save the state of the +generated file. If your Django project is running in debug mode +(``settings.DEBUG`` is true), this will be a dummy cache by default. Otherwise, +it will use your project's default cache. + +In normal operation, your generated files will never be deleted; once they're +created, they'll stay created. So the simplest optimization you can make is to +set your ``IMAGEKIT_CACHE_BACKEND`` to a cache with a very long, or infinite, +timeout. + + +Even More Advanced +------------------ + +For many applications—particularly those using local storage for generated image +files—a cache with a long timeout is all the optimization you'll need. However, +there may be times when that simply doesn't cut it. In these cases, you'll want +to change when the generation is actually done. + +The objects responsible for specifying when generated files are created are +called *generated file strategies*. The default strategy can be set using the +``IMAGEKIT_DEFAULT_GENERATEDFILE_STRATEGY`` setting, and its default value is +`'imagekit.generatedfiles.strategies.JustInTime'`. As we've already seen above, +the "just in time" strategy determines whether a file needs to be generated each +time it's accessed and, if it does, generates it synchronously. + + + +__ https://docs.djangoproject.com/en/dev/ref/files/storage/ From d22c49a4652929ff1c9de55f828c3ba1dc52ba90 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Sat, 2 Feb 2013 22:01:30 -0500 Subject: [PATCH 197/213] Don't delete the file when source is deleted We can't be sure another spec isn't using this file. --- imagekit/generatedfiles/strategies.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/imagekit/generatedfiles/strategies.py b/imagekit/generatedfiles/strategies.py index 72bc1df..7e19692 100644 --- a/imagekit/generatedfiles/strategies.py +++ b/imagekit/generatedfiles/strategies.py @@ -23,9 +23,6 @@ class Optimistic(object): def on_source_created(self, file): file.generate() - def on_source_deleted(self, file): - file.delete() - def on_source_changed(self, file): file.generate() From 5f8f651def4f0792e416eb6e1810e4da34da206c Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Sat, 2 Feb 2013 23:35:32 -0500 Subject: [PATCH 198/213] More advanced docs! --- docs/advanced_usage/optimizing.rst | 52 ++++++++++++++++++++++++++- docs/advanced_usage/source_groups.rst | 24 +++++++++++++ 2 files changed, 75 insertions(+), 1 deletion(-) create mode 100644 docs/advanced_usage/source_groups.rst diff --git a/docs/advanced_usage/optimizing.rst b/docs/advanced_usage/optimizing.rst index ca5ef85..d1f847b 100644 --- a/docs/advanced_usage/optimizing.rst +++ b/docs/advanced_usage/optimizing.rst @@ -41,8 +41,58 @@ called *generated file strategies*. The default strategy can be set using the ``IMAGEKIT_DEFAULT_GENERATEDFILE_STRATEGY`` setting, and its default value is `'imagekit.generatedfiles.strategies.JustInTime'`. As we've already seen above, the "just in time" strategy determines whether a file needs to be generated each -time it's accessed and, if it does, generates it synchronously. +time it's accessed and, if it does, generates it synchronously (that is, as part +of the request-response cycle). + +Another strategy is to simply assume the file exists. This requires the fewest +number of checks (zero!), so we don't have to worry about expensive IO. The +strategy that takes this approach is +``imagekit.generatedfiles.strategies.Optimistic``. In order to use this +strategy, either set the ``IMAGEKIT_DEFAULT_GENERATEDFILE_STRATEGY`` setting or, +to use it on a per-generator basis, set the ``generatedfile_strategy`` attribute +of your spec or generator. Avoiding checking for file existence can be a real +boon to performance, but it also means that ImageKit has no way to know when a +file needs to be generated—well, at least not all the time. + +With image specs, we can know at least some of the times that a new file needs +to be generated: whenever the source image is created or changed. For this +reason, the optimistic strategy defines callbacks for these events. Every +`source registered with ImageKit`__ will automatically cause its specs' files to +be generated when it is created or changed. + +.. note:: + + In order to understand source registration, read :ref:`source-groups` + +If you have specs that `change based on attributes of the source`__, that's not +going to cut it, though; the file will also need to be generated when those +attributes change. Likewise, image generators that don't have sources (i.e. +generators that aren't specs) won't cause files to be generated automatically +when using the optimistic strategy. (ImageKit can't know when those need to be +generated, if not on access.) In both cases, you'll have to trigger the file +generation yourself—either by generating the file in code when necessary, or by +periodically running the ``generateimages`` management command. Luckily, +ImageKit makes this pretty easy: + +.. code-block:: python + + from imagekit.generatedfiles import LazyGeneratedImageFile + + file = LazyGeneratedImageFile('myapp:profile:avatar_thumbnail', source=source_file) + file.generate() + +One final situation in which images won't be generated automatically when using +the optimistic strategy is when you use a spec with a source that hasn't been +registered with it. Unlike the previous two examples, this situation cannot be +rectified by running the ``generateimages`` management command, for the simple +reason that the command has no way of knowing it needs to generate a file for +that spec from that source. Typically, this situation would arise when using the +template tags. Unlike ImageSpecFields, which automatically register all the +possible source images with the spec you define, the template tags +("generateimage" and "thumbnail") let you use any spec with any source. __ https://docs.djangoproject.com/en/dev/ref/files/storage/ +__ +__ diff --git a/docs/advanced_usage/source_groups.rst b/docs/advanced_usage/source_groups.rst new file mode 100644 index 0000000..36fff33 --- /dev/null +++ b/docs/advanced_usage/source_groups.rst @@ -0,0 +1,24 @@ +.. _source-groups: + +ImageKit allows you to register objects—called *source groups*—which do two +things: 1) dispatch signals when a source is created, changed, or deleted, and +2) expose a generator method that enumerates source files. When these objects +are registered (using ``imagekit.register.source_group()``), their signals will +trigger callbacks on the generated file strategies associated with image specs +that use the source. In addition, the generator method is used (indirectly) to +create the list of files to generate with the ``generateimages`` management +command. + +Currently, there is only one source group class bundled with ImageKit, +``imagekit.specs.sourcegroups.ImageFieldSourceGroup``, which represents an +ImageField on every instance of a particular model. In terms of the above +description, ``ImageFieldSourceGroup(Profile, 'avatar')`` 1) dispatches a signal +every time the image in Profile's avatar ImageField changes, and 2) exposes a +generator method that iterates over every Profile's "avatar" image. + +ImageKit automatically creates and registers an instance of +ImageFieldSourceGroup every time you create an ImageSpecField; that's how the +field is connected (internally) to the spec you're defining, and therefore to +the generated file strategy responsible for generating the file. It's also how +the ``generateimages`` management command is able to know which sources to +generate files for. From 59971b6cd44e5f9924f7714649d075d6d43fdb68 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Sun, 3 Feb 2013 00:11:22 -0500 Subject: [PATCH 199/213] More docs --- docs/advanced_usage/optimizing.rst | 5 +- docs/advanced_usage/source_groups.rst | 81 +++++++++++++++++++++------ 2 files changed, 67 insertions(+), 19 deletions(-) diff --git a/docs/advanced_usage/optimizing.rst b/docs/advanced_usage/optimizing.rst index d1f847b..f567567 100644 --- a/docs/advanced_usage/optimizing.rst +++ b/docs/advanced_usage/optimizing.rst @@ -90,7 +90,10 @@ that spec from that source. Typically, this situation would arise when using the template tags. Unlike ImageSpecFields, which automatically register all the possible source images with the spec you define, the template tags ("generateimage" and "thumbnail") let you use any spec with any source. - +Therefore, in order to generate the appropriate files using the +``generateimages`` management command, you'll need to first register a source +group that represents all of the sources you wish to use with the corresponding +specs. See :ref:`source-groups` for more information. __ https://docs.djangoproject.com/en/dev/ref/files/storage/ diff --git a/docs/advanced_usage/source_groups.rst b/docs/advanced_usage/source_groups.rst index 36fff33..c3a2d1c 100644 --- a/docs/advanced_usage/source_groups.rst +++ b/docs/advanced_usage/source_groups.rst @@ -1,24 +1,69 @@ .. _source-groups: -ImageKit allows you to register objects—called *source groups*—which do two -things: 1) dispatch signals when a source is created, changed, or deleted, and -2) expose a generator method that enumerates source files. When these objects -are registered (using ``imagekit.register.source_group()``), their signals will -trigger callbacks on the generated file strategies associated with image specs -that use the source. In addition, the generator method is used (indirectly) to -create the list of files to generate with the ``generateimages`` management -command. +When you run the ``generateimages`` management command, how does ImageKit know +which source images to use with which specs? Obviously, when you define an +ImageSpecField, the source image is being connected to a spec, but what's going +on underneath the hood? -Currently, there is only one source group class bundled with ImageKit, -``imagekit.specs.sourcegroups.ImageFieldSourceGroup``, which represents an -ImageField on every instance of a particular model. In terms of the above -description, ``ImageFieldSourceGroup(Profile, 'avatar')`` 1) dispatches a signal +The answer is that, when you define an ImageSpecField, ImageKit automatically +creates and registers an object called a *source group*. Source groups are +responsible for two things: + +1. They dispatch signals when a source is created, changed, or deleted, and +2. They expose a generator method that enumerates source files. + +When these objects are registered (using ``imagekit.register.source_group()``), +their signals will trigger callbacks on the generated file strategies associated +with image specs that use the source. (So, for example, you can chose to +generate a file every time the source image changes.) In addition, the generator +method is used (indirectly) to create the list of files to generate with the +``generateimages`` management command. + +Currently, there is only one source group class bundled with ImageKit—the one +used by ImageSpecFields. This source group +(``imagekit.specs.sourcegroups.ImageFieldSourceGroup``) represents an ImageField +on every instance of a particular model. In terms of the above description, the +instance ``ImageFieldSourceGroup(Profile, 'avatar')`` 1) dispatches a signal every time the image in Profile's avatar ImageField changes, and 2) exposes a generator method that iterates over every Profile's "avatar" image. -ImageKit automatically creates and registers an instance of -ImageFieldSourceGroup every time you create an ImageSpecField; that's how the -field is connected (internally) to the spec you're defining, and therefore to -the generated file strategy responsible for generating the file. It's also how -the ``generateimages`` management command is able to know which sources to -generate files for. +Chances are, this is the only source group you will ever need to use, however, +ImageKit lets you define and register custom source groups easily. This may be +useful, for example, if you're using the template tags "generateimage" and +"thumbnail" and the optimistic generated file strategy. Again, the purpose is +to tell ImageKit which specs are used with which sources (so the +"generateimages" management command can generate those files) and when the +source image has been created or changed (so that the strategy has the +opportunity to act on it). + +A simple example of a custom source group class is as follows: + +.. code-block:: python + + import glob + import os + + class JpegsInADirectory(object): + def __init__(self, dir): + self.dir = dir + + def files(self): + os.chdir(self.dir) + for name in glob.glob('*.jpg'): + yield open(name) + +Instances of this class could then be registered with one or more spec id: + +.. code-block:: python + + from imagekit import register + + register.source_group('myapp:profile:avatar_thumbnail', JpegsInADirectory('/path/to/some/pics')) + +Running the "generateimages" management command would now cause thumbnails to be +generated (using the "myapp:profile:avatar_thumbnail" spec) for each of the +JPEGs in `/path/to/some/pics`. + +Note that, since this source group doesnt send the `source_created` or +`source_changed` signals, the corresponding generated file strategy callbacks +would not be called for them. From 301adc208707cf7f1735f09e3d33bfc2d50dd1ae Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Mon, 4 Feb 2013 19:39:25 -0500 Subject: [PATCH 200/213] Let's call em cachefiles Changed my mind about 04aa72c1f9f8f7e84a0e5a6ef15fb7f74d81ecef. It's just a better description, even if different strategies can change the behavior so it isn't really very cache-like. --- README.rst | 6 ++-- docs/advanced_usage/optimizing.rst | 24 ++++++------- docs/advanced_usage/source_groups.rst | 6 ++-- docs/upgrading.rst | 24 ++++++------- .../__init__.py | 20 +++++------ .../{generatedfiles => cachefiles}/actions.py | 0 .../backends.py | 4 +-- .../{generatedfiles => cachefiles}/namers.py | 20 +++++------ .../strategies.py | 6 ++-- imagekit/conf.py | 10 +++--- .../management/commands/generateimages.py | 4 +-- imagekit/models/fields/__init__.py | 10 +++--- imagekit/models/fields/utils.py | 2 +- imagekit/registry.py | 36 +++++++++---------- imagekit/specs/__init__.py | 26 +++++++------- imagekit/specs/sourcegroups.py | 6 ++-- imagekit/templatetags/imagekit.py | 8 ++--- imagekit/utils.py | 2 +- 18 files changed, 107 insertions(+), 107 deletions(-) rename imagekit/{generatedfiles => cachefiles}/__init__.py (80%) rename imagekit/{generatedfiles => cachefiles}/actions.py (100%) rename imagekit/{generatedfiles => cachefiles}/backends.py (93%) rename imagekit/{generatedfiles => cachefiles}/namers.py (81%) rename imagekit/{generatedfiles => cachefiles}/strategies.py (86%) diff --git a/README.rst b/README.rst index 4c902f0..d31a1c6 100644 --- a/README.rst +++ b/README.rst @@ -62,7 +62,7 @@ your model class: options={'quality': 60}) profile = Profile.objects.all()[0] - print profile.avatar_thumbnail.url # > /media/generated/images/982d5af84cddddfd0fbf70892b4431e4.jpg + print profile.avatar_thumbnail.url # > /media/CACHE/images/982d5af84cddddfd0fbf70892b4431e4.jpg print profile.avatar_thumbnail.width # > 100 As you can probably tell, ImageSpecFields work a lot like Django's @@ -221,7 +221,7 @@ This will output the following HTML: .. code-block:: html - + You can also add additional HTML attributes; just separate them from your keyword args using two dashes: @@ -259,7 +259,7 @@ Like the generateimage tag, the thumbnail tag outputs an tag: .. code-block:: html - + Comparing this syntax to the generateimage tag above, you'll notice a few differences. diff --git a/docs/advanced_usage/optimizing.rst b/docs/advanced_usage/optimizing.rst index f567567..b28c583 100644 --- a/docs/advanced_usage/optimizing.rst +++ b/docs/advanced_usage/optimizing.rst @@ -2,27 +2,27 @@ Unlike Django's ImageFields, ImageKit's ImageSpecFields and template tags don't persist any data in the database. Therefore, in order to know whether an image file needs to be generated, ImageKit needs to check if the file already exists (using the appropriate file storage object`__). The object responsible for -performing these checks is called a *generated file backend*. +performing these checks is called a *cache file backend*. Cache! ------ -By default, ImageKit checks for the existence of a generated file every time you +By default, ImageKit checks for the existence of a cache file every time you attempt to use the file and, if it doesn't exist, creates it synchronously. This is a very safe behavior because it ensures that your ImageKit-generated images are always available. However, that's a lot of checking with storage and those kinds of operations can be slow—especially if you're using a remote storage—so you'll want to try to avoid them as much as possible. -Luckily, the default generated file backend makes use of Django's caching +Luckily, the default cache file backend makes use of Django's caching abilities to mitigate the number of checks it actually has to do; it will use the cache specified by the ``IMAGEKIT_CACHE_BACKEND`` to save the state of the generated file. If your Django project is running in debug mode (``settings.DEBUG`` is true), this will be a dummy cache by default. Otherwise, it will use your project's default cache. -In normal operation, your generated files will never be deleted; once they're +In normal operation, your cache files will never be deleted; once they're created, they'll stay created. So the simplest optimization you can make is to set your ``IMAGEKIT_CACHE_BACKEND`` to a cache with a very long, or infinite, timeout. @@ -36,10 +36,10 @@ files—a cache with a long timeout is all the optimization you'll need. However there may be times when that simply doesn't cut it. In these cases, you'll want to change when the generation is actually done. -The objects responsible for specifying when generated files are created are -called *generated file strategies*. The default strategy can be set using the -``IMAGEKIT_DEFAULT_GENERATEDFILE_STRATEGY`` setting, and its default value is -`'imagekit.generatedfiles.strategies.JustInTime'`. As we've already seen above, +The objects responsible for specifying when cache files are created are +called *cache file strategies*. The default strategy can be set using the +``IMAGEKIT_DEFAULT_CACHEFILE_STRATEGY`` setting, and its default value is +`'imagekit.cachefiles.strategies.JustInTime'`. As we've already seen above, the "just in time" strategy determines whether a file needs to be generated each time it's accessed and, if it does, generates it synchronously (that is, as part of the request-response cycle). @@ -47,9 +47,9 @@ of the request-response cycle). Another strategy is to simply assume the file exists. This requires the fewest number of checks (zero!), so we don't have to worry about expensive IO. The strategy that takes this approach is -``imagekit.generatedfiles.strategies.Optimistic``. In order to use this -strategy, either set the ``IMAGEKIT_DEFAULT_GENERATEDFILE_STRATEGY`` setting or, -to use it on a per-generator basis, set the ``generatedfile_strategy`` attribute +``imagekit.cachefiles.strategies.Optimistic``. In order to use this +strategy, either set the ``IMAGEKIT_DEFAULT_CACHEFILE_STRATEGY`` setting or, +to use it on a per-generator basis, set the ``cachefile_strategy`` attribute of your spec or generator. Avoiding checking for file existence can be a real boon to performance, but it also means that ImageKit has no way to know when a file needs to be generated—well, at least not all the time. @@ -76,7 +76,7 @@ ImageKit makes this pretty easy: .. code-block:: python - from imagekit.generatedfiles import LazyGeneratedImageFile + from imagekit.cachefiles import LazyGeneratedImageFile file = LazyGeneratedImageFile('myapp:profile:avatar_thumbnail', source=source_file) file.generate() diff --git a/docs/advanced_usage/source_groups.rst b/docs/advanced_usage/source_groups.rst index c3a2d1c..d3ba0e5 100644 --- a/docs/advanced_usage/source_groups.rst +++ b/docs/advanced_usage/source_groups.rst @@ -13,7 +13,7 @@ responsible for two things: 2. They expose a generator method that enumerates source files. When these objects are registered (using ``imagekit.register.source_group()``), -their signals will trigger callbacks on the generated file strategies associated +their signals will trigger callbacks on the cache file strategies associated with image specs that use the source. (So, for example, you can chose to generate a file every time the source image changes.) In addition, the generator method is used (indirectly) to create the list of files to generate with the @@ -30,7 +30,7 @@ generator method that iterates over every Profile's "avatar" image. Chances are, this is the only source group you will ever need to use, however, ImageKit lets you define and register custom source groups easily. This may be useful, for example, if you're using the template tags "generateimage" and -"thumbnail" and the optimistic generated file strategy. Again, the purpose is +"thumbnail" and the optimistic cache file strategy. Again, the purpose is to tell ImageKit which specs are used with which sources (so the "generateimages" management command can generate those files) and when the source image has been created or changed (so that the strategy has the @@ -65,5 +65,5 @@ generated (using the "myapp:profile:avatar_thumbnail" spec) for each of the JPEGs in `/path/to/some/pics`. Note that, since this source group doesnt send the `source_created` or -`source_changed` signals, the corresponding generated file strategy callbacks +`source_changed` signals, the corresponding cache file strategy callbacks would not be called for them. diff --git a/docs/upgrading.rst b/docs/upgrading.rst index a6189af..7a9b438 100644 --- a/docs/upgrading.rst +++ b/docs/upgrading.rst @@ -54,16 +54,16 @@ by providing an ``image_cache_backend``: This gave you great control over *how* your images are generated and stored, but it could be difficult to control *when* they were generated and stored. -IK3 retains the image cache backend concept (now called generated file backends), -but separates the 'when' control out to generated file strategies: +IK3 retains the image cache backend concept (now called cache file backends), +but separates the 'when' control out to cache file strategies: .. code-block:: python class Photo(models.Model): ... thumbnail = ImageSpecField(..., - generatedfile_backend=MyGeneratedFileBackend(), - generatedfile_strategy=MyGeneratedFileStrategy()) + cachefile_backend=MyCacheFileBackend(), + cachefile_strategy=MyCacheFileStrategy()) If you are using the IK2 default image cache backend setting: @@ -71,18 +71,18 @@ If you are using the IK2 default image cache backend setting: IMAGEKIT_DEFAULT_IMAGE_CACHE_BACKEND = 'path.to.MyImageCacheBackend' -IK3 provides analogous settings for generated file backends and strategies: +IK3 provides analogous settings for cache file backends and strategies: .. code-block:: python - IMAGEKIT_DEFAULT_GENERATEDFILE_BACKEND = 'path.to.MyGeneratedFileBackend' - IMAGEKIT_DEFAULT_GENERATEDFILE_STRATEGY = 'path.to.MyGeneratedFileStrategy' + IMAGEKIT_DEFAULT_CACHEFILE_BACKEND = 'path.to.MyCacheFileBackend' + IMAGEKIT_DEFAULT_CACHEFILE_STRATEGY = 'path.to.MyCacheFileStrategy' -See the documentation on `generated file backends`_ and `generated file strategies`_ +See the documentation on `cache file backends`_ and `cache file strategies`_ for more details. -.. _`generated file backends`: -.. _`generated file strategies`: +.. _`cache file backends`: +.. _`cache file strategies`: Conditional model ``processors`` @@ -100,12 +100,12 @@ solution: the custom ``spec``. See the `advanced usage`_ documentation for more. Conditonal ``cache_to`` file names ---------------------------------- -IK2 provided a means of specifying custom generated file names for your +IK2 provided a means of specifying custom cache file names for your image specs by passing a ``cache_to`` callable to an ``ImageSpecField``. IK3 does away with this feature, again, for consistency. There is a way to achieve custom file names by overriding your spec's -``generatedfile_name``, but it is not recommended, as the spec's default +``cachefile_name``, but it is not recommended, as the spec's default behavior is to hash the combination of ``source``, ``processors``, ``format``, and other spec options to ensure that changes to the spec always result in unique file names. See the documentation on `specs`_ for more. diff --git a/imagekit/generatedfiles/__init__.py b/imagekit/cachefiles/__init__.py similarity index 80% rename from imagekit/generatedfiles/__init__.py rename to imagekit/cachefiles/__init__.py index 5bc93f3..25c822f 100644 --- a/imagekit/generatedfiles/__init__.py +++ b/imagekit/cachefiles/__init__.py @@ -12,32 +12,32 @@ class GeneratedImageFile(BaseIKFile, ImageFile): A file that represents the result of a generator. Creating an instance of this class is not enough to trigger the generation of the file. In fact, one of the main points of this class is to allow the creation of the file - to be deferred until the time that the generated file strategy requires it. + to be deferred until the time that the cache file strategy requires it. """ - def __init__(self, generator, name=None, storage=None, generatedfile_backend=None): + def __init__(self, generator, name=None, storage=None, cachefile_backend=None): """ :param generator: The object responsible for generating a new image. :param name: The filename :param storage: A Django storage object that will be used to save the file. - :param generatedfile_backend: The object responsible for managing the + :param cachefile_backend: The object responsible for managing the state of the file. """ self.generator = generator - name = name or getattr(generator, 'generatedfile_name', None) + name = name or getattr(generator, 'cachefile_name', None) if not name: - fn = get_by_qname(settings.IMAGEKIT_GENERATEDFILE_NAMER, 'namer') + fn = get_by_qname(settings.IMAGEKIT_CACHEFILE_NAMER, 'namer') name = fn(generator) self.name = name - storage = storage or getattr(generator, 'generatedfile_storage', + storage = storage or getattr(generator, 'cachefile_storage', None) or get_singleton(settings.IMAGEKIT_DEFAULT_FILE_STORAGE, 'file storage backend') - self.generatedfile_backend = generatedfile_backend or getattr(generator, - 'generatedfile_backend', None) + self.cachefile_backend = cachefile_backend or getattr(generator, + 'cachefile_backend', None) super(GeneratedImageFile, self).__init__(storage=storage) @@ -49,7 +49,7 @@ class GeneratedImageFile(BaseIKFile, ImageFile): if force: self._generate() else: - self.generatedfile_backend.ensure_exists(self) + self.cachefile_backend.ensure_exists(self) def _generate(self): # Generate the file @@ -66,7 +66,7 @@ class GeneratedImageFile(BaseIKFile, ImageFile): ' race condition in the file backend %s. The saved file' ' will not be used.' % (self.storage, self.name, actual_name, - self.generatedfile_backend)) + self.cachefile_backend)) class LazyGeneratedImageFile(LazyObject): diff --git a/imagekit/generatedfiles/actions.py b/imagekit/cachefiles/actions.py similarity index 100% rename from imagekit/generatedfiles/actions.py rename to imagekit/cachefiles/actions.py diff --git a/imagekit/generatedfiles/backends.py b/imagekit/cachefiles/backends.py similarity index 93% rename from imagekit/generatedfiles/backends.py rename to imagekit/cachefiles/backends.py index ed375c1..15813be 100644 --- a/imagekit/generatedfiles/backends.py +++ b/imagekit/cachefiles/backends.py @@ -3,13 +3,13 @@ from django.core.cache import get_cache from django.core.exceptions import ImproperlyConfigured -def get_default_generatedfile_backend(): +def get_default_cachefile_backend(): """ Get the default file backend. """ from django.conf import settings - return get_singleton(settings.IMAGEKIT_DEFAULT_GENERATEDFILE_BACKEND, + return get_singleton(settings.IMAGEKIT_DEFAULT_CACHEFILE_BACKEND, 'file backend') diff --git a/imagekit/generatedfiles/namers.py b/imagekit/cachefiles/namers.py similarity index 81% rename from imagekit/generatedfiles/namers.py rename to imagekit/cachefiles/namers.py index 2a9f9b7..d6bc95a 100644 --- a/imagekit/generatedfiles/namers.py +++ b/imagekit/cachefiles/namers.py @@ -21,18 +21,18 @@ def source_name_as_path(generator): /path/to/generated/images/photos/thumbnails/bulldog/5ff3233527c5ac3e4b596343b440ff67.jpg where "/path/to/generated/images/" is the value specified by the - ``IMAGEKIT_GENERATEDFILE_DIR`` setting. + ``IMAGEKIT_CACHEFILE_DIR`` setting. """ source_filename = getattr(generator.source, 'name', None) if source_filename is None or os.path.isabs(source_filename): - # Generally, we put the file right in the generated file directory. - dir = settings.IMAGEKIT_GENERATEDFILE_DIR + # Generally, we put the file right in the cache file directory. + dir = settings.IMAGEKIT_CACHEFILE_DIR else: # For source files with relative names (like Django media files), # use the source's name to create the new filename. - dir = os.path.join(settings.IMAGEKIT_GENERATEDFILE_DIR, + dir = os.path.join(settings.IMAGEKIT_CACHEFILE_DIR, os.path.splitext(source_filename)[0]) ext = suggest_extension(source_filename or '', generator.format) @@ -51,18 +51,18 @@ def source_name_dot_hash(generator): /path/to/generated/images/photos/thumbnails/bulldog.5ff3233527c5.jpg where "/path/to/generated/images/" is the value specified by the - ``IMAGEKIT_GENERATEDFILE_DIR`` setting. + ``IMAGEKIT_CACHEFILE_DIR`` setting. """ source_filename = getattr(generator.source, 'name', None) if source_filename is None or os.path.isabs(source_filename): - # Generally, we put the file right in the generated file directory. - dir = settings.IMAGEKIT_GENERATEDFILE_DIR + # Generally, we put the file right in the cache file directory. + dir = settings.IMAGEKIT_CACHEFILE_DIR else: # For source files with relative names (like Django media files), # use the source's name to create the new filename. - dir = os.path.join(settings.IMAGEKIT_GENERATEDFILE_DIR, + dir = os.path.join(settings.IMAGEKIT_CACHEFILE_DIR, os.path.dirname(source_filename)) ext = suggest_extension(source_filename or '', generator.format) @@ -82,10 +82,10 @@ def hash(generator): /path/to/generated/images/5ff3233527c5ac3e4b596343b440ff67.jpg where "/path/to/generated/images/" is the value specified by the - ``IMAGEKIT_GENERATEDFILE_DIR`` setting. + ``IMAGEKIT_CACHEFILE_DIR`` setting. """ format = getattr(generator, 'format', None) ext = format_to_extension(format) if format else '' - return os.path.normpath(os.path.join(settings.IMAGEKIT_GENERATEDFILE_DIR, + return os.path.normpath(os.path.join(settings.IMAGEKIT_CACHEFILE_DIR, '%s%s' % (generator.get_hash(), ext))) diff --git a/imagekit/generatedfiles/strategies.py b/imagekit/cachefiles/strategies.py similarity index 86% rename from imagekit/generatedfiles/strategies.py rename to imagekit/cachefiles/strategies.py index 7e19692..1104de6 100644 --- a/imagekit/generatedfiles/strategies.py +++ b/imagekit/cachefiles/strategies.py @@ -15,8 +15,8 @@ class JustInTime(object): class Optimistic(object): """ A strategy that acts immediately when the source file changes and assumes - that the generated files will not be removed (i.e. it doesn't ensure the - generated file exists when it's accessed). + that the cache files will not be removed (i.e. it doesn't ensure the + cache file exists when it's accessed). """ @@ -36,7 +36,7 @@ class DictStrategy(object): class StrategyWrapper(LazyObject): def __init__(self, strategy): if isinstance(strategy, basestring): - strategy = get_singleton(strategy, 'generated file strategy') + strategy = get_singleton(strategy, 'cache file strategy') elif isinstance(strategy, dict): strategy = DictStrategy(strategy) elif callable(strategy): diff --git a/imagekit/conf.py b/imagekit/conf.py index c81e228..3abd6f3 100644 --- a/imagekit/conf.py +++ b/imagekit/conf.py @@ -3,11 +3,11 @@ from django.conf import settings class ImageKitConf(AppConf): - GENERATEDFILE_NAMER = 'imagekit.generatedfiles.namers.hash' - SPEC_GENERATEDFILE_NAMER = 'imagekit.generatedfiles.namers.source_name_as_path' - GENERATEDFILE_DIR = 'generated/images' - DEFAULT_GENERATEDFILE_BACKEND = 'imagekit.generatedfiles.backends.Simple' - DEFAULT_GENERATEDFILE_STRATEGY = 'imagekit.generatedfiles.strategies.JustInTime' + CACHEFILE_NAMER = 'imagekit.cachefiles.namers.hash' + SPEC_CACHEFILE_NAMER = 'imagekit.cachefiles.namers.source_name_as_path' + CACHEFILE_DIR = 'CACHE/images' + DEFAULT_CACHEFILE_BACKEND = 'imagekit.cachefiles.backends.Simple' + DEFAULT_CACHEFILE_STRATEGY = 'imagekit.cachefiles.strategies.JustInTime' DEFAULT_FILE_STORAGE = 'django.core.files.storage.FileSystemStorage' diff --git a/imagekit/management/commands/generateimages.py b/imagekit/management/commands/generateimages.py index 569761f..099fe3d 100644 --- a/imagekit/management/commands/generateimages.py +++ b/imagekit/management/commands/generateimages.py @@ -1,6 +1,6 @@ from django.core.management.base import BaseCommand import re -from ...registry import generator_registry, generatedfile_registry +from ...registry import generator_registry, cachefile_registry class Command(BaseCommand): @@ -22,7 +22,7 @@ well as "a:b" and "a:b:c".""") for generator_id in generators: self.stdout.write('Validating generator: %s\n' % generator_id) - for file in generatedfile_registry.get(generator_id): + for file in cachefile_registry.get(generator_id): self.stdout.write(' %s\n' % file) try: # TODO: Allow other validation actions through command option diff --git a/imagekit/models/fields/__init__.py b/imagekit/models/fields/__init__.py index ee9a789..292f410 100644 --- a/imagekit/models/fields/__init__.py +++ b/imagekit/models/fields/__init__.py @@ -26,15 +26,15 @@ class ImageSpecField(SpecHostField): """ def __init__(self, processors=None, format=None, options=None, - source=None, generatedfile_storage=None, autoconvert=None, - generatedfile_backend=None, generatedfile_strategy=None, spec=None, + source=None, cachefile_storage=None, autoconvert=None, + cachefile_backend=None, cachefile_strategy=None, spec=None, id=None): SpecHost.__init__(self, processors=processors, format=format, - options=options, generatedfile_storage=generatedfile_storage, + options=options, cachefile_storage=cachefile_storage, autoconvert=autoconvert, - generatedfile_backend=generatedfile_backend, - generatedfile_strategy=generatedfile_strategy, spec=spec, + cachefile_backend=cachefile_backend, + cachefile_strategy=cachefile_strategy, spec=spec, spec_id=id) # TODO: Allow callable for source. See https://github.com/jdriscoll/django-imagekit/issues/158#issuecomment-10921664 diff --git a/imagekit/models/fields/utils.py b/imagekit/models/fields/utils.py index d9a2976..bfb6d89 100644 --- a/imagekit/models/fields/utils.py +++ b/imagekit/models/fields/utils.py @@ -1,4 +1,4 @@ -from ...generatedfiles import GeneratedImageFile +from ...cachefiles import GeneratedImageFile from django.db.models.fields.files import ImageField diff --git a/imagekit/registry.py b/imagekit/registry.py index 309bb54..2523f9b 100644 --- a/imagekit/registry.py +++ b/imagekit/registry.py @@ -76,7 +76,7 @@ class SourceGroupRegistry(object): from .specs.sourcegroups import SourceGroupFilesGenerator generator_ids = self._source_groups.setdefault(source_group, set()) generator_ids.add(generator_id) - generatedfile_registry.register(generator_id, + cachefile_registry.register(generator_id, SourceGroupFilesGenerator(source_group, generator_id)) def unregister(self, generator_id, source_group): @@ -84,7 +84,7 @@ class SourceGroupRegistry(object): generator_ids = self._source_groups.setdefault(source_group, set()) if generator_id in generator_ids: generator_ids.remove(generator_id) - generatedfile_registry.unregister(generator_id, + cachefile_registry.unregister(generator_id, SourceGroupFilesGenerator(source_group, generator_id)) def source_group_receiver(self, sender, source, signal, **kwargs): @@ -92,7 +92,7 @@ class SourceGroupRegistry(object): Relay source group signals to the appropriate spec strategy. """ - from .generatedfiles import GeneratedImageFile + from .cachefiles import GeneratedImageFile source_group = sender # Ignore signals from unregistered groups. @@ -108,11 +108,11 @@ class SourceGroupRegistry(object): call_strategy_method(spec, callback_name, file=file) -class GeneratedFileRegistry(object): +class CacheFileRegistry(object): """ An object for registering generated files with image generators. The two are associated with each other via a string id. We do this (as opposed to - associating them directly by, for example, putting a ``generatedfiles`` + associating them directly by, for example, putting a ``cachefiles`` attribute on image generators) so that image generators can be overridden without losing the associated files. That way, a distributable app can define its own generators without locking the users of the app into it. @@ -120,29 +120,29 @@ class GeneratedFileRegistry(object): """ def __init__(self): - self._generatedfiles = {} + self._cachefiles = {} - def register(self, generator_id, generatedfiles): + def register(self, generator_id, cachefiles): """ Associates generated files with a generator id """ - if generatedfiles not in self._generatedfiles: - self._generatedfiles[generatedfiles] = set() - self._generatedfiles[generatedfiles].add(generator_id) + if cachefiles not in self._cachefiles: + self._cachefiles[cachefiles] = set() + self._cachefiles[cachefiles].add(generator_id) - def unregister(self, generator_id, generatedfiles): + def unregister(self, generator_id, cachefiles): """ Disassociates generated files with a generator id """ try: - self._generatedfiles[generatedfiles].remove(generator_id) + self._cachefiles[cachefiles].remove(generator_id) except KeyError: pass def get(self, generator_id): - for k, v in self._generatedfiles.items(): + for k, v in self._cachefiles.items(): if generator_id in v: for file in k(): yield file @@ -164,8 +164,8 @@ class Register(object): generator_registry.register(id, generator) # iterable that returns kwargs or callable that returns iterable of kwargs - def generatedfiles(self, generator_id, generatedfiles): - generatedfile_registry.register(generator_id, generatedfiles) + def cachefiles(self, generator_id, cachefiles): + cachefile_registry.register(generator_id, cachefiles) def source_group(self, generator_id, source_group): source_group_registry.register(generator_id, source_group) @@ -179,15 +179,15 @@ class Unregister(object): def generator(self, id, generator): generator_registry.unregister(id, generator) - def generatedfiles(self, generator_id, generatedfiles): - generatedfile_registry.unregister(generator_id, generatedfiles) + def cachefiles(self, generator_id, cachefiles): + cachefile_registry.unregister(generator_id, cachefiles) def source_group(self, generator_id, source_group): source_group_registry.unregister(generator_id, source_group) generator_registry = GeneratorRegistry() -generatedfile_registry = GeneratedFileRegistry() +cachefile_registry = CacheFileRegistry() source_group_registry = SourceGroupRegistry() register = Register() unregister = Unregister() diff --git a/imagekit/specs/__init__.py b/imagekit/specs/__init__.py index c286a44..969dcf3 100644 --- a/imagekit/specs/__init__.py +++ b/imagekit/specs/__init__.py @@ -2,8 +2,8 @@ from django.conf import settings from django.db.models.fields.files import ImageFieldFile from hashlib import md5 import pickle -from ..generatedfiles.backends import get_default_generatedfile_backend -from ..generatedfiles.strategies import StrategyWrapper +from ..cachefiles.backends import get_default_cachefile_backend +from ..cachefiles.strategies import StrategyWrapper from ..processors import ProcessorPipeline from ..utils import open_image, img_to_fobj, get_by_qname from ..registry import generator_registry, register @@ -16,27 +16,27 @@ class BaseImageSpec(object): """ - generatedfile_storage = None - """A Django storage system to use to save a generated file.""" + cachefile_storage = None + """A Django storage system to use to save a cache file.""" - generatedfile_backend = None + cachefile_backend = None """ - An object responsible for managing the state of generated files. Defaults to - an instance of ``IMAGEKIT_DEFAULT_GENERATEDFILE_BACKEND`` + An object responsible for managing the state of cache files. Defaults to + an instance of ``IMAGEKIT_DEFAULT_CACHEFILE_BACKEND`` """ - generatedfile_strategy = settings.IMAGEKIT_DEFAULT_GENERATEDFILE_STRATEGY + cachefile_strategy = settings.IMAGEKIT_DEFAULT_CACHEFILE_STRATEGY """ A dictionary containing callbacks that allow you to customize how and when the image file is created. Defaults to - ``IMAGEKIT_DEFAULT_GENERATEDFILE_STRATEGY``. + ``IMAGEKIT_DEFAULT_CACHEFILE_STRATEGY``. """ def __init__(self): - self.generatedfile_backend = self.generatedfile_backend or get_default_generatedfile_backend() - self.generatedfile_strategy = StrategyWrapper(self.generatedfile_strategy) + self.cachefile_backend = self.cachefile_backend or get_default_cachefile_backend() + self.cachefile_strategy = StrategyWrapper(self.cachefile_strategy) def generate(self): raise NotImplementedError @@ -81,8 +81,8 @@ class ImageSpec(BaseImageSpec): super(ImageSpec, self).__init__() @property - def generatedfile_name(self): - fn = get_by_qname(settings.IMAGEKIT_SPEC_GENERATEDFILE_NAMER, 'namer') + def cachefile_name(self): + fn = get_by_qname(settings.IMAGEKIT_SPEC_CACHEFILE_NAMER, 'namer') return fn(self) def __getstate__(self): diff --git a/imagekit/specs/sourcegroups.py b/imagekit/specs/sourcegroups.py index e828f91..ec85af8 100644 --- a/imagekit/specs/sourcegroups.py +++ b/imagekit/specs/sourcegroups.py @@ -3,7 +3,7 @@ Source groups are the means by which image spec sources are identified. They have two responsibilities: 1. To dispatch ``source_created``, ``source_changed``, and ``source_deleted`` - signals. (These will be relayed to the corresponding specs' generated file + signals. (These will be relayed to the corresponding specs' cache file strategies.) 2. To provide the source files that they represent, via a generator method named ``files()``. (This is used by the generateimages management command for @@ -13,7 +13,7 @@ have two responsibilities: from django.db.models.signals import post_init, post_save, post_delete from django.utils.functional import wraps -from ..generatedfiles import LazyGeneratedImageFile +from ..cachefiles import LazyGeneratedImageFile from ..signals import source_created, source_changed, source_deleted @@ -137,7 +137,7 @@ class ImageFieldSourceGroup(object): class SourceGroupFilesGenerator(object): """ - A Python generator that yields generated file objects for source groups. + A Python generator that yields cache file objects for source groups. """ def __init__(self, source_group, generator_id): diff --git a/imagekit/templatetags/imagekit.py b/imagekit/templatetags/imagekit.py index fbad511..c845dcc 100644 --- a/imagekit/templatetags/imagekit.py +++ b/imagekit/templatetags/imagekit.py @@ -2,7 +2,7 @@ from django import template from django.utils.html import escape from django.utils.safestring import mark_safe from .compat import parse_bits -from ..generatedfiles import GeneratedImageFile +from ..cachefiles import GeneratedImageFile from ..registry import generator_registry @@ -14,7 +14,7 @@ HTML_ATTRS_DELIMITER = '--' DEFAULT_THUMBNAIL_GENERATOR = 'imagekit:thumbnail' -def get_generatedfile(context, generator_id, generator_kwargs, source=None): +def get_cachefile(context, generator_id, generator_kwargs, source=None): generator_id = generator_id.resolve(context) kwargs = dict((k, v.resolve(context)) for k, v in generator_kwargs.items()) generator = generator_registry.get(generator_id, **kwargs) @@ -47,7 +47,7 @@ class GenerateImageAssignmentNode(template.Node): autodiscover() variable_name = self.get_variable_name(context) - context[variable_name] = get_generatedfile(context, self._generator_id, + context[variable_name] = get_cachefile(context, self._generator_id, self._generator_kwargs) return '' @@ -63,7 +63,7 @@ class GenerateImageTagNode(template.Node): from ..utils import autodiscover autodiscover() - file = get_generatedfile(context, self._generator_id, + file = get_cachefile(context, self._generator_id, self._generator_kwargs) attrs = dict((k, v.resolve(context)) for k, v in self._html_attrs.items()) diff --git a/imagekit/utils.py b/imagekit/utils.py index 2ac4f56..ff02a50 100644 --- a/imagekit/utils.py +++ b/imagekit/utils.py @@ -425,7 +425,7 @@ def generate(generator): def call_strategy_method(generator, method_name, *args, **kwargs): - strategy = getattr(generator, 'generatedfile_strategy', None) + strategy = getattr(generator, 'cachefile_strategy', None) fn = getattr(strategy, method_name, None) if fn is not None: fn(*args, **kwargs) From 55a2a5fc9df61f66041b33904cf1b4f1f8cce352 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Mon, 4 Feb 2013 19:57:00 -0500 Subject: [PATCH 201/213] Typo fix --- README.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index d31a1c6..2846bab 100644 --- a/README.rst +++ b/README.rst @@ -221,7 +221,7 @@ This will output the following HTML: .. code-block:: html - + You can also add additional HTML attributes; just separate them from your keyword args using two dashes: @@ -259,7 +259,7 @@ Like the generateimage tag, the thumbnail tag outputs an tag: .. code-block:: html - + Comparing this syntax to the generateimage tag above, you'll notice a few differences. From 34a7ab975151c2d2b6b9fc165d258f0b13edd653 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Tue, 5 Feb 2013 19:21:26 -0500 Subject: [PATCH 202/213] Remove requirements.txt What was this doing in here? --- requirements.txt | 8 -------- 1 file changed, 8 deletions(-) delete mode 100644 requirements.txt diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 23b38e9..0000000 --- a/requirements.txt +++ /dev/null @@ -1,8 +0,0 @@ -Django>=1.3.1 -django-appconf>=0.5 -PIL>=1.1.7 - -# Required for tests -nose==1.2.1 -nose-progressive==1.3 -django-nose==1.1 From 0ea497261b030cb0f2be1c939d36d851988c2f3e Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Tue, 5 Feb 2013 19:24:36 -0500 Subject: [PATCH 203/213] Update credits --- AUTHORS | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/AUTHORS b/AUTHORS index 24968f0..03028f2 100644 --- a/AUTHORS +++ b/AUTHORS @@ -1,6 +1,7 @@ ImageKit was originally written by `Justin Driscoll`_. -The field-based API was written by the bright minds at HZDG_. +The field-based API and other post-1.0 stuff was written by the bright people at +HZDG_. Maintainers ~~~~~~~~~~~ From 1f86e33d64bee2b975344223058520c0f84f77dc Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Tue, 5 Feb 2013 21:12:39 -0500 Subject: [PATCH 204/213] Doc fixes --- AUTHORS | 4 +- README.rst | 6 +- docs/advanced_usage.rst | 284 ++++++++++++++++++++++++++ docs/advanced_usage/models.rst | 99 --------- docs/advanced_usage/optimizing.rst | 101 --------- docs/advanced_usage/source_groups.rst | 69 ------- docs/apireference.rst | 29 --- docs/configuration.rst | 91 ++------- docs/index.rst | 40 +--- docs/upgrading.rst | 1 + 10 files changed, 314 insertions(+), 410 deletions(-) create mode 100644 docs/advanced_usage.rst delete mode 100644 docs/advanced_usage/models.rst delete mode 100644 docs/advanced_usage/optimizing.rst delete mode 100644 docs/advanced_usage/source_groups.rst delete mode 100644 docs/apireference.rst diff --git a/AUTHORS b/AUTHORS index 03028f2..d91cb93 100644 --- a/AUTHORS +++ b/AUTHORS @@ -4,7 +4,7 @@ The field-based API and other post-1.0 stuff was written by the bright people at HZDG_. Maintainers -~~~~~~~~~~~ +----------- * `Bryan Veloso`_ * `Matthew Tretter`_ @@ -12,7 +12,7 @@ Maintainers * `Greg Newman`_ Contributors -~~~~~~~~~~~~ +------------ * `Josh Ourisman`_ * `Jonathan Slenders`_ diff --git a/README.rst b/README.rst index 2846bab..2f2ad5d 100644 --- a/README.rst +++ b/README.rst @@ -94,7 +94,7 @@ class: This is pretty similar to our previous example. We don't need to specify a "source" any more since we're not processing another image field, but we do need to pass an "upload_to" argument. This behaves exactly as it does for Django -``ImageField``s. +ImageFields. .. note:: @@ -393,7 +393,7 @@ AdminThumbnail can even use a custom template. For more information, see Community ---------- +========= Please use `the GitHub issue tracker `_ to report bugs with django-imagekit. `A mailing list `_ @@ -402,7 +402,7 @@ also exists to discuss the project and ask questions, as well as the official Contributing ------------- +============ We love contributions! And you don't have to be an expert with the library—or even Django—to contribute either: ImageKit's processors are standalone classes diff --git a/docs/advanced_usage.rst b/docs/advanced_usage.rst new file mode 100644 index 0000000..917b642 --- /dev/null +++ b/docs/advanced_usage.rst @@ -0,0 +1,284 @@ +Advanced Usage +************** + + +Models +====== + + +The ``ImageSpecField`` Shorthand Syntax +--------------------------------------- + +If you've read the README, you already know what an ``ImageSpecField`` is and +the basics of defining one: + +.. code-block:: python + + from django.db import models + from imagekit.models import ImageSpecField + from imagekit.processors import ResizeToFill + + class Profile(models.Model): + avatar = models.ImageField(upload_to='avatars') + avatar_thumbnail = ImageSpecField(source='avatar', + processors=[ResizeToFill(100, 50)], + format='JPEG', + options={'quality': 60}) + +This will create an ``avatar_thumbnail`` field which is a resized version of the +image stored in the ``avatar`` image field. But this is actually just shorthand +for creating an ``ImageSpec``, registering it, and associating it with an +``ImageSpecField``: + +.. code-block:: python + + from django.db import models + from imagekit import ImageSpec, register + from imagekit.models import ImageSpecField + from imagekit.processors import ResizeToFill + + class AvatarThumbnail(ImageSpec): + processors = [ResizeToFill(100, 50)] + format = 'JPEG' + options = {'quality': 60} + + register.generator('myapp:profile:avatar_thumbnail', AvatarThumbnail) + + class Profile(models.Model): + avatar = models.ImageField(upload_to='avatars') + avatar_thumbnail = ImageSpecField(source='avatar', + spec_id='myapp:profile:avatar_thumbnail') + +Obviously, the shorthand version is a lot, well…shorter. So why would you ever +want to go through the trouble of using the long form? The answer is that the +long form—creating an image spec class and registering it—gives you a lot more +power over the generated image. + + +.. _dynamic-specs: + +Specs That Change +----------------- + +As you'll remember from the README, an image spec is just a type of image +generator that generates a new image from a source image. How does the image +spec get access to the source image? Simple! It's passed to the constructor as +a keyword argument and stored as an attribute of the spec. Normally, we don't +have to concern ourselves with this; the ``ImageSpec`` knows what to do with the +source image and we're happy to let it do its thing. However, having access to +the source image in our spec class can be very useful… + +Often, when using an ``ImageSpecField``, you may want the spec to vary based on +properties of a model. (For example, you might want to store image dimensions on +the model and then use them to generate your thumbnail.) Now that we know how to +access the source image from our spec, it's a simple matter to extract its model +and use it to create our processors list. In fact, ImageKit includes a utility +for getting this information. + +.. code-block:: python + :emphasize-lines: 11-14 + + from django.db import models + from imagekit import ImageSpec, register + from imagekit.models import ImageSpecField + from imagekit.processors import ResizeToFill + from imagekit.utils import get_field_info + + class AvatarThumbnail(ImageSpec): + format = 'JPEG' + options = {'quality': 60} + + @property + def processors(self): + model, field_name = get_field_info(self.source) + return [ResizeToFill(model.thumbnail_width, thumbnail.avatar_height)] + + register.generator('myapp:profile:avatar_thumbnail', AvatarThumbnail) + + class Profile(models.Model): + avatar = models.ImageField(upload_to='avatars') + avatar_thumbnail = ImageSpecField(source='avatar', + spec_id='myapp:profile:avatar_thumbnail') + thumbnail_width = models.PositiveIntegerField() + thumbnail_height = models.PositiveIntegerField() + +Now each avatar thumbnail will be resized according to the dimensions stored on +the model! + +Of course, processors aren't the only thing that can vary based on the model of +the source image; spec behavior can change in any way you want. + + +Optimizing +========== + +Unlike Django's ImageFields, ImageKit's ImageSpecFields and template tags don't +persist any data in the database. Therefore, in order to know whether an image +file needs to be generated, ImageKit needs to check if the file already exists +(using the appropriate file storage object`__). The object responsible for +performing these checks is called a *cache file backend*. + + +Cache! +------ + +By default, ImageKit checks for the existence of a cache file every time you +attempt to use the file and, if it doesn't exist, creates it synchronously. This +is a very safe behavior because it ensures that your ImageKit-generated images +are always available. However, that's a lot of checking with storage and those +kinds of operations can be slow—especially if you're using a remote storage—so +you'll want to try to avoid them as much as possible. + +Luckily, the default cache file backend makes use of Django's caching +abilities to mitigate the number of checks it actually has to do; it will use +the cache specified by the ``IMAGEKIT_CACHE_BACKEND`` to save the state of the +generated file. If your Django project is running in debug mode +(``settings.DEBUG`` is true), this will be a dummy cache by default. Otherwise, +it will use your project's default cache. + +In normal operation, your cache files will never be deleted; once they're +created, they'll stay created. So the simplest optimization you can make is to +set your ``IMAGEKIT_CACHE_BACKEND`` to a cache with a very long, or infinite, +timeout. + + +Even More Advanced +------------------ + +For many applications—particularly those using local storage for generated image +files—a cache with a long timeout is all the optimization you'll need. However, +there may be times when that simply doesn't cut it. In these cases, you'll want +to change when the generation is actually done. + +The objects responsible for specifying when cache files are created are +called *cache file strategies*. The default strategy can be set using the +``IMAGEKIT_DEFAULT_CACHEFILE_STRATEGY`` setting, and its default value is +`'imagekit.cachefiles.strategies.JustInTime'`. As we've already seen above, +the "just in time" strategy determines whether a file needs to be generated each +time it's accessed and, if it does, generates it synchronously (that is, as part +of the request-response cycle). + +Another strategy is to simply assume the file exists. This requires the fewest +number of checks (zero!), so we don't have to worry about expensive IO. The +strategy that takes this approach is +``imagekit.cachefiles.strategies.Optimistic``. In order to use this +strategy, either set the ``IMAGEKIT_DEFAULT_CACHEFILE_STRATEGY`` setting or, +to use it on a per-generator basis, set the ``cachefile_strategy`` attribute +of your spec or generator. Avoiding checking for file existence can be a real +boon to performance, but it also means that ImageKit has no way to know when a +file needs to be generated—well, at least not all the time. + +With image specs, we can know at least some of the times that a new file needs +to be generated: whenever the source image is created or changed. For this +reason, the optimistic strategy defines callbacks for these events. Every +source registered with ImageKit will automatically cause its specs' files to be +generated when it is created or changed. + +.. note:: + + In order to understand source registration, read :ref:`source-groups` + +If you have specs that :ref:`change based on attributes of the source +`, that's not going to cut it, though; the file will also need to +be generated when those attributes change. Likewise, image generators that don't +have sources (i.e. generators that aren't specs) won't cause files to be +generated automatically when using the optimistic strategy. (ImageKit can't know +when those need to be generated, if not on access.) In both cases, you'll have +to trigger the file generation yourself—either by generating the file in code +when necessary, or by periodically running the ``generateimages`` management +command. Luckily, ImageKit makes this pretty easy: + +.. code-block:: python + + from imagekit.cachefiles import LazyGeneratedImageFile + + file = LazyGeneratedImageFile('myapp:profile:avatar_thumbnail', source=source_file) + file.generate() + +One final situation in which images won't be generated automatically when using +the optimistic strategy is when you use a spec with a source that hasn't been +registered with it. Unlike the previous two examples, this situation cannot be +rectified by running the ``generateimages`` management command, for the simple +reason that the command has no way of knowing it needs to generate a file for +that spec from that source. Typically, this situation would arise when using the +template tags. Unlike ImageSpecFields, which automatically register all the +possible source images with the spec you define, the template tags +("generateimage" and "thumbnail") let you use any spec with any source. +Therefore, in order to generate the appropriate files using the +``generateimages`` management command, you'll need to first register a source +group that represents all of the sources you wish to use with the corresponding +specs. See :ref:`source-groups` for more information. + + +.. _source-groups: + +Source Groups +============= + +When you run the ``generateimages`` management command, how does ImageKit know +which source images to use with which specs? Obviously, when you define an +ImageSpecField, the source image is being connected to a spec, but what's going +on underneath the hood? + +The answer is that, when you define an ImageSpecField, ImageKit automatically +creates and registers an object called a *source group*. Source groups are +responsible for two things: + +1. They dispatch signals when a source is created, changed, or deleted, and +2. They expose a generator method that enumerates source files. + +When these objects are registered (using ``imagekit.register.source_group()``), +their signals will trigger callbacks on the cache file strategies associated +with image specs that use the source. (So, for example, you can chose to +generate a file every time the source image changes.) In addition, the generator +method is used (indirectly) to create the list of files to generate with the +``generateimages`` management command. + +Currently, there is only one source group class bundled with ImageKit—the one +used by ImageSpecFields. This source group +(``imagekit.specs.sourcegroups.ImageFieldSourceGroup``) represents an ImageField +on every instance of a particular model. In terms of the above description, the +instance ``ImageFieldSourceGroup(Profile, 'avatar')`` 1) dispatches a signal +every time the image in Profile's avatar ImageField changes, and 2) exposes a +generator method that iterates over every Profile's "avatar" image. + +Chances are, this is the only source group you will ever need to use, however, +ImageKit lets you define and register custom source groups easily. This may be +useful, for example, if you're using the template tags "generateimage" and +"thumbnail" and the optimistic cache file strategy. Again, the purpose is +to tell ImageKit which specs are used with which sources (so the +"generateimages" management command can generate those files) and when the +source image has been created or changed (so that the strategy has the +opportunity to act on it). + +A simple example of a custom source group class is as follows: + +.. code-block:: python + + import glob + import os + + class JpegsInADirectory(object): + def __init__(self, dir): + self.dir = dir + + def files(self): + os.chdir(self.dir) + for name in glob.glob('*.jpg'): + yield open(name) + +Instances of this class could then be registered with one or more spec id: + +.. code-block:: python + + from imagekit import register + + register.source_group('myapp:profile:avatar_thumbnail', JpegsInADirectory('/path/to/some/pics')) + +Running the "generateimages" management command would now cause thumbnails to be +generated (using the "myapp:profile:avatar_thumbnail" spec) for each of the +JPEGs in `/path/to/some/pics`. + +Note that, since this source group doesnt send the `source_created` or +`source_changed` signals, the corresponding cache file strategy callbacks +would not be called for them. diff --git a/docs/advanced_usage/models.rst b/docs/advanced_usage/models.rst deleted file mode 100644 index 2ccd70d..0000000 --- a/docs/advanced_usage/models.rst +++ /dev/null @@ -1,99 +0,0 @@ -The ``ImageSpecField`` Shorthand Syntax ---------------------------------------- - -If you've read the README, you already know what an ``ImageSpecField`` is and -the basics of defining one: - -.. code-block:: python - - from django.db import models - from imagekit.models import ImageSpecField - from imagekit.processors import ResizeToFill - - class Profile(models.Model): - avatar = models.ImageField(upload_to='avatars') - avatar_thumbnail = ImageSpecField(source='avatar', - processors=[ResizeToFill(100, 50)], - format='JPEG', - options={'quality': 60}) - -This will create an ``avatar_thumbnail`` field which is a resized version of the -image stored in the ``avatar`` image field. But this is actually just shorthand -for creating an ``ImageSpec``, registering it, and associating it with an -``ImageSpecField``: - -.. code-block:: python - - from django.db import models - from imagekit import ImageSpec, register - from imagekit.models import ImageSpecField - from imagekit.processors import ResizeToFill - - class AvatarThumbnail(ImageSpec): - processors = [ResizeToFill(100, 50)] - format = 'JPEG' - options = {'quality': 60} - - register.generator('myapp:profile:avatar_thumbnail', AvatarThumbnail) - - class Profile(models.Model): - avatar = models.ImageField(upload_to='avatars') - avatar_thumbnail = ImageSpecField(source='avatar', - spec_id='myapp:profile:avatar_thumbnail') - -Obviously, the shorthand version is a lot, well…shorter. So why would you ever -want to go through the trouble of using the long form? The answer is that the -long form—creating an image spec class and registering it—gives you a lot more -power over the generated image. - - -Specs That Change ------------------ - -As you'll remember from the README, an image spec is just a type of image -generator that generates a new image from a source image. How does the image -spec get access to the source image? Simple! It's passed to the constructor as -a keyword argument and stored as an attribute of the spec. Normally, we don't -have to concern ourselves with this; the ``ImageSpec`` knows what to do with the -source image and we're happy to let it do its thing. However, having access to -the source image in our spec class can be very useful… - -Often, when using an ``ImageSpecField``, you may want the spec to vary based on -properties of a model. (For example, you might want to store image dimensions on -the model and then use them to generate your thumbnail.) Now that we know how to -access the source image from our spec, it's a simple matter to extract its model -and use it to create our processors list. In fact, ImageKit includes a utility -for getting this information. - -.. code-block:: python - :emphasize-lines: 11-14 - - from django.db import models - from imagekit import ImageSpec, register - from imagekit.models import ImageSpecField - from imagekit.processors import ResizeToFill - from imagekit.utils import get_field_info - - class AvatarThumbnail(ImageSpec): - format = 'JPEG' - options = {'quality': 60} - - @property - def processors(self): - model, field_name = get_field_info(self.source) - return [ResizeToFill(model.thumbnail_width, thumbnail.avatar_height)] - - register.generator('myapp:profile:avatar_thumbnail', AvatarThumbnail) - - class Profile(models.Model): - avatar = models.ImageField(upload_to='avatars') - avatar_thumbnail = ImageSpecField(source='avatar', - spec_id='myapp:profile:avatar_thumbnail') - thumbnail_width = models.PositiveIntegerField() - thumbnail_height = models.PositiveIntegerField() - -Now each avatar thumbnail will be resized according to the dimensions stored on -the model! - -Of course, processors aren't the only thing that can vary based on the model of -the source image; spec behavior can change in any way you want. diff --git a/docs/advanced_usage/optimizing.rst b/docs/advanced_usage/optimizing.rst deleted file mode 100644 index b28c583..0000000 --- a/docs/advanced_usage/optimizing.rst +++ /dev/null @@ -1,101 +0,0 @@ -Unlike Django's ImageFields, ImageKit's ImageSpecFields and template tags don't -persist any data in the database. Therefore, in order to know whether an image -file needs to be generated, ImageKit needs to check if the file already exists -(using the appropriate file storage object`__). The object responsible for -performing these checks is called a *cache file backend*. - - -Cache! ------- - -By default, ImageKit checks for the existence of a cache file every time you -attempt to use the file and, if it doesn't exist, creates it synchronously. This -is a very safe behavior because it ensures that your ImageKit-generated images -are always available. However, that's a lot of checking with storage and those -kinds of operations can be slow—especially if you're using a remote storage—so -you'll want to try to avoid them as much as possible. - -Luckily, the default cache file backend makes use of Django's caching -abilities to mitigate the number of checks it actually has to do; it will use -the cache specified by the ``IMAGEKIT_CACHE_BACKEND`` to save the state of the -generated file. If your Django project is running in debug mode -(``settings.DEBUG`` is true), this will be a dummy cache by default. Otherwise, -it will use your project's default cache. - -In normal operation, your cache files will never be deleted; once they're -created, they'll stay created. So the simplest optimization you can make is to -set your ``IMAGEKIT_CACHE_BACKEND`` to a cache with a very long, or infinite, -timeout. - - -Even More Advanced ------------------- - -For many applications—particularly those using local storage for generated image -files—a cache with a long timeout is all the optimization you'll need. However, -there may be times when that simply doesn't cut it. In these cases, you'll want -to change when the generation is actually done. - -The objects responsible for specifying when cache files are created are -called *cache file strategies*. The default strategy can be set using the -``IMAGEKIT_DEFAULT_CACHEFILE_STRATEGY`` setting, and its default value is -`'imagekit.cachefiles.strategies.JustInTime'`. As we've already seen above, -the "just in time" strategy determines whether a file needs to be generated each -time it's accessed and, if it does, generates it synchronously (that is, as part -of the request-response cycle). - -Another strategy is to simply assume the file exists. This requires the fewest -number of checks (zero!), so we don't have to worry about expensive IO. The -strategy that takes this approach is -``imagekit.cachefiles.strategies.Optimistic``. In order to use this -strategy, either set the ``IMAGEKIT_DEFAULT_CACHEFILE_STRATEGY`` setting or, -to use it on a per-generator basis, set the ``cachefile_strategy`` attribute -of your spec or generator. Avoiding checking for file existence can be a real -boon to performance, but it also means that ImageKit has no way to know when a -file needs to be generated—well, at least not all the time. - -With image specs, we can know at least some of the times that a new file needs -to be generated: whenever the source image is created or changed. For this -reason, the optimistic strategy defines callbacks for these events. Every -`source registered with ImageKit`__ will automatically cause its specs' files to -be generated when it is created or changed. - -.. note:: - - In order to understand source registration, read :ref:`source-groups` - -If you have specs that `change based on attributes of the source`__, that's not -going to cut it, though; the file will also need to be generated when those -attributes change. Likewise, image generators that don't have sources (i.e. -generators that aren't specs) won't cause files to be generated automatically -when using the optimistic strategy. (ImageKit can't know when those need to be -generated, if not on access.) In both cases, you'll have to trigger the file -generation yourself—either by generating the file in code when necessary, or by -periodically running the ``generateimages`` management command. Luckily, -ImageKit makes this pretty easy: - -.. code-block:: python - - from imagekit.cachefiles import LazyGeneratedImageFile - - file = LazyGeneratedImageFile('myapp:profile:avatar_thumbnail', source=source_file) - file.generate() - -One final situation in which images won't be generated automatically when using -the optimistic strategy is when you use a spec with a source that hasn't been -registered with it. Unlike the previous two examples, this situation cannot be -rectified by running the ``generateimages`` management command, for the simple -reason that the command has no way of knowing it needs to generate a file for -that spec from that source. Typically, this situation would arise when using the -template tags. Unlike ImageSpecFields, which automatically register all the -possible source images with the spec you define, the template tags -("generateimage" and "thumbnail") let you use any spec with any source. -Therefore, in order to generate the appropriate files using the -``generateimages`` management command, you'll need to first register a source -group that represents all of the sources you wish to use with the corresponding -specs. See :ref:`source-groups` for more information. - - -__ https://docs.djangoproject.com/en/dev/ref/files/storage/ -__ -__ diff --git a/docs/advanced_usage/source_groups.rst b/docs/advanced_usage/source_groups.rst deleted file mode 100644 index d3ba0e5..0000000 --- a/docs/advanced_usage/source_groups.rst +++ /dev/null @@ -1,69 +0,0 @@ -.. _source-groups: - -When you run the ``generateimages`` management command, how does ImageKit know -which source images to use with which specs? Obviously, when you define an -ImageSpecField, the source image is being connected to a spec, but what's going -on underneath the hood? - -The answer is that, when you define an ImageSpecField, ImageKit automatically -creates and registers an object called a *source group*. Source groups are -responsible for two things: - -1. They dispatch signals when a source is created, changed, or deleted, and -2. They expose a generator method that enumerates source files. - -When these objects are registered (using ``imagekit.register.source_group()``), -their signals will trigger callbacks on the cache file strategies associated -with image specs that use the source. (So, for example, you can chose to -generate a file every time the source image changes.) In addition, the generator -method is used (indirectly) to create the list of files to generate with the -``generateimages`` management command. - -Currently, there is only one source group class bundled with ImageKit—the one -used by ImageSpecFields. This source group -(``imagekit.specs.sourcegroups.ImageFieldSourceGroup``) represents an ImageField -on every instance of a particular model. In terms of the above description, the -instance ``ImageFieldSourceGroup(Profile, 'avatar')`` 1) dispatches a signal -every time the image in Profile's avatar ImageField changes, and 2) exposes a -generator method that iterates over every Profile's "avatar" image. - -Chances are, this is the only source group you will ever need to use, however, -ImageKit lets you define and register custom source groups easily. This may be -useful, for example, if you're using the template tags "generateimage" and -"thumbnail" and the optimistic cache file strategy. Again, the purpose is -to tell ImageKit which specs are used with which sources (so the -"generateimages" management command can generate those files) and when the -source image has been created or changed (so that the strategy has the -opportunity to act on it). - -A simple example of a custom source group class is as follows: - -.. code-block:: python - - import glob - import os - - class JpegsInADirectory(object): - def __init__(self, dir): - self.dir = dir - - def files(self): - os.chdir(self.dir) - for name in glob.glob('*.jpg'): - yield open(name) - -Instances of this class could then be registered with one or more spec id: - -.. code-block:: python - - from imagekit import register - - register.source_group('myapp:profile:avatar_thumbnail', JpegsInADirectory('/path/to/some/pics')) - -Running the "generateimages" management command would now cause thumbnails to be -generated (using the "myapp:profile:avatar_thumbnail" spec) for each of the -JPEGs in `/path/to/some/pics`. - -Note that, since this source group doesnt send the `source_created` or -`source_changed` signals, the corresponding cache file strategy callbacks -would not be called for them. diff --git a/docs/apireference.rst b/docs/apireference.rst deleted file mode 100644 index d4a2ed8..0000000 --- a/docs/apireference.rst +++ /dev/null @@ -1,29 +0,0 @@ -API Reference -============= - - -:mod:`models` Module --------------------- - -.. automodule:: imagekit.models.fields - :members: - - -:mod:`processors` Module ------------------------- - -.. automodule:: imagekit.processors - :members: - -.. automodule:: imagekit.processors.resize - :members: - -.. automodule:: imagekit.processors.crop - :members: - - -:mod:`admin` Module --------------------- - -.. automodule:: imagekit.admin - :members: diff --git a/docs/configuration.rst b/docs/configuration.rst index 18b3595..6216158 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -10,7 +10,7 @@ Settings .. currentmodule:: django.conf.settings -.. attribute:: IMAGEKIT_CACHE_DIR +.. attribute:: IMAGEKIT_CACHEFILE_DIR :default: ``'CACHE/images'`` @@ -27,16 +27,16 @@ Settings will be used. -.. attribute:: IMAGEKIT_DEFAULT_IMAGE_CACHE_BACKEND +.. attribute:: IMAGEKIT_DEFAULT_CACHEFILE_BACKEND - :default: ``'imagekit.imagecache.backends.Simple'`` + :default: ``'imagekit.cachefiles.backends.Simple'`` Specifies the class that will be used to validate cached image files. -.. attribute:: IMAGEKIT_DEFAULT_IMAGE_CACHE_STRATEGY +.. attribute:: IMAGEKIT_DEFAULT_CACHEFILE_STRATEGY - :default: ``'imagekit.imagecache.strategies.JustInTime'`` + :default: ``'imagekit.cachefiles.strategies.JustInTime'`` The class responsible for specifying how and when cache files are generated. @@ -58,80 +58,17 @@ Settings A cache prefix to be used when values are stored in ``IMAGEKIT_CACHE_BACKEND`` -Optimization ------------- +.. attribute:: IMAGEKIT_CACHEFILE_NAMER -Not surprisingly, the trick to getting the most out of ImageKit is to reduce the -number of I/O operations. This can be especially important if your source files -aren't stored on the same server as the application. + :default: ``'imagekit.cachefiles.namers.hash'`` + + A function responsible for generating file names for non-spec cache files. -Image Cache Strategies -^^^^^^^^^^^^^^^^^^^^^^ +.. attribute:: IMAGEKIT_SPEC_CACHEFILE_NAMER -An important way of reducing the number of I/O operations that ImageKit makes is -by controlling when cached images are validated. This is done through "image -cache strategies"—objects that associate signals dispatched on the source file -with file actions. The default image cache strategy is -``'imagekit.imagecache.strategies.JustInTime'``; it looks like this: + :default: ``'imagekit.cachefiles.namers.source_name_as_path'`` -.. code-block:: python - - class JustInTime(object): - def before_access(self, file): - validate_now(file) - -When this strategy is used, the cache file is validated only immediately before -it's required—for example, when you access its url, path, or contents. This -strategy is exceedingly safe: by guaranteeing the presence of the file before -accessing it, you run no risk of it not being there. However, this strategy can -also be costly: verifying the existence of the cache file every time you access -it can be slow—particularly if the file is on another server. For this reason, -ImageKit provides another strategy: ``imagekit.imagecache.strategies.Optimistic``. -Unlike the just-in-time strategy, it does not validate the cache file when it's -accessed, but rather only when the soure file is created or changed. Later, when -the cache file is accessed, it is presumed to still be present. - -If neither of these strategies suits your application, you can create your own -strategy class. For example, you may wish to validate the file immediately when -it's accessed, but schedule validation using Celery when the source file is -saved or changed: - -.. code-block:: python - - from imagekit.imagecache.actions import validate_now, deferred_validate - - class CustomImageCacheStrategy(object): - - def before_access(self, file): - validate_now(file) - - def on_source_created(self, file): - deferred_validate(file) - - def on_source_changed(self, file): - deferred_validate(file) - -To use this cache strategy, you need only set the ``IMAGEKIT_DEFAULT_IMAGE_CACHE_STRATEGY`` -setting, or set the ``image_cache_strategy`` attribute of your image spec. - - -Django Cache Backends -^^^^^^^^^^^^^^^^^^^^^ - -In the "Image Cache Strategies" section above, we said that the just-in-time -strategy verifies the existence of the cache file every time you access -it, however, that's not exactly true. Cache files are actually validated using -image cache backends, and the default (``imagekit.imagecache.backends.Simple``) -memoizes the cache state (valid or invalid) using Django's cache framework. By -default, ImageKit will use a dummy cache backend when your project is in debug -mode (``DEBUG = True``), and the "default" cache (from your ``CACHES`` setting) -when ``DEBUG`` is ``False``. Since other parts of your project may have -different cacheing needs, though, ImageKit has an ``IMAGEKIT_CACHE_BACKEND`` -setting, which allows you to specify a different cache. - -In most cases, you won't be deleting you cached files once they're created, so -using a cache with a large timeout is a great way to optimize your site. Using -a cache that never expires would essentially negate the cost of the just-in-time -strategy, giving you the benefit of generating images on demand without the cost -of unnecessary future filesystem checks. + A function responsible for generating file names for cache files that + correspond to image specs. Since you will likely want to base the name of + your cache files on the name of the source, this extra setting is provided. diff --git a/docs/index.rst b/docs/index.rst index 5484351..04eee0f 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,44 +1,24 @@ -Getting Started -=============== - .. include:: ../README.rst -Commands --------- - -.. automodule:: imagekit.management.commands.ikcacheinvalidate - -.. automodule:: imagekit.management.commands.ikcachevalidate - - Authors -------- +======= .. include:: ../AUTHORS -Community ---------- - -The official Freenode channel for ImageKit is `#imagekit `_. -You should always find some fine people to answer your questions -about ImageKit there. - - -Digging Deeper --------------- - -.. toctree:: - - configuration - apireference - changelog - - Indices and tables ================== * :ref:`genindex` * :ref:`modindex` * :ref:`search` + +.. toctree:: + :glob: + :maxdepth: 2 + + configuration + advanced_usage + changelog + upgrading diff --git a/docs/upgrading.rst b/docs/upgrading.rst index 7a9b438..4efe7c0 100644 --- a/docs/upgrading.rst +++ b/docs/upgrading.rst @@ -5,6 +5,7 @@ ImageKit 3.0 introduces new APIs and tools that augment, improve, and in some cases entirely replace old IK workflows. Below, you'll find some useful guides for migrating your ImageKit 2.0 apps over to the shiny new IK3. + Model Specs ----------- From 088b84627b269c3eb4de613384e6d1101fb7164a Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Tue, 5 Feb 2013 21:19:35 -0500 Subject: [PATCH 205/213] Add section about management commands --- README.rst | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.rst b/README.rst index 2f2ad5d..4263e8c 100644 --- a/README.rst +++ b/README.rst @@ -392,6 +392,14 @@ AdminThumbnail can even use a custom template. For more information, see .. _`Django admin change list`: https://docs.djangoproject.com/en/dev/intro/tutorial02/#customize-the-admin-change-list +Managment Commands +------------------ + +ImageKit has one management command—`generateimages`—which will generate cache +files for all of your registered image generators. You can also pass it a list +of generator ids in order to generate images selectively. + + Community ========= From f6ce251e13415644b8975c974bf33fd074411726 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Wed, 6 Feb 2013 20:40:54 -0500 Subject: [PATCH 206/213] Fix setup.py test --- setup.py | 14 +++++++++++++- testrunner.py | 19 +++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) create mode 100644 testrunner.py diff --git a/setup.py b/setup.py index 7c1495e..209d683 100644 --- a/setup.py +++ b/setup.py @@ -1,21 +1,31 @@ #/usr/bin/env python import codecs import os +from setuptools import setup, find_packages import sys -from setuptools import setup, find_packages + +# Workaround for multiprocessing/nose issue. See http://bugs.python.org/msg170215 +try: + import multiprocessing +except ImportError: + pass + if 'publish' in sys.argv: os.system('python setup.py sdist upload') sys.exit() + read = lambda filepath: codecs.open(filepath, 'r', 'utf-8').read() + # Load package meta from the pkgmeta module without loading imagekit. pkgmeta = {} execfile(os.path.join(os.path.dirname(__file__), 'imagekit', 'pkgmeta.py'), pkgmeta) + setup( name='django-imagekit', version=pkgmeta['__version__'], @@ -35,7 +45,9 @@ setup( 'nose==1.2.1', 'nose-progressive==1.3', 'django-nose==1.1', + 'PIL==1.1.7', ], + test_suite='testrunner.run_tests', install_requires=[ 'django-appconf>=0.5', ], diff --git a/testrunner.py b/testrunner.py new file mode 100644 index 0000000..e4d27c7 --- /dev/null +++ b/testrunner.py @@ -0,0 +1,19 @@ +# A wrapper for Django's test runner. +# See http://ericholscher.com/blog/2009/jun/29/enable-setuppy-test-your-django-apps/ +# and http://gremu.net/blog/2010/enable-setuppy-test-your-django-apps/ +import os +import sys + +os.environ['DJANGO_SETTINGS_MODULE'] = 'tests.settings' +test_dir = os.path.dirname(__file__) +sys.path.insert(0, test_dir) + +from django.test.utils import get_runner +from django.conf import settings + + +def run_tests(): + cls = get_runner(settings) + runner = cls() + failures = runner.run_tests(['tests']) + sys.exit(failures) From 5982e1e549067b962269b924fa543831a376edfb Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Wed, 6 Feb 2013 20:41:18 -0500 Subject: [PATCH 207/213] Remove Makefile --- Makefile | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 Makefile diff --git a/Makefile b/Makefile deleted file mode 100644 index 3ec6c46..0000000 --- a/Makefile +++ /dev/null @@ -1,7 +0,0 @@ -test: - flake8 --ignore=E501,E126,E127,E128 imagekit tests - export PYTHONPATH=$(PWD):$(PYTHONPATH); \ - django-admin.py test --settings=tests.settings tests - - -.PHONY: test From c90e6d637c6c3a8618188d5c519c62d751041710 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Wed, 6 Feb 2013 20:46:06 -0500 Subject: [PATCH 208/213] Simplify tox command --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 5a5e2e7..80c5fd1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,6 +2,6 @@ language: python python: - 2.7 install: pip install tox --use-mirrors -script: tox -e py27-django13,py27-django12,py26-django13,py27-django12 +script: tox notifications: irc: "irc.freenode.org#imagekit" From 7759394df73387e332ab99874652c031154da599 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Wed, 6 Feb 2013 20:46:14 -0500 Subject: [PATCH 209/213] Remove Pillow (PIL is specified in setup.py) --- tox.ini | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/tox.ini b/tox.ini index cedd0d1..c7c54cf 100644 --- a/tox.ini +++ b/tox.ini @@ -4,40 +4,34 @@ envlist = py26-django14, py26-django13, py26-django12 [testenv] -commands = make test +commands = python setup.py test [testenv:py27-django14] basepython = python2.7 deps = Django>=1.4,<1.5 - Pillow [testenv:py27-django13] basepython = python2.7 deps = Django>=1.3,<1.4 - Pillow [testenv:py27-django12] basepython = python2.7 deps = Django>=1.2,<1.3 - Pillow [testenv:py26-django14] basepython = python2.6 deps = Django>=1.4,<1.5 - Pillow [testenv:py26-django13] basepython = python2.6 deps = Django>=1.3,<1.4 - Pillow [testenv:py26-django12] basepython = python2.6 deps = Django>=1.2,<1.3 - Pillow From 4ac6565bec4278b14d224cd8a267a9e464f4486f Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Wed, 6 Feb 2013 21:01:51 -0500 Subject: [PATCH 210/213] Use Pillow for testing --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 209d683..61cc546 100644 --- a/setup.py +++ b/setup.py @@ -45,7 +45,7 @@ setup( 'nose==1.2.1', 'nose-progressive==1.3', 'django-nose==1.1', - 'PIL==1.1.7', + 'Pillow==1.7.8', ], test_suite='testrunner.run_tests', install_requires=[ From 51dcf283fda1492d8e75da2c036848ab4149030d Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Wed, 6 Feb 2013 21:59:26 -0500 Subject: [PATCH 211/213] Fix default cache backend for Django < 1.3 --- imagekit/conf.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/imagekit/conf.py b/imagekit/conf.py index 3abd6f3..4c2f2e7 100644 --- a/imagekit/conf.py +++ b/imagekit/conf.py @@ -16,5 +16,8 @@ class ImageKitConf(AppConf): def configure_cache_backend(self, value): if value is None: - value = 'django.core.cache.backends.dummy.DummyCache' if settings.DEBUG else 'default' + if getattr(settings, 'CACHES', None): + value = 'django.core.cache.backends.dummy.DummyCache' if settings.DEBUG else 'default' + else: + value = 'dummy://' if settings.DEBUG else settings.CACHE_BACKEND return value From 561856abd79d785cb8d1a4e1580c735f68531fd2 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Wed, 6 Feb 2013 22:04:13 -0500 Subject: [PATCH 212/213] Don't use assert_not_in It's not available in Python 2.6 --- tests/test_generateimage_tag.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_generateimage_tag.py b/tests/test_generateimage_tag.py index e7ea091..c39b794 100644 --- a/tests/test_generateimage_tag.py +++ b/tests/test_generateimage_tag.py @@ -1,5 +1,5 @@ from django.template import TemplateSyntaxError -from nose.tools import eq_, assert_not_in, raises, assert_not_equal +from nose.tools import eq_, assert_false, raises, assert_not_equal from . import imagegenerators # noqa from .utils import render_tag, get_html_attrs @@ -43,7 +43,7 @@ def test_single_dimension_attr(): """ ttag = r"""{% generateimage 'testspec' source=img -- width="50" %}""" attrs = get_html_attrs(ttag) - assert_not_in('height', attrs) + assert_false('height' in attrs) def test_assignment_tag(): From 8fabbae86cbddc6d4a256787e4592e426fddf68e Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Wed, 6 Feb 2013 22:21:20 -0500 Subject: [PATCH 213/213] Extract version from pkgmeta --- docs/conf.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index e0913b9..35f184d 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -11,7 +11,7 @@ # All configuration values have a default; values that are commented out # serve to show the default. -import sys, os +import re, sys, os # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the @@ -45,14 +45,18 @@ master_doc = 'index' project = u'ImageKit' copyright = u'2011, Justin Driscoll, Bryan Veloso, Greg Newman, Chris Drackett & Matthew Tretter' +pkgmeta = {} +execfile(os.path.join(os.path.dirname(__file__), '..', 'imagekit', + 'pkgmeta.py'), pkgmeta) + # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. -version = '2.0.2' +version = re.match('\d+\.\d+', pkgmeta['__version__']).group() # The full version, including alpha/beta/rc tags. -release = '2.0.2' +release = pkgmeta['__version__'] # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages.