You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

403 lines
18 KiB

  1. <?php
  2. /**
  3. * (c) Kitodo. Key to digital objects e.V. <contact@kitodo.org>
  4. *
  5. * This file is part of the Kitodo and TYPO3 projects.
  6. *
  7. * @license GNU General Public License version 3 or later.
  8. * For the full copyright and license information, please read the
  9. * LICENSE.txt file that was distributed with this source code.
  10. */
  11. namespace Kitodo\Dlf\Plugin;
  12. use Kitodo\Dlf\Common\Helper;
  13. use Kitodo\Dlf\Common\IiifManifest;
  14. use TYPO3\CMS\Core\Page\PageRenderer;
  15. use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
  16. use TYPO3\CMS\Core\Utility\GeneralUtility;
  17. use TYPO3\CMS\Core\Utility\MathUtility;
  18. use TYPO3\CMS\Core\Utility\PathUtility;
  19. use Ubl\Iiif\Presentation\Common\Model\Resources\ManifestInterface;
  20. use Ubl\Iiif\Presentation\Common\Vocabulary\Motivation;
  21. /**
  22. * Plugin 'Page View' for the 'dlf' extension
  23. *
  24. * @author Sebastian Meyer <sebastian.meyer@slub-dresden.de>
  25. * @package TYPO3
  26. * @subpackage dlf
  27. * @access public
  28. */
  29. class PageView extends \Kitodo\Dlf\Common\AbstractPlugin
  30. {
  31. public $scriptRelPath = 'Classes/Plugin/PageView.php';
  32. /**
  33. * Holds the controls to add to the map
  34. *
  35. * @var array
  36. * @access protected
  37. */
  38. protected $controls = [];
  39. /**
  40. * Holds the current images' URLs and MIME types
  41. *
  42. * @var array
  43. * @access protected
  44. */
  45. protected $images = [];
  46. /**
  47. * Holds the current fulltexts' URLs
  48. *
  49. * @var array
  50. * @access protected
  51. */
  52. protected $fulltexts = [];
  53. /**
  54. * Holds the current AnnotationLists / AnnotationPages
  55. *
  56. * @var array
  57. * @access protected
  58. */
  59. protected $annotationContainers = [];
  60. /**
  61. * Adds Viewer javascript
  62. *
  63. * @access protected
  64. *
  65. * @return string The output string for the ###JAVASCRIPT### template marker
  66. */
  67. protected function addViewerJS()
  68. {
  69. $markerArray = '';
  70. // CSS files.
  71. $cssFiles = [
  72. 'Resources/Public/Javascript/OpenLayers/ol3.css'
  73. ];
  74. // Javascript files.
  75. $jsFiles = [
  76. // OpenLayers
  77. 'Resources/Public/Javascript/OpenLayers/glif.min.js',
  78. 'Resources/Public/Javascript/OpenLayers/ol3-dlf.js',
  79. // Viewer
  80. 'Resources/Public/Javascript/PageView/Utility.js',
  81. 'Resources/Public/Javascript/PageView/OL3.js',
  82. 'Resources/Public/Javascript/PageView/OL3Styles.js',
  83. 'Resources/Public/Javascript/PageView/OL3Sources.js',
  84. 'Resources/Public/Javascript/PageView/AltoParser.js',
  85. 'Resources/Public/Javascript/PageView/AnnotationParser.js',
  86. 'Resources/Public/Javascript/PageView/AnnotationControl.js',
  87. 'Resources/Public/Javascript/PageView/ImageManipulationControl.js',
  88. 'Resources/Public/Javascript/PageView/FulltextDownloadControl.js',
  89. 'Resources/Public/Javascript/PageView/FulltextControl.js',
  90. 'Resources/Public/Javascript/PageView/FullTextUtility.js',
  91. 'Resources/Public/Javascript/PageView/PageView.js'
  92. ];
  93. // Viewer configuration.
  94. $viewerConfiguration = '
  95. $(document).ready(function() {
  96. if (dlfUtils.exists(dlfViewer)) {
  97. tx_dlf_viewer = new dlfViewer({
  98. controls: ["' . implode('", "', $this->controls) . '"],
  99. div: "' . $this->conf['elementId'] . '",
  100. images: ' . json_encode($this->images) . ',
  101. fulltexts: ' . json_encode($this->fulltexts) . ',
  102. annotationContainers: ' . json_encode($this->annotationContainers) . ',
  103. useInternalProxy: ' . ($this->conf['useInternalProxy'] ? 1 : 0) . '
  104. });
  105. }
  106. });
  107. ';
  108. // Add Javascript to page footer if not configured otherwise.
  109. if (empty($this->conf['addJStoBody'])) {
  110. $pageRenderer = GeneralUtility::makeInstance(PageRenderer::class);
  111. foreach ($cssFiles as $cssFile) {
  112. $pageRenderer->addCssFile(PathUtility::stripPathSitePrefix(ExtensionManagementUtility::extPath($this->extKey)) . $cssFile);
  113. }
  114. foreach ($jsFiles as $jsFile) {
  115. $pageRenderer->addJsFooterFile(PathUtility::stripPathSitePrefix(ExtensionManagementUtility::extPath($this->extKey)) . $jsFile);
  116. }
  117. $pageRenderer->addJsFooterInlineCode('kitodo-pageview-configuration', $viewerConfiguration);
  118. } else {
  119. foreach ($jsFiles as $jsFile) {
  120. $markerArray .= '<script type="text/javascript" src="' . PathUtility::stripPathSitePrefix(ExtensionManagementUtility::extPath($this->extKey)) . $jsFile . '"></script>' . "\n";
  121. }
  122. $markerArray .= '
  123. <script type="text/javascript">
  124. /*<![CDATA[*/
  125. /*kitodo-pageview-configuration*/
  126. ' . $viewerConfiguration . '
  127. /*]]>*/
  128. </script>';
  129. }
  130. return $markerArray;
  131. }
  132. /**
  133. * Adds pageview interaction (crop, magnifier and rotation)
  134. *
  135. * @access protected
  136. *
  137. * @return array Marker array
  138. */
  139. protected function addInteraction()
  140. {
  141. $markerArray = [];
  142. if ($this->piVars['id']) {
  143. if ($this->conf['crop']) {
  144. $markerArray['###EDITBUTTON###'] = '<a href="javascript: tx_dlf_viewer.activateSelection();" title="' . htmlspecialchars($this->pi_getLL('editMode', '')) . '">' . htmlspecialchars($this->pi_getLL('editMode', '')) . '</a>';
  145. $markerArray['###EDITREMOVE###'] = '<a href="javascript: tx_dlf_viewer.resetCropSelection();" title="' . htmlspecialchars($this->pi_getLL('editRemove', '')) . '">' . htmlspecialchars($this->pi_getLL('editRemove', '')) . '</a>';
  146. } else {
  147. $markerArray['###EDITBUTTON###'] = '';
  148. $markerArray['###EDITREMOVE###'] = '';
  149. }
  150. if ($this->conf['magnifier']) {
  151. $markerArray['###MAGNIFIER###'] = '<a href="javascript: tx_dlf_viewer.activateMagnifier();" title="' . htmlspecialchars($this->pi_getLL('magnifier', '')) . '">' . htmlspecialchars($this->pi_getLL('magnifier', '')) . '</a>';
  152. } else {
  153. $markerArray['###MAGNIFIER###'] = '';
  154. }
  155. }
  156. return $markerArray;
  157. }
  158. /**
  159. * Adds form to save cropping data to basket
  160. *
  161. * @access protected
  162. *
  163. * @return array Marker array
  164. */
  165. protected function addBasketForm()
  166. {
  167. $markerArray = [];
  168. // Add basket button
  169. if ($this->conf['basketButton'] && $this->conf['targetBasket'] && $this->piVars['id']) {
  170. $label = htmlspecialchars($this->pi_getLL('addBasket', ''));
  171. $params = [
  172. 'id' => $this->piVars['id'],
  173. 'addToBasket' => true
  174. ];
  175. if (empty($this->piVars['page'])) {
  176. $params['page'] = 1;
  177. }
  178. $basketConf = [
  179. 'parameter' => $this->conf['targetBasket'],
  180. 'forceAbsoluteUrl' => !empty($this->conf['forceAbsoluteUrl']) ? 1 : 0,
  181. 'forceAbsoluteUrl.' => ['scheme' => !empty($this->conf['forceAbsoluteUrl']) && !empty($this->conf['forceAbsoluteUrlHttps']) ? 'https' : 'http'],
  182. 'additionalParams' => GeneralUtility::implodeArrayForUrl($this->prefixId, $params, '', true, false),
  183. 'title' => $label
  184. ];
  185. $output = '<form id="addToBasketForm" action="' . $this->cObj->typoLink_URL($basketConf) . '" method="post">';
  186. $output .= '<input type="hidden" name="tx_dlf[startpage]" id="startpage" value="' . htmlspecialchars($this->piVars['page']) . '">';
  187. $output .= '<input type="hidden" name="tx_dlf[endpage]" id="endpage" value="' . htmlspecialchars($this->piVars['page']) . '">';
  188. $output .= '<input type="hidden" name="tx_dlf[startX]" id="startX">';
  189. $output .= '<input type="hidden" name="tx_dlf[startY]" id="startY">';
  190. $output .= '<input type="hidden" name="tx_dlf[endX]" id="endX">';
  191. $output .= '<input type="hidden" name="tx_dlf[endY]" id="endY">';
  192. $output .= '<input type="hidden" name="tx_dlf[rotation]" id="rotation">';
  193. $output .= '<button id="submitBasketForm" onclick="this.form.submit()">' . $label . '</button>';
  194. $output .= '</form>';
  195. $output .= '<script>';
  196. $output .= '$(document).ready(function() { $("#submitBasketForm").click(function() { $("#addToBasketForm").submit(); }); });';
  197. $output .= '</script>';
  198. $markerArray['###BASKETBUTTON###'] = $output;
  199. } else {
  200. $markerArray['###BASKETBUTTON###'] = '';
  201. }
  202. return $markerArray;
  203. }
  204. /**
  205. * Get image's URL and MIME type
  206. *
  207. * @access protected
  208. *
  209. * @param int $page: Page number
  210. *
  211. * @return array URL and MIME type of image file
  212. */
  213. protected function getImage($page)
  214. {
  215. $image = [];
  216. // Get @USE value of METS fileGrp.
  217. $fileGrpsImages = GeneralUtility::trimExplode(',', $this->conf['fileGrpImages']);
  218. while ($fileGrpImages = array_pop($fileGrpsImages)) {
  219. // Get image link.
  220. if (!empty($this->doc->physicalStructureInfo[$this->doc->physicalStructure[$page]]['files'][$fileGrpImages])) {
  221. $image['url'] = $this->doc->getFileLocation($this->doc->physicalStructureInfo[$this->doc->physicalStructure[$page]]['files'][$fileGrpImages]);
  222. if ($this->conf['useInternalProxy']) {
  223. // Configure @action URL for form.
  224. $linkConf = [
  225. 'parameter' => $GLOBALS['TSFE']->id,
  226. 'forceAbsoluteUrl' => !empty($this->conf['forceAbsoluteUrl']) ? 1 : 0,
  227. 'forceAbsoluteUrl.' => ['scheme' => !empty($this->conf['forceAbsoluteUrl']) && !empty($this->conf['forceAbsoluteUrlHttps']) ? 'https' : 'http'],
  228. 'additionalParams' => '&eID=tx_dlf_pageview_proxy&url=' . urlencode($image['url']),
  229. ];
  230. $image['url'] = $this->cObj->typoLink_URL($linkConf);
  231. }
  232. $image['mimetype'] = $this->doc->getFileMimeType($this->doc->physicalStructureInfo[$this->doc->physicalStructure[$page]]['files'][$fileGrpImages]);
  233. break;
  234. } else {
  235. Helper::devLog('File not found in fileGrp "' . $fileGrpImages . '"', DEVLOG_SEVERITY_WARNING);
  236. }
  237. }
  238. return $image;
  239. }
  240. /**
  241. * Get fulltext URL and MIME type
  242. *
  243. * @access protected
  244. *
  245. * @param int $page: Page number
  246. *
  247. * @return array URL and MIME type of fulltext file
  248. */
  249. protected function getFulltext($page)
  250. {
  251. $fulltext = [];
  252. // Get fulltext link.
  253. $fileGrpsFulltext = GeneralUtility::trimExplode(',', $this->conf['fileGrpFulltext']);
  254. while ($fileGrpFulltext = array_shift($fileGrpsFulltext)) {
  255. if (!empty($this->doc->physicalStructureInfo[$this->doc->physicalStructure[$page]]['files'][$fileGrpFulltext])) {
  256. $fulltext['url'] = $this->doc->getFileLocation($this->doc->physicalStructureInfo[$this->doc->physicalStructure[$page]]['files'][$fileGrpFulltext]);
  257. if ($this->conf['useInternalProxy']) {
  258. // Configure @action URL for form.
  259. $linkConf = [
  260. 'parameter' => $GLOBALS['TSFE']->id,
  261. 'forceAbsoluteUrl' => !empty($this->conf['forceAbsoluteUrl']) ? 1 : 0,
  262. 'forceAbsoluteUrl.' => ['scheme' => !empty($this->conf['forceAbsoluteUrl']) && !empty($this->conf['forceAbsoluteUrlHttps']) ? 'https' : 'http'],
  263. 'additionalParams' => '&eID=tx_dlf_pageview_proxy&url=' . urlencode($fulltext['url']),
  264. ];
  265. $fulltext['url'] = $this->cObj->typoLink_URL($linkConf);
  266. }
  267. $fulltext['mimetype'] = $this->doc->getFileMimeType($this->doc->physicalStructureInfo[$this->doc->physicalStructure[$page]]['files'][$fileGrpFulltext]);
  268. break;
  269. }
  270. }
  271. if (empty($fulltext)) {
  272. Helper::devLog('File not found in fileGrp "' . $this->conf['fileGrpFulltext'] . '"', DEVLOG_SEVERITY_WARNING);
  273. }
  274. return $fulltext;
  275. }
  276. /**
  277. * Get all AnnotationPages / AnnotationLists that contain text Annotations with motivation "painting"
  278. *
  279. * @access protected
  280. *
  281. * @param int $page: Page number
  282. * @return array An array containing the IRIs of the AnnotationLists / AnnotationPages as well as
  283. * some information about the canvas.
  284. */
  285. protected function getAnnotationContainers($page)
  286. {
  287. if ($this->doc instanceof IiifManifest) {
  288. $canvasId = $this->doc->physicalStructure[$page];
  289. $iiif = $this->doc->getIiif();
  290. if ($iiif instanceof ManifestInterface) {
  291. $canvas = $iiif->getContainedResourceById($canvasId);
  292. /* @var $canvas \Ubl\Iiif\Presentation\Common\Model\Resources\CanvasInterface */
  293. if ($canvas != null && !empty($canvas->getPossibleTextAnnotationContainers(Motivation::PAINTING))) {
  294. $annotationContainers = [];
  295. /*
  296. * TODO Analyzing the annotations on the server side requires loading the annotation lists / pages
  297. * just to determine wether they contain text annotations for painting. This will take time and lead to a bad user experience.
  298. * It would be better to link every annotation and analyze the data on the client side.
  299. *
  300. * On the other hand, server connections are potentially better than client connections. Downloading annotation lists
  301. */
  302. foreach ($canvas->getPossibleTextAnnotationContainers(Motivation::PAINTING) as $annotationContainer) {
  303. if (($textAnnotations = $annotationContainer->getTextAnnotations(Motivation::PAINTING)) != null) {
  304. foreach ($textAnnotations as $annotation) {
  305. if (
  306. $annotation->getBody()->getFormat() == 'text/plain'
  307. && $annotation->getBody()->getChars() != null
  308. ) {
  309. $annotationListData = [];
  310. $annotationListData['uri'] = $annotationContainer->getId();
  311. $annotationListData['label'] = $annotationContainer->getLabelForDisplay();
  312. $annotationContainers[] = $annotationListData;
  313. break;
  314. }
  315. }
  316. }
  317. }
  318. $result = [
  319. 'canvas' => [
  320. 'id' => $canvas->getId(),
  321. 'width' => $canvas->getWidth(),
  322. 'height' => $canvas->getHeight(),
  323. ],
  324. 'annotationContainers' => $annotationContainers
  325. ];
  326. return $result;
  327. }
  328. }
  329. }
  330. return [];
  331. }
  332. /**
  333. * The main method of the PlugIn
  334. *
  335. * @access public
  336. *
  337. * @param string $content: The PlugIn content
  338. * @param array $conf: The PlugIn configuration
  339. *
  340. * @return string The content that is displayed on the website
  341. */
  342. public function main($content, $conf)
  343. {
  344. $this->init($conf);
  345. // Load current document.
  346. $this->loadDocument();
  347. if (
  348. $this->doc === null
  349. || $this->doc->numPages < 1
  350. ) {
  351. // Quit without doing anything if required variables are not set.
  352. return $content;
  353. } else {
  354. if (!empty($this->piVars['logicalPage'])) {
  355. $this->piVars['page'] = $this->doc->getPhysicalPage($this->piVars['logicalPage']);
  356. // The logical page parameter should not appear again
  357. unset($this->piVars['logicalPage']);
  358. }
  359. // Set default values if not set.
  360. // $this->piVars['page'] may be integer or string (physical structure @ID)
  361. if ((int) $this->piVars['page'] > 0 || empty($this->piVars['page'])) {
  362. $this->piVars['page'] = MathUtility::forceIntegerInRange((int) $this->piVars['page'], 1, $this->doc->numPages, 1);
  363. } else {
  364. $this->piVars['page'] = array_search($this->piVars['page'], $this->doc->physicalStructure);
  365. }
  366. $this->piVars['double'] = MathUtility::forceIntegerInRange($this->piVars['double'], 0, 1, 0);
  367. }
  368. // Load template file.
  369. $this->getTemplate();
  370. // Get image data.
  371. $this->images[0] = $this->getImage($this->piVars['page']);
  372. $this->fulltexts[0] = $this->getFulltext($this->piVars['page']);
  373. $this->annotationContainers[0] = $this->getAnnotationContainers($this->piVars['page']);
  374. if ($this->piVars['double'] && $this->piVars['page'] < $this->doc->numPages) {
  375. $this->images[1] = $this->getImage($this->piVars['page'] + 1);
  376. $this->fulltexts[1] = $this->getFulltext($this->piVars['page'] + 1);
  377. $this->annotationContainers[1] = $this->getAnnotationContainers($this->piVars['page'] + 1);
  378. }
  379. // Get the controls for the map.
  380. $this->controls = explode(',', $this->conf['features']);
  381. // Fill in the template markers.
  382. $markerArray = array_merge($this->addInteraction(), $this->addBasketForm());
  383. $markerArray['###JAVASCRIPT###'] = $this->addViewerJS();
  384. $content .= $this->templateService->substituteMarkerArray($this->template, $markerArray);
  385. return $this->pi_wrapInBaseClass($content);
  386. }
  387. }