From 95d3152a6679c31a0ca927e2ec57e82921bb05f1 Mon Sep 17 00:00:00 2001
From: Alexander O'Neill <alexander@born-digital.com>
Date: Mon, 16 Oct 2023 21:46:58 -0300
Subject: [PATCH 01/11] Refactor IIIF, create IIIF Info Service.

---
 .../islandora_iiif.services.yml               |   4 +
 modules/islandora_iiif/src/IiifInfo.php       | 113 ++++++++++++++++++
 .../src/Plugin/views/style/IIIFManifest.php   |  90 +++++++++++---
 3 files changed, 189 insertions(+), 18 deletions(-)
 create mode 100644 modules/islandora_iiif/islandora_iiif.services.yml
 create mode 100644 modules/islandora_iiif/src/IiifInfo.php

diff --git a/modules/islandora_iiif/islandora_iiif.services.yml b/modules/islandora_iiif/islandora_iiif.services.yml
new file mode 100644
index 00000000..42b6054a
--- /dev/null
+++ b/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']
diff --git a/modules/islandora_iiif/src/IiifInfo.php b/modules/islandora_iiif/src/IiifInfo.php
new file mode 100644
index 00000000..294af19e
--- /dev/null
+++ b/modules/islandora_iiif/src/IiifInfo.php
@@ -0,0 +1,113 @@
+<?php
+
+namespace Drupal\islandora_iiif;
+
+use Drupal\Core\Config\ConfigFactoryInterface;
+use Drupal\Core\Logger\LoggerChannelInterface;
+use Drupal\file\FileInterface;
+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;
+
+  /**
+   * 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.
+   */
+  public function __construct(ConfigFactoryInterface $config_factory, Client $http_client, LoggerChannelInterface $channel) {
+    $this->configFactory = $config_factory;
+
+    $this->iiifConfig= $this->configFactory->get('islandora_iiif.settings');
+    $this->httpClient = $http_client;
+    $this->logger = $channel;
+  }
+
+  /**
+   * 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->get($iiif_url)->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;
+  }
+
+
+}
diff --git a/modules/islandora_iiif/src/Plugin/views/style/IIIFManifest.php b/modules/islandora_iiif/src/Plugin/views/style/IIIFManifest.php
index 5a2fb63b..d94f83e3 100644
--- a/modules/islandora_iiif/src/Plugin/views/style/IIIFManifest.php
+++ b/modules/islandora_iiif/src/Plugin/views/style/IIIFManifest.php
@@ -11,6 +11,7 @@ use Drupal\Core\Field\FieldItemInterface;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Messenger\MessengerInterface;
 use Drupal\Core\Url;
+use Drupal\islandora\IslandoraUtils;
 use Drupal\views\Plugin\views\style\StylePluginBase;
 use Drupal\views\ResultRow;
 use GuzzleHttp\Client;
@@ -35,6 +36,14 @@ use Symfony\Component\HttpFoundation\Request;
  */
 class IIIFManifest extends StylePluginBase {
 
+  /**
+   * Islandora utility functions.
+   *
+   * @var \Drupal\islandora\IslandoraUtils
+   */
+  protected $utils;
+
+
   /**
    * {@inheritdoc}
    */
@@ -104,7 +113,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) {
     parent::__construct($configuration, $plugin_id, $plugin_definition);
 
     $this->serializer = $serializer;
@@ -115,6 +124,7 @@ class IIIFManifest extends StylePluginBase {
     $this->httpClient = $http_client;
     $this->messenger = $messenger;
     $this->moduleHandler = $moduleHandler;
+    $this->utils = $utils;
   }
 
   /**
@@ -132,7 +142,8 @@ 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')
     );
   }
 
@@ -163,6 +174,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 = $this->utils->getTermForUri($this->options['structured_text_term_uri']);
+
       // @see https://iiif.io/api/presentation/2.1/#manifest
       $json += [
         '@type' => 'sc:Manifest',
@@ -182,7 +198,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 +224,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 +293,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',
@@ -355,28 +373,36 @@ 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.
-   *
-   * @return string|false
+   * @param \Drupal\taxonomy\TermInterface|null $structured_text_term
+   *   The term that structured text media references, if any.
+  *
+   * 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;
-        if ($ocr) {
-          $ocr_url = $ocr->entity->createFileUrl(FALSE);
-        }
+        $ocr = isset($ocrs[0]) ? $ocrs[0] : FALSE;
+        $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);
       }
     }
 
@@ -479,10 +505,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 +529,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);
+  }
+
 }

From 13b74009dec5b5f9a84c5a75e21c9457a375b71e Mon Sep 17 00:00:00 2001
From: Alexander O'Neill <alexander@born-digital.com>
Date: Tue, 17 Oct 2023 00:02:56 -0300
Subject: [PATCH 02/11] Islandora IIIF: Add action to retrieve image attributes
 from IIIF server.

---
 ...tion.media_attributes_from_iiif_action.yml |  10 ++
 modules/islandora_iiif/islandora_iiif.install |  16 ++
 .../Plugin/Action/MediaAttributesFromIiif.php | 169 ++++++++++++++++++
 3 files changed, 195 insertions(+)
 create mode 100644 modules/islandora_iiif/config/optional/system.action.media_attributes_from_iiif_action.yml
 create mode 100644 modules/islandora_iiif/islandora_iiif.install
 create mode 100644 modules/islandora_iiif/src/Plugin/Action/MediaAttributesFromIiif.php

diff --git a/modules/islandora_iiif/config/optional/system.action.media_attributes_from_iiif_action.yml b/modules/islandora_iiif/config/optional/system.action.media_attributes_from_iiif_action.yml
new file mode 100644
index 00000000..dc0b18b9
--- /dev/null
+++ b/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: media
+plugin: islandora_iiif:media_attributes_from_iiif_action:media
+configuration: {  }
diff --git a/modules/islandora_iiif/islandora_iiif.install b/modules/islandora_iiif/islandora_iiif.install
new file mode 100644
index 00000000..9b040735
--- /dev/null
+++ b/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);
+}
diff --git a/modules/islandora_iiif/src/Plugin/Action/MediaAttributesFromIiif.php b/modules/islandora_iiif/src/Plugin/Action/MediaAttributesFromIiif.php
new file mode 100644
index 00000000..9bb951e2
--- /dev/null
+++ b/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);
+  }
+
+  }

From 4450248078e91cb87d9444d15ac025f8c4064daf Mon Sep 17 00:00:00 2001
From: Alexander O'Neill <alexander@born-digital.com>
Date: Tue, 17 Oct 2023 13:56:09 -0300
Subject: [PATCH 03/11] Islandora IIIF: Add auth headers to IIIF Info request.

---
 .../islandora_iiif.services.yml               |  2 +-
 modules/islandora_iiif/src/IiifInfo.php       | 20 +++++++++++++++++--
 2 files changed, 19 insertions(+), 3 deletions(-)

diff --git a/modules/islandora_iiif/islandora_iiif.services.yml b/modules/islandora_iiif/islandora_iiif.services.yml
index 42b6054a..fd39211c 100644
--- a/modules/islandora_iiif/islandora_iiif.services.yml
+++ b/modules/islandora_iiif/islandora_iiif.services.yml
@@ -1,4 +1,4 @@
 services:
   islandora_iiif:
     class: Drupal\islandora_iiif\IiifInfo
-    arguments: ['@config.factory', '@http_client', '@logger.channel.islandora']
+    arguments: ['@config.factory', '@http_client', '@logger.channel.islandora', '@jwt.authentication.jwt']
diff --git a/modules/islandora_iiif/src/IiifInfo.php b/modules/islandora_iiif/src/IiifInfo.php
index 294af19e..7e1e7460 100644
--- a/modules/islandora_iiif/src/IiifInfo.php
+++ b/modules/islandora_iiif/src/IiifInfo.php
@@ -5,6 +5,8 @@ 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;
@@ -36,6 +38,13 @@ class IiifInfo {
    */
   protected $iiifConfig;
 
+/**
+ * JWT Auth provider service.
+ *
+ * @var \Drupal\jwt\Authentication\Provider\JwtAuth
+ */
+  protected $jwtAuth;
+
   /**
    * The logger.
    *
@@ -53,13 +62,16 @@ class IiifInfo {
    * 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) {
+  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;
   }
 
   /**
@@ -95,7 +107,11 @@ class IiifInfo {
   public function getImageDimensions(FileInterface $file) {
     $iiif_url = $this->baseUrl($file);
     try {
-      $info_json = $this->httpClient->get($iiif_url)->getBody();
+      $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'];

From ba3024d3fdde68513384064375e93502d155e8d7 Mon Sep 17 00:00:00 2001
From: Alexander O'Neill <alexander@born-digital.com>
Date: Wed, 18 Oct 2023 04:48:59 -0300
Subject: [PATCH 04/11] Islandora IIIF: Change media action to node.

---
 .../system.action.media_attributes_from_iiif_action.yml         | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/modules/islandora_iiif/config/optional/system.action.media_attributes_from_iiif_action.yml b/modules/islandora_iiif/config/optional/system.action.media_attributes_from_iiif_action.yml
index dc0b18b9..36e7c8f9 100644
--- a/modules/islandora_iiif/config/optional/system.action.media_attributes_from_iiif_action.yml
+++ b/modules/islandora_iiif/config/optional/system.action.media_attributes_from_iiif_action.yml
@@ -5,6 +5,6 @@ dependencies:
     - islandora_iiif
 id: media_attributes_from_iiif_action
 label: 'Media attributes from IIIF'
-type: media
+type: node
 plugin: islandora_iiif:media_attributes_from_iiif_action:media
 configuration: {  }

From 21d468218b12e6842679a3f8fb535cc54f8dc5ac Mon Sep 17 00:00:00 2001
From: Alexander O'Neill <alexander@born-digital.com>
Date: Wed, 18 Oct 2023 05:03:30 -0300
Subject: [PATCH 05/11] Islandora IIIF: Update README.

---
 modules/islandora_iiif/README.md | 15 +++++++++++++++
 1 file changed, 15 insertions(+)

diff --git a/modules/islandora_iiif/README.md b/modules/islandora_iiif/README.md
index c1f89872..7cbcc884 100644
--- a/modules/islandora_iiif/README.md
+++ b/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/).

From 565a1b42b9eb641b063b65ac9a522aa923259d18 Mon Sep 17 00:00:00 2001
From: Alexander O'Neill <alexander@born-digital.com>
Date: Wed, 18 Oct 2023 06:50:03 -0300
Subject: [PATCH 06/11] Islandora IIIF: Get image dimensions from field on
 media if they exist.:

---
 .../src/Plugin/views/style/IIIFManifest.php   | 79 ++++++++++++-------
 1 file changed, 51 insertions(+), 28 deletions(-)

diff --git a/modules/islandora_iiif/src/Plugin/views/style/IIIFManifest.php b/modules/islandora_iiif/src/Plugin/views/style/IIIFManifest.php
index d94f83e3..cc684069 100644
--- a/modules/islandora_iiif/src/Plugin/views/style/IIIFManifest.php
+++ b/modules/islandora_iiif/src/Plugin/views/style/IIIFManifest.php
@@ -11,7 +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;
@@ -68,6 +71,13 @@ class IIIFManifest extends StylePluginBase {
    */
   protected $serializer;
 
+  /**
+   * The IIIF Info service.
+   *
+   * @var IiifInfo
+   */
+  protected $iiifInfo;
+
   /**
    * The request service.
    *
@@ -113,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, IslandoraUtils $utils) {
+  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;
@@ -125,6 +135,7 @@ class IIIFManifest extends StylePluginBase {
     $this->messenger = $messenger;
     $this->moduleHandler = $moduleHandler;
     $this->utils = $utils;
+    $this->iiifInfo = $iiif_info;
   }
 
   /**
@@ -143,7 +154,8 @@ class IIIFManifest extends StylePluginBase {
       $container->get('http_client'),
       $container->get('messenger'),
       $container->get('module_handler'),
-      $container->get('islandora.utils')
+      $container->get('islandora.utils'),
+      $container->get('islandora_iiif')
     );
   }
 
@@ -334,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'];
+    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'])];
     }
-    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 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);
-          $image_size = getimagesize($path);
-          if ($image_size) {
-            $width = $image_size[0];
-            $height = $image_size[1];
-          }
+
+    $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.
+      $uri = $image->entity->getFileUri();
+      $path = $this->fileSystem->realpath($uri);
+      if (!empty($path)) {
+        $image_size = getimagesize($path);
+        if ($image_size) {
+          return [intval($image_size[0]),
+            intval($image_size[1])];
         }
       }
     }
-    return [$width, $height];
+
+    // 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 [0, 0];
   }
 
   /**

From 8c8de83e9ddb8f0f2bebffd9eaa88ef0b08e8e03 Mon Sep 17 00:00:00 2001
From: Alexander O'Neill <alexander@born-digital.com>
Date: Wed, 18 Oct 2023 20:35:01 -0300
Subject: [PATCH 07/11] Islandora IIIF: Address PHPCS errors.

---
 modules/islandora_iiif/islandora_iiif.install |  8 ++-
 .../Plugin/Action/MediaAttributesFromIiif.php | 62 +++++++++----------
 2 files changed, 34 insertions(+), 36 deletions(-)

diff --git a/modules/islandora_iiif/islandora_iiif.install b/modules/islandora_iiif/islandora_iiif.install
index 9b040735..6d79442c 100644
--- a/modules/islandora_iiif/islandora_iiif.install
+++ b/modules/islandora_iiif/islandora_iiif.install
@@ -5,12 +5,14 @@
  * Install/update hook implementations.
  */
 
- /**
+use Symfony\Component\Yaml\Yaml;
+
+/**
  * 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);
+  $config_path = \Drupal::service('extension.list.module')->getPath('islandora_iiif') . '/config/optional/' . $config_id . '.yml';
+  $data = Yaml::parseFile($config_path);
   \Drupal::configFactory()->getEditable($config_id)->setData($data)->save(TRUE);
 }
diff --git a/modules/islandora_iiif/src/Plugin/Action/MediaAttributesFromIiif.php b/modules/islandora_iiif/src/Plugin/Action/MediaAttributesFromIiif.php
index 9bb951e2..2cb5c06e 100644
--- a/modules/islandora_iiif/src/Plugin/Action/MediaAttributesFromIiif.php
+++ b/modules/islandora_iiif/src/Plugin/Action/MediaAttributesFromIiif.php
@@ -4,20 +4,15 @@ 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.
  *
@@ -30,18 +25,18 @@ use Symfony\Component\HttpFoundation\Request;
 class MediaAttributesFromIiif extends SaveAction {
 
   /**
-   * The HTTP client
+   * The HTTP client.
    *
-   * @var \GuzzleHttp\Client;
+   * @var \GuzzleHttp\Client
    */
   protected $httpClient;
 
-/**
- * The IIIF Info service.
- *
- * @var IiifInfo
- */
-protected $iiifInfo;
+  /**
+   * The IIIF Info service.
+   *
+   * @var \Drupal\islandora_iiif\IiifInfo
+   */
+  protected $iiifInfo;
 
   /**
    * The logger.
@@ -50,7 +45,7 @@ protected $iiifInfo;
    */
   protected $logger;
 
-    /**
+  /**
    * Islandora utility functions.
    *
    * @var \Drupal\islandora\IslandoraUtils
@@ -76,14 +71,15 @@ protected $iiifInfo;
    * @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.
+   *   The HTTP Client.
+   * @param \Drupal\islandora_iiif\IiifInfo $iiif_info
+   *   The IIIF INfo service.
+   * @param \Drupal\islandora\IslandoraUtils $islandora_utils
+   *   Islandora utility functions.
    * @param \Drupal\islandora\MediaSource\MediaSourceService $media_source
-   *   Media source service.
+   *   Islandora media service.
    * @param \Drupal\Core\Logger\LoggerChannelInterface $channel
    *   Logger channel.
    */
@@ -91,10 +87,10 @@ protected $iiifInfo;
     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;
+    $this->iiifInfo = $iiif_info;
+    $this->utils = $islandora_utils;
+    $this->mediaSource = $media_source;
+    $this->logger = $channel;
   }
 
   /**
@@ -118,7 +114,7 @@ $this->logger = $channel;
   /**
    * {@inheritdoc}
    */
