Skip to main content

djiiif

djiiif is a package designed to make integrating the IIIF Image API easier by extending Django's ImageField. By defining one or more named "profiles", your ImageFields expose IIIF-compatible URLs for each profile.

Why djiiif and not ImageKit

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, 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.

Installation

pip install djiiif

Examples

First, let's setup a new field (or convert an existing ImageField):

models.py

from djiiif import IIIFField

original = IIIFField()

Second, configure the relevant settings.

settings.py

IIIF_HOST = 'http://server/'

IIIF_PROFILES = {
    'thumbnail':
        {'host': IIIF_HOST, 
        'region': 'full', 
        'size': '150,',
        'rotation': '0',
        'quality': 'default',
        'format': 'jpg'}
}

Finally, we can access profile(s) as attributes of the iiif attribute on an instance of original.

In Python:

print(instance.original.name)
> uploads/filename.jpg

print(instance.original.iiif.thumbnail)
> http://server/uploads%2Ffilename.jpg/full/150,/0/default.jpg

(The identifier segment is percent-encoded — slashes become %2F, and other reserved characters are encoded too — so the field name occupies a single IIIF path segment.)

In a Django template:

<img src="{{ instance.original.iiif.thumbnail }}">

As of version 0.15, we can also generate a IIIF info.json URL:

print(instance.original.iiif.info)
> http://server/uploads%2Ffilename.jpg/info.json

As of version 0.21, we also expose a plain identifier-only URL (host + identifier, with no /region/size/rotation/quality.format suffix) — handy for handing the image off to viewers like OpenSeadragon that take just the IIIF identifier:

print(instance.original.iiif.identifier)
> http://server/uploads%2Ffilename.jpg

As of version 0.24, iiif.info_document returns the IIIF info.json document itself (a dict), assembled from the image's own dimensions — as opposed to iiif.info, which returns the URL of an external info.json served by an image server. Both remain available; .info is unchanged. This lets a Django view serve a minimal, spec-valid info.json without a separate image server:

from django.http import JsonResponse

def image_info(request, pk):
    asset = MyModel.objects.get(pk=pk)
    return JsonResponse(asset.original.iiif.info_document)
print(instance.original.iiif.info_document)
> {
>     "@context": "http://iiif.io/api/image/3/context.json",
>     "id": "http://server/uploads/filename.jpg",
>     "type": "ImageService3",
>     "protocol": "http://iiif.io/api/image",
>     "profile": "level2",
>     "width": 4000,
>     "height": 3000,
> }

Because it needs real pixel dimensions, accessing .info_document reads the image from storage (via width/height); the URL attributes above never do. For an empty/unset field it returns None. Two optional settings control the output:

  • IIIF_IMAGE_API_VERSION3 (default) emits an Image API 3.0 document (id / type: ImageService3); 2 emits a 2.x document (@id / profile array).
  • IIIF_COMPLIANCE_LEVEL — the advertised compliance level, default "level2".

Typed profiles

As of version 0.24, instead of a raw dict you can use the Profile dataclass, which carries IIIF Image API 3.0-friendly defaults (size="max") and validates its fields:

from djiiif import Profile

IIIF_PROFILES = {
    "thumbnail": Profile(host=IIIF_HOST, size="150,"),
    "square": Profile(host=IIIF_HOST, region="square", size="256,256"),
}

Note the square example: IIIF 3.0's square region replaces the hand-rolled crop math shown below. Profile also handles the two 3.0 features that are easy to get wrong as hand-written strings:

  • mirror=True prefixes the rotation with ! (mirrored image).
  • upscale=True prefixes the size with ^ (permits upscaling beyond the extracted region).

Plain dict and callable profiles keep working exactly as before — Profile is opt-in, and a callable may return either a dict or a Profile.

callable-based profiles

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.

An example of a callable-based profile named square is below, used to generate a square-cropped image.

def squareProfile(original):
    width, height = original.width, original.height

    if width > height:
        x = int((width - height) / 2)
        y = 0
        w = height
        h = height
        region = '{},{},{},{}'.format(x,y,w,h)
    elif width < height:
        x = 0
        y = int((height - width) / 2)
        w = width
        h = width
        region = '{},{},{},{}'.format(x,y,w,h)
    else:
        region = 'full'

    spec = {'host': IIIF_HOST, 
        'region': region, 
        'size': '256,256',
        'rotation': '0',
        'quality': 'default',
        'format': 'jpg'}
    return spec
IIIF_PROFILES = {
    'thumbnail':
        {'host': IIIF_HOST, 
        'region': 'full', 
        'size': '150,',
        'rotation': '0',
        'quality': 'default',
        'format': 'jpg'},
    'preview':
        {'host': IIIF_HOST, 
        'region': 'full', 
        'size': '600,',
        'rotation': '0',
        'quality': 'default',
        'format': 'jpg'},
    'square': squareProfile
}

IIIF manifest (Presentation API)

As of version 0.24, iiif.manifest returns a minimal single-image IIIF Presentation API 3.0 Manifest (a dict) wrapping the image on one canvas — ready to hand to a viewer like Mirador or OpenSeadragon:

return JsonResponse(asset.original.iiif.manifest)

Like info_document, it reads the image's dimensions from storage and returns None for an empty field. The manifest is always Presentation 3.0; its embedded image service follows IIIF_IMAGE_API_VERSION (ImageService3 by default, ImageService2 when set to 2).

Serving info.json and manifests from Django

djiiif can also serve the info.json and manifest documents itself — no separate image server is required for the metadata. Include its URLconf:

from django.urls import include, path

urlpatterns = [
    path("iiif/", include("djiiif.urls")),
]

