owncloud/core

View on GitHub
lib/private/Preview.php

Summary

Maintainability
F
5 days
Test Coverage
<?php
/**
 * @author Björn Schießle <bjoern@schiessle.org>
 * @author Frank Karlitschek <frank@karlitschek.de>
 * @author Georg Ehrke <georg@owncloud.com>
 * @author Joas Schilling <coding@schilljs.com>
 * @author Jörn Friedrich Dreyer <jfd@butonic.de>
 * @author Lukas Reschke <lukas@statuscode.ch>
 * @author Morris Jobke <hey@morrisjobke.de>
 * @author Olivier Paroz <github@oparoz.com>
 * @author Robin Appelman <icewind@owncloud.com>
 * @author Roeland Jago Douma <rullzer@owncloud.com>
 * @author Thomas Müller <thomas.mueller@tmit.eu>
 * @author Tobias Kaminsky <tobias@kaminsky.me>
 *
 * @copyright Copyright (c) 2018, ownCloud GmbH
 * @license AGPL-3.0
 *
 * This code is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License, version 3,
 * as published by the Free Software Foundation.
 *
 * 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 Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License, version 3,
 * along with this program.  If not, see <http://www.gnu.org/licenses/>
 *
 */
namespace OC;

use OC\Files\Filesystem;
use OC\Files\View;
use OCA\Files_Sharing\SharedMount;
use OCP\Files\FileInfo;
use OCP\Files\Folder;
use OCP\Files\Node;
use OCP\Files\NotFoundException;
use OCP\IImage;
use OCP\Preview\IProvider2;
use OCP\Response;
use OCP\Util;

class Preview {
    //the thumbnail folder
    public const THUMBNAILS_FOLDER = 'thumbnails';

    public const MODE_FILL = 'fill';
    public const MODE_COVER = 'cover';

    //config
    private $maxScaleFactor;
    /** @var int maximum width allowed for a preview */
    private $configMaxWidth;
    /** @var int maximum height allowed for a preview */
    private $configMaxHeight;

    //fileview object
    private $userView;

    //vars
    /** @var Node | null */
    private $file;
    private $maxX;
    private $maxY;
    private $scalingUp;
    private $mimeType;
    private $keepAspect = false;
    private $mode = self::MODE_FILL;

    //used to calculate the size of the preview to generate
    /** @var int $maxPreviewWidth max width a preview can have */
    private $maxPreviewWidth;
    /** @var int $maxPreviewHeight max height a preview can have */
    private $maxPreviewHeight;
    /** @var int $previewWidth calculated width of the preview we're looking for */
    private $previewWidth;
    /** @var int $previewHeight calculated height of the preview we're looking for */
    private $previewHeight;

    /**
     * preview images object
     *
     * @var IImage
     */
    private $preview;
    /** @var string */
    private $versionId;

    /**
     * check if thumbnail or bigger version of thumbnail of file is cached
     *
     * @param string $user userid - if no user is given, OC_User::getUser will be used
     * @param Node|null $file The path to the file where you want a thumbnail from
     * @param int $maxX The maximum X size of the thumbnail. It can be smaller depending on the
     *     shape of the image
     * @param int $maxY The maximum Y size of the thumbnail. It can be smaller depending on the
     *     shape of the image
     * @param bool $scalingUp Disable/Enable up-scaling of previews
     * @param string $versionId
     * @throws \Exception
     */
    public function __construct(
        $user = '',
        $root = '/',
        Node $file = null,
        $maxX = 1,
        $maxY = 1,
        $scalingUp = true,
        $versionId = null
    ) {
        //init fileviews
        if ($user === '') {
            $user = \OC_User::getUser();
        }
        $this->userView = new View('/' . $user);

        //set config
        $sysConfig = \OC::$server->getConfig();
        $this->configMaxWidth = $sysConfig->getSystemValue('preview_max_x', 2048);
        $this->configMaxHeight = $sysConfig->getSystemValue('preview_max_y', 2048);
        $this->maxScaleFactor = $sysConfig->getSystemValue('preview_max_scale_factor', 2);

        //save parameters
        if ($file !== null) {
            $this->setFile($file, $versionId);
        }
        $this->setMaxX((int)$maxX);
        $this->setMaxY((int)$maxY);
        $this->setScalingup($scalingUp);

        //check if there are preview backends
        if (!\OC::$server->getPreviewManager()
                ->hasProviders()
            && \OC::$server->getConfig()
                ->getSystemValue('enable_previews', true)
        ) {
            Util::writeLog('core', 'No preview providers exist', Util::ERROR);
            throw new \Exception('No preview providers');
        }
    }

