# IIIF

The IIIF plugin provides functionality relating to the IIIF data standard. In particular, it provides a configurable endpoint for Manifests and other Resources defined in the IIIF Presentation API (v3.0.0).

# REST Endpoints

# GET /iiif/3/manifest/{manifest_key}/{*path}

Generates and retrieves the IIIF Manifest or embedded Resource for the record identified by the provided manifest key.

The following steps are performed when a request is handled by this endpoint:

  1. A GetByIdentifierRequest is executed against the data service using:

    • The manifest_key path parameter as the identifier field
    • The value of the configuration property, rosetta.plugins.iiif.identifier_type, as the identifier_type field (if specified)
    • The value of the configuration property, rosetta.plugins.iiif.profile, as the root profile for the data service request
  2. Assuming a single result is found, it is expected to have been transformed into a data structure that conforms to the SparseManifest schema using a Glyph configured in the data phase of the Profile, for instance. See Templating a Manifest for further details.

    • If no results or multiple results are found, then this endpoint responds with a 404 error.
  3. The SparseManifest is "expanded" to a full IIIF Manifest, where ids for embedded resources are automatically generated, if absent.

  4. (Optional) Apply a "late" transform against the full IIIF Manifest, allowing it to be populated by custom logic.

    • The input for this transform is a Metadata object whose data field is the full Manifest and whose id and source fields are their respective fields of the retrieved result.
  5. If the path path parameter is provided, navigate to the embedded Resource located at this path within the full IIIF Manifest.

  6. Check that the final output is valid against the IIIF Presentation specification:

    • If not, respond with a 404 error.
    • Otherwise, respond with the IIIF Resource.

See Configuration Properties for details on how to configure this endpoint.

# Request

Parameters

Name In Type Required Description
manifest_key Path String Yes The Manifest key of the Manifest to retrieve. Must be a unique identifier where any forward-slash characters ("/") must be URL-encoded.
path Path String No If present, it is the forward-slash-separated path to the IIIF Resource to retrieve from within the Manifest given by the manifest_key parameter. Otherwise, the full Manifest is retrieved.

# Response

Body
Schema: IIIF Presentation API (v3.0.0) Resource

Error
404: If no unique record exists for the provided manifest key or the path (if provided) does not navigate to a valid IIIF Resource.

# Converting the Request

Upon receiving a request to the IIIF manifest endpoint, a request of the form GetByIdentifierRequest is internally submitted to the Rosetta data service. Providers typically will not support requests of this form, so a request transform must be configured whose target model is accepted by the provider.

This can be done by first declaring a Glyph (such as a Proteus Glyph) in the application.yml:

application.yml

rosetta:
  transform:
    glyphs:
      - name: request-transform
        type: proteus
        properties:
          spec: /rosetta/config/proteus/request-transform.spec

where the above Glyph depends on the spec file, /rosetta/config/proteus/request-transform.spec.

Let us assume our Provider is for an Elasticsearch index named my-index in a cluster running locally on port 9200:

application.yml

rosetta:
  provider:
    providers:
      - name: my-provider
        type: elasticsearch
        properties:
          protocol: http
          host: localhost
          port: 9200
          index: my-index

and the mappings for my-index include a keyword field, system_id, which contains a unique identifier string for each record in the index.

If a request is received by the IIIF manifest endpoint with path /iiif/3/manifest/object-123, then the following GetByIdentifierRequest will be submitted to the data service (provided the configuration property rosetta.plugins.iiif.identifier_type is left unspecified):

{
  "identifier": "object-123"
}

It follows that the Proteus spec for the request transform should be:

/rosetta/config/proteus/request-transform.spec

{
  "query": {
    "term": {
      "system_id": "$.identifier"
    }
  }
}

In the above, we have constructed a term query against the system_id field using the manifest key from the request as the term value and sole template parameter, de-referenced using the JSONPath string $.identifier.

Finally, we must register the request transform within the request phase of the Profile used by the IIIF plugin (see Configuration Properties):

application.yml

