2018-06-26 14:09:39 +02:00
# include "messages/Image.hpp"
2018-04-27 22:11:19 +02:00
2019-09-22 10:27:05 +02:00
# include <QBuffer>
# include <QImageReader>
# include <QNetworkAccessManager>
# include <QNetworkReply>
# include <QNetworkRequest>
# include <QTimer>
# include <functional>
# include <thread>
2018-06-26 14:09:39 +02:00
# include "Application.hpp"
2018-08-11 22:23:06 +02:00
# include "common/Common.hpp"
2018-07-15 14:11:46 +02:00
# include "common/NetworkRequest.hpp"
2020-11-21 16:20:10 +01:00
# include "common/QLogging.hpp"
2018-08-02 14:23:27 +02:00
# include "debug/AssertInGuiThread.hpp"
2018-08-09 18:39:46 +02:00
# include "debug/Benchmark.hpp"
2021-02-13 19:17:22 +01:00
# ifndef CHATTERINO_TEST
# include "singletons / Emotes.hpp"
# endif
2018-06-26 14:09:39 +02:00
# include "singletons/WindowManager.hpp"
2021-02-13 19:17:22 +01:00
# include "singletons/helper/GifTimer.hpp"
2018-08-07 01:35:24 +02:00
# include "util/DebugCount.hpp"
2018-06-26 14:09:39 +02:00
# include "util/PostToThread.hpp"
2017-01-11 18:52:09 +01:00
2021-02-13 19:17:22 +01:00
# include <queue>
2017-01-18 21:30:23 +01:00
namespace chatterino {
2018-09-20 13:09:37 +02:00
namespace detail {
2018-08-15 22:46:20 +02:00
// Frames
Frames : : Frames ( )
{
DebugCount : : increase ( " images " ) ;
2018-08-11 17:15:17 +02:00
}
2018-08-06 18:25:47 +02:00
2018-08-15 22:46:20 +02:00
Frames : : Frames ( const QVector < Frame < QPixmap > > & frames )
: items_ ( frames )
{
assertInGuiThread ( ) ;
DebugCount : : increase ( " images " ) ;
2018-08-11 17:15:17 +02:00
2018-10-21 13:43:02 +02:00
if ( this - > animated ( ) )
{
2018-08-15 22:46:20 +02:00
DebugCount : : increase ( " animated images " ) ;
2018-08-06 18:25:47 +02:00
2021-02-13 19:17:22 +01:00
# ifndef CHATTERINO_TEST
2018-08-15 22:46:20 +02:00
this - > gifTimerConnection_ =
2020-11-08 12:02:19 +01:00
getApp ( ) - > emotes - > gifTimer . signal . connect ( [ this ] {
this - > advance ( ) ;
} ) ;
2021-02-13 19:17:22 +01:00
# endif
2018-08-15 22:46:20 +02:00
}
2020-02-16 14:24:11 +01:00
2020-11-08 12:02:19 +01:00
auto totalLength =
std : : accumulate ( this - > items_ . begin ( ) , this - > items_ . end ( ) , 0UL ,
[ ] ( auto init , auto & & frame ) {
return init + frame . duration ;
} ) ;
2020-02-16 14:24:11 +01:00
2020-04-19 21:05:40 +02:00
if ( totalLength = = 0 )
{
this - > durationOffset_ = 0 ;
}
else
{
2021-02-13 19:17:22 +01:00
# ifndef CHATTERINO_TEST
2020-04-19 21:05:40 +02:00
this - > durationOffset_ = std : : min < int > (
int ( getApp ( ) - > emotes - > gifTimer . position ( ) % totalLength ) ,
60000 ) ;
2021-02-13 19:17:22 +01:00
# endif
2020-04-19 21:05:40 +02:00
}
2020-02-16 14:24:11 +01:00
this - > processOffset ( ) ;
2018-08-15 22:46:20 +02:00
}
2018-08-06 18:25:47 +02:00
2018-08-15 22:46:20 +02:00
Frames : : ~ Frames ( )
{
assertInGuiThread ( ) ;
DebugCount : : decrease ( " images " ) ;
2018-08-11 17:15:17 +02:00
2018-10-21 13:43:02 +02:00
if ( this - > animated ( ) )
{
2018-08-15 22:46:20 +02:00
DebugCount : : decrease ( " animated images " ) ;
2018-08-11 17:15:17 +02:00
}
2018-08-15 22:46:20 +02:00
this - > gifTimerConnection_ . disconnect ( ) ;
2018-08-06 18:25:47 +02:00
}
2018-08-15 22:46:20 +02:00
void Frames : : advance ( )
{
2020-02-16 14:24:11 +01:00
this - > durationOffset_ + = gifFrameLength ;
this - > processOffset ( ) ;
}
2018-08-06 18:25:47 +02:00
2020-02-16 14:24:11 +01:00
void Frames : : processOffset ( )
{
2020-04-19 21:05:40 +02:00
if ( this - > items_ . isEmpty ( ) )
{
return ;
}
2018-10-21 13:43:02 +02:00
while ( true )
{
2018-08-15 22:46:20 +02:00
this - > index_ % = this - > items_ . size ( ) ;
2018-08-06 18:25:47 +02:00
2018-10-21 13:43:02 +02:00
if ( this - > durationOffset_ > this - > items_ [ this - > index_ ] . duration )
{
2018-08-15 22:46:20 +02:00
this - > durationOffset_ - = this - > items_ [ this - > index_ ] . duration ;
this - > index_ = ( this - > index_ + 1 ) % this - > items_ . size ( ) ;
2018-10-21 13:43:02 +02:00
}
else
{
2018-08-15 22:46:20 +02:00
break ;
}
}
2018-08-06 18:25:47 +02:00
}
2018-08-15 22:46:20 +02:00
bool Frames : : animated ( ) const
{
return this - > items_ . size ( ) > 1 ;
}
2018-08-06 18:25:47 +02:00
2018-08-15 22:46:20 +02:00
boost : : optional < QPixmap > Frames : : current ( ) const
{
2018-10-21 13:43:02 +02:00
if ( this - > items_ . size ( ) = = 0 )
return boost : : none ;
2018-08-15 22:46:20 +02:00
return this - > items_ [ this - > index_ ] . image ;
2018-08-06 18:25:47 +02:00
}
2018-08-15 22:46:20 +02:00
boost : : optional < QPixmap > Frames : : first ( ) const
{
2018-10-21 13:43:02 +02:00
if ( this - > items_ . size ( ) = = 0 )
return boost : : none ;
2018-08-15 22:46:20 +02:00
return this - > items_ . front ( ) . image ;
2018-08-06 18:25:47 +02:00
}
2018-08-15 22:46:20 +02:00
// functions
QVector < Frame < QImage > > readFrames ( QImageReader & reader , const Url & url )
{
QVector < Frame < QImage > > frames ;
2018-08-06 18:25:47 +02:00
2018-08-15 22:46:20 +02:00
QImage image ;
2018-10-21 13:43:02 +02:00
for ( int index = 0 ; index < reader . imageCount ( ) ; + + index )
{
if ( reader . read ( & image ) )
{
2018-08-15 22:46:20 +02:00
QPixmap : : fromImage ( image ) ;
2022-01-15 20:23:08 +01:00
// It seems that browsers have special logic for fast animations.
// This implements Chrome and Firefox's behavior which uses
// a duration of 100 ms for any frames that specify a duration of <= 10 ms.
// See http://webkit.org/b/36082 for more information.
// https://github.com/SevenTV/chatterino7/issues/46#issuecomment-1010595231
int duration = reader . nextImageDelay ( ) ;
if ( duration < = 10 )
duration = 100 ;
duration = std : : max ( 20 , duration ) ;
2018-08-15 22:46:20 +02:00
frames . push_back ( Frame < QImage > { image , duration } ) ;
}
2018-08-09 18:39:46 +02:00
}
2018-10-21 13:43:02 +02:00
if ( frames . size ( ) = = 0 )
{
2020-11-21 16:20:10 +01:00
qCDebug ( chatterinoImage )
< < " Error while reading image " < < url . string < < " : ' "
< < reader . errorString ( ) < < " ' " ;
2018-08-15 22:46:20 +02:00
}
2018-08-09 18:39:46 +02:00
2018-08-15 22:46:20 +02:00
return frames ;
}
2018-08-09 18:39:46 +02:00
2018-08-15 22:46:20 +02:00
// parsed
template < typename Assign >
void assignDelayed (
std : : queue < std : : pair < Assign , QVector < Frame < QPixmap > > > > & queued ,
std : : mutex & mutex , std : : atomic_bool & loadedEventQueued )
{
2018-08-09 18:39:46 +02:00
std : : lock_guard < std : : mutex > lock ( mutex ) ;
2018-08-15 22:46:20 +02:00
int i = 0 ;
2018-10-21 13:43:02 +02:00
while ( ! queued . empty ( ) )
{
2018-08-15 22:46:20 +02:00
queued . front ( ) . first ( queued . front ( ) . second ) ;
queued . pop ( ) ;
2018-10-21 13:43:02 +02:00
if ( + + i > 50 )
{
2018-08-15 22:46:20 +02:00
QTimer : : singleShot ( 3 , [ & ] {
assignDelayed ( queued , mutex , loadedEventQueued ) ;
} ) ;
return ;
}
}
2018-08-09 18:39:46 +02:00
2021-02-13 19:17:22 +01:00
# ifndef CHATTERINO_TEST
2018-08-15 22:46:20 +02:00
getApp ( ) - > windows - > forceLayoutChannelViews ( ) ;
2021-02-13 19:17:22 +01:00
# endif
2018-08-15 22:46:20 +02:00
loadedEventQueued = false ;
}
2018-08-09 18:39:46 +02:00
2018-08-15 22:46:20 +02:00
template < typename Assign >
auto makeConvertCallback ( const QVector < Frame < QImage > > & parsed ,
Assign assign )
{
return [ parsed , assign ] {
// convert to pixmap
auto frames = QVector < Frame < QPixmap > > ( ) ;
std : : transform ( parsed . begin ( ) , parsed . end ( ) ,
std : : back_inserter ( frames ) , [ ] ( auto & frame ) {
return Frame < QPixmap > {
QPixmap : : fromImage ( frame . image ) ,
frame . duration } ;
} ) ;
// put into stack
static std : : queue < std : : pair < Assign , QVector < Frame < QPixmap > > > >
queued ;
static std : : mutex mutex ;
std : : lock_guard < std : : mutex > lock ( mutex ) ;
queued . emplace ( assign , frames ) ;
static std : : atomic_bool loadedEventQueued { false } ;
2018-10-21 13:43:02 +02:00
if ( ! loadedEventQueued )
{
2018-08-15 22:46:20 +02:00
loadedEventQueued = true ;
QTimer : : singleShot ( 100 , [ = ] {
assignDelayed ( queued , mutex , loadedEventQueued ) ;
} ) ;
}
} ;
}
2018-09-20 13:09:37 +02:00
} // namespace detail
2017-01-18 21:30:23 +01:00
2018-08-02 14:23:27 +02:00
// IMAGE2
2019-10-07 20:03:15 +02:00
Image : : ~ Image ( )
{
2021-02-13 19:17:22 +01:00
if ( this - > empty_ )
{
// No data in this image, don't bother trying to release it
// The reason we do this check is that we keep a few (or one) static empty image around that are deconstructed at the end of the programs lifecycle, and we want to prevent the isGuiThread call to be called after the QApplication has been exited
return ;
}
2019-10-07 20:03:15 +02:00
// run destructor of Frames in gui thread
if ( ! isGuiThread ( ) )
{
2020-11-08 12:02:19 +01:00
postToThread ( [ frames = this - > frames_ . release ( ) ] ( ) {
delete frames ;
} ) ;
2019-10-07 20:03:15 +02:00
}
}
2018-08-02 14:23:27 +02:00
ImagePtr Image : : fromUrl ( const Url & url , qreal scale )
2017-01-05 16:07:20 +01:00
{
2018-08-02 14:23:27 +02:00
static std : : unordered_map < Url , std : : weak_ptr < Image > > cache ;
static std : : mutex mutex ;
std : : lock_guard < std : : mutex > lock ( mutex ) ;
auto shared = cache [ url ] . lock ( ) ;
2018-10-21 13:43:02 +02:00
if ( ! shared )
{
2018-08-02 14:23:27 +02:00
cache [ url ] = shared = ImagePtr ( new Image ( url , scale ) ) ;
2018-10-21 13:43:02 +02:00
}
2018-08-02 14:23:27 +02:00
return shared ;
2017-01-05 16:07:20 +01:00
}
2018-08-10 18:56:17 +02:00
ImagePtr Image : : fromPixmap ( const QPixmap & pixmap , qreal scale )
2018-04-06 16:37:30 +02:00
{
2019-08-01 13:30:58 +02:00
auto result = ImagePtr ( new Image ( scale ) ) ;
result - > setPixmap ( pixmap ) ;
return result ;
2018-08-02 14:23:27 +02:00
}
2018-04-06 16:37:30 +02:00
2018-08-02 14:23:27 +02:00
ImagePtr Image : : getEmpty ( )
{
static auto empty = ImagePtr ( new Image ) ;
return empty ;
}
2018-04-06 16:37:30 +02:00
2018-08-02 14:23:27 +02:00
Image : : Image ( )
2018-08-06 18:25:47 +02:00
: empty_ ( true )
2018-08-02 14:23:27 +02:00
{
}
Image : : Image ( const Url & url , qreal scale )
2018-08-06 18:25:47 +02:00
: url_ ( url )
, scale_ ( scale )
, shouldLoad_ ( true )
2018-09-20 13:09:37 +02:00
, frames_ ( std : : make_unique < detail : : Frames > ( ) )
2018-08-02 14:23:27 +02:00
{
2017-01-11 18:52:09 +01:00
}
2019-08-01 13:30:58 +02:00
Image : : Image ( qreal scale )
2018-08-06 18:25:47 +02:00
: scale_ ( scale )
2019-08-01 13:30:58 +02:00
, frames_ ( std : : make_unique < detail : : Frames > ( ) )
{
}
void Image : : setPixmap ( const QPixmap & pixmap )
2018-08-02 14:23:27 +02:00
{
2019-08-01 13:30:58 +02:00
auto setFrames = [ shared = this - > shared_from_this ( ) , pixmap ] ( ) {
shared - > frames_ = std : : make_unique < detail : : Frames > (
QVector < detail : : Frame < QPixmap > > { detail : : Frame < QPixmap > { pixmap , 1 } } ) ;
} ;
if ( isGuiThread ( ) )
{
setFrames ( ) ;
}
else
{
postToThread ( setFrames ) ;
}
2018-08-02 14:23:27 +02:00
}
2017-10-27 20:09:02 +02:00
2018-08-06 18:25:47 +02:00
const Url & Image : : url ( ) const
2018-08-02 14:23:27 +02:00
{
return this - > url_ ;
}
2017-10-27 20:09:02 +02:00
2019-08-13 13:42:38 +02:00
bool Image : : loaded ( ) const
{
assertInGuiThread ( ) ;
return bool ( this - > frames_ - > current ( ) ) ;
}
boost : : optional < QPixmap > Image : : pixmapOrLoad ( ) const
2018-08-02 14:23:27 +02:00
{
assertInGuiThread ( ) ;
2018-04-06 17:46:12 +02:00
2019-08-21 01:44:19 +02:00
this - > load ( ) ;
return this - > frames_ - > current ( ) ;
}
void Image : : load ( ) const
{
assertInGuiThread ( ) ;
2018-10-21 13:43:02 +02:00
if ( this - > shouldLoad_ )
{
2018-08-06 18:25:47 +02:00
const_cast < Image * > ( this ) - > shouldLoad_ = false ;
2019-08-21 01:44:19 +02:00
const_cast < Image * > ( this ) - > actuallyLoad ( ) ;
2018-08-02 14:23:27 +02:00
}
}
2018-04-16 23:48:30 +02:00
2018-08-06 18:25:47 +02:00
qreal Image : : scale ( ) const
2018-08-02 14:23:27 +02:00
{
return this - > scale_ ;
}
2017-10-27 20:09:02 +02:00
2018-08-10 18:56:17 +02:00
bool Image : : isEmpty ( ) const
2018-08-02 14:23:27 +02:00
{
2018-08-06 18:25:47 +02:00
return this - > empty_ ;
2018-08-02 14:23:27 +02:00
}
2017-10-27 20:09:02 +02:00
2018-08-06 18:25:47 +02:00
bool Image : : animated ( ) const
2018-08-02 14:23:27 +02:00
{
2018-08-06 18:25:47 +02:00
assertInGuiThread ( ) ;
2017-10-27 20:09:02 +02:00
2018-08-11 17:15:17 +02:00
return this - > frames_ - > animated ( ) ;
2018-08-02 14:23:27 +02:00
}
2017-10-27 20:09:02 +02:00
2018-08-06 18:25:47 +02:00
int Image : : width ( ) const
2018-08-02 14:23:27 +02:00
{
2018-08-06 18:25:47 +02:00
assertInGuiThread ( ) ;
2018-04-16 23:48:30 +02:00
2018-08-11 17:15:17 +02:00
if ( auto pixmap = this - > frames_ - > first ( ) )
2019-09-22 10:27:05 +02:00
return int ( pixmap - > width ( ) * this - > scale_ ) ;
2018-08-06 18:25:47 +02:00
else
return 16 ;
2018-08-02 14:23:27 +02:00
}
2018-04-18 17:20:33 +02:00
2018-08-06 18:25:47 +02:00
int Image : : height ( ) const
2018-08-02 14:23:27 +02:00
{
2018-08-06 18:25:47 +02:00
assertInGuiThread ( ) ;
2017-10-27 20:09:02 +02:00
2018-08-11 17:15:17 +02:00
if ( auto pixmap = this - > frames_ - > first ( ) )
2019-08-20 23:29:11 +02:00
return int ( pixmap - > height ( ) * this - > scale_ ) ;
2018-08-06 18:25:47 +02:00
else
return 16 ;
2018-08-02 14:23:27 +02:00
}
2018-01-19 22:45:33 +01:00
2019-08-21 01:44:19 +02:00
void Image : : actuallyLoad ( )
2018-08-02 14:23:27 +02:00
{
2019-08-20 18:51:23 +02:00
NetworkRequest ( this - > url ( ) . string )
. concurrent ( )
2019-08-20 20:08:49 +02:00
. cache ( )
2019-08-21 00:01:27 +02:00
. onSuccess ( [ weak = weakOf ( this ) ] ( auto result ) - > Outcome {
2019-08-20 18:51:23 +02:00
auto shared = weak . lock ( ) ;
if ( ! shared )
return Failure ;
auto data = result . getData ( ) ;
// const cast since we are only reading from it
QBuffer buffer ( const_cast < QByteArray * > ( & data ) ) ;
buffer . open ( QIODevice : : ReadOnly ) ;
QImageReader reader ( & buffer ) ;
2021-06-24 22:54:36 +02:00
2022-05-28 13:48:31 +02:00
if ( ! reader . canRead ( ) )
{
qCDebug ( chatterinoImage )
< < " Error: image cant be read " < < shared - > url ( ) . string ;
return Failure ;
}
const auto size = reader . size ( ) ;
if ( size . isEmpty ( ) )
{
return Failure ;
}
// returns 1 for non-animated formats
if ( reader . imageCount ( ) < = 0 )
{
qCDebug ( chatterinoImage )
< < " Error: image has less than 1 frame "
< < shared - > url ( ) . string < < " : " < < reader . errorString ( ) ;
return Failure ;
}
2021-07-03 22:11:10 +02:00
// use "double" to prevent int overflows
2022-05-28 13:48:31 +02:00
if ( double ( size . width ( ) ) * double ( size . height ( ) ) *
2021-07-03 22:11:10 +02:00
double ( reader . imageCount ( ) ) * 4.0 >
double ( Image : : maxBytesRam ) )
2021-06-24 22:54:36 +02:00
{
qCDebug ( chatterinoImage ) < < " image too large in RAM " ;
return Failure ;
}
2019-08-21 00:01:27 +02:00
auto parsed = detail : : readFrames ( reader , shared - > url ( ) ) ;
2019-08-20 18:51:23 +02:00
postToThread ( makeConvertCallback ( parsed , [ weak ] ( auto frames ) {
if ( auto shared = weak . lock ( ) )
shared - > frames_ = std : : make_unique < detail : : Frames > ( frames ) ;
} ) ) ;
return Success ;
} )
2019-09-19 19:03:50 +02:00
. onError ( [ weak = weakOf ( this ) ] ( auto /*result*/ ) {
2019-08-20 18:51:23 +02:00
auto shared = weak . lock ( ) ;
if ( ! shared )
return false ;
2019-09-22 10:27:05 +02:00
// fourtf: is this the right thing to do?
2019-08-20 18:51:23 +02:00
shared - > empty_ = true ;
return true ;
} )
. execute ( ) ;
2017-09-12 19:06:16 +02:00
}
2018-08-02 14:23:27 +02:00
bool Image : : operator = = ( const Image & other ) const
2017-09-12 19:06:16 +02:00
{
2018-10-21 13:43:02 +02:00
if ( this - > isEmpty ( ) & & other . isEmpty ( ) )
return true ;
if ( ! this - > url_ . string . isEmpty ( ) & & this - > url_ = = other . url_ )
return true ;
if ( this - > frames_ - > first ( ) = = other . frames_ - > first ( ) )
return true ;
2018-01-19 22:45:33 +01:00
2018-08-02 14:23:27 +02:00
return false ;
2017-09-12 19:06:16 +02:00
}
2018-08-02 14:23:27 +02:00
bool Image : : operator ! = ( const Image & other ) const
2017-09-12 19:06:16 +02:00
{
2018-08-02 14:23:27 +02:00
return ! this - > operator = = ( other ) ;
2017-09-12 19:06:16 +02:00
}
2017-04-14 17:52:22 +02:00
} // namespace chatterino