Compare commits

...

7 Commits

  1. 15
      modules/islandora_iiif/README.md
  2. 10
      modules/islandora_iiif/config/optional/system.action.media_attributes_from_iiif_action.yml
  3. 16
      modules/islandora_iiif/islandora_iiif.install
  4. 4
      modules/islandora_iiif/islandora_iiif.services.yml
  5. 129
      modules/islandora_iiif/src/IiifInfo.php
  6. 169
      modules/islandora_iiif/src/Plugin/Action/MediaAttributesFromIiif.php
  7. 145
      modules/islandora_iiif/src/Plugin/views/style/IIIFManifest.php

15
modules/islandora_iiif/README.md

@ -38,6 +38,21 @@ This module implements a Views Style plugin. It provides the following settings:
1. Tile Source: A field that was added to the views list of fields with the image to be served. This should be a File or Image type field on a Media.
2. Structured Text field: This lets you specify a file field where OCR text with positional data, e.g., hOCR can be found.
### Media Attributes from IIIF Action
The module also provides an action that lets a site owner populate a TIFF or JP2 image's width and
height attributes into fields so the IIIF server is not bogged down trying to generate a manifest if
it doesn't have them.
To use it, either:
- Add it as a derivative reaction to a node with an Original FIle as its child, or
- Use it as a batch action, such as on a Paged Content object's list of child pages.
The action assumes the media type has fields with machine names of field_height and
field_width. Making this configurable would mean they would not appear
on entity list pages.
## Documentation
Official documentation is available on the [Islandora 8 documentation site](https://islandora.github.io/documentation/).

10
modules/islandora_iiif/config/optional/system.action.media_attributes_from_iiif_action.yml

@ -0,0 +1,10 @@
langcode: en
status: true
dependencies:
module:
- islandora_iiif
id: media_attributes_from_iiif_action
label: 'Media attributes from IIIF'
type: node
plugin: islandora_iiif:media_attributes_from_iiif_action:media
configuration: { }

16
modules/islandora_iiif/islandora_iiif.install

@ -0,0 +1,16 @@
<?php
/**
* @file
* Install/update hook implementations.
*/
/**
* Add Media Attributes from IIIF action.
*/
function islandora_iiif_update_92001(&$sandbox) {
$config_id = 'system.action.media_attributes_from_iiif_action';
$config_path = \Drupal::service('extension.list.module')->getPath('islandora_iiif') . '/config/optional/' . $config_id .'.yml';
$data = \Symfony\Component\Yaml\Yaml::parseFile($config_path);
\Drupal::configFactory()->getEditable($config_id)->setData($data)->save(TRUE);
}

4
modules/islandora_iiif/islandora_iiif.services.yml

@ -0,0 +1,4 @@
services:
islandora_iiif:
class: Drupal\islandora_iiif\IiifInfo
arguments: ['@config.factory', '@http_client', '@logger.channel.islandora', '@jwt.authentication.jwt']

129
modules/islandora_iiif/src/IiifInfo.php

@ -0,0 +1,129 @@
<?php
namespace Drupal\islandora_iiif;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Logger\LoggerChannelInterface;
use Drupal\file\FileInterface;
use Drupal\jwt\Authentication\Provider\JwtAuth;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\ClientException;
use GuzzleHttp\Exception\ConnectException;
/**
* Get IIIF related info for a given File or Image entity.
*/
class IiifInfo {
/**
* The config factory.
*
* @var \Drupal\Core\Config\ConfigFactoryInterface
*/
protected $configFactory;
/**
* The HTTP client
*
* @var \GuzzleHttp\Client;
*/
protected $httpClient;
/**
* This module's config.
*
* @var \Drupal\Core\Config\ImmutableConfig
*/
protected $iiifConfig;
/**
* JWT Auth provider service.
*
* @var \Drupal\jwt\Authentication\Provider\JwtAuth
*/
protected $jwtAuth;
/**
* The logger.
*
* @var \Drupal\Core\Logger\LoggerChannelInterface
*/
protected $logger;
/**
* Constructs an IiifInfo object.
*
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The config factory.
* @param \Guzzle\Http\Client $http_client
* The HTTP Client.
* @param \Drupal\Core\Logger\LoggerChannelInterface $channel
* Logger channel.
* @param \Drupal\jwt\Authentication\Provider\JwtAuth $jwt_auth
* The JWT auth provider.
*/
public function __construct(ConfigFactoryInterface $config_factory, Client $http_client, LoggerChannelInterface $channel, JwtAuth $jwt_auth) {
$this->configFactory = $config_factory;
$this->iiifConfig= $this->configFactory->get('islandora_iiif.settings');
$this->httpClient = $http_client;
$this->logger = $channel;
$this->jwtAuth = $jwt_auth;
}
/**
* The IIIF base URL for an image.
* Visiting this URL will resolve to the info.json for the image.
*
* @return string
* The absolute URL on the IIIF server.
*/
public function baseUrl($image) {
if ($this->iiifConfig->get('use_relative_paths')) {
$file_url = ltrim($image->createFileUrl(TRUE), '/');
}
else {
$file_url = $image->createFileUrl(FALSE);
}
$iiif_address = $this->iiifConfig->get('iiif_server');
$iiif_url = rtrim($iiif_address, '/') . '/' . urlencode($file_url);
return $iiif_url;
}
/**
* Retrieve an image's dimensions via the IIIF server.
*
* @param \Drupal\File\FileInterface $file
* The image file.
* @return array|FALSE
* The image dimensions in an array as [$width, $height]
*/
public function getImageDimensions(FileInterface $file) {
$iiif_url = $this->baseUrl($file);
try {
$info_json = $this->httpClient->request('get', $iiif_url, [
'headers' => [
'Authorization' => 'bearer ' . $this->jwtAuth->generateToken()
]
])->getBody();
$resource = json_decode($info_json, TRUE);
$width = $resource['width'];
$height = $resource['height'];
if (is_numeric($width) && is_numeric($height)) {
return [intval($width), intval($height)];
}
}
catch (ClientException | ConnectException $e) {
$this->logger->info("Error getting image file dimensions from IIIF server: " . $e->getMessage());
}
return FALSE;
}
}

169
modules/islandora_iiif/src/Plugin/Action/MediaAttributesFromIiif.php

@ -0,0 +1,169 @@
<?php
namespace Drupal\islandora_iiif\Plugin\Action;
use Drupal\Component\Datetime\TimeInterface;
use Drupal\Core\Action\Plugin\Action\SaveAction;
use Drupal\Core\Config\ImmutableConfig;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\file\FileInterface;
use Drupal\Core\Logger\LoggerChannelInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\islandora\IslandoraUtils;
use Drupal\islandora\MediaSource\MediaSourceService;
use Drupal\islandora_iiif\IiifInfo;
use Drupal\media\MediaInterface;
use Drupal\node\NodeInterface;
use GuzzleHttp\Client;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\Request;
/**
* Provides an action that can save any entity.
*
* @Action(
* id = "islandora_iiif:media_attributes_from_iiif_action",
* action_label = @Translation("Add image dimensions retrieved from the IIIF server"),
* deriver = "Drupal\Core\Action\Plugin\Action\Derivative\EntityChangedActionDeriver",
* )
*/
class MediaAttributesFromIiif extends SaveAction {
/**
* The HTTP client
*
* @var \GuzzleHttp\Client;
*/
protected $httpClient;
/**
* The IIIF Info service.
*
* @var IiifInfo
*/
protected $iiifInfo;
/**
* The logger.
*
* @var \Drupal\Core\Logger\LoggerChannelInterface
*/
protected $logger;
/**
* Islandora utility functions.
*
* @var \Drupal\islandora\IslandoraUtils
*/
protected $utils;
/**
* A MediaSourceService.
*
* @var \Drupal\islandora\MediaSource\MediaSourceService
*/
protected $mediaSource;
/**
* Constructs a TiffMediaSaveAction object.
*
* @param mixed[] $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin ID for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
* @param \Drupal\Component\Datetime\TimeInterface $time
* @param
* The time service.
* @param \Guzzle\Http\Client $http_client
* The HTTP Client.
* @param IiifInfo $iiif_info
* The IIIF INfo service.
* @param \Drupal\islandora\MediaSource\MediaSourceService $media_source
* Media source service.
* @param \Drupal\Core\Logger\LoggerChannelInterface $channel
* Logger channel.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entity_type_manager, TimeInterface $time, Client $http_client, IiifInfo $iiif_info, IslandoraUtils $islandora_utils, MediaSourceService $media_source, LoggerChannelInterface $channel) {
parent::__construct($configuration, $plugin_id, $plugin_definition, $entity_type_manager, $time);
$this->httpClient = $http_client;
$this->iiifInfo = $iiif_info;
$this->utils = $islandora_utils;
$this->mediaSource = $media_source;
$this->logger = $channel;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('entity_type.manager'),
$container->get('datetime.time'),
$container->get('http_client'),
$container->get('islandora_iiif'),
$container->get('islandora.utils'),
$container->get('islandora.media_source_service'),
$container->get('logger.channel.islandora')
);
}
/**
* {@inheritdoc}
*/
public function execute($entity = NULL ) {
$width = $height = FALSE;
// Get the original File media use term.
$original_file_term = $this->utils->getTermForUri('http://pcdm.org/use#OriginalFile');
/**
* @var \Drupal\media\MediaInterface $original_file_media
*/
$original_file_mids = $this->utils->getMediaReferencingNodeAndTerm($entity, $original_file_term);
if (!empty($original_file_mids)) {
// Ordinarily there shouldn't be more than one Original File media but it's not guaranteed.
foreach($original_file_mids as $original_file_mid) {
/*
* @var \Drupal\Media\MediaInterface $original_file_media
*/
$original_file_media = $this->entityTypeManager->getStorage('media')->load($original_file_mid);
// Get the media MIME Type
$original_file = $this->mediaSource->getSourceFile($original_file_media);
$mime_type = $original_file->getMimeType();
if (in_array($mime_type, ['image/tiff', 'image/jp2'])) {
[$width, $height] = $this->iiifInfo->getImageDimensions($original_file);
}
// @todo Make field configurable. Low priority since this whole thing is a workaround for an Islandora limitation.
if ($original_file_media->hasField('field_width') && $original_file_media->hasField('field_height')) {
$original_file_media->set('field_height', $height);
$original_file_media->set('field_width', $width);
$original_file_media->save();
}
}
}
}
/**
* {@inheritdoc}
*/
public function access($object, AccountInterface $account = NULL, $return_as_object = FALSE) {
/** @var \Drupal\Core\Entity\EntityInterface $object */
return $object->access('update', $account, $return_as_object);
}
}

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

@ -11,6 +11,10 @@ use Drupal\Core\Field\FieldItemInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Messenger\MessengerInterface;
use Drupal\Core\Url;
use Drupal\iiif_presentation_api\Encoder\V3\IiifP;
use Drupal\islandora\IslandoraUtils;
use Drupal\islandora_iiif\IiiffInfo;
use Drupal\islandora_iiif\IiifInfo;
use Drupal\views\Plugin\views\style\StylePluginBase;
use Drupal\views\ResultRow;
use GuzzleHttp\Client;
@ -35,6 +39,14 @@ use Symfony\Component\HttpFoundation\Request;
*/
class IIIFManifest extends StylePluginBase {
/**
* Islandora utility functions.
*
* @var \Drupal\islandora\IslandoraUtils
*/
protected $utils;
/**
* {@inheritdoc}
*/
@ -59,6 +71,13 @@ class IIIFManifest extends StylePluginBase {
*/
protected $serializer;
/**
* The IIIF Info service.
*
* @var IiifInfo
*/
protected $iiifInfo;
/**
* The request service.
*
@ -104,7 +123,7 @@ class IIIFManifest extends StylePluginBase {
/**
* {@inheritdoc}
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, SerializerInterface $serializer, Request $request, ImmutableConfig $iiif_config, EntityTypeManagerInterface $entity_type_manager, FileSystemInterface $file_system, Client $http_client, MessengerInterface $messenger, ModuleHandlerInterface $moduleHandler) {
public function __construct(array $configuration, $plugin_id, $plugin_definition, SerializerInterface $serializer, Request $request, ImmutableConfig $iiif_config, EntityTypeManagerInterface $entity_type_manager, FileSystemInterface $file_system, Client $http_client, MessengerInterface $messenger, ModuleHandlerInterface $moduleHandler, IslandoraUtils $utils, IiifInfo $iiif_info) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->serializer = $serializer;
@ -115,6 +134,8 @@ class IIIFManifest extends StylePluginBase {
$this->httpClient = $http_client;
$this->messenger = $messenger;
$this->moduleHandler = $moduleHandler;
$this->utils = $utils;
$this->iiifInfo = $iiif_info;
}
/**
@ -132,7 +153,9 @@ class IIIFManifest extends StylePluginBase {
$container->get('file_system'),
$container->get('http_client'),
$container->get('messenger'),
$container->get('module_handler')
$container->get('module_handler'),
$container->get('islandora.utils'),
$container->get('islandora_iiif')
);
}
@ -163,6 +186,11 @@ class IIIFManifest extends StylePluginBase {
$content_path = implode('/', $url_components);
$iiif_base_id = $request_host . '/' . $content_path;
/**
* @var \Drupal\taxonomy\TermInterface|null
*/
$structured_text_term = !empty($this->options['structured_text_term_uri']) ?$this->utils->getTermForUri($this->options['structured_text_term_uri']) : NULL;
// @see https://iiif.io/api/presentation/2.1/#manifest
$json += [
'@type' => 'sc:Manifest',
@ -182,7 +210,7 @@ class IIIFManifest extends StylePluginBase {
// 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);
$canvases = $this->getTileSourceFromRow($row, $iiif_address, $iiif_base_id, $structured_text_term);
foreach ($canvases as $tile_source) {
$json['sequences'][0]['canvases'][] = $tile_source;
}
@ -208,11 +236,13 @@ class IIIFManifest extends StylePluginBase {
* @param string $iiif_base_id
* The URL for the request, minus the last part of the URL,
* which is likely "manifest".
* @param \Drupal\taxonomy\TermInterface|null $structured_text_term
* The term that structured text media references, if any.
*
* @return array
* List of IIIF URLs to display in the Openseadragon viewer.
*/
protected function getTileSourceFromRow(ResultRow $row, $iiif_address, $iiif_base_id) {
protected function getTileSourceFromRow(ResultRow $row, $iiif_address, $iiif_base_id, $structured_text_term) {
$canvases = [];
foreach (array_filter(array_values($this->options['iiif_tile_field'])) as $iiif_tile_field) {
$viewsField = $this->view->field[$iiif_tile_field];
@ -275,7 +305,7 @@ class IIIFManifest extends StylePluginBase {
],
];
if ($ocr_url = $this->getOcrUrl($entity, $row, $i)) {
if ($ocr_url = $this->getOcrUrl($entity, $structured_text_term)) {
$tmp_canvas['seeAlso'] = [
'@id' => $ocr_url,
'format' => 'text/vnd.hocr+html',
@ -316,38 +346,49 @@ class IIIFManifest extends StylePluginBase {
if (isset($image->width) && is_numeric($image->width)
&& isset($image->height) && is_numeric($image->height)) {
return [intval($image->width), intval($image->height)];
return [intval($image->width),
intval($image->height)];
}
try {
$info_json = $this->httpClient->get($iiif_url)->getBody();
$resource = json_decode($info_json, TRUE);
$width = $resource['width'];
$height = $resource['height'];
}
catch (ClientException | ServerException | ConnectException $e) {
// If we couldn't get the info.json from IIIF
// try seeing if we can get it from Drupal.
if (empty($width) || empty($height)) {
// Get the image properties so we know the image width/height.
$properties = $image->getProperties();
$width = isset($properties['width']) ? $properties['width'] : 0;
$height = isset($properties['height']) ? $properties['height'] : 0;
if ($properties = $image->getProperties()
&& isset($properties['width']) && is_numeric($properties['width'])
&& isset($properties['height']) && is_numeric($properties['width'])) {
return [intval($properties['width']),
intval($properties['height'])];
}
$entity = $image->entity;
if ($entity->hasField('field_height') && !$entity->get('field_height')->isEmpty()
&& $entity->get('field_height')->value > 0
&& $entity->hasField('field_width')
&& !$entity->get('field_width')->isEmpty()
&& $entity->get('field_width')->value > 0) {
return [ $entity->get('field_width')->value,
$entity->get('field_height')->value];
}
if ($mime_type === 'image/tiff') {
// If this is a TIFF AND we don't know the width/height
// see if we can get the image size via PHP's core function.
if ($mime_type === 'image/tiff' && !$width || !$height) {
$uri = $image->entity->getFileUri();
$path = $this->fileSystem->realpath($uri);
if (!empty($path)) {
$image_size = getimagesize($path);
if ($image_size) {
$width = $image_size[0];
$height = $image_size[1];
return [intval($image_size[0]),
intval($image_size[1])];
}
}
}
// As a last resort, get it from the IIIF server.
// This can be very slow and will fail if there are too many pages.
$dimensions = $this->iiifInfo->getImageDimensions($image->entity);
if ($dimensions !== FALSE) {
return $dimensions;
}
return [$width, $height];
return [0, 0];
}
/**
@ -355,30 +396,40 @@ class IIIFManifest extends StylePluginBase {
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity at the current row.
* @param \Drupal\views\ResultRow $row
* Result row.
* @param int $delta
* The delta in case there are multiple canvases on one media.
* @param \Drupal\taxonomy\TermInterface|null $structured_text_term
* The term that structured text media references, if any.
*
* @return string|false
* return String|FALSE
* The absolute URL of the current row's structured text,
* or FALSE if none.
*/
protected function getOcrUrl(EntityInterface $entity, ResultRow $row, $delta) {
protected function getOcrUrl(EntityInterface $entity, $structured_text_term) {
$ocr_url = FALSE;
$iiif_ocr_file_field = !empty($this->options['iiif_ocr_file_field']) ? array_filter(array_values($this->options['iiif_ocr_file_field'])) : [];
$ocrField = count($iiif_ocr_file_field) > 0 ? $this->view->field[$iiif_ocr_file_field[0]] : NULL;
if ($ocrField) {
$ocr_entity = $ocrField->getEntity($row);
$ocr_entity = $entity;
$ocr_field_name = $ocrField->definition['field_name'];
if (!is_null($ocr_field_name)) {
$ocrs = $ocr_entity->{$ocr_field_name};
$ocr = isset($ocrs[$delta]) ? $ocrs[$delta] : FALSE;
$ocr = isset($ocrs[0]) ? $ocrs[0] : FALSE;
if ($ocr) {
$ocr_url = $ocr->entity->createFileUrl(FALSE);
}
}
}
elseif ($structured_text_term) {
$parent_node = $this->utils->getParentNode($entity);
$ocr_entity_array = $this->utils->getMediaReferencingNodeAndTerm($parent_node, $structured_text_term);
$ocr_entity_id = is_array($ocr_entity_array) ? array_shift($ocr_entity_array) : NULL;
$ocr_entity = $ocr_entity_id ? $this->entityTypeManager->getStorage('media')->load($ocr_entity_id) : NULL;
if ($ocr_entity) {
$ocr_file_source = $ocr_entity->getSource();
$ocr_fid = $ocr_file_source->getSourceFieldValue($ocr_entity);
$ocr_file = $this->entityTypeManager->getStorage('file')->load($ocr_fid);
$ocr_url = $ocr_file->createFileUrl(FALSE);
}
}
return $ocr_url;
}
@ -479,10 +530,18 @@ class IIIFManifest extends StylePluginBase {
'#title' => $this->t('Structured OCR data file field'),
'#type' => 'checkboxes',
'#default_value' => $this->options['iiif_ocr_file_field'],
'#description' => $this->t('The source of structured OCR text for each entity.'),
'#description' => $this->t('The source of structured OCR text for each entity. If the term setting below is left blank, it will be the same entity as the source image'),
'#options' => $field_options,
'#required' => FALSE,
];
$form['structured_text_term'] = [
'#type' => 'entity_autocomplete',
'#target_type' => 'taxonomy_term',
'#title' => $this->t('Structured OCR text term'),
'#default_value' => $this->utils->getTermForUri($this->options['structured_text_term_uri']),
'#required' => FALSE,
'#description' => $this->t('Term indicating the media that holds structured text, such as hOCR, for the given object. Use this if the text is on a separate media from the tile source.'),
];
}
/**
@ -495,4 +554,24 @@ class IIIFManifest extends StylePluginBase {
return ['json' => 'json'];
}
/**
* Submit handler for options form.
* Used to store the structured text media term by URL instead of Ttid.
*
* @param array $form
* The form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state object.
*
* @return void
*/
public function submitOptionsForm(&$form, FormStateInterface $form_state) {
$style_options = $form_state->getValue('style_options');
$tid = $style_options['structured_text_term'];
$term = $this->entityTypeManager->getStorage('taxonomy_term')->load($tid);
$style_options['structured_text_term_uri'] = $this->utils->getUriForTerm($term);
$form_state->setValue('style_options', $style_options);
parent::submitOptionsForm($form, $form_state);
}
}

Loading…
Cancel
Save