-  public function execute($entity = NULL ) {
+  public function execute($entity = NULL) {
     $width = $height = FALSE;
 
     // Get the original File media use term.
@@ -130,15 +126,16 @@ $this->logger = $channel;
     $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) {
+      // 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
-        */
+        /**
+         * @var \Drupal\Media\MediaInterface  $original_file_media
+         */
         $original_file_media = $this->entityTypeManager->getStorage('media')->load($original_file_mid);
 
-        // Get the media MIME Type
+        // Get the media MIME Type.
         $original_file = $this->mediaSource->getSourceFile($original_file_media);
         $mime_type = $original_file->getMimeType();
 
@@ -146,7 +143,6 @@ $this->logger = $channel;
           [$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);
@@ -166,4 +162,4 @@ $this->logger = $channel;
     return $object->access('update', $account, $return_as_object);
   }
 
-  }
+}

From c1b41410eddd5cc1d51bc123b683de26e284a9e5 Mon Sep 17 00:00:00 2001
From: Alexander O'Neill <alexander@born-digital.com>
Date: Wed, 18 Oct 2023 21:06:36 -0300
Subject: [PATCH 08/11] Islandora IIIF: Address PHPCS errors.

---
 modules/islandora_iiif/src/IiifInfo.php       | 36 +++++++++---------
 .../src/Plugin/views/style/IIIFManifest.php   | 38 +++++++++----------
 2 files changed, 36 insertions(+), 38 deletions(-)

