diff --git a/retroshare-gui/src/gui/Posted/BoardPostDisplayWidget.cpp b/retroshare-gui/src/gui/Posted/BoardPostDisplayWidget.cpp index ae99a88e4..9eaa80f9c 100644 --- a/retroshare-gui/src/gui/Posted/BoardPostDisplayWidget.cpp +++ b/retroshare-gui/src/gui/Posted/BoardPostDisplayWidget.cpp @@ -27,6 +27,7 @@ #include "rshare.h" #include "BoardPostDisplayWidget.h" +#include "BoardPostImageHelper.h" #include "PhotoView.h" #include "gui/gxs/GxsIdDetails.h" #include "util/misc.h" @@ -330,17 +331,33 @@ void BoardPostDisplayWidget_compact::setup() { if(mPost.mImage.mData != NULL) { - QPixmap pixmap; - GxsIdDetails::loadPixmapFromData(mPost.mImage.mData, mPost.mImage.mSize, pixmap,GxsIdDetails::ORIGINAL); - // Wiping data - as its been passed to thumbnail. + QString format; + if (BoardPostImageHelper::isAnimatedImage(mPost.mImage.mData, mPost.mImage.mSize, &format)) + { + // Animated GIF/WEBP - use QMovie + QMovie* movie = BoardPostImageHelper::createMovieFromData(mPost.mImage.mData, mPost.mImage.mSize); + if (movie) + { + ui->pictureLabel->setMovie(movie); + movie->start(); + // Loop animation when finished + connect(movie, &QMovie::finished, movie, &QMovie::start); + } + } + else + { + // Static image - use QPixmap + QPixmap pixmap; + GxsIdDetails::loadPixmapFromData(mPost.mImage.mData, mPost.mImage.mSize, pixmap,GxsIdDetails::ORIGINAL); #ifdef DEBUG_BOARDPOSTDISPLAYWIDGET - std::cerr << "Got pixmap of size " << pixmap.width() << " x " << pixmap.height() << std::endl; - std::cerr << "Saving to pix.png" << std::endl; - pixmap.save("pix.png","JPG"); + std::cerr << "Got pixmap of size " << pixmap.width() << " x " << pixmap.height() << std::endl; + std::cerr << "Saving to pix.png" << std::endl; + pixmap.save("pix.png","JPG"); #endif - ui->pictureLabel->setPicture(pixmap); + ui->pictureLabel->setPicture(pixmap); + } } else ui->pictureLabel->setPicture( FilesDefs::getPixmapFromQtResourcePath(":/images/thumb-default.png") ); @@ -382,13 +399,28 @@ void BoardPostDisplayWidget_compact::viewPicture() return; QString timestamp = misc::timeRelativeToNow(mPost.mMeta.mPublishTs); - QPixmap pixmap; - GxsIdDetails::loadPixmapFromData(mPost.mImage.mData, mPost.mImage.mSize, pixmap,GxsIdDetails::ORIGINAL); RsGxsId authorID = mPost.mMeta.mAuthorId; PhotoView *PView = new PhotoView(); - PView->setPixmap(pixmap); + // Check if animated image + QString format; + if (BoardPostImageHelper::isAnimatedImage(mPost.mImage.mData, mPost.mImage.mSize, &format)) + { + // Animated GIF/WEBP - use QMovie in popup + QMovie* movie = BoardPostImageHelper::createMovieFromData(mPost.mImage.mData, mPost.mImage.mSize); + if (movie) { + PView->setMovie(movie); + } + } + else + { + // Static image - use QPixmap + QPixmap pixmap; + GxsIdDetails::loadPixmapFromData(mPost.mImage.mData, mPost.mImage.mSize, pixmap,GxsIdDetails::ORIGINAL); + PView->setPixmap(pixmap); + } + PView->setTitle(QString::fromUtf8(mPost.mMeta.mMsgName.c_str())); PView->setName(authorID); PView->setTime(timestamp); @@ -446,16 +478,32 @@ void BoardPostDisplayWidget_card::setup() { if(mPost.mImage.mData != NULL) { - QPixmap pixmap; - GxsIdDetails::loadPixmapFromData(mPost.mImage.mData, mPost.mImage.mSize, pixmap,GxsIdDetails::ORIGINAL); - // Wiping data - as its been passed to thumbnail. + QString format; + if (BoardPostImageHelper::isAnimatedImage(mPost.mImage.mData, mPost.mImage.mSize, &format)) + { + // Animated GIF/WEBP - use QMovie + QMovie* movie = BoardPostImageHelper::createMovieFromData(mPost.mImage.mData, mPost.mImage.mSize); + if (movie) + { + ui->pictureLabel->setMovie(movie); + movie->start(); + // Loop animation when finished + connect(movie, &QMovie::finished, movie, &QMovie::start); + } + } + else + { + // Static image - use QPixmap with scaling + QPixmap pixmap; + GxsIdDetails::loadPixmapFromData(mPost.mImage.mData, mPost.mImage.mSize, pixmap,GxsIdDetails::ORIGINAL); - if(pixmap.width() > 800){ - QPixmap scaledpixmap = pixmap.scaledToWidth(800, Qt::SmoothTransformation); - ui->pictureLabel->setPixmap(scaledpixmap); - }else{ - ui->pictureLabel->setPixmap(pixmap); - } + if(pixmap.width() > 800){ + QPixmap scaledpixmap = pixmap.scaledToWidth(800, Qt::SmoothTransformation); + ui->pictureLabel->setPixmap(scaledpixmap); + }else{ + ui->pictureLabel->setPixmap(pixmap); + } + } ui->pictureLabel->show(); } diff --git a/retroshare-gui/src/gui/Posted/BoardPostImageHelper.cpp b/retroshare-gui/src/gui/Posted/BoardPostImageHelper.cpp new file mode 100644 index 000000000..fc6af81e6 --- /dev/null +++ b/retroshare-gui/src/gui/Posted/BoardPostImageHelper.cpp @@ -0,0 +1,78 @@ +/******************************************************************************* + * retroshare-gui/src/gui/Posted/BoardPostImageHelper.cpp * + * * + * Copyright (C) 2025 RetroShare Team * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU Affero General Public License as * + * published by the Free Software Foundation, either version 3 of the * + * License, or (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU Affero General Public License for more details. * + * * + * You should have received a copy of the GNU Affero General Public License * + * along with this program. If not, see . * + * * + *******************************************************************************/ + +#include "BoardPostImageHelper.h" +#include +#include + +bool BoardPostImageHelper::isAnimatedImage(const uint8_t* data, uint32_t size, QString* format) +{ + if (!data || size == 0 || size > MAX_ANIMATED_SIZE) + return false; + + // Create QByteArray from raw data (no copy, just wraps pointer) + QByteArray imageData = QByteArray::fromRawData(reinterpret_cast(data), size); + QBuffer buffer; + buffer.setData(imageData); + buffer.open(QIODevice::ReadOnly); + + QImageReader reader(&buffer); + QString detectedFormat = QString(reader.format()).toUpper(); + + // Check if format is GIF or WEBP + if (detectedFormat != "GIF" && detectedFormat != "WEBP") + return false; + + // Check frame count - more than 1 frame means animation + int frameCount = reader.imageCount(); + bool isAnimated = (frameCount > 1); + + if (isAnimated && format) + *format = detectedFormat; + + return isAnimated; +} + +QMovie* BoardPostImageHelper::createMovieFromData(const uint8_t* data, uint32_t size) +{ + if (!data || size == 0) + return nullptr; + + // Create persistent QByteArray (QMovie needs data to persist) + QByteArray* imageData = new QByteArray(reinterpret_cast(data), size); + + QBuffer* buffer = new QBuffer(imageData); + buffer->open(QIODevice::ReadOnly); + + QMovie* movie = new QMovie(); + movie->setDevice(buffer); + + // Set buffer and data as children of movie for automatic cleanup + buffer->setParent(movie); + + // Verify movie is valid + if (!movie->isValid()) { + delete movie; + delete imageData; + return nullptr; + } + + return movie; +} diff --git a/retroshare-gui/src/gui/Posted/BoardPostImageHelper.h b/retroshare-gui/src/gui/Posted/BoardPostImageHelper.h new file mode 100644 index 000000000..496579f1c --- /dev/null +++ b/retroshare-gui/src/gui/Posted/BoardPostImageHelper.h @@ -0,0 +1,58 @@ +/******************************************************************************* + * retroshare-gui/src/gui/Posted/BoardPostImageHelper.h * + * * + * Copyright (C) 2025 RetroShare Team * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU Affero General Public License as * + * published by the Free Software Foundation, either version 3 of the * + * License, or (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU Affero General Public License for more details. * + * * + * You should have received a copy of the GNU Affero General Public License * + * along with this program. If not, see . * + * * + *******************************************************************************/ + +#pragma once + +#include +#include +#include +#include + +/** + * Helper class for loading and detecting animated images in Board posts. + * Supports animated GIF and WEBP formats with size limits. + */ +class BoardPostImageHelper +{ +public: + // Maximum allowed size for animated images (194KB as per issue #3095) + static const uint32_t MAX_ANIMATED_SIZE = 194 * 1024; + + /** + * Detect if image data contains an animated GIF or WEBP. + * @param data Raw image data + * @param size Size of image data in bytes + * @param format Optional output parameter for detected format (GIF/WEBP) + * @return true if image is animated and within size limit + */ + static bool isAnimatedImage(const uint8_t* data, uint32_t size, QString* format = nullptr); + + /** + * Create QMovie object from raw image data for animation playback. + * Caller is responsible for memory management (delete when done). + * @param data Raw image data + * @param size Size of image data in bytes + * @return QMovie pointer or nullptr on failure + */ + static QMovie* createMovieFromData(const uint8_t* data, uint32_t size); + +private: + BoardPostImageHelper() = delete; // Utility class, no instantiation +}; diff --git a/retroshare-gui/src/gui/Posted/PhotoView.cpp b/retroshare-gui/src/gui/Posted/PhotoView.cpp index 0c45914f4..590e55994 100644 --- a/retroshare-gui/src/gui/Posted/PhotoView.cpp +++ b/retroshare-gui/src/gui/Posted/PhotoView.cpp @@ -46,6 +46,10 @@ PhotoView::PhotoView(QWidget *parent) /** Destructor */ PhotoView::~PhotoView() { + if (mMovie) { + mMovie->stop(); + delete mMovie; + } delete ui; } @@ -55,6 +59,26 @@ void PhotoView::setPixmap(const QPixmap& pixmap) this->adjustSize(); } +void PhotoView::setMovie(QMovie* movie) +{ + // Clean up previous movie if exists + if (mMovie) { + mMovie->stop(); + delete mMovie; + } + + mMovie = movie; + + if (mMovie) { + ui->photoLabel->setMovie(mMovie); + mMovie->start(); + // Loop animation when finished + connect(mMovie, &QMovie::finished, mMovie, &QMovie::start); + } + + this->adjustSize(); +} + void PhotoView::setTitle(const QString& text) { ui->titleLabel->setText(text); diff --git a/retroshare-gui/src/gui/Posted/PhotoView.h b/retroshare-gui/src/gui/Posted/PhotoView.h index dea6528d1..7bd098601 100644 --- a/retroshare-gui/src/gui/Posted/PhotoView.h +++ b/retroshare-gui/src/gui/Posted/PhotoView.h @@ -43,6 +43,7 @@ public: public slots: void setPixmap(const QPixmap& pixmap); + void setMovie(QMovie* movie); // New: support for animated images void setTitle (const QString &text); void setName(const RsGxsId& authorID); void setTime(const QString& text); @@ -56,6 +57,7 @@ private slots: private: RsGxsMessageId mMessageId; RsGxsGroupId mGroupId; + QMovie* mMovie = nullptr; // Track QMovie for cleanup /** Qt Designer generated object */ Ui::PhotoView *ui; diff --git a/retroshare-gui/src/gui/Posted/PostedCardView.cpp b/retroshare-gui/src/gui/Posted/PostedCardView.cpp index c730f0037..265a93275 100644 --- a/retroshare-gui/src/gui/Posted/PostedCardView.cpp +++ b/retroshare-gui/src/gui/Posted/PostedCardView.cpp @@ -25,6 +25,7 @@ #include "rshare.h" #include "PostedCardView.h" +#include "BoardPostImageHelper.h" #include "gui/feeds/FeedHolder.h" #include "gui/gxs/GxsIdDetails.h" #include "util/misc.h" @@ -253,16 +254,32 @@ void PostedCardView::fill() if(mPost.mImage.mData != NULL) { - QPixmap pixmap; - GxsIdDetails::loadPixmapFromData(mPost.mImage.mData, mPost.mImage.mSize, pixmap,GxsIdDetails::ORIGINAL); - // Wiping data - as its been passed to thumbnail. - - if(pixmap.width() > 800){ - QPixmap scaledpixmap = pixmap.scaledToWidth(800, Qt::SmoothTransformation); - ui->pictureLabel->setPixmap(scaledpixmap); - }else{ - ui->pictureLabel->setPixmap(pixmap); - } + QString format; + if (BoardPostImageHelper::isAnimatedImage(mPost.mImage.mData, mPost.mImage.mSize, &format)) + { + // Animated GIF/WEBP - use QMovie + QMovie* movie = BoardPostImageHelper::createMovieFromData(mPost.mImage.mData, mPost.mImage.mSize); + if (movie) + { + ui->pictureLabel->setMovie(movie); + movie->start(); + // Loop animation when finished + connect(movie, &QMovie::finished, movie, &QMovie::start); + } + } + else + { + // Static image - use QPixmap with scaling + QPixmap pixmap; + GxsIdDetails::loadPixmapFromData(mPost.mImage.mData, mPost.mImage.mSize, pixmap,GxsIdDetails::ORIGINAL); + + if(pixmap.width() > 800){ + QPixmap scaledpixmap = pixmap.scaledToWidth(800, Qt::SmoothTransformation); + ui->pictureLabel->setPixmap(scaledpixmap); + }else{ + ui->pictureLabel->setPixmap(pixmap); + } + } } else //if (mPost.mImage.mData == NULL) { diff --git a/retroshare-gui/src/gui/Posted/PostedCreatePostDialog.cpp b/retroshare-gui/src/gui/Posted/PostedCreatePostDialog.cpp index bfccd3fc7..cdbfcfda2 100644 --- a/retroshare-gui/src/gui/Posted/PostedCreatePostDialog.cpp +++ b/retroshare-gui/src/gui/Posted/PostedCreatePostDialog.cpp @@ -24,6 +24,8 @@ #include #include #include +#include +#include #include "PostedCreatePostDialog.h" #include "ui_PostedCreatePostDialog.h" @@ -220,6 +222,24 @@ void PostedCreatePostDialog::addPicture() // select a picture file if (misc::getOpenFileName(window(), RshareSettings::LASTDIR_IMAGES, tr("Load Picture File"), "Pictures (*.png *.xpm *.jpg *.jpeg *.gif *.webp )", imagefilename)) { + // Check file size for animated images BEFORE loading + QFileInfo fileInfo(imagefilename); + QImageReader reader(imagefilename); + QString format = reader.format().toUpper(); + int frameCount = reader.imageCount(); + + // Validate animated GIF/WEBP size (194KB limit) + if ((format == "GIF" || format == "WEBP") && frameCount > 1) { + if (fileInfo.size() > 194 * 1024) { + QMessageBox::warning(this, tr("Image Too Large"), + tr("Animated images must be under 194KB. This image is %1KB.\n\n" + "Please use a smaller animated image or a static image instead.") + .arg(fileInfo.size() / 1024)); + imagefilename = ""; + return; + } + } + QImage image; if (image.load(imagefilename) == false) { fprintf (stderr, "RsHtml::makeEmbeddedImage() - image \"%s\" can't be load\n", imagefilename.toLatin1().constData()); diff --git a/retroshare-gui/src/retroshare-gui.pro b/retroshare-gui/src/retroshare-gui.pro index 6fd08381d..6fa549564 100644 --- a/retroshare-gui/src/retroshare-gui.pro +++ b/retroshare-gui/src/retroshare-gui.pro @@ -1376,6 +1376,7 @@ posted { gui/Posted/PostedListWidgetWithModel.h \ gui/Posted/PostedPostsModel.h \ gui/Posted/BoardPostDisplayWidget.h \ + gui/Posted/BoardPostImageHelper.h \ gui/Posted/PostedItem.h \ gui/Posted/PostedCardView.h \ gui/Posted/PostedGroupDialog.h \ @@ -1402,6 +1403,7 @@ posted { SOURCES += gui/Posted/PostedDialog.cpp \ gui/Posted/PostedListWidgetWithModel.cpp \ gui/Posted/BoardPostDisplayWidget.cpp \ + gui/Posted/BoardPostImageHelper.cpp \ gui/Posted/PostedPostsModel.cpp \ gui/feeds/PostedGroupItem.cpp \ gui/Posted/PostedItem.cpp \