iiif_get_canvases()
Description
Simple class for IIIF API@internal
final class IIIFRequest
{
public string $rootlevel;
public string $rooturl;
public string $rootimageurl;
public int $identifier_field;
public int $description_field;
public int $sequence_field;
public string $iiif_sequence_prefix;
public int $license_field;
public string $rights_statement;
public int $title_field;
public int $max_width;
public int $max_height;
public bool $custom_sizes;
public bool $preview_tiles;
public int $preview_tile_size;
public array $preview_tile_scale_factors;
public array $media_extensions;
public int $download_chunk_size;
public array $data;
public array $headers;
public array $errors;
public int $errorcode;
public array $searchresults;
public array $processing;
public bool $only_power_of_two_sizes;
private array $response;
private array $request;
private bool $validrequest;
private int $imagewidth;
private int $imageheight;
private int $getwidth;
private int $getheight;
private int $regionx;
private int $regiony;
private int $regionw;
private int $regionh;
public function __construct($iiif_options)
{
foreach ($iiif_options as $key => $value) {
if (property_exists($this, $key)) {
$this->$key = $value;
}
}
$this->response = [];
$this->validrequest = false;
$this->headers = [];
$this->errors = [];
}
Get the IIIF response
public function getResponse(string $element = ""): array
{
return ($element != "" && isset($this->response[$element])) ? $this->response[$element] : $this->response;
}
Return information from the request
public function getRequest(string $element = "")
{
return ($element != "" && isset($this->request[$element])) ? $this->request[$element] : $this->request;
}
Is the current request valid?
public function isValidRequest()
{
return $this->validrequest;
}
Send the IIIF information document
public function infodoc()
{
$this->response["@context"] = "http://iiif.io/api/presentation/2/context.json";
$this->response["id"] = $this->rooturl;
$this->response["type"] = "sc:Manifest";
$arr_langdefault = i18n_get_all_translations("iiif");
foreach ($arr_langdefault as $langcode => $langdefault) {
$this->response["label"][$langcode] = [$langdefault];
}
$this->response["width"] = 6000;
$this->response["height"] = 4000;
$this->response["tiles"] = array();
$this->response["tiles"][] = array("width" => $this->preview_tile_size, "height" => $this->preview_tile_size, "scaleFactors" => $this->preview_tile_scale_factors);
$this->response["profile"] = array("http://iiif.io/api/image/3/level0.json");
$this->validrequest = true;
}
Extract IIIF request details from the URL path
public function parseUrl($url): void
{
$this->request = [];
$request_url = strtok($url, '?');
$path = substr($request_url, strpos($request_url, $this->rootlevel) + strlen($this->rootlevel));
$xpath = explode("/", $path);
// Set API type
if (strtolower($xpath[0]) == "image") {
$this->request["api"] = "image";
} elseif (count($xpath) > 1 || $xpath[0] != "") {
$this->request["api"] = "presentation";
} else {
$this->request["api"] = "root";
return;
}
if ($this->request["api"] == "image") {
// For image need to extract: -
// - Resource ID
// - type (manifest)
// - region
// - size
// - rotation
// - quality
// - format
$this->request["id"] = trim($xpath[1] ?? '');
$this->request["region"] = trim($xpath[2] ?? '');
$this->request["size"] = trim($xpath[3] ?? '');
$this->request["rotation"] = trim($xpath[4] ?? '');
$this->request["filename"] = trim($xpath[5] ?? '');
if ($this->request["id"] === '') {
$this->errors[] = 'Missing identifier';
$this->triggerError(400);
}
if ($this->request["region"] == "") {
// Redirect to image information document
$redirurl = $this->rootimageurl . $this->request["id"] . '/info.json';
if (function_exists("http_response_code")) {
http_response_code(303);
}
header("Location: " . $redirurl);
exit();
}
// Check the request parameters
elseif ($this->request["region"] != "info.json") {
if (
$this->request["size"] == ""
|| !is_int_loose($this->request["rotation"])
|| $this->request["filename"] != "default.jpg"
) {
// Not request for image information document and no sizes specified
$this->errors[] = "Invalid image request format.";
$this->triggerError(400);
}
$formatparts = explode(".", $this->request["filename"]);
if (count($formatparts) != 2) {
// Format. As we only support IIIF Image level 0 a value of 'jpg' is required
$this->errors[] = ["Invalid quality or format requested. Try using 'default.jpg'"];
$this->triggerError(400);
} else {
$this->request["quality"] = $formatparts[0];
$this->request["format"] = $formatparts[1];
}
}
} elseif ($this->request["api"] == "presentation") {
// Presentation - need
// - identifier
// - type (manifest/canvas/sequence/annotation
// - typeid (manifest/canvas/sequence/annotation
$this->request["id"] = trim($xpath[0] ?? '');
$this->request["type"] = trim($xpath[1] ?? '');
$this->request["typeid"] = trim($xpath[2] ?? '');
}
}
Find all the resources to generate an array of all the canvases for the identifier ready for JSON encoding
public function getCanvases($sequencekeys = false): void
{
$canvases = [];
foreach ($this->searchresults as $index => $iiif_result) {
if (in_array(strtolower($iiif_result["file_extension"] ?? ""), $this->media_extensions)) {
$size = "";
$media_path = get_resource_path($iiif_result["ref"], true, $size, false, $iiif_result["file_extension"]);
} else {
$size = $this->largest_jpg_size($iiif_result);
$media_path = get_resource_path($iiif_result["ref"], true, $size, false);
}
if (!file_exists($media_path)) {
// If configured, try and use a preview from a related resource
$pullresource = related_resource_pull($iiif_result);
if ($pullresource !== false) {
$this->processing["resource"] = $pullresource["ref"];
$this->processing["size_info"] = [
'identifier' => $this->largest_jpg_size($pullresource),
'return_height_width' => false,
];
}
}
$canvas = $this->generateCanvas($index);
if ($canvas) {
$canvases[$index] = $canvas;
}
}
if ($sequencekeys) {
// keep the sequence identifiers as keys so a required canvas can be accessed by sequence id
$this->response["items"] = $canvases;
}
ksort($canvases);
foreach ($canvases as $canvas) {
$this->response["items"][] = $canvas;
}
}
Get thumbnail information for the specified resource id ready for IIIF JSON encoding
public function getThumbnail(int $resourceid)
{
$img_path = get_resource_path($resourceid, true, 'thm', false);
if (!file_exists($img_path)) {
return false;
}
$thumbnail = [];
$thumbnail["id"] = $this->rootimageurl . $resourceid . "/full/thm/0/default.jpg";
$thumbnail["type"] = "Image";
$thumbnail["format"] = "image/jpeg";
// Get the size of the images
$GLOBALS["use_error_exception"] = true;
try {
list($tw,$th) = getimagesize($img_path);
$thumbnail["height"] = (int) $th;
$thumbnail["width"] = (int) $tw;
} catch (Exception $e) {
$returned_error = $e->getMessage();
debug("getThumbnail: Unable to get image size for file: $img_path - $returned_error");
// Use defaults
$thumbnail["height"] = 150;
$thumbnail["width"] = 150;
}
unset($GLOBALS["use_error_exception"]);
$thumbnail["service"] = [$this->generateImageService($resourceid)];
return $thumbnail;
}
Get the media file for the specified identifier canvas and resource id
is required to return the height & width (e.g annotations don't require this info).
Please note the identifier - use 'hpr' if the original file is not a JPG file AND
the extension is not in the $iiif_media_extensions arrays.
Example:
$size_info = array(
'identifier' => 'hpr',
'return_height_width' => true
);
public function get_media(int $resource, array $size_info)
{
// Quick validation of the size_info param
if (empty($size_info) || (!isset($size_info['identifier']) && !isset($size_info['return_height_width']))) {
return false;
}
$size = $size_info['identifier'];
$return_height_width = $size_info['return_height_width'];
$resdata = get_resource_data($resource);
if (in_array($resdata["file_extension"], array_merge($this->media_extensions))) {
$media_path = get_resource_path($resource, true, $size, false, $resdata["file_extension"]);
} else {
$useextension = strtolower($resdata["file_extension"]) == "jpeg" ? $resdata["file_extension"] : "jpg";
$media_path = get_resource_path($resource, true, $size, false, $useextension);
}
if (!file_exists($media_path)) {
// If configured, try and use a preview from a related resource
$resdata = get_resource_data($resource);
$pullresource = related_resource_pull($resdata);
if ($pullresource !== false) {
$resource = $pullresource["ref"];
$media_path = get_resource_path($resource, true, $this->largest_jpg_size($pullresource), false);
} else {
return false;
}
}
$media = [];
if (in_array($resdata["file_extension"], array_merge($this->media_extensions))) {
$media["duration"] = get_video_duration($media_path); // Also works for audio
$accesskey = generate_temp_download_key($GLOBALS["userref"], $resource, "");
$url = $GLOBALS["baseurl"] . "/pages/download.php";
$params = [
"ref" => $resource,
"ext" => $resdata["file_extension"],
"noattach" => true,
"access_key" => $accesskey,
];
$media["id"] = generateURL($url, $params);
$media["type"] = in_array(
strtolower($resdata["file_extension"]),
array_merge($GLOBALS["ffmpeg_audio_extensions"], ["mp3"])
) ? "Sound" : "Video";
$media["format"] = $GLOBALS["mime_types_by_extension"][$resdata["file_extension"]] ?? "application/octet-stream";
$size = "";
} else {
$media["id"] = $this->rootimageurl . $resource . "/full/max/0/default.jpg";
$media["type"] = "Image";
$media["format"] = "image/jpeg";
$media["service"] = [$this->generateImageService($resource)];
}
if ($return_height_width) {
$media_size = get_original_imagesize($resource, $media_path, $resdata["file_extension"]);
$media["height"] = intval($media_size[2]);
$media["width"] = intval($media_size[1]);
}
return $media;
}
Handle a IIIF error.
public function triggerError($errorcode = 404)
{
if (function_exists("http_response_code")) {
http_response_code($errorcode); # Send error status
}
echo json_encode($this->errors);
exit();
}
Process a IIIF presentation request
public function processPresentationRequest(): void
{
$this->getResources();
if (is_array($this->searchresults) && count($this->searchresults) > 0) {
if ($this->request["type"] == "manifest" || $this->request["type"] == "") {
$this->generateManifest();
$this->validrequest = true;
} elseif ($this->request["type"] == "canvas") {
$this->getResourceFromPosition($this->request["typeid"]);
$this->response = $this->generateCanvas($this->request["typeid"]);
$this->validrequest = true;
} elseif ($this->request["type"] == "annotationpage") {
$this->getResourceFromPosition($this->request["typeid"]);
$this->response = $this->generateAnnotationPage($this->request["typeid"]);
$this->validrequest = true;
} elseif ($this->request["type"] == "annotation") {
$this->getResourceFromPosition($this->request["typeid"]);
$this->response = $this->generateAnnotation($this->request["typeid"]);
$this->validrequest = true;
}
} // End of valid $identifier check based on search results
else {
$this->errorcode = 404;
$this->errors[] = "Invalid identifier: " . $this->request["id"];
}
}
Generate the top level manifest - see http://iiif.io/api/presentation/3.0/#manifest
public function generateManifest(): void
{
global $lang, $defaultlanguage;
$this->response["@context"] = "http://iiif.io/api/presentation/3/context.json";
$this->response["id"] = $this->rooturl . $this->request["id"] . "/manifest";
$this->response["type"] = "Manifest";
// Descriptive metadata about the object/work
// The manifest data should be the same for all resources that are returned.
// This is the default when using the tms_link plugin for TMS integration.
// Therefore we use the data from the first returned result.
$dataresource = reset($this->searchresults);
$this->data = get_resource_field_data($dataresource["ref"]);
// Label property
foreach ($this->searchresults as $iiif_result) {
// Keep on until we find a label
$iiif_label = get_data_by_field($iiif_result["ref"], $this->title_field);
if (trim($iiif_label) != "") {
$i18n_values = i18n_get_translations($iiif_label);
foreach ($i18n_values as $langcode => $langstring) {
$this->response["label"][$langcode] = [$langstring];
}
break;
}
}
if (!$iiif_label) {
$this->response["label"][$defaultlanguage] = [$lang["notavailableshort"]];
}
foreach ($this->searchresults as $iiif_result) {
$description = get_data_by_field($iiif_result["ref"], $this->description_field);
if (trim($description) != "") {
$i18n_values = i18n_get_translations($description);
foreach ($i18n_values as $langcode => $langstring) {
$this->response["summary"][$langcode] = [$langstring];
}
break; // Only metadata from one resource is required
}
}
// Construct metadata array from resource field data
$this->generateMetadata();
if ($this->license_field != 0) {
$licensevals = get_data_by_field($dataresource["ref"], $this->license_field, false);
if (count($licensevals) > 0) {
// Get all field title translations
$licensefield = get_resource_type_field($this->license_field);
$liclabel_int = i18n_get_translations($licensefield["title"]);
$reqstatements = ["label" => [],"value" => []];
foreach ($licensevals as $licenseval) {
$licensevals_int = i18n_get_translations($licenseval["name"]);
foreach ($licensevals_int as $langcode => $langstring) {
if (!isset($reqstatements["label"][$langcode])) {
// Translated node names may include languages that are not available for the field title
$reqstatements["label"][$langcode][] = $liclabel_int[$langcode] ?? $licensefield["title"];
}
$reqstatements["value"][$langcode][] = $langstring;
}
}
$this->response["requiredStatement"] = $reqstatements;
}
}
if (isset($this->rights_statement) && $this->rights_statement != "") {
$this->response["rights_statement"] = $this->rights_statement;
}
// Thumbnail property
$this->response["thumbnail"] = [];
foreach ($this->searchresults as $iiif_result) {
// Keep on until we find an image
$iiif_thumb = $this->getThumbnail($dataresource["ref"]);
if ($iiif_thumb) {
$this->response["thumbnail"][] = $iiif_thumb;
break;
}
}
// Default behavior property - not currently configurable
$this->response["behavior"] = ["individuals"];
// Default viewingDirection property - not currently configurable
$this->response["viewingDirection"] = "left-to-right";
$this->getCanvases(false);
}
Generate a canvas
public function generateCanvas(int $position)
{
// This is essentially a resource
// {scheme}://{host}/{prefix}/{identifier}/canvas/{name}
$canvas = [];
$resource = $this->searchresults[$position] ?? [];
if (empty($resource)) {
debug("IIIF: generateCanvas() Not a valid canvas identifier:" . $position);
return false;
}
$useimage = $resource;
if ((int)$resource['has_image'] === 0) {
// If configured, try and use a preview from a related resource
debug("No image for IIIF request - check for related resources");
$pullresource = related_resource_pull($resource);
if ($pullresource !== false) {
$useimage = $pullresource;
}
}
if (in_array(strtolower($useimage['file_extension'] ?? ""), $this->media_extensions)) {
$size = '';
$media_path = get_resource_path($useimage["ref"], true, $size, false, $useimage["file_extension"]);
} else {
$size = $this->largest_jpg_size($useimage);
$useextension = strtolower((string) $useimage["file_extension"]) == "jpeg" ? $useimage["file_extension"] : "jpg";
$media_path = get_resource_path($useimage["ref"], true, $size, false, $useextension);
}
if (!file_exists($media_path)) {
debug("IIIF: generateCanvas() No image available for identifier:" . $position);
return false;
}
$sequence_field = get_resource_type_field($this->sequence_field);
$sequenceid = $resource["iiif_position"];
debug("IIIF: Found resource " . $resource['ref'] . " in position " . $position . ", sequence ID: " . $sequenceid);
$sequence_prefix = "";
if (isset($this->iiif_sequence_prefix)) {
$sequence_prefix = $this->iiif_sequence_prefix === "" ? $sequence_field["title"] . " " : $this->iiif_sequence_prefix;
}
$sequence_val = $sequenceid;
$canvas["id"] = $this->rooturl . $this->request["id"] . "/canvas/" . $position;
$canvas["type"] = "Canvas";
$canvas["label"] = [];
$arr_18n_pos_labels = i18n_get_translations($sequence_val);
$arr_18n_pos_prefixes = i18n_get_translations($sequence_prefix);
if (count($arr_18n_pos_prefixes) > 1 || count($arr_18n_pos_labels) > 1) {
foreach (array_unique(array_merge(array_keys($arr_18n_pos_prefixes), array_keys($arr_18n_pos_labels))) as $langcode) {
$prefix = $arr_18n_pos_prefixes[$langcode] ?? ($arr_18n_pos_prefixes[$GLOBALS["defaultlanguage"]] ?? reset($arr_18n_pos_prefixes));
$labelvalue = $arr_18n_pos_labels[$langcode] ?? ($arr_18n_pos_labels[$GLOBALS["defaultlanguage"]] ?? reset($arr_18n_pos_labels));
$canvas["label"][$langcode] = [$prefix . $labelvalue];
}
} else {
$canvas["label"]["none"] = [$sequence_prefix . $sequence_val];
}
// Get the size of the images
$image_size = get_original_imagesize($useimage["ref"], $media_path, $useimage["file_extension"]);
$canvas["height"] = intval($image_size[2]);
$canvas["width"] = intval($image_size[1]);
// Add image (only 1 per canvas currently supported)
$this->getResourceFromPosition($position);
$canvas["items"][] = $this->generateAnnotationPage($position);
return $canvas;
}
Generate the AnnotationPage elements
public function generateAnnotationPage(int $position = 0): array
{
$annotationpage = [];
$annotationpage["id"] = $this->rooturl . $this->request["id"] . "/annotationpage/" . $position;
$annotationpage["type"] = "AnnotationPage";
$annotationpage["items"] = [];
$annotationpage["items"][] = $this->generateAnnotation($position);
return $annotationpage;
}
Generate the Annotation elements
public function generateAnnotation(int $position = 0): array
{
$annotation["id"] = $this->rooturl . $this->request["id"] . "/annotation/" . $position;
$annotation["type"] = "Annotation";
$annotation["motivation"] = "Painting";
$annotation["body"] = $this->get_media($this->processing["resource"], $this->processing["size_info"]);
$annotation["target"] = $this->rooturl . $this->request["id"] . "/canvas/" . $position;
return $annotation;
}
Generates the IIIF response for the current IIIF object (presentation API)
public function generateMetadata(): void
{
$metadata = [];
$n = 0;
foreach ($this->data as $iiif_data_row) {
if (in_array($iiif_data_row["type"], $GLOBALS["FIXED_LIST_FIELD_TYPES"])) {
// Don't use the data as this has already concatenated the translations, add an entry for each node translation by building up a new array
$resnodes = get_resource_nodes(reset($this->searchresults)["ref"], $iiif_data_row["resource_type_field"], true);
if (count($resnodes) == 0) {
continue;
}
// Add all translated field names
$metadata[$n] = [];
$metadata[$n]["label"] = [];
$i18n_titles = i18n_get_translations($iiif_data_row["title"]);
foreach ($i18n_titles as $langcode => $langstring) {
$metadata[$n]["label"][$langcode] = [$langstring];
}
// Add all translated node names
$arr_showlangs = [];
$arr_alllangstrings = [];
$arr_lang_default = [];
foreach ($resnodes as $resnode) {
$node_langs_avail = [];
$i18n_names = i18n_get_translations($resnode["name"]);
// Set default in case no translation available for any languages
$defaultnodename = $i18n_names[$GLOBALS["defaultlanguage"]] ?? reset($i18n_names);
$arr_lang_default[] = $defaultnodename;
foreach ($i18n_names as $langcode => $langstring) {
$node_langs_avail[] = $langcode;
if (!isset($arr_alllangstrings[$langcode])) {
// This is the first time this language has been found for this field
// Initialise the language by copying the default array of values found so far
$arr_alllangstrings[$langcode] = $arr_lang_default;
}
// Add to array
$arr_alllangs[$langcode][] = $langstring;
$arr_showlangs[] = $langcode;
}
// Check that this node string has been added for all translations found so far
foreach ($arr_alllangstrings as $langcode => $strings) {
if (!in_array($langcode, $node_langs_avail)) {
$arr_alllangstrings[$langcode][] = $defaultnodename;
}
}
}
$metadata[$n]["value"] = [];
foreach ($arr_alllangstrings as $langcode => $strings) {
$metadata[$n]["value"][$langcode] = [implode(NODE_NAME_STRING_SEPARATOR, $strings)];
}
} elseif (trim((string) $iiif_data_row["value"]) !== "") {
$metadata[$n] = [];
$metadata[$n]["label"] = [];
$i18n_titles = i18n_get_translations($iiif_data_row["title"]);
foreach ($i18n_titles as $langcode => $langstring) {
$metadata[$n]["label"][$langcode] = [$langstring];
}
$metadata[$n]["value"] = [];
$i18n_titles = i18n_get_translations($iiif_data_row["value"]);
foreach ($i18n_titles as $langcode => $langstring) {
$metadata[$n]["value"][$langcode] = [$langstring];
}
$n++;
}
}
$this->response["metadata"] = $metadata;
}
Process the IIIF Image API request - see http://iiif.io/api/image/3.0/
The IIIF Image API URI for requesting an image must conform to the following URI Template:
{scheme}://{server}{/prefix}/{identifier}/{region}/{size}/{rotation}/{quality}.{format}
public function processImageRequest(): void
{
$this->request["getext"] = "jpg";
if ($this->request["id"] === '') {
$this->errors[] = 'Missing identifier';
$this->triggerError(400);
}
if ($this->request["region"] == "") {
// Redirect to image information document
$redirurl = $this->rootimageurl . $this->request["id"] . '/info.json';
if (function_exists("http_response_code")) {
http_response_code(303);
}
header("Location: " . $redirurl);
exit();
}
if (is_numeric($this->request["id"])) {
$resource = get_resource_data($this->request["id"]);
$resource_access = get_resource_access($this->request["id"]);
} else {
$resource_access = 2;
}
if (
$resource_access == 0
&& !in_array($resource["file_extension"], array_diff(config_merge_non_image_types(), $this->media_extensions))
) {
// Check resource actually exists and is active
if (in_array($resource["file_extension"], $this->media_extensions)) {
$fulljpgsize = "pre";
} else {
$fulljpgsize = $this->largest_jpg_size($resource);
}
$useextension = strtolower($resource["file_extension"]) == "jpeg" ? $resource["file_extension"] : "jpg";
$img_path = get_resource_path($this->request["id"], true, $fulljpgsize, false, $useextension);
$image_size = get_original_imagesize($this->request["id"], $img_path, $useextension);
if ($image_size === false) {
$this->errors[] = "No image available for this identifier";
$this->triggerError(404);
}
$this->imagewidth = (int) $image_size[1];
$this->imageheight = (int) $image_size[2];
// Get all available sizes
$sizes = get_image_sizes($this->request["id"], true, "jpg", false);
$availsizes = [];
if ($this->imagewidth > 0 && $this->imageheight > 0) {
foreach ($sizes as $size) {
if (
$size['width'] > 0
&& $size['height'] > 0
&& $size['width'] <= $this->max_width
&& $size['height'] <= $this->max_height
&& (
!$this->only_power_of_two_sizes
|| (is_power_of_two($size['width']) && is_power_of_two($size['height']))
|| $size['id'] == 'pre'
)
) {
$availsizes[] = [
'id' => $size['id'],
'width' => $size['width'],
'height' => $size['height'],
];
}
}
}
if ($this->request["region"] == "info.json") {
// Image information request. Only fullsize available in this initial version
$this->response["@context"] = "http://iiif.io/api/image/3/context.json";
$this->response["extraFormats"] = [
"jpg",
];
$this->response["extraQualities"] = [
"default",
];
$this->response["id"] = $this->rootimageurl . $this->request["id"];
$this->response["height"] = $this->imageheight;
$this->response["width"] = $this->imagewidth;
$this->response["type"] = "ImageService3";
$this->response["profile"] = "level0";
$this->response["maxWidth"] = $this->max_width;
$this->response["maxHeight"] = $this->max_height;
if ($this->custom_sizes) {
$this->response["extraFeatures"] = ["sizeByH","sizeByW","sizeByWh"];
}
$this->response["protocol"] = "http://iiif.io/api/image";
$this->response["sizes"] = $availsizes;
if ($this->preview_tiles) {
$this->response["tiles"] = [];
$this->response["tiles"][] = array("height" => $this->preview_tile_size, "width" => $this->preview_tile_size, "scaleFactors" => $this->preview_tile_scale_factors);
}
$this->headers[] = 'Link: <http://iiif.io/api/image/3/level0.json>;rel="profile"';
$this->validrequest = true;
} else {
// Process requested region
if (!isset($this->errorcode) && $this->request["region"] != "full" && $this->request["region"] != "max" && $this->preview_tiles) {
// If the request specifies a region which extends beyond the dimensions reported in the image information document,
// then the service should return an image cropped at the image’s edge, rather than adding empty space.
// If the requested region’s height or width is zero, or if the region is entirely outside the bounds
// of the reported dimensions, then the server should return a 400 status code.
$regioninfo = explode(",", $this->request["region"]);
$region_filtered = array_filter($regioninfo, 'is_numeric');
if (count($region_filtered) != 4) {
// Invalid region
$this->errors[] = "Invalid region requested. Use 'full' or 'x,y,w,h'";
$this->triggerError(400);
} else {
$this->regionx = (int)$region_filtered[0];
$this->regiony = (int)$region_filtered[1];
$this->regionw = (int)$region_filtered[2];
$this->regionh = (int)$region_filtered[3];
debug("IIIF: region requested: x:" . $this->regionx . ", y:" . $this->regiony . ", w:" . $this->regionw . ", h:" . $this->regionh);
if (fmod($this->regionx, $this->preview_tile_size) != 0 || fmod($this->regiony, $this->preview_tile_size) != 0) {
// Invalid region
$this->errors[] = "Invalid region requested. Supported tiles are " . $this->preview_tile_size . "x" . $this->preview_tile_size . " at scale factors " . implode(",", $this->preview_tile_scale_factors) . ".";
$this->triggerError(400);
} else {
$tile_request = true;
}
}
} else {
// Full image requested
$tile_request = false;
}
// Process size
if (strpos($this->request["size"], ",") !== false) {
// Currently support 'w,' and ',h' syntax requests
$getdims = explode(",", $this->request["size"]);
$this->getwidth = (int)$getdims[0];
$this->getheight = (int)$getdims[1];
if ($tile_request) {
if (!$this->isValidTileRequest()) {
$this->errors[] = "Invalid tile size requested";
$this->triggerError(400);
}
$this->request["getsize"] = "tile_" . $this->regionx . "_" . $this->regiony . "_" . $this->regionw . "_" . $this->regionh;
debug("IIIF: " . $this->regionx . "_" . $this->regiony . "_" . $this->regionw . "_" . $this->regionh);
} else {
if ($this->getheight == 0) {
$this->getheight = floor($this->getwidth ($this->imageheight / $this->imagewidth));
} elseif ($this->getwidth == 0) {
$this->getwidth = floor($this->getheight ($this->imagewidth / $this->imageheight));
}
// Establish which preview size this request relates to
foreach ($availsizes as $availsize) {
debug("IIIF: checking available size for resource " . $resource["ref"] . ". Size '" . $availsize["id"] . "': " . $availsize["width"] . "x" . $availsize["height"] . ". Requested size: " . $this->getwidth . "x" . $this->getheight);
if ($availsize["width"] == $this->getwidth && $availsize["height"] == $this->getheight) {
$this->request["getsize"] = $availsize["id"];
}
}
if (!isset($this->request["getsize"])) {
if (!$this->custom_sizes || $this->getwidth > $this->max_width || $this->getheight > $this->max_height) {
// Invalid size requested
$this->errors[] = "Invalid size requested";
$this->triggerError(400);
} else {
$this->request["getsize"] = "resized_" . $this->getwidth . "_" . $this->getheight;
}
}
}
} elseif ($this->request["size"] == "full" || $this->request["size"] == "max" || $this->request["size"] == "thm") {
if ($tile_request) {
if ($this->request["size"] == "full" || $this->request["size"] == "max") {
$this->request["getsize"] = "tile_" . $this->regionx . "_" . $this->regiony . "_" . $this->regionw . "_" . $this->regionh;
$this->request["getext"] = "jpg";
} else {
$this->errors[] = "Invalid tile size requested";
$this->triggerError(501);
}
} else {
// Full/max image region requested
if ($this->max_width >= $this->imagewidth && $this->max_height >= $this->imageheight) {
$this->request["getext"] = strtolower($resource["file_extension"]) == "jpeg" ? "jpeg" : "jpg";
if (in_array($resource["file_extension"], $this->media_extensions)) {
// The largest available size for these is 'pre'
$this->request["getsize"] = "pre";
} else {
$this->request["getsize"] = $this->largest_jpg_size($resource);
}
} else {
$this->request["getext"] = "jpg";
$this->request["getsize"] = count($availsizes) > 0 ? $availsizes[0]["id"] : "thm";
}
}
} else {
$this->errors[] = "Invalid size requested";
$this->triggerError(400);
}
if ($this->request["rotation"] != 0) {
// Rotation. As we only support IIIF Image level 0 only a rotation value of 0 is accepted
$this->errors[] = "Invalid rotation requested. Only '0' is permitted.";
$this->triggerError(404);
}
if (isset($this->request["quality"]) && $this->request["quality"] != "default" && $this->request["quality"] != "color") {
// Quality. As we only support IIIF Image level 0 only a quality value of 'default' or 'color' is accepted
$this->errors[] = "Invalid quality requested. Only 'default' is permitted";
$this->triggerError(404);
}
if (isset($this->request["format"]) && strtolower($this->request["format"]) != "jpg") {
// Format. As we only support IIIF Image level 0 only a value of 'jpg' is accepted
$this->errors[] = "Invalid format requested. Only 'jpg' is permitted.";
$this->triggerError(404);
}
if (!isset($this->errorcode)) {
// Request is supported, send the image
$imgpath = get_resource_path($this->request["id"], true, $this->request["getsize"], false, $this->request["getext"]);
$imgfound = false;
debug("IIIF: image path: " . $imgpath);
if (file_exists($imgpath)) {
$imgfound = true;
} elseif ($this->custom_sizes && ($this->request["region"] == "full" || $this->request["region"] == "max")) {
if (is_process_lock('create_previews_' . $resource["ref"] . "_" . $this->request["getsize"])) {
$this->errors[] = "Requested image is not currently available";
$this->triggerError(503);
}
$GLOBALS["use_error_exception"] = true;
try {
$imgfound = create_previews($this->request["id"], false, "jpg", false, true, -1, true, false, false, array($this->request["getsize"]));
clear_process_lock('create_previews_' . $resource["ref"] . "_" . $this->request["getsize"]);
} catch (Exception $e) {
debug("IIIF: error - " . $e->getMessage());
$imgfound = false;
}
unset($GLOBALS["use_error_exception"]);
}
if ($imgfound) {
$this->validrequest = true;
$this->response["image"] = $imgpath;
} else {
$this->errorcode = "404";
$this->errors[] = "No image available for this identifier";
}
}
}
/ IMAGE REQUEST END
} else {
$this->errors[] = "Missing or invalid identifier";
$this->triggerError(404);
}
}
Send the requested image to the IIIF client
public function renderImage(): void
{
// Send the image
$file_size = filesize_unlimited($this->response["image"]);
$file_handle = fopen($this->response["image"], 'rb');
header("Access-Control-Allow-Origin: ");
header('Content-Disposition: inline;');
header('Content-Transfer-Encoding: binary');
$mime = get_mime_type($this->response["image"]);
header("Content-Type: {$mime}");
$sent = 0;
while ($sent < $file_size) {
echo fread($file_handle, $this->download_chunk_size);
ob_flush();
flush();
$sent += $this->download_chunk_size;
if (0 != connection_status()) {
break;
}
}
fclose($file_handle);
}
Find all resources associated with the given identifier and adds to the $iiif object
public function getResources(): void
{
$iiif_field = get_resource_type_field($this->identifier_field);
$iiif_search = $iiif_field["name"] . ":" . $this->request["id"];
$results = do_search($iiif_search);
if (is_array($results)) {
$this->searchresults = $results;
} else {
$this->searchresults = [];
}
// Add sequence position information
$resultcount = count($this->searchresults);
$iiif_results_with_position = [];
$iiif_results_without_position = [];
for ($n = 0; $n < $resultcount; $n++) {
if ($this->sequence_field != 0) {
if (isset($this->searchresults[$n]["field" . $this->sequence_field])) {
$sequenceid = $this->searchresults[$n]["field" . $this->sequence_field];
} else {
$sequenceid = get_data_by_field($this->searchresults[$n]["ref"], $this->sequence_field);
}
if (!isset($sequenceid) || trim($sequenceid) == "") {
// Processing resources without a sequence position separately
debug("IIIF: position empty for resource ref " . $this->searchresults[$n]["ref"]);
$iiif_results_without_position[] = $this->searchresults[$n];
continue;
}
debug("IIIF: position $sequenceid found in resource ref " . $this->searchresults[$n]["ref"]);
$this->searchresults[$n]["iiif_position"] = $sequenceid;
$iiif_results_with_position[] = $this->searchresults[$n];
} else {
$sequenceid = $n;
debug("IIIF: position $sequenceid assigned to resource ref " . $this->searchresults[$n]["ref"]);
$this->searchresults[$n]["iiif_position"] = $sequenceid;
$iiif_results_with_position[] = $this->searchresults[$n];
}
}
// Sort by user supplied position (handle blanks and duplicates)
if ($this->sequence_field != 0) {
# First sort by ref. Any duplicate positions will then be sorted oldest resource first.
usort($iiif_results_with_position, function ($a, $b) {
return $a['ref'] - $b['ref'];
});
# Sort resources with user supplied position.
usort($iiif_results_with_position, function ($a, $b) {
if (is_int_loose($a['iiif_position']) && is_int_loose($b['iiif_position'])) {
return $a['iiif_position'] - $b['iiif_position'];
} elseif (is_int_loose($a['iiif_position']) || is_int_loose($b['iiif_position'])) {
return is_int_loose($a['iiif_position']) ? 1 : -1; // Put strings before numbers
}
return strcmp($a['iiif_position'], $b['iiif_position']);
});
if (count($iiif_results_without_position) > 0 && count($iiif_results_with_position) > 0) {
# Sort resources without a user supplied position by resource reference.
# These will appear at the end of the sequence after those with a user supplied position.
# Only applies if some resources have a sequence position else return in search results order per earlier behaviour.
usort($iiif_results_without_position, function ($a, $b) {
return $a['ref'] - $b['ref'];
});
}
$this->searchresults = array_merge($iiif_results_with_position, $iiif_results_without_position);
$sorted_final = [];
$maxid = 0;
foreach ($this->searchresults as $index => $resource) {
# Update iiif_position after sorting using unique array key, removing potential user entered duplicates in sequence field.
# iiif_get_canvases() requires unique iiif_position values.
$resourcepos = $resource['iiif_position'] ?? ($maxid + 1);
while (isset($sorted_final[$resourcepos])) {
$resourcepos++;
}
debug("IIIF: final position $index given for resource ref " . $resource["ref"] . " sequence id: " . $resourcepos);
$sorted_final[$index] = $resource;
$sorted_final[$index]["iiif_position"] = $resourcepos;
$maxid = max((int) $resourcepos, $maxid);
}
$this->searchresults = $sorted_final;
}
}
Update the $iiif object with the current resource at the given canvas position
public function getResourceFromPosition($position): void
{
$this->processing = [];
// Need to find the resourceid the annotation is linked to
if (isset($this->searchresults[$position])) {
$this->processing["resource"] = $this->searchresults[$position]["ref"];
if (in_array(strtolower($this->searchresults[$position]['file_extension'] ?? ""), $this->media_extensions)) {
$identifier = '';
} else {
$identifier = $this->largest_jpg_size($this->searchresults[$position]);
}
$this->processing["size_info"] = array(
'identifier' => $identifier,
'return_height_width' => true,
);
}
}
Generate the image API data
public function generateImageService(int $resourceid): array
{
$service = [];
$service["id"] = $this->rootimageurl . $resourceid;
$service["type"] = "ImageService3";
$service["profile"] = "level0";
return $service;
}
Is the tile request valid
public function isValidTileRequest(): bool
{
if (
($this->getwidth == $this->preview_tile_size && $this->getheight == 0) // "w,"
|| ($this->getheight == $this->preview_tile_size && $this->getwidth == 0) // ",h"
|| ($this->getheight == $this->preview_tile_size && $this->getwidth == $this->preview_tile_size) // "w,h"
) {
// Standard tile widths
return true;
} elseif (
($this->regionx + $this->regionw) === ($this->imagewidth)
|| ((int)$this->regiony + (int)$this->regionh) === ((int)$this->imageheight)
) {
// Check this is a valid scale from the width/height requested.
// If using just e.g. "x," or ",y" then default to 1)
$hscale = $this->getwidth > 0 ? ceil($this->regionw / $this->getwidth) : 1;
$vscale = $this->getheight > 0 ? ceil($this->regionh / $this->getheight) : 1;
if (
($this->getwidth === 0 || $this->getheight === 0 || $hscale == $vscale)
&& count(array_diff([$hscale,$vscale], $this->preview_tile_scale_factors)) == 0
) {
return true;
}
}
debug('IIIF invalid tile request');
return false;
}
Indicate whether the response is an image file
public function is_image_response()
{
return isset($this->response["image"]);
}
Get the largest resource JPG size available for a given resource in search result set
public function largest_jpg_size($resource)
{
return is_jpeg_extension($resource["file_extension"] ?? "") ? "" : "hpr";
}
}
// Start of IIIF v2.1 functions. These should be replaced with new code or removed when no longer required
Get an array of all the canvases for the identifier ready for JSON encoding
Parameters
Column | Type | Default | Description |
---|---|---|---|
$identifier | integer | IIIF identifier (this associates resources via the metadata field set as $iiif_identifier_field | |
$iiif_results | array | Array of ResourceSpace search results that match the $identifier, sorted | |
$sequencekeys | boolean | false | Get the array with each key matching the value set in the metadata field $iiif_sequence_field. By default the array will be sorted but have a 0 based index |
$element | string | ||
$url | string | The requested URL | |
$resourceid | int | Resource ID | |
$size | array | ResourceSpace size information. Required information: identifier and whether it | |
$errorcode | integer | The error code | |
$iiif | object | The current IIIF request object generated in api/iiif/handler.php | |
$position | int | The annotation position | |
$resource | array | Array of resource data from do_search() |
Return
array | * |
mixed | * |
bool | * |
void | * |
void | * |
array|bool | Thumbnail image data, false if not found |
bool|array | Array holding image file data. Returns false if no image available. |
void | */ |
void | * |
void | */ |
array|bool | $canvas Canvas data for presentation API response, false if no image is available |
array | Array of annotation pages |
array | Array of annotations |
void | */ |
void | * |
void | */ |
void | * |
void | * |
array | * |
bool | * |
bool | * |
string | Size to use - 'hpr', or '' to use original size |
array |
Location
include/iiif_functions.php lines 1199 to 1266
Definition
function iiif_get_canvases($identifier, $iiif_results, $sequencekeys = false)
{
global $rooturl,$iiif_sequence_field;
$canvases = array();
foreach ($iiif_results as $index => $iiif_result) {
$useimage = $iiif_result;
if ((int)$iiif_result['has_image'] === 0) {
// If configured, try and use a preview from a related resource
debug("IIIF: No image for IIIF request - check for related resources");
$pullresource = related_resource_pull($iiif_result);
if ($pullresource !== false) {
$useimage = $pullresource;
}
}
$size = is_jpeg_extension($useimage["file_extension"] ?? "") ? "" : "hpr";
$useextension = strtolower((string) $useimage["file_extension"]) == "jpeg" ? $useimage["file_extension"] : "jpg";
$img_path = get_resource_path($useimage["ref"], true, $size, false, $useextension);
if (!file_exists($img_path)) {
continue;
}
$sequenceid = $iiif_result["iiif_position"];
$sequence_field = get_resource_type_field($iiif_sequence_field);
$sequence_prefix = "";
if (isset($GLOBALS["iiif_sequence_prefix"])) {
$sequence_prefix = $GLOBALS["iiif_sequence_prefix"] === "" ? $sequence_field["title"] . " " : $GLOBALS["iiif_sequence_prefix"];
}
$canvases[$index]["@id"] = $rooturl . $identifier . "/canvas/" . $index;
$canvases[$index]["@type"] = "sc:Canvas";
$canvases[$index]["label"] = $sequence_prefix . $sequenceid;
// Get the size of the images
$image_size = get_original_imagesize($useimage["ref"], $img_path);
$canvases[$index]["height"] = intval($image_size[2]);
$canvases[$index]["width"] = intval($image_size[1]);
// "If the largest image's dimensions are less than 1200 pixels on either edge, then the canvas dimensions
// should be double those of the image." - From http://iiif.io/api/presentation/2.1/#canvas
if ($image_size[1] < 1200 || $image_size[2] < 1200) {
$image_size[1] = $image_size[1] * 2;
$image_size[2] = $image_size[2] * 2;
}
$canvases[$index]["thumbnail"] = iiif_get_thumbnail($useimage["ref"]);
// Add image (only 1 per canvas currently supported)
$canvases[$index]["images"] = array();
$size_info = array(
'identifier' => $size,
'return_height_width' => false,
'original_file_extension' => $useextension
);
$canvases[$index]["images"][] = iiif_get_image($identifier, $useimage["ref"], $index, $size_info);
}
if ($sequencekeys) {
// keep the sequence identifiers as keys so a required canvas can be accessed by sequence id
return $canvases;
}
ksort($canvases);
$return = array();
foreach ($canvases as $canvas) {
$return[] = $canvas;
}
return $return;
}
This article was last updated 20th February 2025 19:05 Europe/London time based on the source file dated 18th February 2025 15:40 Europe/London time.