Skip to content
Snippets Groups Projects
ExifToolService.php 8.25 KiB
Newer Older
Ole Hartwig's avatar
Ole Hartwig committed
<?php
Ole Hartwig's avatar
Ole Hartwig committed

Ole Hartwig's avatar
Ole Hartwig committed
declare(strict_types=1);

Ole Hartwig's avatar
Ole Hartwig committed
/*
 * This file is part of the package itzbund/gsb-metadata-cleaner of the GSB 11 Project by ITZBund.
 *
Kai Ole Hartwig's avatar
Kai Ole Hartwig committed
 * Copyright (C) 2023 - 2024 Bundesrepublik Deutschland, vertreten durch das
 * BMI/ITZBund. Author: Ole Hartwig, Patrick Schriner
Ole Hartwig's avatar
Ole Hartwig committed
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
Ole Hartwig's avatar
Ole Hartwig committed
 *
Ole Hartwig's avatar
Ole Hartwig committed
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
Ole Hartwig's avatar
Ole Hartwig committed
namespace ITZBund\GsbMetadataCleaner\Service;

use Doctrine\DBAL\Exception;
Patrick Schriner's avatar
Patrick Schriner committed
use ITZBund\GsbMetadataCleaner\Configuration\ExtensionConfiguration as GsbMetadataCleanerExtensionConfiguration;
use Psr\Log\LoggerInterface;
use Psr\Log\LogLevel;
Patrick Schriner's avatar
Patrick Schriner committed
use TYPO3\CMS\Core\Database\Connection;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Resource\FileInterface;
Ole Hartwig's avatar
Ole Hartwig committed
use TYPO3\CMS\Core\Utility\CommandUtility;
use TYPO3\CMS\Core\Utility\GeneralUtility;
Ole Hartwig's avatar
Ole Hartwig committed

Ole Hartwig's avatar
Ole Hartwig committed
class ExifToolService
{
    /**
     * @var array<string>
     */
    private array $forcedKeepTagsForPdf = ['Xmp-dc:Rights'];

    /**
     * @var array<string>
     */
    private array $forcedKeepTagsForImage = ['copyright'];

    public function __construct(
Patrick Schriner's avatar
Patrick Schriner committed
        private readonly GsbMetadataCleanerExtensionConfiguration $extensionConfiguration,
        private readonly LoggerInterface $logger
Christian Rath-Ulrich's avatar
Christian Rath-Ulrich committed
    ) {}
    /**
     * We want to process images and pdf files
     *
     * exiftool might throw an error on processing files it cannot write, but will leave the file as is
     *
     * @return bool
     */
    public function canProcessFile(FileInterface $file): bool
    {
        // a bit odd, but passing writeable=false will give us the absolute path for a local file
        // where writeable=true will give us a temp copy
        $absoluteFilePath = $file->getStorage()->getFileForLocalProcessing($file, false);
        if (!is_file($absoluteFilePath) || !is_writable($absoluteFilePath)) {
            return false;
        }

        $mimeType = strtolower($file->getMimeType());
        [$fileType,$specificType] = explode('/', $mimeType);
        if ((($fileType === 'image') && ($specificType !== 'svg')) || $mimeType === 'application/pdf') {
     * Strip the file given of it's exif metadata, *except* for "copyright" (if applicable)
Patrick Schriner's avatar
Patrick Schriner committed
     * @param FileInterface $file the file to be processesed
     *
     * @throws \InvalidArgumentException
    public function removeMetadata(FileInterface $file): void
        $filePath = $file->getStorage()->getFileForLocalProcessing($file, false);
        if (!is_file($filePath) && !is_writable($filePath)) {
            throw new \InvalidArgumentException('File not writeable', 1701857184);
Ole Hartwig's avatar
Ole Hartwig committed
        $command = sprintf(
            '%s -overwrite_original -all= -tagsFromFile @ %s %s',
Patrick Schriner's avatar
Patrick Schriner committed
            escapeshellarg($this->extensionConfiguration->getExifToolPath()),
            $this->getEscapedTagArguments($file),
Ole Hartwig's avatar
Ole Hartwig committed
            escapeshellarg($filePath)
        );
        $output = [];
        $returnValue = 0;
        CommandUtility::exec($command, $output, $returnValue);
        if ($returnValue > 0) {
            $this->logger->log(LogLevel::ERROR, 'exiftool failed to strip tags', array_merge($output, ['file' => $filePath]));
        } else {
            $this->logger->log(LogLevel::DEBUG, 'exiftool used to strip tags', ['file' => $filePath]);
Patrick Schriner's avatar
Patrick Schriner committed
            if ($this->shouldUseQpdf($file)) {
                $this->linearizePdf($file);
            }
Patrick Schriner's avatar
Patrick Schriner committed
    /**
Patrick Schriner's avatar
Patrick Schriner committed
     * Check if a file should be fed to qpdf
Patrick Schriner's avatar
Patrick Schriner committed
     *
Patrick Schriner's avatar
Patrick Schriner committed
     * @param FileInterface $file the file to be processed
     * @return bool true if it's a pdf and the setting has been enabled
Patrick Schriner's avatar
Patrick Schriner committed
     */
Patrick Schriner's avatar
Patrick Schriner committed
    protected function shouldUseQpdf(FileInterface $file): bool
Patrick Schriner's avatar
Patrick Schriner committed
        if (strtolower($file->getMimeType()) === 'application/pdf' && $this->extensionConfiguration->getUseQpdf()) {
            return true;
Patrick Schriner's avatar
Patrick Schriner committed
        return false;
    }
Patrick Schriner's avatar
Patrick Schriner committed
    /**
     * Linearize a PDF file
     * This results in old tags beeing deleted for good and the pdf getting optimized for the web
     *
     * @param FileInterface $file the file to be processed
     * @throws \InvalidArgumentException
Patrick Schriner's avatar
Patrick Schriner committed
     */
    protected function linearizePdf(FileInterface $file): void
    {
        $filePath = $file->getStorage()->getFileForLocalProcessing($file, false);
        if (!is_file($filePath) && !is_writable($filePath)) {
            throw new \InvalidArgumentException('File not writeable', 1701857185);
Patrick Schriner's avatar
Patrick Schriner committed
        }
        $command = sprintf(
            '%s --replace-input --linearize %s',
            escapeshellarg($this->extensionConfiguration->getQpdfToolPath()),
            escapeshellarg($filePath)
        );
        $output = [];
        $returnValue = 0;
        CommandUtility::exec($command, $output, $returnValue);
        if ($returnValue > 0) {
            $this->logger->log(LogLevel::ERROR, 'qpdf failed to linearize file', array_merge($output, ['file' => $filePath]));
        } else {
            $this->logger->log(LogLevel::DEBUG, 'qpdf linearized file', ['file' => $filePath]);
Patrick Schriner's avatar
Patrick Schriner committed
    /**
     * Get all "keep" tag-related shell arguments to exiftool
     *
     * @param FileInterface $file the processed file
     * @return string
     * @throws \InvalidArgumentException
     * @throws \UnexpectedValueException
Patrick Schriner's avatar
Patrick Schriner committed
     * @throws Exception
     */
    protected function getEscapedTagArguments(FileInterface $file): string
        $tags = [];
        if (strtolower($file->getMimeType()) == 'application/pdf') {
            $tags = $this->getKeepTagsForPdfFromStorage($file);
            $tags = array_merge($tags, $this->forcedKeepTagsForPdf);
            $tags = $this->getKeepTagsForImageFromStorage($file);
            $tags = array_merge($tags, $this->forcedKeepTagsForImage);
Patrick Schriner's avatar
Patrick Schriner committed
        $escapedTagArguments = array_map(function ($tag) {
            return escapeshellarg('-' . $tag);
        }, $tags);
        return implode(' ', $escapedTagArguments);
    /**
     * Get the exiftool_keep_pd_tags value of the file's storage as an array
     *
     * @return array<string>
     * @throws \InvalidArgumentException
     * @throws \UnexpectedValueException
     * @throws Exception
     */
    protected function getKeepTagsForPdfFromStorage(FileInterface $file): array
    {
        $storage = $file->getStorage();
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('sys_file_storage');
        $keepPdfTags = $queryBuilder->select('exiftool_keep_pdf_tags')
            ->from('sys_file_storage')
            ->where($queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($storage->getUid(), Connection::PARAM_INT)))
            ->executeQuery()
            ->fetchOne();
        if ($keepPdfTags === false) {
            $keepPdfTags = '';
        }
        return GeneralUtility::trimExplode(',', (string)$keepPdfTags, true);
    /**
     * Get the exiftool_keep_image_tags value of the file's storage as an array
     *
     * @return array<string>
     * @throws \InvalidArgumentException
     * @throws \UnexpectedValueException
     * @throws Exception
     */
    protected function getKeepTagsForImageFromStorage(FileInterface $file): array
    {
        $storage = $file->getStorage();
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('sys_file_storage');
        $keepImageTags = $queryBuilder->select('exiftool_keep_image_tags')
            ->from('sys_file_storage')
            ->where($queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($storage->getUid(), Connection::PARAM_INT)))
            ->executeQuery()
            ->fetchOne();
        if ($keepImageTags === false) {
            $keepImageTags = '';
        }
        return GeneralUtility::trimExplode(',', (string)$keepImageTags, true);

Consent

On this website, we use the web analytics service Matomo to analyze and review the use of our website. Through the collected statistics, we can improve our offerings and make them more appealing for you. Here, you can decide whether to allow us to process your data and set corresponding cookies for these purposes, in addition to technically necessary cookies. Further information on data protection—especially regarding "cookies" and "Matomo"—can be found in our privacy policy. You can withdraw your consent at any time.