    /**
     * returns the node you want a thumbnail from
     *
     * @return Node | null
     */
    public function getFile() {
        return $this->file;
    }

    /**
     * returns the max width of the preview
     *
     * @return integer
     */
    public function getMaxX() {
        return $this->maxX;
    }

    /**
     * returns the max height of the preview
     *
     * @return integer
     */
    public function getMaxY() {
        return $this->maxY;
    }

    /**
     * returns whether or not scalingup is enabled
     *
     * @return bool
     */
    public function getScalingUp() {
        return $this->scalingUp;
    }

    /**
     * returns the name of the thumbnailfolder
     *
     * @return string
     */
    public function getThumbnailsFolder() {
        return self::THUMBNAILS_FOLDER;
    }

    /**
     * returns the max scale factor
     *
     * @return string
     */
    public function getMaxScaleFactor() {
        return $this->maxScaleFactor;
    }

    /**
     * returns the max width set in ownCloud's config
     *
     * @return integer
     */
    public function getConfigMaxX() {
        return $this->configMaxWidth;
    }

    /**
     * returns the max height set in ownCloud's config
     *
     * @return integer
     */
    public function getConfigMaxY() {
        return $this->configMaxHeight;
    }

    /**
     * Sets the path of the file you want a preview of
     *
     * @param Node $file
     * @param string $versionId
     *
     * @return Preview
     */
    public function setFile(Node $file, $versionId = null) {
        $this->file = $file;
        $this->versionId = $versionId;
        $this->mimeType = $this->file->getMimetype();

        return $this;
    }

    /**
     * Forces the use of a specific media type
     *
     * @param string $mimeType
     */
    public function setMimetype($mimeType) {
        $this->mimeType = $mimeType;
    }

    /**
     * Sets the max width of the preview. It's capped by the maximum allowed size set in the
     * configuration
     *
     * @param int $maxX
     *
     * @throws \Exception
     * @return \OC\Preview
     */
    public function setMaxX($maxX = 1) {
        if ($maxX <= 0) {
            throw new \Exception('Cannot set width of 0 or smaller!');
        }
        $configMaxX = $this->getConfigMaxX();
        $maxX = $this->limitMaxDim($maxX, $configMaxX, 'maxX');
        $this->maxX = $maxX;

        return $this;
    }

    /**
     * Sets the max height of the preview. It's capped by the maximum allowed size set in the
     * configuration
     *
     * @param int $maxY
     *
     * @throws \Exception
     * @return \OC\Preview
     */
    public function setMaxY($maxY = 1) {
        if ($maxY <= 0) {
            throw new \Exception('Cannot set height of 0 or smaller!');
        }
        $configMaxY = $this->getConfigMaxY();
        $maxY = $this->limitMaxDim($maxY, $configMaxY, 'maxY');
        $this->maxY = $maxY;

        return $this;
    }

    /**
     * Sets whether we're allowed to scale up when generating a preview. It's capped by the maximum
     * allowed scale factor set in the configuration
     *
     * @param bool $scalingUp
     *
     * @return \OC\Preview
     */
    public function setScalingup($scalingUp) {
        if ($this->getMaxScaleFactor() === 1) {
            $scalingUp = false;
        }
        $this->scalingUp = $scalingUp;

        return $this;
    }

    /**
     * Set whether to cover or fill the specified dimensions
     *
     * @param string $mode
     *
     * @return \OC\Preview
     */
    public function setMode($mode) {
        $this->mode = $mode;

        return $this;
    }

    /**
     * Sets whether we need to generate a preview which keeps the aspect ratio of the original file
     *
     * @param bool $keepAspect
     *
     * @return \OC\Preview
     */
    public function setKeepAspect($keepAspect) {
        $this->keepAspect = $keepAspect;

        return $this;
    }

    /**
     * Makes sure we were given a file to preview and that it exists in the filesystem
     *
     * @return bool
     */
    public function isFileValid() {
        $file = $this->getFile();
        if ($file === null) {
            Util::writeLog('core', 'No filename passed', Util::DEBUG);

            return false;
        }

        if (!$this->getFile() instanceof FileInfo) {
            Util::writeLog('core', 'File:"' . $file->getPath() . '" not found', Util::DEBUG);

            return false;
        }

        return true;
    }

