Мобільна адаптація + Налаштування відступів, p-body-header та верхнього спайсера Збереження або видалення метаданних фотографій для певних користувачів Xenforo! | FMJ Portal – 24/7
Баннер

Збереження або видалення метаданних фотографій для певних користувачів Xenforo!

z3j anti z0r_o

‎z3j
FMJ Balance
☆ 59 Coins ☆
Всім привіт, я довго намагався налаштувати Xenforo так, щоб після завантаженні фотографій з метаданними з фотографії видалялись данні GPS, а інші данні не чіпались, для збереження моделі камери, дати зйомки та іншого, але не вийшло, тому одним з рішенням стало білий список з користувачів, метаданні яких при завантажнні фотографій не чіпаються взагалі, а для всіх інших як і було - видаляються.

Для цього потрібно обрати обробник зображень Imagick в панелі адміністратора Xenforo

Створюєм групу користувачів, дивимось її ID

Змінюєм файли Imagick та Preparer кодами нище

Замініть у кодах нище isMemberOf(19)) на свій id групи користувачів.

Шлях до Imagick.php - xenforo\src\XF\Image\Imagick.php
Шлях до Preparer.php - xenforo\src\XF\Service\Attachment\Preparer.php

Imagick.php

PHP:
<?php

namespace XF\Image;

class Imagick extends AbstractDriver
{
    protected $imagick;
    protected $keepMetadata = false;

    public static function isDriverUsable()
    {
        return class_exists('Imagick');
    }

    public function setKeepMetadata($keep)
    {
        $this->keepMetadata = (bool)$keep;
        return $this;
    }

    protected function _imageFromFile($file, $type)
    {
        switch ($type)
        {
            case IMAGETYPE_GIF:
            case IMAGETYPE_JPEG:
            case IMAGETYPE_PNG:
                $this->imagick = new \Imagick($file);
                break;
            default:
                throw new \InvalidArgumentException("Unknown image type '$type'");
        }

        // АВТО-ПЕРЕВІРКА ГРУПИ 19 ПРИ ВІДКРИТТІ
        if (\XF::visitor()->isMemberOf(19))
        {
            $this->keepMetadata = true;
        }

        $this->setImage($this->imagick);
        return true;
    }

    protected function _createImage($width, $height)
    {
        $image = new \Imagick();
        $image->newImage($width, $height, new \ImagickPixel('white'));
        $this->setImage($image);
        return true;
    }

    public function setImage(\Imagick $image)
    {
        $this->imagick = $image->coalesceImages();
        $this->updateDimensions();
    }

    public function getImage()
    {
        return $this->imagick;
    }

    protected function updateDimensions()
    {
        $this->width = $this->imagick->getImageWidth();
        $this->height = $this->imagick->getImageHeight();
    }

    public function resizeTo($width, $height)
    {
        foreach ($this->imagick as $frame)
        {
            $frame->thumbnailImage($width, $height, true);
        }
        $this->updateDimensions();
        return $this;
    }

    public function crop($width, $height, $x = 0, $y = 0, $srcWidth = null, $srcHeight = null)
    {
        foreach ($this->imagick as $frame)
        {
            $frame->cropImage($srcWidth ?: $width, $srcHeight ?: $height, $x, $y);
            $frame->thumbnailImage($width, $height, true);
        }
        $this->updateDimensions();
        return $this;
    }

    public function rotate($angle)
    {
        foreach ($this->imagick as $frame)
        {
            $frame->rotateImage(new \ImagickPixel('none'), $angle);
        }
        $this->updateDimensions();
        return $this;
    }

    public function flip($mode)
    {
        foreach ($this->imagick as $frame)
        {
            if ($mode === self::FLIP_HORIZONTAL) $frame->flopImage();
            elseif ($mode === self::FLIP_VERTICAL) $frame->flipImage();
        }
        $this->updateDimensions();
        return $this;
    }

    public function setOpacity($opacity)
    {
        foreach ($this->imagick as $frame)
        {
            $frame->evaluateImage(\Imagick::EVALUATE_MULTIPLY, $opacity, \Imagick::CHANNEL_ALPHA);
        }
        return $this;
    }

    public function appendImageAt($x, $y, $toAppend)
    {
        if ($toAppend instanceof \XF\Image\Imagick) { $toAppend = $toAppend->getImage(); }
        if (!($toAppend instanceof \Imagick)) { throw new \InvalidArgumentException('Invalid Imagick object'); }
        foreach ($this->imagick as $frame)
        {
            $frame->compositeImage($toAppend, \Imagick::COMPOSITE_OVER, $x, $y);
        }
        return $this;
    }

    public function save($file, $format = null, $quality = 85)
    {
        $this->applyStandardProcessing($format, $quality);

        if (!$this->keepMetadata)
        {
            $this->imagick->stripImage();
        }
        else
        {
            // ДЛЯ ГРУПИ 19: Фікс орієнтації через заголовок без видалення EXIF
            if (method_exists($this->imagick, 'setImageOrientation'))
            {
                $this->imagick->setImageOrientation(\Imagick::ORIENTATION_TOPLEFT);
            }
        }

        return $this->imagick->writeImages($file, true);
    }

    public function output($format = null, $quality = 85)
    {
        $this->applyStandardProcessing($format, $quality);
        if (!$this->keepMetadata) { $this->imagick->stripImage(); }
        else if (method_exists($this->imagick, 'setImageOrientation')) { $this->imagick->setImageOrientation(\Imagick::ORIENTATION_TOPLEFT); }
        echo $this->imagick->getImagesBlob();
    }

    protected function applyStandardProcessing($format, $quality)
    {
        if ($format === IMAGETYPE_JPEG)
        {
            $this->imagick->setImageFormat('jpeg');
            $this->imagick->setImageCompression(\Imagick::COMPRESSION_JPEG);
            $this->imagick->setImageCompressionQuality($quality);
        }
        elseif ($format === IMAGETYPE_PNG) $this->imagick->setImageFormat('png');
        elseif ($format === IMAGETYPE_GIF) $this->imagick->setImageFormat('gif');
    }

    public function isValid() { $this->imagick->setFirstIterator(); return $this->imagick->valid(); }
    public function __destruct() { if ($this->imagick) { $this->imagick->destroy(); $this->imagick = null; } }
}

Preparer.php
PHP:
<?php

namespace XF\Service\Attachment;

use XF\Util\File;

class Preparer extends \XF\Service\AbstractService
{
    public function insertAttachment(\XF\Attachment\AbstractHandler $handler, \XF\FileWrapper $file, \XF\Entity\User $user, $hash)
    {
        $extra = [];
        $extension = strtolower($file->getExtension());

        if (File::isVideoInlineDisplaySafe($extension))
        {
            $extra['file_path'] = 'data://video/%FLOOR%/%DATA_ID%-%HASH%.' . $extension;
        }
        else if (File::isAudioInlineDisplaySafe($extension))
        {
            $extra['file_path'] = 'data://audio/%FLOOR%/%DATA_ID%-%HASH%.' . $extension;
        }

        $handler->beforeNewAttachment($file, $extra);
        $data = $this->insertDataFromFile($file, $user->user_id, $extra);
        return $this->insertTemporaryAttachment($handler, $data, $hash, $file);
    }

    public function insertDataFromFile(\XF\FileWrapper $file, $userId, array $extra = [])
    {
        $data = $this->setupDataInsertFromFile($file, $userId, $extra);
        if (!$data->preSave())
        {
            throw new \XF\PrintableException($data->getErrors());
        }

        $sourceFile = $file->getFilePath();

        // Створення мініатюри (тут ми передаємо команду на збереження метаданих для групи 19)
        if ($data->width && $data->height && $this->app->imageManager()->canResize($data->width, $data->height))
        {
            $tempThumbFile = $this->generateAttachmentThumbnail($sourceFile, $thumbWidth, $thumbHeight);
            if ($tempThumbFile)
            {
                $data->set('thumbnail_width', $thumbWidth, ['forceSet' => true]);
                $data->set('thumbnail_height', $thumbHeight, ['forceSet' => true]);
            }
        }
        else
        {
            $tempThumbFile = null;
        }

        $this->db()->beginTransaction();

        $data->save(true, false);

        $dataPath = $data->getAbstractedDataPath();
        $thumbnailPath = $data->getAbstractedThumbnailPath();

        try
        {
            // Копіюємо оригінал (максимально стабільно, як в оригінальному коді)
            \XF\Util\File::copyFileToAbstractedPath($sourceFile, $dataPath);

            if ($tempThumbFile)
            {
                \XF\Util\File::copyFileToAbstractedPath($tempThumbFile, $thumbnailPath);
            }
        }
        catch (\Exception $e)
        {
            $this->db()->rollback();
            $this->app->em()->detachEntity($data);

            \XF\Util\File::deleteFromAbstractedPath($dataPath);

            if ($tempThumbFile)
            {
                \XF\Util\File::deleteFromAbstractedPath($thumbnailPath);
                @unlink($tempThumbFile);
            }

            throw $e;
        }

        $this->db()->commit();

        return $data;
    }

    protected function setupDataInsertFromFile(\XF\FileWrapper $file, $userId, array $extra = [])
    {
        $extra = array_replace([
            'file_path' => '',
            'upload_date' => null
        ], $extra);

        /** @var \XF\Entity\AttachmentData $data */
        $data = $this->app->em()->create('XF:AttachmentData');
        $data->user_id = $userId;
        $data->set('filename', $file->getFileName(), ['forceConstraint' => true]);
        $data->file_size = $file->getFileSize();
        $data->file_hash = md5_file($file->getFilePath());
        $data->file_path = $extra['file_path'];
        $data->width = $file->getImageWidth();
        $data->height = $file->getImageHeight();

        if ($extra['upload_date'])
        {
            $data->upload_date = $extra['upload_date'];
        }

        return $data;
    }

