Changelog

The full, version-by-version history is maintained in the project’s CHANGELOG.md and reproduced here.

All notable changes to djiiif are documented here.

The format is based on Keep a Changelog, and this project aims to follow Semantic Versioning. Per the backwards-compatibility policy in CLAUDE.md, any breaking change to the public API is called out under a Breaking heading and triggers a MAJOR version bump.

Entries older than 0.24 were backfilled from git history and may be approximate; dates are the commit dates of the corresponding version bump.

Unreleased

1.1.1 - 2026-07-08

Changed

  • The project moved from GitHub to GitLab. The canonical home is now https://gitlab.com/iiiris-org/djiiif; the GitHub repository is archived and read-only. Package metadata (Homepage, Issues, Changelog) and the links in this file now point at GitLab. No code, import path, or public API changed — pip install djiiif is unaffected. Issues and merge requests should be filed on GitLab from now on.

  • CI/CD moved from GitHub Actions to GitLab CI (.gitlab-ci.yml). Releases are still published to PyPI by OIDC trusted publishing — now minted by GitLab rather than GitHub — and are still triggered by pushing a vX.Y tag.

1.1.0 - 2026-07-06

Added

  • navPlace extension (geolocated manifests) via GeoDjango. A new opt-in IIIF_NAVPLACE setting — a callable (or dotted-path string) receiving the field file and returning a GeoJSON dict, a GEOS geometry, a (geometry, label) pair, or None — adds a navPlace FeatureCollection to the generated manifest, switching its @context to the required two-element array. Resolution lives in a new optional djiiif.geo module that recognizes GEOS geometries by duck-typing (never importing django.contrib.gis), so GeoDjango is not a dependency — plain GeoJSON works without GDAL/GEOS. build_manifest/build_multi_manifest gain a nav_place keyword; IIIFObject.manifest and serve_manifest thread it through. A GEOS geometry whose SRID is set and not 4326 (WGS84) raises ImproperlyConfigured. Unset ⇒ output unchanged. See briefs/NAVPLACE-GEO.md.

  • Declarative info.json enrichment. A new opt-in IIIF_INFO setting — a dict, the typed InfoExtras dataclass, or a callable returning either/None (normalized by resolve_info, mirroring IIIF_AUTH) — passes optional Image API properties through into the generated info.json: sizes, tiles, max_width/max_height/max_area, rights, preferred_formats, and extra_qualities/extra_formats/extra_features. Keys are accepted in both snake_case and spec camelCase (conflicting duplicates raise ImproperlyConfigured); v3 emits all of them, v2 emits only sizes/tiles and rejects v3-only keys. Threaded through iiif.info_document and serve_info_json (where a per-image callable receives the decoded storage name). djiiif only advertises what the operator declares — it does not probe the image server. Unset ⇒ output unchanged. See briefs/INFO-JSON-ENRICHMENT.md.

  • Comprehensive documentation on Read the Docs. A multi-page Sphinx site (MyST Markdown + Furo theme) under docs/, with getting-started guides, a feature guide per API, a full settings reference, and an autodoc API reference generated from the package’s docstrings. Configured for Read the Docs via .readthedocs.yaml; build the toolchain with pip install -e ".[docs]".

  • Web Annotations & Content Search 2.0 (transcriptions, OCR, search-within). A shared frozen Annotation dataclass (or plain dict; resolve_annotation normalizes both) is the one hit/annotation type for two paired, opt-in, model-free features:

    • IIIF_ANNOTATIONS_BACKEND — a callable (identifier, request) -> iterable of annotations — powers serve_annotation_page at /iiif/<identifier>/annotations/1 (a W3C AnnotationPage of transcriptions/OCR/commentary targeting the image’s canvas), and serve_manifest gains the canvas annotations reference when it is set.

    • IIIF_SEARCH_BACKEND — a callable (identifier, q, request) -> iterable of hits — powers serve_search at /iiif/<identifier>/search?q= (a Content Search 2.0 AnnotationPage with embedded hits and a contextualizing match block), and serve_manifest advertises a SearchService2. When only the annotations backend is set, serve_search falls back to a case-insensitive substring filter over it — “serve annotations, get search for free.” Empty/missing q ⇒ empty page; unset backends ⇒ 404.

    New public builders: build_annotation, build_annotation_page, build_search_service, build_search_response. Advertisement is view-only (IIIFObject.manifest is unchanged). See briefs/WEB-ANNOTATIONS.md and briefs/CONTENT-SEARCH.md.

  • IIIF Change Discovery API 1.0 activity stream (harvestable collections). A new opt-in IIIF_ACTIVITY_SOURCE setting — a callable (or dotted-path string) returning an iterable of activity entries (plain dicts or the new frozen Activity dataclass; object_id + end_time, optional type/object_type, normalized by resolve_activity) — powers two drop-in views mounted by djiiif.urls: serve_activity_collection at /iiif/activity/collection (the OrderedCollection entry point) and serve_activity_page at /iiif/activity/page/<n> (paged OrderedCollectionPages in ascending endTime order). Page size from IIIF_ACTIVITY_PAGE_SIZE (default 100); pagination via django.core.paginator (querysets slice lazily, generators are materialized). Level-1 conformance (Create/Update); Delete/level-2 is future work. Module-level builders build_activity / build_ordered_collection / build_collection_page are public. Unset IIIF_ACTIVITY_SOURCE ⇒ the activity URLs 404. See briefs/CHANGE-DISCOVERY.md.

  • Richer Presentation 3.0 manifests — descriptive properties. build_manifest now accepts optional keyword descriptors metadata, rights, required_statement, summary, thumbnail, and nav_date (each emitted only when given, so default output is byte-identical). A new opt-in IIIF_MANIFEST_DESCRIPTORS setting — a dict of those kwargs or a callable receiving the field file and returning one (or None) — flows per-image descriptive metadata into iiif.manifest without djiiif knowing your model. An unknown descriptor key raises ImproperlyConfigured.

  • Multi-image manifests. New build_multi_manifest(id_url, images, *, label, …) builder presents several images as one manifest with indexed canvases; build_manifest is now a thin single-image wrapper over it (output unchanged). A resolved IIIF_AUTH block applies to every image body.

  • Collections. New build_collection(id_url, items, *, label, **descriptors) builder emits a Presentation 3.0 Collection of manifest references, plus an optional drop-in serve_collection view (mounted at /iiif/collection by djiiif.urls) driven by the IIIF_COLLECTION_SOURCE setting (unset ⇒ 404; IIIF_COLLECTION_LABEL sets the collection label). See briefs/PRESENTATION-ENRICHMENT.md. All three pieces are purely additive.

  • IIIF Content State API 1.0 helpers for shareable viewer deep links. New module-level functions encode_content_state / decode_content_state (spec §6 base64url encoding, incl. the encodeURIComponent percent-encoding step) and build_content_state (targets a Manifest, a Canvas with partOf, or a Canvas region via xywh), plus iiif.content_state(xywh=..., encoded=...) which derives this image’s own manifest/canvas URIs (the same ones iiif.manifest emits) with no file I/O, and a {% iiif_content_state image %} /{% iiif_content_state image xywh='x,y,w,h' %} template tag. Drop the result into ?iiif-content= to open an image — optionally zoomed to a region — in Mirador, Theseus, or any content-state-aware viewer. Purely additive; no new settings, views, or dependencies, and no change to existing output. See briefs/CONTENT-STATE.md.