    /**
     * Deletes the preview of a file with specific width and height
     *
     * This should never delete versions preview, use deleteAllPreviews() instead
     *
     * @param bool $keepMax - keep max size preview
     *
     * @return bool
     */
    public function deletePreview($keepMax = true) {
        $fileInfo = $this->getFile();
        if ($fileInfo !== null && $fileInfo !== false) {
            if ($keepMax === true) {
                $previewPath = $this->buildCachePath();
                if (!\strpos($previewPath, 'max')) {
                    return $this->userView->unlink($previewPath);
                }
            } else {
                $previewPath = $this->getPreviewPath();
                foreach ($this->userView->getDirectoryContent($previewPath) as $fileInfo) {
                    if ($fileInfo->getType() === FileInfo::TYPE_FILE) {
                        $this->userView->unlink("$previewPath/{$fileInfo->getName()}");
                    }
                }
                return true;
            }
        }

        return false;
    }

    /**
     * Checks if a preview matching the asked dimensions or a bigger version is already cached
     *
     *    * We first retrieve the size of the max preview since this is what we be used to create
     * all our preview. If it doesn't exist we return false, so that it can be generated
     *    * Using the dimensions of the max preview, we calculate what the size of the new
     * thumbnail should be
     *    * And finally, we look for a suitable candidate in the cache
     *
     * @return string|false path to the cached preview if it exists or false
     */
    public function isCached() {
        $fileId = $this->getFile()->getId();
        if ($fileId === null) {
            return false;
        }

        /**
         * Phase 1: Looking for the max preview
         */
        $previewPath = $this->getPreviewPath();
        // We currently can't look for a single file due to bugs related to #16478
        $allThumbnails = $this->userView->getDirectoryContent($previewPath);
        list($maxPreviewWidth, $maxPreviewHeight) = $this->getMaxPreviewSize($allThumbnails);

        // Only use the cache if we have a max preview
        if ($maxPreviewWidth !== null && $maxPreviewHeight !== null) {
            /**
             * Phase 2: Calculating the size of the preview we need to send back
             */
            $this->maxPreviewWidth = $maxPreviewWidth;
            $this->maxPreviewHeight = $maxPreviewHeight;

            list($previewWidth, $previewHeight) = $this->simulatePreviewDimensions();
            if (empty($previewWidth) || empty($previewHeight)) {
                return false;
            }

            $this->previewWidth = $previewWidth;
            $this->previewHeight = $previewHeight;

            /**
             * Phase 3: We look for a preview of the exact size
             */
            // This gives us a calculated path to a preview of asked dimensions
            // thumbnailFolder/fileId/<maxX>-<maxY>(-max|-with-aspect).png
            $preview = $this->buildCachePath($previewWidth, $previewHeight);

            // This checks if we have a preview of those exact dimensions in the cache
            if ($this->thumbnailSizeExists($allThumbnails, \basename($preview))) {
                return $preview;
            }

            /**
             * Phase 4: We look for a larger preview, matching the aspect ratio
             */
            if (($this->getMaxX() >= $maxPreviewWidth)
                && ($this->getMaxY() >= $maxPreviewHeight)
            ) {
                // The preview we-re looking for is the exact size or larger than the max preview,
                // so return that
                return $this->buildCachePath($maxPreviewWidth, $maxPreviewHeight);
            }

            // The last resort is to look for something bigger than what we've calculated,
            // but still smaller than the max preview
            return $this->isCachedBigger($allThumbnails);
        }

        return false;
    }

    /**
     * Returns the dimensions of the max preview
     *
     * @param FileInfo[] $allThumbnails the list of all our cached thumbnails
     *
     * @return int[]
     */
    private function getMaxPreviewSize($allThumbnails) {
        $maxPreviewX = null;
        $maxPreviewY = null;

        foreach ($allThumbnails as $thumbnail) {
            $name = $thumbnail['name'];
            if (\strpos($name, 'max')) {
                list($maxPreviewX, $maxPreviewY) = $this->getDimensionsFromFilename($name);
                break;
            }
        }

        return [$maxPreviewX, $maxPreviewY];
    }

    /**
     * Check if a specific thumbnail size is cached
     *
     * @param FileInfo[] $allThumbnails the list of all our cached thumbnails
     * @param string $name
     * @return bool
     */
    private function thumbnailSizeExists(array $allThumbnails, $name) {
        foreach ($allThumbnails as $thumbnail) {
            if ($name === $thumbnail->getName()) {
                return true;
            }
        }

        return false;
    }

    /**
     * Determines the size of the preview we should be looking for in the cache
     *
     * @return integer[]
     */
    private function simulatePreviewDimensions() {
        $askedWidth = $this->getMaxX();
        $askedHeight = $this->getMaxY();

        if ($this->keepAspect) {
            list($newPreviewWidth, $newPreviewHeight) =
                $this->applyAspectRatio($askedWidth, $askedHeight);
        } else {
            list($newPreviewWidth, $newPreviewHeight) = $this->fixSize($askedWidth, $askedHeight);
        }

        return [(int)$newPreviewWidth, (int)$newPreviewHeight];
    }