    public function updateDataFromFile(\XF\Entity\AttachmentData $data, \XF\FileWrapper $file, array $extra = [])
    {
        $this->setupDataUpdateFromFile($data, $file, $extra);
        if (!$data->preSave())
        {
            throw new \XF\PrintableException($data->getErrors());
        }

        $sourceFile = $file->getFilePath();
        $width = $data->width;
        $height = $data->height;

        $tempThumbFile = false;
        if ($data->isChanged('file_hash'))
        {
            if ($width && $height && $this->app->imageManager()->canResize($width, $height))
            {
                $tempThumbFile = $this->generateAttachmentThumbnail($sourceFile, $thumbWidth, $thumbHeight);
                if ($tempThumbFile)
                {
                    $data->set('thumbnail_width', $thumbWidth, ['forceSet' => true]);
                    $data->set('thumbnail_height', $thumbHeight, ['forceSet' => true]);
                }
            }
        }

        $this->db()->beginTransaction();

        $fileIsChanged = $data->isChanged(['file_hash', 'file_path']);
        if ($fileIsChanged)
        {
            $previousDataPath = $data->getExistingAbstractedDataPath();
            $previousThumbnailPath = $data->getExistingAbstractedThumbnailPath();
        }

        $data->saveIfChanged($dataChanged, true, false);

        if ($fileIsChanged && $dataChanged)
        {
            $dataPath = $data->getAbstractedDataPath();
            $thumbnailPath = $data->getAbstractedThumbnailPath();

            try
            {
                File::copyFileToAbstractedPath($sourceFile, $dataPath);

                if ($tempThumbFile)
                {
                    File::copyFileToAbstractedPath($tempThumbFile, $thumbnailPath);
                }
            }
            catch (\Exception $e)
            {
                $this->db()->rollback();
                $this->app->em()->detachEntity($data);

                throw $e;
            }

            File::deleteFromAbstractedPath($previousDataPath);
            File::deleteFromAbstractedPath($previousThumbnailPath);
        }

        $this->db()->commit();

        return $data;
    }

    protected function setupDataUpdateFromFile(\XF\Entity\AttachmentData $data, \XF\FileWrapper $file, array $extra = [])
    {
        $data->file_size = $file->getFileSize();
        $data->file_hash = md5_file($file->getFilePath());
        $data->width = $file->getImageWidth();
        $data->height = $file->getImageHeight();

        if (isset($extra['file_path']))
        {
            $data->file_path = $extra['file_path'];
        }
    }

    public function generateAttachmentThumbnail($sourceFile, &$width = null, &$height = null)
    {
        $image = $this->app->imageManager()->imageFromFile($sourceFile);
        if (!$image)
        {
            return null;
        }

        // ПЕРЕВІРКА ГРУПИ 19 ДЛЯ МІНІАТЮР
        if (method_exists($image, 'setKeepMetadata'))
        {
            $image->setKeepMetadata(\XF::visitor()->isMemberOf(19));
        }

        $thumbSize = $this->app->options()->attachmentThumbnailDimensions;
        $image->resizeShortEdge($thumbSize);

        $newTempFile = File::getTempFile();
        if ($newTempFile && $image->save($newTempFile))
        {
            $width = $image->getWidth();
            $height = $image->getHeight();
            unset($image); // Звільняємо пам'ять
            return $newTempFile;
        }
        else
        {
            return null;
        }
    }

    public function insertTemporaryAttachment(\XF\Attachment\AbstractHandler $handler, \XF\Entity\AttachmentData $data, $tempHash, \XF\FileWrapper $file)
    {
        $attachment = $this->app->em()->create('XF:Attachment');
        $attachment->data_id = $data->data_id;
        $attachment->content_type = $handler->getContentType();
        $attachment->temp_hash = $tempHash;
        $attachment->save();

        $handler->onNewAttachment($attachment, $file);

        return $attachment;
    }

    public function associateAttachmentsWithContent($tempHash, $contentType, $contentId)
    {
        $associated = 0;
        $attachmentFinder = $this->finder('XF:Attachment')->where('temp_hash', $tempHash);

        foreach ($attachmentFinder->fetch() AS $attachment)
        {
            $attachment->content_type = $contentType;
            $attachment->content_id = $contentId;
            $attachment->temp_hash = '';
            $attachment->unassociated = 0;

            $attachment->save();

            $container = $attachment->getContainer();
            $attachment->getHandler()->onAssociation($attachment, $container);

            $associated++;
        }

        return $associated;
    }
}



Додатковий архів з файлами, плюс оригінальні.

Цей код від 25 квітня 2026 року, з фіксом збереження портретної орієнтації для фото, у яких зберігаються метаданні.

Версію для збереження байт в байт шукайте нище.
 

Вкладення

  • save photo metadata imagick xenforo.zip
    8 КБ · Перегляди: 15
  • Imagick.php
    4.2 КБ · Перегляди: 15
  • Preparer.php
    7 КБ · Перегляди: 14
Останнє редагування:

z3j anti z0r_o

‎z3j
FMJ Balance
☆ 59 Coins ☆
Один мінус, портретні фото нажаль стають альбомними, пофіксити не вийшло

шок пофіксив декілька днів вийшло, стара версія знизу, нова зверху!
 
Останнє редагування:

z3j anti z0r_o

‎z3j
FMJ Balance
☆ 59 Coins ☆
Old версія, тут глюк з портретною орієнтацію зображення для тих, хто в групі користувачів, але це перший працюючий варіант, залишаю копію тут для історії версій, актуальний код в першому повідомленні теми.

Всім привіт, я довго намагався налаштувати Xenforo так, щоб при завантаженні фотографій з метаданних фотографії видалялись данні GPS, а інші данні не чіпались, для збереження історії, моделі камери, дати зйомки та інше, але не вийшло, тому одним з рішенням стало білий список з користувачів, метаданні яких при завантажнні фотографій не чіпаються взагалі, а всіх інших як і було - видаляються.

Для цього потрібно обрати обробник зображень Imagick в панелі адміністратора Xenforo

Пропоную два варіанти.

Варіант 1. Для групи користувачів, якщо користувач знаходиться в групі користувачів, id якої прописаний в файлах Imagick та Preparer, то з їх фотографій метаданні не видаляються.

Створюєм групу користувачів, дивимось її ID, та змінюєм файли Imagick та Preparer.

Замініть у кодах нище всі "$isTrusted = $visitor->isMemberOf(19);" Замініть 19, на свій id групи користувачів.

Шлях до Imagick.php - xenforo\src\XF\Image\Imagick.php
Шлях до Preparer.php - xenforo\src\XF\Service\Attachment\Preparer.php

Imagick.php

PHP:
<?php

namespace XF\Image;

class Imagick extends AbstractDriver
{
    /**
     * @var \Imagick
     */
    protected $imagick;

    public static function isDriverUsable()
    {
        return class_exists('Imagick');
    }

    protected function _imageFromFile($file, $type)
    {
        switch ($type)
        {
            case IMAGETYPE_GIF:
            case IMAGETYPE_JPEG:
            case IMAGETYPE_PNG:
                $image = new \Imagick($file);
                break;

            default:
                throw new \InvalidArgumentException("Unknown image type '$type'");
        }

        $this->setImage($image);

        return true;
    }

    protected function _createImage($width, $height)
    {
        $image = new \Imagick();
        $background = new \ImagickPixel('white');
        $image->newImage($width, $height, $background);

        $this->setImage($image);

        return true;
    }

    public function getImage()
    {
        return $this->imagick;
    }

    public function setImage(\Imagick $image)
    {
        $image->setImageBackgroundColor(new \ImagickPixel('transparent'));
        $image = $image->coalesceImages();

        $this->imagick = $image;

        $this->updateDimensions();
    }

    protected function updateDimensions()
    {
        $this->width = $this->imagick->getImageWidth();
        $this->height = $this->imagick->getImageHeight();
    }

    protected function isOldImagick()
    {
        $oldImagick = version_compare(phpversion('imagick'), '3', '<');

        $version = $this->imagick->getVersion();
        if (preg_match('#ImageMagick (\d+\.\d+\.\d+)#i', $version['versionString'], $match))
        {
            if (version_compare($match[1], '6.3.2', '<'))
            {
                $oldImagick = true;
            }
        }

        return $oldImagick;
    }

    public function resizeTo($width, $height)
    {
        $scaleUp = ($width > $this->width || $height > $this->height);

        try
        {
            foreach ($this->imagick AS $frame)
            {
                if ($scaleUp)
                {
                    $frame->resizeImage($width, $height, \Imagick::FILTER_QUADRATIC, .5, true);
                }
                else if ($this->isOldImagick())
                {
                    $frame->thumbnailImage($width, $height, false);
                }
                else
                {
                    $frame->thumbnailImage($width, $height, false, true);
                }
                $frame->setImagePage($width, $height, 0, 0);
            }

            $this->updateDimensions();
        }
        catch (\Exception $e) {}

        return $this;
    }

    public function crop($width, $height, $x = 0, $y = 0, $srcWidth = null, $srcHeight = null)
    {
        foreach ($this->imagick AS $frame)
        {
            $frame->cropImage($srcWidth ?: $width, $srcHeight ?: $height, $x, $y);
            if ($this->isOldImagick())
            {
                $frame->thumbnailImage($width, $height, false);
            }
            else
            {
                $frame->thumbnailImage($width, $height, false, true);
            }
            $frame->setImagePage($frame->getImageWidth(), $frame->getImageHeight(), 0, 0);
        }
        $this->updateDimensions();

        return $this;
    }

    public function rotate($angle)
    {
        foreach ($this->imagick AS $frame)
        {
            $frame->rotateImage(new \ImagickPixel('none'), $angle);
        }
        $this->updateDimensions();

        return $this;
    }

    public function flip($mode)
    {
        foreach ($this->imagick AS $frame)
        {
            switch ($mode)
            {
                case self::FLIP_HORIZONTAL:
                    $frame->flopImage();
                    break;

                case self::FLIP_VERTICAL:
                    $frame->flipImage();
                    break;

                case self::FLIP_BOTH:
                    $frame->flopImage();
                    $frame->flipImage();
                    break;

                default:
                    throw new \InvalidArgumentException("Unknown flip mode");
            }
        }

        $this->updateDimensions();

        return $this;
    }