diff --git a/modules/islandora_iiif/src/IiifInfo.php b/modules/islandora_iiif/src/IiifInfo.php
index 7e1e7460..8cc66d4e 100644
--- a/modules/islandora_iiif/src/IiifInfo.php
+++ b/modules/islandora_iiif/src/IiifInfo.php
@@ -25,24 +25,24 @@ class IiifInfo {
 
 
   /**
-   * The HTTP client
+   * The HTTP client.
    *
-   * @var \GuzzleHttp\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
- */
+  /**
+   * JWT Auth provider service.
+   *
+   * @var \Drupal\jwt\Authentication\Provider\JwtAuth
+   */
   protected $jwtAuth;
 
   /**
@@ -52,23 +52,22 @@ class IiifInfo {
    */
   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.
+   *   The HTTP Client.
    * @param \Drupal\Core\Logger\LoggerChannelInterface $channel
    *   Logger channel.
    * @param \Drupal\jwt\Authentication\Provider\JwtAuth $jwt_auth
-   * The JWT auth provider.
+   *   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->iiifConfig = $this->configFactory->get('islandora_iiif.settings');
     $this->httpClient = $http_client;
     $this->logger = $channel;
     $this->jwtAuth = $jwt_auth;
@@ -76,10 +75,11 @@ class IiifInfo {
 
   /**
    * 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.
+   *   The absolute URL on the IIIF server.
    */
   public function baseUrl($image) {
 
@@ -101,7 +101,8 @@ class IiifInfo {
    *
    * @param \Drupal\File\FileInterface $file
    *   The image file.
-   * @return array|FALSE
+   *
+   * @return array|false
    *   The image dimensions in an array as [$width, $height]
    */
   public function getImageDimensions(FileInterface $file) {
@@ -109,9 +110,9 @@ class IiifInfo {
     try {
       $info_json = $this->httpClient->request('get', $iiif_url, [
         'headers' => [
-            'Authorization' => 'bearer ' . $this->jwtAuth->generateToken()
-            ]
-        ])->getBody();
+          'Authorization' => 'bearer ' . $this->jwtAuth->generateToken(),
+        ],
+      ])->getBody();
       $resource = json_decode($info_json, TRUE);
       $width = $resource['width'];
       $height = $resource['height'];
@@ -125,5 +126,4 @@ class IiifInfo {
     return FALSE;
   }
 
-
 }
diff --git a/modules/islandora_iiif/src/Plugin/views/style/IIIFManifest.php b/modules/islandora_iiif/src/Plugin/views/style/IIIFManifest.php
index cc684069..f5a06161 100644
--- a/modules/islandora_iiif/src/Plugin/views/style/IIIFManifest.php
+++ b/modules/islandora_iiif/src/Plugin/views/style/IIIFManifest.php
@@ -6,24 +6,19 @@ use Drupal\Core\Config\ImmutableConfig;
 use Drupal\Core\Entity\EntityInterface;
 use Drupal\Core\Entity\EntityTypeManagerInterface;
 use Drupal\Core\Extension\ModuleHandlerInterface;
-use Drupal\Core\File\FileSystemInterface;
 use Drupal\Core\Field\FieldItemInterface;
+use Drupal\Core\File\FileSystemInterface;
 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;
-use GuzzleHttp\Exception\ClientException;
-use GuzzleHttp\Exception\ConnectException;
-use GuzzleHttp\Exception\ServerException;
 use Symfony\Component\DependencyInjection\ContainerInterface;
-use Symfony\Component\Serializer\SerializerInterface;
 use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\Serializer\SerializerInterface;
 
 /**
  * Provide serializer format for IIIF Manifest.
@@ -74,7 +69,7 @@ class IIIFManifest extends StylePluginBase {
   /**
    * The IIIF Info service.
    *
-   * @var IiifInfo
+   * @var \Drupal\islandora_iiif\IiifInfo
    */
   protected $iiifInfo;
 
@@ -347,14 +342,16 @@ 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)];
+        intval($image->height),
+      ];
     }
 
     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'])];
