├── .gitignore ├── LICENSE.txt ├── MANIFEST.in ├── README.md ├── README.rst ├── build.sh ├── djiiif ├── __init__.py └── templatetags │ ├── __init__.py │ └── iiiftags.py ├── docs ├── Makefile └── source │ ├── conf.py │ └── index.rst ├── requirements.txt └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | *.egg-info 3 | MANIFEST 4 | build 5 | dist 6 | docs/_build 7 | docs/_static 8 | venv* 9 | .tox 10 | *.bak 11 | .DS_Store 12 | .eggs/ 13 | .idea/ 14 | htmlcov/ 15 | .coverage 16 | .cache/ 17 | *.swp 18 | *.swo -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 Roger Howard 2 | 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | 1. Redistributions of source code must retain the above copyright notice, this 9 | list of conditions and the following disclaimer. 10 | 11 | 2. Redistributions in binary form must reproduce the above copyright notice, 12 | this list of conditions and the following disclaimer in the documentation 13 | and/or other materials provided with the distribution. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 16 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIEDi 17 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 19 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 20 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 21 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 22 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 24 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE.txt 2 | include README -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # django-iiif 2 | 3 | django-iiif is a package designed to make integrating the [IIIF Image API](http://iiif.io/api/image/2.1/) easier by extending Django's ImageField. By defining one or more named "profiles", your ImageFields expose IIIF-compatible URLs for each profile. 4 | 5 | ## Why Django-IIIF and not ImageKit 6 | 7 | I love ImageKit, but I recently worked on a project where we already had IIIF handling image derivative generation and serving, and Django ImageKit just got in the way. I wanted to still register my source images with Django, but serve them through an [IIIF server](https://github.com/loris-imageserver/loris), and this is what I came up with. I have lots of ideas for improvements here, but the initial release is just a santized version of what I used on my most recent project. 8 | 9 | ## Installation 10 | 11 | `pip install djiiif` 12 | 13 | ## Examples 14 | 15 | First, let's setup a new field (or convert an existing ImageField): 16 | 17 | `models.py` 18 | ```python 19 | from djiiif import IIIFField 20 | 21 | original = IIIFField() 22 | ``` 23 | 24 | Second, configure the relevant settings. 25 | 26 | `settings.py` 27 | ```python 28 | 29 | IIIF_HOST = 'http://server/' 30 | 31 | IIIF_PROFILES = { 32 | 'thumbnail': 33 | {'host': IIIF_HOST, 34 | 'region': 'full', 35 | 'size': '150,', 36 | 'rotation': '0', 37 | 'quality': 'default', 38 | 'format': 'jpg'} 39 | } 40 | ``` 41 | 42 | 43 | Finally, we can access profile(s) as attributes of the `iiif` attribute on an instance of `original`. 44 | 45 | In Python: 46 | 47 | ```python 48 | print(instance.original.name) 49 | > uploads/filename.jpg 50 | 51 | print(instance.original.iiif.thumbnail) 52 | > http://server/uploads/filename.jpg/full/150,/0/default.jpg 53 | ``` 54 | 55 | 56 | In a Django template: 57 | 58 | ``` 59 | 60 | ``` 61 | 62 | As of version 0.15, we can also generate a IIIF info.json URL: 63 | 64 | ``` 65 | print(instance.original.iiif.info) 66 | > http://server/uploads/filename.jpg/info.json 67 | ``` 68 | 69 | ### callable-based profiles 70 | 71 | You can also use a callable to dynamically generate a URL. The callable will receive the parent `IIIFFieldFile` (a subclass of `ImageFieldFile`) as its sole parameter, `parent`, and must return a `dict` with the following keys: host, region, size, rotation, quality, and format. Using a callable allows you to implement more complex logic in your profile, including the ability to access the original file's name, width, and height. 72 | 73 | An example of a callable-based profile named `square` is below, used to generate a square-cropped image. 74 | 75 | 76 | ```python 77 | def squareProfile(original): 78 | width, height = original.width, original.height 79 | 80 | if width > height: 81 | x = int((width - height) / 2) 82 | y = 0 83 | w = height 84 | h = height 85 | region = '{},{},{},{}'.format(x,y,w,h) 86 | elif width < height: 87 | x = 0 88 | y = int((height - width) / 2) 89 | w = width 90 | h = width 91 | region = '{},{},{},{}'.format(x,y,w,h) 92 | else: 93 | region = 'full' 94 | 95 | spec = {'host': IIIF_HOST, 96 | 'region': region, 97 | 'size': '256,256', 98 | 'rotation': '0', 99 | 'quality': 'default', 100 | 'format': 'jpg'} 101 | return spec 102 | ``` 103 | 104 | ```python 105 | IIIF_PROFILES = { 106 | 'thumbnail': 107 | {'host': IIIF_HOST, 108 | 'region': 'full', 109 | 'size': '150,', 110 | 'rotation': '0', 111 | 'quality': 'default', 112 | 'format': 'jpg'}, 113 | 'preview': 114 | {'host': IIIF_HOST, 115 | 'region': 'full', 116 | 'size': '600,', 117 | 'rotation': '0', 118 | 'quality': 'default', 119 | 'format': 'jpg'}, 120 | 'square': squareProfile 121 | } 122 | ``` 123 | 124 | ### IIIF Template Tag 125 | 126 | An alternate way to access IIIF URLs for your IIIFField is via the `iiif` template tag. 127 | 128 | First, add `djiiif` to your `INSTALLED_APPS`: 129 | 130 | 131 | ``` 132 | INSTALLED_APPS = [ 133 | ... 134 | 'djiiif' 135 | ] 136 | ``` 137 | 138 | 139 | Next, load our template tag library `iiiftags` in your template: 140 | 141 | ``` 142 | {% load iiiftags %} 143 | ``` 144 | 145 | Finally, use it in a template: 146 | 147 | ``` 148 | {% iiif asset.original 'thumbnail' %} 149 | ``` 150 | 151 | The first parameter (asset.original) is a reference to an IIIFField instance. 152 | 153 | The second parameter ('thumbnail') is the name of one of your IIIF profiles. 154 | 155 | This tag syntax is effectively the same as: 156 | 157 | ``` 158 | {{ asset.original.iiif.thumbnail }} 159 | ``` 160 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | 2 | # djiiif 3 | 4 | djiiif is a package designed to make integrating the [IIIF Image API](http://iiif.io/api/image/2.1/) easier by extending Django's ImageField. By defining one or more named "profiles", your ImageFields expose IIIF-compatible URLs for each profile. 5 | 6 | ## Why djiiif and not ImageKit 7 | 8 | I love ImageKit, but I recently worked on a project where we already had IIIF handling image derivative generation and serving, and Django ImageKit just got in the way. I wanted to still register my source images with Django, but serve them through an [IIIF server](https://github.com/loris-imageserver/loris), and this is what I came up with. I have lots of ideas for improvements here, but the initial release is just a santized version of what I used on my most recent project. 9 | 10 | ## Installation 11 | 12 | `pip install djiiif` 13 | 14 | ## Examples 15 | 16 | First, let's setup a new field (or convert an existing ImageField): 17 | 18 | 19 | 20 | `models.py` 21 | ```python 22 | from djiiif import IIIFField 23 | 24 | original = IIIFField() 25 | ``` 26 | 27 | Second, configure the relevant settings. 28 | 29 | `settings.py` 30 | ```python 31 | 32 | IIIF_HOST = 'http://server/' 33 | 34 | IIIF_PROFILES = { 35 | 'thumbnail': 36 | {'host': IIIF_HOST, 37 | 'region': 'full', 38 | 'size': '150,', 39 | 'rotation': '0', 40 | 'quality': 'default', 41 | 'format': 'jpg'} 42 | } 43 | ``` 44 | 45 | 46 | Finally, we can access profile(s) as attributes of the `iiif` attribute on an instance of `original`. 47 | 48 | In Python: 49 | 50 | ```python 51 | print(instance.original.name) 52 | > uploads/filename.jpg 53 | 54 | print(instance.original.iiif.thumbnail) 55 | > http://server/uploads/filename.jpg/full/150,/0/default.jpg 56 | ``` 57 | 58 | 59 | In a Django template: 60 | 61 | ``` 62 | 63 | ``` 64 | 65 | As of version 0.15, we can also generate a IIIF info.json URL: 66 | 67 | ``` 68 | print(instance.original.iiif.info) 69 | > http://server/uploads/filename.jpg/info.json 70 | ``` 71 | 72 | ### callable-based profiles 73 | 74 | You can also use a callable to dynamically generate a URL. The callable will receive the parent `IIIFFieldFile` (a subclass of `ImageFieldFile`) as its sole parameter, `parent`, and must return a `dict` with the following keys: host, region, size, rotation, quality, and format. Using a callable allows you to implement more complex logic in your profile, including the ability to access the original file's name, width, and height. 75 | 76 | An example of a callable-based profile named `square` is below, used to generate a square-cropped image. 77 | 78 | 79 | ```python 80 | def squareProfile(original): 81 | width, height = original.width, original.height 82 | 83 | if width > height: 84 | x = int((width - height) / 2) 85 | y = 0 86 | w = height 87 | h = height 88 | region = '{},{},{},{}'.format(x,y,w,h) 89 | elif width < height: 90 | x = 0 91 | y = int((height - width) / 2) 92 | w = width 93 | h = width 94 | region = '{},{},{},{}'.format(x,y,w,h) 95 | else: 96 | region = 'full' 97 | 98 | spec = {'host': IIIF_HOST, 99 | 'region': region, 100 | 'size': '256,256', 101 | 'rotation': '0', 102 | 'quality': 'default', 103 | 'format': 'jpg'} 104 | return spec 105 | ``` 106 | 107 | ```python 108 | IIIF_PROFILES = { 109 | 'thumbnail': 110 | {'host': IIIF_HOST, 111 | 'region': 'full', 112 | 'size': '150,', 113 | 'rotation': '0', 114 | 'quality': 'default', 115 | 'format': 'jpg'}, 116 | 'preview': 117 | {'host': IIIF_HOST, 118 | 'region': 'full', 119 | 'size': '600,', 120 | 'rotation': '0', 121 | 'quality': 'default', 122 | 'format': 'jpg'}, 123 | 'square': squareProfile 124 | } 125 | ``` 126 | 127 | ### IIIF Template Tag 128 | 129 | An alternate way to access IIIF URLs for your IIIFField is via the `iiif` template tag. 130 | 131 | First, add `djiiif` to your `INSTALLED_APPS`: 132 | 133 | 134 | ``` 135 | INSTALLED_APPS = [ 136 | ... 137 | 'djiiif' 138 | ] 139 | ``` 140 | 141 | 142 | Next, load our template tag library `iiiftags` in your template: 143 | 144 | ``` 145 | {% load iiiftags %} 146 | ``` 147 | 148 | Finally, use it in a template: 149 | 150 | ``` 151 | {% iiif asset.original 'thumbnail' %} 152 | ``` 153 | 154 | The first parameter (asset.original) is a reference to an IIIFField instance. 155 | 156 | The second parameter ('thumbnail') is the name of one of your IIIF profiles. 157 | 158 | This tag syntax is effectively the same as: 159 | 160 | ``` 161 | {{ asset.original.iiif.thumbnail }} 162 | ``` 163 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | python3 setup.py sdist bdist_wheel 4 | python3 -m twine upload dist/* -------------------------------------------------------------------------------- /djiiif/__init__.py: -------------------------------------------------------------------------------- 1 | from django.db.models import ImageField 2 | from django.db.models.fields.files import ImageFieldFile 3 | from django.conf import settings 4 | 5 | 6 | def urljoin(parts): 7 | """ 8 | Takes a list of URL parts and smushes em together into a string, 9 | while ensuring no double slashes, but preserving any trailing slash(es) 10 | """ 11 | if len(parts) == 0: 12 | raise ValueError('urljoin needs a list of at least length 1') 13 | return '/'.join([x.strip('/') for x in parts[0:-1]] + [parts[-1].lstrip('/')]) 14 | 15 | 16 | class IIIFObject(object): 17 | def __init__(self, parent): 18 | 19 | # for each profile defined in settings 20 | for name in settings.IIIF_PROFILES: 21 | profile = settings.IIIF_PROFILES[name] 22 | 23 | if parent.name: 24 | if type(profile) is dict: 25 | iiif = profile 26 | elif callable(profile): 27 | iiif = profile(parent) 28 | 29 | identifier = parent.name.replace("/", "%2F") 30 | 31 | url = urljoin([iiif['host'], identifier, iiif['region'], iiif['size'], iiif['rotation'], '{}.{}'.format(iiif['quality'], iiif['format'])]) 32 | setattr(self, name, url) 33 | else: 34 | setattr(self, name, "") 35 | 36 | # Add info.json URL 37 | if parent.name: 38 | url = urljoin([iiif['host'], identifier, "info.json"]) 39 | setattr(self, "info", url) 40 | else: 41 | setattr(self, "info", "") 42 | 43 | # Add plain identifier URL 44 | if parent.name: 45 | url = urljoin([iiif['host'], identifier) 46 | setattr(self, "identifier", url) 47 | else: 48 | setattr(self, "identifier", "") 49 | 50 | 51 | class IIIFFieldFile(ImageFieldFile): 52 | @property 53 | def iiif(self): 54 | return IIIFObject(self) 55 | 56 | def __init__(self, *args, **kwargs): 57 | super(IIIFFieldFile, self).__init__(*args, **kwargs) 58 | 59 | 60 | class IIIFField(ImageField): 61 | attr_class = IIIFFieldFile 62 | 63 | def __init__(self, *args, **kwargs): 64 | super(IIIFField, self).__init__(*args, **kwargs) 65 | -------------------------------------------------------------------------------- /djiiif/templatetags/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | from django.db.models import ImageField 3 | from django.db.models.fields.files import ImageFieldFile 4 | from django.conf import settings 5 | from django import template 6 | 7 | 8 | 9 | def urljoin(parts): 10 | """ 11 | Takes a list of URL parts and smushes em together into a string, 12 | while ensuring no double slashes, but preserving any trailing slash(es) 13 | """ 14 | if len(parts) == 0: 15 | raise ValueError('urljoin needs a list of at least length 1') 16 | return '/'.join([x.strip('/') for x in parts[0:-1]] + [parts[-1].lstrip('/')]) 17 | 18 | 19 | class IIIFObject(object): 20 | def __init__(self, parent): 21 | for name in settings.IIIF_PROFILES: 22 | 23 | profile = settings.IIIF_PROFILES[name] 24 | 25 | if type(profile) is dict: 26 | iiif = profile 27 | elif callable(profile): 28 | iiif = profile(parent) 29 | 30 | url = urljoin([iiif['host'], parent.name, iiif['region'], iiif['size'], iiif['rotation'], '{}.{}'.format(iiif['quality'], iiif['format'])]) 31 | setattr(self, name, url) 32 | 33 | setattr(self, 'url', urljoin([settings.IIIF_HOST, parent.name])) 34 | 35 | 36 | class IIIFFieldFile(ImageFieldFile): 37 | @property 38 | def iiif(self): 39 | return IIIFObject(self) 40 | 41 | def __init__(self, *args, **kwargs): 42 | super(IIIFFieldFile, self).__init__(*args, **kwargs) 43 | 44 | 45 | class IIIFField(ImageField): 46 | attr_class = IIIFFieldFile 47 | 48 | def __init__(self, *args, **kwargs): 49 | super(IIIFField, self).__init__(*args, **kwargs) 50 | -------------------------------------------------------------------------------- /djiiif/templatetags/iiiftags.py: -------------------------------------------------------------------------------- 1 | 2 | from django.db.models import ImageField 3 | from django.db.models.fields.files import ImageFieldFile 4 | from django.conf import settings 5 | from django import template 6 | from djiiif import IIIFObject 7 | 8 | 9 | class NotAnIIIFField(AttributeError): 10 | def __init__(self, *args, **kwargs): 11 | AttributeError.__init__(self, *args, **kwargs) 12 | 13 | register = template.Library() 14 | 15 | 16 | @register.simple_tag 17 | def iiif(imagefield, profile): 18 | """ 19 | Returns an IIIF URL based on an IIIFField and a profile stored in IIIF_PROFILES. 20 | """ 21 | try: 22 | return getattr(imagefield.iiif, profile) 23 | except AttributeError: 24 | raise NotAnIIIFField('The iiif template tag expects an instance of a IIIFField as its first parameter.') -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = python -msphinx 7 | SPHINXPROJ = djiiif 8 | SOURCEDIR = source 9 | BUILDDIR = build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | # djiiif documentation build configuration file, created by 5 | # sphinx-quickstart on Sun Oct 22 16:02:01 2017. 6 | # 7 | # This file is execfile()d with the current directory set to its 8 | # containing dir. 9 | # 10 | # Note that not all possible configuration values are present in this 11 | # autogenerated file. 12 | # 13 | # All configuration values have a default; values that are commented out 14 | # serve to show the default. 15 | 16 | # If extensions (or modules to document with autodoc) are in another directory, 17 | # add these directories to sys.path here. If the directory is relative to the 18 | # documentation root, use os.path.abspath to make it absolute, like shown here. 19 | # 20 | # import os 21 | # import sys 22 | # sys.path.insert(0, os.path.abspath('.')) 23 | 24 | 25 | # -- General configuration ------------------------------------------------ 26 | 27 | # If your documentation needs a minimal Sphinx version, state it here. 28 | # 29 | # needs_sphinx = '1.0' 30 | 31 | # Add any Sphinx extension module names here, as strings. They can be 32 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 33 | # ones. 34 | extensions = [] 35 | 36 | # Add any paths that contain templates here, relative to this directory. 37 | templates_path = ['_templates'] 38 | 39 | # The suffix(es) of source filenames. 40 | # You can specify multiple suffix as a list of string: 41 | # 42 | # source_suffix = ['.rst', '.md'] 43 | source_suffix = '.rst' 44 | 45 | # The master toctree document. 46 | master_doc = 'index' 47 | 48 | # General information about the project. 49 | project = 'djiiif' 50 | copyright = '2017, Roger Howard' 51 | author = 'Roger Howard' 52 | 53 | # The version info for the project you're documenting, acts as replacement for 54 | # |version| and |release|, also used in various other places throughout the 55 | # built documents. 56 | # 57 | # The short X.Y version. 58 | version = '0.1' 59 | # The full version, including alpha/beta/rc tags. 60 | release = '0.1' 61 | 62 | # The language for content autogenerated by Sphinx. Refer to documentation 63 | # for a list of supported languages. 64 | # 65 | # This is also used if you do content translation via gettext catalogs. 66 | # Usually you set "language" from the command line for these cases. 67 | language = None 68 | 69 | # List of patterns, relative to source directory, that match files and 70 | # directories to ignore when looking for source files. 71 | # This patterns also effect to html_static_path and html_extra_path 72 | exclude_patterns = [] 73 | 74 | # The name of the Pygments (syntax highlighting) style to use. 75 | pygments_style = 'sphinx' 76 | 77 | # If true, `todo` and `todoList` produce output, else they produce nothing. 78 | todo_include_todos = False 79 | 80 | 81 | # -- Options for HTML output ---------------------------------------------- 82 | 83 | # The theme to use for HTML and HTML Help pages. See the documentation for 84 | # a list of builtin themes. 85 | # 86 | html_theme = 'alabaster' 87 | 88 | # Theme options are theme-specific and customize the look and feel of a theme 89 | # further. For a list of options available for each theme, see the 90 | # documentation. 91 | # 92 | # html_theme_options = {} 93 | 94 | # Add any paths that contain custom static files (such as style sheets) here, 95 | # relative to this directory. They are copied after the builtin static files, 96 | # so a file named "default.css" will overwrite the builtin "default.css". 97 | html_static_path = ['_static'] 98 | 99 | # Custom sidebar templates, must be a dictionary that maps document names 100 | # to template names. 101 | # 102 | # This is required for the alabaster theme 103 | # refs: http://alabaster.readthedocs.io/en/latest/installation.html#sidebars 104 | html_sidebars = { 105 | '**': [ 106 | 'about.html', 107 | 'navigation.html', 108 | 'relations.html', # needs 'show_related': True theme option to display 109 | 'searchbox.html', 110 | 'donate.html', 111 | ] 112 | } 113 | 114 | 115 | # -- Options for HTMLHelp output ------------------------------------------ 116 | 117 | # Output file base name for HTML help builder. 118 | htmlhelp_basename = 'djiiifdoc' 119 | 120 | 121 | # -- Options for LaTeX output --------------------------------------------- 122 | 123 | latex_elements = { 124 | # The paper size ('letterpaper' or 'a4paper'). 125 | # 126 | # 'papersize': 'letterpaper', 127 | 128 | # The font size ('10pt', '11pt' or '12pt'). 129 | # 130 | # 'pointsize': '10pt', 131 | 132 | # Additional stuff for the LaTeX preamble. 133 | # 134 | # 'preamble': '', 135 | 136 | # Latex figure (float) alignment 137 | # 138 | # 'figure_align': 'htbp', 139 | } 140 | 141 | # Grouping the document tree into LaTeX files. List of tuples 142 | # (source start file, target name, title, 143 | # author, documentclass [howto, manual, or own class]). 144 | latex_documents = [ 145 | (master_doc, 'djiiif.tex', 'djiiif Documentation', 146 | 'Roger Howard', 'manual'), 147 | ] 148 | 149 | 150 | # -- Options for manual page output --------------------------------------- 151 | 152 | # One entry per manual page. List of tuples 153 | # (source start file, name, description, authors, manual section). 154 | man_pages = [ 155 | (master_doc, 'djiiif', 'djiiif Documentation', 156 | [author], 1) 157 | ] 158 | 159 | 160 | # -- Options for Texinfo output ------------------------------------------- 161 | 162 | # Grouping the document tree into Texinfo files. List of tuples 163 | # (source start file, target name, title, author, 164 | # dir menu entry, description, category) 165 | texinfo_documents = [ 166 | (master_doc, 'djiiif', 'djiiif Documentation', 167 | author, 'djiiif', 'One line description of project.', 168 | 'Miscellaneous'), 169 | ] 170 | 171 | 172 | 173 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | .. djiiif documentation master file, created by 2 | sphinx-quickstart on Sun Oct 22 16:02:01 2017. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to djiiif's documentation! 7 | ================================== 8 | 9 | .. toctree:: 10 | :maxdepth: 2 11 | :caption: Contents: 12 | 13 | 14 | 15 | Indices and tables 16 | ================== 17 | 18 | * :ref:`genindex` 19 | * :ref:`modindex` 20 | * :ref:`search` 21 | 22 | 23 | djiiif 24 | ====== 25 | 26 | djiiif is a package designed to make integrating the `IIIF Image API`_ 27 | easier by extending Django’s ImageField. By defining one or more named 28 | “profiles”, your ImageFields expose IIIF-compatible URLs for each 29 | profile. 30 | 31 | Why djiiif and not ImageKit 32 | --------------------------- 33 | 34 | I love ImageKit, but I recently worked on a project where we already had 35 | IIIF handling image derivative generation and serving, and Django 36 | ImageKit just got in the way. I wanted to still register my source 37 | images with Django, but serve them through an `IIIF server`_, and this 38 | is what I came up with. I have lots of ideas for improvements here, but 39 | the initial release is just a santized version of what I used on my most 40 | recent project. 41 | 42 | Installation 43 | ------------ 44 | 45 | ``pip install djiiif`` 46 | 47 | Examples 48 | -------- 49 | 50 | First, let’s setup a new field (or convert an existing ImageField): 51 | 52 | ``models.py`` 53 | 54 | .. code:: python 55 | 56 | from djiiif import IIIFField 57 | 58 | original = IIIFField() 59 | 60 | Second, configure the relevant settings. 61 | 62 | ``settings.py`` 63 | 64 | .. code:: python 65 | 66 | 67 | IIIF_HOST = 'http://server/' 68 | 69 | IIIF_PROFILES = { 70 | 'thumbnail': 71 | {'host': IIIF_HOST, 72 | 'region': 'full', 73 | 'size': '150,', 74 | 'rotation': '0', 75 | 'quality': 'default', 76 | 'format': 'jpg'} 77 | } 78 | 79 | Finally, we can access profile(s) as attributes of the ``iiif`` 80 | attribute on an instance of ``original``. 81 | 82 | In Python: 83 | 84 | .. code:: python 85 | 86 | print(instance.original.name) 87 | > uploads/filename.jpg 88 | 89 | print(instance.original.iiif.thumbnail) 90 | > http://server/uploads/filename.jpg/full/150,/0/default.jpg 91 | 92 | In a Django template: 93 | 94 | :: 95 | 96 | 97 | 98 | callable-based profiles 99 | ~~~~~~~~~~~~~~~~~~~~~~~ 100 | 101 | You can also use a callable to dynamically generate a URL. The callable 102 | will receive the parent ``IIIFFieldFile`` (a subclass of 103 | ``ImageFieldFile``) as its sole parameter, ``parent``, and must return a 104 | ``dict`` with the following keys: host, region, size, rotation, quality, 105 | and format. Using a callable allows you to implement more complex logic 106 | in your profile, including the ability to access the original file’s 107 | name, width, and height. 108 | 109 | An example of a callable-based profile named ``square`` is below, used 110 | to generate a square-cropped image. 111 | 112 | .. code:: python 113 | 114 | def squareProfile(original): 115 | width, height = original.width, original.height 116 | 117 | if width > height: 118 | x = int((width - height) / 2) 119 | y = 0 120 | w = height 121 | h = height 122 | region = '{},{},{},{}'.format(x,y,w,h) 123 | elif width < height: 124 | x = 0 125 | y = int((height - width) / 2) 126 | w = width 127 | h = width 128 | region = '{},{},{},{}'.format(x,y,w,h) 129 | else: 130 | region = 'full' 131 | 132 | spec = {'host': IIIF_HOST, 133 | 'region': region, 134 | 'size': '256,256', 135 | 'rotation': '0', 136 | 'quality': 'default', 137 | 'format': 'jpg'} 138 | return spec 139 | 140 | .. code:: python 141 | 142 | IIIF_PROFILES = { 143 | 'thumbnail': 144 | {'host': IIIF_HOST, 145 | 'region': 'full', 146 | 'size': '150,', 147 | 'rotation': '0', 148 | 'quality': 'default', 149 | 'format': 'jpg'}, 150 | 'preview': 151 | {'host': IIIF_HOST, 152 | 'region': 'full', 153 | 'size': '600,', 154 | 'rotation': '0', 155 | 'quality': 'default', 156 | 'format': 'jpg'}, 157 | 'square': squareProfile 158 | } 159 | 160 | 161 | IIIF Template Tag 162 | ~~~~~~~~~~~~~~~~~ 163 | 164 | An alternate way to access IIIF URLs for your IIIFField is via the `iiif` template tag. 165 | 166 | First, add ``djiiif`` to your ``INSTALLED_APPS``: 167 | 168 | .. code:: python 169 | 170 | INSTALLED_APPS = [ 171 | ... 172 | 'djiiif' 173 | ] 174 | 175 | 176 | Next, load our template tag library `iiiftags` in your template: 177 | 178 | .. code:: python 179 | 180 | {% load iiiftags %} 181 | 182 | 183 | Finally, use it in a template: 184 | 185 | .. code:: python 186 | 187 | {% iiif asset.original 'thumbnail' %} 188 | 189 | 190 | The first parameter (asset.original) is a reference to an IIIFField instance. 191 | 192 | The second parameter ('thumbnail') is the name of one of your IIIF profiles. 193 | 194 | This tag syntax is effectively the same as: 195 | 196 | .. code:: python 197 | 198 | {{ asset.original.iiif.thumbnail }} 199 | 200 | 201 | .. _IIIF Image API: http://iiif.io/api/image/2.1/ 202 | .. _IIIF server: https://github.com/loris-imageserver/loris 203 | 204 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | 2 | twine 3 | sphinx -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import find_packages, setup 2 | import os 3 | 4 | # allow setup.py to be run from any path 5 | os.chdir(os.path.normpath(os.path.join(os.path.abspath(__file__), os.pardir))) 6 | 7 | setup( 8 | name='djiiif', 9 | version='0.21', 10 | 11 | packages=find_packages(), 12 | install_requires=['Django'], 13 | include_package_data=True, 14 | license='BSD License', # example license 15 | description='Simple IIIF integration for Django.', 16 | long_description='djiiif is a package designed to make integrating the IIIF Image API easier by extending Django\'s ImageField', 17 | url='https://github.com/rogerhoward/djiiif/', 18 | author='Roger Howard', 19 | author_email='rogerhoward+django@gmail.com', 20 | classifiers=[ 21 | 'Environment :: Web Environment', 22 | 'Framework :: Django', 23 | 'Framework :: Django :: 1.11', 24 | 'Intended Audience :: Developers', 25 | 'License :: OSI Approved :: BSD License', 26 | 'Operating System :: OS Independent', 27 | 'Programming Language :: Python', 28 | 'Programming Language :: Python :: 3', 29 | 'Programming Language :: Python :: 3.6', 30 | 'Programming Language :: Python :: 3.7', 31 | 'Programming Language :: Python :: 3.8', 32 | 'Topic :: Internet :: WWW/HTTP', 33 | 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', 34 | ], 35 | ) --------------------------------------------------------------------------------