An image stored as uploads/photo.jpg is then served at /iiif/uploads%2Fphoto.jpg/info.json and /iiif/uploads%2Fphoto.jpg/manifest, each with the application/ld+json content type and the CORS header IIIF clients expect. Each document's id is derived from the request URL, so it always matches where it is served from. The views use the default storage backend and read image dimensions on demand.

Serving identifiers that contain encoded slashes requires your web server to allow encoded slashes in the path (e.g. Apache's AllowEncodedSlashes On); flat identifiers need no such configuration.

All profiles as a dict (as_dict)

iiif.as_dict() returns every profile URL keyed by profile name — handy for iterating in a template or building a JSON response:

print(instance.original.iiif.as_dict())
> {"thumbnail": "http://server/uploads%2Ffilename.jpg/full/150,/0/default.jpg"}

Pass include_meta=True to also include the info and identifier URLs. For an empty field every value is "".

Django REST Framework support

An optional serializer field is available for DRF projects. Install the extra:

pip install djiiif[drf]

Then serialize an IIIFField to its profile URLs (the as_dict() mapping):

from rest_framework import serializers
from djiiif.serializers import IIIFSerializerField

class AssetSerializer(serializers.ModelSerializer):
    original = IIIFSerializerField()          # or IIIFSerializerField(include_meta=True)

    class Meta:
        model = Asset
        fields = ["id", "original"]

The field is read-only and emits {"thumbnail": "…", …}. Importing djiiif itself never imports DRF, so the core package stays dependency-free.

Validating your configuration

With djiiif in INSTALLED_APPS, manage.py check validates IIIF_PROFILES at startup — flagging a non-dict setting, an unsupported profile value, or a dict profile missing required keys before it can produce a broken URL. Callable and Profile entries are accepted as-is (a callable's shape can only be verified when it runs).

IIIF authorization (Auth Flow 2.0)

For access-controlled images served by an image server that implements the IIIF Authorization Flow API 2.0 (e.g. iiiris), djiiif can embed the auth service description in the info_document and manifest it generates, so a viewer (Mirador / OpenSeadragon) knows how to authenticate. djiiif only describes the services — the image server implements and enforces them.

Configure IIIF_AUTH with a ProbeService (or a raw dict), using the typed helpers to build the nested probe → access → token/logout block:

from djiiif import ProbeService, AccessService, TokenService, LogoutService

IIIF_AUTH = ProbeService(
    id="https://iiiris.example/auth/probe",
    access=AccessService(
        id="https://iiiris.example/auth/login",
        profile="active",                      # or "kiosk" / "external"
        label="Log in to Example Institution",
        heading="Restricted material",
        note="Please log in with your institutional account.",
        confirm_label="Log in",
        token=TokenService(id="https://iiiris.example/auth/token"),
        logout=LogoutService(id="https://iiiris.example/auth/logout", label="Log out"),
    ),
)

Label-ish fields accept a plain string, a list of strings, or an already-formed IIIF language map ({"en": ["…"]}).

For a mix of public and restricted images, set IIIF_AUTH to a callable receiving the field file — return the ProbeService for restricted images and None for public ones:

def image_auth(parent):
    if parent.instance.is_public:
        return None
    return ProbeService(id="https://iiiris.example/auth/probe", access=...)

IIIF_AUTH = image_auth

When IIIF_AUTH is unset (or the callable returns None), documents are unchanged. Authorization Flow 2.0 pairs with Image API 3, so setting IIIF_AUTH while IIIF_IMAGE_API_VERSION = 2 raises ImproperlyConfigured.

IIIF Template Tag

An alternate way to access IIIF URLs for your IIIFField is via the iiif template tag.

First, add djiiif to your INSTALLED_APPS:

INSTALLED_APPS = [
    ...
    'djiiif'
]

Next, load our template tag library iiiftags in your template:

{% load iiiftags %}

Finally, use it in a template:

{% iiif asset.original 'thumbnail' %}

The first parameter (asset.original) is a reference to an IIIFField instance.

The second parameter ('thumbnail') is the name of one of your IIIF profiles.

This tag syntax is effectively the same as:

{{ asset.original.iiif.thumbnail }}

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

djiiif-1.0.0.tar.gz (46.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

djiiif-1.0.0-py3-none-any.whl (21.0 kB view details)

Uploaded Python 3

File details

Details for the file djiiif-1.0.0.tar.gz.

File metadata

  • Download URL: djiiif-1.0.0.tar.gz
  • Upload date:
  • Size: 46.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for djiiif-1.0.0.tar.gz
Algorithm Hash digest
SHA256 a65c15f35b851991afddca4b039eae1b3d0bf089ef626a3ea9fe0df9b96e24a4
MD5 79a1949d09cfaa9023c033283d12d018
BLAKE2b-256 f1ddf7ef7acf52486b117280f7470b915a8831e153fd57b748926ac881ade10b

See more details on using hashes here.

Provenance

The following attestation bundles were made for djiiif-1.0.0.tar.gz:

Publisher: release.yml on rogerhoward/djiiif

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file djiiif-1.0.0-py3-none-any.whl.

File metadata

  • Download URL: djiiif-1.0.0-py3-none-any.whl
  • Upload date:
  • Size: 21.0 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for djiiif-1.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 d7347de7483569d79b36b2f2e116077ee1f28f72b8900a0edfb2ff5a3834359b
MD5 e50485d78829cefaf8cdd70dacf2e62d
BLAKE2b-256 e65f343f022ff1eb07ae0b8dc4e38ee157764c82804c7a5f10dac4f72e14b2d6

See more details on using hashes here.

Provenance

The following attestation bundles were made for djiiif-1.0.0-py3-none-any.whl:

Publisher: release.yml on rogerhoward/djiiif

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page