    public function setOpacity($opacity)
    {
        foreach ($this->imagick AS $frame)
        {
            $frame->evaluateImage(\Imagick::EVALUATE_MULTIPLY, $opacity, \Imagick::CHANNEL_ALPHA);
        }

        return $this;
    }

    public function appendImageAt($x, $y, $toAppend)
    {
        if (!($toAppend instanceof \Imagick))
        {
            throw new \InvalidArgumentException('Image to append must be a valid Imagick object.');
        }

        foreach ($this->imagick AS $frame)
        {
            $frame->compositeImage($toAppend, $toAppend->getImageCompose(), $x, $y);
        }

        return $this;
    }

    protected function _unsharpMask($radius, $sigma, $amount, $threshold)
    {
        foreach ($this->imagick AS $frame)
        {
            $frame->unsharpMaskImage($radius, $sigma, $amount, $threshold);
        }

        return $this;
    }

    public function save($file, $format = null, $quality = null)
    {
        if ($format === null)
        {
            $format = $this->type;
        }

        // FMJ.WORLD: Якість 100%
        $quality = 100;

        // Перевірка на групу 19 (VIP/Довірені)
        $visitor = \XF::visitor();
        $isTrusted = $visitor->isMemberOf(19);

        if (!$isTrusted)
        {
            if (method_exists($this->imagick, 'getImageProfiles'))
            {
                $profiles = $this->imagick->getImageProfiles('icc');
                $this->imagick->stripImage();

                if ($profiles && !empty($profiles['icc']))
                {
                    $this->imagick->setImageProfile('icc', $profiles['icc']);
                }
            }
            else
            {
                $this->imagick->stripImage();
            }
        }

        switch ($format)
        {
            case IMAGETYPE_GIF:
                if (is_callable(array($this->imagick, 'optimizeimagelayers')))
                {
                    $optimized = @$this->imagick->optimizeimagelayers();
                    if ($optimized instanceof \Imagick)
                    {
                        $this->imagick = $optimized;
                    }

                    $deconstructed = @$this->imagick->deconstructImages();
                    if ($deconstructed instanceof \Imagick)
                    {
                        $this->imagick = $deconstructed;
                    }
                }
                $success = $this->imagick->setImageFormat('gif');
                break;

            case IMAGETYPE_JPEG:
                $success = $this->imagick->setImageFormat('jpeg')
                    && $this->imagick->setImageCompression(\Imagick::COMPRESSION_JPEG)
                    && $this->imagick->setImageCompressionQuality($quality);
                break;

            case IMAGETYPE_PNG:
                $success = $this->imagick->setImageFormat('png');
                break;

            default:
                throw new \InvalidArgumentException('Invalid format given. Expects IMAGETYPE_XXX constant.');
        }

        if ($success)
        {
            try
            {
                return $this->imagick->writeImages($file, true);
            }
            catch (\ImagickException $e) {}
        }

        return false;
    }

    public function output($format = null, $quality = null)
    {
        if ($format === null)
        {
            $format = $this->type;
        }

        $quality = 100;

        $visitor = \XF::visitor();
        if (!$visitor->isMemberOf(19))
        {
            $this->imagick->stripImage();
        }

        switch ($format)
        {
            case IMAGETYPE_GIF:
                $success = $this->imagick->optimizeImageLayers();
                break;

            case IMAGETYPE_JPEG:
                $success = $this->imagick->setImageFormat('jpeg')
                    && $this->imagick->setImageCompression(\Imagick::COMPRESSION_JPEG)
                    && $this->imagick->setImageCompressionQuality($quality);
                break;

            case IMAGETYPE_PNG:
                $success = $this->imagick->setImageFormat('png');
                break;

            default:
                throw new \InvalidArgumentException('Invalid format given. Expects IMAGETYPE_XXX constant.');
        }

        if ($success)
        {
            try
            {
                echo $this->imagick->getImagesBlob();
            }
            catch (\ImagickException $e) {}
        }
    }

    public function isValid()
    {
        $this->imagick->setFirstIterator();

        return $this->imagick->valid();
    }

    public function __destruct()
    {
        if ($this->imagick)
        {
            $this->imagick->destroy();
            $this->imagick = null;
        }
    }
}

Preparer.php
PHP:
<?php

namespace XF\Service\Attachment;

use XF\Util\File;

class Preparer extends \XF\Service\AbstractService
{
    public function insertAttachment(
        \XF\Attachment\AbstractHandler $handler, \XF\FileWrapper $file, \XF\Entity\User $user, $hash
    )
    {
        $extra = [];

        $extension = strtolower($file->getExtension());

        if (File::isVideoInlineDisplaySafe($extension))
        {
            $extra['file_path'] = 'data://video/%FLOOR%/%DATA_ID%-%HASH%.' . $extension;
        }
        else if (File::isAudioInlineDisplaySafe($extension))
        {
            $extra['file_path'] = 'data://audio/%FLOOR%/%DATA_ID%-%HASH%.' . $extension;
        }

        $handler->beforeNewAttachment($file, $extra);

        $data = $this->insertDataFromFile($file, $user->user_id, $extra);
        return $this->insertTemporaryAttachment($handler, $data, $hash, $file);
    }

    public function insertDataFromFile(\XF\FileWrapper $file, $userId, array $extra = [])
    {
        $data = $this->setupDataInsertFromFile($file, $userId, $extra);
        if (!$data->preSave())
        {
            throw new \XF\PrintableException($data->getErrors());
        }

        $sourceFile = $file->getFilePath();
        $width = $data->width;
        $height = $data->height;

        // FMJ.WORLD: Очистка GPS для тих, хто НЕ в групі 19
        $visitor = \XF::visitor();
        if ($width && $height && !$visitor->isMemberOf(19))
        {
            if (class_exists('Imagick'))
            {
                try
                {
                    $im = new \Imagick($sourceFile);
                    $im->stripImage();
                    $im->writeImage($sourceFile);
                    $im->clear();
                    $im->destroy();
                
                    clearstatcache();
                    $data->set('file_size', filesize($sourceFile), ['forceSet' => true]);
                    $data->set('file_hash', md5_file($sourceFile), ['forceSet' => true]);
                }
                catch (\Exception $e) {}
            }
        }

        if ($width && $height && $this->app->imageManager()->canResize($width, $height))
        {
            $tempThumbFile = $this->generateAttachmentThumbnail($sourceFile, $thumbWidth, $thumbHeight);
            if ($tempThumbFile)
            {
                $data->set('thumbnail_width', $thumbWidth, ['forceSet' => true]);
                $data->set('thumbnail_height', $thumbHeight, ['forceSet' => true]);
            }
        }
        else
        {
            $tempThumbFile = null;
        }

        $this->db()->beginTransaction();

        $data->save(true, false);

        $dataPath = $data->getAbstractedDataPath();
        $thumbnailPath = $data->getAbstractedThumbnailPath();

        try
        {
            \XF\Util\File::copyFileToAbstractedPath($sourceFile, $dataPath);

            if ($tempThumbFile)
            {
                \XF\Util\File::copyFileToAbstractedPath($tempThumbFile, $thumbnailPath);
            }
        }
        catch (\Exception $e)
        {
            $this->db()->rollback();
            $this->app->em()->detachEntity($data);

            \XF\Util\File::deleteFromAbstractedPath($dataPath);

            if ($tempThumbFile)
            {
                \XF\Util\File::deleteFromAbstractedPath($thumbnailPath);
                @unlink($tempThumbFile);
            }

            throw $e;
        }

        $this->db()->commit();

        return $data;
    }

    protected function setupDataInsertFromFile(
        \XF\FileWrapper $file,
        $userId,
        array $extra = []
    )
    {
        $extra = array_replace([
            'file_path' => '',
            'upload_date' => null
        ], $extra);

        $data = $this->app->em()->create('XF:AttachmentData');
        $data->user_id = $userId;
        $data->set('filename', $file->getFileName(), ['forceConstraint' => true]);
        $data->file_size = $file->getFileSize();
        $data->file_hash = md5_file($file->getFilePath());
        $data->file_path = $extra['file_path'];
        $data->width = $file->getImageWidth();
        $data->height = $file->getImageHeight();

        if ($extra['upload_date'])
        {
            $data->upload_date = $extra['upload_date'];
        }

        return $data;
    }

    public function updateDataFromFile(\XF\Entity\AttachmentData $data, \XF\FileWrapper $file, array $extra = [])
    {
        $this->setupDataUpdateFromFile($data, $file, $extra);
        if (!$data->preSave())
        {
            throw new \XF\PrintableException($data->getErrors());
        }

        $sourceFile = $file->getFilePath();
        $width = $data->width;
        $height = $data->height;

        // FMJ.WORLD: Очистка при оновленні для НЕ VIP
        $visitor = \XF::visitor();
        if ($width && $height && !$visitor->isMemberOf(19))
        {
            if (class_exists('Imagick'))
            {
                try
                {
                    $im = new \Imagick($sourceFile);
                    $im->stripImage();
                    $im->writeImage($sourceFile);
                    $im->clear();
                    $im->destroy();
                
                    clearstatcache();
                    $data->set('file_size', filesize($sourceFile), ['forceSet' => true]);
                    $data->set('file_hash', md5_file($sourceFile), ['forceSet' => true]);
                }
                catch (\Exception $e) {}
            }
        }

        $tempThumbFile = false;
        if ($data->isChanged('file_hash'))
        {
            if ($width && $height && $this->app->imageManager()->canResize($width, $height))
            {
                $tempThumbFile = $this->generateAttachmentThumbnail($sourceFile, $thumbWidth, $thumbHeight);
                if ($tempThumbFile)
                {
                    $data->set('thumbnail_width', $thumbWidth, ['forceSet' => true]);
                    $data->set('thumbnail_height', $thumbHeight, ['forceSet' => true]);
                }
            }
        }

        $this->db()->beginTransaction();

        $previousDataPath = null;
        $previousThumbnailPath = null;

        $fileIsChanged = $data->isChanged(['file_hash', 'file_path']);
        if ($fileIsChanged)
        {
            $previousDataPath = $data->getExistingAbstractedDataPath();
            $previousThumbnailPath = $data->getExistingAbstractedThumbnailPath();
        }

        $data->saveIfChanged($dataChanged, true, false);

        if ($fileIsChanged && $dataChanged)
        {
            $dataPath = $data->getAbstractedDataPath();
            $thumbnailPath = $data->getAbstractedThumbnailPath();

            try
            {
                File::copyFileToAbstractedPath($sourceFile, $dataPath);

                if ($tempThumbFile)
                {
                    File::copyFileToAbstractedPath($tempThumbFile, $thumbnailPath);
                }
            }
            catch (\Exception $e)
            {
                $this->db()->rollback();
                $this->app->em()->detachEntity($data);

                throw $e;
            }

            File::deleteFromAbstractedPath($previousDataPath);
            File::deleteFromAbstractedPath($previousThumbnailPath);
        }

        $this->db()->commit();

        return $data;
    }