1.0.0 - 2026-07-03

First stable release. Marks the public API — IIIFField / IIIFFieldFile / IIIFObject, the .iiif attributes and generated documents, the IIIF_PROFILES / IIIF_AUTH config shapes, the typed helpers, the serving views, and the DRF field — as stable and covered by the backwards-compatibility policy. No breaking changes from 0.24; the bump to 1.0 signals maturity, not a break.

Added

  • Optional IIIF Authorization Flow 2.0 support (metadata-only). A new opt-in IIIF_AUTH setting (a ProbeService, a dict, or a callable returning either or None, mirroring IIIF_PROFILES) embeds an Auth 2.0 service block in the generated info_document and manifest, so viewers can authenticate against an image server that implements Auth 2.0 (e.g. iiiris). New typed helpers ProbeService / AccessService / TokenService / LogoutService build the nested block. djiiif only describes the services; the image server implements and enforces them. Auth 2.0 requires IIIF_IMAGE_API_VERSION = 3 (raises ImproperlyConfigured on v2). Documents are unchanged when IIIF_AUTH is unset. See briefs/AUTH-API.md.

0.24 - 2026-07-03

Added

  • iiif.info_document: returns the IIIF info.json document itself (a dict, or None for an empty field), assembled from the image’s own width/height, so a Django view can serve a minimal, spec-valid info.json without a separate image server. Distinct from iiif.info, which is unchanged and still returns the URL of an external info.json. It is the only iiif attribute that reads the file from storage.

  • iiif.manifest: returns a minimal single-image IIIF Presentation API 3.0 Manifest (a dict, or None for an empty field) wrapping the image on one canvas, ready for viewers like Mirador or OpenSeadragon. The embedded image service follows IIIF_IMAGE_API_VERSION.

  • Profile dataclass: a typed, opt-in alternative to raw dict profiles, with IIIF 3.0 defaults (size="max") and mirror / upscale flags that fold into the !-rotation and ^-size prefixes. Callable profiles may now return a Profile as well as a dict.

  • Drop-in serving views (djiiif.views.serve_info_json / serve_manifest) and URLconf (djiiif.urls): path("iiif/", include("djiiif.urls")) serves both info.json and manifest for stored images with the correct content type and CORS header — no separate image server required for the metadata documents.

  • iiif.as_dict(): returns every profile URL keyed by profile name (with an include_meta=True option to add the info/identifier URLs) — convenient for templates and JSON responses.

  • Optional Django REST Framework support: djiiif.serializers.IIIFSerializerField serializes an IIIFField to its as_dict() mapping. Install via the new drf extra (pip install djiiif[drf]); importing djiiif never imports DRF.

  • System check check_iiif_profiles (registered by the new DjiiifConfig app config): manage.py check now validates IIIF_PROFILES at startup, flagging a non-dict setting or a dict profile missing required keys.

  • IIIF_IMAGE_API_VERSION setting (default 3) selects the generated info_document / manifest shapes — 3 for Image API 3.0 (id / type: ImageService3), 2 for the 2.x @id / array-profile shape. An unknown value raises ImproperlyConfigured.

  • IIIF_COMPLIANCE_LEVEL setting (default "level2") sets the compliance level advertised in info_document and the manifest’s image service.

