1<?php
2namespace TYPO3\CMS\Core\Resource\OnlineMedia\Processing;
3
4/*
5 * This file is part of the TYPO3 CMS project.
6 *
7 * It is free software; you can redistribute it and/or modify it under
8 * the terms of the GNU General Public License, either version 2
9 * of the License, or any later version.
10 *
11 * For the full copyright and license information, please read the
12 * LICENSE.txt file that was distributed with this source code.
13 *
14 * The TYPO3 project - inspiring people to share!
15 */
16
17use TYPO3\CMS\Core\Core\Environment;
18use TYPO3\CMS\Core\Imaging\GraphicalFunctions;
19use TYPO3\CMS\Core\Imaging\ImageMagickFile;
20use TYPO3\CMS\Core\Resource\Driver\DriverInterface;
21use TYPO3\CMS\Core\Resource\File;
22use TYPO3\CMS\Core\Resource\OnlineMedia\Helpers\OnlineMediaHelperRegistry;
23use TYPO3\CMS\Core\Resource\ProcessedFile;
24use TYPO3\CMS\Core\Resource\ProcessedFileRepository;
25use TYPO3\CMS\Core\Resource\Processing\LocalImageProcessor;
26use TYPO3\CMS\Core\Resource\Service\FileProcessingService;
27use TYPO3\CMS\Core\Type\File\ImageInfo;
28use TYPO3\CMS\Core\Utility\CommandUtility;
29use TYPO3\CMS\Core\Utility\GeneralUtility;
30use TYPO3\CMS\Core\Utility\PathUtility;
31use TYPO3\CMS\Frontend\Imaging\GifBuilder;
32
33/**
34 * Preview of Online Media item Processing
35 */
36class PreviewProcessing
37{
38    /**
39     * @var LocalImageProcessor
40     */
41    protected $processor;
42
43    /**
44     * @param ProcessedFile $processedFile
45     * @return bool
46     */
47    protected function needsReprocessing($processedFile)
48    {
49        return $processedFile->isNew()
50            || (!$processedFile->usesOriginalFile() && !$processedFile->exists())
51            || $processedFile->isOutdated();
52    }
53
54    /**
55     * Process file
56     * Create static image preview for Online Media item when possible
57     *
58     * @param FileProcessingService $fileProcessingService
59     * @param DriverInterface $driver
60     * @param ProcessedFile $processedFile
61     * @param File $file
62     * @param string $taskType
63     * @param array $configuration
64     */
65    public function processFile(FileProcessingService $fileProcessingService, DriverInterface $driver, ProcessedFile $processedFile, File $file, $taskType, array $configuration)
66    {
67        if ($taskType !== ProcessedFile::CONTEXT_IMAGEPREVIEW && $taskType !== ProcessedFile::CONTEXT_IMAGECROPSCALEMASK) {
68            return;
69        }
70        // Check if processing is needed
71        if (!$this->needsReprocessing($processedFile)) {
72            return;
73        }
74        // Check if there is a OnlineMediaHelper registered for this file type
75        $helper = OnlineMediaHelperRegistry::getInstance()->getOnlineMediaHelper($file);
76        if ($helper === false) {
77            return;
78        }
79        // Check if helper provides a preview image
80        $temporaryFileName = $helper->getPreviewImage($file);
81        if (empty($temporaryFileName) || !file_exists($temporaryFileName)) {
82            return;
83        }
84        $temporaryFileNameForResizedThumb = uniqid(Environment::getVarPath() . '/transient/online_media_' . $file->getHashedIdentifier()) . '.jpg';
85        $configuration = $processedFile->getProcessingConfiguration();
86        switch ($taskType) {
87            case ProcessedFile::CONTEXT_IMAGEPREVIEW:
88                $this->resizeImage($temporaryFileName, $temporaryFileNameForResizedThumb, $configuration);
89                break;
90
91            case ProcessedFile::CONTEXT_IMAGECROPSCALEMASK:
92                $this->cropScaleImage($temporaryFileName, $temporaryFileNameForResizedThumb, $configuration);
93                break;
94        }
95        GeneralUtility::unlink_tempfile($temporaryFileName);
96        if (is_file($temporaryFileNameForResizedThumb)) {
97            $processedFile->setName($this->getTargetFileName($processedFile));
98            $imageInfo = GeneralUtility::makeInstance(ImageInfo::class, $temporaryFileNameForResizedThumb);
99            $processedFile->updateProperties(
100                [
101                    'width' => $imageInfo->getWidth(),
102                    'height' => $imageInfo->getHeight(),
103                    'size' => filesize($temporaryFileNameForResizedThumb),
104                    'checksum' => $processedFile->getTask()->getConfigurationChecksum()
105                ]
106            );
107            $processedFile->updateWithLocalFile($temporaryFileNameForResizedThumb);
108            GeneralUtility::unlink_tempfile($temporaryFileNameForResizedThumb);
109
110            /** @var ProcessedFileRepository $processedFileRepository */
111            $processedFileRepository = GeneralUtility::makeInstance(ProcessedFileRepository::class);
112            $processedFileRepository->add($processedFile);
113        }
114    }
115
116    /**
117     * @param ProcessedFile $processedFile
118     * @param string $prefix
119     * @return string
120     */
121    protected function getTargetFileName(ProcessedFile $processedFile, $prefix = 'preview_')
122    {
123        return $prefix . $processedFile->getTask()->getConfigurationChecksum() . '_' . $processedFile->getOriginalFile()->getNameWithoutExtension() . '.jpg';
124    }
125
126    /**
127     * @param string $originalFileName
128     * @param string $temporaryFileName
129     * @param array $configuration
130     */
131    protected function resizeImage($originalFileName, $temporaryFileName, $configuration)
132    {
133        // Create the temporary file
134        if (empty($GLOBALS['TYPO3_CONF_VARS']['GFX']['processor_enabled'])) {
135            return;
136        }
137
138        if (file_exists($originalFileName)) {
139            $arguments = CommandUtility::escapeShellArguments([
140                'width' => $configuration['width'],
141                'height' => $configuration['height'],
142            ]);
143            $parameters = '-sample ' . $arguments['width'] . 'x' . $arguments['height']
144                . ' ' . ImageMagickFile::fromFilePath($originalFileName, 0)
145                . ' ' . CommandUtility::escapeShellArgument($temporaryFileName);
146
147            $cmd = CommandUtility::imageMagickCommand('convert', $parameters) . ' 2>&1';
148            CommandUtility::exec($cmd);
149        }
150
151        if (!file_exists($temporaryFileName)) {
152            // Create a error image
153            $graphicalFunctions = $this->getGraphicalFunctionsObject();
154            $graphicalFunctions->getTemporaryImageWithText($temporaryFileName, 'No thumb', 'generated!', PathUtility::basename($originalFileName));
155        }
156    }
157
158    /**
159     * cropScaleImage
160     *
161     * @param string $originalFileName
162     * @param string $temporaryFileName
163     * @param array $configuration
164     */
165    protected function cropScaleImage($originalFileName, $temporaryFileName, $configuration)
166    {
167        if (file_exists($originalFileName)) {
168            $gifBuilder = GeneralUtility::makeInstance(GifBuilder::class);
169
170            $options = $this->getConfigurationForImageCropScaleMask($configuration, $gifBuilder);
171            $info = $gifBuilder->getImageDimensions($originalFileName);
172            $data = $gifBuilder->getImageScale($info, $configuration['width'], $configuration['height'], $options);
173
174            $info[0] = $data[0];
175            $info[1] = $data[1];
176            $frame = '';
177            $params = $gifBuilder->cmds['jpg'];
178
179            // Cropscaling:
180            if ($data['crs']) {
181                if (!$data['origW']) {
182                    $data['origW'] = $data[0];
183                }
184                if (!$data['origH']) {
185                    $data['origH'] = $data[1];
186                }
187                $offsetX = (int)(($data[0] - $data['origW']) * ($data['cropH'] + 100) / 200);
188                $offsetY = (int)(($data[1] - $data['origH']) * ($data['cropV'] + 100) / 200);
189                $params .= ' -crop ' . $data['origW'] . 'x' . $data['origH'] . '+' . $offsetX . '+' . $offsetY . '! ';
190            }
191            $command = $gifBuilder->scalecmd . ' ' . $info[0] . 'x' . $info[1] . '! ' . $params . ' ';
192            $gifBuilder->imageMagickExec($originalFileName, $temporaryFileName, $command, $frame);
193        }
194        if (!file_exists($temporaryFileName)) {
195            // Create a error image
196            $graphicalFunctions = $this->getGraphicalFunctionsObject();
197            $graphicalFunctions->getTemporaryImageWithText($temporaryFileName, 'No thumb', 'generated!', PathUtility::basename($originalFileName));
198        }
199    }
200
201    /**
202     * Get configuration for ImageCropScaleMask processing
203     *
204     * @param array $configuration
205     * @param GifBuilder $gifBuilder
206     * @return array
207     */
208    protected function getConfigurationForImageCropScaleMask(array $configuration, GifBuilder $gifBuilder)
209    {
210        if (!empty($configuration['useSample'])) {
211            $gifBuilder->scalecmd = '-sample';
212        }
213        $options = [];
214        if (!empty($configuration['maxWidth'])) {
215            $options['maxW'] = $configuration['maxWidth'];
216        }
217        if (!empty($configuration['maxHeight'])) {
218            $options['maxH'] = $configuration['maxHeight'];
219        }
220        if (!empty($configuration['minWidth'])) {
221            $options['minW'] = $configuration['minWidth'];
222        }
223        if (!empty($configuration['minHeight'])) {
224            $options['minH'] = $configuration['minHeight'];
225        }
226
227        $options['noScale'] = $configuration['noScale'];
228
229        return $options;
230    }
231
232    /**
233     * @return LocalImageProcessor
234     */
235    protected function getProcessor()
236    {
237        if (!$this->processor) {
238            $this->processor = GeneralUtility::makeInstance(LocalImageProcessor::class);
239        }
240        return $this->processor;
241    }
242
243    /**
244     * @return GraphicalFunctions
245     */
246    protected function getGraphicalFunctionsObject(): GraphicalFunctions
247    {
248        return GeneralUtility::makeInstance(GraphicalFunctions::class);
249    }
250}
251