    /**
     * Resizes the boundaries to match the aspect ratio
     *
     * @param int $askedWidth
     * @param int $askedHeight
     *
     * @param int $originalWidth
     * @param int $originalHeight
     * @return integer[]
     */
    private function applyAspectRatio($askedWidth, $askedHeight, $originalWidth = 0, $originalHeight = 0) {
        if (!$originalWidth) {
            $originalWidth = $this->maxPreviewWidth;
        }
        if (!$originalHeight) {
            $originalHeight = $this->maxPreviewHeight;
        }
        $originalRatio = $originalWidth / $originalHeight;
        // Defines the box in which the preview has to fit
        $scaleFactor = $this->scalingUp ? $this->maxScaleFactor : 1;
        $askedWidth = \min($askedWidth, $originalWidth * $scaleFactor);
        $askedHeight = \min($askedHeight, $originalHeight * $scaleFactor);

        if ($askedWidth / $originalRatio < $askedHeight) {
            // width restricted
            $askedHeight = \round($askedWidth / $originalRatio);
        } else {
            $askedWidth = \round($askedHeight * $originalRatio);
        }

        return [(int)$askedWidth, (int)$askedHeight];
    }

    /**
     * Resizes the boundaries to cover the area
     *
     * @param int $askedWidth
     * @param int $askedHeight
     * @param int $previewWidth
     * @param int $previewHeight
     * @return integer[]
     */
    private function applyCover($askedWidth, $askedHeight, $previewWidth, $previewHeight) {
        $originalRatio = $previewWidth / $previewHeight;
        // Defines the box in which the preview has to fit
        $scaleFactor = $this->scalingUp ? $this->maxScaleFactor : 1;
        $askedWidth = \min($askedWidth, $previewWidth * $scaleFactor);
        $askedHeight = \min($askedHeight, $previewHeight * $scaleFactor);

        if ($askedWidth / $originalRatio > $askedHeight) {
            // height restricted
            $askedHeight = \round($askedWidth / $originalRatio);
        } else {
            $askedWidth = \round($askedHeight * $originalRatio);
        }

        return [(int)$askedWidth, (int)$askedHeight];
    }

    /**
     * Makes sure an upscaled preview doesn't end up larger than the max dimensions defined in the
     * config
     *
     * @param int $askedWidth
     * @param int $askedHeight
     *
     * @return integer[]
     */
    private function fixSize($askedWidth, $askedHeight) {
        if ($this->scalingUp) {
            $askedWidth = \min($this->configMaxWidth, $askedWidth);
            $askedHeight = \min($this->configMaxHeight, $askedHeight);
        }

        return [(int)$askedWidth, (int)$askedHeight];
    }

    /**
     * Checks if a bigger version of a file preview is cached and if not
     * return the preview of max allowed dimensions
     *
     * @param FileInfo[] $allThumbnails the list of all our cached thumbnails
     *
     * @return string path to bigger thumbnail
     */
    private function isCachedBigger($allThumbnails) {
        // This is used to eliminate any thumbnail narrower than what we need
        $maxX = $this->getMaxX();

        //array for usable cached thumbnails
        $possibleThumbnails = $this->getPossibleThumbnails($allThumbnails);

        foreach ($possibleThumbnails as $width => $path) {
            if ($width < $maxX) {
                continue;
            }

            return $path;
        }

        // At this stage, we didn't find a preview, so we return the max preview
        return $this->buildCachePath($this->maxPreviewWidth, $this->maxPreviewHeight);
    }

    /**
     * Get possible bigger thumbnails of the given image with the proper aspect ratio
     *
     * @param FileInfo[] $allThumbnails the list of all our cached thumbnails
     *
     * @return string[] an array of paths to bigger thumbnails
     */
    private function getPossibleThumbnails($allThumbnails) {
        if ($this->keepAspect) {
            $wantedAspectRatio = (float)($this->maxPreviewWidth / $this->maxPreviewHeight);
        } else {
            $wantedAspectRatio = (float)($this->getMaxX() / $this->getMaxY());
        }

        //array for usable cached thumbnails
        $possibleThumbnails = [];
        foreach ($allThumbnails as $thumbnail) {
            $name = \rtrim($thumbnail['name'], '.png');
            list($x, $y, $aspectRatio) = $this->getDimensionsFromFilename($name);
            if (\abs($aspectRatio - $wantedAspectRatio) >= 0.000001
                || $this->unscalable($x, $y)
            ) {
                continue;
            }
            $possibleThumbnails[$x] = $thumbnail['path'];
        }

        \ksort($possibleThumbnails);

        return $possibleThumbnails;
    }

