Всім привіт, я довго намагався налаштувати 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
Preparer.php
Додатковий архів з файлами, плюс оригінальні.
Цей код від 25 квітня 2026 року, з фіксом збереження портретної орієнтації для фото, у яких зберігаються метаданні.
Версію для збереження байт в байт шукайте нище.
Для цього потрібно обрати обробник зображень 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 року, з фіксом збереження портретної орієнтації для фото, у яких зберігаються метаданні.
Версію для збереження байт в байт шукайте нище.
Вкладення
Останнє редагування: