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