    /**
     * Looks at the preview filename from the cache and extracts the size of the preview
     *
     * @param string $name
     *
     * @return array<int,int,float>
     */
    private function getDimensionsFromFilename($name) {
        $size = \explode('-', $name);
        $x = (int)$size[0];
        $y = (int)$size[1];
        $aspectRatio = (float)($x / $y);

        return [$x, $y, $aspectRatio];
    }

    /**
     * @param int $x
     * @param int $y
     *
     * @return bool
     */
    private function unscalable($x, $y) {
        $maxX = $this->getMaxX();
        $maxY = $this->getMaxY();
        $scalingUp = $this->getScalingUp();
        $maxScaleFactor = $this->getMaxScaleFactor();

        if ($x < $maxX || $y < $maxY) {
            if ($scalingUp) {
                $scaleFactor = $maxX / $x;
                if ($scaleFactor > $maxScaleFactor) {
                    return true;
                }
            } else {
                return true;
            }
        }

        return false;
    }

    /**
     * Returns a preview of a file
     *
     * The cache is searched first and if nothing usable was found then a preview is
     * generated by one of the providers
     *
     * @return IImage
     */
    public function getPreview() {
        if ($this->preview !== null && $this->preview->valid()) {
            return $this->preview;
        }

        $this->preview = null;
        $fileInfo = $this->getFile();
        if ($fileInfo === null || $fileInfo === false) {
            return new \OC_Image();
        }

        $cached = $this->isCached();
        if ($cached) {
            $this->getCachedPreview($cached);

            if ($this->preview) {
                $mount = $fileInfo->getMountPoint();

                // Updating a shared file will always re-generate the preview for the
                // share owner, but not its sharees. Therefore we need to check if the
                // current cached preview is different from the share owner's one.
                if ($mount instanceof SharedMount) {
                    $owner = $fileInfo->getOwner();
                    $ownerView = new View($owner->getUID() . '/' . $cached);
                    $ownerPreview = $ownerView->getFileInfo('');

                    if ($ownerPreview) {
                        $currentPreview = $this->userView->getFileInfo($cached);

                        // Different checksums mean we need to re-generate the thumbnail
                        // to match with the share owner's one.
                        if ($ownerPreview->getChecksum() !== $currentPreview->getChecksum()) {
                            $this->preview = null;
                        }
                    } else {
                        // Owner preview gets deleted when a sharee edits a file.
                        // Re-generate preview for the sharee in this case.
                        $this->preview = null;
                    }
                }
            }
        }

        if ($this->preview === null) {
            $this->generatePreview();
        }

        // We still don't have a preview, so we send back an empty object
        if ($this->preview === null) {
            $this->preview = new \OC_Image();
        }

        return $this->preview;
    }

    /**
     * Sends the preview, including the headers to client which requested it
     *
     * @param null|string $mimeTypeForHeaders the media type to use when sending back the reply
     *
     * @throws NotFoundException
     */
    public function showPreview($mimeTypeForHeaders = null) {
        // Check if file is valid
        if ($this->isFileValid() === false) {
            throw new NotFoundException('File not found.');
        }

        if ($this->preview === null) {
            $this->getPreview();
        }
        if ($this->preview instanceof IImage) {
            if ($this->preview->valid()) {
                Response::enableCaching(3600 * 24); // 24 hours
            } else {
                $this->getMimeIcon();
            }
            $this->preview->show($mimeTypeForHeaders);
        }
    }

    /**
     * Retrieves the preview from the cache and resizes it if necessary
     *
     * @param string $cached the path to the cached preview
     */
    private function getCachedPreview($cached) {
        $stream = $this->userView->fopen($cached, 'r');
        $this->preview = null;
        if ($stream) {
            $image = new \OC_Image();
            $image->loadFromFileHandle($stream);

            $this->preview = $image->valid() ? $image : null;

            if ($this->preview !== null) {
                // Size of the preview we calculated
                $maxX = $this->previewWidth;
                $maxY = $this->previewHeight;
                // Size of the preview we retrieved from the cache
                $previewX = (int)$this->preview->width();
                $previewY = (int)$this->preview->height();

                // We don't have an exact match
                if ($previewX !== $maxX || $previewY !== $maxY) {
                    $this->resizeAndStore();
                }
            }

            \fclose($stream);
        }
    }