rosetta:
  profile:
    profiles:
      # Create profile for IIIF:
      - name: iiif
        providers:
          policy: list
          names:
            - my-provider # Our Elasticsearch provider configured earlier
        transforms:
          request:
            - glyphs:
                policy: list
                names:
                  - request-transform # <--- Reference request transform Glyph here
              criteria: # Not strictly necessary with only one provider, but it may be with multiple.
                providers:
                  policy: list
                  names:
                    - my-provider
  plugins:
    iiif:
      profile: iiif # <-- Specify that we should use the above profile for /iiif/3/manifest requests
      # (Other IIIF config)

Now Rosetta knows to transform the incoming GetByIdentifierRequest into an Elasticsearch search request using the Glyph named request-transform when serving IIIF requests.

# Templating a Manifest

# Expected Format

In this plugin, IIIF Manifests must be templated in the form of a SparseManifest, which is a simplified model that factors away some of the more verbose aspects of the full IIIF model, such as language maps and canvas structure.

The following is an example of a SparseManifest that includes the label, metadata and items fields:

{
   "type": "manifest",
   "id": "object-123",
   "label": "Impression, Sunrise",
   "metadata": {
      "Creator": "Claude Monet",
      "Genre": "Marine Art",
      "Depicts": [
         "Sunrise",
         "Port of Le Havre",
         "Barque"
      ],
      "Title": {
         "en": "Impression, Sunrise",
         "fr": "Impression, soleil levant"
      }
   },
   "items": [
      {
         "type": "canvas",
         "artifact": {
            "location": "wikipedia/commons/thumb/5/59/Monet_-_Impression,_Sunrise.jpg/960px-Monet_-_Impression,_Sunrise.jpg"
         }
      }
   ]
}

Note that entries in the metadata map are allowed to be specified either as a simple string, an array of strings, or a map whose keys are language codes. This allows manifests to be populated as concisely or as richly as is needed. The metadata field may also be populated "literally" as an array of label-value maps, as in the IIIF specification.

Once specified, a SparseManifest is expanded into a full IIIF manifest on retrieval from the /iiif/3/manifest endpoint. Expanding the above SparseManifest may result in the following output (depending on the configuration properties):

{
   "@context": "http://iiif.io/api/presentation/3/context.json",
   "id": "http://localhost:4923/iiif/3/manifest/object-123",
   "type": "Manifest",
   "label": {
      "en": [
         "Impression, Sunrise"
      ]
   },
   "metadata": [
      {
         "label": {
            "en": [
               "Creator"
            ]
         },
         "value": {
            "en": [
               "Claude Monet"
            ]
         }
      },
      {
         "label": {
            "en": [
               "Genre"
            ]
         },
         "value": {
            "en": [
               "Marine Art"
            ]
         }
      },
      {
         "label": {
            "en": [
               "Depicts"
            ]
         },
         "value": {
            "en": [
               "Sunrise",
               "Port of Le Havre",
               "Barque"
            ]
         }
      },
      {
         "label": {
            "en": [
               "Title"
            ]
         },
         "value": {
            "en": [
               "Impression, Sunrise"
            ],
            "fr": [
               "Impression, soleil levant"
            ]
         }
      }
   ],
   "items": [
      {
         "id": "http://localhost:4923/iiif/3/manifest/object-123/items/0",
         "type": "Canvas",
         "items": [
            {
               "id": "http://localhost:4923/iiif/3/manifest/object-123/items/0/items/0",
               "type": "AnnotationPage",
               "items": [
                  {
                     "id": "http://localhost:4923/iiif/3/manifest/object-123/items/0/items/0/items/0",
                     "type": "Annotation",
                     "body": {
                        "id": "https://upload.wikimedia.org/wikipedia/commons/thumb/5/59/Monet_-_Impression,_Sunrise.jpg/960px-Monet_-_Impression,_Sunrise.jpg",
                        "type": "Image"
                     },
                     "motivation": "painting",
                     "target": "http://localhost:4923/iiif/3/manifest/object-123/items/0"
                  }
               ]
            }
         ]
      }
   ]
}