    protected function setupDataUpdateFromFile(
        \XF\Entity\AttachmentData $data,
        \XF\FileWrapper $file,
        array $extra = []
    )
    {
        $data->file_size = $file->getFileSize();
        $data->file_hash = md5_file($file->getFilePath());
        $data->width = $file->getImageWidth();
        $data->height = $file->getImageHeight();

        if (isset($extra['file_path']))
        {
            $data->file_path = $extra['file_path'];
        }
    }

    public function generateAttachmentThumbnail($sourceFile, &$width = null, &$height = null)
    {
        $image = $this->app->imageManager()->imageFromFile($sourceFile);
        if (!$image)
        {
            return null;
        }

        $thumbSize = $this->app->options()->attachmentThumbnailDimensions;
        $image->resizeShortEdge($thumbSize);

        $newTempFile = File::getTempFile();
        if ($newTempFile && $image->save($newTempFile))
        {
            $width = $image->getWidth();
            $height = $image->getHeight();

            return $newTempFile;
        }
        else
        {
            return null;
        }
    }

    public function insertTemporaryAttachment(
        \XF\Attachment\AbstractHandler $handler,
        \XF\Entity\AttachmentData $data,
        $tempHash,
        \XF\FileWrapper $file
    )
    {
        $attachment = $this->app->em()->create('XF:Attachment');
        $attachment->data_id = $data->data_id;
        $attachment->content_type = $handler->getContentType();
        $attachment->temp_hash = $tempHash;
        $attachment->save();

        $handler->onNewAttachment($attachment, $file);
        return $attachment;
    }

    public function associateAttachmentsWithContent($tempHash, $contentType, $contentId)
    {
        $associated = 0;
        $attachmentFinder = $this->finder('XF:Attachment')->where('temp_hash', $tempHash);

        foreach ($attachmentFinder->fetch() AS $attachment)
        {
            $attachment->content_type = $contentType;
            $attachment->content_id = $contentId;
            $attachment->temp_hash = '';
            $attachment->unassociated = 0;
            $attachment->save();

            $container = $attachment->getContainer();
            $attachment->getHandler()->onAssociation($attachment, $container);
            $associated++;
        }
        return $associated;
    }
}




Варіант 2.


Цей варіант працює без створення групи користувачів, але потрібно вручну прописати ID всіх користувачів, метаданні яких не повинні видалятись.

Змінити файл Imagick.php та Preparer.php, коди яких нижче.

Для внесення користувача в білий список, знайдіть в Preparer.php $allowedUserIds = [4, 2, 3]; та впишіть свої айді користувачів, метаданні яких не повинні видалятись.

Шлях до Imagick.php - xenforo\src\XF\Image\Imagick.php
Шлях до Preparer.php - xenforo\src\XF\Service\Attachment\Preparer.php

Imagick.php
Код:
<?php

namespace XF\Image;

class Imagick extends AbstractDriver
{
    /**
     * @var \Imagick
     */
    protected $imagick;

    public static function isDriverUsable()
    {
        return class_exists('Imagick');
    }

    protected function _imageFromFile($file, $type)
    {
        switch ($type)
        {
            case IMAGETYPE_GIF:
            case IMAGETYPE_JPEG:
            case IMAGETYPE_PNG:
                $image = new \Imagick($file);
                break;

            default:
                throw new \InvalidArgumentException("Unknown image type '$type'");
        }

        $this->setImage($image);

        return true;
    }

    protected function _createImage($width, $height)
    {
        $image = new \Imagick();
        $background = new \ImagickPixel('white');
        $image->newImage($width, $height, $background);

        $this->setImage($image);

        return true;
    }

    public function getImage()
    {
        return $this->imagick;
    }

    public function setImage(\Imagick $image)
    {
        $image->setImageBackgroundColor(new \ImagickPixel('transparent'));
        $image = $image->coalesceImages();

        $this->imagick = $image;

        $this->updateDimensions();
    }

    protected function updateDimensions()
    {
        $this->width = $this->imagick->getImageWidth();
        $this->height = $this->imagick->getImageHeight();
    }

    protected function isOldImagick()
    {
        $oldImagick = version_compare(phpversion('imagick'), '3', '<');

        $version = $this->imagick->getVersion();
        if (preg_match('#ImageMagick (\d+\.\d+\.\d+)#i', $version['versionString'], $match))
        {
            if (version_compare($match[1], '6.3.2', '<'))
            {
                $oldImagick = true;
            }
        }

        return $oldImagick;
    }

    public function resizeTo($width, $height)
    {
        $scaleUp = ($width > $this->width || $height > $this->height);

        try
        {
            foreach ($this->imagick AS $frame)
            {
                if ($scaleUp)
                {
                    $frame->resizeImage($width, $height, \Imagick::FILTER_QUADRATIC, .5, true);
                }
                else if ($this->isOldImagick())
                {
                    $frame->thumbnailImage($width, $height, false);
                }
                else
                {
                    $frame->thumbnailImage($width, $height, false, true);
                }
                $frame->setImagePage($width, $height, 0, 0);
            }

            $this->updateDimensions();
        }
        catch (\Exception $e) {}

        return $this;
    }

    public function crop($width, $height, $x = 0, $y = 0, $srcWidth = null, $srcHeight = null)
    {
        foreach ($this->imagick AS $frame)
        {
            $frame->cropImage($srcWidth ?: $width, $srcHeight ?: $height, $x, $y);
            if ($this->isOldImagick())
            {
                $frame->thumbnailImage($width, $height, false);
            }
            else
            {
                $frame->thumbnailImage($width, $height, false, true);
            }
            $frame->setImagePage($frame->getImageWidth(), $frame->getImageHeight(), 0, 0);
        }
        $this->updateDimensions();

        return $this;
    }

    public function rotate($angle)
    {
        foreach ($this->imagick AS $frame)
        {
            $frame->rotateImage(new \ImagickPixel('none'), $angle);
        }
        $this->updateDimensions();

        return $this;
    }

    public function flip($mode)
    {
        foreach ($this->imagick AS $frame)
        {
            switch ($mode)
            {
                case self::FLIP_HORIZONTAL:
                    $frame->flopImage();
                    break;

                case self::FLIP_VERTICAL:
                    $frame->flipImage();
                    break;

                case self::FLIP_BOTH:
                    $frame->flopImage();
                    $frame->flipImage();
                    break;

                default:
                    throw new \InvalidArgumentException("Unknown flip mode");
            }
        }

        $this->updateDimensions();

        return $this;
    }

    public function setOpacity($opacity)
    {
        foreach ($this->imagick AS $frame)
        {
            $frame->evaluateImage(\Imagick::EVALUATE_MULTIPLY, $opacity, \Imagick::CHANNEL_ALPHA);
        }

        return $this;
    }

    public function appendImageAt($x, $y, $toAppend)
    {
        if (!($toAppend instanceof \Imagick))
        {
            throw new \InvalidArgumentException('Image to append must be a valid Imagick object.');
        }

        foreach ($this->imagick AS $frame)
        {
            $frame->compositeImage($toAppend, $toAppend->getImageCompose(), $x, $y);
        }

        return $this;
    }

    protected function _unsharpMask($radius, $sigma, $amount, $threshold)
    {
        foreach ($this->imagick AS $frame)
        {
            $frame->unsharpMaskImage($radius, $sigma, $amount, $threshold);
        }

        return $this;
    }

    public function save($file, $format = null, $quality = null)
    {
        if ($format === null)
        {
            $format = $this->type;
        }

        // --- FMJ.WORLD CUSTOM LOGIC ---
        $quality = 100; // Максимальна якість

        // Список ID користувачів, чиї метадані НЕ видаляються (наприклад, адмін = 1)
        $allowedUserIds = [4, 2, 3];
        $visitor = \XF::visitor();
        $isTrusted = in_array($visitor->user_id, $allowedUserIds);

        if (!$isTrusted)
        {
            if (method_exists($this->imagick, 'getImageProfiles'))
            {
                $profiles = $this->imagick->getImageProfiles('icc');
                $this->imagick->stripImage();

                if ($profiles && !empty($profiles['icc']))
                {
                    $this->imagick->setImageProfile('icc', $profiles['icc']);
                }
            }
            else
            {
                $this->imagick->stripImage();
            }
        }
        // --- END CUSTOM LOGIC ---

        switch ($format)
        {
            case IMAGETYPE_GIF:
                if (is_callable(array($this->imagick, 'optimizeimagelayers')))
                {
                    $optimized = @$this->imagick->optimizeimagelayers();
                    if ($optimized instanceof \Imagick)
                    {
                        $this->imagick = $optimized;
                    }

                    $deconstructed = @$this->imagick->deconstructImages();
                    if ($deconstructed instanceof \Imagick)
                    {
                        $this->imagick = $deconstructed;
                    }
                }
                $success = $this->imagick->setImageFormat('gif');
                break;

            case IMAGETYPE_JPEG:
                $success = $this->imagick->setImageFormat('jpeg')
                    && $this->imagick->setImageCompression(\Imagick::COMPRESSION_JPEG)
                    && $this->imagick->setImageCompressionQuality($quality);
                break;

            case IMAGETYPE_PNG:
                $success = $this->imagick->setImageFormat('png');
                break;

            default:
                throw new \InvalidArgumentException('Invalid format given. Expects IMAGETYPE_XXX constant.');
        }

        if ($success)
        {
            try
            {
                return $this->imagick->writeImages($file, true);
            }
            catch (\ImagickException $e) {}
        }

        return false;
    }

