Browse Source

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
pull/729/head
Joe Corall 5 years ago committed by dannylamb
parent
commit
a00573f3ed
  1. 1
      modules/islandora_iiif/config/install/islandora_iiif.settings.yml
  2. 7
      modules/islandora_iiif/config/schema/islandora_iiif.schema.yml
  3. 7
      modules/islandora_iiif/islandora_iiif.info.yml
  4. 6
      modules/islandora_iiif/islandora_iiif.links.menu.yml
  5. 24
      modules/islandora_iiif/islandora_iiif.module
  6. 9
      modules/islandora_iiif/islandora_iiif.routing.yml
  7. 92
      modules/islandora_iiif/src/Form/IslandoraIIIFConfigForm.php
  8. 267
      modules/islandora_iiif/src/Plugin/views/style/IIIFManifest.php

1
modules/islandora_iiif/config/install/islandora_iiif.settings.yml

@ -0,0 +1 @@
iiif_server:

7
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'

7
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

6
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

24
modules/islandora_iiif/islandora_iiif.module

@ -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:
}
}

9
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

92
modules/islandora_iiif/src/Form/IslandoraIIIFConfigForm.php

@ -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;
}
}
}

267
modules/islandora_iiif/src/Plugin/views/style/IIIFManifest.php

@ -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…
Cancel
Save