mirror of
https://github.com/RetroShare/RetroShare.git
synced 2026-09-14 11:06:01 +05:00
Implement animated GIF/WEBP support for Board posts (#3095)
Features:
- Detect and animate GIF/WEBP images with multiple frames using QMovie
- Automatic looping for smooth playback
- 194KB size limit enforced at upload time
- User-friendly error message for oversized animated images
- Preserve static image display for non-animated content
Implementation:
- Created BoardPostImageHelper utility class for format detection
- Updated BoardPostDisplayWidget (compact and card variants)
- Updated PostedCardView for feed display
- Updated PhotoView for popup animation
- Added upload validation in PostedCreatePostDialog
- Proper memory management to prevent leaks
Technical details:
- Uses QImageReader for frame counting and format detection
- QMovie with QBuffer for animation playback
- Parent-child cleanup for automatic memory management
- Tested on macOS with successful build
Files modified:
- gui/Posted/BoardPostImageHelper.{h,cpp} (NEW)
- gui/Posted/BoardPostDisplayWidget.cpp
- gui/Posted/PostedCardView.cpp
- gui/Posted/PhotoView.{h,cpp}
- gui/Posted/PostedCreatePostDialog.cpp
- retroshare-gui.pro
This commit is contained in:
parent
a26b5a4be5
commit
b4f56cf4d8
@ -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();
|
||||
}
|
||||
|
||||
78
retroshare-gui/src/gui/Posted/BoardPostImageHelper.cpp
Normal file
78
retroshare-gui/src/gui/Posted/BoardPostImageHelper.cpp
Normal file
@ -0,0 +1,78 @@
|
||||
/*******************************************************************************
|
||||
* retroshare-gui/src/gui/Posted/BoardPostImageHelper.cpp *
|
||||
* *
|
||||
* Copyright (C) 2025 RetroShare Team <retroshare.project@gmail.com> *
|
||||
* *
|
||||
* 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 <https://www.gnu.org/licenses/>. *
|
||||
* *
|
||||
*******************************************************************************/
|
||||
|
||||
#include "BoardPostImageHelper.h"
|
||||
#include <QBuffer>
|
||||
#include <QImageReader>
|
||||
|
||||
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<const char*>(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<const char*>(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;
|
||||
}
|
||||
58
retroshare-gui/src/gui/Posted/BoardPostImageHelper.h
Normal file
58
retroshare-gui/src/gui/Posted/BoardPostImageHelper.h
Normal file
@ -0,0 +1,58 @@
|
||||
/*******************************************************************************
|
||||
* retroshare-gui/src/gui/Posted/BoardPostImageHelper.h *
|
||||
* *
|
||||
* Copyright (C) 2025 RetroShare Team <retroshare.project@gmail.com> *
|
||||
* *
|
||||
* 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 <https://www.gnu.org/licenses/>. *
|
||||
* *
|
||||
*******************************************************************************/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QMovie>
|
||||
#include <QPixmap>
|
||||
#include <QString>
|
||||
#include <cstdint>
|
||||
|
||||
/**
|
||||
* 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
|
||||
};
|
||||
@ -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);
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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)
|
||||
{
|
||||
|
||||
@ -24,6 +24,8 @@
|
||||
#include <QByteArray>
|
||||
#include <QStringList>
|
||||
#include <QSignalMapper>
|
||||
#include <QImageReader>
|
||||
#include <QFileInfo>
|
||||
|
||||
#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());
|
||||
|
||||
@ -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 \
|
||||
|
||||
Loading…
Reference in New Issue
Block a user