TYPO3 v15 dev-main snapshot ()
This commit is contained in:
@@ -0,0 +1,184 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Resource\Processing;
|
||||
|
||||
use TYPO3\CMS\Core\Resource;
|
||||
use TYPO3\CMS\Core\Resource\ProcessedFile;
|
||||
use TYPO3\CMS\Core\Resource\Service\ConfigurationService;
|
||||
use TYPO3\CMS\Core\Utility\MathUtility;
|
||||
|
||||
/**
|
||||
* Abstract base implementation of a processing task.
|
||||
*/
|
||||
abstract class AbstractTask implements TaskInterface
|
||||
{
|
||||
protected Resource\File $sourceFile;
|
||||
protected bool $executed = false;
|
||||
protected bool $successful;
|
||||
|
||||
public function __construct(
|
||||
protected ProcessedFile $targetFile,
|
||||
protected array $configuration
|
||||
) {
|
||||
$this->sourceFile = $targetFile->getOriginalFile();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets parameters needed in the checksum. Can be overridden to add additional parameters to the checksum.
|
||||
* This should include all parameters that could possibly vary between different task instances, e.g. the
|
||||
* TYPO3 image configuration in TYPO3_CONF_VARS[GFX] for graphic processing tasks.
|
||||
*/
|
||||
protected function getChecksumData(): array
|
||||
{
|
||||
return [
|
||||
$this->getSourceFile()->getUid(),
|
||||
$this->getType() . '.' . $this->getName() . $this->getSourceFile()->getModificationTime(),
|
||||
(new ConfigurationService())->serialize($this->configuration),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the checksum for this task's configuration, also taking the file and task type into account.
|
||||
*/
|
||||
public function getConfigurationChecksum(): string
|
||||
{
|
||||
return substr((string)md5(implode('|', $this->getChecksumData())), 0, 10);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the filename
|
||||
*/
|
||||
public function getTargetFilename(): string
|
||||
{
|
||||
return $this->targetFile->getNameWithoutExtension()
|
||||
. '_' . $this->getConfigurationChecksum()
|
||||
. '.' . $this->getTargetFileExtension();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the file extension the processed file should
|
||||
* have in the filesystem.
|
||||
*/
|
||||
public function getTargetFileExtension(): string
|
||||
{
|
||||
return $this->targetFile->getExtension();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the name of this task
|
||||
*/
|
||||
abstract public function getName(): string;
|
||||
|
||||
/**
|
||||
* Returns the type of this task
|
||||
*/
|
||||
abstract public function getType(): string;
|
||||
|
||||
public function getTargetFile(): Resource\ProcessedFile
|
||||
{
|
||||
return $this->targetFile;
|
||||
}
|
||||
|
||||
public function getSourceFile(): Resource\File
|
||||
{
|
||||
return $this->sourceFile;
|
||||
}
|
||||
|
||||
public function getConfiguration(): array
|
||||
{
|
||||
return $this->configuration;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns TRUE if this task has been executed, no matter if the execution was successful.
|
||||
*/
|
||||
public function isExecuted(): bool
|
||||
{
|
||||
return $this->executed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set this task executed. This is used by the Processors in order to transfer the state of this task to
|
||||
* the file processing service.
|
||||
*
|
||||
* @param bool $successful Set this to FALSE if executing the task failed
|
||||
*/
|
||||
public function setExecuted(bool $successful): void
|
||||
{
|
||||
$this->executed = true;
|
||||
$this->successful = $successful;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns TRUE if this task has been successfully executed. Only call this method if the task has been processed
|
||||
* at all.
|
||||
*
|
||||
* @throws \LogicException If the task has not been executed already
|
||||
*/
|
||||
public function isSuccessful(): bool
|
||||
{
|
||||
if (!$this->executed) {
|
||||
throw new \LogicException('Task has not been executed; cannot determine success.', 1352549235);
|
||||
}
|
||||
return $this->successful;
|
||||
}
|
||||
|
||||
/**
|
||||
* We only have to trigger the file processing if the file either is new, does not exist or the
|
||||
* original file has changed since the last processing run (the last case has to trigger a reprocessing
|
||||
* even if the original file was used until now).
|
||||
*/
|
||||
public function fileNeedsProcessing(): bool
|
||||
{
|
||||
$processedFile = $this->getTargetFile();
|
||||
if (!$processedFile->isProcessed()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$checksum = $this->getTargetFile()->getProperty('checksum');
|
||||
$checksumCalculationOk = !$checksum || $this->getConfigurationChecksum() === $checksum;
|
||||
|
||||
$fileNeedsReprocessing = $processedFile->isNew()
|
||||
|| (!$processedFile->usesOriginalFile() && !$processedFile->exists())
|
||||
|| ($processedFile->needsReprocessing() || !$checksumCalculationOk);
|
||||
|
||||
if ($fileNeedsReprocessing && $this->getTargetFile()->exists()) {
|
||||
$this->getTargetFile()->delete();
|
||||
}
|
||||
|
||||
return $fileNeedsReprocessing;
|
||||
}
|
||||
|
||||
/**
|
||||
* Can be extended in the actual subclasses, but be careful on what to sanitize, as Processors might need
|
||||
* information that you actually throw away.
|
||||
*
|
||||
* Ensure that the processing configuration which is part of the hash sum is properly cast, so
|
||||
* unnecessary duplicate images are not produced, see #80942
|
||||
*/
|
||||
public function sanitizeConfiguration(): void
|
||||
{
|
||||
foreach ($this->configuration as &$value) {
|
||||
if (MathUtility::canBeInterpretedAsInteger($value)) {
|
||||
$value = (int)$value;
|
||||
}
|
||||
}
|
||||
// @todo: ideally we would do a sort() on the array to really structure this, but then the checksums would change
|
||||
// @todo: and we would need to re-create all processed files again, but this would be something we should tackle at some point
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Resource\Processing;
|
||||
|
||||
use TYPO3\CMS\Core\Attribute\AsEventListener;
|
||||
use TYPO3\CMS\Core\Database\ConnectionPool;
|
||||
use TYPO3\CMS\Core\Resource\Event\AfterFileAddedEvent;
|
||||
use TYPO3\CMS\Core\Resource\Event\AfterFileDeletedEvent;
|
||||
use TYPO3\CMS\Core\Resource\Event\AfterFileReplacedEvent;
|
||||
use TYPO3\CMS\Core\Resource\File;
|
||||
use TYPO3\CMS\Core\Resource\FileInterface;
|
||||
use TYPO3\CMS\Core\Resource\Index\FileIndexRepository;
|
||||
use TYPO3\CMS\Core\Resource\Index\MetaDataRepository;
|
||||
use TYPO3\CMS\Core\Resource\ProcessedFile;
|
||||
use TYPO3\CMS\Core\Resource\ProcessedFileRepository;
|
||||
|
||||
/**
|
||||
* Clean up database records, processed files and file references
|
||||
*
|
||||
* The aspect which deals with deleted files is a list of PSR-14
|
||||
* event listeners which react on file deletion.
|
||||
*
|
||||
* @internal this is a list of Event Listeners, and not part of TYPO3 Core API.
|
||||
*/
|
||||
final readonly class FileDeletionAspect
|
||||
{
|
||||
public function __construct(
|
||||
private ConnectionPool $connectionPool,
|
||||
private MetaDataRepository $metaDataRepository,
|
||||
private ProcessedFileRepository $processedFileRepository,
|
||||
private FileIndexRepository $fileIndexRepository,
|
||||
) {}
|
||||
|
||||
#[AsEventListener('delete-processed-files-after-add')]
|
||||
public function cleanupProcessedFilesPostFileAdd(AfterFileAddedEvent $event): void
|
||||
{
|
||||
$this->cleanupProcessedFiles($event->getFile());
|
||||
}
|
||||
|
||||
#[AsEventListener('delete-processed-files-after-replace')]
|
||||
public function cleanupProcessedFilesPostFileReplace(AfterFileReplacedEvent $event): void
|
||||
{
|
||||
$this->cleanupProcessedFiles($event->getFile());
|
||||
}
|
||||
|
||||
#[AsEventListener('delete-processed-files-after-delete')]
|
||||
public function removeFromRepositoryAfterFileDeleted(AfterFileDeletedEvent $event): void
|
||||
{
|
||||
$this->removeFromRepository($event->getFile());
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup database record for a deleted file
|
||||
*/
|
||||
private function removeFromRepository(FileInterface $fileObject): void
|
||||
{
|
||||
// remove file from repository
|
||||
if ($fileObject instanceof File) {
|
||||
$this->cleanupProcessedFiles($fileObject);
|
||||
$this->cleanupCategoryReferences($fileObject);
|
||||
$this->fileIndexRepository->remove($fileObject->getUid());
|
||||
$this->metaDataRepository->removeByFileUid($fileObject->getUid());
|
||||
|
||||
// remove all references
|
||||
$this->connectionPool->getConnectionForTable('sys_file_reference')->delete(
|
||||
'sys_file_reference',
|
||||
[
|
||||
'uid_local' => $fileObject->getUid(),
|
||||
]
|
||||
);
|
||||
} elseif ($fileObject instanceof ProcessedFile) {
|
||||
$this->processedFileRepository->remove($fileObject);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all category references of the deleted file.
|
||||
*/
|
||||
private function cleanupCategoryReferences(File $fileObject): void
|
||||
{
|
||||
// Retrieve the file metadata uid which is different from the file uid.
|
||||
$metadataProperties = $fileObject->getMetaData()->get();
|
||||
$metaDataUid = (int)($metadataProperties['_ORIG_uid'] ?? $metadataProperties['uid'] ?? 0);
|
||||
|
||||
if ($metaDataUid <= 0) {
|
||||
// No metadata record exists for the given file. The file might not
|
||||
// have been indexed or the metadata record was deleted manually.
|
||||
return;
|
||||
}
|
||||
|
||||
$this->connectionPool->getConnectionForTable('sys_category_record_mm')->delete(
|
||||
'sys_category_record_mm',
|
||||
[
|
||||
'uid_foreign' => $metaDataUid,
|
||||
'tablenames' => 'sys_file_metadata',
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all processed files that belong to the given File object
|
||||
*/
|
||||
private function cleanupProcessedFiles(FileInterface $fileObject): void
|
||||
{
|
||||
// only delete processed files of File objects
|
||||
if (!$fileObject instanceof File) {
|
||||
return;
|
||||
}
|
||||
foreach ($this->processedFileRepository->findAllByOriginalFile($fileObject) as $processedFile) {
|
||||
if ($processedFile->exists()) {
|
||||
$processedFile->delete(true);
|
||||
}
|
||||
$this->removeFromRepository($processedFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Resource\Processing;
|
||||
|
||||
use TYPO3\CMS\Core\Imaging\GraphicalFunctions;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* A task that takes care of cropping, scaling and/or masking an image.
|
||||
*/
|
||||
class ImageCropScaleMaskTask extends AbstractTask
|
||||
{
|
||||
protected ?string $targetFileExtension;
|
||||
|
||||
public function getType(): string
|
||||
{
|
||||
return 'Image';
|
||||
}
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return 'CropScaleMask';
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the file extension the processed file
|
||||
* should have in the filesystem.
|
||||
*/
|
||||
public function getTargetFileExtension(): string
|
||||
{
|
||||
if (!isset($this->targetFileExtension)) {
|
||||
$this->targetFileExtension = $this->determineTargetFileExtension();
|
||||
}
|
||||
return $this->targetFileExtension;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the file extension the processed file should
|
||||
* have in the filesystem by either using the configuration
|
||||
* setting, or the extension of the original file.
|
||||
*/
|
||||
protected function determineTargetFileExtension(): string
|
||||
{
|
||||
if (!empty($this->configuration['fileExtension'])) {
|
||||
return $this->configuration['fileExtension'];
|
||||
}
|
||||
|
||||
// @todo - See note of determineDefaultProcessingFileExtension() - find a better place for this
|
||||
$imageService = GeneralUtility::makeInstance(GraphicalFunctions::class);
|
||||
return $imageService->determineDefaultProcessingFileExtension($this->getSourceFile()->getExtension());
|
||||
}
|
||||
|
||||
public function getTargetFileName(): string
|
||||
{
|
||||
return 'csm_'
|
||||
. $this->getSourceFile()->getNameWithoutExtension()
|
||||
. '_' . $this->getConfigurationChecksum()
|
||||
. '.' . $this->getTargetFileExtension();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Resource\Processing;
|
||||
|
||||
use TYPO3\CMS\Core\Imaging\GraphicalFunctions;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Core\Utility\MathUtility;
|
||||
|
||||
/**
|
||||
* A task for generating an image preview.
|
||||
*/
|
||||
class ImagePreviewTask extends AbstractTask
|
||||
{
|
||||
protected ?string $targetFileExtension;
|
||||
|
||||
public function getType(): string
|
||||
{
|
||||
return 'Image';
|
||||
}
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return 'Preview';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the name the processed file should have
|
||||
* in the filesystem.
|
||||
*/
|
||||
public function getTargetFilename(): string
|
||||
{
|
||||
return 'preview_'
|
||||
. $this->getSourceFile()->getNameWithoutExtension()
|
||||
. '_' . $this->getConfigurationChecksum()
|
||||
. '.' . $this->getTargetFileExtension();
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the file extension the processed file
|
||||
* should have in the filesystem.
|
||||
*/
|
||||
public function getTargetFileExtension(): string
|
||||
{
|
||||
if (!isset($this->targetFileExtension)) {
|
||||
$this->targetFileExtension = $this->determineTargetFileExtension();
|
||||
}
|
||||
return $this->targetFileExtension;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the file extension the processed file should
|
||||
* have in the filesystem by either using the configuration
|
||||
* setting, or the extension of the original file.
|
||||
*/
|
||||
protected function determineTargetFileExtension(): string
|
||||
{
|
||||
if (!empty($this->configuration['fileExtension'])) {
|
||||
return $this->configuration['fileExtension'];
|
||||
}
|
||||
|
||||
// @todo - See note of determineDefaultProcessingFileExtension() - find a better place for this
|
||||
$imageService = GeneralUtility::makeInstance(GraphicalFunctions::class);
|
||||
return $imageService->determineDefaultProcessingFileExtension($this->getSourceFile()->getExtension());
|
||||
}
|
||||
|
||||
/**
|
||||
* Enforce default configuration for preview processing here,
|
||||
* to be sure we find already processed files below,
|
||||
* which we wouldn't if we would change the configuration later, as configuration is part of the lookup.
|
||||
*/
|
||||
public function sanitizeConfiguration(): void
|
||||
{
|
||||
$configuration = array_replace(
|
||||
[
|
||||
'width' => 64,
|
||||
'height' => 64,
|
||||
],
|
||||
$this->configuration
|
||||
);
|
||||
$configuration['width'] = MathUtility::forceIntegerInRange($configuration['width'], 1, 1000);
|
||||
$configuration['height'] = MathUtility::forceIntegerInRange($configuration['height'], 1, 1000);
|
||||
|
||||
$this->configuration = array_filter(
|
||||
$configuration,
|
||||
static function (string|int|bool|array|null $value, string $name): bool {
|
||||
return !empty($value) && in_array($name, ['width', 'height'], true);
|
||||
},
|
||||
ARRAY_FILTER_USE_BOTH
|
||||
);
|
||||
parent::sanitizeConfiguration();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,436 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Resource\Processing;
|
||||
|
||||
use Psr\Log\LoggerAwareInterface;
|
||||
use Psr\Log\LoggerAwareTrait;
|
||||
use TYPO3\CMS\Core\Core\Environment;
|
||||
use TYPO3\CMS\Core\Imaging\GraphicalFunctions;
|
||||
use TYPO3\CMS\Core\Imaging\ImageProcessingInstructions;
|
||||
use TYPO3\CMS\Core\Resource\File;
|
||||
use TYPO3\CMS\Core\Resource\FileInterface;
|
||||
use TYPO3\CMS\Core\Resource\FileType;
|
||||
use TYPO3\CMS\Core\Type\File\ImageInfo;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Frontend\Imaging\GifBuilder;
|
||||
|
||||
/**
|
||||
* Processes Local Images files
|
||||
*/
|
||||
class LocalImageProcessor implements ProcessorInterface, LoggerAwareInterface
|
||||
{
|
||||
use LoggerAwareTrait;
|
||||
|
||||
/**
|
||||
* Returns TRUE if this processor can process the given task.
|
||||
*/
|
||||
public function canProcessTask(TaskInterface $task): bool
|
||||
{
|
||||
return $task->getType() === 'Image'
|
||||
&& in_array($task->getName(), ['Preview', 'CropScaleMask'], true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes the given task.
|
||||
*
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function processTask(TaskInterface $task): void
|
||||
{
|
||||
if ($this->checkForExistingTargetFile($task)) {
|
||||
return;
|
||||
}
|
||||
$this->processTaskWithLocalFile($task, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes an image described in a task, but optionally uses a given local image
|
||||
*
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
protected function processTaskWithLocalFile(TaskInterface $task, ?string $localFile): void
|
||||
{
|
||||
try {
|
||||
if ($task->getName() === 'CropScaleMask') {
|
||||
if ($localFile === null) {
|
||||
$result = $this->processCropScaleMask($task);
|
||||
} else {
|
||||
$result = $this->processCropScaleMaskWithLocalFile($task, $localFile);
|
||||
}
|
||||
} elseif ($task->getName() === 'Preview') {
|
||||
if ($localFile === null) {
|
||||
$result = $this->processPreview($task);
|
||||
} else {
|
||||
$result = $this->processPreviewWithLocalFile($task, $localFile);
|
||||
}
|
||||
} else {
|
||||
throw new \InvalidArgumentException('Cannot find helper for task name: "' . $task->getName() . '"', 1353401352);
|
||||
}
|
||||
if ($result === null) {
|
||||
$task->setExecuted(true);
|
||||
$task->getTargetFile()->setUsesOriginalFile();
|
||||
} elseif (!empty($result['filePath']) && file_exists($result['filePath'])) {
|
||||
$task->setExecuted(true);
|
||||
$imageInformation = GeneralUtility::makeInstance(ImageInfo::class, $result['filePath']);
|
||||
if (($result['remapProcessedTargetFileExtension'] ?? null) !== null) {
|
||||
// Processing changed the target filename extension to something else.
|
||||
// We need to react on this, because otherwise the file contents will not
|
||||
// match the file extension.
|
||||
$task->getTargetFile()->setName($task->getTargetFileName() . '.' . $result['remapProcessedTargetFileExtension']);
|
||||
} else {
|
||||
$task->getTargetFile()->setName($task->getTargetFileName());
|
||||
}
|
||||
$task->getTargetFile()->updateProperties([
|
||||
'width' => $imageInformation->getWidth(),
|
||||
'height' => $imageInformation->getHeight(),
|
||||
'size' => $imageInformation->getSize(),
|
||||
'checksum' => $task->getConfigurationChecksum(),
|
||||
]);
|
||||
$task->getTargetFile()->updateWithLocalFile($result['filePath']);
|
||||
} else {
|
||||
// Seems we have no valid processing result
|
||||
$task->setExecuted(false);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
// @todo: Swallowing all exceptions including PHP warnings here is a bad idea.
|
||||
// @todo: This should be restricted to more specific exceptions - if at all.
|
||||
// @todo: For now, we at least log the situation.
|
||||
$this->logger->error(sprintf('Processing task of image file'), ['exception' => $e]);
|
||||
$task->setExecuted(false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the target file that is to be processed already exists.
|
||||
* If it exists, use the metadata from that file and mark task as done.
|
||||
*/
|
||||
protected function checkForExistingTargetFile(TaskInterface $task): bool
|
||||
{
|
||||
// the storage of the processed file, not of the original file!
|
||||
$storage = $task->getTargetFile()->getStorage();
|
||||
$processingFolder = $storage->getProcessingFolder($task->getSourceFile());
|
||||
|
||||
// explicitly check for the raw filename here, as we check for files that existed before we even started
|
||||
// processing, i.e. that were processed earlier
|
||||
if ($processingFolder->hasFile($task->getTargetFileName())) {
|
||||
// When the processed file already exists set it as processed file
|
||||
$task->getTargetFile()->setName($task->getTargetFileName());
|
||||
|
||||
// If the processed file is stored on a remote server, we must fetch a local copy of the file, as we
|
||||
// have no API for fetching file metadata from a remote file.
|
||||
$localProcessedFile = $storage->getFileForLocalProcessing($task->getTargetFile(), false);
|
||||
$task->setExecuted(true);
|
||||
$imageInformation = GeneralUtility::makeInstance(ImageInfo::class, $localProcessedFile);
|
||||
$properties = [
|
||||
'width' => $imageInformation->getWidth(),
|
||||
'height' => $imageInformation->getHeight(),
|
||||
'size' => $imageInformation->getSize(),
|
||||
'checksum' => $task->getConfigurationChecksum(),
|
||||
];
|
||||
$task->getTargetFile()->updateProperties($properties);
|
||||
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper methods to locally perform a crop/scale/mask task with the TYPO3 image processing classes.
|
||||
*/
|
||||
|
||||
/**
|
||||
* This method actually does the processing of files locally
|
||||
*
|
||||
* Takes the original file (for remote storages this will be fetched from the remote server),
|
||||
* does the IM magic on the local server by creating a temporary typo3temp/ file,
|
||||
* copies the typo3temp/ file to the processing folder of the target storage and
|
||||
* removes the typo3temp/ file.
|
||||
*
|
||||
* The returned array has the following structure:
|
||||
* width => 100
|
||||
* height => 200
|
||||
* filePath => /some/path
|
||||
*
|
||||
* If filePath isn't set but width and height are the original file is used as ProcessedFile
|
||||
* with the returned width and height. This is for example useful for SVG images.
|
||||
*/
|
||||
protected function processCropScaleMask(TaskInterface $task): ?array
|
||||
{
|
||||
return $this->processCropScaleMaskWithLocalFile($task, $task->getSourceFile()->getForLocalProcessing(false));
|
||||
}
|
||||
|
||||
/**
|
||||
* Does the heavy lifting prescribed in processTask()
|
||||
* except that the processing can be performed on any given local image.
|
||||
* Note that the resize() method usually does not upscale images (depends on "noScale" option),
|
||||
* so the original file would be used for the processor result.
|
||||
*/
|
||||
protected function processCropScaleMaskWithLocalFile(TaskInterface $task, string $originalFileName): ?array
|
||||
{
|
||||
$result = null;
|
||||
$targetFile = $task->getTargetFile();
|
||||
$targetFileExtension = $task->getTargetFileExtension();
|
||||
|
||||
$imageOperations = GeneralUtility::makeInstance(GraphicalFunctions::class);
|
||||
|
||||
$configuration = $targetFile->getProcessingConfiguration();
|
||||
$configuration['additionalParameters'] ??= '';
|
||||
|
||||
// Normal situation (no masking) - just scale the image
|
||||
if (!is_array($configuration['maskImages'] ?? null)) {
|
||||
// the result info is an array with 0=width,1=height,2=extension,3=filename
|
||||
$result = $imageOperations->resize(
|
||||
$originalFileName,
|
||||
$targetFileExtension,
|
||||
$configuration['width'] ?? '',
|
||||
$configuration['height'] ?? '',
|
||||
$configuration['additionalParameters'],
|
||||
$configuration,
|
||||
);
|
||||
} else {
|
||||
$temporaryFileName = $this->getFilenameForImageCropScaleMask($task);
|
||||
$maskImage = $configuration['maskImages']['maskImage'] ?? null;
|
||||
$maskBackgroundImage = $configuration['maskImages']['backgroundImage'];
|
||||
if ($maskImage instanceof FileInterface && $maskBackgroundImage instanceof FileInterface) {
|
||||
// This converts the original image to a temporary PNG file during all steps of the masking process
|
||||
$tempFileInfo = $imageOperations->resize(
|
||||
$originalFileName,
|
||||
'png',
|
||||
$configuration['width'] ?? '',
|
||||
$configuration['height'] ?? '',
|
||||
$configuration['additionalParameters'],
|
||||
$configuration
|
||||
);
|
||||
if ($tempFileInfo !== null) {
|
||||
// Scaling
|
||||
$command = '-geometry ' . $tempFileInfo->getWidth() . 'x' . $tempFileInfo->getHeight() . '!';
|
||||
$imageOperations->mask(
|
||||
$tempFileInfo->getRealPath(),
|
||||
$temporaryFileName,
|
||||
$maskImage->getForLocalProcessing(),
|
||||
$maskBackgroundImage->getForLocalProcessing(),
|
||||
$command,
|
||||
$configuration
|
||||
);
|
||||
$maskBottomImage = $configuration['maskImages']['maskBottomImage'] ?? null;
|
||||
$maskBottomImageMask = $configuration['maskImages']['maskBottomImageMask'] ?? null;
|
||||
if ($maskBottomImage instanceof FileInterface && $maskBottomImageMask instanceof FileInterface) {
|
||||
// Uses the temporary PNG file from the previous step and applies another mask
|
||||
$imageOperations->mask(
|
||||
$temporaryFileName,
|
||||
$temporaryFileName,
|
||||
$maskBottomImage->getForLocalProcessing(),
|
||||
$maskBottomImageMask->getForLocalProcessing(),
|
||||
$command,
|
||||
$configuration
|
||||
);
|
||||
}
|
||||
}
|
||||
$result = $tempFileInfo;
|
||||
}
|
||||
}
|
||||
|
||||
// check if the processing really generated a new file (scaled and/or cropped)
|
||||
if ($result !== null) {
|
||||
// The file processing yielded a different file extension than we anticipated. Most likely because
|
||||
// the processing service found out a file type needed to use fallback storage. In this case, we
|
||||
// append the actually received file extension to our file to be stored, which will also hint at
|
||||
// a failed conversion, like some-file.avif.jpg. Otherwise use the same file extension. This is
|
||||
// evaluated for persistence in @see LocalImageProcessor->processTaskWithLocalFile().
|
||||
$remapProcessedTargetFileExtension = ($targetFileExtension !== $result->getExtension())
|
||||
// Remap to correct image type extension.
|
||||
? $result->getExtension()
|
||||
// No file extension remap required.
|
||||
: null;
|
||||
// @todo: realpath handling should be revisited, they may produce issues
|
||||
// with open_basedir restrictions and/or lockRootPath.
|
||||
if ($result->getRealPath() !== realpath($originalFileName)) {
|
||||
$result = [
|
||||
'width' => $result->getWidth(),
|
||||
'height' => $result->getHeight(),
|
||||
'filePath' => $result->getRealPath(),
|
||||
'remapProcessedTargetFileExtension' => $remapProcessedTargetFileExtension,
|
||||
];
|
||||
} else {
|
||||
// No file was generated
|
||||
$result = null;
|
||||
}
|
||||
}
|
||||
|
||||
// If noScale option is applied, we need to reset the width and height to ensure the scaled values
|
||||
// are used for the generated image tag even if the image itself is not scaled. This is needed, as
|
||||
// the result is discarded due to the fact that the original image is used.
|
||||
// @see https://forge.typo3.org/issues/100972
|
||||
// Note: This should only happen if no image has been generated ($result === null).
|
||||
if ($result === null && ($configuration['noScale'] ?? false)) {
|
||||
$configuration = $task->getConfiguration();
|
||||
$localProcessedFile = $task->getSourceFile()->getForLocalProcessing(false);
|
||||
$imageDimensions = $imageOperations->getImageDimensions($localProcessedFile, true);
|
||||
$imageScaleInfo = ImageProcessingInstructions::fromCropScaleValues(
|
||||
$imageDimensions->getWidth(),
|
||||
$imageDimensions->getHeight(),
|
||||
$configuration['width'] ?? '',
|
||||
$configuration['height'] ?? '',
|
||||
$configuration
|
||||
);
|
||||
$targetFile->updateProperties([
|
||||
'width' => $imageScaleInfo->width,
|
||||
'height' => $imageScaleInfo->height,
|
||||
]);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the filename for a cropped/scaled/masked file which will be put in typo3temp for the time being.
|
||||
*/
|
||||
protected function getFilenameForImageCropScaleMask(TaskInterface $task): string
|
||||
{
|
||||
$targetFileExtension = $task->getTargetFileExtension();
|
||||
$name = $this->generateProcessedFileNameWithoutExtension($task);
|
||||
return Environment::getPublicPath() . '/typo3temp/' . $name . '.' . ltrim(trim($targetFileExtension), '.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the name of the new File. Should be placed somwhere else?
|
||||
*/
|
||||
protected function generateProcessedFileNameWithoutExtension(TaskInterface $task): string
|
||||
{
|
||||
return implode('_', [
|
||||
$task->getSourceFile()->getNameWithoutExtension(),
|
||||
$task->getSourceFile()->getUid(),
|
||||
$task->getConfigurationChecksum(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper for creating local image previews using TYPO3s image processing classes.
|
||||
*/
|
||||
|
||||
/**
|
||||
* This method actually does the processing of files locally
|
||||
*
|
||||
* takes the original file (on remote storages this will be fetched from the remote server)
|
||||
* does the IM magic on the local server by creating a temporary typo3temp/ file
|
||||
* copies the typo3temp/ file to the processing folder of the target storage
|
||||
* removes the typo3temp/ file
|
||||
*
|
||||
* The returned array has the following structure:
|
||||
* width => 100
|
||||
* height => 200
|
||||
* filePath => /some/path
|
||||
*
|
||||
* If filePath isn't set but width and height are the original file is used as ProcessedFile
|
||||
* with the returned width and height. This is for example useful for SVG images.
|
||||
*/
|
||||
protected function processPreview(TaskInterface $task): ?array
|
||||
{
|
||||
$sourceFile = $task->getSourceFile();
|
||||
$task->sanitizeConfiguration();
|
||||
$configuration = $task->getConfiguration();
|
||||
|
||||
// Do not scale up, if the source file has dimensions and any target dimension (width/height) is larger
|
||||
// This is related to $TYPO3_CONF_VARS['GFX']['processor_allowUpscaling'] = false to ensure the original
|
||||
// file can be used (instead of getting processed)
|
||||
if ($sourceFile->getProperty('width') > 0 && $sourceFile->getProperty('height') > 0
|
||||
&& (
|
||||
$configuration['width'] > $sourceFile->getProperty('width')
|
||||
|| $configuration['height'] > $sourceFile->getProperty('height')
|
||||
)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->generatePreviewFromFile($sourceFile, $configuration, $this->getTemporaryFilePathForPreview($task));
|
||||
}
|
||||
|
||||
/**
|
||||
* Does the heavy lifting prescribed in processTask()
|
||||
* except that the processing can be performed on any given local image
|
||||
*/
|
||||
protected function processPreviewWithLocalFile(TaskInterface $task, string $localFile): ?array
|
||||
{
|
||||
return $this->generatePreviewFromLocalFile($localFile, $task->getConfiguration(), $this->getTemporaryFilePathForPreview($task));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the path to a temporary file for processing
|
||||
*
|
||||
* @return non-empty-string
|
||||
*/
|
||||
protected function getTemporaryFilePathForPreview(TaskInterface $task): string
|
||||
{
|
||||
return GeneralUtility::tempnam('preview_', '.' . $task->getTargetFileExtension());
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a preview for a file
|
||||
*
|
||||
* @param File $file The source file
|
||||
* @param array $configuration Processing configuration
|
||||
* @param string $targetFilePath Output file path
|
||||
*/
|
||||
protected function generatePreviewFromFile(File $file, array $configuration, string $targetFilePath): array
|
||||
{
|
||||
// Check file extension
|
||||
if (!$file->isType(FileType::IMAGE) && !$file->isImage()) {
|
||||
// Create a default image
|
||||
$graphicalFunctions = GeneralUtility::makeInstance(GifBuilder::class);
|
||||
$graphicalFunctions->getTemporaryImageWithText(
|
||||
$targetFilePath,
|
||||
'Not imagefile!',
|
||||
'No ext!',
|
||||
$file->getName()
|
||||
);
|
||||
return [
|
||||
'filePath' => $targetFilePath,
|
||||
];
|
||||
}
|
||||
|
||||
return $this->generatePreviewFromLocalFile($file->getForLocalProcessing(false), $configuration, $targetFilePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a preview for a local file
|
||||
*
|
||||
* @param string $originalFileName Optional input file path
|
||||
* @param array $configuration Processing configuration
|
||||
* @param string $targetFilePath Output file path
|
||||
*/
|
||||
protected function generatePreviewFromLocalFile(string $originalFileName, array $configuration, string $targetFilePath): array
|
||||
{
|
||||
// Create the temporary file
|
||||
$imageService = GeneralUtility::makeInstance(GraphicalFunctions::class);
|
||||
$result = $imageService->resize($originalFileName, 'WEB', $configuration['width'] . 'm', $configuration['height'] . 'm', '', ['sample' => true]);
|
||||
if ($result) {
|
||||
$targetFilePath = $result->getRealPath();
|
||||
}
|
||||
if (!file_exists($targetFilePath)) {
|
||||
// Create an error gif
|
||||
$graphicalFunctions = GeneralUtility::makeInstance(GifBuilder::class);
|
||||
$graphicalFunctions->getTemporaryImageWithText(
|
||||
$targetFilePath,
|
||||
'No thumb',
|
||||
'generated!'
|
||||
);
|
||||
}
|
||||
|
||||
return [
|
||||
'filePath' => $targetFilePath,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Resource\Processing;
|
||||
|
||||
/**
|
||||
* Interface for file processors. All classes capable of processing a file have to implement this interface.
|
||||
*/
|
||||
interface ProcessorInterface
|
||||
{
|
||||
/**
|
||||
* Returns TRUE if this processor can process the given task.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function canProcessTask(TaskInterface $task);
|
||||
|
||||
/**
|
||||
* Processes the given task and sets the processing result in the task object.
|
||||
*/
|
||||
public function processTask(TaskInterface $task);
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Resource\Processing;
|
||||
|
||||
use TYPO3\CMS\Core\Service\DependencyOrderingService;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* Registry for images processors.
|
||||
*/
|
||||
class ProcessorRegistry
|
||||
{
|
||||
protected array $registeredProcessors = [];
|
||||
|
||||
/**
|
||||
* Auto register processors from configuration
|
||||
*/
|
||||
public function __construct(DependencyOrderingService $dependencyOrderingService)
|
||||
{
|
||||
$this->registeredProcessors = $dependencyOrderingService->orderByDependencies(
|
||||
$GLOBALS['TYPO3_CONF_VARS']['SYS']['fal']['processors'] ?? []
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a matching processor that can process the given task.
|
||||
* Registered processors will be tested by their priority from high to low.
|
||||
*/
|
||||
public function getProcessorByTask(TaskInterface $task): ProcessorInterface
|
||||
{
|
||||
$processor = null;
|
||||
|
||||
foreach ($this->registeredProcessors as $key => $processorConfiguration) {
|
||||
if (!isset($processorConfiguration['className'])) {
|
||||
throw new \RuntimeException(
|
||||
'Missing key "className" for processor configuration "' . $key . '".',
|
||||
1560875741
|
||||
);
|
||||
}
|
||||
|
||||
$processor = GeneralUtility::makeInstance($processorConfiguration['className']);
|
||||
|
||||
if (!$processor instanceof ProcessorInterface) {
|
||||
throw new \RuntimeException(
|
||||
'Processor "' . get_class($processor) . '" needs to implement interface "' . ProcessorInterface::class . '".',
|
||||
1560876288
|
||||
);
|
||||
}
|
||||
|
||||
if ($processor->canProcessTask($task)) {
|
||||
/*
|
||||
* Stop checking for further processors to speed up image processing.
|
||||
* If another processor should be used, it can be registered with higher priority.
|
||||
*/
|
||||
break;
|
||||
}
|
||||
|
||||
$processor = null;
|
||||
}
|
||||
|
||||
if ($processor === null) {
|
||||
throw new \RuntimeException(
|
||||
sprintf('No matching file processor found for task type "%s" and name "%s".', $task->getType(), $task->getName()),
|
||||
1560876294
|
||||
);
|
||||
}
|
||||
|
||||
return $processor;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Resource\Processing;
|
||||
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
|
||||
use TYPO3\CMS\Core\Imaging\Exception\InvalidSvgException;
|
||||
use TYPO3\CMS\Core\Imaging\Exception\ZeroImageDimensionException;
|
||||
use TYPO3\CMS\Core\Imaging\ImageDimension;
|
||||
use TYPO3\CMS\Core\Imaging\ImageManipulation\Area;
|
||||
use TYPO3\CMS\Core\Imaging\ImageProcessingInstructions;
|
||||
use TYPO3\CMS\Core\Imaging\Svg\SvgDocumentFactory;
|
||||
use TYPO3\CMS\Core\Imaging\Svg\SvgDocumentService;
|
||||
use TYPO3\CMS\Core\Resource\Exception\InsufficientFolderReadPermissionsException;
|
||||
use TYPO3\CMS\Core\Type\File\ImageInfo;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* Processes (scales) SVG Images files or crops them via \DOMDocument
|
||||
* and creates a new locally created processed file which is then pushed
|
||||
* into FAL again.
|
||||
*/
|
||||
#[Autoconfigure(public: true)]
|
||||
readonly class SvgImageProcessor implements ProcessorInterface
|
||||
{
|
||||
private const int DEFAULT_SVG_DIMENSION = 64;
|
||||
|
||||
public function __construct(
|
||||
private SvgDocumentFactory $svgDocumentFactory,
|
||||
private SvgDocumentService $svgDocumentService,
|
||||
) {}
|
||||
|
||||
public function canProcessTask(TaskInterface $task): bool
|
||||
{
|
||||
return $task->getType() === 'Image'
|
||||
&& in_array($task->getName(), ['Preview', 'CropScaleMask'], true)
|
||||
&& $task->getTargetFileExtension() === 'svg';
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes the given task.
|
||||
*
|
||||
* @throws \InvalidArgumentException|InsufficientFolderReadPermissionsException
|
||||
*/
|
||||
public function processTask(TaskInterface $task): void
|
||||
{
|
||||
try {
|
||||
$processingInstructions = ImageProcessingInstructions::fromProcessingTask($task);
|
||||
$imageDimension = new ImageDimension($processingInstructions->width, $processingInstructions->height);
|
||||
} catch (ZeroImageDimensionException) {
|
||||
$processingInstructions = new ImageProcessingInstructions(
|
||||
width: self::DEFAULT_SVG_DIMENSION,
|
||||
height: self::DEFAULT_SVG_DIMENSION,
|
||||
);
|
||||
// To not fail image processing, we just assume an SVG image dimension here
|
||||
$imageDimension = new ImageDimension(
|
||||
width: self::DEFAULT_SVG_DIMENSION,
|
||||
height: self::DEFAULT_SVG_DIMENSION
|
||||
);
|
||||
}
|
||||
|
||||
$task->getTargetFile()->updateProperties(
|
||||
[
|
||||
'width' => $imageDimension->getWidth(),
|
||||
'height' => $imageDimension->getHeight(),
|
||||
'size' => $task->getSourceFile()->getSize(),
|
||||
'checksum' => $task->getConfigurationChecksum(),
|
||||
]
|
||||
);
|
||||
|
||||
if ($this->checkForExistingTargetFile($task)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$cropArea = $processingInstructions->cropArea;
|
||||
if ($cropArea === null || $cropArea->makeRelativeBasedOnFile($task->getSourceFile())->isEmpty()) {
|
||||
$task->setExecuted(true);
|
||||
$task->getTargetFile()->setUsesOriginalFile();
|
||||
return;
|
||||
}
|
||||
|
||||
$this->applyCropping($task, $cropArea, $imageDimension);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrap the source SVG in a crop container and write it to a temporary
|
||||
* processed file. The wrapper carries the viewBox crop and target
|
||||
* dimensions so the result is self-contained when embedded via <img>.
|
||||
*/
|
||||
protected function applyCropping(TaskInterface $task, Area $cropArea, ImageDimension $imageDimension): void
|
||||
{
|
||||
try {
|
||||
$document = $this->svgDocumentFactory->fromFile($task->getSourceFile());
|
||||
$processedSvg = $this->svgDocumentService->cropScale($document, $cropArea, $imageDimension);
|
||||
} catch (InvalidSvgException) {
|
||||
// Source SVG could not be parsed - fall back to the unprocessed original.
|
||||
$task->setExecuted(true);
|
||||
$task->getTargetFile()->setUsesOriginalFile();
|
||||
return;
|
||||
}
|
||||
$temporaryFilename = $this->getFilenameForSvgCropScaleMask($task);
|
||||
GeneralUtility::writeFile($temporaryFilename, $this->svgDocumentService->toXml($processedSvg), true);
|
||||
|
||||
$task->setExecuted(true);
|
||||
$imageInformation = GeneralUtility::makeInstance(ImageInfo::class, $temporaryFilename);
|
||||
|
||||
$task->getTargetFile()->setName($task->getTargetFileName());
|
||||
|
||||
$task->getTargetFile()->updateProperties([
|
||||
// @todo: Use round() instead of int-cast to avoid an implicit floor()?
|
||||
'width' => (string)$imageDimension->getWidth(),
|
||||
'height' => (string)$imageDimension->getHeight(),
|
||||
'size' => $imageInformation->getSize(),
|
||||
'checksum' => $task->getConfigurationChecksum(),
|
||||
]);
|
||||
$task->getTargetFile()->updateWithLocalFile($temporaryFilename);
|
||||
GeneralUtility::unlink_tempfile($temporaryFilename);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the target file that is to be processed already exists.
|
||||
* If it exists, use the metadata from that file and mark task as done.
|
||||
*
|
||||
* @throws InsufficientFolderReadPermissionsException
|
||||
* @todo - Refactor this 80% duplicate code of LocalImageProcessor::checkForExistingTargetFile
|
||||
*/
|
||||
protected function checkForExistingTargetFile(TaskInterface $task): bool
|
||||
{
|
||||
// the storage of the processed file, not of the original file!
|
||||
$storage = $task->getTargetFile()->getStorage();
|
||||
$processingFolder = $storage->getProcessingFolder($task->getSourceFile());
|
||||
|
||||
// explicitly check for the raw filename here, as we check for files that existed before we even started
|
||||
// processing, i.e. that were processed earlier
|
||||
if ($processingFolder->hasFile($task->getTargetFileName())) {
|
||||
// When the processed file already exists set it as processed file
|
||||
$task->getTargetFile()->setName($task->getTargetFileName());
|
||||
|
||||
// If the processed file is stored on a remote server, we must fetch a local copy of the file, as we
|
||||
// have no API for fetching file metadata from a remote file.
|
||||
$localProcessedFile = $storage->getFileForLocalProcessing($task->getTargetFile(), false);
|
||||
$task->setExecuted(true);
|
||||
$imageInformation = GeneralUtility::makeInstance(ImageInfo::class, $localProcessedFile);
|
||||
$properties = [
|
||||
'width' => $imageInformation->getWidth(),
|
||||
'height' => $imageInformation->getHeight(),
|
||||
'size' => $imageInformation->getSize(),
|
||||
'checksum' => $task->getConfigurationChecksum(),
|
||||
];
|
||||
$task->getTargetFile()->updateProperties($properties);
|
||||
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the filename for a cropped/scaled/masked file which will be put
|
||||
* in typo3temp for the time being.
|
||||
*/
|
||||
protected function getFilenameForSvgCropScaleMask(TaskInterface $task): string
|
||||
{
|
||||
$targetFileExtension = $task->getTargetFileExtension();
|
||||
return GeneralUtility::tempnam($this->generateProcessedFileNameWithoutExtension($task), '.' . ltrim(trim($targetFileExtension)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the name of the new File. Should be placed somwhere else?
|
||||
*/
|
||||
protected function generateProcessedFileNameWithoutExtension(TaskInterface $task): string
|
||||
{
|
||||
return implode('_', [
|
||||
$task->getSourceFile()->getNameWithoutExtension(),
|
||||
$task->getSourceFile()->getUid(),
|
||||
$task->getConfigurationChecksum(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Resource\Processing;
|
||||
|
||||
use TYPO3\CMS\Core\Resource;
|
||||
|
||||
/**
|
||||
* A task is a unit of work that can be performed by a file processor. This may include multiple steps in any order,
|
||||
* details depend on the configuration of the task and the tools the processor uses.
|
||||
*
|
||||
* Each task has a type and a name. The type describes the category of the task, like "image" and "video". If your task
|
||||
* is generic or applies to multiple types of files, use "general".
|
||||
*
|
||||
* A task also already has to know the target file it should be executed on, so there is no "abstract" task that just
|
||||
* specifies the steps to be executed without a concrete file. However, new tasks can easily be created from an
|
||||
* existing task object.
|
||||
*/
|
||||
interface TaskInterface
|
||||
{
|
||||
/**
|
||||
* Returns the name of this task.
|
||||
*/
|
||||
public function getName(): string;
|
||||
|
||||
/**
|
||||
* Returns the type of this task.
|
||||
*/
|
||||
public function getType(): string;
|
||||
|
||||
/**
|
||||
* Returns the processed file this task is executed on.
|
||||
*/
|
||||
public function getTargetFile(): Resource\ProcessedFile;
|
||||
|
||||
/**
|
||||
* Returns the original file this task is based on.
|
||||
*/
|
||||
public function getSourceFile(): Resource\File;
|
||||
|
||||
/**
|
||||
* Returns the configuration for this task.
|
||||
*/
|
||||
public function getConfiguration(): array;
|
||||
|
||||
/**
|
||||
* Returns the configuration checksum of this task.
|
||||
*/
|
||||
public function getConfigurationChecksum(): string;
|
||||
|
||||
/**
|
||||
* Returns the name the processed file should have in the filesystem.
|
||||
*/
|
||||
public function getTargetFileName(): string;
|
||||
|
||||
/**
|
||||
* Gets the file extension the processed file should have in the filesystem.
|
||||
*/
|
||||
public function getTargetFileExtension(): string;
|
||||
|
||||
/**
|
||||
* Returns TRUE if the file has to be processed at all, such as e.g. the original file does.
|
||||
*
|
||||
* Note: This does not indicate if the concrete ProcessedFile attached to this task has to be (re)processed.
|
||||
* This check is done in ProcessedFile::isOutdated(). @todo isOutdated()/needsReprocessing()?
|
||||
*/
|
||||
public function fileNeedsProcessing(): bool;
|
||||
|
||||
/**
|
||||
* Returns TRUE if this task has been executed, no matter if the execution was successful.
|
||||
*/
|
||||
public function isExecuted(): bool;
|
||||
|
||||
/**
|
||||
* Mark this task as executed. This is used by the Processors in order to transfer the state of this task to
|
||||
* the file processing service.
|
||||
*
|
||||
* @param bool $successful Set this to FALSE if executing the task failed
|
||||
*/
|
||||
public function setExecuted(bool $successful): void;
|
||||
|
||||
/**
|
||||
* Returns TRUE if this task has been successfully executed. Only call this method if the task has been processed
|
||||
* at all.
|
||||
*
|
||||
* @throws \LogicException If the task has not been executed already
|
||||
*/
|
||||
public function isSuccessful(): bool;
|
||||
|
||||
/**
|
||||
* For some tasks it might be important and useful to clean up the configuration, in order to find the
|
||||
* ProcessedFile that uses this configuration.
|
||||
*
|
||||
* Ideally, a task has some information what needs to be used or not.
|
||||
*/
|
||||
public function sanitizeConfiguration(): void;
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Resource\Processing;
|
||||
|
||||
use TYPO3\CMS\Core\Resource\ProcessedFile;
|
||||
use TYPO3\CMS\Core\SingletonInterface;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* The registry for task types.
|
||||
*/
|
||||
class TaskTypeRegistry implements SingletonInterface
|
||||
{
|
||||
protected array $registeredTaskTypes = [];
|
||||
|
||||
/**
|
||||
* Register task types from configuration
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->registeredTaskTypes = $GLOBALS['TYPO3_CONF_VARS']['SYS']['fal']['processingTaskTypes'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the class that implements the given task type.
|
||||
*/
|
||||
protected function getClassForTaskType(string $taskType): ?string
|
||||
{
|
||||
return $this->registeredTaskTypes[$taskType] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \RuntimeException
|
||||
*/
|
||||
public function getTaskForType(string $taskType, ProcessedFile $processedFile, array $processingConfiguration): TaskInterface
|
||||
{
|
||||
$taskClass = $this->getClassForTaskType($taskType);
|
||||
if ($taskClass === null) {
|
||||
throw new \RuntimeException('Unknown processing task "' . $taskType . '"', 1476049767);
|
||||
}
|
||||
|
||||
return GeneralUtility::makeInstance($taskClass, $processedFile, $processingConfiguration);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user