Changed

  • Identifier encoding now percent-encodes the full IIIF reserved set via urllib.parse.quote, not just /%2F. This changes emitted URLs only for field names that contain characters beyond / (spaces, ?, #, %, etc.), which previously produced malformed URLs; plain names and names whose only special character is / are unaffected. Not a breaking change under the compatibility policy (it corrects previously-broken output), but noted here for anyone who stored such names.

  • An empty IIIF_PROFILES no longer raises when a field has a value; info and identifier return "" (and the documents None), matching the empty-field behavior.

  • Packaging modernized (PEP 621 + setuptools-scm). All metadata moved to pyproject.toml; the package version is now derived from the git tag instead of a hand-bumped setup.py. Removed setup.py, Pipfile, Pipfile.lock, MANIFEST.in, requirements.txt, and build.sh. No change to the installed package or its runtime dependency (Django). Contributors: install the dev toolchain with pip install -e ".[dev]" (Pipenv is no longer used).

0.23 - 2026-05-27

Added

  • iiif.identifier: a plain host/identifier URL (no /region/size/rotation/quality.format suffix) for handing an image to viewers like OpenSeadragon.

Changed

  • Reconciled master with the published 0.22 release.

  • Added a test suite, CI across Python 3.10–3.13, and a trusted-publishing release workflow.

0.22 - 2023-09-05

Fixed

  • Corrected an error in URL assembly.

0.21 - 2023-09-05

Added

  • Plain, identifier-only URL for easy OpenSeadragon integration (later reconciled under 0.23).

0.20 - 2020-01-09

Fixed

  • Made iiif attribute access safe for empty fields.

0.19 - 2020-01-09

Changed

  • Return an empty string for unpopulated (empty/None-name) fields instead of building a broken URL.

0.15

Added

  • iiif.info: the IIIF info.json URL for a field.

0.1

Added

  • Initial implementation: IIIFField / IIIFFieldFile / IIIFObject, IIIF_PROFILES-driven profile URLs (dict and callable shapes), and the {% iiif %} template tag.