    public function output($format = null, $quality = null)
    {
        if ($format === null)
        {
            $format = $this->type;
        }

        if ($quality === null)
        {
            $quality = 100;
        }

        // Для виводу в браузер також застосовуємо логіку безпеки
        $allowedUserIds = [1, 2, 3];
        $visitor = \XF::visitor();
        if (!in_array($visitor->user_id, $allowedUserIds))
        {
            $this->imagick->stripImage();
        }

        switch ($format)
        {
            case IMAGETYPE_GIF:
                $success = $this->imagick->optimizeImageLayers();
                break;

            case IMAGETYPE_JPEG:
                $success = $this->imagick->setImageFormat('jpeg')
                    && $this->imagick->setImageCompression(\Imagick::COMPRESSION_JPEG)
                    && $this->imagick->setImageCompressionQuality($quality);
                break;

            case IMAGETYPE_PNG:
                $success = $this->imagick->setImageFormat('png');
                break;

            default:
                throw new \InvalidArgumentException('Invalid format given. Expects IMAGETYPE_XXX constant.');
        }

        if ($success)
        {
            try
            {
                echo $this->imagick->getImagesBlob();
            }
            catch (\ImagickException $e) {}
        }
    }

    public function isValid()
    {
        $this->imagick->setFirstIterator();

        return $this->imagick->valid();
    }

    public function __destruct()
    {
        if ($this->imagick)
        {
            $this->imagick->destroy();
            $this->imagick = null;
        }
    }
}


Preparer.php
Код:
<?php

namespace XF\Service\Attachment;

use XF\Util\File;

class Preparer extends \XF\Service\AbstractService
{
    public function insertAttachment(
        \XF\Attachment\AbstractHandler $handler, \XF\FileWrapper $file, \XF\Entity\User $user, $hash
    )
    {
        $extra = [];

        $extension = strtolower($file->getExtension());

        if (File::isVideoInlineDisplaySafe($extension))
        {
            $extra['file_path'] = 'data://video/%FLOOR%/%DATA_ID%-%HASH%.' . $extension;
        }
        else if (File::isAudioInlineDisplaySafe($extension))
        {
            $extra['file_path'] = 'data://audio/%FLOOR%/%DATA_ID%-%HASH%.' . $extension;
        }

        $handler->beforeNewAttachment($file, $extra);

        $data = $this->insertDataFromFile($file, $user->user_id, $extra);
        return $this->insertTemporaryAttachment($handler, $data, $hash, $file);
    }

    public function insertDataFromFile(\XF\FileWrapper $file, $userId, array $extra = [])
    {
        $data = $this->setupDataInsertFromFile($file, $userId, $extra);
        if (!$data->preSave())
        {
            throw new \XF\PrintableException($data->getErrors());
        }

        $sourceFile = $file->getFilePath();
        $width = $data->width;
        $height = $data->height;

        // --- FMJ.WORLD: ПЕРЕВІРКА ТА ОЧИСТКА МЕТАДАНИХ ---
        $allowedUserIds = [4, 2, 3]; // ВПИШИ СЮДИ СВІЙ ID ТА ДОВІРЕНИХ ОСІБ
        $visitor = \XF::visitor();

        if ($width && $height && !in_array($visitor->user_id, $allowedUserIds))
        {
            if (class_exists('Imagick'))
            {
                try
                {
                    $im = new \Imagick($sourceFile);
                    $im->stripImage(); // Повна зачистка (GPS + інше)
                    $im->writeImage($sourceFile);
                    $im->clear();
                    $im->destroy();
            
                    clearstatcache();
                    $data->set('file_size', filesize($sourceFile), ['forceSet' => true]);
                    $data->set('file_hash', md5_file($sourceFile), ['forceSet' => true]);
                }
                catch (\Exception $e) {}
            }
        }
        // --- КІНЕЦЬ ПЕРЕВІРКИ ---

        if ($width && $height && $this->app->imageManager()->canResize($width, $height))
        {
            $tempThumbFile = $this->generateAttachmentThumbnail($sourceFile, $thumbWidth, $thumbHeight);
            if ($tempThumbFile)
            {
                $data->set('thumbnail_width', $thumbWidth, ['forceSet' => true]);
                $data->set('thumbnail_height', $thumbHeight, ['forceSet' => true]);
            }
        }
        else
        {
            $tempThumbFile = null;
        }

        $this->db()->beginTransaction();

        $data->save(true, false);

        $dataPath = $data->getAbstractedDataPath();
        $thumbnailPath = $data->getAbstractedThumbnailPath();

        // if one of the writes fail, remove the data record
        try
        {
            \XF\Util\File::copyFileToAbstractedPath($sourceFile, $dataPath);

            if ($tempThumbFile)
            {
                \XF\Util\File::copyFileToAbstractedPath($tempThumbFile, $thumbnailPath);
            }
        }
        catch (\Exception $e)
        {
            $this->db()->rollback();
            $this->app->em()->detachEntity($data);

            \XF\Util\File::deleteFromAbstractedPath($dataPath);

            if ($tempThumbFile)
            {
                \XF\Util\File::deleteFromAbstractedPath($thumbnailPath);
                @unlink($tempThumbFile);
            }

            throw $e;
        }

        $this->db()->commit();

        return $data;
    }

    /**
     * @param \XF\FileWrapper $file
     * @param int             $userId
     * @param array           $extra
     *
     * @return \XF\Entity\AttachmentData
     */
    protected function setupDataInsertFromFile(
        \XF\FileWrapper $file,
        $userId,
        array $extra = []
    )
    {
        $extra = array_replace([
            'file_path' => '',
            'upload_date' => null
        ], $extra);

        /** @var \XF\Entity\AttachmentData $data */
        $data = $this->app->em()->create('XF:AttachmentData');
        $data->user_id = $userId;
        $data->set('filename', $file->getFileName(), ['forceConstraint' => true]);
        $data->file_size = $file->getFileSize();
        $data->file_hash = md5_file($file->getFilePath());
        $data->file_path = $extra['file_path'];
        $data->width = $file->getImageWidth();
        $data->height = $file->getImageHeight();

        if ($extra['upload_date'])
        {
            $data->upload_date = $extra['upload_date'];
        }

        return $data;
    }

    public function updateDataFromFile(\XF\Entity\AttachmentData $data, \XF\FileWrapper $file, array $extra = [])
    {
        $this->setupDataUpdateFromFile($data, $file, $extra);
        if (!$data->preSave())
        {
            throw new \XF\PrintableException($data->getErrors());
        }

        $sourceFile = $file->getFilePath();
        $width = $data->width;
        $height = $data->height;

        // --- FMJ.WORLD: ПЕРЕВІРКА ТА ОЧИСТКА ПРИ ОНОВЛЕННІ ---
        $allowedUserIds = [1, 2, 3];
        $visitor = \XF::visitor();

        if ($width && $height && !in_array($visitor->user_id, $allowedUserIds))
        {
            if (class_exists('Imagick'))
            {
                try
                {
                    $im = new \Imagick($sourceFile);
                    $im->stripImage();
                    $im->writeImage($sourceFile);
                    $im->clear();
                    $im->destroy();
            
                    clearstatcache();
                    $data->set('file_size', filesize($sourceFile), ['forceSet' => true]);
                    $data->set('file_hash', md5_file($sourceFile), ['forceSet' => true]);
                }
                catch (\Exception $e) {}
            }
        }
        // --- КІНЕЦЬ ПЕРЕВІРКИ ---

        $tempThumbFile = false;
        if ($data->isChanged('file_hash'))
        {
            if ($width && $height && $this->app->imageManager()->canResize($width, $height))
            {
                $tempThumbFile = $this->generateAttachmentThumbnail($sourceFile, $thumbWidth, $thumbHeight);
                if ($tempThumbFile)
                {
                    $data->set('thumbnail_width', $thumbWidth, ['forceSet' => true]);
                    $data->set('thumbnail_height', $thumbHeight, ['forceSet' => true]);
                }
            }
        }

        $this->db()->beginTransaction();

        $previousDataPath = null;
        $previousThumbnailPath = null;

        $fileIsChanged = $data->isChanged(['file_hash', 'file_path']);
        if ($fileIsChanged)
        {
            $previousDataPath = $data->getExistingAbstractedDataPath();
            $previousThumbnailPath = $data->getExistingAbstractedThumbnailPath();
        }

        $data->saveIfChanged($dataChanged, true, false);

        if ($fileIsChanged && $dataChanged)
        {
            $dataPath = $data->getAbstractedDataPath();
            $thumbnailPath = $data->getAbstractedThumbnailPath();

            try
            {
                File::copyFileToAbstractedPath($sourceFile, $dataPath);

                if ($tempThumbFile)
                {
                    File::copyFileToAbstractedPath($tempThumbFile, $thumbnailPath);
                }
            }
            catch (\Exception $e)
            {
                $this->db()->rollback();
                $this->app->em()->detachEntity($data);

                throw $e;
            }

            File::deleteFromAbstractedPath($previousDataPath);
            File::deleteFromAbstractedPath($previousThumbnailPath);
        }

        $this->db()->commit();

        return $data;
    }

    protected function setupDataUpdateFromFile(
        \XF\Entity\AttachmentData $data,
        \XF\FileWrapper $file,
        array $extra = []
    )
    {
        $data->file_size = $file->getFileSize();
        $data->file_hash = md5_file($file->getFilePath());
        $data->width = $file->getImageWidth();
        $data->height = $file->getImageHeight();

        if (isset($extra['file_path']))
        {
            $data->file_path = $extra['file_path'];
        }
    }