    /**
     * Resizes, crops, fixes orientation and stores in the cache
     */
    private function resizeAndStore() {
        $image = $this->preview;
        if (!($image instanceof IImage)) {
            Util::writeLog(
                'core',
                '$this->preview is not an instance of \OCP\IImage',
                Util::DEBUG
            );

            return;
        }
        $previewWidth = $image->width();
        $previewHeight = $image->height();
        $askedWidth = $this->getMaxX();
        $askedHeight = $this->getMaxY();

        if ($this->mode === self::MODE_COVER) {
            list($askedWidth, $askedHeight) =
                $this->applyCover($askedWidth, $askedHeight, $previewWidth, $previewHeight);
        }

        /**
         * Phase 1: If required, adjust boundaries to keep aspect ratio
         */
        if ($this->keepAspect) {
            list($askedWidth, $askedHeight) =
                $this->applyAspectRatio($askedWidth, $askedHeight, $previewWidth, $previewHeight);
        }

        /**
         * Phase 2: Resizes preview to try and match requirements.
         * Takes the scaling ratio into consideration
         */
        list($newPreviewWidth, $newPreviewHeight) = $this->scale(
            $image,
            $askedWidth,
            $askedHeight,
            $previewWidth,
            $previewHeight
        );

        // The preview has been resized and should now have the asked dimensions
        if ($newPreviewWidth === $askedWidth && $newPreviewHeight === $askedHeight) {
            $this->storePreview($newPreviewWidth, $newPreviewHeight);

            return;
        }

        /**
         * Phase 3: We're still not there yet, so we're clipping and filling
         * to match the asked dimensions
         */
        // It turns out the scaled preview is now too big, so we crop the image
        if ($newPreviewWidth >= $askedWidth && $newPreviewHeight >= $askedHeight) {
            $this->crop($image, $askedWidth, $askedHeight, $newPreviewWidth, $newPreviewHeight);
            $this->storePreview($askedWidth, $askedHeight);

            return;
        }

        // At least one dimension of the scaled preview is too small,
        // so we fill the space with a transparent background
        if (($newPreviewWidth < $askedWidth || $newPreviewHeight < $askedHeight)) {
            $this->cropAndFill(
                $image,
                $askedWidth,
                $askedHeight,
                $newPreviewWidth,
                $newPreviewHeight
            );
            $this->storePreview($askedWidth, $askedHeight);

            return;
        }

        // The preview is smaller, but we can't touch it
        $this->storePreview($newPreviewWidth, $newPreviewHeight);
    }

    /**
     * Calculates the new dimensions of the preview
     *
     * The new dimensions can be larger or smaller than the ones of the preview we have to resize
     *
     * @param IImage $image
     * @param int $askedWidth
     * @param int $askedHeight
     * @param int $previewWidth
     * @param int $previewHeight
     *
     * @return int[]
     */
    private function scale($image, $askedWidth, $askedHeight, $previewWidth, $previewHeight) {
        $scalingUp = $this->getScalingUp();
        $maxScaleFactor = $this->getMaxScaleFactor();

        $factorX = $askedWidth / $previewWidth;
        $factorY = $askedHeight / $previewHeight;

        if ($factorX >= $factorY) {
            $factor = $factorX;
        } else {
            $factor = $factorY;
        }

        if ($scalingUp === false) {
            if ($factor > 1) {
                $factor = 1;
            }
        }

        // We cap when upscaling
        if ($maxScaleFactor !== null) {
            if ($factor > $maxScaleFactor) {
                Util::writeLog(
                    'core',
                    'scale factor reduced from ' . $factor . ' to ' . $maxScaleFactor,
                    Util::DEBUG
                );
                $factor = $maxScaleFactor;
            }
        }

        $newPreviewWidth = \round($previewWidth * $factor);
        $newPreviewHeight = \round($previewHeight * $factor);

        $image->preciseResize($newPreviewWidth, $newPreviewHeight);
        $this->preview = $image;

        return [$newPreviewWidth, $newPreviewHeight];
    }

    /**
     * Crops a preview which is larger than the dimensions we've received
     *
     * @param IImage $image
     * @param int $askedWidth
     * @param int $askedHeight
     * @param int $previewWidth
     * @param int $previewHeight
     */
    private function crop($image, $askedWidth, $askedHeight, $previewWidth, $previewHeight = null) {
        $cropX = \floor(\abs($askedWidth - $previewWidth) * 0.5);
        //don't crop previews on the Y axis, this sucks if it's a document.
        //$cropY = floor(abs($y - $newPreviewHeight) * 0.5);
        $cropY = 0;
        $image->crop($cropX, $cropY, $askedWidth, $askedHeight);
        $this->preview = $image;
    }

