From a00573f3edaf4ef7aeb00f2bba5ed71bd17c6dc9 Mon Sep 17 00:00:00 2001 From: Joe Corall Date: Wed, 11 Sep 2019 14:16:53 -0400 Subject: [PATCH] Initial implementation of a REST Export serializer for IIIF Manifests (#157) * 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 fields --- .../install/islandora_iiif.settings.yml | 1 + .../config/schema/islandora_iiif.schema.yml | 7 + .../islandora_iiif/islandora_iiif.info.yml | 7 + .../islandora_iiif.links.menu.yml | 6 + modules/islandora_iiif/islandora_iiif.module | 24 ++ .../islandora_iiif/islandora_iiif.routing.yml | 9 + .../src/Form/IslandoraIIIFConfigForm.php | 92 ++++++ .../src/Plugin/views/style/IIIFManifest.php | 267 ++++++++++++++++++ 8 files changed, 413 insertions(+) create mode 100644 modules/islandora_iiif/config/install/islandora_iiif.settings.yml create mode 100644 modules/islandora_iiif/config/schema/islandora_iiif.schema.yml create mode 100644 modules/islandora_iiif/islandora_iiif.info.yml create mode 100644 modules/islandora_iiif/islandora_iiif.links.menu.yml create mode 100644 modules/islandora_iiif/islandora_iiif.module create mode 100644 modules/islandora_iiif/islandora_iiif.routing.yml create mode 100644 modules/islandora_iiif/src/Form/IslandoraIIIFConfigForm.php create mode 100644 modules/islandora_iiif/src/Plugin/views/style/IIIFManifest.php diff --git a/modules/islandora_iiif/config/install/islandora_iiif.settings.yml b/modules/islandora_iiif/config/install/islandora_iiif.settings.yml new file mode 100644 index 00000000..760946fb --- /dev/null +++ b/modules/islandora_iiif/config/install/islandora_iiif.settings.yml @@ -0,0 +1 @@ +iiif_server: diff --git a/modules/islandora_iiif/config/schema/islandora_iiif.schema.yml b/modules/islandora_iiif/config/schema/islandora_iiif.schema.yml new file mode 100644 index 00000000..6ef42bc4 --- /dev/null +++ b/modules/islandora_iiif/config/schema/islandora_iiif.schema.yml @@ -0,0 +1,7 @@ +islandora_iiif.settings: + type: config_object + label: 'Islandora IIIF Settings' + mapping: + iiif_server: + type: string + label: 'IIIF Server Url' diff --git a/modules/islandora_iiif/islandora_iiif.info.yml b/modules/islandora_iiif/islandora_iiif.info.yml new file mode 100644 index 00000000..06c249e1 --- /dev/null +++ b/modules/islandora_iiif/islandora_iiif.info.yml @@ -0,0 +1,7 @@ +name: 'Islandora IIIF' +type: module +description: 'IIIF support for Islandora' +core: 8.x +package: Islandora +dependencies: + - islandora diff --git a/modules/islandora_iiif/islandora_iiif.links.menu.yml b/modules/islandora_iiif/islandora_iiif.links.menu.yml new file mode 100644 index 00000000..4d5b5060 --- /dev/null +++ b/modules/islandora_iiif/islandora_iiif.links.menu.yml @@ -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 diff --git a/modules/islandora_iiif/islandora_iiif.module b/modules/islandora_iiif/islandora_iiif.module new file mode 100644 index 00000000..e9e7526e --- /dev/null +++ b/modules/islandora_iiif/islandora_iiif.module @@ -0,0 +1,24 @@ +' . t('About') . ''; + $output .= '

' . t('IIIF support for Islandora') . '

'; + return $output; + + default: + } +} diff --git a/modules/islandora_iiif/islandora_iiif.routing.yml b/modules/islandora_iiif/islandora_iiif.routing.yml new file mode 100644 index 00000000..dd69c2e5 --- /dev/null +++ b/modules/islandora_iiif/islandora_iiif.routing.yml @@ -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 diff --git a/modules/islandora_iiif/src/Form/IslandoraIIIFConfigForm.php b/modules/islandora_iiif/src/Form/IslandoraIIIFConfigForm.php new file mode 100644 index 00000000..5bb73de4 --- /dev/null +++ b/modules/islandora_iiif/src/Form/IslandoraIIIFConfigForm.php @@ -0,0 +1,92 @@ +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; + } + + } + +} diff --git a/modules/islandora_iiif/src/Plugin/views/style/IIIFManifest.php b/modules/islandora_iiif/src/Plugin/views/style/IIIFManifest.php new file mode 100644 index 00000000..167069df --- /dev/null +++ b/modules/islandora_iiif/src/Plugin/views/style/IIIFManifest.php @@ -0,0 +1,267 @@ +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']; + } + +}