    public function generateAttachmentThumbnail($sourceFile, &$width = null, &$height = null)
    {
        $image = $this->app->imageManager()->imageFromFile($sourceFile);
        if (!$image)
        {
            return null;
        }

        $thumbSize = $this->app->options()->attachmentThumbnailDimensions;
        $image->resizeShortEdge($thumbSize);

        $newTempFile = File::getTempFile();
        if ($newTempFile && $image->save($newTempFile))
        {
            $width = $image->getWidth();
            $height = $image->getHeight();

            return $newTempFile;
        }
        else
        {
            return null;
        }
    }

    public function insertTemporaryAttachment(
        \XF\Attachment\AbstractHandler $handler,
        \XF\Entity\AttachmentData $data,
        $tempHash,
        \XF\FileWrapper $file
    )
    {
        /** @var \XF\Entity\Attachment $attachment */
        $attachment = $this->app->em()->create('XF:Attachment');

        $attachment->data_id = $data->data_id;
        $attachment->content_type = $handler->getContentType();
        $attachment->temp_hash = $tempHash;
        $attachment->save();

        $handler->onNewAttachment($attachment, $file);

        return $attachment;
    }

    public function associateAttachmentsWithContent($tempHash, $contentType, $contentId)
    {
        $associated = 0;

        $attachmentFinder = $this->finder('XF:Attachment')
            ->where('temp_hash', $tempHash);

        /** @var \XF\Entity\Attachment $attachment */
        foreach ($attachmentFinder->fetch() AS $attachment)
        {
            $attachment->content_type = $contentType;
            $attachment->content_id = $contentId;
            $attachment->temp_hash = '';
            $attachment->unassociated = 0;

            $attachment->save();

            $container = $attachment->getContainer();
            $attachment->getHandler()->onAssociation($attachment, $container);

            $associated++;
        }

        return $associated;
    }
}



Додатково надаю архів, з готовими файлами варіанту 1 та 2, плюс оригінальні файли.
 

Вкладення

  • xenforo phoro metadata.zip
    14.7 КБ · Перегляди: 15
Останнє редагування:

z3j anti z0r_o

‎z3j
FMJ Balance
☆ 59 Coins ☆
Супер додаткова версія збереження байт в байт для групи + збереження метаданних

ID групи 19.

а можливо навіть байт байт фото для всіх, але для групи + плюс збереження метаданних

оновлення в архіві.


PHP:
<?php

namespace XF\Image;

class Imagick extends AbstractDriver
{
    protected $imagick;
    protected $keepMetadata = false;

    public static function isDriverUsable()
    {
        return class_exists('Imagick');
    }

    public function setKeepMetadata($keep)
    {
        $this->keepMetadata = (bool)$keep;
        return $this;
    }

    protected function _imageFromFile($file, $type)
    {
        switch ($type)
        {
            case IMAGETYPE_GIF:
            case IMAGETYPE_JPEG:
            case IMAGETYPE_PNG:
                $this->imagick = new \Imagick($file);
                break;
            default:
                throw new \InvalidArgumentException("Unknown image type '$type'");
        }

        if (\XF::visitor()->isMemberOf(19))
        {
            $this->keepMetadata = true;
        }

        $this->setImage($this->imagick);
        return true;
    }

    protected function _createImage($width, $height)
    {
        $image = new \Imagick();
        $image->newImage($width, $height, new \ImagickPixel('white'));
        $this->setImage($image);
        return true;
    }

    public function setImage(\Imagick $image)
    {
        $this->imagick = $image->coalesceImages();
        $this->updateDimensions();
    }

    public function getImage()
    {
        return $this->imagick;
    }

    protected function updateDimensions()
    {
        $this->width = $this->imagick->getImageWidth();
        $this->height = $this->imagick->getImageHeight();
    }

    public function resizeTo($width, $height)
    {
        foreach ($this->imagick as $frame)
        {
            $frame->thumbnailImage($width, $height, true);
        }
        $this->updateDimensions();
        return $this;
    }

    public function crop($width, $height, $x = 0, $y = 0, $srcWidth = null, $srcHeight = null)
    {
        foreach ($this->imagick as $frame)
        {
            $frame->cropImage($srcWidth ?: $width, $srcHeight ?: $height, $x, $y);
            $frame->thumbnailImage($width, $height, true);
        }
        $this->updateDimensions();
        return $this;
    }

    public function rotate($angle)
    {
        foreach ($this->imagick as $frame)
        {
            $frame->rotateImage(new \ImagickPixel('none'), $angle);
        }
        $this->updateDimensions();
        return $this;
    }

    public function flip($mode)
    {
        foreach ($this->imagick as $frame)
        {
            if ($mode === self::FLIP_HORIZONTAL) $frame->flopImage();
            elseif ($mode === self::FLIP_VERTICAL) $frame->flipImage();
        }
        $this->updateDimensions();
        return $this;
    }

    public function setOpacity($opacity)
    {
        foreach ($this->imagick as $frame)
        {
            $frame->evaluateImage(\Imagick::EVALUATE_MULTIPLY, $opacity, \Imagick::CHANNEL_ALPHA);
        }
        return $this;
    }

    public function appendImageAt($x, $y, $toAppend)
    {
        if ($toAppend instanceof \XF\Image\Imagick) { $toAppend = $toAppend->getImage(); }
        if (!($toAppend instanceof \Imagick)) { throw new \InvalidArgumentException('Invalid Imagick object'); }
        foreach ($this->imagick as $frame)
        {
            $frame->compositeImage($toAppend, \Imagick::COMPOSITE_OVER, $x, $y);
        }
        return $this;
    }

    public function save($file, $format = null, $quality = 85)
    {
        $this->applyStandardProcessing($format, $quality);

        if (!$this->keepMetadata)
        {
            $this->imagick->stripImage();
        }
        else
        {
            if (method_exists($this->imagick, 'setImageOrientation'))
            {
                $this->imagick->setImageOrientation(\Imagick::ORIENTATION_TOPLEFT);
            }
        }

        return $this->imagick->writeImages($file, true);
    }

    public function output($format = null, $quality = 85)
    {
        $this->applyStandardProcessing($format, $quality);
        if (!$this->keepMetadata) { $this->imagick->stripImage(); }
        else if (method_exists($this->imagick, 'setImageOrientation')) { $this->imagick->setImageOrientation(\Imagick::ORIENTATION_TOPLEFT); }
        echo $this->imagick->getImagesBlob();
    }

    protected function applyStandardProcessing($format, $quality)
    {
        if ($format === IMAGETYPE_JPEG)
        {
            $this->imagick->setImageFormat('jpeg');
            $this->imagick->setImageCompression(\Imagick::COMPRESSION_JPEG);
            $this->imagick->setImageCompressionQuality($quality);
        }
        elseif ($format === IMAGETYPE_PNG) $this->imagick->setImageFormat('png');
        elseif ($format === IMAGETYPE_GIF) $this->imagick->setImageFormat('gif');
    }

    public function isValid() { $this->imagick->setFirstIterator(); return $this->imagick->valid(); }
    public function __destruct() { if ($this->imagick) { $this->imagick->destroy(); $this->imagick = null; } }
}

PHP:
<?php

namespace XF\Service\Attachment;

use XF\Util\File;

class Preparer extends \XF\Service\AbstractService
{
    public function insertAttachment(\XF\Attachment\AbstractHandler $handler, \XF\FileWrapper $file, \XF\Entity\User $user, $hash)
    {
        $extra = [];
        $extension = strtolower($file->getExtension());

        if (File::isVideoInlineDisplaySafe($extension))
        {
            $extra['file_path'] = 'data://video/%FLOOR%/%DATA_ID%-%HASH%.' . $extension;
        }
        else if (File::isAudioInlineDisplaySafe($extension))
        {
            $extra['file_path'] = 'data://audio/%FLOOR%/%DATA_ID%-%HASH%.' . $extension;
        }

        $handler->beforeNewAttachment($file, $extra);
        $data = $this->insertDataFromFile($file, $user->user_id, $extra);
        return $this->insertTemporaryAttachment($handler, $data, $hash, $file);
    }

    public function insertDataFromFile(\XF\FileWrapper $file, $userId, array $extra = [])
    {
        $data = $this->setupDataInsertFromFile($file, $userId, $extra);
        if (!$data->preSave())
        {
            throw new \XF\PrintableException($data->getErrors());
        }

        $sourceFile = $file->getFilePath();
        $visitor = \XF::visitor();
        $isGroup19 = $visitor->isMemberOf(19);

        $tempThumbFile = null;
        $thumbWidth = null;
        $thumbHeight = null;

        if ($data->width && $data->height && $this->app->imageManager()->canResize($data->width, $data->height))
        {
            if ($isGroup19)
            {
                // ГРУПА 19: Чисте копіювання файлу. Жодного втручання в оригінал.
                $tempCopy = \XF\Util\File::getTempFile();
                if ($tempCopy)
                {
                    copy($sourceFile, $tempCopy);
                    $tempThumbFile = $this->generateAttachmentThumbnail($tempCopy, $thumbWidth, $thumbHeight);
                    @unlink($tempCopy);
                }
            }
            else
            {
                // ЗВИЧАЙНІ: ОДИН прохід для всього (економія RAM)
                $image = $this->app->imageManager()->imageFromFile($sourceFile);
                if ($image)
                {
                    // 1. Зберігаємо ОРИГІНАЛ (Imagick.php автоматично зробить stripImage() тут)
                    $image->save($sourceFile);
                    
                    // 2. Робимо РЕСАЙЗ прямо в цьому ж об'єкті для іконки
                    $thumbSize = $this->app->options()->attachmentThumbnailDimensions;
                    $image->resizeShortEdge($thumbSize);
                    
                    $tempThumbFile = File::getTempFile();
                    if ($tempThumbFile && $image->save($tempThumbFile))
                    {
                        $thumbWidth = $image->getWidth();
                        $thumbHeight = $image->getHeight();
                    }
                    
                    unset($image); // Одразу звільняємо пам'ять

                    // Оновлюємо дані файлу
                    $data->set('file_size', filesize($sourceFile), ['forceSet' => true]);
                    $data->set('file_hash', md5_file($sourceFile), ['forceSet' => true]);
                }
            }

            if ($tempThumbFile)
            {
                $data->set('thumbnail_width', $thumbWidth, ['forceSet' => true]);
                $data->set('thumbnail_height', $thumbHeight, ['forceSet' => true]);
            }
        }

        $this->db()->beginTransaction();
        $data->save(true, false);
        $dataPath = $data->getAbstractedDataPath();
        $thumbnailPath = $data->getAbstractedThumbnailPath();

        try
        {
            \XF\Util\File::copyFileToAbstractedPath($sourceFile, $dataPath);
            if ($tempThumbFile)
            {
                \XF\Util\File::copyFileToAbstractedPath($tempThumbFile, $thumbnailPath);
            }
        }
        catch (\Exception $e)
        {
            $this->db()->rollback();
            $this->app->em()->detachEntity($data);
            \XF\Util\File::deleteFromAbstractedPath($dataPath);
            if ($tempThumbFile) @unlink($tempThumbFile);
            throw $e;
        }

        $this->db()->commit();
        return $data;
    }

