pax_global_header00006660000000000000000000000064145051440410014510gustar00rootroot0000000000000052 comment=697b27f0c00f26e3a9c4e482005c3a2518a74622 django-imagekit-5.0/000077500000000000000000000000001450514404100144065ustar00rootroot00000000000000django-imagekit-5.0/.coveragerc000066400000000000000000000001661450514404100165320ustar00rootroot00000000000000[run] branch = true parallel = true [report] show_missing = true skip_empty = true skip_covered = true precision = 2 django-imagekit-5.0/.github/000077500000000000000000000000001450514404100157465ustar00rootroot00000000000000django-imagekit-5.0/.github/workflows/000077500000000000000000000000001450514404100200035ustar00rootroot00000000000000django-imagekit-5.0/.github/workflows/python.yml000066400000000000000000000011671450514404100220540ustar00rootroot00000000000000name: Python CI on: push: branches: [ develop ] pull_request: branches: [ develop ] jobs: build: runs-on: ubuntu-latest strategy: max-parallel: 4 matrix: python-version: ['3.7', '3.8', '3.9', '3.10', '3.11'] steps: - uses: actions/checkout@v2 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v2 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | python -m pip install --upgrade pip python -m pip install tox tox-gh-actions - name: Run tests run: tox django-imagekit-5.0/.gitignore000066400000000000000000000002161450514404100163750ustar00rootroot00000000000000*.db *.egg* *.orig *.pyc .DS_Store .tox .idea .vscode MANIFEST build dist /tests/media/* !/tests/media/reference.png /venv /venv3 /.env /tags django-imagekit-5.0/AUTHORS000066400000000000000000000031421450514404100154560ustar00rootroot00000000000000ImageKit was originally written by `Justin Driscoll`_. The field-based API and other post-1.0 stuff was written by the bright people at HZDG_. Maintainers ----------- * `Matthew Tretter`_ * `Bryan Veloso`_ * `Chris Drackett`_ * `Greg Newman`_ Contributors ------------ * `Josh Ourisman`_ * `Jonathan Slenders`_ * `Eric Eldredge`_ * `Chris McKenzie`_ * `Markus Kaiserswerth`_ * `Ryan Bagwell`_ * `Alexander Bohn`_ * `Timothée Peignier`_ * `Madis Väin`_ * `Jan Sagemüller`_ * `Clay McClure`_ * `Jannis Leidel`_ * `Sean Bell`_ * `Saul Shanabrook`_ * `Venelin Stoykov`_ * `Jaap Roes`_ .. _Justin Driscoll: http://github.com/jdriscoll .. _HZDG: http://hzdg.com .. _Bryan Veloso: http://github.com/bryanveloso .. _Chris Drackett: http://github.com/chrisdrackett .. _Greg Newman: http://github.com/gregnewman .. _Josh Ourisman: http://github.com/joshourisman .. _Jonathan Slenders: http://github.com/jonathanslenders .. _Matthew Tretter: http://github.com/matthewwithanm .. _Eric Eldredge: http://github.com/lettertwo .. _Chris McKenzie: http://github.com/kenzic .. _Ryan Bagwell: http://github.com/ryanbagwell .. _Markus Kaiserswerth: http://github.com/mkai .. _Alexander Bohn: http://github.com/fish2000 .. _Timothée Peignier: http://github.com/cyberdelia .. _Madis Väin: http://github.com/madisvain .. _Jan Sagemüller: https://github.com/version2 .. _Clay McClure: https://github.com/claymation .. _Jannis Leidel: https://github.com/jezdez .. _Sean Bell: https://github.com/seanbell .. _Saul Shanabrook: https://github.com/saulshanabrook .. _Venelin Stoykov: https://github.com/vstoykov .. Jaap Roes: https://github.com/jaap3 django-imagekit-5.0/CONTRIBUTING.rst000066400000000000000000000022571450514404100170550ustar00rootroot00000000000000Contributing ------------ 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, first install tox, ``pip install tox``, then use ``tox``. 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 pytest_, 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 .. _pytest: https://docs.pytest.org/en/latest/ __ https://github.com/matthewwithanm/django-imagekit/tree/develop/tests django-imagekit-5.0/LICENSE000066400000000000000000000030761450514404100154210ustar00rootroot00000000000000Copyright (c) 2007-2008, Justin C. Driscoll Copyright (c) 2011, Justin C. Driscoll and Primary Maintainers 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-imagekit 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. django-imagekit-5.0/MANIFEST.in000066400000000000000000000010531450514404100161430ustar00rootroot00000000000000include AUTHORS include LICENSE include README.rst include pytest.ini include test-requirements.txt include testrunner.py include setup.cfg include tests/*.py include tests/assets/Lenna.png include tests/assets/lenna-*.jpg include tests/media/lenna.png prune tests/media/CACHE prune tests/media/b prune tests/media/photos include docs/Makefile include docs/conf.py include docs/make.bat include docs/*.rst recursive-include docs/_themes LICENSE README.rst flask_theme_support.py theme.conf *.css_t *.css *.html recursive-include imagekit/templates *.html django-imagekit-5.0/README.rst000066400000000000000000000410171450514404100161000ustar00rootroot00000000000000|Build Status|_ .. |Build Status| image:: https://github.com/matthewwithanm/django-imagekit/actions/workflows/python.yml/badge.svg?branch=develop .. _Build Status: https://github.com/matthewwithanm/django-imagekit/actions/workflows/python.yml 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 programmatically generate one image from another, you need ImageKit. ImageKit comes with a bunch of image processors for common tasks like resizing and cropping, but you can also create your own. For an idea of what's possible, check out the `Instakit`__ project. **For the complete documentation on the latest stable version of ImageKit, see** `ImageKit on RTD`_. .. _`ImageKit on RTD`: http://django-imagekit.readthedocs.org __ https://github.com/fish2000/instakit Installation ============ 1. Install `Pillow`_. (If you're using an ``ImageField`` in Django, you should have already done this.) 2. ``pip install django-imagekit`` 3. Add ``'imagekit'`` to your ``INSTALLED_APPS`` list in your project's ``settings.py`` .. _`Pillow`: http://pypi.python.org/pypi/Pillow Usage Overview ============== .. _specs: Specs ----- You have one image and you want to do something to it to create another image. 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/CACHE/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 from imagekit.processors import ResizeToFill 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 ImageFields. .. 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 from imagekit import ImageSpec from imagekit.processors import ResizeToFill class Thumbnail(ImageSpec): processors = [ResizeToFill(100, 50)] format = 'JPEG' options = {'quality': 60} It's probably 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 source_file = open('/path/to/myimage.jpg', 'rb') image_generator = Thumbnail(source=source_file) result = image_generator.generate() .. note:: You don't have to use ``open``! You can use whatever File-like object you want—including a model's ``ImageField``. 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 dest = open('/path/to/dest.jpg', 'wb') dest.write(result.read()) dest.close() Using Specs In Templates ^^^^^^^^^^^^^^^^^^^^^^^^ 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, register 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 %} {% generateimage 'myapp:thumbnail' source=source_file %} 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: .. code-block:: html {% load imagekit %} {% generateimage 'myapp:thumbnail' source=source_file -- 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: .. code-block:: html {% load imagekit %} {% generateimage 'myapp:thumbnail' source=source_file as th %} Click to download a cool {{ th.width }} x {{ th.height }} image! thumbnail """"""""" Because it's such a common use case, ImageKit also provides a "thumbnail" template tag: .. code-block:: html {% load imagekit %} {% thumbnail '100x50' source_file %} 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. 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 generateimage 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_file -- alt="A picture of Me" id="mypicture" %} {% thumbnail '100x50' source_file as th %} 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 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 from imagekit.processors import ResizeToFill 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. Processors ---------- 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 imagekit import ImageSpec from imagekit.processors import TrimBorderColor, Adjust 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 define a class that implements a ``process()`` method: .. code-block:: python class Watermark(object): def process(self, image): # Code for adding the watermark goes here. return image 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} Note that when you import a processor from ``imagekit.processors``, imagekit in turn imports the processor from `PILKit`_. So if you are looking for available processors, look at PILKit. .. _`PILKit`: https://github.com/matthewwithanm/pilkit Admin ----- ImageKit also contains a class named ``imagekit.admin.AdminThumbnail`` for displaying specs (or even regular ImageFields) in the `Django admin change list`_. AdminThumbnail is used as a property on Django admin classes: .. code-block:: python from django.contrib import admin 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) To use specs defined outside of models: .. code-block:: python from django.contrib import admin from imagekit.admin import AdminThumbnail from imagekit import ImageSpec from imagekit.processors import ResizeToFill from imagekit.cachefiles import ImageCacheFile from .models import Photo class AdminThumbnailSpec(ImageSpec): processors = [ResizeToFill(100, 30)] format = 'JPEG' options = {'quality': 60 } def cached_admin_thumb(instance): # `image` is the name of the image field on the model cached = ImageCacheFile(AdminThumbnailSpec(instance.image)) # only generates the first time, subsequent calls use cache cached.generate() return cached class PhotoAdmin(admin.ModelAdmin): list_display = ('__str__', 'admin_thumbnail') admin_thumbnail = AdminThumbnail(image_field=cached_admin_thumb) admin.site.register(Photo, PhotoAdmin) AdminThumbnail can even use a custom template. For more information, see ``imagekit.admin.AdminThumbnail``. .. _`Django admin change list`: https://docs.djangoproject.com/en/dev/intro/tutorial02/#customize-the-admin-change-list Management 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 ========= Please use `the GitHub issue tracker `_ to report bugs with django-imagekit. `A mailing list `_ also exists to discuss the project and ask questions, as well as the official `#imagekit `_ channel on Freenode. 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 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/matthewwithanm/django-imagekit/issues?labels=contributor-friendly&state=open .. _`contributing guidelines`: https://github.com/matthewwithanm/django-imagekit/blob/develop/CONTRIBUTING.rst django-imagekit-5.0/docs/000077500000000000000000000000001450514404100153365ustar00rootroot00000000000000django-imagekit-5.0/docs/Makefile000066400000000000000000000127311450514404100170020ustar00rootroot00000000000000# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = python $(shell which sphinx-build) PAPER = BUILDDIR = build # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " text to make text files" @echo " man to make manual pages" @echo " texinfo to make Texinfo files" @echo " info to make Texinfo files and run them through makeinfo" @echo " gettext to make PO message catalogs" @echo " changes to make an overview of all changed/added/deprecated items" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" clean: -rm -rf $(BUILDDIR)/* html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/ImageKit.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/ImageKit.qhc" devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/ImageKit" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/ImageKit" @echo "# devhelp" epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." $(MAKE) -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." texinfo: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." @echo "Run \`make' in that directory to run these through makeinfo" \ "(use \`make info' here to do that automatically)." info: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo "Running Texinfo files through makeinfo..." make -C $(BUILDDIR)/texinfo info @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." gettext: $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale @echo @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." django-imagekit-5.0/docs/_themes/000077500000000000000000000000001450514404100167625ustar00rootroot00000000000000django-imagekit-5.0/docs/_themes/LICENSE000077500000000000000000000035071450514404100177770ustar00rootroot00000000000000Modifications: Copyright (c) 2010 Kenneth Reitz. Original Project: Copyright (c) 2010 by Armin Ronacher. Some rights reserved. Redistribution and use in source and binary forms of the theme, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * 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. * The names of the contributors may not be used to endorse or promote products derived from this software without specific prior written permission. We kindly ask you to only use these themes in an unmodified manner just for Flask and Flask-related products, not for unrelated projects. If you like the visual style and want to use it for your own projects, please consider making some larger changes to the themes (such as changing font faces, sizes, colors or margins). THIS THEME 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 THEME, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. django-imagekit-5.0/docs/_themes/README.rst000077500000000000000000000013251450514404100204550ustar00rootroot00000000000000krTheme Sphinx Style ==================== This repository contains sphinx styles Kenneth Reitz uses in most of his projects. It is a derivative of Mitsuhiko's themes for Flask and Flask related projects. To use this style in your Sphinx documentation, follow this guide: 1. put this folder as _themes into your docs folder. Alternatively you can also use git submodules to check out the contents there. 2. add this to your conf.py: :: sys.path.append(os.path.abspath('_themes')) html_theme_path = ['_themes'] html_theme = 'kr' The following themes exist: **kr** the standard flask documentation theme for large projects **kr_small** small one-page theme. Intended to be used by very small addon libraries. django-imagekit-5.0/docs/_themes/flask_theme_support.py000077500000000000000000000114761450514404100234260ustar00rootroot00000000000000# flasky extensions. flasky pygments style based on tango style from pygments.style import Style from pygments.token import (Comment, Error, Generic, Keyword, Literal, Name, Number, Operator, Other, Punctuation, String, Whitespace) class FlaskyStyle(Style): background_color = "#f8f8f8" default_style = "" styles = { # No corresponding class for the following: #Text: "", # class: '' Whitespace: "underline #f8f8f8", # class: 'w' Error: "#a40000 border:#ef2929", # class: 'err' Other: "#000000", # class 'x' Comment: "italic #8f5902", # class: 'c' Comment.Preproc: "noitalic", # class: 'cp' Keyword: "bold #004461", # class: 'k' Keyword.Constant: "bold #004461", # class: 'kc' Keyword.Declaration: "bold #004461", # class: 'kd' Keyword.Namespace: "bold #004461", # class: 'kn' Keyword.Pseudo: "bold #004461", # class: 'kp' Keyword.Reserved: "bold #004461", # class: 'kr' Keyword.Type: "bold #004461", # class: 'kt' Operator: "#582800", # class: 'o' Operator.Word: "bold #004461", # class: 'ow' - like keywords Punctuation: "bold #000000", # class: 'p' # because special names such as Name.Class, Name.Function, etc. # are not recognized as such later in the parsing, we choose them # to look the same as ordinary variables. Name: "#000000", # class: 'n' Name.Attribute: "#c4a000", # class: 'na' - to be revised Name.Builtin: "#004461", # class: 'nb' Name.Builtin.Pseudo: "#3465a4", # class: 'bp' Name.Class: "#000000", # class: 'nc' - to be revised Name.Constant: "#000000", # class: 'no' - to be revised Name.Decorator: "#888", # class: 'nd' - to be revised Name.Entity: "#ce5c00", # class: 'ni' Name.Exception: "bold #cc0000", # class: 'ne' Name.Function: "#000000", # class: 'nf' Name.Property: "#000000", # class: 'py' Name.Label: "#f57900", # class: 'nl' Name.Namespace: "#000000", # class: 'nn' - to be revised Name.Other: "#000000", # class: 'nx' Name.Tag: "bold #004461", # class: 'nt' - like a keyword Name.Variable: "#000000", # class: 'nv' - to be revised Name.Variable.Class: "#000000", # class: 'vc' - to be revised Name.Variable.Global: "#000000", # class: 'vg' - to be revised Name.Variable.Instance: "#000000", # class: 'vi' - to be revised Number: "#990000", # class: 'm' Literal: "#000000", # class: 'l' Literal.Date: "#000000", # class: 'ld' String: "#4e9a06", # class: 's' String.Backtick: "#4e9a06", # class: 'sb' String.Char: "#4e9a06", # class: 'sc' String.Doc: "italic #8f5902", # class: 'sd' - like a comment String.Double: "#4e9a06", # class: 's2' String.Escape: "#4e9a06", # class: 'se' String.Heredoc: "#4e9a06", # class: 'sh' String.Interpol: "#4e9a06", # class: 'si' String.Other: "#4e9a06", # class: 'sx' String.Regex: "#4e9a06", # class: 'sr' String.Single: "#4e9a06", # class: 's1' String.Symbol: "#4e9a06", # class: 'ss' Generic: "#000000", # class: 'g' Generic.Deleted: "#a40000", # class: 'gd' Generic.Emph: "italic #000000", # class: 'ge' Generic.Error: "#ef2929", # class: 'gr' Generic.Heading: "bold #000080", # class: 'gh' Generic.Inserted: "#00A000", # class: 'gi' Generic.Output: "#888", # class: 'go' Generic.Prompt: "#745334", # class: 'gp' Generic.Strong: "bold #000000", # class: 'gs' Generic.Subheading: "bold #800080", # class: 'gu' Generic.Traceback: "bold #a40000", # class: 'gt' } django-imagekit-5.0/docs/_themes/kr/000077500000000000000000000000001450514404100173765ustar00rootroot00000000000000django-imagekit-5.0/docs/_themes/kr/layout.html000077500000000000000000000011411450514404100216010ustar00rootroot00000000000000{%- extends "basic/layout.html" %} {%- block extrahead %} {{ super() }} {% if theme_touch_icon %} {% endif %} {% endblock %} {%- block relbar2 %}{% endblock %} {%- block footer %} {%- endblock %} django-imagekit-5.0/docs/_themes/kr/relations.html000077500000000000000000000011161450514404100222660ustar00rootroot00000000000000

Related Topics

django-imagekit-5.0/docs/_themes/kr/static/000077500000000000000000000000001450514404100206655ustar00rootroot00000000000000django-imagekit-5.0/docs/_themes/kr/static/flasky.css_t000077500000000000000000000166601450514404100232270ustar00rootroot00000000000000/* * flasky.css_t * ~~~~~~~~~~~~ * * :copyright: Copyright 2010 by Armin Ronacher. Modifications by Kenneth Reitz. * :license: Flask Design License, see LICENSE for details. */ {% set page_width = '940px' %} {% set sidebar_width = '220px' %} @import url("basic.css"); /* -- page layout ----------------------------------------------------------- */ body { font-family: 'goudy old style', 'minion pro', 'bell mt', Georgia, 'Hiragino Mincho Pro'; font-size: 17px; background-color: white; color: #000; margin: 0; padding: 0; } div.document { width: {{ page_width }}; margin: 30px auto 0 auto; } div.documentwrapper { float: left; width: 100%; } div.bodywrapper { margin: 0 0 0 {{ sidebar_width }}; } div.sphinxsidebar { width: {{ sidebar_width }}; } hr { border: 1px solid #B1B4B6; } div.body { background-color: #ffffff; color: #3E4349; padding: 0 30px 0 30px; } img.floatingflask { padding: 0 0 10px 10px; float: right; } div.footer { width: {{ page_width }}; margin: 20px auto 30px auto; font-size: 14px; color: #888; text-align: right; } div.footer a { color: #888; } div.related { display: none; } div.sphinxsidebar a { color: #444; text-decoration: none; border-bottom: 1px dotted #999; } div.sphinxsidebar a:hover { border-bottom: 1px solid #999; } div.sphinxsidebar { font-size: 14px; line-height: 1.5; } div.sphinxsidebarwrapper { padding: 18px 10px; } div.sphinxsidebarwrapper p.logo { padding: 0; margin: -10px 0 0 -20px; text-align: center; } div.sphinxsidebar h3, div.sphinxsidebar h4 { font-family: 'Garamond', 'Georgia', serif; color: #444; font-size: 24px; font-weight: normal; margin: 0 0 5px 0; padding: 0; } div.sphinxsidebar h4 { font-size: 20px; } div.sphinxsidebar h3 a { color: #444; } div.sphinxsidebar p.logo a, div.sphinxsidebar h3 a, div.sphinxsidebar p.logo a:hover, div.sphinxsidebar h3 a:hover { border: none; } div.sphinxsidebar p { color: #555; margin: 10px 0; } div.sphinxsidebar ul { margin: 10px 0; padding: 0; color: #000; } div.sphinxsidebar input { border: 1px solid #ccc; font-family: 'Georgia', serif; font-size: 1em; } /* -- body styles ----------------------------------------------------------- */ a { color: #004B6B; text-decoration: underline; } a:hover { color: #6D4100; text-decoration: underline; } div.body h1, div.body h2, div.body h3, div.body h4, div.body h5, div.body h6 { font-family: 'Garamond', 'Georgia', serif; font-weight: normal; margin: 30px 0px 10px 0px; padding: 0; } div.body h1 { margin-top: 0; padding-top: 0; font-size: 240%; } div.body h2 { font-size: 180%; } div.body h3 { font-size: 150%; } div.body h4 { font-size: 130%; } div.body h5 { font-size: 100%; } div.body h6 { font-size: 100%; } a.headerlink { color: #ddd; padding: 0 4px; text-decoration: none; } a.headerlink:hover { color: #444; background: #eaeaea; } div.body p, div.body dd, div.body li { line-height: 1.4em; } div.admonition { background: #fafafa; margin: 20px -30px; padding: 10px 30px; border-top: 1px solid #ccc; border-bottom: 1px solid #ccc; } div.admonition tt.xref, div.admonition a tt { border-bottom: 1px solid #fafafa; } dd div.admonition { margin-left: -60px; padding-left: 60px; } div.admonition p.admonition-title { font-family: 'Garamond', 'Georgia', serif; font-weight: normal; font-size: 24px; margin: 0 0 10px 0; padding: 0; line-height: 1; } div.admonition p.last { margin-bottom: 0; } div.highlight { background-color: white; } dt:target, .highlight { background: #FAF3E8; } div.note { background-color: #eee; border: 1px solid #ccc; } div.seealso { background-color: #ffc; border: 1px solid #ff6; } div.topic { background-color: #eee; } p.admonition-title { display: inline; } p.admonition-title:after { content: ":"; } pre, tt { font-family: 'Consolas', 'Menlo', 'Deja Vu Sans Mono', 'Bitstream Vera Sans Mono', monospace; font-size: 0.9em; } img.screenshot { } tt.descname, tt.descclassname { font-size: 0.95em; } tt.descname { padding-right: 0.08em; } img.screenshot { -moz-box-shadow: 2px 2px 4px #eee; -webkit-box-shadow: 2px 2px 4px #eee; box-shadow: 2px 2px 4px #eee; } table.docutils { border: 1px solid #888; -moz-box-shadow: 2px 2px 4px #eee; -webkit-box-shadow: 2px 2px 4px #eee; box-shadow: 2px 2px 4px #eee; } table.docutils td, table.docutils th { border: 1px solid #888; padding: 0.25em 0.7em; } table.field-list, table.footnote { border: none; -moz-box-shadow: none; -webkit-box-shadow: none; box-shadow: none; } table.footnote { margin: 15px 0; width: 100%; border: 1px solid #eee; background: #fdfdfd; font-size: 0.9em; } table.footnote + table.footnote { margin-top: -15px; border-top: none; } table.field-list th { padding: 0 0.8em 0 0; } table.field-list td { padding: 0; } table.footnote td.label { width: 0px; padding: 0.3em 0 0.3em 0.5em; } table.footnote td { padding: 0.3em 0.5em; } dl { margin: 0; padding: 0; } dl dd { margin-left: 30px; } blockquote { margin: 0 0 0 30px; padding: 0; } ul, ol { margin: 10px 0 10px 30px; padding: 0; } pre { background: #eee; padding: 7px 30px; margin: 15px -30px; line-height: 1.3em; } dl pre, blockquote pre, li pre { margin-left: -60px; padding-left: 60px; } dl dl pre { margin-left: -90px; padding-left: 90px; } tt { background-color: #ecf0f3; color: #222; /* padding: 1px 2px; */ } tt.xref, a tt { background-color: #FBFBFB; border-bottom: 1px solid white; } a.reference { text-decoration: none; border-bottom: 1px dotted #004B6B; } a.reference:hover { border-bottom: 1px solid #6D4100; } a.footnote-reference { text-decoration: none; font-size: 0.7em; vertical-align: top; border-bottom: 1px dotted #004B6B; } a.footnote-reference:hover { border-bottom: 1px solid #6D4100; } a:hover tt { background: #EEE; } @media screen and (max-width: 600px) { div.sphinxsidebar { display: none; } div.document { width: 100%; } div.documentwrapper { margin-left: 0; margin-top: 0; margin-right: 0; margin-bottom: 0; } div.bodywrapper { margin-top: 0; margin-right: 0; margin-bottom: 0; margin-left: 0; } ul { margin-left: 0; } .document { width: auto; } .footer { width: auto; } .bodywrapper { margin: 0; } .footer { width: auto; } .github { display: none; } } /* scrollbars */ ::-webkit-scrollbar { width: 6px; height: 6px; } ::-webkit-scrollbar-button:start:decrement, ::-webkit-scrollbar-button:end:increment { display: block; height: 10px; } ::-webkit-scrollbar-button:vertical:increment { background-color: #fff; } ::-webkit-scrollbar-track-piece { background-color: #eee; -webkit-border-radius: 3px; } ::-webkit-scrollbar-thumb:vertical { height: 50px; background-color: #ccc; -webkit-border-radius: 3px; } ::-webkit-scrollbar-thumb:horizontal { width: 50px; background-color: #ccc; -webkit-border-radius: 3px; } /* misc. */ .revsys-inline { display: none!important; }django-imagekit-5.0/docs/_themes/kr/static/small_flask.css000077500000000000000000000021551450514404100236750ustar00rootroot00000000000000/* * small_flask.css_t * ~~~~~~~~~~~~~~~~~ * * :copyright: Copyright 2010 by Armin Ronacher. * :license: Flask Design License, see LICENSE for details. */ body { margin: 0; padding: 20px 30px; } div.documentwrapper { float: none; background: white; } div.sphinxsidebar { display: block; float: none; width: 102.5%; margin: 50px -30px -20px -30px; padding: 10px 20px; background: #333; color: white; } div.sphinxsidebar h3, div.sphinxsidebar h4, div.sphinxsidebar p, div.sphinxsidebar h3 a { color: white; } div.sphinxsidebar a { color: #aaa; } div.sphinxsidebar p.logo { display: none; } div.document { width: 100%; margin: 0; } div.related { display: block; margin: 0; padding: 10px 0 20px 0; } div.related ul, div.related ul li { margin: 0; padding: 0; } div.footer { display: none; } div.bodywrapper { margin: 0; } div.body { min-height: 0; padding: 0; } .rtd_doc_footer { display: none; } .document { width: auto; } .footer { width: auto; } .footer { width: auto; } .github { display: none; }django-imagekit-5.0/docs/_themes/kr/theme.conf000077500000000000000000000001721450514404100213520ustar00rootroot00000000000000[theme] inherit = basic stylesheet = flasky.css pygments_style = flask_theme_support.FlaskyStyle [options] touch_icon = django-imagekit-5.0/docs/_themes/kr_small/000077500000000000000000000000001450514404100205665ustar00rootroot00000000000000django-imagekit-5.0/docs/_themes/kr_small/layout.html000077500000000000000000000012531450514404100227750ustar00rootroot00000000000000{% extends "basic/layout.html" %} {% block header %} {{ super() }} {% if pagename == 'index' %}
{% endif %} {% endblock %} {% block footer %} {% if pagename == 'index' %}
{% endif %} {% endblock %} {# do not display relbars #} {% block relbar1 %}{% endblock %} {% block relbar2 %} {% if theme_github_fork %} Fork me on GitHub {% endif %} {% endblock %} {% block sidebar1 %}{% endblock %} {% block sidebar2 %}{% endblock %} django-imagekit-5.0/docs/_themes/kr_small/static/000077500000000000000000000000001450514404100220555ustar00rootroot00000000000000django-imagekit-5.0/docs/_themes/kr_small/static/flasky.css_t000077500000000000000000000110011450514404100243770ustar00rootroot00000000000000/* * flasky.css_t * ~~~~~~~~~~~~ * * Sphinx stylesheet -- flasky theme based on nature theme. * * :copyright: Copyright 2007-2010 by the Sphinx team, see AUTHORS. * :license: BSD, see LICENSE for details. * */ @import url("basic.css"); /* -- page layout ----------------------------------------------------------- */ body { font-family: 'Georgia', serif; font-size: 17px; color: #000; background: white; margin: 0; padding: 0; } div.documentwrapper { float: left; width: 100%; } div.bodywrapper { margin: 40px auto 0 auto; width: 700px; } hr { border: 1px solid #B1B4B6; } div.body { background-color: #ffffff; color: #3E4349; padding: 0 30px 30px 30px; } img.floatingflask { padding: 0 0 10px 10px; float: right; } div.footer { text-align: right; color: #888; padding: 10px; font-size: 14px; width: 650px; margin: 0 auto 40px auto; } div.footer a { color: #888; text-decoration: underline; } div.related { line-height: 32px; color: #888; } div.related ul { padding: 0 0 0 10px; } div.related a { color: #444; } /* -- body styles ----------------------------------------------------------- */ a { color: #004B6B; text-decoration: underline; } a:hover { color: #6D4100; text-decoration: underline; } div.body { padding-bottom: 40px; /* saved for footer */ } div.body h1, div.body h2, div.body h3, div.body h4, div.body h5, div.body h6 { font-family: 'Garamond', 'Georgia', serif; font-weight: normal; margin: 30px 0px 10px 0px; padding: 0; } {% if theme_index_logo %} div.indexwrapper h1 { text-indent: -999999px; background: url({{ theme_index_logo }}) no-repeat center center; height: {{ theme_index_logo_height }}; } {% endif %} div.body h2 { font-size: 180%; } div.body h3 { font-size: 150%; } div.body h4 { font-size: 130%; } div.body h5 { font-size: 100%; } div.body h6 { font-size: 100%; } a.headerlink { color: white; padding: 0 4px; text-decoration: none; } a.headerlink:hover { color: #444; background: #eaeaea; } div.body p, div.body dd, div.body li { line-height: 1.4em; } div.admonition { background: #fafafa; margin: 20px -30px; padding: 10px 30px; border-top: 1px solid #ccc; border-bottom: 1px solid #ccc; } div.admonition p.admonition-title { font-family: 'Garamond', 'Georgia', serif; font-weight: normal; font-size: 24px; margin: 0 0 10px 0; padding: 0; line-height: 1; } div.admonition p.last { margin-bottom: 0; } div.highlight{ background-color: white; } dt:target, .highlight { background: #FAF3E8; } div.note { background-color: #eee; border: 1px solid #ccc; } div.seealso { background-color: #ffc; border: 1px solid #ff6; } div.topic { background-color: #eee; } div.warning { background-color: #ffe4e4; border: 1px solid #f66; } p.admonition-title { display: inline; } p.admonition-title:after { content: ":"; } pre, tt { font-family: 'Consolas', 'Menlo', 'Deja Vu Sans Mono', 'Bitstream Vera Sans Mono', monospace; font-size: 0.85em; } img.screenshot { } tt.descname, tt.descclassname { font-size: 0.95em; } tt.descname { padding-right: 0.08em; } img.screenshot { -moz-box-shadow: 2px 2px 4px #eee; -webkit-box-shadow: 2px 2px 4px #eee; box-shadow: 2px 2px 4px #eee; } table.docutils { border: 1px solid #888; -moz-box-shadow: 2px 2px 4px #eee; -webkit-box-shadow: 2px 2px 4px #eee; box-shadow: 2px 2px 4px #eee; } table.docutils td, table.docutils th { border: 1px solid #888; padding: 0.25em 0.7em; } table.field-list, table.footnote { border: none; -moz-box-shadow: none; -webkit-box-shadow: none; box-shadow: none; } table.footnote { margin: 15px 0; width: 100%; border: 1px solid #eee; } table.field-list th { padding: 0 0.8em 0 0; } table.field-list td { padding: 0; } table.footnote td { padding: 0.5em; } dl { margin: 0; padding: 0; } dl dd { margin-left: 30px; } pre { padding: 0; margin: 15px -30px; padding: 8px; line-height: 1.3em; padding: 7px 30px; background: #eee; border-radius: 2px; -moz-border-radius: 2px; -webkit-border-radius: 2px; } dl pre { margin-left: -60px; padding-left: 60px; } tt { background-color: #ecf0f3; color: #222; /* padding: 1px 2px; */ } tt.xref, a tt { background-color: #FBFBFB; } a:hover tt { background: #EEE; } django-imagekit-5.0/docs/_themes/kr_small/theme.conf000077500000000000000000000002701450514404100225410ustar00rootroot00000000000000[theme] inherit = basic stylesheet = flasky.css nosidebar = true pygments_style = flask_theme_support.FlaskyStyle [options] index_logo = '' index_logo_height = 120px github_fork = '' django-imagekit-5.0/docs/advanced_usage.rst000066400000000000000000000160041450514404100210220ustar00rootroot00000000000000Advanced 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', 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, model.thumbnail_height)] register.generator('myapp:profile:avatar_thumbnail', AvatarThumbnail) class Profile(models.Model): avatar = models.ImageField(upload_to='avatars') avatar_thumbnail = ImageSpecField(source='avatar', 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. .. _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 saved, 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, 'rb') 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_saved` signal, the corresponding cache file strategy callbacks would not be called for them. django-imagekit-5.0/docs/caching.rst000066400000000000000000000237631450514404100174770ustar00rootroot00000000000000Caching ******* Default Backend Workflow ======================== ``ImageSpec`` ------------- At the heart of ImageKit are image generators. These are classes with a ``generate()`` method which returns an image file. An image spec is a type of image generator. The thing that makes specs special is that they accept a source image. So an image spec is just an image generator that makes an image from some other image. ``ImageCacheFile`` ------------------ However, an image spec by itself would be vastly inefficient. Every time an an image was accessed in some way, it would have be regenerated and saved. Most of the time, you want to re-use a previously generated image, based on the input image and spec, instead of generating a new one. That's where ``ImageCacheFile`` comes in. ``ImageCacheFile`` is a File-like object that wraps an image generator. They look and feel just like regular file objects, but they've got a little trick up their sleeve: they represent files that may not actually exist! .. _cache-file-strategy: Cache File Strategy ------------------- Each ``ImageCacheFile`` has a cache file strategy, which abstracts away when image is actually generated. It can implement the following three methods: * ``on_content_required`` - called by ``ImageCacheFile`` when it requires the contents of the generated image. For example, when you call ``read()`` or try to access information contained in the file. * ``on_existence_required`` - called by ``ImageCacheFile`` when it requires the generated image to exist but may not be concerned with its contents. For example, when you access its ``url`` or ``path`` attribute. * ``on_source_saved`` - called when the source of a spec is saved The default strategy only defines the first two of these, as follows: .. code-block:: python class JustInTime(object): def on_content_required(self, file): file.generate() def on_existence_required(self, file): file.generate() .. _cache-file-backend: Cache File Backend ------------------ The ``generate`` method on the ``ImageCacheFile`` is further delegated to the cache file backend, which abstracts away how an image is generated. The cache file backend defaults to the setting ``IMAGEKIT_DEFAULT_CACHEFILE_BACKEND`` and can be set explicitly on a spec with the ``cachefile_backend`` attribute. The default works like this: * Checks the file storage to see if a file exists * If not, caches that information for 5 seconds * If it does, caches that information in the ``IMAGEKIT_CACHE_BACKEND`` If file doesn't exist, generates it immediately and synchronously That pretty much covers the architecture of the caching layer, and its default behavior. I like the default behavior. When will an image be regenerated? Whenever it needs to be! When will your storage backend get hit? Depending on your ``IMAGEKIT_CACHE_BACKEND`` settings, as little as twice per file (once for the existence check and once to save the generated file). What if you want to change a spec? The generated file name (which is used as part of the cache keys) vary with the source file name and spec attributes, so if you change any of those, a new file will be generated. The default behavior is easy! .. note:: Like regular Django ImageFields, IK doesn't currently cache width and height values, so accessing those will always result in a read. That will probably change soon though. Optimizing ========== There are several ways to improve the performance (reduce I/O operations) of ImageKit. Each has its own pros and cons. Caching Data About Generated Files ---------------------------------- Generally, once a file is generated, you will never be removing it, so by default ImageKit will use default cache to cache the state of generated files "forever" (or only 5 minutes when ``DEBUG = True``). The time for which ImageKit will cache state is configured with ``IMAGEKIT_CACHE_TIMEOUT``. If set to ``None`` this means "never expire" (default when ``DEBUG = False``). You can reduce this timeout if you want or set it to some numeric value in seconds if your cache backend behaves differently and for example do not cache values if timeout is ``None``. If you clear your cache durring deployment or some other reason probably you do not want to lose the cache for generated images especcialy if you are using some slow remote storage (like Amazon S3). Then you can configure separate cache (for example redis) in your ``CACHES`` config and tell ImageKit to use it instead of the default cache by setting ``IMAGEKIT_CACHE_BACKEND``. Pre-Generating Images --------------------- The default cache file backend generates images immediately and synchronously. If you don't do anything special, that will be when they are first requested—as part of request-response cycle. This means that the first visitor to your page will have to wait for the file to be created before they see any HTML. This can be mitigated, though, by simply generating the images ahead of time, by running the ``generateimages`` management command. .. note:: If using with template tags, be sure to read :ref:`source-groups`. Deferring Image Generation -------------------------- As mentioned above, image generation is normally done synchronously. through the default cache file backend. However, you can also take advantage of deferred generation. In order to do this, you'll need to do two things: 1) install `celery`__ (or `django-celery`__ if you are bound to Celery<3.1) 2) tell ImageKit to use the async cachefile backend. To do this for all specs, set the ``IMAGEKIT_DEFAULT_CACHEFILE_BACKEND`` in your settings .. code-block:: python IMAGEKIT_DEFAULT_CACHEFILE_BACKEND = 'imagekit.cachefiles.backends.Async' Images will now be generated asynchronously. But watch out! Asynchrounous generation means you'll have to account for images that haven't been generated yet. You can do this by checking the truthiness of your files; if an image hasn't been generated, it will be falsy: .. code-block:: html {% if not profile.avatar_thumbnail %} {% else %} {% endif %} Or, in Python: .. code-block:: python profile = Profile.objects.all()[0] if profile.avatar_thumbnail: url = profile.avatar_thumbnail.url else: url = '/path/to/placeholder.jpg' .. note:: If you are using an "async" backend in combination with the "optimistic" cache file strategy (see `Removing Safeguards`_ below), checking for truthiness as described above will not work. The "optimistic" backend is very optimistic so to say, and removes the check. Create and use the following strategy to a) have images only created on save, and b) retain the ability to check whether the images have already been created:: class ImagekitOnSaveStrategy(object): def on_source_saved(self, file): file.generate() .. note:: If you use custom storage backend for some specs, (storage passed to the field different than configured one) it's required the storage to be pickleable __ https://pypi.python.org/pypi/django-celery __ http://www.celeryproject.org Removing Safeguards ------------------- Even with pre-generating images, ImageKit will still try to ensure that your image exists when you access it by default. This is for your benefit: if you forget to generate your images, ImageKit will see that and generate it for you. If the state of the file is cached (see above), this is a pretty cheap operation. However, if the state isn't cached, ImageKit will need to query the storage backend. For those who aren't willing to accept that cost (and who never want ImageKit to generate images in the request-response cycle), there's the "optimistic" cache file strategy. This strategy only generates a new image when a spec's source image is created or changed. Unlike with the "just in time" strategy, accessing the file won't cause it to be generated, ImageKit will just assume that it already exists. To use this cache file strategy for all specs, set the ``IMAGEKIT_DEFAULT_CACHEFILE_STRATEGY`` in your settings: .. code-block:: python IMAGEKIT_DEFAULT_CACHEFILE_STRATEGY = 'imagekit.cachefiles.strategies.Optimistic' 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 LazyImageCacheFile file = LazyImageCacheFile('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. django-imagekit-5.0/docs/conf.py000066400000000000000000000201641450514404100166400ustar00rootroot00000000000000# # ImageKit documentation build configuration file, created by # sphinx-quickstart on Sun Sep 25 17:05:55 2011. # # This file is execfile()d with the current directory set to its containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. import os import re import sys # 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 # documentation root, use os.path.abspath to make it absolute, like shown here. sys.path.insert(0, os.path.abspath('..')) sys.path.append(os.path.abspath('_themes')) os.environ['DJANGO_SETTINGS_MODULE'] = 'tests.settings' # -- General configuration ----------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. #needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = ['sphinx.ext.autodoc'] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix of source filenames. source_suffix = '.rst' # The encoding of source files. #source_encoding = 'utf-8-sig' # The master toctree document. master_doc = 'index' # General information about the project. project = 'ImageKit' copyright = '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 = re.match(r'\d+\.\d+', pkgmeta['__version__']).group() # The full version, including alpha/beta/rc tags. release = pkgmeta['__version__'] # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. #language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: #today = '' # Else, today_fmt is used as the format for a strftime call. #today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = [] # The reST default role (used for this markup: `text`) to use for all documents. #default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. #add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). #add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. #show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # A list of ignored prefixes for module index sorting. #modindex_common_prefix = [] # -- Options for HTML output --------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. html_theme = 'kr' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. #html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. html_theme_path = ['_themes'] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". #html_title = None # A shorter title for the navigation bar. Default is the same as html_title. #html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. #html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. #html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". # html_static_path = ['_static'] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. #html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. #html_use_smartypants = True # Custom sidebar templates, maps document names to template names. #html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. #html_additional_pages = {} # If false, no module index is generated. #html_domain_indices = True # If false, no index is generated. #html_use_index = True # If true, the index is split into individual pages for each letter. #html_split_index = False # If true, links to the reST sources are added to the pages. #html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. #html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. html_show_copyright = False # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. #html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). #html_file_suffix = None # Output file base name for HTML help builder. htmlhelp_basename = 'ImageKitdoc' # -- Options for LaTeX output -------------------------------------------------- latex_elements = { # The paper size ('letterpaper' or 'a4paper'). #'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). #'pointsize': '10pt', # Additional stuff for the LaTeX preamble. #'preamble': '', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ ('index', 'ImageKit.tex', 'ImageKit Documentation', 'Justin Driscoll, Bryan Veloso, Greg Newman, Chris Drackett \\& Matthew Tretter', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of # the title page. #latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. #latex_use_parts = False # If true, show page references after internal links. #latex_show_pagerefs = False # If true, show URL addresses after external links. #latex_show_urls = False # Documents to append as an appendix to all manuals. #latex_appendices = [] # If false, no module index is generated. #latex_domain_indices = True # -- Options for manual page output -------------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ ('index', 'imagekit', 'ImageKit Documentation', ['Justin Driscoll, Bryan Veloso, Greg Newman, Chris Drackett & Matthew Tretter'], 1) ] # If true, show URL addresses after external links. #man_show_urls = False # -- Options for Texinfo output ------------------------------------------------ # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ ('index', 'ImageKit', 'ImageKit Documentation', 'Justin Driscoll, Bryan Veloso, Greg Newman, Chris Drackett & Matthew Tretter', 'ImageKit', 'One line description of project.', 'Miscellaneous'), ] # Documents to append as an appendix to all manuals. #texinfo_appendices = [] # If false, no module index is generated. #texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. #texinfo_show_urls = 'footnote' autoclass_content = 'both' django-imagekit-5.0/docs/configuration.rst000066400000000000000000000055641450514404100207510ustar00rootroot00000000000000.. _settings: Configuration ============= Settings -------- .. currentmodule:: django.conf.settings .. attribute:: IMAGEKIT_CACHEFILE_DIR :default: ``'CACHE/images'`` The directory to which image files will be cached. .. attribute:: IMAGEKIT_DEFAULT_FILE_STORAGE :default: ``None`` Starting with Django 4.2, if you defined ``settings.STORAGES``: the Django storage backend alias to retrieve the storage instance defined in your settings, as described in the `Django file storage documentation`_. If no value is provided for ``IMAGEKIT_DEFAULT_FILE_STORAGE``, and none is specified by the spec definition, the ``default`` file storage will be used. Before Django 4.2, or if ``settings.STORAGES`` is not defined: 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, `your default file storage`__ will be used. .. _`Django file storage documentation`: https://docs.djangoproject.com/en/dev/ref/files/storage/ .. attribute:: IMAGEKIT_DEFAULT_CACHEFILE_BACKEND :default: ``'imagekit.cachefiles.backends.Simple'`` Specifies the class that will be used to validate cached image files. .. attribute:: IMAGEKIT_DEFAULT_CACHEFILE_STRATEGY :default: ``'imagekit.cachefiles.strategies.JustInTime'`` The class responsible for specifying how and when cache files are generated. .. attribute:: IMAGEKIT_CACHE_BACKEND :default: ``'default'`` The Django cache backend alias to retrieve the shared cache instance defined in your settings, as described in the `Django cache section`_. The cache is then used to store information like the state of cached images (i.e. validated or not). .. _`Django cache section`: https://docs.djangoproject.com/en/dev/topics/cache/#accessing-the-cache .. attribute:: IMAGEKIT_CACHE_TIMEOUT :default: ``None`` Use when you need to override the timeout used to cache file state. By default it is "cache forever". It's highly recommended that you use a very high timeout. .. attribute:: IMAGEKIT_CACHE_PREFIX :default: ``'imagekit:'`` A cache prefix to be used when values are stored in ``IMAGEKIT_CACHE_BACKEND`` .. attribute:: IMAGEKIT_CACHEFILE_NAMER :default: ``'imagekit.cachefiles.namers.hash'`` A function responsible for generating file names for non-spec cache files. .. attribute:: IMAGEKIT_SPEC_CACHEFILE_NAMER :default: ``'imagekit.cachefiles.namers.source_name_as_path'`` 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. __ https://docs.djangoproject.com/en/dev/ref/settings/#default-file-storage django-imagekit-5.0/docs/index.rst000066400000000000000000000004151450514404100171770ustar00rootroot00000000000000.. include:: ../README.rst Authors ======= .. include:: ../AUTHORS Indices and tables ================== * :ref:`genindex` * :ref:`modindex` * :ref:`search` .. toctree:: :glob: :maxdepth: 2 configuration advanced_usage caching upgrading django-imagekit-5.0/docs/make.bat000066400000000000000000000117531450514404100167520ustar00rootroot00000000000000@ECHO OFF REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build ) set BUILDDIR=build set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . set I18NSPHINXOPTS=%SPHINXOPTS% . if NOT "%PAPER%" == "" ( set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% ) if "%1" == "" goto help if "%1" == "help" ( :help echo.Please use `make ^` where ^ is one of echo. html to make standalone HTML files echo. dirhtml to make HTML files named index.html in directories echo. singlehtml to make a single large HTML file echo. pickle to make pickle files echo. json to make JSON files echo. htmlhelp to make HTML files and a HTML help project echo. qthelp to make HTML files and a qthelp project echo. devhelp to make HTML files and a Devhelp project echo. epub to make an epub echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter echo. text to make text files echo. man to make manual pages echo. texinfo to make Texinfo files echo. gettext to make PO message catalogs echo. changes to make an overview over all changed/added/deprecated items echo. linkcheck to check all external links for integrity echo. doctest to run all doctests embedded in the documentation if enabled goto end ) if "%1" == "clean" ( for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i del /q /s %BUILDDIR%\* goto end ) if "%1" == "html" ( %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/html. goto end ) if "%1" == "dirhtml" ( %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. goto end ) if "%1" == "singlehtml" ( %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. goto end ) if "%1" == "pickle" ( %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the pickle files. goto end ) if "%1" == "json" ( %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the JSON files. goto end ) if "%1" == "htmlhelp" ( %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run HTML Help Workshop with the ^ .hhp project file in %BUILDDIR%/htmlhelp. goto end ) if "%1" == "qthelp" ( %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run "qcollectiongenerator" with the ^ .qhcp project file in %BUILDDIR%/qthelp, like this: echo.^> qcollectiongenerator %BUILDDIR%\qthelp\ImageKit.qhcp echo.To view the help file: echo.^> assistant -collectionFile %BUILDDIR%\qthelp\ImageKit.ghc goto end ) if "%1" == "devhelp" ( %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp if errorlevel 1 exit /b 1 echo. echo.Build finished. goto end ) if "%1" == "epub" ( %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub if errorlevel 1 exit /b 1 echo. echo.Build finished. The epub file is in %BUILDDIR%/epub. goto end ) if "%1" == "latex" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex if errorlevel 1 exit /b 1 echo. echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. goto end ) if "%1" == "text" ( %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text if errorlevel 1 exit /b 1 echo. echo.Build finished. The text files are in %BUILDDIR%/text. goto end ) if "%1" == "man" ( %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man if errorlevel 1 exit /b 1 echo. echo.Build finished. The manual pages are in %BUILDDIR%/man. goto end ) if "%1" == "texinfo" ( %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo if errorlevel 1 exit /b 1 echo. echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. goto end ) if "%1" == "gettext" ( %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale if errorlevel 1 exit /b 1 echo. echo.Build finished. The message catalogs are in %BUILDDIR%/locale. goto end ) if "%1" == "changes" ( %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes if errorlevel 1 exit /b 1 echo. echo.The overview file is in %BUILDDIR%/changes. goto end ) if "%1" == "linkcheck" ( %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck if errorlevel 1 exit /b 1 echo. echo.Link check complete; look for any errors in the above output ^ or in %BUILDDIR%/linkcheck/output.txt. goto end ) if "%1" == "doctest" ( %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest if errorlevel 1 exit /b 1 echo. echo.Testing of doctests in the sources finished, look at the ^ results in %BUILDDIR%/doctest/output.txt. goto end ) :end django-imagekit-5.0/docs/upgrading.rst000066400000000000000000000104601450514404100200510ustar00rootroot00000000000000Upgrading 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 cache file backends), but separates the 'when' control out to cache file strategies: .. code-block:: python class Photo(models.Model): ... thumbnail = ImageSpecField(..., cachefile_backend=MyCacheFileBackend(), cachefile_strategy=MyCacheFileStrategy()) 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 cache file backends and strategies: .. code-block:: python IMAGEKIT_DEFAULT_CACHEFILE_BACKEND = 'path.to.MyCacheFileBackend' IMAGEKIT_DEFAULT_CACHEFILE_STRATEGY = 'path.to.MyCacheFileStrategy' See the documentation on :ref:`cache file backends ` and :ref:`cache file strategies ` for more details. 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 :doc:`advanced usage ` documentation for more. Conditonal ``cache_to`` file names ---------------------------------- 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 ``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 :ref:`specs` for more. Processors have moved to PILKit ------------------------------- Processors have moved to a separate project: `PILKit`_. You should not have to make any changes to an IK2 project to use PILKit--it should be installed with IK3, and importing from ``imagekit.processors`` will still work. .. _`PILKit`: https://github.com/matthewwithanm/pilkit django-imagekit-5.0/imagekit/000077500000000000000000000000001450514404100162005ustar00rootroot00000000000000django-imagekit-5.0/imagekit/__init__.py000066400000000000000000000004261450514404100203130ustar00rootroot00000000000000from . import conf, generatorlibrary from .pkgmeta import * from .registry import register, unregister from .specs import ImageSpec __all__ = [ 'ImageSpec', 'conf', 'generatorlibrary', 'register', 'unregister', '__title__', '__author__', '__version__', '__license__' ] django-imagekit-5.0/imagekit/admin.py000066400000000000000000000024531450514404100176460ustar00rootroot00000000000000from django.template.loader import render_to_string from django.utils.translation import gettext_lazy as _ class AdminThumbnail: """ A convenience utility for adding thumbnails to Django's admin change list. """ short_description = _('Thumbnail') allow_tags = True def __init__(self, image_field, template=None): """ :param image_field: The name of the ImageField or ImageSpecField on the model to use for the thumbnail. :param template: The template with which to render the thumbnail """ self.image_field = image_field self.template = template def __call__(self, obj): if callable(self.image_field): thumbnail = self.image_field(obj) else: try: thumbnail = getattr(obj, self.image_field) except AttributeError: raise Exception('The property %s is not defined on %s.' % (self.image_field, obj.__class__.__name__)) original_image = getattr(thumbnail, 'source', None) or thumbnail template = self.template or 'imagekit/admin/thumbnail.html' return render_to_string(template, { 'model': obj, 'thumbnail': thumbnail, 'original_image': original_image, }) django-imagekit-5.0/imagekit/cachefiles/000077500000000000000000000000001450514404100202665ustar00rootroot00000000000000django-imagekit-5.0/imagekit/cachefiles/__init__.py000066400000000000000000000162741450514404100224110ustar00rootroot00000000000000import os.path from copy import copy from django.conf import settings from django.core.files import File from django.core.files.images import ImageFile from django.utils.encoding import smart_str from django.utils.functional import SimpleLazyObject from ..files import BaseIKFile from ..registry import generator_registry from ..signals import content_required, existence_required from ..utils import ( generate, get_by_qname, get_logger, get_singleton, get_storage ) class ImageCacheFile(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 cache file strategy requires it. """ def __init__(self, generator, name=None, storage=None, cachefile_backend=None, cachefile_strategy=None): """ :param generator: The object responsible for generating a new image. :param name: The filename :param storage: A Django storage object, or a callable which returns a storage object that will be used to save the file. :param cachefile_backend: The object responsible for managing the state of the file. :param cachefile_strategy: The object responsible for handling events for this file. """ self.generator = generator if not name: try: name = generator.cachefile_name except AttributeError: fn = get_by_qname(settings.IMAGEKIT_CACHEFILE_NAMER, 'namer') name = fn(generator) self.name = name storage = (callable(storage) and storage()) or storage or \ getattr(generator, 'cachefile_storage', None) or get_storage() self.cachefile_backend = ( cachefile_backend or getattr(generator, 'cachefile_backend', None) or get_singleton(settings.IMAGEKIT_DEFAULT_CACHEFILE_BACKEND, 'cache file backend')) self.cachefile_strategy = ( cachefile_strategy or getattr(generator, 'cachefile_strategy', None) or get_singleton(settings.IMAGEKIT_DEFAULT_CACHEFILE_STRATEGY, 'cache file strategy') ) super().__init__(storage=storage) def _require_file(self): if getattr(self, '_file', None) is None: content_required.send(sender=self, file=self) self._file = self.storage.open(self.name, 'rb') # The ``path`` and ``url`` properties are overridden so as to not call # ``_require_file``, which is only meant to be called when the file object # will be directly interacted with (e.g. when using ``read()``). These only # require the file to exist; they do not need its contents to work. This # distinction gives the user the flexibility to create a cache file # strategy that assumes the existence of a file, but can still make the file # available when its contents are required. def _storage_attr(self, attr): if getattr(self, '_file', None) is None: existence_required.send(sender=self, file=self) fn = getattr(self.storage, attr) return fn(self.name) @property def path(self): return self._storage_attr('path') @property def url(self): return self._storage_attr('url') def generate(self, force=False): """ Generate the file. If ``force`` is ``True``, the file will be generated whether the file already exists or not. """ if force or getattr(self, '_file', None) is None: self.cachefile_backend.generate(self, force) def _generate(self): # Generate the file content = generate(self.generator) actual_name = self.storage.save(self.name, content) # We're going to reuse the generated file, so we need to reset the pointer. if not hasattr(content, "seekable") or content.seekable(): content.seek(0) # Store the generated file. If we don't do this, the next time the # "file" attribute is accessed, it will result in a call to the storage # backend (in ``BaseIKFile._get_file``). Since we already have the # contents of the file, what would the point of that be? self.file = File(content) # ``actual_name`` holds the output of ``self.storage.save()`` that # by default returns filenames with forward slashes, even on windows. # On the other hand, ``self.name`` holds OS-specific paths results # from applying path normalizers like ``os.path.normpath()`` in the # ``namer``. So, the filenames should be normalized before their # equality checking. if os.path.normpath(actual_name) != os.path.normpath(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 generate() instead of' ' generate(force=True), 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.cachefile_backend ) ) def __bool__(self): if not self.name: return False # Dispatch the existence_required signal before checking to see if the # file exists. This gives the strategy a chance to create the file. existence_required.send(sender=self, file=self) try: check = self.cachefile_strategy.should_verify_existence(self) except AttributeError: # All synchronous backends should have created the file as part of # `existence_required` if they wanted to. check = getattr(self.cachefile_backend, 'is_async', False) return self.cachefile_backend.exists(self) if check else True def __getstate__(self): state = copy(self.__dict__) # file is hidden link to "file" attribute state.pop('_file', None) # remove storage from state as some non-FileSystemStorage can't be # pickled settings_storage = get_storage() if state['storage'] == settings_storage: state.pop('storage') return state def __setstate__(self, state): if 'storage' not in state: state['storage'] = get_storage() self.__dict__.update(state) def __repr__(self): return smart_str("<%s: %s>" % ( self.__class__.__name__, self if self.name else "None") ) class LazyImageCacheFile(SimpleLazyObject): def __init__(self, generator_id, *args, **kwargs): def setup(): generator = generator_registry.get(generator_id, *args, **kwargs) return ImageCacheFile(generator) super().__init__(setup) def __repr__(self): return '<%s: %s>' % (self.__class__.__name__, str(self) or 'None') django-imagekit-5.0/imagekit/cachefiles/backends.py000066400000000000000000000153531450514404100224210ustar00rootroot00000000000000import warnings from copy import copy from django.conf import settings from django.core.exceptions import ImproperlyConfigured from ..utils import get_cache, get_singleton, sanitize_cache_key class CacheFileState: EXISTS = 'exists' GENERATING = 'generating' DOES_NOT_EXIST = 'does_not_exist' def get_default_cachefile_backend(): """ Get the default file backend. """ from django.conf import settings return get_singleton(settings.IMAGEKIT_DEFAULT_CACHEFILE_BACKEND, 'file backend') class InvalidFileBackendError(ImproperlyConfigured): pass class AbstractCacheFileBackend: """ An abstract cache file backend. This isn't used by any internal classes and is included simply to illustrate the minimum interface of a cache file backend for users who wish to implement their own. """ def generate(self, file, force=False): raise NotImplementedError def exists(self, file): raise NotImplementedError class CachedFileBackend: existence_check_timeout = 5 """ The number of seconds to wait before rechecking to see if the file exists. If the image is found to exist, that information will be cached using the timeout specified in your CACHES setting (which should be very high). However, when the file does not exist, you probably want to check again in a relatively short amount of time. This attribute allows you to do that. """ @property def cache(self): if not getattr(self, '_cache', None): self._cache = get_cache() return self._cache def get_key(self, file): from django.conf import settings return sanitize_cache_key('%s%s-state' % (settings.IMAGEKIT_CACHE_PREFIX, file.name)) def get_state(self, file, check_if_unknown=True): key = self.get_key(file) state = self.cache.get(key) if state is None and check_if_unknown: exists = self._exists(file) state = CacheFileState.EXISTS if exists else CacheFileState.DOES_NOT_EXIST self.set_state(file, state) return state def set_state(self, file, state): key = self.get_key(file) if state == CacheFileState.DOES_NOT_EXIST: self.cache.set(key, state, self.existence_check_timeout) else: self.cache.set(key, state, settings.IMAGEKIT_CACHE_TIMEOUT) def __getstate__(self): state = copy(self.__dict__) # Don't include the cache when pickling. It'll be reconstituted based # on the settings. state.pop('_cache', None) return state def exists(self, file): return self.get_state(file) == CacheFileState.EXISTS def generate(self, file, force=False): raise NotImplementedError def generate_now(self, file, force=False): if force or self.get_state(file) not in (CacheFileState.GENERATING, CacheFileState.EXISTS): self.set_state(file, CacheFileState.GENERATING) file._generate() self.set_state(file, CacheFileState.EXISTS) file.close() class Simple(CachedFileBackend): """ The most basic file backend. The storage is consulted to see if the file exists. Files are generated synchronously. """ def generate(self, file, force=False): self.generate_now(file, force=force) def _exists(self, file): return bool(getattr(file, '_file', None) or (file.name and file.storage.exists(file.name))) def _generate_file(backend, file, force=False): backend.generate_now(file, force=force) class BaseAsync(Simple): """ Base class for cache file backends that generate files asynchronously. """ is_async = True def generate(self, file, force=False): # Schedule the file for generation, unless we know for sure we don't # need to. If an already-generated file sneaks through, that's okay; # ``generate_now`` will catch it. We just want to make sure we don't # schedule anything we know is unnecessary--but we also don't want to # force a costly existence check. state = self.get_state(file, check_if_unknown=False) if state not in (CacheFileState.GENERATING, CacheFileState.EXISTS): self.schedule_generation(file, force=force) def schedule_generation(self, file, force=False): # overwrite this to have the file generated in the background, # e. g. in a worker queue. raise NotImplementedError try: from celery import shared_task as task except ImportError: pass else: _celery_task = task(ignore_result=True, serializer='pickle')(_generate_file) class Celery(BaseAsync): """ A backend that uses Celery to generate the images. """ def __init__(self, *args, **kwargs): try: import celery # noqa except ImportError: raise ImproperlyConfigured('You must install celery to use' ' imagekit.cachefiles.backends.Celery.') super().__init__(*args, **kwargs) def schedule_generation(self, file, force=False): _celery_task.delay(self, file, force=force) # Stub class to preserve backwards compatibility and issue a warning class Async(Celery): def __init__(self, *args, **kwargs): message = '{path}.Async is deprecated. Use {path}.Celery instead.' warnings.warn(message.format(path=__name__), DeprecationWarning) super().__init__(*args, **kwargs) try: from django_rq import job except ImportError: pass else: _rq_job = job('default', result_ttl=0)(_generate_file) class RQ(BaseAsync): """ A backend that uses RQ to generate the images. """ def __init__(self, *args, **kwargs): try: import django_rq # noqa except ImportError: raise ImproperlyConfigured('You must install django-rq to use' ' imagekit.cachefiles.backends.RQ.') super().__init__(*args, **kwargs) def schedule_generation(self, file, force=False): _rq_job.delay(self, file, force=force) try: from dramatiq import actor except ImportError: pass else: _dramatiq_actor = actor()(_generate_file) class Dramatiq(BaseAsync): """ A backend that uses Dramatiq to generate the images. """ def __init__(self, *args, **kwargs): try: import dramatiq # noqa except ImportError: raise ImproperlyConfigured('You must install django-dramatiq to use' ' imagekit.cachefiles.backends.Dramatiq.') super().__init__(*args, **kwargs) def schedule_generation(self, file, force=False): _dramatiq_actor.send(self, file, force=force) django-imagekit-5.0/imagekit/cachefiles/namers.py000066400000000000000000000061151450514404100221300ustar00rootroot00000000000000""" 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. """ import os from django.conf import settings 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_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 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_CACHEFILE_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): """ 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_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 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_CACHEFILE_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): """ 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_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_CACHEFILE_DIR, '%s%s' % (generator.get_hash(), ext))) django-imagekit-5.0/imagekit/cachefiles/strategies.py000066400000000000000000000020341450514404100230110ustar00rootroot00000000000000from ..utils import get_singleton class JustInTime: """ A strategy that ensures the file exists right before it's needed. """ def on_existence_required(self, file): file.generate() def on_content_required(self, file): file.generate() class Optimistic: """ A strategy that acts immediately when the source file changes and assumes that the cache files will not be removed (i.e. it doesn't ensure the cache file exists when it's accessed). """ def on_source_saved(self, file): file.generate() def should_verify_existence(self, file): return False class DictStrategy: def __init__(self, callbacks): for k, v in callbacks.items(): setattr(self, k, v) def load_strategy(strategy): if isinstance(strategy, str): strategy = get_singleton(strategy, 'cache file strategy') elif isinstance(strategy, dict): strategy = DictStrategy(strategy) elif callable(strategy): strategy = strategy() return strategy django-imagekit-5.0/imagekit/conf.py000066400000000000000000000030571450514404100175040ustar00rootroot00000000000000from appconf import AppConf from django.conf import settings from django.core.exceptions import ImproperlyConfigured class ImageKitConf(AppConf): 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 = None CACHE_BACKEND = None CACHE_PREFIX = 'imagekit:' CACHE_TIMEOUT = None USE_MEMCACHED_SAFE_CACHE_KEY = True def configure_cache_backend(self, value): if value is None: from django.core.cache import DEFAULT_CACHE_ALIAS return DEFAULT_CACHE_ALIAS if value not in settings.CACHES: raise ImproperlyConfigured("{0} is not present in settings.CACHES".format(value)) return value def configure_cache_timeout(self, value): if value is None and settings.DEBUG: # If value is not configured and is DEBUG set it to 5 minutes return 300 # Otherwise leave it as is. If it is None then valies will never expire return value def configure_default_file_storage(self, value): if value is None: try: from django.conf import DEFAULT_STORAGE_ALIAS except ImportError: # Django < 4.2 return settings.DEFAULT_FILE_STORAGE else: return DEFAULT_STORAGE_ALIAS return value django-imagekit-5.0/imagekit/exceptions.py000066400000000000000000000005501450514404100207330ustar00rootroot00000000000000from pilkit.exceptions import UnknownExtension, UnknownFormat class AlreadyRegistered(Exception): pass class NotRegistered(Exception): pass class MissingGeneratorId(Exception): pass class MissingSource(ValueError): pass # Aliases for backwards compatibility UnknownExtensionError = UnknownExtension UnknownFormatError = UnknownFormat django-imagekit-5.0/imagekit/files.py000066400000000000000000000056321450514404100176620ustar00rootroot00000000000000import os from django.core.files.base import ContentFile, File from .utils import extension_to_mimetype, format_to_mimetype 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 out. It's only extended by one class, but we keep it separate for organizational reasons. """ def __init__(self, storage): self.storage = storage 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 getattr(self, '_committed', False): return self.file.size return self.storage.size(self.name) size = property(_get_size) def open(self, mode='rb'): self._require_file() try: self.file.open(mode) except ValueError: # if the underlying file can't be reopened # then we will use the storage to try to open it again if self.file.closed: # clear cached file instance del self.file # Because file is a property we can acces it after # we deleted it return self.file.open(mode) raise 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 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 @property def name(self): return self.file.name def __str__(self): return str(self.file.name or '') django-imagekit-5.0/imagekit/forms/000077500000000000000000000000001450514404100173265ustar00rootroot00000000000000django-imagekit-5.0/imagekit/forms/__init__.py000066400000000000000000000000701450514404100214340ustar00rootroot00000000000000# flake8: noqa from .fields import ProcessedImageField django-imagekit-5.0/imagekit/forms/fields.py000066400000000000000000000023621450514404100211510ustar00rootroot00000000000000from django.forms import ImageField from ..specs import SpecHost from ..utils import generate class ProcessedImageField(ImageField, SpecHost): def __init__(self, processors=None, format=None, options=None, autoconvert=True, spec_id=None, spec=None, *args, **kwargs): if spec_id is None: # 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, spec_id=spec_id) super().__init__(*args, **kwargs) def clean(self, data, initial=None): data = super().clean(data, initial) if data and data != initial: spec = self.get_spec(source=data) f = generate(spec) # Name is required in Django 1.4. When we drop support for it # then we can directly return the result from `generate(spec)` f.name = data.name return f return data django-imagekit-5.0/imagekit/generatorlibrary.py000066400000000000000000000007431450514404100221310ustar00rootroot00000000000000from .processors import Thumbnail as ThumbnailProcessor from .registry import register from .specs import ImageSpec class Thumbnail(ImageSpec): def __init__(self, width=None, height=None, anchor=None, crop=None, upscale=None, **kwargs): self.processors = [ThumbnailProcessor(width, height, anchor=anchor, crop=crop, upscale=upscale)] super().__init__(**kwargs) register.generator('imagekit:thumbnail', Thumbnail) django-imagekit-5.0/imagekit/hashers.py000066400000000000000000000012661450514404100202140ustar00rootroot00000000000000from copy import copy from hashlib import md5 from io import BytesIO from pickle import DICT, MARK, _Pickler class CanonicalizingPickler(_Pickler): dispatch = copy(_Pickler.dispatch) def save_set(self, obj): rv = obj.__reduce_ex__(0) rv = (rv[0], (sorted(rv[1][0]),), rv[2]) self.save_reduce(obj=obj, *rv) dispatch[set] = save_set def save_dict(self, obj): write = self.write write(MARK + DICT) self.memoize(obj) self._batch_setitems(sorted(obj.items())) dispatch[dict] = save_dict def pickle(obj): file = BytesIO() CanonicalizingPickler(file, 0).dump(obj) return md5(file.getvalue()).hexdigest() django-imagekit-5.0/imagekit/management/000077500000000000000000000000001450514404100203145ustar00rootroot00000000000000django-imagekit-5.0/imagekit/management/__init__.py000066400000000000000000000000001450514404100224130ustar00rootroot00000000000000django-imagekit-5.0/imagekit/management/commands/000077500000000000000000000000001450514404100221155ustar00rootroot00000000000000django-imagekit-5.0/imagekit/management/commands/__init__.py000066400000000000000000000000001450514404100242140ustar00rootroot00000000000000django-imagekit-5.0/imagekit/management/commands/generateimages.py000066400000000000000000000043331450514404100254520ustar00rootroot00000000000000import re from django.core.management.base import BaseCommand from ...exceptions import MissingSource from ...registry import cachefile_registry, generator_registry class Command(BaseCommand): 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 add_arguments(self, parser): parser.add_argument('generator_id', nargs='*', help=':: for model specs') def handle(self, *args, **options): generators = generator_registry.get_ids() generator_ids = options['generator_id'] if 'generator_id' in options else args if generator_ids: patterns = self.compile_patterns(generator_ids) generators = (id for id in generators if any(p.match(id) for p in patterns)) for generator_id in generators: self.stdout.write('Validating generator: %s\n' % generator_id) for image_file in cachefile_registry.get(generator_id): if image_file.name: self.stdout.write(' %s\n' % image_file.name) try: image_file.generate() except MissingSource as err: self.stdout.write('\t No source associated with\n') except Exception as err: self.stdout.write('\tFailed %s\n' % (err)) def compile_patterns(self, 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) django-imagekit-5.0/imagekit/models/000077500000000000000000000000001450514404100174635ustar00rootroot00000000000000django-imagekit-5.0/imagekit/models/__init__.py000066400000000000000000000001341450514404100215720ustar00rootroot00000000000000# flake8: noqa from .. import conf from .fields import ImageSpecField, ProcessedImageField django-imagekit-5.0/imagekit/models/fields/000077500000000000000000000000001450514404100207315ustar00rootroot00000000000000django-imagekit-5.0/imagekit/models/fields/__init__.py000066400000000000000000000113151450514404100230430ustar00rootroot00000000000000from django.db import models from django.db.models.signals import class_prepared from ...registry import register from ...specs import SpecHost from ...specs.sourcegroups import ImageFieldSourceGroup from .files import ProcessedImageFieldFile from .utils import ImageSpecFileDescriptor class SpecHostField(SpecHost): def _set_spec_id(self, cls, name): spec_id = getattr(self, 'spec_id', None) # Generate a spec_id to register the spec with. The default spec id is # ":_" if not spec_id: spec_id = ('%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().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. """ def __init__(self, processors=None, format=None, options=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, cachefile_storage=cachefile_storage, autoconvert=autoconvert, cachefile_backend=cachefile_backend, cachefile_strategy=cachefile_strategy, spec=spec, spec_id=id) # TODO: Allow callable for source. See https://github.com/matthewwithanm/django-imagekit/issues/158#issuecomment-10921664 self.source = source def contribute_to_class(self, cls, name): # If the source field name isn't defined, figure it out. def register_source_group(source): setattr(cls, name, ImageSpecFileDescriptor(self, name, source)) self._set_spec_id(cls, name) # Add the model and field as a source for this spec id register.source_group(self.spec_id, ImageFieldSourceGroup(cls, source)) if self.source: register_source_group(self.source) else: # The source argument is not defined # Then we need to see if there is only one ImageField in that model # But we need to do that after full model initialization def handle_model_preparation(sender, **kwargs): image_fields = [f.attname for f in cls._meta.fields if isinstance(f, models.ImageField)] if len(image_fields) == 0: raise Exception( '%s does not define any ImageFields, so your %s' ' ImageSpecField has no image to act on.' % (cls.__name__, name)) elif len(image_fields) > 1: raise Exception( '%s defines multiple ImageFields, but you have not' ' specified a source for your %s ImageSpecField.' % (cls.__name__, name)) register_source_group(image_fields[0]) class_prepared.connect(handle_model_preparation, sender=cls, weak=False) 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 maintain the original. Useful for coercing fileformats or keeping images within a reasonable size. """ attr_class = ProcessedImageFieldFile def __init__(self, processors=None, format=None, options=None, verbose_name=None, name=None, width_field=None, height_field=None, autoconvert=None, 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 as the ``processors``, ``format``, and ``options`` arguments of :class:`imagekit.models.ImageSpecField`. """ # if spec is not provided then autoconvert will be True by default if spec is None and autoconvert is None: autoconvert = True 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) def contribute_to_class(self, cls, name): self._set_spec_id(cls, name) return super().contribute_to_class(cls, name) django-imagekit-5.0/imagekit/models/fields/files.py000066400000000000000000000007541450514404100224130ustar00rootroot00000000000000import os from django.db.models.fields.files import ImageFieldFile from ...utils import generate, suggest_extension class ProcessedImageFieldFile(ImageFieldFile): def save(self, name, content, save=True): filename, ext = os.path.splitext(name) spec = self.field.get_spec(source=content) ext = suggest_extension(name, spec.format) new_name = '%s%s' % (filename, ext) content = generate(spec) return super().save(new_name, content, save) django-imagekit-5.0/imagekit/models/fields/utils.py000066400000000000000000000012451450514404100224450ustar00rootroot00000000000000from ...cachefiles import ImageCacheFile class ImageSpecFileDescriptor: def __init__(self, field, attname, source_field_name): self.attname = attname self.field = field self.source_field_name = source_field_name def __get__(self, instance, owner): if instance is None: return self.field else: source = getattr(instance, self.source_field_name) spec = self.field.get_spec(source=source) file = ImageCacheFile(spec) instance.__dict__[self.attname] = file return file def __set__(self, instance, value): instance.__dict__[self.attname] = value django-imagekit-5.0/imagekit/pkgmeta.py000066400000000000000000000004071450514404100202030ustar00rootroot00000000000000__title__ = 'django-imagekit' __author__ = 'Matthew Tretter, Venelin Stoykov, Eric Eldredge, Bryan Veloso, Greg Newman, Chris Drackett, Justin Driscoll' __version__ = '5.0.0' __license__ = 'BSD' __all__ = ['__title__', '__author__', '__version__', '__license__'] django-imagekit-5.0/imagekit/processors/000077500000000000000000000000001450514404100204025ustar00rootroot00000000000000django-imagekit-5.0/imagekit/processors/__init__.py000066400000000000000000000005221450514404100225120ustar00rootroot00000000000000from pilkit.processors import * __all__ = [ # Base 'ProcessorPipeline', 'Adjust', 'Reflection', 'Transpose', 'Anchor', 'MakeOpaque', # Crop 'TrimBorderColor', 'Crop', 'SmartCrop', # Resize 'Resize', 'ResizeToCover', 'ResizeToFill', 'SmartResize', 'ResizeCanvas', 'AddBorder', 'ResizeToFit', 'Thumbnail' ] django-imagekit-5.0/imagekit/processors/base.py000066400000000000000000000004011450514404100216610ustar00rootroot00000000000000import warnings from pilkit.processors.base import * warnings.warn('imagekit.processors.base is deprecated use imagekit.processors instead', DeprecationWarning) __all__ = ['ProcessorPipeline', 'Adjust', 'Reflection', 'Transpose', 'Anchor', 'MakeOpaque'] django-imagekit-5.0/imagekit/processors/crop.py000066400000000000000000000003271450514404100217210ustar00rootroot00000000000000import warnings from pilkit.processors.crop import * warnings.warn('imagekit.processors.crop is deprecated use imagekit.processors instead', DeprecationWarning) __all__ = ['TrimBorderColor', 'Crop', 'SmartCrop'] django-imagekit-5.0/imagekit/processors/resize.py000066400000000000000000000004461450514404100222610ustar00rootroot00000000000000import warnings from pilkit.processors.resize import * warnings.warn('imagekit.processors.resize is deprecated use imagekit.processors instead', DeprecationWarning) __all__ = ['Resize', 'ResizeToCover', 'ResizeToFill', 'SmartResize', 'ResizeCanvas', 'AddBorder', 'ResizeToFit', 'Thumbnail'] django-imagekit-5.0/imagekit/processors/utils.py000066400000000000000000000002511450514404100221120ustar00rootroot00000000000000import warnings from pilkit.processors.utils import * warnings.warn('imagekit.processors.utils is deprecated use pilkit.processors.utils instead', DeprecationWarning) django-imagekit-5.0/imagekit/registry.py000066400000000000000000000150321450514404100204230ustar00rootroot00000000000000from .exceptions import AlreadyRegistered, NotRegistered from .signals import content_required, existence_required, source_saved from .utils import autodiscover, call_strategy_method class GeneratorRegistry: """ 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. """ def __init__(self): self._generators = {} content_required.connect(self.content_required_receiver) existence_required.connect(self.existence_required_receiver) def register(self, id, generator): registered_generator = self._generators.get(id) if registered_generator and generator != self._generators[id]: raise AlreadyRegistered('The generator with id %s is' ' already registered' % id) self._generators[id] = generator def unregister(self, id): try: del self._generators[id] except KeyError: raise NotRegistered('The generator with id %s is not' ' registered' % id) def get(self, id, **kwargs): autodiscover() try: generator = self._generators[id] except KeyError: raise NotRegistered('The generator with id %s is not' ' registered' % id) if callable(generator): return generator(**kwargs) else: return generator def get_ids(self): autodiscover() return self._generators.keys() def content_required_receiver(self, sender, file, **kwargs): self._receive(file, 'on_content_required') def existence_required_receiver(self, sender, file, **kwargs): self._receive(file, 'on_existence_required') def _receive(self, file, callback): generator = file.generator # 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(file, callback) class SourceGroupRegistry: """ The source group registry is responsible for listening to source_* signals 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. """ _signals = { source_saved: 'on_source_saved', } 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 SourceGroupFilesGenerator generator_ids = self._source_groups.setdefault(source_group, set()) generator_ids.add(generator_id) cachefile_registry.register(generator_id, SourceGroupFilesGenerator(source_group, generator_id)) def unregister(self, generator_id, source_group): 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) cachefile_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 .cachefiles import ImageCacheFile 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: file = ImageCacheFile(spec) call_strategy_method(file, callback_name) class CacheFileRegistry: """ 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 ``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. """ def __init__(self): self._cachefiles = {} def register(self, generator_id, cachefiles): """ Associates generated files with a generator id """ if cachefiles not in self._cachefiles: self._cachefiles[cachefiles] = set() self._cachefiles[cachefiles].add(generator_id) def unregister(self, generator_id, cachefiles): """ Disassociates generated files with a generator id """ try: self._cachefiles[cachefiles].remove(generator_id) except KeyError: pass def get(self, generator_id): for k, v in self._cachefiles.items(): if generator_id in v: yield from k() class Register: """ Register generators and generated files. """ def generator(self, id, generator=None): if generator is None: # Return a decorator def decorator(cls): self.generator(id, cls) return cls return decorator generator_registry.register(id, generator) # iterable that returns kwargs or callable that returns iterable of kwargs 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) class Unregister: """ Unregister generators and generated files. """ def generator(self, id): generator_registry.unregister(id) 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() cachefile_registry = CacheFileRegistry() source_group_registry = SourceGroupRegistry() register = Register() unregister = Unregister() django-imagekit-5.0/imagekit/signals.py000066400000000000000000000002471450514404100202150ustar00rootroot00000000000000from django.dispatch import Signal # Generated file signals content_required = Signal() existence_required = Signal() # Source group signals source_saved = Signal() django-imagekit-5.0/imagekit/specs/000077500000000000000000000000001450514404100173155ustar00rootroot00000000000000django-imagekit-5.0/imagekit/specs/__init__.py000066400000000000000000000203621450514404100214310ustar00rootroot00000000000000from copy import copy from django.conf import settings from django.db.models.fields.files import ImageFieldFile from .. import hashers from ..cachefiles.backends import get_default_cachefile_backend from ..cachefiles.strategies import load_strategy from ..exceptions import AlreadyRegistered, MissingSource from ..registry import generator_registry, register from ..utils import get_by_qname, open_image, process_image class BaseImageSpec: """ An object that defines how an new image should be generated from a source image. """ cachefile_storage = None """A Django storage system to use to save a cache file.""" cachefile_backend = None """ An object responsible for managing the state of cache files. Defaults to an instance of ``IMAGEKIT_DEFAULT_CACHEFILE_BACKEND`` """ 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_CACHEFILE_STRATEGY``. """ def __init__(self): self.cachefile_backend = self.cachefile_backend or get_default_cachefile_backend() self.cachefile_strategy = load_strategy(self.cachefile_strategy) def generate(self): raise NotImplementedError MissingSource = MissingSource """ Raised when an operation requiring a source is attempted on a spec that has no source. """ 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 = [] """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, source): self.source = source super().__init__() @property def cachefile_name(self): if not self.source: return None fn = get_by_qname(settings.IMAGEKIT_SPEC_CACHEFILE_NAMER, 'namer') return fn(self) @property def source(self): src = getattr(self, '_source', None) if not src: field_data = getattr(self, '_field_data', None) if field_data: src = self._source = getattr(field_data['instance'], field_data['attname']) del self._field_data return src @source.setter def source(self, value): self._source = value def __getstate__(self): state = copy(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. # Unfortunately, this also requires us to add the source getter to # lazily retrieve the source on the reconstructed object; simply trying # to look up the source in ``__setstate__`` would require us to get the # model instance but, if ``__setstate__`` was called as part of # deserializing that model, the model wouldn't be fully reconstructed # yet, preventing us from accessing the source field. # (This is issue #234.) if isinstance(self.source, ImageFieldFile): field = self.source.field state['_field_data'] = { 'instance': getattr(self.source, 'instance', None), 'attname': getattr(field, 'name', None), } state.pop('_source', None) return state def get_hash(self): return hashers.pickle([ self.source.name, self.processors, self.format, self.options, self.autoconvert, ]) def generate(self): if not self.source: raise MissingSource("The spec '%s' has no source file associated" " with it." % 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.) closed = self.source.closed if closed: # Django file object should know how to reopen itself if it was closed # https://code.djangoproject.com/ticket/13750 self.source.open() try: img = open_image(self.source) new_image = process_image(img, processors=self.processors, format=self.format, autoconvert=self.autoconvert, options=self.options) finally: if closed: # We need to close the file if it was opened by us self.source.close() return new_image def create_spec_class(class_attrs): class DynamicSpecBase(ImageSpec): def __reduce__(self): 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, state): cls = create_spec_class(class_attrs) 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: """ An object that ostensibly has a spec attribute but really delegates to the spec registry. """ def __init__(self, spec=None, spec_id=None, **kwargs): spec_attrs = {k: v for k, v in kwargs.items() if v is not None} 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 = create_spec_class(spec_attrs) self._original_spec = spec if spec_id: self.set_spec_id(spec_id) def set_spec_id(self, id): """ 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). If the object was initialized with a spec, it will be registered under the provided id. """ self.spec_id = id if self._original_spec: try: register.generator(id, self._original_spec) except AlreadyRegistered: # Fields should not cause AlreadyRegistered exceptions. If a # spec is already registered, that should be used. It is # especially important that an error is not thrown here because # of South, which will create duplicate models as part of its # "fake orm," therefore re-registering specs. pass 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 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 generator_registry.get(self.spec_id, source=source) django-imagekit-5.0/imagekit/specs/sourcegroups.py000066400000000000000000000142561450514404100224370ustar00rootroot00000000000000""" Source groups are the means by which image spec sources are identified. They have two responsibilities: 1. To dispatch ``source_saved`` 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 "pre-caching" image files.) """ import inspect from django.db.models.signals import post_init, post_save from django.utils.functional import wraps from ..cachefiles import LazyImageCacheFile from ..signals import source_saved from ..utils import get_nonabstract_descendants def ik_model_receiver(fn): """ A method decorator that filters out signals coming from models that don't have fields that function as ImageFieldSourceGroup sources. """ @wraps(fn) def receiver(self, sender, **kwargs): if not inspect.isclass(sender): return for src in self._source_groups: if issubclass(sender, src.model_class): fn(self, sender=sender, **kwargs) # If we find a match, return. We don't want to handle the signal # more than once. return return receiver class ModelSignalRouter: """ 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/matthewwithanm/django-imagekit/issues/126 https://code.djangoproject.com/ticket/9318 """ def __init__(self): 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) def add(self, source_group): self._source_groups.append(source_group) 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'] = { attname: hash(getattr(instance, attname)) for attname in self.get_source_fields(instance)} return instance._ik['source_hashes'] def get_source_fields(self, instance): """ Returns a list of the source fields for the given instance. """ return { src.image_field for src in self._source_groups if isinstance(instance, src.model_class)} @ik_model_receiver def post_save_receiver(self, sender, instance=None, created=False, update_fields=None, 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 in self.get_source_fields(instance): if update_fields and attname not in update_fields: continue file = getattr(instance, attname) if file and old_hashes.get(attname) != new_hashes[attname]: self.dispatch_signal(source_saved, file, sender, instance, attname) @ik_model_receiver def post_init_receiver(self, sender, instance=None, **kwargs): self.init_instance(instance) source_fields = self.get_source_fields(instance) local_fields = { field.name: field for field in instance._meta.local_fields if field.name in source_fields} instance._ik['source_hashes'] = { attname: hash(file_field) for attname, file_field in local_fields.items()} def dispatch_signal(self, signal, file, model_class, instance, attname): """ 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_group in self._source_groups: if issubclass(model_class, source_group.model_class) and source_group.image_field == attname: signal.send(sender=source_group, source=file) class ImageFieldSourceGroup: """ A source group that represents a particular field across all instances of a model and its subclasses. """ def __init__(self, model_class, image_field): self.model_class = model_class self.image_field = image_field signal_router.add(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 and its subclasses. """ for model in get_nonabstract_descendants(self.model_class): for instance in model.objects.all().iterator(): yield getattr(instance, self.image_field) class SourceGroupFilesGenerator: """ A Python generator that yields cache file objects for source groups. """ 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 LazyImageCacheFile(self.generator_id, source=source_file) signal_router = ModelSignalRouter() django-imagekit-5.0/imagekit/templates/000077500000000000000000000000001450514404100201765ustar00rootroot00000000000000django-imagekit-5.0/imagekit/templates/imagekit/000077500000000000000000000000001450514404100217705ustar00rootroot00000000000000django-imagekit-5.0/imagekit/templates/imagekit/admin/000077500000000000000000000000001450514404100230605ustar00rootroot00000000000000django-imagekit-5.0/imagekit/templates/imagekit/admin/thumbnail.html000066400000000000000000000002121450514404100257240ustar00rootroot00000000000000{% if thumbnail %} {% endif %}django-imagekit-5.0/imagekit/templatetags/000077500000000000000000000000001450514404100206725ustar00rootroot00000000000000django-imagekit-5.0/imagekit/templatetags/__init__.py000066400000000000000000000000001450514404100227710ustar00rootroot00000000000000django-imagekit-5.0/imagekit/templatetags/imagekit.py000066400000000000000000000240541450514404100230430ustar00rootroot00000000000000from django import template from django.template.library import parse_bits from django.utils.encoding import force_str from django.utils.html import escape from django.utils.safestring import mark_safe from ..cachefiles import ImageCacheFile from ..registry import generator_registry register = template.Library() ASSIGNMENT_DELIMETER = 'as' HTML_ATTRS_DELIMITER = '--' DEFAULT_THUMBNAIL_GENERATOR = 'imagekit:thumbnail' def get_cachefile(context, generator_id, generator_kwargs, source=None): generator_id = generator_id.resolve(context) kwargs = {k: v.resolve(context) for k, v in generator_kwargs.items()} generator = generator_registry.get(generator_id, **kwargs) return ImageCacheFile(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() and int(d) or None for d in dimensions.split('x')] return {'width': width, 'height': height} 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 force_str(self._variable_name) def render(self, context): variable_name = self.get_variable_name(context) context[variable_name] = get_cachefile(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): file = get_cachefile(context, self._generator_id, self._generator_kwargs) attrs = {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 'width' not in attrs and 'height' not 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('' % 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 force_str(self._variable_name) def render(self, context): variable_name = self.get_variable_name(context) generator_id = self._generator_id.resolve(context) if self._generator_id else DEFAULT_THUMBNAIL_GENERATOR kwargs = {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))) if kwargs.get('anchor'): # ImageKit uses pickle at protocol 0, which throws infinite # recursion errors when anchor is set to a SafeString instance. # This converts the SafeString into a str instance. kwargs['anchor'] = kwargs['anchor'][:] generator = generator_registry.get(generator_id, **kwargs) context[variable_name] = ImageCacheFile(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): generator_id = self._generator_id.resolve(context) if self._generator_id else DEFAULT_THUMBNAIL_GENERATOR dimensions = parse_dimensions(self._dimensions.resolve(context)) kwargs = {k: v.resolve(context) for k, v in self._generator_kwargs.items()} kwargs['source'] = self._source.resolve(context) kwargs.update(dimensions) if kwargs.get('anchor'): # ImageKit uses pickle at protocol 0, which throws infinite # recursion errors when anchor is set to a SafeString instance. # This converts the SafeString into a str instance. kwargs['anchor'] = kwargs['anchor'][:] generator = generator_registry.get(generator_id, **kwargs) file = ImageCacheFile(generator) attrs = {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 'width' not in attrs and 'height' not 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('' % attr_str) 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_attrs = {} tag_name = bits.pop(0) if len(bits) >= 2 and bits[-2] == ASSIGNMENT_DELIMETER: varname = bits[-1] bits = bits[:-2] 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] if not html_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, [], 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) @register.tag def generateimage(parser, token): """ Creates an image based on the provided arguments. By default:: {% generateimage 'myapp:thumbnail' source=mymodel.profile_image %} generates an ```` tag:: You can add additional attributes to the tag using "--". For example, this:: {% generateimage 'myapp:thumbnail' source=mymodel.profile_image -- alt="Hello!" %} will result in the following markup:: Hello! For more flexibility, ``generateimage`` also works as an assignment tag:: {% generateimage 'myapp:thumbnail' source=mymodel.profile_image as th %} """ bits = token.split_contents() tag_name, bits, html_attrs, varname = parse_ik_tag_bits(parser, bits) args, kwargs = parse_bits(parser, bits, ['generator_id'], 'args', 'kwargs', None, [], 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 thumbnail(parser, token): """ A convenient shortcut syntax for generating a thumbnail. The following:: {% thumbnail '100x100' mymodel.profile_image %} is equivalent to:: {% 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 kwargs "anchor", and "crop". To use "smart cropping" (the ``SmartResize`` processor):: {% thumbnail '100x100' mymodel.profile_image %} To crop, anchoring the image to the top right (the ``ResizeToFill`` processor):: {% thumbnail '100x100' mymodel.profile_image anchor='tr' %} To resize without cropping (using the ``ResizeToFit`` processor):: {% thumbnail '100x100' mymodel.profile_image crop=0 %} """ bits = token.split_contents() tag_name, bits, html_attrs, varname = parse_ik_tag_bits(parser, bits) args, kwargs = parse_bits(parser, bits, [], 'args', 'kwargs', None, [], 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) django-imagekit-5.0/imagekit/utils.py000066400000000000000000000110561450514404100177150ustar00rootroot00000000000000import logging import re from hashlib import md5 from importlib import import_module from django.conf import settings from django.core.exceptions import ImproperlyConfigured from django.core.files import File from pilkit.utils import * bad_memcached_key_chars = re.compile('[\u0000-\u001f\\s]+') _autodiscovered = False def get_nonabstract_descendants(model): """ Returns all non-abstract descendants of the model. """ if not model._meta.abstract: yield model for s in model.__subclasses__(): yield from get_nonabstract_descendants(s) def get_by_qname(path, desc): try: dot = path.rindex('.') except ValueError: raise ImproperlyConfigured("%s isn't a %s module." % (path, desc)) module, objname = path[:dot], path[dot + 1:] try: mod = import_module(module) except ImportError as e: raise ImproperlyConfigured('Error importing %s module %s: "%s"' % (desc, module, e)) try: obj = getattr(mod, objname) return obj except AttributeError: raise ImproperlyConfigured('%s module "%s" does not define "%s"' % (desc[0].upper() + desc[1:], module, objname)) _singletons = {} def get_singleton(class_path, desc): global _singletons cls = get_by_qname(class_path, desc) instance = _singletons.get(cls) if not instance: instance = _singletons[cls] = cls() return instance def autodiscover(): """ 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 """ global _autodiscovered if _autodiscovered: return from django.utils.module_loading import autodiscover_modules autodiscover_modules('imagegenerators') _autodiscovered = True 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 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), ) 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() f = File(content) # The size of the File must be known or Django will try to open a file # without a name and raise an Exception. f.size = len(content.read()) # After getting the size reset the file pointer for future reads. content.seek(0) return f def call_strategy_method(file, method_name): strategy = getattr(file, 'cachefile_strategy', None) fn = getattr(strategy, method_name, None) if fn is not None: fn(file) def get_cache(): from django.core.cache import caches return caches[settings.IMAGEKIT_CACHE_BACKEND] def get_storage(): try: from django.core.files.storage import storages, InvalidStorageError except ImportError: # Django < 4.2 return get_singleton( settings.IMAGEKIT_DEFAULT_FILE_STORAGE, 'file storage backend' ) else: try: return storages[settings.IMAGEKIT_DEFAULT_FILE_STORAGE] except InvalidStorageError: return get_singleton( settings.IMAGEKIT_DEFAULT_FILE_STORAGE, 'file storage backend' ) def sanitize_cache_key(key): if settings.IMAGEKIT_USE_MEMCACHED_SAFE_CACHE_KEY: # Memcached keys can't contain whitespace or control characters. new_key = bad_memcached_key_chars.sub('', key) # The also can't be > 250 chars long. Since we don't know what the # user's cache ``KEY_FUNCTION`` setting is like, we'll limit it to 200. if len(new_key) >= 200: new_key = '%s:%s' % (new_key[:200 - 33], md5(key.encode('utf-8')).hexdigest()) key = new_key return key django-imagekit-5.0/pytest.ini000066400000000000000000000001151450514404100164340ustar00rootroot00000000000000[pytest] django_find_project = false DJANGO_SETTINGS_MODULE = tests.settings django-imagekit-5.0/setup.cfg000066400000000000000000000000331450514404100162230ustar00rootroot00000000000000[bdist_wheel] universal = 1django-imagekit-5.0/setup.py000066400000000000000000000042571450514404100161300ustar00rootroot00000000000000#!/usr/bin/env python import codecs import os import sys from setuptools import find_packages, setup if 'publish' in sys.argv: os.system('python3 -m build') os.system('python3 -m twine upload --repository django_imagekit dist/*') sys.exit() def read(filepath): with codecs.open(filepath, 'r', 'utf-8') as f: return f.read() def exec_file(filepath, globalz=None, localz=None): exec(read(filepath), globalz, localz) # Load package meta from the pkgmeta module without loading imagekit. pkgmeta = {} exec_file( os.path.join(os.path.dirname(__file__), 'imagekit', 'pkgmeta.py'), pkgmeta ) setup( name='django-imagekit', version=pkgmeta['__version__'], description='Automated image processing for Django models.', long_description=read(os.path.join(os.path.dirname(__file__), 'README.rst')), author='Matthew Tretter', author_email='m@tthewwithanm.com', maintainer='Venelin Stoykov', maintainer_email='venelin.stoykov@industria.tech', license='BSD', url='http://github.com/matthewwithanm/django-imagekit/', packages=find_packages(exclude=['*.tests', '*.tests.*', 'tests.*', 'tests']), zip_safe=False, include_package_data=True, install_requires=[ 'django-appconf', 'pilkit', ], extras_require={ 'async': ['django-celery>=3.0'], 'async_rq': ['django-rq>=0.6.0'], 'async_dramatiq': ['django-dramatiq>=0.4.0'], }, classifiers=[ 'Development Status :: 5 - Production/Stable', 'Environment :: Web Environment', 'Framework :: Django', 'Intended Audience :: Developers', 'License :: OSI Approved :: BSD License', 'Operating System :: OS Independent', 'Programming Language :: Python', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3 :: Only', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', 'Topic :: Utilities' ], ) django-imagekit-5.0/test-requirements.txt000066400000000000000000000001431450514404100206450ustar00rootroot00000000000000# Test requirements mock; python_version<'3' beautifulsoup4 Pillow pytest pytest-cov pytest-django django-imagekit-5.0/tests/000077500000000000000000000000001450514404100155505ustar00rootroot00000000000000django-imagekit-5.0/tests/__init__.py000066400000000000000000000000001450514404100176470ustar00rootroot00000000000000django-imagekit-5.0/tests/conftest.py000066400000000000000000000003141450514404100177450ustar00rootroot00000000000000import pytest from .utils import clear_imagekit_test_files @pytest.fixture(scope='session', autouse=True) def imagekit_test_files_teardown(request): request.addfinalizer(clear_imagekit_test_files) django-imagekit-5.0/tests/imagegenerators.py000066400000000000000000000006631450514404100213030ustar00rootroot00000000000000from imagekit import ImageSpec, register from imagekit.processors import ResizeToFill class TestSpec(ImageSpec): __test__ = False class ResizeTo1PixelSquare(ImageSpec): def __init__(self, width=None, height=None, anchor=None, crop=None, **kwargs): self.processors = [ResizeToFill(1, 1)] super().__init__(**kwargs) register.generator('testspec', TestSpec) register.generator('1pxsq', ResizeTo1PixelSquare) django-imagekit-5.0/tests/media/000077500000000000000000000000001450514404100166275ustar00rootroot00000000000000django-imagekit-5.0/tests/media/reference.png000066400000000000000000003511761450514404100213100ustar00rootroot00000000000000PNG  IHDR?1IDATxlYmr&}_s{K^I"KF (A2,0 /~'a TK%Tl.yo;uguZp HfP z`{F)3<"H(tl&ʀKJN%)SwMjHa.@#<VMIGHD!AMA `SNAE#FGH35$"hp%i23R Ԋ&,]NniIRkʽ@K.VB`^~7/^Ww6chefL̚q3vv{hRUiW}qZBۡ=[xG{/R lH@3ܣ)nB9:RPn܅Ba %"@?:r rss{_ꁊK"quśKj0lі @ !w2 Q Ü ipD0P@3h_>ALShC Dx¬ H* b$M AD`4Be'p7 q!n^D(s X 9-2M{4Zc&)V䛛/_7:VLJqy5o~Ίfyo|_PQuّ( cBv t%A(""@"Hwgw>şe1u/CiwWM$o/=479 p C  Q*Άp$Ja[X Ɣn]DBR]yRRbx#hHZͼZࠖ$4SGpw'N}a[uZ-6d/^H+UJ*qyuq])2>~ +VT6)VF5*14&BCᆤ#eGE` 1VD"Zogf%5I*tttJ]B=OOeCWH%0lޏ&I')"hW=DCx3a,q3!jU@ZY|~}jf DX,fsrv6OΞ>:[<{lZm6G}=y^[/N/Ky[vW_ W_E7!2Xm"' J{iP \ n AXlNur*FGc &E [?YN_.Ȫ92%eẎǏV^GvПZ͋onbi$~yu!}TV-{gJb@M "cK! FM!P4hnX@}Y  /5DbV)-y$GD;֪z;d+餸yCB,DBUF5J;6<=(TzHQ%n %"U5Tt{}wj3 BH7a|DBsWal|pll 7A?}9qU{͛b8;='~Wg=,[l硧gaX.e: ݠ=%%а`m0Nn.?#uNz(1Y&RC ˋ]ORBsTwpPDz@].d@2R`CTJjrpg"D)^ka$QbWJUpjB8 Q2*QT݃G ejue[ !aRRbQ0  7'#̫TGFb CŠ8sA(ZøILis>=ϫYY-6I~z^a%PGJQ%Ϫ9x5^ӤVr""m+sf@ FDZ- hrI]>)C9Rg'} llNw{ԕߜ6rrZ-uNΌɜd@``OקgR?DcuFׂi7hte7(IDraM*S-Bi ;S%!*"ݠ)S2R-(PGW @΀UrXD?tyuLUHT#H ʾ.(Gƽ___OvU?nOϞtޭ$alŸZ_7٢)2V0N7ۉ5-2=$~B$U_Y][݈N}aaajaUQ!0McOӝ[CI&Br^kaE$D4b""rرh JXT}44&,0dX *j4&xZjd# 0aKC7B"+-S)QŲ_{}t]N> i2 }~ɻe?^aYnwysq|{˯\^]n̾]ZD9Qry\HH7<}ԯNjbY?3C~/O?钦;?=.$>)Ю;sFcpK3Tvl|hn\whV@eRճ'Zs33"[Yp 7˃aW#!\-ZpXq]|Z.s)-AձTEy`PQl?yu7[F@͐9K7@ q}Xq@ܬ/0UnNYJgJ)!P~\KS0h uQM=ٜ?;Ov?XĖwћMN.TVRBDn&"SԠ{ܻ0\e_A#hc +Pm+Z+@s+$k,DU.1* c2\a%!%`3I!Ő=]EHbD PG=:% 47ҧzhT̔r~~l߼x7m~??zλq!`08_Xhe&f$p ³JK珎r n0֟\,Ճ hgǸ5jyYru;Y{G Hl`Nkcb/S<&iJ7wDRLd$\[oo>K5 =}|e/>ǻ"z$ BQؿo)$/͕EҡN/T7.y8$nnb5`]Ju/`nX /VMçqj1iBroމN5F* ̔mAR#~ 0)W(k2¨=f5ħNeVYvJ%P$>9s=ī9}CPݺnZt=ݬ$wy8#WьBg<Ka{lJ972lH5. +BoETa j ÂB?&GvkBtRj N%$(Ct Ѓ::qE1 mJt_ܧfGJ/w.z Oz=__.:^WG_vbd9VT%i0+Q9ղz "~"(D@jO4)a>012moS $#;A>9Nw%,}0Ѭ 4H !mԈI-@JTg , ;,;K@KC]e Q 7XDd*E?ZꆏsDi#! ; DkZXHƏ qZ]iVPLMH (nAJ{ˬ9;"^2oBZ3 ԧ=[/5u-KVI^Wu#~,s[Ǫ'4 +iS+n}X.ww13œ.C..U.+{lJ׷("<YLn5&{}3U6B;,"&ZJ@f2dc-a0E Xu 1`Q'3T4d''n$Q8]R.!YanpMJ(!B]T̀{L=Qg. Q"R!n(nn-fm K&8}|t)E`[(;` 11*4OA iE l4r(x2sjx& HH *R70dv.@XٷY+h@$4ZŌoWXD:"<0E?Nc=87\xO^^T/ p;`?dSC(p:8yH4{vT۩~= B"HOjvDX9i5w6N`Z ARfS)L#8Nc47RJ"(4qwVTbp^p0'Yu Vswa,AE@PUxDX9"{$G+bLe: &2|*r6B{Xm ΃-9`HPeΎ5*"Z3RZ*L RXc@o؈s)6 B63`6(.Tca eo{YqzH}nsJIj@/nbꮹ?GRWw?n:Oz&6xdF "twyaMv:z,K_iޥ¤#D$e7 b군F) a:LcSe8lG*] NA;w7u*S[0&rʩKI6΀L9u)ҦBQ0@蠇vIE91R@R UKejmhM*rw}y^/g:4Myf @jP5f($..´@RRH~ɂ "Ĩ.}!7isJ- ֚́c4 sftJN9f1w<6Wр qoMbv@EFl hyQ4z'ʿ c''?2ȉ0RPR/cRROWZˤSHIS2@i:dM%2ZY ijY,{A 2 %E҉pGJ&j(*E͊hҼ$s4jvy_Hj1M]4}YJN1I2}2r yq{}qu>4E֘\ pVhe wz]G!D42d-}6A^vgؼ&IƱ&}?`;j<2|~ ?"m{huY)R{j?lu?ԣMxwՊڇi@$54$e$BhHNɪ"(@}tdr`0e8au0ڴz D [Y1Q>!T:u KE\PHគ[pر:6`8^ ه|i1[?⿝*$[;bI2g{sLHH5n)IUS6mdʿ <(Ol![@TFxrrQji^COI:Rß\ Rq;<{~燏={vv*~ }s؍INJ̹V-K֧2N{{漎2d~'#O6@jJY`%Dߘe f27bUu4uCDH-Lޔ]I:3T$w}uJ2j2/krx sZ5Ű:Rd=Já볹kd<4:$( R%i[4#(ɧd8r !:WQ[R`1@BIE%6&ëG0w]$#1ZDp@Efݑ 4! O&`hL&^)hjA렙QT2Фh3AmM6 < c$֐x<^][t%ۚ3Ly[?#,OVv{$u `ҞڿwFԓ㤹~;?}fR3Et9;UMHJuRi7o)K=w7͛}jXΖ;#݋ݬNLb:l`4&$k͹N7pT$D@Q*e.V9 }DZG$IIj]jRZba)iJc;t$5#"as3hkB#39 m 4PCr   o@-Rb;׵# ,Q{FEu:4[j*#:CCxι+5,]x{Б=k[̡}8s4,lЉ =KrIݨ$ƒj wσMUl?~9 <սrx'P(#(: QID@5`z_|Ea{'5=~G! $C!nՀ0+Q,Ja+-qG43M)fXy\Hw!E RJ. $fwrOZ^#<"Di<q/dߩϚR n_nEhzl, Ten8@YT=u ԨL (.uwWXGN{IaP4[۠nDZc8bZBy?Lj 1gSL^fr]~,geR-+$+RH]\|~n7 gZ }FvDUЌaN01o^}W? KVR߼vlb1tU*暟2H'?1ƌƱPXSt"@= sg=maBD9$JhJhFi* OWo^ݼ~nwj)Qz V `!I$4zLN~0D@(G{#4(h*mص+6"H@J, @\PY`'[zC1yR;JR 0̞fѡ3VRN*YzaX45`>fq6ݽ?W\@ x^h} J#QLجuYJdW|b:L@/V| ͣM HaRBչC`R13̶ݼH&,|LM=~ f= d9+aH*"Sپ>ۯ\]s+fQ~sr~99U4lJGhzu~/zryyA P`\*^Am%$)CԐJeRjc5%>0F@n#u*%;zJv$-uKw]~~(c7p$J:>X~tsu($2uئ] Üh2 F I۫o^}W큑s:ݖi׷oo0Nq$H[=Ѱ@L"`K"L` IbMS'M.ٙAF$n>1K=o!1wܛ~ʜ7@ =>PkԤ9(<8Kb)zGO.xI9.;I?]L%n:M{/wz 7KY(XÏs\Ǘ>A}P[ww`-cq5$Xjq~B"I$pk[.Z-MǩTYfpD3K%]oet<7LB0RdHR42  6DBn(h4T_,PeZ,-M\n"OdFCFep AcaPXa .YYָa<2!*Fe ]&C$l,?ͭLhi(f;xZ5Ty}{?}IE6VJ뜔D%Zr9``&IpQ}0=R:@IḮIj@Dh!$anmF%-UR>B DhR̯.n~ɰXkr(#*m F#\ u[Xݡpicoocy4Erp3Z%(S%n#s4 DE4I-"0PC R]]PHP"g:%A|s&#oyx?n)!h<%-yEpXsE&CsPm_)}tԡt8 n;%)|mAFjÝm&T#x9r$EC 7@Y}\-',KI,9kYٽ:W{2/T81G/ {[.MyIFg|$ܛP>) o2܃h|H]RO>y>ތ}g)9r%47֭χʃ@RDB{f`Va:η*aqcYy ӹiVe DmxF @1XD;tTU'0H{(:(K(hd`L$oD׸'SM9[kj2?6)9'hx+!O٪0!W}Lz$QjeaPVɄG|H{^?O 5&9ͧSjop嗟}OVC1u?ч?|}YXؖ0E*@S"F#YgJ3JӜ\wF4y ϱ KQ9~Mx1$)Z]5-_>Kh,iW˝q9XEuEYX*H6В")]QIcV!o9<6+Bȼg'"Z/YA/X^ؽڿ^3:[1= *`CG3--0% g8s5 >Wn/dq)\[% JqZN}|uôkeqRS6bQPD ^k\87f]2 d?=}'ϟ$dMAQʖ&œI&8cNH|$ wD%g9T?xL1MೲL ֆ"" I9/2 ڝ`L&D r߈8A.tk6.6gG]kpG`zя7''UzTwvQB8ޤg$HÔ"?;^2ZBpp9T`ˋ]s]7g'f}.A&3)`mC%9]$ݨQ)/PX@v6ۼl,Q7T%iD[hRA8zn5 \PU6fgWwZ?N|Z^ɛr+G8}@7@J@cglj"ao5t_GLfdҏ kAIcâ(ŀQ^xx[jvs мn_n.cڣj͝.ש?nMR2͓tLwq1mB@ v:.6y8; GAU% 5FM~4h{]E|ɿydYpx-aXnrݏ5tOY8acH^7&B M#z֎ flo,Y:IHNכ%U K#(X+Œv5X nnq䍷E=B*hup/-D.[fР湻".)uCzrym_^m_/.~cl_|vZy4אц;17>4oN8s0LA!8J8Nu?Y)P-rҡ(Q'^aS͋,Nf љE-7ja4:^uԠ_MXtY:,G~FzJH!3/2mtcSQv3,ƣ"ܼI!} \^|q7=_oGhNmu=|Ս$ 82Z֔C%[ҊOH87/sNm՚jx}<ɢ!>2TS o‘! :GZx9LmZ v˰U&fXC"R9 Z[DTToh$LOxw˟?~{z̞_Ň;naV6ꏠ[aw!z6c"o ]l8MEׯ/oC.W]~uvJ!nZJ)pn9jggi5VLc]R$ɢle,uH紿C~yy_>.q!6v.9@7&_F==ϒ@HIwrXr~Ɖ!NH2/YM*^o_'J2NXa?(^x>Md noYVSJC~Ӆ#[MR,$$Te =jAJ2  #r[Oy4xR" w0^zCN@7;#~p7|%:8}ww]'}>ڝQMsmg߇;%$u}x7~3ayrt98>`_aN j+YX'Gq{M!Wo|#94ЙVA H@h+b:XD!hxbh;64v{9q9ONN?7~"}SN؜}!0^ݸ2t 6> vu"y0yj|}8ZwwׇiRNն/iN0Dx0a҃.O ww>Z)|Xo/_P,-,i#w Ɨ͋|'h糏S.N#О"B`@Ҫ VhadT#0/>w/>U%IFe:I(f 9㶎ew S)4p[/@_0 Gׯ"/dBЩzB(U#A9*fxlęRsxy,&%5moܣz(Ek0BD_n"Bs<N7ױU?'{b7X)bׅԆa5+ `x2nR87K)w՛۫7ӮTLJڧS~s`<*aiNc_c hʩRNYXQP^ =3X d\9SwNV뗿8/ϟ ';XqzR5H0 Ca%9oq&QF%I \mV,/_jZX͝]9#D=pȺKy+#poׇ1/$w"'Tr>{:"M/+"@! [k?>}9s{Y۩ј%]=Z3:2aMU.C tL-8wdǃAp}ɿETLXwwZnV3_w7W˓y&`ni}fL{Z9Rt{ݫ|t77~蓟nBR ա;X 3ڔA=B ܐHsNr=;\S^eDŽ4;M0Xb=!%0cy[`lXݼ?Mw^^{E+"yNFtmR+B!#yo/)J̯Yh:1H*򼜞-ɡC(Q Ym@aܪ^4e=ۚ68__{.ؘ&dׯ]oGh)Ӌ7={͛kÁ.!QCЩȲbVGX ן bPxۖMA.Չ̽6AuMu,7@dMg"vy=,j)ʰZިF(UmnD|yzoOׁAXuy4E"!;磻}Ea~gO v$e`q`Sɰŀ0D/X@z˫>dH!gϞbo cb<y z7ob9o$Őg1 =D=1)D"KG 6Mk&ߒzۦvnX JNK;=4MVjT~o?O8*q3``w7~p_.뗗ߖqF?\NH^fqzZyy\n`, Xxk7 1Cq3ɢq?^o^J.?Mt_e4-XetYy},e;XJwkATww}fVL(aETR@P(W?lwe) ]{/믝'g:;UBWS-)-5}Su5PG EWz/z{ɫ9jN=}೜ \wdEJ;{[;Rc}!ܕE4tiy^')RV E.j vb՟?\v uoojh^B0]Ⓩ?oMv{Ytր^!.wZ&MXOVCNjџty$QFm%|DG8:PPbz}WƻۛKŇ?})\S܎fvwgIDRԚvW.)!ױMwf$tƜ/7g?d89[v6%2eYyq~xç|pr!]yI$"MVZ7$iˀ{nl's:_zzK%#$ٻUVĽ[_kp-2\fޘ8#gGDGh>ZyY=췇jln I o_^W0 '}WFU5OO}@l}aҥ܉h="=$QjiJhim ?ĸWXl2r۪EχǏOh馌qyJs7Fߗft*j8x?=ŲIKj%{Hv9w?RYZpj{{o^a$e2$dK(Rr{ճO,nᇑџUJg޾Kn%L.}U/"b)Ξ<)9=|p3cg#{IO'}sCf#>&q$85<*Di-c%в*6?W訩\U⨩_M1:zl'F߹zoyrW?뻋#ݰ''yտ:>>XnN5mrH hw%[V=ݛ勮O2x;IXNbwQh >3|iI!KPuV#bUK.3G԰*OZGֈ~&IBM"Reft@,~^nfw5Xϟ?~2'Aq* ܻA6V׷oT_NyT9OXN>ySj[^w><,Vwe}?Y ld/G5Ymt?,|gC' Ϭyio(m0 (qLQ^\_<{#臼:;i,:::Y4gpBÛ/?r7v>K)ibz1Nfs]gnfm pR秉Vϙ djYv}u1i<.]O'OAE`WoDλNk=Z9H)OjnelTx_o~u:]wq^tчw]{.N)őhh}*]h/teX,~stxxZwoF闿^|m.!nX,V ̩->Sš;̈́S{wD=_2&u[yyKjw"-J,hvH O?8m/^~W_zw< B`^rw(iSijjx䝏6gRҋW+S7,{=}|=z%B$Ԏ]˻חZ}vnӿyy{q?JFs)NyGâ'u:$/#JÁjy؏.zj;!!zT&T\$!$Sz۫gOۯ>i^v>/V#- cx:ﶇ_-;zwQjO~Slo^aPCV$ yp',dJ|Rm$3䳟zzE¼D8wmȽ WjMXǓ~cOWGc{FӀpNw*z6lWBjʂ&p~>+)a* b>Nm S5ML 1j#p(r*[n6g>:^ݛ[>Xv~^#fG|8g֭k//jf}uvg3{>fF|1s{[31FEbG8$Mkns}9]|y}}܇zY቉twO`|&"%cyΏ]x4>1a}z7VEq<ڢ~?QZ/@ 7x o.u bӧgO`1!!*NGDfJ5 $:tWwog>Jʱq/?N~o_~ g'?.y /,06JntH)ޙR7 V+Qëaa)-Es;'uF<vחM  Xl]v?աNg'CެƘ|r{8VuZ}n_7DTzٻXO J0ͬ25;|ۑɡ8)9c{B%=E< [sg/_}綿RXDC/bJpѤV`g?wxu̒` H5~wqz'o֋a]椢ZƲ (nG0_T g~iXx"0 3@|*VODzN!pQmL6/J侯w>։*y:]èiyvvݛnjn/Z%yd[2u9"+UбGn0Mu)%_ZM.$Q"琌d.֖tkl#Tռ.\v9nnƋ>.ޜ,v; `{kcAbW>%F` +tpo,cJE\y19ZmRvHcG~a,y } 7J1x#m1&A+ͻnb+w^1461D,vWZ 8(!_mZ!nx*nX{v''H_}"k:oX-zWmpSL6ONro6]q BVnl[(0(SBiM(g\2޲Ij,-WTYC1Lϟyڬv륫ݕb/>E߯}7(͛Wׯ_ ?l!4]5"v{D 1nrծK Kt+UY4Ι-zR[&Ju,FJ|"rWOƝ>KuDcM_|o[' ,-& Uvy tGo}MY(8淴FwO8{Z]6__7_gNE%Z wF7'"L4Gŧo^y_i._~^</y;0GH)5ɻ٧|@Og܋Ic)\Y8`p ݡ|'Op^X=u+tRnlHGTb/vڦF $V7gIEgi:1; ݤqde.gA&_JpZLݸ kj{H'ª%&gk+]^(n +$)t7쁮h۽5(e}J^kݍ0 }*T;˶Z2^/ .74;{ϓgE 8Q0'őPwZ]r~]. | %H_˯>͉S$8N=MeHѡQSlSej,۫WOrq8<~>pP(i*ܟv?O||͟nA/pݽn'I4,$/!qKI )gv߼0,TtGaVbAicqX.Qב@klu>{/W//^^_4_=}nq:oox24lXt]jnZ.\_ƺX]Ξ.>N"C6TClSMyLP @J7pNz8?{vw;-d-Tu/%w]Nq -KMJ:(4Z}:,gOF` 6є1dtbn]hrX|H{?V 9=6<:z"#c޾_t?moȝ(1LnᎡGc="%hG" 3T}G `qmn>{RkIm6Itɮ.PGݬ?N/ptyƮv; cCQ'OEUږ'%v/=L,V,K޵g9"ʬʬf7&)2% Hd}c X׆ol-CٔQ&CU<sk-_|̖"p }^cdڦC߀]ZJDgC9Vyq!8׎4B{7Kr\̢ۤ/{?.մ{/ǚ.Ɯ|fx _m۪2l*^ )ȶn)5W BCdǪ6Cj^@JI|0޳W1|u[XBz(JyUU9mR kGlf ˡn1"uSC +(h\Ok_hnUb'##z^*:S`>.V|;gO*8ۦһl}ޱ͙0*AG8 dX0 %+Y,{4 eIOќ^h즇zw۶OrVk7!*"62 4=6Ä~򳇷u:}]5V30A.L-'.vKPF\Yzr:׾7]0(&Kn֫fyyͼjّqqqrޝ7?|r}h4ˏ?z|RNME`L3Sks6oRګz4@]Q\-'V -y W:Y-5)l9=dFqciڞDȜ??;}hL9]kn߼;h;%[<#)xXpf⻭}ō|%OEٿ>#v]]+ԟPҧxq>ߙ\xӾ=?} v?Q|P^pclAQc\RQkUn6>*BLjAG2':c|1sM2V]&VM"HvI5rr \D,+iMnfb)bv>xE`OId9/||j#.%6dZy簃6]hyrw(@z&` Ϭ?oYs*:"ȼw?=Fe}Y?.MY19Hc4CeJ* DߊeիW3$ŨDVT%֙\TdWIҭVM!0{byN2aӡ,H 7UZMG&Ťo.G[w?__]6.kxDžvr^.ɀt$m'O0J6FͻPbTM_Ưz Vv{yijٶmLr] FK(U#Vb $9JI4J|5v9+wpF_Gn#6>@d3^a dͳxwx ޗGscr^rdGc%HH9&<ʾ**֧vۯWmLqzzVWѭ{u)!f8g&DޣلUw΅@8˿7xʅl\Ξ,/buUbL x5єomJ5"lCsp\I\U8u٣>DR%HFzH&q<]ODjs uT(!^J[vϋSM-lI4_NlQ@R m\Ja_s~zg]H#8(By7 HE.:, p0% BTVTLGϿߟo|z:)gg˥'O϶ۼm49ER a sO~4.n||⼟ܨoM_)ʊA-G٢o7Ϟ>{M~WbQ7Ln4YD[S7\Ƈˋ`kpuqD}rڵ.O ƮJ۴0ͫEL](#]fU 3 j 1Ǩ]8'xrn^+ff-+;#"cRclV"e;E@`6}zX7`O%'wϒM*tZ92ma~j&vYqa + RE,DSH(ͤ׸nΚhTKv؎ kR(I:ϋgҎR?4>/N=y!Yr\^۟̕bJגw{UA<|V8a'Ί>VO1 oN-0F{jhr/Mo vgϦ7s梚fɇ7cqp jsyO<8DTtqvl$~ptT޽wz?Fdչ'gq؛GlKgȕqszyc7^=7jVxEe_^;|J6m{!f%*iͨ*LD.We >)eM*1 R/yW^{-Gz2 ;2MҿsVڐTyw-Urb̀,j=ѐ+gp1T&ч\BbP} *`9ϒ<٬E͔]o؛ J@ͦ<{kJr1M]u_oj?t>W"=hc^c/-y.% r^W̭7'9LGYa'8QRV_`^i;ͭ'U;6}".PioHSvT]^]\4<ߴmw7md[w^ &a5$! CTLS텺d`481tD0ll1z&oäB5E@4hSco;ne<ၹ[bDzpΌ$2L6'_x\j<\aKx%5?i!/^=TC V8&A+h6Q%$C&٪HW߈V=Ihm4>_s׿TO͢9GTqkӞL).ff:U٨fӽb<hBC}>u;6 ͖eg0j 5NO6ߛ|8%&S@̬Ӕ0mU#jlV9y鋪֔'*A`9\ @$ L)f$^RgVfTx^.F%/q>\ۿ;~_N9v%Τfx7(U*sԬj4M."!2"Jv2 /d#λ^4;Ast%FK=i2P,3B&ؤa(8Pmo[e]4ȒFSĘs^toV*=fVNHt~$ *ʁev)wDيĔ{xngϟ<jJ,1`Kd@$cmClF qIwno7\w1˿+hE2_J.kQ٭/obj;Q/F\\O)r]TU?~xv~7MߟÖJڠ`ٝ^ރЇ?k2ΫDLІ ]ج܄!.vYf9 xqʮ,C !Pէ"rWeVoKWDzPFj6X֭0W@ x5 T*s-3 0D>30Hw셧}Dd17 FvpΩf2aAO]^l`ܙlTM.|{Vm[8לMH~yr/) |&gr/(H28DH/cL71v$ʖf~~$?" Pct=xV5`AYns `]hPA[IҧRd/.7nAOfO,?.+wxt岛Le) 8Jfm(zMw*QC s%F(=߼wp_FI>1}f"dMKgiK`٦màp~PwifBGNݒ$lnhd긘Ȉ▣Ev=ngA9B2R5gC2H*1y,-w?'zCR]n쾳;<ӎ Q8["EI6M5&G{h"Ё9;@ dSV0DLJ?/5$l Lh׮_܋*!~y4.@JR15WeuȔPtoQN,>v1+k A]PG s b bR^^t}9S6BzO-ƶ]퉣.nܶg'M;8{Bv|.Y(k(By/8u??;qQ3+B 3T4o5`j u>PK|Y$xS,;WxQibrqK}4xJr<*h<ƄJwS99&ptxL vT%g`~AAHҸ(B40HHri ͒&%4;@HKT3WF DB֨16NzM;Š/u@/&?|ӱQdJ0XVG rn=ѲI &L'ᬚI }@ 00ZRtV~/|\/d߸vuֵ;EY|}OIUVbtt+Ibq|#n79#FFzqZV>mSJ!0B?f%F0#nsM EmB%41؇Syz4Qڷ)g@i[Ř5gFPN:xY] *<FO=]]e|.WJ}(ĈP2vdYv1(U'r찧+?<}Ak}>MchwL@sT?/D;X) r.16-r/F9 adUqѯ.FST#1TLdHU1fRf ѠOWz1 f:0Grs_|"l,w+|HKfb<v(T|a"$-R5_Fݸ` ` ރH()^vA3n('[9/mR5A.z2r{ۯ}H5JĄdx2Sp٤iESMJain>眪37qYaӭWh@; T(vvGgz 9EDD 2awzJS#X"SI4PʟCzCN}o)#0c'Spҿ_y@JFLDFl[ DFtxc7DJ esdP|@N0(ᇆ*] ٰ:J)Y5Ea\T%7ˋÛ}jWgf{y֫PۦO}>;=5pQG%Ixi2.vba09W 0IbWė`&p`ҏT3Zʎ.gh5'Yh:s&rx^m§6r4—M4=g|C3863K2hͻխhf9.Q :*,&3N5ZUVW>&9$>h9Ç,2!q1 S%PVR.S+/-\:22@nJ7"9F.Ǯ舳w #1HIDATS҅(EͦwE?~-j͖MMro]>~{{gǯP9TRS\LjҎ C(֋pOoO>%H'N䣤Qſ6{C#R%2S^ˋo/i1Ǩ僽kýG>Gro_k.zyw6JH TG l;Q+ވ)cJ Ts32V㺲IdV==z 7EY*|%_'s6zcmwtt"}Ӧfv#G<}nԧFĞH]n>\}A9!` C L]R"TY&>',9mbym;&oЧY@ $B *4ib,G9E"?{NnH떼*}YQuqJ*9T.v)T~靾_9/OR}{W_su?}]D68*FnILPE4E9E^_Gٟ]cvk5$^ߴl}2k7uSQVŚQ4EY&jY5$*}n6mbܮW'O 9{J}b[_N»A&V.-p%~>2!6]x $/[mOl}fkjʂQרDjpZ}_`F|\x6uླe``{T*dH3 ?;Ӕ[-ׅٳGO?;sģ*!ryO@PX^޺udz`.aG}MwA 8٬^_(fIa)ök6ߞ^+_vu+ٛw_}j[LfdvѕP@,1 \e\E*p^ .z@ޞ8+@9Fz 7D9$6L@4YZ΍%@RUzZ}r<|Ѽٟ_߬/U+³+}&=ףIvO#lyT߼o]%Lv2R'3<8o~3E Ir°lIz/?i[ $6,K1blS*"WϙAB g(mG]2) h߇M)"sMs7Xl.FIK]87J OO`wbxwh,8B@!!cwj*ywK(*@AyCllUb{Pb٬}$.l/6͹*}TOs޸y[nMQIݧWxMDDD ! WDk> f]}-tx Ç0茇\c7|kؕUP`h5fSS56Pc6r8!:aqq|vdYz2:KȺ0) =֦cT.,yLw*Րn/IF&M_uVW^w|ƻ?Ql7l~(b4+Bݭ.b,FժPM ,_N}9Bxq/Ѥbw҆rUP{*f.ƌboYəB"~~ٳeZ_WG>)6zqfQv1o杽c͏B]];8 I9"R D#u>1Ɣ a>CK18?c'wG~Av7Q=]yr.0,n0sd\^nn겗9RڈvveZ"+#wp Ƭ}3J<}G 0"Be.ǵϹW爸޾/]G0IyP;=L`Bd׵sH 9fܹŢ,& ҄@΁e} ؗ$6k7n|g?;:oEwA3$.X<%l=( D°!^L~vY!Lvw1*ABT;N𢃶]Z  + iΪYcWU+hgrރՔȋP ^%Wmwb(|H3cI4Ushk0LA\=R=4jsM7Gxcd[ e{ək'59Ģ]>O:JP1Hc;닳sQ誓gMPxCqfv!x!*XC3g7\DOB4sD2eVqR?}}߻yk|z1ceBA 1EC.E iˑg%r !dYxj5>80PGBDYobkd \3_ix>BO~lPi(wAxX y`ml'p@2GJ ovl h`:B epfSl3(8+*$ZwOY-eY,(KyW؇U%F<3W5dE5]b4i, G}^&&Si x9D"ȓ017+_6En1bw`BY#($oMbʚ1n"L(y99X4!X@QqP 3Uf7tKs]&:umQ{X=oVEdoZᵶ_ο.Vf'I͈PW`$T= 9z@9sz`P|2I1?oz ۵&foI/\sg:s`NC  Al2RGL'dys~r@yCWiTʴ߸]ߺu%Ғ*(ޜb&j9LqAx1:em.Q!!3 19]bt7&;߮]$v4>xHJwҾTPS0TU5pٓs0S!y)( =sÏD (LMt2I/5M2wuѿWN͏2UvDL&L M^pPn\m^W *D!%fD0(2]k6wKw~aM^D&j"n/OsXm~pm4sC`OxAYhcC91TwRn"Q$wC+ (@a[MG}j&]_^FC88 # D<{S8jD4z?x^E10a .VxD(0cdmn:oӳ[?Z_~W͛U`SؑSS‘i7 PqQ Wc0|1xvLg3qfm^{- D ]Q?ǎڦM'MŮLI8gj9b۷՜jpҞ^~؁Oè f\TKsq\/&4:vUjj&EQ* LLzfGoޡLn`qBwl RIOKfS X+گAʛ\yg!ņM` DP .Er]1-o~2mC҇GQh[el#Jrpx8;?Z,.rZ[wƇe(+9"sزP{E)@Q. \Hgv̎ 9X̬vT!#?y_$2DD(j(jiԓdڵ/ fd4fEصvWx ?DNedL0ݹU ԁ@;dnZ"?HO90e,:8%} st>T>+ȲN)Me/ݽ{ͿM72IFG#bs(*r„f=_ORT\Ŕ"3#rES+Ff' \; R D&Z)@ ` ]`lhtVU/S3bb7}W5ÔdI!E]k]~銎4Im.</Ҳos7{xx!\GdNչ}E/NN,Ϩu_=½cB"Njٍjњxfr,`ƎU$;Q0!Q*ޗd a ,VUeL@1fӉw/8wP ]6hAa|οR3+qg*߻ \$ /L 0S3O{'Ͽ_KnYVIZjMDiT(DF,G>n䕛1gW쉜QIn̮r~d~8?{@DK5$pQyGԇ*xOBQaj tPEF\\]ο gsL ftj zk^}+\JEY{v%%Lz d63dS1j&"K2BjIXPٍfL4OGv@bwqn?{tNo9;";w_>[MlF4[6&S5bbfG$Hz9_8gD*>vQ9f,}1jLI>Qbc9Zm\ލo);;!0^F<;;[B]I U'=/^g@Gd-cXx [a@n&d HOY''XnS}iolMT$i6UϞ^JLvMnڤrշqt+ر3tEjJc<[o~ˆgs]-}`Ȕ9WHP'r+>>Wwu aԃI  3\f f 9o}1bW9 .t+Dlh /\ޮrۉsqV28ֵfR(QΒ O|47W`+X4D/qsoo񄂡݈u zLw<|vmG䌨,DF̐ƌ`QYJMv]J)ScV5N9i91ɳo꟣Py.a5ټ;2^ûv(i!:`fQ }Ԉd% $d$p..gYIx~8o)ig؇'tCfR%MFwH3/k%+T,_1WcJe>.-B΁K]Qذw0هn~AK[3cEA#+(H:4Mҝ(ƾPbML\4f #LL rmSNbIMET.db;'R^m__\Ƈ[s:Q(l(珷]>J?7~>z[Kg JΉ y1> )e$Ó"I9g*&ռgv>('J8MY]7s<"&3.yFOWeCN(zU@fL4ӎt;I-3 [I~ryi9Ů[=O.ܧ_(ß4m|h K6iUG!r|/yw~r;'~zzygryQ[;zF5}Q)KIKȑcreB{?LW}' [t:yC,fqu:SWק"+ \a~`  49#e_ 琙&L0aLRNZV"gfLhJIM'9piIH"SLl9i"e `ӱrṘLukm,"ӣrαӊ\|x뛿ⷃ+ʉO{{|ƭ0OITs~׍^}{,'z;?>#X.ö{ݦ ǥ1Y YP=*#x4x}Y_kO/''\MGE8ݫ&"kl"Pyv+ɁH4׼QFnzN:-}0.ޑweAat4ab͋ۓOضDPe Ss{2\VX;C0h =.CpDE10l2@::Y2s Jf2' 昉J5Ӱ^1&YL5Ѡ#RN{3U3jSh#jart8={7u1t)³Y"")ʬ;|W~q6α\1PM* &} y'?;|KY3Sda)tH)`ZbDEja_-ꕯ}ػf=톞 ݉vԣW̬DTyf&&agKů)@)em[ٙ]r'^3Ï6|,f }?EPd~7nޜ]R_>ygnlܘk&IM0ds hLΑ6..4;fgI聶UHS{~ hLNeϗ}Zݜ;wdC6D.f"9xsŽw;! ؁K [F(fdj=xBJ(hSأ,a6b4K,nK9g~0|.&B ML%{*i20+קG;þوyWVURBbǮh6+WgTň}qƗCgJV#A$hM=(7emT,,!9ͩhJp*50Wz}̘H~%2 Lk+ِaQҡR#VR 4݈a{bOYNX<;жtSt;?ȫ.yuKgگ\?Oo/_siAޞ?iɬcZΌХ!\OOѾF~Y%v0u2yS2c.>;ꛋM5.{@ֹNbIjfc+xf+ ց"1>\ЫPZ!T%%t)5"yO,@7YcD)ENjח*څrezIRϖ<ፗn5 V51;I}N&·Ե[fPT\pH-TI jჺLR꼫@.%X$ƇlVh,3̀IV\0 ?\*f5t0 V%* 5#V*#h9gb8e2CiU4^}>_>'j;|-//S'nwNH t矆88G_x5ݬĖ/zڗlml>ol~}կF%4hr4M8Gm|Ȳvl vfB 0r9ff]h˾%TAs_h.NBHFIt)}y~? MNjҦ{i(6MW T.~Gӯu45:Û q O$Q:!HS5(e)߶H5{9rD)K7n^{Ku_FP4f}^Vcɉ| ifI0Hi+zI@HIj*z~bf$mj`|4;qjfa9:f20(sUE;罫&c?Eq/M{_d`@4 b&fxqaW^aeBDsn蘠dp&`LoB]%4«w~ 4.}ٞZdfM.qگ:l7k.ˏapQ[>y3gNϟ_4G?z~$:^6`짳PC. MGߊϖ=z&S/Y"ʹmkQo}H{q]Vبs@T7}\R: Č/Q?,` {۟HE{\c#3Qǥ$bP#.vXF%s9X&FPM}J3;Ǫ_yu5y,&2SHZn/oe](L\\^qȡ:.&s9{֜w;[2d~G>ŬF_(ק?*(+?yoꍭIpnTt1i"ʘ@ R=GQ cA CR)3{jFNHbE9FUC}q8-V =3IN&Ƴvwޠ١@q59z5ˋܝGRQ6ePhvY^|NPNG>2P_*ԹP88Gfbtd{rףY=Gޤb2qŌ]Mb1k;bx;ŏ!L] `°55ʮ@oMf5# t0J/?5ov۬6nJ&^Vezt'#Q!d7 }˯}O>ُ: ePd\9W0vTBnMƮH̀9 ɍ;no\nMȱ/kzr"`. goR\U f\9>}T-mKa`V_(ƾx]5Pʦqݧ乐ꑟHh=ٯV/R|P6)1u}E[`>8ɍH $t 38vCxP4çr.urҘb׉NQ4Oo~|-SUeW}>~Ͼ=ĒoM|qM߼ί_;qf  q6%rjjvK[Z΅}O7hrQEZ^4I+R:m:ʊk.1n>FmsΖ[+evަe\llmmg٤7k hGBr{@8^I\K@aeќc]7X,fC.k#23 !X_o|u\qyͺ%b()zl9'2 E,N|`|t3a슱8}Sj.NggCD~Q<]G(vڟw/˟o>*^6X)`l;ôf1B` \L/jgP lW9Ut+>hbuߛ,fi\rN5g6+UNݹ=-Qڥ`oݸu Ѻ};/f=^cd7nQWv8n~'Ж\QCd<Y lDⓧqݗ'Zs.}z^OnܻK72?qt2?D](,+_$Y92j"Sݞd׻Aon\NeLvZ1bdRw9,<mt6v\ ˭GQS\-ÝKzw؇nCҥ$D Yv#Ǩ>`yoQ18wzk/D$; |E /Zr~>j ~[ÂtoPO=k,H>)S4w>̺woɉG.i>sS%rnVgڪ{kM./6ƳI6}NFլ1>=eZrf/C3V)6FP @xeyFV1W_ɟ?ÉUd kĘF25 |J6U&7lgc볖ͭZxR 'ޔy8ZD')!"6,D!$:0RF\ L{w-P4ȍ4'7I59G6;'rNJdw;F&/d O-BT^*\8 '\uUtRdXCL iEYlrpDgq_廀؎Ҕ 8qc5_ nN7;Ye B?lo"2rJ7sGYY|YCm'II Ɏ|loE l}QBOE). }t@jJulqu+,e|&7>Sy5tipo4|BM|"r1I߷A?Snп$z JUWDQqlpTdcf}_ P )V>ӷI*RV).fY',˩@(ʷy P&kkpb`D9 sHFiERmU6($ #* MdVuugwE9T41^E3~,@ R\<]#<՟馼kctG(3@JD %1ItʨUfT٬fbmf|ޝ,cMsݲЀFq+ jZ<~/j;®*WFV"'$n.d[ i08>5)zB)ZD.4; O:xQ=en~tFJ4\Л?BH654خJ!5+_3vuEZ-Bƶ{{>~nggkT$UJ B6|ל &"duVm]"V%D/]ζ/M3q}yzZ;S|]lgL[;2tQ  I>+hKY|lY:Ju_ϨmOyK!HODlD & `DQZKJ!$Dg)%0'D߮7́P[PD2*_2J|bTk:)ˁp~)5Y[Ţ>]{J]ԫ8E6WEo)^eYlX7YarR*~s@Pܼ16EFu6{[Om_6lCm<66vLg(_-EgH/)AJ}2WuBxh\7iͨ"((a}恋_^4`詭#G߀]Ꮚ?S}cX C]l"GP&vO.,wmXV͇Ol75a@T&,%cQH ԖP7x( JTgzCPdSBJ&_b>]7?A![> <ڨ~_B}q:! 'N!G]!j  NT {Z{K—nzUe=ګ_;}noFD?`ISmFH">Rd"^YE.>h@D jZC6}9K- %hBlkM%0 ֧s6I%jyvllqpTOfaٻ2ĐBʲ,닰O #1ej)Qm]:8'snMٻy+e0Wn<{UGTټwPRſ8%$BΧJmelTr%53y[iM^@mi K" S}`')JP SIẗ$!1DN) #rFp vmU-٬mtgc F@U$Oz7;8# $YAkmRtbI* He'cP%2YJw# |AQ_I4Nw7ۡN(J R" X1(PAZO]l! EK֞֔$tᡀ h-~Ϋ=#C˷ok\\?̭=o>LCH>l74q4,:6Gu3B bQrxVԝ?;y֓MFН8z^&u7Fҍœ&Uݳ'd~皺ŕKۨaa:{կ~ꮱYϷRd7?893\l+Bз2Bm˳z/GY{EpR0" JAe檙ρL⤴ZQ ^M*d! Rjqw CYB蚺NmL η`{ uN>?uca@hG{BJ`攒Y/ybcsQC1J&dj Se;F FgW^_(r)i4,6ƓJ 9uȚ(hWSry9dvy`ǘH+:h"c?/x=D$HZp=Wpa\ "zԎrЩnW)- !涃ZmkuncXI=v,U[t,AŝMי)!{Z\Mfou1dʘxb#Oz5M"p޻Eo r)E|WUX t-&v6oh=:؛V9&{Osju􆣢ߏ!8x}5׹jPETD)1$DZ)ő㺒~ 'AW71|3ޮ2 {~q웿ߌtɩiO1!Qh*2_D$|Pb1V%r 1 3H-0jv5=:i] 2Ռ "phSrKe'+e211̬S@BBDQ]Da T ?Tu[59ZݎOAӟ?~xm,(EZRL HVl'sm3{q<~P I,ȦU`7GU=7e?[^?xӟ߿[x XiQ .ݸ@tX2!6 tK'v^ ]NO~0yx1p ~9_.Wu* 3h_g>Nx\LGd9yxy .z 8vNO )S1Egh2DffmL f&"qR!Rpjn -j&ϟ^-у;w .Y~qRQ^͎;=:R;wWy0if$yíkvuX +0K?O}M^ ;ߎ]hHr%Σq;:į@l)/箪nyVB :J !0'"ݐSnƑ%jƘQY(j'*[tz*/@8@)e5㣻âYvq)G`D\U`hn|pl^ڲk;Ziba%h!&Җ45ʘ mB@DPh:Ḱ udNu>zx ׹6M>K2f$+-7 _ܾɷ^kjС< A"P ʾ~XxgҬɝkGo=^B\۞>gyt٭py|Y-bc}15YuJ:~t_gUgcvBꗝiw11ڣs |͝g$.hJ@ " () Z EPƄJnXNmNFN qgJ3>}PdR3_߃G!Е1 I"2`+_-&gEI1,=~P3="R Į"iQf22ʘ1hu%hmœFL)&(C\XZכuk.LOO9~J|ϟޒ? z(Sc{eo?iTK[ V{`;?sv?=i1EL"Pybb؈K'o_ L{ôz2VaU1b H@La]Y8|_.jĥgWt?'ST*RlGϗӹ2;ΕnEZvn:tz n4)]DO!j QbtZWbftx_fFQ} BuhQo,ߺ/$ +!*h HEBh#2VBTJc!plqA굫O{اޞuDyxvkNA%,,u?gxNMl$Dc2Rtx3=gsr!uɯ3 Ɋ/zݰ T !!9ħ_{[?Jiv HUT ŚFX!- #`F$zbx|oWMkP!; : rhJ ^E%뢃"!$.C M&6_^ح.:M)Fl^*%[!18m5Lj%$L)FIӦS̚<_4[KȧƊPfdDҚm[/<}'r;?{r?%yq9Gʲv !)`^ͦL{zZ$(*NfT FBjū84J)aCmZqiTnR6Z,+^_zNf0BLd9iƦ9|>#?]XF+.?Us2Bm˅ Փ'j9yNU\H%Nf>1YerY-m+ešLX)~5[Nj%HS@ @%)m5=']pME@؏%x_oA$A`֯ ^.PpGIY^zكͭ0LOLf ibjF7:YcDǶhkOcgN sbd6q#' Qb.~ڷ._LBQP1 6yV%`jt NNB!-&48GU ͭowwo?ȤbFh?9tR")!kaO z j||^mc1!I"v!U_e!Z '"9YJ(16DYY\S''.oO*}b\ ;bS^dh66QfgcZMT'\g8Itl-@\]&ںrn8|Ї`]P N`eQyލ>?w:6˔*RJ礵ҹ26!Q!re5/ 㛧m rX\lzƬ$k?E9 OlS֫T`a'{O2Ivrx,jF(4EP*)(Dm/a3bJEޝ!zQ/δ$HS]ڽG7&WuPaL.Ppz\Atӳ9x3_@J{]ee k;?~{6NHUC NtT}҈/2i#δN:0t>Ә~f{$M:?b2Q|3~bw^݇\-tN@YN@:E` Դ0Kgb})Mھ͡ STm\D čg*@ MeW$?rsRS4Y6; Ѩ!n /޿1r@I{ߨC-reBF]kIbt*I SeË>>=W~?q?LDWsK1xza(L$#)*~p38_/\%r{8}2]WA EڲYZEp6-3[U QSheTJmXA Be)v7{~emZ"EЀZmWƵ:3jU.l{7mSpH lf(¤>8L6ڱ}qiN&ղlrLm>llj  FDB!׎JTRFc('|;\zc<_RQ._1d @ۏǿԭ*0qscތ!u5P_eҕK;[Picjyr H#ɘY6j68w(;sNo2jyw@0Ajbk-iKn QHl6Ӏ5 ͠6JeXלX18q3n|ħL@\W[;z>_L/p$(x,Bg ̗ǣne^O_ZNΎN$ ώH()~IbHE.VLZ)DDŽDh26J11'؞N4b/_}soג*RjbTѽ/dÇڪ &bX|:k*^,&@M'İ1}+7ţӇa'^9 ?pZ/Y0'AXD ("Tfw[ӟ<'B::2Mt}vEL ɒKPߦ; 9{d :hfcx®-=ȼIb"j&eKeO}!k#'`yho,:@JXѨ"l2cEk,&ST- P1h8~rf:뒂Z16`%!Ph)6)9tjNoJ=G{@\7b&A ħ` f \GAQ@gz֨#Rdj~wJ&'!NW l~m݄$zՐF34wlYCIdcT:^ 5[Slu՛Lؓp(}esi56 )llgL]ۺTj3 #D9DWzGgS=ok %a z~ѝRW,JD(*f.28jw!֞~+ݾ]Y˽oYl~)٠ҙRN%cLuc9/Q^ܺ-֓~lݫG-G(m{Alݽ\2!I0RJXekXRk(HRT)?!kCQ1% +#F18>F?פ䵱&+,Wm)/R9ѹ(@H]f`ׅgvvkKdJO' ($IםÑ0ʅ"M(wot\Z>\[e$u /z,WM9X/G%k)AHRShe^ kE U-rXmI D-)Wbڃ8W %T;[]Dn[]L!YB,KQ{!˷nA|Hp7%V(6.=>{(:fh9%͒Ȳۭ< Ͼy~t4-Q}T~< yE?[1A7+brM`lV IA<2rA$G+N%RՒXT,\eR=$#@*RYJ,?~nM L2ϳkYɈ_f",43 3"LQ%ƐaĜWF+#s`E&uF:Ƹ}4֑J/SbEsmu2u!^W@Z#z W~gXTe:kv6ԝ^狣xo4Gk:!1GI\F63%qʱdBІo G8#֨cBu9I, F ~ L|DCSjĐ[V#ǔ]SUz@̮ojY׀mF jkw2hS @$Ւ*^"ey=7u!Υ>,ް[{ӇgØj*p-Ond62dWuHB3V=  O|vl] Yͤt\߻|xk|BQ9ItYw(ɷS$Z,׽9A*+0&ZUI ujn)K) IY˭#!/NWbH1L*hUG|͎1v13ʎ"c(]ԺpJa??R[?Mz^,>j=Goħ O SJ Fkgg"` _noHP;wu7ۗ^?UuNQRӴŠUJ)ԥZ*L Ŕ(/ +"#Uj_&?Sl  @6چ_4iO?RUW̵Y˥ݭ˺QY>r6}Z@I7Tōgnԧc.9/I8T͔!d4D81q4OSKyl֙VF \[mzQU}ٯ#֝d~t̖Ik2`w`1w&mMx ul6/mZ&`DݽZSDE]vJg;r4x4kT~1Vý,,@M D@"g)2TVKط`ujM~?WnVsQ(,BhsǧVUpTB-Pp(6!1 ~|w昢oE9SR $Eϵ2C&o녫Mȋ3mPeR)]bS'},N/7vʯ}S;=ʷEDŋxd \שE*;yWw>{[/ LGjQ];=1,÷;sdDã֕6B 1ĭ[v7e8* ~|ڰ$)9"],Ζ&$]ux!ڥ&@-ER1Z?>lƭ $bS̭͕v.3?k)MAcB Uj%|ʽ$bUj bpX)J;SZFJFA:AVmZe2JIBb"pt:&ACXV݇O<^ kTY/լ :@PY8f8|MpcTQ5>з|ղ$t^8Rℜ<̒6բZ)uFYl1]h (q5uEWl9=k7.ι__7w]@ԏ_XoR @D 7~9YF`X\]v+ձUSݻ:YN޽W:hqpZWKN>Ȟ{YV к\;1Bs쵺iY=5[̎cH\([B,ChZ}dfM-n\>_͸2qbљ,%{c'[TFzR { Sra?2ɜZΒo"ey&*;;lO&-T%$ULLx)@BEJ"ol]Р!I7~ĉQ)\CYU D]`~էMw~|yu egO= 9S 3"+~9[kZ陶=uSUɁt NsJ7D-{rA)wtE.dMȞLoC7!~{#j:>]P_jR&ayKs0Ou3ôtݴLf XQp3ȭ-)Η'!&vWgLs\[E.ɪi{\-ύArфѕ:HH$_вk|:i"=|M}3M)ʹMثuY~XaB A*0FaZ jrfHBE 8Ƶ\ }8?9 ^MG}r ݳ %QQ-yR^_/1Q"åkM~Y!VlAJ,7F~9H@,JOb >n77~iolg?.jew.q8;9ˇ[O:TGEP^.UFFt B^7ζ" ʽo|?Qfݙqʷq6]"Iw<ӻUz2F;k/h+ yPϳ贻CB7OE3_xfQB({/ ]3- 6" 7θ* S (Ccu5N+ۻvL:˺޾Tˬi2oίe/;ńJ$I7-: P̋NgawcH ѻbZ}Do}p+tM]eJVv]Mb 8sVAR8raNQB//\C% M5s:i@,DQT"MB92;o|[+o2oOFYսvr՞7wW$Pacq|ər"\$?jEjck@H(*l*Bڐ+W}׾ٙ@a&=M~h2|ŐO}w}*˻)冀 )kM`] @!o|_fD)jllB)@7߳ ˃e\M&JTgc|[/Ce +$[v{CӶS?׮Bc~HQ2kQۉn:}کo<,Xv0(boVs;mmvWo},#I"PP+kb Gh8b%gbsx:HFȢV@uem|7,H (H<~t/4s\kN9!fMImUlyxo~ B1(Tlk,:aD={k+K/NeJYcΑ"kP" :IPd|ǿ_뚍x* {v~>eZ& ]xf;3˯GlH@` JĠZ=ڴYd'GV)b,W#%*ك{%?_7m/&\\r[]~|wHBWW R+^ylvϿ+uy RHRR sikՔQХݾ"Fa ˆ@wٿ|wGDX2E6 $AyfNTU\X<2@4@b}J@.,⪶,$]04 ^ATІ_ܫ>pZk3͙];D)2){l2l;7/}swo;}3RmE!IJ CЦm߲FCål\j%se&Bfm(LoPbBcEEiHѥg?"D)qRѿYo1Ƀ/<{m{Vk:٠Y z)$k|Ϧa+BI|xQQ|FkY7UY\dALG :z0ӹʌ);W K~jXh^5(z1S61%"==:ό 3:mEy|ER6z||MY++cN\BI+hEHXP6HIC:PˑeY-@tXgPv,k`@EH` AS5:1]8.i `S8@!K$ X8 3(E!2m,f;iN'/~%eU)?=M}~6>{|8==[Ϫ@~OiՃ?ҳzc[־DwOo~//}oۛ/RFղ]NKXM`w$ъ :@̝_ͲNcVtڶB(HDrs{'F=:tBɹh`3Loz{;7n4޽PݥH9 jDEq]Vw/6KQGGw smmVi_J9 {vs~/' -!;8ؚ-+onnPy T]S`rȋr:cN*|\5L:N"V!1%Sb !alXj' z cl($1JqJu QR+iE";ys%`b`Ũ2(Ó έR0K/^xa+/XD7/9xk / F &P! (`DIDATm5d*U1rF}7_Ƃ''{g6y4 h޽oz_i]YbD. J68./o_}r_lAujuS19d%hmD"jeH>$f+J^)@}F$uHuh 2xRtGo~tJdGYbbh!O擳O~gɻ@Z֯R?i TY +EO'>y"s1Wݿ'ےU2|=Yz v\m;GBZYn䴚>5:ˣ3_ϧnbX Y"lov2_mHMF,g =mb[WDa޻1? 2B*Ϻ^EI"šQT`lbh$lv@=\)pߺX|3vLeD$T /e}pl]0:SXLȠ5uV&Exn1jL{s'oUn6]X|mK4* br0;gN磾K`"V=x:o;ݝM="իfx~Ha{ie;}nn+QڜQ[ B `ҶTÇ7._>+z&dzOW?s8 §^q R6WkQwEHُg7\.d`Bk;Ì2Evgs/k;v mlfIɬ敶Y Т^p1m׫]xsA%bmIeBLarq|~RY/^Rro_77w} 1>>`)Inb4mQd-tΏ0,2 Rl%* V AA#Y_&""(Ξ+7(tk~Иnj@ l:[tÕkUɳA,ᣲ^|c$n| aՀՠ$&1, "gٷ{|{g꧿7. vAE0ڴ^)&ѣo|/!]t:t 'pe/cG&JkÍj2y7_}Ց?{{%"YFHR mj$AD':/ 3TJRjRY\-o=F}PYX.2rEqĒΛ(Oq=u3Vάݭn>:XTSl ߍdlJB !c;O]Nއ DNfbpvtI o7߁,݇oRji-6:Ŭ -7.r|bLXE/-xH"DI!2@"IE pfńdɲZ')` /BX|QcB BIe Fc+?뗮]ȿ ˹0nE4(\(.Qi!DR$s=ʫ/ݼbњQ1J I(_j%nYW? ><:9y jI5߼v.tgL7EɽՂ|jS_! nU#R*I ZPtd֢9E 'YOAG"IUUcL% M>.th: Q_OlVخ}3̨A!u?Tw5[+֤6wmlo1^j8m^9zDezٌׯVaΪcs/9ƓYUYQڬkṡe9hUcĂh`@(IԶy^z5m_=:z0;v̴(R!|1nL:9:YaQ4[jUYb"80XRHRL0 ~` .$D*[Ә 4d)b1f )7]=y q\LIa pdԴ!,a@ S@ZAp !~~|t aaP3FPv0Q[[, + JtSNV lS"@Ԙ21aZ*-<||g2PW`3pMyeKFC$ɖÓg^L 'oh_lVt7|R%_vm`6vFhGnwuz'bXH]s"Pbe&&FU 7Fq\fK~jĄIbu 1LBL"MvKDS$@J!PHR Ngsco.ʲRJ{R I$D"(5(^vkQ/WJ DҦU>ݏ <(hki}L=$ 1἖R& >wKD+,*"2 >jĈα;`NPt#N?Ovr|ᛷW':['HmTk{ikڵkܸr-@ 8 @R*$΁k(JH @@͕PXN[֫zQZS[/ox61!:F ZWj#FWJ$uE)̲) ?yhن_B$VD^ka`y? EL0?7ɩv|Zhw*@-$[e|zly<<7'^0uzbA=#'':)B!MmLfNô6.ImI7%Eռ^fd2ƃAҶꐄ%##"VmC0>*Y!H"\c`AQ,U0dhcDp'hRj`H ]Rj/MTΧ*NuX)B %:%$,Z!)oCT&um{Wt{+_' `7ox󙗞۸q ]TM@1`(Dt()3Dj1 ir+Fh) GY;_s^8$dtY;=n!R5?ѽ'LJ-|t`Ɉ@Js "H /3k׾Zk, 6e;Xy?ƀR]MCdcQ Hroу2۶b<{(`< )PnUWϧ~g~vbK"~N SHClwaGr p{|xŹoɽ]]L5!<<8=9,ĘR J4'oDT`fDdMFݛ7ZtYiTiAi@iY"hiZk(x/(@NpH%T8FE%D̬JB6@H=ȁ4h ֐w*f$%( F.3 o!!.}^2gՠW_֬{o{W> 7 D4B$]t_@"$aeMAAB kDeA\v`(i;iY/vQ8bjTgcO_oYHA&(:/QiG-\wt8D-4[S=(\,&Z4(b}8%  lQnS/dno^͗dzb1)4ڗ_(lLvoⵝk/v59:ߧ.x-zG u3L'.|x#Ď 퓩 W"/7Ky=PcjY'>aa%4BZmHvg6HA2 tx%Y qj1iUaLdY"I+6Rfc0Ub *%*Q؋' U .zȚMaD'0 I(cP-C6! _-:hLU\gNvfnf1TBڗFQ"V]/;Ǘ?b9]co}ɲ|< D36Dmu̐:Y/dA!bd@d otxlbl1(4FGg(v2=Ӆ4*H.")YY*/j2>ybJ 06S A Y\ uK\+%AR $HQ<+?}C!/`QX\~K~S^$ V\W"* SMMYP hn0:ha9 mE%*2G&F!CKY}ť^-5gj덑n PrDQr4*6^]=:|RټRώ 4pɊ!B2 P@"aλ WmlUt+ |Dkf\M Oӕ2FI5mr@j4Xm'>";dzw~ꅝmjgw{s÷糱v7&O=.&wϏOz{xΝfi/ǣgǫբk%Ct)F糕n'2ԫ&HHgY;l޽,F[ AN-+%0Y/MӠW?&1zbP ͢iCJF imҙ- 4 ArĀZ P)U-mE`A4BRJPV~'rx轷̶*?Ͽb35oD@Z$X+R$blm9p{|5`i*X(PXM'Y[z+YkڪKBllt61Ώ|>~hy6ݽNkk/vU͟U;99g7]^1nI٣{wy'??زx_՘D8jN~|` 4"P]r|em4Ju ʹY'"+F˗SJHZP0u:@ݿzpz~|YR r{4Awe{&j/SDWܵoA&U\4e{vrT׾Wptty_;\VI-ez]iǡiE DF#@yܦMXE`@p`5243B-e M!]S%|AjQkׯa|fa9r7/yjŢ66;_ʧ<#8]+XUR"H)% 8o0g#DeBNB_aǿwGk 2Q.E \? F_xoPv4 ֝fvpY6{ʤi!b}<+]FPV@E@1`f;7^(nvR`l<{V''g|`5z?_#G}ӯj2"&GCղ7#BJQ,˃7~soT=kK/ <l(k9)A"Jk߿rA?O|[Ycl88=9B"ڶEU"1ʐY4B6&BJg٨ן{v|g?K=~ݢ߭tܿzV*+ k0\`q߯&z9?@Z<:vgR$ &9F 3TF̚zrbPcS~ZI7kVmDPI~l.*ZR#UB9(2 ^;Ǐыi͌O 1).$Aԥ#J(fB?yӂQm8+= ΖGğ,Gh !F$"DH5LGbij(8gM? YݛYkQV&tJ+џ=&I 5ZnZ$!n5mF~p#ȵj!2-(0$Dy~o[}UJIRh4`T ͚kBk8-ϭ~s{e:oeo3I(1#ۅ͆JBvbjNݢkbUS7չ<;Y ֥b)yͼA!ueUHXDj_:dU-6zӺbBU6Ĥ42JdGRپt ʭ*bQܳ3\on/\uo?)+;ݖ]wi6>S"ʖҶ kVUmȿyG__>y.''T_uxڶJ%;͝,uyiwd<X*Pf],OO f I:VEo,Mg^ҭ I ycF[1"+clմ6]-ܥ$QaPS(Ev L|m`* $h4l]0{&°f+v'?yJV C"a=|_Qwyx`w~z5bUJ0  !$ X=ʁ[DJ7ٛvm]o/J>{)fCI1&@7eۚ;$6:v6sI) jI*3H*Fo͛2ʯγD PmQ[5!C( zrQ0`'snЂ2x:\tuG4msx:oϜttugMddLR)"菏;@aZ|fY ҺD\.d!ՠe 7vVţWm37:}-ipi!$Ibaж&#@!)Py: O>:)iu8993T[F Uщʳ2M"%Q'Nu6A;RdDBT%imӁ/Q뚝wbk;=͕kstNk[P(l u]aq"@|̀.fP'SҪݤewy/./>OS Jy*[w>? j6eB'EPY+E[H.`d"jr9WR {@q_?ӿU?*{E0m;!z ΀AVRκWόF*H.sޖnRwpѾxy\6Ϊb6/;S2 !s!$}LCbMyk G÷ϖח"9,;>pڋWsUJUT>z l1ԇ˃沞Z/n; 8ǓnӈrXX}2uFx<ʯm]/NN/2FH$\'a(5l֩IqַŢ= ,IP)c%+I+|Z fݛ_^L!C)c]ʹ0Bu1 Oz#|NW& aL) A_X iIRY4Zևm~[2{{#vN,?nsw|w{cbvGRIF mP2ܢ ʂv;F;"W?wO?.ڪ{{~0<<($#e!$')9L(ƕ?nv,H[,٪ڱ}]V-@B,RGǷ\Q $DۭjS>M,"U'vg OzG/Ms@K7]bbn5%]N6ŝi㗧)F-bJi",\- kcZҰ̗WŒcWՊx:%2Ӄ X}`fsv\$x@Ub0QU@Фu4wcBvfn|;3Ph h )[mI>65A鬪eѨAfH,,jTXh>QC`$H"QDrd ͸kW??}o~~wv?ݻAAk6VAm@‚J XV$ _w 5Hn@UXVn9~o'ʮi.O_^YآV7. /?"Am3($)(v9wtwgS@hV )"T@ŷL׋N)0W.g'g^}_E玏omڙr!ز#ON^Ezv5;;a6hkECFa5Y3t;ˋ~jԌ_7m?y* dtg=qZ`^6 !M̎UHY2 շ>xqt%Fݽ© BAal( >ͻ~Q9vHP,XXUEC)mHs`զ@+g&ieY &Pέ͏?Gݦ]d+#F9 !ga/dz;?G}_KGoJ=z1͈Jnp @YjĠ gMT"+PiAD`-(JDM?>Fo{5M\gۢ zst|}p߹ɩ1\Ae HЦO?ɣG 򸮴5SF7+m zT+e|Bkpx3m "jG/b29}@j&bGF-*tgڴbs}}fsIF`֒t{oIYgnyřAiMΕM9!i4M]EI|GKhn֋9,.mۮa΂wAi+]OvJ6}hh)(E|@6UD &>y1Rs!ἎSܟN -^۳e{uMUPԣr"|1 * 3Y R"{Tg9qaQ &L,> xӆ(A`x6aB(ZS RzHzA"@F(9JR!DAB@BmiiܹWhUJν5Wg?w@J+m&@ZW_\-嫳'7j"DѧQ$Lu`m-Yk}o?FJ!@TIi[jޜ|7={js+BQ9R+u]>@H9cZ)ő&Ѡz÷\p.u27USol:DN/ϡ Yu5?mgT, 9wsyJ-ۺܽKԔu[RW,점be]ýC $@$)6Z\.;bΜآ(Q\fj}ބ}H! e\-'AG(X,iT%W|CTM60e YnR9a{mOq`[@;JI@on m em <+Jq#IF s@`#rFQ=Aܞ_/gd ((͑"tu1F]=Zwm=2>/|Yxtx0N+2E9Z^ZiTf/dN?{#M}jm{Ii"3]v^]4J3iuHW ĐJ{e'M,s&bh9gD*H0)1M)Ѵ,˶,oPj;|w( X׾ FTy[s&`25ay}l-06u @#jH$T9ܵi얛{.J B8UQzfd~~_-$o٘9i$t]0NPvBeo|4soݾ7;o} Yo?rӟݹ{[!UFDnզ#M̙-mVOS#*,g\*O#bHsu2ͦHɐ5}BQRb&iTtp0؅U %nW?;ki2^=/jT:N{^/_=>UZq_'rO?_>06@[x_.8z_]q?uEŒrbvX #4sH|/t2n9*7_ ԲT81FrVQ&ӥz1aVT&$]V2l/*mBENNti7hn]soNŲXŪĜ@w 9 $& n.}yC0[!B-(E:/ΖWzg/ꅫ2$J;]直;W8$.&TCLj2ۻ} yLC0Mb~5N 2*qg|~pWH?<9G?_wO`0Umh\&NAbɘãa23$dfȪ q褡ԽߗCC᳝gbp4+xq*<'2oY {ګ'fTѤhkz9O=\'ݛMW_JdR,oݻsrztjèX*$M/$^7LdvPHE2pΤ7zZ ƚM8l1_v* DEmsN=0M"6,tY/C_z420MqLecjfT:vonGP6vr8|gHJqk5GMmk"Ld Axb}959 z/x4shpt JqY U{E`E7PQަ$$D"R_w.m׳=F웸4/O|,m5{|x];*aKٹ/_ظBrvpΛU6ރL7?j61'rLJe[="PNs mL R9oKpDO2<~~|Uv=_}4AzmQ+ uñIu58><#]>=QEU[뫗u1\͢EGӋtgp\jhV~0YZ61. Jip0P$mi:X.lduvzp|gb13CKZq޻F's84~Spqz Y#B QB0l۠QMFâ0WOɧX:Yf|q BŠoÇc41ae `R=8??Y6} _{GY * *VF@ P% ofmArEN)2nܗE%d I0], L)$kA8դ )*,)N ;~2خ׋sNtq٧_=}㷿[r@i=>/OzL=OFw8yQ3T?gO9&o$E`̤u9;H(M1bgrt}hӋ+3m??{$Yj}@ S y+Wp}_7zy5Hjqqf\Y@cr@Fe`t1K?pVIl۫݋VVO* )Mߦ^*f+z:B -s" 㑨SxQR&,̭mk.SY)T`w'"9eB`2 ַ&!lș,"oF v0G8~mN[cPf_'V/s3qç;x*j{fNAdn T[3h-I!э7=?s/cj/ 0׃Hw_=ɏo~1۹I8c  69g@Vp()Z3M@PUcu[4mWYEzzޯ.ޚMefV¦KYi?8Tfq7p!aœJ|~]`{kST nlί`)LdRF3^N͂$Nz7jzϞ.71;/Q۷n//7k߮03r/gqWAE,İ XډXKNgS$L̍H5!e9sFS{kVtD6T2N5v72C3׃V,gm8F7H7YgW_|vxpp(n ك/qdvd0 UYXwybëv+~ޝ=ܽ55hނXH# (oN&A# F#)EhQUgFHɯ' *Ęs~?wY_.sN;xzV(^ljw^ E1{[.jЖ DVa= ݙmw|lڣ۷zWkD>vOHJk@7lo.t=@DdJpnZ2Z+3 FCTv\_\ԓ= 8M"ay%WBV2HFQ"E)-'? }ŗf09x͝{|8N6؉jPl zK7l/OFEPMXö\].^-^xgf<h*DXn#R ވbg''$wOVmE 9M)kuI )R G#R(̤, 9"sL|xj WEI g P; yTԕ[EΎfn^-z 'Gj*֯{FxR\͗ڣT*Nx30_}vr~ZvmB;bU=W/Ɠ!i H<|NY}mOS!)F1[{GbU\sʮdP/'q]N՘/^1XocDMmUVͪ*L=,@ !c "Lg2J)1)EBkqŏʺ-:*bHRV*&!A6yu扆 X@hȞ6כO'|aq_;m5@5b; @xۧYxvLdU%B`~f~uO䇛g_ vVC%N ŢJh#)Bʒ" Q^νwR FU~5"&κ'U 8%`MKԨ4 IefH1)$Ձt44RyN=ǔ{rqxlx: B9ҁcL z+5 [G7 \QX$zr|>6( S]xq\xirRʀ4='׿) 8M+1ZrzTd% $n"x%1R6Ar2Bh__}dX%!:v: fcx8C !Lg=n! $tZDExHH+Ƀ#s ^khz!ǃ Owj58XB)M:@Iu8hF:ʟ>x֗Wn:TdY=@30ܠp@[%nm %Iߤ0$An4i.N'g3>o&@sj P@2Ҩ~$- -gmP*ڷ~gX x1ula^AV9f{Ip"1`]"7kGQ›6%DX|~S6vXC^6[25'/ףN?FRZ%ݣf3F6EqiјitHj2$+MqnA!H U޻|qW?{d Īl 4` }u"ݽ+&ftDcwgm8AdH,ZWvJ'LUvtqUZA@pJjM}4amkir:M+d޽{! F/qUHQNz0,C6Y@Yξۿ>~X{LJ4=BP@ X`S@Ƞ~2Qo"r'x}#l7vYn}4Di=:cvqx]{d~ҕۛ/:ҷ!'dڿ9 (ʪGt5$}1buؠ ̠D bxdN Gigb.2jܛXXJ `!,}ߥ@8 (La5_~u|X;rÌrx+ oGr?)֮  Go(|㰸l‚ttp6Z\=}ɋrB[IrҀ$5RV`JKή@4 4:4!X1}(l9!8Rb4!pFs&R YћMIN˂&NO3ףdv>9Y.^]YN1e%}T ߿6: Z]gQҎυx<-M8UTU)$vJ`̞9J[m1,CwL'T`R_]__}g_gO媹:4 q ddݮz ̑ZrF#చZY]T\Y[4 H"Fua>1?z˗WɪUf RTE`L׵9fsz{dZ[kH ;*Ń_>^\ws;|LM^g.~o[v .(y BܾvQݰo,7Vz_UQr@_RivAֲ$ 3#"2kw&[}B޸"n?7w,t7n A}7~k2R[0ATZ~0?_n  Kbonʮi~룗ϿK*+gBBLFrj]Ylj@caL"bX0=g? #笺atIXT0{(@C5f 3&t>̯/],_=uˋ}l]bʐ !l @0'TJb\i(Z (B%rRZep GIϮqUHw-b+Pc%Z#)m+ gf>VU '`xlQߵ"qrtjF*3zVV!Zˮ]^}Ve]*sg?~|t{u >$`pVb-^])@ @"ڢS lg[ fbś{cD@B~Z{4<θ `*n//" EH `( @QH2p!PmDž2VɁ|6aJ[ '  4e9w݃ɽo}}sng) 3 cm 2+X$'隔:$C6,ƪ#\aT┲pF ? QJ)Zoz6Zu ^W<T.ŴZW\,_]=ZXNPϞ*t1BizPj3b|׏.단uY\}ޭfwTR{цM&Uc8?esqEZ@Op2$kG fٸl:mۀƷn߬z& Օ<)L?O^>ȬvKS8e0acUH+ŞT^=zy!mY# k;(Ui10$]X"۲ d%w>8T֓k-O{;Pe0"BA% m\<\ozq뫫31 (]51Y[AdrsdJ.D=* @gm̭S9fQ(l""Qq"")E̙fyuNtcLc]I(RH3*,Mo[ʹ޶ְ 8 5v'^5m {X~A9MiݤVc|oql6DW@lS;pf.^4^EoɊݣW/>z#<]\#!M%LJ+EjrJ9Y6pI>moKDPF)EJRJ7"Uu_}>(jL!:FL=,r#P9n[n,nQHшw[DȖ9Vg!,"9I P @! sU$I8Θ )ř*TE4C@AS dV_J21TUZӰYLڮ6d'tdlڰXB#7)faURh Jm5.ßs1 befT@9IIfD.5Ҧ2A bJiQ-;R=l|{0qd7}߭8q:LĢ[Bw][8O.泙D8dLZW"!) Fc RH1쪢P + "GVF+"(J :3BB0 sQ떏r2)ehJrBd`0IT[$[@sx=y~d=N@ؚ̘$,C«̒s(_Uj\.ӟya5ݺ3;)# =` 1b'M_T5*RKdYrJIDbq/z伋hB&MV]\ 5䌀u/h} 0:猈:CVPmt1!"rT"9(ζUOBݝZŀ$\4Ͱ*҅6v>_mή\__{0FyD@ U+-4)H@2:CEe h֥SSG.Z3" pd ژ{Ι>g_Q (d"AG{wjS$m(Nm6j h@gNem@ذJhAPYSQ+R!BTYWb(yP8Vg/ON_v?[% hD0#!1Y ۮ^t}ųGVwwћ9cք @ll`@PS[% S",&N> >Ͼ\-O_>YxgdwowpV}tш@u!nť뫗uI iAL91m@ ,\U6h1ZId(,JE(+ *8(4Ds1d#q`(issqz' ]|w佻;JJ}{mzsgP7)[gJC. &C;ҦaĨf?Bljc %X9%Zڬx4VleY101p7>dĈ ͜B 9JfaDBE!R,Q|l5#!L#4,ꐣSLd jcͺ(rsc"+Q@tEYmg?x[hwJ GH0H DݤQ$wnI9a_jzGy睮yHsq%$V̠- @E@plYLlzWz~Ǐ;&{e#D֭!/>{#3s,)yzjf>("X =s'%DJf ?܁3o,"$eΌ$ PbEhgaŪ#bL9Vh0> *{ /8 *6CK{p?~thEQӲtZ;C yMJ!Yϱ P8žaRn. 43&DaU;PYRΎ5䜯y/֮(Y ^ϛJL내Qq@2Ҧc_)32bU]uIb,V; Cbl#QrشYRaQR8"M"9@Ŝ_YCT*.m[1%ΌD;x:Q\_}zpoãe(c2B5ҕ_>b1W̬m)T.JRI Hm- DDTVEUr}h//~W{xgrV;;O~3ʃ%t sn~yq資_ kD4nPbUW!z̜ɰVÙAqn(@A@̤P黳N f-MOWmJYJK(6MA Y D@=+!ǜj@)M=-4O>|xF@)8_70!H @tUI=iTe 9R\vMcSBFHAQR5ة2 ϻi8(R+ 4Hkic2 }P9z e&IFaXqt5 )u}ǠUb dNHE.T v.6kb w>&TXD(K1ykݚҢ$ #u]@Vy1J!!@@7GLqwnCNa|4XxZ;jg_rSiyvWogg1jUXUxS팇Ghv@%9mtRQڸu_go|j<0!SU'g8*Q@|_~tֆhFU="o%V#J^9 )A(-%" 1A5L.zA.$&X\SvхU8u}*0m櫮iί/ %2Z.cZ NwsY$Y#[vnccQE`fN!ժGY$rиUkgU![R(;V#RMiicR%jn} k9v<+?v@_ u""eȩ>apqUD2H)s\.`R"PH9jzm-l8ǵzϺ~}擧>yӭ0_55ͺ_p5߸{Yz_nȨ`kn n妣1)$ pL I8hWE;ݻ{٧?^]`LjM@AXtHgktL "@Y (op,fj/Vw\hJ5Jc1ˢ\wQ=)-t2 F‰9#a=0)E K}EMQX1DqUZ H(眀b>n!ή)[[$ChϚϮzqzG,Rh.νo?<²N.čOI~}v9YW[o5 &W*6:s|W?9}|ճgѶC>y]~=ڭi9*z#̥4 h$嶕X6k@RJuh8۽EG)-J@H5 r ެPdimc5vZQDԉ պ\(İ جD¿l5Xkb,V2,M]0zR)](ȵ]#SbB&$MF벪T]9LJY$(ȸ% N ֋CkDR?R(>""S֔aAIYFW?ohe+q#`EU싓_MۉYl4:z>_w[n0#PD#!bhU]hxe_=W|w7{wWXtEZ[ Ȝvޖh7y5?4'#ד}G'R2珟/||\S{ u;}JDDB&m5x`H}[, D$Q#2#$eANn]\.sߴҌ*7)mN1&Jk9'\w&Y7ֺ } dBtTP",)3 3K R4Mn֮t",S6щ׳4]Zi#vG} i xմņ(qݲ1J2"t}}F+MNL8H-rRjRc̡9$1fN)gs嚮E3ƀ* QEOibľwJlYEFM vJbc3YXBĐ &vfd4(E%R7'_]|ՃW^CS7 U9A7!+9,9 (#AN).Ewf~ѭVrq`<+'ξNFt&[2Hf,@r HxΛad:_<|<9 )׫奻.ڿ_=J@+L,ғHiA8,&@&QŒ@"a$9?) 0% R01B 9kr9o旯TzP81iч؇ԆwWj_m̈R5AatB7%w"5m\ ބ%cdmN=0:@e5& KcK3*䜈%jbȉ*}m%&FKQVBb- 0 kK1iPA2BWEZiB(ZFcLJCTLT J+sI,PefCb1zݞ-׷own xWZ%_珯OWgF;Α-ƨnzi8zIJJz _6(9 ,^={x]^L7~[1NDE(gbf6O?k'|O>Qw/=уQkP5J+Gԁ&cvhmsw?1TںʬޖJ7mZf)MY+͡"9Ǡ.*_(R"j^,zAb$KUhP%>9g5NsSȚMisYdJ1)2dR00YAF%F(5M10GZ3/΂3fZL$3eFCǬJn4((…)cjsthR}H1VzkJJks0,s6eeԯﺐĊ,$ZI|b$ưVz<)CFSLBa*2x˔bJA.I+j<*NN~?M{G^'9~vvj݃{N}6*@:R,H@Rf 00Im+r0OٳEUk:MUOn1;HS̤(g`ɜ2-Va9*-.Wl@ g.}/t8(kJD`άhEuaRr2"*6 i{wX3r$uB "icE`wtMͦ۬>İK T]Đ ^Ĕݙp"^ !f`:oZؒizo>1X[r9`Ѥ:Ț7ẙKN%0DE9(VF}, [Zobl.))GƀdP(D8HY16& '15BuQ:v,DŽ^p*%NF{hI+s裠.Mh!c]NbtKQB8{ "Od:Eŗ_<{lO|tL_g[rɐ @ g~adR@9rwO_}u ^>V!v1 Q1G`dsc >kabNXO_8APho6]⾗2"+aA2 bHDoKY!udcAnJ)eM ,fAd ʑ01PSCuB)T@[3CH uޘ]KJrPU TkMiEns^'V|Ū,LXfw2) ٢FA-eMuH<sY` uRJY!\(cH@${Q+kaDmlE+1N3`cJ!cbP("+ۦ͠Q%ҡ)\I"&hJֺQAfeAv׈7V_\>=Y0rwv* .$ĸ]ama| fF@Ȝuc -A TFĔ@&zO3+ƘRH!&N>yfVmgG>ˇOJí`h㝮_v>v1)1"*@BT H0BĔ3!")zD9m@2azfј)!yڎO8F޷VY[T1I g$S*%'aNMBty~^Kօ`hr*.$_5-l fӢtt*k&!3hm%t1#1eT]$&l ȽZv|ޕXgxXTZml*k TR' ,>3ʺAeFH1K2m!۬}UQ+K e@l}̘c$ 23DɐDiC)DRn *KVy>~Ol6Z.+RNĤbPF%m9Y@Hi f)n!b IףzW 3x'U 9ІzCyЅWM_}B2CiYp( cesu\ `sXaUu) mݯX6,"B:L,6 FT WD:%tQŬ,#Ie$Ydo6:q+m(I8BGGMs[.yۆa M5A%[Mm}Zf骢tzU!.3ƒ61!B(Ô4 q+Wi(JUj(SV(Rֈ:r8{9]N;o\}@k,y7]-)4X=s N)ľM@DVMY[eH+B53` |.+/./ܺX7(|_,!Nk3 )ĨԖ-H,_'0# QAD8%,91'h2{:oO&{S޴B7}7$&!^ŃW 1r-eqHGDBsuUҋXo*IqH_]KGo69ggEUt{|_9^rZ]] KΕ*ez H6&gФH$1"l,C&)GA\-z߄$E,iEN@KeəR ΒpŴtʺͩom6!F\J!e!oz5 08 1%J+ti;w1r昳0,DwA OMJYF4+q2+튢(E2!c ShS"̜R1mrJM$i2)ן|j!kf(֬"CZIBR$Dl1#$漝r:,>1R1)j[~o.67= JQ!Mn|6~w|5_~d0@a0X]4me BK"t, uQ=RY?|wqmq8M=1M*˺;={QYGD*/J oQH;F3&1 D)BزCH8)8gt$LݽiȾfn- =1ccNQ56됓, o<[ 팸,JdZq}Jѧ`$VmV))g`(P WhJWBƪHpvs Y*pܱC JfFΗ rhX*M F RByXFID0A +M:$c9Xv zyy]af8T5n$đcP@ߴ\%0$IkCd7) T"g,H]o5]svG/,[}m~ݟ_] y o8%tER̨2{*n8jސ26XeI+c6:Y1bfDୡMWE:FYkԠr, 3@$BZG$}qY6!tRl Y@oz/#~l2`4 F.RSы52 !^DPP8MM$> h ,pJ!D1fZEM&s>.v|>[]\k5`N(@*͝^,¨]a"@iѠ+2^VDO9q1kGhb&{?Oudw{TcaFE,5TR9)۰uSNA8 Vq+9ǢAf̤41 t}4Mט95 ժOLD@)"Ye H$g-"P> {96CHrE v!*?T)G"͉) >$Q)>J>p.@D kErV#r]֖A5Yf`$ Ve]LN9)R.>{xzݽlw8`ZFd* WD#J+9#s c@ù)2cʙcKلC9CN>" ; C ͢z9Z`߾}XAl[Dɤ511 jz̻hR`P7[o,H7emV<""Bm&p"֭^XW#Hie׊JW2dbBj(#!+wsY96d~rF" tU3RZi'{,`ah;U,KXV!uiw>ͦqsŨs"21K5!I 3jȪ!et|Td"dڅIi}nByDALO\"(ILW/&"f+CXI8K`[ Dfni(7SMQ\ )D2@7)Td֐R [Kl.r؝㽝A50;CgIl5 W ѣ(HP$9gژE8l?i,ĠQ99^DčyW%iR2 z  LfIGנA  F'L2߻ux)v֚s\ǟ~| n/ݠ7jZ[.|?~z% 2q+Ӥ M39=ژ9u t4y)x|o_\ ba4}/SJL87#=RJDou[8Eu}x0vx8Glt$,mpt?gIjsFys>~$bILrf T Ihm53P )]S:Ȝͽ0s^+\v_/ Vk[lvV$TT{k}x?G <P9qD3C.Xu}YzK5̒!KnADŒ(LC[Y݂አd@ I0M6@IqJS Ol S*%Z[m޻Bq?2dMQIDATi<`\^t79w/0|z$y:fHe[6sgk/-*`߫wnM[v׸+M0DEj]ګNӔ 0 uKe"mE@dN\f?z,Ȉaeo&|A$ҰF3m@@Nu߭k73eFhzrM0( S9?qx,ٖStkbC|Y4"G%Uu"tں$))$avwr8vӺ,(VHo2<ߗyn۶*eY}YֵZz.tgY5H9yA]CD yFml1PpwNǶ.%'sRw5s5Wu"tk} 0 \ )DtG8'ކyZ;23cW%k=\I'+ LtUj ;7AG4\omp]w7}:|<9a]8BmQj\wokZc3px|~$rNL$k26CiD\ P! #KsL`ח'A;U}Oח//Ħ][_.ͭ:AA8iV#R(e/[xRpM yoƒ$-LXiaqЖ@}xx8/~|$x0޹LMضu1qŀL  1OSt1 3BvEt0U6S>L;th[ov]^2ϷE_CEG(z .ɑzWX#Q2<1MGHet :; ӔY@[%IR5nJH.ĒXPH:^+ ,[պcXՊ!!,2Q #!vU$ЮV"an=TdAuH,)F$=0qʓ졌atS赵Б=3`y;.ܛOi6i$?JRD$|KDo\9 p)Hmۺ.?}t>KajO {Ufj8t>fV̉ Ήq\L8T9! w}6 FsuI2&L%l]S\{G.s$$4|"ܴBC.TzOItl!JN…p.RWu[o54Z]{P[{k-/L{|(SΙIRڢ۶Z4MmSjGDf 箾*!rѐILA$y#\n"6 r7֦{<$<|jʅURc@7C"3_ H{_pS)0IS D4D)OnA,]|]{9Otxy]XSJiYI%A`0]G SK)G !# TL"r:>N}[k^~?[o/J:rԖmYUЛ1tӜ߿kS.Sdzz@ $ZVri0VsäKW$P2 Јg ›CDڽ<L%[$HeDYR _/߯O[vi,(ҫ].~-kff6Ln~t]n%r_[&Dj;9hl,Lj]oRNxeRfI%%ibQޡtx~y|3- ͂<Gszf%QW S*;1E5"~u6L궺h8$rrN9Q;cHLsIYd)r&Tj׆0 =4$uJi*"Б3VQF G$Uu"qs@n{*"RC$w@к~\ܚ;_oOִT5: "~~9vaß9FXLY gޮ90i,S\ F$dsX+ӜeRkzJMBL*| 0fcR.))B"f뽽{5_unmRBE0xK*u`a%$/7|[4y6,kaĄH;nE! 0!#BT&E(\Mv$TwwffGw FkN3'Ftӄ, =0:CvS!P@dK!"f%$6O10Gh5ljKqkrdo? `ßpkA"fB$@r a$ zD_LpWnZ./?y=?|x~;9Ynt>ߟ~n0ns̙IorTfbnmeq*I81LtNl]7,Rn,kW|<0ڈCb~,y*8FORdRbU&1usgH`03aN1խmp>ܽ˶+Qܮǥynqʤ=$>T6\aekKmNݺp%ÔjӠ޼+ޗ/d;rY\f^`‡Ǐmj뗧ϟo>=k?ӳ;e|y=ksFx<A% χ3JWuzօr& 0[ >>TማvmNS*8KҮrOI]E{$ڑyq[J_VjrݮW ׵ݿ) 1{bؕF0U46@)I&{x\ڜI"'U*={hjeE, %ONSPSsD@DĄ"(oBxA @fj"A:Hш@(Ђ {uCHU"G3PVB䀃P( @lf̳K9>gDy6:""9CC0$$i ]3c"twUc&>ax8Ƙ5DjC?==]__/?FkۖKxvW+cW49HH>TI͇ A[wm[/rw'L24˨E8 ,)) z<صEvZ,PrERNH)P(zlMm^rmۮͳHY&:E jUmkq]G sI \׮%buk zZ@El[ 1F’D$ˈ ea@8Ba9A^qN}eh(t!C@ EF6'IDh[+1)r"ANbD)@DBI$u- s1?O{C?$7A0'Q@!@N0 ' sBR2)A2$Nj=,D1?ϗv^_~orݟpmˋ/[T~m^5[N29%<|8Z9Oye]nZ۶nח% <r|<firc~;ovzSuPW^/ۚZ1b4cG$a >KkUnvdュD$1€Q"3u0lAr` F!A!6U^K7Y_K 031ڕ U,TnJ#njC-X ߿F$GDě: "o0(w܍)Dv$"bSeDZ[rGDs!Bpw&";wҐ-v{.?t\~ϯKmivmrRMD?~| 01cDǒǞH.7 B21RC'!Cwy:3iX D"\JJA ཱུa>.WF<>"1%Rp%l xAziԖ׮|mx[:KH@mD,uD@JCZ3!-[wb*̦6r #`LI2$H" pz]EĦĄ"T!(0!i`$)3@HxNuYS|ZC9b KNo/£]LN78!x &$vwhL@N{owK)0BH w'Q<<MEp$SbnP[k\ri_@YވP)X_BI%n #$s߮nQ QL4'a3.&n2Us_7[~u-\y,Bek'2w2%#d|Y4CApa Q[X܉'wu!n~D$+rdbHdQk!&SL-L5ZBhxo PmKP$ʁ&U[| z`[s?8MȄC։ss1"X <"#Y GZ[Eԙ=w & { p#܎4o~m߾~~m߭si6'?>om/(2tw Iʲ޻*,۶砆n/V2徭i>B>7B:+w`NJ1CtIjme; TJBk@?ծ}PF~ֵ>]\R"*"sЗMVkc.vJ yԖS뺦U۲lpXkHEjw\ ()#8B&ro=O%!l!!z9q&@ȩx$ Kf–i ypk0Ws0U|ߦ5}* "CptN6r`hD؛[ 8~9iEBBDwC 6C⽰C,éF[D ;"DI$R.L@0|˿ /^|~龼|zYvӵꜧeӌ??t(a,=̠mibntr"NLjz(Zv9l)gI]׭[2H,i]#ƀ庛*qH)/5p# ܩ̿h/o۲1B{Wyl"s*>ǣko4͹Cm>OuiLéG*,%XbTdzi0dY}Κ%6'H`fJq:YaXZ4=01Y$&*]3#Aubq 2^9!!b0Q!z]{g "a8,"D  a>H$fw'B&+%p"F3FĈhj,A((r2o?Z_U,nϟ>^[ Wdx)b?nYHDC]+)VLEMPI&3#zz˻31n}mRz@iyGֈKT{Kn[wCb"D'BV,b0|sz#,,` 1=\ i v-2!8`s SG]@䣤 Q7 ^" b@Q1J!#{03Ҁ "EOIÇߌkvy?wi{IbT-!gi)nRҁ1)rLػ3ҶVIQIJȁ mdÄh]VWYL$ TBڡ^Hr8yVDFYj5]rvp;NDlۭZOdڪ*-[ln;X'L f-Njz$>Q7,0#eG8eU#d֡bdj!ŔO r(GdBpZfd颰e~qx9y(JvLG|<"05*pbp3"QSB4W3,m'p !36+wgfwG[#|2_w"[ QUVNVuO?OO?R oLLp,pN*|S&rR<ƜQ8дPR /%"¨xTwp>4ޖ5֭r(,Y}C{RI|OR))o]TʹψSZՊR [z[lQ=:[Xp|p4}uʅ)}޾6R r]m583WD0 3n  1Op(%C4cLȄ-mρnX&D")_o>OHd AIH4ь#0]]7y#h,ױnf}oZ5"oa>gb< ûl}n 9ëY8=NlyNF$%b"c>:hsV!rJD4 h@D)n$yko9p<ǔf0:M‚yzNHmݬo?6Tjjoj^;^n}s$43v0[~2E1_,wDa 04á#%He"0K .j ϝs L,_D C"hJ;g/#][cL8@S%~Ï3BLnND&[6`(fA!#h ie8B!4xD!qq1cmۿ]~o?\%]qʉ|Ppd$i>8xJY"4ܻ(T͍꺺2f RBuliA<J<2 PVE.E<-D]#8͒3y"֚y}}H)T||mD)Yr:Y3 |jąܱm^Y~zkScV|G2Q05ZdN{߷=&՛҆ء3@-1T9#\e.rN8؁THMS03Sq< @b@:qA|u|Ю7Q{kAC"` ZϨ)w1cdnNȀaNݑP!5q"q@0D#puX|w?+eY/Ͽ>w' |%COx|&S)$ryQ+zzncB$I:8a.s>9{D ?џ9T̤̈jok:="$ b!I=:1NQq0% ""I " ͕0; "xu@#9wDD ! .`d@(Jcnxr Z]kקw_~O^.wB)3cJGd4L`ZEH!B=8tۮ:D…ҽ ڂ[<8i { ˌ(^r;VZ) | p8Lt¼U{ -pw"$ 9MRr)M#gծm7#k)("@? ᜧ2P"%9:"! $vuc`ЀG%|X!29 :P  q3 hMHFL~"Q N^2{g$ 7cADSIX?[u~>Х>!-s" d9183׺$#2.S"n9T# І$-[OE5(A9 uJ3¶ݗtM#n.D[n|jfxpfB@2!'8-Ir9iF MFn_G"cTDI‡G;'b}Kϸf"\# y$"eӠG!cS19{lEp;Bŕ!Q=\m@cG")3q&d`w7$& %o^km[ey]_>S`*mӉ !"'"btEdS(=IAB<̀@~7@o18$46  2 $ /wHCא$-1|d@PR@Ѱ-2e Bd}[-|><kUǵma~;Gz_6u:ƣЬptû8Ȃzn ~|#bkz} ɄfjifNpA xY"ӘuDIu`PÈTXqs21"D Aހe?( $TmD$a^@z*Hɭ!bb3s  ,HBDa`Y;aܥ)i>2Oү̶ٶnz{}yr\ݝ^rs$$Cڶ;!mK'6"p{JɇN$eWEJ}Bi7F` f91)uSk̜In}?Ns>O,8"CDZ8'EwpOt(9nϷηSQZm]o1Zo@u='ֶ4SJKD|x|hV[Uv\ `1WcL 8t#TLhSN[DΒ j 2s.ÃFeJNCd)5GIm֫(iHߒz ؃GpʜPBPYDɉo+Qq2HI<D<߅=Z;[ Xڶ֗A.uuᄄJM"0| D cJfÝ]= ab8f'; "ޓ?`(H(Nk5#b~;ƠT9!!cjwxP5G~0A0WiѸ^#@`D٨18v8="Vۺ/z?ߟ^ݟԉ>UüojEB3('ݘSٽ*ȩV$jU$#G0cIĨM1$CEBfl@h6:NP ,a%"Qx90 0&S9rHE:fQt8jtS\ $oG`Rp:Ny7FČ6(aCӁ `J#@ח[璦<=+wȕh#(vg17`@0#3إ0{E cIA É]˱R`xhF {O7 DAqB`FPo2 HV"\#pAPua|VIĤZ1ARiY("&WDZM@iA {w9!q*fqND5t *OL2Nn2DL Hޮ} 5$@!71"[C{ {g`t e`E<* FÝH\Q0ƭ2DغtĬ fQU`&e*A?Ē"vPVF0CS)MW^^?/뗟l!LJxݪiߘ4vynI`ZN9YX(s. @5M%CPjߒLYj8Oy:*ap\J [8Y2$"HֻMRcVAC$eʂq.NM ޜ@w<{"f&;aNC{0Qvh1Bn!)G1󠎎ln3D!1w8v/^I# ywR168 C(T*Bbnћ5h}ؑV5}48vylqV7iÉ͌cXHAlX|%"(b Gp 9j!n\>~er۳]۵}I3km@-ͶyVג;0| 6 KuZg&u%.|PN%uxnT\+いP0 tzڂe^j9WY-jCpL(`@ t<g𠟍oԓ:dCb>cFod&YxzO @Yǘ .n{wP#h/]!٬#Yk"،SRaLwр,ɼwD_-w(7hqG\PTaOG`&ppf ; j;0B}#JfNȔE7&1n}PKжNeI|h]{]^r{n_~_oէ뭫gy>l!J@w L杈TB0an?0 Gj zhkUaJya-4m7mq"wh? q o]cY))vhҽ] V|]O0 †Vk=;@K. W$52PWA6x#ȇx$A(?y' !` 7bBІ 0FH ?zׇB~_uk[].)k_)6p z,QS$SnD0RJ5L/5%i2"tUpWw EtDs@Fvl IUұ(tiDf $ǠdZÇyJCV.BA4¤`P;s2sOS&d7.`|Mh `yP0‰xp[+5x6ɛ"W`-@͈ m1{2{ppÝ;:䝢6olrH 0$ȄL0lCbyHMitCH%!df)sW)k몐}KQ"P!J$~"y6>͇xwE7Zćglj 5,tzxG,H"u w0p"TmyL(GwPfio D e|bv4t-nb1iXY-@$r`CDs!o)C0={%acJ c쮀ocogwn3Fw3@p\M&"_{-N{"ܴ~)Bt=aAR!rFTGs\QDZ?OPZXEASe;V E<" 8Ne1Ln,D܌m.$F<xO`>)!`FĶÅ nJӽF8 }̞ p܀n:U$6щxwP3P) vAP@'f[ `6D"q U3ǨGF RO1 !MG'UN.o$sa;56HIx|}y۲VksXg.֫D7L M"u/ m2`J@XXĬ[8;BLл&wT-fU#@"R?WO$L>ʿƨ ͇݉𯒈0YC( coC9hjF!w`phUL8GV>@D4)Bnao(k,#NnF mKnEL Rѣ=,XEm&دxb1"`__/ B1wz'N>tD 4(w#Q!=b\j @}ho?8k8 )g:[۶޽O~EzU!Bbt Ƅ[2#D-E)f((F"uSev4&jok[hEd_&><ȻO pov]! SPy>My&`pG_ì #80Yv%:"@oTu+!%0&n0ܭ!Ю{Raވ͈ŽC|mԀm. !$wstwy琥WwpSiGbq3^Wu@zG0¾bDD2@Gypj6^J=P hp!;1'F,q"Px9 DXWCo>B}0`#b}y X,jv'xG fn } aS@ EFBBI >oǨlXkvQ3 36!x XJ\LxC1fFaӃ ecݡ |`@eSD?n϶b -]ٶ]8m ܣZ^k` 3dB Nl@#e:(#@~;&^. D̚U䀐ADd;}k<TψiDZ4v"rTGB"6sf1KpbuOc;@h8>"x $B3Yg d3a()RHFĴQApZו`hM|!Hv8=R00x"k k@t<@ƀ.6Du0EC"&|&% ͇SDLtoݼ}U;vu\KK ګ9F Y!$!o+9ַ'"|@/_;v B`bj }TA;EB?y;-"T)bd5@= $Apc`0}F˸_e<}_(w#TwU8]7oMlhvR*a{V&C|BiawBވ0d{{潵b/FIBNf#hCh=iEj{LwcXNE$<?)b@v0 ݱpܧ>R 4??Gîm]/^o lͶ5t]U3$ݚ)q"ݽt o/?>~3MF,{Ѽ* f QV3" }'F.S@F" P4<)dPNjol@đuX(ܑ8}8 rᙇQD*;<` _1Enn"_''c8f'|& Cfa;p0Z!P(baG*ǰmZov T$DrlLzY+\ zzͫTt1@rU!K_<1, ,Ucq!DWP?BOP.tEXtcomment CREATOR: GIMP PNM Filter Version 1.1 8IENDB`django-imagekit-5.0/tests/models.py000066400000000000000000000040661450514404100174130ustar00rootroot00000000000000from django.db import models from imagekit import ImageSpec from imagekit.models import ImageSpecField, ProcessedImageField from imagekit.processors import Adjust, ResizeToFill, SmartCrop class Thumbnail(ImageSpec): processors = [ResizeToFill(100, 60)] format = 'JPEG' options = {'quality': 60} class ImageModel(models.Model): image = models.ImageField(upload_to='b') class Photo(models.Model): original_image = models.ImageField(upload_to='photos') # Implicit source field thumbnail = ImageSpecField([Adjust(contrast=1.2, sharpness=1.1), ResizeToFill(50, 50)], format='JPEG', options={'quality': 90}) smartcropped_thumbnail = ImageSpecField([Adjust(contrast=1.2, sharpness=1.1), SmartCrop(50, 50)], source='original_image', format='JPEG', options={'quality': 90}) class ProcessedImageFieldModel(models.Model): processed = ProcessedImageField([SmartCrop(50, 50)], format='JPEG', options={'quality': 90}, upload_to='p') class ProcessedImageFieldWithSpecModel(models.Model): processed = ProcessedImageField(spec=Thumbnail, upload_to='p') class CountingCacheFileStrategy: def __init__(self): self.on_existence_required_count = 0 self.on_content_required_count = 0 self.on_source_saved_count = 0 def on_existence_required(self, file): self.on_existence_required_count += 1 def on_content_required(self, file): self.on_content_required_count += 1 def on_source_saved(self, file): self.on_source_saved_count += 1 class AbstractImageModel(models.Model): original_image = models.ImageField(upload_to='photos') abstract_class_spec = ImageSpecField(source='original_image', format='JPEG', cachefile_strategy=CountingCacheFileStrategy()) class Meta: abstract = True class ConcreteImageModel(AbstractImageModel): pass class ConcreteImageModelSubclass(ConcreteImageModel): pass django-imagekit-5.0/tests/settings.py000066400000000000000000000021761450514404100177700ustar00rootroot00000000000000import os ADMINS = ( ('test@example.com', 'TEST-R'), ) BASE_PATH = os.path.abspath(os.path.dirname(__file__)) MEDIA_ROOT = os.path.normpath(os.path.join(BASE_PATH, 'media')) DATABASES = { 'default': { 'ENGINE': 'django.db.backends.sqlite3', 'NAME': 'imagekit.db', }, } SECRET_KEY = '_uobce43e5osp8xgzle*yag2_16%y$sf*5(12vfg25hpnxik_*' INSTALLED_APPS = [ 'django.contrib.auth', 'django.contrib.contenttypes', 'imagekit', 'tests', ] CACHE_BACKEND = 'locmem://' TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [ 'django.contrib.auth.context_processors.auth', 'django.template.context_processors.debug', 'django.template.context_processors.i18n', 'django.template.context_processors.media', 'django.template.context_processors.static', 'django.template.context_processors.tz', 'django.contrib.messages.context_processors.messages', ], }, }, ] django-imagekit-5.0/tests/test_abstract_models.py000066400000000000000000000005511450514404100223300ustar00rootroot00000000000000from imagekit.utils import get_nonabstract_descendants from .models import (AbstractImageModel, ConcreteImageModel, ConcreteImageModelSubclass) def test_nonabstract_descendants_generator(): descendants = list(get_nonabstract_descendants(AbstractImageModel)) assert descendants == [ConcreteImageModel, ConcreteImageModelSubclass] django-imagekit-5.0/tests/test_cachefiles.py000066400000000000000000000067031450514404100212550ustar00rootroot00000000000000from hashlib import md5 from unittest import mock import pytest from django.conf import settings from imagekit.cachefiles import ImageCacheFile, LazyImageCacheFile from imagekit.cachefiles.backends import Simple from .imagegenerators import TestSpec from .utils import (DummyAsyncCacheFileBackend, assert_file_is_falsy, assert_file_is_truthy, get_image_file, get_unique_image_file) def test_no_source_falsiness(): """ Ensure cache files generated from sourceless specs are falsy. """ spec = TestSpec(source=None) file = ImageCacheFile(spec) assert_file_is_falsy(file) def test_sync_backend_truthiness(): """ Ensure that a cachefile with a synchronous cache file backend (the default) is truthy. """ spec = TestSpec(source=get_unique_image_file()) file = ImageCacheFile(spec) assert_file_is_truthy(file) def test_async_backend_falsiness(): """ Ensure that a cachefile with an asynchronous cache file backend is falsy. """ spec = TestSpec(source=get_unique_image_file()) file = ImageCacheFile(spec, cachefile_backend=DummyAsyncCacheFileBackend()) assert_file_is_falsy(file) def test_no_source_error(): spec = TestSpec(source=None) file = ImageCacheFile(spec) with pytest.raises(TestSpec.MissingSource): file.generate() def test_repr_does_not_send_existence_required(): """ Ensure that `__repr__` method does not send `existance_required` signal Cachefile strategy may be configured to generate file on `existance_required`. To generate images, backend passes `ImageCacheFile` instance to worker. Both celery and RQ calls `__repr__` method for each argument to enque call. And if `__repr__` of object will send this signal, we will get endless recursion """ with mock.patch('imagekit.cachefiles.existence_required') as signal: # import here to apply mock from imagekit.cachefiles import ImageCacheFile spec = TestSpec(source=get_unique_image_file()) file = ImageCacheFile( spec, cachefile_backend=DummyAsyncCacheFileBackend() ) file.__repr__() assert signal.send.called is False def test_memcached_cache_key(): """ Ensure the default cachefile backend is sanitizing its cache key for memcached by default. """ class MockFile: def __init__(self, name): self.name = name backend = Simple() extra_char_count = len('state-') + len(settings.IMAGEKIT_CACHE_PREFIX) length = 199 - extra_char_count filename = '1' * length file = MockFile(filename) assert backend.get_key(file) == '%s%s-state' % (settings.IMAGEKIT_CACHE_PREFIX, file.name) length = 200 - extra_char_count filename = '1' * length file = MockFile(filename) assert backend.get_key(file) == '%s%s:%s' % ( settings.IMAGEKIT_CACHE_PREFIX, '1' * (200 - len(':') - 32 - len(settings.IMAGEKIT_CACHE_PREFIX)), md5(('%s%s-state' % (settings.IMAGEKIT_CACHE_PREFIX, filename)).encode('utf-8')).hexdigest()) def test_lazyfile_stringification(): file = LazyImageCacheFile('testspec', source=None) assert str(file) == '' assert repr(file) == '' with get_image_file() as source_file: file = LazyImageCacheFile('testspec', source=source_file) file.name = 'a.jpg' assert str(file) == 'a.jpg' assert repr(file) == '' django-imagekit-5.0/tests/test_closing_fieldfiles.py000066400000000000000000000015121450514404100230040ustar00rootroot00000000000000import pytest from .models import Thumbnail from .utils import create_photo @pytest.mark.django_db(transaction=True) def test_do_not_leak_open_files(): instance = create_photo('leak-test.jpg') source_file = instance.original_image # Ensure the FieldFile is closed before generation source_file.close() image_generator = Thumbnail(source=source_file) image_generator.generate() assert source_file.closed @pytest.mark.django_db(transaction=True) def test_do_not_close_open_files_after_generate(): instance = create_photo('do-not-close-test.jpg') source_file = instance.original_image # Ensure the FieldFile is opened before generation source_file.open() image_generator = Thumbnail(source=source_file) image_generator.generate() assert not source_file.closed source_file.close() django-imagekit-5.0/tests/test_fields.py000066400000000000000000000033571450514404100204370ustar00rootroot00000000000000import pytest 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 . import imagegenerators # noqa from .models import (ImageModel, ProcessedImageFieldModel, ProcessedImageFieldWithSpecModel) from .utils import get_image_file @pytest.mark.django_db(transaction=True) def test_model_processedimagefield(): instance = ProcessedImageFieldModel() with File(get_image_file()) as file: instance.processed.save('whatever.jpeg', file) instance.save() assert instance.processed.width == 50 assert instance.processed.height == 50 @pytest.mark.django_db(transaction=True) def test_model_processedimagefield_with_spec(): instance = ProcessedImageFieldWithSpecModel() with File(get_image_file()) as file: instance.processed.save('whatever.jpeg', file) instance.save() assert instance.processed.width == 100 assert instance.processed.height == 60 @pytest.mark.django_db(transaction=True) 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 fields = 'image', with get_image_file() as upload_file: files = { 'image': SimpleUploadedFile('abc.jpg', upload_file.read()) } form = TestForm({}, files) instance = form.save() assert instance.image.width == 50 assert instance.image.height == 50 django-imagekit-5.0/tests/test_generateimage_tag.py000066400000000000000000000032131450514404100226100ustar00rootroot00000000000000import pytest from django.template import TemplateSyntaxError from . import imagegenerators # noqa from .utils import clear_imagekit_cache, get_html_attrs, render_tag def test_img_tag(): ttag = r"""{% generateimage 'testspec' source=img %}""" clear_imagekit_cache() attrs = get_html_attrs(ttag) expected_attrs = {'src', 'width', 'height'} assert set(attrs.keys()) == expected_attrs for k in expected_attrs: assert attrs[k].strip() != '' def test_img_tag_attrs(): ttag = r"""{% generateimage 'testspec' source=img -- alt="Hello" %}""" clear_imagekit_cache() attrs = get_html_attrs(ttag) assert attrs.get('alt') == 'Hello' def test_dangling_html_attrs_delimiter(): ttag = r"""{% generateimage 'testspec' source=img -- %}""" with pytest.raises(TemplateSyntaxError): render_tag(ttag) def test_html_attrs_assignment(): """ You can either use generateimage as an assignment tag or specify html attrs, but not both. """ ttag = r"""{% generateimage 'testspec' source=img -- alt="Hello" as th %}""" with pytest.raises(TemplateSyntaxError): 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' source=img -- width="50" %}""" clear_imagekit_cache() attrs = get_html_attrs(ttag) assert 'height' not in attrs def test_assignment_tag(): ttag = r"""{% generateimage 'testspec' source=img as th %}{{ th.url }}{{ th.height }}{{ th.width }}""" clear_imagekit_cache() html = render_tag(ttag) assert html.strip() != '' django-imagekit-5.0/tests/test_no_extra_queries.py000066400000000000000000000006641450514404100225430ustar00rootroot00000000000000from unittest.mock import Mock, PropertyMock, patch from .models import Photo def test_dont_access_source(): """ Touching the source may trigger an unneeded query. See """ pmock = PropertyMock() pmock.__get__ = Mock() with patch.object(Photo, 'original_image', pmock): photo = Photo() # noqa assert not pmock.__get__.called django-imagekit-5.0/tests/test_optimistic_strategy.py000066400000000000000000000024341450514404100232720ustar00rootroot00000000000000from unittest.mock import Mock from django.core.files.storage import FileSystemStorage from imagekit.cachefiles import ImageCacheFile from imagekit.cachefiles.backends import Simple as SimpleCFBackend from imagekit.cachefiles.strategies import Optimistic as OptimisticStrategy from .utils import create_image class ImageGenerator: def generate(self): return create_image() def get_hash(self): return 'abc123' def get_image_cache_file(): storage = Mock(FileSystemStorage) backend = SimpleCFBackend() strategy = OptimisticStrategy() generator = ImageGenerator() return ImageCacheFile(generator, storage=storage, cachefile_backend=backend, cachefile_strategy=strategy) def test_no_io_on_bool(): """ When checking the truthiness of an ImageCacheFile, the storage shouldn't perform IO operations. """ file = get_image_cache_file() bool(file) assert not file.storage.exists.called assert not file.storage.open.called def test_no_io_on_url(): """ When getting the URL of an ImageCacheFile, the storage shouldn't be checked. """ file = get_image_cache_file() file.url assert not file.storage.exists.called assert not file.storage.open.called django-imagekit-5.0/tests/test_serialization.py000066400000000000000000000025351450514404100220430ustar00rootroot00000000000000""" Make sure that the various IK classes can be successfully serialized and deserialized. This is important when using IK with Celery. """ import pytest from imagekit.cachefiles import ImageCacheFile from .imagegenerators import TestSpec from .utils import (clear_imagekit_cache, create_photo, get_unique_image_file, pickleback) @pytest.mark.django_db(transaction=True) def test_imagespecfield(): clear_imagekit_cache() instance = create_photo('pickletest2.jpg') thumbnail = pickleback(instance.thumbnail) thumbnail.generate() @pytest.mark.django_db(transaction=True) def test_circular_ref(): """ A model instance with a spec field in its dict shouldn't raise a KeyError. This corresponds to #234 """ clear_imagekit_cache() instance = create_photo('pickletest3.jpg') instance.thumbnail # Cause thumbnail to be added to instance's __dict__ pickleback(instance) def test_cachefiles(): clear_imagekit_cache() spec = TestSpec(source=get_unique_image_file()) file = ImageCacheFile(spec) file.url # remove link to file from spec source generator # test __getstate__ of ImageCacheFile file.generator.source = None restored_file = pickleback(file) assert file is not restored_file # Assertion for #437 and #451 assert file.storage is restored_file.storage django-imagekit-5.0/tests/test_settings.py000066400000000000000000000040711450514404100210230ustar00rootroot00000000000000import django from django.test import override_settings import pytest from imagekit.conf import ImageKitConf, settings from imagekit.utils import get_storage @pytest.mark.skipif( django.VERSION < (4, 2), reason="STORAGES was introduced in Django 4.2", ) def test_custom_storages(): with override_settings( STORAGES={ "default": { "BACKEND": "tests.utils.CustomStorage", } }, ): conf = ImageKitConf() assert conf.configure_default_file_storage(None) == "default" @pytest.mark.skipif( django.VERSION >= (5, 1), reason="DEFAULT_FILE_STORAGE is removed in Django 5.1.", ) def test_custom_default_file_storage(): with override_settings(DEFAULT_FILE_STORAGE="tests.utils.CustomStorage"): # If we don’t remove this, Django 4.2 will keep the old value. del settings.STORAGES conf = ImageKitConf() if django.VERSION >= (4, 2): assert conf.configure_default_file_storage(None) == "default" else: assert ( conf.configure_default_file_storage(None) == "tests.utils.CustomStorage" ) def test_get_storage_default(): from django.core.files.storage import FileSystemStorage assert isinstance(get_storage(), FileSystemStorage) @pytest.mark.skipif( django.VERSION >= (5, 1), reason="DEFAULT_FILE_STORAGE is removed in Django 5.1.", ) def test_get_storage_custom_path(): from tests.utils import CustomStorage with override_settings(IMAGEKIT_DEFAULT_FILE_STORAGE="tests.utils.CustomStorage"): assert isinstance(get_storage(), CustomStorage) @pytest.mark.skipif( django.VERSION < (4, 2), reason="STORAGES was introduced in Django 4.2", ) def test_get_storage_custom_key(): from tests.utils import CustomStorage with override_settings( STORAGES={ "custom": { "BACKEND": "tests.utils.CustomStorage", } }, IMAGEKIT_DEFAULT_FILE_STORAGE="custom", ): assert isinstance(get_storage(), CustomStorage) django-imagekit-5.0/tests/test_sourcegroups.py000066400000000000000000000036371450514404100217320ustar00rootroot00000000000000import pytest from django.core.files import File from imagekit.signals import source_saved from imagekit.specs.sourcegroups import ImageFieldSourceGroup from .models import AbstractImageModel, ConcreteImageModel, ImageModel from .utils import get_image_file def make_counting_receiver(source_group): def receiver(sender, *args, **kwargs): if sender is source_group: receiver.count += 1 receiver.count = 0 return receiver @pytest.mark.django_db(transaction=True) def test_source_saved_signal(): """ Creating a new instance with an image causes the source_saved signal to be dispatched. """ source_group = ImageFieldSourceGroup(ImageModel, 'image') receiver = make_counting_receiver(source_group) source_saved.connect(receiver) with File(get_image_file(), name='reference.png') as image: ImageModel.objects.create(image=image) assert receiver.count == 1 @pytest.mark.django_db(transaction=True) def test_no_source_saved_signal(): """ Creating a new instance without an image shouldn't cause the source_saved signal to be dispatched. https://github.com/matthewwithanm/django-imagekit/issues/214 """ source_group = ImageFieldSourceGroup(ImageModel, 'image') receiver = make_counting_receiver(source_group) source_saved.connect(receiver) ImageModel.objects.create() assert receiver.count == 0 @pytest.mark.django_db(transaction=True) def test_abstract_model_signals(): """ Source groups created for abstract models must cause signals to be dispatched on their concrete subclasses. """ source_group = ImageFieldSourceGroup(AbstractImageModel, 'original_image') receiver = make_counting_receiver(source_group) source_saved.connect(receiver) with File(get_image_file(), name='reference.png') as image: ConcreteImageModel.objects.create(original_image=image) assert receiver.count == 1 django-imagekit-5.0/tests/test_thumbnail_tag.py000066400000000000000000000046211450514404100220020ustar00rootroot00000000000000import pytest from django.template import TemplateSyntaxError from . import imagegenerators # noqa from .utils import clear_imagekit_cache, get_html_attrs, render_tag def test_img_tag(): ttag = r"""{% thumbnail '100x100' img %}""" clear_imagekit_cache() attrs = get_html_attrs(ttag) expected_attrs = {'src', 'width', 'height'} assert set(attrs.keys()) == expected_attrs for k in expected_attrs: assert attrs[k].strip() != '' def test_img_tag_anchor(): ttag = r"""{% thumbnail '100x100' img anchor='c' %}""" clear_imagekit_cache() attrs = get_html_attrs(ttag) expected_attrs = {'src', 'width', 'height'} assert set(attrs.keys()) == expected_attrs for k in expected_attrs: assert attrs[k].strip() != '' def test_img_tag_attrs(): ttag = r"""{% thumbnail '100x100' img -- alt="Hello" %}""" clear_imagekit_cache() attrs = get_html_attrs(ttag) assert attrs.get('alt') == 'Hello' def test_dangling_html_attrs_delimiter(): ttag = r"""{% thumbnail '100x100' img -- %}""" with pytest.raises(TemplateSyntaxError): render_tag(ttag) def test_not_enough_args(): ttag = r"""{% thumbnail '100x100' %}""" with pytest.raises(TemplateSyntaxError): render_tag(ttag) def test_too_many_args(): ttag = r"""{% thumbnail 'generator_id' '100x100' img 'extra' %}""" with pytest.raises(TemplateSyntaxError): render_tag(ttag) def test_html_attrs_assignment(): """ You can either use thumbnail as an assignment tag or specify html attrs, but not both. """ ttag = r"""{% thumbnail '100x100' img -- alt="Hello" as th %}""" with pytest.raises(TemplateSyntaxError): render_tag(ttag) def test_assignment_tag(): ttag = r"""{% thumbnail '100x100' img as th %}{{ th.url }}""" clear_imagekit_cache() html = render_tag(ttag) assert html != '' def test_assignment_tag_anchor(): ttag = r"""{% thumbnail '100x100' img anchor='c' as th %}{{ th.url }}""" clear_imagekit_cache() html = render_tag(ttag) assert html != '' def test_single_dimension(): ttag = r"""{% thumbnail '100x' img as th %}{{ th.width }}""" clear_imagekit_cache() html = render_tag(ttag) assert html == '100' def test_alternate_generator(): ttag = r"""{% thumbnail '1pxsq' '100x' img as th %}{{ th.width }}""" clear_imagekit_cache() html = render_tag(ttag) assert html == '1' django-imagekit-5.0/tests/test_utils.py000066400000000000000000000022061450514404100203210ustar00rootroot00000000000000import django from django.test import override_settings import pytest from imagekit.utils import get_storage def test_get_storage_default(): from django.core.files.storage import default_storage if django.VERSION >= (4, 2): assert get_storage() == default_storage else: assert isinstance(get_storage(), type(default_storage._wrapped)) @pytest.mark.skipif( django.VERSION >= (5, 1), reason="DEFAULT_FILE_STORAGE is removed in Django 5.1.", ) def test_get_storage_custom_import_path(): from tests.utils import CustomStorage with override_settings(IMAGEKIT_DEFAULT_FILE_STORAGE="tests.utils.CustomStorage"): assert isinstance(get_storage(), CustomStorage) @pytest.mark.skipif( django.VERSION < (4, 2), reason="STORAGES was introduced in Django 4.2", ) def test_get_storage_custom_key(): from tests.utils import CustomStorage with override_settings( STORAGES={ "custom": { "BACKEND": "tests.utils.CustomStorage", } }, IMAGEKIT_DEFAULT_FILE_STORAGE="custom", ): assert isinstance(get_storage(), CustomStorage) django-imagekit-5.0/tests/utils.py000066400000000000000000000053261450514404100172700ustar00rootroot00000000000000import os import pickle import shutil from io import BytesIO from tempfile import NamedTemporaryFile from bs4 import BeautifulSoup from django.core.files import File from django.core.files.storage import FileSystemStorage from django.template import Context, Template from PIL import Image from imagekit.cachefiles.backends import Simple from imagekit.conf import settings from imagekit.utils import get_cache from .models import Photo def get_image_file(): """ See also: http://en.wikipedia.org/wiki/Lenna http://sipi.usc.edu/database/database.php?volume=misc&image=12 https://lintian.debian.org/tags/license-problem-non-free-img-lenna.html https://github.com/libav/libav/commit/8895bf7b78650c0c21c88cec0484e138ec511a4b """ path = os.path.join(settings.MEDIA_ROOT, 'reference.png') return open(path, 'r+b') def get_unique_image_file(): file = NamedTemporaryFile() with get_image_file() as image: file.write(image.read()) return file def create_image(): return Image.open(get_image_file()) def create_instance(model_class, image_name): instance = model_class() img = File(get_image_file()) instance.original_image.save(image_name, img) instance.save() img.close() return instance def create_photo(name): return create_instance(Photo, name) def pickleback(obj): pickled = BytesIO() pickle.dump(obj, pickled) pickled.seek(0) return pickle.load(pickled) def render_tag(ttag): with get_image_file() as img: template = Template('{%% load imagekit %%}%s' % ttag) context = Context({'img': img}) return template.render(context) def get_html_attrs(ttag): return BeautifulSoup(render_tag(ttag), features="html.parser").img.attrs def assert_file_is_falsy(file): assert not bool(file), 'File is not falsy' def assert_file_is_truthy(file): assert bool(file), 'File is not truthy' class CustomStorage(FileSystemStorage): pass class DummyAsyncCacheFileBackend(Simple): """ A cache file backend meant to simulate async generation. """ is_async = True def generate(self, file, force=False): pass def clear_imagekit_cache(): cache = get_cache() cache.clear() # Clear IMAGEKIT_CACHEFILE_DIR cache_dir = os.path.join(settings.MEDIA_ROOT, settings.IMAGEKIT_CACHEFILE_DIR) if os.path.exists(cache_dir): shutil.rmtree(cache_dir) def clear_imagekit_test_files(): clear_imagekit_cache() for fname in os.listdir(settings.MEDIA_ROOT): if fname != 'reference.png': path = os.path.join(settings.MEDIA_ROOT, fname) if os.path.isdir(path): shutil.rmtree(path) else: os.remove(path) django-imagekit-5.0/tox.ini000066400000000000000000000014561450514404100157270ustar00rootroot00000000000000[tox] envlist = py37-django{32} py38-django{42, 41, 32} py39-django{42, 41, 32} py310-django{42, 41, 32} py311-django{42, 41} py311-djangomain, coverage-report [gh-actions] python = 3.7: py37 3.8: py38 3.9: py39 3.10: py310 3.11: py311, coverage-report [testenv] deps = -r test-requirements.txt django32: django~=3.2.0 django41: django~=4.1.0 django42: django~=4.2.0 djangomain: https://github.com/django/django/archive/refs/heads/main.zip setenv = COVERAGE_FILE=.coverage.{envname} commands = python -m pytest --cov --cov-report term-missing:skip-covered ignore_outcome = djangomain: true [testenv:coverage-report] deps = coverage skip_install = true setenv = COVERAGE_FILE=.coverage commands = coverage combine coverage report