    /**
     * Crops an image if it's larger than the dimensions we've received and fills the empty space
     * with a transparent background
     *
     * @param IImage $image
     * @param int $askedWidth
     * @param int $askedHeight
     * @param int $previewWidth
     * @param int $previewHeight
     */
    private function cropAndFill($image, $askedWidth, $askedHeight, $previewWidth, $previewHeight) {
        if ($previewWidth > $askedWidth) {
            $cropX = \floor(($previewWidth - $askedWidth) * 0.5);
            $image->crop($cropX, 0, $askedWidth, $previewHeight);
            $previewWidth = $askedWidth;
        }

        if ($previewHeight > $askedHeight) {
            $cropY = \floor(($previewHeight - $askedHeight) * 0.5);
            $image->crop(0, $cropY, $previewWidth, $askedHeight);
            $previewHeight = $askedHeight;
        }

        // Creates a transparent background
        $backgroundLayer = \imagecreatetruecolor($askedWidth, $askedHeight);
        \imagealphablending($backgroundLayer, false);
        $transparency = \imagecolorallocatealpha($backgroundLayer, 0, 0, 0, 127);
        \imagefill($backgroundLayer, 0, 0, $transparency);
        \imagesavealpha($backgroundLayer, true);

        $image = $image->resource();

        $mergeX = \floor(\abs($askedWidth - $previewWidth) * 0.5);
        $mergeY = \floor(\abs($askedHeight - $previewHeight) * 0.5);

        // Pastes the preview on top of the background
        \imagecopy(
            $backgroundLayer,
            $image,
            $mergeX,
            $mergeY,
            0,
            0,
            $previewWidth,
            $previewHeight
        );

        $image = new \OC_Image($backgroundLayer);

        $this->preview = $image;
    }

    /**
     * Saves a preview in the cache to speed up future calls
     *
     * Do not nullify the preview as it might send the whole process in a loop
     *
     * @param int $previewWidth
     * @param int $previewHeight
     */
    private function storePreview($previewWidth, $previewHeight) {
        if (empty($previewWidth) || empty($previewHeight)) {
            Util::writeLog(
                'core',
                'Cannot save preview of dimension ' . $previewWidth . 'x' . $previewHeight,
                Util::DEBUG
            );
        } else {
            $cachePath = $this->buildCachePath($previewWidth, $previewHeight);
            $this->userView->file_put_contents($cachePath, $this->preview->data());
        }
    }

    /**
     * Returns the path to a preview based on its dimensions and aspect
     *
     * @param int|null $maxX
     * @param int|null $maxY
     *
     * @return string
     */
    private function buildCachePath($maxX = null, $maxY = null) {
        if ($maxX === null) {
            $maxX = $this->getMaxX();
        }
        if ($maxY === null) {
            $maxY = $this->getMaxY();
        }

        $previewPath = $this->getPreviewPath();
        $previewPath = $previewPath . \strval($maxX) . '-' . \strval($maxY);
        $isMaxPreview =
            ($maxX === $this->maxPreviewWidth && $maxY === $this->maxPreviewHeight) ? true : false;
        if ($isMaxPreview) {
            $previewPath .= '-max';
        }
        if ($this->keepAspect && !$isMaxPreview) {
            $previewPath .= '-with-aspect';
        }
        if ($this->mode === self::MODE_COVER) {
            $previewPath .= '-cover';
        }
        $previewPath .= '.png';

        return $previewPath;
    }

    /**
     * Returns the path to the folder where the previews are stored, identified by the fileId
     *
     * @param int $fileId (optional)
     *
     * @return string
     */
    private function getPreviewPath($fileId = null) {
        if ($fileId === null) {
            $fileId = $this->getFile()->getId();
            if ($this->versionId !== null) {
                $fileId .= "/{$this->versionId}";
            }
        }
        return $this->getThumbnailsFolder() . '/' . $fileId . '/';
    }