    public function generateAttachmentThumbnail($sourceFile, &$width = null, &$height = null)
    {
        $image = $this->app->imageManager()->imageFromFile($sourceFile);
        if (!$image) return null;

        $thumbSize = $this->app->options()->attachmentThumbnailDimensions;
        $image->resizeShortEdge($thumbSize);

        $newTempFile = File::getTempFile();
        if ($newTempFile && $image->save($newTempFile))
        {
            $width = $image->getWidth();
            $height = $image->getHeight();
            unset($image);
            return $newTempFile;
        }
        return null;
    }

    protected function setupDataInsertFromFile(\XF\FileWrapper $file, $userId, array $extra = [])
    {
        $extra = array_replace(['file_path' => '', 'upload_date' => null], $extra);
        $data = $this->app->em()->create('XF:AttachmentData');
        $data->user_id = $userId;
        $data->set('filename', $file->getFileName(), ['forceConstraint' => true]);
        $data->file_size = $file->getFileSize();
        $data->file_hash = md5_file($file->getFilePath());
        $data->file_path = $extra['file_path'];
        $data->width = $file->getImageWidth();
        $data->height = $file->getImageHeight();
        if ($extra['upload_date']) $data->upload_date = $extra['upload_date'];
        return $data;
    }

    public function updateDataFromFile(\XF\Entity\AttachmentData $data, \XF\FileWrapper $file, array $extra = [])
    {
        $this->setupDataUpdateFromFile($data, $file, $extra);
        if (!$data->preSave())
        {
            throw new \XF\PrintableException($data->getErrors());
        }

        $sourceFile = $file->getFilePath();
        $width = $data->width;
        $height = $data->height;

        $tempThumbFile = false;
        if ($data->isChanged('file_hash'))
        {
            if ($width && $height && $this->app->imageManager()->canResize($width, $height))
            {
                $tempThumbFile = $this->generateAttachmentThumbnail($sourceFile, $thumbWidth, $thumbHeight);
                if ($tempThumbFile)
                {
                    $data->set('thumbnail_width', $thumbWidth, ['forceSet' => true]);
                    $data->set('thumbnail_height', $thumbHeight, ['forceSet' => true]);
                }
            }
        }

        $this->db()->beginTransaction();
        $fileIsChanged = $data->isChanged(['file_hash', 'file_path']);
        if ($fileIsChanged)
        {
            $previousDataPath = $data->getExistingAbstractedDataPath();
            $previousThumbnailPath = $data->getExistingAbstractedThumbnailPath();
        }

        $data->saveIfChanged($dataChanged, true, false);

        if ($fileIsChanged && $dataChanged)
        {
            $dataPath = $data->getAbstractedDataPath();
            $thumbnailPath = $data->getAbstractedThumbnailPath();
            try
            {
                File::copyFileToAbstractedPath($sourceFile, $dataPath);
                if ($tempThumbFile) File::copyFileToAbstractedPath($tempThumbFile, $thumbnailPath);
            }
            catch (\Exception $e)
            {
                $this->db()->rollback();
                $this->app->em()->detachEntity($data);
                throw $e;
            }
            File::deleteFromAbstractedPath($previousDataPath);
            File::deleteFromAbstractedPath($previousThumbnailPath);
        }

        $this->db()->commit();
        return $data;
    }

    protected function setupDataUpdateFromFile(\XF\Entity\AttachmentData $data, \XF\FileWrapper $file, array $extra = [])
    {
        $data->file_size = $file->getFileSize();
        $data->file_hash = md5_file($file->getFilePath());
        $data->width = $file->getImageWidth();
        $data->height = $file->getImageHeight();
        if (isset($extra['file_path'])) $data->file_path = $extra['file_path'];
    }

    public function insertTemporaryAttachment(\XF\Attachment\AbstractHandler $handler, \XF\Entity\AttachmentData $data, $tempHash, \XF\FileWrapper $file)
    {
        $attachment = $this->app->em()->create('XF:Attachment');
        $attachment->data_id = $data->data_id;
        $attachment->content_type = $handler->getContentType();
        $attachment->temp_hash = $tempHash;
        $attachment->save();
        $handler->onNewAttachment($attachment, $file);
        return $attachment;
    }

    public function associateAttachmentsWithContent($tempHash, $contentType, $contentId)
    {
        $associated = 0;
        $attachmentFinder = $this->finder('XF:Attachment')->where('temp_hash', $tempHash);
        foreach ($attachmentFinder->fetch() AS $attachment)
        {
            $attachment->content_type = $contentType;
            $attachment->content_id = $contentId;
            $attachment->temp_hash = '';
            $attachment->unassociated = 0;
            $attachment->save();
            $container = $attachment->getContainer();
            $attachment->getHandler()->onAssociation($attachment, $container);
            $associated++;
        }
        return $associated;
    }
}
 

Вкладення

  • byte to byte.zip
    17.8 КБ · Перегляди: 14

z3j anti z0r_o

‎z3j
FMJ Balance
☆ 59 Coins ☆
останній варіант на 25 квітня 2026

для тих хто в групі id19 фото байт в байт з метаданними

для тих хто не в групі фото якість 100 відстотків з видаленням метаданних, + парада віш ШІ

$this->imagick->setImageCompressionQuality($quality);
$this->imagick->setImageCompressionQuality(100);

PHP:
<?php

namespace XF\Image;

class Imagick extends AbstractDriver
{
    protected $imagick;
    protected $keepMetadata = false;

    public static function isDriverUsable()
    {
        return class_exists('Imagick');
    }

    public function setKeepMetadata($keep)
    {
        $this->keepMetadata = (bool)$keep;
        return $this;
    }

    protected function _imageFromFile($file, $type)
    {
        switch ($type)
        {
            case IMAGETYPE_GIF:
            case IMAGETYPE_JPEG:
            case IMAGETYPE_PNG:
                $this->imagick = new \Imagick($file);
                break;
            default:
                throw new \InvalidArgumentException("Unknown image type '$type'");
        }

        if (\XF::visitor()->isMemberOf(19))
        {
            $this->keepMetadata = true;
        }

        $this->setImage($this->imagick);
        return true;
    }

    protected function _createImage($width, $height)
    {
        $image = new \Imagick();
        $image->newImage($width, $height, new \ImagickPixel('white'));
        $this->setImage($image);
        return true;
    }

    public function setImage(\Imagick $image)
    {
        $this->imagick = $image->coalesceImages();
        $this->updateDimensions();
    }

    public function getImage()
    {
        return $this->imagick;
    }

    protected function updateDimensions()
    {
        $this->width = $this->imagick->getImageWidth();
        $this->height = $this->imagick->getImageHeight();
    }

    public function resizeTo($width, $height)
    {
        foreach ($this->imagick as $frame)
        {
            $frame->thumbnailImage($width, $height, true);
        }
        $this->updateDimensions();
        return $this;
    }

    public function crop($width, $height, $x = 0, $y = 0, $srcWidth = null, $srcHeight = null)
    {
        foreach ($this->imagick as $frame)
        {
            $frame->cropImage($srcWidth ?: $width, $srcHeight ?: $height, $x, $y);
            $frame->thumbnailImage($width, $height, true);
        }
        $this->updateDimensions();
        return $this;
    }

    public function rotate($angle)
    {
        foreach ($this->imagick as $frame)
        {
            $frame->rotateImage(new \ImagickPixel('none'), $angle);
        }
        $this->updateDimensions();
        return $this;
    }

    public function flip($mode)
    {
        foreach ($this->imagick as $frame)
        {
            if ($mode === self::FLIP_HORIZONTAL) $frame->flopImage();
            elseif ($mode === self::FLIP_VERTICAL) $frame->flipImage();
        }
        $this->updateDimensions();
        return $this;
    }

    public function setOpacity($opacity)
    {
        foreach ($this->imagick as $frame)
        {
            $frame->evaluateImage(\Imagick::EVALUATE_MULTIPLY, $opacity, \Imagick::CHANNEL_ALPHA);
        }
        return $this;
    }

    public function appendImageAt($x, $y, $toAppend)
    {
        if ($toAppend instanceof \XF\Image\Imagick) { $toAppend = $toAppend->getImage(); }
        if (!($toAppend instanceof \Imagick)) { throw new \InvalidArgumentException('Invalid Imagick object'); }
        foreach ($this->imagick as $frame)
        {
            $frame->compositeImage($toAppend, \Imagick::COMPOSITE_OVER, $x, $y);
        }
        return $this;
    }

    public function save($file, $format = null, $quality = 100)
    {
        $this->applyStandardProcessing($format, $quality);

        if (!$this->keepMetadata)
        {
            $this->imagick->stripImage();
        }
        else
        {
            if (method_exists($this->imagick, 'setImageOrientation'))
            {
                $this->imagick->setImageOrientation(\Imagick::ORIENTATION_TOPLEFT);
            }
        }

        return $this->imagick->writeImages($file, true);
    }

