Browse Source
* Initial implementation of a REST Export serializer for IIIF Manifests * Coding standards * Pass full URL to IIIF * Basic implementation of manifest per https://iiif.io/api/presentation/2.1/#manifest * Remove unused variable * Remove dependency for OSD, add config variable for IIIF server * Update settings.yml * Fix key name * fix schema/settings issue * Broaden approach of discovering file/image fieldspull/729/head
Joe Corall
5 years ago
committed by
dannylamb
8 changed files with 413 additions and 0 deletions
@ -0,0 +1 @@ |
|||||||
|
iiif_server: |
@ -0,0 +1,7 @@ |
|||||||
|
islandora_iiif.settings: |
||||||
|
type: config_object |
||||||
|
label: 'Islandora IIIF Settings' |
||||||
|
mapping: |
||||||
|
iiif_server: |
||||||
|
type: string |
||||||
|
label: 'IIIF Server Url' |
@ -0,0 +1,7 @@ |
|||||||
|
name: 'Islandora IIIF' |
||||||
|
type: module |
||||||
|
description: 'IIIF support for Islandora' |
||||||
|
core: 8.x |
||||||
|
package: Islandora |
||||||
|
dependencies: |
||||||
|
- islandora |
@ -0,0 +1,6 @@ |
|||||||
|
islandora_iiif.islandora_iiif_config_form: |
||||||
|
title: 'IIIF Settings' |
||||||
|
route_name: islandora_iiif.islandora_iiif_config_form |
||||||
|
description: 'Configure Islandora IIIF settings' |
||||||
|
parent: system.admin_config_islandora |
||||||
|
weight: 99 |
@ -0,0 +1,24 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
/** |
||||||
|
* @file |
||||||
|
* Contains islandora_iiif.module. |
||||||
|
*/ |
||||||
|
|
||||||
|
use Drupal\Core\Routing\RouteMatchInterface; |
||||||
|
|
||||||
|
/** |
||||||
|
* Implements hook_help(). |
||||||
|
*/ |
||||||
|
function islandora_iiif_help($route_name, RouteMatchInterface $route_match) { |
||||||
|
switch ($route_name) { |
||||||
|
// Main module help for the islandora_iiif module. |
||||||
|
case 'help.page.islandora_iiif': |
||||||
|
$output = ''; |
||||||
|
$output .= '<h3>' . t('About') . '</h3>'; |
||||||
|
$output .= '<p>' . t('IIIF support for Islandora') . '</p>'; |
||||||
|
return $output; |
||||||
|
|
||||||
|
default: |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,9 @@ |
|||||||
|
islandora_iiif.islandora_iiif_config_form: |
||||||
|
path: '/admin/config/islandora/iiif' |
||||||
|
defaults: |
||||||
|
_form: '\Drupal\islandora_iiif\Form\IslandoraIIIFConfigForm' |
||||||
|
_title: 'IslandoraIIIFConfigForm' |
||||||
|
requirements: |
||||||
|
_permission: 'access administration pages' |
||||||
|
options: |
||||||
|
_admin_route: TRUE |
@ -0,0 +1,92 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
namespace Drupal\islandora_iiif\Form; |
||||||
|
|
||||||
|
use Drupal\Core\Form\ConfigFormBase; |
||||||
|
use Drupal\Core\Form\FormStateInterface; |
||||||
|
use Drupal\Component\Utility\UrlHelper; |
||||||
|
use GuzzleHttp\Exception\ClientException; |
||||||
|
|
||||||
|
/** |
||||||
|
* Class IslandoraIIIFConfigForm. |
||||||
|
*/ |
||||||
|
class IslandoraIIIFConfigForm extends ConfigFormBase { |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
protected function getEditableConfigNames() { |
||||||
|
return [ |
||||||
|
'islandora_iiif.settings', |
||||||
|
]; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
public function getFormId() { |
||||||
|
return 'islandora_iiif_config_form'; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
public function buildForm(array $form, FormStateInterface $form_state) { |
||||||
|
$config = $this->config('islandora_iiif.settings'); |
||||||
|
$form['iiif_server'] = [ |
||||||
|
'#type' => 'url', |
||||||
|
'#title' => $this->t('IIIF Image server location'), |
||||||
|
'#description' => $this->t('Please enter the image server location without trailing slash. e.g. http://www.example.org/iiif/2.'), |
||||||
|
'#default_value' => $config->get('iiif_server'), |
||||||
|
]; |
||||||
|
return parent::buildForm($form, $form_state); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
public function validateForm(array &$form, FormStateInterface $form_state) { |
||||||
|
if (!empty($form_state->getValue('iiif_server'))) { |
||||||
|
$server = $form_state->getValue('iiif_server'); |
||||||
|
if (!UrlHelper::isValid($server, UrlHelper::isExternal($server))) { |
||||||
|
$form_state->setErrorByName('iiif_server', "IIIF Server address is not a valid URL"); |
||||||
|
} |
||||||
|
elseif (!$this->validateIiifUrl($server)) { |
||||||
|
$form_state->setErrorByName('iiif_server', "IIIF Server does not seem to be accessible."); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
public function submitForm(array &$form, FormStateInterface $form_state) { |
||||||
|
parent::submitForm($form, $form_state); |
||||||
|
|
||||||
|
$this->config('islandora_iiif.settings') |
||||||
|
->set('iiif_server', $form_state->getValue('iiif_server')) |
||||||
|
->save(); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Ensure the IIIF server is accessible. |
||||||
|
* |
||||||
|
* @param string $server_uri |
||||||
|
* The absolute or relative URI to the server. |
||||||
|
* |
||||||
|
* @return bool |
||||||
|
* True if server returns 200 on a HEAD request. |
||||||
|
*/ |
||||||
|
private function validateIiifUrl($server_uri) { |
||||||
|
$client = \Drupal::httpClient(); |
||||||
|
try { |
||||||
|
$result = $client->head($server_uri); |
||||||
|
return ($result->getStatusCode() == 200); |
||||||
|
} |
||||||
|
catch (ClientException $e) { |
||||||
|
return FALSE; |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,267 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
namespace Drupal\islandora_iiif\Plugin\views\style; |
||||||
|
|
||||||
|
use Drupal\views\Plugin\views\style\StylePluginBase; |
||||||
|
use Drupal\Core\Form\FormStateInterface; |
||||||
|
use Drupal\views\ResultRow; |
||||||
|
use Symfony\Component\DependencyInjection\ContainerInterface; |
||||||
|
use Symfony\Component\Serializer\SerializerInterface; |
||||||
|
use Symfony\Component\HttpFoundation\Request; |
||||||
|
use Drupal\Core\Config\ImmutableConfig; |
||||||
|
|
||||||
|
/** |
||||||
|
* Provide serializer format for IIIF Manifest. |
||||||
|
* |
||||||
|
* @ingroup views_style_plugins |
||||||
|
* |
||||||
|
* @ViewsStyle( |
||||||
|
* id = "iiif_manifest", |
||||||
|
* title = @Translation("IIIF Manifest"), |
||||||
|
* help = @Translation("Display images as an IIIF Manifest."), |
||||||
|
* display_types = {"data"} |
||||||
|
* ) |
||||||
|
*/ |
||||||
|
class IIIFManifest extends StylePluginBase { |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
protected $usesRowPlugin = TRUE; |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
protected $usesGrouping = FALSE; |
||||||
|
|
||||||
|
/** |
||||||
|
* The allowed formats for this serializer. Default to only JSON. |
||||||
|
* |
||||||
|
* @var array |
||||||
|
*/ |
||||||
|
protected $formats = ['json']; |
||||||
|
|
||||||
|
/** |
||||||
|
* The serializer which serializes the views result. |
||||||
|
* |
||||||
|
* @var \Symfony\Component\Serializer\Serializer |
||||||
|
*/ |
||||||
|
protected $serializer; |
||||||
|
|
||||||
|
/** |
||||||
|
* The request service. |
||||||
|
* |
||||||
|
* @var \Symfony\Component\HttpFoundation\Request |
||||||
|
*/ |
||||||
|
protected $request; |
||||||
|
|
||||||
|
/** |
||||||
|
* This module's config. |
||||||
|
* |
||||||
|
* @var \Drupal\Core\Config\ImmutableConfig |
||||||
|
*/ |
||||||
|
protected $iiifConfig; |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
public function __construct(array $configuration, $plugin_id, $plugin_definition, SerializerInterface $serializer, Request $request, ImmutableConfig $iiif_config) { |
||||||
|
parent::__construct($configuration, $plugin_id, $plugin_definition); |
||||||
|
|
||||||
|
$this->serializer = $serializer; |
||||||
|
$this->request = $request; |
||||||
|
$this->iiifConfig = $iiif_config; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { |
||||||
|
return new static( |
||||||
|
$configuration, |
||||||
|
$plugin_id, |
||||||
|
$plugin_definition, |
||||||
|
$container->get('serializer'), |
||||||
|
$container->get('request_stack')->getCurrentRequest(), |
||||||
|
$container->get('config.factory')->get('islandora_iiif.settings') |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
public function render() { |
||||||
|
$json = []; |
||||||
|
$iiif_address = $this->iiifConfig->get('iiif_server'); |
||||||
|
if (!is_null($iiif_address) && !empty($iiif_address)) { |
||||||
|
// Get the current URL being requested. |
||||||
|
$request_url = $this->request->getSchemeAndHttpHost() . $this->request->getRequestUri(); |
||||||
|
// Strip off the last URI component to get the base ID of the URL. |
||||||
|
// @todo assumming the view is a path like /node/1/manifest.json |
||||||
|
$url_components = explode('/', $request_url); |
||||||
|
array_pop($url_components); |
||||||
|
$iiif_base_id = implode('/', $url_components); |
||||||
|
// @see https://iiif.io/api/presentation/2.1/#manifest |
||||||
|
$json += [ |
||||||
|
'@type' => 'sc:Manifest', |
||||||
|
'@id' => $request_url, |
||||||
|
'@context' => 'http://iiif.io/api/presentation/2/context.json', |
||||||
|
// @see https://iiif.io/api/presentation/2.1/#sequence |
||||||
|
'sequences' => [ |
||||||
|
'@context' => 'http://iiif.io/api/presentation/2/context.json', |
||||||
|
'@id' => $iiif_base_id . '/sequence/normal', |
||||||
|
'@type' => 'sc:Sequence', |
||||||
|
], |
||||||
|
]; |
||||||
|
// For each row in the View result. |
||||||
|
foreach ($this->view->result as $row) { |
||||||
|
// Add the IIIF URL to the image to print out as JSON. |
||||||
|
$canvases = $this->getTileSourceFromRow($row, $iiif_address, $iiif_base_id); |
||||||
|
foreach ($canvases as $tile_source) { |
||||||
|
$json['sequences'][0]['canvases'][] = $tile_source; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
unset($this->view->row_index); |
||||||
|
|
||||||
|
$content_type = 'json'; |
||||||
|
|
||||||
|
return $this->serializer->serialize($json, $content_type, ['views_style_plugin' => $this]); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Render array from views result row. |
||||||
|
* |
||||||
|
* @param \Drupal\views\ResultRow $row |
||||||
|
* Result row. |
||||||
|
* @param string $iiif_address |
||||||
|
* The URL to the IIIF server endpoint. |
||||||
|
* @param string $iiif_base_id |
||||||
|
* The URL for the request, minus the last part of the URL, |
||||||
|
* which is likely "manifest". |
||||||
|
* |
||||||
|
* @return array |
||||||
|
* List of IIIF URLs to display in the Openseadragon viewer. |
||||||
|
*/ |
||||||
|
protected function getTileSourceFromRow(ResultRow $row, $iiif_address, $iiif_base_id) { |
||||||
|
$canvases = []; |
||||||
|
$viewsField = $this->view->field[$this->options['iiif_tile_field']]; |
||||||
|
$entity = $viewsField->getEntity($row); |
||||||
|
|
||||||
|
if (isset($entity->{$viewsField->definition['field_name']})) { |
||||||
|
|
||||||
|
/** @var \Drupal\Core\Field\FieldItemListInterface $images */ |
||||||
|
$images = $entity->{$viewsField->definition['field_name']}; |
||||||
|
foreach ($images as $image) { |
||||||
|
// Create the IIIF URL for this file |
||||||
|
// Visiting $iiif_url will resolve to the info.json for the image. |
||||||
|
$file_url = $image->entity->url(); |
||||||
|
$iiif_url = rtrim($iiif_address, '/') . '/' . urlencode($file_url); |
||||||
|
|
||||||
|
// Create the necessary ID's for the canvas and annotation. |
||||||
|
$canvas_id = $iiif_base_id . '/canvas/' . $entity->id(); |
||||||
|
$annotation_id = $iiif_base_id . '/annotation/' . $entity->id(); |
||||||
|
|
||||||
|
$canvases[] = [ |
||||||
|
// @see https://iiif.io/api/presentation/2.1/#canvas |
||||||
|
'@id' => $canvas_id, |
||||||
|
'@type' => 'sc:Canvas', |
||||||
|
'label' => $entity->label(), |
||||||
|
// @see https://iiif.io/api/presentation/2.1/#image-resources |
||||||
|
'images' => [[ |
||||||
|
'@id' => $annotation_id, |
||||||
|
"@type" => "oa:Annotation", |
||||||
|
'motivation' => 'sc:painting', |
||||||
|
'resource' => [ |
||||||
|
'@id' => $iiif_url . '/full/full/0/default.jpg', |
||||||
|
'service' => [ |
||||||
|
'@id' => $iiif_url, |
||||||
|
'@context' => 'http://iiif.io/api/image/2/context.json', |
||||||
|
'profile' => 'http://iiif.io/api/image/2/profiles/level2.json', |
||||||
|
], |
||||||
|
], |
||||||
|
'on' => $canvas_id, |
||||||
|
], |
||||||
|
], |
||||||
|
]; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return $canvases; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
protected function defineOptions() { |
||||||
|
$options = parent::defineOptions(); |
||||||
|
|
||||||
|
$options['iiif_tile_field'] = ['default' => '']; |
||||||
|
|
||||||
|
return $options; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
public function buildOptionsForm(&$form, FormStateInterface $form_state) { |
||||||
|
parent::buildOptionsForm($form, $form_state); |
||||||
|
|
||||||
|
$field_options = []; |
||||||
|
|
||||||
|
$fields = $this->displayHandler->getHandlers('field'); |
||||||
|
$islandora_default_file_fields = [ |
||||||
|
'field_media_file', |
||||||
|
'field_media_image', |
||||||
|
]; |
||||||
|
$file_views_field_formatters = [ |
||||||
|
// Image formatters. |
||||||
|
'image', 'image_url', |
||||||
|
// File formatters. |
||||||
|
'file_default', 'file_url_plain', |
||||||
|
]; |
||||||
|
/** @var \Drupal\views\Plugin\views\field\FieldPluginBase[] $fields */ |
||||||
|
foreach ($fields as $field_name => $field) { |
||||||
|
// If this is a known Islandora file/image field |
||||||
|
// OR it is another/custom field add it as an available option. |
||||||
|
// @todo find better way to identify file fields |
||||||
|
// Currently $field->options['type'] is storing the "formatter" of the |
||||||
|
// file/image so there are a lot of possibilities. |
||||||
|
// The default formatters are 'image' and 'file_default' |
||||||
|
// so this approach should catch most... |
||||||
|
if (in_array($field_name, $islandora_default_file_fields) || |
||||||
|
(!empty($field->options['type']) && in_array($field->options['type'], $file_views_field_formatters))) { |
||||||
|
$field_options[$field_name] = $field->adminLabel(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// If no fields to choose from, add an error message indicating such. |
||||||
|
if (count($field_options) == 0) { |
||||||
|
drupal_set_message($this->t('No image or file fields were found in the View. |
||||||
|
You will need to add a field to this View'), 'error'); |
||||||
|
} |
||||||
|
|
||||||
|
$form['iiif_tile_field'] = [ |
||||||
|
'#title' => $this->t('Tile source field'), |
||||||
|
'#type' => 'select', |
||||||
|
'#default_value' => $this->options['iiif_tile_field'], |
||||||
|
'#description' => $this->t("The source of image for each entity."), |
||||||
|
'#options' => $field_options, |
||||||
|
// Only make the form element required if |
||||||
|
// we have more than one option to choose from |
||||||
|
// otherwise could lock up the form when setting up a View. |
||||||
|
'#required' => count($field_options) > 0, |
||||||
|
]; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Returns an array of format options. |
||||||
|
* |
||||||
|
* @return string[] |
||||||
|
* An array of the allowed serializer formats. In this case just JSON. |
||||||
|
*/ |
||||||
|
public function getFormats() { |
||||||
|
return ['json' => 'json']; |
||||||
|
} |
||||||
|
|
||||||
|
} |
Loading…
Reference in new issue