As can be seen above, many fields have been added or altered:

  • The field @context field has been added with the appropriate value for version 3 of the IIIF Presentation API.
  • The id field is now fully qualified against the base retrieval URL.
  • The label and metadata values have been used to generate full language maps using the pre-configured default language code.
  • Each item in the items array (just one in this example) has been exploded into the multiply-nested series of IIIF Resources including a Canvas, AnnotationPage, Annotation and an Image, each of which are required to display the referenced image.
  • Media artifact locations have been resolved against the base media url (in this case https://upload.wikimedia.org), which is configured using the property, rosetta.plugins.iiif.external_media_base_url. This URL is used to populate the id field of Image Resources.
  • Each structural Resource embedded in the root manifest has been automatically given an id that allows the Resource to be retrieved in isolation using the /iiif/3/manifest endpoint.
    • Note: ids are not generated for Resources whose id is already specified in the template.

See SparseManifest for its full schema and details on how different embedded data structures are expanded.

# Transforming to a SparseManifest

In general, the records retrieved by a Provider will not already be in the SparseManifest format, so they must be transformed such that they are. To do this, at least one Glyph must be declared (such as a Proteus Glyph) and then used in the data phase of the profile.

The following example demonstrates how this might be done. First, let us assume our Provider produces results that have the following form:

{
  "system_id": "object-123",
  "title": "Impression, Sunrise",
  "title_french": "Impression, soleil levant",
  "creator": "Claude Monet",
  "genre": "Marine Art",
  "subjects": [
    "Sunrise",
    "Port of Le Havre",
    "Barque"
  ],
  "media": [
    {
      "uri": "wikipedia/commons/thumb/5/59/Monet_-_Impression,_Sunrise.jpg/960px-Monet_-_Impression,_Sunrise.jpg"
    }
  ]
}

We will define a Proteus Glyph in the application.yml file with the following properties:

application.yml

rosetta:
  transform:
    glyphs:
      - name: iiif-template
        type: proteus
        properties:
          spec: /rosetta/config/proteus/iiif-template.json

The Glyph depends on a spec file, /rosetta/config/proteus/iiif-template.json, whose content is as follows:

/rosetta/config/proteus/iiif-template.json

{
   "type": "manifest",
   "id": "$.system_id",
   "label": "$.title",
   "metadata": {
      "Creator": "$.creator",
      "Genre": "$.genre",
      "Depicts": "$.subjects",
      "Title": {
         "en": "$.title",
         "fr": "$.title_french"
      }
   },
   "items": {
      "#type": "for_each",
      "values": "$.media",
      "spec": {
         "type": "canvas",
         "artifact": {
            "location": "$.uri"
         }
      }
   }
}

The above configuration will reproduce the example SparseManifest from the previous section.

# Using the Transform

Finally, we need to wire our formatter/templating Glyph into the IIIF Manifest endpoint. We do this by first declaring a Profile that uses the Glyph in the data transformation phase, as follows:

application.yml

rosetta:
  profile:
    profiles:
      - name: iiif
        providers:
          policy: list
          names:
            - my-provider # A provider with this name must be declared
        transforms:
          request: # A request transform may be necessary, depending on the provider.
          data:
            - glyphs:
                policy: list
                names:
                  - iiif-template # <--- Refers to our example Glyph

Then, specify that the IIIF endpoint should use this profile in the plugin configuration properties:

rosetta:
  plugins:
    iiif:
      profile: iiif # <--- Specify profile here
      # Also specify the necessary base urls:
      base_url: http://localhost:4923
      external_media_base_url: https://upload.wikimedia.org

Now, requests to http://localhost:4923/iiif/3/manifest/{manifest_key} will respond with full IIIF manifests.

# Configuration Properties

Each of the following properties is prefixed by the property path rosetta.plugins.iiif.

Property Type Default Description
profile String default The name of the root Profile to use when retrieving records by their manifest key.
base_url String null The base URL used to populate ids in IIIF Resources. This value MUST be populated and the format will typically be "http(s)://my-domain" where "my-domain" is the domain used to retrieve the IIIF Manifests.
exclude_api_path Boolean false If true, then the "/iiif/3/manifest" path segment will be excluded from generated Resource ids. This may be useful if the Manifest endpoint is mapped to a different path behind a reverse proxy. In which case, the alternative path may be included in the base_url property.
identifier_type String null If specified, then this value will be used as the identifier_type field of the GetByIdentifierRequest requests used to retrieve records for a particular manifest key.
iiif_media_base_url String null The base URL to use for IIIF Image Resources (i.e. zoomable images). This base URL will be used for SparseCanvas Resources that have artifact.use_service set to true or artifact.name set to zoom.
external_media_base_url String null The base URL to use for static Image Resources (i.e. those that do not fit the criteria for iiif_media_base_url to be used instead).
default_language String en The default BCP 47 language code to use when expanding Sparse Resources.
late_transform.enabled Boolean false Enables the late transform, which is executed after expanding the SparseManifest but before applying the path request parameter.
late_transform.glyphs Selector A selection of Glyphs to apply during the late transform phase.

# Data Structures

# SparseManifest

A sparse representation of a IIIF Manifest. In the following schema, all fields correspond to the property of the same name as described in the IIIF Presentation API specification unless otherwise stated.

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "description": "Generates a IIIF Manifest Resource",
  "$defs": {
    "SparseResource": {
      "type": "object",
      "properties": {
        "type": {
          "description": "The type of SparseResource. Determines the type of IIIF Resource it generates and what additional properties are available.",
          "enum": [
            "manifest",
            "canvas",
            "range",
            "image"
          ]
        },
        "id": {
          "type": "string",
          "description": "The id for this SparseResource. Leave unspecified to use path-based auto-generation. For the root SparseManifest, this must be set to the value of the 'manifest_key' path parameter used to retrieve this manifest."
        },
        "label": {
          "$ref": "#/$defs/AdaptiveStringMap"
        },
        "summary": {
          "$ref": "#/$defs/AdaptiveStringMap"
        },
        "metadata": {
          "$ref": "#/$defs/AdaptiveMetadata"
        },
        "thumbnail": {
          "type": "array",
          "items": {
            "$ref": "#/$defs/SparseImage"
          }
        }
      },
      "required": [
        "type",
        "id"
      ]
    },
    "AdaptiveStringMap": {
      "oneOf": [
        {
          "$ref": "#/$defs/StringValueNode",
          "description": "If this is a value node (i.e. not an object or array), then on expansion, the resulting language map will have one entry whose key is given by the default language code and whose value will be this value, converted to a string if necessary, and wrapped in an array."
        },
        {
          "type": "array",
          "items": {
            "$ref": "#/$defs/StringValueNode"
          },
          "description": "If this is an array, then on expansion, the resulting language map will have one entry whose key is given by the default language map, and the value will be this array where each item has been converted to a string, if necessary."
        },
        {
          "type": "object",
          "description": "If this is an object, then on expansion, the resulting language map will have the same keys as this map, interpreted as language codes, and each value may either by a value node or an array, where value nodes are converted to strings and wrapped (as above), and arrays are used as-is after each item has been converted to a string as necessary.",
          "additionalProperties": {
            "oneOf": [
              {
                "$ref": "#/$defs/StringValueNode"
              },
              {
                "type": "array",
                "items": {
                  "$ref": "#/$defs/StringValueNode"
                }
              }
            ]
          }
        }
      ]
    },
    "AdaptiveMetadata": {
      "oneOf": [
        {
          "type": "object",
          "description": "If this is an object, then on expansion, each entry will contribute one item to the resulting 'metadata' array, where the 'label' language map will have one entry whose key is the default language code, and whose value is the key of the map entry, wrapped in an array. The 'value' language map will be the value of the map entry interpreted as an 'AdaptiveStringMap'.",
          "additionalProperties": {
            "$ref": "#/$defs/AdaptiveStringMap"
          }
        },
        {
          "type": "array",
          "description": "If this is an array, then on expansion, the resulting 'metadata' array will contain one item for each item in this array, where the 'label' and 'value' fields of each item are each interpreted as an 'AdaptiveStringMap'.",
          "items": {
            "type": "object",
            "properties": {
              "label": {
                "$ref": "#/$defs/AdaptiveStringMap"
              },
              "value": {
                "$ref": "#/$defs/AdaptiveStringMap"
              }
            }
          }
        }
      ]
    },
    "SparseImage": {
      "description": "Generates a IIIF Image Resource",
      "allOf": [
        {
          "$ref": "#/$defs/SparseResource"
        },
        {
          "type": "object",
          "properties": {
            "type": {
              "const": "image"
            },
            "format": {
              "type": "string",
              "description": "The media/MIME type of the image"
            },
            "height": {
              "type": "integer",
              "description": "The height of the image in pixels"
            },
            "width": {
              "type": "integer",
              "description": "The width of the image in pixels"
            }
          }
        }
      ]
    },
    "SparseCanvas": {
      "description": "Generates a IIIF Canvas Resource",
      "allOf": [
        {
          "$ref": "#/$defs/SparseResource"
        },
        {
          "type": "object",
          "properties": {
            "type": {
              "const": "canvas"
            },
            "artifact": {
              "$ref": "#/$defs/SparseArtifact",
              "description": "A media artifact object that contains all necessary information required to generate a fully populated AnnotationPage for this Canvas"
            }
          }
        }
      ]
    },
    "SparseRange": {
      "description": "Generates a IIIF Range Resource",
      "allOf": [
        {
          "$ref": "#/$defs/SparseResource"
        },
        {
          "type": "object",
          "properties": {
            "type": {
              "const": "range"
            },
            "items": {
              "type": "array",
              "items": {
                "$ref": "#/$defs/SparseResource"
              }
            }
          }
        }
      ]
    },
    "SparseArtifact": {
      "type": "object",
      "properties": {
        "name": {
          "type": "string",
          "description": "The name of the media artifact. If set to 'zoom', then the IIIF Image service will be used."
        },
        "location": {
          "type": "string",
          "description": "The relative path of the media artifact. This value will be resolved against one of the configured media base urls upon expansion, depending on whether the IIIF Image service is used."
        },
        "format": {
          "type": "string",
          "description": "The media/MIME type of the image"
        },
        "height": {
          "type": "integer",
          "description": "The height of the image in pixels"
        },
        "width": {
          "type": "integer",
          "description": "The width of the image in pixels"
        },
        "file_size": {
          "type": "integer",
          "description": "The size of the media artifact in bytes"
        },
        "use_service": {
          "type": "boolean",
          "description": "If true, then the IIIF Image service will be used."
        },
        "info_path": {
          "type": "string",
          "description": "The path template for the id of the IIIF Service Resource, if the IIIF Image service is used. The placeholder '{location}' will be replaced with the 'location' value of this artifact.",
          "default": "/{location}/info.json"
        },
        "request_path": {
          "type": "string",
          "description": "The path template for the id of the Image resource, if the IIIF Image service is used. The placeholder '{location}' will be replaced with the 'location' value of this artifact.",
          "default": "/{location}/full/max/0/default.jpg"
        }
      },
      "required": [
        "location"
      ]
    },
    "StringValueNode": {
      "description": "A value node (i.e. not an object or array) that is converted to a string (if it is not one already) upon expansion into a language map.",
      "not": {
        "anyOf": [
          {
            "type": "object"
          },
          {
            "type": "array"
          }
        ]
      }
    }
  },
  "allOf": [
    {
      "$ref": "#/$defs/SparseResource"
    },
    {
      "type": "object",
      "properties": {
        "type": {
          "const": "manifest"
        },
        "items": {
          "type": "array",
          "items": {
            "$ref": "#/$defs/SparseCanvas"
          }
        },
        "structures": {
          "type": "array",
          "items": {
            "$ref": "#/$defs/SparseRange"
          }
        }
      }
    }
  ]
}