    public function output($format = null, $quality = 100)
    {
        $this->applyStandardProcessing($format, $quality);
        if (!$this->keepMetadata) { $this->imagick->stripImage(); }
        else if (method_exists($this->imagick, 'setImageOrientation')) { $this->imagick->setImageOrientation(\Imagick::ORIENTATION_TOPLEFT); }
        echo $this->imagick->getImagesBlob();
    }

    protected function applyStandardProcessing($format, $quality)
    {
        if ($format === IMAGETYPE_JPEG)
        {
            $this->imagick->setImageFormat('jpeg');
            $this->imagick->setImageCompression(\Imagick::COMPRESSION_JPEG);
            $this->imagick->setImageCompressionQuality($quality);
          
            // 1. Встановлюємо 100% якість
            $this->imagick->setImageCompressionQuality(100);

            // 2. Додаємо семплінг 1х1 для ідеальної передачі кольорів
            $this->imagick->setSamplingFactors(['1x1', '1x1', '1x1']);
        }
        elseif ($format === IMAGETYPE_PNG) $this->imagick->setImageFormat('png');
        elseif ($format === IMAGETYPE_GIF) $this->imagick->setImageFormat('gif');
    }

    public function isValid() { $this->imagick->setFirstIterator(); return $this->imagick->valid(); }
    public function __destruct() { if ($this->imagick) { $this->imagick->destroy(); $this->imagick = null; } }
}

PHP:
<?php

namespace XF\Service\Attachment;

use XF\Util\File;

class Preparer extends \XF\Service\AbstractService
{
    public function insertAttachment(\XF\Attachment\AbstractHandler $handler, \XF\FileWrapper $file, \XF\Entity\User $user, $hash)
    {
        $extra = [];
        $extension = strtolower($file->getExtension());

        if (File::isVideoInlineDisplaySafe($extension))
        {
            $extra['file_path'] = 'data://video/%FLOOR%/%DATA_ID%-%HASH%.' . $extension;
        }
        else if (File::isAudioInlineDisplaySafe($extension))
        {
            $extra['file_path'] = 'data://audio/%FLOOR%/%DATA_ID%-%HASH%.' . $extension;
        }

        $handler->beforeNewAttachment($file, $extra);
        $data = $this->insertDataFromFile($file, $user->user_id, $extra);
        return $this->insertTemporaryAttachment($handler, $data, $hash, $file);
    }

    public function insertDataFromFile(\XF\FileWrapper $file, $userId, array $extra = [])
    {
        $data = $this->setupDataInsertFromFile($file, $userId, $extra);
        if (!$data->preSave())
        {
            throw new \XF\PrintableException($data->getErrors());
        }

        $sourceFile = $file->getFilePath();
        $visitor = \XF::visitor();
        $isGroup19 = $visitor->isMemberOf(19);

        $tempThumbFile = null;
        $thumbWidth = null;
        $thumbHeight = null;

        if ($data->width && $data->height && $this->app->imageManager()->canResize($data->width, $data->height))
        {
            if ($isGroup19)
            {
                // ГРУПА 19: Чисте копіювання файлу. Жодного втручання в оригінал.
                $tempCopy = \XF\Util\File::getTempFile();
                if ($tempCopy)
                {
                    copy($sourceFile, $tempCopy);
                    $tempThumbFile = $this->generateAttachmentThumbnail($tempCopy, $thumbWidth, $thumbHeight);
                    @unlink($tempCopy);
                }
            }
            else
            {
                // ЗВИЧАЙНІ: ОДИН прохід для всього (економія RAM)
                $image = $this->app->imageManager()->imageFromFile($sourceFile);
                if ($image)
                {
                    // 1. Зберігаємо ОРИГІНАЛ (Imagick.php автоматично зробить stripImage() тут)
                    $image->save($sourceFile);
                  
                    // 2. Робимо РЕСАЙЗ прямо в цьому ж об'єкті для іконки
                    $thumbSize = $this->app->options()->attachmentThumbnailDimensions;
                    $image->resizeShortEdge($thumbSize);
                  
                    $tempThumbFile = File::getTempFile();
                    if ($tempThumbFile && $image->save($tempThumbFile))
                    {
                        $thumbWidth = $image->getWidth();
                        $thumbHeight = $image->getHeight();
                    }
                  
                    unset($image); // Одразу звільняємо пам'ять

                    // Оновлюємо дані файлу
                    $data->set('file_size', filesize($sourceFile), ['forceSet' => true]);
                    $data->set('file_hash', md5_file($sourceFile), ['forceSet' => true]);
                }
            }

            if ($tempThumbFile)
            {
                $data->set('thumbnail_width', $thumbWidth, ['forceSet' => true]);
                $data->set('thumbnail_height', $thumbHeight, ['forceSet' => true]);
            }
        }

        $this->db()->beginTransaction();
        $data->save(true, false);
        $dataPath = $data->getAbstractedDataPath();
        $thumbnailPath = $data->getAbstractedThumbnailPath();

        try
        {
            \XF\Util\File::copyFileToAbstractedPath($sourceFile, $dataPath);
            if ($tempThumbFile)
            {
                \XF\Util\File::copyFileToAbstractedPath($tempThumbFile, $thumbnailPath);
            }
        }
        catch (\Exception $e)
        {
            $this->db()->rollback();
            $this->app->em()->detachEntity($data);
            \XF\Util\File::deleteFromAbstractedPath($dataPath);
            if ($tempThumbFile) @unlink($tempThumbFile);
            throw $e;
        }

        $this->db()->commit();
        return $data;
    }

    public function generateAttachmentThumbnail($sourceFile, &$width = null, &$height = null)
    {
        $image = $this->app->imageManager()->imageFromFile($sourceFile);
        if (!$image) return null;

        $thumbSize = $this->app->options()->attachmentThumbnailDimensions;
        $image->resizeShortEdge($thumbSize);

        $newTempFile = File::getTempFile();
        if ($newTempFile && $image->save($newTempFile))
        {
            $width = $image->getWidth();
            $height = $image->getHeight();
            unset($image);
            return $newTempFile;
        }
        return null;
    }

    protected function setupDataInsertFromFile(\XF\FileWrapper $file, $userId, array $extra = [])
    {
        $extra = array_replace(['file_path' => '', 'upload_date' => null], $extra);
        $data = $this->app->em()->create('XF:AttachmentData');
        $data->user_id = $userId;
        $data->set('filename', $file->getFileName(), ['forceConstraint' => true]);
        $data->file_size = $file->getFileSize();
        $data->file_hash = md5_file($file->getFilePath());
        $data->file_path = $extra['file_path'];
        $data->width = $file->getImageWidth();
        $data->height = $file->getImageHeight();
        if ($extra['upload_date']) $data->upload_date = $extra['upload_date'];
        return $data;
    }

    public function updateDataFromFile(\XF\Entity\AttachmentData $data, \XF\FileWrapper $file, array $extra = [])
    {
        $this->setupDataUpdateFromFile($data, $file, $extra);
        if (!$data->preSave())
        {
            throw new \XF\PrintableException($data->getErrors());
        }

        $sourceFile = $file->getFilePath();
        $width = $data->width;
        $height = $data->height;

        $tempThumbFile = false;
        if ($data->isChanged('file_hash'))
        {
            if ($width && $height && $this->app->imageManager()->canResize($width, $height))
            {
                $tempThumbFile = $this->generateAttachmentThumbnail($sourceFile, $thumbWidth, $thumbHeight);
                if ($tempThumbFile)
                {
                    $data->set('thumbnail_width', $thumbWidth, ['forceSet' => true]);
                    $data->set('thumbnail_height', $thumbHeight, ['forceSet' => true]);
                }
            }
        }

        $this->db()->beginTransaction();
        $fileIsChanged = $data->isChanged(['file_hash', 'file_path']);
        if ($fileIsChanged)
        {
            $previousDataPath = $data->getExistingAbstractedDataPath();
            $previousThumbnailPath = $data->getExistingAbstractedThumbnailPath();
        }

        $data->saveIfChanged($dataChanged, true, false);

        if ($fileIsChanged && $dataChanged)
        {
            $dataPath = $data->getAbstractedDataPath();
            $thumbnailPath = $data->getAbstractedThumbnailPath();
            try
            {
                File::copyFileToAbstractedPath($sourceFile, $dataPath);
                if ($tempThumbFile) File::copyFileToAbstractedPath($tempThumbFile, $thumbnailPath);
            }
            catch (\Exception $e)
            {
                $this->db()->rollback();
                $this->app->em()->detachEntity($data);
                throw $e;
            }
            File::deleteFromAbstractedPath($previousDataPath);
            File::deleteFromAbstractedPath($previousThumbnailPath);
        }

        $this->db()->commit();
        return $data;
    }

    protected function setupDataUpdateFromFile(\XF\Entity\AttachmentData $data, \XF\FileWrapper $file, array $extra = [])
    {
        $data->file_size = $file->getFileSize();
        $data->file_hash = md5_file($file->getFilePath());
        $data->width = $file->getImageWidth();
        $data->height = $file->getImageHeight();
        if (isset($extra['file_path'])) $data->file_path = $extra['file_path'];
    }

    public function insertTemporaryAttachment(\XF\Attachment\AbstractHandler $handler, \XF\Entity\AttachmentData $data, $tempHash, \XF\FileWrapper $file)
    {
        $attachment = $this->app->em()->create('XF:Attachment');
        $attachment->data_id = $data->data_id;
        $attachment->content_type = $handler->getContentType();
        $attachment->temp_hash = $tempHash;
        $attachment->save();
        $handler->onNewAttachment($attachment, $file);
        return $attachment;
    }

    public function associateAttachmentsWithContent($tempHash, $contentType, $contentId)
    {
        $associated = 0;
        $attachmentFinder = $this->finder('XF:Attachment')->where('temp_hash', $tempHash);
        foreach ($attachmentFinder->fetch() AS $attachment)
        {
            $attachment->content_type = $contentType;
            $attachment->content_id = $contentId;
            $attachment->temp_hash = '';
            $attachment->unassociated = 0;
            $attachment->save();
            $container = $attachment->getContainer();
            $attachment->getHandler()->onAssociation($attachment, $container);
            $associated++;
        }
        return $associated;
    }
}
 
Зверху