+        intval($properties['height']),
+      ];
     }
 
     $entity = $image->entity;
@@ -363,8 +360,9 @@ class IIIFManifest extends StylePluginBase {
       && $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];
+      return [$entity->get('field_width')->value,
+        $entity->get('field_height')->value,
+      ];
     }
 
     if ($mime_type === 'image/tiff') {
@@ -376,7 +374,8 @@ class IIIFManifest extends StylePluginBase {
         $image_size = getimagesize($path);
         if ($image_size) {
           return [intval($image_size[0]),
-            intval($image_size[1])];
+            intval($image_size[1]),
+          ];
         }
       }
     }
@@ -398,8 +397,8 @@ class IIIFManifest extends StylePluginBase {
    *   The entity at the current row.
    * @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.
    */
@@ -412,7 +411,7 @@ class IIIFManifest extends StylePluginBase {
       $ocr_field_name = $ocrField->definition['field_name'];
       if (!is_null($ocr_field_name)) {
         $ocrs = $ocr_entity->{$ocr_field_name};
-        $ocr = isset($ocrs[0]) ? $ocrs[0] : FALSE;
+        $ocr = $ocrs[0] ?? FALSE;
         $ocr_url = $ocr->entity->createFileUrl(FALSE);
       }
     }
@@ -554,14 +553,13 @@ class IIIFManifest extends StylePluginBase {
 
   /**
    * Submit handler for options form.
+   *
    * Used to store the structured text media term by URL instead of Ttid.
    *
    * @param array $form
-   * The form.
+   *   The form.
    * @param \Drupal\Core\Form\FormStateInterface $form_state
-   * The form state object.
-   *
-   * @return void
+   *   The form state object.
    */
   public function submitOptionsForm(&$form, FormStateInterface $form_state) {
     $style_options = $form_state->getValue('style_options');

From 8151059d7c56507ffad4435bea7e83e828ae6288 Mon Sep 17 00:00:00 2001
From: Alexander O'Neill <alexander@born-digital.com>
Date: Wed, 18 Oct 2023 21:23:58 -0300
Subject: [PATCH 09/11] Islandora IIIF: Address PHPCS errors.

---
 modules/islandora_iiif/src/Plugin/views/style/IIIFManifest.php | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/modules/islandora_iiif/src/Plugin/views/style/IIIFManifest.php b/modules/islandora_iiif/src/Plugin/views/style/IIIFManifest.php
index f5a06161..b2ea900d 100644
--- a/modules/islandora_iiif/src/Plugin/views/style/IIIFManifest.php
+++ b/modules/islandora_iiif/src/Plugin/views/style/IIIFManifest.php
@@ -561,7 +561,9 @@ class IIIFManifest extends StylePluginBase {
    * @param \Drupal\Core\Form\FormStateInterface $form_state
    *   The form state object.
    */
+  // @codingStandardsIgnoreStart
   public function submitOptionsForm(&$form, FormStateInterface $form_state) {
+    // @codingStandardsIgnoreEnd
     $style_options = $form_state->getValue('style_options');
     $tid = $style_options['structured_text_term'];
     $term = $this->entityTypeManager->getStorage('taxonomy_term')->load($tid);

From 847fb4f3cf74f13cb821a4ed49ba3dd1c3061476 Mon Sep 17 00:00:00 2001
From: Alexander O'Neill <alexander@born-digital.com>
Date: Mon, 13 Nov 2023 17:21:28 -0400
Subject: [PATCH 10/11] islandora_iiif: Fix Authorization header syntax.

---
 modules/islandora_iiif/src/IiifInfo.php | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/modules/islandora_iiif/src/IiifInfo.php b/modules/islandora_iiif/src/IiifInfo.php
index 8cc66d4e..32ccedfa 100644
--- a/modules/islandora_iiif/src/IiifInfo.php
+++ b/modules/islandora_iiif/src/IiifInfo.php
@@ -110,7 +110,7 @@ class IiifInfo {
     try {
       $info_json = $this->httpClient->request('get', $iiif_url, [
         'headers' => [
-          'Authorization' => 'bearer ' . $this->jwtAuth->generateToken(),
+          'Authorization' => 'Bearer ' . $this->jwtAuth->generateToken(),
         ],
       ])->getBody();
       $resource = json_decode($info_json, TRUE);

From ec29a45bde5af814c1717c85238013c07495154b Mon Sep 17 00:00:00 2001
From: Alexander O'Neill <alexander@born-digital.com>
Date: Mon, 20 Nov 2023 01:22:12 -0400
Subject: [PATCH 11/11] 959-iiif-width-height-caching Islandora IIIF: Add
 search endpoint config to manifest.

---
 .../config/schema/islandora_iiif.schema.yml   | 12 ++++++++
 .../src/Plugin/views/style/IIIFManifest.php   | 28 +++++++++++++++++++
 2 files changed, 40 insertions(+)

diff --git a/modules/islandora_iiif/config/schema/islandora_iiif.schema.yml b/modules/islandora_iiif/config/schema/islandora_iiif.schema.yml
index f9e870ef..e02f443f 100644
--- a/modules/islandora_iiif/config/schema/islandora_iiif.schema.yml
+++ b/modules/islandora_iiif/config/schema/islandora_iiif.schema.yml
@@ -16,3 +16,15 @@ views.style.iiif_manifest:
       type: sequence
       sequence:
         type: string
+      label: "Tile source field(s)"
+    iiif_ocr_file_field:
+      type: sequence
+      sequence:
+        type: string:
+      label: "IIIF hOCR file field"
+    structured_text_term:
+      type: string
+      label: "Structured text term"
+    search_endpoint:
+      type: string
+      label: "Search endpoint path"
diff --git a/modules/islandora_iiif/src/Plugin/views/style/IIIFManifest.php b/modules/islandora_iiif/src/Plugin/views/style/IIIFManifest.php
index b2ea900d..77763458 100644
--- a/modules/islandora_iiif/src/Plugin/views/style/IIIFManifest.php
+++ b/modules/islandora_iiif/src/Plugin/views/style/IIIFManifest.php
@@ -215,6 +215,9 @@ class IIIFManifest extends StylePluginBase {
 
     $content_type = 'json';
 
+// Add a search endpoint if one is defined
+$this->addSearchEndpoint($json, $url_components);
+
     // Give other modules a chance to alter the manifest.
     $this->moduleHandler->alter('islandora_iiif_manifest', $json, $this);
 
@@ -459,6 +462,23 @@ class IIIFManifest extends StylePluginBase {
     return $entity_title;
   }
 
+  protected function addSearchEndpoint(array &$json, array $url_components) {
+    $url_base = $this->getRequest()->getSchemeAndHttpHost();
+    $hocr_search_path = $this->options['search_endpoint'];
+    $hocr_search_url = $url_base . '/' . ltrim($hocr_search_path, '/');
+
+    $hocr_search_url = str_replace('%node', $url_components[1], $hocr_search_url);
+
+    $json['service'][] = [
+          "@context" => "http://iiif.io/api/search/0/context.json",
+          "@id" => $hocr_search_url,
+          "profile" => "http://iiif.io/api/search/0/search",
+          "label" => t("Search inside this work"),
+    ];
+
+
+  }
+
   /**
    * {@inheritdoc}
    */
@@ -539,6 +559,14 @@ class IIIFManifest extends StylePluginBase {
       '#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.'),
     ];
+
+    $form['search_endpoint'] = [
+      '#type' => 'textfield',
+      '#title' => $this->t("Search endpoint path."),
+      '#description' => $this->t("If there is a search endpoint to search within the book that returns IIIF annotations, put it here. Use substitutions %node and %keywords.<br>E.g., paged-content-search/%node?search-in-pages=%keywords"),
+      '#default_value' => $this->options['search_endpoint'],
+      '#required' => FALSE,
+    ];
   }
 
   /**