    /**
     * Asks the provider to send a preview of the file which respects the maximum dimensions
     * defined in the configuration and after saving it in the cache, it is then resized to the
     * asked dimensions
     *
     * This is only called once in order to generate a large PNG of dimensions defined in the
     * configuration file. We'll be able to quickly resize it later on.
     * We never upscale the original conversion as this will be done later by the resizing
     * operation
     *
     */
    private function generatePreview() {
        $file = $this->getFile();
        $preview = null;

        $previewProviders = \OC::$server->getPreviewManager()
            ->getProviders();
        foreach ($previewProviders as $supportedMimeType => $providers) {
            if (!\preg_match($supportedMimeType, $this->mimeType)) {
                continue;
            }

            foreach ($providers as $closure) {
                $provider = $closure();
                if (!($provider instanceof IProvider2)) {
                    continue;
                }

                Util::writeLog(
                    'core',
                    'Generating preview for "' . $file->getPath() . '" with "' . \get_class($provider)
                    . '"',
                    Util::DEBUG
                );

                $preview = $provider->getThumbnail($file, $this->configMaxWidth, $this->configMaxHeight, false);

                if (!($preview instanceof IImage)) {
                    continue;
                }

                $this->preview = $preview;
                $previewPath = $this->getPreviewPath();

                if ($this->userView->is_dir($this->getThumbnailsFolder() . '/') === false) {
                    $this->userView->mkdir($this->getThumbnailsFolder() . '/');
                }

                if ($this->userView->is_dir($previewPath) === false) {
                    $this->userView->mkdir($previewPath);
                }

                // This stores our large preview so that it can be used in subsequent resizing requests
                $this->storeMaxPreview($previewPath);

                break 2;
            }
        }

        // The providers have been kind enough to give us a preview
        if ($preview) {
            $this->resizeAndStore();
        }
    }

    /**
     * Defines the media icon, for the media type of the original file, as the preview
     */
    private function getMimeIcon() {
        $image = new \OC_Image();
        $mimeIconWebPath = \OC::$server->getMimeTypeDetector()->mimeTypeIcon($this->mimeType);
        if (empty(\OC::$WEBROOT)) {
            $mimeIconServerPath = \OC::$SERVERROOT . $mimeIconWebPath;
        } else {
            $mimeIconServerPath = \str_replace(\OC::$WEBROOT, \OC::$SERVERROOT, $mimeIconWebPath);
        }
        $image->loadFromFile($mimeIconServerPath);

        $this->preview = $image;
    }

    /**
     * Stores the max preview in the cache
     *
     * @param string $previewPath path to the preview
     */
    private function storeMaxPreview($previewPath) {
        $maxPreviewExists = false;
        $preview = $this->preview;

        $allThumbnails = $this->userView->getDirectoryContent($previewPath);
        // This is so that the cache doesn't need emptying when upgrading
        // Can be replaced by an upgrade script...
        foreach ($allThumbnails as $thumbnail) {
            $name = \rtrim($thumbnail['name'], '.png');
            if (\strpos($name, 'max')) {
                $maxPreviewExists = true;
                break;
            }
        }
        // We haven't found the max preview, so we create it
        if (!$maxPreviewExists) {
            $previewWidth = $preview->width();
            $previewHeight = $preview->height();
            $previewPath = $previewPath . \strval($previewWidth) . '-' . \strval($previewHeight);
            $previewPath .= '-max.png';
            $this->userView->file_put_contents($previewPath, $preview->data());
            $this->maxPreviewWidth = $previewWidth;
            $this->maxPreviewHeight = $previewHeight;
        }
    }

    /**
     * Limits a dimension to the maximum dimension provided as argument
     *
     * @param int $dim
     * @param int $maxDim
     * @param string $dimName
     *
     * @return integer
     */
    private function limitMaxDim($dim, $maxDim, $dimName) {
        if ($maxDim !== null) {
            if ($dim > $maxDim) {
                Util::writeLog(
                    'core',
                    $dimName . ' reduced from ' . $dim . ' to ' . $maxDim,
                    Util::DEBUG
                );
                $dim = $maxDim;
            }
        }

        return $dim;
    }

    /**
     * @param string[] $args
     */
    private static function getUserFolder($args): ?Folder {
        $user = $args['user'] ?? \OC_User::getUser();
        if ($user === false) {
            try {
                $path = Files\Filesystem::normalizePath($args['path']);
                $user = Filesystem::getOwner($path);
            } catch (NotFoundException $e) {
                return null;
            }
        }
        return \OC::$server->getUserFolder($user);
    }

    public static function post_write($args) {
        $path = Files\Filesystem::normalizePath($args['path']);
        $userFolder = self::getUserFolder($args);
        if (!$userFolder) {
            return;
        }
        if (\strpos($path, '/files_trashbin/') === 0) {
            $node = $userFolder->getParent()->get($path);
        } else {
            $node = $userFolder->get($path);
        }

        $preview = new Preview($node->getOwner()->getUID(), '', $node);
        $preview->deletePreview(false);
    }
}